diff --git a/apps/web/app/versions/[id]/page.tsx b/apps/web/app/versions/[id]/page.tsx index 3f5191f..be59346 100644 --- a/apps/web/app/versions/[id]/page.tsx +++ b/apps/web/app/versions/[id]/page.tsx @@ -27,6 +27,7 @@ import { useBugStore } from '@/stores/useBugStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { calcGroupProgress as calcDevTaskProgress, calcActualHoursByDates } from '@/lib/dev-task'; +import { formatDateTime } from '@/lib/format'; const PRIORITY_STYLE: Record = { P0: 'bg-red-500/10 text-red-600', @@ -397,7 +398,7 @@ export default function VersionDetailPage() { }); vDTs.forEach((t) => { if (t.startDate) startDates.push(t.startDate); }); vTCsAll.forEach((c) => { if (c.startedAt) startDates.push(c.startedAt); }); - const actualStart = startDates.length > 0 ? startDates.sort()[0].slice(0, 16).replace('T', ' ') : (version.startDate ?? null); + const actualStart = startDates.length > 0 ? formatDateTime(startDates.sort()[0]) : (version.startDate ?? null); // 实际截止日期 const endDates: string[] = []; @@ -405,7 +406,7 @@ export default function VersionDetailPage() { vDTs.forEach((t) => { if (t.completedAt) endDates.push(t.completedAt); }); vTCsAll.forEach((c) => { if (c.completedAt) endDates.push(c.completedAt); }); vBugsAll.forEach((b) => { if (b.closedAt) endDates.push(b.closedAt); }); - const actualEnd = endDates.length > 0 ? endDates.sort().reverse()[0].slice(0, 16).replace('T', ' ') : null; + const actualEnd = endDates.length > 0 ? formatDateTime(endDates.sort().reverse()[0]) : null; // 逾期天数 const deadline = version.expectedReleaseDate; diff --git a/apps/web/app/workspace/page.tsx b/apps/web/app/workspace/page.tsx index 1f7cf0b..2e24917 100644 --- a/apps/web/app/workspace/page.tsx +++ b/apps/web/app/workspace/page.tsx @@ -19,6 +19,7 @@ import { TestCaseDetailDrawer } from '@/components/test-case/TestCaseDetailDrawe import { BugDetailDrawer } from '@/components/bug/BugDetailDrawer'; import { BugCreateModal } from '@/components/bug/BugCreateModal'; import { DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR } from '@/lib/dev-task'; +import { formatDateTime } from '@/lib/format'; import { TEST_CASE_STATUS_LABEL, TEST_CASE_STATUS_COLOR } from '@/lib/test-case'; import { BUG_STATUS_LABEL, BUG_STATUS_COLOR, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug'; @@ -322,7 +323,7 @@ function WorkItemCard({ item, onNavigate, onClick }: { item: WorkItem; onNavigat / {item.extra?.dueDate && 截止 {item.extra.dueDate}} - {item.extra?.startTime && {item.extra.startTime.slice(0, 16).replace('T', ' ')} → {item.extra.endTime?.slice(0, 16).replace('T', ' ')}} + {item.extra?.startTime && {formatDateTime(item.extra.startTime)} → {formatDateTime(item.extra.endTime)}} ); diff --git a/apps/web/components/bug/BugDetailDrawer.tsx b/apps/web/components/bug/BugDetailDrawer.tsx index e90581e..fdb5b52 100644 --- a/apps/web/components/bug/BugDetailDrawer.tsx +++ b/apps/web/components/bug/BugDetailDrawer.tsx @@ -9,6 +9,7 @@ import { useRequirementStore } from '@/stores/useRequirementStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { useAuthStore } from '@/stores/useAuthStore'; import { BUG_ALLOWED_TRANSITIONS, BUG_STATUS_LABEL, BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug'; +import { formatDateTime } from '@/lib/format'; import type { BugStatus } from '@/lib/bug'; const LOG_ACTION_LABEL: Record = { @@ -160,9 +161,9 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) {
提交人:{bug.reportedBy}
修复人:{bug.assigneeId}
-
提交:{bug.createdAt.slice(0, 16).replace('T', ' ')}
- {bug.resolvedAt &&
修复:{bug.resolvedAt.slice(0, 16).replace('T', ' ')}
} - {bug.closedAt &&
关闭:{bug.closedAt.slice(0, 16).replace('T', ' ')}
} +
提交:{formatDateTime(bug.createdAt)}
+ {bug.resolvedAt &&
修复:{formatDateTime(bug.resolvedAt)}
} + {bug.closedAt &&
关闭:{formatDateTime(bug.closedAt)}
}
@@ -199,7 +200,7 @@ export function BugDetailDrawer({ bugId, onClose, contextLabel }: Props) {
{[...bug.logs].reverse().map((log) => (
- {log.createdAt.slice(0, 16).replace('T', ' ')} + {formatDateTime(log.createdAt)}
{log.operator} {LOG_ACTION_LABEL[log.action] || log.action} diff --git a/apps/web/components/dev-task/DevTaskDetailDrawer.tsx b/apps/web/components/dev-task/DevTaskDetailDrawer.tsx index b9955e5..f5596d5 100644 --- a/apps/web/components/dev-task/DevTaskDetailDrawer.tsx +++ b/apps/web/components/dev-task/DevTaskDetailDrawer.tsx @@ -9,6 +9,7 @@ import { useTaskCategoryStore } from '@/stores/useTaskCategoryStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { ALLOWED_TRANSITIONS, DEV_TASK_STATUS_LABEL, DEV_TASK_STATUS_COLOR, formatHours, calcActualHoursByDates } from '@/lib/dev-task'; +import { formatDateTime } from '@/lib/format'; import type { DevTaskStatus } from '@/lib/dev-task'; interface Props { @@ -188,7 +189,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
开始开发 - {task.startDate.slice(0, 16).replace('T', ' ')} + {formatDateTime(task.startDate)}
)} {task.dueDate && ( @@ -202,7 +203,7 @@ export function DevTaskDetailDrawer({ taskId, allTaskIds, onClose, contextLabel
提测 - {task.completedAt.slice(0, 16).replace('T', ' ')} + {formatDateTime(task.completedAt)}
)}
diff --git a/apps/web/components/test-case/TestCaseDetailDrawer.tsx b/apps/web/components/test-case/TestCaseDetailDrawer.tsx index 75ecd13..95d880f 100644 --- a/apps/web/components/test-case/TestCaseDetailDrawer.tsx +++ b/apps/web/components/test-case/TestCaseDetailDrawer.tsx @@ -10,6 +10,7 @@ import { useRequirementStore } from '@/stores/useRequirementStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { TC_ALLOWED_TRANSITIONS, TEST_CASE_STATUS_LABEL } from '@/lib/test-case'; import { calcActualHoursByDates } from '@/lib/dev-task'; +import { formatDateTime } from '@/lib/format'; import { BUG_SEVERITY_LABEL, BUG_SEVERITY_COLOR } from '@/lib/bug'; import type { TestCaseStatus } from '@/lib/test-case'; @@ -153,8 +154,8 @@ export function TestCaseDetailDrawer({ testCaseId, onClose, onCreateBug, context
负责人:{tc.assigneeId || '-'}
创建人:{tc.createdBy}
创建日期:{tc.createdAt.slice(0, 10)}
- {tc.startedAt &&
开始执行:{tc.startedAt.slice(0, 16).replace('T', ' ')}
} - {tc.completedAt &&
执行完成:{tc.completedAt.slice(0, 16).replace('T', ' ')}
} + {tc.startedAt &&
开始执行:{formatDateTime(tc.startedAt)}
} + {tc.completedAt &&
执行完成:{formatDateTime(tc.completedAt)}
} {tc.startedAt &&
测试耗时:{calcActualHoursByDates(tc.startedAt, tc.completedAt)}h
}
diff --git a/apps/web/components/version/PlanDetailDrawer.tsx b/apps/web/components/version/PlanDetailDrawer.tsx index c1ece92..5fca3ed 100644 --- a/apps/web/components/version/PlanDetailDrawer.tsx +++ b/apps/web/components/version/PlanDetailDrawer.tsx @@ -6,6 +6,7 @@ import { useVersionPlanStore } from '@/stores/useVersionPlanStore'; import { useRequirementStore } from '@/stores/useRequirementStore'; import { useMemberStore } from '@/stores/useMemberStore'; import { calcPlanProgress, calcLinkedReqProgress } from '@/lib/version-plan'; +import { formatDateTime } from '@/lib/format'; import type { PlanTask, VersionPlan } from '@/lib/version-plan'; interface Props { @@ -109,18 +110,18 @@ export function PlanDetailDrawer({ planId, onClose, contextLabel }: Props) {
计划时间 -
{plan.startTime.slice(0, 16).replace('T', ' ')} → {plan.endTime.slice(0, 16).replace('T', ' ')}
+
{formatDateTime(plan.startTime)} → {formatDateTime(plan.endTime)}
{plan.actualStartAt && (
实际开始 -
{plan.actualStartAt.slice(0, 16).replace('T', ' ')}
+
{formatDateTime(plan.actualStartAt)}
)} {plan.completedAt && (
完成时间 -
{plan.completedAt.slice(0, 16).replace('T', ' ')}
+
{formatDateTime(plan.completedAt)}
)} diff --git a/apps/web/components/version/PlanTab.tsx b/apps/web/components/version/PlanTab.tsx index 115e395..05e5b2b 100644 --- a/apps/web/components/version/PlanTab.tsx +++ b/apps/web/components/version/PlanTab.tsx @@ -4,6 +4,7 @@ import { useState } from 'react'; import { Plus, Pencil, Trash2, X, Check, ExternalLink, FileUp, Link2, Play, ArrowRightLeft } from 'lucide-react'; import type { VersionPlan, PlanTask } from '@/lib/version-plan'; import { calcPlanDuration, formatDuration, calcTotalDuration, calcPlanProgress, calcLinkedReqProgress } from '@/lib/version-plan'; +import { formatDateTime } from '@/lib/format'; interface Props { plans: VersionPlan[]; @@ -85,9 +86,9 @@ export function PlanTab({ plans, versionId, versionDeadline, currentUserName, pl
- 计划:{plan.startTime.slice(0, 16).replace('T', ' ')} → {plan.endTime.slice(0, 16).replace('T', ' ')} - {plan.actualStartAt && 实际开始:{plan.actualStartAt.slice(0, 16).replace('T', ' ')}} - {plan.completedAt && 完成:{plan.completedAt.slice(0, 16).replace('T', ' ')}} + 计划:{formatDateTime(plan.startTime)} → {formatDateTime(plan.endTime)} + {plan.actualStartAt && 实际开始:{formatDateTime(plan.actualStartAt)}} + {plan.completedAt && 完成:{formatDateTime(plan.completedAt)}} 已耗时 {durText}
{plan.overdueReason && ( diff --git a/apps/web/lib/format.ts b/apps/web/lib/format.ts new file mode 100644 index 0000000..f07f010 --- /dev/null +++ b/apps/web/lib/format.ts @@ -0,0 +1,46 @@ +/** + * 时间格式化工具——统一处理 ISO 时间戳到本地时区显示 + * 避免在 UI 直接 slice ISO 字符串导致显示 UTC 时间 + */ + +/** + * 格式化为 YYYY-MM-DD HH:mm(本地时区) + */ +export function formatDateTime(iso?: string | null): string { + if (!iso) return '-'; + const d = new Date(iso); + if (isNaN(d.getTime())) return '-'; + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + const hh = String(d.getHours()).padStart(2, '0'); + const mi = String(d.getMinutes()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd} ${hh}:${mi}`; +} + +/** + * 格式化为 YYYY-MM-DD(本地时区) + */ +export function formatDate(iso?: string | null): string { + if (!iso) return '-'; + const d = new Date(iso); + if (isNaN(d.getTime())) return '-'; + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + return `${yyyy}-${mm}-${dd}`; +} + +/** + * 格式化为 MM-DD HH:mm(紧凑版,本地时区) + */ +export function formatDateTimeShort(iso?: string | null): string { + if (!iso) return '-'; + const d = new Date(iso); + if (isNaN(d.getTime())) return '-'; + const mm = String(d.getMonth() + 1).padStart(2, '0'); + const dd = String(d.getDate()).padStart(2, '0'); + const hh = String(d.getHours()).padStart(2, '0'); + const mi = String(d.getMinutes()).padStart(2, '0'); + return `${mm}-${dd} ${hh}:${mi}`; +}