fix(admin/player/api): 同步单关串关持久化、仅串关可选与盘口状态展示
1. 后台/API:赛事详情返回 allowSingle/allowParlay,管理端 mapMarkets 正确回读 2. 玩家端:仅关单关时仍可点选加入串关;投注抽屉禁用单关提交并提示 3. 玩家端:盘口暂停/已关闭状态标签;MarketTypeTile 展示 statusLabel Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}>();
|
}>();
|
||||||
@@ -21,9 +22,9 @@ const { t } = useI18n();
|
|||||||
@click="emit('toggle')"
|
@click="emit('toggle')"
|
||||||
>
|
>
|
||||||
<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(255, 184, 0, 0.35);
|
border: 1px solid rgba(255, 184, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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: var(--primary-light);
|
color: var(--primary-light);
|
||||||
}
|
}
|
||||||
@@ -95,16 +107,4 @@ const { t } = useI18n();
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.row-chevron {
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--text-muted);
|
|
||||||
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: var(--primary-light);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -304,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',
|
||||||
@@ -387,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',
|
||||||
|
|||||||
@@ -310,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',
|
||||||
@@ -393,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',
|
||||||
|
|||||||
@@ -304,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: '下载',
|
||||||
@@ -387,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
@@ -554,6 +585,20 @@ function onPickSelection(selId: string, marketId: string) {
|
|||||||
border-left: 1px solid rgba(200, 168, 78, 0.3);
|
border-left: 1px solid rgba(200, 168, 78, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
|||||||
Reference in New Issue
Block a user