feat(trash): 商品删除→垃圾桶(软删可恢复)+ 左侧垃圾桶入口
后端(无需迁移,复用 Product.status active/archived):
- destroy 改软删(status→archived);正常列表/详情只看 active
- 新增 /products/trash(列已删)、/products/{id}/restore(恢复)、/{id}/purge(彻底删)
- tests.py:软删/列表隐藏/回收站/恢复/彻底删/拒绝彻底删 active —— 5 测试全过
前端:
- api:productsTrash / restoreProduct / purgeProduct
- 左侧导航加「垃圾桶」(新增 trash 图标);route-config 接 /trash 路由
- 新页 routes/trash.tsx + trash-page.css(仅 token):已删商品列表 + 恢复 + 彻底删(二次确认)
- 删除确认文案「不可撤销」→「移至垃圾桶可恢复」;toast「商品已删除」→「已移至垃圾桶」
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b8fc1a0749
commit
3469799da5
@@ -0,0 +1,91 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { CSSProperties } from "react";
|
||||
import { api } from "../api";
|
||||
import type { NavigateFn } from "./route-config";
|
||||
import type { 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 || "";
|
||||
const mediaStyle = (url: string): CSSProperties => ({ ["--mock-media-url"]: `url(${url})` } as CSSProperties);
|
||||
|
||||
// 垃圾桶:已软删(status=archived)的商品 · 可恢复 / 可彻底删除(不可恢复)
|
||||
export function TrashPage({ onRestore, onPurge }: {
|
||||
navigate: NavigateFn;
|
||||
onRestore: (id: string) => Promise<unknown> | void;
|
||||
onPurge: (id: string) => Promise<unknown> | void;
|
||||
}) {
|
||||
const [items, setItems] = useState<Product[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [confirmId, setConfirmId] = useState<string | null>(null);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
api.productsTrash().then((r) => { if (alive) setItems(r.results); }).catch(() => { if (alive) setItems([]); }).finally(() => { if (alive) 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); }
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="trash-page">
|
||||
<div className="page-head">
|
||||
<div>
|
||||
<h1>垃圾桶</h1>
|
||||
<div className="sub">
|
||||
<span className="mono">// 已删除商品</span>
|
||||
<span>·</span>
|
||||
<span>可恢复,或彻底删除(不可恢复)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="placeholder" style={{ minHeight: 160 }}><span className="ph-frame">// 加载中…</span></div>
|
||||
) : items.length === 0 ? (
|
||||
<div className="placeholder trash-empty"><span className="ph-frame">// 垃圾桶是空的</span></div>
|
||||
) : (
|
||||
<div className="trash-list">
|
||||
{items.map((p) => {
|
||||
const cover = coverOf(p);
|
||||
const busy = busyId === p.id;
|
||||
return (
|
||||
<div className="trash-row" key={p.id}>
|
||||
<div className={`placeholder trash-thumb${cover ? " has-mock-media" : ""}`} style={cover ? mediaStyle(cover) : undefined}>
|
||||
{!cover && <span className="ph-frame">无图</span>}
|
||||
</div>
|
||||
<div className="trash-meta">
|
||||
<div className="trash-name" title={p.title}>{p.title}</div>
|
||||
<div className="trash-sub mono">// {p.category || "未分类"}</div>
|
||||
</div>
|
||||
<div className="trash-actions">
|
||||
{confirmId === p.id ? (
|
||||
<>
|
||||
<span className="trash-confirm-txt mono">彻底删除?不可恢复</span>
|
||||
<button className="btn btn-sm trash-del" type="button" disabled={busy} onClick={() => void purge(p.id)}>确认删除</button>
|
||||
<button className="btn btn-sm" type="button" disabled={busy} onClick={() => setConfirmId(null)}>取消</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button className="btn btn-sm btn-primary" type="button" disabled={busy} onClick={() => void restore(p.id)}>恢复</button>
|
||||
<button className="btn btn-sm" type="button" disabled={busy} onClick={() => setConfirmId(p.id)}>彻底删除</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user