zlwu 11 ヶ月 前
親
コミット
874dab9dff

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

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

+ 1144 - 0
frontend/src/pages/ChatBotPage copy.tsx

@@ -0,0 +1,1144 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { message as antMessage } from 'antd';
+import { v4 as uuidv4 } from 'uuid';
+import { useAppStore } from '@/store';
+import { AudioRecorder } from '@/utils/audioRecorder';
+import { AudioPlayer } from '@/utils/audioPlayer';
+import { WebSocketManager } from '@/utils/websocket';
+import { prepareMobileAudio, requestVolumeControl, isMobileDevice } from '@/utils/audioHelper';
+import { setStorage, getStorage, STORAGE_KEYS, markRouteVisited, getUserId } from '@/utils/storage';
+import { onScreenResize, calculateLayout, type CalculatedLayout, updateTooSmallFlag } from '@/utils/screenSize';
+import { performanceLogger } from '@/utils/logger';
+import { getUserInfo } from '@/utils/api';
+import type { Message } from '@/types';
+import '@/styles/chatbot-fix.css';
+import avatarC from '@/assets/Avatar-C.svg';
+import treesImage from '@/assets/Group-3465183.svg';
+import animaZoneAvatar from '@/assets/Avatar-anima-zone.png';
+import animaZoneAvatarGif from '@/assets/02.GIF';
+import audioButton from '@/assets/audio-button.png';
+import { AudioWave } from '@/components/AudioWave';
+import microphoneIcon from '@/assets/microphone-icon.svg';
+import progressRing from '@/assets/progress-ring.svg';
+import progressFull from '@/assets/progress-full.svg';
+import tapToChat from '@/assets/TapToChat.svg';
+import cancelIcon from '@/assets/cancel-icon.svg';
+import sendIcon from '@/assets/send-icon.svg';
+import tooltipsCancel from '@/assets/tooltips-cancel.svg';
+import tooltipsSend from '@/assets/tooltips-send.svg';
+import tooltipsProgressBar from '@/assets/tooltips-progress-bar.svg';
+
+const MAX_RECORDING_DURATION_MS = 60_000;
+const COUNTDOWN_THRESHOLD_MS = 10_000;
+
+const WS_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
+
+export const ChatBotPage = () => {
+  const {
+    currentState,
+    currentRequestId,
+    currentEmotion,
+    conversationProgress,
+    progressLoopCount,
+    setState,
+    setRequestId,
+    setEmotion,
+    setRecognizedText,
+    setError,
+    setConnected,
+    updateProgressByDuration,
+    setProgress,
+    reset,
+    isConnected,
+  } = useAppStore();
+
+  const recorderRef = useRef<AudioRecorder | null>(null);
+  const playerRef = useRef<AudioPlayer | null>(null);
+  const wsRef = useRef<WebSocketManager | null>(null);
+  const chunkIndexRef = useRef(0);
+  const connectionLostRef = useRef(false);
+  const mobileAudioPreparedRef = useRef(false);
+  const sendTimeRef = useRef<number>(0);
+  // 初始化时检查 localStorage,确保第一次打开时立即显示提示
+  const [showTapToChat, setShowTapToChat] = useState(() => {
+    const hasShown = getStorage(STORAGE_KEYS.HAS_SHOWN_TAP_TO_CHAT);
+    if (!hasShown) {
+      // 第一次打开,立即保存标记并显示
+      setStorage(STORAGE_KEYS.HAS_SHOWN_TAP_TO_CHAT, 'true');
+      return true;
+    }
+    return false;
+  });
+  const sendingTimeoutRef = useRef<number | null>(null);
+  const [showCancelTooltip, setShowCancelTooltip] = useState(false);
+  const [showSendTooltip, setShowSendTooltip] = useState(false);
+  const [showProgressBarTooltip, setShowProgressBarTooltip] = useState(false);
+  const progressBarTooltipTimerRef = useRef<number | null>(null);
+  
+  // 控制声纹容器高度(延迟降低,等待衰减动画完成)
+  const [shouldExpandWaveform, setShouldExpandWaveform] = useState(false);
+  const waveformCollapseTimerRef = useRef<number | null>(null);
+  
+  // 用于动画的显示进度(平滑过渡)
+  const [displayProgress, setDisplayProgress] = useState(0);
+  const animationFrameRef = useRef<number>();
+  const totalProgressRef = useRef(0);
+  const shouldSkipProgressAnimationRef = useRef(true);
+
+  const recordingTimerRef = useRef<number | null>(null);
+  const recordingDeadlineRef = useRef<number>(0);
+  const handleSendRef = useRef<() => void>();
+  const [remainingTimeMs, setRemainingTimeMs] = useState<number | null>(null);
+  
+  // V2: 完全计算式布局系统
+  const [layout, setLayout] = useState<CalculatedLayout>(() => {
+    const initialLayout = calculateLayout();
+    return initialLayout;
+  });
+  
+  const timingRef = useRef({
+    recordStart: 0,
+    recordEnd: 0,
+    firstAudioReceived: 0,
+    emotionReceived: 0,
+  });
+
+  const stopRecordingTimer = useCallback((options?: { preserveCountdown?: boolean }) => {
+    if (recordingTimerRef.current) {
+      clearInterval(recordingTimerRef.current);
+      recordingTimerRef.current = null;
+    }
+    recordingDeadlineRef.current = 0;
+    if (!options?.preserveCountdown) {
+      setRemainingTimeMs(null);
+    }
+  }, []);
+
+  const startRecordingTimer = useCallback(() => {
+    stopRecordingTimer();
+    recordingDeadlineRef.current = Date.now() + MAX_RECORDING_DURATION_MS;
+    setRemainingTimeMs(MAX_RECORDING_DURATION_MS);
+    recordingTimerRef.current = window.setInterval(() => {
+      const remaining = recordingDeadlineRef.current - Date.now();
+      if (remaining <= 0) {
+        setRemainingTimeMs(0);
+        stopRecordingTimer({ preserveCountdown: true });
+        if (useAppStore.getState().currentState === 'recording') {
+          handleSendRef.current?.();
+        }
+        return;
+      }
+      setRemainingTimeMs(remaining);
+    }, 250);
+  }, [stopRecordingTimer]);
+
+  useEffect(() => {
+    return () => {
+      stopRecordingTimer();
+    };
+  }, [stopRecordingTimer]);
+
+  useEffect(() => {
+    if (currentState !== 'recording') {
+      stopRecordingTimer();
+    }
+  }, [currentState, stopRecordingTimer]);
+
+  const formatCountdown = (ms: number) => {
+    return `${Math.max(0, Math.ceil(ms / 1000))}s`;
+  };
+
+  // 进度动画效果(支持多圈连续动画)
+  useEffect(() => {
+    const targetTotal = progressLoopCount * 100 + conversationProgress;
+    const startTotal = totalProgressRef.current;
+    const diff = targetTotal - startTotal;
+
+    if (shouldSkipProgressAnimationRef.current) {
+      shouldSkipProgressAnimationRef.current = false;
+      totalProgressRef.current = targetTotal;
+      const normalized = ((targetTotal % 100) + 100) % 100;
+      setDisplayProgress(normalized);
+      return;
+    }
+
+    // 若变化极小,直接同步
+    if (Math.abs(diff) < 0.1) {
+      totalProgressRef.current = targetTotal;
+      const normalized = ((targetTotal % 100) + 100) % 100;
+      setDisplayProgress(normalized);
+      return;
+    }
+
+    // 每 1% 的进度约 8ms,限制在 0.4s~2.4s 之间
+    const durationPerPercent = 8;
+    const duration = Math.max(400, Math.min(2400, Math.abs(diff) * durationPerPercent));
+    const startTime = performance.now();
+
+    if (animationFrameRef.current) {
+      cancelAnimationFrame(animationFrameRef.current);
+    }
+
+    const animate = (currentTime: number) => {
+      const elapsed = currentTime - startTime;
+      const progress = Math.min(elapsed / duration, 1);
+
+      // easeOutCubic 缓动
+      const easeProgress = 1 - Math.pow(1 - progress, 3);
+      const currentTotal = startTotal + diff * easeProgress;
+      totalProgressRef.current = currentTotal;
+
+      const normalized = ((currentTotal % 100) + 100) % 100;
+      setDisplayProgress(normalized);
+
+      if (progress < 1) {
+        animationFrameRef.current = requestAnimationFrame(animate);
+      } else {
+        totalProgressRef.current = targetTotal;
+        const finalNormalized = ((targetTotal % 100) + 100) % 100;
+        setDisplayProgress(finalNormalized);
+      }
+    };
+
+    animationFrameRef.current = requestAnimationFrame(animate);
+
+    return () => {
+      if (animationFrameRef.current) {
+        cancelAnimationFrame(animationFrameRef.current);
+      }
+    };
+  }, [conversationProgress, progressLoopCount]);
+
+  // 控制声纹容器高度变化
+  useEffect(() => {
+    // 清除之前的定时器
+    if (waveformCollapseTimerRef.current) {
+      clearTimeout(waveformCollapseTimerRef.current);
+      waveformCollapseTimerRef.current = null;
+    }
+
+    if (currentState === 'recording' || currentState === 'playing') {
+      // 录音或播放时,立即展开
+      setShouldExpandWaveform(true);
+    } else {
+      // 其他状态(sending/idle 等),延迟 3 秒后再收起(等待衰减动画完成)
+      waveformCollapseTimerRef.current = window.setTimeout(() => {
+        setShouldExpandWaveform(false);
+      }, 3000); // 3秒足够让所有竖条衰减为静态高度
+    }
+
+    return () => {
+      if (waveformCollapseTimerRef.current) {
+        clearTimeout(waveformCollapseTimerRef.current);
+      }
+    };
+  }, [currentState]);
+
+  // 监听录音状态,首次显示取消提示(3秒后自动隐藏)
+  useEffect(() => {
+    if (currentState === 'recording') {
+      const hasShownCancel = getStorage(STORAGE_KEYS.HAS_SHOWN_CANCEL_TOOLTIP);
+      if (!hasShownCancel) {
+        setShowCancelTooltip(true);
+        setStorage(STORAGE_KEYS.HAS_SHOWN_CANCEL_TOOLTIP, 'true');
+        
+        // 3秒后自动隐藏
+        const timer = setTimeout(() => {
+          setShowCancelTooltip(false);
+        }, 3000);
+        
+        return () => clearTimeout(timer);
+      } else {
+        // 已显示过,确保隐藏
+        setShowCancelTooltip(false);
+      }
+    } else {
+      // 非录音状态,隐藏提示
+      setShowCancelTooltip(false);
+    }
+  }, [currentState]);
+
+  // 监听录音状态,首次显示发送提示(3秒后自动隐藏)
+  useEffect(() => {
+    if (currentState === 'recording') {
+      const hasShownSend = getStorage(STORAGE_KEYS.HAS_SHOWN_SEND_TOOLTIP);
+      if (!hasShownSend) {
+        setShowSendTooltip(true);
+        setStorage(STORAGE_KEYS.HAS_SHOWN_SEND_TOOLTIP, 'true');
+        
+        // 3秒后自动隐藏
+        const timer = setTimeout(() => {
+          setShowSendTooltip(false);
+        }, 3000);
+        
+        return () => clearTimeout(timer);
+      } else {
+        // 已显示过,确保隐藏
+        setShowSendTooltip(false);
+      }
+    } else {
+      // 非录音状态,隐藏提示
+      setShowSendTooltip(false);
+    }
+  }, [currentState]);
+
+  // 监听进度条变化,首次有进度时显示进度条提示(2秒后自动隐藏)
+  // 🔧 临时修改:为了UI调试,进度更新时始终显示提示
+  useEffect(() => {
+    // const hasShownProgressBar = getStorage(STORAGE_KEYS.HAS_SHOWN_PROGRESS_BAR_TOOLTIP);
+    
+    // // 如果已经显示过,不再处理
+    // if (hasShownProgressBar) {
+    //   return;
+    // }
+    
+    // 当进度条第一次大于0时显示提示(只显示一次)
+    const hasShownProgressBarTooltip = getStorage(STORAGE_KEYS.HAS_SHOWN_PROGRESS_BAR_TOOLTIP);
+    if (displayProgress > 0 && !showProgressBarTooltip && !hasShownProgressBarTooltip) {
+      setShowProgressBarTooltip(true);
+      setStorage(STORAGE_KEYS.HAS_SHOWN_PROGRESS_BAR_TOOLTIP, 'true');
+      
+      // 2秒后自动隐藏
+      progressBarTooltipTimerRef.current = window.setTimeout(() => {
+        setShowProgressBarTooltip(false);
+        progressBarTooltipTimerRef.current = null;
+      }, 2000);
+    }
+    
+    return () => {
+      if (progressBarTooltipTimerRef.current) {
+        clearTimeout(progressBarTooltipTimerRef.current);
+        progressBarTooltipTimerRef.current = null;
+      }
+    };
+  }, [displayProgress, showProgressBarTooltip]);
+
+  // 监听屏幕尺寸变化
+  useEffect(() => {
+    // 初始化时更新标记
+    updateTooSmallFlag();
+    
+    const cleanup = onScreenResize(() => {
+      const newLayout = calculateLayout();
+      setLayout(newLayout); // V2: 更新完全计算式布局
+      
+      // 更新 data-too-small 标记
+      updateTooSmallFlag();
+    });
+    
+    return cleanup;
+  }, []);
+
+  // 初始化
+  useEffect(() => {
+    // 记录用户当前所在页面
+    setStorage(STORAGE_KEYS.CURRENT_PAGE, 'chatbot');
+    
+    // 记录路由访问
+    markRouteVisited('/chatbot');
+    
+    const init = async () => {
+      try {
+        // 获取用户信息并初始化进度条
+        const user_id = getUserId();
+        if (user_id) {
+          try {
+            const userInfo = await getUserInfo(user_id);
+            if (userInfo.success && userInfo.data.total_speech_duration_ms) {
+              // 根据总说话时长计算初始进度(30秒=100%)
+              const PROGRESS_FULL_DURATION_MS = 30 * 1000; // 30000ms
+              const totalDurationMs = userInfo.data.total_speech_duration_ms || 0;
+              const totalProgressPercent = (totalDurationMs / PROGRESS_FULL_DURATION_MS) * 100;
+              const initialLoopCount = Math.floor(totalProgressPercent / 100);
+              const initialProgress = totalProgressPercent % 100;
+              shouldSkipProgressAnimationRef.current = true;
+              setProgress(initialProgress, initialLoopCount);
+            }
+          } catch (error) {
+            // 获取用户信息失败不影响主流程,静默处理
+            console.warn('获取用户信息失败:', error);
+          }
+        }
+
+        // 初始化录音器
+        const recorder = new AudioRecorder();
+        await recorder.initialize();
+        recorderRef.current = recorder;
+
+        // 初始化播放器
+        const player = new AudioPlayer();
+        await player.initialize();
+        player.onEnd(() => {
+          const requestId = useAppStore.getState().currentRequestId;
+          if (requestId) {
+            performanceLogger.recordPlaybackEnd(requestId);
+          }
+          // 进度条已基于用户说话时长更新,无需在播放完成后再次更新
+          setState('idle');
+          reset();
+        });
+        playerRef.current = player;
+
+        // 初始化WebSocket(添加user_id查询参数)
+        const ws = new WebSocketManager();
+        const wsUrl = user_id ? `${WS_URL}?user_id=${encodeURIComponent(user_id)}` : WS_URL;
+        ws.connect(wsUrl, (connected) => {
+          const appState = useAppStore.getState();
+          const wasConnected = appState.isConnected;
+          
+          setConnected(connected);
+          
+          if (connected) {
+            // 记录WebSocket连接建立时间
+            performanceLogger.recordWebSocketConnected();
+            
+            if (connectionLostRef.current) {
+              antMessage.success('语音服务已恢复,可以继续对话');
+              connectionLostRef.current = false;
+              recorderRef.current?.stop();
+              playerRef.current?.stop();
+              reset();
+              setError(null);
+            }
+            return;
+          }
+          
+          if (!wasConnected) {
+            // 初次连接失败,静默等待自动重连
+            return;
+          }
+          
+          connectionLostRef.current = true;
+          
+          if (appState.currentRequestId) {
+            performanceLogger.clearRequest(appState.currentRequestId, 'websocket_disconnected');
+          }
+          
+          recorderRef.current?.stop();
+          playerRef.current?.stop();
+          
+          antMessage.error('语音服务连接断开,请稍后重试');
+          setError('语音服务连接断开,请稍后重试');
+        });
+        ws.onMessage('app', handleMessage);
+        wsRef.current = ws;
+      } catch (error) {
+        antMessage.error('初始化失败,请检查麦克风权限');
+        setError('初始化失败');
+      }
+    };
+
+    init();
+
+    return () => {
+      recorderRef.current?.release();
+      playerRef.current?.release();
+      wsRef.current?.disconnect();
+      if (sendingTimeoutRef.current) {
+        clearTimeout(sendingTimeoutRef.current);
+      }
+      if (animationFrameRef.current) {
+        cancelAnimationFrame(animationFrameRef.current);
+      }
+      if (progressBarTooltipTimerRef.current) {
+        clearTimeout(progressBarTooltipTimerRef.current);
+      }
+    };
+  }, []);
+
+  // 处理WebSocket消息
+  const handleMessage = (message: Message) => {
+    const requestId = message.requestId || currentRequestId;
+    
+    switch (message.type) {
+      case 'text_result':
+        if (message.isFinal) {
+          performanceLogger.recordFinalText(requestId, message.text, message.timestamp);
+        } else if (!performanceLogger.hasFirstText(requestId)) {
+          performanceLogger.recordFirstText(requestId, message.text, message.timestamp);
+        }
+        setRecognizedText(message.text);
+        break;
+
+      case 'emotion':
+        performanceLogger.recordEmotion(requestId, message.emotion, message.timestamp);
+        setEmotion(message.emotion);
+        break;
+
+      case 'audio_response':
+        // 清除发送超时
+        if (sendingTimeoutRef.current) {
+          clearTimeout(sendingTimeoutRef.current);
+          sendingTimeoutRef.current = null;
+        }
+        
+        if (currentState !== 'playing') {
+          performanceLogger.recordFirstAudio(requestId, message.timestamp);
+          performanceLogger.recordPlaybackStart(requestId);
+          setState('playing');
+        }
+        
+        if (message.data) {
+          const audioData = atob(message.data);
+          const audioBytes = new Uint8Array(audioData.length);
+          
+          for (let i = 0; i < audioData.length; i++) {
+            audioBytes[i] = audioData.charCodeAt(i);
+          }
+          
+          playerRef.current?.addChunk(audioBytes, message.sampleRate || 24000);
+        }
+        
+        if (message.isLast) {
+          performanceLogger.recordLastAudio(requestId);
+          playerRef.current?.markStreamEnd();
+          // 进度已在发送时增加,这里不再重复增加
+        }
+        break;
+
+      case 'error':
+        console.error(`[错误] [${requestId?.substring(0, 8) || ''}]`, message.message, message.code);
+        antMessage.error(message.message);
+        setError(message.message);
+        performanceLogger.clearRequest(requestId, `${message.code}: ${message.message}`);
+        break;
+
+      case 'pong':
+        break;
+
+      default:
+        console.warn(`[未知消息] [${requestId?.substring(0, 8) || ''}]`, message.type);
+        break;
+    }
+  };
+
+  // 开始录音
+  const handleStartRecord = async () => {
+    // 如果正在录音,忽略
+    if (currentState === 'recording') {
+      return;
+    }
+    
+    if (!isConnected) {
+      antMessage.warning('语音服务正在重连,请稍后再试');
+      return;
+    }
+    
+    // 如果正在播放,先停止播放
+    if (currentState === 'playing') {
+      playerRef.current?.stop();
+    }
+    
+    // 其他状态(idle/error/sending)都允许开始录音
+
+    // 首次点击录音按钮,隐藏 TapToChat 提示
+    if (showTapToChat) {
+      setShowTapToChat(false);
+    }
+
+    // 点击录音按钮时,隐藏进度条提示
+    if (showProgressBarTooltip) {
+      setShowProgressBarTooltip(false);
+      if (progressBarTooltipTimerRef.current) {
+        clearTimeout(progressBarTooltipTimerRef.current);
+        progressBarTooltipTimerRef.current = null;
+      }
+    }
+
+    if (isMobileDevice() && !mobileAudioPreparedRef.current) {
+      try {
+        await prepareMobileAudio();
+        requestVolumeControl();
+        mobileAudioPreparedRef.current = true;
+        antMessage.info('请确保设备音量已调高');
+      } catch (error) {
+        // 准备移动端音频失败,静默处理
+      }
+    }
+
+    // 🔥 清理播放器状态,确保上一次对话的残留状态被重置
+    playerRef.current?.stop();
+
+    const requestId = uuidv4();
+    performanceLogger.startRequest(requestId);
+    setRequestId(requestId);
+    setState('recording');
+    chunkIndexRef.current = 0;
+    setRecognizedText('');
+    
+    timingRef.current = {
+      recordStart: Date.now(),
+      recordEnd: 0,
+      firstAudioReceived: 0,
+      emotionReceived: 0,
+    };
+
+    recorderRef.current?.start(
+      (pcmData) => {
+        if (chunkIndexRef.current === 0) {
+          performanceLogger.recordFirstChunkSent(requestId);
+        }
+        const uint8Array = new Uint8Array(pcmData.buffer);
+        const base64 = btoa(String.fromCharCode(...Array.from(uint8Array)));
+        wsRef.current?.sendAudioChunk(requestId, base64, chunkIndexRef.current);
+        chunkIndexRef.current++;
+      }
+    );
+    startRecordingTimer();
+  };
+
+  // 发送录音
+  const handleSend = () => {
+    if (currentState !== 'recording' || !currentRequestId) {
+      return;
+    }
+
+    stopRecordingTimer();
+    
+    if (!isConnected) {
+      antMessage.error('语音服务未连接,请重新录音发送');
+      recorderRef.current?.stop();
+      performanceLogger.clearRequest(currentRequestId, 'websocket_disconnected_before_send');
+      reset();
+      return;
+    }
+
+    timingRef.current.recordEnd = Date.now();
+    sendTimeRef.current = Date.now();
+    performanceLogger.recordEnd(currentRequestId, chunkIndexRef.current);
+
+    // 计算说话时长(毫秒)
+    const speechDuration = timingRef.current.recordEnd - timingRef.current.recordStart;
+    
+    // 更新进度条(基于说话时长,30秒=100%)
+    if (speechDuration > 0) {
+      updateProgressByDuration(speechDuration);
+    }
+
+    recorderRef.current?.stop();
+
+    // 发送音频结束消息,包含说话时长信息
+    wsRef.current?.sendAudioEnd(currentRequestId, chunkIndexRef.current, speechDuration);
+    setState('sending');
+    
+    // 设置超时保护:10秒后如果还在 sending 或 playing 状态,自动重置
+    if (sendingTimeoutRef.current) {
+      clearTimeout(sendingTimeoutRef.current);
+    }
+    sendingTimeoutRef.current = window.setTimeout(() => {
+      const state = useAppStore.getState().currentState;
+      if (state === 'sending' || state === 'playing') {
+        console.warn(`[超时] [${currentRequestId.substring(0, 8)}] 响应超时,自动重置`);
+        antMessage.warning('响应超时,请重试');
+        setState('idle');
+        reset();
+        performanceLogger.clearRequest(currentRequestId, '响应超时');
+      }
+    }, 10000);
+  };
+
+  handleSendRef.current = handleSend;
+
+  // 取消录音
+  const handleCancel = () => {
+    if (currentState !== 'recording' || !currentRequestId) return;
+
+    console.log(`[取消] [${currentRequestId.substring(0, 8)}] 取消录音`);
+    recorderRef.current?.stop();
+    stopRecordingTimer();
+
+    wsRef.current?.sendCancel(currentRequestId);
+    performanceLogger.clearRequest(currentRequestId);
+    reset();
+  };
+
+  const actionButtonSize = Math.round(layout.recordButtonSize * 0.58);
+  const actionButtonTop = layout.progressRingTop + layout.recordButtonOffset;
+  const shouldShowCountdown =
+    currentState === 'recording' &&
+    remainingTimeMs !== null &&
+    remainingTimeMs <= COUNTDOWN_THRESHOLD_MS;
+
+  const isAvatarAnimating = currentState === 'playing';
+  const currentAvatarSrc = isAvatarAnimating ? animaZoneAvatarGif : animaZoneAvatar;
+  const currentAvatarAlt = isAvatarAnimating ? 'AnimaZone Avatar Animated' : 'AnimaZone Avatar';
+
+  return (
+    <div 
+      className={`chatbot-container relative w-full ${
+        layout.needsScroll ? 'h-auto overflow-y-auto overflow-x-hidden' : 'h-screen overflow-hidden'
+      }`}
+      style={{
+        minHeight: layout.needsScroll ? `${layout.contentHeight}px` : '100vh',
+        // ⚠️ 移除 safe-area-inset class,改用手动控制顶部安全区域
+        // 底部不加 padding,让声波纹可以直接贴底定位
+        paddingTop: 'env(safe-area-inset-top)',
+        paddingLeft: 'env(safe-area-inset-left)',
+        paddingRight: 'env(safe-area-inset-right)',
+      }}
+    >
+      {/* 背景图片 */}
+      <div
+        className="chatbot-bg absolute inset-0 pointer-events-none"
+        style={{
+          backgroundImage: 'url(/tolan-bg.svg)',
+          backgroundSize: 'cover',
+          backgroundPosition: 'center',
+          backgroundRepeat: 'no-repeat',
+          zIndex: 0,
+        }}
+      />
+
+      {/* 内容容器 - 自适应所有屏幕尺寸 */}
+      <div 
+        className="relative w-full"
+        style={{
+          boxSizing: 'border-box',
+          minHeight: layout.needsScroll ? `${layout.contentHeight}px` : '100vh',
+          height: layout.needsScroll ? 'auto' : '100%',
+        }}
+      >
+        {/* 树木装饰 - 设计稿:top 175px, 宽度 395.03px, 高度 220.66px, 左右居中 */}
+        <img
+          src={treesImage}
+          alt="Trees"
+          className="chatbot-trees absolute pointer-events-none"
+          style={{
+            width: `${layout.treesWidth}px`,
+            height: 'auto',
+            top: `${layout.treesTop}px`,
+            left: '50%',
+            transform: 'translateX(-50%)',
+            opacity: 1,
+            zIndex: 1,
+          }}
+        />
+
+        {/* Avatar-C 头像 - 设计稿:top 63px, right 63px, 尺寸 40×40 */}
+        <img
+          src={avatarC}
+          alt="Avatar C"
+          className="chatbot-avatar-c absolute pointer-events-none rounded-full"
+          style={{
+            width: `${layout.avatarCSize}px`,
+            height: `${layout.avatarCSize}px`,
+            top: `${layout.avatarCTop}px`,
+            right: `${layout.avatarCRight}px`,
+            opacity: 0.2,
+            zIndex: 5,
+          }}
+        />
+
+        {/* AnimaZone 头像 - 设计稿:top 248px (树木175px + 73px), 尺寸 343×340, 左右居中 */}
+        <img
+          src={currentAvatarSrc}
+          alt={currentAvatarAlt}
+          className="chatbot-anima-zone-avatar absolute"
+          style={{
+            width: `${layout.mainAvatarWidth}px`,
+            height: `${layout.mainAvatarHeight}px`,
+            top: `${layout.animaZoneTop}px`,
+            left: '50%',
+            transform: 'translateX(-50%)',
+            opacity: 1,
+            zIndex: 2,
+          }}
+        />
+
+      {/* 情感展示 - 设计稿:距离界面顶部 50px,左右居中 */}
+      <div
+        className="chatbot-emotion absolute"
+        style={{
+          top: `${layout.emotionTop}px`,
+          left: '50%',
+          transform: 'translateX(-50%)',
+          textAlign: 'center',
+          zIndex: 3,
+        }}
+      >
+        <div
+          className="text-fluid-sm"
+          style={{
+            fontWeight: 500,
+            color: 'rgba(255, 255, 255, 0.9)',
+            textShadow: '0px 2px 4px rgba(0, 0, 0, 0.3)',
+            padding: '6px 16px',
+            borderRadius: '12px',
+            background: 'rgba(255, 255, 255, 0.1)',
+            backdropFilter: 'blur(10px)',
+            border: '1px solid rgba(255, 255, 255, 0.2)',
+          }}
+        >
+          {currentEmotion}
+        </div>
+      </div>
+
+      {/* 音频波形 - 宽度 100% 自适应 */}
+      <div
+        className="chatbot-audio-wave overflow-hidden"
+        style={{
+          position: 'fixed', // 改为 fixed,直接相对于视口定位
+          width: '100%',
+          height: shouldExpandWaveform
+            ? `${layout.waveformHeight}px`
+            : '20px',
+          bottom: `${layout.waveformBottom}px`,
+          left: 0,
+          transition: 'height 0.3s ease-in-out',
+          zIndex: 10,
+        }}
+        data-expanded={shouldExpandWaveform ? 'true' : 'false'}
+        data-waveform-bottom={layout.waveformBottom}
+      >
+        <div
+          style={{
+            position: 'absolute',
+            bottom: 0,
+            left: 0,
+            width: '100%',
+            height: `${layout.waveformHeight}px`,
+          }}
+        >
+          <AudioWave
+            isAnimating={currentState === 'recording'}
+            width={typeof window !== 'undefined' ? window.innerWidth : 375}
+            height={layout.waveformHeight}
+          />
+        </div>
+      </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-ring absolute"
+            style={{
+              width: '100%',
+              height: '100%',
+              top: 0,
+              left: '-0.5px',
+            }}
+          >
+            {/* 空状态背景 */}
+            <img
+              src={progressRing}
+              alt="Progress Ring"
+              className="pointer-events-none"
+              style={{
+                position: 'absolute',
+                top: 0,
+                left: 0,
+                width: '100%',
+                height: '100%',
+                opacity: 1,
+              }}
+            />
+            
+            {/* 进度填充层 - 使用conic-gradient mask */}
+            <div
+              className="pointer-events-none"
+              style={{
+                position: 'absolute',
+                top: '5px',
+                left: 0,
+                width: '100%',
+                height: '100%',
+                maskImage: `conic-gradient(from 215deg at 50% 50%, black 0deg, black ${displayProgress * 2.88}deg, transparent ${displayProgress * 2.88}deg, transparent 360deg)`,
+                WebkitMaskImage: `conic-gradient(from 215deg at 50% 50%, black 0deg, black ${displayProgress * 2.88}deg, transparent ${displayProgress * 2.88}deg, transparent 360deg)`,
+              }}
+            >
+              <img
+                src={progressFull}
+                alt="Progress Full"
+                style={{
+                  width: '100%',
+                  height: '100%',
+                  display: 'block',
+                }}
+              />
+            </div>
+          </div>
+
+          {/* 录音按钮 - 设计稿:尺寸 96×96,在进度环内居中,左右居中 */}
+          <div
+            className={`chatbot-record-button absolute touch-manipulation ${isConnected ? 'cursor-pointer' : 'cursor-not-allowed'}`}
+            onClick={handleStartRecord}
+            style={{
+              width: `${layout.recordButtonSize}px`,
+              height: `${layout.recordButtonSize}px`,
+              minWidth: '44px',
+              minHeight: '44px',
+              top: `${layout.recordButtonOffset}px`,
+              left: '50%',
+              transform: 'translateX(-50%)',
+              borderRadius: '100px',
+              opacity: isConnected ? 1 : 0.55,
+              display: 'flex',
+              alignItems: 'center',
+              justifyContent: 'center',
+              userSelect: 'none',
+              WebkitUserSelect: 'none',
+              caretColor: 'transparent',
+              outline: 'none',
+              filter: isConnected ? 'none' : 'grayscale(0.25)',
+            }}
+            contentEditable={false}
+            tabIndex={-1}
+            aria-disabled={!isConnected}
+          >
+          {/* 录音按钮背景 - PNG 图片 */}
+          <img
+            src={audioButton}
+            alt="Audio Button Background"
+            className="pointer-events-none"
+            style={{
+              width: '100%',
+              height: '100%',
+              position: 'absolute',
+              top: 0,
+              left: 0,
+            }}
+          />
+
+          {/* 麦克风图标 - 居中 - 响应式尺寸 */}
+          <img
+              src={microphoneIcon}
+              alt="Microphone"
+              className="pointer-events-none"
+              style={{
+                width: `${layout.recordButtonSize * 0.33}px`,
+                height: `${layout.recordButtonSize * 0.33}px`,
+                opacity: 1,
+                position: 'relative',
+                zIndex: 1,
+              }}
+            />
+          </div>
+        </div>
+      )}
+
+      {!isConnected && currentState !== 'recording' && (
+        <div
+          className="absolute text-white/80 text-center"
+          style={{
+            top: `${layout.progressRingTop - 36}px`,
+            left: '50%',
+            transform: 'translateX(-50%)',
+            zIndex: 25,
+            fontSize: `${Math.max(12, layout.recordButtonSize * 0.18)}px`,
+            textShadow: '0 2px 6px rgba(0,0,0,0.35)',
+          }}
+        >
+          正在重连语音服务...
+        </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>
+  );
+};
+

