"use client"; import { useEffect, useMemo, useRef, useState } from "react"; import { Download, Eye, ImageIcon, Info, Loader2, Music, RefreshCw, Trash2, X } from "lucide-react"; import { clampPage, pageItems, Pagination } from "@/components/pagination"; import { modalEnter, modalExit, pulseFeedback, revealChildren, runScopedMotion } from "@/lib/ui/motion"; import type { Asset, GenerationJob } from "@/lib/types"; type AssetView = "assets" | "tasks"; const ASSET_PAGE_SIZE = 8; const TASK_PAGE_SIZE = 8; export function AssetManager() { const [assets, setAssets] = useState([]); const [jobs, setJobs] = useState([]); const [view, setView] = useState("assets"); const [loading, setLoading] = useState(false); const [syncingJobId, setSyncingJobId] = useState(null); const [deletingAssetId, setDeletingAssetId] = useState(null); const [deletingJobId, setDeletingJobId] = useState(null); const [previewAsset, setPreviewAsset] = useState(null); const [expandedJobId, setExpandedJobId] = useState(null); const [assetPage, setAssetPage] = useState(1); const [jobPage, setJobPage] = useState(1); const [error, setError] = useState(null); const managerRef = useRef(null); const listRef = useRef(null); const feedbackRef = useRef(null); const previewDialogRef = useRef(null); const closeButtonRef = useRef(null); useEffect(() => { refresh().catch(() => undefined); }, []); const jobByOutputAssetId = useMemo(() => { const map = new Map(); for (const job of jobs) { for (const assetId of job.outputAssetIds) map.set(assetId, job); } return map; }, [jobs]); const visibleAssets = pageItems(assets, assetPage, ASSET_PAGE_SIZE); const visibleJobs = pageItems(jobs, jobPage, TASK_PAGE_SIZE); useEffect(() => { setAssetPage((page) => clampPage(page, assets.length, ASSET_PAGE_SIZE)); }, [assets.length]); useEffect(() => { setJobPage((page) => clampPage(page, jobs.length, TASK_PAGE_SIZE)); }, [jobs.length]); useEffect(() => { if (view === "assets") setAssetPage((page) => clampPage(page, assets.length, ASSET_PAGE_SIZE)); if (view === "tasks") setJobPage((page) => clampPage(page, jobs.length, TASK_PAGE_SIZE)); }, [view, assets.length, jobs.length]); useEffect(() => { return runScopedMotion(managerRef, (scope) => revealChildren(scope)); }, []); useEffect(() => { if (listRef.current) revealChildren(listRef.current, ".asset-history-card, .compact-job, .job-detail-panel"); }, [view, assetPage, jobPage, visibleAssets.length, visibleJobs.length, expandedJobId]); useEffect(() => { pulseFeedback(feedbackRef.current); }, [error]); useEffect(() => { if (!previewAsset) return undefined; modalEnter(previewDialogRef.current); window.requestAnimationFrame(() => closeButtonRef.current?.focus()); const handleKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") closePreview(); }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [previewAsset]); async function refresh() { setLoading(true); setError(null); try { const [assetResponse, imageResponse, videoResponse] = await Promise.all([ fetch("/api/assets", { cache: "no-store" }), fetch("/api/generations/image", { cache: "no-store" }), fetch("/api/generations/video", { cache: "no-store" }) ]); const [assetPayload, imagePayload, videoPayload] = await Promise.all([ assetResponse.json(), imageResponse.json(), videoResponse.json() ]); if (!assetResponse.ok) throw new Error(assetPayload.error || "读取资产失败"); if (!imageResponse.ok) throw new Error(imagePayload.error || "读取图片任务失败"); if (!videoResponse.ok) throw new Error(videoPayload.error || "读取视频任务失败"); const dedupedJobs = new Map(); for (const job of [...(imagePayload.jobs || []), ...(videoPayload.jobs || [])] as GenerationJob[]) { dedupedJobs.set(job.id, job); } const nextJobs = [...dedupedJobs.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); const outputAssetIds = new Set(nextJobs.flatMap((job) => job.outputAssetIds)); setAssets((assetPayload.assets || []).filter((asset: Asset) => isResultAsset(asset, outputAssetIds))); setJobs(nextJobs); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setLoading(false); } } async function syncJob(job: GenerationJob) { setSyncingJobId(job.id); setError(null); try { const response = await fetch(jobPath(job), { cache: "no-store" }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || "查询任务失败"); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSyncingJobId(null); } } async function removeAsset(asset: Asset) { if (!window.confirm(`删除「${asset.name}」?对应 OSS 对象会一并删除。`)) return; setDeletingAssetId(asset.id); setError(null); try { const response = await fetch(`/api/assets/${asset.id}`, { method: "DELETE" }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || "删除资产失败"); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setDeletingAssetId(null); } } async function removeJob(job: GenerationJob) { if (!window.confirm(`删除「${capabilityLabel(job.capability)}」任务?任务输出结果和对应 OSS 对象会一并删除。`)) return; setDeletingJobId(job.id); setError(null); try { const response = await fetch(jobPath(job), { method: "DELETE" }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error || "删除任务失败"); if (expandedJobId === job.id) setExpandedJobId(null); await refresh(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setDeletingJobId(null); } } function closePreview() { modalExit(previewDialogRef.current, () => setPreviewAsset(null)); } return (

结果资产

{error ?
{error}
: null} {view === "assets" ? ( <>
{visibleAssets.map((asset) => (

{asset.name}

{sourceLabel(asset.source)}

{kindLabel(asset)} / {formatDate(asset.createdAt)}

{jobByOutputAssetId.get(asset.id) ? ( {capabilityLabel(jobByOutputAssetId.get(asset.id)?.capability)} ) : null}
))} {!assets.length ?
暂无生成结果。提交创作、局部重绘或超清后,结果会自动保留在这里。
: null}
) : ( <>
{visibleJobs.map((job) => (

{job.prompt?.slice(0, 90) || capabilityLabel(job.capability)}

{capabilityLabel(job.capability)} / {job.reqKey}

{durationLabel(job)} / 输入 {job.inputAssetIds.length || job.inputUrls.length} 个,输出 {job.outputAssetIds.length} 个

{statusLabel(job.status)}
{expandedJobId === job.id ? : null}
))} {!jobs.length ?
暂无生成任务。提交图片或视频生成后会保留任务历史。
: null}
)}
{previewAsset ? (
event.stopPropagation()}>
{previewAsset.name} {kindLabel(previewAsset)} / {sourceLabel(previewAsset.source)} / {formatDate(previewAsset.createdAt)}
{renderAssetPreviewLarge(previewAsset)}
) : null}
); } function JobDetails({ job }: { job: GenerationJob }) { return (
状态
{statusLabel(job.status)}
耗时
{durationValue(job)}
提交时间
{formatDateTime(job.createdAt)}
更新时间
{formatDateTime(job.updatedAt)}
任务类型
{capabilityLabel(job.capability)}
服务
{providerLabel(job.provider)}
Req Key
{job.reqKey}
任务 ID
{job.providerTaskId || job.id}
输入/输出
{job.inputAssetIds.length || job.inputUrls.length} / {job.outputAssetIds.length}
{job.error ? (
错误
{job.error.message}
) : null}
{job.prompt ? (
提示词

{job.prompt}

) : null}
); } function renderAssetPreview(asset: Asset) { if (isAudio(asset)) { return (
音频素材
); } if (asset.kind === "video" || isVideo(asset)) { return