- 阶段 A: 增加 KeepAlive 缓存高频列表页,改用 onActivated 进行后台静默刷新,优化 tab 切换与 HomeEntry 闪屏 - 阶段 B: beforeEach 守卫在 TTL 内走同步快速路径,api 请求拦截器缓存 token 避免重复解析 JWT - 阶段 C: 引入 unplugin 插件启用 Element Plus 组件按需加载,清理 App.vue 中 960+ 行冗余暗色主题 CSS - i18n 拆包: 将三语文案包提取为独立懒加载 chunks,主包体积从 209KB 减少至 99KB (降低 53%)
259 lines
8.2 KiB
TypeScript
259 lines
8.2 KiB
TypeScript
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;
|