zlwu 11 mesi fa
parent
commit
4c8d64ddad

+ 19 - 2
backend/services/user_service.py

@@ -10,7 +10,7 @@ from datetime import datetime
 from sqlalchemy import select, update, func, insert
 from sqlalchemy.ext.asyncio import AsyncSession
 from database import db_manager
-from models_db import User, UserUIState, UserBackgroundImage
+from models_db import User, UserUIState, UserBackgroundImage, BackgroundImage
 
 logger = logging.getLogger(__name__)
 
@@ -191,6 +191,7 @@ class UserService:
         
         Returns:
             用户信息字典,如果不存在返回None
+            包含 current_background_image_metadata 字段(背景图片的metadata配置)
         """
         try:
             async with db_manager.get_session() as session:
@@ -200,7 +201,23 @@ class UserService:
                 user = result.scalar_one_or_none()
             
             if user:
-                return user.to_dict()
+                user_dict = user.to_dict()
+                
+                # 如果用户有背景图片ID,查询背景图片的metadata
+                if user.current_background_image_id:
+                    try:
+                        bg_result = await session.execute(
+                            select(BackgroundImage).where(
+                                BackgroundImage.image_id == user.current_background_image_id
+                            )
+                        )
+                        bg_image = bg_result.scalar_one_or_none()
+                        if bg_image and bg_image.metadata_json:
+                            user_dict["current_background_image_metadata"] = bg_image.metadata_json
+                    except Exception as e:
+                        logger.warning(f"⚠️ 获取背景图片metadata失败: image_id={user.current_background_image_id}, error={e}")
+                
+                return user_dict
             return None
             
         except Exception as e:

+ 1 - 1
frontend/public/favicon.svg

@@ -1,5 +1,5 @@
 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
   <circle cx="50" cy="50" r="45" fill="#1890ff"/>
-  <text x="50" y="70" font-size="60" font-weight="bold" fill="white" text-anchor="middle" font-family="Arial">T</text>
+  <text x="50" y="70" font-size="60" font-weight="bold" fill="white" text-anchor="middle" font-family="Arial">L</text>
 </svg>
 

File diff suppressed because it is too large
+ 3 - 0
frontend/src/assets/Group-3465183-1.svg


+ 116 - 39
frontend/src/pages/ChatBotPage.tsx

@@ -18,6 +18,8 @@ import {
   getVisitedRoutes,
   setUserLevel,
   getCurrentBackgroundImageUrl,
+  getCurrentBackgroundImageId,
+  getCurrentBackgroundImageOpacity,
   syncBackgroundImageToStorage,
 } from '@/utils/storage';
 import { onScreenResize, calculateLayout, type CalculatedLayout, updateTooSmallFlag } from '@/utils/screenSize';
@@ -28,7 +30,6 @@ import type { Message, EnergyStoneSpawnMessage } from '@/types';
 import '@/styles/chatbot-fix.css';
 import avatarC from '@/assets/Avatar-C.svg';
 import avatarGift from '@/assets/avatar-gift.png';
-import treesImage from '@/assets/Group-3465183.svg';
 import audioButton from '@/assets/audio-button.png';
 import avatarHint from '@/assets/Avatar-hint.png';
 import { AudioWave, type AudioWaveHandle } from '@/components/AudioWave';
@@ -115,6 +116,10 @@ export const ChatBotPage = () => {
     const url = getCurrentBackgroundImageUrl();
     return url || null;
   });
+  // 背景图片 ID
+  const [backgroundImageId, setBackgroundImageId] = useState<string | null>(() => {
+    return getCurrentBackgroundImageId();
+  });
   // 初始化时检查 localStorage,确保第一次打开时立即显示提示
   const [showTapToChat, setShowTapToChat] = useState(() => {
     const hasShown = getStorage(STORAGE_KEYS.HAS_SHOWN_TAP_TO_CHAT);
@@ -214,10 +219,16 @@ export const ChatBotPage = () => {
   // GIF 切换定时器
   const emotionGifTimerRef = useRef<number | null>(null);
   const handleAvatarClick = useCallback(() => {
+    if (!isAvatarCUnlocked) {
+      return;
+    }
     const isLoggedIn = getStorage(STORAGE_KEYS.IS_LOGGED_IN) === 'true';
-    if (isLoggedIn || !isAvatarCUnlocked) {
+    if (isLoggedIn) {
+      // 已登录:跳转到 profile 界面
+      navigate('/profile');
       return;
     }
+    // 未登录:根据用户等级跳转
     if (userLevelValue >= 5) {
       levelRedirectRef.current = true;
       navigate('/level5-welcome');
@@ -600,22 +611,43 @@ export const ChatBotPage = () => {
     return cleanup;
   }, []);
 
+  // 同步背景图片状态的函数
+  const syncBackgroundImageState = useCallback(() => {
+    const bgUrl = getCurrentBackgroundImageUrl();
+    const bgId = getCurrentBackgroundImageId();
+    // 直接更新状态,React 会自动处理重复更新
+    setBackgroundImageUrl(bgUrl);
+    setBackgroundImageId(bgId);
+  }, []);
+
   // 初始化背景图片(从 localStorage 读取)
   useEffect(() => {
     const bgUrl = getCurrentBackgroundImageUrl();
+    const bgId = getCurrentBackgroundImageId();
     if (bgUrl) {
       setBackgroundImageUrl(bgUrl);
-    } else {
+    }
+    if (bgId) {
+      setBackgroundImageId(bgId);
+    }
+    if (!bgUrl || !bgId) {
       // 如果没有背景图片,尝试从用户信息同步
       const user_id = getUserId();
       if (user_id) {
         getUserInfo(user_id).then((userInfo) => {
           if (userInfo.success && userInfo.data?.current_background_image_id) {
-            syncBackgroundImageToStorage(userInfo.data.current_background_image_id);
+            syncBackgroundImageToStorage(
+              userInfo.data.current_background_image_id,
+              userInfo.data.current_background_image_metadata
+            );
             const newBgUrl = getCurrentBackgroundImageUrl();
+            const newBgId = getCurrentBackgroundImageId();
             if (newBgUrl) {
               setBackgroundImageUrl(newBgUrl);
             }
+            if (newBgId) {
+              setBackgroundImageId(newBgId);
+            }
           }
         }).catch(() => {
           // 静默处理错误
@@ -624,6 +656,38 @@ export const ChatBotPage = () => {
     }
   }, []);
 
+  // 监听 localStorage 中背景图片的变化
+  useEffect(() => {
+    // 监听 storage 事件(其他标签页/窗口的变化)
+    const handleStorageChange = (e: StorageEvent) => {
+      if (e.key === STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_ID || 
+          e.key === STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_URL) {
+        syncBackgroundImageState();
+      }
+    };
+
+    // 监听页面可见性变化(当页面重新获得焦点时检查)
+    const handleVisibilityChange = () => {
+      if (!document.hidden) {
+        syncBackgroundImageState();
+      }
+    };
+
+    // 定期检查背景图片变化(作为后备机制,每5秒检查一次)
+    const intervalId = setInterval(() => {
+      syncBackgroundImageState();
+    }, 5000);
+
+    window.addEventListener('storage', handleStorageChange);
+    document.addEventListener('visibilitychange', handleVisibilityChange);
+
+    return () => {
+      window.removeEventListener('storage', handleStorageChange);
+      document.removeEventListener('visibilitychange', handleVisibilityChange);
+      clearInterval(intervalId);
+    };
+  }, [syncBackgroundImageState]);
+
   // 初始化
   useEffect(() => {
     // 记录用户当前所在页面
@@ -728,11 +792,18 @@ export const ChatBotPage = () => {
               }
               // 同步背景图片
               if (userInfo.data.current_background_image_id) {
-                syncBackgroundImageToStorage(userInfo.data.current_background_image_id);
+                syncBackgroundImageToStorage(
+              userInfo.data.current_background_image_id,
+              userInfo.data.current_background_image_metadata
+            );
                 const bgUrl = getCurrentBackgroundImageUrl();
+                const bgId = getCurrentBackgroundImageId();
                 if (bgUrl) {
                   setBackgroundImageUrl(bgUrl);
                 }
+                if (bgId) {
+                  setBackgroundImageId(bgId);
+                }
               }
               if (totalDurationMs > 0) {
                 // 根据总说话时长计算初始进度(30秒=100%)
@@ -1327,49 +1398,55 @@ export const ChatBotPage = () => {
     return firstChar;
   };
 
+  // 根据背景图片获取对应的树木装饰图片URL
+  const getTreesImage = (): string | null => {
+    // 实时从 localStorage 读取最新的背景图片URL,使用背景图片作为树木装饰
+    const currentBgUrl = getCurrentBackgroundImageUrl() || backgroundImageUrl;
+    return currentBgUrl;
+  };
+
+  // 根据背景图片ID获取树木装饰图片的样式配置
+  const getTreesImageStyle = (): React.CSSProperties => {
+    const baseStyle: React.CSSProperties = {
+      width: '100%',
+      height: '100%',
+      top: 0,
+      left: 0,
+      objectFit: 'cover',
+      zIndex: 1,
+    };
+
+    // 从 localStorage 读取透明度配置(从数据库metadata中获取)
+    const opacity = getCurrentBackgroundImageOpacity();
+    return { ...baseStyle, opacity };
+  };
+
   const userInitial = getUserInitial();
   
+  // 计算 Avatar-C 头像的显示逻辑
+  const isLoggedIn = getStorage(STORAGE_KEYS.IS_LOGGED_IN) === 'true';
+  // 图片选择:未登录或已登录且已分享 → avatarC,已登录且未分享 → avatarGift
+  const avatarSrc = (!isLoggedIn || hasSharedAvatar) ? avatarC : avatarGift;
+  // 首字母显示:未登录或已登录且已分享 → 显示,已登录且未分享 → 不显示
+  const shouldShowInitial = userInitial && (!isLoggedIn || hasSharedAvatar);
+  
   return (
     <div
       style={{
         position: 'relative',
         width: '100%',
-        minHeight: '100vh',
         minHeight: '100dvh',
       }}
     >
-      {/* 背景图片层 - 铺满整个界面 */}
-      {backgroundImageUrl && (
-        <div
-          style={{
-            position: 'fixed',
-            top: 0,
-            left: 0,
-            width: '100%',
-            height: '100%',
-            backgroundImage: `url(${backgroundImageUrl})`,
-            backgroundSize: 'cover',
-            backgroundPosition: 'center',
-            backgroundRepeat: 'no-repeat',
-            zIndex: 0,
-            pointerEvents: 'none',
-          }}
+      {/* 树木装饰 - 根据背景图片调整图片和样式 */}
+      {getTreesImage() && (
+        <img
+          src={getTreesImage() || undefined}
+          alt="Trees"
+          className="chatbot-trees absolute pointer-events-none"
+          style={getTreesImageStyle()}
         />
       )}
-      {/* 树木装饰 - 设计稿:height 220px,top 175px */}
-      <img
-        src={treesImage}
-        alt="Trees"
-        className="chatbot-trees absolute pointer-events-none"
-        style={{
-          height: px2vmin(220),  // 设计稿 height 220px,基于 vw/vh 最小值
-          top: px2vh(175),       // 设计稿 top 175px,基于 vh
-          left: '50%',
-          transform: 'translateX(-50%)',
-          opacity: 1,
-          zIndex: 1,
-        }}
-      />
 
       {/* Avatar-C 头像 - 设计稿:top 63px, right 33px, 尺寸 40×40,使用 px2vmin 等比缩放 */}
       <div
@@ -1402,7 +1479,7 @@ export const ChatBotPage = () => {
           />
         )}
         <img
-          src={hasSharedAvatar ? avatarC : avatarGift}
+          src={avatarSrc}
           alt="Avatar C"
           className="rounded-full"
           style={{
@@ -1414,7 +1491,7 @@ export const ChatBotPage = () => {
             zIndex: hasSharedAvatar ? 1 : 30,
           }}
         />
-        {userInitial && (
+        {shouldShowInitial && (
           <div
             style={{
               position: 'absolute',
@@ -1430,7 +1507,7 @@ export const ChatBotPage = () => {
               fontWeight: 600,
               opacity: 0.9,
               pointerEvents: 'none',
-              zIndex: 2,
+              zIndex: 31,
             }}
           >
             {userInitial}

+ 3 - 2
frontend/src/pages/Level34WelcomePage.tsx

@@ -1,12 +1,13 @@
 import { useEffect } from 'react';
 import { useNavigate } from 'react-router-dom';
-import { setStorage, STORAGE_KEYS, getUserId, getVisitedRoutes } from '@/utils/storage';
+import { setStorage, STORAGE_KEYS, getUserId, getVisitedRoutes, getUserLevel } from '@/utils/storage';
 import { updateUserUIState } from '@/utils/api';
 import { px2vw, px2vh, px2vmin } from '@/utils/viewport';
 import giftProgressBar from '@/assets/gift-progress-bar.png';
 
 export const Level34WelcomePage = () => {
   const navigate = useNavigate();
+  const userLevel = getUserLevel();
 
   useEffect(() => {
     // 记录用户当前所在页面
@@ -99,7 +100,7 @@ export const Level34WelcomePage = () => {
           animation: 'textSlideUpAndShakeAndFadeOut 6.4s ease-out 2s forwards',
         }}
       >
-        恭喜你成功升到4级!能坚持到3-4级真的很棒,登录后有惊喜礼物等你领取哦
+        恭喜你成功升到{userLevel}级!能坚持到{userLevel}级真的很棒,登录后有惊喜礼物等你领取哦
       </div>
       {/* 扩展圆形动画 - 设计稿:圆心(323, 147),最大半径236 */}
       <div

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

@@ -84,10 +84,13 @@ export const createUser = async (username: string = ''): Promise<CreateUserRespo
     const data = await response.json();
     console.log('[API] 创建用户成功:', data);
     
-    // 同步背景图片到 localStorage
+    // 同步背景图片到 localStorage(包括metadata中的透明度配置)
     if (data.success && data.data?.current_background_image_id) {
       const { syncBackgroundImageToStorage } = await import('./storage');
-      syncBackgroundImageToStorage(data.data.current_background_image_id);
+      syncBackgroundImageToStorage(
+        data.data.current_background_image_id,
+        data.data.current_background_image_metadata
+      );
     }
     
     return data;
@@ -116,6 +119,7 @@ export interface GetUserResponse {
     total_speech_duration_ms?: number;
     user_level?: number;
     current_background_image_id?: string;
+    current_background_image_metadata?: Record<string, any>;
     created_at: string;
   };
 }
@@ -135,10 +139,13 @@ export const getUserInfo = async (user_id: string): Promise<GetUserResponse> =>
 
   const data = await response.json();
   
-  // 同步背景图片到 localStorage
+  // 同步背景图片到 localStorage(包括metadata中的透明度配置)
   if (data.success && data.data?.current_background_image_id) {
     const { syncBackgroundImageToStorage } = await import('./storage');
-    syncBackgroundImageToStorage(data.data.current_background_image_id);
+    syncBackgroundImageToStorage(
+      data.data.current_background_image_id,
+      data.data.current_background_image_metadata
+    );
   }
   
   return data;

+ 53 - 2
frontend/src/utils/storage.ts

@@ -22,6 +22,7 @@ export const STORAGE_KEYS = {
   HAS_SHARED_AVATAR: 'Lyrria_has_shared_avatar', // 是否已分享头像奖励
   CURRENT_BACKGROUND_IMAGE_ID: 'Lyrria_current_background_image_id', // 当前背景图片ID
   CURRENT_BACKGROUND_IMAGE_URL: 'Lyrria_current_background_image_url', // 当前背景图片URL
+  CURRENT_BACKGROUND_IMAGE_OPACITY: 'Lyrria_current_background_image_opacity', // 当前背景图片透明度
 } as const;
 
 // 路由顺序表
@@ -314,7 +315,7 @@ export const clearBackgroundImage = (): void => {
  * 背景图片 URL 映射
  */
 export const BACKGROUND_IMAGE_URL_MAP: Record<string, string> = {
-  default: 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/init.jpg',
+  default: 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/init.svg',
   happynewyear: 'https://lyrria-pro.oss-cn-beijing.aliyuncs.com/happynewyear.jpg',
 };
 
@@ -328,11 +329,46 @@ export const getBackgroundImageUrlById = (image_id: string | null | undefined):
   return BACKGROUND_IMAGE_URL_MAP[image_id] || null;
 };
 
+/**
+ * 获取当前背景图片透明度
+ */
+export const getCurrentBackgroundImageOpacity = (): number => {
+  const value = getStorage(STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_OPACITY);
+  if (value === null || value === undefined) {
+    return 0.5; // 默认透明度
+  }
+  const parsed = parseFloat(value);
+  return Number.isFinite(parsed) && parsed >= 0 && parsed <= 1 ? parsed : 0.5;
+};
+
+/**
+ * 设置当前背景图片透明度
+ */
+export const setCurrentBackgroundImageOpacity = (opacity: number): boolean => {
+  if (typeof opacity !== 'number' || opacity < 0 || opacity > 1) {
+    console.warn(`[storage] 无效的透明度值: ${opacity},应在 0-1 之间`);
+    return false;
+  }
+  return setStorage(STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_OPACITY, String(opacity));
+};
+
+/**
+ * 清除当前背景图片透明度
+ */
+export const clearCurrentBackgroundImageOpacity = (): boolean => {
+  return removeStorage(STORAGE_KEYS.CURRENT_BACKGROUND_IMAGE_OPACITY);
+};
+
 /**
  * 同步背景图片信息到 localStorage
  * 根据 image_id 自动获取对应的 URL
+ * @param image_id 背景图片ID
+ * @param metadata 背景图片的metadata配置(可选,包含opacity等配置)
  */
-export const syncBackgroundImageToStorage = (image_id: string | null | undefined): void => {
+export const syncBackgroundImageToStorage = (
+  image_id: string | null | undefined,
+  metadata?: Record<string, any> | null
+): void => {
   if (!image_id) {
     // 如果没有 image_id,使用默认值
     const defaultId = 'default';
@@ -340,6 +376,8 @@ export const syncBackgroundImageToStorage = (image_id: string | null | undefined
     if (defaultUrl) {
       setBackgroundImage(defaultId, defaultUrl);
     }
+    // 设置默认透明度
+    setCurrentBackgroundImageOpacity(0.5);
     return;
   }
   
@@ -351,6 +389,19 @@ export const syncBackgroundImageToStorage = (image_id: string | null | undefined
     setCurrentBackgroundImageId(image_id);
     console.warn(`[storage] 未找到背景图片URL映射: image_id=${image_id}`);
   }
+  
+  // 如果提供了metadata,提取并存储透明度配置
+  if (metadata && typeof metadata === 'object') {
+    if (typeof metadata.opacity === 'number' && metadata.opacity >= 0 && metadata.opacity <= 1) {
+      setCurrentBackgroundImageOpacity(metadata.opacity);
+    } else {
+      // 如果没有配置透明度,使用默认值
+      setCurrentBackgroundImageOpacity(0.5);
+    }
+  } else {
+    // 如果没有metadata,使用默认透明度
+    setCurrentBackgroundImageOpacity(0.5);
+  }
 };
 
 /**

Some files were not shown because too many files changed in this diff