feat: M3-M7 全模块落地 + 集成接线
- M3 认证/权限/审计/限流 + 全部 REST 接口 - M4 管理面板(MIUIx + 成绩录入/积分榜下钻/大屏控制) - M5a 大屏页面(六页 + 明细覆盖层 + 缩放适配) - M6 WebSocket 实时同步中心 - M7 xlsx 导出 + 赛事包 zip + 备份 - 规则包 YAML 导入导出(差异预览) - 字体 vendor 子集化(4 套 x2 字重) - 集成:realtime/export/rules 接入 main+httpapi;backup 子命令 - 修复:中间件包装 ResponseWriter 丢失 Hijacker,导致 WS 握手 501
This commit is contained in:
1 parent
d0e3b93e76
commit
bc8b1ee6c0
145 files changed
+25159
-321
No files matched your search
@@ -0,0 +1,172 @@
|
||||
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<string | null>(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<T>(real: () => Promise<T>, mock: () => T | Promise<T>): Promise<T> {
|
||||
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<Bootstrap> {
|
||||
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<Event> =>
|
||||
call(() => client.events.get(id), () => mockDb.eventDetail(id)),
|
||||
|
||||
entries: (eventId: number): Promise<EntryWithResult[]> =>
|
||||
call(() => client.events.entries(eventId), () => mockDb.listEntries(eventId)),
|
||||
|
||||
saveDraft: (payload: SaveDraftPayload): Promise<Result[]> =>
|
||||
call(() => client.results.saveDraft(payload), () => mockDb.saveDraft(payload)),
|
||||
|
||||
publish: (payload: PublishPayload): Promise<Result[]> =>
|
||||
call(() => client.results.publish(payload), () => mockDb.publish(payload)),
|
||||
|
||||
unpublish: (payload: PublishPayload): Promise<Result[]> =>
|
||||
call(() => client.results.unpublish(payload), () => mockDb.unpublish(payload)),
|
||||
|
||||
standings: (): Promise<StandingRow[]> =>
|
||||
call(() => client.standings.teams(), () => mockDb.standings()),
|
||||
|
||||
athleteStandings: (): Promise<AthleteStanding[]> =>
|
||||
call(() => client.standings.athletes(), () => mockDb.athleteStandings()),
|
||||
|
||||
breakdown: (teamId: number): Promise<TeamBreakdown> =>
|
||||
call(() => client.standings.breakdown(teamId), () => mockDb.breakdown(teamId)),
|
||||
|
||||
records: (): Promise<RecordEntry[]> =>
|
||||
call(() => client.records.list(), () => mockDb.records()),
|
||||
|
||||
adjustments: (query: PageQuery = {}): Promise<Page<PointAdjustment>> =>
|
||||
call(() => client.adjustments.list(query), () => mockDb.adjustments()),
|
||||
|
||||
auditLogs: (query: PageQuery = {}): Promise<Page<AuditLog>> =>
|
||||
call(() => client.auditLogs.list(query), () => mockDb.auditLogs(query.page ?? 1, query.size ?? 20)),
|
||||
|
||||
displayCommand: (command: DisplayCommand): Promise<void> =>
|
||||
call(() => client.display.command(command), () => undefined),
|
||||
|
||||
login: (username: string, password: string): Promise<CurrentUser> =>
|
||||
call(
|
||||
() => client.auth.login(username, password),
|
||||
() => mockDb.login(username),
|
||||
),
|
||||
|
||||
logout: (): Promise<void> => call(() => client.auth.logout(), () => undefined),
|
||||
|
||||
me: (): Promise<CurrentUser> =>
|
||||
call(
|
||||
() => client.auth.me(),
|
||||
() => {
|
||||
// 示例数据模式下没有真实会话,统一按「未登录」处理。
|
||||
throw new ApiError('AUTH_REQUIRED', '示例数据模式下无会话', 0)
|
||||
},
|
||||
),
|
||||
|
||||
health: (): Promise<HealthInfo> =>
|
||||
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 }
|
||||
Reference in new issue
Block a user