/** * 前台公开端点 —— 预测主站与积分榜所需的最小数据访问。 * * 为什么要单独成文件(而不是继续挂在 `admin/dal.ts` 里): * * 1. **分层方向错了。** 面向公众的页面反向 import `../admin/dal`, * 意味着想重构后台就会牵连前台,目录名也持续误导人以为 * `admin/` 是可独立删除的包。公开端点的归属本就不该是 admin。 * * 2. **它真的会进用户的首屏。** `dal.ts` 是单个大模块,内部函数 * 相互引用,打包器无法按调用点做 tree-shaking —— 实测 * Matches 只用 2 个函数,但 `/admin/ingest/jobs`、`/admin/llm/models`、 * `/admin/eval`、`/admin/backtest`、`/admin/logs` 这些字符串全部 * 留在了公开页所在 chunk 里。抽出来后可省下约 55 kB 首屏 JS。 * * 本文件只放**无需登录即可访问**的端点。任何需要鉴权的端点继续留在 * `./dal.ts`。新增公开端点时请加在这里,不要加回 admin。 */ import { api, API_BASE } from './api' import type { MatchDetailOut, MatchContextOut } from './types' // ── 积分榜 ────────────────────────────────────────────────────── export interface StandingRow { position: number team: string team_en: string played: number won: number drawn: number lost: number goals_for: number goals_against: number goal_diff: number points: number xg_for: number | null xg_against: number | null form: string | null zone: string | null } export interface StandingsLeague { league_code: string league_name: string season: string retrieved_at: string | null rows: StandingRow[] } export async function fetchStandings(league?: string, season?: string): Promise<{ leagues: StandingsLeague[] }> { const sp = new URLSearchParams() if (league) sp.set('league', league) if (season) sp.set('season', season) const qs = sp.toString() return api.get<{ leagues: StandingsLeague[] }>(`${API_BASE}/standings${qs ? `?${qs}` : ''}`) } // ── 比赛详情 ──────────────────────────────────────────────────── export function fetchMatchDetail(id: number): Promise { return api.get(`${API_BASE}/matches/${id}`) } export function fetchMatchContext(id: number): Promise { return api.get(`${API_BASE}/matches/${id}/context`) }