feat: role CRUD page
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
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 { PermissionEditDialog } from "./PermissionEditDialog";
|
||||
import { RoleEditDialog } from "./RoleEditDialog";
|
||||
|
||||
import "../../styles/admin.css";
|
||||
|
||||
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 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 removeRole = async (role: Role): Promise<void> => {
|
||||
if (role.is_builtin) return;
|
||||
if (!window.confirm(`确定删除角色"${role.role_name}"吗?`)) return;
|
||||
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="admin-page">
|
||||
<div className="admin-toolbar">
|
||||
<div className="admin-toolbar__search">
|
||||
<Search size={14} className="text-[#8a99a8]" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索角色编码 / 名称"
|
||||
className="admin-toolbar__input"
|
||||
value={searchTerm}
|
||||
onChange={(event) => setSearchTerm(event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={openCreate}
|
||||
className="h-9 gap-1.5 rounded-[6px] bg-gradient-to-br from-[#2e86de] to-[#1978d4] px-3.5 text-[12px] font-semibold text-white shadow-[0_4px_12px_rgb(28_119_222/18%)] enabled:hover:from-[#1f7ed8] enabled:hover:to-[#1669c0]"
|
||||
>
|
||||
<Plus size={15} />
|
||||
新建角色
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!canManage && (
|
||||
<div className="admin-readonly">当前为开发人员,只能查看角色列表。</div>
|
||||
)}
|
||||
{loadError && <div className="admin-readonly">{loadError}</div>}
|
||||
|
||||
<div className="role-table">
|
||||
<div className="role-table__head">
|
||||
<span>角色编码</span><span>角色名称</span><span>类型</span><span>权限</span><span>操作</span>
|
||||
</div>
|
||||
{loading ? (
|
||||
<p className="admin-empty">正在加载角色…</p>
|
||||
) : filteredRoles.length === 0 ? (
|
||||
<p className="admin-empty">
|
||||
{roles.length === 0 ? "暂无角色" : "没有匹配的角色"}
|
||||
</p>
|
||||
) : (
|
||||
filteredRoles.map((role) => (
|
||||
<div className="role-row" key={role.role_id}>
|
||||
<code className="role-code">{role.role_code}</code>
|
||||
<span className="role-name">{role.role_name}</span>
|
||||
<span className={`builtin-badge${role.is_builtin ? "" : " is-custom"}`}>
|
||||
{role.is_builtin ? "内置" : "自定义"}
|
||||
</span>
|
||||
<span className="permission-count">
|
||||
{role.permission_codes.length} 项权限
|
||||
</span>
|
||||
<span className="role-actions">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={() => openEdit(role)}
|
||||
className="h-6 gap-1 rounded-[4px] border-[#d9e3ec] bg-white px-[9px] text-[11px] text-[#4c6c88] hover:bg-[#f7fafc] disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
>
|
||||
编辑
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="xs"
|
||||
type="button"
|
||||
disabled={!canManage}
|
||||
onClick={() => openPermissions(role)}
|
||||
className="h-6 gap-1 rounded-[4px] border-[#d9e3ec] bg-white px-[9px] text-[11px] text-[#4c6c88] hover:bg-[#f7fafc] disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
>
|
||||
权限
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="xs"
|
||||
type="button"
|
||||
disabled={!canManage || role.is_builtin}
|
||||
title={role.is_builtin ? "内置角色不可删除" : "删除角色"}
|
||||
onClick={() => void removeRole(role)}
|
||||
className="h-6 gap-1 rounded-[4px] border-red-200 bg-white px-[9px] text-[11px] text-red-700 hover:bg-red-50 disabled:cursor-not-allowed disabled:opacity-[0.45]"
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{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)}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user