836 lines
22 KiB
Vue
836 lines
22 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
|
import { useRoute, useRouter } from 'vue-router';
|
|
import { useI18n } from 'vue-i18n';
|
|
import api from '../../api';
|
|
import { useBetSlipStore } from '../../stores/betSlip';
|
|
import TeamEmblem from '../../components/TeamEmblem.vue';
|
|
import MatchBetGuide from '../../components/match-detail/MatchBetGuide.vue';
|
|
import MarketSelectionsPanel from '../../components/match-detail/MarketSelectionsPanel.vue';
|
|
import CorrectScorePanel from '../../components/match-detail/CorrectScorePanel.vue';
|
|
import { isCorrectScoreMarket, parseScoreCode } from '../../utils/correctScoreLayout';
|
|
import { useOnLocaleChange } from '../../composables/useOnLocaleChange';
|
|
import GoldSpinner from '../../components/GoldSpinner.vue';
|
|
import { matchPhaseLabel } from '../../utils/matchPhase';
|
|
import { formatLocalMatchDateTime, defaultMarketName, DETAIL_MARKET_TYPES } from '@thebet365/shared';
|
|
import { resolveSelectionLabel } from '../../utils/selectionLabel';
|
|
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
|
|
import { useDesktopBetLocate, scrollToBetSelection } from '../../composables/useDesktopBetLocate';
|
|
|
|
const route = useRoute();
|
|
const router = useRouter();
|
|
const { t, locale } = useI18n();
|
|
const slip = useBetSlipStore();
|
|
const { openAt, visible: popoverVisible, pendingItem: popoverItem, cancelClose, scheduleClose } = useDesktopBetPopover();
|
|
const { pendingLocate, clearLocate } = useDesktopBetLocate();
|
|
|
|
function goBack() {
|
|
if (window.history.state && window.history.state.back) {
|
|
router.back();
|
|
} else {
|
|
router.push('/bet');
|
|
}
|
|
}
|
|
|
|
interface Selection {
|
|
id: string;
|
|
selectionCode: string;
|
|
selectionName: string;
|
|
selectionDisplayName?: string;
|
|
odds: string;
|
|
oddsVersion: string;
|
|
}
|
|
|
|
interface Market {
|
|
id: string;
|
|
marketType: string;
|
|
marketDisplayName?: string;
|
|
lineValue?: string | number | null;
|
|
status: string;
|
|
allowSingle?: boolean;
|
|
allowParlay?: boolean;
|
|
promoLabel?: string | null;
|
|
selections: 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;
|
|
status?: string;
|
|
bettingOpen?: boolean;
|
|
matchPhase?: string;
|
|
score?: {
|
|
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;
|
|
} | null;
|
|
markets: Market[];
|
|
}
|
|
|
|
const match = ref<MatchDetail | null>(null);
|
|
const loading = ref(true);
|
|
const selectedMarketTypes = ref<Set<string>>(new Set());
|
|
|
|
async function loadMatch() {
|
|
loading.value = true;
|
|
try {
|
|
const { data } = await api.get(`/player/matches/${route.params.id}`);
|
|
match.value = data.data;
|
|
selectedMarketTypes.value = new Set();
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
void loadMatch();
|
|
});
|
|
|
|
useOnLocaleChange(loadMatch);
|
|
|
|
const kickoff = computed(() => {
|
|
if (!match.value) return '';
|
|
return formatLocalMatchDateTime(match.value.startTime, locale.value, { variant: 'full' });
|
|
});
|
|
|
|
const bettingOpen = computed(() => match.value?.bettingOpen !== false);
|
|
const matchPhase = computed<import('../../utils/matchPhase').MatchPhase | null>(() => (match.value?.matchPhase ?? (bettingOpen.value ? 'open' : 'closed_pending')) as import('../../utils/matchPhase').MatchPhase);
|
|
const phaseLabel = computed(() => matchPhaseLabel(t, matchPhase.value));
|
|
|
|
const liveScoreText = computed(() => {
|
|
const s = match.value?.score;
|
|
if (!s || s.ftHome == null || s.ftAway == null) return '';
|
|
return `${s.ftHome} - ${s.ftAway}`;
|
|
});
|
|
|
|
const resultStats = computed(() => {
|
|
const s = match.value?.score;
|
|
if (!s || matchPhase.value !== 'settled') return [];
|
|
|
|
const statVal = (val: number | null | undefined) => val == null ? '-' : String(val);
|
|
const pair = (h: number | null | undefined, a: number | null | undefined) => `${statVal(h)} - ${statVal(a)}`;
|
|
|
|
return [
|
|
{ key: 'ht', label: t('bet.result_ht'), value: pair(s.htHome, s.htAway) },
|
|
{ key: 'ft', label: t('bet.result_ft'), value: pair(s.ftHome, s.ftAway) },
|
|
{ key: 'corners', label: t('bet.result_corners'), value: pair(s.homeCorners, s.awayCorners) },
|
|
{ key: 'yellow', label: t('bet.result_yellow_cards'), value: pair(s.homeYellowCards, s.awayYellowCards) },
|
|
{ key: 'red', label: t('bet.result_red_cards'), value: pair(s.homeRedCards, s.awayRedCards) },
|
|
];
|
|
});
|
|
|
|
const showResultStats = computed(() => resultStats.value.length > 0);
|
|
|
|
const allMarkets = computed(() => match.value?.markets ?? []);
|
|
|
|
const compactMarkets = computed(() =>
|
|
allMarkets.value.filter((m) => !isCorrectScoreMarket(m.marketType)),
|
|
);
|
|
|
|
const scoreMarkets = computed(() =>
|
|
allMarkets.value.filter((m) => isCorrectScoreMarket(m.marketType)),
|
|
);
|
|
|
|
const marketFilterOptions = computed(() => {
|
|
const labels = new Map<string, string>();
|
|
for (const market of allMarkets.value) {
|
|
if (labels.has(market.marketType)) continue;
|
|
labels.set(
|
|
market.marketType,
|
|
market.marketDisplayName?.trim() || defaultMarketName(market.marketType, locale.value),
|
|
);
|
|
}
|
|
return Array.from(labels.entries())
|
|
.map(([type, label]) => ({ type, label }))
|
|
.sort((a, b) => marketTypeSortIndex(a.type) - marketTypeSortIndex(b.type));
|
|
});
|
|
|
|
const hasMarketFilter = computed(() => selectedMarketTypes.value.size > 0);
|
|
|
|
const filteredCompactMarkets = computed(() =>
|
|
compactMarkets.value.filter((market) => matchesMarketFilter(market.marketType)),
|
|
);
|
|
|
|
const filteredScoreMarkets = computed(() =>
|
|
scoreMarkets.value.filter((market) => matchesMarketFilter(market.marketType)),
|
|
);
|
|
|
|
function marketTypeSortIndex(marketType: string) {
|
|
const idx = (DETAIL_MARKET_TYPES as readonly string[]).indexOf(marketType);
|
|
return idx >= 0 ? idx : 999;
|
|
}
|
|
|
|
function matchesMarketFilter(marketType: string) {
|
|
if (!hasMarketFilter.value) return true;
|
|
return selectedMarketTypes.value.has(marketType);
|
|
}
|
|
|
|
function isMarketFilterActive(marketType: string) {
|
|
return selectedMarketTypes.value.has(marketType);
|
|
}
|
|
|
|
function isAllMarketFilterActive() {
|
|
return !hasMarketFilter.value;
|
|
}
|
|
|
|
function selectAllMarketFilters() {
|
|
selectedMarketTypes.value = new Set();
|
|
}
|
|
|
|
function toggleMarketFilter(marketType: string) {
|
|
const next = new Set(selectedMarketTypes.value);
|
|
if (next.has(marketType)) next.delete(marketType);
|
|
else next.add(marketType);
|
|
selectedMarketTypes.value = next;
|
|
}
|
|
|
|
function ensureMarketVisibleForLocate(target: { marketId?: string; selectionId: string }) {
|
|
const market =
|
|
allMarkets.value.find((m) => m.id === target.marketId) ??
|
|
allMarkets.value.find((m) => m.selections.some((s) => s.id === target.selectionId));
|
|
if (!market) return;
|
|
if (hasMarketFilter.value && !matchesMarketFilter(market.marketType)) {
|
|
selectedMarketTypes.value = new Set();
|
|
}
|
|
}
|
|
|
|
watch(
|
|
[pendingLocate, () => match.value?.id, loading],
|
|
async ([target, matchId, isLoading]) => {
|
|
if (!target || isLoading || !matchId) return;
|
|
if (String(matchId) !== String(target.matchId)) return;
|
|
ensureMarketVisibleForLocate(target);
|
|
await nextTick();
|
|
const found = await scrollToBetSelection(target.selectionId);
|
|
if (found) clearLocate();
|
|
},
|
|
{ flush: 'post' },
|
|
);
|
|
|
|
watch(
|
|
() => route.params.id,
|
|
() => {
|
|
selectedMarketTypes.value = new Set();
|
|
},
|
|
);
|
|
|
|
function marketLabel(market: Market) {
|
|
return market.marketDisplayName?.trim() || market.marketType || '';
|
|
}
|
|
|
|
function normalizeMarketStatus(status?: string) {
|
|
return (status ?? 'OPEN').toUpperCase();
|
|
}
|
|
|
|
function isMarketOpen(market: Market) {
|
|
return normalizeMarketStatus(market.status) === 'OPEN';
|
|
}
|
|
|
|
function marketStatusLabel(status?: string) {
|
|
const normalized = normalizeMarketStatus(status);
|
|
if (normalized === 'SUSPENDED') return t('bet.market_status_suspended');
|
|
if (normalized === 'CLOSED') return t('bet.market_status_closed');
|
|
return '';
|
|
}
|
|
|
|
function isMarketLocked(market: Market) {
|
|
if (!bettingOpen.value || !isMarketOpen(market)) return true;
|
|
return market.allowSingle !== true && market.allowParlay !== true;
|
|
}
|
|
|
|
function selectionLabel(sel: Selection, market: Market) {
|
|
const parsedScore = parseScoreCode(sel.selectionCode, t);
|
|
if (parsedScore) return parsedScore.display;
|
|
return resolveSelectionLabel(t, sel.selectionCode, sel.selectionName, {
|
|
lineValue: market.lineValue,
|
|
});
|
|
}
|
|
|
|
function isSelected(id: string) {
|
|
if (slip.isInSlip(id)) return true;
|
|
return popoverVisible.value && popoverItem.value?.selectionId === id;
|
|
}
|
|
|
|
function buildSlipItem(sel: Selection, market: Market) {
|
|
if (!match.value) return null;
|
|
return {
|
|
selectionId: sel.id,
|
|
oddsVersion: String(sel.oddsVersion),
|
|
matchId: match.value.id,
|
|
matchName: `${match.value.homeTeamName} vs ${match.value.awayTeamName}`,
|
|
marketId: market.id,
|
|
marketName: marketLabel(market),
|
|
selectionName: selectionLabel(sel, market),
|
|
odds: parseFloat(sel.odds),
|
|
marketType: market.marketType,
|
|
lineValue:
|
|
market.lineValue != null && market.lineValue !== ''
|
|
? parseFloat(String(market.lineValue))
|
|
: null,
|
|
allowSingle: market.allowSingle,
|
|
allowParlay: market.allowParlay,
|
|
};
|
|
}
|
|
|
|
function toggleSelection(sel: Selection, market: Market, event?: MouseEvent) {
|
|
if (!match.value || isMarketLocked(market)) return;
|
|
const item = buildSlipItem(sel, market);
|
|
if (!item || !event) return;
|
|
openAt(event, item);
|
|
}
|
|
|
|
function getHoveredSide() {
|
|
if (!match.value || !popoverVisible.value || !popoverItem.value) return '';
|
|
if (String(popoverItem.value.matchId) !== String(match.value.id)) return '';
|
|
|
|
const selId = popoverItem.value.selectionId;
|
|
for (const market of allMarkets.value) {
|
|
const sel = market.selections?.find((s) => s.id === selId);
|
|
if (!sel) continue;
|
|
const code = (sel.selectionCode || '').toUpperCase();
|
|
if (code === 'HOME') return 'home';
|
|
if (code === 'AWAY') return 'away';
|
|
if (code === 'DRAW') return 'draw';
|
|
if (code === 'OVER') return 'home';
|
|
if (code === 'UNDER') return 'away';
|
|
if (code === 'ODD' || code === 'EVEN') return 'draw';
|
|
const selIndex = market.selections?.findIndex((s) => s.id === selId) ?? -1;
|
|
if (selIndex === -1) continue;
|
|
if (market.marketType === 'FT_1X2') {
|
|
if (selIndex === 0) return 'home';
|
|
if (selIndex === 1) return 'draw';
|
|
if (selIndex === 2) return 'away';
|
|
} else {
|
|
if (selIndex === 0) return 'home';
|
|
if (selIndex === 1) return 'away';
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="desktop-match-detail">
|
|
<!-- Fixed Header Breadcrumb -->
|
|
<div class="detail-breadcrumbs">
|
|
<button type="button" class="back-btn" @click="goBack">← {{ t('bet.back') || '返回' }}</button>
|
|
<span class="sep">/</span>
|
|
<span class="curr">{{ match?.leagueName }}</span>
|
|
</div>
|
|
|
|
<div class="detail-main">
|
|
<!-- Loading -->
|
|
<div v-if="loading" class="loading-state">
|
|
<GoldSpinner :active="true" />
|
|
</div>
|
|
|
|
<template v-else-if="match">
|
|
<!-- High Density Score Band -->
|
|
<div
|
|
class="match-score-band"
|
|
:class="[{ 'phase-settled': matchPhase === 'settled' }, getHoveredSide()]"
|
|
@mouseenter="cancelClose"
|
|
@mouseleave="scheduleClose()"
|
|
>
|
|
<div class="team-side home">
|
|
<TeamEmblem size="xl" :team-code="match.homeTeamCode" :team-name="match.homeTeamName" :logo-url="match.homeTeamLogoUrl" />
|
|
<span class="name">{{ match.homeTeamName }}</span>
|
|
</div>
|
|
|
|
<div class="score-arena">
|
|
<div class="versus-row" v-if="liveScoreText">
|
|
<span class="score">{{ match.score?.ftHome }}</span>
|
|
<span class="divider">-</span>
|
|
<span class="score">{{ match.score?.ftAway }}</span>
|
|
</div>
|
|
<div class="versus-row vs" v-else>VS</div>
|
|
<span class="kickoff-time">{{ kickoff }}</span>
|
|
</div>
|
|
|
|
<div class="team-side away">
|
|
<TeamEmblem size="xl" :team-code="match.awayTeamCode" :team-name="match.awayTeamName" :logo-url="match.awayTeamLogoUrl" />
|
|
<span class="name">{{ match.awayTeamName }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Result Stats -->
|
|
<div v-if="showResultStats" class="result-stats-card">
|
|
<h4>{{ t('bet.result_stats_title') || '赛果数据' }}</h4>
|
|
<div class="stats-row">
|
|
<div v-for="item in resultStats" :key="item.key" class="stat-pill">
|
|
<span class="lbl">{{ item.label }}</span>
|
|
<span class="val">{{ item.value }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="marketFilterOptions.length" class="market-filter-bar">
|
|
<span class="market-filter-label">{{ t('bet.filter_market') }}</span>
|
|
<div class="market-filter-chips">
|
|
<button
|
|
type="button"
|
|
class="market-filter-chip"
|
|
:class="{ active: isAllMarketFilterActive() }"
|
|
@click="selectAllMarketFilters"
|
|
>
|
|
{{ t('bet.time_all') }}
|
|
</button>
|
|
<button
|
|
v-for="option in marketFilterOptions"
|
|
:key="option.type"
|
|
type="button"
|
|
class="market-filter-chip"
|
|
:class="{ active: isMarketFilterActive(option.type) }"
|
|
@click="toggleMarketFilter(option.type)"
|
|
>
|
|
{{ option.label }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Standard markets: 2-col card grid -->
|
|
<div v-if="filteredCompactMarkets.length" class="markets-compact-grid">
|
|
<div
|
|
v-for="market in filteredCompactMarkets"
|
|
:key="market.id"
|
|
class="market-card"
|
|
:class="{ locked: isMarketLocked(market) }"
|
|
@mouseenter="cancelClose"
|
|
@mouseleave="scheduleClose()"
|
|
>
|
|
<div class="market-hdr">
|
|
<span class="market-title">{{ marketLabel(market) }}</span>
|
|
<span v-if="isMarketLocked(market)" class="locked-lbl">{{ t('bet.market_status_closed') }}</span>
|
|
</div>
|
|
<div class="market-body">
|
|
<MarketSelectionsPanel
|
|
desktop-card
|
|
compact
|
|
:locked="isMarketLocked(market)"
|
|
:line-value="market.lineValue"
|
|
:selections="market.selections"
|
|
:is-selected="isSelected"
|
|
@pick="(id, ev) => toggleSelection(market.selections.find(s => s.id === id)!, market, ev)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Correct score -->
|
|
<div v-if="filteredScoreMarkets.length" class="markets-score-section">
|
|
<h4 class="score-section-title">{{ t('bet.correct_score') }}</h4>
|
|
<div class="markets-score-grid">
|
|
<div
|
|
v-for="market in filteredScoreMarkets"
|
|
:key="market.id"
|
|
class="score-block"
|
|
:class="{ locked: isMarketLocked(market) }"
|
|
@mouseenter="cancelClose"
|
|
@mouseleave="scheduleClose()"
|
|
>
|
|
<div class="score-block-hdr">
|
|
<span>{{ marketLabel(market) }}</span>
|
|
<span v-if="isMarketLocked(market)" class="locked-lbl">{{ t('bet.market_status_closed') }}</span>
|
|
</div>
|
|
<CorrectScorePanel
|
|
dense
|
|
:market-type="market.marketType"
|
|
:selections="market.selections"
|
|
:locked="isMarketLocked(market)"
|
|
:is-selected="isSelected"
|
|
@pick="(id, ev) => toggleSelection(market.selections.find(s => s.id === id)!, market, ev)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="!filteredCompactMarkets.length && !filteredScoreMarkets.length"
|
|
class="markets-empty"
|
|
>
|
|
<p>{{ hasMarketFilter ? t('bet.no_markets_filtered') : (t('bet.no_markets') || '暂无可用盘口') }}</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.desktop-match-detail {
|
|
display: flex;
|
|
flex-direction: column;
|
|
max-width: 100%;
|
|
min-height: 100%;
|
|
}
|
|
|
|
.detail-breadcrumbs {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 30;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 16px;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
background: var(--bg-card);
|
|
border-bottom: 1px solid var(--border);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.detail-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
padding: 10px 16px 14px;
|
|
}
|
|
|
|
.detail-breadcrumbs .sep {
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.detail-breadcrumbs .curr {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
color: var(--text);
|
|
font-weight: 700;
|
|
}
|
|
|
|
.back-btn {
|
|
flex-shrink: 0;
|
|
background: transparent;
|
|
color: var(--primary-light);
|
|
font-weight: 700;
|
|
border: none;
|
|
padding: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.back-btn:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.loading-state {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 300px;
|
|
}
|
|
|
|
.match-score-band {
|
|
background: linear-gradient(180deg, var(--bg-body) 0%, var(--bg-card) 100%);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 10px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.team-side {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
flex: 1;
|
|
}
|
|
|
|
.team-side.away {
|
|
flex-direction: row-reverse;
|
|
}
|
|
|
|
.team-side .name {
|
|
font-size: 13px;
|
|
font-weight: 800;
|
|
font-family: var(--font-heading);
|
|
color: var(--text);
|
|
}
|
|
|
|
.score-arena {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
width: 240px;
|
|
}
|
|
|
|
.versus-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.versus-row .score {
|
|
font-size: 32px;
|
|
font-weight: 900;
|
|
color: var(--primary-light);
|
|
text-shadow: 0 0 10px rgba(0, 102, 204, 0.3);
|
|
}
|
|
|
|
.versus-row .divider {
|
|
font-size: 24px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.versus-row.vs {
|
|
font-size: 22px;
|
|
font-weight: 800;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.kickoff-time {
|
|
font-size: 11px;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.result-stats-card {
|
|
background: var(--bg-card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 16px;
|
|
}
|
|
|
|
.result-stats-card h4 {
|
|
margin: 0 0 12px;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
color: var(--primary-light);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.stats-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
}
|
|
|
|
.stat-pill {
|
|
background: var(--bg-body);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 6px 12px;
|
|
display: flex;
|
|
gap: 10px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.stat-pill .lbl { color: var(--text-muted); }
|
|
.stat-pill .val { color: var(--text); font-weight: 700; }
|
|
|
|
.market-filter-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 8px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
background: var(--bg-card);
|
|
min-width: 0;
|
|
}
|
|
|
|
.market-filter-label {
|
|
flex-shrink: 0;
|
|
font-size: 10px;
|
|
font-weight: 800;
|
|
color: var(--primary-light);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.market-filter-chips {
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
align-items: center;
|
|
gap: 4px;
|
|
min-width: 0;
|
|
flex: 1;
|
|
overflow-x: auto;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
.market-filter-chips::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.market-filter-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
flex-shrink: 0;
|
|
padding: 3px 7px;
|
|
border-radius: 4px;
|
|
border: 1px solid transparent;
|
|
background: rgba(0, 0, 0, 0.04);
|
|
color: var(--text-muted);
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
white-space: nowrap;
|
|
transition: background 0.18s, border-color 0.18s, color 0.18s;
|
|
}
|
|
|
|
.market-filter-chip:hover {
|
|
color: var(--primary);
|
|
background: var(--bg-hover);
|
|
}
|
|
|
|
.market-filter-chip.active {
|
|
color: var(--primary-light);
|
|
background: rgba(0, 102, 204, 0.1);
|
|
border-color: var(--border-active);
|
|
box-shadow: inset 0 0 0 1px rgba(0, 102, 204, 0.08);
|
|
}
|
|
|
|
.markets-score-section {
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.score-section-title {
|
|
margin: 0 0 8px;
|
|
font-size: 12px;
|
|
font-weight: 800;
|
|
color: var(--primary-light);
|
|
}
|
|
|
|
.markets-empty {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
min-height: 120px;
|
|
color: var(--text-muted);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.markets-compact-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
gap: 8px;
|
|
align-items: start;
|
|
}
|
|
|
|
@media (min-width: 1360px) {
|
|
.markets-compact-grid {
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
.market-card {
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
background: var(--bg-card);
|
|
align-self: start;
|
|
}
|
|
|
|
.market-card.locked {
|
|
opacity: 0.55;
|
|
}
|
|
|
|
.market-hdr {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
padding: 6px 10px;
|
|
background: var(--bg-body);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.market-title {
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
color: var(--text);
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.market-body {
|
|
padding: 6px;
|
|
}
|
|
|
|
.markets-score-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
gap: 8px;
|
|
align-items: start;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.score-block {
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
background: var(--bg-card);
|
|
align-self: start;
|
|
}
|
|
|
|
.score-block.locked {
|
|
opacity: 0.55;
|
|
}
|
|
|
|
.score-block-hdr {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
padding: 6px 10px;
|
|
background: var(--bg-body);
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: 11px;
|
|
font-weight: 800;
|
|
color: var(--text);
|
|
}
|
|
|
|
.locked-lbl {
|
|
font-size: 10px;
|
|
color: var(--danger);
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.team-side {
|
|
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease;
|
|
}
|
|
|
|
.match-score-band.home .team-side.home {
|
|
transform: scale(1.06);
|
|
filter: brightness(1.08) saturate(1.15);
|
|
}
|
|
|
|
.match-score-band.home .team-side.away {
|
|
opacity: 0.35;
|
|
transform: scale(0.94);
|
|
filter: grayscale(0.6) brightness(0.7);
|
|
}
|
|
|
|
.match-score-band.away .team-side.away {
|
|
transform: scale(1.06);
|
|
filter: brightness(1.08) saturate(1.15);
|
|
}
|
|
|
|
.match-score-band.away .team-side.home {
|
|
opacity: 0.35;
|
|
transform: scale(0.94);
|
|
filter: grayscale(0.6) brightness(0.7);
|
|
}
|
|
|
|
.match-score-band.draw .team-side {
|
|
transform: scale(1.04);
|
|
filter: brightness(1.08) saturate(1.1);
|
|
}
|
|
</style>
|