## 管理端 / API(Bug 修复) - matches.service:getAdminMatchDetail 返回 markets 时补充 allowSingle、allowParlay 字段 - MatchMarketsPanel:mapMarkets 从接口读取单关/串关开关,不再硬编码为 true - match-form.ts:AdminMarket 类型补充 allowSingle、allowParlay ## 玩家端 — 全局主题(styles.css / index.html / site.webmanifest) - 海军暗色 + 白色强调 token,卡片高光渐变(--gradient-card) - 下拉/弹窗实底不透明(--dropdown-bg: #0A2540) - 统一 sub-toolbar、status-ribbon、filter-tab 等全局样式 - theme-color 同步为 #001A33 ## 玩家端 — 布局与壳层 - MainLayout:详情子页去顶栏/公告,sub-toolbar 全宽铺底 - WalletBalanceCard(新):个人页钱包卡片(反水 + 未结算) - walletStats.ts(新):钱包统计逻辑抽取 ## 玩家端 — 赛事 / 投注 - MatchDetailView:顶栏 8px 顶距、卡片全宽(--detail-gutter-x: 0) - 盘口状态标签(暂停/已关闭)、去掉 MarketTypeTile 右侧箭头 - VsBadge(新):纯文字 VS,删除 vs.png - isMarketLocked:仅关单关但允许串关时仍可点击,支持串关流程 - BetSlipDrawer:仅串关盘口禁用单关提交并提示 slip_parlay_only_hint - betSlip:SlipItem 增加 allowSingle 字段 - MatchBetCard / LeagueAccordionItem:45° 待开赛角标,去掉展开左侧白边 - OutrightEventSection:去掉展开时左侧白色选中条 - FootballView:加大左右边距、筛选栏去 sticky、选中态增强 - BannerCarousel:恢复原始全宽轮播 ## 玩家端 — 钱包 / 个人 / 认证 / 其他页面 - WalletView:移除顶部钱包卡片,保留账单列表 - ProfileView:保留 WalletBalanceCard - 充值/账单/注单/登录注册等页面配色与间距统一 - 公告标签、余额面板、语言切换、区号选择等弹层改为实底 ## 国际化 - zh-CN / en-US / ms-MY:新增 slip_parlay_only_hint(仅串关盘口提示) ## 资产 - 更新 banner.svg、empty-matches.svg 配色 - 删除 vs.png Co-authored-by: Cursor <cursoragent@cursor.com>
311 lines
9.2 KiB
TypeScript
311 lines
9.2 KiB
TypeScript
/** 后台手动新增赛事(投注平台最小字段) */
|
|
|
|
import {
|
|
countryDisplayName,
|
|
countryLogoUrl,
|
|
hasCountryCrest,
|
|
type BuiltinCountry,
|
|
} from '../data/builtinCountries';
|
|
import { FormValidationError } from '../i18n/form-validation';
|
|
import {
|
|
isoToPlatformPickerDateTime,
|
|
platformPickerDateTimeToIso,
|
|
} from '@thebet365/shared';
|
|
|
|
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;
|
|
nameI18n?: Record<string, string>;
|
|
odds: number;
|
|
status: string;
|
|
sortOrder?: number;
|
|
}
|
|
|
|
export interface AdminMarket {
|
|
id: string;
|
|
marketType: string;
|
|
marketKey?: string;
|
|
lineKey?: string | null;
|
|
period: string;
|
|
lineValue: number | null;
|
|
paramsJson?: Record<string, unknown> | null;
|
|
status: string;
|
|
allowSingle?: boolean;
|
|
allowParlay?: boolean;
|
|
showOnPlayer: boolean;
|
|
promoLabel: string;
|
|
promoLabelI18n?: Record<string, string>;
|
|
nameI18n?: Record<string, string>;
|
|
sortOrder?: number;
|
|
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;
|
|
homeCorners?: number | null;
|
|
awayCorners?: number | null;
|
|
homeYellowCards?: number | null;
|
|
awayYellowCards?: number | null;
|
|
homeRedCards?: number | null;
|
|
awayRedCards?: number | null;
|
|
homeCards?: number | null;
|
|
awayCards?: number | null;
|
|
winnerTeamId?: string | null;
|
|
} | null;
|
|
markets?: AdminMarket[];
|
|
};
|
|
|
|
export function normalizeStartTimeForPicker(iso?: string): string {
|
|
return isoToPlatformPickerDateTime(iso);
|
|
}
|
|
|
|
export function normalizeStartTimeForApi(value: string): string {
|
|
return platformPickerDateTimeToIso(value);
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|