feat: 更新统计卡片与分组对应关系,优化样式与可访问性
CI / verify (push) Successful in 1m41s
CI / 部署到服务器 (push) Successful in 26s

This commit is contained in:
chenshoulin
2026-10-24 18:31:45 +08:00
parent 81ef0cc43f
commit 300b2de37e
7 changed files with 158 additions and 98 deletions
+10 -4
View File
@@ -48,7 +48,7 @@ const LINKS = [
>
<a
: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="
isExactActive
? 'bg-slate-900 font-medium text-white shadow-sm dark:bg-zinc-100 dark:text-zinc-900'
@@ -62,7 +62,7 @@ const LINKS = [
</RouterLink>
</nav>
<div class="ml-auto flex items-center gap-1.5">
<div class="ml-auto flex flex-none items-center gap-1.5">
<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"
@@ -118,9 +118,14 @@ const LINKS = [
</svg>
</button>
<!--
320px 是 WCAG 1.4.10 要求的最小重排宽度,此时单行放不下四个操作按钮,
会横向溢出 20px。快捷键帮助是纯桌面向的次要入口(手机上也没有物理键盘),
在 <360 收起,优先保住「不横向滚动」。
-->
<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="快捷键帮助"
@click="ui.openHelp()"
>
@@ -172,6 +177,7 @@ const LINKS = [
<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"
aria-label="备份"
@click="ui.openBackup()"
>
<svg viewBox="0 0 20 20" class="size-4" fill="none" aria-hidden="true">
@@ -189,7 +195,7 @@ const LINKS = [
stroke-linecap="round"
/>
</svg>
备份
<span class="hidden sm:inline">备份</span>
</button>
</div>
</header>
+7 -1
View File
@@ -66,8 +66,14 @@ useDraggable(listRef, {
</script>
<template>
<!--
列容器是「托盘」:页面 slate-100 → 列 slate-50 → 卡片 white,三级递增。
早先是 bg-slate-100/70,叠在同样是 slate-100 的页面上等于同色(差值为 0),
列只剩一根 1px 边框,看不到边界;深色下 zinc-900/60 叠 zinc-950 反而是分层的,
两套模式就出现了「深色有容器、浅色没有」的不对等。
-->
<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">
<span class="size-1.5 shrink-0 rounded-full" :class="dotClass" aria-hidden="true" />
+27 -2
View File
@@ -113,10 +113,26 @@ describe('截止日期', () => {
})
describe('已完成样式', () => {
it('标题加删除线并整体降饱和', () => {
it('标题加删除线,但不再整卡降透明度', () => {
const wrapper = mountCard({ status: 'done' })
const classes = wrapper.find('article').classes().join(' ')
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('未完成时不加删除线', () => {
@@ -189,6 +205,15 @@ describe('交互', () => {
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 () => {
const wrapper = mountCard({ id: 'task-7' })
await wrapper.find('article').trigger('click')
+37 -21
View File
@@ -91,21 +91,28 @@ const timeTitle = computed(() => {
/**
* 卡片外观。选中态与「已完成」都要覆盖边框/背景,
* 因此这些颜色放在条件分支里,而不是和基础类各写一份靠优先级取胜。
*
* 已完成不再整卡降透明度:opacity-60 会把标题压到 1.69:1、标签压到 1.61:1,
* 回看上周做了什么、或者要取消勾选时都读不清。改为「删除线 + 收敛文字色 +
* 实心绿勾 + 完成日期」四个局部信号,底色保持白色。
*/
const cardClass = computed(() => {
if (props.selected) {
return 'border-sky-400 bg-sky-50 dark:border-sky-600 dark:bg-sky-950/40'
}
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'
})
/**
* 优先级用左侧色条表达,而不是一枚彩色胶囊。
* 色条几乎不占空间,扫视一列卡片时却比文字更快分辨出轻重。
* 已完成的任务把色条降为中性灰:完成态再挂一条饱和色条,
* 会和绿勾、删除线一起抢注意力,看起来像还在报警。
*/
const priorityBarClass = computed(() => {
if (isDone.value) return 'bg-slate-300 dark:bg-zinc-600'
switch (props.task.priority) {
case 3:
return 'bg-rose-400'
@@ -123,7 +130,7 @@ const priorityTextClass = computed(() => {
case 2:
return 'text-amber-700 dark:text-amber-400'
default:
return 'text-slate-400 dark:text-zinc-500'
return 'text-slate-500 dark:text-zinc-400'
}
})
</script>
@@ -170,35 +177,44 @@ const priorityTextClass = computed(() => {
</svg>
</span>
<!--
命中区 32×36,视觉仍是 16px 圆点:靠负 margin 把按钮撑大,再用内层 span 保持圆点尺寸。
WCAG 2.5.8(AA)要求 24×24,原来 16×16 不合格;卡片只有 59px 高,
再往上撑到 44 就会把整条左边缘都变成「勾选」,容易误触,36 是这里的实际上限。
-->
<button
v-else
type="button"
class="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border transition"
: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'
"
class="group -mx-2 -my-2 flex h-9 w-8 shrink-0 items-center justify-center rounded-full"
:aria-label="isDone ? '标记为未完成' : '标记为已完成'"
:aria-pressed="isDone"
@click.stop="emit('toggle', task.id)"
>
<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
class="flex size-4 items-center justify-center rounded-full border transition"
:class="
isDone
? 'border-emerald-600 bg-emerald-600 text-white'
: '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'
"
>
<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>
<h3
class="min-w-0 flex-1 text-sm leading-snug wrap-break-word"
:class="
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'
"
>
@@ -222,10 +238,10 @@ const priorityTextClass = computed(() => {
{{ dueLabel }}
</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 }}
</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 }}
</span>
@@ -233,7 +249,7 @@ const priorityTextClass = computed(() => {
v-if="completedAtShort"
data-role="completed-at"
: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 }}
</span>