Jelajahi Sumber

提交welcome界面

zlwu 11 bulan lalu
induk
melakukan
82a085c684

+ 1 - 0
frontend/SSL证书配置.md

@@ -122,5 +122,6 @@ ssl/
 
 
 
+
 
 

TEMPAT SAMPAH
frontend/src/assets/02-bak.GIF


TEMPAT SAMPAH
frontend/src/assets/02.GIF


TEMPAT SAMPAH
frontend/src/assets/Hello,.png


TEMPAT SAMPAH
frontend/src/assets/Lumi-image.png


+ 50 - 4
frontend/src/main.tsx

@@ -5,13 +5,59 @@ import { initRem } from '@/utils/rem';
 import { logBrowserInfo } from '@/utils/browserDetect';
 import './index.css';
 
+// 全局错误处理
+window.addEventListener('error', (event) => {
+  console.error('🚨 全局错误:', event.error);
+  // 在页面上显示错误信息(开发环境)
+  if (import.meta.env.DEV) {
+    const root = document.getElementById('root');
+    if (root) {
+      root.innerHTML = `
+        <div style="padding: 20px; color: red;">
+          <h2>❌ 应用加载失败</h2>
+          <pre>${event.error?.stack || event.message}</pre>
+          <p>请查看浏览器控制台获取更多信息</p>
+        </div>
+      `;
+    }
+  }
+});
+
+window.addEventListener('unhandledrejection', (event) => {
+  console.error('🚨 未处理的 Promise 拒绝:', event.reason);
+});
+
 // 初始化 rem 适配(必须在渲染前执行)
-initRem();
+try {
+  initRem();
+} catch (error) {
+  console.error('❌ rem 初始化失败:', error);
+}
 
 // 开发环境输出浏览器信息
 logBrowserInfo();
 
-ReactDOM.createRoot(document.getElementById('root')!).render(
-  <RouterProvider router={router} />
-);
+// 渲染应用
+try {
+  const rootElement = document.getElementById('root');
+  if (!rootElement) {
+    throw new Error('找不到 #root 元素');
+  }
+  
+  ReactDOM.createRoot(rootElement).render(
+    <RouterProvider router={router} />
+  );
+} catch (error) {
+  console.error('❌ React 应用渲染失败:', error);
+  const root = document.getElementById('root');
+  if (root) {
+    root.innerHTML = `
+      <div style="padding: 20px; color: red;">
+        <h2>❌ 应用初始化失败</h2>
+        <pre>${error instanceof Error ? error.stack : String(error)}</pre>
+        <p>请查看浏览器控制台获取更多信息</p>
+      </div>
+    `;
+  }
+}
 

+ 18 - 2
frontend/src/pages/RootRedirect.tsx

