feat: 更新管理员导航,添加管理员权限模块和相关API导出,优化仪表盘链接样式
This commit is contained in:
35
src/api/admin-users.ts
Normal file
35
src/api/admin-users.ts
Normal file
@@ -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<AdminUserPermissionListData> {
|
||||||
|
return adminRequest.get<AdminUserPermissionListData>(`${A}/admin-users`, {
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAdminUserPermissionCatalog(): Promise<AdminPermissionCatalogData> {
|
||||||
|
return adminRequest.get<AdminPermissionCatalogData>(`${A}/admin-user-permission-catalog`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function putAdminUserPermissions(
|
||||||
|
adminUserId: number,
|
||||||
|
permissionSlugs: string[],
|
||||||
|
): Promise<AdminUserPermissionSyncData> {
|
||||||
|
return adminRequest.put<AdminUserPermissionSyncData>(
|
||||||
|
`${A}/admin-users/${adminUserId}/permissions`,
|
||||||
|
{ permission_slugs: permissionSlugs },
|
||||||
|
);
|
||||||
|
}
|
||||||
16
src/app/admin/(shell)/admin-users/page.tsx
Normal file
16
src/app/admin/(shell)/admin-users/page.tsx
Normal file
@@ -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 (
|
||||||
|
<ModuleScaffold className="w-full max-w-none">
|
||||||
|
<AdminUsersConsole />
|
||||||
|
</ModuleScaffold>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
ScrollText,
|
ScrollText,
|
||||||
Settings,
|
Settings,
|
||||||
ShieldAlert,
|
ShieldAlert,
|
||||||
|
ShieldCheck,
|
||||||
SlidersHorizontal,
|
SlidersHorizontal,
|
||||||
Ticket,
|
Ticket,
|
||||||
Users,
|
Users,
|
||||||
@@ -35,6 +36,7 @@ export const adminNavIconBySegment: Record<AdminNavItem["segment"], LucideIcon>
|
|||||||
reports: FileSpreadsheet,
|
reports: FileSpreadsheet,
|
||||||
reconcile: Scale,
|
reconcile: Scale,
|
||||||
audit: ScrollText,
|
audit: ScrollText,
|
||||||
|
admin_users: ShieldCheck,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ export type AdminNavItem = {
|
|||||||
| "jackpot"
|
| "jackpot"
|
||||||
| "reports"
|
| "reports"
|
||||||
| "reconcile"
|
| "reconcile"
|
||||||
| "audit";
|
| "audit"
|
||||||
|
| "admin_users";
|
||||||
activeMatchPrefix?: string;
|
activeMatchPrefix?: string;
|
||||||
/** 拥有任一权限 slug 即显示侧栏项 */
|
/** 拥有任一权限 slug 即显示侧栏项 */
|
||||||
requiredAny?: readonly string[];
|
requiredAny?: readonly string[];
|
||||||
@@ -30,32 +31,6 @@ export type AdminNavItem = {
|
|||||||
|
|
||||||
export const adminShellNavItems: AdminNavItem[] = [
|
export const adminShellNavItems: AdminNavItem[] = [
|
||||||
{ segment: "dashboard", label: "仪表盘", href: "/admin" },
|
{ 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",
|
segment: "draws",
|
||||||
label: "开奖",
|
label: "开奖",
|
||||||
@@ -77,36 +52,6 @@ export const adminShellNavItems: AdminNavItem[] = [
|
|||||||
"prd.jackpot.view",
|
"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",
|
segment: "risk",
|
||||||
label: "风控",
|
label: "风控",
|
||||||
@@ -131,14 +76,17 @@ export const adminShellNavItems: AdminNavItem[] = [
|
|||||||
requiredAny: ["prd.jackpot.manage", "prd.jackpot.view"],
|
requiredAny: ["prd.jackpot.manage", "prd.jackpot.view"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
segment: "reports",
|
segment: "wallet",
|
||||||
label: "报表导出",
|
label: "钱包流水",
|
||||||
href: "/admin/reports",
|
href: "/admin/wallet/transactions",
|
||||||
|
activeMatchPrefix: "/admin/wallet",
|
||||||
requiredAny: [
|
requiredAny: [
|
||||||
"prd.report.all",
|
"prd.wallet_reconcile.manage",
|
||||||
"prd.report.risk",
|
"prd.wallet_reconcile.view",
|
||||||
"prd.report.finance",
|
"prd.wallet_reconcile.view_cs",
|
||||||
"prd.report.player",
|
"prd.users.manage",
|
||||||
|
"prd.users.view_finance",
|
||||||
|
"prd.users.view_cs",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -151,11 +99,70 @@ export const adminShellNavItems: AdminNavItem[] = [
|
|||||||
"prd.wallet_reconcile.view_cs",
|
"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",
|
segment: "audit",
|
||||||
label: "审计日志",
|
label: "审计日志",
|
||||||
href: "/admin/audit-logs",
|
href: "/admin/audit-logs",
|
||||||
requiredAny: ["prd.audit.all", "prd.audit.self", "prd.audit.finance"],
|
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" },
|
{ segment: "settings", label: "系统设置", href: "/admin/settings" },
|
||||||
];
|
];
|
||||||
|
|||||||
301
src/modules/admin-users/admin-users-console.tsx
Normal file
301
src/modules/admin-users/admin-users-console.tsx
Normal file
@@ -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<AdminPermissionCatalogData | null>(null);
|
||||||
|
const [items, setItems] = useState<AdminUserPermissionRow[]>([]);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [lastPage, setLastPage] = useState(1);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [err, setErr] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
|
const [draftPermissions, setDraftPermissions] = useState<string[]>([]);
|
||||||
|
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<void> {
|
||||||
|
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 (
|
||||||
|
<div className="flex w-full max-w-none flex-col gap-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<CardTitle>管理员用户列表</CardTitle>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full max-w-lg gap-2">
|
||||||
|
<Input
|
||||||
|
value={keyword}
|
||||||
|
placeholder="按用户名 / 昵称 / 邮箱搜索"
|
||||||
|
onChange={(e) => setKeyword(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
setPage(1);
|
||||||
|
setQuery(keyword.trim());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setPage(1);
|
||||||
|
setQuery(keyword.trim());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
搜索
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="secondary" onClick={() => void load()}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{err ? <p className="text-sm text-red-600 dark:text-red-400">{err}</p> : null}
|
||||||
|
{loading && items.length === 0 ? (
|
||||||
|
<p className="text-sm text-muted-foreground">加载中…</p>
|
||||||
|
) : null}
|
||||||
|
<div className="rounded-md border">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="w-16">ID</TableHead>
|
||||||
|
<TableHead>账号</TableHead>
|
||||||
|
<TableHead>昵称</TableHead>
|
||||||
|
<TableHead>角色</TableHead>
|
||||||
|
<TableHead>直接权限</TableHead>
|
||||||
|
<TableHead>有效权限</TableHead>
|
||||||
|
<TableHead className="w-24">操作</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{items.length === 0 ? (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-muted-foreground">
|
||||||
|
暂无数据
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
) : (
|
||||||
|
items.map((row) => (
|
||||||
|
<TableRow key={row.id}>
|
||||||
|
<TableCell>{row.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">{row.username}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{row.email ?? "—"}</span>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{row.nickname}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{row.roles.length === 0 ? (
|
||||||
|
<span className="text-xs text-muted-foreground">无</span>
|
||||||
|
) : (
|
||||||
|
row.roles.map((slug) => (
|
||||||
|
<Badge key={slug} variant="secondary">
|
||||||
|
{slug}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="tabular-nums">{row.direct_permissions.length}</TableCell>
|
||||||
|
<TableCell className="tabular-nums">{row.effective_permissions.length}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant={selectedId === row.id ? "default" : "outline"}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedId(row.id);
|
||||||
|
setDraftPermissions([...row.direct_permissions].sort());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
权限
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<AdminListPaginationFooter
|
||||||
|
selectId="admin-users-per-page"
|
||||||
|
total={total}
|
||||||
|
page={page}
|
||||||
|
lastPage={lastPage}
|
||||||
|
perPage={perPage}
|
||||||
|
loading={loading}
|
||||||
|
onPerPageChange={(n) => {
|
||||||
|
setPerPage(n);
|
||||||
|
setPage(1);
|
||||||
|
}}
|
||||||
|
onPageChange={setPage}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row flex-wrap items-end justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
<CardTitle>权限分配</CardTitle>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => void savePermissions()}
|
||||||
|
disabled={!selectedUser || saving}
|
||||||
|
>
|
||||||
|
{saving ? "保存中…" : "保存权限"}
|
||||||
|
</Button>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{!selectedUser ? (
|
||||||
|
<p className="text-sm text-muted-foreground">请先在上方列表选择一个管理员。</p>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="rounded-md border bg-muted/20 p-3 text-sm">
|
||||||
|
当前用户:
|
||||||
|
<span className="ml-1 font-medium">{selectedUser.username}</span>
|
||||||
|
<span className="ml-2 text-muted-foreground">({selectedUser.nickname})</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-3 rounded-md border p-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{(catalog?.permissions ?? []).map((p) => {
|
||||||
|
const checked = draftPermissions.includes(p.slug);
|
||||||
|
return (
|
||||||
|
<label key={p.slug} className="flex items-start gap-2 text-sm">
|
||||||
|
<Checkbox
|
||||||
|
checked={checked}
|
||||||
|
onCheckedChange={(v) => togglePermission(p.slug, v === true)}
|
||||||
|
/>
|
||||||
|
<span className="space-y-0.5">
|
||||||
|
<span className="block leading-none">{p.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground">{p.slug}</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>角色带来的权限(只读)</Label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedUser.roles.length === 0 ? (
|
||||||
|
<span className="text-sm text-muted-foreground">无角色</span>
|
||||||
|
) : (
|
||||||
|
selectedUser.roles.map((slug) => (
|
||||||
|
<Badge key={slug} variant="outline">
|
||||||
|
{slug}
|
||||||
|
</Badge>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/modules/admin-users/meta.ts
Normal file
5
src/modules/admin-users/meta.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const adminUsersModuleMeta = {
|
||||||
|
segment: "admin_users",
|
||||||
|
title: "管理员权限",
|
||||||
|
description: "",
|
||||||
|
} as const;
|
||||||
@@ -489,7 +489,10 @@ export function DashboardConsole(): ReactElement {
|
|||||||
</p>
|
</p>
|
||||||
{drawId != null ? (
|
{drawId != null ? (
|
||||||
<Link
|
<Link
|
||||||
className="mt-2 inline-block text-xs font-medium text-[#2563eb] underline-offset-4 hover:underline"
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
|
"mt-2 h-7 border-[#2563eb]/30 px-2 text-xs text-[#2563eb] hover:bg-[#2563eb]/5",
|
||||||
|
)}
|
||||||
href={`/admin/draws/${drawId}`}
|
href={`/admin/draws/${drawId}`}
|
||||||
>
|
>
|
||||||
期号详情
|
期号详情
|
||||||
@@ -533,7 +536,10 @@ export function DashboardConsole(): ReactElement {
|
|||||||
</div>
|
</div>
|
||||||
{drawId != null ? (
|
{drawId != null ? (
|
||||||
<Link
|
<Link
|
||||||
className="mt-1 inline-block text-xs font-medium text-[#2563eb] underline-offset-4 hover:underline"
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
|
"mt-1 h-7 border-[#2563eb]/30 px-2 text-xs text-[#2563eb] hover:bg-[#2563eb]/5",
|
||||||
|
)}
|
||||||
href={`/admin/risk/draws/${drawId}/occupancy`}
|
href={`/admin/risk/draws/${drawId}/occupancy`}
|
||||||
>
|
>
|
||||||
占用明细
|
占用明细
|
||||||
@@ -576,7 +582,10 @@ export function DashboardConsole(): ReactElement {
|
|||||||
{drawId != null ? (
|
{drawId != null ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/risk/draws/${drawId}/hot`}
|
href={`/admin/risk/draws/${drawId}/hot`}
|
||||||
className="text-xs font-medium text-[#2563eb] underline-offset-4 hover:underline"
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
|
"h-7 border-[#2563eb]/30 px-2 text-xs text-[#2563eb] hover:bg-[#2563eb]/5",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
查看全部
|
查看全部
|
||||||
</Link>
|
</Link>
|
||||||
@@ -600,7 +609,10 @@ export function DashboardConsole(): ReactElement {
|
|||||||
{drawId != null ? (
|
{drawId != null ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/risk/draws/${drawId}/sold-out`}
|
href={`/admin/risk/draws/${drawId}/sold-out`}
|
||||||
className="text-xs font-medium text-[#2563eb] underline-offset-4 hover:underline"
|
className={cn(
|
||||||
|
buttonVariants({ variant: "outline", size: "sm" }),
|
||||||
|
"h-7 border-[#2563eb]/30 px-2 text-xs text-[#2563eb] hover:bg-[#2563eb]/5",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
查看全部
|
查看全部
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
40
src/types/api/admin-user.ts
Normal file
40
src/types/api/admin-user.ts
Normal file
@@ -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[];
|
||||||
|
};
|
||||||
@@ -55,3 +55,9 @@ export type {
|
|||||||
RiskCapItemRow,
|
RiskCapItemRow,
|
||||||
RiskCapVersionDetail,
|
RiskCapVersionDetail,
|
||||||
} from "./admin-config";
|
} from "./admin-config";
|
||||||
|
export type {
|
||||||
|
AdminPermissionCatalogData,
|
||||||
|
AdminUserPermissionListData,
|
||||||
|
AdminUserPermissionRow,
|
||||||
|
AdminUserPermissionSyncData,
|
||||||
|
} from "./admin-user";
|
||||||
|
|||||||
Reference in New Issue
Block a user