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

@@ -33,10 +33,39 @@ function orderForMarket(marketType: string) {
return HT_CORRECT_SCORE_ORDER;
}
export function parseScoreCode(code: string): { display: string; column: CsColumn } | null {
if (code === 'OTHER_HOME') return { display: '其它', column: 'home' };
if (code === 'OTHER_DRAW') return { display: '其它', column: 'draw' };
if (code === 'OTHER_AWAY') return { display: '其它', column: 'away' };
const OTHER_SCORE_FALLBACK: Record<string, string> = {
OTHER_HOME: '主胜其它比分',
OTHER_DRAW: '和局其它比分',
OTHER_AWAY: '客胜其它比分',
};
export type OtherScoreCode = 'OTHER_HOME' | 'OTHER_DRAW' | 'OTHER_AWAY';
export function otherScoreDisplay(
code: OtherScoreCode,
t?: (key: string) => string,
): string {
if (t) {
const key = `bet.cs_${code.toLowerCase()}`;
const v = t(key);
if (v !== key) return v;
}
return OTHER_SCORE_FALLBACK[code];
}
export function parseScoreCode(
code: string,
t?: (key: string) => string,
): { display: string; column: CsColumn } | null {
if (code === 'OTHER_HOME') {
return { display: otherScoreDisplay('OTHER_HOME', t), column: 'home' };
}
if (code === 'OTHER_DRAW') {
return { display: otherScoreDisplay('OTHER_DRAW', t), column: 'draw' };
}
if (code === 'OTHER_AWAY') {
return { display: otherScoreDisplay('OTHER_AWAY', t), column: 'away' };
}
const m = code.match(/^SCORE_(\d+)_(\d+)$/);
if (!m) return null;
const h = Number(m[1]);
@@ -63,6 +92,7 @@ export function groupCorrectScoreSelections(
oddsVersion: string;
}>,
marketType: string,
t?: (key: string) => string,
) {
const template = orderForMarket(marketType);
const home: CsSelection[] = [];
@@ -70,7 +100,7 @@ export function groupCorrectScoreSelections(
const away: CsSelection[] = [];
for (const sel of selections) {
const parsed = parseScoreCode(sel.selectionCode);
const parsed = parseScoreCode(sel.selectionCode, t);
if (!parsed) continue;
const row: CsSelection = { ...sel, scoreDisplay: parsed.display };
if (parsed.column === 'home') home.push(row);

View File

@@ -0,0 +1,28 @@
import { parseScoreCode } from './correctScoreLayout';
const CODE_I18N: Record<string, string> = {
HOME: 'parlay_sel_home',
AWAY: 'parlay_sel_away',
DRAW: 'parlay_sel_draw',
OVER: 'parlay_sel_over',
UNDER: 'parlay_sel_under',
ODD: 'parlay_sel_odd',
EVEN: 'parlay_sel_even',
};
/** 标准选项按 code 显示固定文案,不依赖后台手填的 selectionName */
export function resolveSelectionLabel(
t: (key: string) => string,
code: string,
fallback: string,
): string {
const i18nKey = CODE_I18N[code];
if (i18nKey) {
const fullKey = `bet.${i18nKey}`;
const v = t(fullKey);
if (v !== fullKey) return v;
}
const parsed = parseScoreCode(code, t);
if (parsed) return parsed.display;
return fallback;
}

View File

@@ -159,7 +159,12 @@ const NAME_TO_ISO: Record<string, string> = {
西: 'gb',
};
export function teamFlagUrl(code?: string, name?: string): string {
export function teamFlagUrl(
code?: string,
name?: string,
logoUrl?: string | null,
): string {
if (logoUrl?.trim()) return logoUrl.trim();
const key = (code ?? '').toUpperCase();
if (key && CODE_TO_ISO[key]) {
return `https://flagcdn.com/w40/${CODE_TO_ISO[key]}.png`;