diff --git a/src/api/admin/competitions.ts b/src/api/admin/competitions.ts index 63b2bac..ba9e511 100644 --- a/src/api/admin/competitions.ts +++ b/src/api/admin/competitions.ts @@ -69,7 +69,8 @@ export async function createCompetition(payload: CompetitionPayload): Promise { - const { data } = await adminHttp.put(`/competitions/${id}`, payload) + // 正式环境部分网关会挂起 PUT,改用 PATCH + const { data } = await adminHttp.patch(`/competitions/${id}`, payload) const body = (data as { data?: CompetitionRow })?.data ?? (data as CompetitionRow) if (!body || typeof body !== 'object') throw new Error('更新响应无效') return body as CompetitionRow diff --git a/src/utils/datetimeShanghai.ts b/src/utils/datetimeShanghai.ts new file mode 100644 index 0000000..6f4950d --- /dev/null +++ b/src/utils/datetimeShanghai.ts @@ -0,0 +1,63 @@ +/** 业务时间统一按北京时间(Asia/Shanghai,无夏令时)处理。 */ + +const SHANGHAI = 'Asia/Shanghai' + +function pad(n: number): string { + return String(n).padStart(2, '0') +} + +/** + * API ISO / 任意可解析时间 → 日期时间选择器模型(北京时间 `YYYY-MM-DD HH:mm:ss`)。 + */ +export function toShanghaiPickerValue(iso: string | null | undefined): string | null { + if (!iso) return null + const d = new Date(iso) + if (Number.isNaN(d.getTime())) return null + + const parts = new Intl.DateTimeFormat('en-GB', { + timeZone: SHANGHAI, + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hourCycle: 'h23', + }).formatToParts(d) + + const get = (type: Intl.DateTimeFormatPartTypes): string => + parts.find((p) => p.type === type)?.value ?? '00' + + const hour = get('hour') === '24' ? '00' : get('hour') + return `${get('year')}-${get('month')}-${get('day')} ${hour}:${get('minute')}:${get('second')}` +} + +/** + * 选择器值(按北京时间理解)→ API ISO8601(带偏移,避免被当成 UTC)。 + */ +export function fromShanghaiPickerValue(s: string | null | undefined): string | null { + const t = (s ?? '').trim() + if (!t) return null + const m = t.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?/) + if (!m) return null + const [, y, mo, d, h, mi, sec = '00'] = m + return `${y}-${mo}-${d}T${h}:${mi}:${sec}+08:00` +} + +/** 列表/详情展示用北京时间。 */ +export function formatShanghaiDateTime(iso: string | null | undefined, empty = '—'): string { + const v = toShanghaiPickerValue(iso) + if (!v) return empty + return v.replace(/:\d{2}$/, '') // YYYY-MM-DD HH:mm +} + +export function formatShanghaiDateTimeFull(iso: string | null | undefined, empty = '—'): string { + return toShanghaiPickerValue(iso) ?? empty +} + +/** 调试/校验用 */ +export function shanghaiOffsetLabel(): string { + return `${SHANGHAI} (UTC+8)` +} + +export { pad } diff --git a/src/views/admin/AdminDashboardView.vue b/src/views/admin/AdminDashboardView.vue index 624d58a..9d35994 100644 --- a/src/views/admin/AdminDashboardView.vue +++ b/src/views/admin/AdminDashboardView.vue @@ -10,6 +10,7 @@ import type { } from '../../api/admin/types' import { useAdminCompetitionStore } from '../../stores/adminCompetition' import { competitionStatusLabel } from '../../utils/competitionAdminLabels' +import { formatShanghaiDateTime } from '../../utils/datetimeShanghai' type SetupKey = keyof AdminDashboardSetup @@ -147,8 +148,7 @@ async function loadDashboard() { function formatDateTime(value: string | null | undefined): string { if (!value) return '未配置' - const d = new Date(value) - return Number.isNaN(d.getTime()) ? value : d.toLocaleString() + return formatShanghaiDateTime(value, '未配置') } function statusTagType(status: string): 'success' | 'info' | 'warning' | 'danger' | 'primary' { diff --git a/src/views/admin/competition/CompetitionFormView.vue b/src/views/admin/competition/CompetitionFormView.vue index e3f8c02..d24ae89 100644 --- a/src/views/admin/competition/CompetitionFormView.vue +++ b/src/views/admin/competition/CompetitionFormView.vue @@ -15,6 +15,7 @@ import { } from '../../../api/admin/competitions' import * as formSchemaApi from '../../../api/admin/formSchemas' import { setSignupFieldRegistry } from '../../../utils/signupApplicationFields' +import { fromShanghaiPickerValue, toShanghaiPickerValue } from '../../../utils/datetimeShanghai' import { createSignupChannel, getSignupChannel, @@ -337,22 +338,14 @@ function resolveCompetitionId(): number | null { return sid && sid > 0 ? sid : null } -/** API ISO → Element Plus 日期时间选择器(value-format `YYYY-MM-DD HH:mm:ss`,本地时区) */ +/** API ISO → Element Plus 日期时间选择器(北京时间 `YYYY-MM-DD HH:mm:ss`) */ function toElDateTimeModel(iso: string | null | undefined): string | null { - if (!iso) return null - const d = new Date(iso) - if (Number.isNaN(d.getTime())) return null - const pad = (n: number) => String(n).padStart(2, '0') - return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` + return toShanghaiPickerValue(iso) } +/** 选择器北京时间 → API(显式 +08:00,避免被当成 UTC 再偏移) */ function fromDatetimePickerValue(s: string | null | undefined): string | null { - const t = (s ?? '').trim() - if (!t) return null - const forParse = t.includes('T') ? t : t.replace(/^(\d{4}-\d{2}-\d{2})\s+/, '$1T') - const d = new Date(forParse) - if (Number.isNaN(d.getTime())) return null - return d.toISOString() + return fromShanghaiPickerValue(s) } async function refreshSignupSchemas() { diff --git a/src/views/admin/competition/CompetitionListView.vue b/src/views/admin/competition/CompetitionListView.vue index 37339c3..e500267 100644 --- a/src/views/admin/competition/CompetitionListView.vue +++ b/src/views/admin/competition/CompetitionListView.vue @@ -4,6 +4,7 @@ import { ElMessage } from 'element-plus' import { listCompetitions } from '../../../api/admin/competitions' import type { CompetitionRow } from '../../../api/admin/types' import { competitionStatusLabel } from '../../../utils/competitionAdminLabels' +import { formatShanghaiDateTime } from '../../../utils/datetimeShanghai' import { useAdminCompetitionStore } from '../../../stores/adminCompetition' const rows = ref([]) @@ -37,9 +38,7 @@ function goPage(p: number) { } function formatDt(iso: string | null) { - if (!iso) return '—' - const d = new Date(iso) - return Number.isNaN(d.getTime()) ? iso : d.toLocaleString() + return formatShanghaiDateTime(iso) } onMounted(() => {