需求: 消费记录 detail modal 加多一个视频展示框
- completed + result_url → 视频播放器
- failed → 失败图标 (RGB 故障字风格)
- processing/queued → spinner
现有信息排版整体搬到右侧,布局不动。
后端 (apps/generation/views.py):
admin_records_view + team_records_view 返回字段加 'result_url'
(model 早就有 result_url, 只是这俩 list API 没暴露)
前端类型 (web/src/types/index.ts):
AdminRecord 加 result_url?: string
RecordDetailModal 重构 (web/src/components/RecordDetailModal.tsx):
- 弹窗宽 560 → 1080 (maxWidth 95vw, maxHeight 85vh)
- body display: flex 双栏
- 左侧 480 固定宽 + 16:9 视频框 + object-fit: contain
(沿用 GenerationCard.resultArea 同一套 sizing 思路,
不同长宽比视频用 contain 居中 + 黑边补足)
- 右侧 flex 1, overflow-y auto, 现有内容整体搬过去排版不动
- 视频用 controls + preload="metadata", 不自动播放, 跟全局音量走
MediaArea 组件分支:
- completed + result_url: <video controls />
- completed - result_url: 视频 icon + "视频已生成" 占位
- failed: FailureGlitch RGB 故障字
- processing/queued: 旋转 spinner + 文字
FailureGlitch 视觉细节:
- 标题 "生成失败" 44px Space Grotesk weight 700
text-shadow: -2px 0 var(--info) cyan, 2px 0 #ff00aa magenta
+ 30px 红色 glow → 模拟 CRT RGB 信号偏移
- 副标题: 错误原因摘要 (truncate 80 char) 等宽字体
- 背景: 红色斜纹 + 顶/底彩色条纹 (red/cyan 间隔) 仿信号丢失
验证 (docs/screenshots/v2/modal/):
completed__{dark,light}.png - 视频框 + 右侧信息
failed__{dark,light}.png - RGB 故障字
TS 编译过, backend 已重启
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
106 lines
4.0 KiB
JavaScript
106 lines
4.0 KiB
JavaScript
/**
|
||
* 一次性脚本:截 RecordDetailModal 双栏新版 — 成功态 / 失败态 × 深/浅 = 4 张
|
||
*/
|
||
import { chromium } from '@playwright/test';
|
||
import { mkdir } from 'node:fs/promises';
|
||
import { resolve, dirname } from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||
const OUT = resolve(__dirname, '../../docs/screenshots/v2/modal');
|
||
const BASE = 'http://localhost:5173';
|
||
const API = 'http://localhost:8000';
|
||
|
||
async function loginAdmin(page) {
|
||
const res = await page.request.post(`${API}/api/v1/auth/login`, {
|
||
data: { username: 'admin', password: 'admin123' },
|
||
});
|
||
const body = await res.json();
|
||
await page.goto(`${BASE}/login`, { waitUntil: 'domcontentloaded' });
|
||
await page.evaluate(({ access, refresh, user }) => {
|
||
localStorage.setItem('access_token', access);
|
||
if (refresh) localStorage.setItem('refresh_token', refresh);
|
||
if (user) localStorage.setItem('user', JSON.stringify(user));
|
||
}, { access: body?.tokens?.access, refresh: body?.tokens?.refresh, user: body?.user });
|
||
}
|
||
|
||
async function setTheme(page, theme) {
|
||
await page.evaluate((t) => {
|
||
localStorage.setItem('airdrama-theme', t);
|
||
document.documentElement.dataset.theme = t;
|
||
}, theme);
|
||
}
|
||
|
||
async function findRowByStatus(page, statusText) {
|
||
// 表格行里有 "已完成" / "失败" 文字的 row,点 username 链接打开 modal
|
||
const rows = page.locator('tr').filter({ hasText: statusText });
|
||
const count = await rows.count();
|
||
if (count === 0) return null;
|
||
// 找一行的 username 链接(usernameLink 触发 detail modal)
|
||
return rows.first();
|
||
}
|
||
|
||
async function main() {
|
||
await mkdir(OUT, { recursive: true });
|
||
const browser = await chromium.launch({ headless: true });
|
||
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
||
const page = await ctx.newPage();
|
||
page.on('console', (msg) => {
|
||
if (msg.type() === 'error' && !/401|404|Failed to load|DevTools/.test(msg.text())) {
|
||
console.log(' [console]', msg.text());
|
||
}
|
||
});
|
||
|
||
console.log('login admin...');
|
||
await loginAdmin(page);
|
||
|
||
for (const theme of ['dark', 'light']) {
|
||
await setTheme(page, theme);
|
||
await page.goto(`${BASE}/admin/records`, { waitUntil: 'domcontentloaded' });
|
||
await page.waitForTimeout(1500);
|
||
|
||
// 找成功行 → 点击 username 链接 / 详情按钮打开 modal
|
||
const completedRow = await findRowByStatus(page, '已完成');
|
||
if (completedRow) {
|
||
// RecordsPage tr 自带 onClick → 整行点击
|
||
await completedRow.click({ force: true }).catch(() => {});
|
||
await page.waitForTimeout(1200);
|
||
const modalOpen = await page.locator('text=任务详情').first().isVisible().catch(() => false);
|
||
if (modalOpen) {
|
||
await page.screenshot({ path: resolve(OUT, `completed__${theme}.png`) });
|
||
console.log(` ✓ completed__${theme}.png`);
|
||
// 关闭
|
||
await page.locator('button:has-text("✕")').first().click().catch(() => {});
|
||
await page.waitForTimeout(400);
|
||
} else {
|
||
console.log(` ✗ completed__${theme} — modal 没打开`);
|
||
}
|
||
} else {
|
||
console.log(` - 没找到 已完成 行 (${theme})`);
|
||
}
|
||
|
||
// 找失败行
|
||
const failedRow = await findRowByStatus(page, '失败');
|
||
if (failedRow) {
|
||
await failedRow.click({ force: true }).catch(() => {});
|
||
await page.waitForTimeout(1200);
|
||
const modalOpen = await page.locator('text=任务详情').first().isVisible().catch(() => false);
|
||
if (modalOpen) {
|
||
await page.screenshot({ path: resolve(OUT, `failed__${theme}.png`) });
|
||
console.log(` ✓ failed__${theme}.png`);
|
||
await page.locator('button:has-text("✕")').first().click().catch(() => {});
|
||
await page.waitForTimeout(400);
|
||
} else {
|
||
console.log(` ✗ failed__${theme} — modal 没打开`);
|
||
}
|
||
} else {
|
||
console.log(` - 没找到 失败 行 (${theme})`);
|
||
}
|
||
}
|
||
|
||
await browser.close();
|
||
console.log('\n✅ done — see docs/screenshots/v2/modal/');
|
||
}
|
||
|
||
main().catch(e => { console.error(e); process.exit(1); });
|