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

@@ -27,6 +27,7 @@ interface Market {
period: string;
lineValue?: string | number | null;
allowParlay?: boolean;
promoLabel?: string | null;
selections: Selection[];
}
@@ -40,11 +41,17 @@ interface Selection {
interface MatchDetail {
id: string;
leagueName?: string;
leagueLogoUrl?: string | null;
homeTeamName: string;
awayTeamName: string;
homeTeamCode?: string;
awayTeamCode?: string;
homeTeamLogoUrl?: string | null;
awayTeamLogoUrl?: string | null;
startTime: string;
stage?: string | null;
groupName?: string | null;
markets: Market[];
}
@@ -65,12 +72,17 @@ const marketsByType = computed(() => {
});
const homeFlag = computed(() =>
teamFlagUrl(match.value?.homeTeamCode, match.value?.homeTeamName),
teamFlagUrl(match.value?.homeTeamCode, match.value?.homeTeamName, match.value?.homeTeamLogoUrl),
);
const awayFlag = computed(() =>
teamFlagUrl(match.value?.awayTeamCode, match.value?.awayTeamName),
teamFlagUrl(match.value?.awayTeamCode, match.value?.awayTeamName, match.value?.awayTeamLogoUrl),
);
function marketPromoLabel(marketType: string) {
const m = marketsByType.value.get(marketType);
return m?.promoLabel?.trim() || '';
}
const kickoff = computed(() => {
if (!match.value) return '';
return new Date(match.value.startTime).toLocaleString(locale.value, {
@@ -123,7 +135,7 @@ const csConfirmLines = computed((): CsConfirmLine[] => {
return market.selections
.filter((s) => (correctScoreStakes.value[s.id] ?? 0) > 0)
.map((s) => {
const parsed = parseScoreCode(s.selectionCode);
const parsed = parseScoreCode(s.selectionCode, t);
return {
scoreDisplay: parsed?.display ?? s.selectionName,
odds: s.odds,
@@ -262,6 +274,16 @@ function hasSlipPickForMarket(marketType: string) {
<div v-if="loading" class="state">{{ t('bet.loading') }}</div>
<template v-else-if="match">
<section v-if="match.leagueName" class="league-banner">
<span class="league-title">*{{ match.leagueName }}</span>
<img
v-if="match.leagueLogoUrl"
:src="match.leagueLogoUrl"
alt=""
class="league-logo"
/>
</section>
<section class="match-hero">
<p class="kickoff">{{ kickoff }}</p>
<div class="match-line">
@@ -298,6 +320,7 @@ function hasSlipPickForMarket(marketType: string) {
>
<MarketTypeTile
:label="marketLabel(marketType)"
:promo-label="marketPromoLabel(marketType)"
:has-market="marketsByType.has(marketType)"
:expanded="isExpanded(marketType)"
@toggle="toggleMarket(marketType)"
@@ -382,6 +405,30 @@ function hasSlipPickForMarket(marketType: string) {
padding: 2px 12px 10px;
}
.league-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 6px 12px 2px;
}
.league-banner .league-title {
flex: 1;
font-size: 12px;
font-weight: 800;
color: var(--primary-light);
line-height: 1.35;
}
.league-logo {
flex-shrink: 0;
height: 40px;
width: auto;
max-width: 44px;
object-fit: contain;
}
.kickoff {
font-size: 11px;
color: var(--text-muted);