+
+
-
-
- {{ PRIORITY_LABELS[task.priority] }}
-
+
+ {{ task.title }}
+
+
-
- {{ overdue ? '⚠ ' : '' }}{{ formatDueDate(task.dueDate) }}
-
+
+
+ {{ PRIORITY_LABELS[task.priority] }}
+
-
- #{{ tag }}
-
- +{{ hiddenTagCount }}
+
+ {{ formatDueDate(task.dueDate) }}
+
+
+ #{{ tag }}
+ +{{ hiddenTagCount }}
+
diff --git a/src/stores/filters.ts b/src/stores/filters.ts
index 3190888..c26c452 100644
--- a/src/stores/filters.ts
+++ b/src/stores/filters.ts
@@ -73,6 +73,10 @@ export const useFilterStore = defineStore('filters', () => {
due.value = due.value === value ? null : value
}
+ function clearTags(): void {
+ tags.value = []
+ }
+
function toggleArchived(): void {
includeArchived.value = !includeArchived.value
}
@@ -97,6 +101,7 @@ export const useFilterStore = defineStore('filters', () => {
togglePriority,
toggleTag,
toggleDue,
+ clearTags,
toggleArchived,
clear,
}
diff --git a/src/stores/ui.ts b/src/stores/ui.ts
new file mode 100644
index 0000000..75e25fb
--- /dev/null
+++ b/src/stores/ui.ts
@@ -0,0 +1,49 @@
+/**
+ * 应用级面板状态
+ *
+ * 备份面板与快捷键帮助都是**应用级**操作(在哪个视图都该能用),
+ * 因此状态放在这里,由 App.vue 统一渲染,
+ * 导航栏负责打开,看板负责在它们打开时让出快捷键。
+ */
+
+import { defineStore } from 'pinia'
+import { computed, ref } from 'vue'
+
+export const useUiStore = defineStore('ui', () => {
+ const backupOpen = ref(false)
+ const helpOpen = ref(false)
+
+ const anyPanelOpen = computed(() => backupOpen.value || helpOpen.value)
+
+ function openBackup(): void {
+ backupOpen.value = true
+ }
+
+ function closeBackup(): void {
+ backupOpen.value = false
+ }
+
+ function openHelp(): void {
+ helpOpen.value = true
+ }
+
+ function closeHelp(): void {
+ helpOpen.value = false
+ }
+
+ function closeAll(): void {
+ backupOpen.value = false
+ helpOpen.value = false
+ }
+
+ return {
+ backupOpen,
+ helpOpen,
+ anyPanelOpen,
+ openBackup,
+ closeBackup,
+ openHelp,
+ closeHelp,
+ closeAll,
+ }
+})
diff --git a/src/style.css b/src/style.css
index ff50501..51d3833 100644
--- a/src/style.css
+++ b/src/style.css
@@ -3,14 +3,42 @@
@theme {
--font-sans:
system-ui, 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', Roboto, sans-serif;
+
+ /* 卡片与面板的阴影:用极低不透明度的多层阴影代替单层重阴影,看起来更干净 */
+ --shadow-card: 0 1px 2px rgb(15 23 42 / 0.04), 0 1px 3px rgb(15 23 42 / 0.06);
+ --shadow-card-hover: 0 6px 16px rgb(15 23 42 / 0.09), 0 1px 3px rgb(15 23 42 / 0.05);
+ --shadow-panel: 0 16px 40px rgb(15 23 42 / 0.12), 0 2px 8px rgb(15 23 42 / 0.06);
+ --shadow-pop: 0 8px 24px rgb(15 23 42 / 0.12), 0 1px 3px rgb(15 23 42 / 0.06);
}
-html,
-body,
-#app {
- height: 100%;
-}
+@layer base {
+ html,
+ body,
+ #app {
+ height: 100%;
+ }
-body {
- margin: 0;
+ body {
+ margin: 0;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+ }
+
+ /* 统一的键盘焦点样式,鼠标点击不显示 */
+ :focus-visible {
+ outline: 2px solid var(--color-sky-500);
+ outline-offset: 2px;
+ border-radius: 4px;
+ }
+
+ ::selection {
+ background: var(--color-sky-200);
+ }
+
+ /* 细滚动条,避免在列里出现粗重的系统滚动条 */
+ * {
+ scrollbar-width: thin;
+ scrollbar-color: var(--color-slate-300) transparent;
+ }
}
diff --git a/src/views/BoardView.spec.ts b/src/views/BoardView.spec.ts
index 7e0ed0c..1e56c33 100644
--- a/src/views/BoardView.spec.ts
+++ b/src/views/BoardView.spec.ts
@@ -9,6 +9,7 @@ import { createMemoryTaskRepository } from '@/data/taskRepository'
import { useFilterStore } from '@/stores/filters'
import { useTaskStore } from '@/stores/tasks'
import { useToastStore } from '@/stores/toast'
+import { useUiStore } from '@/stores/ui'
import BoardView from './BoardView.vue'
@@ -53,7 +54,7 @@ describe('BoardView 结构', () => {
store.createTask({ title: '进行中事项', status: 'doing' })
const counters = mountBoard(pinia)
- .findAll('section header span')
+ .findAll('[data-role="column-count"]')
.map((node) => node.text())
expect(counters).toEqual(['1', '1', '0'])
})
@@ -245,7 +246,7 @@ describe('BoardView 筛选', () => {
expect(wrapper.findAll('article')).toHaveLength(1)
expect(wrapper.text()).toContain('高优先级')
expect(wrapper.text()).not.toContain('低优先级')
- expect(wrapper.findAll('section header span')[0]?.text()).toBe('1')
+ expect(wrapper.findAll('[data-role="column-count"]')[0]?.text()).toBe('1')
})
it('显示命中数量提示', async () => {
@@ -257,7 +258,7 @@ describe('BoardView 筛选', () => {
useFilterStore().togglePriority(3)
await nextTick()
- expect(wrapper.text()).toContain('显示 1 / 2 个任务')
+ expect(wrapper.text()).toContain('已隐藏 1 个')
})
it('清空筛选后恢复全部卡片', async () => {
@@ -273,7 +274,7 @@ describe('BoardView 筛选', () => {
await nextTick()
expect(wrapper.findAll('article')).toHaveLength(2)
- expect(wrapper.text()).not.toContain('显示 1 / 2')
+ expect(wrapper.text()).not.toContain('已隐藏')
})
it('点击「逾期」只显示逾期任务', async () => {
@@ -327,7 +328,7 @@ describe('BoardView 归档', () => {
const wrapper = mountBoard(pinia)
expect(wrapper.findAll('article')).toHaveLength(0)
- expect(wrapper.text()).toContain('显示 0 / 1 个任务')
+ expect(wrapper.text()).toContain('已隐藏 1 个')
})
it('开启「显示归档」后归档任务重新出现', async () => {
@@ -400,17 +401,6 @@ describe('BoardView 快捷键', () => {
expect(document.activeElement).toBe(wrapper.find('input[aria-label="搜索任务"]').element)
})
- it('按 ? 打开快捷键帮助', async () => {
- const { pinia } = setup()
- const wrapper = mountBoard(pinia)
- await nextTick()
-
- pressKey('?')
- await nextTick()
-
- expect(wrapper.text()).toContain('聚焦「待办」列的快速新增输入框')
- })
-
it('在输入框内按 N 不触发快捷键', async () => {
const { pinia } = setup()
const wrapper = mountBoard(pinia)
@@ -422,29 +412,16 @@ describe('BoardView 快捷键', () => {
expect(document.activeElement).not.toBe(wrapper.find('input[aria-label="添加任务"]').element)
})
- it('面板打开时不响应快捷键', async () => {
+ it('应用级面板打开时不响应快捷键', async () => {
const { pinia } = setup()
const wrapper = mountBoard(pinia)
await nextTick()
- await wrapper.find('button[aria-label="快捷键帮助"]').trigger('click')
+ // 帮助面板由 App 渲染,这里只验证看板会让出快捷键
+ useUiStore().openHelp()
await nextTick()
pressKey('n')
expect(document.activeElement).not.toBe(wrapper.find('input[aria-label="添加任务"]').element)
})
-
- it('按 Esc 关闭已打开的面板', async () => {
- const { pinia } = setup()
- const wrapper = mountBoard(pinia)
- await nextTick()
-
- await wrapper.find('button[aria-label="快捷键帮助"]').trigger('click')
- await nextTick()
- expect(wrapper.text()).toContain('聚焦「待办」列的快速新增输入框')
-
- pressKey('Escape')
- await nextTick()
- expect(wrapper.text()).not.toContain('聚焦「待办」列的快速新增输入框')
- })
})
diff --git a/src/views/BoardView.vue b/src/views/BoardView.vue
index 3213542..deb2cf2 100644
--- a/src/views/BoardView.vue
+++ b/src/views/BoardView.vue
@@ -2,15 +2,14 @@
import { storeToRefs } from 'pinia'
import { computed, ref } from 'vue'
-import BackupPanel from '@/components/BackupPanel.vue'
import BoardColumn from '@/components/BoardColumn.vue'
import BoardToolbar from '@/components/BoardToolbar.vue'
-import SideDrawer from '@/components/SideDrawer.vue'
import TaskDetailDrawer from '@/components/TaskDetailDrawer.vue'
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
import { useFilterStore } from '@/stores/filters'
import { useTaskStore } from '@/stores/tasks'
import { useToastStore } from '@/stores/toast'
+import { useUiStore } from '@/stores/ui'
import { COLUMNS, type ColumnId } from '@/types/task'
import { findArchivableTasks } from '@/utils/archive'
import { filterTasks } from '@/utils/filter'
@@ -19,23 +18,14 @@ import { groupByColumn } from '@/utils/sortOrder'
const store = useTaskStore()
const filters = useFilterStore()
const toast = useToastStore()
+const ui = useUiStore()
const { notice } = storeToRefs(store)
const { filter } = storeToRefs(filters)
-const SHORTCUTS = [
- { keys: 'N', description: '聚焦「待办」列的快速新增输入框' },
- { keys: '/', description: '聚焦搜索框' },
- { keys: '?', description: '打开本帮助' },
- { keys: 'Esc', description: '关闭当前面板' },
-] as const
-
/** 详情抽屉当前编辑的任务 id;任务被删除后自动变为 null,抽屉随之关闭 */
const editingId = ref
(null)
const editingTask = computed(() => store.tasks.find((task) => task.id === editingId.value) ?? null)
-const backupOpen = ref(false)
-const helpOpen = ref(false)
-
const toolbarRef = ref | null>(null)
const columnRefs = new Map>()
@@ -47,10 +37,6 @@ function setColumnRef(element: unknown, index: number): void {
/** 实际渲染的看板:先按条件筛选,再按列分组,列计数随之反映结果 */
const visibleColumns = computed(() => groupByColumn(filterTasks(store.tasks, filter.value)))
-const visibleCount = computed(() =>
- COLUMNS.reduce((total, column) => total + visibleColumns.value[column.id].length, 0),
-)
-
/**
* 每列被隐藏的数量(被筛选掉 + 已归档)。
* 大于 0 说明该列渲染的是子集,此时必须禁用列内排序,
@@ -84,14 +70,10 @@ function archiveCompleted(): void {
})
}
-function anyPanelOpen(): boolean {
- return editingId.value !== null || backupOpen.value || helpOpen.value
-}
-
/** 有面板打开时不响应全局快捷键,避免误触 */
function whenNoPanel(action: () => void): () => void {
return () => {
- if (anyPanelOpen()) return
+ if (editingId.value !== null || ui.anyPanelOpen) return
action()
}
}
@@ -99,9 +81,6 @@ function whenNoPanel(action: () => void): () => void {
useKeyboardShortcuts({
n: whenNoPanel(() => columnRefs.get(0)?.focusQuickAdd()),
'/': whenNoPanel(() => toolbarRef.value?.focusSearch()),
- '?': whenNoPanel(() => {
- helpOpen.value = true
- }),
})
function toggleTask(id: string): void {
@@ -124,26 +103,25 @@ function handleMove(payload: { id: string; toColumn: ColumnId; toIndex: number }
-
+
{{ notice }}
-
- 显示 {{ visibleCount }} / {{ store.tasks.length }} 个任务
-
-
-
+
+
-
-
-
-
-
-
-
-
- {{ item.keys }}
-
- - {{ item.description }}
-
-
- 在输入框内输入时不会触发快捷键。
-
diff --git a/src/views/TodayView.vue b/src/views/TodayView.vue
index 3a23748..6eff9b1 100644
--- a/src/views/TodayView.vue
+++ b/src/views/TodayView.vue
@@ -13,9 +13,9 @@ const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '
/** 用完整类名字符串,避免 Tailwind 扫描不到动态拼接的类 */
const TONES = {
- rose: { dot: 'bg-rose-500', badge: 'bg-rose-100 text-rose-700' },
- amber: { dot: 'bg-amber-500', badge: 'bg-amber-100 text-amber-700' },
- sky: { dot: 'bg-sky-500', badge: 'bg-sky-100 text-sky-700' },
+ rose: { dot: 'bg-rose-500', badge: 'border-rose-200 bg-rose-50 text-rose-700' },
+ amber: { dot: 'bg-amber-500', badge: 'border-amber-200 bg-amber-50 text-amber-700' },
+ sky: { dot: 'bg-sky-500', badge: 'border-sky-200 bg-sky-50 text-sky-700' },
} as const
const store = useTaskStore()
@@ -31,10 +31,10 @@ const stats = computed(() => computeStats(tasks.value))
const todayCount = computed(() => countTodayTasks(buckets.value))
const statCards = computed(() => [
- { label: '今日待办', value: todayCount.value },
- { label: '逾期', value: stats.value.overdue },
- { label: '今天到期', value: stats.value.dueToday },
- { label: '本周完成', value: stats.value.completedThisWeek },
+ { label: '今日待办', value: todayCount.value, dot: 'bg-slate-400' },
+ { label: '逾期', value: stats.value.overdue, dot: 'bg-rose-500' },
+ { label: '今天到期', value: stats.value.dueToday, dot: 'bg-amber-500' },
+ { label: '本周完成', value: stats.value.completedThisWeek, dot: 'bg-emerald-500' },
])
/**
@@ -79,58 +79,62 @@ function toggleTask(id: string): void {
-
-
- 今日
- {{ todayLabel }}
-
+
+
+
+
+ 今日
+ {{ todayLabel }}
+
-
-
-
- {{ card.label }}
- - {{ card.value }}
-
-
-
-
- 今天没有待办事项
-
-
-
-
-
- {{ section.title }}
-
+
- {{ section.items.length }}
-
- {{ section.hint }}
-
+
+
+ {{ card.label }}
+
+
+ {{ card.value }}
+
+
+
-
-
-
-
+
+ 今天没有待办事项
+
+
+
+
+
+ {{ section.title }}
+
+ {{ section.items.length }}
+
+ {{ section.hint }}
+
+
+
+
+
+
+