zlwu 8 месяцев назад
Родитель
Сommit
b5978fcdd6

+ 41 - 23
frontend/src/components/ChatBotStaticShell.tsx

@@ -29,7 +29,7 @@ const diaryMenuIcon = getOssUrl('diary-icon.png');
 const luyinDefault = getOssUrl('luyin-default.png');
 const ccCloseIcon = getOssUrl('cc-close-icon.png');
 const ccOpenIcon = getOssUrl('cc-open-icon.png');
-const treesImage = getOssUrl('init1.svg');
+const treesImage = getOssUrl('init2.svg');
 const t1OverlayImage = getOssUrl('t1-overlay.png');
 const tutorialStep1AvatarGif = getOssUrl('03-Cheer.gif');
 const primaryButtonImage = getOssUrl('primary-default.png');
@@ -50,7 +50,11 @@ const tutorialStep8EnUnionImage = getOssUrl('t8-en-union.png');
 const qinmiduImage = getOssUrl('qinmidu.png');
 const cancelIcon = getOssUrl('cancel-icon.png');
 const sendIcon = getOssUrl('send-icon.png');
-const duihuaYinying = getOssUrl('duihuayinying.svg');
+// 教学步骤 6:四边对话阴影图(OSS)
+const chatBgTop = getOssUrl('chat-bg-top.svg');
+const chatBgRight = getOssUrl('chat-bg-right.svg');
+const chatBgBottom = getOssUrl('chat-bg-bottom.svg');
+const chatBgLeft = getOssUrl('chat-bg-left.svg');
 
 interface ChatBotStaticShellProps {
   step: number;
@@ -883,28 +887,42 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
       </div>
       )}
 
