import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; import { PARLAY_MIN_LEGS, PARLAY_MAX_LEGS, canSelectForParlay, type ParlayRejectReason, } from '@thebet365/shared'; export interface SlipItem { selectionId: string; oddsVersion: string; matchId: string; matchName: string; marketId?: string; marketName?: string; selectionName: string; odds: number; marketType: string; lineValue?: number | null; allowSingle?: boolean; allowParlay?: boolean; } export type SlipMode = 'single' | 'parlay'; export type ParlaySlipError = ParlayRejectReason | 'MAX_LEGS' | 'SAME_MATCH'; export const useBetSlipStore = defineStore('betSlip', () => { const singleItem = ref(null); const parlayItems = ref([]); const stake = ref(5); const mode = ref('single'); const lastParlayError = ref(null); const items = computed(() => mode.value === 'parlay' ? parlayItems.value : singleItem.value ? [singleItem.value] : [], ); const count = computed(() => items.value.length); const parlayCount = computed(() => parlayItems.value.length); const singleCount = computed(() => (singleItem.value ? 1 : 0)); const totalCount = computed(() => singleCount.value + parlayCount.value); const isParlay = computed(() => mode.value === 'parlay' && parlayItems.value.length >= PARLAY_MIN_LEGS); function parseLineValue(v: number | string | null | undefined): number | null { if (v == null || v === '') return null; const n = typeof v === 'number' ? v : parseFloat(String(v)); return Number.isFinite(n) ? n : null; } function setMode(nextMode: SlipMode) { mode.value = nextMode; lastParlayError.value = null; } /** 球赛/详情页:点击任意选项后作为当前单注,打开投注抽屉 */ function setSingleItem(item: SlipItem) { mode.value = 'single'; singleItem.value = item; lastParlayError.value = null; } function addItem(item: SlipItem) { setSingleItem(item); } /** 串关:必须来自不同赛事,仍按盘口规则过滤不可串关项 */ function addParlayLeg(item: SlipItem): ParlaySlipError | null { mode.value = 'parlay'; const samePick = parlayItems.value.findIndex((i) => i.selectionId === item.selectionId); if (samePick >= 0) { lastParlayError.value = null; return null; } if (parlayItems.value.some((i) => i.matchId === item.matchId)) { lastParlayError.value = 'SAME_MATCH'; return 'SAME_MATCH'; } const check = canSelectForParlay({ marketType: item.marketType, lineValue: parseLineValue(item.lineValue), allowParlay: item.allowParlay, }); if (!check.ok) { lastParlayError.value = check.reason; return check.reason; } if (parlayItems.value.length >= PARLAY_MAX_LEGS) { lastParlayError.value = 'MAX_LEGS'; return 'MAX_LEGS'; } parlayItems.value.push(item); lastParlayError.value = null; return null; } function addSingleToParlay(): ParlaySlipError | null { if (!singleItem.value) return null; return addParlayLeg(singleItem.value); } function removeItem(selectionId: string) { if (singleItem.value?.selectionId === selectionId) { singleItem.value = null; } parlayItems.value = parlayItems.value.filter((i) => i.selectionId !== selectionId); if (mode.value === 'parlay' && !parlayItems.value.length) mode.value = 'single'; lastParlayError.value = null; } function clearSingle() { singleItem.value = null; if (mode.value === 'single') lastParlayError.value = null; } function clearParlay() { parlayItems.value = []; mode.value = 'single'; lastParlayError.value = null; } function clear() { if (mode.value === 'parlay') { clearParlay(); return; } clearSingle(); } function clearAll() { singleItem.value = null; parlayItems.value = []; mode.value = 'single'; lastParlayError.value = null; } const totalOdds = computed(() => items.value.reduce((acc, i) => acc * i.odds, 1), ); const potentialReturn = computed(() => { if (!items.value.length) return 0; if (mode.value === 'parlay' && canPlaceParlay.value) { return stake.value * totalOdds.value; } return items.value.reduce((acc, i) => acc + stake.value * i.odds, 0); }); const canPlaceParlay = computed( () => mode.value === 'parlay' && parlayItems.value.length >= PARLAY_MIN_LEGS && parlayItems.value.length <= PARLAY_MAX_LEGS, ); const canPlaceBatchSingles = computed( () => mode.value === 'single' && singleItem.value != null, ); const canSubmit = computed( () => canPlaceParlay.value || canPlaceBatchSingles.value, ); const drawerOpen = ref(false); function openDrawer() { drawerOpen.value = true; } function closeDrawer() { drawerOpen.value = false; } function updateSelectionOdds(selectionId: string, odds: number, oddsVersion: string) { if (singleItem.value?.selectionId === selectionId) { singleItem.value = { ...singleItem.value, odds, oddsVersion }; } const idx = parlayItems.value.findIndex((i) => i.selectionId === selectionId); if (idx >= 0) { parlayItems.value[idx] = { ...parlayItems.value[idx], odds, oddsVersion }; } } return { singleItem, parlayItems, items, stake, mode, singleCount, parlayCount, totalCount, count, isParlay, totalOdds, potentialReturn, canPlaceParlay, canPlaceBatchSingles, canSubmit, lastParlayError, drawerOpen, setMode, setSingleItem, addItem, addParlayLeg, addSingleToParlay, removeItem, clearSingle, clearParlay, clear, clearAll, openDrawer, closeDrawer, updateSelectionOdds, }; });