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>
295 lines
8.8 KiB
TypeScript
295 lines
8.8 KiB
TypeScript
/** 后台手动新增赛事(投注平台最小字段) */
|
|
|
|
import {
|
|
countryDisplayName,
|
|
countryLogoUrl,
|
|
hasCountryCrest,
|
|
type BuiltinCountry,
|
|
} from '../data/builtinCountries';
|
|
import { FormValidationError } from '../i18n/form-validation';
|
|
|
|
export interface MatchCreateForm {
|
|
leagueId: string;
|
|
leagueEn: string;
|
|
leagueZh: string;
|
|
leagueMs: string;
|
|
startTime: string;
|
|
homeTeamCode: string;
|
|
awayTeamCode: 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: '',
|
|
homeTeamCode: '',
|
|
awayTeamCode: '',
|
|
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;
|
|
leagueId?: string;
|
|
leagueCode?: 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;
|
|
score?: {
|
|
htHome: number;
|
|
htAway: number;
|
|
ftHome: number;
|
|
ftAway: number;
|
|
} | null;
|
|
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: d.leagueId ?? '',
|
|
leagueEn: d.leagueEn,
|
|
leagueZh: d.leagueZh,
|
|
leagueMs: d.leagueMs ?? '',
|
|
startTime: normalizeStartTimeForPicker(d.startTime),
|
|
homeTeamCode: d.homeTeamCode ?? '',
|
|
awayTeamCode: d.awayTeamCode ?? '',
|
|
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 ?? '',
|
|
};
|
|
}
|
|
|
|
export function fillBuiltinTeam(
|
|
form: MatchCreateForm,
|
|
side: 'home' | 'away',
|
|
country: BuiltinCountry,
|
|
) {
|
|
const msName = countryDisplayName(country, 'ms-MY');
|
|
const logo = countryLogoUrl(country, hasCountryCrest(country) ? 'crest' : 'flag');
|
|
if (side === 'home') {
|
|
form.homeTeamCode = country.code;
|
|
form.homeTeamZh = country.nameZh;
|
|
form.homeTeamEn = country.nameEn;
|
|
form.homeTeamMs = msName;
|
|
form.homeTeamLogoUrl = logo;
|
|
} else {
|
|
form.awayTeamCode = country.code;
|
|
form.awayTeamZh = country.nameZh;
|
|
form.awayTeamEn = country.nameEn;
|
|
form.awayTeamMs = msName;
|
|
form.awayTeamLogoUrl = logo;
|
|
}
|
|
}
|
|
|
|
export function clearBuiltinTeam(form: MatchCreateForm, side: 'home' | 'away') {
|
|
if (side === 'home') {
|
|
form.homeTeamCode = '';
|
|
form.homeTeamZh = '';
|
|
form.homeTeamEn = '';
|
|
form.homeTeamMs = '';
|
|
form.homeTeamLogoUrl = '';
|
|
} else {
|
|
form.awayTeamCode = '';
|
|
form.awayTeamZh = '';
|
|
form.awayTeamEn = '';
|
|
form.awayTeamMs = '';
|
|
form.awayTeamLogoUrl = '';
|
|
}
|
|
}
|
|
|
|
export function buildPlatformPayload(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');
|
|
}
|
|
}
|
|
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,
|
|
homeTeamCode: homeCode || undefined,
|
|
awayTeamCode: awayCode || undefined,
|
|
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,
|
|
leagueLogoUrl: form.leagueLogoUrl.trim() || undefined,
|
|
homeTeamLogoUrl: form.homeTeamLogoUrl.trim() || undefined,
|
|
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,
|
|
};
|
|
}
|