Files
model-platform/frontend/app/features/admin/RoleManagementPage.tsx
T
2026-08-28 12:00:14 +08:00

343 lines
12 KiB
TypeScript

import { useEffect, useState } from "react";
import { Plus, Search } from "lucide-react";
import {
ApiRequestError,
type PlatformPermission,
type Role,
type RoleCreatePayload,
type RoleUpdatePayload,
} from "../../services/api";
import { useApi, useAuth } from "../../context/AuthContext";
import { Button } from "~/components/ui/button";
import { ConfirmDialog } from "~/components/common/ConfirmDialog";
import { PermissionEditDialog } from "./PermissionEditDialog";
import { RoleEditDialog } from "./RoleEditDialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "~/components/ui/table";
import { AdminColgroup, ROLE_COL_WIDTHS } from "./AdminTable";
import {
adminEmptyClass,
adminPageClass,
adminReadonlyClass,
adminToolbarClass,
adminToolbarInputClass,
adminToolbarSearchClass,
builtinBadgeClass,
permissionCountClass,
roleActionsClass,
roleCodeClass,
roleNameClass,
rowButtonClass,
rowDangerButtonClass,
} from "./adminUi";
import { primaryGradientButtonClass } from "~/components/common/buttonClasses";
export function RoleManagementPage({
onNotify,
onConnectionChange,
}: {
onNotify: (notice: { tone: "success" | "error" | "info"; message: string }) => void;
onConnectionChange: (online: boolean) => void;
}) {
const api = useApi();
const { user } = useAuth();
const [roles, setRoles] = useState<Role[]>([]);
const [permissions, setPermissions] = useState<PlatformPermission[]>([]);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [searchTerm, setSearchTerm] = useState("");
// 新建 / 编辑弹窗
const [dialogOpen, setDialogOpen] = useState(false);
const [editing, setEditing] = useState<Role | null>(null);
// 权限配置弹窗
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
const [permissionTarget, setPermissionTarget] = useState<Role | null>(null);
const [deleteTarget, setDeleteTarget] = useState<Role | null>(null);
const canManage = user?.role_code === "admin";
const load = async (): Promise<void> => {
setLoading(true);
setLoadError(null);
try {
const [roleList, permissionList] = await Promise.all([
api.listPlatformRoles(),
api.listPlatformPermissions(),
]);
setRoles(roleList);
setPermissions(permissionList);
onConnectionChange(true);
} catch (error) {
onConnectionChange(false);
const message = error instanceof Error ? error.message : "角色列表加载失败";
setLoadError(message);
onNotify({ tone: "error", message });
} finally {
setLoading(false);
}
};
useEffect(() => {
void load();
}, []);
const openCreate = (): void => {
setEditing(null);
setDialogOpen(true);
};
const openEdit = (role: Role): void => {
setEditing(role);
setDialogOpen(true);
};
const openPermissions = (role: Role): void => {
setPermissionTarget(role);
setPermissionDialogOpen(true);
};
const handleRoleSubmit = async (
payload: RoleCreatePayload | RoleUpdatePayload,
): Promise<void> => {
setSaving(true);
try {
if (editing) {
const updated = await api.updatePlatformRole(
editing.role_code,
payload as RoleUpdatePayload,
);
setRoles((current) => current.map(
(item) => item.role_code === updated.role_code ? updated : item,
));
onNotify({ tone: "success", message: "角色信息已更新" });
} else {
const created = await api.createPlatformRole(
payload as RoleCreatePayload,
);
setRoles((current) => [...current, created]);
onNotify({ tone: "success", message: "角色已创建" });
}
setDialogOpen(false);
} catch (error) {
onNotify({
tone: "error",
message: error instanceof ApiRequestError
? error.message
: (editing ? "保存角色失败" : "创建角色失败"),
});
} finally {
setSaving(false);
}
};
const savePermissions = async (codes: string[]): Promise<void> => {
if (!permissionTarget) return;
setSaving(true);
try {
const updated = await api.updatePlatformRolePermissions(
permissionTarget.role_code,
codes,
);
setRoles((current) => current.map(
(item) => item.role_code === updated.role_code ? updated : item,
));
onNotify({ tone: "success", message: "权限已保存" });
setPermissionDialogOpen(false);
} catch (error) {
onNotify({
tone: "error",
message: error instanceof ApiRequestError ? error.message : "保存权限失败",
});
} finally {
setSaving(false);
}
};
const executeRemoveRole = async (role: Role): Promise<void> => {
try {
await api.deletePlatformRole(role.role_code);
setRoles((current) => current.filter(
(item) => item.role_code !== role.role_code,
));
onNotify({ tone: "success", message: "角色已删除" });
} catch (error) {
onNotify({
tone: "error",
message: error instanceof Error ? error.message : "删除角色失败",
});
}
};
const filteredRoles = roles.filter((role) => {
const term = searchTerm.toLowerCase().trim();
if (!term) return true;
return (
role.role_code.toLowerCase().includes(term) ||
role.role_name.toLowerCase().includes(term)
);
});
return (
<section className={adminPageClass}>
<div className={adminToolbarClass}>
<div className={adminToolbarSearchClass}>
<Search size={14} />
<input
type="text"
placeholder="搜索角色编码 / 名称"
className={adminToolbarInputClass}
value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)}
/>
</div>
<Button
variant="default"
size="sm"
type="button"
disabled={!canManage}
onClick={openCreate}
className={primaryGradientButtonClass}
>
<Plus size={15} />
新建角色
</Button>
</div>
{!canManage && (
<div className={adminReadonlyClass}>当前为开发人员,只能查看角色列表。</div>
)}
{loadError && <div className={adminReadonlyClass}>{loadError}</div>}
<Table className="rounded-[9px] border border-line overflow-hidden text-[11px] text-[#44576a] [--border-color:var(--color-line)] table-fixed bg-white">
<AdminColgroup widths={ROLE_COL_WIDTHS} />
<TableHeader className="bg-[#f5f8fb] text-ink-caption">
<TableRow className="hover:bg-transparent border-b border-[#edf1f5]">
<TableHead className="h-10 px-4 text-[10px] font-bold text-ink-caption">角色编码</TableHead>
<TableHead className="h-10 px-4 text-[10px] font-bold text-ink-caption">角色名称</TableHead>
<TableHead className="h-10 px-4 text-[10px] font-bold text-ink-caption">类型</TableHead>
<TableHead className="h-10 px-4 text-[10px] font-bold text-ink-caption">权限</TableHead>
<TableHead className="h-10 px-4 text-[10px] font-bold text-ink-caption">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow className="min-h-[60px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
<TableCell colSpan={5} className="p-0">
<p className={adminEmptyClass}>正在加载角色…</p>
</TableCell>
</TableRow>
) : filteredRoles.length === 0 ? (
<TableRow className="min-h-[60px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
<TableCell colSpan={5} className="p-0">
<p className={adminEmptyClass}>
{roles.length === 0 ? "暂无角色" : "没有匹配的角色"}
</p>
</TableCell>
</TableRow>
) : (
filteredRoles.map((role) => (
<TableRow key={role.role_id} className="min-h-[60px] border-t border-[#edf1f5] hover:bg-[#f7fafc] data-[state=selected]:bg-transparent">
<TableCell className="p-3 px-4 align-middle"><code className={roleCodeClass}>{role.role_code}</code></TableCell>
<TableCell className="p-3 px-4 align-middle"><span className={roleNameClass}>{role.role_name}</span></TableCell>
<TableCell className="p-3 px-4 align-middle"><span className={builtinBadgeClass(role.is_builtin)}>
{role.is_builtin ? "内置" : "自定义"}
</span></TableCell>
<TableCell className="p-3 px-4 align-middle"><span className={permissionCountClass}>
{role.permission_codes.length} 项权限
</span></TableCell>
<TableCell className="p-3 px-4 align-middle"><span className={roleActionsClass}>
<Button
variant="outline"
size="xs"
type="button"
disabled={!canManage}
onClick={() => openEdit(role)}
className={rowButtonClass}
>
编辑
</Button>
<Button
variant="outline"
size="xs"
type="button"
disabled={!canManage}
onClick={() => openPermissions(role)}
className={rowButtonClass}
>
权限
</Button>
<Button
variant="destructive"
size="xs"
type="button"
disabled={!canManage || role.is_builtin}
title={role.is_builtin ? "内置角色不可删除" : "删除角色"}
onClick={() => setDeleteTarget(role)}
className={rowDangerButtonClass}
>
删除
</Button>
</span></TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
{dialogOpen && (
<RoleEditDialog
open={dialogOpen}
editing={editing}
permissions={permissions}
saving={saving}
onNotify={onNotify}
onSubmit={(payload) => void handleRoleSubmit(payload)}
onClose={() => setDialogOpen(false)}
/>
)}
{permissionDialogOpen && permissionTarget && (
<PermissionEditDialog
open={permissionDialogOpen}
target={permissionTarget}
permissions={permissions}
currentCodes={permissionTarget.permission_codes}
saving={saving}
onSubmit={(codes) => void savePermissions(codes)}
onClose={() => setPermissionDialogOpen(false)}
/>
)}
<ConfirmDialog
open={deleteTarget !== null}
onOpenChange={(nextOpen) => {
if (!nextOpen) setDeleteTarget(null);
}}
title="确定删除角色?"
description={
deleteTarget
? `确定删除角色"${deleteTarget.role_name}"吗?`
: ""
}
confirmLabel="删除"
destructive
onConfirm={async () => {
if (!deleteTarget || deleteTarget.is_builtin) return;
await executeRemoveRole(deleteTarget);
setDeleteTarget(null);
}}
/>
</section>
);
}