feat(admin): Phase 5 火山人像审核队列 — 跨团队 person 资产审核(列/筛/批量送审/轮询)

后端:adminpanel asset-reviews(列跨团队 person + 状态筛分页)+ submit(批量送审/重试)+ poll(轮询 processing),
复用 assets/review.py,IsPlatformAdmin + 审计;AdminReviewAssetSerializer;修 N+1(prefetch 缓存取首图,列表 5s→1s)。
前端:adminApi 审核系列;Admin 资产审核页(状态筛 + 多选表格 + 绿/红/灰状态盾 + 重试 + 吸底 bulk-bar 批量送审 + 刷新状态)。
测试:adminpanel 28 单测过(mock 火山:队列只列 person/状态筛/批量送审计数/poll 只轮 processing);
无头 e2e _admin-p5.mjs 5 断言过 + 0 console error(只读 poll,不点真送审);tsc+build 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
seaislee1209
2026-06-19 22:07:11 +08:00
co-authored by Claude Opus 4.8
parent 01d4f235ed
commit 5aab8a568f
11 changed files with 494 additions and 0 deletions
+75
View File
@@ -0,0 +1,75 @@
// Phase 5 e2e:火山人像审核队列 —— 队列加载、状态盾、筛选、多选 bulk-bar、刷新状态(只读 poll)。
// ⚠ live 库 Volcano 已启用:e2e 只做只读 poll + UI 在场校验,绝不点真送审(送审逻辑由后端 mock 单测覆盖)。
// wrapper 预建一条 person 资产保证有行,跑完删除。
import { chromium } from "playwright";
import fs from "node:fs";
import path from "node:path";
const BASE = process.env.BASE || "http://127.0.0.1:5188";
const API = process.env.API || "http://127.0.0.1:8010";
const OUT = path.resolve("output/admin");
fs.mkdirSync(OUT, { recursive: true });
async function apiLogin(u, p) {
const res = await fetch(`${API}/api/auth/login/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: u, password: p }) });
return res.json();
}
const admin = await apiLogin("admin", "admin123");
const r = { page: {}, select: {}, poll: {}, consoleErrors: [], pass: false };
const browser = await chromium.launch({ headless: true });
const hook = (p, tag) => {
p.on("console", (m) => { if (m.type() === "error") r.consoleErrors.push(`${tag}:${m.text()}`); });
p.on("pageerror", (e) => r.consoleErrors.push(`${tag}:PAGEERR:${e.message}`));
};
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
await ctx.addInitScript((tk) => localStorage.setItem("airshelf_token", tk), admin.token);
const p = await ctx.newPage(); hook(p, "reviews");
await p.goto(BASE + "/admin/reviews", { waitUntil: "load" });
await p.waitForSelector(".admin-app", { timeout: 12000 });
// 等真表格(加载态也用 .empty-state.show,不能用它判完成);no rows 时退而求其次等非加载空态
await p.waitForFunction(() => {
const t = document.querySelector(".admin-table");
const h3 = document.querySelector(".empty-state.show h3");
return Boolean(t) || (h3 && h3.textContent !== "加载中…");
}, { timeout: 20000 }).catch(() => {});
r.page.title = (await p.locator(".content h1").first().innerText().catch(() => "")).trim();
r.page.tabs = await p.locator(".tabs-sub .tab-sub").count();
r.page.refreshBtn = (await p.locator(".page-head .actions .btn").count()) >= 1;
r.page.rows = await p.locator(".admin-table tbody tr").count();
await p.screenshot({ path: path.join(OUT, "p5-reviews-list.png"), fullPage: true });
// 多选 → bulk-bar 出现 + 批量送审在场(不点击,避免真送审)
if (r.page.rows >= 1) {
await p.locator(".admin-table tbody tr:first-child .col-check input").check();
await p.waitForTimeout(300);
r.select.bulkBar = (await p.locator(".bulk-bar").count()) >= 1;
r.select.submitBtn = (await p.locator(".bulk-bar .btn-primary").count()) >= 1;
r.select.submitEnabled = await p.locator(".bulk-bar .btn-primary").isEnabled().catch(() => false);
await p.screenshot({ path: path.join(OUT, "p5-bulkbar.png") });
// 取消选择(避免后面误触)
await p.locator(".bulk-bar .btn").first().click();
await p.waitForTimeout(200);
}
// 刷新状态(只读 poll,安全)
await p.locator(".page-head .actions .btn").click();
await p.waitForTimeout(1200);
r.poll.clicked = true;
await ctx.close();
await browser.close();
const checks = {
pageLoads: r.page.title === "资产审核" && r.page.tabs === 5 && r.page.refreshBtn === true,
hasRows: r.page.rows >= 1,
bulkBarWorks: r.select.bulkBar === true && r.select.submitBtn === true && r.select.submitEnabled === true,
pollClicked: r.poll.clicked === true,
zeroConsoleErrors: r.consoleErrors.length === 0,
};
r.checks = checks;
r.pass = Object.values(checks).every(Boolean);
console.log(JSON.stringify(r, null, 2));
fs.writeFileSync(path.join(OUT, "p5-summary.json"), JSON.stringify(r, null, 2));
process.exit(r.pass ? 0 : 1);