Parcourir la source

style(mobile): 优化移动端 Header 和输入框布局

Header 优化:
- 移动端使用两行布局,避免元素挤在一起
- 调整字体大小和间距适配移动端
- 添加顶部安全区域支持

输入框优化:
- 移动端固定在视口底部,避免被浏览器地址栏遮挡
- 使用 100dvh 解决移动端视口高度问题
- 添加底部安全区域支持(iPhone 刘海屏)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude AI il y a 3 jours
Parent
commit
4b6b12ce63
3 fichiers modifiés avec 82 ajouts et 46 suppressions
  1. 57 12
      frontend-v2/app/globals.css
  2. 3 3
      frontend-v2/app/page.tsx
  3. 22 31
      frontend-v2/components/Header.tsx

+ 57 - 12
frontend-v2/app/globals.css

@@ -14,12 +14,21 @@
   }
 }
 
+/* 移动端视口高度 - 解决浏览器地址栏遮挡问题 */
+@supports (height: 100dvh) {
+  .h-screen-mobile {
+    height: 100dvh;
+  }
+}
+
 body {
   color: var(--foreground);
   background: var(--background);
   font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
+  /* 防止 iOS Safari 中 body 弹性滚动 */
+  overscroll-behavior: none;
 }
 
 /* 移动端优化 */
@@ -28,6 +37,11 @@ body {
     font-size: 16px;
     line-height: 1.5;
   }
