zlwu 11 mesiacov pred
rodič
commit
c404dac954

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

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

BIN
frontend/src/assets/confirm-btn.png


+ 5 - 2
frontend/src/index.css

@@ -89,7 +89,11 @@ body[data-too-small="true"] {
 .mobile-container {
   width: 100%;
   height: 100%;
-  background: #E8E8E8;
+  background-color: #0f0f14;
+  background-image: url("/src/assets/tolan-bg.svg");
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
   display: flex;
   align-items: center;
   justify-content: center;
@@ -149,7 +153,6 @@ body[data-too-small="true"] {
 @media (min-width: 768px) {
   .mobile-container {
     padding: 40px;
-    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   }
   
   .content-card {

+ 269 - 0
frontend/src/pages/GreetingPage copy.tsx

@@ -0,0 +1,269 @@
+import { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { getStorage, setStorage, STORAGE_KEYS, markRouteVisited, getUserId } from '../utils/storage';
+import { getUserInfo, updateUsername } from '../utils/api';
+import { px2rem } from '../utils/rem';
+import AvatarZone from '../assets/Avatar-anima-zone.png';
+import NamingBg from '../assets/tolan-bg.svg';
+
+export const GreetingPage = () => {
+  const navigate = useNavigate();
+  const [userName, setUserName] = useState('');
+  const [isFocused, setIsFocused] = useState(false);
+
+  // 组件加载时从 localStorage 读取姓名,并记录当前页面
+  useEffect(() => {
+    const loadUserData = async () => {
+      // 记录用户当前所在页面
+      setStorage(STORAGE_KEYS.CURRENT_PAGE, 'naming');
+      
+      // 记录路由访问
+      markRouteVisited('/naming');
+      
+      // 检查 localStorage 中是否存在 tolan_user_name
+      const savedName = getStorage(STORAGE_KEYS.USER_NAME);
+      if (savedName) {
+        setUserName(savedName);
+      } else {
+        // 如果不存在,从后端获取用户信息
+        const user_id = getUserId();
+        if (user_id) {
+          try {
+            const response = await getUserInfo(user_id);
+            
+            if (response.success && response.data) {
+              const { username, bot_name } = response.data;
+              
+              // 将 username 存储到 tolan_user_name
+              if (username) {
+                setStorage(STORAGE_KEYS.USER_NAME, username);
+                setUserName(username);
+              }
+              
+              // 将 bot_name 存储到 tolan_lumi_name
+              if (bot_name) {
+                setStorage(STORAGE_KEYS.LUMI_NAME, bot_name);
+              }
+            }
+          } catch (error) {
+            // 获取用户信息失败,静默处理
+          }
+        }
+      }
+    };
+    
+    loadUserData();
+  }, []);
+
+  // 监听用户名变化,同步更新 localStorage
+  useEffect(() => {
+    if (userName.trim()) {
+      setStorage(STORAGE_KEYS.USER_NAME, userName.trim());
+    }
+  }, [userName]);
+
+  const handleSubmit = async () => {
+    if (userName.trim()) {
+      const trimmedName = userName.trim();
+      
+      // 将名称存入到 tolan_user_name
+      setStorage(STORAGE_KEYS.USER_NAME, trimmedName);
+      
+      // 调用接口通过 user_id 将名称存入到 user 表的 username
+      const user_id = getUserId();
+      if (user_id) {
+        try {
+          await updateUsername(user_id, trimmedName);
+        } catch (error) {
+          // 即使后端更新失败,也继续跳转(因为 localStorage 已保存)
+        }
+      }
+      
+      // 跳转到 Lumi 命名页面
+      navigate('/lumi-naming');
+    }
+  };
+
+  const handleReplayIntro = () => {
+    navigate('/welcome');
+  };
+
+  const handleBack = () => {
+    navigate('/welcome');
+  };
+
+  return (
+    <div className="mobile-container fixed top-0 left-0 animate-fade-in">
+      <div
+        className="content-card safe-area-inset"
+        style={{
+          backgroundImage: `url(${NamingBg})`,
+          backgroundSize: 'cover',
+          backgroundPosition: 'center',
+          backgroundColor: '#25232E',
+        }}
+      >
+        {/* 返回按钮 - 响应式定位,与 Replay intro 垂直居中对齐 */}
+        <button 
+          className="absolute z-10 flex items-center justify-center 
+                     rounded-full transition-colors touch-manipulation
+                     hover:bg-[rgba(255,255,255,0.16)] active:bg-[rgba(255,255,255,0.24)]"
+          style={{
+            top: px2rem(56),
+            left: px2rem(16),
+            width: px2rem(38),
+            height: px2rem(38),
+            // minWidth: '38px',
+            // minHeight: '38px',
+          }}
+          onClick={handleBack}
+          aria-label="返回"
+        >
+          <svg 
+            width="20" 
+            height="20" 
+            viewBox="0 0 24 24" 
+            fill="none"
+          >
+            <path d="M14 17L9.7 12L14 7" stroke="#FFFFFF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
+          </svg>
+        </button>
+
+        {/* Replay intro 文本 - 响应式定位,与返回按钮垂直居中对齐 */}
+        <button 
+          className="absolute z-10 flex items-center text-fluid-xs font-normal tracking-[0.4px]
+                     text-white transition-opacity hover:opacity-70 active:opacity-50"
+          style={{
+            top: px2rem(56),
+            left: px2rem(58),
+            height: px2rem(38),
+            lineHeight: px2rem(16),
+          }}
+          onClick={handleReplayIntro}
+        >
+          Replay intro
+        </button>
+
+        {/* 标题区域 - 响应式定位和字体 */}
+        <div className="absolute"
+             style={{
+               top: px2rem(200),
+               left: px2rem(32),
+              //  right: px2rem(32),
+             }}>
+          <h1 className="font-medium text-white mb-1"
+              style={{
+                fontSize: px2rem(31),
+                lineHeight: px2rem(50),
+              }}>
+            What is your name?
+          </h1>
+          <p className="text-fluid-base font-medium text-white"
+             style={{
+               lineHeight: px2rem(24),
+               letterSpacing: '0.15px',
+             }}>
+            我应该怎么称呼你呢?
+          </p>
+        </div>
+
+        {/* 输入区域 - 响应式定位和尺寸 */}
+        <div className="absolute"
+             style={{
+               top: px2rem(300),
+               left: px2rem(32),
+               right: px2rem(32),
+             }}>
+          <div className="relative w-full">
+            <div className="relative w-full bg-black rounded-t-[4px] overflow-visible"
+                 style={{
+                   height: px2rem(56),
+                 }}>
+              <input
+                type="text"
+                className="w-full h-full text-fluid-sm font-normal leading-normal tracking-[0.5px]
+                           text-[rgba(245,243,240,0.4)] bg-transparent border-none outline-none
+                           placeholder:text-[rgba(25,24,15,0.4)]"
+              style={{ 
+                paddingLeft: px2rem(24), 
+                paddingRight: px2rem(48),
+                borderTopLeftRadius: '12px',
+                borderTopRightRadius: '12px',
+                borderBottomRightRadius: '2px',
+                borderBottomLeftRadius: '2px',
+                boxShadow: '0px 1px 0px 0px rgba(255, 255, 255, 0.19), inset -2px 2px 2px 0px rgba(0, 0, 0, 0.11)'
+              }}
+                placeholder="Coco"
+                value={userName}
+                onChange={(e) => setUserName(e.target.value)}
+                onFocus={() => setIsFocused(true)}
+                onBlur={() => setIsFocused(false)}
+                onKeyPress={(e) => {
+                  if (e.key === 'Enter') {
+                    handleSubmit();
+                  }
+                }}
+              />
+              {userName && (
+                <button 
+                  className="absolute top-1/2 -translate-y-1/2 
+                             flex items-center justify-center
+                             transition-opacity hover:opacity-70 touch-manipulation"
+                  style={{
+                    right: px2rem(12),
+                    width: px2rem(20),
+                    height: px2rem(20),
+                  }}
+                  onClick={() => setUserName('')}
+                  aria-label="清除"
+                >
+                  <svg 
+                    width="20" 
+                    height="20" 
+                    viewBox="0 0 24 24" 
+                    fill="none"
+                  >
+                    <circle cx="12" cy="12" r="11" stroke="rgba(191, 157, 248, 1)" strokeWidth="2.5" fill="none"/>
+                    <path d="M8 8L16 16M16 8L8 16" stroke="rgba(191, 157, 248, 1)" strokeWidth="2.5" strokeLinecap="round"/>
+                  </svg>
+                </button>
+              )}
+            </div>
+            {/* 输入框底部边框 - 聚焦时显示渐变 - 响应式高度 */}
+            <div 
+              className="absolute top-full left-0 w-full rounded-b-[4px] transition-opacity duration-300"
+              style={{
+                height: '2px',
+                background: isFocused 
+                  ? 'linear-gradient(270deg, #F67B7C 0%, #FFBEF4 29.64%, #A699FE 49.41%, #84CCF8 74.11%, #FFDD5E 98.81%)'
+                  : 'var(--Schemes-Primary, rgba(103, 80, 164, 1))',
+                opacity: isFocused ? 1 : 1
+              }}
+            ></div>
+          </div>
+        </div>
+
+        {/* 角色头像区域 - 响应式定位 */}
+        <div
+          className="absolute flex items-center justify-center pointer-events-none"
+          style={{
+            top: px2rem(300),
+            bottom: px2rem(80),
+            left: px2rem(16),
+            right: px2rem(16),
+          }}
+        >
+          <img
+            src={AvatarZone}
+            alt="Character Avatar"
+            className="w-full h-full object-contain object-[center_bottom]"
+            style={{
+              maxHeight: '50vh',
+            }}
+          />
+        </div>
+      </div>
+    </div>
+  );
+};
+

+ 155 - 75
frontend/src/pages/GreetingPage.tsx

@@ -1,14 +1,17 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { getStorage, setStorage, STORAGE_KEYS, markRouteVisited, getUserId } from '../utils/storage';
 import { getUserInfo, updateUsername } from '../utils/api';
 import { px2rem } from '../utils/rem';
-import LumiImage from '../assets/lumi.png';
+import AvatarZone from '../assets/Avatar-anima-zone.png';
+import NamingBg from '../assets/tolan-bg.svg';
+import ConfirmBtnImage from '../assets/confirm-btn.png';
 
 export const GreetingPage = () => {
   const navigate = useNavigate();
   const [userName, setUserName] = useState('');
   const [isFocused, setIsFocused] = useState(false);
+  const [isCompactLayout, setIsCompactLayout] = useState(false);
 
   // 组件加载时从 localStorage 读取姓名,并记录当前页面
   useEffect(() => {
@@ -84,21 +87,59 @@ export const GreetingPage = () => {
   };
 
   const handleReplayIntro = () => {
-    navigate('/video');
+    navigate('/welcome');
   };
 
   const handleBack = () => {
-    navigate('/video');
+    navigate('/welcome');
   };
 
+  const updateLayoutMode = useCallback(() => {
+    if (typeof window === 'undefined') {
+      return;
+    }
+    const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
+    setIsCompactLayout(viewportHeight < 720);
+  }, []);
+
+  useEffect(() => {
+    updateLayoutMode();
+    if (typeof window === 'undefined') {
+      return;
+    }
+
+    window.addEventListener('resize', updateLayoutMode);
+    const visualViewport = window.visualViewport;
+    visualViewport?.addEventListener('resize', updateLayoutMode);
+
+    return () => {
+      window.removeEventListener('resize', updateLayoutMode);
+      visualViewport?.removeEventListener('resize', updateLayoutMode);
+    };
+  }, [updateLayoutMode]);
+
+  const contentPaddingTop = isCompactLayout ? px2rem(110) : px2rem(180);
+  const sectionGap = isCompactLayout ? px2rem(28) : px2rem(40);
+  const avatarMaxHeight = isCompactLayout ? '38vh' : '50vh';
+  const avatarMinHeight = isCompactLayout ? px2rem(160) : px2rem(220);
+  const avatarPaddingBottom = isCompactLayout ? px2rem(24) : px2rem(40);
+
   return (
     <div className="mobile-container fixed top-0 left-0 animate-fade-in">
-      <div className="content-card safe-area-inset">
+      <div
+        className="content-card safe-area-inset relative flex flex-col min-h-screen overflow-y-auto"
+        style={{
+          backgroundImage: `url(${NamingBg})`,
+          backgroundSize: 'cover',
+          backgroundPosition: 'center',
+          backgroundColor: '#25232E',
+        }}
+      >
         {/* 返回按钮 - 响应式定位,与 Replay intro 垂直居中对齐 */}
         <button 
           className="absolute z-10 flex items-center justify-center 
                      rounded-full transition-colors touch-manipulation
-                     hover:bg-[rgba(25,24,15,0.08)] active:bg-[rgba(25,24,15,0.12)]"
+                     hover:bg-[rgba(255,255,255,0.16)] active:bg-[rgba(255,255,255,0.24)]"
           style={{
             top: px2rem(56),
             left: px2rem(16),
@@ -116,14 +157,14 @@ export const GreetingPage = () => {
             viewBox="0 0 24 24" 
             fill="none"
           >
-            <path d="M14 17L9.7 12L14 7" stroke="#19180F" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
+            <path d="M14 17L9.7 12L14 7" stroke="#FFFFFF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
           </svg>
         </button>
 
         {/* Replay intro 文本 - 响应式定位,与返回按钮垂直居中对齐 */}
         <button 
           className="absolute z-10 flex items-center text-fluid-xs font-normal tracking-[0.4px]
-                     text-black transition-opacity hover:opacity-70 active:opacity-50"
+                     text-white transition-opacity hover:opacity-70 active:opacity-50"
           style={{
             top: px2rem(56),
             left: px2rem(58),
@@ -135,63 +176,74 @@ export const GreetingPage = () => {
           Replay intro
         </button>
 
-        {/* 标题区域 - 响应式定位和字体 */}
-        <div className="absolute"
-             style={{
-               top: px2rem(140),
-               left: px2rem(32),
-              //  right: px2rem(32),
-             }}>
-          <h1 className="font-medium text-black mb-1"
+        <div
+          className="flex flex-col flex-1 overflow-hidden"
+          style={{
+            paddingLeft: px2rem(32),
+            paddingRight: px2rem(32),
+            paddingTop: contentPaddingTop,
+            paddingBottom: px2rem(32),
+            gap: sectionGap,
+          }}
+        >
+          {/* 标题区域 */}
+          <div>
+            <h1
+              className="font-medium text-white mb-1"
               style={{
-                fontSize: px2rem(31),
-                lineHeight: px2rem(50),
-              }}>
-            What is your name?
-          </h1>
-          <p className="text-fluid-base font-medium text-black"
-             style={{
-               lineHeight: px2rem(24),
-               letterSpacing: '0.15px',
-             }}>
-            我应该怎么称呼你呢?
-          </p>
-        </div>
+                fontSize: isCompactLayout ? px2rem(26) : px2rem(31),
+                lineHeight: isCompactLayout ? px2rem(40) : px2rem(50),
+              }}
+            >
+              What is your name?
+            </h1>
+            <p
+              className="text-fluid-base font-medium text-white"
+              style={{
+                lineHeight: px2rem(24),
+                letterSpacing: '0.15px',
+              }}
+            >
+              我应该怎么称呼你呢?
+            </p>
+          </div>
 
-        {/* 输入区域 - 响应式定位和尺寸 */}
-        <div className="absolute"
-             style={{
-               top: px2rem(230),
-               left: px2rem(32),
-               right: px2rem(32),
-             }}>
-          <div className="relative w-full">
-            <div className="relative w-full bg-white rounded-t-[4px] overflow-visible"
-                 style={{
-                   height: px2rem(56),
-                 }}>
+          {/* 输入区域 */}
+          <div className="w-full">
+            <div
+              className="relative w-full bg-black rounded-t-[4px] overflow-visible"
+              style={{
+                height: px2rem(56),
+                borderTopLeftRadius: '12px',
+                borderTopRightRadius: '12px',
+                borderBottomRightRadius: '2px',
+                borderBottomLeftRadius: '2px',
+                boxShadow:
+                  '0px 1px 0px 0px rgba(255, 255, 255, 0.19), inset -2px 2px 2px 0px rgba(0, 0, 0, 0.11)',
+              }}
+            >
               <input
                 type="text"
                 className="w-full h-full text-fluid-sm font-normal leading-normal tracking-[0.5px]
-                           text-black bg-transparent border-none outline-none
+                           text-[rgba(245,243,240,0.4)] bg-transparent border-none outline-none
                            placeholder:text-[rgba(25,24,15,0.4)]"
-                style={{ 
-                  paddingLeft: px2rem(24), 
-                  paddingRight: px2rem(48)
+                style={{
+                  paddingLeft: px2rem(24),
+                  paddingRight: px2rem(48),
                 }}
                 placeholder="Coco"
                 value={userName}
                 onChange={(e) => setUserName(e.target.value)}
                 onFocus={() => setIsFocused(true)}
                 onBlur={() => setIsFocused(false)}
-                onKeyPress={(e) => {
+                onKeyDown={(e) => {
                   if (e.key === 'Enter') {
                     handleSubmit();
                   }
                 }}
               />
               {userName && (
-                <button 
+                <button
                   className="absolute top-1/2 -translate-y-1/2 
                              flex items-center justify-center
                              transition-opacity hover:opacity-70 touch-manipulation"
@@ -203,48 +255,76 @@ export const GreetingPage = () => {
                   onClick={() => setUserName('')}
                   aria-label="清除"
                 >
-                  <svg 
-                    width="20" 
-                    height="20" 
-                    viewBox="0 0 24 24" 
+                  <svg
+                    width="20"
+                    height="20"
+                    viewBox="0 0 24 24"
                     fill="none"
                   >
-                    <circle cx="12" cy="12" r="11" stroke="#000000" strokeWidth="1.5" fill="none"/>
-                    <path d="M8 8L16 16M16 8L8 16" stroke="#000000" strokeWidth="1.5" strokeLinecap="round"/>
+                    <circle cx="12" cy="12" r="11" stroke="rgba(191, 157, 248, 1)" strokeWidth="2.5" fill="none" />
+                    <path
+                      d="M8 8L16 16M16 8L8 16"
+                      stroke="rgba(191, 157, 248, 1)"
+                      strokeWidth="2.5"
+                      strokeLinecap="round"
+                    />
                   </svg>
                 </button>
               )}
             </div>
-            {/* 输入框底部边框 - 聚焦时显示渐变 - 响应式高度 */}
-            <div 
-              className="absolute top-full left-0 w-full rounded-b-[4px] transition-opacity duration-300"
+            {/* 输入框底部边框 */}
+            <div
+              className="w-full rounded-b-[4px] transition-opacity duration-300"
               style={{
-                height: px2rem(2),
-                background: isFocused 
+                height: '2px',
+                background: isFocused
                   ? 'linear-gradient(270deg, #F67B7C 0%, #FFBEF4 29.64%, #A699FE 49.41%, #84CCF8 74.11%, #FFDD5E 98.81%)'
-                  : '#000000',
-                opacity: isFocused ? 1 : 1
+                  : 'var(--Schemes-Primary, rgba(103, 80, 164, 1))',
+                opacity: isFocused ? 1 : 1,
               }}
             ></div>
           </div>
-        </div>
 
-        {/* 角色头像区域 - 响应式定位 */}
-        <div className="absolute flex items-center justify-center pointer-events-none"
-             style={{
-               top: px2rem(300),
-               bottom: px2rem(80),
-               left: px2rem(16),
-               right: px2rem(16),
-             }}>
-          <img 
-            src={LumiImage} 
-            alt="Character Avatar" 
-            className="w-full h-full object-contain object-[center_bottom]"
+          {/* 角色头像区域 */}
+          <div
+            className="flex-1 flex items-center justify-center pointer-events-none"
             style={{
-              maxHeight: '50vh',
+              width: '100%',
+              maxHeight: avatarMaxHeight,
+              minHeight: avatarMinHeight,
+              paddingBottom: avatarPaddingBottom,
             }}
-          />
+          >
+            <img
+              src={AvatarZone}
+              alt="Character Avatar"
+              className="w-full h-full object-contain object-bottom"
+            />
+          </div>
+
+          {/* 确认按钮 */}
+          <div className="w-full flex justify-center items-center">
+            <button
+              type="button"
+              onClick={handleSubmit}
+              disabled={!userName.trim()}
+              className="relative text-white font-semibold tracking-[0.5px] transition-all disabled:opacity-40 disabled:cursor-not-allowed"
+              style={{
+                width: px2rem(163),
+                height: px2rem(56),
+                borderRadius: px2rem(100),
+                backgroundImage: `url(${ConfirmBtnImage})`,
+                backgroundSize: 'cover',
+                backgroundRepeat: 'no-repeat',
+                backgroundPosition: 'center',
+                boxShadow: '0px 22px 54px rgba(0, 0, 0, 0.35)',
+                display: 'flex',
+                alignItems: 'center',
+                justifyContent: 'center',
+              }}
+              aria-label="Confirm"
+            />
+          </div>
         </div>
       </div>
     </div>

+ 1 - 1
frontend/src/pages/LoadingPage.tsx

@@ -44,7 +44,7 @@ const LoadingPage: React.FC = () => {
 
   // 用户点击进入按钮
   const handleEnter = () => {
-    navigate('/video');
+    navigate('/welcome');
   };
 
   return (

+ 156 - 78
frontend/src/pages/LumiNamingPage.tsx

@@ -1,14 +1,17 @@
-import { useState, useEffect } from 'react';
+import { useState, useEffect, useCallback } from 'react';
 import { useNavigate } from 'react-router-dom';
 import { getStorage, setStorage, STORAGE_KEYS, markRouteVisited, getUserId } from '../utils/storage';
 import { updateBotName } from '../utils/api';
 import { px2rem } from '../utils/rem';
 import LumiImage from '../assets/lumi.png';
+import NamingBg from '../assets/tolan-bg.svg';
+import ConfirmBtnImage from '../assets/confirm-btn.png';
 
 export const LumiNamingPage = () => {
   const navigate = useNavigate();
   const [lumiName, setLumiName] = useState('');
   const [isFocused, setIsFocused] = useState(false);
+  const [isCompactLayout, setIsCompactLayout] = useState(false);
 
   // 组件加载时从 localStorage 读取 Lumi 姓名,并记录当前页面
   useEffect(() => {
@@ -59,78 +62,125 @@ export const LumiNamingPage = () => {
     navigate('/naming');
   };
 
+  const updateLayoutMode = useCallback(() => {
+    if (typeof window === 'undefined') {
+      return;
+    }
+    const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
+    setIsCompactLayout(viewportHeight < 720);
+  }, []);
+
+  useEffect(() => {
+    updateLayoutMode();
+    if (typeof window === 'undefined') {
+      return;
+    }
+
+    window.addEventListener('resize', updateLayoutMode);
+    const visualViewport = window.visualViewport;
+    visualViewport?.addEventListener('resize', updateLayoutMode);
+
+    return () => {
+      window.removeEventListener('resize', updateLayoutMode);
+      visualViewport?.removeEventListener('resize', updateLayoutMode);
+    };
+  }, [updateLayoutMode]);
+
+  const contentPaddingTop = isCompactLayout ? px2rem(110) : px2rem(180);
+  const sectionGap = isCompactLayout ? px2rem(28) : px2rem(40);
+  const avatarMaxHeight = isCompactLayout ? '38vh' : '50vh';
+  const avatarMinHeight = isCompactLayout ? px2rem(160) : px2rem(220);
+  const avatarPaddingBottom = isCompactLayout ? px2rem(24) : px2rem(40);
+
   return (
     <div className="mobile-container fixed top-0 left-0 animate-fade-in">
-      <div className="content-card safe-area-inset">
-        {/* 返回按钮 - 响应式定位 */}
-        <button 
+      <div
+        className="content-card safe-area-inset relative flex flex-col min-h-screen overflow-y-auto"
+        style={{
+          backgroundImage: `url(${NamingBg})`,
+          backgroundSize: 'cover',
+          backgroundPosition: 'center',
+          backgroundColor: '#25232E',
+        }}
+      >
+        {/* 返回按钮 */}
+        <button
           className="absolute z-10 flex items-center justify-center 
                      rounded-full transition-colors touch-manipulation
-                     hover:bg-[rgba(25,24,15,0.08)] active:bg-[rgba(25,24,15,0.12)]"
+                     hover:bg-[rgba(255,255,255,0.16)] active:bg-[rgba(255,255,255,0.24)]"
           style={{
             top: px2rem(56),
             left: px2rem(16),
             width: px2rem(38),
             height: px2rem(38),
-            minWidth: '38px',
-            minHeight: '38px',
           }}
           onClick={handleBack}
           aria-label="返回"
         >
-          <svg 
-            width="20" 
-            height="20" 
-            viewBox="0 0 24 24" 
+          <svg
+            width="20"
+            height="20"
+            viewBox="0 0 24 24"
             fill="none"
           >
-            <path d="M14 17L9.7 12L14 7" stroke="#19180F" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
+            <path d="M14 17L9.7 12L14 7" stroke="#FFFFFF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
           </svg>
         </button>
 
-        {/* 标题区域 - 响应式定位和字体 */}
-        <div className="absolute"
-             style={{
-               top: px2rem(140),
-               left: px2rem(32),
-              //  right: px2rem(32),
-             }}>
-          <h1 className="font-medium text-black mb-1"
+        <div
+          className="flex flex-col flex-1 overflow-hidden"
+          style={{
+            paddingLeft: px2rem(32),
+            paddingRight: px2rem(32),
+            paddingTop: contentPaddingTop,
+            paddingBottom: px2rem(32),
+            gap: sectionGap,
+          }}
+        >
+          {/* 标题区域 */}
+          <div>
+            <h1
+              className="font-medium text-white mb-1"
               style={{
-                fontSize: px2rem(31),
-                lineHeight: px2rem(50),
-              }}>
-            What is my name?
-          </h1>
-          <p className="text-fluid-base font-medium text-black"
-             style={{
-               lineHeight: px2rem(24),
-               letterSpacing: '0.15px',
-             }}>
-            给我一个地球人的名字吧!
-          </p>
-        </div>
+                fontSize: isCompactLayout ? px2rem(26) : px2rem(31),
+                lineHeight: isCompactLayout ? px2rem(40) : px2rem(50),
+              }}
+            >
+              What is my name?
+            </h1>
+            <p
+              className="text-fluid-base font-medium text-white"
+              style={{
+                lineHeight: px2rem(24),
+                letterSpacing: '0.15px',
+              }}
+            >
+              给我一个地球人的名字吧!
+            </p>
+          </div>
 
-        {/* 输入区域 - 响应式定位和尺寸 */}
-        <div className="absolute"
-             style={{
-               top: px2rem(230),
-               left: px2rem(32),
-               right: px2rem(32),
-             }}>
-          <div className="relative w-full">
-            <div className="relative w-full bg-white rounded-t-[4px] overflow-visible"
-                 style={{
-                   height: px2rem(56),
-                 }}>
+          {/* 输入区域 */}
+          <div className="w-full">
+            <div
+              className="relative w-full bg-black rounded-t-[4px] overflow-visible"
+              style={{
+                height: px2rem(56),
+                borderTopLeftRadius: '12px',
+                borderTopRightRadius: '12px',
+                borderBottomRightRadius: '2px',
+                borderBottomLeftRadius: '2px',
+                boxShadow:
+                  '0px 1px 0px 0px rgba(255, 255, 255, 0.19), inset -2px 2px 2px 0px rgba(0, 0, 0, 0.11)',
+              }}
+            >
               <input
                 type="text"
                 className="w-full h-full text-fluid-sm font-normal leading-normal tracking-[0.5px]
-                           text-black bg-transparent border-none outline-none
+                           text-[rgba(245,243,240,0.4)] bg-transparent border-none outline-none
                            placeholder:text-[rgba(25,24,15,0.4)]"
-                style={{ 
-                  paddingLeft: px2rem(24), 
-                  paddingRight: px2rem(48)
+                style={{
+                  paddingLeft: px2rem(24),
+                  paddingRight: px2rem(48),
                 }}
                 placeholder="Lumi"
                 value={lumiName}
@@ -144,7 +194,7 @@ export const LumiNamingPage = () => {
                 }}
               />
               {lumiName && (
-                <button 
+                <button
                   className="absolute top-1/2 -translate-y-1/2 
                              flex items-center justify-center
                              transition-opacity hover:opacity-70 touch-manipulation"
@@ -156,48 +206,76 @@ export const LumiNamingPage = () => {
                   onClick={() => setLumiName('')}
                   aria-label="清除"
                 >
-                  <svg 
-                    width="20" 
-                    height="20" 
-                    viewBox="0 0 24 24" 
+                  <svg
+                    width="20"
+                    height="20"
+                    viewBox="0 0 24 24"
                     fill="none"
                   >
-                    <circle cx="12" cy="12" r="11" stroke="#000000" strokeWidth="1.5" fill="none"/>
-                    <path d="M8 8L16 16M16 8L8 16" stroke="#000000" strokeWidth="1.5" strokeLinecap="round"/>
+                    <circle cx="12" cy="12" r="11" stroke="rgba(191, 157, 248, 1)" strokeWidth="2.5" fill="none" />
+                    <path
+                      d="M8 8L16 16M16 8L8 16"
+                      stroke="rgba(191, 157, 248, 1)"
+                      strokeWidth="2.5"
+                      strokeLinecap="round"
+                    />
                   </svg>
                 </button>
               )}
             </div>
-            {/* 输入框底部边框 - 聚焦时显示渐变 - 响应式高度 */}
-            <div 
-              className="absolute top-full left-0 w-full rounded-b-[4px] transition-opacity duration-300"
+            {/* 输入框底部边框 */}
+            <div
+              className="w-full rounded-b-[4px] transition-opacity duration-300"
               style={{
-                height: px2rem(2),
-                background: isFocused 
+                height: '2px',
+                background: isFocused
                   ? 'linear-gradient(270deg, #F67B7C 0%, #FFBEF4 29.64%, #A699FE 49.41%, #84CCF8 74.11%, #FFDD5E 98.81%)'
-                  : '#000000',
-                opacity: isFocused ? 1 : 1
+                  : 'var(--Schemes-Primary, rgba(103, 80, 164, 1))',
+                opacity: isFocused ? 1 : 1,
               }}
             ></div>
           </div>
-        </div>
 
-        {/* 角色头像区域 - 响应式定位 */}
-        <div className="absolute flex items-center justify-center pointer-events-none"
-             style={{
-               top: px2rem(300),
-               bottom: px2rem(80),
-               left: px2rem(16),
-               right: px2rem(16),
-             }}>
-          <img 
-            src={LumiImage} 
-            alt="Character Avatar" 
-            className="w-full h-full object-contain object-[center_bottom]"
+          {/* 角色头像区域 */}
+          <div
+            className="flex-1 flex items-center justify-center pointer-events-none"
             style={{
-              maxHeight: '50vh',
+              width: '100%',
+              maxHeight: avatarMaxHeight,
+              minHeight: avatarMinHeight,
+              paddingBottom: avatarPaddingBottom,
             }}
-          />
+          >
+            <img
+              src={LumiImage}
+              alt="Character Avatar"
+              className="w-full h-full object-contain object-bottom"
+            />
+          </div>
+
+          {/* 确认按钮 */}
+          <div className="flex justify-center pb-6">
+            <button
+              type="button"
+              onClick={handleSubmit}
+              disabled={!lumiName.trim()}
+              className="relative transition-all disabled:opacity-40 disabled:cursor-not-allowed"
+              style={{
+                width: px2rem(163),
+                height: px2rem(56),
+                borderRadius: px2rem(100),
+                backgroundImage: `url(${ConfirmBtnImage})`,
+                backgroundSize: 'cover',
+                backgroundRepeat: 'no-repeat',
+                backgroundPosition: 'center',
+                boxShadow: '0px 22px 54px rgba(0, 0, 0, 0.35)',
+                display: 'flex',
+                alignItems: 'center',
+                justifyContent: 'center',
+              }}
+              aria-label="Confirm"
+            />
+          </div>
         </div>
       </div>
     </div>

+ 0 - 3
frontend/src/pages/VideoIntroPage.tsx

@@ -402,9 +402,6 @@ export const VideoIntroPage = () => {
 
       // 监听播放结束事件
       player.on('ended', () => {
-        // 播放完毕后才记录当前页面的路由访问
-        markRouteVisited('/video');
-
         // 跳转到下一个页面(命名页)
         navigate('/naming');
       });

+ 0 - 5
frontend/src/pages/WelcomePage.tsx

@@ -107,7 +107,6 @@ export const WelcomePage = () => {
         // 绑定播放结束事件
         const handleEnded = () => {
           console.log('[WelcomePage] 视频播放完成,跳转到 /naming');
-          markRouteVisited('/video');
           navigate('/naming');
         };
         player.on('ended', handleEnded);
@@ -178,7 +177,6 @@ export const WelcomePage = () => {
     if (!(player as any)._endedHandler) {
       const handleEnded = () => {
         console.log('[WelcomePage] 视频播放完成,跳转到 /naming');
-        markRouteVisited('/video');
         navigate('/naming');
       };
       player.on('ended', handleEnded);
@@ -187,9 +185,6 @@ export const WelcomePage = () => {
   }, [navigate, viewState]); // 当播放状态改变时,确保事件已绑定
 
   const handleHelloClick = () => {
-    // 记录即将访问的路由
-    markRouteVisited('/video');
-    
     // 切换到加载状态
     setViewState('loading');
     setIsLoading(true);

+ 0 - 5
frontend/src/router/index.tsx

@@ -2,7 +2,6 @@ import { createBrowserRouter, Navigate, Outlet } from 'react-router-dom';
 import { RootRedirect } from '@/pages/RootRedirect';
 import { WelcomePage } from '@/pages/WelcomePage';
 import LoadingPage from '@/pages/LoadingPage';
-import { VideoIntroPage } from '@/pages/VideoIntroPage';
 import { GreetingPage } from '@/pages/GreetingPage';
 import { LumiNamingPage } from '@/pages/LumiNamingPage';
 import { ChatBotPage } from '@/pages/ChatBotPage';
@@ -35,10 +34,6 @@ export const router = createBrowserRouter([
         path: '/loading',
         element: <LoadingPage />,
       },
-      {
-        path: '/video',
-        element: <VideoIntroPage />,
-      },
       {
         path: '/naming',
         element: <GreetingPage />,

+ 29 - 5
frontend/src/utils/storage.ts

@@ -18,15 +18,26 @@ export const STORAGE_KEYS = {
 
 // 路由顺序表
 export const ROUTE_ORDER = [
-  '/welcome',    // 欢迎页
-  '/video',      // 视频介绍页
-  '/naming',     // 用户命名页
+  '/welcome', // 欢迎页
+  '/naming', // 用户命名页
   '/lumi-naming', // Lumi 命名页
-  '/chatbot',    // 聊天页
+  '/chatbot', // 聊天页
 ] as const;
 
 export type RouteType = typeof ROUTE_ORDER[number];
 
+const isKnownRoute = (route: unknown): route is RouteType => {
+  return typeof route === 'string' && ROUTE_ORDER.includes(route as RouteType);
+};
+
+const sanitizeVisitedRoutes = (routes: unknown): RouteType[] => {
+  if (!Array.isArray(routes)) {
+    return [];
+  }
+
+  return routes.filter(isKnownRoute);
+};
+
 /**
  * 保存数据到 localStorage
  */
@@ -109,7 +120,20 @@ export const markRouteVisited = (route: RouteType): void => {
 export const getVisitedRoutes = (): RouteType[] => {
   try {
     const data = localStorage.getItem(STORAGE_KEYS.VISITED_ROUTES);
-    return data ? JSON.parse(data) : [];
+    if (!data) {
+      return [];
+    }
+
+    const parsed = JSON.parse(data);
+    const sanitized = sanitizeVisitedRoutes(parsed);
+    const parsedLength = Array.isArray(parsed) ? parsed.length : 0;
+
+    // 如果有历史数据(例如 /video)被过滤,回写一次保持存储干净
+    if (sanitized.length !== parsedLength) {
+      localStorage.setItem(STORAGE_KEYS.VISITED_ROUTES, JSON.stringify(sanitized));
+    }
+
+    return sanitized;
   } catch (error) {
     return [];
   }