From cabbb4be0bb777ea958fabbb9ae7f8aba06bfaf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 21:47:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=E6=96=B0=E5=A2=9E=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E6=97=B6=E9=97=B4=E6=A0=BC=E5=BC=8F=E5=8C=96=20format?= =?UTF-8?q?DateTime=EF=BC=88YYYY-MM-DD=20HH:mm:ss=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin-frontend-vue/src/utils/datetime.ts | 45 ++++++++++++++++ .../tests/datetime-format.test.ts | 52 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 admin-frontend-vue/src/utils/datetime.ts create mode 100644 admin-frontend-vue/tests/datetime-format.test.ts diff --git a/admin-frontend-vue/src/utils/datetime.ts b/admin-frontend-vue/src/utils/datetime.ts new file mode 100644 index 00000000..62634dd8 --- /dev/null +++ b/admin-frontend-vue/src/utils/datetime.ts @@ -0,0 +1,45 @@ +/** + * 后台统一时间展示:字符串「YYYY-MM-DD HH:mm:ss」。 + * 兼容 Java/MySQL 常见形态:ISO 带 T / 带毫秒 / 纯数字秒或毫秒 / Date 实例。 + */ +export function formatDateTime(value: unknown, fallback = '—'): string { + if (value == null || value === '') return fallback + if (value instanceof Date) { + if (Number.isNaN(value.getTime())) return fallback + return padDate(value) + } + const raw = String(value).trim() + if (!raw) return fallback + // 已是目标格式 + if (/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(raw)) return raw + const match = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{1,2}):(\d{2})(?::(\d{2}))?/.exec(raw) + if (match) { + const [, y, mo, d, h, mi, s] = match + return `${y}-${mo}-${d} ${pad(h)}:${mi}:${s ?? '00'}` + } + const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw) + if (dateOnly) return `${dateOnly[1]}-${dateOnly[2]}-${dateOnly[3]} 00:00:00` + if (/^\d+$/.test(raw)) { + const num = Number(raw) + const ms = num > 1e11 ? num : num * 1000 + const d = new Date(ms) + if (!Number.isNaN(d.getTime())) return padDate(d) + } + const parsed = new Date(raw) + if (!Number.isNaN(parsed.getTime())) return padDate(parsed) + return fallback +} + +function pad(n: string | number): string { + return String(n).padStart(2, '0') +} + +function padDate(d: Date): string { + const y = d.getFullYear() + const mo = pad(d.getMonth() + 1) + const day = pad(d.getDate()) + const h = pad(d.getHours()) + const mi = pad(d.getMinutes()) + const s = pad(d.getSeconds()) + return `${y}-${mo}-${day} ${h}:${mi}:${s}` +} diff --git a/admin-frontend-vue/tests/datetime-format.test.ts b/admin-frontend-vue/tests/datetime-format.test.ts new file mode 100644 index 00000000..ed0df8c3 --- /dev/null +++ b/admin-frontend-vue/tests/datetime-format.test.ts @@ -0,0 +1,52 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { formatDateTime } from '../src/utils/datetime.ts' + +// 全局时间展示统一为字符串「YYYY-MM-DD HH:mm:ss」(module13 用户需求)。 + +test('test_datetime_format_normal_primary_path', () => { + assert.equal(formatDateTime('2026-04-12T17:12:24'), '2026-04-12 17:12:24', 'ISO 带 T 需转空格') + assert.equal(formatDateTime('2026-09-05 08:00:00'), '2026-09-05 08:00:00', '已是目标格式原样返回') + assert.equal(formatDateTime('2026-04-12T10:45:46'), '2026-04-12 10:45:46') +}) + +test('test_datetime_format_normal_variant_input', () => { + assert.equal(formatDateTime('2026-09-05T08:00:00.000Z'), '2026-09-05 08:00:00', '毫秒/时区后缀应剥离') + assert.equal(formatDateTime('2026-09-05 08:00:00.000'), '2026-09-05 08:00:00') + assert.equal(formatDateTime('2026-04-12'), '2026-04-12 00:00:00', '仅日期补 00:00:00') +}) + +test('test_datetime_format_normal_repeated_operation_is_idempotent', () => { + const once = formatDateTime('2026-09-05T08:00:00') + assert.equal(formatDateTime(once), once, '对已格式化结果重复调用幂等') +}) + +test('test_datetime_format_boundary_empty_input', () => { + assert.equal(formatDateTime(null), '—', '空值回退默认 —') + assert.equal(formatDateTime(undefined), '—') + assert.equal(formatDateTime(''), '—') + assert.equal(formatDateTime('', '空'), '空', '支持自定义兜底') +}) + +test('test_datetime_format_boundary_single_item', () => { + const out = formatDateTime(1787890000) // 秒级时间戳 + assert.match(out, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/, '秒级时间戳输出完整格式') + const ms = formatDateTime(1787890000000) // 毫秒级时间戳 + assert.match(ms, /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/, '毫秒级时间戳输出完整格式') +}) + +test('test_datetime_format_boundary_limit_or_missing_field', () => { + assert.equal(formatDateTime(new Date(2026, 0, 15, 9, 30, 5)), '2026-01-15 09:30:05', 'Date 实例本地化格式化') +}) + +test('test_datetime_format_invalid_input_rejected', () => { + assert.equal(formatDateTime('abc'), '—', '非日期字符串回退') + assert.equal(formatDateTime('not-a-date'), '—') + assert.equal(formatDateTime(new Date('invalid')), '—', '无效 Date 回退') +}) + +test('test_datetime_format_dependency_failure_returns_actionable_message', () => { + // 兜底参数可区分“无值”与“解析失败”,避免把空态当异常。 + assert.equal(formatDateTime('', '暂无'), '暂无') + assert.equal(formatDateTime('2026/13/99'), '—', '非法历法日期回退而非抛错') +})