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

@@ -0,0 +1,22 @@
import { defaultSelectionName } from './selectionDefaults';
/** 管理后台盘口选项展示名(按 code + 当前语言) */
export function adminSelectionLabel(
t: (key: string) => string,
code: string,
opts?: { lineValue?: number | null; period?: string },
): string {
const i18nKey = `matchEditor.selection.${code}`;
const translated = t(i18nKey);
if (translated !== i18nKey) return translated;
const score = code.match(/^SCORE_(\d+)_(\d+)$/);
if (score) return `${score[1]}-${score[2]}`;
if (opts) {
const name = defaultSelectionName(code, opts);
if (name !== code) return name;
}
return code;
}

View File

@@ -0,0 +1,51 @@
/** 赛事列表 UI 状态(返回列表时恢复展开等) */
const STORAGE_KEY = 'admin_matches_list_ui';
export type MatchesListUiState = {
expandedLeagueIds: string[];
page: number;
pageSize: number;
filterStatus: string;
keyword: string;
};
function defaultState(): MatchesListUiState {
return {
expandedLeagueIds: [],
page: 1,
pageSize: 10,
filterStatus: '',
keyword: '',
};
}
export function readMatchesListUiState(): MatchesListUiState | null {
try {
const raw = sessionStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw) as MatchesListUiState;
if (!Array.isArray(parsed.expandedLeagueIds)) return null;
return parsed;
} catch {
return null;
}
}
export function writeMatchesListUiState(state: MatchesListUiState) {
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
export function patchMatchesListUiState(patch: Partial<MatchesListUiState>) {
const base = readMatchesListUiState() ?? defaultState();
writeMatchesListUiState({ ...base, ...patch });
}
/** 从子页返回前确保该赛事行处于展开记录中 */
export function ensureLeagueExpanded(leagueId: string) {
if (!leagueId) return;
const base = readMatchesListUiState() ?? defaultState();
const ids = new Set(base.expandedLeagueIds);
ids.add(leagueId);
writeMatchesListUiState({ ...base, expandedLeagueIds: [...ids] });
}

View File

@@ -0,0 +1,42 @@
/** 标准盘口选项的固定显示名(写入 DB玩家端也会按 code 做 i18n */
function handicapName(side: 'home' | 'away', line: number, half: boolean) {
const sideLabel = side === 'home' ? '主队' : '客队';
const value = side === 'home' ? line : -line;
const lineText = value > 0 ? `+${value}` : `${value}`;
return half ? `半场${sideLabel} ${lineText}` : `${sideLabel} ${lineText}`;
}
function ouName(side: 'over' | 'under', line: number, half: boolean) {
const sideLabel = side === 'over' ? '大' : '小';
return half ? `半场${sideLabel} ${line}` : `${sideLabel} ${line}`;
}
export function defaultSelectionName(
code: string,
opts: { lineValue?: number | null; period?: string },
): string {
const half = opts.period === 'HT';
const line = opts.lineValue;
if (code === 'HOME' && line != null) return handicapName('home', line, half);
if (code === 'AWAY' && line != null) return handicapName('away', line, half);
if (code === 'OVER' && line != null) return ouName('over', line, half);
if (code === 'UNDER' && line != null) return ouName('under', line, half);
const fixed: Record<string, string> = {
HOME: '主胜',
DRAW: '和',
AWAY: '客胜',
ODD: '单',
EVEN: '双',
OTHER_DRAW: '和局其它比分',
OTHER_HOME: '主胜其它比分',
OTHER_AWAY: '客胜其它比分',
};
if (fixed[code]) return fixed[code];
if (code.startsWith('SCORE_')) {
return code.replace('SCORE_', '').replace('_', '-');
}
return code;
}