diff --git a/src/api/admin-users.ts b/src/api/admin-users.ts
new file mode 100644
index 0000000..37068b7
--- /dev/null
+++ b/src/api/admin-users.ts
@@ -0,0 +1,35 @@
+import { adminRequest } from "@/lib/admin-http";
+
+import { API_V1_PREFIX } from "./paths";
+
+import type {
+ AdminPermissionCatalogData,
+ AdminUserPermissionListData,
+ AdminUserPermissionSyncData,
+} from "@/types/api/admin-user";
+
+const A = `${API_V1_PREFIX}/admin`;
+
+export async function getAdminUsers(params?: {
+ page?: number;
+ per_page?: number;
+ keyword?: string;
+}): Promise {
+ return adminRequest.get(`${A}/admin-users`, {
+ params,
+ });
+}
+
+export async function getAdminUserPermissionCatalog(): Promise {
+ return adminRequest.get(`${A}/admin-user-permission-catalog`);
+}
+
+export async function putAdminUserPermissions(
+ adminUserId: number,
+ permissionSlugs: string[],
+): Promise {
+ return adminRequest.put(
+ `${A}/admin-users/${adminUserId}/permissions`,
+ { permission_slugs: permissionSlugs },
+ );
+}
diff --git a/src/app/admin/(shell)/admin-users/page.tsx b/src/app/admin/(shell)/admin-users/page.tsx
new file mode 100644
index 0000000..634b189
--- /dev/null
+++ b/src/app/admin/(shell)/admin-users/page.tsx
@@ -0,0 +1,16 @@
+import { ModuleScaffold } from "@/components/admin/module-scaffold";
+import { AdminUsersConsole } from "@/modules/admin-users/admin-users-console";
+import { adminUsersModuleMeta } from "@/modules/admin-users/meta";
+import type { Metadata } from "next";
+
+export const metadata: Metadata = {
+ title: adminUsersModuleMeta.title,
+};
+
+export default function AdminUsersPage() {
+ return (
+
+
+
+ );
+}
diff --git a/src/modules/_config/admin-nav-icons.tsx b/src/modules/_config/admin-nav-icons.tsx
index e34adcb..ad06537 100644
--- a/src/modules/_config/admin-nav-icons.tsx
+++ b/src/modules/_config/admin-nav-icons.tsx
@@ -11,6 +11,7 @@ import {
ScrollText,
Settings,
ShieldAlert,
+ ShieldCheck,
SlidersHorizontal,
Ticket,
Users,
@@ -35,6 +36,7 @@ export const adminNavIconBySegment: Record
reports: FileSpreadsheet,
reconcile: Scale,
audit: ScrollText,
+ admin_users: ShieldCheck,
settings: Settings,
};
diff --git a/src/modules/_config/admin-nav.ts b/src/modules/_config/admin-nav.ts
index a5d9b8b..dd48b63 100644
--- a/src/modules/_config/admin-nav.ts
+++ b/src/modules/_config/admin-nav.ts
@@ -22,7 +22,8 @@ export type AdminNavItem = {
| "jackpot"
| "reports"
| "reconcile"
- | "audit";
+ | "audit"
+ | "admin_users";
activeMatchPrefix?: string;
/** 拥有任一权限 slug 即显示侧栏项 */
requiredAny?: readonly string[];
@@ -30,32 +31,6 @@ export type AdminNavItem = {
export const adminShellNavItems: AdminNavItem[] = [
{ segment: "dashboard", label: "仪表盘", href: "/admin" },
- {
- segment: "service_desk",
- label: "客服 / 财务",
- href: "/admin/service-desk",
- requiredAny: [
- "prd.users.view_cs",
- "prd.users.view_finance",
- "prd.users.manage",
- "prd.wallet_reconcile.view_cs",
- "prd.wallet_reconcile.view",
- "prd.wallet_reconcile.manage",
- "prd.report.finance",
- "prd.report.player",
- "prd.draw_result.view",
- ],
- },
- {
- segment: "players",
- label: "玩家查询",
- href: "/admin/players",
- requiredAny: [
- "prd.users.manage",
- "prd.users.view_finance",
- "prd.users.view_cs",
- ],
- },
{
segment: "draws",
label: "开奖",
@@ -77,36 +52,6 @@ export const adminShellNavItems: AdminNavItem[] = [
"prd.jackpot.view",
],
},
- {
- segment: "tickets",
- label: "玩家注单",
- 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: "wallet",
- label: "钱包流水",
- 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: "risk",
label: "风控",
@@ -131,14 +76,17 @@ export const adminShellNavItems: AdminNavItem[] = [
requiredAny: ["prd.jackpot.manage", "prd.jackpot.view"],
},
{
- segment: "reports",
- label: "报表导出",
- href: "/admin/reports",
+ segment: "wallet",
+ label: "钱包流水",
+ href: "/admin/wallet/transactions",
+ activeMatchPrefix: "/admin/wallet",
requiredAny: [
- "prd.report.all",
- "prd.report.risk",
- "prd.report.finance",
- "prd.report.player",
+ "prd.wallet_reconcile.manage",
+ "prd.wallet_reconcile.view",
+ "prd.wallet_reconcile.view_cs",
+ "prd.users.manage",
+ "prd.users.view_finance",
+ "prd.users.view_cs",
],
},
{
@@ -151,11 +99,70 @@ export const adminShellNavItems: AdminNavItem[] = [
"prd.wallet_reconcile.view_cs",
],
},
+ {
+ segment: "tickets",
+ label: "玩家注单",
+ 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: "players",
+ label: "玩家查询",
+ href: "/admin/players",
+ requiredAny: [
+ "prd.users.manage",
+ "prd.users.view_finance",
+ "prd.users.view_cs",
+ ],
+ },
+ {
+ segment: "service_desk",
+ label: "客服 / 财务",
+ href: "/admin/service-desk",
+ requiredAny: [
+ "prd.users.view_cs",
+ "prd.users.view_finance",
+ "prd.users.manage",
+ "prd.wallet_reconcile.view_cs",
+ "prd.wallet_reconcile.view",
+ "prd.wallet_reconcile.manage",
+ "prd.report.finance",
+ "prd.report.player",
+ "prd.draw_result.view",
+ ],
+ },
+ {
+ segment: "reports",
+ label: "报表导出",
+ href: "/admin/reports",
+ requiredAny: [
+ "prd.report.all",
+ "prd.report.risk",
+ "prd.report.finance",
+ "prd.report.player",
+ ],
+ },
{
segment: "audit",
label: "审计日志",
href: "/admin/audit-logs",
requiredAny: ["prd.audit.all", "prd.audit.self", "prd.audit.finance"],
},
+ {
+ segment: "admin_users",
+ label: "管理员权限",
+ href: "/admin/admin-users",
+ requiredAny: ["prd.admin_user.manage"],
+ },
{ segment: "settings", label: "系统设置", href: "/admin/settings" },
];
diff --git a/src/modules/admin-users/admin-users-console.tsx b/src/modules/admin-users/admin-users-console.tsx
new file mode 100644
index 0000000..de86152
--- /dev/null
+++ b/src/modules/admin-users/admin-users-console.tsx
@@ -0,0 +1,301 @@
+"use client";
+
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { toast } from "sonner";
+
+import {
+ getAdminUserPermissionCatalog,
+ getAdminUsers,
+ putAdminUserPermissions,
+} from "@/api/admin-users";
+import { AdminListPaginationFooter } from "@/components/admin/admin-list-pagination-footer";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Checkbox } from "@/components/ui/checkbox";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { LotteryApiBizError } from "@/types/api/errors";
+import type { AdminPermissionCatalogData, AdminUserPermissionRow } from "@/types/api/index";
+
+export function AdminUsersConsole(): React.ReactElement {
+ const [page, setPage] = useState(1);
+ const [perPage, setPerPage] = useState(25);
+ const [keyword, setKeyword] = useState("");
+ const [query, setQuery] = useState("");
+
+ const [catalog, setCatalog] = useState(null);
+ const [items, setItems] = useState([]);
+ const [total, setTotal] = useState(0);
+ const [lastPage, setLastPage] = useState(1);
+ const [loading, setLoading] = useState(true);
+ const [err, setErr] = useState(null);
+
+ const [selectedId, setSelectedId] = useState(null);
+ const [draftPermissions, setDraftPermissions] = useState([]);
+ const [saving, setSaving] = useState(false);
+
+ const selectedUser = useMemo(
+ () => items.find((u) => u.id === selectedId) ?? null,
+ [items, selectedId],
+ );
+
+ const load = useCallback(async () => {
+ setLoading(true);
+ setErr(null);
+ try {
+ const [catalogData, listData] = await Promise.all([
+ getAdminUserPermissionCatalog(),
+ getAdminUsers({
+ page,
+ per_page: perPage,
+ keyword: query.trim() || undefined,
+ }),
+ ]);
+ setCatalog(catalogData);
+ setItems(listData.items);
+ setTotal(listData.meta.total);
+ setLastPage(Math.max(1, listData.meta.last_page));
+ } catch (e) {
+ const msg = e instanceof LotteryApiBizError ? e.message : "加载管理员列表失败";
+ setErr(msg);
+ setItems([]);
+ setTotal(0);
+ setLastPage(1);
+ } finally {
+ setLoading(false);
+ }
+ }, [page, perPage, query]);
+
+ useEffect(() => {
+ queueMicrotask(() => {
+ void load();
+ });
+ }, [load]);
+
+ function togglePermission(slug: string, checked: boolean): void {
+ setDraftPermissions((prev) => {
+ if (checked) {
+ return Array.from(new Set([...prev, slug])).sort();
+ }
+ return prev.filter((s) => s !== slug);
+ });
+ }
+
+ async function savePermissions(): Promise {
+ if (!selectedUser) {
+ return;
+ }
+ setSaving(true);
+ try {
+ const result = await putAdminUserPermissions(selectedUser.id, draftPermissions);
+ setDraftPermissions([...result.direct_permissions].sort());
+ setItems((prev) =>
+ prev.map((row) =>
+ row.id === result.id
+ ? {
+ ...row,
+ direct_permissions: result.direct_permissions,
+ effective_permissions: result.effective_permissions,
+ }
+ : row,
+ ),
+ );
+ toast.success(`已更新 ${result.username} 的权限`);
+ } catch (e) {
+ const msg = e instanceof LotteryApiBizError ? e.message : "保存权限失败";
+ toast.error(msg);
+ } finally {
+ setSaving(false);
+ }
+ }
+
+ return (
+
+
+
+
+ 管理员用户列表
+
+
+ setKeyword(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ setPage(1);
+ setQuery(keyword.trim());
+ }
+ }}
+ />
+
+
+
+
+
+ {err ? {err}
: null}
+ {loading && items.length === 0 ? (
+ 加载中…
+ ) : null}
+
+
+
+
+ ID
+ 账号
+ 昵称
+ 角色
+ 直接权限
+ 有效权限
+ 操作
+
+
+
+ {items.length === 0 ? (
+
+
+ 暂无数据
+
+
+ ) : (
+ items.map((row) => (
+
+ {row.id}
+
+
+ {row.username}
+ {row.email ?? "—"}
+
+
+ {row.nickname}
+
+
+ {row.roles.length === 0 ? (
+ 无
+ ) : (
+ row.roles.map((slug) => (
+
+ {slug}
+
+ ))
+ )}
+
+
+ {row.direct_permissions.length}
+ {row.effective_permissions.length}
+
+
+
+
+ ))
+ )}
+
+
+
+ {
+ setPerPage(n);
+ setPage(1);
+ }}
+ onPageChange={setPage}
+ />
+
+
+
+
+
+
+ 权限分配
+
+
+
+
+ {!selectedUser ? (
+ 请先在上方列表选择一个管理员。
+ ) : (
+ <>
+
+ 当前用户:
+ {selectedUser.username}
+ ({selectedUser.nickname})
+
+
+
+ {(catalog?.permissions ?? []).map((p) => {
+ const checked = draftPermissions.includes(p.slug);
+ return (
+
+ );
+ })}
+
+
+
+
+ {selectedUser.roles.length === 0 ? (
+ 无角色
+ ) : (
+ selectedUser.roles.map((slug) => (
+
+ {slug}
+
+ ))
+ )}
+
+
+ >
+ )}
+
+
+
+ );
+}
diff --git a/src/modules/admin-users/meta.ts b/src/modules/admin-users/meta.ts
new file mode 100644
index 0000000..94e3b83
--- /dev/null
+++ b/src/modules/admin-users/meta.ts
@@ -0,0 +1,5 @@
+export const adminUsersModuleMeta = {
+ segment: "admin_users",
+ title: "管理员权限",
+ description: "",
+} as const;
diff --git a/src/modules/dashboard/dashboard-console.tsx b/src/modules/dashboard/dashboard-console.tsx
index 2272e69..c6df38d 100644
--- a/src/modules/dashboard/dashboard-console.tsx
+++ b/src/modules/dashboard/dashboard-console.tsx
@@ -489,7 +489,10 @@ export function DashboardConsole(): ReactElement {
{drawId != null ? (
期号详情
@@ -533,7 +536,10 @@ export function DashboardConsole(): ReactElement {
{drawId != null ? (
占用明细
@@ -576,7 +582,10 @@ export function DashboardConsole(): ReactElement {
{drawId != null ? (
查看全部
@@ -600,7 +609,10 @@ export function DashboardConsole(): ReactElement {
{drawId != null ? (
查看全部
diff --git a/src/types/api/admin-user.ts b/src/types/api/admin-user.ts
new file mode 100644
index 0000000..4891804
--- /dev/null
+++ b/src/types/api/admin-user.ts
@@ -0,0 +1,40 @@
+export type AdminUserPermissionRow = {
+ id: number;
+ username: string;
+ nickname: string;
+ email: string | null;
+ status: number;
+ roles: string[];
+ direct_permissions: string[];
+ effective_permissions: string[];
+};
+
+export type AdminUserPermissionListData = {
+ items: AdminUserPermissionRow[];
+ meta: {
+ current_page: number;
+ per_page: number;
+ total: number;
+ last_page: number;
+ };
+};
+
+export type AdminPermissionCatalogData = {
+ permissions: { id: number; slug: string; name: string }[];
+ roles: {
+ id: number;
+ slug: string;
+ name: string;
+ permission_slugs: string[];
+ user_count: number;
+ }[];
+};
+
+export type AdminUserPermissionSyncData = {
+ id: number;
+ username: string;
+ nickname: string;
+ roles: string[];
+ direct_permissions: string[];
+ effective_permissions: string[];
+};
diff --git a/src/types/api/index.ts b/src/types/api/index.ts
index e994a4f..a0e0ba8 100644
--- a/src/types/api/index.ts
+++ b/src/types/api/index.ts
@@ -55,3 +55,9 @@ export type {
RiskCapItemRow,
RiskCapVersionDetail,
} from "./admin-config";
+export type {
+ AdminPermissionCatalogData,
+ AdminUserPermissionListData,
+ AdminUserPermissionRow,
+ AdminUserPermissionSyncData,
+} from "./admin-user";