fix: 完善商品—模特库-视频项目-自由创作等删除先进垃圾桶及可恢复等流程
This commit is contained in:
@@ -434,7 +434,7 @@ export function FreeCreatePage({ modelConfigs, onNotify }: {
|
||||
setTasks((prev) => prev.filter((t) => t.id !== target.id));
|
||||
setTotal((prev) => Math.max(0, prev - 1));
|
||||
if (detailId === target.id) setDetailId(null);
|
||||
notify("success", "已删除");
|
||||
notify("success", "已移至垃圾桶");
|
||||
} catch (error) {
|
||||
notify("error", error instanceof Error ? error.message : "删除失败");
|
||||
} finally {
|
||||
|
||||
@@ -74,7 +74,7 @@ function ModelDetailModal({ model, close, onZoom }: { model: ModelEntity | null;
|
||||
|
||||
// 模特库:顶级实体(团队级可复用)= 形象图 + 三视图 + 声线(尾期)。官方预制打「官方模板」标签。
|
||||
// 图片创作(模特上身图)与视频项目(角色)都引用它。期1:看归好的成套模特 + 真人上传 + 删自建。
|
||||
export function ModelsPage() {
|
||||
export function ModelsPage({ onNotify }: { onNotify?: (type: "success" | "error", text: string) => void }) {
|
||||
const [tab, setTab] = useState<Tab>("all");
|
||||
const [items, setItems] = useState<ModelEntity[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -115,6 +115,9 @@ export function ModelsPage() {
|
||||
try {
|
||||
await api.deleteModel(id);
|
||||
setItems((list) => list.filter((m) => m.id !== id));
|
||||
onNotify?.("success", "已移至垃圾桶");
|
||||
} catch (error) {
|
||||
onNotify?.("error", error instanceof Error ? error.message : "删除失败");
|
||||
} finally {
|
||||
setConfirmId(null);
|
||||
}
|
||||
|
||||
+223
-114
@@ -1,111 +1,257 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, 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 type { Asset, FreeVideoTask, ImageConversation, ModelEntity, Product, Project } 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 || "";
|
||||
type TrashKind = "product" | "asset" | "model" | "image" | "freeVideo" | "project";
|
||||
|
||||
type TrashRow = {
|
||||
id: string;
|
||||
kind: TrashKind;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
cover?: string;
|
||||
};
|
||||
|
||||
type TrashSection = {
|
||||
key: TrashKind;
|
||||
title: string;
|
||||
rows: TrashRow[];
|
||||
};
|
||||
|
||||
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 => {
|
||||
|
||||
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<string, string> = { image: "图片", video: "视频", audio: "音频" };
|
||||
|
||||
// 垃圾桶:已软删商品(status=archived)+ 已软删资产(is_deleted,R108)· 均可恢复 / 可彻底删除(不可恢复)
|
||||
export function TrashPage({ onRestore, onPurge }: {
|
||||
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: ImageConversation[]): 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)}` : ""}`
|
||||
}));
|
||||
|
||||
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, onChanged }: {
|
||||
navigate: NavigateFn;
|
||||
onRestore: (id: string) => Promise<unknown> | void;
|
||||
onPurge: (id: string) => Promise<unknown> | void;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const [items, setItems] = useState<Product[]>([]);
|
||||
const [assets, setAssets] = useState<Asset[]>([]);
|
||||
const [sections, setSections] = useState<TrashSection[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [confirmId, setConfirmId] = useState<string | null>(null);
|
||||
const [busyId, setBusyId] = useState<string | null>(null);
|
||||
const [busyKey, setBusyKey] = useState<string | null>(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;
|
||||
|
||||
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()]).then(([p, a]) => {
|
||||
void Promise.allSettled([
|
||||
api.productsTrash(),
|
||||
api.assetsTrash(),
|
||||
api.modelsTrash(),
|
||||
api.conversationsTrash("image"),
|
||||
api.freeVideoTrash(0, 100),
|
||||
api.projectsTrash()
|
||||
]).then(([products, assets, models, conversations, freeVideos, projects]) => {
|
||||
if (!alive) return;
|
||||
setItems(p.status === "fulfilled" ? p.value.results : []);
|
||||
setAssets(a.status === "fulfilled" ? a.value.results : []);
|
||||
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: "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; };
|
||||
}, []);
|
||||
|
||||
async function restore(id: string) {
|
||||
setBusyId(id);
|
||||
try { await onRestore(id); setItems((list) => list.filter((p) => p.id !== id)); }
|
||||
finally { setBusyId(null); }
|
||||
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)
|
||||
);
|
||||
}
|
||||
async function purge(id: string) {
|
||||
setBusyId(id);
|
||||
try { await onPurge(id); setItems((list) => list.filter((p) => p.id !== id)); setConfirmId(null); }
|
||||
finally { setBusyId(null); }
|
||||
|
||||
function rowKey(row: TrashRow): string {
|
||||
return `${row.kind}:${row.id}`;
|
||||
}
|
||||
// 资产的恢复/彻底删除直接走 api(商品那对经 App 的 action 包装,只发商品口)。失败不移行、如实亮错误。
|
||||
async function restoreAsset(id: string) {
|
||||
setBusyId(id);
|
||||
|
||||
function restoreRow(row: TrashRow): Promise<unknown> {
|
||||
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 === "freeVideo") return api.restoreFreeVideo(row.id);
|
||||
return api.restoreProject(row.id);
|
||||
}
|
||||
|
||||
function purgeRow(row: TrashRow): Promise<unknown> {
|
||||
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 === "freeVideo") return api.purgeFreeVideo(row.id);
|
||||
return api.purgeProject(row.id);
|
||||
}
|
||||
|
||||
async function restore(row: TrashRow) {
|
||||
setBusyKey(rowKey(row));
|
||||
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); }
|
||||
try {
|
||||
await restoreRow(row);
|
||||
removeRow(row);
|
||||
onChanged?.();
|
||||
} catch (error) {
|
||||
setErrText(error instanceof Error ? error.message : "恢复失败");
|
||||
} finally {
|
||||
setBusyKey(null);
|
||||
}
|
||||
}
|
||||
async function purgeAsset(id: string) {
|
||||
setBusyId(id);
|
||||
|
||||
async function purge(row: TrashRow) {
|
||||
setBusyKey(rowKey(row));
|
||||
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); }
|
||||
try {
|
||||
await purgeRow(row);
|
||||
removeRow(row);
|
||||
setConfirmId(null);
|
||||
onChanged?.();
|
||||
} catch (error) {
|
||||
setErrText(error instanceof Error ? error.message : "彻底删除失败");
|
||||
} finally {
|
||||
setBusyKey(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");
|
||||
const rows = [...allRows];
|
||||
const results = await Promise.allSettled(rows.map((row) => restoreRow(row)));
|
||||
const failed = new Set(rows.filter((_, i) => results[i].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 = results.find((r): r is PromiseRejectedResult => r.status === "rejected");
|
||||
if (firstFail) setErrText(firstFail.reason instanceof Error ? firstFail.reason.message : "部分恢复失败");
|
||||
if (results.some((r) => r.status === "fulfilled")) onChanged?.();
|
||||
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");
|
||||
const rows = [...allRows];
|
||||
const results = await Promise.allSettled(rows.map((row) => purgeRow(row)));
|
||||
const failed = new Set(rows.filter((_, i) => results[i].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 = results.find((r): r is PromiseRejectedResult => r.status === "rejected");
|
||||
if (firstFail) setErrText(firstFail.reason instanceof Error ? firstFail.reason.message : "部分删除失败");
|
||||
if (results.some((r) => r.status === "fulfilled")) onChanged?.();
|
||||
setBulkBusy(null);
|
||||
}
|
||||
|
||||
@@ -115,7 +261,7 @@ export function TrashPage({ onRestore, onPurge }: {
|
||||
<div>
|
||||
<h1>垃圾桶</h1>
|
||||
<div className="sub">
|
||||
<span className="mono">// 已删除商品与资产</span>
|
||||
<span className="mono">// 已删除内容</span>
|
||||
<span>·</span>
|
||||
<span>可恢复,或彻底删除(不可恢复)</span>
|
||||
</div>
|
||||
@@ -123,45 +269,45 @@ export function TrashPage({ onRestore, onPurge }: {
|
||||
<div className="actions">
|
||||
<button className="btn" type="button" disabled={isEmpty || bulkBusy !== null} onClick={() => setPurgeAllOpen(true)}>清空垃圾桶</button>
|
||||
<button className="btn btn-primary" type="button" disabled={isEmpty || bulkBusy !== null} onClick={() => void restoreAll()}>
|
||||
{bulkBusy === "restore" ? "恢复中…" : "全部恢复"}
|
||||
{bulkBusy === "restore" ? "恢复中..." : "全部恢复"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="placeholder" style={{ minHeight: 160 }}><span className="ph-frame">// 加载中…</span></div>
|
||||
) : items.length === 0 && assets.length === 0 ? (
|
||||
<div className="placeholder" style={{ minHeight: 160 }}><span className="ph-frame">// 加载中...</span></div>
|
||||
) : isEmpty ? (
|
||||
<div className="placeholder trash-empty"><span className="ph-frame">// 垃圾桶是空的</span></div>
|
||||
) : (
|
||||
<>
|
||||
{errText && <div className="trash-err mono">// {errText}</div>}
|
||||
{items.length > 0 && (
|
||||
<div className="trash-section">
|
||||
<div className="trash-section-title mono">// 商品 · {items.length}</div>
|
||||
{sections.map((section) => (
|
||||
<div className="trash-section" key={section.key}>
|
||||
<div className="trash-section-title mono">// {section.title} · {section.rows.length}</div>
|
||||
<div className="trash-list">
|
||||
{items.map((p) => {
|
||||
const cover = coverOf(p);
|
||||
const busy = busyId === p.id;
|
||||
{section.rows.map((row) => {
|
||||
const key = rowKey(row);
|
||||
const busy = busyKey === key;
|
||||
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 className="trash-row" key={key}>
|
||||
<div className={`placeholder trash-thumb${row.cover ? " has-mock-media" : ""}`} style={row.cover ? mediaStyle(row.cover) : undefined}>
|
||||
{!row.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 className="trash-name" title={row.title}>{row.title}</div>
|
||||
<div className="trash-sub mono">// {row.subtitle}</div>
|
||||
</div>
|
||||
<div className="trash-actions">
|
||||
{confirmId === p.id ? (
|
||||
{confirmId === key ? (
|
||||
<>
|
||||
<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 trash-del" type="button" disabled={busy} onClick={() => void purge(row)}>确认删除</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>
|
||||
<button className="btn btn-sm btn-primary" type="button" disabled={busy} onClick={() => void restore(row)}>恢复</button>
|
||||
<button className="btn btn-sm" type="button" disabled={busy} onClick={() => setConfirmId(key)}>彻底删除</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -170,44 +316,7 @@ export function TrashPage({ onRestore, onPurge }: {
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{assets.length > 0 && (
|
||||
<div className="trash-section">
|
||||
<div className="trash-section-title mono">// 资产 · {assets.length}</div>
|
||||
<div className="trash-list">
|
||||
{assets.map((a) => {
|
||||
const cover = assetCoverOf(a);
|
||||
const busy = busyId === a.id;
|
||||
const deletedAt = dateOf(a.updated_at);
|
||||
return (
|
||||
<div className="trash-row" key={a.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={a.name}>{a.name}</div>
|
||||
<div className="trash-sub mono">// {ASSET_TYPE_LABEL[a.asset_type] || a.asset_type}{deletedAt ? ` · 删除于 ${deletedAt}` : ""}</div>
|
||||
</div>
|
||||
<div className="trash-actions">
|
||||
{confirmId === a.id ? (
|
||||
<>
|
||||
<span className="trash-confirm-txt mono">彻底删除?不可恢复</span>
|
||||
<button className="btn btn-sm trash-del" type="button" disabled={busy} onClick={() => void purgeAsset(a.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 restoreAsset(a.id)}>恢复</button>
|
||||
<button className="btn btn-sm" type="button" disabled={busy} onClick={() => setConfirmId(a.id)}>彻底删除</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user