diff --git a/src/components/admin/admin-shell.tsx b/src/components/admin/admin-shell.tsx index 814e0c8..2a65f14 100644 --- a/src/components/admin/admin-shell.tsx +++ b/src/components/admin/admin-shell.tsx @@ -16,7 +16,7 @@ export function AdminShell({ children }: { children: ReactNode }) { return ( - +
@@ -25,7 +25,7 @@ export function AdminShell({ children }: { children: ReactNode }) {
-
+
{children}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 6042c42..a4895f6 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -20,9 +20,9 @@ const buttonVariants = cva( link: "text-primary underline-offset-4 hover:underline", }, size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", + default: + "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", icon: "size-8", diff --git a/src/lib/admin-datetime.ts b/src/lib/admin-datetime.ts index d4eea5d..137140b 100644 --- a/src/lib/admin-datetime.ts +++ b/src/lib/admin-datetime.ts @@ -1,7 +1,26 @@ import type { AdminApiLocale } from "@/lib/admin-locale"; -function pad2(n: number): string { - return String(n).padStart(2, "0"); +function formatParts(date: Date, timeZone?: string): string { + const parts = new Intl.DateTimeFormat("en-CA", { + timeZone, + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hourCycle: "h23", + }).formatToParts(date); + + const map = new Map(parts.map((part) => [part.type, part.value])); + const year = map.get("year") ?? "0000"; + const month = map.get("month") ?? "00"; + const day = map.get("day") ?? "00"; + const hour = map.get("hour") ?? "00"; + const minute = map.get("minute") ?? "00"; + const second = map.get("second") ?? "00"; + + return `${year}-${month}-${day} ${hour}:${minute}:${second}`; } /** @@ -21,12 +40,30 @@ export function formatAdminInstant( if (Number.isNaN(ms)) { return "—"; } - const date = new Date(ms); - const y = date.getFullYear(); - const m = pad2(date.getMonth() + 1); - const d = pad2(date.getDate()); - const h = pad2(date.getHours()); - const min = pad2(date.getMinutes()); - const s = pad2(date.getSeconds()); - return `${y}-${m}-${d} ${h}:${min}:${s}`; + return formatParts(new Date(ms)); +} + +/** + * 将接口返回的 ISO 时间串格式化到指定时区,便于并列展示多个地区的时间。 + */ +export function formatAdminInstantInTimeZone( + iso: string | null | undefined, + options: { + locale: AdminApiLocale; + timeZone: string; + }, +): string { + void options.locale; + if (iso == null || iso === "") { + return "—"; + } + const ms = Date.parse(iso); + if (Number.isNaN(ms)) { + return "—"; + } + try { + return formatParts(new Date(ms), options.timeZone); + } catch { + return formatParts(new Date(ms)); + } } diff --git a/src/modules/config/config-version-actions.tsx b/src/modules/config/config-version-actions.tsx index 17885b9..6f4719a 100644 --- a/src/modules/config/config-version-actions.tsx +++ b/src/modules/config/config-version-actions.tsx @@ -40,7 +40,7 @@ export function ConfigVersionActions({