zlwu 11 месяцев назад
Родитель
Сommit
944c5204ec
3 измененных файлов с 295 добавлено и 255 удалено
  1. 250 219
      frontend/src/pages/ChatBotPage.tsx
  2. 6 10
      frontend/src/styles/mobile-fix.css
  3. 39 26
      frontend/src/utils/screenSize.ts

+ 250 - 219
frontend/src/pages/ChatBotPage.tsx

@@ -222,7 +222,10 @@ export const ChatBotPage = () => {
   }, [currentState, stopRecordingTimer]);
 
   const formatCountdown = (ms: number) => {
-    return `${Math.max(0, Math.ceil(ms / 1000))}s`;
+    const totalSeconds = Math.max(0, Math.ceil(ms / 1000));
+    const minutes = Math.floor(totalSeconds / 60);
+    const seconds = totalSeconds % 60;
+    return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
   };
 
   // 进度动画效果(支持多圈连续动画)
@@ -319,12 +322,13 @@ export const ChatBotPage = () => {
         setShowCancelTooltip(true);
         setStorage(STORAGE_KEYS.HAS_SHOWN_CANCEL_TOOLTIP, 'true');
         
+        // 🔧 调试模式:注释掉自动隐藏,方便调试
         // 3秒后自动隐藏
-        const timer = setTimeout(() => {
-          setShowCancelTooltip(false);
-        }, 3000);
+        // const timer = setTimeout(() => {
+        //   setShowCancelTooltip(false);
+        // }, 3000);
         
-        return () => clearTimeout(timer);
+        // return () => clearTimeout(timer);
       } else {
         // 已显示过,确保隐藏
         setShowCancelTooltip(false);
@@ -343,12 +347,13 @@ export const ChatBotPage = () => {
         setShowSendTooltip(true);
         setStorage(STORAGE_KEYS.HAS_SHOWN_SEND_TOOLTIP, 'true');
         
+        // 🔧 调试模式:注释掉自动隐藏,方便调试
         // 3秒后自动隐藏
-        const timer = setTimeout(() => {
-          setShowSendTooltip(false);
-        }, 3000);
+        // const timer = setTimeout(() => {
+        //   setShowSendTooltip(false);
+        // }, 3000);
         
-        return () => clearTimeout(timer);
+        // return () => clearTimeout(timer);
       } else {
         // 已显示过,确保隐藏
         setShowSendTooltip(false);
@@ -768,7 +773,11 @@ export const ChatBotPage = () => {
   };
 
   const actionButtonSize = Math.round(layout.recordButtonSize * 0.58);
-  const actionButtonTop = layout.progressRingTop + layout.recordButtonOffset;
+  // 计算取消/发送按钮的 top 位置,使其中心与录音按钮中心对齐
+  // 录音按钮中心 = recordButtonOffset + recordButtonSize / 2
+  // 取消/发送按钮中心 = top + actionButtonSize / 2
+  // 所以:top = recordButtonOffset + (recordButtonSize - actionButtonSize) / 2
+  const actionButtonTop = layout.recordButtonOffset + (layout.recordButtonSize - actionButtonSize) / 2;
   const avatarHintSize = Math.round(layout.avatarCSize * 1.8);
   const shouldShowCountdown =
     currentState === 'recording' &&
@@ -1061,19 +1070,20 @@ export const ChatBotPage = () => {
       </div>
 
       {/* 进度条+录音按钮容器 - 设计稿:bottom 84px, 尺寸 114×114, 左右居中 */}
-      {currentState !== 'recording' && (
-        <div
-          className="chatbot-progress-container absolute"
-          style={{
-            width: `${layout.progressRingSize}px`,
-            height: `${layout.progressRingSize}px`,
-            top: `${layout.progressRingTop}px`,
-            left: '50%',
-            transform: 'translateX(-50%)',
-            zIndex: 20,
-          }}
-        >
-          {/* 进度环 - 背景层 */}
+      {/* 容器在非录音状态显示进度环,在录音状态显示按钮 */}
+      <div
+        className="chatbot-progress-container absolute"
+        style={{
+          width: `${layout.progressRingSize}px`,
+          height: `${layout.progressRingSize}px`,
+          top: `${layout.progressRingTop}px`,
+          left: '50%',
+          transform: 'translateX(-50%)',
+          zIndex: 20,
+        }}
+      >
+          {/* 进度环 - 背景层 - 只在非录音状态显示 */}
+          {currentState !== 'recording' && (
           <div
             className="chatbot-progress-ring absolute"
             style={{
@@ -1121,9 +1131,31 @@ export const ChatBotPage = () => {
                 }}
               />
             </div>
+
+            {/* 进度条提示 - 相对于进度环定位 - 首次有进度时显示 */}
+            {showProgressBarTooltip && isConnected && (
+              <img
+                src={tooltipsProgressBar}
+                alt="Progress Bar Tooltip"
+                className="chatbot-tooltips absolute pointer-events-none"
+                style={{
+                  width: `${layout.tooltips.progressBar.width}px`,
+                  height: `${layout.tooltips.progressBar.height}px`,
+                  top: '-100%',
+                  left: '50%',
+                  transform: `translateX(calc(-50% + ${layout.tooltips.progressBar.left}px))`,
+                  opacity: 1,
+                  zIndex: 10,
+                  maxWidth: 'none', // 确保不被限制
+                  maxHeight: 'none', // 确保不被限制
+                }}
+              />
+            )}
           </div>
+          )}
 
-          {/* 录音按钮 - 设计稿:尺寸 96×96,在进度环内居中,左右居中 */}
+          {/* 录音按钮 - 设计稿:尺寸 96×96,在进度环内居中,左右居中 - 只在非录音状态显示 */}
+          {currentState !== 'recording' && (
           <div
             className={`chatbot-record-button absolute touch-manipulation ${isConnected ? 'cursor-pointer' : 'cursor-not-allowed'}`}
             onClick={handleStartRecord}
@@ -1145,6 +1177,7 @@ export const ChatBotPage = () => {
               caretColor: 'transparent',
               outline: 'none',
               filter: isConnected ? 'none' : 'grayscale(0.25)',
+              overflow: 'visible', // 允许 tooltips 超出按钮边界
             }}
             contentEditable={false}
             tabIndex={-1}
@@ -1177,9 +1210,201 @@ export const ChatBotPage = () => {
                 zIndex: 1,
               }}
             />
+
+          {/* Tap to Chat 提示 - 相对于录音按钮定位 - 只显示一次 - 在按钮内部使用绝对定位 */}
+          {showTapToChat && isConnected && (
+            <img
+              src={tapToChat}
+              alt="Tap to Chat"
+              className="chatbot-tooltips absolute pointer-events-none"
+              style={{
+                width: `${layout.tooltips.tapToChat.width}px`,
+                height: 'auto',
+                top: '-100%',
+                right: '-100%',
+                borderRadius: `${Math.round(8 * layout.avatarScale)}px`,
+                opacity: 1,
+                zIndex: 10,
+                maxWidth: 'none', // 确保不被限制
+                maxHeight: 'none', // 确保不被限制
+              }}
+            />
+          )}
           </div>
+          )}
+
+          {/* 取消语音发送按钮 - 录音状态显示,与录音按钮同高度,左侧对齐 */}
+          {currentState === 'recording' && (
+            <div
+              className="chatbot-action-buttons absolute flex items-center justify-center cursor-pointer touch-manipulation"
+              onClick={handleCancel}
+              style={{
+                width: `${actionButtonSize}px`,
+                height: `${actionButtonSize}px`,
+                minWidth: '44px',
+                minHeight: '44px',
+                top: `${actionButtonTop}px`,
+                left: '50%',
+                transform: `translateX(calc(-100% - ${layout.buttonGap}px))`,
+                borderRadius: '100px',
+                opacity: 1,
+                background: 'rgba(255, 255, 255, 0.1)',
+                backdropFilter: 'blur(15px)',
+                zIndex: 20,
+                userSelect: 'none',
+                WebkitUserSelect: 'none',
+                caretColor: 'transparent',
+                outline: 'none',
+                overflow: 'visible', // 允许 tooltip 超出按钮边界
+              }}
+              contentEditable={false}
+              tabIndex={-1}
+            >
+              {/* 按钮背景装饰图片 */}
+              <img
+                src={audioButton}
+                alt="Button Background"
+                className="pointer-events-none"
+                style={{
+                  width: 'calc(100% + 1px)',
+                  height: 'calc(100% + 1px)',
+                  position: 'absolute',
+                  top: '0',
+                  left: '0',
+                }}
+              />
+
+              {/* Cancel 图标 - 居中 - 响应式尺寸 */}
+              <img
+                src={cancelIcon}
+                alt="Cancel"
+                className="pointer-events-none"
+                style={{
+                  width: `${layout.recordButtonSize * 0.25}px`,
+                  height: `${layout.recordButtonSize * 0.25}px`,
+                  position: 'relative',
+                  zIndex: 1,
+                }}
+              />
+
+              {/* 取消语音发送提示 - 相对于取消按钮定位 - 只在首次录音时显示 */}
+              {showCancelTooltip && (
+                <img
+                  src={tooltipsCancel}
+                  alt="Cancel Tooltip"
+                  className="chatbot-tooltips absolute pointer-events-none"
+                  style={{
+                    width: `${layout.tooltips.cancel.width}px`,
+                    height: 'auto',
+                    bottom: layout.tooltips.cancel.bottom,
+                    left: layout.tooltips.cancel.left,
+                    borderRadius: `${Math.round(8 * layout.avatarScale)}px`,
+                    opacity: 1,
+                    zIndex: 10,
+                    maxWidth: 'none', // 确保不被限制
+                    maxHeight: 'none', // 确保不被限制
+                  }}
+                />
+              )}
+            </div>
+          )}
+
+          {/* 语音发送按钮 - 录音状态显示,与录音按钮同高度,右侧对齐 */}
+          {currentState === 'recording' && (
+            <div
+              className="chatbot-action-buttons absolute flex items-center justify-center cursor-pointer touch-manipulation"
+              onClick={handleSend}
+              style={{
+                width: `${actionButtonSize}px`,
+                height: `${actionButtonSize}px`,
+                minWidth: '44px',
+                minHeight: '44px',
+                top: `${actionButtonTop}px`,
+                left: '50%',
+                transform: `translateX(${layout.buttonGap}px)`,
+                borderRadius: '100px',
+                opacity: 1,
+                background: 'rgba(255, 255, 255, 0.1)',
+                backdropFilter: 'blur(15px)',
+                zIndex: 20,
+                userSelect: 'none',
+                WebkitUserSelect: 'none',
+                caretColor: 'transparent',
+                outline: 'none',
+                overflow: 'visible', // 允许 tooltip 超出按钮边界
+              }}
+              contentEditable={false}
+              tabIndex={-1}
+            >
+              {/* 按钮背景装饰图片 */}
+              <img
+                src={audioButton}
+                alt="Button Background"
+                className="pointer-events-none"
+                style={{
+                  width: 'calc(100% + 1px)',
+                  height: 'calc(100% + 1px)',
+                  position: 'absolute',
+                  top: '0',
+                  left: '0',
+                }}
+              />
+
+              {/* Send 图标 - 居中 - 响应式尺寸 */}
+              <img
+                src={sendIcon}
+                alt="Send"
+                className="pointer-events-none"
+                style={{
+                  width: `${layout.recordButtonSize * 0.25}px`,
+                  height: `${layout.recordButtonSize * 0.25}px`,
+                  position: 'relative',
+                  zIndex: 1,
+                }}
+              />
+
+              {/* 语音发送提示 - 相对于发送按钮定位 - 只在首次录音时显示 */}
+              {showSendTooltip && (
+                <img
+                  src={tooltipsSend}
+                  alt="Send Tooltip"
+                  className="chatbot-tooltips absolute pointer-events-none"
+                  style={{
+                    width: `${layout.tooltips.send.width}px`,
+                    height: 'auto',
+                    bottom: layout.tooltips.send.bottom,
+                    right: layout.tooltips.send.right,
+                    borderRadius: `${Math.round(8 * layout.avatarScale)}px`,
+                    opacity: 1,
+                    zIndex: 10,
+                    maxWidth: 'none', // 确保不被限制
+                    maxHeight: 'none', // 确保不被限制
+                  }}
+                />
+              )}
+            </div>
+          )}
+
+          {/* 倒计时组件 - 只在录音50秒后(最后10秒)显示 */}
+          {currentState === 'recording' &&
+            remainingTimeMs !== null &&
+            remainingTimeMs <= COUNTDOWN_THRESHOLD_MS && (
+              <div
+                className="chatbot-countdown absolute text-white font-semibold"
+                style={{
+                  top: `${actionButtonTop + actionButtonSize / 2}px`,
+                  left: '50%',
+                  transform: 'translate(-50%, -50%)',
+                  zIndex: 21,
+                  fontSize: `${Math.min(18, Math.max(12, layout.recordButtonSize * 0.15))}px`,
+                  textShadow: '0 2px 8px rgba(0,0,0,0.55)',
+                  letterSpacing: '0.08em',
+                }}
+              >
+                {formatCountdown(remainingTimeMs)}
+              </div>
+            )}
         </div>
-      )}
 
       {!isConnected && currentState !== 'recording' && (
         <div
@@ -1196,200 +1421,6 @@ export const ChatBotPage = () => {
           正在重连语音服务...
         </div>
       )}
-
-      {/* 进度条提示 - 首次有进度时显示 */}
-      {showProgressBarTooltip && isConnected && (
-        <img
-          src={tooltipsProgressBar}
-          alt="Progress Bar Tooltip"
-          className="chatbot-tooltips absolute pointer-events-none"
-          style={{
-            width: `${layout.tooltips.progressBar.width}px`,
-            height: `${layout.tooltips.progressBar.height}px`,
-            top: `${layout.tooltips.progressBar.top}px`,
-            left: `${layout.tooltips.progressBar.left}px`,
-            opacity: 1,
-          }}
-        />
-      )}
-
-      {/* Tap to Chat 提示 - 只显示一次 - 等比例缩放 */}
-      {showTapToChat && isConnected && (
-        <img
-          src={tapToChat}
-          alt="Tap to Chat"
-          className="chatbot-tooltips absolute pointer-events-none"
-          style={{
-            width: `${layout.tooltips.tapToChat.width}px`,
-            height: 'auto',
-            top: `${layout.tooltips.tapToChat.top}px`,
-            right: `${layout.tooltips.tapToChat.right}px`,
-            borderRadius: `${Math.round(8 * layout.avatarScale)}px`,
-            opacity: 1,
-          }}
-        />
-      )}
-
-      {/* 取消语音发送提示 - 只在首次录音时显示 - 等比例缩放 */}
-      {currentState === 'recording' && showCancelTooltip && (
-        <img
-          src={tooltipsCancel}
-          alt="Cancel Tooltip"
-          className="chatbot-tooltips absolute pointer-events-none"
-          style={{
-            width: `${layout.tooltips.cancel.width}px`,
-            height: 'auto',
-            top: `${layout.tooltips.cancel.top}px`,
-            left: `${layout.tooltips.cancel.left}px`,
-            borderRadius: `${Math.round(8 * layout.avatarScale)}px`,
-            opacity: 1,
-          }}
-        />
-      )}
-
-      {/* 取消语音发送按钮 - 录音状态显示,与录音按钮同高度,左侧对齐 */}
-      {currentState === 'recording' && (
-        <div
-          className="chatbot-action-buttons absolute flex items-center justify-center cursor-pointer touch-manipulation"
-          onClick={handleCancel}
-          style={{
-            width: `${actionButtonSize}px`,
-            height: `${actionButtonSize}px`,
-            minWidth: '44px',
-            minHeight: '44px',
-            top: `${actionButtonTop}px`,
-            left: '50%',
-            transform: `translateX(calc(-100% - ${layout.verticalGap}px))`,
-            borderRadius: '100px',
-            opacity: 1,
-            background: 'rgba(255, 255, 255, 0.1)',
-            backdropFilter: 'blur(15px)',
-            zIndex: 20,
-            userSelect: 'none',
-            WebkitUserSelect: 'none',
-            caretColor: 'transparent',
-            outline: 'none',
-          }}
-          contentEditable={false}
-          tabIndex={-1}
-        >
-          {/* 按钮背景装饰图片 */}
-          <img
-            src={audioButton}
-            alt="Button Background"
-            className="pointer-events-none"
-            style={{
-              width: 'calc(100% + 1px)',
-              height: 'calc(100% + 1px)',
-              position: 'absolute',
-              top: '0',
-              left: '0',
-            }}
-          />
-
-          {/* Cancel 图标 - 居中 - 响应式尺寸 */}
-          <img
-            src={cancelIcon}
-            alt="Cancel"
-            className="pointer-events-none"
-            style={{
-              width: `${layout.recordButtonSize * 0.25}px`,
-              height: `${layout.recordButtonSize * 0.25}px`,
-              position: 'relative',
-              zIndex: 1,
-            }}
-          />
-        </div>
-      )}
-
-      {shouldShowCountdown && (
-      <div
-        className="chatbot-countdown absolute text-white font-semibold"
-        style={{
-          top: `${actionButtonTop + actionButtonSize / 2}px`,
-          left: '50%',
-          transform: 'translate(-50%, -50%)',
-          zIndex: 21,
-          fontSize: `${Math.max(12, layout.recordButtonSize * 0.2)}px`,
-          textShadow: '0 2px 8px rgba(0,0,0,0.55)',
-          letterSpacing: '0.08em',
-        }}
-      >
-        {formatCountdown(remainingTimeMs ?? 0)}
-        </div>
-      )}
-
-      {/* 语音发送提示 - 只在首次录音时显示 - 等比例缩放 */}
-      {currentState === 'recording' && showSendTooltip && (
-        <img
-          src={tooltipsSend}
-          alt="Send Tooltip"
-          className="chatbot-tooltips absolute pointer-events-none"
-          style={{
-            width: `${layout.tooltips.send.width}px`,
-            height: 'auto',
-            top: `${layout.tooltips.send.top}px`,
-            left: `${layout.tooltips.send.left}px`,
-            borderRadius: `${Math.round(8 * layout.avatarScale)}px`,
-            opacity: 1,
-          }}
-        />
-      )}
-
-      {/* 语音发送按钮 - 录音状态显示,与录音按钮同高度,右侧对齐 */}
-      {currentState === 'recording' && (
-        <div
-          className="chatbot-action-buttons absolute flex items-center justify-center cursor-pointer touch-manipulation"
-          onClick={handleSend}
-          style={{
-            width: `${actionButtonSize}px`,
-            height: `${actionButtonSize}px`,
-            minWidth: '44px',
-            minHeight: '44px',
-            top: `${actionButtonTop}px`,
-            left: '50%',
-            transform: `translateX(${layout.verticalGap}px)`,
-            borderRadius: '100px',
-            opacity: 1,
-            background: 'rgba(255, 255, 255, 0.1)',
-            backdropFilter: 'blur(15px)',
-            zIndex: 20,
-            userSelect: 'none',
-            WebkitUserSelect: 'none',
-            caretColor: 'transparent',
-            outline: 'none',
-          }}
-          contentEditable={false}
-          tabIndex={-1}
-        >
-          {/* 按钮背景装饰图片 */}
-          <img
-            src={audioButton}
-            alt="Button Background"
-            className="pointer-events-none"
-            style={{
-              width: 'calc(100% + 1px)',
-              height: 'calc(100% + 1px)',
-              position: 'absolute',
-              top: '0',
-              left: '0',
-            }}
-          />
-
-          {/* Send 图标 - 居中 - 响应式尺寸 */}
-          <img
-            src={sendIcon}
-            alt="Send"
-            className="pointer-events-none"
-            style={{
-              width: `${layout.recordButtonSize * 0.25}px`,
-              height: `${layout.recordButtonSize * 0.25}px`,
-              position: 'relative',
-              zIndex: 1,
-            }}
-          />
-        </div>
-      )}
       </div>
     </div>
   );

+ 6 - 10
frontend/src/styles/mobile-fix.css

@@ -30,10 +30,8 @@
   /* 进度环和录音按钮 - 位置由 JavaScript 控制 */
   /* top 由父容器 .chatbot-progress-container 和内部计算值控制 */
 
-  /* 取消/发送按钮 - 调整位置 */
-  .chatbot-action-buttons {
-    top: clamp(493px, 75vh, 620px) !important;
-  }
+  /* 取消/发送按钮 - 位置由 JavaScript 控制,相对于容器定位 */
+  /* top 已移除,由 JavaScript 动态计算相对于容器的位置 */
 
   /* 音频波形 - 减小高度 */
   .chatbot-audio-wave[data-expanded="false"] {
@@ -60,9 +58,8 @@
 
   /* 进度环和录音按钮 - 位置由 JavaScript 控制 */
 
-  .chatbot-action-buttons {
-    top: clamp(423px, 68vh, 540px) !important;
-  }
+  /* 取消/发送按钮 - 位置由 JavaScript 控制,相对于容器定位 */
+  /* top 已移除,由 JavaScript 动态计算相对于容器的位置 */
 
   /* 树木装饰 - 保持可见 */
   .chatbot-trees {
@@ -101,9 +98,8 @@
     height: clamp(64px, 20vw, 80px) !important;
   }
 
-  .chatbot-action-buttons {
-    top: clamp(353px, 62vh, 460px) !important;
-  }
+  /* 取消/发送按钮 - 位置由 JavaScript 控制,相对于容器定位 */
+  /* top 已移除,由 JavaScript 动态计算相对于容器的位置 */
 
   /* 隐藏装饰元素 */
   .chatbot-avatar-c {

+ 39 - 26
frontend/src/utils/screenSize.ts

@@ -148,6 +148,7 @@ export interface LayoutParams {
   
   // 间距
   verticalGap: number;
+  buttonGap: number;  // 取消和发送按钮之间的水平间距(固定值)
   
   // 进度环边距(进度环比录音按钮大的尺寸)
   progressRingPadding: number;
@@ -167,6 +168,7 @@ export const getLayoutParams = (): LayoutParams => {
       recordButtonSize: 72, // 最小可点击尺寸
       waveformHeight: 44,
       verticalGap: 10,
+      buttonGap: 35, // 固定值,不缩放
       progressRingPadding: 14, // 72 * 0.75 * 0.26 ≈ 14 (按比例缩放)
     };
   } else if (isVerySmallScreen) {
@@ -179,6 +181,7 @@ export const getLayoutParams = (): LayoutParams => {
       recordButtonSize: 80,
       waveformHeight: 48,
       verticalGap: 12,
+      buttonGap: 35, // 固定值,不缩放
       progressRingPadding: 15, // 80 * 0.83 * 0.23 ≈ 15 (按比例缩放)
     };
   } else if (isSmallScreen) {
@@ -191,6 +194,7 @@ export const getLayoutParams = (): LayoutParams => {
       recordButtonSize: 88,
       waveformHeight: 52,
       verticalGap: 16,
+      buttonGap: 35, // 固定值,不缩放
       progressRingPadding: 17, // 88 * 0.92 * 0.21 ≈ 17 (按比例缩放)
     };
   } else {
@@ -203,6 +207,7 @@ export const getLayoutParams = (): LayoutParams => {
       recordButtonSize: 96,
       waveformHeight: 56,
       verticalGap: 20,
+      buttonGap: 35, // 固定值,不缩放
       progressRingPadding: 18, // 基准值:96 * 0.1875 = 18
     };
   }
@@ -302,7 +307,8 @@ const BASE_LAYOUT = {
   waveformHeight: 56,
   
   // 间距
-  verticalGap: 20,      // 波形与按钮的间距
+  verticalGap: 20,      // 波形与按钮的垂直间距
+  buttonGap: 35,        // 取消和发送按钮之间的水平间距(固定值,不缩放)
   minGapBetweenElements: 16, // 元素之间最小间距
 } as const;
 
@@ -342,6 +348,7 @@ export interface CalculatedLayout {
   
   // 间距
   verticalGap: number;
+  buttonGap: number;  // 取消和发送按钮之间的水平间距(固定值)
   progressRingPadding: number;
   recordButtonOffset: number; // 录音按钮相对于进度环的 top 偏移
   
@@ -349,8 +356,8 @@ export interface CalculatedLayout {
   tooltips: {
     progressBar: { width: number; height: number; top: number; left: number };
     tapToChat: { width: number; height: number; top: number; right: number };
-    cancel: { width: number; height: number; top: number; left: number };
-    send: { width: number; height: number; top: number; left: number };
+    cancel: { width: number; height: number; bottom: string; left: string };
+    send: { width: number; height: number; bottom: string; right: string };
   };
   
   // 状态标记
@@ -394,6 +401,7 @@ export function calculateLayout(
   // 2. 基于缩放比例计算所有元素尺寸
   const waveformHeight = Math.round(BASE.waveformHeight * scale);
   const verticalGap = Math.round(BASE.verticalGap * scale);
+  const buttonGap = BASE.buttonGap; // 按钮间距固定为 35px,不缩放
   const recordButtonSize = Math.max(44, Math.round(BASE.recordButtonSize * scale)); // 最小 44px 可点击
   const progressRingPadding = Math.round(BASE.progressRingPadding * scale);
   const progressRingSize = recordButtonSize + progressRingPadding;
@@ -460,56 +468,60 @@ export function calculateLayout(
   const recordButtonTop = progressRingTop + recordButtonOffset;
   
   // 计算发送按钮左上角位置
-  // 发送按钮:left: 50%, transform: translateX(verticalGap)
-  // 实际 left = viewportWidth/2 + verticalGap
-  const sendButtonLeft = viewportWidth / 2 + verticalGap;
+  // 发送按钮:left: 50%, transform: translateX(buttonGap)
+  // 实际 left = viewportWidth/2 + buttonGap
+  const sendButtonLeft = viewportWidth / 2 + buttonGap;
   const sendButtonTop = recordButtonTop;
   
   // 计算取消按钮左上角位置
-  // 取消按钮:left: 50%, transform: translateX(-verticalGap - 100%)
-  // 实际 left = viewportWidth/2 - verticalGap - cancelButtonSize
+  // 取消按钮:left: 50%, transform: translateX(-buttonGap - 100%)
+  // 实际 left = viewportWidth/2 - buttonGap - cancelButtonSize
   const cancelButtonSize = Math.round(recordButtonSize * 0.58);
-  const cancelButtonLeft = viewportWidth / 2 - verticalGap - cancelButtonSize;
+  const cancelButtonLeft = viewportWidth / 2 - buttonGap - cancelButtonSize;
   const cancelButtonTop = recordButtonTop;
   
   // 统一取消和发送按钮tooltip的垂直位置(上下居中对齐)
   const tooltipVerticalOffset = -95; // 统一偏移值,确保两个tooltip上下对齐
   
-  // 计算 tapToChat 的 right 值
+  // 计算 tapToChat 相对于录音按钮的偏移量
+  // 设计稿基准:录音按钮 96×96,TapToChat 128px 宽
+  // top: 相对于录音按钮顶部向上偏移 74px
+  // left: 相对于录音按钮左边缘向右偏移 83px(设计稿基准值)
   const tapToChatWidth = Math.round(128 * scale);
-  const tapToChatLeft = recordButtonLeft + Math.round(83 * scale);
-  const tapToChatRight = viewportWidth - tapToChatLeft - tapToChatWidth;
+  const tapToChatLeftOffset = Math.round(83 * scale); // 从录音按钮左边缘到 TapToChat 左边缘的距离
   
   const tooltips = {
-    // 进度条提示:相对于进度环左上角,top -120px, left -115px
+    // 进度条提示:相对于录音按钮左上顶点定位(top: 图片高度的一半的负值,left: 图片宽度的一半的负值)
     progressBar: {
       width: Math.round(167 * scale),
       height: Math.round(130.35 * scale),
-      top: progressRingTop + Math.round(-120 * scale),
-      left: progressRingLeft + Math.round(-115 * scale),
+      top: -Math.round(130.35 * scale) / 2, // 图片高度的一半的负值
+      left: -Math.round(167 * scale) / 2, // 图片宽度的一半的负值
     },
-    // Tap to Chat:相对于录音按钮,使用 right 定位
+    // Tap to Chat:相对于录音按钮定位(top 向上偏移 74px,left 从左边缘向右偏移 83px)
     tapToChat: {
       width: tapToChatWidth,
       height: 0, // auto
-      top: recordButtonTop + Math.round(-74 * scale),
-      right: tapToChatRight,
+      top: Math.round(-74 * scale), // 相对于录音按钮顶部向上偏移
+      left: tapToChatLeftOffset, // 相对于录音按钮左边缘向右偏移
     },
-    // 取消按钮提示:相对于取消按钮左上角,使用统一垂直偏移
+    // 取消按钮提示:使用百分比定位(tooltip 放在按钮容器内)
     cancel: {
       width: Math.round(110 * scale),
       height: 0, // auto
-      top: cancelButtonTop + Math.round(tooltipVerticalOffset * scale),
-      // 现状需求:left = 取消按钮 left − tips 宽度 + tips宽度的三分之一
-      // 即:left = cancelButtonLeft - tooltipWidth + tooltipWidth / 3
-      left: cancelButtonLeft - Math.round(110 * scale) + Math.round((110 * scale) / 3),
+      // bottom: 110% - tooltip 底部在按钮底部下方 10% 的位置
+      bottom: '110%',
+      // left: -100% - tooltip 左边缘对齐到按钮的右边缘(tooltip 在按钮左侧)
+      left: '-100%',
     },
-    // 发送按钮提示:相对于发送按钮左上角,left 23px
+    // 发送按钮提示:使用百分比定位(tooltip 放在按钮容器内)
     send: {
       width: Math.round(111 * scale),
       height: 0, // auto
-      top: sendButtonTop + Math.round(tooltipVerticalOffset * scale),
-      left: sendButtonLeft + Math.round(23 * scale),
+      // bottom: 110% - tooltip 底部在按钮底部下方 10% 的位置
+      bottom: '110%',
+      // right: -100% - tooltip 右边缘对齐到按钮的左边缘(tooltip 在按钮左侧)
+      right: '-100%',
     },
   };
   
@@ -540,6 +552,7 @@ export function calculateLayout(
     progressRingSize,
     waveformHeight,
     verticalGap,
+    buttonGap,
     progressRingPadding,
     recordButtonOffset,
     tooltips,