+ 17 - 1
frontend/src/pages/ChatBotPage.tsx

@@ -659,6 +659,11 @@ export const ChatBotPage = () => {
     remainingTimeMs !== null &&
     remainingTimeMs <= COUNTDOWN_THRESHOLD_MS;
 
+  // 自适应:根据视口宽度,在大屏上将内容收窄居中展示(保持“手机卡片”视觉)
+  const isWideScreen = layout.viewportWidth >= 768;
+  const contentMaxWidth = isWideScreen ? 480 : layout.viewportWidth;
+  const horizontalPadding = isWideScreen ? 24 : 0;
+
   const isAvatarAnimating = currentState === 'playing';
   const currentAvatarSrc = isAvatarAnimating ? animaZoneAvatarGif : animaZoneAvatar;
   const currentAvatarAlt = isAvatarAnimating ? 'AnimaZone Avatar Animated' : 'AnimaZone Avatar';
@@ -670,6 +675,9 @@ export const ChatBotPage = () => {
       }`}
       style={{
         minHeight: layout.needsScroll ? `${layout.contentHeight}px` : '100vh',
+        // 大屏时使用 flex 居中,让内容以“居中手机卡片”的形式展示
+        display: 'flex',
+        justifyContent: 'center',
         // ⚠️ 移除 safe-area-inset class,改用手动控制顶部安全区域
         // 底部不加 padding,让声波纹可以直接贴底定位
         paddingTop: 'env(safe-area-inset-top)',
@@ -696,6 +704,11 @@ export const ChatBotPage = () => {
           boxSizing: 'border-box',
           minHeight: layout.needsScroll ? `${layout.contentHeight}px` : '100vh',
           height: layout.needsScroll ? 'auto' : '100%',
+          // 大屏上收窄宽度并居中,避免在桌面端过于“摊平”
+          maxWidth: `${contentMaxWidth}px`,
+          margin: '0 auto',
+          paddingLeft: `${horizontalPadding}px`,
+          paddingRight: `${horizontalPadding}px`,
         }}
       >
         {/* 树木装饰 - 设计稿:top 175px, 宽度 395.03px, 高度 220.66px, 左右居中 */}
@@ -736,7 +749,10 @@ export const ChatBotPage = () => {
           className="chatbot-anima-zone-avatar absolute"
           style={{
             width: `${layout.mainAvatarWidth}px`,
-            height: `${layout.mainAvatarHeight}px`,
+            // 使用等比缩放防止在特殊分辨率下出现拉伸变形
+            height: 'auto',
+            maxWidth: '100%',
+            objectFit: 'contain',
             top: `${layout.animaZoneTop}px`,
             left: '50%',
             transform: 'translateX(-50%)',

+ 521 - 0
frontend/src/pages/WelcomePage copy.css

@@ -0,0 +1,521 @@
+/**
+ * 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;
+    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;
+    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;
+}
+
+/* ========================================
+   视频播放器容器
+   ======================================== */
+.player-container {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 10;
+  transition: opacity 0.3s ease-in;
+}
+
+.player-container.player-visible {
+  opacity: 1;
+  visibility: visible;
+  pointer-events: auto;
+}
+
+.player-container.player-hidden {
+  opacity: 0;
+  visibility: hidden;
+  pointer-events: none;
+}
+
+/* Video.js 容器样式 */
+.video-js {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100% !important;
+  height: 100% !important;
+  padding-top: 0 !important;
+  background: transparent;
+  z-index: 1;
+}
+
+/* Video.js 技术层(实际video元素) */
+.video-js .vjs-tech {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  z-index: 2;
+}
+
+/* 隐藏 Video.js 控制栏 */
+.video-js .vjs-control-bar {
+  display: none !important;
+  opacity: 0 !important;
+  visibility: hidden !important;
+}
+
+.video-js:hover .vjs-control-bar,
+.video-js.vjs-user-active .vjs-control-bar,
+.video-js.vjs-user-inactive .vjs-control-bar,
+.video-js.vjs-paused .vjs-control-bar {
+  display: none !important;
+  opacity: 0 !important;
+  visibility: hidden !important;
+}
+
+/* 隐藏原生浏览器控制栏 */
+.video-js video::-webkit-media-controls {
+  display: none !important;
+  -webkit-appearance: none !important;
+  opacity: 0 !important;
+  visibility: hidden !important;
+}
+
+.video-js video::-webkit-media-controls-enclosure {
+  display: none !important;
+  -webkit-appearance: none !important;
+  opacity: 0 !important;
+  visibility: hidden !important;
+}
+
+.video-js video::-webkit-media-controls-panel,
+.video-js video::-webkit-media-controls-play-button,
+.video-js video::-webkit-media-controls-timeline,
+.video-js video::-webkit-media-controls-current-time-display,
+.video-js video::-webkit-media-controls-time-remaining-display,
+.video-js video::-webkit-media-controls-mute-button,
+.video-js video::-webkit-media-controls-volume-slider,
+.video-js video::-webkit-media-controls-fullscreen-button {
+  display: none !important;
+}
+
+/* 移动端 iOS Safari - 隐藏播放开始按钮和叠加层 */
+video::-webkit-media-controls-start-playback-button,
+video::-webkit-media-controls-overlay-enclosure,
+video::-webkit-media-controls-overlay-play-button {
+  display: none !important;
+  -webkit-appearance: none !important;
+  opacity: 0 !important;
+}
+
+/* 移动端强制隐藏所有原生控件 */
+.player-container video,
+.video-js video {
+  -webkit-appearance: none !important;
+  appearance: none !important;
+}
+
+/* 移除Video.js的聚焦轮廓 */
+.video-js *:focus {
+  outline: none;
+}
+
+/* 隐藏大播放按钮 */
+.video-js .vjs-big-play-button {
+  display: none !important;
+  opacity: 0 !important;
+  visibility: hidden !important;
+  pointer-events: none !important;
+}
+
+/* 隐藏加载动画 */
+.video-js .vjs-loading-spinner {
+  display: none !important;
+  opacity: 0 !important;
+  visibility: hidden !important;
+  pointer-events: none !important;
+}
+
+/* 视频元素本身需要可见和可交互(用于播放) */
+.video-js,
+.video-js .vjs-tech,
+.video-js video {
+  pointer-events: auto !important;
+  cursor: default;
+}
+
+/* Video.js 响应式处理 */
+.vjs-fluid {
+  padding-top: 0 !important;
+}
+
+.vjs-16-9 {
+  padding-top: 0 !important;
+}
+
+/* 确保video元素填充满容器 */
+.video-js video {
+  width: 100% !important;
+  height: 100% !important;
+  object-fit: contain;
+  z-index: 2;
+  display: block !important;
+}
+
+/* 禁用文本选择和拖拽 */
+.player-container,
+.video-js {
+  user-select: none;
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  -webkit-touch-callout: none;
+}
+
+/* ========================================
+   加载动画遮罩
+   ======================================== */
+.video-loading-overlay {
+  position: fixed;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  background: rgba(0, 0, 0, 0.7);
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  z-index: 10000;
+  backdrop-filter: blur(4px);
+}
+
+.video-loading-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 0.24rem;
+}
+
+.video-loading-spinner {
+  position: relative;
+  width: 0.8rem;
+  height: 0.8rem;
+}
+
+.spinner-ring {
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  border: 3px solid transparent;
+  border-top-color: #fff;
+  border-radius: 50%;
+  animation: spin 1s linear infinite;
+}
+
+.spinner-ring-1 {
+  border-top-color: rgba(255, 255, 255, 0.9);
+  animation-duration: 1s;
+}
+
+.spinner-ring-2 {
+  width: 70%;
+  height: 70%;
+  top: 15%;
+  left: 15%;
+  border-top-color: rgba(255, 255, 255, 0.7);
+  animation-duration: 1.5s;
+  animation-direction: reverse;
+}
+
+.spinner-ring-3 {
+  width: 40%;
+  height: 40%;
+  top: 30%;
+  left: 30%;
+  border-top-color: rgba(255, 255, 255, 0.5);
+  animation-duration: 2s;
+}
+
+@keyframes spin {
+  0% {
+    transform: rotate(0deg);
+  }
+  100% {
+    transform: rotate(360deg);
+  }
+}
+
+.video-loading-text {
+  color: #fff;
+  font-size: 0.16rem;
+  font-weight: 500;
+  text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
+  animation: pulse 1.5s ease-in-out infinite;
+}
+
+@keyframes pulse {
+  0%, 100% {
+    opacity: 1;
+  }
+  50% {
+    opacity: 0.6;
+  }
+}
+

+ 268 - 0
frontend/src/pages/WelcomePage copy.tsx

@@ -0,0 +1,268 @@
+import { useEffect, useRef, useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import videojs from 'video.js';
+import { setStorage, STORAGE_KEYS, markRouteVisited } from '@/utils/storage';
+import 'video.js/dist/video-js.css';
+import chatbotBg from '@/assets/chatbot-bg.svg';
+import lumiImage from '@/assets/Lumi-image.png';
+import helloImage from '@/assets/Hello,.png';
+import './WelcomePage.css';
+
+// 扩展 videojs 的类型定义
+type VideoJsPlayer = ReturnType<typeof videojs>;
+
+type WelcomeViewState = 'welcome' | 'loading' | 'playing';
+
+export const WelcomePage = () => {
+  const navigate = useNavigate();
+  const videoRef = useRef<HTMLVideoElement>(null);
+  const playerRef = useRef<VideoJsPlayer | null>(null);
+  const [viewState, setViewState] = useState<WelcomeViewState>('welcome');
+  const [isLoading, setIsLoading] = useState(false);
+
+  useEffect(() => {
+    // 记录用户当前所在页面
+    setStorage(STORAGE_KEYS.CURRENT_PAGE, 'welcome');
+    
+    // 记录路由访问
+    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);
+    };
+  }, []);
+
+  // 页面打开后,后台预加载视频(在 video 元素挂载后)
+  useEffect(() => {
+    // 使用 setTimeout 确保 video 元素已经挂载到 DOM
+    const initTimer = setTimeout(() => {
+      if (!playerRef.current && videoRef.current) {
+        console.log('[WelcomePage] 后台预加载视频...');
+        const player = videojs(videoRef.current, {
+          controls: false,
+          autoplay: false,
+          preload: 'auto', // 预加载视频
+          fluid: true,
+          aspectRatio: '16:9',
+          muted: false,
+          nativeControlsForTouch: false,
+          playsinline: true,
+          bigPlayButton: false,
+          textTrackDisplay: false,
+          posterImage: false,
+          errorDisplay: false,
+        });
+
+        playerRef.current = player;
+
+        // 配置原生 video 元素
+        const videoElement = player.el().querySelector('video') as HTMLVideoElement;
+        if (videoElement) {
+          videoElement.removeAttribute('controls');
+          videoElement.controls = false;
+          videoElement.setAttribute('playsinline', '');
+          videoElement.setAttribute('webkit-playsinline', '');
+          videoElement.setAttribute('x5-playsinline', '');
+          videoElement.setAttribute('x5-video-player-type', 'h5');
+          videoElement.setAttribute('x5-video-player-fullscreen', 'false');
+          videoElement.setAttribute('controlsList', 'nodownload nofullscreen noremoteplayback');
+          videoElement.setAttribute('disablePictureInPicture', 'true');
+          
+          // 隐藏视频元素(后台加载)
+          videoElement.style.visibility = 'hidden';
+          videoElement.style.opacity = '0';
+          videoElement.style.position = 'absolute';
+          videoElement.style.top = '-9999px';
+        }
+
+        // 持续监控并移除 controls 属性
+        const controlWatcher = setInterval(() => {
+          const playerEl = player.el();
+          if (!playerEl) return;
+          const videoEl = playerEl.querySelector('video') as HTMLVideoElement;
+          if (videoEl && videoEl.hasAttribute('controls')) {
+            videoEl.removeAttribute('controls');
+            videoEl.controls = false;
+          }
+        }, 100);
+
+        (player as any)._controlWatcher = controlWatcher;
+
+        // 绑定播放结束事件
+        const handleEnded = () => {
+          console.log('[WelcomePage] 视频播放完成,跳转到 /naming');
+          navigate('/naming');
+        };
+        player.on('ended', handleEnded);
+        (player as any)._endedHandler = handleEnded;
+      }
+    }, 100);
+
+    // 清理函数
+    return () => {
+      clearTimeout(initTimer);
+      if (playerRef.current) {
+        const player = playerRef.current;
+        const watcher = (player as any)._controlWatcher;
+        if (watcher) {
+          clearInterval(watcher);
+        }
+        const endedHandler = (player as any)._endedHandler;
+        if (endedHandler) {
+          player.off('ended', endedHandler);
+        }
+        // 注意:不在这里销毁播放器,因为视频需要保持加载状态
+      }
+    };
+  }, [navigate]);
+
+  // 当进入播放状态时,显示视频并开始播放
+  useEffect(() => {
+    if (viewState === 'playing' && playerRef.current) {
+      console.log('[WelcomePage] 开始播放视频');
+      const player = playerRef.current;
+      const videoElement = player.el().querySelector('video') as HTMLVideoElement;
+      
+      if (videoElement) {
+        // 显示视频元素
+        videoElement.style.visibility = 'visible';
+        videoElement.style.opacity = '1';
+        videoElement.style.position = 'absolute';
+        videoElement.style.top = '0';
+        videoElement.style.left = '0';
+        videoElement.style.width = '100%';
+        videoElement.style.height = '100%';
+        videoElement.style.zIndex = '2';
+
+        // 确保视频未静音
+        player.muted(false);
+        videoElement.muted = false;
+        videoElement.volume = 1.0;
+        
+        // 尝试播放
+        player.play().catch((error) => {
+          console.warn('[WelcomePage] 自动播放失败,尝试静音播放:', error);
+          player.muted(true);
+          player.play().catch((err) => {
+            console.error('[WelcomePage] 静音播放也失败:', err);
+          });
+        });
+      }
+    }
+  }, [viewState]);
+
+  // 监听播放结束事件(备用方案,确保事件已绑定)
+  useEffect(() => {
+    if (!playerRef.current) return;
+
+    const player = playerRef.current;
+    
+    // 如果还没有绑定 ended 事件,则绑定
+    if (!(player as any)._endedHandler) {
+      const handleEnded = () => {
+        console.log('[WelcomePage] 视频播放完成,跳转到 /naming');
+        navigate('/naming');
+      };
+      player.on('ended', handleEnded);
+      (player as any)._endedHandler = handleEnded;
+    }
+  }, [navigate, viewState]); // 当播放状态改变时,确保事件已绑定
+
+  const handleHelloClick = () => {
+    // 切换到加载状态
+    setViewState('loading');
+    setIsLoading(true);
+
+    // 3秒加载倒计时
+    setTimeout(() => {
+      setIsLoading(false);
+      setViewState('playing');
+    }, 3000);
+  };
+
+  return (
+    <div className="welcome-page">
+      {/* 背景层 - 全屏覆盖 */}
+      <div 
+        className="welcome-bg"
+        style={{
+          backgroundImage: `url(${chatbotBg})`,
+        }}
+      />
+      
+      {/* 欢迎界面内容 */}
+      {viewState === 'welcome' && (
+        <div className="welcome-content">
+          {/* Lumi-image 图片 - 左下角 */}
+          <img 
+            src={lumiImage} 
+            alt="Lumi"
+            className="welcome-lumi"
+          />
+          
+          {/* Hello 图片 - 可点击 */}
+          <img 
+            src={helloImage} 
+            alt="Hello"
+            className="welcome-hello"
+            onClick={handleHelloClick}
+          />
+        </div>
+      )}
+
+      {/* 加载动画遮罩 */}
+      {isLoading && (
+        <div className="video-loading-overlay">
+          <div className="video-loading-content">
+            {/* 加载动画 */}
+            <div className="video-loading-spinner">
+              <div className="spinner-ring spinner-ring-1" />
+              <div className="spinner-ring spinner-ring-2" />
+              <div className="spinner-ring spinner-ring-3" />
+            </div>
+            {/* 加载文本 */}
+            <p className="video-loading-text">视频加载中...</p>
+          </div>
+        </div>
+      )}
+      
+      {/* 视频播放器容器 */}
+      <div className={`player-container ${viewState === 'playing' ? 'player-visible' : 'player-hidden'}`}>
+        <video
+          ref={videoRef}
+          className="video-js"
+          playsInline
+          webkit-playsinline="true"
+          x5-playsinline="true"
+          x5-video-player-type="h5"
+          x5-video-player-fullscreen="false"
+          data-setup="{}"
+          controlsList="nodownload nofullscreen noremoteplayback"
+          disablePictureInPicture
+        >
+          <source src="https://tolan.oss-cn-hangzhou.aliyuncs.com/ABC.mp4" type="video/mp4" />
+          <p className="vjs-no-js">
+            您的浏览器不支持HTML5视频,请升级浏览器。
+          </p>
+        </video>
+      </div>
+    </div>
+  );
+};
+

+ 43 - 73
frontend/src/pages/WelcomePage.css

@@ -21,7 +21,9 @@
 .welcome-page {
   position: relative;
   width: 100%;
-  height: 100%;
+  /* 让欢迎页本身也跟随视口高度自适应,而不是依赖父级固定高度 */
+  min-height: 100dvh;
+  min-height: calc(var(--vh, 1vh) * 100);
   overflow: hidden;
   background-color: #E8E8E8;
   /* iOS 安全区域适配 */
@@ -51,10 +53,12 @@
    ======================================== */
 .welcome-content {
   position: relative;
-  width: 100vw;
+  /* 宽度跟随父级,在大屏上也保持全宽自适应 */
+  width: 100%;
   /* 使用动态视口高度(dvh),降级方案使用 JavaScript 设置的 CSS 变量 */
-  height: 100dvh; /* 现代浏览器:动态视口高度 */
-  height: calc(var(--vh, 1vh) * 100); /* 降级方案:JavaScript 计算的视口高度 */
+  min-height: 100dvh; /* 现代浏览器:动态视口高度 */
+  min-height: calc(var(--vh, 1vh) * 100); /* 降级方案:JavaScript 计算的视口高度 */
+  margin: 0 auto;
   z-index: 1;
 }
 
@@ -68,15 +72,11 @@
   /* 使用 contain 确保图片完整显示,不裁剪 */
   object-fit: contain;
   object-position: left bottom;
-  /* 自适应宽度,最大不超过容器 */
-  width: auto;
-  max-width: 100%;
-  /* 自适应高度,最大不超过下半部分 */
+  /* 宽度驱动的等比缩放,减少对视口高度的依赖,增强自适应 */
+  width: 100%;
   height: auto;
-  max-height: 50dvh; /* 使用动态视口高度 */
-  max-height: calc(var(--vh, 1vh) * 50); /* 降级方案 */
-  /* 确保图片从底部对齐 */
-  align-self: flex-end;
+  /* 无论放大还是缩小,高度不超过半个屏幕 */
+  max-height: calc(var(--vh, 1vh) * 50);
 }
 
 /* ========================================
@@ -85,15 +85,13 @@
    ======================================== */
 .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;
+  /* 使用相对内容容器高度/宽度的百分比,避免直接跟视口高度绑定 */
+  /* 约等于设计稿中 229 / 812 ≈ 28.2% */
+  top: 28.2%;
+  /* 约等于 45 / 375 ≈ 12% */
+  left: 12%;
+  /* 约等于 245 / 375 ≈ 65.33% */
+  width: 65.33%;
   cursor: pointer;
   transition: opacity 0.2s ease;
   user-select: none;
@@ -116,28 +114,21 @@
 /* 极小屏幕(< 320px):适当缩小 */
 @media (max-width: 319px) {
   .welcome-hello {
-    width: 60vw;
-    left: 10vw;
+    width: 60%;
+    left: 10%;
   }
   
   .welcome-lumi {
-    /* 极小屏幕确保完整显示 */
-    max-height: 45dvh; /* 使用动态视口高度 */
-    max-height: calc(var(--vh, 1vh) * 45); /* 降级方案 */
+    /* 极小屏幕适当缩小宽度,保证完整显示 */
+    width: 90%;
   }
 }
 
 /* 小屏手机(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); /* 降级方案 */
+    width: 65.33%;
+    left: 12%;
   }
 }
 
@@ -146,27 +137,16 @@
   .welcome-hello {
     width: 245px;
     left: 45px;
-    top: 28.2dvh; /* 使用动态视口高度 */
-    top: calc(var(--vh, 1vh) * 28.2); /* 降级方案 */
+    /* 仍然按内容容器高度的约 28.2% 布局,避免受视口高度波动影响 */
+    top: 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); /* 降级方案 */
+    /* 在大屏手机上保持自适应宽度 */
+    width: 100%;
   }
   
   .welcome-hello {
@@ -185,22 +165,17 @@
   }
   
   .welcome-content {
-    width: 375px;
-    height: 812px;
-    max-width: 375px;
-    max-height: calc(100dvh - 80px); /* 使用动态视口高度 */
-    max-height: calc(calc(var(--vh, 1vh) * 100) - 80px); /* 降级方案 */
+    /* 桌面端保持全宽自适应 */
+    width: 100%;
     margin: 0 auto;
-    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
-    border-radius: 20px;
+    /* 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); /* 降级方案 */
+    /* 桌面端保持自适应宽度 */
+    width: 100%;
   }
   
   .welcome-hello {
@@ -215,28 +190,23 @@
    ======================================== */
 @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;
+    /* 横屏时仍然使用全高,同时保持全宽自适应 */
+    min-height: 100dvh; /* 使用动态视口高度 */
+    min-height: calc(var(--vh, 1vh) * 100); /* 降级方案 */
+    width: 100%;
     margin: 0 auto;
   }
   
   .welcome-lumi {
     /* 横屏时保持自适应,确保完整显示 */
-    max-width: 100%;
-    max-height: 50dvh; /* 使用动态视口高度 */
-    max-height: calc(var(--vh, 1vh) * 50); /* 降级方案 */
+    width: 100%;
   }
   
   .welcome-hello {
     /* 横屏时保持相对位置 */
-    top: 28.2dvh; /* 使用动态视口高度 */
-    top: calc(var(--vh, 1vh) * 28.2); /* 降级方案 */
-    left: 12vw;
-    width: 65.33vw;
-    max-width: 245px;
+    top: 28.2%;
+    left: 12%;
+    width: 65.33%;
   }
 }