|
@@ -6,7 +6,7 @@ import { setStorage, STORAGE_KEYS, markRouteVisited, getVisitedRoutes, getUserId
|
|
|
import { updateUserUIState, createUser, getUserInfo } from '@/utils/api';
|
|
import { updateUserUIState, createUser, getUserInfo } from '@/utils/api';
|
|
|
import 'video.js/dist/video-js.css';
|
|
import 'video.js/dist/video-js.css';
|
|
|
import { getOssUrl } from '@/utils/oss';
|
|
import { getOssUrl } from '@/utils/oss';
|
|
|
-import { px2vmin } from '@/utils/viewport';
|
|
|
|
|
|
|
+import { px2vmin, px2vw, px2vh } from '@/utils/viewport';
|
|
|
|
|
|
|
|
// 判断是否为后端生成的默认用户名
|
|
// 判断是否为后端生成的默认用户名
|
|
|
const isBackendGeneratedName = (name: string): boolean => /^User\d+$/.test(name.trim());
|
|
const isBackendGeneratedName = (name: string): boolean => /^User\d+$/.test(name.trim());
|
|
@@ -14,23 +14,25 @@ const isBackendGeneratedName = (name: string): boolean => /^User\d+$/.test(name.
|
|
|
// 扩展 videojs 的类型定义
|
|
// 扩展 videojs 的类型定义
|
|
|
type VideoJsPlayer = ReturnType<typeof videojs>;
|
|
type VideoJsPlayer = ReturnType<typeof videojs>;
|
|
|
|
|
|
|
|
-type WelcomeViewState = 'welcome' | 'loading' | 'playing';
|
|
|
|
|
|
|
+type WelcomeViewState = 'gif1' | 'gif2' | 'button' | 'loading' | 'playing';
|
|
|
|
|
|
|
|
export const WelcomePage = () => {
|
|
export const WelcomePage = () => {
|
|
|
const navigate = useNavigate();
|
|
const navigate = useNavigate();
|
|
|
const [searchParams] = useSearchParams();
|
|
const [searchParams] = useSearchParams();
|
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
const videoRef = useRef<HTMLVideoElement>(null);
|
|
|
const playerRef = useRef<VideoJsPlayer | null>(null);
|
|
const playerRef = useRef<VideoJsPlayer | null>(null);
|
|
|
- const [viewState, setViewState] = useState<WelcomeViewState>('welcome');
|
|
|
|
|
|
|
+ const [viewState, setViewState] = useState<WelcomeViewState>('gif1');
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
const isVideoReadyRef = useRef(false); // 视频是否已加载完成
|
|
const isVideoReadyRef = useRef(false); // 视频是否已加载完成
|
|
|
const loadingStartTimeRef = useRef<number | null>(null); // 加载开始时间
|
|
const loadingStartTimeRef = useRef<number | null>(null); // 加载开始时间
|
|
|
const userInteractionRef = useRef(false); // 用户是否已交互(用于确保声音播放)
|
|
const userInteractionRef = useRef(false); // 用户是否已交互(用于确保声音播放)
|
|
|
- const viewStateRef = useRef<WelcomeViewState>('welcome'); // 用于在回调中访问最新状态
|
|
|
|
|
|
|
+ const viewStateRef = useRef<WelcomeViewState>('gif1'); // 用于在回调中访问最新状态
|
|
|
const hasNavigatedRef = useRef(false); // 是否已经跳转到 /naming,防止重复执行
|
|
const hasNavigatedRef = useRef(false); // 是否已经跳转到 /naming,防止重复执行
|
|
|
const checkIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null); // 检查定时器
|
|
const checkIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null); // 检查定时器
|
|
|
const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // 兜底定时器
|
|
const fallbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // 兜底定时器
|
|
|
const hasShownTimeoutTipRef = useRef(false); // 是否已经提示过"响应超时"
|
|
const hasShownTimeoutTipRef = useRef(false); // 是否已经提示过"响应超时"
|
|
|
|
|
+ const gif1TimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // GIF1 定时器
|
|
|
|
|
+ const gif2TimerRef = useRef<ReturnType<typeof setTimeout> | null>(null); // GIF2 定时器
|
|
|
|
|
|
|
|
// 处理错误参数(如QQ登录时用户未注册)
|
|
// 处理错误参数(如QQ登录时用户未注册)
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
@@ -259,6 +261,34 @@ export const WelcomePage = () => {
|
|
|
viewStateRef.current = viewState;
|
|
viewStateRef.current = viewState;
|
|
|
}, [viewState]);
|
|
}, [viewState]);
|
|
|
|
|
|
|
|
|
|
+ // GIF 动画流程控制
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ // 页面打开后立即显示 leaf-one.gif
|
|
|
|
|
+ setViewState('gif1');
|
|
|
|
|
+
|
|
|
|
|
+ // 2秒后切换到 leaf-two.gif
|
|
|
|
|
+ gif1TimerRef.current = window.setTimeout(() => {
|
|
|
|
|
+ setViewState('gif2');
|
|
|
|
|
+
|
|
|
|
|
+ // 3秒后显示按钮
|
|
|
|
|
+ gif2TimerRef.current = window.setTimeout(() => {
|
|
|
|
|
+ setViewState('button');
|
|
|
|
|
+ }, 3000) as unknown as ReturnType<typeof setTimeout>;
|
|
|
|
|
+ }, 2000) as unknown as ReturnType<typeof setTimeout>;
|
|
|
|
|
+
|
|
|
|
|
+ // 清理函数
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ if (gif1TimerRef.current) {
|
|
|
|
|
+ clearTimeout(gif1TimerRef.current);
|
|
|
|
|
+ gif1TimerRef.current = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (gif2TimerRef.current) {
|
|
|
|
|
+ clearTimeout(gif2TimerRef.current);
|
|
|
|
|
+ gif2TimerRef.current = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
// 组件卸载时清理定时器
|
|
// 组件卸载时清理定时器
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
return () => {
|
|
return () => {
|
|
@@ -577,7 +607,7 @@ export const WelcomePage = () => {
|
|
|
}
|
|
}
|
|
|
}, [navigate, viewState]); // 当播放状态改变时,确保事件已绑定
|
|
}, [navigate, viewState]); // 当播放状态改变时,确保事件已绑定
|
|
|
|
|
|
|
|
- const handleHelloClick = async () => {
|
|
|
|
|
|
|
+ const handleStartButtonClick = async () => {
|
|
|
// 标记用户已交互(用于确保声音播放)
|
|
// 标记用户已交互(用于确保声音播放)
|
|
|
userInteractionRef.current = true;
|
|
userInteractionRef.current = true;
|
|
|
|
|
|
|
@@ -590,6 +620,14 @@ export const WelcomePage = () => {
|
|
|
clearTimeout(fallbackTimerRef.current);
|
|
clearTimeout(fallbackTimerRef.current);
|
|
|
fallbackTimerRef.current = null;
|
|
fallbackTimerRef.current = null;
|
|
|
}
|
|
}
|
|
|
|
|
+ if (gif1TimerRef.current) {
|
|
|
|
|
+ clearTimeout(gif1TimerRef.current);
|
|
|
|
|
+ gif1TimerRef.current = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (gif2TimerRef.current) {
|
|
|
|
|
+ clearTimeout(gif2TimerRef.current);
|
|
|
|
|
+ gif2TimerRef.current = null;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 直接进入播放状态(页面已预加载视频,无需再显示加载层)
|
|
// 直接进入播放状态(页面已预加载视频,无需再显示加载层)
|
|
|
setIsLoading(false);
|
|
setIsLoading(false);
|
|
@@ -750,57 +788,97 @@ export const WelcomePage = () => {
|
|
|
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
- {/* 欢迎界面内容 */}
|
|
|
|
|
- {viewState === 'welcome' && (
|
|
|
|
|
|
|
+ {/* GIF 动画层 */}
|
|
|
|
|
+ {(viewState === 'gif1' || viewState === 'gif2' || viewState === 'button') && (
|
|
|
<div
|
|
<div
|
|
|
style={{
|
|
style={{
|
|
|
- position: 'relative',
|
|
|
|
|
|
|
+ position: 'fixed',
|
|
|
|
|
+ top: 0,
|
|
|
|
|
+ left: 0,
|
|
|
width: '100%',
|
|
width: '100%',
|
|
|
- minHeight: 'calc(var(--vh, 1vh) * 100)',
|
|
|
|
|
- margin: '0 auto',
|
|
|
|
|
|
|
+ height: '100%',
|
|
|
zIndex: 1,
|
|
zIndex: 1,
|
|
|
|
|
+ display: 'flex',
|
|
|
|
|
+ alignItems: 'center',
|
|
|
|
|
+ justifyContent: 'center',
|
|
|
}}
|
|
}}
|
|
|
>
|
|
>
|
|
|
- {/* Hello 文字 - 可点击 */}
|
|
|
|
|
- <div
|
|
|
|
|
- onClick={handleHelloClick}
|
|
|
|
|
- style={{
|
|
|
|
|
- position: 'absolute',
|
|
|
|
|
- top: px2vmin(138),
|
|
|
|
|
- left: px2vmin(45),
|
|
|
|
|
- width: '100%',
|
|
|
|
|
- height: px2vmin(113),
|
|
|
|
|
- background: 'linear-gradient(97.3deg, rgba(245, 243, 240, 0.8) 2.21%, rgba(245, 243, 240, 0.3) 150.89%)',
|
|
|
|
|
- WebkitBackgroundClip: 'text',
|
|
|
|
|
- WebkitTextFillColor: 'transparent',
|
|
|
|
|
- backgroundClip: 'text',
|
|
|
|
|
- opacity: 1,
|
|
|
|
|
- display: 'flex',
|
|
|
|
|
- alignItems: 'center',
|
|
|
|
|
- justifyContent: 'center',
|
|
|
|
|
- fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Arial, sans-serif',
|
|
|
|
|
- fontWeight: 500,
|
|
|
|
|
- fontStyle: 'normal',
|
|
|
|
|
- fontSize: px2vmin(96),
|
|
|
|
|
- lineHeight: 1,
|
|
|
|
|
- letterSpacing: 0,
|
|
|
|
|
- textAlign: 'center',
|
|
|
|
|
- textTransform: 'capitalize',
|
|
|
|
|
- cursor: 'pointer',
|
|
|
|
|
- transition: 'opacity 0.2s ease, transform 0.2s ease',
|
|
|
|
|
- userSelect: 'none',
|
|
|
|
|
- WebkitTapHighlightColor: 'transparent',
|
|
|
|
|
- touchAction: 'manipulation',
|
|
|
|
|
- transform: 'translateZ(0)',
|
|
|
|
|
- willChange: 'transform',
|
|
|
|
|
- }}
|
|
|
|
|
- onMouseEnter={(e) => { e.currentTarget.style.opacity = '0.9'; }}
|
|
|
|
|
- onMouseLeave={(e) => { e.currentTarget.style.opacity = '1'; }}
|
|
|
|
|
- onMouseDown={(e) => { e.currentTarget.style.opacity = '0.8'; }}
|
|
|
|
|
- onMouseUp={(e) => { e.currentTarget.style.opacity = '1'; }}
|
|
|
|
|
- >
|
|
|
|
|
- Hello,
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {/* leaf-one.gif - 全屏显示 */}
|
|
|
|
|
+ {viewState === 'gif1' && (
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={getOssUrl('leaf-one.gif')}
|
|
|
|
|
+ alt="Leaf One"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ height: '100%',
|
|
|
|
|
+ objectFit: 'cover',
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* leaf-two.gif - 全屏显示 */}
|
|
|
|
|
+ {viewState === 'gif2' && (
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={getOssUrl('leaf-two.gif')}
|
|
|
|
|
+ alt="Leaf Two"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ height: '100%',
|
|
|
|
|
+ objectFit: 'cover',
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* 按钮 - 在 gif2 播放3秒后显示 */}
|
|
|
|
|
+ {viewState === 'button' && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {/* 继续显示 leaf-two.gif 作为背景 */}
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={getOssUrl('leaf-two.gif')}
|
|
|
|
|
+ alt="Leaf Two"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ position: 'absolute',
|
|
|
|
|
+ top: 0,
|
|
|
|
|
+ left: 0,
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ height: '100%',
|
|
|
|
|
+ objectFit: 'cover',
|
|
|
|
|
+ zIndex: 1,
|
|
|
|
|
+ }}
|
|
|
|
|
+ />
|
|
|
|
|
+ {/* 按钮 */}
|
|
|
|
|
+ <button
|
|
|
|
|
+ onClick={handleStartButtonClick}
|
|
|
|
|
+ style={{
|
|
|
|
|
+ position: 'absolute',
|
|
|
|
|
+ top: px2vh(616),
|
|
|
|
|
+ left: '50%',
|
|
|
|
|
+ transform: 'translateX(-50%)',
|
|
|
|
|
+ width: px2vw(163),
|
|
|
|
|
+ height: px2vmin(56),
|
|
|
|
|
+ background: 'transparent',
|
|
|
|
|
+ border: 'none',
|
|
|
|
|
+ padding: 0,
|
|
|
|
|
+ cursor: 'pointer',
|
|
|
|
|
+ zIndex: 2,
|
|
|
|
|
+ touchAction: 'manipulation',
|
|
|
|
|
+ WebkitTapHighlightColor: 'transparent',
|
|
|
|
|
+ }}
|
|
|
|
|
+ aria-label="Start your journey"
|
|
|
|
|
+ >
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={getOssUrl('start-your-journey.png')}
|
|
|
|
|
+ alt="Start your journey"
|
|
|
|
|
+ style={{
|
|
|
|
|
+ width: '100%',
|
|
|
|
|
+ height: '100%',
|
|
|
|
|
+ objectFit: 'contain',
|
|
|
|
|
+ }}
|
|
|
|
|
+ draggable={false}
|
|
|
|
|
+ />
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
|
|
|