-      {/* 教学步骤 6:录音态阴影图(与 /chatbot 一致),放在蒙层下方 zIndex 1 */}
-      {step === 6 && duihuaYinying && (
+      {/* 教学步骤 6:四边对话阴影图(上/右/下/左),放在蒙层下方 zIndex 1 */}
+      {step === 6 && (
         <div
-          className="pointer-events-none absolute overflow-hidden"
-          style={{
-            top: 0,
-            left: 0,
-            width: '100%',
-            height: '100%',
-            zIndex: 1,
-            display: 'flex',
-            alignItems: 'center',
-          }}
+          className="pointer-events-none absolute inset-0 overflow-hidden"
+          style={{ zIndex: 1 }}
         >
+          {/* 上边 */}
           <img
-            src={duihuaYinying}
+            src={chatBgTop}
             alt=""
-            style={{
-              width: '100%',
-              height: 'auto',
-              display: 'block',
-            }}
+            className="absolute left-0 right-0 top-0 w-full object-contain object-top"
+            style={{ display: 'block' }}
+            draggable={false}
+          />
+          {/* 右边 */}
+          <img
+            src={chatBgRight}
+            alt=""
+            className="absolute right-0 top-0 bottom-0 h-full object-contain object-right"
+            style={{ display: 'block' }}
+            draggable={false}
+          />
+          {/* 下边 */}
+          <img
+            src={chatBgBottom}
+            alt=""
+            className="absolute left-0 right-0 bottom-0 w-full object-contain object-bottom"
+            style={{ display: 'block' }}
+            draggable={false}
+          />
+          {/* 左边 */}
+          <img
+            src={chatBgLeft}
+            alt=""
+            className="absolute left-0 top-0 bottom-0 h-full object-contain object-left"
+            style={{ display: 'block' }}
             draggable={false}
           />
         </div>
@@ -1435,9 +1453,9 @@ export const ChatBotStaticShell: React.FC<ChatBotStaticShellProps> = ({ step })
             style={{
               position: 'fixed',
               bottom: 0,
-              left: '50%',
-              transform: 'translateX(-50%)',
-              width: px2vw(375),
+              left: 0,
+              right: 0,
+              width: '100%',
               height: px2vmin(192),
               backgroundColor: '#34313D',
               borderTopLeftRadius: px2vmin(32),

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

@@ -59,7 +59,11 @@ const ENABLE_CHATBOT_TIPS = false;
 
 const luyinDefault = getOssUrl('luyin-default.png');
 const luyinDisable = getOssUrl('luyin-disable.png');
-const duihuaYinying = getOssUrl('duihuayinying.svg');
+// 录音态四边对话阴影图(OSS)
+const chatBgTop = getOssUrl('chat-bg-top.svg');
+const chatBgRight = getOssUrl('chat-bg-right.svg');
+const chatBgBottom = getOssUrl('chat-bg-bottom.svg');
+const chatBgLeft = getOssUrl('chat-bg-left.svg');
 const settingIcon = getOssUrl('setting-icon.png');
 const funcLockIcon = getOssUrl('func-Lock.png');
 const profileMenuIcon = getOssUrl('profile-icon.png');
@@ -75,6 +79,7 @@ const conversationEnergyStoneHint = getOssUrl('Conversation-Energy-Stone-hint.pn
 const heartFilled = getOssUrl('Heart.png');
 const heartBroken = getOssUrl('Heart_broken.png');
 const conversationEnergyStoneCountBg = getOssUrl('conversion-energy-stone-count.png');
+const qinmiduImage = getOssUrl('qinmidu.png');
 const YpImage = getOssUrl('yp.png');
 const audioWaveGif = getOssUrl('audio-wave.gif');
 import { useEnergyStones, type EnergyStoneEventPayload } from '@/hooks/useEnergyStones';
@@ -146,6 +151,7 @@ const getEmotionGifDuration = (_avatarId: string | null, emotionCategory: 'posit
 export const ChatBotPage = () => {
   const { t: tCommon } = useTranslation('common');
   const { t: tProfile } = useTranslation('profile');
+  const { t: tWelcome } = useTranslation('welcome');
   const navigate = useNavigate();
   const [searchParams] = useSearchParams();
   const location = useLocation();
@@ -234,6 +240,7 @@ export const ChatBotPage = () => {
   const [showMissionStoneModal, setShowMissionStoneModal] = useState(false);
   const [energyStoneModalType, setEnergyStoneModalType] = useState<'mission' | 'conversation'>('mission');
   const [showIntimacyModal, setShowIntimacyModal] = useState(false);
+  const [showBondLevelModal, setShowBondLevelModal] = useState(false);
   const [showFuncMenuPopup, setShowFuncMenuPopup] = useState(false);
   const [intimacyHeartCount, setIntimacyHeartCount] = useState(3);
   const [intimacyDaysSinceLastVisit, setIntimacyDaysSinceLastVisit] = useState(0);
@@ -2820,7 +2827,7 @@ export const ChatBotPage = () => {
 
   // 根据背景图片获取对应的树木装饰图片URL(/chatbot 界面统一使用默认图片,不随用户选择的新年等背景切换)
   const getTreesImage = (): string | null => {
-    return getOssUrl('init1.svg');
+    return getOssUrl('init2.svg');
   };
 
   // 根据背景图片ID获取树木装饰图片的样式配置(/chatbot 使用默认背景,透明度固定为 1)
@@ -2866,20 +2873,15 @@ export const ChatBotPage = () => {
 
       {/* 说话时的阴影蒙层:仅录音中出现,发送/取消后立即消失 */}
       {currentState === 'recording' && (
-        <img
-          src={duihuaYinying}
-          alt=""
-          className="pointer-events-none absolute"
-          style={{
-            top: 0,
-            left: 0,
-            width: '100%',
-            height: '100%',
-            objectFit: 'cover',
-            zIndex: 2,
-          }}
-          draggable={false}
-        />
+        <div
+          className="pointer-events-none absolute inset-0 overflow-hidden"
+          style={{ zIndex: 2 }}
+        >
+          <img src={chatBgTop} alt="" className="absolute left-0 right-0 top-0 w-full object-contain object-top" style={{ display: 'block' }} draggable={false} />
+          <img src={chatBgRight} alt="" className="absolute right-0 top-0 bottom-0 h-full object-contain object-right" style={{ display: 'block' }} draggable={false} />
+          <img src={chatBgBottom} alt="" className="absolute left-0 right-0 bottom-0 w-full object-contain object-bottom" style={{ display: 'block' }} draggable={false} />
+          <img src={chatBgLeft} alt="" className="absolute left-0 top-0 bottom-0 h-full object-contain object-left" style={{ display: 'block' }} draggable={false} />
+        </div>
       )}
 
       {/* 隐藏的能量石容器 - 用于 useEnergyStones hook 计算位置 */}
@@ -2912,8 +2914,21 @@ export const ChatBotPage = () => {
           zIndex: 6,
         }}
       >
-        {/* 亲密度心形展示:顶部 6,宽 58,高 14,左右居中 */}
+        {/* 亲密度心形展示:点击弹出羁绊指数说明弹窗 */}
         <div
+          role="button"
+          tabIndex={0}
+          aria-label={tWelcome('bondLevelTitle')}
+          onClick={(e) => {
+            e.stopPropagation();
+            setShowBondLevelModal(true);
+          }}
+          onKeyDown={(e) => {
+            if (e.key === 'Enter' || e.key === ' ') {
+              e.preventDefault();
+              setShowBondLevelModal(true);
+            }
+          }}
           style={{
             position: 'absolute',
             top: px2vmin(6, 'v2'),
@@ -2924,6 +2939,7 @@ export const ChatBotPage = () => {
             display: 'flex',
             alignItems: 'center',
             justifyContent: 'space-between',
+            cursor: 'pointer',
           }}
         >
           {Array.from({ length: 3 }, (_, index) => {
@@ -3646,6 +3662,105 @@ export const ChatBotPage = () => {
         stoneType={energyStoneModalType}
       />
 
+      {/* 羁绊指数底部弹窗:点击亲密度心形时展示,中英文随语言切换 */}
+      {showBondLevelModal && (
+        <>
+          <div
+            className="mission-energy-stone-modal-mask"
+            style={{
+              position: 'fixed',
+              top: 0,
+              left: 0,
+              right: 0,
+              bottom: 0,
+              zIndex: 9998,
+              animation: 'fadeIn 0.3s ease-out',
+            }}
+            onClick={() => setShowBondLevelModal(false)}
+            aria-hidden
+          />
+          <div
+            className="mission-energy-stone-modal"
+            style={{
+              position: 'fixed',
+              bottom: 0,
+              left: 0,
+              right: 0,
+              width: '100%',
+              height: px2vmin(192),
+              backgroundColor: '#34313D',
+              borderTopLeftRadius: px2vmin(32),
+              borderTopRightRadius: px2vmin(32),
+              zIndex: 9999,
+              display: 'flex',
+              flexDirection: 'column',
+              alignItems: 'center',
+              paddingTop: px2vmin(12),
+            }}
+          >
+            <div
+              style={{
+                width: px2vmin(32),
+                height: px2vmin(4),
+                backgroundColor: '#79747E',
+                borderRadius: px2vmin(4),
+              }}
+            />
+            <div
+              style={{
+                marginTop: px2vmin(54 - 12 - 4),
+                height: px2vmin(100),
+                display: 'flex',
+                flexDirection: 'row',
+                alignItems: 'center',
+                gap: px2vmin(32),
+                marginLeft: px2vmin(32),
+                marginRight: px2vmin(32),
+              }}
+            >
+              <img
+                src={qinmiduImage}
+                alt={tWelcome('bondLevelTitle')}
+                style={{
+                  width: px2vmin(80),
+                  height: px2vmin(80),
+                  flexShrink: 0,
+                }}
+              />
+              <div
+                style={{
+                  flex: 1,
+                  display: 'flex',
+                  flexDirection: 'column',
+                  justifyContent: 'center',
+                  gap: px2vmin(8),
+                }}
+              >
+                <div
+                  style={{
+                    color: '#FFFFFF',
+                    fontSize: px2vmin(20),
+                    fontWeight: 'bold',
+                    lineHeight: 1.2,
+                  }}
+                >
+                  {tWelcome('bondLevelTitle')}
+                </div>
+                <div
+                  style={{
+                    color: '#FFFFFF',
+                    fontSize: px2vmin(14),
+                    lineHeight: 1.4,
+                  }}
+                >
+                  {tWelcome('bondLevelDescription')}
+                </div>
+              </div>
+            </div>
+          </div>
+        </>
+      )}
+
       {/* 亲密度弹窗:不满 3 颗心时展示,点击「点亮」增加一颗并关闭 */}
       <IntimacyModal
         visible={showIntimacyModal}

+ 8 - 3
frontend/src/utils/storage.ts

@@ -212,7 +212,12 @@ export const getLastOnboardingRoute = (visitedRoutes?: RouteType[] | string[]):
     return '/tutorial-1';
   }
 
-  // 2. 对已经进入过教程的用户:优先从最近访问的教程页面继续
+  // 2. 用户已访问过 /chatbot(即已完成教程并进入聊天页),下次登录应直接进 /chatbot
+  if (hasChatbotVisited) {
+    return '/chatbot';
+  }
+
+  // 3. 对已经进入过教程但尚未到 chatbot 的用户:从最近访问的教程页面继续
   if (hasTutorialVisited) {
     const lastTutorial = TUTORIAL_ROUTES.filter(route => visited.includes(route)).pop();
     if (lastTutorial) {
@@ -220,7 +225,7 @@ export const getLastOnboardingRoute = (visitedRoutes?: RouteType[] | string[]):
     }
   }
 
-  // 3. 其他情况(例如全新用户),保持原有行为:按 ROUTE_ORDER 取最后访问的路由
+  // 4. 其他情况(例如全新用户),保持原有行为:按 ROUTE_ORDER 取最后访问的路由
   const last = ROUTE_ORDER.filter(route => visited.includes(route)).pop();
   return last ?? ROUTE_ORDER[0];
 };
@@ -441,7 +446,7 @@ export const loadBackgroundImageConfig = async (): Promise<void> => {
       const { getOssUrl } = await import('./oss');
       backgroundImageConfigCache = {
         default: {
-          image_url: getOssUrl('init1.svg'),
+          image_url: getOssUrl('init2.svg'),
         },
       };
     } finally {