From 4e56597adba699897edf78c3fa2eeed4173b6598 Mon Sep 17 00:00:00 2001 From: zhx Date: Tue, 9 Jun 2026 13:53:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=B9=E9=87=8F=E9=87=8D=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/resources/api.test.ts | 2 +- src/features/resources/api.ts | 2 +- .../resources/pages/ResourceListPage.jsx | 108 +++++++++++++++--- src/features/resources/resources.module.css | 58 ++++++++++ 4 files changed, 155 insertions(+), 15 deletions(-) diff --git a/src/features/resources/api.test.ts b/src/features/resources/api.test.ts index a460a95..f78c428 100644 --- a/src/features/resources/api.test.ts +++ b/src/features/resources/api.test.ts @@ -268,7 +268,7 @@ test("resource mp4 layout batch API uses narrow update endpoint", async () => { ]); const [url, init] = vi.mocked(fetch).mock.calls[0]; - expect(String(url)).toContain("/api/v1/admin/resources/mp4-layouts"); + expect(String(url)).toContain("/api/v1/admin/resources/mp4-layouts/batch"); expect(init?.method).toBe("PUT"); expect(JSON.parse(String(init?.body))).toMatchObject({ items: [{ resourceId: 11 }], diff --git a/src/features/resources/api.ts b/src/features/resources/api.ts index e9facc1..df290ab 100644 --- a/src/features/resources/api.ts +++ b/src/features/resources/api.ts @@ -312,7 +312,7 @@ export function updateResource(resourceId: EntityId, payload: ResourcePayload): } export function updateResourceMp4Layouts(items: ResourceMp4LayoutUpdatePayload[]): Promise { - return apiRequest("/v1/admin/resources/mp4-layouts", { + return apiRequest("/v1/admin/resources/mp4-layouts/batch", { body: { items }, method: "PUT", }); diff --git a/src/features/resources/pages/ResourceListPage.jsx b/src/features/resources/pages/ResourceListPage.jsx index 682c748..6274109 100644 --- a/src/features/resources/pages/ResourceListPage.jsx +++ b/src/features/resources/pages/ResourceListPage.jsx @@ -6,6 +6,10 @@ import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined"; import RefreshOutlined from "@mui/icons-material/RefreshOutlined"; import Checkbox from "@mui/material/Checkbox"; import CircularProgress from "@mui/material/CircularProgress"; +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; import LinearProgress from "@mui/material/LinearProgress"; import MenuItem from "@mui/material/MenuItem"; import { AdminSwitch } from "@/shared/ui/AdminSwitch.jsx"; @@ -21,7 +25,7 @@ import { import { DataState } from "@/shared/ui/DataState.jsx"; import { DataTable } from "@/shared/ui/DataTable.jsx"; import { UploadField } from "@/shared/ui/UploadField.jsx"; -import { useConfirm } from "@/shared/ui/ConfirmProvider.jsx"; +import { Button } from "@/shared/ui/Button.jsx"; import { AdminActionIconButton, AdminListBody, @@ -132,13 +136,13 @@ const baseColumns = [ export function ResourceListPage() { const page = useResourceListPage(); - const confirm = useConfirm(); const { showToast } = useToast(); const folderInputRef = useRef(null); const [batchOpen, setBatchOpen] = useState(false); const [batchPlan, setBatchPlan] = useState({ errors: [], resources: [] }); const [batchProgress, setBatchProgress] = useState(emptyBatchProgress); const [batchMp4Progress, setBatchMp4Progress] = useState(emptyBatchProgress); + const [mp4BatchResult, setMp4BatchResult] = useState(null); const [selectedResourceIds, setSelectedResourceIds] = useState([]); const rawItems = page.data.items; const items = useMemo(() => rawItems || [], [rawItems]); @@ -370,7 +374,7 @@ export function ResourceListPage() { } else { layoutCounts.transparent += 1; } - updates.push({ metadataJson, resourceId: resource.resourceId }); + updates.push({ layout: confirmedLayout, metadataJson, resource, resourceId: resource.resourceId }); } catch (err) { failures.push(`${resource.name || resource.resourceCode || resource.resourceId}: ${err.message || "解析失败"}`); } @@ -384,19 +388,20 @@ export function ResourceListPage() { setBatchMp4Progress(emptyBatchProgress); return; } - const accepted = await confirm({ - confirmText: "写入", - message: `检测完成:普通 MP4 ${layoutCounts.normal} 个,透明 MP4 ${layoutCounts.transparent} 个,非 MP4 自动忽略 ${ignoredCount} 个。`, - title: "写入 MP4 布局", - }); - if (!accepted) { - setBatchMp4Progress(emptyBatchProgress); + setBatchMp4Progress(emptyBatchProgress); + setMp4BatchResult({ ignoredCount, layoutCounts, rows: updates }); + }; + + const writeMp4BatchResult = async () => { + const rows = mp4BatchResult?.rows || []; + if (!rows.length || batchMp4Running) { return; } try { - setBatchMp4Progress({ label: `保存 MP4 布局 ${updates.length} 个`, running: true, value: 90 }); - await updateResourceMp4Layouts(updates); - showToast(`已更新 ${updates.length} 个 MP4 布局`, "success"); + setBatchMp4Progress({ label: `保存 MP4 布局 ${rows.length} 个`, running: true, value: 90 }); + await updateResourceMp4Layouts(rows.map(({ metadataJson, resourceId }) => ({ metadataJson, resourceId }))); + showToast(`已更新 ${rows.length} 个 MP4 布局`, "success"); + setMp4BatchResult(null); setSelectedResourceIds([]); await page.reload(); } catch (err) { @@ -582,6 +587,13 @@ export function ResourceListPage() { onFileChange={handleFolderChange} onSubmit={submitBatchUpload} /> + setMp4BatchResult(null)} + onSubmit={writeMp4BatchResult} + /> ); } @@ -807,6 +819,68 @@ function BatchMp4LayoutCell({ onChangeFrame, onChangeLayout, onConfirm, resource ); } +function Mp4BatchResultDialog({ loading, onClose, onSubmit, open, result }) { + const rows = result?.rows || []; + const layoutCounts = result?.layoutCounts || {}; + + return ( + + 写入 MP4 布局 + +
+ 检测完成:普通 MP4 {layoutCounts.normal || 0} 个,透明 MP4 {layoutCounts.transparent || 0} 个,非 + MP4 自动忽略 {result?.ignoredCount || 0} 个。 +
+
+ + + + + + + + + + + + + + + {rows.map(({ layout, resource, resourceId }) => ( + + + + + + + + + + + ))} + +
ID资源类型布局视频宽高RGB 区域Alpha 区域置信度
{resourceId} + + {resource?.name || resource?.resourceCode || resourceId} + + {resource?.resourceCode || "-"} + {resourceTypeLabels[resource?.resourceType] || resource?.resourceType || "-"}{mp4LayoutLabel(layout)} + {layout?.video_w || "-"}x{layout?.video_h || "-"} + {frameText(layout?.rgb_frame) || "-"}{frameText(layout?.alpha_frame) || "-"}{formatConfidence(layout?.confidence)}
+
+
+ + + + +
+ ); +} + function ResourceRowActions({ page, resource }) { const { showToast } = useToast(); const [recalculating, setRecalculating] = useState(false); @@ -853,6 +927,14 @@ function ResourceRowActions({ page, resource }) { ); } +function formatConfidence(value) { + const confidence = Number(value || 0); + if (!Number.isFinite(confidence)) { + return "0"; + } + return confidence.toFixed(3); +} + function mp4ResourceAnimationSource(resource) { const source = String(resource?.animationUrl || resource?.assetUrl || "").trim(); return isMp4Source(source) ? source : ""; diff --git a/src/features/resources/resources.module.css b/src/features/resources/resources.module.css index 55442c1..ae72989 100644 --- a/src/features/resources/resources.module.css +++ b/src/features/resources/resources.module.css @@ -404,6 +404,64 @@ cursor: not-allowed; } +.mp4ResultSummary { + margin-bottom: var(--space-3); + color: var(--text-primary); + font-weight: 650; + line-height: 1.6; +} + +.mp4ResultTableWrap { + overflow: auto; + border: 1px solid var(--border); + border-radius: var(--radius-sm); +} + +.mp4ResultTable { + width: 100%; + min-width: 980px; + border-collapse: collapse; + font-size: var(--admin-font-size); +} + +.mp4ResultTable th, +.mp4ResultTable td { + padding: 10px 12px; + border-bottom: 1px solid var(--border-muted); + color: var(--text-primary); + text-align: left; + vertical-align: top; + white-space: nowrap; +} + +.mp4ResultTable th { + background: var(--bg-card-strong); + color: var(--text-secondary); + font-weight: 750; +} + +.mp4ResultTable tbody tr:last-child td { + border-bottom: 0; +} + +.mp4ResultResource, +.mp4ResultCode { + display: block; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; +} + +.mp4ResultResource { + color: var(--text-primary); + font-weight: 650; +} + +.mp4ResultCode { + margin-top: 2px; + color: var(--text-tertiary); +} + .batchTableHead { border-bottom: 1px solid var(--border); background: var(--bg-card-strong);