feat(player): 完善 H5 投注端与 API 演示数据
- 球赛/串关/优胜冠军、赛事详情、历史投注与个人资料编辑 - 固定顶栏、公告与底栏,仅内容区滚动 - 底部导航与站点 favicon 使用 logo,登录页精简 - API 种子、冠军盘与历史注单增强 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
86
apps/player/src/utils/correctScoreLayout.ts
Normal file
86
apps/player/src/utils/correctScoreLayout.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/** 与后台 settlement 模板顺序一致,用于列内排序 */
|
||||
export const FT_CORRECT_SCORE_ORDER = [
|
||||
'SCORE_1_0', 'SCORE_2_0', 'SCORE_2_1', 'SCORE_3_0', 'SCORE_3_1', 'SCORE_3_2',
|
||||
'SCORE_4_0', 'SCORE_4_1', 'SCORE_4_2', 'SCORE_4_3', 'OTHER_HOME',
|
||||
'SCORE_0_0', 'SCORE_1_1', 'SCORE_2_2', 'SCORE_3_3', 'SCORE_4_4', 'OTHER_DRAW',
|
||||
'SCORE_0_1', 'SCORE_0_2', 'SCORE_1_2', 'SCORE_0_3', 'SCORE_1_3', 'SCORE_2_3',
|
||||
'SCORE_0_4', 'SCORE_1_4', 'SCORE_2_4', 'SCORE_3_4', 'OTHER_AWAY',
|
||||
];
|
||||
|
||||
export const HT_CORRECT_SCORE_ORDER = [
|
||||
'SCORE_1_0', 'SCORE_2_0', 'SCORE_2_1', 'SCORE_3_0', 'OTHER_HOME',
|
||||
'SCORE_0_0', 'SCORE_1_1', 'SCORE_2_2', 'OTHER_DRAW',
|
||||
'SCORE_0_1', 'SCORE_0_2', 'SCORE_1_2', 'SCORE_0_3', 'OTHER_AWAY',
|
||||
];
|
||||
|
||||
export type CsColumn = 'home' | 'draw' | 'away';
|
||||
|
||||
export interface CsSelection {
|
||||
id: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
odds: string;
|
||||
oddsVersion: string;
|
||||
scoreDisplay: string;
|
||||
}
|
||||
|
||||
export function isCorrectScoreMarket(marketType: string) {
|
||||
return marketType.includes('CORRECT_SCORE');
|
||||
}
|
||||
|
||||
function orderForMarket(marketType: string) {
|
||||
if (marketType === 'FT_CORRECT_SCORE') return FT_CORRECT_SCORE_ORDER;
|
||||
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 m = code.match(/^SCORE_(\d+)_(\d+)$/);
|
||||
if (!m) return null;
|
||||
const h = Number(m[1]);
|
||||
const a = Number(m[2]);
|
||||
if (h > a) return { display: `${h}:${a}`, column: 'home' };
|
||||
if (h < a) return { display: `${h}:${a}`, column: 'away' };
|
||||
return { display: `${h}:${a}`, column: 'draw' };
|
||||
}
|
||||
|
||||
function sortByTemplate(items: CsSelection[], template: string[]) {
|
||||
return [...items].sort((a, b) => {
|
||||
const ia = template.indexOf(a.selectionCode);
|
||||
const ib = template.indexOf(b.selectionCode);
|
||||
return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib);
|
||||
});
|
||||
}
|
||||
|
||||
export function groupCorrectScoreSelections(
|
||||
selections: Array<{
|
||||
id: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
odds: string;
|
||||
oddsVersion: string;
|
||||
}>,
|
||||
marketType: string,
|
||||
) {
|
||||
const template = orderForMarket(marketType);
|
||||
const home: CsSelection[] = [];
|
||||
const draw: CsSelection[] = [];
|
||||
const away: CsSelection[] = [];
|
||||
|
||||
for (const sel of selections) {
|
||||
const parsed = parseScoreCode(sel.selectionCode);
|
||||
if (!parsed) continue;
|
||||
const row: CsSelection = { ...sel, scoreDisplay: parsed.display };
|
||||
if (parsed.column === 'home') home.push(row);
|
||||
else if (parsed.column === 'draw') draw.push(row);
|
||||
else away.push(row);
|
||||
}
|
||||
|
||||
return {
|
||||
home: sortByTemplate(home, template),
|
||||
draw: sortByTemplate(draw, template),
|
||||
away: sortByTemplate(away, template),
|
||||
};
|
||||
}
|
||||
2
apps/player/src/utils/localeDisplay.js
Normal file
2
apps/player/src/utils/localeDisplay.js
Normal file
@@ -0,0 +1,2 @@
|
||||
// 兼容旧缓存:转发到 TypeScript 源文件
|
||||
export { getLocaleDisplay, parseAmount, sumAmounts, formatMoney } from './localeDisplay.ts';
|
||||
55
apps/player/src/utils/localeDisplay.ts
Normal file
55
apps/player/src/utils/localeDisplay.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export interface LocaleDisplay {
|
||||
countryCode: 'cn' | 'us' | 'my';
|
||||
currency: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const LOCALE_DISPLAY: Record<string, LocaleDisplay> = {
|
||||
'zh-CN': { countryCode: 'cn', currency: 'CNY', label: '中文' },
|
||||
'en-US': { countryCode: 'us', currency: 'USD', label: 'English' },
|
||||
'ms-MY': { countryCode: 'my', currency: 'MYR', label: 'BM' },
|
||||
};
|
||||
|
||||
export function getLocaleDisplay(locale: string): LocaleDisplay {
|
||||
return LOCALE_DISPLAY[locale] ?? LOCALE_DISPLAY['en-US'];
|
||||
}
|
||||
|
||||
export function parseAmount(amount: unknown): number {
|
||||
if (amount == null) return 0;
|
||||
if (typeof amount === 'number') return Number.isFinite(amount) ? amount : 0;
|
||||
if (typeof amount === 'string') {
|
||||
const n = Number(amount);
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
if (typeof amount === 'object') {
|
||||
const value = amount as { toString?: () => string; d?: number[] };
|
||||
if (typeof value.toString === 'function') {
|
||||
const n = Number(value.toString());
|
||||
if (Number.isFinite(n)) return n;
|
||||
}
|
||||
if (Array.isArray(value.d) && value.d.length) {
|
||||
const n = Number(value.d.join(''));
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function sumAmounts(...amounts: unknown[]): number {
|
||||
return amounts.reduce<number>((sum, item) => sum + parseAmount(item), 0);
|
||||
}
|
||||
|
||||
export function formatMoney(amount: unknown, locale: string): string {
|
||||
const value = parseAmount(amount);
|
||||
const { currency } = getLocaleDisplay(locale);
|
||||
try {
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
} catch {
|
||||
return `${getLocaleDisplay(locale).currency} ${value.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
33
apps/player/src/utils/marketCatalog.ts
Normal file
33
apps/player/src/utils/marketCatalog.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/** 详情页展示顺序(与产品设计稿一致) */
|
||||
export const FEATURED_MARKET_TYPES = [
|
||||
'FT_CORRECT_SCORE',
|
||||
'HT_CORRECT_SCORE',
|
||||
'SH_CORRECT_SCORE',
|
||||
] as const;
|
||||
|
||||
export const GRID_MARKET_TYPES = [
|
||||
'FT_HANDICAP',
|
||||
'FT_OVER_UNDER',
|
||||
'FT_1X2',
|
||||
'FT_ODD_EVEN',
|
||||
'HT_HANDICAP',
|
||||
'HT_OVER_UNDER',
|
||||
'HT_1X2',
|
||||
] as const;
|
||||
|
||||
export type CatalogMarketType =
|
||||
| (typeof FEATURED_MARKET_TYPES)[number]
|
||||
| (typeof GRID_MARKET_TYPES)[number];
|
||||
|
||||
export const MARKET_I18N_KEY: Record<string, string> = {
|
||||
FT_CORRECT_SCORE: 'bet.market_cs',
|
||||
HT_CORRECT_SCORE: 'bet.market_ht_cs',
|
||||
SH_CORRECT_SCORE: 'bet.market_sh_cs',
|
||||
FT_HANDICAP: 'bet.market_ft_handicap',
|
||||
FT_OVER_UNDER: 'bet.market_ft_ou',
|
||||
FT_1X2: 'bet.market_ft_1x2',
|
||||
FT_ODD_EVEN: 'bet.market_ft_oe',
|
||||
HT_HANDICAP: 'bet.market_ht_handicap',
|
||||
HT_OVER_UNDER: 'bet.market_ht_ou',
|
||||
HT_1X2: 'bet.market_ht_1x2',
|
||||
};
|
||||
23
apps/player/src/utils/parlayColumns.ts
Normal file
23
apps/player/src/utils/parlayColumns.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/** 串关列表表头与玩法类型 */
|
||||
export const PARLAY_MARKET_TYPES = [
|
||||
{ key: 'FT_HANDICAP', label: 'FT.HDP' },
|
||||
{ key: 'FT_OVER_UNDER', label: 'FT.O/U' },
|
||||
{ key: 'FT_1X2', label: 'FT.1X2' },
|
||||
{ key: 'FT_ODD_EVEN', label: 'FT.O/E' },
|
||||
{ key: 'HT_HANDICAP', label: '1H.HDP' },
|
||||
{ key: 'HT_OVER_UNDER', label: '1H.O/U' },
|
||||
{ key: 'HT_1X2', label: '1H.1X2' },
|
||||
] as const;
|
||||
|
||||
export type ParlayMarketType = (typeof PARLAY_MARKET_TYPES)[number]['key'];
|
||||
|
||||
/** 选项简称(串关格内展示) */
|
||||
export const SELECTION_SHORT: Record<string, string> = {
|
||||
HOME: '主',
|
||||
AWAY: '客',
|
||||
DRAW: '和',
|
||||
OVER: '大',
|
||||
UNDER: '小',
|
||||
ODD: '单',
|
||||
EVEN: '双',
|
||||
};
|
||||
94
apps/player/src/utils/teamFlag.ts
Normal file
94
apps/player/src/utils/teamFlag.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/** 球队 code / 名称 → ISO 3166-1 alpha-2,用于 flagcdn 国旗图 */
|
||||
const CODE_TO_ISO: Record<string, string> = {
|
||||
MEX: 'mx',
|
||||
USA: 'us',
|
||||
CAN: 'ca',
|
||||
BRA: 'br',
|
||||
ARG: 'ar',
|
||||
ENG: 'gb',
|
||||
MUN: 'gb',
|
||||
CHE: 'gb',
|
||||
LIV: 'gb',
|
||||
MCI: 'gb',
|
||||
CZE: 'cz',
|
||||
KOR: 'kr',
|
||||
BIH: 'ba',
|
||||
PAR: 'py',
|
||||
RSA: 'za',
|
||||
SUI: 'ch',
|
||||
SCO: 'gb-sct',
|
||||
TUR: 'tr',
|
||||
CZE: 'cz',
|
||||
BIH: 'ba',
|
||||
FRA: 'fr',
|
||||
ESP: 'es',
|
||||
ENG: 'gb',
|
||||
GER: 'de',
|
||||
POR: 'pt',
|
||||
NED: 'nl',
|
||||
NOR: 'no',
|
||||
BEL: 'be',
|
||||
COL: 'co',
|
||||
JPN: 'jp',
|
||||
MAR: 'ma',
|
||||
CRO: 'hr',
|
||||
SEN: 'sn',
|
||||
};
|
||||
|
||||
const NAME_TO_ISO: Record<string, string> = {
|
||||
Mexico: 'mx',
|
||||
'South Africa': 'za',
|
||||
'United States': 'us',
|
||||
USA: 'us',
|
||||
Canada: 'ca',
|
||||
Brazil: 'br',
|
||||
Switzerland: 'ch',
|
||||
Scotland: 'gb-sct',
|
||||
Turkey: 'tr',
|
||||
'South Korea': 'kr',
|
||||
Paraguay: 'py',
|
||||
墨西哥: 'mx',
|
||||
美国: 'us',
|
||||
加拿大: 'ca',
|
||||
曼联: 'gb',
|
||||
切尔西: 'gb',
|
||||
墨西哥: 'mx',
|
||||
南非: 'za',
|
||||
捷克: 'cz',
|
||||
韩国: 'kr',
|
||||
波黑: 'ba',
|
||||
巴拉圭: 'py',
|
||||
瑞士: 'ch',
|
||||
巴西: 'br',
|
||||
苏格兰: 'gb-sct',
|
||||
土耳其: 'tr',
|
||||
法国: 'fr',
|
||||
阿根廷: 'ar',
|
||||
法国: 'fr',
|
||||
西班牙: 'es',
|
||||
英格兰: 'gb',
|
||||
德国: 'de',
|
||||
葡萄牙: 'pt',
|
||||
荷兰: 'nl',
|
||||
挪威: 'no',
|
||||
比利时: 'be',
|
||||
哥伦比亚: 'co',
|
||||
日本: 'jp',
|
||||
摩洛哥: 'ma',
|
||||
克罗地亚: 'hr',
|
||||
塞内加尔: 'sn',
|
||||
};
|
||||
|
||||
export function teamFlagUrl(code?: string, name?: string): string {
|
||||
const key = (code ?? '').toUpperCase();
|
||||
if (key && CODE_TO_ISO[key]) {
|
||||
return `https://flagcdn.com/w40/${CODE_TO_ISO[key]}.png`;
|
||||
}
|
||||
if (name && NAME_TO_ISO[name]) {
|
||||
return `https://flagcdn.com/w40/${NAME_TO_ISO[name]}.png`;
|
||||
}
|
||||
if (key.length === 2) {
|
||||
return `https://flagcdn.com/w40/${key.toLowerCase()}.png`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
Reference in New Issue
Block a user