feat(theme-4): fix dark backgrounds, quick bet, inbox spacing, customer service theme
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, computed } from 'vue';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import {
|
||||
PARLAY_MIN_LEGS,
|
||||
PARLAY_MAX_LEGS,
|
||||
canSelectForParlay,
|
||||
type ParlayRejectReason,
|
||||
} from '@thebet365/shared';
|
||||
import { useAuthStore } from './auth';
|
||||
|
||||
export interface SlipItem {
|
||||
selectionId: string;
|
||||
@@ -25,26 +26,208 @@ export interface SlipItem {
|
||||
export type SlipMode = 'single' | 'parlay';
|
||||
export type ParlaySlipError = ParlayRejectReason | 'MAX_LEGS' | 'SAME_MATCH';
|
||||
|
||||
const BET_SLIP_STORAGE_PREFIX = 'player:betSlip:v1';
|
||||
const DESKTOP_PANEL_EXPANDED_KEY = 'player:desktopBetSlipExpanded:v1';
|
||||
|
||||
function readDesktopPanelExpanded() {
|
||||
try {
|
||||
return localStorage.getItem(DESKTOP_PANEL_EXPANDED_KEY) !== '0';
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function writeDesktopPanelExpanded(expanded: boolean) {
|
||||
try {
|
||||
localStorage.setItem(DESKTOP_PANEL_EXPANDED_KEY, expanded ? '1' : '0');
|
||||
} catch {
|
||||
// ignore quota / private mode errors
|
||||
}
|
||||
}
|
||||
|
||||
interface BetSlipPersisted {
|
||||
singleItem: SlipItem | null;
|
||||
singleCartItems: SlipItem[];
|
||||
parlayItems: SlipItem[];
|
||||
stake: number;
|
||||
singleStakes: Record<string, number>;
|
||||
mode: SlipMode;
|
||||
}
|
||||
|
||||
function betSlipStorageKey(userId?: string | null) {
|
||||
return userId ? `${BET_SLIP_STORAGE_PREFIX}:${userId}` : `${BET_SLIP_STORAGE_PREFIX}:guest`;
|
||||
}
|
||||
|
||||
function isValidSlipItem(item: unknown): item is SlipItem {
|
||||
if (!item || typeof item !== 'object') return false;
|
||||
const row = item as SlipItem;
|
||||
return Boolean(
|
||||
row.selectionId &&
|
||||
row.oddsVersion &&
|
||||
row.matchId &&
|
||||
row.matchName &&
|
||||
row.selectionName &&
|
||||
typeof row.odds === 'number' &&
|
||||
Number.isFinite(row.odds),
|
||||
);
|
||||
}
|
||||
|
||||
function readPersisted(userId?: string | null): BetSlipPersisted | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(betSlipStorageKey(userId));
|
||||
if (!raw) return null;
|
||||
return JSON.parse(raw) as BetSlipPersisted;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function writePersisted(payload: BetSlipPersisted, userId?: string | null) {
|
||||
try {
|
||||
localStorage.setItem(betSlipStorageKey(userId), JSON.stringify(payload));
|
||||
} catch {
|
||||
// ignore quota / private mode errors
|
||||
}
|
||||
}
|
||||
|
||||
function removePersisted(userId?: string | null) {
|
||||
localStorage.removeItem(betSlipStorageKey(userId));
|
||||
}
|
||||
|
||||
export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
const singleItem = ref<SlipItem | null>(null);
|
||||
const singleCartItems = ref<SlipItem[]>([]);
|
||||
const parlayItems = ref<SlipItem[]>([]);
|
||||
const stake = ref<number>(5);
|
||||
const singleStakes = ref<Record<string, number>>({});
|
||||
const mode = ref<SlipMode>('single');
|
||||
const lastParlayError = ref<ParlaySlipError | null>(null);
|
||||
|
||||
const auth = useAuthStore();
|
||||
let persistPaused = false;
|
||||
|
||||
function slipSnapshot(): BetSlipPersisted {
|
||||
return {
|
||||
singleItem: singleItem.value,
|
||||
singleCartItems: singleCartItems.value,
|
||||
parlayItems: parlayItems.value,
|
||||
stake: stake.value,
|
||||
singleStakes: singleStakes.value,
|
||||
mode: mode.value,
|
||||
};
|
||||
}
|
||||
|
||||
function hydrateFromStorage(userId?: string | null) {
|
||||
persistPaused = true;
|
||||
try {
|
||||
const saved = readPersisted(userId);
|
||||
if (!saved) {
|
||||
singleItem.value = null;
|
||||
singleCartItems.value = [];
|
||||
parlayItems.value = [];
|
||||
singleStakes.value = {};
|
||||
stake.value = 5;
|
||||
mode.value = 'single';
|
||||
lastParlayError.value = null;
|
||||
return;
|
||||
}
|
||||
singleItem.value = saved.singleItem && isValidSlipItem(saved.singleItem) ? saved.singleItem : null;
|
||||
singleCartItems.value = (saved.singleCartItems ?? []).filter(isValidSlipItem);
|
||||
parlayItems.value = (saved.parlayItems ?? []).filter(isValidSlipItem);
|
||||
stake.value =
|
||||
typeof saved.stake === 'number' && Number.isFinite(saved.stake) && saved.stake > 0
|
||||
? saved.stake
|
||||
: 5;
|
||||
singleStakes.value =
|
||||
saved.singleStakes && typeof saved.singleStakes === 'object' ? { ...saved.singleStakes } : {};
|
||||
mode.value = saved.mode === 'parlay' ? 'parlay' : 'single';
|
||||
lastParlayError.value = null;
|
||||
} finally {
|
||||
persistPaused = false;
|
||||
}
|
||||
}
|
||||
|
||||
function persistToStorage() {
|
||||
if (persistPaused) return;
|
||||
const userId = auth.user?.id ?? null;
|
||||
const snapshot = slipSnapshot();
|
||||
const hasItems =
|
||||
Boolean(snapshot.singleItem) ||
|
||||
snapshot.singleCartItems.length > 0 ||
|
||||
snapshot.parlayItems.length > 0;
|
||||
if (!hasItems) {
|
||||
removePersisted(userId);
|
||||
return;
|
||||
}
|
||||
writePersisted(snapshot, userId);
|
||||
}
|
||||
|
||||
hydrateFromStorage(auth.user?.id ?? null);
|
||||
|
||||
watch(
|
||||
() => auth.user?.id ?? null,
|
||||
(userId) => {
|
||||
hydrateFromStorage(userId);
|
||||
},
|
||||
);
|
||||
|
||||
watch(
|
||||
[singleItem, singleCartItems, parlayItems, stake, singleStakes, mode],
|
||||
() => {
|
||||
persistToStorage();
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const items = computed(() =>
|
||||
mode.value === 'parlay'
|
||||
? parlayItems.value
|
||||
: singleItem.value
|
||||
? [singleItem.value]
|
||||
: [],
|
||||
: singleCartItems.value.length
|
||||
? singleCartItems.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 singleCount = computed(() =>
|
||||
singleCartItems.value.length || (singleItem.value ? 1 : 0),
|
||||
);
|
||||
const totalCount = computed(() => singleCount.value + parlayCount.value);
|
||||
const isParlay = computed(() => mode.value === 'parlay' && parlayItems.value.length >= PARLAY_MIN_LEGS);
|
||||
|
||||
function defaultStakeAmount() {
|
||||
const base = Number(stake.value);
|
||||
return Number.isFinite(base) && base > 0 ? base : 5;
|
||||
}
|
||||
|
||||
function initItemStake(selectionId: string) {
|
||||
if (singleStakes.value[selectionId] == null) {
|
||||
singleStakes.value = { ...singleStakes.value, [selectionId]: defaultStakeAmount() };
|
||||
}
|
||||
}
|
||||
|
||||
function getItemStake(selectionId: string) {
|
||||
const v = singleStakes.value[selectionId];
|
||||
if (v != null && Number.isFinite(v)) return v;
|
||||
return defaultStakeAmount();
|
||||
}
|
||||
|
||||
function setItemStake(selectionId: string, amount: number) {
|
||||
singleStakes.value = { ...singleStakes.value, [selectionId]: amount };
|
||||
}
|
||||
|
||||
function clearItemStake(selectionId: string) {
|
||||
if (!(selectionId in singleStakes.value)) return;
|
||||
const next = { ...singleStakes.value };
|
||||
delete next[selectionId];
|
||||
singleStakes.value = next;
|
||||
}
|
||||
|
||||
function clearAllItemStakes() {
|
||||
singleStakes.value = {};
|
||||
}
|
||||
|
||||
function parseLineValue(v: number | string | null | undefined): number | null {
|
||||
if (v == null || v === '') return null;
|
||||
const n = typeof v === 'number' ? v : parseFloat(String(v));
|
||||
@@ -60,6 +243,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
function setSingleItem(item: SlipItem) {
|
||||
mode.value = 'single';
|
||||
singleItem.value = item;
|
||||
initItemStake(item.selectionId);
|
||||
lastParlayError.value = null;
|
||||
}
|
||||
|
||||
@@ -67,6 +251,23 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
setSingleItem(item);
|
||||
}
|
||||
|
||||
/** PC 单关购买列表 */
|
||||
function addToSingleCart(item: SlipItem) {
|
||||
mode.value = 'single';
|
||||
if (singleCartItems.value.some((i) => i.selectionId === item.selectionId)) return;
|
||||
singleCartItems.value.unshift(item);
|
||||
initItemStake(item.selectionId);
|
||||
lastParlayError.value = null;
|
||||
}
|
||||
|
||||
function isInSlip(selectionId: string) {
|
||||
return (
|
||||
singleItem.value?.selectionId === selectionId ||
|
||||
singleCartItems.value.some((i) => i.selectionId === selectionId) ||
|
||||
parlayItems.value.some((i) => i.selectionId === selectionId)
|
||||
);
|
||||
}
|
||||
|
||||
/** 串关:必须来自不同赛事,仍按盘口规则过滤不可串关项 */
|
||||
function addParlayLeg(item: SlipItem): ParlaySlipError | null {
|
||||
mode.value = 'parlay';
|
||||
@@ -97,7 +298,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
return 'MAX_LEGS';
|
||||
}
|
||||
|
||||
parlayItems.value.push(item);
|
||||
parlayItems.value.unshift(item);
|
||||
lastParlayError.value = null;
|
||||
return null;
|
||||
}
|
||||
@@ -111,13 +312,19 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
if (singleItem.value?.selectionId === selectionId) {
|
||||
singleItem.value = null;
|
||||
}
|
||||
singleCartItems.value = singleCartItems.value.filter((i) => i.selectionId !== selectionId);
|
||||
parlayItems.value = parlayItems.value.filter((i) => i.selectionId !== selectionId);
|
||||
clearItemStake(selectionId);
|
||||
if (mode.value === 'parlay' && !parlayItems.value.length) mode.value = 'single';
|
||||
lastParlayError.value = null;
|
||||
}
|
||||
|
||||
function clearSingle() {
|
||||
const legacy = singleItem.value;
|
||||
for (const item of singleCartItems.value) clearItemStake(item.selectionId);
|
||||
if (legacy) clearItemStake(legacy.selectionId);
|
||||
singleItem.value = null;
|
||||
singleCartItems.value = [];
|
||||
if (mode.value === 'single') lastParlayError.value = null;
|
||||
}
|
||||
|
||||
@@ -137,9 +344,12 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
|
||||
function clearAll() {
|
||||
singleItem.value = null;
|
||||
singleCartItems.value = [];
|
||||
parlayItems.value = [];
|
||||
clearAllItemStakes();
|
||||
mode.value = 'single';
|
||||
lastParlayError.value = null;
|
||||
removePersisted(auth.user?.id ?? null);
|
||||
}
|
||||
|
||||
const totalOdds = computed(() =>
|
||||
@@ -151,9 +361,16 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
if (mode.value === 'parlay' && canPlaceParlay.value) {
|
||||
return stake.value * totalOdds.value;
|
||||
}
|
||||
return items.value.reduce((acc, i) => acc + stake.value * i.odds, 0);
|
||||
return items.value.reduce(
|
||||
(acc, i) => acc + getItemStake(i.selectionId) * i.odds,
|
||||
0,
|
||||
);
|
||||
});
|
||||
|
||||
const totalSingleStake = computed(() =>
|
||||
items.value.reduce((acc, i) => acc + getItemStake(i.selectionId), 0),
|
||||
);
|
||||
|
||||
const canPlaceParlay = computed(
|
||||
() =>
|
||||
mode.value === 'parlay' &&
|
||||
@@ -162,7 +379,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
);
|
||||
|
||||
const canPlaceBatchSingles = computed(
|
||||
() => mode.value === 'single' && singleItem.value != null,
|
||||
() => mode.value === 'single' && (singleCartItems.value.length > 0 || singleItem.value != null),
|
||||
);
|
||||
|
||||
const canSubmit = computed(
|
||||
@@ -170,6 +387,11 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
);
|
||||
|
||||
const drawerOpen = ref(false);
|
||||
const panelExpanded = ref(readDesktopPanelExpanded());
|
||||
|
||||
watch(panelExpanded, (expanded) => {
|
||||
writeDesktopPanelExpanded(expanded);
|
||||
});
|
||||
|
||||
function openDrawer() {
|
||||
drawerOpen.value = true;
|
||||
@@ -179,10 +401,26 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
drawerOpen.value = false;
|
||||
}
|
||||
|
||||
function expandPanel() {
|
||||
panelExpanded.value = true;
|
||||
}
|
||||
|
||||
function collapsePanel() {
|
||||
panelExpanded.value = false;
|
||||
}
|
||||
|
||||
function togglePanelExpanded() {
|
||||
panelExpanded.value = !panelExpanded.value;
|
||||
}
|
||||
|
||||
function updateSelectionOdds(selectionId: string, odds: number, oddsVersion: string) {
|
||||
if (singleItem.value?.selectionId === selectionId) {
|
||||
singleItem.value = { ...singleItem.value, odds, oddsVersion };
|
||||
}
|
||||
const cartIdx = singleCartItems.value.findIndex((i) => i.selectionId === selectionId);
|
||||
if (cartIdx >= 0) {
|
||||
singleCartItems.value[cartIdx] = { ...singleCartItems.value[cartIdx], odds, oddsVersion };
|
||||
}
|
||||
const idx = parlayItems.value.findIndex((i) => i.selectionId === selectionId);
|
||||
if (idx >= 0) {
|
||||
parlayItems.value[idx] = { ...parlayItems.value[idx], odds, oddsVersion };
|
||||
@@ -191,7 +429,9 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
|
||||
return {
|
||||
singleItem,
|
||||
singleCartItems,
|
||||
parlayItems,
|
||||
singleStakes,
|
||||
items,
|
||||
stake,
|
||||
mode,
|
||||
@@ -202,14 +442,18 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
isParlay,
|
||||
totalOdds,
|
||||
potentialReturn,
|
||||
totalSingleStake,
|
||||
canPlaceParlay,
|
||||
canPlaceBatchSingles,
|
||||
canSubmit,
|
||||
lastParlayError,
|
||||
drawerOpen,
|
||||
panelExpanded,
|
||||
setMode,
|
||||
setSingleItem,
|
||||
addItem,
|
||||
addToSingleCart,
|
||||
isInSlip,
|
||||
addParlayLeg,
|
||||
addSingleToParlay,
|
||||
removeItem,
|
||||
@@ -219,6 +463,12 @@ export const useBetSlipStore = defineStore('betSlip', () => {
|
||||
clearAll,
|
||||
openDrawer,
|
||||
closeDrawer,
|
||||
expandPanel,
|
||||
collapsePanel,
|
||||
togglePanelExpanded,
|
||||
updateSelectionOdds,
|
||||
getItemStake,
|
||||
setItemStake,
|
||||
initItemStake,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user