feat(review): 全站统一「审核盾」徽章 + 灰盾点击手动提交兜底

火山人像审核状态此前三处文案各异(已审核/通过/审核✓…)。统一成一个共享 ReviewBadge:
全色填充盾(无对勾叉)+ 文案 + hover 气泡说明,4 态:
  🟢已过审(active) / 🔴未过审(failed) / 🟠审核中(processing,橙盾光扫动画) / ⚪待审核(空)
灰盾(未送审)可点 → 提交审核(兜底,不加固定按钮):后端新增 POST /api/assets/{id}/submit-review/
(团队级,仅送审三类),前端点击乐观置 processing。气泡组件顺带成为全站首个统一 tooltip。

- 后端 submit-review 端点 + 单测(送审类 200 / 非送审类 400)
- 共享 components/review-badge.{tsx,css}(盾用 SVG 蒙版填色 + 橙态 rv-sweep 光扫)
- 资产库详情弹窗 / 商品详情素材组弹窗 / 视频流程基础资产 三处全换成它(各自接 onSubmit)

验收:assets 16 测绿;tsc+build 绿;无头核对四态(灰可点+气泡/绿已过审/橙审核中不可点)、0 console error
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-21 22:46:51 +08:00
parent 9108b7679d
commit 708b9ea572
9 changed files with 242 additions and 11 deletions
+21 -6
View File
@@ -1,9 +1,10 @@
import { useEffect, useMemo, useRef, useState } from "react";
import type { FormEvent, ReactNode } from "react";
import { createPortal } from "react-dom";
import { Download, Image as ImageIcon, Images, Info, LayoutGrid, Music, Shield, Trash2, Upload, X } from "lucide-react";
import { Download, Image as ImageIcon, Images, Info, LayoutGrid, Music, Trash2, Upload, X } from "lucide-react";
import { api } from "../api";
import { SkeletonGrid } from "../components/loading";
import { ReviewBadge, type ReviewStatus } from "../components/review-badge";
import type { Asset } from "../types";
import { ConfirmModal, MediaLightbox, useBodyScrollLock, useOverlayTransition } from "../components/overlays";
import { Pager } from "../components/pager";
@@ -130,8 +131,22 @@ function AssetDetailModal({ asset, close, onZoom }: {
{ k: "入库时间", v: createdAt }
];
// 人物审核盾:仅真人有 review_status(processing/active 绿/failed 红)
const review = asset.review_status;
// 审核盾:仅送审类(角色/三视图/分镜)显示;灰盾可点提交。本地覆盖提交后即时反映,切资产清空
const REVIEW_CATS = ["person", "tri_view", "storyboard"];
const [reviewOverride, setReviewOverride] = useState<string | null>(null);
const [submittingReview, setSubmittingReview] = useState(false);
useEffect(() => { setReviewOverride(null); }, [asset?.id]);
const review = (reviewOverride ?? asset.review_status ?? "") as ReviewStatus;
async function submitReview() {
if (!asset) return;
setSubmittingReview(true);
try {
const r = await api.submitAssetReview(asset.id);
setReviewOverride(r.review_status || "processing");
} catch { /* 提交失败保持灰盾,用户可再点 */ } finally {
setSubmittingReview(false);
}
}
function renderLead(): ReactNode {
if (leadSrc && isVideo) {
@@ -151,9 +166,9 @@ function AssetDetailModal({ asset, close, onZoom }: {
<div className="adx-h">
<h2>{asset.name || "资产详情"}</h2>
<span className="adx-kind">/ {kindLabel}</span>
{review === "active" && <span className="pill ok adx-shield"><Shield size={11} /> 已审核</span>}
{review === "processing" && <span className="pill info adx-shield">审核中</span>}
{review === "failed" && <span className="pill err adx-shield">审核未过</span>}
{REVIEW_CATS.includes(asset.category) && (
<ReviewBadge status={review} error={asset.review_error} busy={submittingReview} onSubmit={submitReview} />
)}
<button className="x adx-x" type="button" onClick={close} aria-label="关闭"><X size={14} /></button>
</div>