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
+20 -3
View File
@@ -8,6 +8,7 @@ import { money, stageOrder, statusPill } from "./stage-config";
import { CornerMarks, Decorations, Sidebar, ToastLike } from "../components/app-shell";
import { MediaLightbox, useBodyScrollLock } from "../components/overlays";
import { ActorLibrary } from "../components/actor-library";
import { ReviewBadge, type ReviewStatus } from "../components/review-badge";
import { IconKitSvg } from "../components/IconKitSvg";
// 真实资产缩略图注入:与全站一致用 --mock-media-url(.placeholder.has-mock-media 负责 cover 裁切 + 8px 圆角)
@@ -859,6 +860,17 @@ export function PipelinePage(props: {
// 火山人像审核状态(真人资产绿/红盾):本地覆盖 map(轮询刷新),回退 asset.review_status
const [reviews, setReviews] = useState<Record<string, string>>({});
const assetReview = (id?: string | null) => (id ? (reviews[id] || byId.get(id)?.review_status || "") : "");
// 手动兜底:灰盾点击 → 提交审核,乐观置 processing(下次轮询回真态)
const [reviewBusyId, setReviewBusyId] = useState<string | null>(null);
async function submitAssetReview(id: string) {
setReviewBusyId(id);
try {
const r = await api.submitAssetReview(id);
setReviews((m) => ({ ...m, [id]: r.review_status || "processing" }));
} catch { /* 失败保持灰盾可再点 */ } finally {
setReviewBusyId(null);
}
}
// 在基础资产趴(stage 2)定时轮询审核状态,刷新徽章(processing → active绿 / failed红)
useEffect(() => {
if (viewStage !== 2) return;
@@ -2805,9 +2817,14 @@ export function PipelinePage(props: {
<div className="hstack">
<strong style={{ fontSize: "13.5px", cursor: "pointer" }} onClick={() => openAssetDetail(kind, entity)}>{entity.name}</strong>
<span className="spacer"></span>
{rs === "active" && <span className="pill ok" title="火山真人审核已通过"><span className="dot"></span></span>}
{rs === "failed" && <span className="pill err" title={`审核未过:${grp.adopted_asset_review_error || byId.get(grp.adopted_asset!)?.review_error || "建议改提示词重新生成"}`}><span className="dot"></span></span>}
{rs === "processing" && <span className="pill neutral"><span className="dot"></span></span>}
{kind === "person" && grp.adopted_asset && (
<ReviewBadge
status={rs as ReviewStatus}
error={grp.adopted_asset_review_error || byId.get(grp.adopted_asset)?.review_error}
busy={reviewBusyId === grp.adopted_asset}
onSubmit={() => submitAssetReview(grp.adopted_asset!)}
/>
)}
</div>
<PromptBox key={entity.key} value={promptValue} onChange={(v) => setAssetPromptDraft((m) => ({ ...m, [entity.key]: v }))} />
<div className="hstack" style={{ marginTop: 10 }}>