All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m20s
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>
195 lines
5.7 KiB
TypeScript
195 lines
5.7 KiB
TypeScript
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>
|
||
);
|
||
}
|