重构
This commit is contained in:
@@ -1,19 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { ref, computed, onActivated, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useBetSlipStore } from '../stores/betSlip';
|
||||
import { usePlayerMatches } from '../composables/usePlayerMatches';
|
||||
import LeagueAccordionItem from '../components/LeagueAccordionItem.vue';
|
||||
import OutrightPanel from '../components/outright/OutrightPanel.vue';
|
||||
import ParlayPanel from '../components/parlay/ParlayPanel.vue';
|
||||
import emptyMatchesImg from '../assets/images/empty-matches.svg';
|
||||
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import type { MatchPhase } from '../utils/matchPhase';
|
||||
import {
|
||||
isAfterLocalToday as isAfterTodayMatchWindow,
|
||||
isInLocalToday as isInTodayMatchWindow,
|
||||
} from '@thebet365/shared';
|
||||
|
||||
type MainTab = 'matches' | 'outright' | 'parlay';
|
||||
type MainTab = 'matches' | 'outright';
|
||||
type TimeTab = 'today' | 'early';
|
||||
|
||||
interface Match {
|
||||
@@ -34,10 +36,18 @@ interface Match {
|
||||
bettingOpen?: boolean;
|
||||
matchPhase?: MatchPhase;
|
||||
score?: {
|
||||
htHome: number;
|
||||
htAway: number;
|
||||
ftHome: number;
|
||||
ftAway: number;
|
||||
htHome: number | null;
|
||||
htAway: number | null;
|
||||
ftHome: number | null;
|
||||
ftAway: number | null;
|
||||
homeCorners?: number | null;
|
||||
awayCorners?: number | null;
|
||||
homeYellowCards?: number | null;
|
||||
awayYellowCards?: number | null;
|
||||
homeRedCards?: number | null;
|
||||
awayRedCards?: number | null;
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
@@ -50,17 +60,18 @@ interface LeagueGroup {
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const slip = useBetSlipStore();
|
||||
|
||||
const mainTab = ref<MainTab>('matches');
|
||||
const timeTab = ref<TimeTab>('early');
|
||||
const timeTab = ref<TimeTab>('today');
|
||||
const showAll = ref(false);
|
||||
const filterNow = ref(new Date());
|
||||
const { summaryMatches, summaryLoading, loadSummary } = usePlayerMatches();
|
||||
const matches = summaryMatches;
|
||||
const loading = summaryLoading;
|
||||
const expandedLeagues = ref<Set<string>>(new Set());
|
||||
|
||||
async function loadMatches() {
|
||||
filterNow.value = new Date();
|
||||
await loadSummary(true);
|
||||
}
|
||||
|
||||
@@ -75,26 +86,14 @@ const pullIndicatorStyle = () => ({
|
||||
opacity: Math.min(pullDistance.value / 48, 1),
|
||||
});
|
||||
|
||||
function dayStart(d: Date) {
|
||||
const x = new Date(d);
|
||||
x.setHours(0, 0, 0, 0);
|
||||
return x;
|
||||
}
|
||||
|
||||
function isKickoffToday(startTime: string) {
|
||||
const kick = new Date(startTime);
|
||||
const now = new Date();
|
||||
const start = dayStart(now);
|
||||
const end = new Date(start);
|
||||
end.setDate(end.getDate() + 1);
|
||||
return kick >= start && kick < end;
|
||||
}
|
||||
|
||||
const filteredMatches = computed(() => {
|
||||
if (mainTab.value !== 'matches') return [];
|
||||
const now = filterNow.value;
|
||||
return matches.value.filter((m) => {
|
||||
const today = isKickoffToday(m.startTime);
|
||||
const timeMatch = timeTab.value === 'today' ? today : !today;
|
||||
const timeMatch =
|
||||
timeTab.value === 'today'
|
||||
? isInTodayMatchWindow(m.startTime, now)
|
||||
: isAfterTodayMatchWindow(m.startTime, now);
|
||||
if (!timeMatch) return false;
|
||||
if (!showAll.value && m.matchPhase !== 'open' && m.matchPhase !== undefined) return false;
|
||||
return true;
|
||||
@@ -157,6 +156,11 @@ function selectMainTab(tab: MainTab) {
|
||||
mainTab.value = tab;
|
||||
}
|
||||
|
||||
onActivated(() => {
|
||||
filterNow.value = new Date();
|
||||
timeTab.value = 'today';
|
||||
});
|
||||
|
||||
function goMatch(id: string) {
|
||||
router.push(`/match/${id}`);
|
||||
}
|
||||
@@ -190,16 +194,6 @@ function goMatch(id: string) {
|
||||
<span class="tab-icon">🏆</span>
|
||||
{{ t('bet.tab_outright') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="main-tab parlay-tab"
|
||||
:class="{ active: mainTab === 'parlay', 'tab-gold-active': mainTab === 'parlay' }"
|
||||
@click="selectMainTab('parlay')"
|
||||
>
|
||||
<span class="tab-icon">+</span>
|
||||
{{ t('bet.tab_parlay') }}
|
||||
<span v-if="slip.count" class="tab-badge">{{ slip.count }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-show="mainTab === 'matches'">
|
||||
@@ -226,11 +220,12 @@ function goMatch(id: string) {
|
||||
<button
|
||||
type="button"
|
||||
class="phase-toggle"
|
||||
:class="{ 'phase-toggle--active': showAll }"
|
||||
:class="{ 'phase-toggle--active': !showAll }"
|
||||
:aria-pressed="!showAll"
|
||||
@click="showAll = !showAll"
|
||||
>
|
||||
<span class="phase-toggle-dot" />
|
||||
{{ showAll ? t('bet.show_all_matches') : t('bet.show_open_only') }}
|
||||
{{ t('bet.show_open_only') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -259,7 +254,6 @@ function goMatch(id: string) {
|
||||
|
||||
<OutrightPanel v-if="mainTab === 'outright'" class="outright-tab" />
|
||||
|
||||
<ParlayPanel v-if="mainTab === 'parlay'" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -308,20 +302,6 @@ function goMatch(id: string) {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 6px;
|
||||
min-width: 16px;
|
||||
padding: 0 4px;
|
||||
border-radius: 8px;
|
||||
background: #1a1000;
|
||||
color: var(--primary-light);
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.time-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
@@ -340,7 +320,14 @@ function goMatch(id: string) {
|
||||
}
|
||||
|
||||
.time-tab.active {
|
||||
background: var(--gradient-gold) !important;
|
||||
border-color: #fff1a8 !important;
|
||||
color: #2a1a00 !important;
|
||||
font-weight: 800;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255, 244, 200, 0.28) inset,
|
||||
0 4px 16px rgba(212, 175, 55, 0.28);
|
||||
text-shadow: 0 1px 0 rgba(255, 252, 235, 0.75);
|
||||
}
|
||||
|
||||
.phase-filter {
|
||||
@@ -403,10 +390,6 @@ function goMatch(id: string) {
|
||||
padding: 80px 20px;
|
||||
}
|
||||
|
||||
.parlay-tab.tab-gold-active {
|
||||
flex: 1.15;
|
||||
}
|
||||
|
||||
.outright-tab {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { usePlayerHome } from '../composables/usePlayerHome';
|
||||
import TeamEmblem from '../components/TeamEmblem.vue';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
|
||||
const matchCardBg = `url(${cardBg})`;
|
||||
const { t, locale } = useI18n();
|
||||
@@ -29,14 +30,7 @@ function goMatch(id: string) {
|
||||
}
|
||||
|
||||
function formatKickoff(startTime: string) {
|
||||
return new Date(startTime).toLocaleString(locale.value, {
|
||||
year: 'numeric',
|
||||
month: 'numeric',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
return formatLocalMatchDateTime(startTime, locale.value, { variant: 'full' });
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
@@ -7,21 +7,17 @@ import { formatMoney } from '../utils/localeDisplay';
|
||||
import { useBetSlipStore } from '../stores/betSlip';
|
||||
import { useAuthStore } from '../stores/auth';
|
||||
import TeamEmblem from '../components/TeamEmblem.vue';
|
||||
import { DETAIL_MARKET_TYPES, MARKET_I18N_KEY } from '../utils/marketCatalog';
|
||||
import MatchBetGuide from '../components/match-detail/MatchBetGuide.vue';
|
||||
import MarketTypeTile from '../components/match-detail/MarketTypeTile.vue';
|
||||
import MarketSelectionsPanel from '../components/match-detail/MarketSelectionsPanel.vue';
|
||||
import CorrectScorePanel from '../components/match-detail/CorrectScorePanel.vue';
|
||||
import CorrectScoreConfirmModal, {
|
||||
type CsConfirmLine,
|
||||
} from '../components/match-detail/CorrectScoreConfirmModal.vue';
|
||||
import { isCorrectScoreMarket, parseScoreCode } from '../utils/correctScoreLayout';
|
||||
import { useOnLocaleChange } from '../composables/useOnLocaleChange';
|
||||
import { usePullToRefresh } from '../composables/usePullToRefresh';
|
||||
import vsImg from '../assets/images/vs.png';
|
||||
import GoldSpinner from '../components/GoldSpinner.vue';
|
||||
import BetSuccessOverlay from '../components/BetSuccessOverlay.vue';
|
||||
import { matchPhaseLabel, type MatchPhase } from '../utils/matchPhase';
|
||||
import { formatLocalMatchDateTime } from '@thebet365/shared';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
@@ -36,8 +32,11 @@ function goLogin() {
|
||||
interface Market {
|
||||
id: string;
|
||||
marketType: string;
|
||||
marketDisplayName?: string;
|
||||
lineKey?: string | null;
|
||||
period: string;
|
||||
lineValue?: string | number | null;
|
||||
allowSingle?: boolean;
|
||||
allowParlay?: boolean;
|
||||
promoLabel?: string | null;
|
||||
selections: Selection[];
|
||||
@@ -47,6 +46,7 @@ interface Selection {
|
||||
id: string;
|
||||
selectionCode: string;
|
||||
selectionName: string;
|
||||
selectionDisplayName?: string;
|
||||
odds: string;
|
||||
oddsVersion: string;
|
||||
}
|
||||
@@ -67,25 +67,25 @@ interface MatchDetail {
|
||||
status?: string;
|
||||
bettingOpen?: boolean;
|
||||
matchPhase?: MatchPhase;
|
||||
correctScoreEnabled?: boolean;
|
||||
score?: {
|
||||
htHome: number;
|
||||
htAway: number;
|
||||
ftHome: number;
|
||||
ftAway: number;
|
||||
htHome: number | null;
|
||||
htAway: number | null;
|
||||
ftHome: number | null;
|
||||
ftAway: number | null;
|
||||
homeCorners?: number | null;
|
||||
awayCorners?: number | null;
|
||||
homeYellowCards?: number | null;
|
||||
awayYellowCards?: number | null;
|
||||
homeRedCards?: number | null;
|
||||
awayRedCards?: number | null;
|
||||
homeCards?: number | null;
|
||||
awayCards?: number | null;
|
||||
} | null;
|
||||
markets: Market[];
|
||||
}
|
||||
|
||||
const match = ref<MatchDetail | null>(null);
|
||||
const loading = ref(true);
|
||||
const expandedKey = ref<string | null>(null);
|
||||
const correctScoreStakes = ref<Record<string, number>>({});
|
||||
const placingCs = ref(false);
|
||||
const csMessage = ref('');
|
||||
const showCsSuccess = ref(false);
|
||||
const csConfirmOpen = ref(false);
|
||||
const csConfirmMarketType = ref<string | null>(null);
|
||||
|
||||
interface MyBet {
|
||||
betNo: string;
|
||||
@@ -131,37 +131,19 @@ const statusClass = (status: string) => {
|
||||
if (s === 'LOST' || s === 'LOSE') return 'bet-status-lost';
|
||||
return 'bet-status-pending';
|
||||
};
|
||||
const marketsByType = computed(() => {
|
||||
const marketsById = computed(() => {
|
||||
const map = new Map<string, Market>();
|
||||
for (const m of match.value?.markets ?? []) {
|
||||
map.set(m.marketType, m);
|
||||
map.set(m.id, m);
|
||||
}
|
||||
return map;
|
||||
});
|
||||
|
||||
const CS_MARKET_TYPES = new Set(['FT_CORRECT_SCORE', 'HT_CORRECT_SCORE', 'SH_CORRECT_SCORE']);
|
||||
|
||||
const visibleMarketTypes = computed(() => {
|
||||
const csEnabled = match.value?.correctScoreEnabled ?? true;
|
||||
if (csEnabled) return DETAIL_MARKET_TYPES;
|
||||
return DETAIL_MARKET_TYPES.filter((t) => !CS_MARKET_TYPES.has(t));
|
||||
});
|
||||
|
||||
function marketPromoLabel(marketType: string) {
|
||||
const m = marketsByType.value.get(marketType);
|
||||
return m?.promoLabel?.trim() || '';
|
||||
}
|
||||
const visibleMarkets = computed(() => match.value?.markets ?? []);
|
||||
|
||||
const kickoff = computed(() => {
|
||||
if (!match.value) return '';
|
||||
return new Date(match.value.startTime).toLocaleString(locale.value, {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true,
|
||||
});
|
||||
return formatLocalMatchDateTime(match.value.startTime, locale.value, { variant: 'full' });
|
||||
});
|
||||
|
||||
const bettingOpen = computed(() => match.value?.bettingOpen !== false);
|
||||
@@ -175,119 +157,44 @@ const phaseLabel = computed(() => matchPhaseLabel(t, matchPhase.value));
|
||||
|
||||
const liveScoreText = computed(() => {
|
||||
const s = match.value?.score;
|
||||
if (!s) return '';
|
||||
if (!s || s.ftHome == null || s.ftAway == null) return '';
|
||||
return `${s.ftHome} - ${s.ftAway}`;
|
||||
});
|
||||
|
||||
function marketLabel(marketType: string) {
|
||||
const key = MARKET_I18N_KEY[marketType];
|
||||
return key ? t(key) : marketType;
|
||||
function statValue(value: number | null | undefined) {
|
||||
return value == null ? '-' : String(value);
|
||||
}
|
||||
|
||||
function expandKey(marketType: string) {
|
||||
return marketType;
|
||||
function statPair(home: number | null | undefined, away: number | null | undefined) {
|
||||
return `${statValue(home)} - ${statValue(away)}`;
|
||||
}
|
||||
|
||||
function isExpanded(marketType: string) {
|
||||
return expandedKey.value === expandKey(marketType);
|
||||
}
|
||||
|
||||
function openMarket(marketType: string) {
|
||||
if (!marketsByType.value.has(marketType)) return;
|
||||
expandedKey.value = expandKey(marketType);
|
||||
}
|
||||
|
||||
function closeMarket() {
|
||||
expandedKey.value = null;
|
||||
}
|
||||
|
||||
function toggleMarket(marketType: string) {
|
||||
if (!marketsByType.value.has(marketType)) return;
|
||||
if (isExpanded(marketType)) closeMarket();
|
||||
else openMarket(marketType);
|
||||
}
|
||||
|
||||
function genRequestId() {
|
||||
return `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
||||
}
|
||||
|
||||
const csConfirmLines = computed((): CsConfirmLine[] => {
|
||||
const marketType = csConfirmMarketType.value;
|
||||
if (!marketType) return [];
|
||||
const market = marketsByType.value.get(marketType);
|
||||
if (!market) return [];
|
||||
return market.selections
|
||||
.filter((s) => (correctScoreStakes.value[s.id] ?? 0) > 0)
|
||||
.map((s) => {
|
||||
const parsed = parseScoreCode(s.selectionCode, t);
|
||||
return {
|
||||
scoreDisplay: parsed?.display ?? s.selectionName,
|
||||
odds: s.odds,
|
||||
stake: correctScoreStakes.value[s.id],
|
||||
};
|
||||
});
|
||||
const resultStats = computed(() => {
|
||||
const s = match.value?.score;
|
||||
if (!s || matchPhase.value !== 'settled') return [];
|
||||
return [
|
||||
{ key: 'ht', label: t('bet.result_ht'), value: statPair(s.htHome, s.htAway) },
|
||||
{ key: 'ft', label: t('bet.result_ft'), value: statPair(s.ftHome, s.ftAway) },
|
||||
{ key: 'corners', label: t('bet.result_corners'), value: statPair(s.homeCorners, s.awayCorners) },
|
||||
{
|
||||
key: 'yellow',
|
||||
label: t('bet.result_yellow_cards'),
|
||||
value: statPair(s.homeYellowCards, s.awayYellowCards),
|
||||
},
|
||||
{ key: 'red', label: t('bet.result_red_cards'), value: statPair(s.homeRedCards, s.awayRedCards) },
|
||||
];
|
||||
});
|
||||
|
||||
function openCorrectScoreConfirm(marketType: string) {
|
||||
const market = marketsByType.value.get(marketType);
|
||||
if (!market || !match.value) return;
|
||||
const hasStake = market.selections.some((s) => (correctScoreStakes.value[s.id] ?? 0) > 0);
|
||||
if (!hasStake) {
|
||||
csMessage.value = t('bet.cs_stake_required');
|
||||
return;
|
||||
}
|
||||
csMessage.value = '';
|
||||
csConfirmMarketType.value = marketType;
|
||||
csConfirmOpen.value = true;
|
||||
const showResultStats = computed(() => resultStats.value.length > 0);
|
||||
|
||||
function marketLabel(market: Market | null | undefined) {
|
||||
return market?.marketDisplayName?.trim() || market?.marketType || '';
|
||||
}
|
||||
|
||||
function closeCorrectScoreConfirm() {
|
||||
csConfirmOpen.value = false;
|
||||
}
|
||||
|
||||
async function confirmCorrectScoreBets() {
|
||||
const marketType = csConfirmMarketType.value;
|
||||
if (!marketType) return;
|
||||
csConfirmOpen.value = false;
|
||||
await placeCorrectScoreBets(marketType);
|
||||
}
|
||||
|
||||
async function placeCorrectScoreBets(marketType: string) {
|
||||
if (!bettingOpen.value) return;
|
||||
if (!auth.token) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
const market = marketsByType.value.get(marketType);
|
||||
if (!market || !match.value) return;
|
||||
const entries = market.selections.filter((s) => (correctScoreStakes.value[s.id] ?? 0) > 0);
|
||||
if (!entries.length) {
|
||||
csMessage.value = t('bet.cs_stake_required');
|
||||
return;
|
||||
}
|
||||
placingCs.value = true;
|
||||
csMessage.value = '';
|
||||
try {
|
||||
for (const sel of entries) {
|
||||
await api.post('/player/bets/single', {
|
||||
selectionId: sel.id,
|
||||
oddsVersion: String(sel.oddsVersion),
|
||||
stake: correctScoreStakes.value[sel.id],
|
||||
requestId: genRequestId(),
|
||||
});
|
||||
}
|
||||
csMessage.value = t('bet.cs_place_success');
|
||||
const next = { ...correctScoreStakes.value };
|
||||
for (const sel of entries) delete next[sel.id];
|
||||
correctScoreStakes.value = next;
|
||||
showCsSuccess.value = true;
|
||||
} catch (e: unknown) {
|
||||
csMessage.value =
|
||||
(e as { response?: { data?: { error?: string } } })?.response?.data?.error ||
|
||||
t('bet.cs_place_failed');
|
||||
} finally {
|
||||
placingCs.value = false;
|
||||
}
|
||||
function selectionLabel(sel: Selection) {
|
||||
const parsedScore = parseScoreCode(sel.selectionCode, t);
|
||||
if (parsedScore) return parsedScore.display;
|
||||
return sel.selectionDisplayName?.trim() || sel.selectionName;
|
||||
}
|
||||
|
||||
async function loadMatch() {
|
||||
@@ -313,17 +220,24 @@ const pullIndicatorStyle = () => ({
|
||||
});
|
||||
|
||||
function isSelected(id: string) {
|
||||
return slip.items.some((i) => i.selectionId === id);
|
||||
return slip.singleItem?.selectionId === id || slip.parlayItems.some((i) => i.selectionId === id);
|
||||
}
|
||||
|
||||
function toggleSelection(sel: Selection, market: Market) {
|
||||
if (!match.value || !bettingOpen.value) return;
|
||||
if (market.allowSingle === false) return;
|
||||
if (!auth.token) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
slip.addItem({
|
||||
selectionId: sel.id,
|
||||
oddsVersion: String(sel.oddsVersion),
|
||||
matchId: match.value.id,
|
||||
matchName: `${match.value.homeTeamName} vs ${match.value.awayTeamName}`,
|
||||
selectionName: sel.selectionName,
|
||||
marketId: market.id,
|
||||
marketName: marketLabel(market),
|
||||
selectionName: selectionLabel(sel),
|
||||
odds: parseFloat(sel.odds),
|
||||
marketType: market.marketType,
|
||||
lineValue:
|
||||
@@ -332,29 +246,14 @@ function toggleSelection(sel: Selection, market: Market) {
|
||||
: null,
|
||||
allowParlay: market.allowParlay,
|
||||
});
|
||||
}
|
||||
|
||||
function onPickSelection(selId: string, marketType: string) {
|
||||
const market = marketsByType.value.get(marketType);
|
||||
const sel = market?.selections.find((s) => s.id === selId);
|
||||
if (!market || !sel) return;
|
||||
toggleSelection(sel, market);
|
||||
}
|
||||
|
||||
function openBetSlipDrawer() {
|
||||
if (!auth.token) {
|
||||
goLogin();
|
||||
return;
|
||||
}
|
||||
slip.openDrawer();
|
||||
}
|
||||
|
||||
/** 当前玩法是否已有选项加入投注单 */
|
||||
function hasSlipPickForMarket(marketType: string) {
|
||||
if (!match.value || slip.mode !== 'single') return false;
|
||||
return slip.items.some(
|
||||
(item) => item.matchId === match.value!.id && item.marketType === marketType,
|
||||
);
|
||||
function onPickSelection(selId: string, marketId: string) {
|
||||
const market = marketsById.value.get(marketId);
|
||||
const sel = market?.selections.find((s) => s.id === selId);
|
||||
if (!market || !sel) return;
|
||||
toggleSelection(sel, market);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -446,6 +345,19 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
<p v-if="liveScoreText" class="live-score">{{ liveScoreText }}</p>
|
||||
</section>
|
||||
|
||||
<section v-if="showResultStats" class="result-stats-section">
|
||||
<div class="result-stats-head">
|
||||
<h3>{{ t('bet.result_stats_title') }}</h3>
|
||||
<span>{{ match.homeTeamName }} / {{ match.awayTeamName }}</span>
|
||||
</div>
|
||||
<div class="result-stats-grid">
|
||||
<div v-for="item in resultStats" :key="item.key" class="result-stat-item">
|
||||
<span class="result-stat-label">{{ item.label }}</span>
|
||||
<span class="result-stat-value">{{ item.value }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 我的投注 -->
|
||||
<section v-if="myBets.length" class="my-bets-section">
|
||||
<h3 class="my-bets-title">{{ t('history.my_bets') || '我的投注' }}</h3>
|
||||
@@ -466,77 +378,45 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
</section>
|
||||
|
||||
<section class="markets-section">
|
||||
<CorrectScoreConfirmModal
|
||||
:open="csConfirmOpen"
|
||||
:market-label="csConfirmMarketType ? marketLabel(csConfirmMarketType) : ''"
|
||||
:home-team-name="match.homeTeamName"
|
||||
:away-team-name="match.awayTeamName"
|
||||
:lines="csConfirmLines"
|
||||
:loading="placingCs"
|
||||
@close="closeCorrectScoreConfirm"
|
||||
@confirm="confirmCorrectScoreBets"
|
||||
/>
|
||||
|
||||
<p v-if="csMessage" class="cs-toast">{{ csMessage }}</p>
|
||||
|
||||
<div class="market-list">
|
||||
<div
|
||||
v-for="marketType in visibleMarketTypes"
|
||||
:key="marketType"
|
||||
v-for="market in visibleMarkets"
|
||||
:key="market.id"
|
||||
class="market-group"
|
||||
:class="{ open: isExpanded(marketType) }"
|
||||
:class="{ open: true }"
|
||||
>
|
||||
<MarketTypeTile
|
||||
:label="marketLabel(marketType)"
|
||||
:promo-label="marketPromoLabel(marketType)"
|
||||
:has-market="marketsByType.has(marketType)"
|
||||
:expanded="isExpanded(marketType)"
|
||||
@toggle="toggleMarket(marketType)"
|
||||
:label="marketLabel(market)"
|
||||
:promo-label="market.promoLabel?.trim() || ''"
|
||||
:has-market="true"
|
||||
:expanded="true"
|
||||
/>
|
||||
<template v-if="isExpanded(marketType) && marketsByType.get(marketType)">
|
||||
<div class="market-panel-wrap" :class="{ locked: !bettingOpen }">
|
||||
<span v-if="!bettingOpen && matchPhase === 'settled'" class="market-status-tag market-status-tag--settled">{{ phaseLabel }}</span>
|
||||
<span v-else-if="!bettingOpen" class="market-status-tag market-status-tag--pending">{{ phaseLabel }}</span>
|
||||
<div class="market-panel-wrap" :class="{ locked: !bettingOpen || market.allowSingle === false }">
|
||||
<span v-if="!bettingOpen && matchPhase === 'settled'" class="market-status-tag market-status-tag--settled">{{ phaseLabel }}</span>
|
||||
<span v-else-if="!bettingOpen" class="market-status-tag market-status-tag--pending">{{ phaseLabel }}</span>
|
||||
<CorrectScorePanel
|
||||
v-if="isCorrectScoreMarket(marketType)"
|
||||
:market-type="marketType"
|
||||
:selections="marketsByType.get(marketType)!.selections"
|
||||
:locked="!bettingOpen"
|
||||
v-model:stakes="correctScoreStakes"
|
||||
v-if="isCorrectScoreMarket(market.marketType)"
|
||||
:market-type="market.marketType"
|
||||
:selections="market.selections"
|
||||
:locked="!bettingOpen || market.allowSingle === false"
|
||||
:is-selected="isSelected"
|
||||
@pick="onPickSelection($event, market.id)"
|
||||
/>
|
||||
<button
|
||||
v-if="isCorrectScoreMarket(marketType) && bettingOpen"
|
||||
type="button"
|
||||
class="market-foot-btn"
|
||||
@click="openCorrectScoreConfirm(marketType)"
|
||||
>
|
||||
{{ t('bet.cs_confirm_cell') }}
|
||||
</button>
|
||||
<MarketSelectionsPanel
|
||||
v-else
|
||||
compact
|
||||
:locked="!bettingOpen"
|
||||
:selections="marketsByType.get(marketType)!.selections"
|
||||
:locked="!bettingOpen || market.allowSingle === false"
|
||||
:line-value="market.lineValue"
|
||||
:selections="market.selections"
|
||||
:is-selected="isSelected"
|
||||
@pick="onPickSelection($event, marketType)"
|
||||
@pick="onPickSelection($event, market.id)"
|
||||
/>
|
||||
<button
|
||||
v-if="!isCorrectScoreMarket(marketType) && bettingOpen && hasSlipPickForMarket(marketType)"
|
||||
type="button"
|
||||
class="market-foot-btn"
|
||||
@click="openBetSlipDrawer"
|
||||
>
|
||||
{{ t('bet.cs_confirm_cell') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<BetSuccessOverlay :show="showCsSuccess" @done="showCsSuccess = false" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -704,6 +584,76 @@ function hasSlipPickForMarket(marketType: string) {
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.result-stats-section {
|
||||
margin: 0 10px 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(212, 175, 55, 0.22);
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(180deg, #1b1b1b 0%, #121212 100%);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.28);
|
||||
}
|
||||
|
||||
.result-stats-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.result-stats-head h3 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
font-weight: 900;
|
||||
color: var(--primary-light);
|
||||
}
|
||||
|
||||
.result-stats-head span {
|
||||
min-width: 0;
|
||||
max-width: 58%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.result-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.result-stat-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 38px;
|
||||
gap: 8px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.result-stat-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.result-stat-value {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
font-weight: 900;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.hero-teams {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
Reference in New Issue
Block a user