|
|
@@ -1,9 +1,8 @@
|
|
|
-import { useState, useEffect, useCallback, useRef } from 'react';
|
|
|
+import { useState, useEffect, useRef } 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 { browser } from '../utils/browserDetect';
|
|
|
+import { px2vw, px2vh, px2vmin } from '../utils/viewport';
|
|
|
import AvatarZone from '../assets/Avatar-anima-zone.png';
|
|
|
import ConfirmBtnImage from '../assets/confirm-btn.png';
|
|
|
|
|
|
@@ -15,7 +14,6 @@ export const GreetingPage = () => {
|
|
|
const [initialUserName, setInitialUserName] = useState('');
|
|
|
const [hasUserEdited, setHasUserEdited] = useState(false);
|
|
|
const [isFocused, setIsFocused] = useState(false);
|
|
|
- const [isCompactLayout, setIsCompactLayout] = useState(false);
|
|
|
const [placeholderName, setPlaceholderName] = useState('');
|
|
|
const [allowPlaceholderSubmit, setAllowPlaceholderSubmit] = useState(false);
|
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
|
@@ -128,360 +126,231 @@ export const GreetingPage = () => {
|
|
|
navigate('/welcome');
|
|
|
};
|
|
|
|
|
|
- // 检测是否为移动端 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;
|
|
|
- });
|
|
|
-
|
|
|
- const updateLayoutMode = useCallback(() => {
|
|
|
- if (typeof window === 'undefined') {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 获取视口高度 - 兼容所有浏览器
|
|
|
- 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();
|
|
|
- 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]);
|
|
|
-
|
|
|
- // 基于视口高度的动态计算(等比例缩放)
|
|
|
- // 基准: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 layout = {
|
|
|
+ sidePadding: 16,
|
|
|
+ backButtonTop: 72,
|
|
|
+ backButtonSize: 38,
|
|
|
+ replayButtonLeft: 58,
|
|
|
+ replayButtonHeight: 38,
|
|
|
+ replayButtonLineHeight: 16,
|
|
|
+ questionGroupTop: 200,
|
|
|
+ inputGroupTop: 300,
|
|
|
+ avatarTop: 376,
|
|
|
+ confirmButtonTop: 656,
|
|
|
+ contentWidth: 343,
|
|
|
+ avatarWidth: 343,
|
|
|
+ avatarHeight: 258,
|
|
|
+ inputHeight: 56,
|
|
|
+ confirmButtonWidth: 147,
|
|
|
+ confirmButtonHeight: 51,
|
|
|
};
|
|
|
-
|
|
|
- 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 h-screen overflow-hidden">
|
|
|
- {/* 返回按钮 - 响应式定位,与 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)]"
|
|
|
+ <div className="content-card safe-area-inset relative w-full min-h-screen overflow-hidden">
|
|
|
+ <div
|
|
|
+ className="absolute z-10 flex items-center"
|
|
|
style={{
|
|
|
- top: px2rem(56),
|
|
|
- left: px2rem(16),
|
|
|
- width: px2rem(38),
|
|
|
- height: px2rem(38),
|
|
|
- // minWidth: '38px',
|
|
|
- // minHeight: '38px',
|
|
|
+ top: px2vh(layout.backButtonTop), // 72px → vh
|
|
|
+ left: 0, // 距离左侧 0px
|
|
|
}}
|
|
|
- onClick={handleBack}
|
|
|
- aria-label="返回"
|
|
|
>
|
|
|
- <svg
|
|
|
- width="20"
|
|
|
- height="20"
|
|
|
- viewBox="0 0 24 24"
|
|
|
- fill="none"
|
|
|
+ <button
|
|
|
+ className="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={{
|
|
|
+ // 宽高 48px,使用 vw/vh 中的较小值
|
|
|
+ width: px2vmin(48),
|
|
|
+ height: px2vmin(48),
|
|
|
+ }}
|
|
|
+ onClick={handleBack}
|
|
|
+ aria-label="返回"
|
|
|
>
|
|
|
- <path d="M14 17L9.7 12L14 7" stroke="#FFFFFF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round"/>
|
|
|
- </svg>
|
|
|
- </button>
|
|
|
+ <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"
|
|
|
+ <button
|
|
|
+ className="flex items-center font-normal tracking-[0.4px] text-white transition-opacity hover:opacity-70 active:opacity-50"
|
|
|
+ style={{
|
|
|
+ height: px2vmin(48),
|
|
|
+ lineHeight: px2vmin(48),
|
|
|
+ fontSize: '14PX', // 固定字号 14px
|
|
|
+ }}
|
|
|
+ onClick={handleReplayIntro}
|
|
|
+ >
|
|
|
+ Replay intro
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div
|
|
|
+ className="absolute flex flex-col"
|
|
|
style={{
|
|
|
- top: px2rem(56),
|
|
|
- left: px2rem(58),
|
|
|
- height: px2rem(38),
|
|
|
- lineHeight: px2rem(16),
|
|
|
+ top: px2vh(layout.questionGroupTop),
|
|
|
+ left: px2vw(layout.sidePadding),
|
|
|
+ width: px2vw(layout.contentWidth),
|
|
|
}}
|
|
|
- onClick={handleReplayIntro}
|
|
|
>
|
|
|
- Replay intro
|
|
|
- </button>
|
|
|
+ <h1
|
|
|
+ className="font-medium text-white mb-1"
|
|
|
+ style={{
|
|
|
+ // 设计稿 34px,使用 vw/vh 中的较小值
|
|
|
+ fontSize: px2vmin(34),
|
|
|
+ lineHeight: px2vh(50),
|
|
|
+ height: px2vh(50),
|
|
|
+ marginBottom: '10PX',
|
|
|
+ whiteSpace: 'nowrap',
|
|
|
+ wordBreak: 'keep-all',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ What is your name ?
|
|
|
+ </h1>
|
|
|
+ <p
|
|
|
+ className="text-fluid-base font-medium text-white"
|
|
|
+ style={{
|
|
|
+ lineHeight: '1em',
|
|
|
+ marginTop: 0,
|
|
|
+ letterSpacing: '0.15px',
|
|
|
+ // 设计稿 16px,使用 vw/vh 中的较小值
|
|
|
+ fontSize: px2vmin(16),
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ 我应该怎么称呼你呢 ?
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
|
|
|
<div
|
|
|
- className="flex flex-col flex-1 overflow-y-auto"
|
|
|
+ className="absolute w-full"
|
|
|
style={{
|
|
|
- paddingLeft: px2rem(32),
|
|
|
- paddingRight: px2rem(32),
|
|
|
- paddingTop: contentPaddingTop,
|
|
|
- paddingBottom: contentPaddingBottom,
|
|
|
- gap: sectionGap,
|
|
|
- minHeight: 0, // 允许 flex 子元素缩小
|
|
|
+ top: px2vh(layout.inputGroupTop),
|
|
|
+ // 设计稿 padding: 0 16px,左右 16px 基于 vw
|
|
|
+ paddingLeft: px2vw(16),
|
|
|
+ paddingRight: px2vw(16),
|
|
|
}}
|
|
|
>
|
|
|
- {/* 标题区域 */}
|
|
|
- <div>
|
|
|
- <h1
|
|
|
- className="font-medium text-white mb-1"
|
|
|
+ <div
|
|
|
+ className="relative w-full rounded-t-[4px] overflow-visible"
|
|
|
+ style={{
|
|
|
+ height: px2vh(layout.inputHeight),
|
|
|
+ background: 'linear-gradient(180deg, #26232F 0%, #18151F 100%)',
|
|
|
+ borderTopLeftRadius: '12PX',
|
|
|
+ borderTopRightRadius: '12PX',
|
|
|
+ borderBottomRightRadius: '2PX',
|
|
|
+ borderBottomLeftRadius: '2PX',
|
|
|
+ boxShadow:
|
|
|
+ '0PX 1PX 0PX 0PX rgba(255, 255, 255, 0.18), inset 0PX 0PX 0PX 1PX rgba(0, 0, 0, 0.65), inset 0PX 4PX 8PX rgba(0, 0, 0, 0.55)',
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <input
|
|
|
+ ref={inputRef}
|
|
|
+ type="text"
|
|
|
+ className="w-full h-full font-normal leading-normal tracking-[0.5px] text-[rgba(245,243,240,0.7)] bg-transparent border-none outline-none placeholder:text-[rgba(245,243,240,0.45)]"
|
|
|
style={{
|
|
|
- fontSize: isCompactLayout ? px2rem(26) : px2rem(31),
|
|
|
- lineHeight: isCompactLayout ? px2rem(40) : px2rem(50),
|
|
|
- whiteSpace: 'nowrap',
|
|
|
- wordBreak: 'keep-all',
|
|
|
+ fontSize: '18PX',
|
|
|
+ paddingLeft: px2vw(24),
|
|
|
+ paddingRight: px2vw(48),
|
|
|
}}
|
|
|
- >
|
|
|
- What is your name?
|
|
|
- </h1>
|
|
|
- <p
|
|
|
- className="text-fluid-base font-medium text-white"
|
|
|
- style={{
|
|
|
- lineHeight: px2rem(24),
|
|
|
- letterSpacing: '0.15px',
|
|
|
+ placeholder={placeholderName || 'Coco'}
|
|
|
+ value={userName}
|
|
|
+ onChange={(e) => {
|
|
|
+ setHasUserEdited(true);
|
|
|
+ setUserName(e.target.value);
|
|
|
}}
|
|
|
- >
|
|
|
- 我应该怎么称呼你呢?
|
|
|
- </p>
|
|
|
- </div>
|
|
|
-
|
|
|
- {/* 输入区域 */}
|
|
|
- <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)',
|
|
|
+ onFocus={() => {
|
|
|
+ setIsFocused(true);
|
|
|
+ if (!hasUserEdited && userName && isBackendGeneratedName(userName)) {
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ inputRef.current?.select();
|
|
|
+ });
|
|
|
+ }
|
|
|
}}
|
|
|
- >
|
|
|
- <input
|
|
|
- ref={inputRef}
|
|
|
- type="text"
|
|
|
- className="w-full h-full text-fluid-sm font-normal leading-normal tracking-[0.5px]
|
|
|
- text-[rgba(245,243,240,0.7)] bg-transparent border-none outline-none
|
|
|
- placeholder:text-[rgba(245,243,240,0.45)]"
|
|
|
+ onBlur={() => setIsFocused(false)}
|
|
|
+ onKeyDown={(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={{
|
|
|
- paddingLeft: px2rem(24),
|
|
|
- paddingRight: px2rem(48),
|
|
|
+ right: px2vw(12),
|
|
|
+ width: px2vw(20),
|
|
|
+ height: px2vh(20),
|
|
|
}}
|
|
|
- placeholder={placeholderName || 'Coco'}
|
|
|
- value={userName}
|
|
|
- onChange={(e) => {
|
|
|
+ onClick={() => {
|
|
|
setHasUserEdited(true);
|
|
|
- setUserName(e.target.value);
|
|
|
- }}
|
|
|
- onFocus={() => {
|
|
|
- setIsFocused(true);
|
|
|
- if (!hasUserEdited && userName && isBackendGeneratedName(userName)) {
|
|
|
- requestAnimationFrame(() => {
|
|
|
- inputRef.current?.select();
|
|
|
- });
|
|
|
- }
|
|
|
- }}
|
|
|
- onBlur={() => setIsFocused(false)}
|
|
|
- onKeyDown={(e) => {
|
|
|
- if (e.key === 'Enter') {
|
|
|
- handleSubmit();
|
|
|
- }
|
|
|
+ setUserName('');
|
|
|
}}
|
|
|
- />
|
|
|
- {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={() => {
|
|
|
- setHasUserEdited(true);
|
|
|
- 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="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>
|
|
|
+ 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>
|
|
|
-
|
|
|
- {/* 角色头像区域 - 使用 flex-shrink 确保按钮区域可见 */}
|
|
|
<div
|
|
|
- className="flex items-center justify-center pointer-events-none"
|
|
|
+ className="w-full rounded-b-[4px] transition-opacity duration-300"
|
|
|
style={{
|
|
|
- width: '100%',
|
|
|
- maxHeight: avatarMaxHeight,
|
|
|
- minHeight: avatarMinHeight,
|
|
|
- paddingBottom: avatarPaddingBottom,
|
|
|
- flex: '1 1 0', // 允许缩小,确保按钮可见
|
|
|
- overflow: 'hidden', // 防止内容溢出
|
|
|
+ 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))',
|
|
|
}}
|
|
|
- >
|
|
|
- <img
|
|
|
- src={AvatarZone}
|
|
|
- alt="Character Avatar"
|
|
|
- className="w-full h-full object-contain object-bottom"
|
|
|
- />
|
|
|
- </div>
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
|
|
|
- {/* 确认按钮 - 固定位置,确保始终可见 */}
|
|
|
- <div className="w-full flex justify-center items-center flex-shrink-0">
|
|
|
- <button
|
|
|
- type="button"
|
|
|
- onClick={handleSubmit}
|
|
|
- disabled={!userName.trim() && !(allowPlaceholderSubmit && placeholderName.trim())}
|
|
|
- className="relative text-white font-semibold tracking-[0.5px] transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
|
- style={{
|
|
|
- width: buttonWidth,
|
|
|
- height: buttonHeight,
|
|
|
- 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
|
|
|
+ className="absolute"
|
|
|
+ style={{
|
|
|
+ top: px2vh(layout.avatarTop),
|
|
|
+ left: '50%',
|
|
|
+ transform: 'translateX(-50%)',
|
|
|
+ width: px2vmin(layout.avatarWidth),
|
|
|
+ height: px2vmin(layout.avatarHeight),
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ src={AvatarZone}
|
|
|
+ alt="Character Avatar"
|
|
|
+ className="w-full h-full object-contain object-bottom"
|
|
|
+ style={{ width: '100%', height: '100%' }}
|
|
|
+ draggable={false}
|
|
|
+ />
|
|
|
</div>
|
|
|
+
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ onClick={handleSubmit}
|
|
|
+ disabled={!userName.trim() && !(allowPlaceholderSubmit && placeholderName.trim())}
|
|
|
+ className="absolute text-white font-semibold tracking-[0.5px] transition-all disabled:opacity-40 disabled:cursor-not-allowed"
|
|
|
+ style={{
|
|
|
+ top: px2vh(layout.confirmButtonTop),
|
|
|
+ left: '50%',
|
|
|
+ transform: 'translateX(-50%)',
|
|
|
+ // 设计稿:宽 163、高 56,使用 vw/vh 中的较小值
|
|
|
+ width: px2vmin(163),
|
|
|
+ height: px2vmin(56),
|
|
|
+ borderRadius: px2vw(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>
|
|
|
);
|