video-shuoshan/web/test/modal-preview.mjs
seaislee1209 a1c16be1ea feat(records): 任务详情弹窗加视频展示 + RGB 故障字失败态
需求: 消费记录 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>
2026-05-12 14:48:23 +08:00

106 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 一次性脚本:截 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); });