+
+  /* 防止触摸时的高亮 */
+  * {
+    -webkit-tap-highlight-color: transparent;
+  }
 }
 
 @layer utilities {
@@ -82,10 +96,6 @@ body {
 
 /* 移动端滚动条优化 */
 @media (max-width: 768px) {
-  * {
-    -webkit-tap-highlight-color: transparent;
-  }
-
   /* 自定义滚动条 */
   ::-webkit-scrollbar {
     width: 4px;
@@ -106,25 +116,60 @@ body {
   }
 }
 
-/* 触摸友好的按钮 */
+/* 安全区域支持 */
+.safe-area-inset-bottom {
+  padding-bottom: env(safe-area-inset-bottom, 0);
+}
+
+.safe-area-inset-top {
+  padding-top: env(safe-area-inset-top, 0);
+}
+
+/* 固定底部输入框 - 解决浏览器地址栏遮挡 */
 @media (max-width: 768px) {
+  /* 触摸友好的按钮 */
   button, .btn {
     min-height: 44px;
     min-width: 44px;
   }
 
-  /* 安全区域支持 - iPhone 刘海屏 */
-  .safe-area-inset-bottom {
-    padding-bottom: calc(env(safe-area-inset-bottom, 0) + 0.5rem);
+  /* 输入框区域固定在视口底部 */
+  .fixed-bottom-input {
+    position: fixed;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    z-index: 50;
+    background: white;
+    padding-bottom: max(env(safe-area-inset-bottom, 0), 8px);
   }
 
-  .safe-area-inset-top {
-    padding-top: calc(env(safe-area-inset-top, 0) + 0.5rem);
+  .dark .fixed-bottom-input {
+    background: #1f2937; /* gray-800 */
   }
 
-  /* 固定底部输入框安全区域 */
+  /* 为消息列表添加底部 padding,避免被输入框遮挡 */
+  .messages-with-fixed-input {
+    padding-bottom: 80px; /* 大约输入框高度 */
+  }
+
+  /* pb-safe 类:底部安全区域 + 额外间距 */
   .pb-safe {
-    padding-bottom: calc(env(safe-area-inset-bottom, 0) + 0.75rem);
+    padding-bottom: max(env(safe-area-inset-bottom, 0), 12px);
   }
 }
 
+/* 桌面端保持原有样式 */
+@media (min-width: 769px) {
+  .fixed-bottom-input {
+    position: relative;
+  }
+
+  .messages-with-fixed-input {
+    padding-bottom: 1rem;
+  }
+
+  .pb-safe {
+    padding-bottom: 1rem;
+  }
+}

+ 3 - 3
frontend-v2/app/page.tsx

@@ -160,7 +160,7 @@ export default function Home() {
   }), [handleSend]);
 
   return (
-    <div className="flex flex-col h-screen bg-gray-50 dark:bg-gray-900">
+    <div className="flex flex-col h-screen-mobile min-h-screen bg-gray-50 dark:bg-gray-900">
       <Header />
 
       <main className="flex-1 overflow-hidden flex">
@@ -179,7 +179,7 @@ export default function Home() {
             ) : (
               // 使用 ActionProvider 包装整个消息区域,使 ChatMessage 内部的 JsonRenderer 可以访问 ActionContext
               <ActionProvider actions={actionContext()}>
-                <div className="space-y-3 md:space-y-4 pb-20 md:pb-4">
+                <div className="space-y-3 md:space-y-4 messages-with-fixed-input">
                   {history.map((msg, idx) => {
                     console.log(`[Render] Message ${idx}:`, msg);
                     return (
@@ -199,7 +199,7 @@ export default function Home() {
             )}
           </div>
 
-          <div className="border-t dark:border-gray-700 p-2 md:p-4 pb-safe bg-white dark:bg-gray-800">
+          <div className="fixed-bottom-input md:relative border-t dark:border-gray-700 p-2 md:p-4 bg-white dark:bg-gray-800">
             <ChatInput onSend={handleSend} isLoading={isLoading} onAbort={handleAbort} />
           </div>
         </div>

+ 22 - 31
frontend-v2/components/Header.tsx

@@ -1,9 +1,10 @@
 /**
- * 页面头部组件
+ * 页面头部组件 - 移动端优化
  *
  * 核心概念:
  * - Web UI 是 MCP 测试工具,不需要全局登录
  * - 显示已连接的 MCP 服务器数量
+ * - 移动端:两行布局,避免挤在一起
  */
 'use client';
 
@@ -34,27 +35,30 @@ export default function Header() {
   }, []);
 
   return (
-    <header className="bg-white dark:bg-gray-800 border-b dark:border-gray-700 px-6 py-4">
-      <div className="flex items-center justify-between">
-        <div className="flex items-center space-x-4">
-          <h1 className="text-xl font-bold text-gray-800 dark:text-white">
+    <header className="bg-white dark:bg-gray-800 border-b dark:border-gray-700 px-3 py-2 md:px-6 md:py-4 safe-area-inset-top">
+      {/* 移动端:两行布局 */}
+      <div className="flex flex-col md:flex-row md:items-center md:justify-between gap-2 md:gap-0">
+        {/* 第一行:标题和导航 */}
+        <div className="flex items-center justify-between">
+          <h1 className="text-lg md:text-xl font-bold text-gray-800 dark:text-white">
             AI MCP Web UI
           </h1>
-          <nav className="flex space-x-4">
-            <Link href="/" className="text-gray-600 dark:text-gray-300 hover:text-blue-500">
+          <nav className="flex space-x-2 md:space-x-4">
+            <Link href="/" className="text-sm md:text-base text-gray-600 dark:text-gray-300 hover:text-blue-500 px-2 py-1">
               聊天
             </Link>
-            <Link href="/mcp" className="text-gray-600 dark:text-gray-300 hover:text-blue-500">
+            <Link href="/mcp" className="text-sm md:text-base text-gray-600 dark:text-gray-300 hover:text-blue-500 px-2 py-1">
               MCP 管理
             </Link>
           </nav>
         </div>
 
-        <div className="flex items-center space-x-4">
+        {/* 第二行(移动端)/ 右侧(桌面端):MCP 状态 */}
+        <div className="flex items-center justify-between md:justify-end space-x-2 md:space-x-4">
           {/* MCP 连接状态 */}
-          <div className="flex items-center space-x-2 text-sm">
+          <div className="flex items-center space-x-1 md:space-x-2 text-xs md:text-sm">
             <span className="text-gray-600 dark:text-gray-400">MCP:</span>
-            <span className={`px-2 py-1 rounded ${
+            <span className={`px-1.5 md:px-2 py-0.5 md:py-1 rounded ${
               enabledCount > 0
                 ? 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200'
                 : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
@@ -62,31 +66,18 @@ export default function Header() {
               已启用 {enabledCount}/{total}
             </span>
             {loggedInCount > 0 && (
-              <span className={`px-2 py-1 rounded ${
-                loggedInCount > 0
-                  ? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'
-                  : 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400'
-              }`}>
+              <span className="hidden sm:inline-block px-1.5 md:px-2 py-0.5 md:py-1 rounded bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
                 已登录 {loggedInCount}/{total}
               </span>
             )}
           </div>
 
-          {loggedInCount > 0 ? (
-            <Link
-              href="/mcp"
-              className="text-sm text-blue-500 hover:text-blue-600"
-            >
-              管理连接
-            </Link>
-          ) : (
-            <Link
-              href="/mcp"
-              className="px-4 py-2 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
-            >
-              连接 MCP
-            </Link>
-          )}
+          <Link
+            href="/mcp"
+            className="text-xs md:text-sm px-2 md:px-4 py-1.5 md:py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
+          >
+            {loggedInCount > 0 ? '管理' : '连接 MCP'}
+          </Link>
         </div>
       </div>
     </header>