import { ref } from 'vue' import { ApiError, createApiClient } from '@/shared/api' import type { AthleteStanding, AuditLog, CurrentUser, DisplayCommand, EntryWithResult, Event, EventSettings, HealthInfo, Page, PageQuery, PointAdjustment, PublishPayload, RecordEntry, Result, SaveDraftPayload, ScoringProfile, Sport, StandingRow, Team, TeamBreakdown, } from '@/shared' import { mockDb } from './mock' /** * 管理端数据入口。 * * 后端未就绪(连接失败 / 超时 / 响应不是约定结构)时自动降级到内存示例数据, * 并把 `mockMode` 置为 true,界面据此显示「示例数据」标记。 * 业务错误(鉴权失败、校验失败、冲突)不降级——那些是真实服务端的回答,必须原样呈现。 */ const FALLBACK_CODES = ['NETWORK', 'TIMEOUT', 'PARSE_FAILED'] export const mockMode = ref(false) export const mockReason = ref(null) let unauthorizedHandler: (() => void) | null = null export function setUnauthorizedHandler(handler: () => void): void { unauthorizedHandler = handler } const client = createApiClient({ onUnauthorized: () => unauthorizedHandler?.(), }) function isFallback(error: unknown): boolean { // 404 表示该接口在当前服务端尚未实现(或整个后端没起来),同样按「未就绪」降级。 return error instanceof ApiError && (FALLBACK_CODES.includes(error.code) || error.status === 404) } async function call(real: () => Promise, mock: () => T | Promise): Promise { try { const value = await real() if (mockMode.value) { mockMode.value = false mockReason.value = null } return value } catch (error) { if (!isFallback(error)) throw error mockMode.value = true mockReason.value = error instanceof Error ? error.message : '无法连接服务端' return await mock() } } export interface Bootstrap { settings: EventSettings teams: Team[] sports: Sport[] events: Event[] profiles: ScoringProfile[] standings: StandingRow[] athleteStandings: AthleteStanding[] } async function bootstrap(): Promise { return call( async () => { const [settings, teams, sports, events, profiles, standings, athleteStandings] = await Promise.all([ client.settings.get(), client.teams.list(), client.sports.list(), client.events.list(), client.scoringProfiles.list(), client.standings.teams(), client.standings.athletes(), ]) return { settings, teams, sports, events, profiles, standings, athleteStandings } }, () => mockDb.bootstrap(), ) } export const adminApi = { client, bootstrap, eventDetail: (id: number): Promise => call(() => client.events.get(id), () => mockDb.eventDetail(id)), entries: (eventId: number): Promise => call(() => client.events.entries(eventId), () => mockDb.listEntries(eventId)), saveDraft: (payload: SaveDraftPayload): Promise => call(() => client.results.saveDraft(payload), () => mockDb.saveDraft(payload)), publish: (payload: PublishPayload): Promise => call(() => client.results.publish(payload), () => mockDb.publish(payload)), unpublish: (payload: PublishPayload): Promise => call(() => client.results.unpublish(payload), () => mockDb.unpublish(payload)), standings: (): Promise => call(() => client.standings.teams(), () => mockDb.standings()), athleteStandings: (): Promise => call(() => client.standings.athletes(), () => mockDb.athleteStandings()), breakdown: (teamId: number): Promise => call(() => client.standings.breakdown(teamId), () => mockDb.breakdown(teamId)), records: (): Promise => call(() => client.records.list(), () => mockDb.records()), adjustments: (query: PageQuery = {}): Promise> => call(() => client.adjustments.list(query), () => mockDb.adjustments()), auditLogs: (query: PageQuery = {}): Promise> => call(() => client.auditLogs.list(query), () => mockDb.auditLogs(query.page ?? 1, query.size ?? 20)), displayCommand: (command: DisplayCommand): Promise => call(() => client.display.command(command), () => undefined), login: (username: string, password: string): Promise => call( () => client.auth.login(username, password), () => mockDb.login(username), ), logout: (): Promise => call(() => client.auth.logout(), () => undefined), me: (): Promise => call( () => client.auth.me(), () => { // 示例数据模式下没有真实会话,统一按「未登录」处理。 throw new ApiError('AUTH_REQUIRED', '示例数据模式下无会话', 0) }, ), changePassword: (currentPassword: string, newPassword: string): Promise<{ relogin: boolean }> => call( () => client.auth.changePassword(currentPassword, newPassword), () => { throw new ApiError('INTERNAL', '示例数据模式下无法修改密码', 0) }, ), health: (): Promise => call( () => client.health(), () => mockDb.health(), ), exportUrl: (kind: 'standings' | 'breakdown' | 'results'): string | null => { if (mockMode.value) return null if (kind === 'standings') return client.exports.standingsUrl() if (kind === 'breakdown') return client.exports.breakdownUrl({}) return client.exports.resultsUrl() }, } export type AdminApi = typeof adminApi export { ApiError }