feat: 更新统计卡片与分组对应关系,优化样式与可访问性
This commit is contained in:
+4
-2
@@ -159,8 +159,10 @@ describe('App 路由', () => {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
const values = wrapper.findAll('dd').map((node) => node.text())
|
const values = wrapper.findAll('dd').map((node) => node.text())
|
||||||
// [今日待办, 逾期, 今天到期, 本周完成]
|
// 统计带 = [逾期, 今天到期, 进行中, 待办]:刚完成的任务不进任何分组,四格都应是 0
|
||||||
expect(values[3]).toBe('1')
|
expect(values).toEqual(['0', '0', '0', '0'])
|
||||||
|
// 「本周完成」已移到页头副标题,不再占统计格
|
||||||
|
expect(wrapper.find('[data-role="completed-this-week"]').text()).toBe('1')
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ const LINKS = [
|
|||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
:href="href"
|
:href="href"
|
||||||
class="rounded-md px-3 py-1.5 text-sm transition"
|
class="shrink-0 rounded-md px-3 py-1.5 text-sm whitespace-nowrap transition"
|
||||||
:class="
|
:class="
|
||||||
isExactActive
|
isExactActive
|
||||||
? 'bg-slate-900 font-medium text-white shadow-sm dark:bg-zinc-100 dark:text-zinc-900'
|
? 'bg-slate-900 font-medium text-white shadow-sm dark:bg-zinc-100 dark:text-zinc-900'
|
||||||
@@ -62,7 +62,7 @@ const LINKS = [
|
|||||||
</RouterLink>
|
</RouterLink>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-1.5">
|
<div class="ml-auto flex flex-none items-center gap-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex size-8 items-center justify-center rounded-md text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
|
class="flex size-8 items-center justify-center rounded-md text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
|
||||||
@@ -118,9 +118,14 @@ const LINKS = [
|
|||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
320px 是 WCAG 1.4.10 要求的最小重排宽度,此时单行放不下四个操作按钮,
|
||||||
|
会横向溢出 20px。快捷键帮助是纯桌面向的次要入口(手机上也没有物理键盘),
|
||||||
|
在 <360 收起,优先保住「不横向滚动」。
|
||||||
|
-->
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex size-8 items-center justify-center rounded-md text-slate-400 transition hover:bg-slate-100 hover:text-slate-700 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
|
class="flex size-8 items-center justify-center rounded-md text-slate-400 transition max-[359px]:hidden hover:bg-slate-100 hover:text-slate-700 dark:text-zinc-500 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
|
||||||
aria-label="快捷键帮助"
|
aria-label="快捷键帮助"
|
||||||
@click="ui.openHelp()"
|
@click="ui.openHelp()"
|
||||||
>
|
>
|
||||||
@@ -172,6 +177,7 @@ const LINKS = [
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex items-center gap-1.5 rounded-md border border-slate-200 px-2.5 py-1.5 text-sm text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 dark:border-zinc-700 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"
|
class="flex items-center gap-1.5 rounded-md border border-slate-200 px-2.5 py-1.5 text-sm text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 hover:text-slate-900 dark:border-zinc-700 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"
|
||||||
|
aria-label="备份"
|
||||||
@click="ui.openBackup()"
|
@click="ui.openBackup()"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 20 20" class="size-4" fill="none" aria-hidden="true">
|
<svg viewBox="0 0 20 20" class="size-4" fill="none" aria-hidden="true">
|
||||||
@@ -189,7 +195,7 @@ const LINKS = [
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
备份
|
<span class="hidden sm:inline">备份</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -66,8 +66,14 @@ useDraggable(listRef, {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!--
|
||||||
|
列容器是「托盘」:页面 slate-100 → 列 slate-50 → 卡片 white,三级递增。
|
||||||
|
早先是 bg-slate-100/70,叠在同样是 slate-100 的页面上等于同色(差值为 0),
|
||||||
|
列只剩一根 1px 边框,看不到边界;深色下 zinc-900/60 叠 zinc-950 反而是分层的,
|
||||||
|
两套模式就出现了「深色有容器、浅色没有」的不对等。
|
||||||
|
-->
|
||||||
<section
|
<section
|
||||||
class="flex flex-col rounded-xl border border-slate-200 bg-slate-100/70 md:min-h-0 dark:border-zinc-800 dark:bg-zinc-900/60"
|
class="flex flex-col rounded-xl border border-slate-200 bg-slate-50 md:min-h-0 dark:border-zinc-800 dark:bg-zinc-900/60"
|
||||||
>
|
>
|
||||||
<header class="flex items-center gap-2 px-3 pt-3 pb-2">
|
<header class="flex items-center gap-2 px-3 pt-3 pb-2">
|
||||||
<span class="size-1.5 shrink-0 rounded-full" :class="dotClass" aria-hidden="true" />
|
<span class="size-1.5 shrink-0 rounded-full" :class="dotClass" aria-hidden="true" />
|
||||||
|
|||||||
@@ -113,10 +113,26 @@ describe('截止日期', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('已完成样式', () => {
|
describe('已完成样式', () => {
|
||||||
it('标题加删除线并整体降饱和', () => {
|
it('标题加删除线,但不再整卡降透明度', () => {
|
||||||
const wrapper = mountCard({ status: 'done' })
|
const wrapper = mountCard({ status: 'done' })
|
||||||
|
const classes = wrapper.find('article').classes().join(' ')
|
||||||
|
|
||||||
expect(wrapper.find('h3').classes().join(' ')).toContain('line-through')
|
expect(wrapper.find('h3').classes().join(' ')).toContain('line-through')
|
||||||
expect(wrapper.find('article').classes().join(' ')).toContain('opacity-60')
|
// 整卡 opacity-60 会把标题压到 1.69:1、标签压到 1.61:1(WCAG AA 要求 4.5:1)
|
||||||
|
expect(classes).not.toContain('opacity-60')
|
||||||
|
expect(classes).toContain('bg-white')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('已完成卡片的文字不低于 slate-500', () => {
|
||||||
|
const wrapper = mountCard({ status: 'done', priority: 1, tags: ['设计'] })
|
||||||
|
const html = wrapper.html()
|
||||||
|
|
||||||
|
expect(wrapper.find('h3').classes().join(' ')).toContain('text-slate-500')
|
||||||
|
// 标签与「低」优先级曾用 slate-400(白底 2.56:1),统一提到 slate-500(4.76:1)
|
||||||
|
expect(html).not.toContain('text-slate-400')
|
||||||
|
expect(html).toContain('text-slate-500')
|
||||||
|
// 完成日期曾带 /80 透明度,混色后 3.75:1
|
||||||
|
expect(html).not.toContain('emerald-700/80')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('未完成时不加删除线', () => {
|
it('未完成时不加删除线', () => {
|
||||||
@@ -189,6 +205,15 @@ describe('交互', () => {
|
|||||||
expect(mountCard({ status: 'done' }).find('button').attributes('aria-pressed')).toBe('true')
|
expect(mountCard({ status: 'done' }).find('button').attributes('aria-pressed')).toBe('true')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('勾选按钮的命中区不小于 24×24(视觉仍是 16px 圆点)', () => {
|
||||||
|
const button = mountCard().find('button')
|
||||||
|
|
||||||
|
// WCAG 2.5.8:命中区 24×24 起;视觉尺寸由内层 span 保持 size-4
|
||||||
|
expect(button.classes().join(' ')).toContain('h-9')
|
||||||
|
expect(button.classes().join(' ')).toContain('w-8')
|
||||||
|
expect(button.find('span').classes().join(' ')).toContain('size-4')
|
||||||
|
})
|
||||||
|
|
||||||
it('点击卡片 emit open 而不是 toggle', async () => {
|
it('点击卡片 emit open 而不是 toggle', async () => {
|
||||||
const wrapper = mountCard({ id: 'task-7' })
|
const wrapper = mountCard({ id: 'task-7' })
|
||||||
await wrapper.find('article').trigger('click')
|
await wrapper.find('article').trigger('click')
|
||||||
|
|||||||
+37
-21
@@ -91,21 +91,28 @@ const timeTitle = computed(() => {
|
|||||||
/**
|
/**
|
||||||
* 卡片外观。选中态与「已完成」都要覆盖边框/背景,
|
* 卡片外观。选中态与「已完成」都要覆盖边框/背景,
|
||||||
* 因此这些颜色放在条件分支里,而不是和基础类各写一份靠优先级取胜。
|
* 因此这些颜色放在条件分支里,而不是和基础类各写一份靠优先级取胜。
|
||||||
|
*
|
||||||
|
* 已完成不再整卡降透明度:opacity-60 会把标题压到 1.69:1、标签压到 1.61:1,
|
||||||
|
* 回看上周做了什么、或者要取消勾选时都读不清。改为「删除线 + 收敛文字色 +
|
||||||
|
* 实心绿勾 + 完成日期」四个局部信号,底色保持白色。
|
||||||
*/
|
*/
|
||||||
const cardClass = computed(() => {
|
const cardClass = computed(() => {
|
||||||
if (props.selected) {
|
if (props.selected) {
|
||||||
return 'border-sky-400 bg-sky-50 dark:border-sky-600 dark:bg-sky-950/40'
|
return 'border-sky-400 bg-sky-50 dark:border-sky-600 dark:bg-sky-950/40'
|
||||||
}
|
}
|
||||||
return isDone.value
|
return isDone.value
|
||||||
? 'border-slate-200 bg-white opacity-60 dark:border-zinc-800 dark:bg-zinc-900'
|
? 'border-slate-200 bg-white dark:border-zinc-800 dark:bg-zinc-900'
|
||||||
: 'border-slate-200 bg-white hover:-translate-y-px hover:border-slate-300 hover:shadow-card-hover dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700 dark:hover:bg-zinc-800/25'
|
: 'border-slate-200 bg-white hover:-translate-y-px hover:border-slate-300 hover:shadow-card-hover dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700 dark:hover:bg-zinc-800/25'
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 优先级用左侧色条表达,而不是一枚彩色胶囊。
|
* 优先级用左侧色条表达,而不是一枚彩色胶囊。
|
||||||
* 色条几乎不占空间,扫视一列卡片时却比文字更快分辨出轻重。
|
* 色条几乎不占空间,扫视一列卡片时却比文字更快分辨出轻重。
|
||||||
|
* 已完成的任务把色条降为中性灰:完成态再挂一条饱和色条,
|
||||||
|
* 会和绿勾、删除线一起抢注意力,看起来像还在报警。
|
||||||
*/
|
*/
|
||||||
const priorityBarClass = computed(() => {
|
const priorityBarClass = computed(() => {
|
||||||
|
if (isDone.value) return 'bg-slate-300 dark:bg-zinc-600'
|
||||||
switch (props.task.priority) {
|
switch (props.task.priority) {
|
||||||
case 3:
|
case 3:
|
||||||
return 'bg-rose-400'
|
return 'bg-rose-400'
|
||||||
@@ -123,7 +130,7 @@ const priorityTextClass = computed(() => {
|
|||||||
case 2:
|
case 2:
|
||||||
return 'text-amber-700 dark:text-amber-400'
|
return 'text-amber-700 dark:text-amber-400'
|
||||||
default:
|
default:
|
||||||
return 'text-slate-400 dark:text-zinc-500'
|
return 'text-slate-500 dark:text-zinc-400'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -170,35 +177,44 @@ const priorityTextClass = computed(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
命中区 32×36,视觉仍是 16px 圆点:靠负 margin 把按钮撑大,再用内层 span 保持圆点尺寸。
|
||||||
|
WCAG 2.5.8(AA)要求 24×24,原来 16×16 不合格;卡片只有 59px 高,
|
||||||
|
再往上撑到 44 就会把整条左边缘都变成「勾选」,容易误触,36 是这里的实际上限。
|
||||||
|
-->
|
||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
type="button"
|
type="button"
|
||||||
class="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border transition"
|
class="group -mx-2 -my-2 flex h-9 w-8 shrink-0 items-center justify-center rounded-full"
|
||||||
:class="
|
|
||||||
isDone
|
|
||||||
? 'border-emerald-500 bg-emerald-500 text-white'
|
|
||||||
: 'border-slate-300 hover:border-emerald-500 hover:bg-emerald-50 dark:border-zinc-600 dark:hover:border-emerald-500 dark:hover:bg-emerald-950'
|
|
||||||
"
|
|
||||||
:aria-label="isDone ? '标记为未完成' : '标记为已完成'"
|
:aria-label="isDone ? '标记为未完成' : '标记为已完成'"
|
||||||
:aria-pressed="isDone"
|
:aria-pressed="isDone"
|
||||||
@click.stop="emit('toggle', task.id)"
|
@click.stop="emit('toggle', task.id)"
|
||||||
>
|
>
|
||||||
<svg v-if="isDone" viewBox="0 0 12 12" class="size-2.5" fill="none" aria-hidden="true">
|
<span
|
||||||
<path
|
class="flex size-4 items-center justify-center rounded-full border transition"
|
||||||
d="m2.5 6.2 2.2 2.2 4.8-4.8"
|
:class="
|
||||||
stroke="currentColor"
|
isDone
|
||||||
stroke-width="1.8"
|
? 'border-emerald-600 bg-emerald-600 text-white'
|
||||||
stroke-linecap="round"
|
: 'border-slate-300 group-hover:border-emerald-500 group-hover:bg-emerald-50 dark:border-zinc-600 dark:group-hover:border-emerald-500 dark:group-hover:bg-emerald-950'
|
||||||
stroke-linejoin="round"
|
"
|
||||||
/>
|
>
|
||||||
</svg>
|
<svg v-if="isDone" viewBox="0 0 12 12" class="size-2.5" fill="none" aria-hidden="true">
|
||||||
|
<path
|
||||||
|
d="m2.5 6.2 2.2 2.2 4.8-4.8"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<h3
|
<h3
|
||||||
class="min-w-0 flex-1 text-sm leading-snug wrap-break-word"
|
class="min-w-0 flex-1 text-sm leading-snug wrap-break-word"
|
||||||
:class="
|
:class="
|
||||||
isDone
|
isDone
|
||||||
? 'text-slate-400 line-through dark:text-zinc-500'
|
? 'text-slate-500 line-through dark:text-zinc-400'
|
||||||
: 'text-slate-800 dark:text-zinc-100'
|
: 'text-slate-800 dark:text-zinc-100'
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -222,10 +238,10 @@ const priorityTextClass = computed(() => {
|
|||||||
{{ dueLabel }}
|
{{ dueLabel }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<span v-for="tag in visibleTags" :key="tag" class="text-slate-400 dark:text-zinc-500">
|
<span v-for="tag in visibleTags" :key="tag" class="text-slate-500 dark:text-zinc-400">
|
||||||
#{{ tag }}
|
#{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="hiddenTagCount > 0" class="text-slate-400 dark:text-zinc-500">
|
<span v-if="hiddenTagCount > 0" class="text-slate-500 dark:text-zinc-400">
|
||||||
+{{ hiddenTagCount }}
|
+{{ hiddenTagCount }}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
@@ -233,7 +249,7 @@ const priorityTextClass = computed(() => {
|
|||||||
v-if="completedAtShort"
|
v-if="completedAtShort"
|
||||||
data-role="completed-at"
|
data-role="completed-at"
|
||||||
:title="`完成于 ${completedAtFull}`"
|
:title="`完成于 ${completedAtFull}`"
|
||||||
class="ml-auto text-emerald-700/80 dark:text-emerald-400/80"
|
class="ml-auto text-emerald-700 dark:text-emerald-400"
|
||||||
>
|
>
|
||||||
完成于 {{ completedAtShort }}
|
完成于 {{ completedAtShort }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -51,10 +51,10 @@ describe('TodayView 顶部', () => {
|
|||||||
expect(wrapper.text()).toContain(dayjs().format('YYYY年M月D日'))
|
expect(wrapper.text()).toContain(dayjs().format('YYYY年M月D日'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('显示四张统计卡片', () => {
|
it('显示四张统计卡片,与下方四个分组一一对应', () => {
|
||||||
const { wrapper } = setup()
|
const { wrapper } = setup()
|
||||||
const labels = wrapper.findAll('dt').map((node) => node.text())
|
const labels = wrapper.findAll('dt').map((node) => node.text())
|
||||||
expect(labels).toEqual(['逾期', '今天到期', '进行中', '本周完成'])
|
expect(labels).toEqual(['逾期', '今天到期', '进行中', '待办'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('统计卡片数值正确', () => {
|
it('统计卡片数值正确', () => {
|
||||||
@@ -66,8 +66,31 @@ describe('TodayView 顶部', () => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
const values = wrapper.findAll('dd').map((node) => node.text())
|
const values = wrapper.findAll('dd').map((node) => node.text())
|
||||||
// 逾期 1、今天到期 1、进行中 1、本周完成 1
|
// 逾期 1、今天到期 1、进行中 1、待办 0(已完成的不进任何分组)
|
||||||
expect(values).toEqual(['1', '1', '1', '1'])
|
expect(values).toEqual(['1', '1', '1', '0'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('「本周完成」放在页头,不占统计格', () => {
|
||||||
|
const { wrapper } = setup([{ title: '刚做完', status: 'done' }])
|
||||||
|
|
||||||
|
expect(wrapper.find('[data-role="completed-this-week"]').text()).toBe('1')
|
||||||
|
expect(wrapper.findAll('dt').map((node) => node.text())).not.toContain('本周完成')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('统计带四个数字等于对应分组的条数(同一屏不重复强调)', () => {
|
||||||
|
const { wrapper } = setup([
|
||||||
|
{ title: '逾期 A', dueDate: yesterday },
|
||||||
|
{ title: '逾期 B', dueDate: yesterday },
|
||||||
|
{ title: '今天', dueDate: today },
|
||||||
|
{ title: '进行中', status: 'doing' },
|
||||||
|
{ title: '待办 A' },
|
||||||
|
{ title: '待办 B' },
|
||||||
|
])
|
||||||
|
|
||||||
|
const stats = wrapper.findAll('dd').map((node) => node.text())
|
||||||
|
const counts = wrapper.findAll('[data-role="section-count"]').map((node) => node.text())
|
||||||
|
expect(stats).toEqual(['2', '1', '1', '2'])
|
||||||
|
expect(counts).toEqual(['2 项', '1 项', '1 项', '2 项'])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -91,7 +114,7 @@ describe('TodayView 分组', () => {
|
|||||||
|
|
||||||
// 每组各 1 条
|
// 每组各 1 条
|
||||||
const badges = wrapper.findAll('[data-role="section-count"]').map((node) => node.text())
|
const badges = wrapper.findAll('[data-role="section-count"]').map((node) => node.text())
|
||||||
expect(badges).toEqual(['1', '1', '1'])
|
expect(badges).toEqual(['1 项', '1 项', '1 项'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('没有任务的分组不渲染', () => {
|
it('没有任务的分组不渲染', () => {
|
||||||
|
|||||||
+45
-63
@@ -12,28 +12,16 @@ import { buildTodayBuckets, countTodayTasks } from '@/utils/today'
|
|||||||
|
|
||||||
const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] as const
|
const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'] as const
|
||||||
|
|
||||||
/** 用完整类名字符串,避免 Tailwind 扫描不到动态拼接的类 */
|
/**
|
||||||
|
* 用完整类名字符串,避免 Tailwind 扫描不到动态拼接的类。
|
||||||
|
* 只保留色点:分组计数已经降为纯文字,彩色徽章会把权重抬到标题之上,
|
||||||
|
* 而且和上方统计带说的还是同一个数字。
|
||||||
|
*/
|
||||||
const TONES = {
|
const TONES = {
|
||||||
rose: {
|
rose: { dot: 'bg-rose-500' },
|
||||||
dot: 'bg-rose-500',
|
amber: { dot: 'bg-amber-500' },
|
||||||
badge:
|
sky: { dot: 'bg-sky-500' },
|
||||||
'border-rose-200 bg-rose-50 text-rose-700 dark:border-rose-800 dark:bg-rose-950 dark:text-rose-300',
|
slate: { dot: 'bg-slate-400' },
|
||||||
},
|
|
||||||
amber: {
|
|
||||||
dot: 'bg-amber-500',
|
|
||||||
badge:
|
|
||||||
'border-amber-200 bg-amber-50 text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-300',
|
|
||||||
},
|
|
||||||
sky: {
|
|
||||||
dot: 'bg-sky-500',
|
|
||||||
badge:
|
|
||||||
'border-sky-200 bg-sky-50 text-sky-700 dark:border-sky-800 dark:bg-sky-950 dark:text-sky-300',
|
|
||||||
},
|
|
||||||
slate: {
|
|
||||||
dot: 'bg-slate-400',
|
|
||||||
badge:
|
|
||||||
'border-slate-200 bg-slate-50 text-slate-600 dark:border-zinc-700 dark:bg-zinc-800 dark:text-zinc-300',
|
|
||||||
},
|
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const store = useTaskStore()
|
const store = useTaskStore()
|
||||||
@@ -49,13 +37,18 @@ const stats = computed(() => computeStats(activeTasks.value))
|
|||||||
const todayCount = computed(() => countTodayTasks(buckets.value))
|
const todayCount = computed(() => countTodayTasks(buckets.value))
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统计卡片与下方分组一一对应,避免「数字和列表对不上」的困惑。
|
* 统计带与下方分组一一对应:四个数字就是四组任务量。
|
||||||
|
*
|
||||||
|
* 早先这里放的是「逾期 / 今天到期 / 进行中 / 本周完成」——「本周完成」在下方
|
||||||
|
* 没有对应分组,而「待办」这一组又没有统计卡,所谓「一一对应」并不成立;
|
||||||
|
* 三个数字还会在分组徽章上再说一遍。
|
||||||
|
* 现在「本周完成」属于回顾性指标,移到页头副标题;分组头只留纯文字「N 项」。
|
||||||
*/
|
*/
|
||||||
const statCards = computed(() => [
|
const statCards = computed(() => [
|
||||||
{ label: '逾期', value: stats.value.overdue, dot: 'bg-rose-500' },
|
{ label: '逾期', value: buckets.value.overdue.length, dot: 'bg-rose-500' },
|
||||||
{ label: '今天到期', value: stats.value.dueToday, dot: 'bg-amber-500' },
|
{ label: '今天到期', value: buckets.value.dueToday.length, dot: 'bg-amber-500' },
|
||||||
{ label: '进行中', value: buckets.value.inProgress.length, dot: 'bg-sky-500' },
|
{ label: '进行中', value: buckets.value.inProgress.length, dot: 'bg-sky-500' },
|
||||||
{ label: '本周完成', value: stats.value.completedThisWeek, dot: 'bg-emerald-500' },
|
{ label: '待办', value: buckets.value.pending.length, dot: 'bg-slate-400' },
|
||||||
])
|
])
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -65,34 +58,10 @@ const statCards = computed(() => [
|
|||||||
*/
|
*/
|
||||||
const sections = computed(() =>
|
const sections = computed(() =>
|
||||||
[
|
[
|
||||||
{
|
{ key: 'overdue', title: '已逾期', tone: 'rose' as const, items: buckets.value.overdue },
|
||||||
key: 'overdue',
|
{ key: 'dueToday', title: '今天到期', tone: 'amber' as const, items: buckets.value.dueToday },
|
||||||
title: '已逾期',
|
{ key: 'inProgress', title: '进行中', tone: 'sky' as const, items: buckets.value.inProgress },
|
||||||
hint: '建议优先处理',
|
{ key: 'pending', title: '待办', tone: 'slate' as const, items: buckets.value.pending },
|
||||||
tone: 'rose' as const,
|
|
||||||
items: buckets.value.overdue,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'dueToday',
|
|
||||||
title: '今天到期',
|
|
||||||
hint: '今天要完成',
|
|
||||||
tone: 'amber' as const,
|
|
||||||
items: buckets.value.dueToday,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'inProgress',
|
|
||||||
title: '进行中',
|
|
||||||
hint: '已开始,尚无紧迫截止日期',
|
|
||||||
tone: 'sky' as const,
|
|
||||||
items: buckets.value.inProgress,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'pending',
|
|
||||||
title: '待办',
|
|
||||||
hint: '没有紧迫截止时间,有空再做',
|
|
||||||
tone: 'slate' as const,
|
|
||||||
items: buckets.value.pending,
|
|
||||||
},
|
|
||||||
].filter((section) => section.items.length > 0),
|
].filter((section) => section.items.length > 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -128,20 +97,37 @@ function handleKeyMove(payload: { id: string; direction: MoveDirection }): void
|
|||||||
<h1 class="text-xl font-semibold tracking-tight text-slate-900 md:text-2xl dark:text-zinc-100">
|
<h1 class="text-xl font-semibold tracking-tight text-slate-900 md:text-2xl dark:text-zinc-100">
|
||||||
今日
|
今日
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-sm text-slate-500 dark:text-zinc-400">{{ todayLabel }}</p>
|
<!-- 「本周完成」在下方没有对应分组,属于回顾性指标,放页头而不是占一格统计 -->
|
||||||
|
<p class="text-sm text-slate-500 dark:text-zinc-400">
|
||||||
|
{{ todayLabel }} · 本周完成
|
||||||
|
<span
|
||||||
|
data-role="completed-this-week"
|
||||||
|
class="font-medium tabular-nums text-slate-600 dark:text-zinc-300"
|
||||||
|
>{{ stats.completedThisWeek }}</span
|
||||||
|
>
|
||||||
|
个
|
||||||
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<dl class="grid grid-cols-2 gap-3 md:grid-cols-4">
|
<!--
|
||||||
|
四格合成一条带:gap-px + 露出父级底色,得到 1px 分隔线。
|
||||||
|
比四张独立卡片少三道投影、四条边框,读起来是「一个总览」而不是四个小组件。
|
||||||
|
-->
|
||||||
|
<dl
|
||||||
|
class="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-slate-200 bg-slate-200 md:grid-cols-4 dark:border-zinc-800 dark:bg-zinc-800"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="card in statCards"
|
v-for="card in statCards"
|
||||||
:key="card.label"
|
:key="card.label"
|
||||||
class="rounded-xl border border-slate-200 bg-white px-4 py-3.5 shadow-card dark:border-zinc-800 dark:bg-zinc-900"
|
class="bg-white px-4 py-3.5 dark:bg-zinc-900"
|
||||||
>
|
>
|
||||||
<dt class="flex items-center gap-1.5 text-xs text-slate-500 dark:text-zinc-400">
|
<dt class="flex items-center gap-1.5 text-xs text-slate-500 dark:text-zinc-400">
|
||||||
<span class="size-1.5 rounded-full" :class="card.dot" aria-hidden="true" />
|
<span class="size-1.5 rounded-full" :class="card.dot" aria-hidden="true" />
|
||||||
{{ card.label }}
|
{{ card.label }}
|
||||||
</dt>
|
</dt>
|
||||||
<dd class="mt-1.5 text-2xl font-semibold tracking-tight text-slate-900 dark:text-zinc-100">
|
<dd
|
||||||
|
class="mt-1.5 text-2xl font-semibold tracking-tight tabular-nums text-slate-900 dark:text-zinc-100"
|
||||||
|
>
|
||||||
{{ card.value }}
|
{{ card.value }}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,13 +148,9 @@ function handleKeyMove(payload: { id: string; direction: MoveDirection }): void
|
|||||||
{{ section.title }}
|
{{ section.title }}
|
||||||
<span
|
<span
|
||||||
data-role="section-count"
|
data-role="section-count"
|
||||||
class="rounded-full border px-2 py-0.5 text-xs"
|
class="text-xs font-normal tabular-nums text-slate-500 dark:text-zinc-400"
|
||||||
:class="TONES[section.tone].badge"
|
|
||||||
>
|
>
|
||||||
{{ section.items.length }}
|
{{ section.items.length }} 项
|
||||||
</span>
|
|
||||||
<span class="text-xs font-normal text-slate-400 dark:text-zinc-500">
|
|
||||||
{{ section.hint }}
|
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user