video-shuoshan/web/src/pages/AssetsPage.tsx
seaislee1209 f8358a28c6 feat: 前端UI重构 — Air Spark设计系统对标
- 全局样式对标Air Spark设计系统(背景、glass card、配色、圆角)
- 视频详情弹窗(VideoDetailModal)全屏预览+信息面板
- GenerationCard重构:fixed定位tooltip、9:16视频适配、blob下载
- 个人中心:总额度/今日/本月三卡片布局
- Dashboard图表配色统一为#6c63ff主色调
- Sidebar、InputBar、Toolbar等组件样式优化
- 新增AmbientBackground、AssetsPage组件

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 18:48:07 +08:00

177 lines
5.2 KiB
TypeScript

import { useEffect, useState, useRef, useMemo } from 'react';
import { Sidebar } from '../components/Sidebar';
import { VideoDetailModal } from '../components/VideoDetailModal';
import { useGenerationStore } from '../store/generation';
import type { GenerationTask } from '../types';
import styles from './AssetsPage.module.css';
function groupByDate(tasks: GenerationTask[]): { label: string; tasks: GenerationTask[] }[] {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const yesterday = today - 86400000;
const groups = new Map<string, GenerationTask[]>();
const order: string[] = [];
for (const task of tasks) {
const d = new Date(task.createdAt);
const dayStart = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
let label: string;
if (dayStart >= today) {
label = '今天';
} else if (dayStart >= yesterday) {
label = '昨天';
} else {
label = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
}
if (!groups.has(label)) {
groups.set(label, []);
order.push(label);
}
groups.get(label)!.push(task);
}
return order.map((label) => ({ label, tasks: groups.get(label)! }));
}
function VideoThumbnail({
task,
onClick,
}: {
task: GenerationTask;
onClick: () => void;
}) {
const videoRef = useRef<HTMLVideoElement>(null);
const [hover, setHover] = useState(false);
const handleEnter = () => {
setHover(true);
videoRef.current?.play().catch(() => {});
};
const handleLeave = () => {
setHover(false);
if (videoRef.current) {
videoRef.current.pause();
videoRef.current.currentTime = 0;
}
};
const durationLabel = `00:${String(task.duration).padStart(2, '0')}`;
return (
<div
className={styles.thumbnail}
onMouseEnter={handleEnter}
onMouseLeave={handleLeave}
onClick={onClick}
>
{task.resultUrl ? (
<video
ref={videoRef}
src={task.resultUrl}
className={styles.thumbVideo}
muted
loop
preload="metadata"
/>
) : (
<div className={styles.thumbPlaceholder} />
)}
<span className={styles.durationBadge}>{durationLabel}</span>
{hover && <div className={styles.thumbOverlay} />}
</div>
);
}
export function AssetsPage() {
const tasks = useGenerationStore((s) => s.tasks);
const loadTasks = useGenerationStore((s) => s.loadTasks);
const reEdit = useGenerationStore((s) => s.reEdit);
const regenerate = useGenerationStore((s) => s.regenerate);
const removeTask = useGenerationStore((s) => s.removeTask);
const [detailTask, setDetailTask] = useState<GenerationTask | null>(null);
useEffect(() => {
loadTasks();
}, [loadTasks]);
const completedTasks = useMemo(
() => tasks.filter((t) => t.status === 'completed'),
[tasks],
);
const dateGroups = useMemo(() => groupByDate(completedTasks), [completedTasks]);
const handleReEdit = (id: string) => {
reEdit(id);
setDetailTask(null);
};
const handleRegenerate = (id: string) => {
regenerate(id);
setDetailTask(null);
};
const handleDelete = (id: string) => {
removeTask(id);
setDetailTask(null);
};
const detailIdx = detailTask ? completedTasks.findIndex((t) => t.id === detailTask.id) : -1;
return (
<div className={styles.layout}>
<Sidebar />
<main className={styles.main}>
{/* Tab header */}
<div className={styles.tabHeader}>
<div className={styles.tabs}>
<span className={`${styles.tab} ${styles.tabActive}`}></span>
</div>
<div className={styles.subTabs}>
<span className={`${styles.subTab} ${styles.subTabActive}`}></span>
<span className={styles.subTab}></span>
</div>
</div>
{/* Video grid by date */}
<div className={styles.content}>
{completedTasks.length === 0 ? (
<div className={styles.empty}>
<p></p>
</div>
) : (
dateGroups.map((group) => (
<section key={group.label} className={styles.dateGroup}>
<h3 className={styles.dateLabel}>{group.label}</h3>
<div className={styles.grid}>
{group.tasks.map((task) => (
<VideoThumbnail
key={task.id}
task={task}
onClick={() => setDetailTask(task)}
/>
))}
</div>
</section>
))
)}
</div>
</main>
<VideoDetailModal
task={detailTask}
onClose={() => setDetailTask(null)}
onReEdit={handleReEdit}
onRegenerate={handleRegenerate}
onDelete={handleDelete}
hasPrev={detailIdx > 0}
hasNext={detailIdx >= 0 && detailIdx < completedTasks.length - 1}
onPrev={() => detailIdx > 0 && setDetailTask(completedTasks[detailIdx - 1])}
onNext={() => detailIdx < completedTasks.length - 1 && setDetailTask(completedTasks[detailIdx + 1])}
/>
</div>
);
}