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>
This commit is contained in:
parent
5f30258112
commit
a1c16be1ea
@ -1818,6 +1818,7 @@ def admin_records_view(request):
|
||||
'duration': r.duration,
|
||||
'seed': r.seed,
|
||||
'ark_task_id': r.ark_task_id or '',
|
||||
'result_url': r.result_url or '',
|
||||
})
|
||||
|
||||
return Response({
|
||||
@ -1882,6 +1883,7 @@ def team_records_view(request):
|
||||
'duration': r.duration,
|
||||
'seed': r.seed,
|
||||
'ark_task_id': r.ark_task_id or '',
|
||||
'result_url': r.result_url or '',
|
||||
})
|
||||
|
||||
return Response({
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { AdminRecord } from '../types';
|
||||
import { ReferenceList } from './ReferenceList';
|
||||
import { rewriteTosUrl } from '../lib/api';
|
||||
|
||||
const STATUS_MAP: Record<string, { label: string; color: string; bg: string }> = {
|
||||
completed: { label: '已完成', color: 'var(--color-success)', bg: 'var(--color-success-bg)' },
|
||||
@ -43,54 +44,63 @@ export function RecordDetailModal({ record: r, onClose, showTeam, showCost }: Pr
|
||||
<button style={closeBtn} onClick={onClose}>✕</button>
|
||||
</div>
|
||||
|
||||
{/* Body — 左视频 / 右信息 双栏 */}
|
||||
<div style={body}>
|
||||
{/* Status */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ ...statusBadge, color: st.color, background: st.bg }}>{st.label}</span>
|
||||
{/* ── 左:视频 ── */}
|
||||
<div style={mediaPanel}>
|
||||
<MediaArea record={r} />
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{r.status === 'failed' && r.error_message && (
|
||||
<div style={errorBox}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 4 }}>失败原因</div>
|
||||
<div>{r.error_message}</div>
|
||||
{r.raw_error && r.raw_error !== r.error_message && (
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--color-text-tertiary)', fontFamily: 'monospace', wordBreak: 'break-all' }}>
|
||||
原始错误:{r.raw_error}
|
||||
</div>
|
||||
)}
|
||||
{/* ── 右:信息(原有内容整体搬过来,排版不动)── */}
|
||||
<div style={infoPanel}>
|
||||
{/* Status */}
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<span style={{ ...statusBadge, color: st.color, background: st.bg }}>{st.label}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info Grid */}
|
||||
<div style={sectionTitle}>基本信息</div>
|
||||
<div style={infoGrid}>
|
||||
{r.ark_task_id && <InfoItem label="任务ID" value={r.ark_task_id} />}
|
||||
{r.username && <InfoItem label="用户" value={r.username} />}
|
||||
{showTeam && r.team_name && <InfoItem label="团队" value={r.team_name} />}
|
||||
<InfoItem label="提交时间" value={new Date(r.created_at).toLocaleString('zh-CN')} />
|
||||
<InfoItem label="耗时" value={elapsed} />
|
||||
<InfoItem label="模型" value={r.model === 'seedance_2.0_fast' ? 'AirDrama Fast' : 'AirDrama'} />
|
||||
<InfoItem label="模式" value={MODE_MAP[r.mode] || r.mode} />
|
||||
<InfoItem label="比例" value={r.aspect_ratio || '-'} />
|
||||
<InfoItem label="分辨率" value={r.resolution ? r.resolution.toUpperCase() : '-'} />
|
||||
<InfoItem label="时长" value={r.duration != null ? `${r.duration}秒` : '-'} />
|
||||
<InfoItem label="Tokens" value={(r.tokens_consumed || 0).toLocaleString()} />
|
||||
{showCost && <InfoItem label="费用" value={`¥${(r.cost_amount || 0).toFixed(2)}`} />}
|
||||
{r.seed != null && r.seed !== -1 && <InfoItem label="种子值" value={String(r.seed)} />}
|
||||
{/* Error */}
|
||||
{r.status === 'failed' && r.error_message && (
|
||||
<div style={errorBox}>
|
||||
<div style={{ fontWeight: 500, marginBottom: 4 }}>失败原因</div>
|
||||
<div>{r.error_message}</div>
|
||||
{r.raw_error && r.raw_error !== r.error_message && (
|
||||
<div style={{ marginTop: 8, fontSize: 11, color: 'var(--color-text-tertiary)', fontFamily: 'monospace', wordBreak: 'break-all' }}>
|
||||
原始错误:{r.raw_error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info Grid */}
|
||||
<div style={sectionTitle}>基本信息</div>
|
||||
<div style={infoGrid}>
|
||||
{r.ark_task_id && <InfoItem label="任务ID" value={r.ark_task_id} />}
|
||||
{r.username && <InfoItem label="用户" value={r.username} />}
|
||||
{showTeam && r.team_name && <InfoItem label="团队" value={r.team_name} />}
|
||||
<InfoItem label="提交时间" value={new Date(r.created_at).toLocaleString('zh-CN')} />
|
||||
<InfoItem label="耗时" value={elapsed} />
|
||||
<InfoItem label="模型" value={r.model === 'seedance_2.0_fast' ? 'AirDrama Fast' : 'AirDrama'} />
|
||||
<InfoItem label="模式" value={MODE_MAP[r.mode] || r.mode} />
|
||||
<InfoItem label="比例" value={r.aspect_ratio || '-'} />
|
||||
<InfoItem label="分辨率" value={r.resolution ? r.resolution.toUpperCase() : '-'} />
|
||||
<InfoItem label="时长" value={r.duration != null ? `${r.duration}秒` : '-'} />
|
||||
<InfoItem label="Tokens" value={(r.tokens_consumed || 0).toLocaleString()} />
|
||||
{showCost && <InfoItem label="费用" value={`¥${(r.cost_amount || 0).toFixed(2)}`} />}
|
||||
{r.seed != null && r.seed !== -1 && <InfoItem label="种子值" value={String(r.seed)} />}
|
||||
</div>
|
||||
|
||||
{/* Prompt */}
|
||||
<div style={sectionTitle}>提示词</div>
|
||||
<div style={promptBox}>{r.prompt || '(无提示词)'}</div>
|
||||
|
||||
{/* References */}
|
||||
{refs.length > 0 && (
|
||||
<>
|
||||
<div style={sectionTitle}>参考素材({refs.length})</div>
|
||||
<ReferenceList references={refs} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Prompt */}
|
||||
<div style={sectionTitle}>提示词</div>
|
||||
<div style={promptBox}>{r.prompt || '(无提示词)'}</div>
|
||||
|
||||
{/* References */}
|
||||
{refs.length > 0 && (
|
||||
<>
|
||||
<div style={sectionTitle}>参考素材({refs.length})</div>
|
||||
<ReferenceList references={refs} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -98,6 +108,63 @@ export function RecordDetailModal({ record: r, onClose, showTeam, showCost }: Pr
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 左侧媒体区 — 根据任务状态决定显示什么:
|
||||
* - completed + result_url → 视频播放器(controls,不自动播放)
|
||||
* - completed - result_url → "视频已生成"占位
|
||||
* - failed → RGB 故障字 "生成失败" + 错误原因摘要 + 斜纹底纹
|
||||
* - processing / queued → 旋转 spinner + 文字
|
||||
*/
|
||||
function MediaArea({ record: r }: { record: AdminRecord }) {
|
||||
return (
|
||||
<div style={mediaFrame}>
|
||||
{r.status === 'completed' && r.result_url ? (
|
||||
<video
|
||||
src={rewriteTosUrl(r.result_url)}
|
||||
style={mediaVideo}
|
||||
controls
|
||||
preload="metadata"
|
||||
/>
|
||||
) : r.status === 'completed' ? (
|
||||
<div style={mediaPlaceholder}>
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2"/>
|
||||
</svg>
|
||||
<span>视频已生成</span>
|
||||
</div>
|
||||
) : r.status === 'failed' ? (
|
||||
<FailureGlitch errorMessage={r.error_message} />
|
||||
) : (
|
||||
<div style={mediaPlaceholder}>
|
||||
<svg className="spinner" width="44" height="44" viewBox="0 0 50 50" style={spinnerStyle}>
|
||||
<circle cx="25" cy="25" r="20" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeDasharray="80 40" />
|
||||
</svg>
|
||||
<span>{r.status === 'queued' ? '排队中' : '生成中'}</span>
|
||||
{/* 内联 keyframes — 没有 module.css */}
|
||||
<style>{`@keyframes shoot-spin { to { transform: rotate(360deg); } }`}</style>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* RGB 故障字失败态 — "生成失败"主标题用 cyan/magenta text-shadow 偏移
|
||||
* 模拟坏掉的 CRT 信号丢失;副标题等宽字体显示错误摘要。
|
||||
*/
|
||||
function FailureGlitch({ errorMessage }: { errorMessage?: string }) {
|
||||
const msg = (errorMessage || 'Generation failed').slice(0, 80);
|
||||
return (
|
||||
<div style={failBg}>
|
||||
<div style={failStripe} aria-hidden="true" />
|
||||
<div style={glitchTitle}>生成失败</div>
|
||||
<div style={glitchSub}>{msg}</div>
|
||||
<div style={{ ...failStripe, top: 'auto', bottom: 0 }} aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InfoItem({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div style={{ minWidth: 0 }}>
|
||||
@ -107,45 +174,181 @@ function InfoItem({ label, value }: { label: string; value: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────
|
||||
// Styles
|
||||
// ─────────────────────────────────────
|
||||
|
||||
const overlay: React.CSSProperties = {
|
||||
position: 'fixed', inset: 0, background: 'var(--color-modal-overlay)', display: 'flex',
|
||||
alignItems: 'center', justifyContent: 'center', zIndex: 10000,
|
||||
};
|
||||
|
||||
const modal: React.CSSProperties = {
|
||||
background: 'var(--color-bg-modal-glass)',
|
||||
backdropFilter: 'blur(24px) saturate(180%)',
|
||||
WebkitBackdropFilter: 'blur(24px) saturate(180%)',
|
||||
border: '1px solid var(--color-border-modal-soft)',
|
||||
borderRadius: 12,
|
||||
width: 560, maxHeight: '80vh', display: 'flex', flexDirection: 'column',
|
||||
width: 1080,
|
||||
maxWidth: '95vw',
|
||||
maxHeight: '85vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
boxShadow: 'var(--shadow-glass-light)',
|
||||
};
|
||||
|
||||
const header: React.CSSProperties = {
|
||||
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
|
||||
padding: '16px 20px', borderBottom: '1px solid var(--color-border-modal)',
|
||||
padding: '16px 24px', borderBottom: '1px solid var(--color-border-modal-soft)',
|
||||
flexShrink: 0,
|
||||
};
|
||||
|
||||
const closeBtn: React.CSSProperties = {
|
||||
background: 'none', border: 'none', color: 'var(--color-text-tertiary)', fontSize: 16, cursor: 'pointer',
|
||||
padding: '4px 8px', borderRadius: 4,
|
||||
};
|
||||
|
||||
const body: React.CSSProperties = {
|
||||
padding: 20, overflowY: 'auto', flex: 1,
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
gap: 24,
|
||||
padding: 24,
|
||||
overflow: 'hidden', /* infoPanel 内部滚动 */
|
||||
minHeight: 0,
|
||||
};
|
||||
|
||||
/* 左侧媒体区 — 固定 480 宽 */
|
||||
const mediaPanel: React.CSSProperties = {
|
||||
width: 480,
|
||||
flexShrink: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
const mediaFrame: React.CSSProperties = {
|
||||
width: '100%',
|
||||
aspectRatio: '16 / 9',
|
||||
maxHeight: 360,
|
||||
background: 'var(--color-bg-video)',
|
||||
borderRadius: 12,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '1px solid var(--color-border-modal-soft)',
|
||||
};
|
||||
|
||||
const mediaVideo: React.CSSProperties = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'contain',
|
||||
display: 'block',
|
||||
};
|
||||
|
||||
const mediaPlaceholder: React.CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 12,
|
||||
color: 'var(--color-text-on-glass-soft)',
|
||||
fontSize: 13,
|
||||
};
|
||||
|
||||
const spinnerStyle: React.CSSProperties = {
|
||||
color: 'var(--color-primary)',
|
||||
animation: 'shoot-spin 1s linear infinite',
|
||||
};
|
||||
|
||||
/* ── 失败态:RGB 故障字 + 斜纹底 ── */
|
||||
const failBg: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 18,
|
||||
/* 半透明红斜纹 — 仿 CRT 信号丢失 */
|
||||
background: `
|
||||
repeating-linear-gradient(135deg,
|
||||
var(--color-danger-bg-soft) 0,
|
||||
var(--color-danger-bg-soft) 14px,
|
||||
transparent 14px,
|
||||
transparent 22px),
|
||||
var(--color-bg-elevated)
|
||||
`,
|
||||
};
|
||||
|
||||
const failStripe: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
top: 0, left: 0, right: 0,
|
||||
height: 6,
|
||||
background: `repeating-linear-gradient(90deg,
|
||||
var(--color-danger) 0,
|
||||
var(--color-danger) 6px,
|
||||
var(--color-info) 6px,
|
||||
var(--color-info) 8px,
|
||||
transparent 8px,
|
||||
transparent 14px)`,
|
||||
opacity: 0.6,
|
||||
};
|
||||
|
||||
const glitchTitle: React.CSSProperties = {
|
||||
fontSize: 44,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '0.12em',
|
||||
color: 'var(--color-danger)',
|
||||
fontFamily: "'Space Grotesk', 'JetBrains Mono', ui-monospace, monospace",
|
||||
/* RGB 偏移:左 cyan 右 magenta */
|
||||
textShadow: `
|
||||
-2px 0 var(--color-info),
|
||||
2px 0 #ff00aa,
|
||||
0 0 30px var(--color-danger-bg)
|
||||
`,
|
||||
textAlign: 'center',
|
||||
userSelect: 'none',
|
||||
};
|
||||
|
||||
const glitchSub: React.CSSProperties = {
|
||||
fontSize: 12,
|
||||
color: 'var(--color-danger)',
|
||||
opacity: 0.85,
|
||||
fontFamily: "'JetBrains Mono', ui-monospace, monospace",
|
||||
maxWidth: '85%',
|
||||
textAlign: 'center',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: 1.6,
|
||||
};
|
||||
|
||||
/* 右侧信息区 — 现有内容整体搬过来 */
|
||||
const infoPanel: React.CSSProperties = {
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
overflowY: 'auto',
|
||||
paddingRight: 4, /* 给滚动条让位 */
|
||||
};
|
||||
|
||||
const statusBadge: React.CSSProperties = {
|
||||
padding: '4px 12px', borderRadius: 6, fontSize: 13, fontWeight: 500,
|
||||
};
|
||||
|
||||
const errorBox: React.CSSProperties = {
|
||||
background: 'var(--color-danger-bg-soft)', border: '1px solid var(--color-danger-border)',
|
||||
borderRadius: 8, padding: 12, marginBottom: 16, fontSize: 13, color: 'var(--color-danger)',
|
||||
};
|
||||
|
||||
const sectionTitle: React.CSSProperties = {
|
||||
fontSize: 12, color: 'var(--color-text-tertiary)', fontWeight: 500, marginBottom: 8, marginTop: 16,
|
||||
textTransform: 'uppercase', letterSpacing: 1,
|
||||
};
|
||||
|
||||
const infoGrid: React.CSSProperties = {
|
||||
display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '12px 16px',
|
||||
};
|
||||
|
||||
const promptBox: React.CSSProperties = {
|
||||
background: 'var(--color-bg-elevated)', borderRadius: 8, padding: 12, fontSize: 13,
|
||||
color: 'var(--color-text-monochrome)', lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-all',
|
||||
|
||||
@ -217,6 +217,7 @@ export interface AdminRecord {
|
||||
duration?: number;
|
||||
seed?: number;
|
||||
ark_task_id?: string;
|
||||
result_url?: string;
|
||||
}
|
||||
|
||||
export interface SystemSettings {
|
||||
|
||||
105
web/test/modal-preview.mjs
Normal file
105
web/test/modal-preview.mjs
Normal file
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* 一次性脚本:截 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); });
|
||||
Loading…
x
Reference in New Issue
Block a user