video-shuoshan/web/src/pages/LandingPage.tsx
seaislee1209 e2973284d0
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m20s
feat: 账号安全管控 + 内容资产页 + UI修缮 (v0.9.5 & v0.9.6)
v0.9.5 — 账号安全管控 + 内容资产页:
- 首次登录强制改密(must_change_password + ForceChangePasswordModal)
- 并发会话限制(ActiveSession + SessionJWT认证,可配置桌面/移动端会话数)
- Token生命周期缩短(access 30min, refresh 1天)
- 登录IP记录(LoginRecord模型,为异常检测打基础)
- 内容资产页(超管三级折叠/团队管两级折叠,按需懒加载)

v0.9.6 — UI修缮:
- 侧栏导航排序(内容资产移到用户管理下方)
- 视频网格高度调整(440px,3行+暗示可滚动)
- 秒数单位统一(不再换算为分钟/小时)
- 提示词标签溢出修复 + 弹窗方向自适应

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 12:02:54 +08:00

195 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useAuthStore } from '../store/auth';
import { AuroraCanvas } from '../components/AuroraCanvas';
import { LoginModal } from '../components/LoginModal';
import { ForceChangePasswordModal } from '../components/ForceChangePasswordModal';
import logoImg from '../assets/logo_512.png';
import styles from './LandingPage.module.css';
interface Props {
autoLogin?: boolean;
}
export function LandingPage({ autoLogin }: Props) {
const navigate = useNavigate();
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const mustChangePassword = useAuthStore((s) => s.mustChangePassword);
const [showLogin, setShowLogin] = useState(false);
const [showForceChange, setShowForceChange] = useState(false);
const [showSpark, setShowSpark] = useState(false);
const [playing, setPlaying] = useState(false);
const audioRef = useRef<HTMLAudioElement | null>(null);
const [barHeights, setBarHeights] = useState([0.4, 0.4, 0.4, 0.4]);
// Auto-open login modal when rendered via /login route
useEffect(() => {
if (autoLogin && !isAuthenticated) {
setShowLogin(true);
}
}, [autoLogin, isAuthenticated]);
// Auto-show force change password modal if authenticated but must change
useEffect(() => {
if (isAuthenticated && mustChangePassword) {
setShowForceChange(true);
}
}, [isAuthenticated, mustChangePassword]);
const handleAirDrama = () => {
if (isAuthenticated) {
if (mustChangePassword) {
setShowForceChange(true);
} else {
navigate('/app');
}
} else {
setShowLogin(true);
}
};
const handleAirSpark = () => {
setShowSpark(true);
};
const handleLoginSuccess = () => {
setShowLogin(false);
if (mustChangePassword) {
setShowForceChange(true);
} else {
navigate('/app', { replace: true });
}
};
const handleForceChangeSuccess = () => {
setShowForceChange(false);
navigate('/app', { replace: true });
};
// Close spark overlay on click
const closeSpark = useCallback(() => setShowSpark(false), []);
// Animate bar heights when playing
useEffect(() => {
if (!playing) {
setBarHeights([0.4, 0.4, 0.4, 0.4]);
return;
}
const id = setInterval(() => {
setBarHeights([
0.3 + Math.random() * 0.7,
0.3 + Math.random() * 0.7,
0.3 + Math.random() * 0.7,
0.3 + Math.random() * 0.7,
]);
}, 150);
return () => clearInterval(id);
}, [playing]);
// Stop music on unmount (e.g. navigating to /app after login)
useEffect(() => {
return () => {
if (audioRef.current) {
audioRef.current.pause();
audioRef.current.currentTime = 0;
}
};
}, []);
// Music toggle
const toggleMusic = useCallback(() => {
if (!audioRef.current) {
audioRef.current = new Audio('/bgm.mp3');
audioRef.current.loop = true;
}
if (playing) {
audioRef.current.pause();
} else {
audioRef.current.play();
}
setPlaying((p) => !p);
}, [playing]);
return (
<div className={styles.page}>
{/* Layer 1-4: Aurora background */}
<AuroraCanvas />
{/* Layer 5: Content */}
<div className={styles.content}>
<img src={logoImg} alt="AirDrama" className={styles.logo} />
<h1 className={styles.title}>AIRFLOW STUDIO</h1>
<p className={styles.tagline}>AI VISUAL NARRATIVE</p>
<div className={styles.actions}>
<div className={styles.btnGroup}>
<button className={styles.btnPrimary} onClick={handleAirDrama}>
<span className={styles.btnName}>Air Drama</span>
</button>
<span className={styles.btnSub}></span>
</div>
<div className={styles.btnGroup}>
<button className={styles.btnGhost} onClick={handleAirSpark}>
<span className={styles.btnName}>Air Spark</span>
</button>
<span className={styles.btnSubGhost}></span>
</div>
</div>
</div>
{/* Easter egg */}
<p className={styles.easter}>Every frame was once just air.</p>
{/* Air Spark full-screen overlay */}
{showSpark && (
<div className={styles.sparkOverlay} onClick={closeSpark}>
<div className={styles.sparkContent}>
<p className={styles.sparkTitle}><span className={styles.sparkEmoji}>🥔</span> coding </p>
<p className={styles.sparkSub}>线</p>
</div>
</div>
)}
{/* Music easter egg — bottom right */}
<button
className={styles.musicBtn}
onClick={toggleMusic}
aria-label={playing ? '暂停音乐' : '播放音乐'}
>
<svg width="20" height="18" viewBox="0 0 20 18" fill="none">
{[0, 1, 2, 3].map((i) => {
const maxH = 16;
const h = maxH * barHeights[i];
const y = 9 - h / 2; // center at y=9
return (
<rect
key={i}
x={1 + i * 5}
y={y}
width="3"
height={h}
rx="1.5"
fill="currentColor"
style={{ transition: 'y 0.15s ease, height 0.15s ease' }}
/>
);
})}
</svg>
</button>
{/* Login modal */}
<LoginModal
isOpen={showLogin}
onClose={() => setShowLogin(false)}
onSuccess={handleLoginSuccess}
/>
{/* Force change password modal (unclosable) */}
{showForceChange && (
<ForceChangePasswordModal onSuccess={handleForceChangeSuccess} />
)}
</div>
);
}