Files
Kanban/scripts/screenshot.mjs
T
chenshoulin 442cbbd734 style: 重做界面视觉与信息层级
先截图审视现状,再针对性修改。主要问题与改动如下。

工具栏
- 标签筛选从「平铺全部标签」改为下拉面板,标签多时不再换行挤爆工具栏
- 备份与快捷键帮助移到导航栏 —— 它们是应用级操作,不该挤在看板工具栏里,
  顺带修好了「? 帮助只在看板可用」的问题
- 到期统计(今日到期 / 逾期)合并为可点击的筛选入口,去掉两个独立控件
- 移动端工具栏从占半屏降到三行紧凑布局

卡片
- 优先级从彩色胶囊改为左侧色条,扫视一列时比读文字更快分辨轻重
- 元信息改为「优先级 · 日期 · 标签」的轻量文本行,不再三种胶囊叠在一起
- 完成态用勾选图标 + 降低不透明度

布局
- 今日视图限宽居中(max-w-3xl):此前 1440px 下卡片被拉满整屏,
  标题与元信息之间出现大片空白
- 修复移动端卡片被压成细条:TaskCard 的 overflow-hidden(用于裁切左侧色条)
  会让 flex 项的最小尺寸解析为 0,补 shrink-0 修正;
  同时改为移动端按内容高度堆叠、桌面端列内滚动

其他
- 导航栏品牌改用看板图标,消除「看板」重复出现两次
- 统一卡片阴影、圆角与键盘焦点样式
- 新增 scripts/screenshot.mjs,便于后续对照审视视觉改动

验证:单元测试 452 个、端到端测试 23 个全部通过,lint 与构建无错误。
2026-10-18 14:05:06 +08:00

161 lines
4.8 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()
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()