Files
thebet365/apps/admin/src/router/index.ts
Mars 2487724692 perf(admin): 优化管理端页面切换性能与包体积 (同步自 main)
- 阶段 A: 增加 KeepAlive 缓存高频列表页,改用 onActivated 进行后台静默刷新,优化 tab 切换与 HomeEntry 闪屏

- 阶段 B: beforeEach 守卫在 TTL 内走同步快速路径,api 请求拦截器缓存 token 避免重复解析 JWT

- 阶段 C: 引入 unplugin 插件启用 Element Plus 组件按需加载,清理 App.vue 中 960+ 行冗余暗色主题 CSS

- i18n 拆包: 将三语文案包提取为独立懒加载 chunks,主包体积从 209KB 减少至 99KB (降低 53%)
2026-06-18 15:31:57 +08:00

259 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { createRouter, createWebHistory } from 'vue-router';
import { useAuthStore } from '../stores/auth';
import { useSmokeTestsAllowed } from '../composables/useSmokeTestsAllowed';
import { ensureStaffSession, isSessionFresh } from '../utils/session-hydrate';
import { reconcileStaffSessionFromToken } from '../stores/auth';
import { AdminPerm } from '../constants/permissions';
import { adminCanAccess, firstAdminFallback } from '../utils/admin-access';
/** Paths denied for specific admin roles (SUPER_ADMIN bypasses). */
const ROLE_ROUTE_DENY: Record<string, string[]> = {
'/finance-logs': ['MATCH_ADMIN'],
'/cashback': ['MATCH_ADMIN', 'SUPPORT'],
};
function pathDeniedForRole(path: string, role?: string): boolean {
if (!role || role === 'SUPER_ADMIN') return false;
const base = path.split('?')[0];
for (const [prefix, roles] of Object.entries(ROLE_ROUTE_DENY)) {
if ((base === prefix || base.startsWith(`${prefix}/`)) && roles.includes(role)) {
return true;
}
}
return false;
}
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/login', component: () => import('../views/Login.vue'), meta: { public: true } },
{
path: '/',
component: () => import('../layouts/ManageLayout.vue'),
meta: { auth: true },
children: [
{
path: '',
component: () => import('../views/HomeEntry.vue'),
meta: { permissions: [AdminPerm.reports] },
children: [
{
path: '',
component: () => import('../views/dashboard/DashboardMatches.vue'),
},
{
path: 'dashboard/players',
component: () => import('../views/dashboard/DashboardPlayers.vue'),
},
],
},
{
path: 'invites',
redirect: '/users',
},
{
path: 'users',
component: () => import('../views/AgentManager.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.usersView, AdminPerm.agentsView] },
},
{
path: 'finance-logs',
component: () => import('../views/FinanceLogs.vue'),
meta: { permissions: [AdminPerm.reports] },
},
{
path: 'agent-credit-transactions',
redirect: (to) => ({
path: '/finance-logs',
query: { ...to.query, tab: 'credit' },
}),
},
{
path: 'agents',
redirect: '/users',
},
{
path: 'matches',
component: () => import('../views/Matches.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
},
{
path: 'matches/outrights',
component: () => import('../views/MatchesOutrights.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
},
{
path: 'matches/market-templates',
component: () => import('../views/MarketTemplates.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
},
{
path: 'matches/audit-logs',
component: () => import('../views/MatchesAudit.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
},
{
path: 'matches/:matchId/edit',
name: 'admin-match-edit',
component: () => import('../views/matches/MatchEventEditor.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
},
{
path: 'matches/:matchId/markets',
name: 'admin-match-markets',
component: () => import('../views/matches/MatchMarketsPage.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
},
{ path: 'outrights', redirect: '/matches/outrights' },
{
path: 'outrights/:matchId/edit',
name: 'admin-outright-edit',
component: () => import('../views/outrights/OutrightEditRedirect.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
},
{ path: 'world-cup-outright', redirect: '/matches/outrights' },
{
path: 'bets',
component: () => import('../views/Bets.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.bets] },
},
{
path: 'settlement/:id',
component: () => import('../views/Settlement.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.settlement, AdminPerm.matches] },
},
{
path: 'cashback',
component: () => import('../views/Cashback.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.cashback] },
},
{
path: 'contents',
component: () => import('../views/Contents.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.content] },
},
{
path: 'audit',
component: () => import('../views/Audit.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.audit] },
},
{
path: 'smoke-tests',
component: () => import('../views/SmokeTests.vue'),
meta: { adminOnly: true, smokeTestsOnly: true, permissions: [AdminPerm.settings] },
},
{
path: 'media',
component: () => import('../views/MediaLibrary.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.content, AdminPerm.matches] },
},
{
path: 'payment-methods',
redirect: (to) => ({
path: '/deposit',
query: { ...to.query, tab: 'methods' },
}),
},
{
path: 'deposit-orders',
redirect: '/deposit',
},
{
path: 'deposit',
component: () => import('../views/DepositManage.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.depositManage, AdminPerm.depositReview] },
},
{
path: 'staff',
component: () => import('../views/StaffManage.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.settings] },
},
{
path: 'my-players',
component: () => import('../views/agent/Players.vue'),
meta: { agentOnly: true },
},
{
path: 'sub-agents',
redirect: '/my-players',
},
{
path: 'my-bets',
component: () => import('../views/agent/Bets.vue'),
meta: { agentOnly: true },
},
],
},
],
});
router.beforeEach(async (to) => {
const auth = useAuthStore();
const hasToken = !!auth.token.value;
if (hasToken) {
if (isSessionFresh()) {
// Session 在 TTL 内且完整:只做同步 reconcile跳过网络请求
reconcileStaffSessionFromToken();
} else {
// Session 过期或不完整:才 await 远程刷新
await ensureStaffSession();
}
}
const hasUser = !!auth.user.value?.userType;
if (to.meta.public) {
if (hasToken && hasUser) return '/';
return true;
}
if (!hasToken || !hasUser) {
auth.clearStaffSession();
return { path: '/login', query: { redirect: to.fullPath } };
}
if (to.meta.adminOnly && !auth.isAdmin.value) {
return '/';
}
if (auth.isAdmin.value && to.meta.permissions) {
const required = to.meta.permissions as string[];
const role = auth.user.value?.role;
const permissions = auth.user.value?.permissions;
if (pathDeniedForRole(to.path, role)) {
const fallback = firstAdminFallback(role, permissions);
if (fallback && to.path !== fallback) return fallback;
}
if (!adminCanAccess(role, permissions, required)) {
const fallback = firstAdminFallback(role, permissions);
if (!fallback) {
return true;
}
if (to.path !== fallback) return fallback;
}
}
if (to.path.startsWith('/dashboard/') && !auth.isAdmin.value) {
return '/';
}
if (to.meta.agentOnly && !auth.isAgent.value) {
return '/';
}
if (to.meta.tier1AgentOnly && !auth.isTier1Agent.value) {
return '/';
}
if (to.meta.smokeTestsOnly) {
const { ensureLoaded, allowed } = useSmokeTestsAllowed();
await ensureLoaded();
if (!allowed.value) return '/';
}
return true;
});
export default router;