feat(board): 展示任务时间,并修正已完成任务的逾期基准
时间展示(ADR-008): - 详情抽屉新增只读「时间」区块:创建 / 更新常驻,完成时间缺失时标注「未完成」,归档任务追加归档时间;时间不进入草稿与 dirty 判定 - 已完成卡片在元信息行右侧显示「完成于 M月D日」,卡片根元素 title 汇总创建 / 完成 / 归档时间(精确到分钟) - 回收站条目补创建时间,删掉内联 formatTime,统一走 @/utils/date - date.ts 新增 formatDateTime(YYYY-MM-DD HH:mm)与 formatCompactDate(同年 M月D日 / 跨年带年份),空值与非法时间戳返回 null 逾期基准修复: - 新增 formatDueDateForTask:未完成任务仍以「今天」推算;已完成任务改以「完成日」推算,19 号截止、19 号当天完成的任务不会在 20 号显示「逾期 1 天」,完成时确实超期则定格为「逾期 N 天完成」,不再随日期增长 - 新增 localDateOf:completedAt 存 UTC 时间戳,按本地时区取日期,避免跨日偏差 - TaskCard 改用 dueLabel computed 取代模板内直接调用 formatDueDate;颜色仍由 isOverdue 决定,已完成卡片保持中性色 测试:date / TaskCard 补时间格式化与逾期基准用例(当天完成、提前完成、完成时超期定格、缺 completedAt、跨年),全量 728 单测通过;vue-tsc 与 eslint 干净。
This commit is contained in:
@@ -4,6 +4,7 @@ import { computed, ref, watch } from 'vue'
|
||||
import { useTaskStore } from '@/stores/tasks'
|
||||
import { useToastStore } from '@/stores/toast'
|
||||
import { COLUMNS, PRIORITY_LABELS, type ColumnId, type Priority, type Task } from '@/types/task'
|
||||
import { formatDateTime } from '@/utils/date'
|
||||
|
||||
import DatePicker from './DatePicker.vue'
|
||||
import SideDrawer from './SideDrawer.vue'
|
||||
@@ -53,6 +54,29 @@ watch(
|
||||
|
||||
const archived = computed(() => props.task !== null && props.task.archivedAt !== null)
|
||||
|
||||
/**
|
||||
* 只读时间信息。
|
||||
*
|
||||
* createdAt 与 updatedAt 由 store 维护,completedAt 在状态进出「已完成」时
|
||||
* 由 applyTaskPatch 写入 / 清除;这里只负责展示,不参与草稿与 dirty 判定
|
||||
* —— 时间不是用户能编辑的字段,改了标题也不该让「创建于」看起来被改过。
|
||||
*/
|
||||
const createdAtText = computed(() => formatDateTime(props.task?.createdAt ?? null))
|
||||
const updatedAtText = computed(() => formatDateTime(props.task?.updatedAt ?? null))
|
||||
const completedAtText = computed(() => formatDateTime(props.task?.completedAt ?? null))
|
||||
const archivedAtText = computed(() => formatDateTime(props.task?.archivedAt ?? null))
|
||||
|
||||
/**
|
||||
* 完成时间的兜底文案。
|
||||
*
|
||||
* 旧数据或手工导入的数据可能出现「status 是已完成、completedAt 却是空」的情况,
|
||||
* 此时显示「未记录」而不是「未完成」—— 后者会和卡片上的已完成状态自相矛盾。
|
||||
*/
|
||||
const completedLabel = computed(() => {
|
||||
if (completedAtText.value) return completedAtText.value
|
||||
return props.task?.status === 'done' ? '未记录' : '未完成'
|
||||
})
|
||||
|
||||
function sameTags(left: readonly string[], right: readonly string[]): boolean {
|
||||
return left.length === right.length && left.every((tag, index) => tag === right[index])
|
||||
}
|
||||
@@ -224,6 +248,40 @@ function remove(): void {
|
||||
<span class="text-sm font-medium text-slate-600 dark:text-zinc-400">标签</span>
|
||||
<TagInput v-model="tags" :suggestions="store.allTags" />
|
||||
</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"
|
||||
>
|
||||
<h3 id="task-time-heading" class="text-sm font-medium text-slate-600 dark:text-zinc-400">
|
||||
时间
|
||||
</h3>
|
||||
<dl class="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
|
||||
<dt class="text-slate-400 dark:text-zinc-500">创建</dt>
|
||||
<dd data-role="created-at" class="text-slate-600 dark:text-zinc-300">
|
||||
{{ createdAtText ?? '—' }}
|
||||
</dd>
|
||||
|
||||
<dt class="text-slate-400 dark:text-zinc-500">完成</dt>
|
||||
<dd data-role="completed-at" class="text-slate-600 dark:text-zinc-300">
|
||||
<span :class="completedAtText ? '' : 'text-slate-400 dark:text-zinc-500'">
|
||||
{{ completedLabel }}
|
||||
</span>
|
||||
</dd>
|
||||
|
||||
<dt class="text-slate-400 dark:text-zinc-500">更新</dt>
|
||||
<dd data-role="updated-at" class="text-slate-600 dark:text-zinc-300">
|
||||
{{ updatedAtText ?? '—' }}
|
||||
</dd>
|
||||
|
||||
<template v-if="archivedAtText">
|
||||
<dt class="text-slate-400 dark:text-zinc-500">归档</dt>
|
||||
<dd data-role="archived-at" class="text-slate-600 dark:text-zinc-300">
|
||||
{{ archivedAtText }}
|
||||
</dd>
|
||||
</template>
|
||||
</dl>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<template #footer>
|
||||
|
||||
Reference in New Issue
Block a user