zlwu 10 mesi fa
parent
commit
008c2a9e23

+ 21 - 1
backend/main.py

@@ -459,7 +459,26 @@ async def claim_share_reward(user_id: str):
                 .values(current_background_image_id="happynewyear")
             )
             
-            # 5. 获取背景图片的 metadata(用于前端设置透明度等)
+            # 5. 更新用户的 has_shared_avatar 状态
+            from models_db import UserUIState
+            ui_state_result = await session.execute(
+                select(UserUIState).where(UserUIState.user_id == user_id)
+            )
+            ui_state = ui_state_result.scalar_one_or_none()
+            if ui_state:
+                ui_state.has_shared_avatar = True
+                session.add(ui_state)
+                logger.info(f"✅ 更新用户分享头像状态: user_id={user_id}, has_shared_avatar=True")
+            else:
+                # 如果不存在 UI 状态记录,创建一个新的
+                new_ui_state = UserUIState(
+                    user_id=user_id,
+                    has_shared_avatar=True
+                )
+                session.add(new_ui_state)
+                logger.info(f"✅ 创建用户UI状态并设置分享头像状态: user_id={user_id}, has_shared_avatar=True")
+            
+            # 6. 获取背景图片的 metadata(用于前端设置透明度等)
             bg_metadata = None
             try:
                 from models_db import BackgroundImage
@@ -1088,6 +1107,7 @@ async def update_user_ui_state(
             has_clicked_mission_stone=request.has_clicked_mission_stone,
             has_clicked_conversation_stone=request.has_clicked_conversation_stone,
             avatar_c_unlocked=request.avatar_c_unlocked,
+            has_shared_avatar=request.has_shared_avatar,
             current_avatar_id=request.current_avatar_id,
         )
         if not updated:

+ 135 - 76
frontend/src/pages/ChatBotPage.tsx

@@ -599,17 +599,29 @@ export const ChatBotPage = () => {
       return;
     }
 
