/** * 界面截图工具(开发辅助) * * 用法:先起 dev server,再执行 * node scripts/screenshot.mjs [输出目录] * * 会注入一份有代表性的种子数据,分别截取看板、今日视图与移动端布局, * 便于对照审视视觉改动。 */ import { mkdir } from 'node:fs/promises' import path from 'node:path' import { chromium } from '@playwright/test' const BASE_URL = process.env.SCREENSHOT_URL ?? 'http://localhost:5199' const OUT_DIR = path.resolve(process.argv[2] ?? '.screenshots') const CHANNEL = process.env.PLAYWRIGHT_CHANNEL ?? 'chrome' /** 相对今天偏移若干天的日期 */ function dayOffset(days) { const date = new Date() date.setDate(date.getDate() + days) const month = String(date.getMonth() + 1).padStart(2, '0') const day = String(date.getDate()).padStart(2, '0') return `${date.getFullYear()}-${month}-${day}` } function iso(hoursAgo) { return new Date(Date.now() - hoursAgo * 3600 * 1000).toISOString() } function task(id, title, status, priority, extra = {}) { return { id, title, description: extra.description ?? '', status, priority, tags: extra.tags ?? [], dueDate: extra.dueDate ?? null, sortOrder: extra.sortOrder ?? 1024, createdAt: iso(72), updatedAt: iso(2), completedAt: extra.completedAt ?? (status === 'done' ? iso(20) : null), archivedAt: extra.archivedAt ?? null, } } const SEED = [ task('t1', '整理下季度 OKR 初稿', 'todo', 3, { tags: ['规划'], dueDate: dayOffset(-2), description: '先把去年 Q4 的复盘结论过一遍,再拉上两位组长对一下目标口径。', sortOrder: 1024, }), task('t2', '修复导出 JSON 时文件名重复的问题', 'todo', 3, { tags: ['前端', 'Bug'], dueDate: dayOffset(0), sortOrder: 2048, }), task('t3', '调研 IndexedDB 替换 localStorage 的可行性', 'todo', 2, { tags: ['技术调研'], dueDate: dayOffset(3), sortOrder: 3072, }), task('t4', '给看板加一个「本周完成」的统计', 'todo', 2, { tags: ['前端'], sortOrder: 4096, }), task('t5', '把设计稿里的图标补齐', 'todo', 1, { tags: ['设计'], dueDate: dayOffset(9), sortOrder: 5120, }), task('t6', '写 PWA 离线缓存的说明文档', 'todo', 1, { tags: ['文档'], sortOrder: 6144 }), task('t7', '重写任务排序算法,改用相邻中点插入', 'doing', 3, { tags: ['前端', '重构'], dueDate: dayOffset(-1), sortOrder: 1024, }), task('t8', '补充端到端测试覆盖拖拽场景', 'doing', 2, { tags: ['测试'], dueDate: dayOffset(1), sortOrder: 2048, }), task('t9', '梳理快捷键与输入法的冲突场景', 'doing', 2, { tags: ['前端'], sortOrder: 3072 }), task('t10', '搭好项目骨架与工程规范', 'done', 2, { tags: ['基建'], completedAt: iso(30), sortOrder: 1024, }), task('t11', '实现任务的增删改查与持久化', 'done', 3, { tags: ['前端'], completedAt: iso(26), sortOrder: 2048, }), task('t12', '接入 Tailwind 并统一设计变量', 'done', 2, { tags: ['设计', '基建'], completedAt: iso(22), sortOrder: 3072, }), task('t13', '写第一版 ADR', 'done', 1, { tags: ['文档'], completedAt: iso(400), archivedAt: iso(300), sortOrder: 4096, }), ] const STORAGE = { schemaVersion: 2, tasks: SEED, } async function main() { await mkdir(OUT_DIR, { recursive: true }) const browser = await chromium.launch({ channel: CHANNEL === 'chromium' ? undefined : CHANNEL }) const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, }) const page = await context.newPage() await page.goto(BASE_URL) await page.evaluate((state) => { localStorage.setItem('kanban.tasks', JSON.stringify(state)) }, STORAGE) const shots = [ { name: 'board', url: '/', viewport: { width: 1440, height: 900 } }, { name: 'today', url: '/today', viewport: { width: 1440, height: 900 } }, { name: 'board-detail', url: '/', viewport: { width: 1440, height: 900 }, openDetail: true }, { name: 'board-mobile', url: '/', viewport: { width: 390, height: 844 } }, ] for (const shot of shots) { await page.setViewportSize(shot.viewport) await page.goto(`${BASE_URL}${shot.url}`) await page.waitForSelector('article, [data-role="empty-state"], h1') if (shot.openDetail) { await page.locator('article').first().click() await page.waitForSelector('[role="dialog"]') await page.waitForTimeout(300) } await page.waitForTimeout(400) const file = path.join(OUT_DIR, `${shot.name}.png`) await page.screenshot({ path: file }) console.log(`saved ${path.relative(process.cwd(), file)}`) } await browser.close() } await main()