feat(i18n): 管理端与玩家端三语支持(中/英/马来语)

- 管理后台 adminT 文案库、结算与代理端页面、表单校验
- 玩家端 vue-i18n 补全首页/公告/串关与 ms 文案
- Element Plus ms 语言包与共享 locale 工具
This commit is contained in:
2026-06-03 15:05:36 +08:00
parent 80adc0e928
commit cbfa18d1d3
63 changed files with 3081 additions and 1038 deletions

View File

@@ -4,7 +4,9 @@ import api from '../api';
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('token') || '');
const user = ref(JSON.parse(localStorage.getItem('user') || 'null'));
const user = ref<{ id?: string; username?: string; locale?: string } | null>(
JSON.parse(localStorage.getItem('user') || 'null'),
);
async function login(username: string, password: string) {
const { data } = await api.post('/player/auth/login', { username, password });

View File

@@ -1,5 +1,11 @@
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;
@@ -9,20 +15,30 @@ export interface SlipItem {
selectionName: string;
odds: number;
marketType: string;
lineValue?: number | null;
allowParlay?: boolean;
}
export const useBetSlipStore = defineStore('betSlip', () => {
const items = ref<SlipItem[]>([]);
const stake = ref<number>(10);
const mode = ref<'single' | 'parlay'>('single');
const lastParlayError = ref<ParlayRejectReason | 'MAX_LEGS' | null>(null);
const count = computed(() => items.value.length);
const isParlay = computed(() => mode.value === 'parlay' && items.value.length >= 2);
const isParlay = computed(() => mode.value === 'parlay' && items.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;
}
/** 球赛/详情页:仅单关,且投注单同时只能有 1 项 */
function addItem(item: SlipItem) {
if (mode.value === 'parlay') items.value = [];
mode.value = 'single';
lastParlayError.value = null;
const existing = items.value.findIndex(
(i) => i.selectionId === item.selectionId,
@@ -35,27 +51,48 @@ export const useBetSlipStore = defineStore('betSlip', () => {
}
/** 串关页专用:跨场组合,同场只保留一个选项 */
function addParlayLeg(item: SlipItem) {
function addParlayLeg(item: SlipItem): ParlayRejectReason | 'MAX_LEGS' | null {
if (mode.value === 'single') items.value = [];
mode.value = 'parlay';
const samePick = items.value.findIndex((i) => i.selectionId === item.selectionId);
if (samePick >= 0) {
items.value.splice(samePick, 1);
return;
lastParlayError.value = null;
return null;
}
const check = canSelectForParlay({
marketType: item.marketType,
lineValue: parseLineValue(item.lineValue),
allowParlay: item.allowParlay,
});
if (!check.ok) {
lastParlayError.value = check.reason;
return check.reason;
}
if (items.value.length >= PARLAY_MAX_LEGS) {
lastParlayError.value = 'MAX_LEGS';
return 'MAX_LEGS';
}
items.value = items.value.filter((i) => i.matchId !== item.matchId);
items.value.push(item);
lastParlayError.value = null;
return null;
}
function removeItem(selectionId: string) {
items.value = items.value.filter((i) => i.selectionId !== selectionId);
if (!items.value.length) mode.value = 'single';
lastParlayError.value = null;
}
function clear() {
items.value = [];
mode.value = 'single';
lastParlayError.value = null;
}
const totalOdds = computed(() =>
@@ -64,7 +101,7 @@ export const useBetSlipStore = defineStore('betSlip', () => {
const potentialReturn = computed(() => {
if (!items.value.length) return 0;
if (mode.value === 'parlay' && items.value.length >= 2) {
if (mode.value === 'parlay' && items.value.length >= PARLAY_MIN_LEGS) {
return stake.value * totalOdds.value;
}
return stake.value * items.value[0].odds;
@@ -75,6 +112,14 @@ export const useBetSlipStore = defineStore('betSlip', () => {
return new Set(matchIds).size !== matchIds.length;
});
const canPlaceParlay = computed(
() =>
mode.value === 'parlay' &&
items.value.length >= PARLAY_MIN_LEGS &&
items.value.length <= PARLAY_MAX_LEGS &&
!hasSameMatch.value,
);
const drawerOpen = ref(false);
function openDrawer() {
@@ -94,6 +139,8 @@ export const useBetSlipStore = defineStore('betSlip', () => {
totalOdds,
potentialReturn,
hasSameMatch,
canPlaceParlay,
lastParlayError,
drawerOpen,
addItem,
addParlayLeg,