fix: YYX测试清单16条收口——资产库弹窗层级/批次多选删/商品筛选/模特卡单卡化/垃圾桶清空等

- 资产库:批次弹窗降层(under-confirm 990<999)修单图删除确认弹窗被压;编辑态批次卡多选批删;facets 新增 products 关联商品筛选(前后端,+2 测试);视频成品 tab 补搜索/排序/分页
- 平台套图工作台:「更多」气泡与 icon 左对齐+鼠标移出自动收起;模特卡改单卡(空态「+」占位,点卡进模特库单选回填)
- 工作台:删余额条,冻结/共计移至小标题行右侧(.dash-funds-mono)
- 模特库:点模特卡弹详情(形象图/标签/三视图+灯箱放大)
- 垃圾桶:新增「全部恢复」/「清空垃圾桶」(清空二次确认)
- 视频项目页:字重/阴影等轻量对齐设计规范
- api.assetFacets 返回体逐键兜底:旧后端缺 products 键会致资产库整页 undefined.find 白屏(前后端部署时序防护,浏览器已复现验证)

验证:tsc --noEmit / vite build / apps.assets 测试全过;暂存树已独立编译验证,不依赖工作区其余在途改动

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
zyc
2026-07-02 18:03:26 +08:00
co-authored by Claude Fable 5
parent a048c32527
commit 5ec69ad91f
14 changed files with 496 additions and 176 deletions
+55
View File
@@ -1,6 +1,7 @@
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";
@@ -32,6 +33,9 @@ export function TrashPage({ onRestore, onPurge }: {
const [confirmId, setConfirmId] = useState<string | null>(null);
const [busyId, setBusyId] = 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;
useEffect(() => {
let alive = true;
@@ -70,6 +74,41 @@ export function TrashPage({ onRestore, onPurge }: {
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 (
<div className="trash-page">
<div className="page-head">
@@ -81,6 +120,12 @@ export function TrashPage({ onRestore, onPurge }: {
<span>,()</span>
</div>
</div>
<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" ? "恢复中…" : "全部恢复"}
</button>
</div>
</div>
{loading ? (
@@ -165,6 +210,16 @@ export function TrashPage({ onRestore, onPurge }: {
)}
</>
)}
<ConfirmModal
open={purgeAllOpen}
title="清空垃圾桶"
subtitle="// PURGE ALL"
detail="清空垃圾桶数据无法恢复,是否需要清空?"
confirmText="清空"
onCancel={() => setPurgeAllOpen(false)}
onConfirm={purgeAll}
/>
</div>
);
}