feat: 更新统计卡片与分组对应关系,优化样式与可访问性
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user