@@ -16,21 +16,36 @@ export const RootRedirect = () => {
   useEffect(() => {
     const handleRedirect = async () => {
       try {
+        console.log('[RootRedirect] 开始处理重定向...');
+        
         // 检查用户ID是否存在
         if (!hasUserId()) {
-          // 调用后端接口创建用户
-          const response = await createUser();
+          console.log('[RootRedirect] 用户ID不存在,开始创建用户...');
+          
+          // 添加超时处理(10秒超时)
+          const createUserPromise = createUser();
+          const timeoutPromise = new Promise<never>((_, reject) => {
+            setTimeout(() => reject(new Error('创建用户超时(10秒)')), 10000);
+          });
+          
+          const response = await Promise.race([createUserPromise, timeoutPromise]);
+          
+          console.log('[RootRedirect] 创建用户响应:', response);
           
           if (response.success && response.data?.user_id) {
             // 保存用户ID到 localStorage
             setUserId(response.data.user_id);
+            console.log('[RootRedirect] 用户ID已保存:', response.data.user_id);
           } else {
             throw new Error('创建用户失败:响应数据无效');
           }
+        } else {
+          console.log('[RootRedirect] 用户ID已存在,跳过创建');
         }
 
         // 获取下一个未访问的路由
         const nextRoute = getNextUnvisitedRoute();
+        console.log('[RootRedirect] 下一个路由:', nextRoute);
         
         if (nextRoute) {
           // 有未访问的路由,跳转到下一个
@@ -40,6 +55,7 @@ export const RootRedirect = () => {
           navigate('/chatbot', { replace: true });
         }
       } catch (err) {
+        console.error('[RootRedirect] 错误:', err);
         setError(err instanceof Error ? err.message : '未知错误');
         setIsLoading(false);
       }

+ 270 - 0
frontend/src/pages/WelcomePage.css

@@ -0,0 +1,270 @@
+/**
+ * WelcomePage 屏幕适配方案 - 推荐方案优化版
+ * 
+ * 设计稿基准: 375px × 812px (iPhone X/11/12/13/14)
+ * 适配策略: vw/vh + 媒体查询(推荐方案)
+ * 
+ * 核心思路:
+ * 1. 移动端(< 430px):使用 vw/vh 单位,精确响应视口
+ * 2. 大屏手机(430px - 768px):固定设计稿尺寸
+ * 3. 平板/桌面(≥ 768px):固定 375×812,居中显示
+ * 4. 所有尺寸基于设计稿精确计算
+ * 
+ * 尺寸转换公式:
+ * - 宽度: px ÷ 375 × 100 = vw%
+ * - 高度: px ÷ 812 × 100 = vh%
+ */
+
+/* ========================================
+   外层容器 - 全屏布局
+   ======================================== */
+.welcome-page {
+  position: relative;
+  width: 100%;
+  height: 100%;
+  overflow: hidden;
+  background-color: #E8E8E8;
+  /* iOS 安全区域适配 */
+  padding-top: env(safe-area-inset-top);
+  padding-left: env(safe-area-inset-left);
+  padding-right: env(safe-area-inset-right);
+  padding-bottom: env(safe-area-inset-bottom);
+}
+
+/* ========================================
+   背景层 - 全屏覆盖,消除空白
+   ======================================== */
+.welcome-bg {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  z-index: 0;
+}
+
+/* ========================================
+   内容容器 - 基于 vw/vh 响应式适配
+   ======================================== */
+.welcome-content {
+  position: relative;
+  width: 100vw;
+  /* 使用动态视口高度(dvh),降级方案使用 JavaScript 设置的 CSS 变量 */
+  height: 100dvh; /* 现代浏览器:动态视口高度 */
+  height: calc(var(--vh, 1vh) * 100); /* 降级方案:JavaScript 计算的视口高度 */
+  z-index: 1;
+}
+
+/* ========================================
+   Lumi 图片 - 左下角,自适应完整显示
+   ======================================== */
+.welcome-lumi {
+  position: absolute;
+  left: 0;
+  bottom: 0;
+  /* 使用 contain 确保图片完整显示,不裁剪 */
+  object-fit: contain;
+  object-position: left bottom;
+  /* 自适应宽度,最大不超过容器 */
+  width: auto;
+  max-width: 100%;
+  /* 自适应高度,最大不超过下半部分 */
+  height: auto;
+  max-height: 50dvh; /* 使用动态视口高度 */
+  max-height: calc(var(--vh, 1vh) * 50); /* 降级方案 */
+  /* 确保图片从底部对齐 */
+  align-self: flex-end;
+}
+
+/* ========================================
+   Hello 图片 - 可点击,基于设计稿位置
+   设计稿: top: 229px, left: 45px, width: 245px
+   ======================================== */
+.welcome-hello {
+  position: absolute;
+  /* 移动端:使用 vw/vh 单位精确计算 */
+  /* top: 229px ÷ 812px × 100 = 28.2vh */
+  top: 28.2dvh; /* 使用动态视口高度 */
+  top: calc(var(--vh, 1vh) * 28.2); /* 降级方案 */
+  /* left: 45px ÷ 375px × 100 = 12vw */
+  left: 12vw;
+  /* width: 245px ÷ 375px × 100 = 65.33vw */
+  width: 65.33vw;
+  max-width: 245px;
+  cursor: pointer;
+  transition: opacity 0.2s ease;
+  user-select: none;
+  -webkit-tap-highlight-color: transparent;
+  touch-action: manipulation;
+}
+
+.welcome-hello:hover {
+  opacity: 0.9;
+}
+
+.welcome-hello:active {
+  opacity: 0.8;
+}
+
+/* ========================================
+   移动端适配(280px - 430px)
+   ======================================== */
+
+/* 极小屏幕(< 320px):适当缩小 */
+@media (max-width: 319px) {
+  .welcome-hello {
+    width: 60vw;
+    left: 10vw;
+  }
+  
+  .welcome-lumi {
+    /* 极小屏幕确保完整显示 */
+    max-height: 45dvh; /* 使用动态视口高度 */
+    max-height: calc(var(--vh, 1vh) * 45); /* 降级方案 */
+  }
+}
+
+/* 小屏手机(320px - 375px):保持比例 */
+@media (min-width: 320px) and (max-width: 374px) {
+  .welcome-hello {
+    width: 65.33vw;
+    left: 12vw;
+  }
+  
+  .welcome-lumi {
+    /* 小屏手机确保完整显示 */
+    max-height: 48dvh; /* 使用动态视口高度 */
+    max-height: calc(var(--vh, 1vh) * 48); /* 降级方案 */
+  }
+}
+
+/* 标准手机(375px - 429px):使用设计稿尺寸 */
+@media (min-width: 375px) and (max-width: 429px) {
+  .welcome-hello {
+    width: 245px;
+    left: 45px;
+    top: 28.2dvh; /* 使用动态视口高度 */
+    top: calc(var(--vh, 1vh) * 28.2); /* 降级方案 */
+  }
+}
+
+/* 大屏手机(430px - 767px):固定设计稿尺寸 */
+@media (min-width: 430px) and (max-width: 767px) {
+  .welcome-content {
+    width: 375px;
+    height: 812px;
+    max-width: 100vw;
+    max-height: 100dvh; /* 使用动态视口高度 */
+    max-height: calc(var(--vh, 1vh) * 100); /* 降级方案 */
+    margin: 0 auto;
+  }
+  
+  .welcome-lumi {
+    /* 保持自适应,确保完整显示 */
+    max-width: 375px;
+    max-height: 50dvh; /* 使用动态视口高度 */
+    max-height: calc(var(--vh, 1vh) * 50); /* 降级方案 */
+  }
+  
+  .welcome-hello {
+    width: 245px;
+    left: 45px;
+    top: 229px;
+  }
+}
+
+/* ========================================
+   平板/桌面适配(≥ 768px)
+   ======================================== */
+@media (min-width: 768px) {
+  .welcome-page {
+    background: linear-gradient(135deg, #E8E8E8 0%, #D8D8D8 100%);
+  }
+  
+  .welcome-content {
+    width: 375px;
+    height: 812px;
+    max-width: 375px;
+    max-height: calc(100dvh - 80px); /* 使用动态视口高度 */
+    max-height: calc(calc(var(--vh, 1vh) * 100) - 80px); /* 降级方案 */
+    margin: 0 auto;
+    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
+    border-radius: 20px;
+    overflow: hidden;
+  }
+  
+  .welcome-lumi {
+    /* 保持自适应,确保完整显示 */
+    max-width: 375px;
+    max-height: 50dvh; /* 使用动态视口高度 */
+    max-height: calc(var(--vh, 1vh) * 50); /* 降级方案 */
+  }
+  
+  .welcome-hello {
+    width: 245px;
+    left: 45px;
+    top: 229px;
+  }
+}
+
+/* ========================================
+   横屏适配
+   ======================================== */
+@media (orientation: landscape) and (max-height: 500px) {
+  .welcome-content {
+    height: 100dvh; /* 使用动态视口高度 */
+    height: calc(var(--vh, 1vh) * 100); /* 降级方案 */
+    width: calc(100dvh * 0.462); /* 375/812 ≈ 0.462,使用动态视口高度 */
+    width: calc(calc(var(--vh, 1vh) * 100) * 0.462); /* 降级方案 */
+    max-width: 100vw;
+    margin: 0 auto;
+  }
+  
+  .welcome-lumi {
+    /* 横屏时保持自适应,确保完整显示 */
+    max-width: 100%;
+    max-height: 50dvh; /* 使用动态视口高度 */
+    max-height: calc(var(--vh, 1vh) * 50); /* 降级方案 */
+  }
+  
+  .welcome-hello {
+    /* 横屏时保持相对位置 */
+    top: 28.2dvh; /* 使用动态视口高度 */
+    top: calc(var(--vh, 1vh) * 28.2); /* 降级方案 */
+    left: 12vw;
+    width: 65.33vw;
+    max-width: 245px;
+  }
+}
+
+/* ========================================
+   触摸优化(移动端)
+   ======================================== */
+@media (hover: none) and (pointer: coarse) {
+  .welcome-hello {
+    /* 移动端增大点击区域(最小 44×44px) */
+    min-width: 44px;
+    min-height: 44px;
+    padding: 10px;
+    margin: -10px;
+  }
+}
+
+/* ========================================
+   性能优化
+   ======================================== */
+.welcome-lumi,
+.welcome-hello {
+  /* 启用硬件加速 */
+  transform: translateZ(0);
+  will-change: transform;
+  /* 图片优化 */
+  image-rendering: -webkit-optimize-contrast;
+  image-rendering: crisp-edges;
+  /* 防止图片拖拽 */
+  -webkit-user-drag: none;
+}
+

+ 49 - 83
frontend/src/pages/WelcomePage.tsx

@@ -1,8 +1,10 @@
 import { useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { setStorage, STORAGE_KEYS, markRouteVisited } from '@/utils/storage';
-import { px2rem } from '@/utils/rem';
-import lumiImage from '@/assets/lumi.png';
+import chatbotBg from '@/assets/chatbot-bg.svg';
+import lumiImage from '@/assets/Lumi-image.png';
+import helloImage from '@/assets/Hello,.png';
+import './WelcomePage.css';
 
 export const WelcomePage = () => {
   const navigate = useNavigate();
@@ -13,96 +15,60 @@ export const WelcomePage = () => {
     
     // 记录路由访问
     markRouteVisited('/welcome');
+
+    // 移动端视口高度修复:处理浏览器地址栏/工具栏导致的视口高度变化
+    const setViewportHeight = () => {
+      // 获取实际可视区域高度(不包括浏览器UI)
+      const vh = window.innerHeight * 0.01;
+      document.documentElement.style.setProperty('--vh', `${vh}px`);
+    };
+
+    // 初始设置
+    setViewportHeight();
+
+    // 监听窗口大小变化(包括浏览器UI显示/隐藏)
+    window.addEventListener('resize', setViewportHeight);
+    window.addEventListener('orientationchange', setViewportHeight);
+
+    // 清理函数
+    return () => {
+      window.removeEventListener('resize', setViewportHeight);
+      window.removeEventListener('orientationchange', setViewportHeight);
+    };
   }, []);
 
-  const handleStart = () => {
+  const handleHelloClick = () => {
     // 记录即将访问的路由
     markRouteVisited('/video');
     navigate('/video');
   };
 
   return (
-    <div className="w-full h-full flex items-center justify-center bg-white overflow-hidden safe-area-inset">
-      <div className="w-full h-full bg-white flex flex-col relative overflow-hidden
-                      max-sm:border-none max-sm:rounded-none"
-           style={{
-             maxWidth: '100%',
-             maxHeight: '100%',
-             borderWidth: px2rem(8),
-             borderColor: '#1B0F33',
-             borderRadius: px2rem(40),
-           }}>
-        {/* 角色展示区域 - 响应式内边距 */}
-        <div className="flex-1 flex items-center justify-center"
-             style={{
-               padding: px2rem(50),
-             }}>
-          <img 
-            src={lumiImage} 
-            alt="Lumi"
-            className="object-contain animate-[float_3s_ease-in-out_infinite]"
-            style={{
-              width: px2rem(320),
-              height: px2rem(320),
-              maxWidth: '85vw',
-              maxHeight: '85vw',
-            }}
-          />
-        </div>
+    <div className="welcome-page">
+      {/* 背景层 - 全屏覆盖 */}
+      <div 
+        className="welcome-bg"
+        style={{
+          backgroundImage: `url(${chatbotBg})`,
+        }}
+      />
+      
+      {/* 内容容器 - 基于 vw/vh 响应式适配 */}
+      <div className="welcome-content">
+        {/* Lumi-image 图片 - 左下角 */}
+        <img 
+          src={lumiImage} 
+          alt="Lumi"
+          className="welcome-lumi"
+        />
         
-        {/* 底部内容区域 - 响应式尺寸 */}
-        <div className="bg-gradient-to-br from-purple-50 to-blue-50 
-                        flex flex-col items-center justify-center
-                        max-sm:rounded-none"
-             style={{
-               height: px2rem(280),
-               minHeight: '25vh',
-               padding: px2rem(40),
-               borderBottomLeftRadius: px2rem(32),
-               borderBottomRightRadius: px2rem(32),
-             }}>
-          <button 
-            className="flex items-center justify-center
-                       font-extrabold text-white text-center leading-relaxed
-                       bg-gradient-to-br from-purple-500 via-purple-600 to-indigo-600
-                       rounded-full shadow-2xl shadow-purple-500/60
-                       transition-all duration-500 ease-out
-                       relative overflow-hidden touch-manipulation
-                       hover:scale-110 hover:shadow-[0_20px_60px_rgba(168,85,247,0.7)]
-                       hover:rotate-3
-                       active:scale-105 active:rotate-0
-                       before:content-[''] before:absolute before:inset-0
-                       before:bg-gradient-to-tr before:from-transparent before:via-white/40 before:to-transparent
-                       before:translate-x-[-200%] before:translate-y-[-200%]
-                       hover:before:translate-x-[200%] hover:before:translate-y-[200%]
-                       before:transition-transform before:duration-1000
-                       after:content-[''] after:absolute after:rounded-full after:border-2 after:border-white/30"
-            style={{
-              width: px2rem(140),
-              height: px2rem(140),
-              minWidth: '32vw',
-              minHeight: '32vw',
-              maxWidth: px2rem(160),
-              maxHeight: px2rem(160),
-              fontSize: px2rem(14),
-            }}
-            onClick={handleStart}
-          >
-            <style>
-              {`
-                button:after {
-                  inset: ${px2rem(2)};
-                }
-              `}
-            </style>
-            <span className="relative z-10"
-                  style={{
-                    padding: `0 ${px2rem(12)}`,
-                  }}>
-              ✨ 欢迎进入<br/>Lumi的世界 ✨
-            </span>
-          </button>
-        </div>
+        {/* Hello 图片 - 可点击跳转 */}
+        <img 
+          src={helloImage} 
+          alt="Hello"
+          className="welcome-hello"
+          onClick={handleHelloClick}
+        />
       </div>
     </div>
   );

+ 56 - 11
frontend/src/utils/api.ts

@@ -7,6 +7,18 @@
  * 获取后端 API 基础 URL
  */
 const getApiBaseUrl = (): string => {
+  // 开发环境:使用相对路径,通过 Vite 代理访问后端
+  // 这样可以避免 Safari 对自签名证书的跨域限制
+  if (import.meta.env.DEV) {
+    return '';  // 使用相对路径,Vite 会代理到后端
+  }
+  
+  // 生产环境:使用完整 URL
+  // 优先使用环境变量指定的 API 地址
+  if (import.meta.env.VITE_API_BASE_URL) {
+    return import.meta.env.VITE_API_BASE_URL;
+  }
+  
   // 从 WebSocket URL 推导 API 基础 URL
   const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
   
@@ -34,19 +46,52 @@ export interface CreateUserResponse {
 
 export const createUser = async (username: string = ''): Promise<CreateUserResponse> => {
   const apiBaseUrl = getApiBaseUrl();
-  const response = await fetch(`${apiBaseUrl}/api/users/create`, {
-    method: 'POST',
-    headers: {
-      'Content-Type': 'application/json',
-    },
-    body: JSON.stringify({ username }),
+  const url = `${apiBaseUrl}/api/users/create`;
+  
+  console.log('[API] 创建用户请求:', {
+    url,
+    apiBaseUrl,
+    isDev: import.meta.env.DEV,
   });
-
-  if (!response.ok) {
-    throw new Error(`创建用户失败: ${response.statusText}`);
+  
+  try {
+    const response = await fetch(url, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ username }),
+    });
+
+    console.log('[API] 创建用户响应:', {
+      status: response.status,
+      statusText: response.statusText,
+      ok: response.ok,
+    });
+
+    if (!response.ok) {
+      const errorText = await response.text().catch(() => '无法读取错误信息');
+      console.error('[API] 创建用户失败:', {
+        status: response.status,
+        statusText: response.statusText,
+        errorText,
+      });
+      throw new Error(`创建用户失败: ${response.status} ${response.statusText} - ${errorText}`);
+    }
+
+    const data = await response.json();
+    console.log('[API] 创建用户成功:', data);
+    return data;
+  } catch (error) {
+    console.error('[API] 创建用户异常:', error);
+    
+    // 如果是网络错误,提供更友好的提示
+    if (error instanceof TypeError && error.message.includes('fetch')) {
+      throw new Error('网络请求失败,请检查后端服务是否运行');
+    }
+    
+    throw error;
   }
-
-  return response.json();
 };
 
 /**

+ 41 - 4
frontend/vite.config.ts

@@ -1,15 +1,41 @@
-import { defineConfig } from 'vite';
+import { defineConfig, loadEnv } from 'vite';
 import react from '@vitejs/plugin-react';
 import path from 'path';
 import fs from 'fs';
 
+// 加载环境变量(Vite 不会自动在配置文件中加载 .env)
+const env = loadEnv(process.env.NODE_ENV || 'development', process.cwd(), '');
+
+// 后端 API 地址配置(用于代理)
+// 从 WebSocket URL 推导后端地址,或使用环境变量直接指定
+const getBackendUrl = (): string => {
+  // 优先使用环境变量指定的后端地址
+  if (env.VITE_API_BASE_URL) {
+    return env.VITE_API_BASE_URL;
+  }
+  
+  // 从 WebSocket URL 推导
+  const wsUrl = env.VITE_WS_URL || 'ws://localhost:8000/ws';
+  const backendUrl = wsUrl
+    .replace(/^wss:\/\//, 'https://')
+    .replace(/^ws:\/\//, 'http://')
+    .replace(/\/ws$/, '');
+  
+  return backendUrl;
+};
+
+const backendUrl = getBackendUrl();
+console.log('🔗 后端 API 代理配置:');
+console.log(`  目标地址: ${backendUrl}`);
+console.log(`  代理路径: /api -> ${backendUrl}/api`);
+
 // SSL证书配置
 // HTTPS开关:通过环境变量 VITE_ENABLE_HTTPS 控制(true/false)
-const enableHttps = process.env.VITE_ENABLE_HTTPS === 'true';
+const enableHttps = env.VITE_ENABLE_HTTPS === 'true';
 
 // 优先使用环境变量指定的证书路径(生产环境)
-const sslKeyPath = process.env.VITE_SSL_KEYFILE || path.resolve(__dirname, '../ssl/key.pem');
-const sslCertPath = process.env.VITE_SSL_CERTFILE || path.resolve(__dirname, '../ssl/cert.pem');
+const sslKeyPath = env.VITE_SSL_KEYFILE || path.resolve(__dirname, '../ssl/key.pem');
+const sslCertPath = env.VITE_SSL_CERTFILE || path.resolve(__dirname, '../ssl/cert.pem');
 
 // 根据开关和证书文件决定HTTPS配置
 let httpsConfig: any = false;
@@ -43,6 +69,17 @@ export default defineConfig({
     port: 3000,
     host: '0.0.0.0',  // 允许外部访问
     https: httpsConfig,
+    // 代理配置:将 /api 请求代理到后端服务器
+    // 这样可以避免跨域和证书问题(Safari 对自签名证书的跨域请求很严格)
+    proxy: {
+      '/api': {
+        target: backendUrl,
+        changeOrigin: true,
+        secure: false,  // 允许自签名证书
+        // 如果需要 WebSocket 代理,取消下面的注释
+        // ws: true,
+      },
+    },
   },
 });
 

+ 317 - 0
项目文档/前端自适应方案总结.md

@@ -0,0 +1,317 @@
+# 前端自适应方案总结
+
+## 📋 设计稿基准
+
+- **基准尺寸**: 375px × 812px (iPhone X/11/12/13/14)
+- **适配目标**: 移动端(280px - 430px)、平板(430px - 768px)、桌面(768px+)
+
+---
+
+## 🎯 当前项目适配方案
+
+### 方案一:完全计算式布局系统(V3.0)
+
+**适用场景**: ChatBotPage 等复杂布局页面
+
+**核心思路**: 等比例缩放,从可用高度出发计算所有元素位置
+
+**实现位置**: `frontend/src/utils/screenSize.ts`
+
+**特点**:
+- ✅ 基于 375×812 设计稿等比例缩放
+- ✅ 取高度和宽度的较小缩放比例,确保不溢出
+- ✅ 最小缩放 0.6,无最大限制
+- ✅ 完全计算式布局,保证无重叠
+
+**使用示例**:
+```typescript
+import { calculateLayout } from '@/utils/screenSize';
+
+const layout = calculateLayout(window.innerWidth, window.innerHeight);
+
+// 使用计算后的布局参数
+<div style={{
+  top: `${layout.mainAvatarTop}px`,
+  width: `${layout.mainAvatarWidth}px`,
+  height: `${layout.mainAvatarHeight}px`,
+}} />
+```
+
+---
+
+### 方案二:rem 适配方案
+
+**适用场景**: 全局基础适配、简单页面
+
+**核心思路**: 动态设置根字体大小,使用 rem 单位
+
+**实现位置**: `frontend/src/utils/rem.ts`
+
+**特点**:
+- ✅ 1rem = 100px(便于计算)
+- ✅ 280px - 430px:动态缩放
+- ✅ 430px - 768px:渐进式缩放
+- ✅ 768px+:固定 100px,内容居中
+
+**使用示例**:
+```typescript
+// 初始化(在 main.tsx 中)
+import { initRem } from '@/utils/rem';
+initRem();
+
+// CSS 中使用
+.element {
+  width: 3.75rem; /* 375px */
+  height: 8.12rem; /* 812px */
+}
+```
+
+---
+
+### 方案三:vw/vh 视口单位适配
+
+**适用场景**: WelcomePage 等简单页面、背景图片
+
+**核心思路**: 直接使用视口单位,响应式布局
+
+**实现位置**: `frontend/src/pages/WelcomePage.css`
+
+**特点**:
+- ✅ 直接响应视口变化
+- ✅ 适合背景图片、简单布局
+- ✅ 结合媒体查询实现断点控制
+
+**使用示例**:
+```css
+.element {
+  /* 设计稿 45px / 375px ≈ 12% */
+  left: 12%;
+  /* 设计稿 229px / 812px ≈ 28.2% */
+  top: 28.2%;
+  width: 65.3%;
+}
+```
+
+---
+
+### 方案四:Tailwind CSS 响应式
+
+**适用场景**: 通用组件、快速开发
+
+**核心思路**: 使用 Tailwind 的响应式工具类和自定义尺寸
+
+**实现位置**: `frontend/tailwind.config.js`
+
+**特点**:
+- ✅ 预定义响应式断点
+- ✅ 自定义 vw/vh 单位
+- ✅ clamp() 流式排版
+- ✅ 快速开发
+
+**使用示例**:
+```tsx
+<div className="w-responsive-md h-responsive-lg text-fluid-base">
+  {/* 响应式内容 */}
+</div>
+```
+
+---
+
+## 📊 适配方案对比
+
+| 方案 | 适用场景 | 优点 | 缺点 | 推荐度 |
+|------|---------|------|------|--------|
+| **完全计算式布局** | 复杂布局、精确控制 | 无重叠、可预测、完全响应 | 代码复杂、需要计算 | ⭐⭐⭐⭐⭐ |
+| **rem 适配** | 全局基础、简单页面 | 统一单位、易维护 | 需要转换、精度问题 | ⭐⭐⭐⭐ |
+| **vw/vh 适配** | 简单布局、背景图片 | 直接响应、简单直观 | 精度问题、复杂布局难控制 | ⭐⭐⭐ |
+| **Tailwind 响应式** | 快速开发、通用组件 | 开发快速、维护方便 | 需要配置、灵活性一般 | ⭐⭐⭐⭐ |
+
+---
+
+## 🎨 针对 375×812 设计稿的最佳实践
+
+### 1. 复杂页面(如 ChatBotPage)
+
+**推荐方案**: 完全计算式布局系统
+
+```typescript
+// ✅ 推荐:使用 calculateLayout()
+const layout = calculateLayout();
+<div style={{
+  top: `${layout.mainAvatarTop}px`,
+  width: `${layout.mainAvatarWidth}px`,
+}} />
+
+// ❌ 不推荐:硬编码像素值
+<div style={{ top: '248px', width: '343px' }} />
+```
+
+**优势**:
+- 自动适配所有屏幕尺寸
+- 保证元素不重叠、不溢出
+- 支持极小屏幕降级(滚动)
+
+---
+
+### 2. 简单页面(如 WelcomePage)
+
+**推荐方案**: vw/vh + 媒体查询
+
+```css
+/* ✅ 推荐:使用视口单位 */
+.element {
+  top: 28.2%; /* 229px / 812px */
+  left: 12%;  /* 45px / 375px */
+  width: 65.3%; /* 245px / 375px */
+}
+
+/* 平板/桌面:固定尺寸 */
+@media (min-width: 768px) {
+  .element {
+    width: 245px;
+    left: 45px;
+    top: 229px;
+  }
+}
+```
+
+**优势**:
+- 代码简洁
+- 直接响应视口变化
+- 适合简单布局
+
+---
+
+### 3. 全局基础适配
+
+**推荐方案**: rem 适配 + Tailwind CSS
+
+```typescript
+// 1. 初始化 rem(main.tsx)
+import { initRem } from '@/utils/rem';
+initRem();
+
+// 2. 使用 Tailwind 工具类
+<div className="w-responsive-md h-responsive-lg">
+  {/* 内容 */}
+</div>
+```
+
+**优势**:
+- 统一适配策略
+- 开发效率高
+- 维护成本低
+
+---
+
+## 📐 尺寸转换公式
+
+### px → rem(rem 适配)
+
+```
+设计稿 px 值 ÷ 100 = rem 值
+例如:375px ÷ 100 = 3.75rem
+```
+
+### px → vw/vh(视口单位)
+
+```
+宽度:设计稿 px 值 ÷ 375 × 100 = vw%
+高度:设计稿 px 值 ÷ 812 × 100 = vh%
+例如:45px ÷ 375 × 100 = 12vw
+```
+
+### px → 缩放比例(完全计算式布局)
+
+```
+实际尺寸 = 设计稿尺寸 × scale
+scale = min(viewportHeight / 812, viewportWidth / 375)
+例如:343px × 0.8 = 274.4px
+```
+
+---
+
+## 🔧 实施建议
+
+### 1. 新页面开发流程
+
+```typescript
+// Step 1: 判断页面复杂度
+if (复杂布局) {
+  // 使用完全计算式布局
+  const layout = calculateLayout();
+} else if (简单布局) {
+  // 使用 vw/vh + 媒体查询
+  // 或使用 Tailwind 响应式工具类
+}
+```
+
+### 2. 设计稿还原步骤
+
+1. **确定基准尺寸**: 375×812
+2. **选择适配方案**: 根据页面复杂度选择
+3. **转换尺寸**: 使用对应公式转换
+4. **测试验证**: 在不同设备上测试
+
+### 3. 常见问题处理
+
+| 问题 | 解决方案 |
+|------|---------|
+| 元素重叠 | 使用完全计算式布局系统 |
+| 小屏幕溢出 | 启用滚动降级(`needsScroll`) |
+| 横屏适配 | 检测 `orientation`,调整布局 |
+| 安全区域 | 使用 `env(safe-area-inset-*)` |
+
+---
+
+## 📱 设备适配范围
+
+| 设备类型 | 宽度范围 | 高度范围 | 适配策略 |
+|---------|---------|---------|---------|
+| **极小手机** | 280px | 320px+ | 0.6 缩放 + 滚动 |
+| **小屏手机** | 320px | 568px+ | 0.7-0.8 缩放 |
+| **标准手机** | 375px | 812px | 1.0 基准 |
+| **大屏手机** | 430px | 932px+ | 1.0-1.2 缩放 |
+| **平板** | 768px+ | 1024px+ | 固定 375px 居中 |
+| **桌面** | 1024px+ | 768px+ | 固定 375px 居中 |
+
+---
+
+## ✅ 最佳实践总结
+
+### 1. 复杂页面
+- ✅ 使用 `calculateLayout()` 完全计算式布局
+- ✅ 所有尺寸从 `layout` 对象获取
+- ✅ 监听 `resize` 事件更新布局
+
+### 2. 简单页面
+- ✅ 使用 vw/vh 单位
+- ✅ 结合媒体查询处理断点
+- ✅ 平板/桌面固定设计稿尺寸
+
+### 3. 全局适配
+- ✅ 初始化 rem 适配
+- ✅ 使用 Tailwind 响应式工具类
+- ✅ 统一使用设计规范
+
+### 4. 避免事项
+- ❌ 硬编码像素值
+- ❌ 混合使用多种适配方案
+- ❌ 忽略极小屏幕降级
+- ❌ 忽略安全区域适配
+
+---
+
+## 📚 相关文档
+
+- [完全计算式自适应布局系统设计.md](./完全计算式自适应布局系统设计.md)
+- [完全计算式布局系统使用指南.md](./完全计算式布局系统使用指南.md)
+- [前端尺寸规范总结.md](./前端尺寸规范总结.md)
+- [V0.6-移动端适配总结.md](./V0.6-移动端适配总结.md)
+
+---
+
+**更新时间**: 2025-01-18  
+**版本**: V1.0  
+**状态**: ✅ 已实施
+