feat: 统一管理端导航为后端下发菜单,移除本地权限过滤

This commit is contained in:
2026-05-19 09:34:52 +08:00
parent 1b1dfc92ab
commit d625c59393
8 changed files with 57 additions and 179 deletions

View File

@@ -1,152 +1,25 @@
/**
* Single source of truth for admin navigation and routes.
*
* `requiredAny` matches `admin.permissions` from the login response (Laravel `prd.*`).
* When omitted, the item is visible to any signed-in admin.
*/
export const ADMIN_BASE = "/admin" as const;
export type AdminNavSegment =
| "dashboard"
| "players"
| "draws"
| "config"
| "tickets"
| "wallet"
| "risk"
| "settings"
| "settlement"
| "jackpot"
| "reports"
| "reconcile"
| "audit"
| "admin_users";
export type AdminNavItem = {
label: string;
href: string;
segment:
| "dashboard"
| "players"
| "draws"
| "config"
| "tickets"
| "wallet"
| "risk"
| "settings"
| "settlement"
| "jackpot"
| "reports"
| "reconcile"
| "audit"
| "admin_users";
segment: AdminNavSegment;
activeMatchPrefix?: string;
/** Show the nav item when the user has any of these permission slugs. */
requiredAny?: readonly string[];
};
export const adminShellNavItems: AdminNavItem[] = [
{ segment: "dashboard", label: "Dashboard", href: "/admin" },
{
segment: "admin_users",
label: "Admin Users",
href: "/admin/admin-users",
requiredAny: ["prd.admin_user.manage"],
},
{
segment: "players",
label: "Players",
href: "/admin/players",
requiredAny: [
"prd.users.manage",
"prd.users.view_finance",
"prd.users.view_cs",
],
},
{
segment: "wallet",
label: "Wallet",
href: "/admin/wallet/transactions",
activeMatchPrefix: "/admin/wallet",
requiredAny: [
"prd.wallet_reconcile.manage",
"prd.wallet_reconcile.view",
"prd.wallet_reconcile.view_cs",
"prd.users.manage",
"prd.users.view_finance",
"prd.users.view_cs",
],
},
{
segment: "draws",
label: "Draws",
href: "/admin/draws",
requiredAny: ["prd.draw_result.manage", "prd.draw_result.view"],
},
{
segment: "config",
label: "Configuration",
href: "/admin/config",
requiredAny: [
"prd.play_switch.manage",
"prd.odds.manage",
"prd.risk_cap.manage",
"prd.risk_cap.view",
"prd.rebate.manage",
"prd.rebate.view",
"prd.jackpot.manage",
"prd.jackpot.view",
],
},
{
segment: "risk",
label: "Risk",
href: "/admin/risk",
requiredAny: ["prd.draw_result.view", "prd.draw_result.manage"],
},
{
segment: "settlement",
label: "Settlement",
href: "/admin/settlement-batches",
requiredAny: [
"prd.payout.manage",
"prd.payout.review",
"prd.payout.view",
],
},
{
segment: "jackpot",
label: "Jackpot",
href: "/admin/jackpot/pools",
activeMatchPrefix: "/admin/jackpot",
requiredAny: ["prd.jackpot.manage", "prd.jackpot.view"],
},
{
segment: "reconcile",
label: "Reconcile",
href: "/admin/reconcile",
requiredAny: [
"prd.wallet_reconcile.manage",
"prd.wallet_reconcile.view",
"prd.wallet_reconcile.view_cs",
],
},
{
segment: "tickets",
label: "Tickets",
href: "/admin/tickets",
requiredAny: [
"prd.users.view_cs",
"prd.users.manage",
"prd.users.view_finance",
"prd.draw_result.view",
"prd.draw_result.manage",
"prd.payout.view",
"prd.payout.review",
"prd.payout.manage",
"prd.report.player",
],
},
{
segment: "reports",
label: "Reports",
href: "/admin/reports",
requiredAny: [
"prd.report.all",
"prd.report.risk",
"prd.report.finance",
"prd.report.player",
],
},
{
segment: "audit",
label: "Audit Logs",
href: "/admin/audit-logs",
requiredAny: ["prd.audit.all", "prd.audit.self", "prd.audit.finance"],
},
{ segment: "settings", label: "Settings", href: "/admin/settings" },
];

View File

@@ -30,9 +30,9 @@ export function WalletSubnav(): React.ReactElement {
aria-label={t("subnavLabel")}
className="mb-6 flex flex-wrap gap-2 border-b border-border pb-3"
>
{tabs.map((t) => {
const allowed = adminHasAnyPermission(perms, [...t.requiredAny]);
const active = pathname === t.href || pathname.startsWith(`${t.href}/`);
{tabs.map((tab) => {
const allowed = adminHasAnyPermission(perms, [...tab.requiredAny]);
const active = pathname === tab.href || pathname.startsWith(`${tab.href}/`);
const className = cn(
"rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
active
@@ -42,14 +42,14 @@ export function WalletSubnav(): React.ReactElement {
);
if (!allowed) {
return (
<span key={t.href} className={className} title={t("noPermission")}>
{t(t.label)}
<span key={tab.href} className={className} title={t("noPermission")}>
{t(tab.label)}
</span>
);
}
return (
<Link key={t.href} href={t.href} className={className}>
{t(t.label)}
<Link key={tab.href} href={tab.href} className={className}>
{t(tab.label)}
</Link>
);
})}