|
@@ -18,6 +18,8 @@ import {
|
|
|
getVisitedRoutes,
|
|
getVisitedRoutes,
|
|
|
setUserLevel,
|
|
setUserLevel,
|
|
|
getCurrentBackgroundImageUrl,
|
|
getCurrentBackgroundImageUrl,
|
|
|
|
|
+ getCurrentBackgroundImageId,
|
|
|
|
|
+ getCurrentBackgroundImageOpacity,
|
|
|
syncBackgroundImageToStorage,
|
|
syncBackgroundImageToStorage,
|
|
|
} from '@/utils/storage';
|
|
} from '@/utils/storage';
|
|
|
import { onScreenResize, calculateLayout, type CalculatedLayout, updateTooSmallFlag } from '@/utils/screenSize';
|
|
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 '@/styles/chatbot-fix.css';
|
|
|
import avatarC from '@/assets/Avatar-C.svg';
|
|
import avatarC from '@/assets/Avatar-C.svg';
|
|
|
import avatarGift from '@/assets/avatar-gift.png';
|
|
import avatarGift from '@/assets/avatar-gift.png';
|
|
|
-import treesImage from '@/assets/Group-3465183.svg';
|
|
|
|
|
import audioButton from '@/assets/audio-button.png';
|
|
import audioButton from '@/assets/audio-button.png';
|
|
|
import avatarHint from '@/assets/Avatar-hint.png';
|
|
import avatarHint from '@/assets/Avatar-hint.png';
|
|
|
import { AudioWave, type AudioWaveHandle } from '@/components/AudioWave';
|
|
import { AudioWave, type AudioWaveHandle } from '@/components/AudioWave';
|
|
@@ -115,6 +116,10 @@ export const ChatBotPage = () => {
|
|
|
const url = getCurrentBackgroundImageUrl();
|
|
const url = getCurrentBackgroundImageUrl();
|
|
|
return url || null;
|
|
return url || null;
|
|
|
});
|
|
});
|
|
|
|
|
+ // 背景图片 ID
|
|
|
|
|
+ const [backgroundImageId, setBackgroundImageId] = useState<string | null>(() => {
|
|
|
|
|
+ return getCurrentBackgroundImageId();
|
|
|
|
|
+ });
|
|
|
// 初始化时检查 localStorage,确保第一次打开时立即显示提示
|
|
// 初始化时检查 localStorage,确保第一次打开时立即显示提示
|
|
|
const [showTapToChat, setShowTapToChat] = useState(() => {
|
|
const [showTapToChat, setShowTapToChat] = useState(() => {
|
|
|
const hasShown = getStorage(STORAGE_KEYS.HAS_SHOWN_TAP_TO_CHAT);
|
|
const hasShown = getStorage(STORAGE_KEYS.HAS_SHOWN_TAP_TO_CHAT);
|
|
@@ -214,10 +219,16 @@ export const ChatBotPage = () => {
|
|
|
// GIF 切换定时器
|
|
// GIF 切换定时器
|
|
|
const emotionGifTimerRef = useRef<number | null>(null);
|
|
const emotionGifTimerRef = useRef<number | null>(null);
|
|
|
const handleAvatarClick = useCallback(() => {
|
|
const handleAvatarClick = useCallback(() => {
|
|
|
|
|
+ if (!isAvatarCUnlocked) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
const isLoggedIn = getStorage(STORAGE_KEYS.IS_LOGGED_IN) === 'true';
|
|
const isLoggedIn = getStorage(STORAGE_KEYS.IS_LOGGED_IN) === 'true';
|
|
|
- if (isLoggedIn || !isAvatarCUnlocked) {
|
|
|
|
|
|
|
+ if (isLoggedIn) {
|
|
|
|
|
+ // 已登录:跳转到 profile 界面
|
|
|
|
|
+ navigate('/profile');
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
+ // 未登录:根据用户等级跳转
|
|
|
if (userLevelValue >= 5) {
|
|
if (userLevelValue >= 5) {
|
|
|
levelRedirectRef.current = true;
|
|
levelRedirectRef.current = true;
|
|
|
navigate('/level5-welcome');
|
|
navigate('/level5-welcome');
|
|
@@ -600,22 +611,43 @@ export const ChatBotPage = () => {
|
|
|
return cleanup;
|
|
return cleanup;
|
|
|
}, []);
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
+ // 同步背景图片状态的函数
|
|
|
|
|
+ const syncBackgroundImageState = useCallback(() => {
|
|
|
|
|
+ const bgUrl = getCurrentBackgroundImageUrl();
|
|
|
|
|
+ const bgId = getCurrentBackgroundImageId();
|
|
|
|
|
+ // 直接更新状态,React 会自动处理重复更新
|
|
|
|
|
+ setBackgroundImageUrl(bgUrl);
|
|
|
|
|
+ setBackgroundImageId(bgId);
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
// 初始化背景图片(从 localStorage 读取)
|
|
// 初始化背景图片(从 localStorage 读取)
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
const bgUrl = getCurrentBackgroundImageUrl();
|
|
const bgUrl = getCurrentBackgroundImageUrl();
|
|
|
|
|
+ const bgId = getCurrentBackgroundImageId();
|
|
|
if (bgUrl) {
|
|
if (bgUrl) {
|
|
|
setBackgroundImageUrl(bgUrl);
|
|
setBackgroundImageUrl(bgUrl);
|
|
|
- } else {
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ if (bgId) {
|
|
|
|
|
+ setBackgroundImageId(bgId);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!bgUrl || !bgId) {
|
|
|
// 如果没有背景图片,尝试从用户信息同步
|
|
// 如果没有背景图片,尝试从用户信息同步
|
|
|
const user_id = getUserId();
|
|
const user_id = getUserId();
|
|
|
if (user_id) {
|
|
if (user_id) {
|
|
|
getUserInfo(user_id).then((userInfo) => {
|
|
getUserInfo(user_id).then((userInfo) => {
|
|
|
if (userInfo.success && userInfo.data?.current_background_image_id) {
|
|
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 newBgUrl = getCurrentBackgroundImageUrl();
|
|
|
|
|
+ const newBgId = getCurrentBackgroundImageId();
|
|
|
if (newBgUrl) {
|
|
if (newBgUrl) {
|
|
|
setBackgroundImageUrl(newBgUrl);
|
|
setBackgroundImageUrl(newBgUrl);
|
|
|
}
|
|
}
|
|
|
|
|
+ if (newBgId) {
|
|
|
|
|
+ setBackgroundImageId(newBgId);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}).catch(() => {
|
|
}).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(() => {
|
|
useEffect(() => {
|
|
|
// 记录用户当前所在页面
|
|
// 记录用户当前所在页面
|
|
@@ -728,11 +792,18 @@ export const ChatBotPage = () => {
|
|
|
}
|
|
}
|
|
|
// 同步背景图片
|
|
// 同步背景图片
|
|
|
if (userInfo.data.current_background_image_id) {
|
|
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 bgUrl = getCurrentBackgroundImageUrl();
|
|
|
|
|
+ const bgId = getCurrentBackgroundImageId();
|
|
|
if (bgUrl) {
|
|
if (bgUrl) {
|
|
|
setBackgroundImageUrl(bgUrl);
|
|
setBackgroundImageUrl(bgUrl);
|
|
|
}
|
|
}
|
|
|
|
|
+ if (bgId) {
|
|
|
|
|
+ setBackgroundImageId(bgId);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
if (totalDurationMs > 0) {
|
|
if (totalDurationMs > 0) {
|
|
|
// 根据总说话时长计算初始进度(30秒=100%)
|
|
// 根据总说话时长计算初始进度(30秒=100%)
|
|
@@ -1327,49 +1398,55 @@ export const ChatBotPage = () => {
|
|
|
return firstChar;
|
|
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();
|
|
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 (
|
|
return (
|
|
|
<div
|
|
<div
|
|
|
style={{
|
|
style={{
|
|
|
position: 'relative',
|
|
position: 'relative',
|
|
|
width: '100%',
|
|
width: '100%',
|
|
|
- minHeight: '100vh',
|
|
|
|
|
minHeight: '100dvh',
|
|
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 等比缩放 */}
|
|
{/* Avatar-C 头像 - 设计稿:top 63px, right 33px, 尺寸 40×40,使用 px2vmin 等比缩放 */}
|
|
|
<div
|
|
<div
|
|
@@ -1402,7 +1479,7 @@ export const ChatBotPage = () => {
|
|
|
/>
|
|
/>
|
|
|
)}
|
|
)}
|
|
|
<img
|
|
<img
|
|
|
- src={hasSharedAvatar ? avatarC : avatarGift}
|
|
|
|
|
|
|
+ src={avatarSrc}
|
|
|
alt="Avatar C"
|
|
alt="Avatar C"
|
|
|
className="rounded-full"
|
|
className="rounded-full"
|
|
|
style={{
|
|
style={{
|
|
@@ -1414,7 +1491,7 @@ export const ChatBotPage = () => {
|
|
|
zIndex: hasSharedAvatar ? 1 : 30,
|
|
zIndex: hasSharedAvatar ? 1 : 30,
|
|
|
}}
|
|
}}
|
|
|
/>
|
|
/>
|
|
|
- {userInitial && (
|
|
|
|
|
|
|
+ {shouldShowInitial && (
|
|
|
<div
|
|
<div
|
|
|
style={{
|
|
style={{
|
|
|
position: 'absolute',
|
|
position: 'absolute',
|
|
@@ -1430,7 +1507,7 @@ export const ChatBotPage = () => {
|
|
|
fontWeight: 600,
|
|
fontWeight: 600,
|
|
|
opacity: 0.9,
|
|
opacity: 0.9,
|
|
|
pointerEvents: 'none',
|
|
pointerEvents: 'none',
|
|
|
- zIndex: 2,
|
|
|
|
|
|
|
+ zIndex: 31,
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
{userInitial}
|
|
{userInitial}
|