|
|
@@ -3,7 +3,8 @@ 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 { browser } from '../utils/browserDetect';
|
|
|
+import AvatarZone from '../assets/Avatar-anima-zone.png';
|
|
|
import ConfirmBtnImage from '../assets/confirm-btn.png';
|
|
|
|
|
|
export const LumiNamingPage = () => {
|
|
|
@@ -11,6 +12,17 @@ export const LumiNamingPage = () => {
|
|
|
const [lumiName, setLumiName] = useState('');
|
|
|
const [isFocused, setIsFocused] = useState(false);
|
|
|
const [isCompactLayout, setIsCompactLayout] = useState(false);
|
|
|
+
|
|
|
+ // 检测是否为移动端 Chrome(需要在 updateLayoutMode 之前定义)
|
|
|
+ const isMobileChrome = browser.isMobile && browser.isChrome && browser.isAndroid;
|
|
|
+
|
|
|
+ // 动态计算视口高度(兼容所有浏览器)
|
|
|
+ const [viewportHeight, setViewportHeight] = useState(() => {
|
|
|
+ if (typeof window === 'undefined') return 800;
|
|
|
+ // 优先使用 visualViewport,如果不可用则使用 innerHeight
|
|
|
+ // Chrome 移动端可能需要特殊处理
|
|
|
+ return window.visualViewport?.height ?? window.innerHeight;
|
|
|
+ });
|
|
|
|
|
|
// 组件加载时从 localStorage 读取 Lumi 姓名,并记录当前页面
|
|
|
useEffect(() => {
|
|
|
@@ -65,9 +77,67 @@ export const LumiNamingPage = () => {
|
|
|
if (typeof window === 'undefined') {
|
|
|
return;
|
|
|
}
|
|
|
- const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
|
|
|
- setIsCompactLayout(viewportHeight < 720);
|
|
|
- }, []);
|
|
|
+
|
|
|
+ // 获取视口高度 - 兼容所有浏览器
|
|
|
+ let height: number;
|
|
|
+ if (window.visualViewport) {
|
|
|
+ height = window.visualViewport.height;
|
|
|
+ } else {
|
|
|
+ height = window.innerHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Chrome 移动端特殊处理:如果 innerHeight 和 visualViewport 差异很大,使用 innerHeight
|
|
|
+ // 因为 Chrome 的 visualViewport 可能不准确
|
|
|
+ if (isMobileChrome && window.visualViewport) {
|
|
|
+ const innerHeight = window.innerHeight;
|
|
|
+ const visualHeight = window.visualViewport.height;
|
|
|
+ // 如果差异超过 100px,说明 visualViewport 可能不准确,使用 innerHeight
|
|
|
+ if (Math.abs(innerHeight - visualHeight) > 100) {
|
|
|
+ height = innerHeight;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ setViewportHeight(height);
|
|
|
+ setIsCompactLayout(height < 720);
|
|
|
+ }, [isMobileChrome]);
|
|
|
+
|
|
|
+ // 计算 Chrome 底部安全区域(设置合理的初始值)
|
|
|
+ const [chromeBottomPadding, setChromeBottomPadding] = useState(isMobileChrome ? 60 : 0);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (!isMobileChrome || typeof window === 'undefined') {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算底部工具栏高度
|
|
|
+ const calculateBottomPadding = () => {
|
|
|
+ const visualHeight = window.visualViewport?.height ?? window.innerHeight;
|
|
|
+ const windowHeight = window.innerHeight;
|
|
|
+ const bottomBarHeight = windowHeight - visualHeight;
|
|
|
+
|
|
|
+ // 如果检测到底部工具栏,添加额外 padding
|
|
|
+ // Chrome 底部工具栏通常约 48-56px,加上安全边距
|
|
|
+ // 限制最大 padding 为 100px,避免按钮被推出视窗
|
|
|
+ if (bottomBarHeight > 0) {
|
|
|
+ const calculatedPadding = Math.max(bottomBarHeight + 20, 60);
|
|
|
+ setChromeBottomPadding(Math.min(calculatedPadding, 100));
|
|
|
+ } else {
|
|
|
+ // 即使没有检测到,也添加基础安全边距(降低到 60px)
|
|
|
+ setChromeBottomPadding(60);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ calculateBottomPadding();
|
|
|
+
|
|
|
+ // 监听视口变化
|
|
|
+ const visualViewport = window.visualViewport;
|
|
|
+ if (visualViewport) {
|
|
|
+ visualViewport.addEventListener('resize', calculateBottomPadding);
|
|
|
+ return () => {
|
|
|
+ visualViewport.removeEventListener('resize', calculateBottomPadding);
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }, [isMobileChrome]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
updateLayoutMode();
|
|
|
@@ -85,15 +155,57 @@ export const LumiNamingPage = () => {
|
|
|
};
|
|
|
}, [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);
|
|
|
+ // 基于视口高度的动态计算(等比例缩放)
|
|
|
+ // 基准:720px 高度对应紧凑布局,900px+ 对应正常布局
|
|
|
+ const baseHeight = 900; // 正常布局的基准高度
|
|
|
+ const compactThreshold = 720; // 紧凑布局的阈值
|
|
|
+
|
|
|
+ // 计算缩放比例(0.6 - 1.0)
|
|
|
+ const scale = Math.max(0.6, Math.min(1.0, viewportHeight / baseHeight));
|
|
|
+
|
|
|
+ // 正常布局的值
|
|
|
+ const normalPaddingTop = 180;
|
|
|
+ const normalSectionGap = 40;
|
|
|
+ const normalAvatarMinHeight = 220;
|
|
|
+ const normalAvatarPaddingBottom = 40;
|
|
|
+ const normalButtonWidth = 163;
|
|
|
+ const normalButtonHeight = 56;
|
|
|
+
|
|
|
+ // 紧凑布局的值
|
|
|
+ const compactPaddingTop = 110;
|
|
|
+ const compactSectionGap = 28;
|
|
|
+ const compactAvatarMinHeight = 160;
|
|
|
+ const compactAvatarPaddingBottom = 24;
|
|
|
+ const compactButtonWidth = 130;
|
|
|
+ const compactButtonHeight = 45;
|
|
|
+
|
|
|
+ // 根据视口高度在正常和紧凑之间插值
|
|
|
+ const getInterpolatedValue = (normal: number, compact: number) => {
|
|
|
+ if (viewportHeight >= baseHeight) return normal;
|
|
|
+ if (viewportHeight <= compactThreshold) return compact;
|
|
|
+ // 线性插值
|
|
|
+ const ratio = (viewportHeight - compactThreshold) / (baseHeight - compactThreshold);
|
|
|
+ return compact + (normal - compact) * ratio;
|
|
|
+ };
|
|
|
+
|
|
|
+ const contentPaddingTop = px2rem(getInterpolatedValue(normalPaddingTop, compactPaddingTop));
|
|
|
+ const sectionGap = px2rem(getInterpolatedValue(normalSectionGap, compactSectionGap));
|
|
|
+ const avatarMinHeight = px2rem(getInterpolatedValue(normalAvatarMinHeight, compactAvatarMinHeight));
|
|
|
+ const avatarPaddingBottom = px2rem(getInterpolatedValue(normalAvatarPaddingBottom, compactAvatarPaddingBottom));
|
|
|
+ const buttonWidth = px2rem(getInterpolatedValue(normalButtonWidth, compactButtonWidth));
|
|
|
+ const buttonHeight = px2rem(getInterpolatedValue(normalButtonHeight, compactButtonHeight));
|
|
|
+
|
|
|
+ // 头像最大高度基于视口高度动态计算
|
|
|
+ const avatarMaxHeight = `${Math.max(30, Math.min(50, scale * 50))}vh`;
|
|
|
+
|
|
|
+ // 移动端 Chrome 需要额外的底部 padding,避免按钮被浏览器底部工具栏遮挡
|
|
|
+ const contentPaddingBottom = isMobileChrome
|
|
|
+ ? px2rem(chromeBottomPadding || 60) // 动态计算或使用默认值(降低到 60px)
|
|
|
+ : px2rem(32);
|
|
|
|
|
|
return (
|
|
|
<div className="mobile-container fixed top-0 left-0 animate-fade-in">
|
|
|
- <div className="content-card safe-area-inset relative flex flex-col min-h-screen overflow-y-auto">
|
|
|
+ <div className="content-card safe-area-inset relative flex flex-col h-screen overflow-hidden">
|
|
|
{/* 返回按钮 */}
|
|
|
<button
|
|
|
className="absolute z-10 flex items-center justify-center
|
|
|
@@ -119,13 +231,14 @@ export const LumiNamingPage = () => {
|
|
|
</button>
|
|
|
|
|
|
<div
|
|
|
- className="flex flex-col flex-1 overflow-hidden"
|
|
|
+ className="flex flex-col flex-1 overflow-y-auto"
|
|
|
style={{
|
|
|
paddingLeft: px2rem(32),
|
|
|
paddingRight: px2rem(32),
|
|
|
paddingTop: contentPaddingTop,
|
|
|
- paddingBottom: px2rem(32),
|
|
|
+ paddingBottom: contentPaddingBottom,
|
|
|
gap: sectionGap,
|
|
|
+ minHeight: 0, // 允许 flex 子元素缩小
|
|
|
}}
|
|
|
>
|
|
|
{/* 标题区域 */}
|
|
|
@@ -135,6 +248,8 @@ export const LumiNamingPage = () => {
|
|
|
style={{
|
|
|
fontSize: isCompactLayout ? px2rem(26) : px2rem(31),
|
|
|
lineHeight: isCompactLayout ? px2rem(40) : px2rem(50),
|
|
|
+ whiteSpace: 'nowrap',
|
|
|
+ wordBreak: 'keep-all',
|
|
|
}}
|
|
|
>
|
|
|
What is my name?
|
|
|
@@ -178,7 +293,7 @@ export const LumiNamingPage = () => {
|
|
|
onChange={(e) => setLumiName(e.target.value)}
|
|
|
onFocus={() => setIsFocused(true)}
|
|
|
onBlur={() => setIsFocused(false)}
|
|
|
- onKeyPress={(e) => {
|
|
|
+ onKeyDown={(e) => {
|
|
|
if (e.key === 'Enter') {
|
|
|
handleSubmit();
|
|
|
}
|
|
|
@@ -227,33 +342,35 @@ export const LumiNamingPage = () => {
|
|
|
></div>
|
|
|
</div>
|
|
|
|
|
|
- {/* 角色头像区域 */}
|
|
|
+ {/* 角色头像区域 - 使用 flex-shrink 确保按钮区域可见 */}
|
|
|
<div
|
|
|
- className="flex-1 flex items-center justify-center pointer-events-none"
|
|
|
+ className="flex items-center justify-center pointer-events-none"
|
|
|
style={{
|
|
|
width: '100%',
|
|
|
maxHeight: avatarMaxHeight,
|
|
|
minHeight: avatarMinHeight,
|
|
|
paddingBottom: avatarPaddingBottom,
|
|
|
+ flex: '1 1 0', // 允许缩小,确保按钮可见
|
|
|
+ overflow: 'hidden', // 防止内容溢出
|
|
|
}}
|
|
|
>
|
|
|
<img
|
|
|
- src={LumiImage}
|
|
|
+ src={AvatarZone}
|
|
|
alt="Character Avatar"
|
|
|
className="w-full h-full object-contain object-bottom"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
- {/* 确认按钮 */}
|
|
|
- <div className="flex justify-center pb-6">
|
|
|
+ {/* 确认按钮 - 固定位置,确保始终可见 */}
|
|
|
+ <div className="w-full flex justify-center items-center flex-shrink-0">
|
|
|
<button
|
|
|
type="button"
|
|
|
onClick={handleSubmit}
|
|
|
disabled={!lumiName.trim()}
|
|
|
- className="relative transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
|
+ 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),
|
|
|
+ width: buttonWidth,
|
|
|
+ height: buttonHeight,
|
|
|
borderRadius: px2rem(100),
|
|
|
backgroundImage: `url(${ConfirmBtnImage})`,
|
|
|
backgroundSize: 'cover',
|