feat: 管理端 RBAC 权限体系与员工管理

新增多角色权限控制(赛事/财务/客服管理员),支持员工 CRUD、路由菜单按权限显隐、审计日志范围过滤;登录返回角色与权限列表。玩家端赛事列表增加静默刷新避免图片闪烁。Seed 补充演示员工账号与充值相关权限。附带 RBAC/审计范围单元测试及 UAT 文档更新。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-15 17:52:39 +08:00
parent be5b4a4921
commit 567ec9ec8a
44 changed files with 1717 additions and 125 deletions

View File

@@ -4,6 +4,8 @@ import { RouterView, RouterLink, useRoute, useRouter } from 'vue-router';
import { useAuthStore } from '../stores/auth';
import { useAdminLocale } from '../composables/useAdminLocale';
import { useSmokeTestsAllowed } from '../composables/useSmokeTestsAllowed';
import { usePermissions } from '../composables/usePermissions';
import { AdminPerm } from '../constants/permissions';
import AdminLocaleSwitcher from '../components/AdminLocaleSwitcher.vue';
import AdminNavIcon from '../components/AdminNavIcon.vue';
import { resolveAdminBreadcrumb } from '../utils/admin-breadcrumb';
@@ -13,28 +15,44 @@ const router = useRouter();
const auth = useAuthStore();
const { t } = useAdminLocale();
const { allowed: smokeTestsAllowed, ensureLoaded: ensureSmokeTestsAllowed } = useSmokeTestsAllowed();
const { hasPermission, role: adminRole } = usePermissions();
const sidebarOpen = ref(false);
const isMobileNav = ref(false);
type AdminMenuItem = {
path: string;
label: string;
icon: string;
matchPrefix?: boolean;
permissions: string[];
/** Hide for these admin role codes (SUPER_ADMIN is never excluded). */
excludeRoles?: string[];
};
function menuVisible(item: AdminMenuItem): boolean {
const code = adminRole.value;
if (item.path === '/smoke-tests' && smokeTestsAllowed.value === false) return false;
if (code && code !== 'SUPER_ADMIN' && item.excludeRoles?.includes(code)) return false;
return hasPermission(...item.permissions);
}
const adminMenus = computed(() => {
const items = [
{ path: '/', label: t('nav.dashboard'), icon: 'dashboard', matchPrefix: true },
{ path: '/matches', label: t('nav.matches'), icon: 'matches', matchPrefix: true },
{ path: '/users', label: t('nav.agents_players'), icon: 'users' },
{ path: '/finance-logs', label: t('nav.finance_logs'), icon: 'finance' },
{ path: '/deposit', label: t('nav.deposit_manage'), icon: 'deposit', matchPrefix: true },
{ path: '/cashback', label: t('nav.cashback'), icon: 'cashback' },
{ path: '/bets', label: t('nav.bets'), icon: 'bets' },
{ path: '/contents', label: t('nav.contents'), icon: 'contents' },
{ path: '/media', label: t('nav.media'), icon: 'media' },
{ path: '/audit', label: t('nav.audit'), icon: 'audit' },
{ path: '/smoke-tests', label: t('nav.smoke_tests'), icon: 'smoke-tests' },
const items: AdminMenuItem[] = [
{ path: '/', label: t('nav.dashboard'), icon: 'dashboard', matchPrefix: true, permissions: [AdminPerm.reports], excludeRoles: ['SUPPORT'] },
{ path: '/matches', label: t('nav.matches'), icon: 'matches', matchPrefix: true, permissions: [AdminPerm.matches] },
{ path: '/users', label: t('nav.agents_players'), icon: 'users', permissions: [AdminPerm.usersView, AdminPerm.agentsView] },
{ path: '/finance-logs', label: t('nav.finance_logs'), icon: 'finance', permissions: [AdminPerm.reports], excludeRoles: ['MATCH_ADMIN'] },
{ path: '/deposit', label: t('nav.deposit_manage'), icon: 'deposit', matchPrefix: true, permissions: [AdminPerm.depositManage, AdminPerm.depositReview] },
{ path: '/cashback', label: t('nav.cashback'), icon: 'cashback', permissions: [AdminPerm.cashback], excludeRoles: ['MATCH_ADMIN', 'SUPPORT'] },
{ path: '/bets', label: t('nav.bets'), icon: 'bets', permissions: [AdminPerm.bets] },
{ path: '/contents', label: t('nav.contents'), icon: 'contents', permissions: [AdminPerm.content] },
{ path: '/media', label: t('nav.media'), icon: 'media', permissions: [AdminPerm.content, AdminPerm.matches] },
{ path: '/audit', label: t('nav.audit'), icon: 'audit', permissions: [AdminPerm.audit] },
{ path: '/staff', label: t('nav.staff'), icon: 'users', permissions: [AdminPerm.settings] },
{ path: '/smoke-tests', label: t('nav.smoke_tests'), icon: 'smoke-tests', permissions: [AdminPerm.settings] },
];
if (smokeTestsAllowed.value === false) {
return items.filter((item) => item.path !== '/smoke-tests');
}
return items;
return items.filter(menuVisible);
});
const agentMenus = computed(() => [
@@ -78,7 +96,14 @@ const currentLabel = computed(() => {
const topbarCrumbs = computed(() => resolveAdminBreadcrumb(route.path, t));
const roleLabel = computed(() => {
if (auth.isAdmin.value) return t('role.admin');
if (auth.isAdmin.value) {
const code = adminRole.value;
if (code === 'MATCH_ADMIN') return t('role.match_admin');
if (code === 'FINANCE_ADMIN') return t('role.finance_admin');
if (code === 'SUPPORT') return t('role.support');
if (code === 'SUPER_ADMIN') return t('role.super_admin');
return t('role.admin');
}
const level = auth.user.value?.agentLevel;
if (auth.isAgent.value && level != null && level > 0) {
return t('role.agent_level', { n: level });