Files
thebet365/apps/admin/src/router/index.ts
Mars cffad00652 sync(theme-4): 同步 main 最新 API/Admin 与玩家端逻辑
从 main 同步站内信手动发送、媒体库选择、列表子路由重构等 API/Admin 改动;玩家端仅合并 ADMIN_CUSTOM 富文本、消息预览与充值截图压缩逻辑,保留 theme-4 样式。
2026-06-22 14:12:40 +08:00

304 lines
9.7 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 { hydrateStaffSession } 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] },
children: [
{
path: 'agents/:agentId/players',
name: 'admin-agent-direct-players',
component: () => import('../views/agent/AgentDirectPlayersView.vue'),
},
{
path: 'settings',
name: 'admin-global-settings',
component: () => import('../views/agent/GlobalSettingsView.vue'),
},
],
},
{
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] },
children: [
{
path: 'leagues/:leagueId',
name: 'admin-league-matches',
component: () => import('../views/matches/LeagueMatchesPage.vue'),
},
],
},
{
path: 'matches/outrights',
component: () => import('../views/MatchesOutrights.vue'),
meta: { adminOnly: true, permissions: [AdminPerm.matches] },
children: [
{
path: 'leagues/:leagueId',
name: 'admin-league-outrights',
component: () => import('../views/matches/LeagueOutrightsPage.vue'),
},
],
},
{
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) {
reconcileStaffSessionFromToken();
// 后台刷新 session不阻塞路由切换401 由 api 拦截器处理
void hydrateStaffSession();
}
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 { allowed } = useSmokeTestsAllowed();
if (allowed.value === false) return '/';
}
return true;
});
/** 发版后旧 index.html 可能引用已不存在的 hash chunk自动刷新一次拉最新入口。 */
function isLazyChunkLoadError(err: unknown): boolean {
const msg = err instanceof Error ? err.message : String(err ?? '');
return (
msg.includes('Failed to fetch dynamically imported module') ||
msg.includes('Importing a module script failed') ||
msg.includes('error loading dynamically imported module')
);
}
const CHUNK_RELOAD_FLAG = 'admin:chunk-reload';
router.onError((error, to) => {
if (!isLazyChunkLoadError(error)) throw error;
const target = to.fullPath || '/';
if (!sessionStorage.getItem(CHUNK_RELOAD_FLAG)) {
sessionStorage.setItem(CHUNK_RELOAD_FLAG, target);
window.location.assign(target);
return;
}
sessionStorage.removeItem(CHUNK_RELOAD_FLAG);
throw error;
});
export default router;