283 lines
9.3 KiB
TypeScript
283 lines
9.3 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 { 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>
|
|
);
|
|
}
|