-    // 若变化极小,直接同步
+    // 处理整数圈:如果 diff 接近 0 且是整数圈,播放完整一圈动画
+    let actualDiff = diff;
     if (Math.abs(diff) < 0.1) {
-      totalProgressRef.current = targetTotal;
-      const normalized = ((targetTotal % 100) + 100) % 100;
-      setDisplayProgress(normalized);
-      return;
+      // 判断是否是整数圈(目标值是 100 的整数倍)
+      const isIntegerLoop = Math.abs(targetTotal % 100) < 0.1;
+      if (isIntegerLoop && targetTotal > startTotal) {
+        // 整数圈增长,播放完整一圈动画
+        actualDiff = 100;
+      } else if (isIntegerLoop && targetTotal < startTotal) {
+        // 整数圈减少,播放完整一圈反向动画
+        actualDiff = -100;
+      } else {
+        // 非整数圈的微小变化,直接同步
+        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 duration = Math.max(400, Math.min(2400, Math.abs(actualDiff) * durationPerPercent));
     const startTime = performance.now();
 
     if (animationFrameRef.current) {
@@ -622,7 +634,7 @@ export const ChatBotPage = () => {
 
       // easeOutCubic 缓动
       const easeProgress = 1 - Math.pow(1 - progress, 3);
-      const currentTotal = startTotal + diff * easeProgress;
+      const currentTotal = startTotal + actualDiff * easeProgress;
       totalProgressRef.current = currentTotal;
 
       const normalized = ((currentTotal % 100) + 100) % 100;
@@ -631,6 +643,7 @@ export const ChatBotPage = () => {
       if (progress < 1) {
         animationFrameRef.current = requestAnimationFrame(animate);
       } else {
+        // 动画完成后,同步到实际的目标值
         totalProgressRef.current = targetTotal;
         const finalNormalized = ((targetTotal % 100) + 100) % 100;
         setDisplayProgress(finalNormalized);
@@ -932,75 +945,162 @@ export const ChatBotPage = () => {
     const visited = getVisitedRoutes();
     const userId = getUserId();
     if (userId) {
-      // 先尝试从后端拉取 UI 状态用于恢复本地提示与 Avatar 状态
-      getUserUIState(userId)
-        .then((resp) => {
-          if (!resp.success || !resp.data) {
-            return;
+      // 从数据库加载所有用户信息到 localStorage
+      // 1. 先加载用户基本信息(getUserInfo)
+      // 2. 再加载用户 UI 状态(getUserUIState)
+      Promise.all([
+        getUserInfo(userId).catch(() => null),
+        getUserUIState(userId).catch(() => null),
+      ]).then(([userInfoResult, uiStateResult]) => {
+        // 加载用户基本信息到 localStorage
+        if (userInfoResult?.success && userInfoResult.data) {
+          const userInfo = userInfoResult.data;
+          
+          // Lyrria_user_name
+          if (userInfo.username) {
+            setUserName(userInfo.username);
+          }
+          
+          // Lyrria_lumi_name
+          if (userInfo.bot_name) {
+            setStorage(STORAGE_KEYS.LUMI_NAME, userInfo.bot_name);
+          }
+          
+          // Lyrria_user_level
+          if (typeof userInfo.user_level === 'number') {
+            syncUserLevel(userInfo.user_level);
+          }
+          
+          // Lyrria_is_logged_in (根据 user_type 判断:registered 为已登录)
+          if (userInfo.user_type === 'registered') {
+            setStorage(STORAGE_KEYS.IS_LOGGED_IN, 'true');
+          } else {
+            setStorage(STORAGE_KEYS.IS_LOGGED_IN, 'false');
           }
-          const state = resp.data;
+          
+          // Lyrria_current_background_image_id, Lyrria_current_background_image_url, Lyrria_current_background_image_opacity
+          if (userInfo.current_background_image_id) {
+            syncBackgroundImageToStorage(
+              userInfo.current_background_image_id,
+              userInfo.current_background_image_metadata
+            );
+            const bgUrl = getCurrentBackgroundImageUrl();
+            const bgId = getCurrentBackgroundImageId();
+            if (bgUrl) {
+              setBackgroundImageUrl(bgUrl);
+            }
+            if (bgId) {
+              setBackgroundImageId(bgId);
+            }
+          }
+          
+          // 初始化进度条(基于 total_speech_duration_ms)
+          const totalDurationMs = userInfo.total_speech_duration_ms ?? 0;
+          if (totalDurationMs > 0) {
+            const PROGRESS_FULL_DURATION_MS = 30 * 1000; // 30000ms
+            totalSpeechDurationRef.current = Math.max(
+              totalDurationMs,
+              avatarCUnlockedRef.current ? AVATAR_C_UNLOCK_THRESHOLD_MS : 0
+            );
+            if (totalDurationMs >= AVATAR_C_UNLOCK_THRESHOLD_MS) {
+              unlockAvatarC();
+            }
+            const totalProgressPercent = (totalDurationMs / PROGRESS_FULL_DURATION_MS) * 100;
+            const initialProgress = totalProgressPercent % 100;
+            shouldSkipProgressAnimationRef.current = true;
+            setProgress(initialProgress, 0);
+          }
+        }
+        
+        // 加载用户 UI 状态到 localStorage
+        if (uiStateResult?.success && uiStateResult.data) {
+          const state = uiStateResult.data;
+          
+          // Lyrria_current_avatar_id
           if (state.current_avatar_id) {
             setCurrentAvatarId(state.current_avatar_id);
             setStorage(STORAGE_KEYS.CURRENT_AVATAR_ID, state.current_avatar_id);
           }
-
+          
+          // Lyrria_current_page
           if (state.current_page) {
             setStorage(STORAGE_KEYS.CURRENT_PAGE, state.current_page);
           }
+          
+          // Lyrria_visited_routes (无论是否为空数组,都从数据库同步)
           if (Array.isArray(state.visited_routes)) {
             setStorage(STORAGE_KEYS.VISITED_ROUTES, JSON.stringify(state.visited_routes));
+          } else if (state.visited_routes === null || state.visited_routes === undefined) {
+            // 如果数据库中是 null 或 undefined,设置为空数组
+            setStorage(STORAGE_KEYS.VISITED_ROUTES, JSON.stringify([]));
           }
+          
+          // Lyrria_has_shown_tap_to_chat
           if (state.has_shown_tap_to_chat) {
             setStorage(STORAGE_KEYS.HAS_SHOWN_TAP_TO_CHAT, 'true');
-            // 只有在 WebSocket 已连接时才隐藏提示(确保用户能看到提示)
-            // 如果 WebSocket 还未连接,保持 showTapToChat 为 true,等连接成功后再隐藏
             if (isConnected) {
               setShowTapToChat(false);
             }
           }
+          
+          // Lyrria_has_shown_cancel_tooltip
           if (state.has_shown_cancel_tooltip) {
             setStorage(STORAGE_KEYS.HAS_SHOWN_CANCEL_TOOLTIP, 'true');
             setShowCancelTooltip(false);
           }
+          
+          // Lyrria_has_shown_send_tooltip
           if (state.has_shown_send_tooltip) {
             setStorage(STORAGE_KEYS.HAS_SHOWN_SEND_TOOLTIP, 'true');
             setShowSendTooltip(false);
           }
+          
+          // Lyrria_has_shown_progress_bar_tooltip
           if (state.has_shown_progress_bar_tooltip) {
             setStorage(STORAGE_KEYS.HAS_SHOWN_PROGRESS_BAR_TOOLTIP, 'true');
             setHasShownProgressBarTooltip(true);
           }
+          
+          // Lyrria_avatar_c_unlocked
           if (state.avatar_c_unlocked && !avatarCUnlockedRef.current) {
             avatarCUnlockedRef.current = true;
             setIsAvatarCUnlocked(true);
             setStorage(STORAGE_KEYS.AVATAR_C_UNLOCKED, 'true');
           }
-          if (state.has_shared_avatar) {
-            setStorage(STORAGE_KEYS.HAS_SHARED_AVATAR, 'true');
-            setHasSharedAvatarState(true);
-          }
+          
+          // Lyrria_has_shared_avatar (无论 true/false,都从数据库同步)
+          setStorage(STORAGE_KEYS.HAS_SHARED_AVATAR, state.has_shared_avatar ? 'true' : 'false');
+          setHasSharedAvatarState(state.has_shared_avatar);
+          
+          // Lyrria_has_shown_mission_stone_modal
           if (state.has_shown_mission_stone_modal) {
             setStorage(STORAGE_KEYS.HAS_SHOWN_MISSION_STONE_MODAL, 'true');
           }
+          
+          // Lyrria_has_shown_conversation_stone_modal
           if (state.has_shown_conversation_stone_modal) {
             setStorage(STORAGE_KEYS.HAS_SHOWN_CONVERSATION_STONE_MODAL, 'true');
           }
+          
+          // Lyrria_has_clicked_mission_stone
           if (state.has_clicked_mission_stone) {
             setStorage(STORAGE_KEYS.HAS_CLICKED_MISSION_STONE, 'true');
             setHasClickedMissionStone(true);
           }
+          
+          // Lyrria_has_clicked_conversation_stone
           if (state.has_clicked_conversation_stone) {
             setStorage(STORAGE_KEYS.HAS_CLICKED_CONVERSATION_STONE, 'true');
             setHasClickedConversationStone(true);
           }
-
-          // 如果本地记录为 true 但后端为 false,尝试补偿同步
+          
+          // 补偿同步:如果本地记录为 true 但后端为 false,尝试补偿同步
           const localMissionShown = getStorage(STORAGE_KEYS.HAS_SHOWN_MISSION_STONE_MODAL) === 'true';
           if (localMissionShown && !state.has_shown_mission_stone_modal) {
             updateUserUIState(userId, {
               has_shown_mission_stone_modal: true,
             }).catch(() => {
-              // 静默处理,后续会继续尝试
+              // 静默处理
             });
           }
 
@@ -1014,7 +1114,6 @@ export const ChatBotPage = () => {
             });
           }
 
-          // 补偿同步能量石点击状态
           const localMissionClicked = getStorage(STORAGE_KEYS.HAS_CLICKED_MISSION_STONE) === 'true';
           if (localMissionClicked && !state.has_clicked_mission_stone) {
             updateUserUIState(userId, {
@@ -1032,10 +1131,11 @@ export const ChatBotPage = () => {
               // 静默处理
             });
           }
-        })
-        .catch(() => {
-          // 拉取失败不影响主流程
-        });
+        }
+      }).catch(() => {
+        // 加载失败不影响主流程
+        console.warn('[ChatBotPage] 从数据库加载用户信息失败');
+      });
 
       // 同步当前页面与已访问路由
       updateUserUIState(userId, {
@@ -1048,7 +1148,7 @@ export const ChatBotPage = () => {
     
     const init = async () => {
       try {
-        // 获取用户信息并初始化进度条
+        // 获取用户信息并初始化进度条(已在上面 Promise.all 中处理,这里保留作为兜底)
         const user_id = getUserId();
         if (user_id) {
           try {
@@ -1092,11 +1192,12 @@ export const ChatBotPage = () => {
                 if (totalDurationMs >= AVATAR_C_UNLOCK_THRESHOLD_MS) {
                   unlockAvatarC();
                 }
+                // 只显示非整数圈的进度,不转多圈
                 const totalProgressPercent = (totalDurationMs / PROGRESS_FULL_DURATION_MS) * 100;
-                const initialLoopCount = Math.floor(totalProgressPercent / 100);
                 const initialProgress = totalProgressPercent % 100;
+                // 重置圈数为 0,只显示当前周期的进度
                 shouldSkipProgressAnimationRef.current = true;
-                setProgress(initialProgress, initialLoopCount);
+                setProgress(initialProgress, 0);
               }
             }
           } catch (error) {
@@ -1294,13 +1395,12 @@ export const ChatBotPage = () => {
             message.totalDurationMs,
             avatarCUnlockedRef.current ? AVATAR_C_UNLOCK_THRESHOLD_MS : 0
           );
-          // 根据总时长计算进度条位置
+          // 根据总时长计算进度条位置(只显示非整数圈的进度)
           const totalProgressPercent = (message.totalDurationMs / PROGRESS_FULL_DURATION_MS) * 100;
-          const loopCount = Math.floor(totalProgressPercent / 100);
           const progress = totalProgressPercent % 100;
-          // 跳过动画,直接同步到后端返回的值
+          // 跳过动画,直接同步到后端返回的值,重置圈数为 0
           shouldSkipProgressAnimationRef.current = true;
-          setProgress(progress, loopCount);
+          setProgress(progress, 0);
         }
         break;
 
@@ -2056,47 +2156,6 @@ export const ChatBotPage = () => {
         />
       </div>
 
-      {/* 情感展示 - 设计稿:距离界面顶部 50px,左右居中,使用 px2vmin 等比缩放 */}
-      <div
-        className="chatbot-emotion absolute"
-        style={{
-          top: px2vmin(50),
-          left: '50%',
-          transform: 'translateX(-50%)',
-          textAlign: 'center',
-          zIndex: 3,
-        }}
-      >
-        <div
-          className="text-fluid-sm"
-          onClick={() => navigate('/level5-welcome')}
-          style={{
-            fontWeight: 500,
-            color: 'rgba(255, 255, 255, 0.9)',
-            textShadow: '0px 2px 4px rgba(0, 0, 0, 0.3)',
-            padding: `${px2vh(6)} ${px2vw(16)}`,
-            borderRadius: px2vw(12),
-            background: 'rgba(255, 255, 255, 0.1)',
-            backdropFilter: 'blur(10px)',
-            border: '1px solid rgba(255, 255, 255, 0.2)',
-            cursor: 'pointer',
-            userSelect: 'none',
-            WebkitUserSelect: 'none',
-            transition: 'all 0.2s ease',
-          }}
-          onMouseEnter={(e) => {
-            e.currentTarget.style.background = 'rgba(255, 255, 255, 0.15)';
-            e.currentTarget.style.transform = 'scale(1.05)';
-          }}
-          onMouseLeave={(e) => {
-            e.currentTarget.style.background = 'rgba(255, 255, 255, 0.1)';
-            e.currentTarget.style.transform = 'scale(1)';
-          }}
-        >
-          {currentEmotion}
-        </div>
-      </div>
-
       {/* 音频波形 - 宽度 100% 自适应 */}
       <div
         className="chatbot-audio-wave overflow-hidden"

+ 7 - 3
frontend/src/pages/FirstLoginRewardPage.tsx

@@ -118,15 +118,19 @@ export const FirstLoginRewardPage = () => {
           setCurrentBackgroundImageUrl(backgroundImageUrl);
         }
         
-        // 更新分享头像状态:更新 localStorage 和数据库
+        // 更新分享头像状态:更新 localStorage
+        // 注意:数据库中的 has_shared_avatar 已在 claimShareReward 接口中更新
         setHasSharedAvatar(true);
+        
+        // 额外同步一次确保数据一致性(claimShareReward 接口已更新,这里作为双重保障)
         try {
           await updateUserUIState(userId, {
             has_shared_avatar: true,
           });
+          console.log('✅ 分享头像状态已同步到数据库');
         } catch (error) {
-          console.error('同步分享头像状态到数据库失败:', error);
-          // 即使同步失败,localStorage 已更新,不影响主流程
+          console.error('⚠️ 同步分享头像状态到数据库失败(claimShareReward 接口已更新,不影响主流程):', error);
+          // 即使同步失败,claimShareReward 接口已经更新了数据库,不影响主流程
         }
         
         message.success('分享奖励已领取!新年背景已解锁');

+ 66 - 29
frontend/src/pages/GreetingPage.tsx

@@ -31,27 +31,68 @@ export const GreetingPage = () => {
 
   // 组件加载时从 localStorage 读取姓名,并记录当前页面
   useEffect(() => {
+    // 先同步读取 localStorage,立即设置状态,不阻塞 UI 渲染
+    const savedName = getStorage(STORAGE_KEYS.USER_NAME);
+    if (savedName) {
+      setPlaceholderName(savedName);
+      setUserName(savedName);
+      setInitialUserName(savedName);
+      setAllowPlaceholderSubmit(false);
+      setHasUserEdited(false);
+    }
+
+    // 记录用户当前所在页面
+    setStorage(STORAGE_KEYS.CURRENT_PAGE, 'naming');
+    
+    // 记录路由访问
+    markRouteVisited('/naming');
+    const visited = getVisitedRoutes();
+
+    // 将 API 调用放在下一个事件循环,不阻塞 UI 渲染
+    // 注意:如果数据已在 welcome 页面预加载完成,这里会跳过大部分 API 调用
     const loadUserData = async () => {
       console.log('[GreetingPage] loadUserData start');
-      // 记录用户当前所在页面
-      const ok = setStorage(STORAGE_KEYS.CURRENT_PAGE, 'naming');
-      console.log('[GreetingPage] set CURRENT_PAGE=naming result:', ok);
       
-      // 记录路由访问
-      markRouteVisited('/naming');
-      const visited = getVisitedRoutes();
-      console.log('[GreetingPage] visited routes after markRouteVisited:', visited);
-
-      // 确保存在 userId:如果没有,则在进入 /naming 时创建用户
-      let userId = getUserId();
-      if (!userId) {
+      // 检查数据是否已经预加载完成(userId 存在且 localStorage 中有用户名)
+      const userId = getUserId();
+      const hasPreloadedData = userId && savedName;
+      
+      if (hasPreloadedData) {
+        console.log('[GreetingPage] ✅ 数据已预加载,跳过 API 调用');
+        // 只更新 UI 状态即可
+        updateUserUIState(userId, {
+          current_page: 'naming',
+          visited_routes: visited,
+        }).catch(() => {
+          // 同步失败不影响前端流程
+        });
+        return;
+      }
+      
+      // 如果数据未预加载,则调用 API(兜底逻辑)
+      let currentUserId = userId;
+      if (!currentUserId) {
         try {
           const response = await createUser();
           if (response.success && response.data?.user_id) {
-            userId = response.data.user_id;
-            setUserId(userId);
+            currentUserId = response.data.user_id;
+            setUserId(currentUserId);
             if (response.data.username) {
-              setUserName(response.data.username);
+              // 如果 localStorage 中没有保存的名字,才使用后端返回的
+              if (!savedName) {
+                if (isBackendGeneratedName(response.data.username)) {
+                  setPlaceholderName(response.data.username);
+                  setUserName('');
+                  setInitialUserName('');
+                  setAllowPlaceholderSubmit(true);
+                } else {
+                  setPlaceholderName(response.data.username);
+                  setUserName(response.data.username);
+                  setInitialUserName(response.data.username);
+                  setAllowPlaceholderSubmit(false);
+                  setStorage(STORAGE_KEYS.USER_NAME, response.data.username);
+                }
+              }
             }
             if (typeof response.data.user_level === 'number') {
               setUserLevel(response.data.user_level);
@@ -63,12 +104,13 @@ export const GreetingPage = () => {
           console.error('[GreetingPage] 创建用户失败:', error);
           antMessage.warning('响应超时,请重试');
           // 创建用户失败时,不阻塞页面,后续依赖 userId 的逻辑将被跳过
-          userId = null;
+          currentUserId = null;
         }
       }
 
-      if (userId) {
-        updateUserUIState(userId, {
+      // 更新用户 UI 状态(非阻塞)
+      if (currentUserId) {
+        updateUserUIState(currentUserId, {
           current_page: 'naming',
           visited_routes: visited,
         }).catch(() => {
@@ -76,17 +118,9 @@ export const GreetingPage = () => {
         });
       }
       
-      // 检查 localStorage 中是否存在 Lyrria_user_name
-      const savedName = getStorage(STORAGE_KEYS.USER_NAME);
-      if (savedName) {
-        setPlaceholderName(savedName);
-        setUserName(savedName);
-        setInitialUserName(savedName);
-        setAllowPlaceholderSubmit(false);
-        setHasUserEdited(false);
-      } else {
-        // 如果不存在,从后端获取用户信息
-        const user_id = userId || getUserId();
+      // 如果 localStorage 中没有保存的名字,从后端获取用户信息
+      if (!savedName) {
+        const user_id = currentUserId || getUserId();
         if (user_id) {
           try {
             const response = await getUserInfo(user_id);
@@ -132,7 +166,10 @@ export const GreetingPage = () => {
       }
     };
     
-    loadUserData();
+    // 使用 setTimeout 将 API 调用延迟到下一个事件循环,确保 UI 先渲染
+    setTimeout(() => {
+      loadUserData();
+    }, 0);
   }, []);
 
   // 监听用户名变化,同步更新 localStorage

+ 45 - 7
frontend/src/pages/ProfilePage.tsx

@@ -2,6 +2,25 @@ import { useEffect, useState, useCallback, useRef } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { px2vmin, px2vmax, px2vw } from '@/utils/viewport';
 import { getUserInfo, updateUsername, updateBotName } from '@/utils/api';
+
+// 获取后端 API 基础 URL(与 api.ts 中的逻辑保持一致)
+const getApiBaseUrl = (): string => {
+  if (import.meta.env.DEV) {
+    return '';  // 使用相对路径,Vite 会代理到后端
+  }
+  
+  if (import.meta.env.VITE_API_BASE_URL) {
+    return import.meta.env.VITE_API_BASE_URL;
+  }
+  
+  const wsUrl = import.meta.env.VITE_WS_URL || 'ws://localhost:8000/ws';
+  const apiUrl = wsUrl
+    .replace(/^wss:\/\//, 'https://')
+    .replace(/^ws:\/\//, 'http://')
+    .replace(/\/ws$/, '');
+  
+  return apiUrl;
+};
 import { getUserId, getStorage, setStorage, STORAGE_KEYS } from '@/utils/storage';
 import { getOssUrl } from '@/utils/oss';
 import { MissionEnergyStoneModal } from '@/components/MissionEnergyStoneModal';
@@ -42,7 +61,7 @@ export const ProfilePage = () => {
   // 获取能量石数量
   const fetchEnergyStoneCounts = useCallback(async (userId: string) => {
     try {
-      const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
+      const apiBaseUrl = getApiBaseUrl();
       const response = await fetch(`${apiBaseUrl}/api/users/${userId}/energy-stones/count`, {
         method: 'GET',
         headers: {
@@ -58,14 +77,20 @@ export const ProfilePage = () => {
         }
       }
     } catch (error) {
-      console.error('获取能量石数量失败:', error);
+      // 静默处理网络错误,避免在开发环境中显示过多错误
+      if (error instanceof TypeError && error.message.includes('fetch')) {
+        // 网络错误(如后端未运行),静默处理
+        console.debug('[ProfilePage] 获取能量石数量失败(后端可能未运行)');
+      } else {
+        console.error('[ProfilePage] 获取能量石数量失败:', error);
+      }
     }
   }, []);
 
   // 获取用户头像信息
   const fetchUserAvatars = useCallback(async (userId: string) => {
     try {
-      const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
+      const apiBaseUrl = getApiBaseUrl();
       const response = await fetch(`${apiBaseUrl}/api/users/${userId}/avatars`, {
         method: 'GET',
         headers: {
@@ -83,14 +108,20 @@ export const ProfilePage = () => {
         }
       }
     } catch (error) {
-      console.error('获取用户头像信息失败:', error);
+      // 静默处理网络错误,避免在开发环境中显示过多错误
+      if (error instanceof TypeError && error.message.includes('fetch')) {
+        // 网络错误(如后端未运行),静默处理
+        console.debug('[ProfilePage] 获取用户头像信息失败(后端可能未运行)');
+      } else {
+        console.error('[ProfilePage] 获取用户头像信息失败:', error);
+      }
     }
   }, []);
 
   // 获取用户背景图片信息
   const fetchUserBackgroundImages = useCallback(async (userId: string) => {
     try {
-      const apiBaseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000';
+      const apiBaseUrl = getApiBaseUrl();
       const response = await fetch(`${apiBaseUrl}/api/users/${userId}/background-images`, {
         method: 'GET',
         headers: {
@@ -113,7 +144,13 @@ export const ProfilePage = () => {
         }
       }
     } catch (error) {
-      console.error('获取用户背景图片信息失败:', error);
+      // 静默处理网络错误,避免在开发环境中显示过多错误
+      if (error instanceof TypeError && error.message.includes('fetch')) {
+        // 网络错误(如后端未运行),静默处理
+        console.debug('[ProfilePage] 获取用户背景图片信息失败(后端可能未运行)');
+      } else {
+        console.error('[ProfilePage] 获取用户背景图片信息失败:', error);
+      }
     }
   }, []);
 
@@ -276,11 +313,12 @@ export const ProfilePage = () => {
     
     // 如果已经是最高等级
     if (level >= 5) {
+      const displaySeconds = Math.max(0, Math.floor(totalSeconds - 120));
       return {
         currentTime: 120,
         requiredTime: 120,
         progress: 100,
-        displayText: '120/120',
+        displayText: `${displaySeconds}/120`,
       };
     }
 

+ 90 - 4
frontend/src/pages/WelcomePage.tsx

@@ -2,14 +2,17 @@ import { useEffect, useRef, useState } from 'react';
 import { useNavigate, useSearchParams } from 'react-router-dom';
 import { message as antMessage } from 'antd';
 import videojs from 'video.js';
-import { setStorage, STORAGE_KEYS, markRouteVisited, getVisitedRoutes, getUserId, getStorage } from '@/utils/storage';
-import { updateUserUIState } from '@/utils/api';
+import { setStorage, STORAGE_KEYS, markRouteVisited, getVisitedRoutes, getUserId, getStorage, setUserId, setUserLevel } from '@/utils/storage';
+import { updateUserUIState, createUser, getUserInfo } from '@/utils/api';
 import 'video.js/dist/video-js.css';
 import { getOssUrl } from '@/utils/oss';
 
 const lumiImage = getOssUrl('Lumi-image.png');
 import './WelcomePage.css';
 
+// 判断是否为后端生成的默认用户名
+const isBackendGeneratedName = (name: string): boolean => /^User\d+$/.test(name.trim());
+
 // 扩展 videojs 的类型定义
 type VideoJsPlayer = ReturnType<typeof videojs>;
 
@@ -361,14 +364,97 @@ export const WelcomePage = () => {
         videoElement.style.height = '100%';
         videoElement.style.zIndex = '2';
 
-        // 用户可控的“跳过动画”按钮:进入播放阶段一段时间后才出现,避免误触
+        // 🚀 在视频播放期间预加载用户数据,避免进入 naming 页面时卡顿
+        const preloadUserData = async () => {
+          console.log('[WelcomePage] 开始预加载用户数据...');
+          try {
+            // 确保存在 userId:如果没有,则创建用户
+            let userId = getUserId();
+            if (!userId) {
+              try {
+                const response = await createUser();
+                if (response.success && response.data?.user_id) {
+                  userId = response.data.user_id;
+                  setUserId(userId);
+                  
+                  // 保存用户名到 localStorage
+                  if (response.data.username) {
+                    if (!isBackendGeneratedName(response.data.username)) {
+                      setStorage(STORAGE_KEYS.USER_NAME, response.data.username);
+                    }
+                  }
+                  
+                  if (typeof response.data.user_level === 'number') {
+                    setUserLevel(response.data.user_level);
+                  }
+                  
+                  console.log('[WelcomePage] ✅ 预加载:创建用户成功', userId);
+                }
+              } catch (error) {
+                console.error('[WelcomePage] 预加载:创建用户失败', error);
+              }
+            }
+
+            // 如果 localStorage 中没有保存的用户名,从后端获取
+            const savedName = getStorage(STORAGE_KEYS.USER_NAME);
+            if (!savedName) {
+              const user_id = userId || getUserId();
+              if (user_id) {
+                try {
+                  const response = await getUserInfo(user_id);
+                  if (response.success && response.data) {
+                    const { username, bot_name } = response.data;
+                    
+                    if (typeof response.data.user_level === 'number') {
+                      setUserLevel(response.data.user_level);
+                    }
+                    
+                    // 保存用户名到 localStorage
+                    if (username) {
+                      if (!isBackendGeneratedName(username)) {
+                        setStorage(STORAGE_KEYS.USER_NAME, username);
+                      }
+                    }
+                    
+                    // 保存 bot_name 到 localStorage
+                    if (bot_name) {
+                      setStorage(STORAGE_KEYS.LUMI_NAME, bot_name);
+                    }
+                    
+                    console.log('[WelcomePage] ✅ 预加载:获取用户信息成功');
+                  }
+                } catch (error) {
+                  console.error('[WelcomePage] 预加载:获取用户信息失败', error);
+                }
+              }
+            }
+
+            // 更新用户 UI 状态(非阻塞)
+            if (userId) {
+              const visited = getVisitedRoutes();
+              updateUserUIState(userId, {
+                current_page: 'naming',
+                visited_routes: visited,
+              }).catch(() => {
+                // 同步失败不影响前端流程
+              });
+            }
+          } catch (error) {
+            console.error('[WelcomePage] 预加载用户数据异常:', error);
+          }
+        };
+        
+        // 立即开始预加载(不阻塞视频播放)
+        preloadUserData();
+
+        // 用户可控的"跳过动画"按钮:进入播放阶段一段时间后才出现,避免误触
         setShowSkipButton(false);
         const skipTimer = setTimeout(() => {
           if (!hasNavigatedRef.current && viewStateRef.current === 'playing') {
             console.log('[WelcomePage] 播放时间较长,展示跳过动画按钮');
             setShowSkipButton(true);
           }
-        }, 20000); // 播放 20 秒后才出现“跳过动画”
+        }, 20000); // 播放 20 秒后才出现"跳过动画"
         (player as any)._skipTimer = skipTimer;
 
         // 🚀 统一的安全跳转函数,避免多次 navigate