feat(player): 注册账号、登录双模式与移动端性能优化

注册必填 7-32 位账号,手机号区号/本地号分存;登录默认账号模式并支持切换手机号登录;Player i18n 拆包与赛事接口优化。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-12 10:56:51 +08:00
parent 83f0f380c5
commit 312c3c5816
35 changed files with 1944 additions and 1394 deletions

View File

@@ -1,8 +1,8 @@
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import api from '../../api';
import { useBetSlipStore } from '../../stores/betSlip';
import { usePlayerMatches, type ParlayMatch } from '../../composables/usePlayerMatches';
import { useAuthStore } from '../../stores/auth';
import { PARLAY_MAX_LEGS, canSelectForParlay } from '@thebet365/shared';
import { PARLAY_MARKET_TYPES, PARLAY_SELECTION_KEYS, PARLAY_MARKET_GROUPS } from '../../utils/parlayColumns';
@@ -33,28 +33,6 @@ interface Market {
selections: Selection[];
}
interface ParlayMatch {
id: string;
leagueName: string;
leagueId?: string;
homeTeamName: string;
awayTeamName: string;
homeTeamCode?: string;
awayTeamCode?: string;
homeTeamLogoUrl?: string | null;
awayTeamLogoUrl?: string | null;
startTime: string;
bettingOpen?: boolean;
matchPhase?: MatchPhase;
score?: {
htHome: number;
htAway: number;
ftHome: number;
ftAway: number;
} | null;
markets: Market[];
}
const { t, locale } = useI18n();
const slip = useBetSlipStore();
const auth = useAuthStore();
@@ -63,8 +41,9 @@ function goLogin() {
auth.showLoginPrompt('/bet');
}
const loading = ref(true);
const matches = ref<ParlayMatch[]>([]);
const { parlayMatches, parlayLoading, loadParlay } = usePlayerMatches();
const matches = parlayMatches;
const loading = parlayLoading;
const timeFilter = ref<TimeFilter>('all');
const leagueFilter = ref('');
const showClosed = ref(false);
@@ -72,25 +51,6 @@ const collapsed = ref<Set<string>>(new Set());
const parlayMarketKeys = PARLAY_MARKET_TYPES.map((c) => c.key);
async function loadParlayMatches() {
const hadData = matches.value.length > 0;
if (!hadData) loading.value = true;
try {
const { data } = await api.get('/player/matches');
const fresh = (data.data ?? []).filter(
(m: ParlayMatch) => m.markets?.length && hasParlayMarkets(m),
);
if (!hadData) {
matches.value = fresh;
syncCollapsedAfterLoad();
} else {
mergeOddsOnly(fresh);
}
} finally {
if (!hadData) loading.value = false;
}
}
function syncCollapsedAfterLoad() {
const ids = matches.value.map((m) => m.id);
// 只保留仍然存在的 id
@@ -107,32 +67,10 @@ function syncCollapsedAfterLoad() {
}
}
function mergeOddsOnly(fresh: ParlayMatch[]) {
const matchMap = new Map<string, ParlayMatch>();
for (const m of fresh) matchMap.set(m.id, m);
for (const match of matches.value) {
const freshMatch = matchMap.get(match.id);
if (!freshMatch) continue;
const marketMap = new Map<string, Market>();
for (const mk of freshMatch.markets) marketMap.set(mk.id, mk);
for (const market of match.markets) {
const freshMarket = marketMap.get(market.id);
if (!freshMarket) continue;
const selMap = new Map<string, Selection>();
for (const s of freshMarket.selections) selMap.set(s.id, s);
for (const sel of market.selections) {
const fs = selMap.get(sel.id);
if (fs) {
sel.odds = fs.odds;
sel.oddsVersion = fs.oddsVersion;
}
}
}
}
}
useOnLocaleChange(loadParlayMatches);
useOnLocaleChange(async () => {
await loadParlay(true);
syncCollapsedAfterLoad();
});
const leagues = computed(() => {
const seen = new Set<string>();
@@ -583,7 +521,7 @@ function toggleCollapse(id: string) {
content: '';
position: absolute;
inset: 0;
background: v-bind(matchCardBg) center top / 100% auto no-repeat;
background: v-bind(matchCardBg) center / 100% 100% no-repeat;
opacity: 0.25;
z-index: -1;
pointer-events: none;
@@ -600,6 +538,13 @@ function toggleCollapse(id: string) {
min-width: 0;
}
.match-head-teams {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
}
.toggle-icon {
display: flex;
align-items: center;