import { useEffect, useMemo, useState } from "react"; import type { CSSProperties } from "react"; import { api } from "../api"; import { SystemLoading } from "../components/loading"; import { ConfirmModal } from "../components/overlays"; import type { NavigateFn } from "./route-config"; import type { Asset, FreeVideoTask, ImageConversationTrash, ImageExceptionBatchTrash, ModelEntity, Product, Project } from "../types"; import "../trash-page.css"; type TrashKind = "product" | "asset" | "model" | "image" | "imageExceptionBatch" | "freeVideo" | "project"; type TrashRow = { id: string; kind: TrashKind; title: string; subtitle: string; cover?: string; }; type TrashSection = { key: TrashKind; title: string; rows: TrashRow[]; }; type ProductBatchResult = { succeededIds: string[]; failedIds: string[]; }; const mediaStyle = (url: string): CSSProperties => ({ ["--mock-media-url"]: `url(${url})` } as CSSProperties); const dateOf = (iso?: string | null): string => { if (!iso) return ""; const d = new Date(iso); if (Number.isNaN(d.getTime())) return ""; return `${String(d.getMonth() + 1).padStart(2, "0")}.${String(d.getDate()).padStart(2, "0")}`; }; const coverOf = (p: Product): string => p.cover_preview_url || p.images?.find((i) => i.preview_url)?.preview_url || ""; const assetCoverOf = (a: Asset): string => a.files?.find((f) => f.is_primary && f.preview_url)?.preview_url || a.files?.find((f) => f.preview_url)?.preview_url || ""; const ASSET_TYPE_LABEL: Record = { image: "图片", video: "视频", audio: "音频" }; const rowsFromProducts = (items: Product[]): TrashRow[] => items.map((p) => ({ id: p.id, kind: "product", title: p.title, subtitle: p.category || "未分类", cover: coverOf(p) })); const rowsFromAssets = (items: Asset[]): TrashRow[] => items.map((a) => { const deletedAt = dateOf(a.updated_at); return { id: a.id, kind: "asset", title: a.name, subtitle: `${ASSET_TYPE_LABEL[a.asset_type] || a.asset_type}${deletedAt ? ` · 删除于 ${deletedAt}` : ""}`, cover: assetCoverOf(a) }; }); const rowsFromModels = (items: ModelEntity[]): TrashRow[] => items.map((m) => ({ id: m.id, kind: "model", title: m.name, subtitle: m.source === "upload" ? "上传模特" : "AI 模特", cover: m.portrait || m.triview || "" })); const rowsFromConversations = (items: ImageConversationTrash[]): TrashRow[] => items.map((c) => ({ id: c.id, kind: "image", title: c.title || "未命名图片创作", subtitle: `${c.task_count || 0} 个生成任务${dateOf(c.updated_at) ? ` · 删除于 ${dateOf(c.updated_at)}` : ""}`, cover: c.cover_preview_url || "" })); const IMAGE_MODE_LABEL: Record = { image: "图片创作", model: "模特上身图", cover: "平台套图" }; const rowsFromImageExceptionBatches = (items: ImageExceptionBatchTrash[]): TrashRow[] => items.map((batch) => ({ id: batch.id, kind: "imageExceptionBatch", title: batch.prompt || batch.product_title || "未命名图片生成", subtitle: `${IMAGE_MODE_LABEL[batch.mode]} · ${batch.count} 张${dateOf(batch.updated_at) ? ` · 删除于 ${dateOf(batch.updated_at)}` : ""}`, cover: batch.cover_preview_url || "" })); const rowsFromFreeVideos = (items: FreeVideoTask[]): TrashRow[] => items.map((task) => ({ id: task.id, kind: "freeVideo", title: task.prompt || "未命名视频创作", subtitle: `${task.status}${task.duration ? ` · ${task.duration}s` : ""}${task.aspect_ratio ? ` · ${task.aspect_ratio}` : ""}`, cover: task.thumbnail_url || "" })); const rowsFromProjects = (items: Project[]): TrashRow[] => items.map((p) => ({ id: p.id, kind: "project", title: p.name, subtitle: p.product_title || p.current_stage || p.status, cover: p.cover_preview_url || "" })); export function TrashPage({ onRestore, onPurge, onRestoreProducts, onPurgeProducts, onChanged }: { navigate: NavigateFn; onRestore: (id: string) => Promise | void; onPurge: (id: string) => Promise | void; onRestoreProducts: (ids: string[]) => Promise; onPurgeProducts: (ids: string[]) => Promise; onChanged?: () => void; }) { const [sections, setSections] = useState([]); const [loading, setLoading] = useState(true); const [confirmId, setConfirmId] = useState(null); const [busyKey, setBusyKey] = useState(null); const [errText, setErrText] = useState(""); const [bulkBusy, setBulkBusy] = useState<"restore" | "purge" | null>(null); const [purgeAllOpen, setPurgeAllOpen] = useState(false); const allRows = useMemo(() => sections.flatMap((section) => section.rows), [sections]); const isEmpty = allRows.length === 0; useEffect(() => { let alive = true; void Promise.allSettled([ api.productsTrash(), api.assetsTrash(), api.modelsTrash(), api.conversationsTrash("image"), api.imageExceptionBatchesTrash(), api.freeVideoTrash(0, 100), api.projectsTrash() ]).then(([products, assets, models, conversations, imageExceptionBatches, freeVideos, projects]) => { if (!alive) return; const rawSections: TrashSection[] = [ { key: "product", title: "商品", rows: products.status === "fulfilled" ? rowsFromProducts(products.value.results) : [] }, { key: "asset", title: "资产", rows: assets.status === "fulfilled" ? rowsFromAssets(assets.value.results) : [] }, { key: "model", title: "模特", rows: models.status === "fulfilled" ? rowsFromModels(models.value.results) : [] }, { key: "image", title: "自由创作图片", rows: conversations.status === "fulfilled" ? rowsFromConversations(conversations.value.results) : [] }, { key: "imageExceptionBatch", title: "图片异常批次", rows: imageExceptionBatches.status === "fulfilled" ? rowsFromImageExceptionBatches(imageExceptionBatches.value.results) : [] }, { key: "freeVideo", title: "自由创作视频", rows: freeVideos.status === "fulfilled" ? rowsFromFreeVideos(freeVideos.value.results) : [] }, { key: "project", title: "视频项目", rows: projects.status === "fulfilled" ? rowsFromProjects(projects.value.results) : [] } ]; const nextSections = rawSections.filter((section) => section.rows.length > 0); setSections(nextSections); setLoading(false); }); return () => { alive = false; }; }, []); function removeRow(row: TrashRow) { setSections((list) => list .map((section) => section.key === row.kind ? { ...section, rows: section.rows.filter((item) => item.id !== row.id) } : section) .filter((section) => section.rows.length > 0) ); } function rowKey(row: TrashRow): string { return `${row.kind}:${row.id}`; } function restoreRow(row: TrashRow): Promise { if (row.kind === "product") return Promise.resolve(onRestore(row.id)); if (row.kind === "asset") return api.restoreAsset(row.id); if (row.kind === "model") return api.restoreModel(row.id); if (row.kind === "image") return api.restoreConversation(row.id); if (row.kind === "imageExceptionBatch") return api.restoreWorkbenchImageBatch(row.id); if (row.kind === "freeVideo") return api.restoreFreeVideo(row.id); return api.restoreProject(row.id); } function purgeRow(row: TrashRow): Promise { if (row.kind === "product") return Promise.resolve(onPurge(row.id)); if (row.kind === "asset") return api.purgeAsset(row.id); if (row.kind === "model") return api.purgeModel(row.id); if (row.kind === "image") return api.purgeConversation(row.id); if (row.kind === "imageExceptionBatch") return api.purgeWorkbenchImageBatch(row.id); if (row.kind === "freeVideo") return api.purgeFreeVideo(row.id); return api.purgeProject(row.id); } async function restore(row: TrashRow) { setBusyKey(rowKey(row)); setErrText(""); try { await restoreRow(row); removeRow(row); onChanged?.(); } catch (error) { setErrText(error instanceof Error ? error.message : "恢复失败"); } finally { setBusyKey(null); } } async function purge(row: TrashRow) { setBusyKey(rowKey(row)); setErrText(""); try { await purgeRow(row); removeRow(row); setConfirmId(null); onChanged?.(); } catch (error) { setErrText(error instanceof Error ? error.message : "彻底删除失败"); } finally { setBusyKey(null); } } async function restoreAll() { setBulkBusy("restore"); setErrText(""); const rows = [...allRows]; const productRows = rows.filter((row) => row.kind === "product"); const otherRows = rows.filter((row) => row.kind !== "product"); const [productResult, otherResults] = await Promise.all([ productRows.length ? onRestoreProducts(productRows.map((row) => row.id)) : Promise.resolve({ succeededIds: productRows.map((row) => row.id), failedIds: [] }), Promise.allSettled(otherRows.map((row) => restoreRow(row))) ]); const failed = new Set([ ...productResult.failedIds.map((id) => `product:${id}`), ...otherRows.filter((_, index) => otherResults[index].status === "rejected").map(rowKey) ]); setSections((list) => list .map((section) => ({ ...section, rows: section.rows.filter((row) => failed.has(rowKey(row))) })) .filter((section) => section.rows.length > 0) ); const firstFail = otherResults.find((r): r is PromiseRejectedResult => r.status === "rejected"); if (firstFail) setErrText(firstFail.reason instanceof Error ? firstFail.reason.message : "部分恢复失败"); if (productResult.succeededIds.length || otherResults.some((r) => r.status === "fulfilled")) onChanged?.(); setBulkBusy(null); } async function purgeAll() { setPurgeAllOpen(false); setBulkBusy("purge"); setErrText(""); const rows = [...allRows]; const productRows = rows.filter((row) => row.kind === "product"); const otherRows = rows.filter((row) => row.kind !== "product"); const [productResult, otherResults] = await Promise.all([ productRows.length ? onPurgeProducts(productRows.map((row) => row.id)) : Promise.resolve({ succeededIds: productRows.map((row) => row.id), failedIds: [] }), Promise.allSettled(otherRows.map((row) => purgeRow(row))) ]); const failed = new Set([ ...productResult.failedIds.map((id) => `product:${id}`), ...otherRows.filter((_, index) => otherResults[index].status === "rejected").map(rowKey) ]); setSections((list) => list .map((section) => ({ ...section, rows: section.rows.filter((row) => failed.has(rowKey(row))) })) .filter((section) => section.rows.length > 0) ); const firstFail = otherResults.find((r): r is PromiseRejectedResult => r.status === "rejected"); if (firstFail) setErrText(firstFail.reason instanceof Error ? firstFail.reason.message : "部分删除失败"); if (productResult.succeededIds.length || otherResults.some((r) => r.status === "fulfilled")) onChanged?.(); setBulkBusy(null); } return (

垃圾桶

已删除内容 · 可恢复,或彻底删除(不可恢复)
{loading ? ( ) : isEmpty ? (
垃圾桶是空的
) : ( <> {errText &&
{errText}
} {sections.map((section) => (
{section.title} · {section.rows.length}
{section.rows.map((row) => { const key = rowKey(row); const busy = busyKey === key; return (
{!row.cover && 无图}
{row.title}
{row.subtitle}
{confirmId === key ? ( <> 彻底删除?不可恢复 ) : ( <> )}
); })}
))} )}
setPurgeAllOpen(false)} onConfirm={purgeAll} />
); }