Compare commits
3 Commits
theme-3
...
ec51b675e3
| Author | SHA1 | Date | |
|---|---|---|---|
| ec51b675e3 | |||
| 9f1d263741 | |||
| cb4e48b9f4 |
@@ -81,6 +81,8 @@ export interface AdminMarket {
|
|||||||
lineValue: number | null;
|
lineValue: number | null;
|
||||||
paramsJson?: Record<string, unknown> | null;
|
paramsJson?: Record<string, unknown> | null;
|
||||||
status: string;
|
status: string;
|
||||||
|
allowSingle?: boolean;
|
||||||
|
allowParlay?: boolean;
|
||||||
showOnPlayer: boolean;
|
showOnPlayer: boolean;
|
||||||
promoLabel: string;
|
promoLabel: string;
|
||||||
promoLabelI18n?: Record<string, string>;
|
promoLabelI18n?: Record<string, string>;
|
||||||
|
|||||||
@@ -198,8 +198,8 @@ function mapMarkets(detail: AdminMatchDetail) {
|
|||||||
lineValue: m.lineValue,
|
lineValue: m.lineValue,
|
||||||
paramsJson: m.paramsJson ?? null,
|
paramsJson: m.paramsJson ?? null,
|
||||||
status: m.status,
|
status: m.status,
|
||||||
allowSingle: true,
|
allowSingle: m.allowSingle ?? true,
|
||||||
allowParlay: true,
|
allowParlay: m.allowParlay ?? true,
|
||||||
showOnPlayer: m.showOnPlayer ?? true,
|
showOnPlayer: m.showOnPlayer ?? true,
|
||||||
sortOrder: m.sortOrder ?? index,
|
sortOrder: m.sortOrder ?? index,
|
||||||
promoLabel: m.promoLabel ?? '',
|
promoLabel: m.promoLabel ?? '',
|
||||||
|
|||||||
@@ -871,6 +871,8 @@ export class MatchesService {
|
|||||||
lineValue: m.lineValue != null ? Number(m.lineValue) : null,
|
lineValue: m.lineValue != null ? Number(m.lineValue) : null,
|
||||||
paramsJson: m.paramsJson ?? null,
|
paramsJson: m.paramsJson ?? null,
|
||||||
status: m.status,
|
status: m.status,
|
||||||
|
allowSingle: m.allowSingle,
|
||||||
|
allowParlay: m.allowParlay,
|
||||||
showOnPlayer: m.showOnPlayer,
|
showOnPlayer: m.showOnPlayer,
|
||||||
promoLabel: m.promoLabel ?? '',
|
promoLabel: m.promoLabel ?? '',
|
||||||
promoLabelI18n: sanitizeLocalizedText(m.promoLabelI18n),
|
promoLabelI18n: sanitizeLocalizedText(m.promoLabelI18n),
|
||||||
|
|||||||
@@ -57,9 +57,17 @@ const canSubmitActive = computed(() => {
|
|||||||
if (activeTab.value === 'parlay') {
|
if (activeTab.value === 'parlay') {
|
||||||
return slip.parlayItems.length >= PARLAY_MIN_LEGS && slip.parlayItems.length <= PARLAY_MAX_LEGS;
|
return slip.parlayItems.length >= PARLAY_MIN_LEGS && slip.parlayItems.length <= PARLAY_MAX_LEGS;
|
||||||
}
|
}
|
||||||
return Boolean(slip.singleItem);
|
return Boolean(slip.singleItem) && slip.singleItem.allowSingle !== false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const singleParlayOnlyHint = computed(
|
||||||
|
() =>
|
||||||
|
activeTab.value === 'single' &&
|
||||||
|
Boolean(slip.singleItem) &&
|
||||||
|
slip.singleItem!.allowSingle === false &&
|
||||||
|
slip.singleItem!.allowParlay !== false,
|
||||||
|
);
|
||||||
|
|
||||||
const balanceText = computed(() => {
|
const balanceText = computed(() => {
|
||||||
if (balanceLoading.value) return t('bet.loading');
|
if (balanceLoading.value) return t('bet.loading');
|
||||||
if (balance.value == null) return '--';
|
if (balance.value == null) return '--';
|
||||||
@@ -356,6 +364,7 @@ watch(
|
|||||||
</div>
|
</div>
|
||||||
<div class="item-odds">{{ slip.singleItem.odds.toFixed(2) }}</div>
|
<div class="item-odds">{{ slip.singleItem.odds.toFixed(2) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="singleParlayOnlyHint" class="warning">{{ t('bet.slip_parlay_only_hint') }}</p>
|
||||||
|
|
||||||
<template v-if="activeTab === 'parlay'">
|
<template v-if="activeTab === 'parlay'">
|
||||||
<p v-if="parlayWarning" class="warning">{{ parlayWarning }}</p>
|
<p v-if="parlayWarning" class="warning">{{ parlayWarning }}</p>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
import MatchBetCard from './MatchBetCard.vue';
|
import MatchBetCard from './MatchBetCard.vue';
|
||||||
import saishiImg from '../assets/images/saishi.webp';
|
import saishiImg from '../assets/images/saishi.webp';
|
||||||
import cardBg from '../assets/images/card-bg.webp';
|
import cardBg from '../assets/images/card-bg.webp';
|
||||||
@@ -40,6 +41,7 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{ toggle: []; bet: [id: string] }>();
|
const emit = defineEmits<{ toggle: []; bet: [id: string] }>();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
const totalCount = computed(() => props.matches.length);
|
const totalCount = computed(() => props.matches.length);
|
||||||
const liveCount = computed(() => props.matches.filter(m => m.matchPhase === 'closed_pending').length);
|
const liveCount = computed(() => props.matches.filter(m => m.matchPhase === 'closed_pending').length);
|
||||||
@@ -55,9 +57,9 @@ const openCount = computed(() => props.matches.filter(m => m.matchPhase === 'ope
|
|||||||
<span class="league-info">
|
<span class="league-info">
|
||||||
<span class="league-title">*{{ leagueName }}</span>
|
<span class="league-title">*{{ leagueName }}</span>
|
||||||
<span class="league-stats">
|
<span class="league-stats">
|
||||||
<span class="stat-item">{{ totalCount }}场</span>
|
<span class="stat-item">{{ totalCount }} {{ t('bet.match_unit') }}</span>
|
||||||
<span v-if="liveCount > 0" class="stat-item stat-live">{{ liveCount }}进行中</span>
|
<span v-if="liveCount > 0" class="stat-item stat-live">{{ liveCount }} {{ t('bet.status_live') }}</span>
|
||||||
<span v-if="openCount > 0" class="stat-item stat-open">{{ openCount }}待开赛</span>
|
<span v-if="openCount > 0" class="stat-item stat-open">{{ openCount }} {{ t('bet.status_open') }}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ const liveScoreText = computed(() => {
|
|||||||
:class="{ 'match-card--phase': phase !== 'open' }"
|
:class="{ 'match-card--phase': phase !== 'open' }"
|
||||||
@click="emit('bet', match.id)"
|
@click="emit('bet', match.id)"
|
||||||
>
|
>
|
||||||
<span v-if="phase === 'open'" class="status-tag status-tag--open">待开赛</span>
|
<span v-if="phase === 'open'" class="status-tag status-tag--open">{{ t('bet.status_open') }}</span>
|
||||||
<span v-else-if="phase === 'settled'" class="status-tag status-tag--settled">{{ phaseLabel }}</span>
|
<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>
|
<span v-else class="status-tag status-tag--pending">{{ phaseLabel }}</span>
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useI18n } from 'vue-i18n';
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
label: string;
|
label: string;
|
||||||
promoLabel?: string;
|
promoLabel?: string;
|
||||||
|
statusLabel?: string;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
hasMarket: boolean;
|
hasMarket: boolean;
|
||||||
}>();
|
}>();
|
||||||
@@ -22,8 +23,8 @@ const { t } = useI18n();
|
|||||||
>
|
>
|
||||||
<span class="row-label">{{ label }}</span>
|
<span class="row-label">{{ label }}</span>
|
||||||
<span v-if="promoLabel" class="row-promo">{{ promoLabel }}</span>
|
<span v-if="promoLabel" class="row-promo">{{ promoLabel }}</span>
|
||||||
<span v-if="!hasMarket" class="row-muted">{{ t('bet.market_closed') }}</span>
|
<span v-if="statusLabel" class="row-status">{{ statusLabel }}</span>
|
||||||
<span v-else class="row-chevron" :class="{ open: expanded }" aria-hidden="true">▸</span>
|
<span v-else-if="!hasMarket" class="row-muted">{{ t('bet.market_closed') }}</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -86,6 +87,17 @@ const { t } = useI18n();
|
|||||||
border: 1px solid rgba(248, 151, 31, 0.3);
|
border: 1px solid rgba(248, 151, 31, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.row-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 9px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #f0b429;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(240, 180, 41, 0.12);
|
||||||
|
border: 1px solid rgba(240, 180, 41, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
.row.expanded .row-label {
|
.row.expanded .row-label {
|
||||||
color: #003D6B;
|
color: #003D6B;
|
||||||
}
|
}
|
||||||
@@ -95,16 +107,4 @@ const { t } = useI18n();
|
|||||||
color: #6B7280;
|
color: #6B7280;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-chevron {
|
|
||||||
font-size: 11px;
|
|
||||||
color: #6B7280;
|
|
||||||
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), color 0.2s;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.row-chevron.open {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
color: #003D6B;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -266,6 +266,13 @@ export default {
|
|||||||
tab_outright: 'Outright',
|
tab_outright: 'Outright',
|
||||||
tab_today: 'Today',
|
tab_today: 'Today',
|
||||||
tab_early: 'Early',
|
tab_early: 'Early',
|
||||||
|
filter_status_all: 'All',
|
||||||
|
filter_status_open: 'Open',
|
||||||
|
filter_status_settled: 'Settled',
|
||||||
|
filter_leagues_all: 'All Leagues',
|
||||||
|
status_open: 'Open',
|
||||||
|
status_live: 'Live',
|
||||||
|
match_unit: 'matches',
|
||||||
show_open_only: 'Open only',
|
show_open_only: 'Open only',
|
||||||
show_all_matches: 'Show all',
|
show_all_matches: 'Show all',
|
||||||
today: 'Today',
|
today: 'Today',
|
||||||
@@ -297,6 +304,8 @@ export default {
|
|||||||
parlay_block_quarter: 'Quarter-ball HDP/O-U cannot be parlayed',
|
parlay_block_quarter: 'Quarter-ball HDP/O-U cannot be parlayed',
|
||||||
parlay_block_not_allowed: 'This market cannot be parlayed',
|
parlay_block_not_allowed: 'This market cannot be parlayed',
|
||||||
parlay_need_more: 'Select at least 2 legs for parlay',
|
parlay_need_more: 'Select at least 2 legs for parlay',
|
||||||
|
market_status_suspended: 'Suspended',
|
||||||
|
market_status_closed: 'Closed',
|
||||||
back: 'Back',
|
back: 'Back',
|
||||||
refresh: 'Refresh',
|
refresh: 'Refresh',
|
||||||
download: 'Download',
|
download: 'Download',
|
||||||
@@ -380,6 +389,7 @@ export default {
|
|||||||
slip_tab_parlay: 'Parlay',
|
slip_tab_parlay: 'Parlay',
|
||||||
slip_parlay_empty_hint: 'Pick a selection, then tap Add to parlay',
|
slip_parlay_empty_hint: 'Pick a selection, then tap Add to parlay',
|
||||||
slip_add_parlay: 'Add to parlay',
|
slip_add_parlay: 'Add to parlay',
|
||||||
|
slip_parlay_only_hint: 'This market is parlay-only. Tap Add to parlay to bet.',
|
||||||
slip_parlay_same_match: 'Parlay selections cannot be from the same match. Remove the same-match leg first.',
|
slip_parlay_same_match: 'Parlay selections cannot be from the same match. Remove the same-match leg first.',
|
||||||
slip_parlay_count: '{n} parlay leg(s)',
|
slip_parlay_count: '{n} parlay leg(s)',
|
||||||
slip_total_stake: 'Total stake',
|
slip_total_stake: 'Total stake',
|
||||||
|
|||||||
@@ -272,6 +272,13 @@ export default {
|
|||||||
tab_outright: 'Juara',
|
tab_outright: 'Juara',
|
||||||
tab_today: 'Hari Ini',
|
tab_today: 'Hari Ini',
|
||||||
tab_early: 'Awal',
|
tab_early: 'Awal',
|
||||||
|
filter_status_all: 'Semua',
|
||||||
|
filter_status_open: 'Buka',
|
||||||
|
filter_status_settled: 'Selesai',
|
||||||
|
filter_leagues_all: 'Semua Liga',
|
||||||
|
status_open: 'Buka',
|
||||||
|
status_live: 'Sedang berlangsung',
|
||||||
|
match_unit: 'perlawanan',
|
||||||
show_open_only: 'Buka sahaja',
|
show_open_only: 'Buka sahaja',
|
||||||
show_all_matches: 'Tunjuk semua',
|
show_all_matches: 'Tunjuk semua',
|
||||||
today: 'Hari Ini',
|
today: 'Hari Ini',
|
||||||
@@ -303,6 +310,8 @@ export default {
|
|||||||
parlay_block_quarter: 'HDP/O-U suku bola tidak boleh parlay',
|
parlay_block_quarter: 'HDP/O-U suku bola tidak boleh parlay',
|
||||||
parlay_block_not_allowed: 'Pasaran ini tidak boleh parlay',
|
parlay_block_not_allowed: 'Pasaran ini tidak boleh parlay',
|
||||||
parlay_need_more: 'Pilih sekurang-kurangnya 2 pilihan',
|
parlay_need_more: 'Pilih sekurang-kurangnya 2 pilihan',
|
||||||
|
market_status_suspended: 'Digantung',
|
||||||
|
market_status_closed: 'Ditutup',
|
||||||
back: 'Kembali',
|
back: 'Kembali',
|
||||||
refresh: 'Muat semula',
|
refresh: 'Muat semula',
|
||||||
download: 'Muat turun',
|
download: 'Muat turun',
|
||||||
@@ -386,6 +395,7 @@ export default {
|
|||||||
slip_tab_parlay: 'Parlay',
|
slip_tab_parlay: 'Parlay',
|
||||||
slip_parlay_empty_hint: 'Pilih satu odds, kemudian ketik Tambah ke parlay',
|
slip_parlay_empty_hint: 'Pilih satu odds, kemudian ketik Tambah ke parlay',
|
||||||
slip_add_parlay: 'Tambah ke parlay',
|
slip_add_parlay: 'Tambah ke parlay',
|
||||||
|
slip_parlay_only_hint: 'Pasaran ini hanya parlay. Ketik Tambah ke parlay untuk bertaruh.',
|
||||||
slip_parlay_same_match: 'Pilihan parlay tidak boleh daripada perlawanan sama. Buang pilihan perlawanan sama dahulu.',
|
slip_parlay_same_match: 'Pilihan parlay tidak boleh daripada perlawanan sama. Buang pilihan perlawanan sama dahulu.',
|
||||||
slip_parlay_count: '{n} pilihan parlay',
|
slip_parlay_count: '{n} pilihan parlay',
|
||||||
slip_total_stake: 'Jumlah pertaruhan',
|
slip_total_stake: 'Jumlah pertaruhan',
|
||||||
|
|||||||
@@ -266,6 +266,13 @@ export default {
|
|||||||
tab_outright: '优胜冠军',
|
tab_outright: '优胜冠军',
|
||||||
tab_today: '今日',
|
tab_today: '今日',
|
||||||
tab_early: '早盘',
|
tab_early: '早盘',
|
||||||
|
filter_status_all: '全部',
|
||||||
|
filter_status_open: '待开赛',
|
||||||
|
filter_status_settled: '已结束',
|
||||||
|
filter_leagues_all: '全部联赛',
|
||||||
|
status_open: '待开赛',
|
||||||
|
status_live: '进行中',
|
||||||
|
match_unit: '场',
|
||||||
show_open_only: '仅显示待开赛',
|
show_open_only: '仅显示待开赛',
|
||||||
show_all_matches: '显示全部',
|
show_all_matches: '显示全部',
|
||||||
today: '今日',
|
today: '今日',
|
||||||
@@ -297,6 +304,8 @@ export default {
|
|||||||
parlay_block_quarter: '四分盘让球/大小不可串关',
|
parlay_block_quarter: '四分盘让球/大小不可串关',
|
||||||
parlay_block_not_allowed: '该玩法不可串关',
|
parlay_block_not_allowed: '该玩法不可串关',
|
||||||
parlay_need_more: '请至少选择 2 项进行串关',
|
parlay_need_more: '请至少选择 2 项进行串关',
|
||||||
|
market_status_suspended: '暂停',
|
||||||
|
market_status_closed: '已关闭',
|
||||||
back: '返回',
|
back: '返回',
|
||||||
refresh: '刷新',
|
refresh: '刷新',
|
||||||
download: '下载',
|
download: '下载',
|
||||||
@@ -380,6 +389,7 @@ export default {
|
|||||||
slip_tab_parlay: '串关',
|
slip_tab_parlay: '串关',
|
||||||
slip_parlay_empty_hint: '先选择一个下注项,再点「加入串关」',
|
slip_parlay_empty_hint: '先选择一个下注项,再点「加入串关」',
|
||||||
slip_add_parlay: '加入串关',
|
slip_add_parlay: '加入串关',
|
||||||
|
slip_parlay_only_hint: '此盘口不支持单关,请点「加入串关」后再投注',
|
||||||
slip_parlay_same_match: '串关选项不得为同一场比赛,请先移除同场选项后再加入。',
|
slip_parlay_same_match: '串关选项不得为同一场比赛,请先移除同场选项后再加入。',
|
||||||
slip_parlay_count: '{n} 项串关',
|
slip_parlay_count: '{n} 项串关',
|
||||||
slip_total_stake: '总投注金额',
|
slip_total_stake: '总投注金额',
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export interface SlipItem {
|
|||||||
odds: number;
|
odds: number;
|
||||||
marketType: string;
|
marketType: string;
|
||||||
lineValue?: number | null;
|
lineValue?: number | null;
|
||||||
|
allowSingle?: boolean;
|
||||||
allowParlay?: boolean;
|
allowParlay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
} from '@thebet365/shared';
|
} from '@thebet365/shared';
|
||||||
|
|
||||||
type MainTab = 'matches' | 'outright';
|
type MainTab = 'matches' | 'outright';
|
||||||
type TimeTab = 'today' | 'early';
|
|
||||||
|
|
||||||
interface Match {
|
interface Match {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -62,17 +61,16 @@ const { t } = useI18n();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const mainTab = ref<MainTab>('matches');
|
const mainTab = ref<MainTab>('matches');
|
||||||
const timeTab = ref<TimeTab>('today');
|
const filterState = ref({
|
||||||
const showAll = ref(false);
|
time: 'all' as 'all' | 'today' | 'early',
|
||||||
|
status: 'open' as 'all' | 'open' | 'settled',
|
||||||
|
leagueIds: [] as string[]
|
||||||
|
});
|
||||||
const outrightActivated = ref(false);
|
const outrightActivated = ref(false);
|
||||||
const filterNow = ref(new Date());
|
const filterNow = ref(new Date());
|
||||||
const { summaryMatches, summaryLoading, loadSummary } = usePlayerMatches();
|
const { summaryMatches, summaryLoading, loadSummary } = usePlayerMatches();
|
||||||
const matches = summaryMatches;
|
const matches = summaryMatches;
|
||||||
const loading = summaryLoading;
|
const loading = summaryLoading;
|
||||||
const expandedLeagues = ref<Record<TimeTab, Set<string>>>({
|
|
||||||
today: new Set(),
|
|
||||||
early: new Set(),
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadMatches() {
|
async function loadMatches() {
|
||||||
filterNow.value = new Date();
|
filterNow.value = new Date();
|
||||||
@@ -90,18 +88,81 @@ const pullIndicatorStyle = () => ({
|
|||||||
opacity: Math.min(pullDistance.value / 48, 1),
|
opacity: Math.min(pullDistance.value / 48, 1),
|
||||||
});
|
});
|
||||||
|
|
||||||
function filterMatchesForTab(tab: TimeTab) {
|
function normalizeLeagueName(name: string): string {
|
||||||
|
return name
|
||||||
|
.replace(/\.unit$/i, '')
|
||||||
|
.replace(/[-_]+/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredMatches = computed(() => {
|
||||||
if (mainTab.value !== 'matches') return [];
|
if (mainTab.value !== 'matches') return [];
|
||||||
const now = filterNow.value;
|
const now = filterNow.value;
|
||||||
return matches.value.filter((m) => {
|
return matches.value.filter((m) => {
|
||||||
const timeMatch =
|
let timeMatch = true;
|
||||||
tab === 'today'
|
if (filterState.value.time === 'today') {
|
||||||
? isInTodayMatchWindow(m.startTime, now)
|
timeMatch = isInTodayMatchWindow(m.startTime, now);
|
||||||
: isAfterTodayMatchWindow(m.startTime, now);
|
} else if (filterState.value.time === 'early') {
|
||||||
|
timeMatch = isAfterTodayMatchWindow(m.startTime, now);
|
||||||
|
}
|
||||||
if (!timeMatch) return false;
|
if (!timeMatch) return false;
|
||||||
if (!showAll.value && m.matchPhase !== 'open' && m.matchPhase !== undefined) return false;
|
|
||||||
|
if (filterState.value.status === 'open' && m.matchPhase !== 'open' && m.matchPhase !== undefined) return false;
|
||||||
|
if (filterState.value.status === 'settled' && m.matchPhase !== 'settled') return false;
|
||||||
|
|
||||||
|
if (filterState.value.leagueIds.length > 0) {
|
||||||
|
const id = m.leagueId ?? m.leagueName;
|
||||||
|
if (!filterState.value.leagueIds.includes(id)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const availableLeagues = computed(() => {
|
||||||
|
const map = new Map<string, { id: string, name: string }>();
|
||||||
|
for (const m of matches.value) {
|
||||||
|
let timeMatch = true;
|
||||||
|
if (filterState.value.time === 'today') {
|
||||||
|
timeMatch = isInTodayMatchWindow(m.startTime, filterNow.value);
|
||||||
|
} else if (filterState.value.time === 'early') {
|
||||||
|
timeMatch = isAfterTodayMatchWindow(m.startTime, filterNow.value);
|
||||||
|
}
|
||||||
|
if (!timeMatch) continue;
|
||||||
|
|
||||||
|
const id = m.leagueId ?? m.leagueName;
|
||||||
|
if (!map.has(id)) {
|
||||||
|
map.set(id, { id, name: normalizeLeagueName(m.leagueName) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...map.values()].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => filterState.value.time, () => {
|
||||||
|
filterState.value.leagueIds = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const dropdownOpen = ref(false);
|
||||||
|
|
||||||
|
const selectedLeagueName = computed(() => {
|
||||||
|
if (filterState.value.leagueIds.length === 0) return '';
|
||||||
|
const firstId = filterState.value.leagueIds[0];
|
||||||
|
const lg = availableLeagues.value.find(l => l.id === firstId);
|
||||||
|
return lg ? lg.name : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
function selectLeague(id: string) {
|
||||||
|
if (id === '') {
|
||||||
|
filterState.value.leagueIds = [];
|
||||||
|
} else {
|
||||||
|
filterState.value.leagueIds = [id];
|
||||||
|
}
|
||||||
|
dropdownOpen.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleStatusOpen() {
|
||||||
|
filterState.value.status = filterState.value.status === 'open' ? 'all' : 'open';
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLeagueGroups(source: Match[]): LeagueGroup[] {
|
function buildLeagueGroups(source: Match[]): LeagueGroup[] {
|
||||||
@@ -111,7 +172,7 @@ function buildLeagueGroups(source: Match[]): LeagueGroup[] {
|
|||||||
if (!map.has(id)) {
|
if (!map.has(id)) {
|
||||||
map.set(id, {
|
map.set(id, {
|
||||||
leagueId: id,
|
leagueId: id,
|
||||||
leagueName: m.leagueName,
|
leagueName: normalizeLeagueName(m.leagueName),
|
||||||
leagueLogoUrl: m.leagueLogoUrl ?? null,
|
leagueLogoUrl: m.leagueLogoUrl ?? null,
|
||||||
matches: [],
|
matches: [],
|
||||||
});
|
});
|
||||||
@@ -133,32 +194,28 @@ function buildLeagueGroups(source: Match[]): LeagueGroup[] {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const todayLeagueGroups = computed(() => buildLeagueGroups(filterMatchesForTab('today')));
|
const leagueGroups = computed(() => buildLeagueGroups(filteredMatches.value));
|
||||||
const earlyLeagueGroups = computed(() => buildLeagueGroups(filterMatchesForTab('early')));
|
|
||||||
|
|
||||||
function syncExpandedLeagues(groups: LeagueGroup[], tab: TimeTab) {
|
const expandedLeagues = ref(new Set<string>());
|
||||||
const current = expandedLeagues.value[tab];
|
|
||||||
const ids = new Set(current);
|
watch(leagueGroups, (groups) => {
|
||||||
|
const ids = new Set(expandedLeagues.value);
|
||||||
for (const id of [...ids]) {
|
for (const id of [...ids]) {
|
||||||
if (!groups.some((g) => g.leagueId === id)) ids.delete(id);
|
if (!groups.some((g) => g.leagueId === id)) ids.delete(id);
|
||||||
}
|
}
|
||||||
if (groups.length > 0 && ids.size === 0) {
|
if (groups.length > 0 && ids.size === 0) {
|
||||||
ids.add(groups[0].leagueId);
|
ids.add(groups[0].leagueId);
|
||||||
}
|
}
|
||||||
if (ids.size !== current.size || [...ids].some((id) => !current.has(id))) {
|
if (ids.size !== expandedLeagues.value.size || [...ids].some((id) => !expandedLeagues.value.has(id))) {
|
||||||
expandedLeagues.value = { ...expandedLeagues.value, [tab]: ids };
|
expandedLeagues.value = ids;
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
watch(todayLeagueGroups, (groups) => syncExpandedLeagues(groups, 'today'));
|
|
||||||
watch(earlyLeagueGroups, (groups) => syncExpandedLeagues(groups, 'early'));
|
|
||||||
|
|
||||||
function toggleLeague(leagueId: string) {
|
function toggleLeague(leagueId: string) {
|
||||||
const tab = timeTab.value;
|
const next = new Set(expandedLeagues.value);
|
||||||
const next = new Set(expandedLeagues.value[tab]);
|
|
||||||
if (next.has(leagueId)) next.delete(leagueId);
|
if (next.has(leagueId)) next.delete(leagueId);
|
||||||
else next.add(leagueId);
|
else next.add(leagueId);
|
||||||
expandedLeagues.value = { ...expandedLeagues.value, [tab]: next };
|
expandedLeagues.value = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectMainTab(tab: MainTab) {
|
function selectMainTab(tab: MainTab) {
|
||||||
@@ -168,7 +225,7 @@ function selectMainTab(tab: MainTab) {
|
|||||||
|
|
||||||
onActivated(() => {
|
onActivated(() => {
|
||||||
filterNow.value = new Date();
|
filterNow.value = new Date();
|
||||||
timeTab.value = 'today';
|
filterState.value.time = 'all';
|
||||||
void loadSummary(true, true);
|
void loadSummary(true, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -208,72 +265,90 @@ function goMatch(id: string) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div :class="['tab-panel', { 'tab-panel--hidden': mainTab !== 'matches' }]">
|
<div :class="['tab-panel', { 'tab-panel--hidden': mainTab !== 'matches' }]">
|
||||||
|
<div class="filters-bar">
|
||||||
|
<div class="phase-filter">
|
||||||
|
<div class="league-dropdown">
|
||||||
|
<button type="button" class="dropdown-trigger" @click="dropdownOpen = !dropdownOpen">
|
||||||
|
<span class="dropdown-text">{{ selectedLeagueName || t('bet.filter_leagues_all') }}</span>
|
||||||
|
<span class="arrow-icon" :class="{ open: dropdownOpen }">▼</span>
|
||||||
|
</button>
|
||||||
|
<div v-if="dropdownOpen" class="dropdown-backdrop" @click="dropdownOpen = false"></div>
|
||||||
|
<div v-if="dropdownOpen" class="dropdown-menu">
|
||||||
|
<div
|
||||||
|
class="dropdown-item"
|
||||||
|
:class="{ active: filterState.leagueIds.length === 0 }"
|
||||||
|
@click="selectLeague('')"
|
||||||
|
>
|
||||||
|
{{ t('bet.filter_leagues_all') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="lg in availableLeagues"
|
||||||
|
:key="lg.id"
|
||||||
|
class="dropdown-item"
|
||||||
|
:class="{ active: filterState.leagueIds.includes(lg.id) }"
|
||||||
|
@click="selectLeague(lg.id)"
|
||||||
|
>
|
||||||
|
{{ lg.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-filters">
|
||||||
<div class="time-tabs">
|
<div class="time-tabs">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="time-tab"
|
class="time-tab"
|
||||||
:class="{ active: timeTab === 'today', 'tab-gold-active': timeTab === 'today' }"
|
:class="{ active: filterState.time === 'all' }"
|
||||||
@click="timeTab = 'today'"
|
@click="filterState.time = 'all'"
|
||||||
|
>
|
||||||
|
{{ t('bet.filter_status_all') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="time-tab"
|
||||||
|
:class="{ active: filterState.time === 'today' }"
|
||||||
|
@click="filterState.time = 'today'"
|
||||||
>
|
>
|
||||||
{{ t('bet.tab_today') }}
|
{{ t('bet.tab_today') }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="time-tab"
|
class="time-tab"
|
||||||
:class="{ active: timeTab === 'early', 'tab-gold-active': timeTab === 'early' }"
|
:class="{ active: filterState.time === 'early' }"
|
||||||
@click="timeTab = 'early'"
|
@click="filterState.time = 'early'"
|
||||||
>
|
>
|
||||||
{{ t('bet.tab_early') }}
|
{{ t('bet.tab_early') }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="phase-filter">
|
<div class="divider"></div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="phase-toggle"
|
class="status-toggle-btn"
|
||||||
:class="{ 'phase-toggle--active': !showAll }"
|
:class="{ active: filterState.status === 'open' }"
|
||||||
:aria-pressed="!showAll"
|
@click="toggleStatusOpen"
|
||||||
@click="showAll = !showAll"
|
|
||||||
>
|
>
|
||||||
<span class="phase-toggle-dot" />
|
{{ t('bet.filter_status_open') }}
|
||||||
{{ t('bet.show_open_only') }}
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="loading" class="state">
|
<div v-if="loading" class="state">
|
||||||
<GoldSpinner :size="36" />
|
<GoldSpinner :size="36" />
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-show="timeTab === 'today'">
|
<div>
|
||||||
<div v-if="todayLeagueGroups.length" class="league-list">
|
<div v-if="leagueGroups.length" class="league-list">
|
||||||
<LeagueAccordionItem
|
<LeagueAccordionItem
|
||||||
v-for="group in todayLeagueGroups"
|
v-for="group in leagueGroups"
|
||||||
:key="`today-${group.leagueId}`"
|
:key="group.leagueId"
|
||||||
:league-id="group.leagueId"
|
:league-id="group.leagueId"
|
||||||
:league-name="group.leagueName"
|
:league-name="group.leagueName"
|
||||||
:league-logo-url="group.leagueLogoUrl"
|
:league-logo-url="group.leagueLogoUrl"
|
||||||
:matches="group.matches"
|
:matches="group.matches"
|
||||||
:expanded="expandedLeagues.today.has(group.leagueId)"
|
:expanded="expandedLeagues.has(group.leagueId)"
|
||||||
@toggle="toggleLeague(group.leagueId)"
|
|
||||||
@bet="goMatch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-else class="empty">
|
|
||||||
<img :src="emptyMatchesImg" alt="" class="empty-icon" />
|
|
||||||
<p>{{ t('bet.no_matches') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="timeTab === 'early'">
|
|
||||||
<div v-if="earlyLeagueGroups.length" class="league-list">
|
|
||||||
<LeagueAccordionItem
|
|
||||||
v-for="group in earlyLeagueGroups"
|
|
||||||
:key="`early-${group.leagueId}`"
|
|
||||||
:league-id="group.leagueId"
|
|
||||||
:league-name="group.leagueName"
|
|
||||||
:league-logo-url="group.leagueLogoUrl"
|
|
||||||
:matches="group.matches"
|
|
||||||
:expanded="expandedLeagues.early.has(group.leagueId)"
|
|
||||||
@toggle="toggleLeague(group.leagueId)"
|
@toggle="toggleLeague(group.leagueId)"
|
||||||
@bet="goMatch"
|
@bet="goMatch"
|
||||||
/>
|
/>
|
||||||
@@ -292,6 +367,8 @@ function goMatch(id: string) {
|
|||||||
:activated="mainTab === 'outright'"
|
:activated="mainTab === 'outright'"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -307,7 +384,7 @@ function goMatch(id: string) {
|
|||||||
.bet-page {
|
.bet-page {
|
||||||
margin: 0 -16px;
|
margin: 0 -16px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
background: #E8EDF2;
|
background: var(--bg-body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tab panel toggle: avoid display:none to prevent browser releasing image resources */
|
/* Tab panel toggle: avoid display:none to prevent browser releasing image resources */
|
||||||
@@ -341,22 +418,22 @@ function goMatch(id: string) {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 10px 8px;
|
padding: 10px 8px;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-sm);
|
||||||
background: #FFFFFF;
|
background: var(--bg-card);
|
||||||
border: 1px solid #CBD5E1;
|
border: 1px solid var(--border);
|
||||||
color: #6B7280;
|
color: var(--text-muted);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: var(--shadow);
|
||||||
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
transition: background 0.2s, border-color 0.2s, color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-tab.active {
|
.main-tab.active {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
background: #EBF3FB;
|
background: rgba(244, 162, 97, 0.1);
|
||||||
border-color: #003D6B;
|
border-color: var(--primary);
|
||||||
color: #003D6B;
|
color: var(--primary-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-icon {
|
.tab-icon {
|
||||||
@@ -364,68 +441,174 @@ function goMatch(id: string) {
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-tabs {
|
.filters-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
align-items: center;
|
||||||
padding: 0 12px 14px;
|
justify-content: flex-start;
|
||||||
}
|
gap: 12px;
|
||||||
|
padding: 0 16px 12px;
|
||||||
.time-tab {
|
|
||||||
flex: 1;
|
|
||||||
padding: 9px 16px;
|
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #003D6B;
|
|
||||||
background: #FFFFFF;
|
|
||||||
border: 1px solid #CBD5E1;
|
|
||||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: background 0.2s, border-color 0.2s, color 0.2s, box-shadow 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-tab.active {
|
|
||||||
background: #003D6B !important;
|
|
||||||
border-color: #003D6B !important;
|
|
||||||
color: #FFFFFF !important;
|
|
||||||
font-weight: 700;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 61, 107, 0.25);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.phase-filter {
|
.phase-filter {
|
||||||
padding: 0 12px 10px;
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phase-toggle {
|
.right-filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 3px;
|
||||||
|
height: 26px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tab {
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time-tab.active {
|
||||||
|
color: var(--primary-light);
|
||||||
|
background: rgba(244, 162, 97, 0.15);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-filters .divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 12px;
|
||||||
|
background: var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toggle-btn {
|
||||||
|
padding: 0 10px;
|
||||||
|
height: 26px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-toggle-btn.active {
|
||||||
|
color: var(--primary-light);
|
||||||
|
background: rgba(244, 162, 97, 0.15);
|
||||||
|
border-color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* League Dropdown Selector */
|
||||||
|
.league-dropdown {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-trigger {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 6px 14px;
|
padding: 0 12px;
|
||||||
border-radius: 999px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
color: #6B7280;
|
color: var(--text);
|
||||||
background: #FFFFFF;
|
background: var(--bg-card);
|
||||||
border: 1px solid #CBD5E1;
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s ease;
|
height: 24px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phase-toggle--active {
|
.dropdown-text {
|
||||||
color: #003D6B;
|
min-width: 0;
|
||||||
border-color: #003D6B;
|
overflow: hidden;
|
||||||
background: rgba(0, 61, 107, 0.06);
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.phase-toggle-dot {
|
.dropdown-trigger .arrow-icon {
|
||||||
width: 6px;
|
font-size: 8px;
|
||||||
height: 6px;
|
color: var(--text-muted);
|
||||||
border-radius: 50%;
|
transition: transform 0.2s ease;
|
||||||
background: #CBD5E1;
|
display: inline-block;
|
||||||
transition: background 0.2s ease;
|
transform: scale(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.phase-toggle--active .phase-toggle-dot {
|
.dropdown-trigger .arrow-icon.open {
|
||||||
background: #003D6B;
|
transform: scale(0.8) rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 98;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
left: 0;
|
||||||
|
z-index: 99;
|
||||||
|
min-width: 170px;
|
||||||
|
max-width: 240px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background: #FFFFFF;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.35);
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover,
|
||||||
|
.dropdown-item:active {
|
||||||
|
background: var(--bg-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item.active {
|
||||||
|
color: var(--primary-light);
|
||||||
|
background: rgba(244, 162, 97, 0.1);
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.league-list {
|
.league-list {
|
||||||
@@ -436,7 +619,7 @@ function goMatch(id: string) {
|
|||||||
.placeholder,
|
.placeholder,
|
||||||
.empty {
|
.empty {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #6B7280;
|
color: var(--text-muted);
|
||||||
padding: 48px 20px;
|
padding: 48px 20px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,12 +68,6 @@ function formatKickoff(startTime: string) {
|
|||||||
</span>
|
</span>
|
||||||
<span class="qe-label">{{ t('wallet.view_all') }}</span>
|
<span class="qe-label">{{ t('wallet.view_all') }}</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="qe-item" @click="router.push('/bet')">
|
|
||||||
<span class="qe-icon qe-icon--promo">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" width="22" height="22"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
|
||||||
</span>
|
|
||||||
<span class="qe-label">{{ t('home.hot_matches') || '热门赛事' }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="section-title">{{ t('home.hot_matches') }}</h2>
|
<h2 class="section-title">{{ t('home.hot_matches') }}</h2>
|
||||||
@@ -147,7 +141,7 @@ function formatKickoff(startTime: string) {
|
|||||||
|
|
||||||
.quick-entries {
|
.quick-entries {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-bottom: 14px;
|
margin-bottom: 14px;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
@@ -196,11 +190,6 @@ function formatKickoff(startTime: string) {
|
|||||||
color: #E8870A;
|
color: #E8870A;
|
||||||
}
|
}
|
||||||
|
|
||||||
.qe-icon--promo {
|
|
||||||
background: rgba(220, 38, 38, 0.08);
|
|
||||||
color: #DC2626;
|
|
||||||
}
|
|
||||||
|
|
||||||
.qe-label {
|
.qe-label {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ interface Market {
|
|||||||
lineKey?: string | null;
|
lineKey?: string | null;
|
||||||
period: string;
|
period: string;
|
||||||
lineValue?: string | number | null;
|
lineValue?: string | number | null;
|
||||||
|
status?: string;
|
||||||
allowSingle?: boolean;
|
allowSingle?: boolean;
|
||||||
allowParlay?: boolean;
|
allowParlay?: boolean;
|
||||||
promoLabel?: string | null;
|
promoLabel?: string | null;
|
||||||
@@ -191,6 +192,28 @@ function marketLabel(market: Market | null | undefined) {
|
|||||||
return market?.marketDisplayName?.trim() || market?.marketType || '';
|
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;
|
||||||
|
const singleOk = market.allowSingle !== false;
|
||||||
|
const parlayOk = market.allowParlay !== false;
|
||||||
|
return !singleOk && !parlayOk;
|
||||||
|
}
|
||||||
|
|
||||||
function selectionLabel(sel: Selection) {
|
function selectionLabel(sel: Selection) {
|
||||||
const parsedScore = parseScoreCode(sel.selectionCode, t);
|
const parsedScore = parseScoreCode(sel.selectionCode, t);
|
||||||
if (parsedScore) return parsedScore.display;
|
if (parsedScore) return parsedScore.display;
|
||||||
@@ -224,8 +247,7 @@ function isSelected(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleSelection(sel: Selection, market: Market) {
|
function toggleSelection(sel: Selection, market: Market) {
|
||||||
if (!match.value || !bettingOpen.value) return;
|
if (!match.value || isMarketLocked(market)) return;
|
||||||
if (market.allowSingle === false) return;
|
|
||||||
if (!auth.token) {
|
if (!auth.token) {
|
||||||
goLogin();
|
goLogin();
|
||||||
return;
|
return;
|
||||||
@@ -244,6 +266,7 @@ function toggleSelection(sel: Selection, market: Market) {
|
|||||||
market.lineValue != null && market.lineValue !== ''
|
market.lineValue != null && market.lineValue !== ''
|
||||||
? parseFloat(String(market.lineValue))
|
? parseFloat(String(market.lineValue))
|
||||||
: null,
|
: null,
|
||||||
|
allowSingle: market.allowSingle,
|
||||||
allowParlay: market.allowParlay,
|
allowParlay: market.allowParlay,
|
||||||
});
|
});
|
||||||
slip.openDrawer();
|
slip.openDrawer();
|
||||||
@@ -388,24 +411,32 @@ function onPickSelection(selId: string, marketId: string) {
|
|||||||
<MarketTypeTile
|
<MarketTypeTile
|
||||||
:label="marketLabel(market)"
|
:label="marketLabel(market)"
|
||||||
:promo-label="market.promoLabel?.trim() || ''"
|
:promo-label="market.promoLabel?.trim() || ''"
|
||||||
|
:status-label="bettingOpen && !isMarketOpen(market) ? marketStatusLabel(market.status) : ''"
|
||||||
:has-market="true"
|
:has-market="true"
|
||||||
:expanded="true"
|
:expanded="true"
|
||||||
/>
|
/>
|
||||||
<div class="market-panel-wrap" :class="{ locked: !bettingOpen || market.allowSingle === false }">
|
<div class="market-panel-wrap" :class="{ locked: isMarketLocked(market) }">
|
||||||
<span v-if="!bettingOpen && matchPhase === 'settled'" class="market-status-tag market-status-tag--settled">{{ phaseLabel }}</span>
|
<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>
|
<span v-else-if="!bettingOpen" class="market-status-tag market-status-tag--pending">{{ phaseLabel }}</span>
|
||||||
|
<span
|
||||||
|
v-else-if="!isMarketOpen(market)"
|
||||||
|
class="market-status-tag"
|
||||||
|
:class="normalizeMarketStatus(market.status) === 'CLOSED' ? 'market-status-tag--closed' : 'market-status-tag--suspended'"
|
||||||
|
>
|
||||||
|
{{ marketStatusLabel(market.status) }}
|
||||||
|
</span>
|
||||||
<CorrectScorePanel
|
<CorrectScorePanel
|
||||||
v-if="isCorrectScoreMarket(market.marketType)"
|
v-if="isCorrectScoreMarket(market.marketType)"
|
||||||
:market-type="market.marketType"
|
:market-type="market.marketType"
|
||||||
:selections="market.selections"
|
:selections="market.selections"
|
||||||
:locked="!bettingOpen || market.allowSingle === false"
|
:locked="isMarketLocked(market)"
|
||||||
:is-selected="isSelected"
|
:is-selected="isSelected"
|
||||||
@pick="onPickSelection($event, market.id)"
|
@pick="onPickSelection($event, market.id)"
|
||||||
/>
|
/>
|
||||||
<MarketSelectionsPanel
|
<MarketSelectionsPanel
|
||||||
v-else
|
v-else
|
||||||
compact
|
compact
|
||||||
:locked="!bettingOpen || market.allowSingle === false"
|
:locked="isMarketLocked(market)"
|
||||||
:line-value="market.lineValue"
|
:line-value="market.lineValue"
|
||||||
:selections="market.selections"
|
:selections="market.selections"
|
||||||
:is-selected="isSelected"
|
:is-selected="isSelected"
|
||||||
@@ -422,6 +453,7 @@ function onPickSelection(selId: string, marketId: string) {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.detail-page {
|
.detail-page {
|
||||||
margin: 0 -16px;
|
margin: 0 -16px;
|
||||||
|
padding-top: 8px;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 16px;
|
||||||
background: #E8EDF2;
|
background: #E8EDF2;
|
||||||
}
|
}
|
||||||
@@ -441,9 +473,9 @@ function onPickSelection(selId: string, marketId: string) {
|
|||||||
padding: 4px 12px 8px;
|
padding: 4px 12px 8px;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: -12px;
|
top: 0;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
margin-top: -12px;
|
margin-bottom: 8px;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border-bottom: 1px solid #CBD5E1;
|
border-bottom: 1px solid #CBD5E1;
|
||||||
}
|
}
|
||||||
@@ -557,6 +589,20 @@ function onPickSelection(selId: string, marketId: string) {
|
|||||||
border-left: 1px solid #FDE68A;
|
border-left: 1px solid #FDE68A;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.market-status-tag--suspended {
|
||||||
|
background: linear-gradient(180deg, rgba(240, 180, 41, 0.28) 0%, rgba(120, 80, 10, 0.92) 100%);
|
||||||
|
color: #ffe08a;
|
||||||
|
border-bottom: 1px solid rgba(240, 180, 41, 0.45);
|
||||||
|
border-left: 1px solid rgba(240, 180, 41, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-status-tag--closed {
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.12) 0%, rgba(40, 40, 55, 0.95) 100%);
|
||||||
|
color: #8a9ab8;
|
||||||
|
border-bottom: 1px solid rgba(120, 140, 180, 0.3);
|
||||||
|
border-left: 1px solid rgba(120, 140, 180, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
.market-panel-wrap {
|
.market-panel-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -142,21 +142,21 @@ const balanceAmountClass = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bank-card-balance">
|
<div class="bank-card-balance">
|
||||||
<span class="bank-card-label">当前余额</span>
|
<span class="bank-card-label">{{ t('wallet.balance') }}</span>
|
||||||
<p class="bank-card-number balance-amount" :class="balanceAmountClass">{{ balanceDisplay }}</p>
|
<p class="bank-card-number balance-amount" :class="balanceAmountClass">{{ balanceDisplay }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="bank-card-footer">
|
<div class="bank-card-footer">
|
||||||
<div class="bank-card-field">
|
<div class="bank-card-field">
|
||||||
<span class="bank-card-label">持卡人</span>
|
<span class="bank-card-label">{{ t('wallet.card_holder') }}</span>
|
||||||
<span class="bank-card-holder">{{ profileRaw?.username }}</span>
|
<span class="bank-card-holder">{{ profileRaw?.username }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bank-card-field bank-card-field--center">
|
<div class="bank-card-field bank-card-field--center">
|
||||||
<span class="bank-card-label">累计返水</span>
|
<span class="bank-card-label">{{ t('cashback.total_received') }}</span>
|
||||||
<span class="bank-card-stat">{{ formatMoney(cashbackTotal, locale) }}</span>
|
<span class="bank-card-stat">{{ formatMoney(cashbackTotal, locale) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="bank-card-field bank-card-field--right">
|
<div class="bank-card-field bank-card-field--right">
|
||||||
<span class="bank-card-label">未结算</span>
|
<span class="bank-card-label">{{ t('wallet.unsettled') }}</span>
|
||||||
<span class="bank-card-stat">{{ formatMoney(profileRaw?.wallet?.frozenBalance, locale) }}</span>
|
<span class="bank-card-stat">{{ formatMoney(profileRaw?.wallet?.frozenBalance, locale) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -528,11 +528,12 @@ const balanceAmountClass = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.qa-item {
|
.qa-item {
|
||||||
|
min-width: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 14px 4px 12px;
|
padding: 12px 4px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: #FFFFFF;
|
background: #FFFFFF;
|
||||||
border: 1px solid #CBD5E1;
|
border: 1px solid #CBD5E1;
|
||||||
@@ -577,15 +578,19 @@ const balanceAmountClass = computed(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.qa-label {
|
.qa-label {
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #1A1A2E;
|
color: #1A1A2E;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
white-space: nowrap;
|
white-space: normal;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
min-height: 2.4em;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-group {
|
.settings-group {
|
||||||
|
|||||||
Reference in New Issue
Block a user