feat(admin,api,player): 赛事分组管理、盘口独立页与多语言展示优化

- 管理端按联赛展示单场,新增赛事/单场流程与列表展开状态保持

- 盘口赔率迁至独立页面,保存按钮仅在有修改时高亮

- API 新增联赛列表与子场查询,按 locale 返回队名并修复编译

- 波胆其它选项与促销标签等 i18n 补齐,文案更易懂
This commit is contained in:
2026-06-04 16:25:03 +08:00
parent c68abadceb
commit cc737e2924
39 changed files with 3330 additions and 378 deletions

View File

@@ -3,54 +3,133 @@
import { FormValidationError } from '../i18n/form-validation';
export interface MatchCreateForm {
leagueId: string;
leagueEn: string;
leagueZh: string;
leagueMs: string;
startTime: string;
homeTeamZh: string;
homeTeamEn: string;
homeTeamMs: string;
awayTeamZh: string;
awayTeamEn: string;
awayTeamMs: string;
isHot: boolean;
displayOrder: number;
matchName: string;
stage: string;
groupName: string;
leagueLogoUrl: string;
homeTeamLogoUrl: string;
awayTeamLogoUrl: string;
}
export function emptyMatchForm(): MatchCreateForm {
return {
leagueId: '',
leagueEn: 'FIFA World Cup 2026',
leagueZh: '2026 世界杯',
leagueMs: 'Piala Dunia 2026',
startTime: '',
homeTeamZh: '',
homeTeamEn: '',
homeTeamMs: '',
awayTeamZh: '',
awayTeamEn: '',
awayTeamMs: '',
isHot: false,
displayOrder: 0,
matchName: '',
stage: '',
groupName: '',
leagueLogoUrl: '',
homeTeamLogoUrl: '',
awayTeamLogoUrl: '',
};
}
export interface AdminMarketSelection {
id: string;
selectionCode: string;
selectionName: string;
odds: number;
status: string;
}
export interface AdminMarket {
id: string;
marketType: string;
period: string;
lineValue: number | null;
status: string;
promoLabel: string;
selections: AdminMarketSelection[];
}
export type AdminMatchDetail = {
id: string;
status: string;
isOutright: boolean;
isHot: boolean;
displayOrder: number;
startTime: string;
leagueEn: string;
leagueZh: string;
leagueMs: string;
leagueLogoUrl?: string;
homeTeamEn: string;
homeTeamZh: string;
homeTeamMs: string;
homeTeamCode?: string;
homeTeamLogoUrl?: string;
awayTeamEn: string;
awayTeamZh: string;
awayTeamMs: string;
awayTeamCode?: string;
awayTeamLogoUrl?: string;
matchName: string;
stage?: string;
groupName?: string;
markets?: AdminMarket[];
};
export function normalizeStartTimeForPicker(iso?: string): string {
if (!iso?.trim()) return '';
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso.slice(0, 19);
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
export function normalizeStartTimeForApi(value: string): string {
const trimmed = value.trim();
if (!trimmed) return '';
const d = new Date(trimmed);
if (Number.isNaN(d.getTime())) return trimmed;
return d.toISOString();
}
export function formFromDetail(d: AdminMatchDetail): MatchCreateForm {
return {
leagueId: '',
leagueEn: d.leagueEn,
leagueZh: d.leagueZh,
startTime: d.startTime,
leagueMs: d.leagueMs ?? '',
startTime: normalizeStartTimeForPicker(d.startTime),
homeTeamZh: d.homeTeamZh,
homeTeamEn: d.homeTeamEn,
homeTeamMs: d.homeTeamMs ?? '',
awayTeamZh: d.awayTeamZh,
awayTeamEn: d.awayTeamEn,
awayTeamMs: d.awayTeamMs ?? '',
isHot: d.isHot,
displayOrder: d.displayOrder ?? 0,
matchName: d.matchName ?? '',
stage: d.stage ?? '',
groupName: d.groupName ?? '',
leagueLogoUrl: d.leagueLogoUrl ?? '',
homeTeamLogoUrl: d.homeTeamLogoUrl ?? '',
awayTeamLogoUrl: d.awayTeamLogoUrl ?? '',
};
}
@@ -58,23 +137,39 @@ export function buildPlatformPayload(form: MatchCreateForm) {
if (!form.startTime.trim()) {
throw new FormValidationError('err.kickoff_required');
}
const homeOk = form.homeTeamZh.trim() || form.homeTeamEn.trim();
const awayOk = form.awayTeamZh.trim() || form.awayTeamEn.trim();
const homeOk = form.homeTeamZh.trim() || form.homeTeamEn.trim() || form.homeTeamMs.trim();
const awayOk = form.awayTeamZh.trim() || form.awayTeamEn.trim() || form.awayTeamMs.trim();
if (!homeOk || !awayOk) {
throw new FormValidationError('err.teams_required');
}
if (!form.leagueZh.trim() && !form.leagueEn.trim()) {
if (
!form.leagueId.trim() &&
!form.leagueZh.trim() &&
!form.leagueEn.trim() &&
!form.leagueMs.trim()
) {
throw new FormValidationError('err.league_required');
}
return {
leagueId: form.leagueId.trim() || undefined,
leagueEn: form.leagueEn.trim(),
leagueZh: form.leagueZh.trim(),
leagueMs: form.leagueMs.trim() || undefined,
homeTeamEn: form.homeTeamEn.trim(),
homeTeamZh: form.homeTeamZh.trim(),
homeTeamMs: form.homeTeamMs.trim() || undefined,
awayTeamEn: form.awayTeamEn.trim(),
awayTeamZh: form.awayTeamZh.trim(),
startTime: form.startTime.trim(),
awayTeamMs: form.awayTeamMs.trim() || undefined,
startTime: normalizeStartTimeForApi(form.startTime),
isHot: form.isHot,
displayOrder: form.displayOrder,
matchName: form.matchName.trim() || undefined,
stage: form.stage.trim() || undefined,
groupName: form.groupName.trim() || undefined,
leagueLogoUrl: form.leagueLogoUrl.trim() || undefined,
homeTeamLogoUrl: form.homeTeamLogoUrl.trim() || undefined,
awayTeamLogoUrl: form.awayTeamLogoUrl.trim() || undefined,
};
}