feat: 优化页面样式
CI / verify (push) Successful in 1m43s
CI / 部署到服务器 (push) Successful in 26s

This commit is contained in:
chenshoulin
2026-10-24 16:44:54 +08:00
parent 9ee1261b90
commit 81ef0cc43f
4 changed files with 268 additions and 96 deletions
+177 -91
View File
@@ -47,12 +47,17 @@ defineExpose({ focusSearch })
</script>
<template>
<header class="flex flex-col gap-2.5">
<header class="flex flex-col gap-2">
<!--
单行三段:搜索 │ 筛选 │ 操作。
早先是两行(第一行筛选、第二行统计 + 批量操作),「今日到期 / 逾期」
又被 ml-auto 顶到最右端,与搜索之间空出半个屏幕 —— 同一件事被切到两个空间。
-->
<div class="flex flex-wrap items-center gap-2">
<div class="relative min-w-56 flex-1 sm:max-w-xs">
<div class="relative min-w-44 flex-1 max-sm:order-1 max-sm:min-w-0 sm:max-w-[260px] xl:max-w-[320px]">
<svg
viewBox="0 0 20 20"
class="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-slate-400 dark:text-zinc-500"
class="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-slate-500 dark:text-zinc-400"
fill="none"
aria-hidden="true"
>
@@ -63,136 +68,217 @@ defineExpose({ focusSearch })
ref="searchRef"
v-model="keyword"
type="search"
class="h-9 w-full rounded-md border border-slate-200 bg-white pr-2.5 pl-8 text-sm text-slate-700 outline-none transition placeholder:text-slate-400 focus:border-sky-400 focus:ring-2 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:placeholder:text-zinc-500 dark:focus:border-sky-600 dark:focus:ring-sky-950"
class="h-9 w-full rounded-lg border border-slate-200 bg-white pr-2.5 pl-8 text-sm text-slate-700 outline-none transition placeholder:text-slate-500 focus:border-sky-400 focus:ring-2 focus:ring-sky-100 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-200 dark:placeholder:text-zinc-400 dark:focus:border-sky-600 dark:focus:ring-sky-950"
placeholder="搜索标题或描述"
aria-label="搜索任务"
/>
</div>
<div
class="flex h-9 items-center gap-0.5 rounded-md border border-slate-200 bg-white p-0.5 dark:border-zinc-700 dark:bg-zinc-900"
role="group"
aria-label="按优先级筛选"
>
<button
v-for="value in PRIORITY_ORDER"
:key="value"
type="button"
class="flex h-full items-center rounded px-2.5 text-xs transition"
:class="
priorities.includes(value)
? 'bg-slate-900 font-medium text-white dark:bg-zinc-100 dark:text-zinc-900'
: 'text-slate-600 hover:bg-slate-100 dark:text-zinc-400 dark:hover:bg-zinc-800'
"
:aria-pressed="priorities.includes(value)"
@click="filters.togglePriority(value)"
>
{{ PRIORITY_LABELS[value] }}
</button>
</div>
<TagFilter
:tags="store.allTags"
:selected="tags"
@toggle="filters.toggleTag($event)"
@clear="filters.clearTags()"
@manage="emit('manage-tags')"
/>
<div class="ml-auto flex items-center gap-1.5">
<!--
筛选:快捷截止日期 + 优先级 + 标签,同一个语义组。
这里**不能用 overflow-x-auto**:浏览器会把 overflow-y 一并算成 auto,
于是这一条 36px 高的行变成滚动容器,标签下拉面板(~175px 高)被裁在盒外
—— 表现就是「点了没反应」,右侧还多出一条滚动条。空间不够时改为换行。
-->
<div class="flex min-w-0 flex-wrap items-center gap-1.5 max-sm:order-3 max-sm:w-full">
<button
type="button"
class="flex h-9 items-center gap-1.5 rounded-md border px-2.5 text-xs transition"
class="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border px-2.5 text-[13px] whitespace-nowrap transition"
:class="
due === 'today'
? 'border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300'
? 'border-amber-300 bg-amber-50 font-medium text-amber-800 dark:border-amber-700 dark:bg-amber-950 dark:text-amber-300'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
"
:aria-pressed="due === 'today'"
@click="filters.toggleDue('today')"
>
今日到期
<span class="font-semibold">{{ dueTodayCount }}</span>
<span
class="font-semibold tabular-nums"
:class="due === 'today' || dueTodayCount === 0 ? '' : 'text-amber-700 dark:text-amber-400'"
>
{{ dueTodayCount }}
</span>
</button>
<button
type="button"
class="flex h-9 items-center gap-1.5 rounded-md border px-2.5 text-xs transition"
class="flex h-9 shrink-0 items-center gap-1.5 rounded-lg border px-2.5 text-[13px] whitespace-nowrap transition"
:class="
due === 'overdue'
? 'border-rose-300 bg-rose-50 text-rose-700 dark:border-rose-700 dark:bg-rose-950 dark:text-rose-300'
? 'border-rose-300 bg-rose-50 font-medium text-rose-700 dark:border-rose-700 dark:bg-rose-950 dark:text-rose-300'
: overdueCount > 0
? 'border-slate-200 bg-white text-rose-600 hover:border-rose-200 hover:bg-rose-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-rose-400 dark:hover:border-rose-800 dark:hover:bg-rose-950'
? 'border-slate-200 bg-white text-slate-600 hover:border-rose-200 hover:bg-rose-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-rose-800 dark:hover:bg-rose-950'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
"
:aria-pressed="due === 'overdue'"
@click="filters.toggleDue('overdue')"
>
逾期
<span class="font-semibold">{{ overdueCount }}</span>
<span
class="font-semibold tabular-nums"
:class="due === 'overdue' || overdueCount === 0 ? '' : 'text-rose-600 dark:text-rose-400'"
>
{{ overdueCount }}
</span>
</button>
</div>
</div>
<div class="flex flex-wrap items-center gap-x-2 gap-y-1.5 text-xs text-slate-500 dark:text-zinc-400">
<span>共 {{ store.activeTasks.length }} 个任务</span>
<span v-if="hiddenNote" class="text-slate-400 dark:text-zinc-500">{{ hiddenNote }}</span>
<span class="hidden h-5 w-px shrink-0 bg-slate-200 sm:block dark:bg-zinc-700" aria-hidden="true" />
<div class="ml-auto flex items-center gap-1.5">
<button
type="button"
class="flex h-8 items-center rounded-md border border-slate-200 bg-white px-2.5 text-xs text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"
@click="emit('batch-add')"
<div
class="flex h-9 shrink-0 items-center gap-0.5 rounded-lg border border-slate-200 bg-white p-0.5 dark:border-zinc-700 dark:bg-zinc-900"
role="group"
aria-label="按优先级筛选"
>
批量添加
</button>
<button
v-for="value in PRIORITY_ORDER"
:key="value"
type="button"
class="flex h-full items-center rounded-md px-2.5 text-[13px] whitespace-nowrap transition"
:class="
priorities.includes(value)
? 'bg-sky-50 font-semibold text-sky-700 ring-1 ring-sky-200 dark:bg-sky-950 dark:text-sky-300 dark:ring-sky-800'
: 'text-slate-600 hover:bg-slate-100 dark:text-zinc-400 dark:hover:bg-zinc-800'
"
:aria-pressed="priorities.includes(value)"
@click="filters.togglePriority(value)"
>
{{ PRIORITY_LABELS[value] }}
</button>
</div>
<button
type="button"
class="flex h-8 items-center rounded-md border px-2.5 text-xs transition"
:class="
selecting
? 'border-sky-300 bg-sky-50 text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
"
:aria-pressed="selecting === true"
@click="emit('toggle-select')"
>
{{ selecting ? '退出多选' : '多选' }}
</button>
<button
v-if="archivableCount > 0"
type="button"
class="flex h-8 items-center rounded-md border border-slate-200 bg-white px-2.5 text-xs text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"
@click="emit('archive')"
>
归档 {{ archivableCount }} 个已完成
</button>
<button
type="button"
class="flex h-8 items-center rounded-md border px-2.5 text-xs transition"
:class="
includeArchived
? 'border-sky-300 bg-sky-50 text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
"
:aria-pressed="includeArchived"
@click="filters.toggleArchived()"
>
显示归档
</button>
<TagFilter
:tags="store.allTags"
:selected="tags"
@toggle="filters.toggleTag($event)"
@clear="filters.clearTags()"
@manage="emit('manage-tags')"
/>
<!-- 筛选生效时才出现:常态下工具栏不摆一个点了没事干的按钮 -->
<button
v-if="active"
type="button"
class="flex h-8 items-center rounded-md px-2.5 text-xs text-slate-500 underline-offset-2 transition hover:text-slate-800 hover:underline dark:text-zinc-400 dark:hover:text-zinc-200"
class="flex h-9 shrink-0 items-center gap-1 rounded-lg px-2 text-[13px] whitespace-nowrap text-slate-600 transition hover:bg-slate-100 hover:text-slate-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-200"
@click="filters.clear()"
>
<svg
viewBox="0 0 20 20"
class="size-3.5 shrink-0"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
aria-hidden="true"
>
<path d="m5.5 5.5 9 9M14.5 5.5l-9 9" />
</svg>
清空筛选
</button>
</div>
<!-- 操作:主操作(批量添加)+ 两个视图开关,全部平铺,不再收进菜单 -->
<div class="ml-auto flex shrink-0 items-center gap-1.5 max-sm:order-2">
<button
type="button"
class="flex h-9 items-center gap-1.5 rounded-lg bg-sky-700 px-3 text-[13px] font-medium whitespace-nowrap text-white transition hover:bg-sky-800 active:scale-[0.98] max-lg:w-9 max-lg:justify-center max-lg:px-0 dark:bg-sky-500 dark:text-slate-950 dark:hover:bg-sky-400"
aria-label="批量添加"
title="批量添加"
@click="emit('batch-add')"
>
<svg
viewBox="0 0 20 20"
class="size-4"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M3.5 5.5h9M3.5 10h9M3.5 14.5h5" />
<path d="M15 11.5v5M12.5 14h5" />
</svg>
<span class="max-lg:hidden">批量添加</span>
</button>
<button
type="button"
class="flex h-9 items-center gap-1.5 rounded-lg border px-2.5 text-[13px] whitespace-nowrap transition max-lg:w-9 max-lg:justify-center max-lg:px-0"
:class="
selecting
? 'border-sky-300 bg-sky-50 font-medium text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
"
:aria-pressed="selecting === true"
:aria-label="selecting ? '退出多选' : '多选'"
:title="selecting ? '退出多选' : '多选'"
@click="emit('toggle-select')"
>
<svg
viewBox="0 0 20 20"
class="size-4 shrink-0"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect x="3.5" y="3.5" width="13" height="13" rx="3" />
<path d="m7.2 10.2 2 2 3.6-4.1" stroke-width="1.7" />
</svg>
<span class="max-lg:hidden">{{ selecting ? '退出多选' : '多选' }}</span>
</button>
<button
type="button"
class="flex h-9 items-center gap-1.5 rounded-lg border px-2.5 text-[13px] whitespace-nowrap transition max-lg:w-9 max-lg:justify-center max-lg:px-0"
:class="
includeArchived
? 'border-sky-300 bg-sky-50 font-medium text-sky-700 dark:border-sky-700 dark:bg-sky-950 dark:text-sky-300'
: 'border-slate-200 bg-white text-slate-600 hover:border-slate-300 hover:bg-slate-50 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800'
"
:aria-pressed="includeArchived"
aria-label="显示归档"
title="显示归档"
@click="filters.toggleArchived()"
>
<svg
viewBox="0 0 20 20"
class="size-4 shrink-0"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<rect x="3" y="4.5" width="14" height="3.5" rx="1" />
<path d="M4.6 8v6.4c0 .9.7 1.6 1.6 1.6h7.6c.9 0 1.6-.7 1.6-1.6V8" />
<path d="M8.4 11h3.2" />
</svg>
<span class="max-lg:hidden">显示归档</span>
</button>
</div>
</div>
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-slate-500 dark:text-zinc-400">
<span>共 {{ store.activeTasks.length }} 个任务</span>
<span v-if="hiddenNote" class="cursor-help" :title="`${hiddenNote};此时列内排序会暂停`">
{{ hiddenNote }}
</span>
<!--
归档是「有活了才出现」的清理动作,不该常驻占一个按钮位;
挪到这一行后只有真的攒够可归档任务时才露头。
-->
<button
v-if="archivableCount > 0"
type="button"
class="rounded text-slate-600 underline decoration-slate-300 underline-offset-2 transition hover:text-slate-900 hover:decoration-slate-500 dark:text-zinc-400 dark:decoration-zinc-600 dark:hover:text-zinc-200"
@click="emit('archive')"
>
归档 {{ archivableCount }} 个已完成
</button>
</div>
</header>
</template>