feat: 添加新建任务按钮功能,聚焦待办列输入框
CI / verify (push) Successful in 1m42s
CI / 部署到服务器 (push) Successful in 27s

This commit is contained in:
chenshoulin
2026-10-24 19:14:58 +08:00
parent 5133bf768c
commit 1d92ab2cb5
7 changed files with 117 additions and 13 deletions
+10
View File
@@ -46,6 +46,16 @@ describe('AppNav 导航', () => {
expect(links[1]?.attributes('aria-current')).toBe('page') expect(links[1]?.attributes('aria-current')).toBe('page')
}) })
it('当前入口用主色(sky-700)铺底,与实心主按钮同一套色', async () => {
const { wrapper } = await setup('/today')
const [board, today] = wrapper.findAll('a')
expect(today?.classes()).toContain('bg-sky-700')
// 早先是接近黑的 slate-900,与页面主色脱节
expect(today?.classes()).not.toContain('bg-slate-900')
expect(board?.classes()).not.toContain('bg-sky-700')
})
it('点击可切换路由', async () => { it('点击可切换路由', async () => {
const { wrapper, router } = await setup('/') const { wrapper, router } = await setup('/')
+2 -2
View File
@@ -51,8 +51,8 @@ const LINKS = [
class="shrink-0 rounded-md px-3 py-1.5 text-sm whitespace-nowrap 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-sky-700 font-medium text-white shadow-sm dark:bg-sky-500 dark:text-slate-950'
: 'text-slate-600 hover:bg-slate-100 hover:text-slate-900 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100' : 'text-slate-600 hover:bg-sky-50 hover:text-sky-800 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-100'
" "
:aria-current="isExactActive ? 'page' : undefined" :aria-current="isExactActive ? 'page' : undefined"
@click="navigate" @click="navigate"
+35
View File
@@ -189,6 +189,41 @@ describe('BoardToolbar 归档', () => {
}) })
}) })
describe('BoardToolbar 主次按钮', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('点击「新建任务」emit new-task', async () => {
const { wrapper } = setup()
await buttonByText(wrapper, '新建任务')?.trigger('click')
expect(wrapper.emitted('new-task')).toHaveLength(1)
})
it('实心主按钮只有「新建任务」,「批量添加」降为描边次要按钮', () => {
const { wrapper } = setup()
const primary = buttonByText(wrapper, '新建任务')
const batch = buttonByText(wrapper, '批量添加')
expect(primary?.classes()).toContain('bg-sky-700')
expect(primary?.classes()).toContain('text-white')
expect(batch?.classes()).toContain('bg-white')
expect(batch?.classes()).not.toContain('bg-sky-700')
})
it('「新建任务」在平板上仍带文字,不退化成纯图标', () => {
const { wrapper } = setup()
const button = buttonByText(wrapper, '新建任务')
// 主操作只在手机(<640)收成图标
expect(button?.find('span').classes()).toContain('max-sm:hidden')
expect(button?.classes()).not.toContain('max-lg:w-9')
})
})
describe('BoardToolbar 批量添加', () => { describe('BoardToolbar 批量添加', () => {
beforeEach(() => { beforeEach(() => {
setActivePinia(createPinia()) setActivePinia(createPinia())
+30 -2
View File
@@ -14,6 +14,7 @@ defineProps<{ hiddenNote?: string; selecting?: boolean }>()
const emit = defineEmits<{ const emit = defineEmits<{
archive: [] archive: []
'new-task': []
'batch-add': [] 'batch-add': []
'toggle-select': [] 'toggle-select': []
'manage-tags': [] 'manage-tags': []
@@ -177,11 +178,38 @@ defineExpose({ focusSearch })
</button> </button>
</div> </div>
<!-- 操作:主操作(批量添加)+ 两个视图开关,全部平铺,不再收进菜单 --> <!--
操作:主操作(新建任务)+ 三个次要操作(批量添加 / 多选 / 显示归档)。
主次靠「实心 vs 描边」表达:真正高频的是「再加一条」,批量录入是偶发动作,
早先把它做成唯一实心按钮,把用户最常做的事藏进了每列的输入框。
新建任务在 ≥640 一律带文字(主操作不该在平板上退化成一个小图标),
三个次要操作在 <1024 收成图标,靠 title / aria-label 保留可访问名。
-->
<div class="ml-auto flex shrink-0 items-center gap-1.5 max-sm:order-2"> <div class="ml-auto flex shrink-0 items-center gap-1.5 max-sm:order-2">
<button <button
type="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" 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] dark:bg-sky-500 dark:text-slate-950 dark:hover:bg-sky-400"
aria-label="新建任务"
title="新建任务(N)"
@click="emit('new-task')"
>
<svg
viewBox="0 0 20 20"
class="size-4 shrink-0"
fill="none"
stroke="currentColor"
stroke-width="1.7"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M10 4.5v11M4.5 10h11" />
</svg>
<span class="max-sm:hidden">新建任务</span>
</button>
<button
type="button"
class="flex h-9 items-center gap-1.5 rounded-lg border border-slate-200 bg-white px-2.5 text-[13px] whitespace-nowrap text-slate-600 transition hover:border-slate-300 hover:bg-slate-50 max-lg:w-9 max-lg:justify-center max-lg:px-0 dark:border-zinc-700 dark:bg-zinc-900 dark:text-zinc-300 dark:hover:border-zinc-600 dark:hover:bg-zinc-800"
aria-label="批量添加" aria-label="批量添加"
title="批量添加" title="批量添加"
@click="emit('batch-add')" @click="emit('batch-add')"
+13 -9
View File
@@ -249,34 +249,38 @@ function remove(): void {
<TagInput v-model="tags" :suggestions="store.allTags" /> <TagInput v-model="tags" :suggestions="store.allTags" />
</div> </div>
<section <!--
class="flex flex-col gap-2 rounded-md border border-slate-200 bg-slate-50/70 px-3 py-2.5 dark:border-zinc-800 dark:bg-zinc-800/40" 时间:标题与「标签」同级平铺,明细单独用一个描边框圈起来。
aria-labelledby="task-time-heading" 只靠字号(标题 text-sm / 明细 text-xs)分层,框只负责把只读信息
> 和上方的可编辑字段区分开,不参与层级表达。
-->
<section class="flex flex-col gap-1.5" aria-labelledby="task-time-heading">
<h3 id="task-time-heading" class="text-sm font-medium text-slate-600 dark:text-zinc-400"> <h3 id="task-time-heading" class="text-sm font-medium text-slate-600 dark:text-zinc-400">
时间 时间
</h3> </h3>
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs"> <dl
class="grid grid-cols-[auto_1fr] items-baseline gap-x-3 gap-y-0.5 rounded-md border border-slate-300 px-3 py-2 text-xs dark:border-zinc-700"
>
<dt class="text-slate-400 dark:text-zinc-500">创建</dt> <dt class="text-slate-400 dark:text-zinc-500">创建</dt>
<dd data-role="created-at" class="text-slate-600 dark:text-zinc-300"> <dd data-role="created-at" class="tabular-nums text-slate-600 dark:text-zinc-300">
{{ createdAtText ?? '—' }} {{ createdAtText ?? '—' }}
</dd> </dd>
<dt class="text-slate-400 dark:text-zinc-500">完成</dt> <dt class="text-slate-400 dark:text-zinc-500">完成</dt>
<dd data-role="completed-at" class="text-slate-600 dark:text-zinc-300"> <dd data-role="completed-at" class="tabular-nums text-slate-600 dark:text-zinc-300">
<span :class="completedAtText ? '' : 'text-slate-400 dark:text-zinc-500'"> <span :class="completedAtText ? '' : 'text-slate-400 dark:text-zinc-500'">
{{ completedLabel }} {{ completedLabel }}
</span> </span>
</dd> </dd>
<dt class="text-slate-400 dark:text-zinc-500">更新</dt> <dt class="text-slate-400 dark:text-zinc-500">更新</dt>
<dd data-role="updated-at" class="text-slate-600 dark:text-zinc-300"> <dd data-role="updated-at" class="tabular-nums text-slate-600 dark:text-zinc-300">
{{ updatedAtText ?? '—' }} {{ updatedAtText ?? '—' }}
</dd> </dd>
<template v-if="archivedAtText"> <template v-if="archivedAtText">
<dt class="text-slate-400 dark:text-zinc-500">归档</dt> <dt class="text-slate-400 dark:text-zinc-500">归档</dt>
<dd data-role="archived-at" class="text-slate-600 dark:text-zinc-300"> <dd data-role="archived-at" class="tabular-nums text-slate-600 dark:text-zinc-300">
{{ archivedAtText }} {{ archivedAtText }}
</dd> </dd>
</template> </template>
+17
View File
@@ -437,6 +437,23 @@ describe('BoardView 快捷键', () => {
}) })
}) })
describe('BoardView 新建任务', () => {
function buttonByText(wrapper: ReturnType<typeof mountBoard>, text: string) {
return wrapper.findAll('button').find((node) => node.text().includes(text))
}
it('点击工具栏「新建任务」聚焦待办列的快速新增输入框', async () => {
const { pinia } = setup()
const wrapper = mountBoard(pinia)
await nextTick()
await buttonByText(wrapper, '新建任务')?.trigger('click')
await nextTick()
expect(document.activeElement).toBe(wrapper.find('input[aria-label="添加任务"]').element)
})
})
describe('BoardView 键盘移动', () => { describe('BoardView 键盘移动', () => {
type Direction = 'left' | 'right' | 'up' | 'down' type Direction = 'left' | 'right' | 'up' | 'down'
+10
View File
@@ -141,6 +141,15 @@ useKeyboardShortcuts({
'/': whenNoPanel(() => toolbarRef.value?.focusSearch()), '/': whenNoPanel(() => toolbarRef.value?.focusSearch()),
}) })
/**
* 「新建任务」主按钮:焦点落到第一列(待办)的快速添加输入框 ——
* 与按 N 同一条路径,不再另造一套弹窗,保持「在列内就地录入」的心智。
*/
function focusNewTask(): void {
if (ui.anyPanelOpen) return
columnRefs.get(0)?.focusQuickAdd()
}
function toggleTask(id: string): void { function toggleTask(id: string): void {
const task = store.activeTasks.find((item) => item.id === id) const task = store.activeTasks.find((item) => item.id === id)
if (!task) return if (!task) return
@@ -318,6 +327,7 @@ watch(filter, () => {
:hidden-note="hiddenNote" :hidden-note="hiddenNote"
:selecting="selecting" :selecting="selecting"
@archive="archiveCompleted" @archive="archiveCompleted"
@new-task="focusNewTask"
@batch-add="batchOpen = true" @batch-add="batchOpen = true"
@toggle-select="toggleSelectMode" @toggle-select="toggleSelectMode"
@manage-tags="ui.openTags()" @manage-tags="ui.openTags()"