diff --git a/README.md b/README.md index ae782cf..f640655 100644 --- a/README.md +++ b/README.md @@ -5,8 +5,9 @@ - 固定三列:**待办 / 进行中 / 已完成** - 快速新增、拖拽排序、详情编辑、删除撤销 - 搜索与筛选(关键词 / 优先级 / 标签 / 今日到期 / 逾期) -- **今日视图**:按紧急度汇总今天要做的事,含本周完成等统计 -- 键盘快捷键 +- **今日视图**:按紧急度分组汇总(逾期 / 今天到期 / 进行中 / 待办),含本周完成等统计 +- 键盘快捷键;任务卡片可用方向键换列与调序 +- **深色模式**:跟随系统 / 浅色 / 深色三态 - 导出导入 JSON + 启动自动备份 - **可安装为 PWA,支持离线使用** @@ -44,7 +45,15 @@ npm run dev | `?` | 打开快捷键帮助 | | `Esc` | 关闭当前面板 | -在输入框内输入时不会触发快捷键。 +卡片获得焦点(`Tab` 或点击)后: + +| 按键 | 作用 | +|---|---| +| `←` `→` | 把卡片移到上一列 / 下一列 | +| `↑` `↓` | 在列内上移 / 下移 | +| `Enter` | 打开任务详情 | + +在输入框内输入时不会触发全局快捷键。方向键移动在筛选态下只允许跨列(列内排序需要看到完整列表才有意义)。 --- @@ -55,11 +64,13 @@ npm run dev | 视图 | 路径 | 组织方式 | |---|---|---| | 看板 | `/` | 按**状态**:待办 / 进行中 / 已完成 | -| 今日 | `/today` | 按**紧急度**:已逾期 → 今天到期 → 进行中 | +| 今日 | `/today` | 按**紧急度**:已逾期 → 今天到期 → 进行中 → 待办 | -今日视图顶部有四张统计卡片(今日待办 / 逾期 / 今天到期 / 本周完成),下方是可直接勾选完成的待办列表。 +今日视图顶部有四张统计卡片(逾期 / 今天到期 / 进行中 / 本周完成),下方是可直接勾选完成的待办列表。 -分组是**互斥**的:一张卡片只会出现在一组里,归属优先级为「逾期 > 今天到期 > 进行中」,所以你能一眼数清今天到底有几件事。已完成和已归档的任务不会出现在今日视图。 +分组是**互斥**的:一张卡片只会出现在一组里,归属优先级为「逾期 > 今天到期 > 进行中 > 待办」,所以你能一眼数清今天到底有几件事。已完成和已归档的任务不会出现在今日视图。 + +只要任务未完成且未归档,就一定会出现在某个分组里 —— 包括没设截止日期的待办(归入「待办」组)。 --- @@ -177,8 +188,8 @@ scripts/ # 图标生成等辅助脚本 ## 测试 -- 单元测试:437 个用例,覆盖 `src/` 下的纯逻辑与组件行为,行覆盖率约 97%。 -- 端到端测试:23 个用例,分两组运行 —— 交互类跑 dev server,PWA 类跑生产构建(Service Worker 只在构建产物中生成)。覆盖「新增 → 拖拽 → 完成 → 刷新保持」「导出 → 清空 → 导入还原」「删除撤销」「搜索筛选」「键盘快捷键」「今日视图」「离线打开」等主链路,每个用例使用独立的浏览器上下文。 +- 单元测试:480 个用例,覆盖 `src/` 下的纯逻辑与组件行为,行覆盖率约 96%。 +- 端到端测试:25 个用例,分两组运行 —— 交互类跑 dev server,PWA 类跑生产构建(Service Worker 只在构建产物中生成)。覆盖「新增 → 拖拽 → 完成 → 刷新保持」「导出 → 清空 → 导入还原」「删除撤销」「搜索筛选」「键盘快捷键与方向键移动」「深色模式切换」「今日视图」「离线打开」等主链路,每个用例使用独立的浏览器上下文。 ```bash npm run test:coverage @@ -202,5 +213,6 @@ PLAYWRIGHT_CHANNEL=chromium npm run test:e2e # 用自带 Chromium(需先 npx - 标签是字符串数组,重命名标签需要逐个任务修改。 - 看板列固定为三列,不支持自定义。 - 归档阈值固定为「已完成超过 7 天」,暂不可配置。 -- 今日视图的分组规则固定(逾期 > 今天到期 > 进行中),不可自定义。 +- 今日视图的分组规则固定(逾期 > 今天到期 > 进行中 > 待办),不可自定义。 - 统计只覆盖本周完成数等基础指标,没有趋势图或历史留存。 +- 深色模式只跟随系统或手动三态切换,没有按时间段自动切换。 diff --git a/e2e/board.spec.ts b/e2e/board.spec.ts index 5f78a12..cca4dde 100644 --- a/e2e/board.spec.ts +++ b/e2e/board.spec.ts @@ -113,3 +113,39 @@ test('输入框内输入不会触发快捷键', async ({ page }) => { await expect(search).toBeFocused() await expect(search).toHaveValue('n') }) + +test('深色模式可切换并在刷新后保持', async ({ page }) => { + await page.goto('/') + const html = page.locator('html') + const themeButton = page.getByRole('button', { name: /主题/ }) + + await expect(html).not.toHaveClass(/dark/) + + // 系统 → 浅色 → 深色 + await themeButton.click() + await expect(html).not.toHaveClass(/dark/) + await themeButton.click() + await expect(html).toHaveClass(/dark/) + + await page.reload() + await expect(html).toHaveClass(/dark/) + + // 再点一次回到跟随系统 + await themeButton.click() + await expect(html).not.toHaveClass(/dark/) +}) + +test('键盘方向键可以移动任务卡片', async ({ page }) => { + await addTask(page, '键盘移动我') + const card = page.locator('article').filter({ hasText: '键盘移动我' }) + await card.focus() + + await page.keyboard.press('ArrowRight') + await expect(page.locator('[data-column-id="doing"] article')).toHaveCount(1) + + await page.keyboard.press('ArrowLeft') + await expect(page.locator('[data-column-id="todo"] article')).toHaveCount(1) + + await page.reload() + await expect(page.locator('[data-column-id="todo"] article')).toHaveCount(1) +}) diff --git a/e2e/pwa.spec.ts b/e2e/pwa.spec.ts index 0d2893a..d3c2843 100644 --- a/e2e/pwa.spec.ts +++ b/e2e/pwa.spec.ts @@ -106,7 +106,7 @@ test('离线时深链 /today 也能打开(navigateFallback 生效)', async ( await page.goto('/today') await expect(page.getByRole('heading', { name: '今日' })).toBeVisible() - await expect(page.getByText('今日待办')).toBeVisible() + await expect(page.getByText('本周完成')).toBeVisible() await context.setOffline(false) }) diff --git a/e2e/today.spec.ts b/e2e/today.spec.ts index 2e443ed..edd58a1 100644 --- a/e2e/today.spec.ts +++ b/e2e/today.spec.ts @@ -34,7 +34,7 @@ test('导航可在看板与今日之间切换', async ({ page }) => { await page.getByRole('link', { name: '今日' }).click() await expect(page.getByRole('heading', { name: '今日' })).toBeVisible() - await expect(page.getByText('今日待办')).toBeVisible() + await expect(page.getByText('本周完成')).toBeVisible() await page.getByRole('link', { name: '看板' }).click() await expect(page.getByText('共 0 个任务')).toBeVisible() @@ -58,13 +58,15 @@ test('今日视图汇总今天到期与进行中的任务', async ({ page }) => await expect(page.getByRole('heading', { level: 2, name: /进行中/ })).toBeVisible() }) -test('未开始且无截止日期的任务不进入今日视图', async ({ page }) => { +test('未开始且无截止日期的任务出现在「待办」组', async ({ page }) => { await addTask(page, '以后再说') await page.getByRole('link', { name: '今日' }).click() - await expect(page.locator('article')).toHaveCount(0) - await expect(page.getByText('今天没有待办事项')).toBeVisible() + // 早期版本这类任务在今日视图里完全消失,是一个真实的缺口 + await expect(page.locator('article')).toHaveCount(1) + await expect(page.locator('article').filter({ hasText: '以后再说' })).toBeVisible() + await expect(page.getByRole('heading', { level: 2, name: /待办/ })).toBeVisible() }) test('今日视图可以勾选完成', async ({ page }) => { diff --git a/index.html b/index.html index 1e7a138..0aa1bcf 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,22 @@ 看板 · 个人任务管理 +
diff --git a/scripts/screenshot.mjs b/scripts/screenshot.mjs index 840dd57..fcf1f43 100644 --- a/scripts/screenshot.mjs +++ b/scripts/screenshot.mjs @@ -125,20 +125,40 @@ async function main() { }) 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 } }, + { 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') diff --git a/src/App.spec.ts b/src/App.spec.ts index 59ddda3..787d06c 100644 --- a/src/App.spec.ts +++ b/src/App.spec.ts @@ -1,14 +1,17 @@ -import { mount } from '@vue/test-utils' +import { enableAutoUnmount, mount } from '@vue/test-utils' import { createPinia } from 'pinia' import { nextTick } from 'vue' import { createMemoryHistory } from 'vue-router' -import { beforeEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { SCHEMA_VERSION } from '@/data/schema' import App from './App.vue' import { createAppRouter } from './router' +// App 会在 document 上注册快捷键监听,必须逐个卸载避免相互干扰 +enableAutoUnmount(afterEach) + /** PWA 的虚拟模块在测试环境下无法解析,用桩替代 */ vi.mock('virtual:pwa-register/vue', async () => { const { ref } = await import('vue') @@ -111,7 +114,7 @@ describe('App 路由', () => { it('/today 渲染今日视图', async () => { const { wrapper } = await mountApp('/today') - expect(wrapper.text()).toContain('今日待办') + expect(wrapper.text()).toContain('本周完成') expect(wrapper.text()).not.toContain('共 0 个任务') wrapper.unmount() }) diff --git a/src/App.vue b/src/App.vue index 46f5de8..debbc75 100644 --- a/src/App.vue +++ b/src/App.vue @@ -9,6 +9,7 @@ import SideDrawer from '@/components/SideDrawer.vue' import ToastHost from '@/components/ToastHost.vue' import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts' import { useTaskStore } from '@/stores/tasks' +import { useThemeStore } from '@/stores/theme' import { useUiStore } from '@/stores/ui' const SHORTCUTS = [ @@ -18,11 +19,21 @@ const SHORTCUTS = [ { keys: 'Esc', description: '关闭当前面板' }, ] as const +/** 这些只在卡片获得焦点后生效 */ +const CARD_SHORTCUTS = [ + { keys: 'Tab', description: '依次聚焦到任务卡片' }, + { keys: '← →', description: '把卡片移到上一列 / 下一列' }, + { keys: '↑ ↓', description: '在列内上移 / 下移' }, + { keys: 'Enter', description: '打开任务详情' }, +] as const + const store = useTaskStore() const ui = useUiStore() +const theme = useThemeStore() onMounted(() => { store.init() + theme.init() }) // 帮助面板是应用级的,因此在任何视图下都能用 ? @@ -34,7 +45,7 @@ useKeyboardShortcuts({