feat(board): 支持批量添加与批量删除任务
- 批量添加面板:一行一条,整批共用目标列、优先级、标签与截止日期 - 快速新增支持粘贴多行文本,直接拆成多条任务 - 多选模式支持全选可见任务、整批删除,撤销时连原顺序一起还原 - 批量创建与删除走 store 的批量方法,一次落盘、一次广播 - 补充单元测试与 E2E 用例
This commit is contained in:
@@ -6,12 +6,18 @@ import { formatDueDate, isOverdue } from '@/utils/date'
|
||||
|
||||
const MAX_VISIBLE_TAGS = 3
|
||||
|
||||
const props = defineProps<{ task: Task }>()
|
||||
const props = defineProps<{
|
||||
task: Task
|
||||
/** 多选模式:卡片改为可勾选,点击是「选中」而不是「打开详情」 */
|
||||
selectable?: boolean
|
||||
selected?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
toggle: [id: string]
|
||||
open: [id: string]
|
||||
keymove: [payload: { id: string; direction: MoveDirection }]
|
||||
select: [id: string]
|
||||
}>()
|
||||
|
||||
/** 方向键映射;左右换列,上下调顺序 */
|
||||
@@ -22,6 +28,15 @@ const MOVE_KEYS: Readonly<Record<string, MoveDirection>> = {
|
||||
ArrowDown: 'down',
|
||||
}
|
||||
|
||||
/** 多选态下点击/回车都是「切换选中」;平时是「打开详情」 */
|
||||
function activate(): void {
|
||||
if (props.selectable) {
|
||||
emit('select', props.task.id)
|
||||
return
|
||||
}
|
||||
emit('open', props.task.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 卡片获得焦点后的键盘操作。
|
||||
* 注意:方向键必须 preventDefault,否则会连带滚动整个看板。
|
||||
@@ -29,10 +44,13 @@ const MOVE_KEYS: Readonly<Record<string, MoveDirection>> = {
|
||||
function onKeydown(event: KeyboardEvent): void {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault()
|
||||
emit('open', props.task.id)
|
||||
activate()
|
||||
return
|
||||
}
|
||||
|
||||
// 多选态不做方向键移动:此时拖拽与排序都已停用,避免误操作打乱顺序
|
||||
if (props.selectable) return
|
||||
|
||||
const direction = MOVE_KEYS[event.key]
|
||||
if (!direction) return
|
||||
|
||||
@@ -45,6 +63,19 @@ const overdue = computed(() => isOverdue(props.task.dueDate, props.task.status))
|
||||
const visibleTags = computed(() => props.task.tags.slice(0, MAX_VISIBLE_TAGS))
|
||||
const hiddenTagCount = computed(() => Math.max(0, props.task.tags.length - MAX_VISIBLE_TAGS))
|
||||
|
||||
/**
|
||||
* 卡片外观。选中态与「已完成」都要覆盖边框/背景,
|
||||
* 因此这些颜色放在条件分支里,而不是和基础类各写一份靠优先级取胜。
|
||||
*/
|
||||
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 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'
|
||||
})
|
||||
|
||||
/**
|
||||
* 优先级用左侧色条表达,而不是一枚彩色胶囊。
|
||||
* 色条几乎不占空间,扫视一列卡片时却比文字更快分辨出轻重。
|
||||
@@ -74,22 +105,47 @@ const priorityTextClass = computed(() => {
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="relative shrink-0 cursor-pointer overflow-hidden rounded-lg border border-slate-200 bg-white shadow-card transition duration-150 dark:border-zinc-800 dark:bg-zinc-900"
|
||||
:class="
|
||||
isDone
|
||||
? 'opacity-60'
|
||||
: 'hover:-translate-y-px hover:border-slate-300 hover:shadow-card-hover dark:hover:border-zinc-700'
|
||||
"
|
||||
class="relative shrink-0 cursor-pointer overflow-hidden rounded-lg border shadow-card transition duration-150"
|
||||
:class="cardClass"
|
||||
:data-task-id="task.id"
|
||||
:role="selectable ? 'checkbox' : undefined"
|
||||
:aria-checked="selectable ? selected === true : undefined"
|
||||
:aria-label="selectable ? `选择任务「${task.title}」` : undefined"
|
||||
tabindex="0"
|
||||
@click="emit('open', task.id)"
|
||||
@click="activate"
|
||||
@keydown="onKeydown"
|
||||
>
|
||||
<span class="absolute inset-y-0 left-0 w-[3px]" :class="priorityBarClass" aria-hidden="true" />
|
||||
|
||||
<div class="py-2 pr-2.5 pl-3.5">
|
||||
<div class="flex items-start gap-2">
|
||||
<!--
|
||||
多选态下用一枚纯装饰的勾选框:真正的可交互元素是整张卡片,
|
||||
这样读屏与键盘只有「一个」可聚焦控件,不会出现嵌套按钮。
|
||||
-->
|
||||
<span
|
||||
v-if="selectable"
|
||||
class="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded border transition"
|
||||
:class="
|
||||
selected
|
||||
? 'border-sky-500 bg-sky-500 text-white'
|
||||
: 'border-slate-300 bg-white dark:border-zinc-600 dark:bg-zinc-800'
|
||||
"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg v-if="selected" viewBox="0 0 12 12" class="size-2.5" fill="none">
|
||||
<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
|
||||
v-else
|
||||
type="button"
|
||||
class="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border transition"
|
||||
:class="
|
||||
|
||||
Reference in New Issue
Block a user