feat: split admin dashboard, improve match ops, and player closed-match UX

Admin: add match/player overview sub-nav; refine settlement flow and league
match management UI; improve action button enabled/disabled styles; enhance
logo upload and outright odds sync.

API: expose matchPhase/bettingOpen for closed matches; league publish guards;
settlement preview with auto score save; outright team auto-sync.

Player: watermark for closed/settled states; keep match and bet details visible;
remove default login credentials.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-10 13:00:14 +08:00
parent 6124313369
commit 03f54ca689
43 changed files with 2787 additions and 519 deletions

View File

@@ -83,6 +83,8 @@ export type AdminMatchDetail = {
isHot: boolean;
displayOrder: number;
startTime: string;
leagueId?: string;
leagueCode?: string;
leagueEn: string;
leagueZh: string;
leagueMs: string;
@@ -127,7 +129,7 @@ export function normalizeStartTimeForApi(value: string): string {
export function formFromDetail(d: AdminMatchDetail): MatchCreateForm {
return {
leagueId: '',
leagueId: d.leagueId ?? '',
leagueEn: d.leagueEn,
leagueZh: d.leagueZh,
leagueMs: d.leagueMs ?? '',
@@ -246,3 +248,47 @@ export function buildPlatformPayload(form: MatchCreateForm) {
awayTeamLogoUrl: form.awayTeamLogoUrl.trim() || undefined,
};
}
/** 编辑单场基本信息(不含联赛字段,联赛在赛事列表单独维护) */
export function buildMatchUpdatePayload(form: MatchCreateForm) {
if (!form.startTime.trim()) {
throw new FormValidationError('err.kickoff_required');
}
const homeCode = form.homeTeamCode.trim().toUpperCase();
const awayCode = form.awayTeamCode.trim().toUpperCase();
if (homeCode && awayCode) {
if (homeCode === awayCode) {
throw new FormValidationError('err.teams_same');
}
} else if (homeCode || awayCode) {
throw new FormValidationError('err.team_country_required');
} else {
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.team_country_required');
}
const homeKey = `${form.homeTeamZh.trim()}|${form.homeTeamEn.trim()}|${form.homeTeamMs.trim()}`.toLowerCase();
const awayKey = `${form.awayTeamZh.trim()}|${form.awayTeamEn.trim()}|${form.awayTeamMs.trim()}`.toLowerCase();
if (homeKey === awayKey) {
throw new FormValidationError('err.teams_same');
}
}
return {
homeTeamEn: form.homeTeamEn.trim(),
homeTeamZh: form.homeTeamZh.trim(),
homeTeamMs: form.homeTeamMs.trim() || undefined,
awayTeamEn: form.awayTeamEn.trim(),
awayTeamZh: form.awayTeamZh.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,
homeTeamLogoUrl: form.homeTeamLogoUrl.trim() || undefined,
awayTeamLogoUrl: form.awayTeamLogoUrl.trim() || undefined,
};
}