feat: 添加起始日期功能并更新相关组件与测试用例
CI / verify (push) Successful in 1m57s
CI / 部署到服务器 (push) Successful in 29s

- 在任务创建和编辑界面中新增起始日期字段,默认值为当天。
- 更新任务卡片展示逻辑,仅在起始日期不是当天时显示相关信息。
- 修改相关测试用例以覆盖起始日期的功能,包括批量创建和编辑任务的场景。
- 数据库模式版本更新至4,以支持起始日期字段的存储与迁移。
This commit is contained in:
chenshoulin
2026-11-02 00:17:14 +08:00
parent 6cbc0cfc0f
commit 6d58227614
25 changed files with 204 additions and 13 deletions
+16 -2
View File
@@ -5,6 +5,7 @@ import { afterEach, describe, expect, it } from 'vitest'
import { createMemoryTaskRepository } from '@/data/taskRepository'
import { useTaskStore } from '@/stores/tasks'
import { useToastStore } from '@/stores/toast'
import { todayIso } from '@/utils/date'
import BatchAddDrawer from './BatchAddDrawer.vue'
@@ -68,13 +69,16 @@ describe('BatchAddDrawer 提交', () => {
expect(store.byColumn.todo.map((task) => task.title)).toEqual(['第一条', '第二条', '第三条'])
})
it('目标列、优先级、截止日期与标签整批共用', async () => {
it('目标列、优先级、起始日期、截止日期与标签整批共用', async () => {
const { store, wrapper } = setup()
await wrapper.find('textarea').setValue('第一条\n第二条')
await buttonByText(wrapper, '进行中')?.trigger('click')
await buttonByText(wrapper, '高')?.trigger('click')
await wrapper.find('input[type="date"]').setValue('2026-10-20')
// 两个日期输入按出现顺序为:起始日期、截止日期;
// 每次赋值后 DOM 会被重渲染重建,必须重新查询再操作下一个
await wrapper.findAll('input[type="date"]')[0]?.setValue('2026-10-12')
await wrapper.findAll('input[type="date"]')[1]?.setValue('2026-10-20')
const tagInput = wrapper.find('input[aria-label="添加标签"]')
await tagInput.setValue('前端')
@@ -85,11 +89,21 @@ describe('BatchAddDrawer 提交', () => {
const created = store.byColumn.doing
expect(created.map((task) => task.title)).toEqual(['第一条', '第二条'])
expect(created[0]?.priority).toBe(3)
expect(created[0]?.startDate).toBe('2026-10-12')
expect(created[0]?.dueDate).toBe('2026-10-20')
expect(created[0]?.tags).toEqual(['前端'])
expect(created[1]?.tags).toEqual(['前端'])
})
it('起始日期默认预填当天', async () => {
const { store, wrapper } = setup()
await wrapper.find('textarea').setValue('A')
await buttonByText(wrapper, '添加 1 个任务')?.trigger('click')
expect(store.tasks[0]?.startDate).toBe(todayIso())
})
it('提交后请求关闭并提示数量', async () => {
const { wrapper } = setup()
await wrapper.find('textarea').setValue('A\nB')
+11 -1
View File
@@ -5,7 +5,7 @@
* 一行一条,整段粘贴即可录入一批 —— 从笔记、Markdown 清单、聊天记录里
* 直接复制过来就能用,行首的「-」「1.」这类列表符号会被自动去掉。
*
* 目标列、优先级、标签、截止日期是这一批**共用**的属性:批量录入时逐个
* 目标列、优先级、标签、起始日期、截止日期是这一批**共用**的属性:批量录入时逐个
* 任务调字段不现实,而「先统一建出来、再单独改个别任务」正是看板的用法。
*/
@@ -15,6 +15,7 @@ import { useTaskStore } from '@/stores/tasks'
import { useToastStore } from '@/stores/toast'
import { COLUMNS, PRIORITY_LABELS, type ColumnId, type Priority } from '@/types/task'
import { parseTaskLines } from '@/utils/batch'
import { todayIso } from '@/utils/date'
import DatePicker from './DatePicker.vue'
import SideDrawer from './SideDrawer.vue'
@@ -33,6 +34,7 @@ const text = ref('')
const status = ref<ColumnId>('todo')
const priority = ref<Priority>(2)
const dueDate = ref<string | null>(null)
const startDate = ref<string | null>(todayIso())
const tags = ref<string[]>([])
const titles = computed(() => parseTaskLines(text.value))
@@ -47,6 +49,8 @@ watch(
status.value = 'todo'
priority.value = 2
dueDate.value = null
// 每批都从「今天开工」开始,与 store 的默认值保持一致
startDate.value = todayIso()
tags.value = []
},
)
@@ -60,6 +64,7 @@ function submit(): void {
status: status.value,
priority: priority.value,
dueDate: dueDate.value,
startDate: startDate.value,
tags: [...tags.value],
})),
)
@@ -138,6 +143,11 @@ function submit(): void {
</div>
</fieldset>
<div class="flex flex-col gap-1.5">
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">起始日期</span>
<DatePicker v-model="startDate" />
</div>
<div class="flex flex-col gap-1.5">
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">截止日期</span>
<DatePicker v-model="dueDate" />
+1
View File
@@ -38,6 +38,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-18T00:00:00.000Z',
updatedAt: '2026-10-18T00:00:00.000Z',
+19
View File
@@ -26,6 +26,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-18T00:00:00.000Z',
updatedAt: '2026-10-18T00:00:00.000Z',
@@ -131,6 +132,24 @@ describe('截止日期', () => {
})
})
describe('起始日期', () => {
it('起始日期为当天时不显示「开始」', () => {
const wrapper = mountCard({ startDate: dayjs().format('YYYY-MM-DD') })
expect(wrapper.text()).not.toContain('开始')
})
it('起始日期不是当天时显示「M月D日 开始」', () => {
const startDate = dayjs().add(3, 'day').format('YYYY-MM-DD')
const wrapper = mountCard({ startDate })
expect(wrapper.text()).toContain('开始')
expect(wrapper.text()).toContain(dayjs(startDate).format('M月D日'))
})
it('未设置起始日期时不显示「开始」', () => {
expect(mountCard({ startDate: null }).text()).not.toContain('开始')
})
})
describe('已完成样式', () => {
it('标题加删除线,但不再整卡降透明度', () => {
const wrapper = mountCard({ status: 'done' })
+28 -1
View File
@@ -3,7 +3,13 @@ import { computed } from 'vue'
import { useTagColorStore } from '@/stores/tagColors'
import { PRIORITY_LABELS, type MoveDirection, type Task } from '@/types/task'
import { formatCompactDate, formatDateTime, formatDueDateForTask, isOverdue } from '@/utils/date'
import {
formatCompactDate,
formatDateTime,
formatDueDateForTask,
isOverdue,
todayIso,
} from '@/utils/date'
const MAX_VISIBLE_TAGS = 3
@@ -73,6 +79,19 @@ const dueLabel = computed(() =>
? formatDueDateForTask(props.task.dueDate, props.task.status, props.task.completedAt)
: null,
)
/**
* 起始日期:只在「不是今天」时显示。
*
* 新建任务的起始日期默认就是当天,若照常展示,几乎每张卡片都会挂一条
* 相同的「M月D日 开始」,把标签和截止日期挤到第二行却提供不了新信息。
* 保留下来的是真正有排期差异的那些:提前排的、拖到未来开工的。
*/
const startLabel = computed(() => {
const startDate = props.task.startDate
if (!startDate || startDate === todayIso()) return null
const compact = formatCompactDate(startDate)
return compact ? `${compact} 开始` : null
})
const visibleTags = computed(() =>
props.task.tags.slice(0, MAX_VISIBLE_TAGS).map((tag) => ({
name: tag,
@@ -244,6 +263,14 @@ const priorityTextClass = computed(() => {
{{ PRIORITY_LABELS[task.priority] }}
</span>
<span
v-if="startLabel"
class="text-slate-500 dark:text-zinc-400"
:title="`开始于 ${props.task.startDate}`"
>
{{ startLabel }}
</span>
<span
v-if="dueLabel"
:class="
+13 -5
View File
@@ -6,6 +6,7 @@ import { describe, expect, it } from 'vitest'
import { createMemoryTaskRepository } from '@/data/taskRepository'
import { useTaskStore } from '@/stores/tasks'
import { useToastStore } from '@/stores/toast'
import { todayIso } from '@/utils/date'
import TaskDetailDrawer from './TaskDetailDrawer.vue'
@@ -50,9 +51,10 @@ describe('TaskDetailDrawer 回填', () => {
'原描述',
)
expect(wrapper.text()).toContain('#设计')
expect((wrapper.find('input[type="date"]').element as HTMLInputElement).value).toBe(
'2026-12-01',
)
// 两个日期输入按出现顺序为:起始日期、截止日期;起始日期由 store 默认为当天
const dateInputs = wrapper.findAll('input[type="date"]')
expect((dateInputs[0]?.element as HTMLInputElement).value).toBe(todayIso())
expect((dateInputs[1]?.element as HTMLInputElement).value).toBe('2026-12-01')
})
it('回填当前优先级(低)', () => {
@@ -81,7 +83,10 @@ describe('TaskDetailDrawer 保存', () => {
await wrapper.find('input[aria-label="标题"]').setValue('新标题')
await wrapper.find('textarea[aria-label="描述"]').setValue('新描述')
await buttonByText(wrapper, '高')?.trigger('click')
await wrapper.find('input[type="date"]').setValue('2026-11-11')
// 两个日期输入按出现顺序为:起始日期、截止日期;
// 每次赋值后 DOM 会被重渲染重建,必须重新查询再操作下一个
await wrapper.findAll('input[type="date"]')[0]?.setValue('2026-11-05')
await wrapper.findAll('input[type="date"]')[1]?.setValue('2026-11-11')
await wrapper.find('input[aria-label="添加标签"]').setValue('前端')
await wrapper.find('input[aria-label="添加标签"]').trigger('keydown', { key: 'Enter' })
await buttonByText(wrapper, '保存')?.trigger('click')
@@ -90,6 +95,7 @@ describe('TaskDetailDrawer 保存', () => {
expect(task?.title).toBe('新标题')
expect(task?.description).toBe('新描述')
expect(task?.priority).toBe(3)
expect(task?.startDate).toBe('2026-11-05')
expect(task?.dueDate).toBe('2026-11-11')
expect(task?.tags).toEqual(['设计', '前端'])
expect(wrapper.emitted('close')).toHaveLength(1)
@@ -118,7 +124,9 @@ describe('TaskDetailDrawer 保存', () => {
it('清空截止日期会写入 null', async () => {
const { store, wrapper, taskId } = setup()
await buttonByText(wrapper, '清除')?.trigger('click')
// 起始日期与截止日期各有一个「清除」,按出现顺序取后者
const clearButtons = wrapper.findAll('button').filter((node) => node.text() === '清除')
await clearButtons[1]?.trigger('click')
await buttonByText(wrapper, '保存')?.trigger('click')
expect(store.tasks.find((item) => item.id === taskId)?.dueDate).toBeNull()
})
+9
View File
@@ -24,6 +24,7 @@ const description = ref('')
const status = ref<ColumnId>('todo')
const priority = ref<Priority>(2)
const dueDate = ref<string | null>(null)
const startDate = ref<string | null>(null)
const tags = ref<string[]>([])
const titleError = ref<string | null>(null)
@@ -45,6 +46,7 @@ watch(
status.value = task?.status ?? 'todo'
priority.value = task?.priority ?? 2
dueDate.value = task?.dueDate ?? null
startDate.value = task?.startDate ?? null
tags.value = task ? [...task.tags] : []
titleError.value = null
confirmingClose.value = false
@@ -92,6 +94,7 @@ const dirty = computed(() => {
status.value !== task.status ||
priority.value !== task.priority ||
dueDate.value !== task.dueDate ||
startDate.value !== task.startDate ||
!sameTags(tags.value, task.tags)
)
})
@@ -126,6 +129,7 @@ function save(): void {
status: status.value,
priority: priority.value,
dueDate: dueDate.value,
startDate: startDate.value,
tags: [...tags.value],
})
emit('close')
@@ -239,6 +243,11 @@ function remove(): void {
</div>
</fieldset>
<div class="flex flex-col gap-1.5">
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">起始日期</span>
<DatePicker v-model="startDate" />
</div>
<div class="flex flex-col gap-1.5">
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">截止日期</span>
<DatePicker v-model="dueDate" />
+38 -2
View File
@@ -21,6 +21,7 @@ function validTask(overrides: Record<string, unknown> = {}): Record<string, unkn
priority: 2,
tags: ['设计'],
dueDate: '2026-10-20',
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-18T00:00:00.000Z',
updatedAt: '2026-10-18T00:00:00.000Z',
@@ -97,6 +98,12 @@ describe('coerceTask', () => {
expect(result?.sortOrder).toBe(0)
})
it('合法 startDate 保留,非法 startDate 收敛为 null', () => {
expect(coerceTask(validTask({ startDate: '2026-10-01' }))?.startDate).toBe('2026-10-01')
expect(coerceTask(validTask({ startDate: '2026-02-31' }))?.startDate).toBeNull()
expect(coerceTask(validTask({ startDate: undefined }))?.startDate).toBeNull()
})
it('tags 过滤掉非字符串与空白项', () => {
const result = coerceTask(validTask({ tags: ['ok', 1, null, ' ', 'ok2'] }))
expect(result?.tags).toEqual(['ok', 'ok2'])
@@ -265,17 +272,46 @@ describe('版本迁移', () => {
expect(state.tasks[0]?.archivedAt).toBe('2026-01-03T00:00:00.000Z')
})
it('v0/v1/v2 都能一路迁移到 v3', () => {
it('v3 数据迁移到 v4 时补上 startDate,其余字段不变', () => {
const v3 = JSON.stringify({
schemaVersion: 3,
tasks: [
{
id: 'legacy',
title: '旧任务',
status: 'todo',
priority: 2,
sortOrder: 1024,
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
completedAt: null,
archivedAt: null,
deletedAt: null,
},
],
})
const state = parsePersistedState(v3)
expect(state.schemaVersion).toBe(SCHEMA_VERSION)
expect(state.tasks[0]?.startDate).toBeNull()
expect(state.tasks[0]?.deletedAt).toBeNull()
expect(state.tasks[0]?.title).toBe('旧任务')
})
it('v0/v1/v2/v3 都能一路迁移到当前版本', () => {
const builds = [
() => ({ tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
() => ({ schemaVersion: 1, tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
() => ({ schemaVersion: 2, tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
() => ({ schemaVersion: 3, tasks: [{ id: 'a', title: 'A', status: 'todo', priority: 2 }] }),
]
for (const build of builds) {
const state = parsePersistedState(JSON.stringify(build()))
expect(state.schemaVersion).toBe(3)
expect(state.schemaVersion).toBe(SCHEMA_VERSION)
expect(state.tasks[0]?.deletedAt).toBeNull()
expect(state.tasks[0]?.startDate).toBeNull()
}
})
+8 -1
View File
@@ -9,7 +9,7 @@
import { isColumnId, isPriority, type Task } from '@/types/task'
export const SCHEMA_VERSION = 3
export const SCHEMA_VERSION = 4
/** 主数据在 localStorage 中的键名 */
export const STORAGE_KEY = 'kanban.tasks'
@@ -69,6 +69,7 @@ export function coerceTask(value: unknown): Task | null {
const tags = rawTags.filter((tag): tag is string => typeof tag === 'string' && tag.trim() !== '')
const dueDate = value.dueDate
const startDate = value.startDate
const sortOrder = value.sortOrder
const createdAt = readString(value, 'createdAt') ?? new Date().toISOString()
const updatedAt = readString(value, 'updatedAt') ?? createdAt
@@ -85,6 +86,7 @@ export function coerceTask(value: unknown): Task | null {
priority,
tags,
dueDate: isValidDateString(dueDate) ? dueDate : null,
startDate: isValidDateString(startDate) ? startDate : null,
sortOrder: typeof sortOrder === 'number' && Number.isFinite(sortOrder) ? sortOrder : 0,
createdAt,
updatedAt,
@@ -117,6 +119,7 @@ type Migration = (state: PersistedState) => PersistedState
* - v0:早期数据没有 schemaVersion 字段,任务结构与 v1 一致
* - v1 -> v2:新增 archivedAt 字段(归档功能)
* - v2 -> v3:新增 deletedAt 字段(回收站 / 软删除)
* - v3 -> v4:新增 startDate 字段(起始日期)
* 未来新增字段时,在此登记 n -> n+1 的转换,并同步提升 SCHEMA_VERSION。
*/
const MIGRATIONS: Record<number, Migration> = {
@@ -129,6 +132,10 @@ const MIGRATIONS: Record<number, Migration> = {
schemaVersion: 3,
tasks: state.tasks.map((task) => ({ ...task, deletedAt: null })),
}),
3: (state) => ({
schemaVersion: 4,
tasks: state.tasks.map((task) => ({ ...task, startDate: null })),
}),
}
/**
+1
View File
@@ -39,6 +39,7 @@ function makeTask(id: string): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-18T00:00:00.000Z',
updatedAt: '2026-10-18T00:00:00.000Z',
+1
View File
@@ -38,6 +38,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-18T00:00:00.000Z',
updatedAt: '2026-10-18T00:00:00.000Z',
+38 -1
View File
@@ -3,6 +3,7 @@ import { describe, expect, it, vi } from 'vitest'
import { createMemoryTaskRepository, type TaskRepository } from '@/data/taskRepository'
import type { Task } from '@/types/task'
import { todayIso } from '@/utils/date'
import { useTagColorStore } from './tagColors'
import { UNDO_LIMIT, useTaskStore } from './tasks'
@@ -33,6 +34,7 @@ describe('初始化', () => {
priority: 3,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-18T00:00:00.000Z',
updatedAt: '2026-10-18T00:00:00.000Z',
@@ -162,7 +164,14 @@ describe('批量创建', () => {
it('每条任务可以带各自的列、优先级、标签与截止日期', () => {
const store = freshStore()
const created = store.createTasks([
{ title: 'A', status: 'doing', priority: 3, tags: ['前端'], dueDate: '2026-10-20' },
{
title: 'A',
status: 'doing',
priority: 3,
tags: ['前端'],
dueDate: '2026-10-20',
startDate: '2026-10-12',
},
{ title: 'B', status: 'done' },
])
@@ -170,9 +179,21 @@ describe('批量创建', () => {
expect(created[0]?.priority).toBe(3)
expect(created[0]?.tags).toEqual(['前端'])
expect(created[0]?.dueDate).toBe('2026-10-20')
expect(created[0]?.startDate).toBe('2026-10-12')
expect(created[1]?.completedAt).not.toBeNull()
})
it('起始日期默认为当天,显式传 null 时保持为空', () => {
const store = freshStore()
const created = store.createTasks([
{ title: '未指定' },
{ title: '显式清空', startDate: null },
])
expect(created[0]?.startDate).toBe(todayIso())
expect(created[1]?.startDate).toBeNull()
})
it('跨列批量时各列内部都保持输入顺序', () => {
const store = freshStore()
store.createTasks([
@@ -533,6 +554,7 @@ describe('导出与导入', () => {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-18T00:00:00.000Z',
updatedAt: '2026-10-18T00:00:00.000Z',
@@ -691,6 +713,7 @@ function makePersistedTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-09-01T00:00:00.000Z',
updatedAt: '2026-09-01T00:00:00.000Z',
@@ -938,6 +961,19 @@ describe('批量编辑', () => {
expect(store.activeTasks.every((task) => task.dueDate === null)).toBe(true)
})
it('批量设置与清除起始日期', () => {
const store = freshStore()
const a = store.createTask({ title: 'A' })
const b = store.createTask({ title: 'B' })
const ids = [a?.id ?? '', b?.id ?? '']
store.updateTasks(ids, { startDate: '2026-11-03' })
expect(store.activeTasks.every((task) => task.startDate === '2026-11-03')).toBe(true)
store.updateTasks(ids, { startDate: null })
expect(store.activeTasks.every((task) => task.startDate === null)).toBe(true)
})
it('批量归档只处理未删除任务', () => {
const store = freshStore()
const a = store.createTask({ title: 'A', status: 'done' })
@@ -1279,6 +1315,7 @@ describe('撤销与重做', () => {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: longAgo,
updatedAt: longAgo,
+6
View File
@@ -19,6 +19,7 @@ import {
type TaskRepository,
} from '@/data/taskRepository'
import { COLUMN_IDS, DEFAULT_PRIORITY, type ColumnId, type Priority, type Task } from '@/types/task'
import { todayIso } from '@/utils/date'
import { createId } from '@/utils/id'
import { mergeTasks as mergeTaskLists } from '@/utils/merge'
import {
@@ -61,6 +62,8 @@ export interface CreateTaskInput {
priority?: Priority
tags?: string[]
dueDate?: string | null
/** 不传则默认当天;显式传 null 表示不填起始日期 */
startDate?: string | null
status?: ColumnId
}
@@ -358,6 +361,8 @@ export const useTaskStore = defineStore('tasks', () => {
priority: input.priority ?? DEFAULT_PRIORITY,
tags: normalizeTags(input.tags ?? []),
dueDate: input.dueDate ?? null,
// 起始日期默认当天:新建任务天然「今天开工」,用户显式清空时仍保持 null
startDate: input.startDate === undefined ? todayIso() : input.startDate,
sortOrder: sortOrderForPosition(working[status], 0),
createdAt: now,
updatedAt: now,
@@ -431,6 +436,7 @@ export const useTaskStore = defineStore('tasks', () => {
if (patch.status !== undefined) taskPatch.status = patch.status
if (patch.priority !== undefined) taskPatch.priority = patch.priority
if (patch.dueDate !== undefined) taskPatch.dueDate = patch.dueDate
if (patch.startDate !== undefined) taskPatch.startDate = patch.startDate
if (patch.addTags !== undefined || patch.removeTags !== undefined) {
const tags = tagsAfterBatchPatch(task.tags, patch)
+2
View File
@@ -28,6 +28,8 @@ export interface Task {
tags: string[]
/** 仅日期,格式 YYYY-MM-DD;无截止日期时为 null */
dueDate: string | null
/** 仅日期,格式 YYYY-MM-DD;任务尚未排期时为 null */
startDate: string | null
/** 同列内排序用的浮点数,采用相邻中点插入 */
sortOrder: number
createdAt: string
+1
View File
@@ -28,6 +28,7 @@ function makeTask(completedAt: string | null, overrides: Partial<Task> = {}): Ta
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:00:00.000Z',
+1
View File
@@ -25,6 +25,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:00:00.000Z',
+1
View File
@@ -14,6 +14,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-18T00:00:00.000Z',
updatedAt: '2026-10-18T00:00:00.000Z',
+1
View File
@@ -13,6 +13,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:00:00.000Z',
+1
View File
@@ -27,6 +27,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: sequence * SORT_ORDER_STEP,
createdAt: '2026-10-18T00:00:00.000Z',
updatedAt: '2026-10-18T00:00:00.000Z',
+1
View File
@@ -22,6 +22,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:00:00.000Z',
+1
View File
@@ -13,6 +13,7 @@ function makeTask(id: string, tags: string[]): Task {
priority: 2,
tags,
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:00:00.000Z',
+1
View File
@@ -15,6 +15,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: ['设计'],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:00:00.000Z',
+3
View File
@@ -23,6 +23,8 @@ export interface BatchPatch {
priority?: Priority
/** null 表示清空截止日期 */
dueDate?: string | null
/** null 表示清空起始日期 */
startDate?: string | null
addTags?: readonly string[]
removeTags?: readonly string[]
/** true 归档,false 取消归档 */
@@ -78,6 +80,7 @@ export function isBatchPatchEmpty(patch: BatchPatch): boolean {
patch.status === undefined &&
patch.priority === undefined &&
patch.dueDate === undefined &&
patch.startDate === undefined &&
!patch.addTags?.length &&
!patch.removeTags?.length &&
patch.archived === undefined
+1
View File
@@ -15,6 +15,7 @@ function makeTask(overrides: Partial<Task> = {}): Task {
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:00:00.000Z',
+1
View File
@@ -26,6 +26,7 @@ function makeTask(completedAt: string | null, overrides: Partial<Task> = {}): Ta
priority: 2,
tags: [],
dueDate: null,
startDate: null,
sortOrder: 1024,
createdAt: '2026-10-01T00:00:00.000Z',
updatedAt: '2026-10-01T00:00:00.000Z',