zlwu hai 7 meses
pai
achega
bafc0b5b57

+ 3 - 3
.cursorrules

@@ -132,7 +132,7 @@ const base64 = btoa(String.fromCharCode(...audioData));
 ### 移动端适配 (postcss-px-to-viewport)
 
 #### 配置信息
-- **设计稿基准**: 375px × 812px (iPhone X/11/12/13/14)
+- **设计稿基准**: 393px × 852px(与 postcss / `px2vw` 一致)
 - **转换单位**: vw (viewport width)
 - **自动转换**: CSS 文件中的 px 自动转换为 vw
 - **排除目录**: node_modules(第三方组件不转换)
@@ -273,8 +273,8 @@ npm run dev
 // frontend/src/utils/viewport.ts
 import { px2vw, px2vh } from '@/utils/viewport';
 
-px2vw(100)  // → '26.66667vw'  (基于 375px)
-px2vh(100)  // → '12.31527vh'  (基于 812px)
+px2vw(100)  // → '25.44529vw'  (基于 393px)
+px2vh(100)  // → '11.73709vh'  (基于 852px)
 ```
 
 ---

+ 2 - 2
frontend/postcss.config.js

@@ -2,8 +2,8 @@ export default {
   plugins: {
     '@tailwindcss/postcss': {},
     'postcss-px-to-viewport': {
-      viewportWidth: 375, // 设计稿宽度
-      viewportHeight: 812, // 设计稿高度
+      viewportWidth: 393, // 设计稿宽度
+      viewportHeight: 852, // 设计稿高度
       unitPrecision: 5, // 转换后的小数精度
       viewportUnit: 'vw', // 转换的单位
       selectorBlackList: [

+ 1 - 1
frontend/src/components/AudioWave.tsx

@@ -103,7 +103,7 @@ const buildTargetsFromFrequency = (frequency: Uint8Array | null, barCount: numbe
 };
 
 export const AudioWave = forwardRef<AudioWaveHandle, AudioWaveProps>(
-  ({ isAnimating = false, width = 375, height = 56 }, ref) => {
+  ({ isAnimating = false, width = 393, height = 56 }, ref) => {
   const containerRef = useRef<HTMLDivElement>(null);
   const [actualWidth, setActualWidth] = useState(width);
   

+ 9 - 9
frontend/src/components/ChatBotStaticShell.tsx

@@ -666,7 +666,7 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
           </span>
         </div>
       )}
-      {/* 教学步骤 10:字幕区域,样式与 tutorial-1 一致,基于 375 设计稿 */}
+      {/* 教学步骤 10:字幕区域,样式与 tutorial-1 一致,基于 393 设计稿 */}
       {step === 10 && (
         <div
           style={{
@@ -881,7 +881,7 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
         style={{
           width: px2vmin(312),
           height: px2vmin(98),
-          top: px2vmin(592),
+          top: px2vmin(622),
           left: '50%',
           transform: 'translateX(-50%)',
           // 教学步骤 3/4/5/7/8/9 时,底部按钮区域放在蒙层下方
@@ -1047,7 +1047,7 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
             style={{
               width: px2vmin(64),
               height: px2vmin(32),
-              top: px2vmin(592 - 32 - 8),
+              top: px2vmin(622 - 32 - 8),
               zIndex: 1,
               backgroundColor: 'rgba(0, 0, 0, 0.4)',
               borderRadius: 20,
@@ -1069,7 +1069,7 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
             style={{
               width: px2vmin(312),
               height: px2vmin(98),
-              top: px2vmin(592),
+              top: px2vmin(622),
               left: '50%',
               transform: 'translateX(-50%)',
               zIndex: 6,
@@ -1239,10 +1239,10 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
         <div
           className="absolute"
           style={{
-            top: px2vmin(592),
+            top: px2vmin(622),
             left: '50%',
             transform: 'translateX(-50%)',
-            // 根元素宽高基于 375 设计稿
+            // 根元素宽高基于 393 设计稿
             width: px2vmin(312),
             height: px2vmin(98),
             zIndex: 6,
@@ -1300,7 +1300,7 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
                 style={{
                   left: 0,
                   bottom: '100%',
-                  // 教学步骤 7 弹窗以 375×812 设计稿为基准(mvp)
+                  // 教学步骤 7 弹窗以 393×852 设计稿为基准(mvp)
                   marginBottom: px2vmin(2),
                   width: px2vmin(126),
                   height: px2vmin(166),
@@ -1411,7 +1411,7 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
                 className="absolute z-40 touch-manipulation border-0 bg-transparent p-0 cursor-pointer"
                 style={{
                   // 弹窗 bottom: 100% + 2px(设计稿),高度 166px,再加 5px 间距
-                  // 这里统一用 px2vmin 做换算:2 + 166 + 5 = 173(基于 375 设计稿)
+                  // 这里统一用 px2vmin 做换算:2 + 166 + 5 = 173(基于 393 设计稿)
                   bottom: `calc(100% + ${px2vmin(173)})`,
                   left: `calc(-${px2vmin(20)})`,
                   height: px2vmin(75),
@@ -1444,7 +1444,7 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
         <div
           className="absolute"
           style={{
-            top: px2vmin(592),
+            top: px2vmin(622),
             left: '50%',
             transform: 'translateX(-50%)',
             width: px2vmin(312, 'v2'),

+ 1 - 1
frontend/src/components/LanguageSelector.tsx

@@ -1,5 +1,5 @@
 /**
- * 语言下拉(与 login-register 页同款样式,默认 mvp 375 设计稿 px2vmin)
+ * 语言下拉(与 login-register 页同款样式,默认 mvp 393 设计稿 px2vmin)
  */
 import { useState } from 'react';
 import { useTranslation } from 'react-i18next';

+ 1 - 1
frontend/src/components/MissionEnergyStoneModal.tsx

@@ -98,7 +98,7 @@ export const MissionEnergyStoneModal: React.FC<MissionEnergyStoneModalProps> = (
           bottom: 0,
           left: '50%',
           transform: 'translateX(-50%)',
-          width: px2vw(375),
+          width: px2vw(393),
           backgroundColor: '#34313D',
           borderTopLeftRadius: px2vmin(32),
           borderTopRightRadius: px2vmin(32),

+ 7 - 7
frontend/src/constants/chatbot.ts

@@ -3,14 +3,14 @@
  * 凳子图与特殊场景 GIF 均基于此计算,修改此处即可同步两处位置
  */
 export const STOOL_LAYOUT = {
-  left: 101,
-  top: 356,
-  width: 102,
-  height: 45,
+  left: 106,
+  top: 374,
+  width: 107,
+  height: 47,
   /** 特殊场景凳子上 GIF 的宽高 */
-  sceneGifSize: 185,
+  sceneGifSize: 194,
   /** 特殊场景 GIF 向下偏移量(正数=更靠近凳子,像坐在凳子上) */
-  sceneGifOffsetY: 24,
+  sceneGifOffsetY: 25,
   /** 按场景覆盖垂直偏移:window 负值让 GIF 离凳子再远一点 */
-  sceneGifOffsetYByScene: { window: -48 } as Record<string, number>,
+  sceneGifOffsetYByScene: { window: -50 } as Record<string, number>,
 } as const;

+ 9 - 9
frontend/src/pages/ChatBotPage.tsx

@@ -925,8 +925,8 @@ export const ChatBotPage = () => {
 
         // 计算终点 top 位置(像素)
         const endTopPx = Math.min(
-          (452 * Math.min(viewportWidth, 600) / 375),
-          (452 / 812) * viewportHeight
+          (452 * Math.min(viewportWidth, 600) / 393),
+          (452 / 852) * viewportHeight
         );
         const endLeft = viewportWidth / 2; // 50% 位置(像素)
         const dx = endLeft - startLeft;
@@ -983,8 +983,8 @@ export const ChatBotPage = () => {
         const viewportWidth = window.innerWidth;
         const viewportHeight = window.innerHeight;
         const endTopPx = Math.min(
-          (452 * Math.min(viewportWidth, 600) / 375),
-          (452 / 812) * viewportHeight
+          (452 * Math.min(viewportWidth, 600) / 393),
+          (452 / 852) * viewportHeight
         );
         const endLeft = viewportWidth / 2;
         const dx = endLeft - startLeft;
@@ -1026,7 +1026,7 @@ export const ChatBotPage = () => {
     }
   }, [startStoneAnimationAndRemove]);
 
-  // 能量石展示区域:宽高 & 位置按设计稿 375×812 固定,以 px2vw/px2vh/px2vmin 适配
+  // 能量石展示区域:宽高 & 位置按设计稿 393×852 固定,以 px2vw/px2vh/px2vmin 适配
 
   const stopRecordingTimer = useCallback((options?: { preserveCountdown?: boolean }) => {
     if (recordingTimerRef.current) {
@@ -3554,7 +3554,7 @@ export const ChatBotPage = () => {
           style={{
             width: px2vmin(64),
             height: px2vmin(32),
-            top: px2vmin(592 - 32 - 8),
+            top: px2vmin(622 - 32 - 8),
             zIndex: 19,
             backgroundColor: 'rgba(0, 0, 0, 0.4)',
             borderRadius: 20,
@@ -3581,13 +3581,13 @@ export const ChatBotPage = () => {
         </div>
       )}
 
-      {/* 按钮区域 - 设计稿 393×852:top 592,高 98,宽 312,左右居中 */}
+      {/* 按钮区域 - 设计稿 393×852:top 622,高 98,宽 312,左右居中 */}
       <div
         className="chatbot-progress-container absolute"
         style={{
           width: px2vmin(312),
           height: px2vmin(98),
-          top: px2vmin(592),
+          top: px2vmin(622),
           left: '50%',
           transform: 'translateX(-50%)',
           zIndex: 20,
@@ -3647,7 +3647,7 @@ export const ChatBotPage = () => {
                     style={{
                       left: 0,
                       bottom: '100%',
-                      // ChatBot 设置弹窗以 375×812 设计稿为基准(mvp)
+                      // ChatBot 设置弹窗以 393×852 设计稿为基准(mvp)
                       marginBottom: px2vmin(2),
                       width: px2vmin(126),
                       height: px2vmin(166),

+ 2 - 2
frontend/src/pages/FirstLoginRewardPage.tsx

@@ -436,7 +436,7 @@ export const FirstLoginRewardPage = () => {
             <div
               style={{
                 position: 'relative',
-                width: px2vw(375),
+                width: px2vw(393),
                 display: 'flex',
                 justifyContent: 'center',
               }}
@@ -681,7 +681,7 @@ export const FirstLoginRewardPage = () => {
                 bottom: 0,
                 left: '50%',
                 transform: 'translateX(-50%)',
-                width: px2vw(375),
+                width: px2vw(393),
                 height: px2vmin(162),
                 backgroundColor: '#34313D',
                 borderTopLeftRadius: px2vmin(32),

+ 19 - 23
frontend/src/pages/LoginVerificationCodePage.tsx

@@ -13,10 +13,6 @@ const primaryBtn = getOssUrl('primary-default.png');
 const backButton = getOssUrl('backbutn-icon.png');
 const XIconImage = getOssUrl('x-icon.png');
 
-// 设计稿 393×852 转 375×812 基准(横向 / 纵向)
-const w393 = (px: number) => (px * 375) / 393;
-const h852 = (px: number) => (px * 812) / 852;
-
 interface LocationState {
   loginType?: 'phone' | 'email';
   areaCode?: string;
@@ -160,10 +156,10 @@ export const LoginVerificationCodePage = () => {
         onClick={wrapTrackedClick('login_verification_code', 'back', handleBack)}
         style={{
           position: 'absolute',
-          left: px2vw(w393(24)),
-          top: px2vh(h852(125)),
-          width: px2vw(w393(48)),
-          height: px2vh(h852(48)),
+          left: px2vw(24),
+          top: px2vh(125),
+          width: px2vw(48),
+          height: px2vh(48),
           padding: 0,
           border: 'none',
           background: 'none',
@@ -190,9 +186,9 @@ export const LoginVerificationCodePage = () => {
         style={{
           position: 'absolute',
           left: '50%',
-          top: px2vh(h852(182)),
-          width: px2vw(w393(343)),
-          height: px2vh(h852(184)),
+          top: px2vh(182),
+          width: px2vw(343),
+          height: px2vh(184),
           transform: 'translateX(-50%)',
           objectFit: 'contain',
         }}
@@ -203,13 +199,13 @@ export const LoginVerificationCodePage = () => {
         style={{
           left: '50%',
           transform: 'translateX(-50%)',
-          top: px2vh(h852(376)),
-          width: px2vw(w393(312)),
-          height: px2vh(h852(160)),
+          top: px2vh(376),
+          width: px2vw(312),
+          height: px2vh(160),
           display: 'flex',
           flexDirection: 'column',
-          paddingTop: px2vh(h852(26)),
-          gap: px2vh(h852(22)),
+          paddingTop: px2vh(26),
+          gap: px2vh(22),
         }}
       >
         {/* 区号+手机号一行:距整体顶 26,高 56,宽 312;区号 94 + 间距 6 + 手机号 212 */}
@@ -217,13 +213,13 @@ export const LoginVerificationCodePage = () => {
           <div
             style={{
               display: 'flex',
-              gap: px2vw(w393(6)),
+              gap: px2vw(6),
               width: '100%',
-              height: px2vh(h852(56)),
+              height: px2vh(56),
             }}
           >
             {/* 区号下拉选择 宽94,四角圆角 */}
-            <div style={{ width: px2vw(w393(94)), position: 'relative', flexShrink: 0 }} ref={areaCodeContainerRef}>
+            <div style={{ width: px2vw(94), position: 'relative', flexShrink: 0 }} ref={areaCodeContainerRef}>
               <div
                 className="relative overflow-visible"
                 style={{
@@ -317,7 +313,7 @@ export const LoginVerificationCodePage = () => {
               </div>
             </div>
             {/* 手机号输入框 宽212;四角圆角,聚焦时边框 #BF9DF8 1px */}
-            <div style={{ width: px2vw(w393(212)), position: 'relative', flexShrink: 0 }}>
+            <div style={{ width: px2vw(212), position: 'relative', flexShrink: 0 }}>
               <div
                 className="relative overflow-visible"
                 style={{
@@ -373,7 +369,7 @@ export const LoginVerificationCodePage = () => {
         )}
         {/* 邮箱登录:邮箱输入框(占满一行高56),四角圆角 */}
         {selectedLoginType === 'email' && (
-          <div style={{ width: '100%', height: px2vh(h852(56)), position: 'relative', flexShrink: 0 }}>
+          <div style={{ width: '100%', height: px2vh(56), position: 'relative', flexShrink: 0 }}>
             <div
               className="relative overflow-visible"
               style={{
@@ -429,7 +425,7 @@ export const LoginVerificationCodePage = () => {
           <div
             style={{
               width: '100%',
-              height: px2vh(h852(56)),
+              height: px2vh(56),
               flexShrink: 0,
               display: 'flex',
               alignItems: 'center',
@@ -451,7 +447,7 @@ export const LoginVerificationCodePage = () => {
           }
           style={{
             width: '100%',
-            height: px2vh(h852(56)),
+            height: px2vh(56),
             flexShrink: 0,
             border: 'none',
             padding: 0,

+ 2 - 2
frontend/src/pages/LumiOutfitsPage.tsx

@@ -185,7 +185,7 @@ export const LumiOutfitsPage = () => {
               width: px2vmin(240),
               height: px2vmin(240),
               left: '50%',
-              top: px2vmin(231),
+              top: px2vmin(251),
               transform: 'translateX(-50%)',
               zIndex: 5,
               objectFit: 'contain',
@@ -233,7 +233,7 @@ export const LumiOutfitsPage = () => {
           bottom: 0,
           left: '50%',
           transform: 'translateX(-50%)',
-          width: px2vw(375),
+          width: px2vw(393),
           zIndex: 9999,
           boxSizing: 'border-box',
         }}

+ 17 - 21
frontend/src/pages/PhoneLogin.tsx

@@ -13,10 +13,6 @@ const primaryBtn = getOssUrl('primary-default.png');
 const backButton = getOssUrl('backbutn-icon.png');
 const XIconImage = getOssUrl('x-icon.png');
 
-// 设计稿 393×852 转 375×812 基准(横向 / 纵向)
-const w393 = (px: number) => (px * 375) / 393;
-const h852 = (px: number) => (px * 812) / 852;
-
 interface LocationState {
   loginType?: 'phone' | 'qq' | 'email';
   areaCode?: string;
@@ -172,8 +168,8 @@ export const PhoneLogin = () => {
         onClick={wrapTrackedClick('phone_login', 'back', handleBack)}
         style={{
           position: 'absolute',
-          left: px2vw(w393(24)),
-          top: px2vh(h852(125)),
+          left: px2vw(24),
+          top: px2vh(125),
           width: px2vmin(48),
           height: px2vmin(48),
           padding: 0,
@@ -203,9 +199,9 @@ export const PhoneLogin = () => {
         style={{
           position: 'absolute',
           left: '50%',
-          top: px2vh(h852(182)),
-          width: px2vw(w393(343)),
-          height: px2vh(h852(184)),
+          top: px2vh(182),
+          width: px2vw(343),
+          height: px2vh(184),
           transform: 'translateX(-50%)',
           objectFit: 'contain',
         }}
@@ -216,13 +212,13 @@ export const PhoneLogin = () => {
         style={{
           left: '50%',
           transform: 'translateX(-50%)',
-          top: px2vh(h852(376)),
-          width: px2vw(w393(312)),
-          height: px2vh(h852(160)),
+          top: px2vh(376),
+          width: px2vw(312),
+          height: px2vh(160),
           display: 'flex',
           flexDirection: 'column',
-          paddingTop: px2vh(h852(26)),
-          gap: px2vh(h852(22)),
+          paddingTop: px2vh(26),
+          gap: px2vh(22),
         }}
       >
         {/* 区号+手机号一行:距整体顶 26,高 56,宽 312;区号 94 + 间距 6 + 手机号 212 */}
@@ -230,13 +226,13 @@ export const PhoneLogin = () => {
           <div
             style={{
               display: 'flex',
-              gap: px2vw(w393(6)),
+              gap: px2vw(6),
               width: '100%',
-              height: px2vh(h852(56)),
+              height: px2vh(56),
             }}
           >
             {/* 区号下拉选择 宽94 */}
-            <div style={{ width: px2vw(w393(94)), position: 'relative', flexShrink: 0 }} ref={areaCodeContainerRef}>
+            <div style={{ width: px2vw(94), position: 'relative', flexShrink: 0 }} ref={areaCodeContainerRef}>
               <div
                 className="relative overflow-visible"
                 style={{
@@ -330,7 +326,7 @@ export const PhoneLogin = () => {
               </div>
             </div>
             {/* 手机号输入框 宽212;聚焦时边框 #BF9DF8 1px */}
-            <div style={{ width: px2vw(w393(212)), position: 'relative', flexShrink: 0 }}>
+            <div style={{ width: px2vw(212), position: 'relative', flexShrink: 0 }}>
               <div
                 className="relative overflow-visible"
                 style={{
@@ -386,7 +382,7 @@ export const PhoneLogin = () => {
         )}
         {/* 邮箱登录:邮箱输入框(占满一行高56) */}
         {selectedLoginType === 'email' && (
-          <div style={{ width: '100%', height: px2vh(h852(56)), position: 'relative', flexShrink: 0 }}>
+          <div style={{ width: '100%', height: px2vh(56), position: 'relative', flexShrink: 0 }}>
             <div
               className="relative overflow-visible"
               style={{
@@ -452,7 +448,7 @@ export const PhoneLogin = () => {
           <div
             style={{
               width: '100%',
-              height: px2vh(h852(56)),
+              height: px2vh(56),
               flexShrink: 0,
               display: 'flex',
               alignItems: 'center',
@@ -472,7 +468,7 @@ export const PhoneLogin = () => {
           disabled={isChecking}
           style={{
             width: '100%',
-            height: px2vh(h852(56)),
+            height: px2vh(56),
             flexShrink: 0,
             border: 'none',
             padding: 0,

+ 3 - 3
frontend/src/pages/ProfilePage.tsx

@@ -1855,7 +1855,7 @@ export const ProfilePage = () => {
               bottom: 0,
               left: '50%',
               transform: 'translateX(-50%)',
-              width: px2vw(375),
+              width: px2vw(393),
               backgroundColor: '#34313D',
               borderTopLeftRadius: px2vmin(32),
               borderTopRightRadius: px2vmin(32),
@@ -2061,7 +2061,7 @@ export const ProfilePage = () => {
               bottom: 0,
               left: '50%',
               transform: 'translateX(-50%)',
-              width: px2vw(375),
+              width: px2vw(393),
               backgroundColor: '#34313D',
               borderTopLeftRadius: px2vmin(32),
               borderTopRightRadius: px2vmin(32),
@@ -2283,7 +2283,7 @@ export const ProfilePage = () => {
               bottom: 0,
               left: '50%',
               transform: 'translateX(-50%)',
-              width: px2vw(375),
+              width: px2vw(393),
               height: px2vmin(420),
               backgroundColor: '#34313D',
               borderTopLeftRadius: px2vmin(32),

+ 8 - 8
frontend/src/styles/chatbot-fix copy.css

@@ -123,13 +123,13 @@ body[data-too-small="true"] .chatbot-bg,
 /* 正式能量石缩小动画 - 从 48px 缩小到 4.8px (10%) */
 @keyframes energyStoneShrink {
   0% {
-    width: min(calc(48 * min(100vw, 600PX) / 375), calc(5.911330049261084 * var(--vh, 1vh))) !important;
-    height: min(calc(48 * min(100vw, 600PX) / 375), calc(5.911330049261084 * var(--vh, 1vh))) !important;
+    width: min(calc(48 * min(100vw, 600PX) / 393), calc(5.633802816901408 * var(--vh, 1vh))) !important;
+    height: min(calc(48 * min(100vw, 600PX) / 393), calc(5.633802816901408 * var(--vh, 1vh))) !important;
     opacity: 1 !important;
   }
   100% {
-    width: min(calc(48 * min(100vw, 600PX) / 375), calc(5.911330049261084 * var(--vh, 1vh))) !important;
-    height: min(calc(48 * min(100vw, 600PX) / 375), calc(5.911330049261084 * var(--vh, 1vh))) !important;
+    width: min(calc(48 * min(100vw, 600PX) / 393), calc(5.633802816901408 * var(--vh, 1vh))) !important;
+    height: min(calc(48 * min(100vw, 600PX) / 393), calc(5.633802816901408 * var(--vh, 1vh))) !important;
     opacity: 0 !important;
   }
 }
@@ -137,13 +137,13 @@ body[data-too-small="true"] .chatbot-bg,
 /* 正式能量石光晕缩小动画 - 从 92px 缩小到 9.2px (10%) */
 @keyframes energyStoneHintShrink {
   0% {
-    width: min(calc(92 * min(100vw, 600PX) / 375), calc(11.330049261083744 * var(--vh, 1vh))) !important;
-    height: min(calc(92 * min(100vw, 600PX) / 375), calc(11.330049261083744 * var(--vh, 1vh))) !important;
+    width: min(calc(92 * min(100vw, 600PX) / 393), calc(10.7981220657277 * var(--vh, 1vh))) !important;
+    height: min(calc(92 * min(100vw, 600PX) / 393), calc(10.7981220657277 * var(--vh, 1vh))) !important;
     opacity: 1 !important;
   }
   100% {
-    width: min(calc(92 * min(100vw, 600PX) / 375), calc(11.330049261083744 * var(--vh, 1vh))) !important;
-    height: min(calc(92 * min(100vw, 600PX) / 375), calc(11.330049261083744 * var(--vh, 1vh))) !important;
+    width: min(calc(92 * min(100vw, 600PX) / 393), calc(10.7981220657277 * var(--vh, 1vh))) !important;
+    height: min(calc(92 * min(100vw, 600PX) / 393), calc(10.7981220657277 * var(--vh, 1vh))) !important;
     opacity: 0 !important;
   }
 }

+ 8 - 8
frontend/src/styles/chatbot-fix.css

@@ -119,13 +119,13 @@ body[data-too-small="true"] .chatbot-bg,
 /* 正式能量石缩小动画 - 保持尺寸不变,只做渐隐 */
 @keyframes energyStoneShrink {
   0% {
-    width: min(calc(48 * min(100vw, 600PX) / 375), calc(5.911330049261084 * var(--vh, 1vh))) !important;
-    height: min(calc(48 * min(100vw, 600PX) / 375), calc(5.911330049261084 * var(--vh, 1vh))) !important;
+    width: min(calc(48 * min(100vw, 600PX) / 393), calc(5.633802816901408 * var(--vh, 1vh))) !important;
+    height: min(calc(48 * min(100vw, 600PX) / 393), calc(5.633802816901408 * var(--vh, 1vh))) !important;
     opacity: 1 !important;
   }
   100% {
-    width: min(calc(48 * min(100vw, 600PX) / 375), calc(5.911330049261084 * var(--vh, 1vh))) !important;
-    height: min(calc(48 * min(100vw, 600PX) / 375), calc(5.911330049261084 * var(--vh, 1vh))) !important;
+    width: min(calc(48 * min(100vw, 600PX) / 393), calc(5.633802816901408 * var(--vh, 1vh))) !important;
+    height: min(calc(48 * min(100vw, 600PX) / 393), calc(5.633802816901408 * var(--vh, 1vh))) !important;
     opacity: 0 !important;
   }
 }
@@ -133,13 +133,13 @@ body[data-too-small="true"] .chatbot-bg,
 /* 正式能量石光晕缩小动画 - 保持尺寸不变,只做渐隐 */
 @keyframes energyStoneHintShrink {
   0% {
-    width: min(calc(92 * min(100vw, 600PX) / 375), calc(11.330049261083744 * var(--vh, 1vh))) !important;
-    height: min(calc(92 * min(100vw, 600PX) / 375), calc(11.330049261083744 * var(--vh, 1vh))) !important;
+    width: min(calc(92 * min(100vw, 600PX) / 393), calc(10.7981220657277 * var(--vh, 1vh))) !important;
+    height: min(calc(92 * min(100vw, 600PX) / 393), calc(10.7981220657277 * var(--vh, 1vh))) !important;
     opacity: 1 !important;
   }
   100% {
-    width: min(calc(92 * min(100vw, 600PX) / 375), calc(11.330049261083744 * var(--vh, 1vh))) !important;
-    height: min(calc(92 * min(100vw, 600PX) / 375), calc(11.330049261083744 * var(--vh, 1vh))) !important;
+    width: min(calc(92 * min(100vw, 600PX) / 393), calc(10.7981220657277 * var(--vh, 1vh))) !important;
+    height: min(calc(92 * min(100vw, 600PX) / 393), calc(10.7981220657277 * var(--vh, 1vh))) !important;
     opacity: 0 !important;
   }
 }

+ 1 - 1
frontend/src/styles/design-tokens.js

@@ -4,7 +4,7 @@
  * 统一管理项目中的设计基础元素:间距、颜色、字体、圆角、阴影等
  * 所有响应式值使用 clamp() 实现流式设计
  * 
- * 基准设计尺寸: 375x812 (iPhone X/XS/11 Pro)
+ * 基准设计尺寸: 393×852(与 postcss / px2vw 一致)
  */
 
 export const designTokens = {

+ 3 - 3
frontend/src/utils/rem.ts

@@ -1,12 +1,12 @@
 /**
  * rem 适配工具 - 全终端适配方案(防御式设计)
- * 设计稿基准: 375px (iPhone X/11/12/13/14 标准宽度)
+ * 设计稿基准: 393px(与 postcss / px2vw 一致)
  * 1rem = 100px (便于计算,设计稿 100px = 1rem)
  * 
  * 适配范围: 280px - 768px (支持手机到平板)
  * - 280px 屏幕: 根字体 75px (iPhone SE 竖屏)
  * - 320px 屏幕: 根字体 85px (小屏手机)
- * - 375px 屏幕: 根字体 100px (标准手机)
+ * - 393px 屏幕: 根字体约 100px (标准手机)
  * - 430px 屏幕: 根字体 114px (大屏手机)
  * - 768px 屏幕: 根字体 100px (平板,固定标准大小)
  * - 1024px+ 屏幕: 根字体 100px (桌面,居中显示)
@@ -14,7 +14,7 @@
  * 兼容性: iOS 8+, Android 4.4+, 所有现代浏览器
  */
 
-const BASE_WIDTH = 375; // 设计稿宽度
+const BASE_WIDTH = 393; // 设计稿宽度
 const BASE_FONT_SIZE = 100; // 1rem = 100px
 const MIN_WIDTH = 280; // 最小宽度(支持极小屏幕)
 const MOBILE_MAX_WIDTH = 430; // 移动端最大宽度(超过则按平板处理)

+ 52 - 55
frontend/src/utils/screenSize.ts

@@ -116,10 +116,10 @@ export const getScreenSize = (): ScreenSize => {
 
 /**
  * 根据屏幕高度计算缩放比例
- * @param baseHeight 基准高度(设计稿高度,如 812px)
+ * @param baseHeight 基准高度(设计稿高度,如 852px)
  * @returns 缩放比例 (0.7 - 1.0)
  */
-export const getScaleFactor = (baseHeight: number = 812): number => {
+export const getScaleFactor = (baseHeight: number = 852): number => {
   const { height } = getScreenSize();
   
   // 计算缩放比例,最小 0.7,最大 1.0
@@ -198,17 +198,17 @@ export const getLayoutParams = (): LayoutParams => {
       progressRingPadding: 17, // 88 * 0.92 * 0.21 ≈ 17 (按比例缩放)
     };
   } else {
-    // 正常屏幕:使用设计稿尺寸
+    // 正常屏幕:使用设计稿尺寸(393×852)
     return {
       avatarScale: 1.0,
-      avatarTopOffset: 152,
-      emotionTopOffset: 610,
-      recordButtonTopOffset: 661,
-      recordButtonSize: 96,
-      waveformHeight: 56,
-      verticalGap: 20,
-      buttonGap: 35,
-      progressRingPadding: 18, // 基准值:96 * 0.1875 = 18
+      avatarTopOffset: 160,
+      emotionTopOffset: 640,
+      recordButtonTopOffset: 693,
+      recordButtonSize: 101,
+      waveformHeight: 59,
+      verticalGap: 21,
+      buttonGap: 37,
+      progressRingPadding: 19,
     };
   }
 };
@@ -264,52 +264,52 @@ export const updateTooSmallFlag = (): void => {
 // ============================================================================
 
 /**
- * 基准布局参数(设计稿:375×812)
+ * 基准布局参数(设计稿:393×852,由原 375×812 等比换算)
  */
 const BASE_LAYOUT = {
   // 视口
-  baseWidth: 375,
-  baseHeight: 812,
+  baseWidth: 393,
+  baseHeight: 852,
   
   // 安全区域
-  topSafeArea: 44,      // 顶部安全区(状态栏)
-  bottomSafeArea: 34,   // ⚠️ 已废弃:底部安全区现在使用 detectBottomSafeArea() 动态检测
+  topSafeArea: 46,      // 顶部安全区(状态栏)
+  bottomSafeArea: 36,   // ⚠️ 已废弃:底部安全区现在使用 detectBottomSafeArea() 动态检测
   
   // 进度环容器距离底部(设计稿基准)
-  progressContainerBottom: 84, // 进度环容器距离视口底部的距离
+  progressContainerBottom: 88, // 进度环容器距离视口底部的距离
   
   // AnimaZone 头像区域(新设计稿)
-  avatarWidth: 343,     // 设计稿宽度
-  avatarHeight: 340,    // 设计稿高度
-  avatarTop: 248,       // 距离顶部位置
-  avatarContainerHeight: 478,
+  avatarWidth: 360,     // 设计稿宽度
+  avatarHeight: 357,    // 设计稿高度
+  avatarTop: 260,       // 距离顶部位置
+  avatarContainerHeight: 502,
   
   // 装饰元素
-  treesWidth: 395,
-  treesLeft: -26,      // 设计稿中树木 left: -26px(向左偏移,部分超出屏幕)
-  avatarCSize: 40,
+  treesWidth: 414,
+  treesLeft: -27,      // 设计稿中树木 left(向左偏移,部分超出屏幕)
+  avatarCSize: 42,
   
   // 情感标签
-  emotionTop: 50,       // 距离顶部位置
-  emotionHeight: 36,
-  emotionGap: 20,       // 情感标签与头像的间距
+  emotionTop: 53,       // 距离顶部位置
+  emotionHeight: 38,
+  emotionGap: 21,       // 情感标签与头像的间距
   
   // 录音按钮
-  recordButtonSize: 96,
-  recordButtonMinSize: 44,
+  recordButtonSize: 101,
+  recordButtonMinSize: 46,
   
-  // 进度环(基于设计稿 375x812)
-  progressRingPadding: 18,
-  progressRingToAvatarGap: 26,  // 进度环顶部距离头像底部的间距
-  progressRingSize: 114,    // 进度环容器尺寸
+  // 进度环(基于设计稿 393×852)
+  progressRingPadding: 19,
+  progressRingToAvatarGap: 27,  // 进度环顶部距离头像底部的间距
+  progressRingSize: 120,    // 进度环容器尺寸
   
   // 波形图
-  waveformHeight: 56,
+  waveformHeight: 59,
   
   // 间距
-  verticalGap: 20,      // 波形与按钮的垂直间距
-  buttonGap: 35,        // 取消和发送按钮之间的水平间距(仅用于旧布局,V3 等比例版本中会根据按钮尺寸重新计算)
-  minGapBetweenElements: 16, // 元素之间最小间距
+  verticalGap: 21,      // 波形与按钮的垂直间距
+  buttonGap: 37,        // 取消和发送按钮之间的水平间距(仅用于旧布局,V3 等比例版本中会根据按钮尺寸重新计算)
+  minGapBetweenElements: 17, // 元素之间最小间距
 } as const;
 
 /**
@@ -372,7 +372,7 @@ export interface CalculatedLayout {
 /**
  * 完全计算式布局系统 - V3.0 等比例缩放版本
  * 
- * 基于 375×812 基准设计稿,进行等比例缩放适配
+ * 基于 393×852 基准设计稿,进行等比例缩放适配
  * 
  * @param viewportWidth 视口宽度
  * @param viewportHeight 视口高度
@@ -429,9 +429,9 @@ export function calculateLayout(
   
   // 6. 计算各元素位置(等比例缩放,基于设计稿位置)
   // 所有位置按相同缩放比例计算,保持设计稿布局
-  const avatarCTop = Math.round(63 * scale); // 设计稿中 Avatar-C 距顶部 63px
-  const avatarCRight = Math.round(33 * scale); // 设计稿中 Avatar-C 距右侧 33px
-  const treesTop = Math.round(175 * scale); // 设计稿中树木距顶部 175px
+  const avatarCTop = Math.round(66 * scale); // 设计稿中 Avatar-C 距顶部(393×852)
+  const avatarCRight = Math.round(35 * scale); // 设计稿中 Avatar-C 距右侧
+  const treesTop = Math.round(184 * scale); // 设计稿中树木距顶部
   const treesLeft = Math.round(BASE.treesLeft * scale); // 设计稿中树木 left: -26px(自适应)
   const animaZoneTop = Math.round(BASE.avatarTop * scale); // 设计稿中头像距顶部 248px
   const mainAvatarTop = topSafeArea; // 头像区域从安全区域开始
@@ -468,32 +468,29 @@ export function calculateLayout(
   const recordButtonOffset = (progressRingSize - recordButtonSize) / 2 + baseOffset;
   
   // 10. Tooltips 位置和尺寸(等比例缩放)
-  // 设计稿基准值(375×812)
+  // 设计稿基准值(393×852)
   // 计算 tapToChat 相对于录音按钮的偏移量
-  // 设计稿基准:录音按钮 96×96,TapToChat 128px 宽
-  // top: 相对于录音按钮顶部向上偏移 74px
-  // left: 相对于录音按钮左边缘向右偏移 83px(设计稿基准值)
-  const tapToChatWidth = Math.round(128 * scale);
-  const tapToChatLeftOffset = Math.round(83 * scale); // 从录音按钮左边缘到 TapToChat 左边缘的距离
+  const tapToChatWidth = Math.round(134 * scale);
+  const tapToChatLeftOffset = Math.round(87 * scale); // 从录音按钮左边缘到 TapToChat 左边缘的距离
   
   const tooltips = {
     // 进度条提示:相对于录音按钮左上顶点定位(top: 图片高度的一半的负值,left: 图片宽度的一半的负值)
     progressBar: {
-      width: Math.round(167 * scale),
-      height: Math.round(130.35 * scale),
-      top: -Math.round(130.35 * scale) / 2, // 图片高度的一半的负值
-      left: -Math.round(167 * scale) / 2, // 图片宽度的一半的负值
+      width: Math.round(175 * scale),
+      height: Math.round(137 * scale),
+      top: -Math.round(137 * scale) / 2, // 图片高度的一半的负值
+      left: -Math.round(175 * scale) / 2, // 图片宽度的一半的负值
     },
-    // Tap to Chat:相对于录音按钮定位(top 向上偏移 74px,left 从左边缘向右偏移 83px)
+    // Tap to Chat:相对于录音按钮定位
     tapToChat: {
       width: tapToChatWidth,
       height: 0, // auto
-      top: Math.round(-74 * scale), // 相对于录音按钮顶部向上偏移
+      top: Math.round(-78 * scale), // 相对于录音按钮顶部向上偏移
       left: tapToChatLeftOffset, // 相对于录音按钮左边缘向右偏移
     },
     // 取消按钮提示:使用百分比定位(tooltip 放在按钮容器内)
     cancel: {
-      width: Math.round(110 * scale),
+      width: Math.round(115 * scale),
       height: 0, // auto
       // bottom: 110% - tooltip 底部在按钮底部下方 10% 的位置
       bottom: '110%',
@@ -502,7 +499,7 @@ export function calculateLayout(
     },
     // 发送按钮提示:使用百分比定位(tooltip 放在按钮容器内)
     send: {
-      width: Math.round(111 * scale),
+      width: Math.round(116 * scale),
       height: 0, // auto
       // bottom: 110% - tooltip 底部在按钮底部下方 10% 的位置
       bottom: '110%',

+ 7 - 7
frontend/src/utils/viewport.ts

@@ -1,9 +1,9 @@
 /**
  * viewport 适配工具 - 移动端适配方案
  *
- * 支持两套设计稿:
- * - mvp:375 × 812(老界面默认)
- * - v2:393 × 852(V2.0 新需求界面)
+ * 支持两套设计稿(当前统一为 393 × 852):
+ * - mvp:393 × 852(默认)
+ * - v2:393 × 852(与 mvp 一致,便于旧页面显式传参)
  *
  * 适配策略:
  * - CSS 文件:使用 postcss-px-to-viewport 自动转换 px 为 vw
@@ -16,11 +16,11 @@
  * - 字体 → px2fluidFontSize(px, options?)
  */
 
-/** 设计稿规格:mvp=375×812,v2=393×852 */
+/** 设计稿规格:mvp / v2 均为 393×852 */
 export type ViewportDesign = 'mvp' | 'v2';
 
 const DESIGN_SPECS: Record<ViewportDesign, { width: number; height: number }> = {
-  mvp: { width: 375, height: 812 },
+  mvp: { width: 393, height: 852 },
   v2: { width: 393, height: 852 },
 };
 
@@ -30,7 +30,7 @@ const DESIGN_SPECS: Record<ViewportDesign, { width: number; height: number }> =
  * - 使用 --vw(visualViewport)解决曲面屏问题
  *
  * @param px 设计稿上的 px 值
- * @param design 设计稿规格,默认 'mvp'(375×812)
+ * @param design 设计稿规格,默认 'mvp'(393×852)
  */
 export const px2vw = (px: number, design: ViewportDesign = 'mvp'): string => {
   const baseWidth = DESIGN_SPECS[design].width;
@@ -42,7 +42,7 @@ export const px2vw = (px: number, design: ViewportDesign = 'mvp'): string => {
  * 使用 --vh 避免移动端地址栏/工具栏影响
  *
  * @param px 设计稿上的 px 值
- * @param design 设计稿规格,默认 'mvp'(375×812)
+ * @param design 设计稿规格,默认 'mvp'(393×852)
  */
 export const px2vh = (px: number, design: ViewportDesign = 'mvp'): string => {
   const baseHeight = DESIGN_SPECS[design].height;