Files
thebet365/apps/player/src/views/MatchDetailView.vue
Mars 4a93fcfce0 feat(player): 全站 UI 主题重构与布局优化 — 从暗金奢华风格迁移至 Pinnacle 风格蓝白专业主题
一、全局主题重构(52 文件)
  - 设计体系从深色/金色奢华调性全面迁移至 Pinnacle 风格的清爽专业调性
  - 主色:深海蓝 #003D6B,辅色:橙色 #F8971F / #E8870A(赔率强调色)
  - 背景从纯黑/深灰改为白色/浅灰 (#F5F7FA, #E8EDF2)
  - 更新 CSS 变量设计令牌体系:--primary, --border, --text-muted, --bg-body, --bg-hover, --radius-sm 等
  - styles.css 全量重写,所有组件 scoped CSS 同步适配

二、紧凑布局优化
  - 首页:BannerCarousel 轮播高度缩减 (clamp 148→120px),MatchBetCard 内边距/间距/字号全面收紧
  - 首页:赛事卡片 padding 14px→10px,队旗尺寸 72×48→56×38,VS 区域缩小
  - 个人中心:钱包横幅宽高比 2/1→1.75/1,设置单元格 min-height 48→42px
  - 钱包页:交易行 padding/字号收紧,金额字号 15→14px
  - MainLayout:顶栏高度 50→48px,底部导航间距微调

三、对比度与可读性增强
  - 赛事详情/投注页背景从透明加深至 #E8EDF2
  - 所有卡片边框从 #E5E7EB 加深至 #CBD5E1,阴影强度提升
  - 盘口选择面板背景 #F5F7FA→#EDF0F4,增加 border-top 分隔线
  - 赔率颜色从 #F8971F 加深至 #E8870A,选中态 border-width 加粗至 2px
  - 标签字号增大并加粗 (font-weight: 600),颜色从 #6B7280 加深至 #4B5563

四、首页快捷入口 & 个人中心网格布局
  - 首页新增 4 宫格快捷入口(投注/充值/账单/活动),彩色图标区分功能
  - 个人中心重构为「4 宫格快捷操作 + 列表」结构:钱包/充值/返水/记录
  - 移除原个人中心金色入口列表项,精简设置列表为修改资料/投注规则/语言

五、顶部导航栏重设计
  - Logo 从 img 标签替换为内联 SVG 纯文字:THE(灰) + BET(深蓝) + 365(橙)
  - 客服按钮改为纯图标圆形按钮 (32×32px),增加竖线分隔符
  - Chip 高度 34→32px,间距 8→6px
  - 底部导航激活态指示条加粗至 2.5px,左右缩进从 22% 调至 18%

六、SVG Favicon
  - 新增纯路径 SVG favicon(无 <text> 元素,确保浏览器 favicon 沙箱兼容)
  - 深蓝圆角方块 + 白色 "B" 字母路径 + 橙色圆点 + 白色 "365" 数字路径
  - index.html 更新 favicon 引用为 /favicon.svg

🤖 Generated with [Qoder][https://qoder.com]
2026-06-16 12:24:41 +08:00

948 lines
23 KiB
Vue

<script setup lang="ts">
import { ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import api from '../api';
import { formatMoney } from '../utils/localeDisplay';
import { useBetSlipStore } from '../stores/betSlip';
import { useAuthStore } from '../stores/auth';
import TeamEmblem from '../components/TeamEmblem.vue';
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 { 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 { matchPhaseLabel, type MatchPhase } from '../utils/matchPhase';
import { formatLocalMatchDateTime } from '@thebet365/shared';
const route = useRoute();
const router = useRouter();
const { t, locale } = useI18n();
const slip = useBetSlipStore();
const auth = useAuthStore();
function goLogin() {
auth.showLoginPrompt(route.fullPath);
}
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[];
}
interface Selection {
id: string;
selectionCode: string;
selectionName: string;
selectionDisplayName?: string;
odds: string;
oddsVersion: string;
}
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;
status?: string;
bettingOpen?: boolean;
matchPhase?: MatchPhase;
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;
homeCards?: number | null;
awayCards?: number | null;
} | null;
markets: Market[];
}
const match = ref<MatchDetail | null>(null);
const loading = ref(true);
interface MyBet {
betNo: string;
betType: string;
stake: string;
totalOdds: string;
potentialReturn: string;
actualReturn: string;
status: string;
placedAt: string;
pickLabel: string;
matchTitle: string;
}
const myBets = ref<MyBet[]>([]);
const loadingMyBets = ref(false);
async function loadMyBets() {
if (!match.value || !auth.token) return;
loadingMyBets.value = true;
try {
const { data } = await api.get('/player/bets', {
params: { page: 1, matchId: match.value.id },
});
myBets.value = (data.data?.items ?? data.data ?? []) as MyBet[];
} catch {
myBets.value = [];
} finally {
loadingMyBets.value = false;
}
}
const statusLabel = (status: string) => {
const s = status.toUpperCase();
if (s === 'WON' || s === 'WIN') return t('history.status_won');
if (s === 'LOST' || s === 'LOSE') return t('history.status_lost');
if (s === 'PUSH' || s === 'VOID' || s === 'CANCELLED') return t('history.status_push');
return t('history.status_pending');
};
const statusClass = (status: string) => {
const s = status.toUpperCase();
if (s === 'WON' || s === 'WIN') return 'bet-status-won';
if (s === 'LOST' || s === 'LOSE') return 'bet-status-lost';
return 'bet-status-pending';
};
const marketsById = computed(() => {
const map = new Map<string, Market>();
for (const m of match.value?.markets ?? []) {
map.set(m.id, m);
}
return map;
});
const visibleMarkets = computed(() => match.value?.markets ?? []);
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(
(): MatchPhase =>
match.value?.matchPhase ?? (bettingOpen.value ? 'open' : 'closed_pending'),
);
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}`;
});
function statValue(value: number | null | undefined) {
return value == null ? '-' : String(value);
}
function statPair(home: number | null | undefined, away: number | null | undefined) {
return `${statValue(home)} - ${statValue(away)}`;
}
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) },
];
});
const showResultStats = computed(() => resultStats.value.length > 0);
function marketLabel(market: Market | null | undefined) {
return market?.marketDisplayName?.trim() || market?.marketType || '';
}
function selectionLabel(sel: Selection) {
const parsedScore = parseScoreCode(sel.selectionCode, t);
if (parsedScore) return parsedScore.display;
return sel.selectionDisplayName?.trim() || sel.selectionName;
}
async function loadMatch() {
loading.value = true;
try {
const { data } = await api.get(`/player/matches/${route.params.id}`);
match.value = data.data;
} finally {
loading.value = false;
}
loadMyBets();
}
useOnLocaleChange(loadMatch);
const { pullDistance, spinning, progress } = usePullToRefresh({
onRefresh: loadMatch,
});
const pullIndicatorStyle = () => ({
height: `${pullDistance.value}px`,
opacity: Math.min(pullDistance.value / 48, 1),
});
function isSelected(id: string) {
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}`,
marketId: market.id,
marketName: marketLabel(market),
selectionName: selectionLabel(sel),
odds: parseFloat(sel.odds),
marketType: market.marketType,
lineValue:
market.lineValue != null && market.lineValue !== ''
? parseFloat(String(market.lineValue))
: null,
allowParlay: market.allowParlay,
});
slip.openDrawer();
}
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>
<template>
<div class="detail-page">
<div
class="pull-indicator"
:style="pullIndicatorStyle()"
>
<GoldSpinner v-if="spinning" :size="28" :progress="progress" :active="spinning" />
</div>
<header class="toolbar">
<button type="button" class="icon-btn" :aria-label="t('bet.back')" @click="router.back()"></button>
<span class="toolbar-title">{{ match?.leagueName ?? '' }}</span>
<div class="toolbar-actions">
<MatchBetGuide />
<button
type="button"
class="icon-btn"
:aria-label="t('bet.refresh')"
:disabled="loading"
@click="loadMatch"
>
</button>
</div>
</header>
<div v-if="loading" class="state">
<GoldSpinner :size="36" />
</div>
<template v-else-if="match">
<section class="match-hero" :class="{ 'match-hero--phase': matchPhase !== 'open' }">
<span v-if="matchPhase === 'settled'" class="status-tag status-tag--settled">{{ phaseLabel }}</span>
<span v-else-if="matchPhase === 'closed_pending'" class="status-tag status-tag--pending">{{ phaseLabel }}</span>
<div class="hero-teams">
<!-- home -->
<div class="hero-team">
<TeamEmblem
size="lg"
:team-code="match.homeTeamCode"
:team-name="match.homeTeamName"
:logo-url="match.homeTeamLogoUrl"
/>
<span class="hero-name">{{ match.homeTeamName }}</span>
</div>
<!-- VS arena with lightning -->
<div class="vs-arena">
<svg class="hz-lightning" viewBox="0 0 72 28" aria-hidden="true">
<defs>
<linearGradient id="hzDetailGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#5eb8ff" stop-opacity="0.2" />
<stop offset="35%" stop-color="#b8ecff" stop-opacity="1" />
<stop offset="50%" stop-color="#ffffff" stop-opacity="1" />
<stop offset="65%" stop-color="#ffd080" stop-opacity="1" />
<stop offset="100%" stop-color="#ff9040" stop-opacity="0.2" />
</linearGradient>
</defs>
<path
class="hz-path hz-path-main"
stroke="url(#hzDetailGrad)"
d="M1 14 H16 L20 5 L24 23 L28 9 L32 14 H40 L44 6 L48 22 L52 12 L56 14 H71"
/>
<path
class="hz-path hz-path-sub"
stroke="url(#hzDetailGrad)"
d="M3 19 H14 L18 15 L22 19 H50 L54 16 L58 19 H69"
/>
</svg>
<span class="hz-beam" aria-hidden="true" />
<img :src="vsImg" alt="" class="vs-img" />
</div>
<!-- away -->
<div class="hero-team">
<TeamEmblem
size="lg"
:team-code="match.awayTeamCode"
:team-name="match.awayTeamName"
:logo-url="match.awayTeamLogoUrl"
/>
<span class="hero-name">{{ match.awayTeamName }}</span>
</div>
</div>
<p class="kickoff">{{ t('bet.kickoff_time') }}{{ kickoff }}</p>
<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>
<div class="my-bets-list">
<div v-for="bet in myBets" :key="bet.betNo" class="my-bet-card" @click="router.push(`/bets/${bet.betNo}`)">
<div class="bet-header">
<span class="bet-type">{{ bet.betType === 'PARLAY' ? t('history.parlay_league') : bet.pickLabel }}</span>
<span class="bet-status" :class="statusClass(bet.status)">{{ statusLabel(bet.status) }}</span>
</div>
<div class="bet-footer">
<span class="bet-stake">{{ t('history.stake') }} {{ formatMoney(bet.stake, locale) }}</span>
<span class="bet-return" :class="statusClass(bet.status)">
{{ statusClass(bet.status) === 'bet-status-won' ? '+' : '' }}{{ formatMoney(bet.actualReturn || bet.potentialReturn, locale) }}
</span>
</div>
</div>
</div>
</section>
<section class="markets-section">
<div class="market-list">
<div
v-for="market in visibleMarkets"
:key="market.id"
class="market-group"
:class="{ open: true }"
>
<MarketTypeTile
:label="marketLabel(market)"
:promo-label="market.promoLabel?.trim() || ''"
:has-market="true"
:expanded="true"
/>
<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(market.marketType)"
:market-type="market.marketType"
:selections="market.selections"
:locked="!bettingOpen || market.allowSingle === false"
:is-selected="isSelected"
@pick="onPickSelection($event, market.id)"
/>
<MarketSelectionsPanel
v-else
compact
:locked="!bettingOpen || market.allowSingle === false"
:line-value="market.lineValue"
:selections="market.selections"
:is-selected="isSelected"
@pick="onPickSelection($event, market.id)"
/>
</div>
</div>
</div>
</section>
</template>
</div>
</template>
<style scoped>
.detail-page {
margin: 0 -16px;
padding-bottom: 16px;
background: #E8EDF2;
}
.pull-indicator {
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
transition: height 0.15s ease;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 4px 12px 8px;
gap: 8px;
position: sticky;
top: -12px;
z-index: 50;
margin-top: -12px;
background: #FFFFFF;
border-bottom: 1px solid #CBD5E1;
}
.toolbar-title {
flex: 1;
text-align: center;
font-size: 13px;
font-weight: 700;
color: #003D6B;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0 4px;
}
.toolbar-actions {
display: flex;
align-items: center;
gap: 6px;
}
.icon-btn {
flex-shrink: 0;
width: 30px;
height: 30px;
border-radius: 50%;
border: 1px solid #E5E7EB;
background: #F5F7FA;
color: #003D6B;
font-size: 16px;
font-weight: 700;
line-height: 1;
padding: 0;
}
.icon-btn:disabled {
opacity: 0.45;
}
/* -- match hero -- */
.match-hero {
position: relative;
isolation: isolate;
margin: 0 12px 10px;
padding: 14px 12px 16px;
overflow: hidden;
background: #FFFFFF;
border: 1px solid #CBD5E1;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.match-hero--phase {
opacity: 0.98;
}
/* -- hero status tag (top-right corner) -- */
.status-tag {
position: absolute;
top: 0;
right: 0;
z-index: 5;
font-size: 11px;
font-weight: 700;
padding: 4px 14px;
border-radius: 0 6px 0 10px;
line-height: 1.3;
white-space: nowrap;
}
.status-tag--settled {
background: #E8EDF2;
color: #4B5563;
border-bottom: 1px solid #D1D5DB;
border-left: 1px solid #D1D5DB;
}
.status-tag--pending {
background: #FEF3C7;
color: #92400E;
border-bottom: 1px solid #FDE68A;
border-left: 1px solid #FDE68A;
}
/* -- market panel smaller status tag -- */
.market-status-tag {
position: absolute;
top: 0;
right: 0;
z-index: 5;
font-size: 10px;
font-weight: 700;
padding: 3px 10px;
border-radius: 0 4px 0 8px;
line-height: 1.3;
white-space: nowrap;
}
.market-status-tag--settled {
background: #E8EDF2;
color: #4B5563;
border-bottom: 1px solid #D1D5DB;
border-left: 1px solid #D1D5DB;
}
.market-status-tag--pending {
background: #FEF3C7;
color: #92400E;
border-bottom: 1px solid #FDE68A;
border-left: 1px solid #FDE68A;
}
.market-panel-wrap {
position: relative;
overflow: hidden;
}
.market-panel-wrap.locked {
opacity: 0.82;
}
.kickoff {
position: relative;
z-index: 1;
font-size: 11px;
color: #6B7280;
text-align: left;
margin-top: 10px;
padding-left: 2px;
}
.live-score {
position: relative;
z-index: 2;
margin: 8px 0 0;
font-size: 28px;
font-weight: 900;
color: #003D6B;
text-align: center;
}
.result-stats-section {
margin: 0 10px 12px;
padding: 12px;
border: 1px solid #CBD5E1;
border-radius: 8px;
background: #FFFFFF;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.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: 700;
color: #003D6B;
}
.result-stats-head span {
min-width: 0;
max-width: 58%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
color: #6B7280;
}
.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: #F5F7FA;
border: 1px solid #E5E7EB;
}
.result-stat-label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 11px;
color: #6B7280;
}
.result-stat-value {
flex-shrink: 0;
font-size: 14px;
line-height: 1;
font-weight: 700;
color: #1A1A2E;
}
.hero-teams {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.hero-team {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
flex: 1;
min-width: 0;
}
.hero-name {
font-size: 13px;
font-weight: 700;
color: #1A1A2E;
text-align: center;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
/* VS arena -- blue themed */
.vs-arena {
position: relative;
flex-shrink: 0;
width: 140px;
height: 126px;
display: flex;
align-items: center;
justify-content: center;
}
.hz-lightning {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
z-index: 1;
pointer-events: none;
overflow: visible;
}
.hz-path {
fill: none;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 0 4px rgba(0, 102, 204, 0.6)) drop-shadow(0 0 8px rgba(0, 91, 159, 0.35));
opacity: 0;
}
.hz-path-main {
animation: hz-strike-main 2.6s ease-in-out infinite;
}
.hz-path-sub {
stroke-width: 1.6;
animation: hz-strike-sub 2.6s ease-in-out infinite;
animation-delay: 0.12s;
}
.hz-beam {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 2px;
transform: translateY(-50%);
pointer-events: none;
z-index: 1;
background: linear-gradient(
90deg,
rgba(0, 91, 159, 0) 0%,
rgba(0, 102, 204, 0.7) 28%,
#0066CC 50%,
rgba(0, 91, 159, 0.7) 72%,
rgba(0, 61, 107, 0) 100%
);
opacity: 0;
filter: blur(0.4px);
animation: hz-beam-flash 2.6s ease-in-out infinite;
}
.vs-img {
position: relative;
z-index: 0;
width: 126px;
height: auto;
object-fit: contain;
}
@keyframes hz-strike-main {
0%, 72%, 100% { opacity: 0; }
74% { opacity: 1; }
75% { opacity: 0.25; }
76% { opacity: 0.95; }
78% { opacity: 0; }
}
@keyframes hz-strike-sub {
0%, 74%, 100% { opacity: 0; }
76% { opacity: 0.85; }
77% { opacity: 0.2; }
78% { opacity: 0.7; }
80% { opacity: 0; }
}
@keyframes hz-beam-flash {
0%, 71%, 100% { opacity: 0; transform: translateY(-50%) scaleX(0.6); }
73% { opacity: 0.85; transform: translateY(-50%) scaleX(1); }
75% { opacity: 0.15; transform: translateY(-50%) scaleX(0.95); }
76% { opacity: 0.75; transform: translateY(-50%) scaleX(1); }
78% { opacity: 0; transform: translateY(-50%) scaleX(1.05); }
}
@keyframes vs-glow {
0%, 100% {
opacity: 0.82;
filter: drop-shadow(0 0 2px rgba(0, 61, 107, 0.2));
}
50% {
opacity: 1;
filter: drop-shadow(0 0 3px rgba(0, 102, 204, 0.5)) drop-shadow(0 0 6px rgba(0, 61, 107, 0.25));
}
}
@media (prefers-reduced-motion: reduce) {
.hz-path, .hz-beam { animation: none; opacity: 0; }
}
.markets-section {
padding: 0 10px 8px;
}
.market-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 4px 0;
}
.market-group {
border-radius: 8px;
border: 1px solid #CBD5E1;
background: #FFFFFF;
overflow: hidden;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
transition: border-color 0.2s, box-shadow 0.2s;
}
.market-group.open {
border-color: #9CA3AF;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
}
.market-group :deep(.row) {
border-radius: 8px;
}
.market-group.open :deep(.row) {
border-radius: 0;
}
.market-foot-btn {
display: block;
width: calc(100% - 16px);
margin: 0 8px 10px;
padding: 9px;
border-radius: 6px;
border: 1px solid #E5E7EB;
background: #F5F7FA;
color: #003D6B;
font-size: 13px;
font-weight: 700;
}
.state {
text-align: center;
padding: 48px;
color: #6B7280;
}
.cs-toast {
text-align: center;
font-size: 12px;
color: #003D6B;
padding: 2px 0 6px;
}
/* -- my bets -- */
.my-bets-section {
padding: 0 12px;
margin-bottom: 12px;
}
.my-bets-title {
font-size: 14px;
font-weight: 700;
color: #1A1A2E;
margin: 0 0 8px;
}
.my-bets-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.my-bet-card {
background: #FFFFFF;
border: 1px solid #CBD5E1;
border-radius: 8px;
padding: 10px 12px;
cursor: pointer;
transition: background 0.15s;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
.my-bet-card:active {
background: #F5F7FA;
}
.bet-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.bet-type {
font-size: 12px;
font-weight: 700;
color: #1A1A2E;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex: 1;
margin-right: 8px;
}
.bet-status {
font-size: 11px;
font-weight: 700;
padding: 2px 8px;
border-radius: 4px;
flex-shrink: 0;
}
.bet-status-won {
background: #E6F4EA;
color: #059669;
}
.bet-status-lost {
background: #FDE8E8;
color: #DC2626;
}
.bet-status-pending {
background: #FEF3C7;
color: #D97706;
}
.bet-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.bet-stake {
font-size: 11px;
color: #6B7280;
}
.bet-return {
font-size: 13px;
font-weight: 700;
}
</style>