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
@@ -0,0 +1,42 @@
import "./review-badge.css";
// 火山人像合规审核盾(全站统一):全绿=已过审 / 全红=未过审 / 橙光扫=审核中 / 全灰=待审核。
// 灰盾(未送审)可点 → 提交审核(兜底,火山很快)。只在送审类(角色/三视图/分镜)上用。
export type ReviewStatus = "active" | "failed" | "processing" | "" | undefined;
const META: Record<string, { label: string; cls: string }> = {
active: { label: "已过审", cls: "rv-pass" },
failed: { label: "未过审", cls: "rv-fail" },
processing: { label: "审核中", cls: "rv-proc" },
"": { label: "待审核", cls: "rv-idle" }
};
export function ReviewBadge({ status, onSubmit, error, busy }: {
status: ReviewStatus;
onSubmit?: () => void; // 仅灰盾(待审核)时点击触发提交
error?: string; // 未过审原因(气泡里显)
busy?: boolean; // 提交中
}) {
const key = status || "";
const meta = META[key] || META[""];
const clickable = key === "" && !!onSubmit && !busy;
const hint =
key === "active" ? "火山人像合规审核 · 已通过,可安全用于视频生成"
: key === "failed" ? `火山人像合规审核 · 未通过${error ? " · " + error : ",建议改提示词重新生成"}`
: key === "processing" ? "火山人像合规审核 · 审核中,通常很快"
: clickable ? "尚未送审 · 点击立即提交火山人像审核"
: "尚未送审 · 含人脸的视频素材需过火山人像审核";
return (
<span
className={`review-badge ${meta.cls}${clickable ? " clickable" : ""}`}
role={clickable ? "button" : undefined}
tabIndex={clickable ? 0 : undefined}
onClick={clickable ? (e) => { e.stopPropagation(); onSubmit!(); } : undefined}
onKeyDown={clickable ? (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); e.stopPropagation(); onSubmit!(); } } : undefined}
>
<span className="rv-shield" aria-hidden="true" />
<span className="rv-label">{busy ? "提交中…" : meta.label}</span>
<span className="review-pop" role="tooltip">{hint}</span>
</span>
);
}