225 lines
5.8 KiB
TypeScript
225 lines
5.8 KiB
TypeScript
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<SlipItem | null>(null);
|
|
const parlayItems = ref<SlipItem[]>([]);
|
|
const stake = ref<number>(5);
|
|
const mode = ref<SlipMode>('single');
|
|
const lastParlayError = ref<ParlaySlipError | null>(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,
|
|
};
|
|
});
|