343 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|