feat(player): 充值订单详情与审核记录,优化桌面端交互与记录列表样式

新增充值详情页及单条订单 API,修复桌面充值提交路径;充值/注单记录编号改为中性色,并包含桌面首页与投注相关 UI 改进。
This commit is contained in:
2026-07-02 10:47:46 +08:00
parent 49eb3cb7e0
commit 66ac69c7a7
31 changed files with 2251 additions and 905 deletions

View File

@@ -68,7 +68,12 @@ onUnmounted(() => {
</script>
<template>
<div ref="root" class="cash-chip-wrap">
<div
ref="root"
class="cash-chip-wrap"
@mouseenter="open = true; refreshProfile()"
@mouseleave="open = false"
>
<button type="button" class="cash-chip" @click="toggle">
<span class="chip-body">
<span class="chip-label">{{ t('wallet.cash_balance') }}</span>
@@ -99,8 +104,6 @@ onUnmounted(() => {
{{ t('recharge.title') }}
</button>
</div>
<div v-if="open" class="backdrop" @click="close" />
</div>
</template>

View File

@@ -19,7 +19,32 @@ const { isDesktop } = useViewport();
const auth = useAuthStore();
const { inboxEnabled, hubTitleKey } = useInboxFeature();
const { unreadCount, messages, listLoaded, refreshUnreadCount, markAllRead, deleteAllMessages } = usePlayerMessages();
const { isOpen, toggle, close } = useFloatingMailbox();
const { isOpen, toggle, open, close } = useFloatingMailbox();
const hovering = ref(false);
let hoverTimer: number | undefined = undefined;
function onMouseEnter() {
if (hoverTimer !== undefined) {
clearTimeout(hoverTimer);
hoverTimer = undefined;
}
hovering.value = true;
}
function onMouseLeave() {
hoverTimer = window.setTimeout(() => {
hovering.value = false;
hoverTimer = undefined;
}, 150);
}
function openTab(tab: HubTab) {
activeTab.value = tab;
selectedMessageId.value = null;
open();
hovering.value = false;
}
const markingAll = ref(false);
const deletingAll = ref(false);
@@ -91,7 +116,42 @@ onMounted(() => {
</script>
<template>
<div class="floating-mailbox" :class="{ 'is-betting-desktop': isBettingDesktop }">
<div
class="floating-mailbox"
:class="{ 'is-betting-desktop': isBettingDesktop }"
@mouseenter="onMouseEnter"
@mouseleave="onMouseLeave"
>
<!-- Hover Menu -->
<Transition name="fade-slide-mini">
<div v-if="isDesktop && hovering && !isOpen" class="hover-menu">
<button
v-if="inboxEnabled"
type="button"
class="menu-item"
@click="openTab('messages')"
>
<svg class="menu-icon" viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/>
</svg>
<span>{{ t('messages.tab_inbox') }}</span>
<span v-if="unreadCount > 0" class="menu-badge">
{{ unreadCount > 99 ? '99+' : unreadCount }}
</span>
</button>
<button
type="button"
class="menu-item"
@click="openTab('support')"
>
<svg class="menu-icon" viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 9h12v2H6V9zm8 5H6v-2h8v2zm4-6H6V6h12v2z"/>
</svg>
<span>{{ t('messages.tab_support') }}</span>
</button>
</div>
</Transition>
<!-- Floating Button -->
<button
type="button"
@@ -121,25 +181,9 @@ onMounted(() => {
<Transition name="fade-slide">
<div v-if="isOpen" class="popup-panel">
<div class="popup-header">
<div class="tabs">
<button
v-if="inboxEnabled"
type="button"
class="tab-btn"
:class="{ active: activeTab === 'messages' }"
@click="switchTab('messages')"
>
{{ t('messages.tab_inbox') }}
<span v-if="unreadInList > 0" class="tab-badge">{{ unreadInList }}</span>
</button>
<button
type="button"
class="tab-btn"
:class="{ active: activeTab === 'support' }"
@click="switchTab('support')"
>
{{ t('messages.tab_support') }}
</button>
<div class="popup-title">
<span>{{ activeTab === 'messages' ? t('messages.tab_inbox') : t('messages.tab_support') }}</span>
<span v-if="activeTab === 'messages' && unreadInList > 0" class="tab-badge">{{ unreadInList }}</span>
</div>
<div class="header-actions">
@@ -168,6 +212,100 @@ onMounted(() => {
</template>
<style scoped>
.hover-menu {
position: absolute;
bottom: 64px;
right: 0;
background: rgba(22, 22, 22, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(212, 175, 55, 0.25);
border-radius: 8px;
padding: 4px;
display: flex;
flex-direction: column;
gap: 2px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
width: 120px;
z-index: 1001;
}
.hover-menu::after,
.hover-menu::before {
content: '';
position: absolute;
width: 0;
height: 0;
border-style: solid;
pointer-events: none;
}
.hover-menu::after {
bottom: -6px;
right: 22px;
border-width: 6px 6px 0 6px;
border-color: rgba(22, 22, 22, 0.95) transparent transparent transparent;
z-index: 2;
}
.hover-menu::before {
bottom: -7px;
right: 21px;
border-width: 7px 7px 0 7px;
border-color: rgba(212, 175, 55, 0.25) transparent transparent transparent;
z-index: 1;
}
.menu-item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
background: transparent;
border: none;
color: var(--text-muted);
font-size: 12px;
font-weight: 600;
border-radius: 6px;
cursor: pointer;
width: 100%;
text-align: left;
transition: all 0.2s;
}
.menu-item:hover {
color: var(--primary-light);
background: rgba(212, 175, 55, 0.08);
}
.menu-icon {
color: var(--primary-light);
flex-shrink: 0;
}
.menu-badge {
margin-left: auto;
background: var(--danger, #ff4d4f);
color: #fff;
font-size: 9px;
font-weight: 700;
padding: 1px 5px;
border-radius: 8px;
line-height: 1;
}
.fade-slide-mini-enter-active,
.fade-slide-mini-leave-active {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
transform-origin: bottom right;
}
.fade-slide-mini-enter-from,
.fade-slide-mini-leave-to {
opacity: 0;
transform: scale(0.95) translateY(10px);
}
.floating-mailbox {
position: fixed;
bottom: 32px;
@@ -249,45 +387,23 @@ onMounted(() => {
height: 48px;
}
.tabs {
display: flex;
height: 100%;
}
.tab-btn {
background: transparent;
border: none;
border-bottom: 2px solid transparent;
color: var(--text-muted);
.popup-title {
color: var(--primary-light);
font-size: 14px;
font-weight: 600;
padding: 0 16px;
font-weight: 700;
padding: 0 8px;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.2s;
}
.tab-btn:hover {
color: #fff;
}
.tab-btn.active {
color: var(--primary-light);
border-bottom-color: var(--primary);
}
.tab-badge {
background: rgba(255, 255, 255, 0.1);
color: #fff;
.popup-title .tab-badge {
background: var(--primary);
color: #141414;
font-size: 11px;
padding: 2px 6px;
border-radius: 10px;
}
.tab-btn.active .tab-badge {
background: var(--primary);
color: #141414;
font-weight: 700;
}
.header-actions {

View File

@@ -45,7 +45,13 @@ onUnmounted(() => {
</script>
<template>
<div ref="root" class="locale-switch" :class="{ compact, open }">
<div
ref="root"
class="locale-switch"
:class="{ compact, open }"
@mouseenter="open = true"
@mouseleave="open = false"
>
<button type="button" class="locale-trigger" :aria-expanded="open" aria-haspopup="listbox" @click.stop="toggle">
<LocaleFlag :locale="locale" :size="compact ? 16 : 18" />
</button> <ul v-show="open" class="locale-menu" role="listbox" :aria-label="compact ? 'Language' : undefined">

View File

@@ -82,7 +82,40 @@ const liveScoreText = computed(() => {
const router = useRouter();
const slip = useBetSlipStore();
const auth = useAuthStore();
const { openAt } = useDesktopBetPopover();
const { openAt, visible: popoverVisible, pendingItem, cancelClose, scheduleClose } = useDesktopBetPopover();
function getHoveredSide() {
if (!popoverVisible.value || !pendingItem.value) return '';
if (String(pendingItem.value.matchId) !== String(props.match.id)) return '';
const selId = pendingItem.value.selectionId;
// Scan all markets to find the selection and get its code
for (const market of props.match.markets ?? []) {
const sel = market.selections?.find((s: any) => 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';
// Over/Under: over = home side, under = away side (for visual cue)
if (code === 'OVER') return 'home';
if (code === 'UNDER') return 'away';
// Odd/Even: highlight both
if (code === 'ODD' || code === 'EVEN') return 'draw';
// Fallback: use selection index
const selIndex = market.selections?.findIndex((s: any) => 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 '';
}
const activeMarketType = ref<string>('');
@@ -173,7 +206,7 @@ function goMatchDetail() {
<span v-else-if="phase === 'settled'" class="status-tag status-tag--settled">{{ phaseLabel }}</span>
<span v-else class="status-tag status-tag--pending">{{ phaseLabel }}</span>
<div class="teams-row">
<div class="teams-row" :class="getHoveredSide()">
<div class="team">
<span class="team-name">{{ match.homeTeamName }}</span>
<img
@@ -217,7 +250,7 @@ function goMatchDetail() {
</button>
<!-- Morphing Quick Bet panel (Desktop Only, disabled when noQuickBet=true) -->
<div v-if="!noQuickBet" class="card-quick-bet" @click.stop>
<div v-if="!noQuickBet" class="card-quick-bet" @click.stop @mouseenter="cancelClose" @mouseleave="scheduleClose()">
<div class="quick-bet-tabs" v-if="getAvailableMarketsForMatch(match).length">
<button
v-for="m in getAvailableMarketsForMatch(match)"
@@ -379,7 +412,7 @@ function goMatchDetail() {
letter-spacing: 0.08em;
line-height: 1;
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.9);
transition: font-size 0.25s ease;
transition: font-size 0.25s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), text-shadow 0.3s ease;
}
.status-tag {
@@ -593,4 +626,35 @@ function goMatchDetail() {
font-size: 10px;
color: var(--text-muted);
}
/* --- VS Selection Highlight: scale selected team, no VS animation --- */
.teams-row .team {
transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, filter 0.3s ease;
}
.teams-row.home .team:first-child {
transform: scale(1.08);
filter: brightness(1.3) saturate(1.2);
z-index: 2;
}
.teams-row.home .team:last-child {
opacity: 0.35 !important;
transform: scale(0.94);
filter: grayscale(0.6) brightness(0.7);
}
.teams-row.away .team:last-child {
transform: scale(1.08);
filter: brightness(1.3) saturate(1.2);
z-index: 2;
}
.teams-row.away .team:first-child {
opacity: 0.35 !important;
transform: scale(0.94);
filter: grayscale(0.6) brightness(0.7);
}
.teams-row.draw .team {
transform: scale(1.05);
filter: brightness(1.15) saturate(1.1);
}
</style>

View File

@@ -58,7 +58,12 @@ function logout() {
</script>
<template>
<div ref="root" class="avatar-wrap">
<div
ref="root"
class="avatar-wrap"
@mouseenter="open = true"
@mouseleave="open = false"
>
<button type="button" class="avatar-btn" :aria-expanded="open" @click="toggle">
<img v-if="displayAvatarUrl" :src="displayAvatarUrl" alt="" class="avatar-img" />
</button>
@@ -69,8 +74,6 @@ function logout() {
<button type="button" class="menu-item" @click="goEdit">{{ t('profile.edit') }}</button>
<button type="button" class="menu-item danger" @click="logout">{{ t('auth.logout') }}</button>
</div>
<div v-if="open" class="backdrop" @click="close" />
</div>
</template>

View File

@@ -10,9 +10,10 @@ import { usePlayerProfile } from '../../composables/usePlayerProfile';
import ConfirmDialog from '../ConfirmDialog.vue';
import { buildBetPlaceConfirmMessage } from '../../utils/betPlaceConfirmMessage';
import { useAppToast } from '../../composables/useAppToast';
import BetSuccessOverlay from '../BetSuccessOverlay.vue';
const { t, locale } = useI18n();
const { visible, anchorX, anchorY, pendingItem, close } = useDesktopBetPopover();
const { visible, anchorX, anchorY, pendingItem, placement, close, cancelClose, scheduleClose } = useDesktopBetPopover();
const slip = useBetSlipStore();
const auth = useAuthStore();
const { refreshProfile } = usePlayerProfile();
@@ -25,6 +26,20 @@ const error = ref('');
const showPlaceConfirm = ref(false);
const placeConfirmMessage = ref('');
const MIN_STAKE = 5;
const showSuccess = ref(false);
const confirmingItem = ref<any>(null);
watch(showPlaceConfirm, (val) => {
if (!val) {
confirmingItem.value = null;
}
});
function onPopoverMouseLeave() {
if (showPlaceConfirm.value) return;
scheduleClose(200);
}
let outsideClickTimer = 0;
@@ -95,6 +110,7 @@ function validatePlaceNow(): boolean {
function onPlaceNowClick() {
if (!validatePlaceNow()) return;
const item = pendingItem.value!;
confirmingItem.value = item;
placeConfirmMessage.value = buildBetPlaceConfirmMessage(t, {
mode: 'single',
items: [item],
@@ -107,7 +123,7 @@ function onPlaceNowClick() {
showPlaceConfirm.value = true;
}
async function executePlaceNow(item = pendingItem.value) {
async function executePlaceNow(item = confirmingItem.value || pendingItem.value) {
if (!item) return;
loading.value = true;
error.value = '';
@@ -122,7 +138,7 @@ async function executePlaceNow(item = pendingItem.value) {
await refreshProfile();
showPlaceConfirm.value = false;
close();
showToast(t('bet.place_success'));
showSuccess.value = true;
} catch (e: unknown) {
error.value =
(e as { response?: { data?: { error?: string } } })?.response?.data?.error ||
@@ -134,7 +150,7 @@ async function executePlaceNow(item = pendingItem.value) {
}
async function confirmPlaceNow() {
const item = pendingItem.value;
const item = confirmingItem.value;
if (!item) return;
showPlaceConfirm.value = false;
await executePlaceNow(item);
@@ -184,11 +200,13 @@ function addToParlayList() {
v-if="visible && pendingItem"
ref="popRef"
class="bet-popover"
:class="`placement-${placement}`"
tabindex="-1"
:style="{ left: `${anchorX}px`, top: `${anchorY}px` }"
@click.stop
@mouseenter="cancelClose"
@mouseleave="onPopoverMouseLeave"
>
<button type="button" class="pop-close" aria-label="Close" @click="close"></button>
<div class="pop-match">{{ pendingItem.matchName }}</div>
<div class="pop-market">{{ pendingItem.marketName }}</div>
@@ -233,104 +251,146 @@ function addToParlayList() {
@confirm="confirmPlaceNow"
/>
<BetSuccessOverlay :show="showSuccess" @done="showSuccess = false" />
</template>
<style scoped>
.bet-popover {
position: fixed;
z-index: 1000;
width: 280px;
padding: 12px 28px 12px 12px;
width: 190px;
padding: 8px;
border-radius: 8px;
border: 1px solid var(--border-gold-soft);
background: rgba(18, 18, 18, 0.98);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
border: 1px solid rgba(212, 175, 55, 0.2);
background: rgba(22, 22, 22, 0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.55);
}
.pop-close {
.bet-popover::after,
.bet-popover::before {
content: '';
position: absolute;
top: 6px;
right: 6px;
background: none;
border: none;
color: var(--text-muted);
font-size: 13px;
line-height: 1;
cursor: pointer;
padding: 2px 4px;
width: 0;
height: 0;
border-style: solid;
pointer-events: none;
}
.pop-close:hover {
color: #fff;
.bet-popover.placement-bottom::after {
top: -6px;
left: 50%;
transform: translateX(-50%);
border-width: 0 6px 6px 6px;
border-color: transparent transparent rgba(22, 22, 22, 0.9) transparent;
z-index: 2;
}
.bet-popover.placement-bottom::before {
top: -7px;
left: 50%;
transform: translateX(-50%);
border-width: 0 7px 7px 7px;
border-color: transparent transparent rgba(212, 175, 55, 0.2) transparent;
z-index: 1;
}
.bet-popover.placement-top::after {
bottom: -6px;
left: 50%;
transform: translateX(-50%);
border-width: 6px 6px 0 6px;
border-color: rgba(22, 22, 22, 0.9) transparent transparent transparent;
z-index: 2;
}
.bet-popover.placement-top::before {
bottom: -7px;
left: 50%;
transform: translateX(-50%);
border-width: 7px 7px 0 7px;
border-color: rgba(212, 175, 55, 0.2) transparent transparent transparent;
z-index: 1;
}
.pop-match {
font-size: 11px;
font-size: 9px;
color: var(--text-muted);
line-height: 1.35;
margin-bottom: 4px;
line-height: 1.3;
margin-bottom: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pop-market {
font-size: 10px;
font-size: 8px;
color: var(--text-muted);
margin-bottom: 6px;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pop-pick-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
padding: 6px 8px;
margin-bottom: 10px;
border-radius: 4px;
background: rgba(212, 175, 55, 0.06);
border: 1px solid var(--border-gold-soft);
gap: 6px;
padding: 4px 6px;
margin-bottom: 6px;
border-radius: 3px;
background: rgba(212, 175, 55, 0.04);
border: 1px solid rgba(212, 175, 55, 0.15);
}
.pick {
font-size: 12px;
font-size: 10px;
font-weight: 700;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.odds {
font-size: 12px;
font-size: 10px;
font-weight: 800;
color: var(--primary-light);
}
.stake-label {
display: block;
font-size: 10px;
font-size: 8px;
color: var(--text-muted);
margin-bottom: 4px;
margin-bottom: 2px;
}
.stake-input {
width: 100%;
box-sizing: border-box;
padding: 6px 8px;
margin-bottom: 8px;
border-radius: 4px;
border: 1px solid var(--border);
background: #0d0d0d;
padding: 4px 6px;
margin-bottom: 5px;
border-radius: 3px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: #080808;
color: #fff;
font-size: 12px;
font-size: 10px;
}
.stake-input:focus {
border-color: var(--border-gold-soft);
border-color: rgba(212, 175, 55, 0.3);
outline: none;
}
.est-row {
display: flex;
justify-content: space-between;
font-size: 10px;
font-size: 8px;
color: var(--text-muted);
margin-bottom: 10px;
margin-bottom: 6px;
}
.est-val {
@@ -339,20 +399,20 @@ function addToParlayList() {
}
.pop-error {
font-size: 10px;
font-size: 8px;
color: var(--danger);
margin: 0 0 8px;
margin: 0 0 5px;
}
.pop-actions {
display: flex;
flex-direction: column;
gap: 6px;
gap: 4px;
}
.pop-actions-row {
display: flex;
gap: 6px;
gap: 4px;
}
.pop-actions-row .btn-outline {
@@ -363,18 +423,19 @@ function addToParlayList() {
.btn-primary-gold {
width: 100%;
padding: 8px;
padding: 5px;
border: none;
border-radius: 4px;
border-radius: 3px;
background: linear-gradient(180deg, #f0d875 0%, #d4af37 100%);
color: #3d2800;
font-size: 12px;
font-size: 10px;
font-weight: 800;
cursor: pointer;
transition: opacity 0.2s;
}
.btn-primary-gold:hover:not(:disabled) {
opacity: 0.92;
opacity: 0.9;
}
.btn-primary-gold:disabled {
@@ -384,17 +445,18 @@ function addToParlayList() {
.btn-outline {
width: 100%;
padding: 7px;
border-radius: 4px;
border: 1px solid var(--border-gold-soft);
background: rgba(212, 175, 55, 0.05);
padding: 4px;
border-radius: 3px;
border: 1px solid rgba(212, 175, 55, 0.25);
background: rgba(212, 175, 55, 0.03);
color: var(--primary-light);
font-size: 11px;
font-size: 9px;
font-weight: 700;
cursor: pointer;
transition: background 0.2s;
}
.btn-outline:hover {
background: rgba(212, 175, 55, 0.12);
background: rgba(212, 175, 55, 0.08);
}
</style>

View File

@@ -2,6 +2,8 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { groupCorrectScoreSelections, type CsSelection } from '../../utils/correctScoreLayout';
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
const props = defineProps<{
marketType: string;
@@ -24,6 +26,7 @@ const emit = defineEmits<{
}>();
const { t } = useI18n();
const { scheduleClose, cancelClose } = useDesktopBetPopover();
const columns = computed(() =>
groupCorrectScoreSelections(
@@ -55,7 +58,7 @@ function formatOdds(odds: string) {
<span>{{ t('bet.col_away') }}</span>
</div>
<div class="cols-grid">
<div class="cols-grid" @mouseenter="cancelClose" @mouseleave="scheduleClose()">
<div v-for="colKey in ['home', 'draw', 'away'] as const" :key="colKey" class="col">
<button
v-for="sel in columns[colKey]"
@@ -65,6 +68,7 @@ function formatOdds(odds: string) {
:class="{ selected: isSelected(sel.id), 'score-card--locked': locked }"
:data-bet-selection-id="sel.id"
:disabled="locked"
@mouseenter="onPick(sel, $event)"
@click="onPick(sel, $event)"
>
<span class="score-line">{{ sel.scoreDisplay }}</span>

View File

@@ -2,6 +2,8 @@
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { resolveSelectionLabel } from '../../utils/selectionLabel';
import { useDesktopBetPopover } from '../../composables/useDesktopBetPopover';
const props = defineProps<{
selections: {
@@ -23,6 +25,7 @@ const props = defineProps<{
const emit = defineEmits<{ pick: [id: string, event?: MouseEvent] }>();
const { t } = useI18n();
const { scheduleClose, cancelClose } = useDesktopBetPopover();
function label(sel: (typeof props.selections)[number]) {
if (sel.selectionDisplayName?.trim()) return sel.selectionDisplayName;
@@ -53,7 +56,7 @@ const panelStyle = computed(() =>
</script>
<template>
<div class="wrap" :class="{ compact, horizontal, desktopCard, locked }">
<div class="wrap" :class="{ compact, horizontal, desktopCard, locked }" @mouseenter="cancelClose" @mouseleave="scheduleClose()">
<div
class="panel"
:class="{ horizontal, 'desktop-card': desktopCard }"
@@ -67,6 +70,7 @@ const panelStyle = computed(() =>
:class="{ selected: isSelected(sel.id), 'odds-btn--locked': locked }"
:data-bet-selection-id="sel.id"
:disabled="locked"
@mouseenter="onPick(sel.id, $event)"
@click="onPick(sel.id, $event)"
>
<span class="label">{{ label(sel) }}</span>