feat(admin): 新增统一时间格式化 formatDateTime(YYYY-MM-DD HH:mm:ss)

This commit is contained in:
2026-09-05 21:47:31 +08:00
parent 6a0eb8cf63
commit cabbb4be0b
2 changed files with 97 additions and 0 deletions
+45
View File
@@ -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}`
}
@@ -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'), '—', '非法历法日期回退而非抛错')
})