import { useEffect, useState } from "react"; import type { CSSProperties } from "react"; import { api } from "../api"; import { ConfirmModal } from "../components/overlays"; import type { NavigateFn } from "./route-config"; import type { Asset, Product } from "../types"; import "../trash-page.css"; const coverOf = (p: Product): string => p.cover_preview_url || p.images?.find((i) => i.preview_url)?.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 mediaStyle = (url: string): CSSProperties => ({ ["--mock-media-url"]: `url(${url})` } as CSSProperties); // mono 时间戳(设计规范 // 05.22 式):软删走 update updated_at → updated_at ≈ 删除时间 const dateOf = (iso?: string): 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 ASSET_TYPE_LABEL: Record = { image: "图片", video: "视频", audio: "音频" }; // 垃圾桶:已软删商品(status=archived)+ 已软删资产(is_deleted,R108)· 均可恢复 / 可彻底删除(不可恢复) export function TrashPage({ onRestore, onPurge }: { navigate: NavigateFn; onRestore: (id: string) => Promise | void; onPurge: (id: string) => Promise | void; }) { const [items, setItems] = useState([]); const [assets, setAssets] = useState([]); const [loading, setLoading] = useState(true); const [confirmId, setConfirmId] = useState(null); const [busyId, setBusyId] = useState(null); const [errText, setErrText] = useState(""); const [bulkBusy, setBulkBusy] = useState<"restore" | "purge" | null>(null); const [purgeAllOpen, setPurgeAllOpen] = useState(false); const isEmpty = items.length === 0 && assets.length === 0; useEffect(() => { let alive = true; void Promise.allSettled([api.productsTrash(), api.assetsTrash()]).then(([p, a]) => { if (!alive) return; setItems(p.status === "fulfilled" ? p.value.results : []); setAssets(a.status === "fulfilled" ? a.value.results : []); setLoading(false); }); return () => { alive = false; }; }, []); async function restore(id: string) { setBusyId(id); try { await onRestore(id); setItems((list) => list.filter((p) => p.id !== id)); } finally { setBusyId(null); } } async function purge(id: string) { setBusyId(id); try { await onPurge(id); setItems((list) => list.filter((p) => p.id !== id)); setConfirmId(null); } finally { setBusyId(null); } } // 资产的恢复/彻底删除直接走 api(商品那对经 App 的 action 包装,只发商品口)。失败不移行、如实亮错误。 async function restoreAsset(id: string) { setBusyId(id); setErrText(""); try { await api.restoreAsset(id); setAssets((list) => list.filter((a) => a.id !== id)); } catch (error) { setErrText(error instanceof Error ? error.message : "恢复失败"); } finally { setBusyId(null); } } async function purgeAsset(id: string) { setBusyId(id); setErrText(""); try { await api.purgeAsset(id); setAssets((list) => list.filter((a) => a.id !== id)); setConfirmId(null); } catch (error) { setErrText(error instanceof Error ? error.message : "彻底删除失败"); } finally { setBusyId(null); } } // 全部恢复:商品走 onRestore、资产走 api.restoreAsset,各自独立通道并发跑;失败的留在列表、错误如实亮出 // 按 id 而非下标比对结果:防止执行期间单条操作改动了 items/assets 导致下标错位 async function restoreAll() { setBulkBusy("restore"); setErrText(""); const itemIds = items.map((p) => p.id); const assetIds = assets.map((a) => a.id); const itemResults = await Promise.allSettled(itemIds.map((id) => Promise.resolve(onRestore(id)))); const assetResults = await Promise.allSettled(assetIds.map((id) => api.restoreAsset(id))); const failedItemIds = new Set(itemIds.filter((_, i) => itemResults[i].status === "rejected")); const failedAssetIds = new Set(assetIds.filter((_, i) => assetResults[i].status === "rejected")); setItems((list) => list.filter((p) => failedItemIds.has(p.id))); setAssets((list) => list.filter((a) => failedAssetIds.has(a.id))); const firstFail = [...itemResults, ...assetResults].find((r): r is PromiseRejectedResult => r.status === "rejected"); if (firstFail) setErrText(firstFail.reason instanceof Error ? firstFail.reason.message : "部分恢复失败"); setBulkBusy(null); } // 清空垃圾桶:同上,走各自的彻底删除通道 async function purgeAll() { setPurgeAllOpen(false); setBulkBusy("purge"); setErrText(""); const itemIds = items.map((p) => p.id); const assetIds = assets.map((a) => a.id); const itemResults = await Promise.allSettled(itemIds.map((id) => Promise.resolve(onPurge(id)))); const assetResults = await Promise.allSettled(assetIds.map((id) => api.purgeAsset(id))); const failedItemIds = new Set(itemIds.filter((_, i) => itemResults[i].status === "rejected")); const failedAssetIds = new Set(assetIds.filter((_, i) => assetResults[i].status === "rejected")); setItems((list) => list.filter((p) => failedItemIds.has(p.id))); setAssets((list) => list.filter((a) => failedAssetIds.has(a.id))); const firstFail = [...itemResults, ...assetResults].find((r): r is PromiseRejectedResult => r.status === "rejected"); if (firstFail) setErrText(firstFail.reason instanceof Error ? firstFail.reason.message : "部分删除失败"); setBulkBusy(null); } return (

垃圾桶

// 已删除商品与资产 · 可恢复,或彻底删除(不可恢复)
{loading ? (
// 加载中…
) : items.length === 0 && assets.length === 0 ? (
// 垃圾桶是空的
) : ( <> {errText &&
// {errText}
} {items.length > 0 && (
// 商品 · {items.length}
{items.map((p) => { const cover = coverOf(p); const busy = busyId === p.id; return (
{!cover && 无图}
{p.title}
// {p.category || "未分类"}
{confirmId === p.id ? ( <> 彻底删除?不可恢复 ) : ( <> )}
); })}
)} {assets.length > 0 && (
// 资产 · {assets.length}
{assets.map((a) => { const cover = assetCoverOf(a); const busy = busyId === a.id; const deletedAt = dateOf(a.updated_at); return (
{!cover && 无图}
{a.name}
// {ASSET_TYPE_LABEL[a.asset_type] || a.asset_type}{deletedAt ? ` · 删除于 ${deletedAt}` : ""}
{confirmId === a.id ? ( <> 彻底删除?不可恢复 ) : ( <> )}
); })}
)} )} setPurgeAllOpen(false)} onConfirm={purgeAll} />
); }