feat(player): 新增桌面端 UI 与响应式双端路由架构

- 将原移动端页面重命名为 Mobile*,新增 22 个 Desktop* 视图与 DesktopShell 布局体系
- 路由入口改为 Wrapper 视图,通过 useViewport(≥1024px)自动切换移动/桌面端
- 新增投注单面板、盘口弹层、联赛侧栏、数据表格等桌面组件及独立样式令牌
- 扩展 betSlip store、串关筛选、冠军盘、Toast/悬浮信箱等交互与三语 i18n
- 公告/消息 Hub 拆分移动与桌面实现;API 投注/钱包/充值记录支持 pageSize 分页
This commit is contained in:
2026-06-29 18:01:39 +08:00
parent 9c5e8d6f5c
commit 8a1ba0fd95
116 changed files with 22249 additions and 8693 deletions

View File

@@ -0,0 +1,131 @@
<script setup lang="ts">
import { computed, onMounted, watch } from 'vue';
import { useRoute, RouterView } from 'vue-router';
import { useI18n } from 'vue-i18n';
import DesktopTopNav from './DesktopTopNav.vue';
import DesktopOddsBetPopover from './DesktopOddsBetPopover.vue';
import SportsCategoryBar from './SportsCategoryBar.vue';
import LeagueSidebar from './LeagueSidebar.vue';
import MatchSidebar from './MatchSidebar.vue';
import BetSlipPanel from './BetSlipPanel.vue';
import FloatingMailbox from '../FloatingMailbox.vue';
import { usePlayerHome } from '../../composables/usePlayerHome';
import { useAuthStore } from '../../stores/auth';
const route = useRoute();
const { locale } = useI18n();
const auth = useAuthStore();
const { load: loadPlayerHome } = usePlayerHome();
const layoutMode = computed<'betting' | 'account' | 'hub' | 'marketing'>(() => {
const p = route.path;
if (p === '/bet' || p.startsWith('/match/') || p.startsWith('/outright/')) {
return 'betting';
}
if (
p === '/bets' ||
p.startsWith('/bets/') ||
p.startsWith('/wallet') ||
p === '/profile/cashbacks'
) {
return 'account';
}
if (p.startsWith('/announcements')) {
return 'hub';
}
if (p.startsWith('/messages')) {
return 'hub';
}
return 'marketing';
});
const isMatchDetail = computed(() => route.path.startsWith('/match/'));
const isBettingDetail = computed(
() => route.path.startsWith('/match/') || route.path.startsWith('/outright/'),
);
onMounted(() => {
void loadPlayerHome(true);
});
watch(locale, (next, prev) => {
if (prev && next !== prev) void loadPlayerHome(true);
});
watch(
() => auth.token,
() => {
void loadPlayerHome(true);
},
);
</script>
<template>
<div class="desktop-shell">
<header class="desktop-header-wrap">
<DesktopTopNav />
</header>
<div class="desktop-main-container">
<!-- Betting Layout -->
<div v-if="layoutMode === 'betting'" class="desktop-layout-betting-wrap">
<div class="desktop-betting-sports-row">
<SportsCategoryBar />
</div>
<div class="desktop-layout-betting">
<aside class="desktop-betting-left">
<LeagueSidebar v-if="!isMatchDetail" />
<MatchSidebar v-else />
</aside>
<main class="desktop-betting-center" :class="{ 'is-betting-detail': isBettingDetail }">
<RouterView v-slot="{ Component, route: viewRoute }">
<KeepAlive v-if="viewRoute.meta.keepAlive" :max="10">
<component :is="Component" :key="viewRoute.path" />
</KeepAlive>
<component v-else :is="Component" :key="viewRoute.fullPath" />
</RouterView>
</main>
<aside class="desktop-betting-right">
<BetSlipPanel />
</aside>
</div>
</div>
<!-- Account Layout -->
<div v-else-if="layoutMode === 'account'" class="desktop-layout-account">
<main class="desktop-account-center">
<RouterView v-slot="{ Component, route: viewRoute }">
<KeepAlive v-if="viewRoute.meta.keepAlive" :max="10">
<component :is="Component" :key="viewRoute.path" />
</KeepAlive>
<component v-else :is="Component" :key="viewRoute.fullPath" />
</RouterView>
</main>
</div>
<!-- Hub Layout: Messages / Announcements split pane -->
<div v-else-if="layoutMode === 'hub'" class="desktop-layout-hub-outer">
<RouterView v-slot="{ Component, route: viewRoute }">
<KeepAlive v-if="viewRoute.meta.keepAlive" :max="10">
<component :is="Component" :key="viewRoute.path" />
</KeepAlive>
<component v-else :is="Component" :key="viewRoute.fullPath" />
</RouterView>
</div>
<!-- Marketing/Single Column Layout -->
<div v-else class="desktop-layout-marketing">
<main class="desktop-marketing-content">
<RouterView v-slot="{ Component, route: viewRoute }">
<KeepAlive v-if="viewRoute.meta.keepAlive" :max="10">
<component :is="Component" :key="viewRoute.path" />
</KeepAlive>
<component v-else :is="Component" :key="viewRoute.fullPath" />
</RouterView>
</main>
</div>
</div>
<FloatingMailbox />
<DesktopOddsBetPopover />
</div>
</template>