今日视图 - 补上「待办」组。此前只收 逾期 / 今天到期 / 进行中,导致待办列里没设 截止日期的任务在今日视图里完全消失 —— 用户明明有一堆事没做,却看到 「今天没有待办事项」。现在只要任务未完成且未归档,就必定出现在某个 分组里(归属优先级:逾期 > 今天到期 > 进行中 > 待办)。 - 统计卡片改为与分组一一对应(逾期 / 今天到期 / 进行中 / 本周完成), 不再用「今日待办」这种把四组混在一起、与列表长度对不上的指标。 键盘操作(可访问性) - 卡片可聚焦(Tab / 点击),← → 换列、↑ ↓ 列内调序、Enter 打开详情。 - 换列时追加到目标列末尾:用全量长度而不是可见下标,筛选态下也安全; 列内调序要求该列渲染的是全量,否则禁用(与拖拽的规则一致)。 - 移动后手动恢复焦点 —— 卡片跨列会被重建,不恢复的话连续按键会失效。 深色模式 - 三态:跟随系统 / 浅色 / 深色,选择持久化到 localStorage。 - index.html 内联脚本在首帧之前应用主题,避免深色下闪白。 - 用 @custom-variant dark 做 class 控制(而不是纯 media query), 才能支持「强制浅色」;html 的 color-scheme 让原生控件与滚动条跟随。 验证:单元测试 480 个、端到端测试 25 个全部通过,lint 与构建无错误。 截图脚本新增深色与移动端深色两组,便于后续对照。
181 lines
5.4 KiB
JavaScript
181 lines
5.4 KiB
JavaScript
/**
|
|
* 界面截图工具(开发辅助)
|
|
*
|
|
* 用法:先起 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()
|
|
|
|
const shots = [
|
|
{ name: 'board', url: '/', viewport: { width: 1440, height: 900 }, theme: 'light' },
|
|
{ name: 'today', url: '/today', viewport: { width: 1440, height: 900 }, theme: 'light' },
|
|
{
|
|
name: 'board-detail',
|
|
url: '/',
|
|
viewport: { width: 1440, height: 900 },
|
|
theme: 'light',
|
|
openDetail: true,
|
|
},
|
|
{ name: 'board-mobile', url: '/', viewport: { width: 390, height: 844 }, theme: 'light' },
|
|
{ name: 'board-dark', url: '/', viewport: { width: 1440, height: 900 }, theme: 'dark' },
|
|
{ name: 'today-dark', url: '/today', viewport: { width: 1440, height: 900 }, theme: 'dark' },
|
|
{
|
|
name: 'board-dark-mobile',
|
|
url: '/',
|
|
viewport: { width: 390, height: 844 },
|
|
theme: 'dark',
|
|
},
|
|
]
|
|
|
|
for (const shot of shots) {
|
|
await page.setViewportSize(shot.viewport)
|
|
await page.goto(BASE_URL)
|
|
|
|
// 先写入种子数据与主题偏好,再重新进入目标页面
|
|
await page.evaluate(
|
|
({ state, theme }) => {
|
|
localStorage.setItem('kanban.tasks', JSON.stringify(state))
|
|
localStorage.setItem('kanban.theme', theme)
|
|
},
|
|
{ state: STORAGE, theme: shot.theme },
|
|
)
|
|
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()
|