3850d05bdd
2. 给文档类型添加入口模块和相关数据的渲染。并且给文档类型进行功能上的角色权限区分 3. 新增角色权限管理页面
514 lines
17 KiB
TypeScript
514 lines
17 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { ClientLoaderFunctionArgs, ClientActionFunctionArgs } from "@remix-run/react";
|
|
import { Card } from "~/components/ui/Card";
|
|
import { Button } from "~/components/ui/Button";
|
|
import { toastService } from "~/components/ui/Toast";
|
|
import {
|
|
getRoles,
|
|
getRoutes,
|
|
getRoleRoutePermissions,
|
|
updateRoleRoutePermissions,
|
|
getRoleUsers,
|
|
getAllUsers,
|
|
assignUserRoles,
|
|
createRole,
|
|
updateRole,
|
|
deleteRole,
|
|
type RoleInfo,
|
|
type RouteInfo,
|
|
type UserInfo
|
|
} from "~/api/role-permissions/role-permissions";
|
|
import rolePermissionsStyles from "~/styles/pages/role-permissions.css?url";
|
|
|
|
// 引入样式
|
|
export function links() {
|
|
return [
|
|
{ rel: "stylesheet", href: rolePermissionsStyles }
|
|
];
|
|
}
|
|
|
|
// 页面元数据
|
|
export const meta = () => {
|
|
return [
|
|
{ title: "角色权限管理 - 中国烟草AI合同及卷宗审核系统" },
|
|
{ name: "description", content: "管理系统角色和权限分配" }
|
|
];
|
|
};
|
|
|
|
// ClientLoader - 加载初始数据
|
|
export async function clientLoader({ request }: ClientLoaderFunctionArgs) {
|
|
try {
|
|
const [roles, routes, users] = await Promise.all([
|
|
getRoles(),
|
|
getRoutes(),
|
|
getAllUsers()
|
|
]);
|
|
|
|
return {
|
|
roles,
|
|
routes,
|
|
users
|
|
};
|
|
} catch (error) {
|
|
console.error("加载数据失败:", error);
|
|
return {
|
|
roles: [],
|
|
routes: [],
|
|
users: []
|
|
};
|
|
}
|
|
}
|
|
|
|
// ClientAction - 处理用户操作
|
|
export async function clientAction({ request }: ClientActionFunctionArgs) {
|
|
const formData = await request.formData();
|
|
const action = formData.get("action") as string;
|
|
|
|
try {
|
|
switch (action) {
|
|
case "updatePermissions": {
|
|
const roleId = parseInt(formData.get("roleId") as string);
|
|
const routeIds = JSON.parse(formData.get("routeIds") as string);
|
|
const result = await updateRoleRoutePermissions(roleId, routeIds);
|
|
return result;
|
|
}
|
|
|
|
case "assignUserRoles": {
|
|
const userId = parseInt(formData.get("userId") as string);
|
|
const roleIds = JSON.parse(formData.get("roleIds") as string);
|
|
const result = await assignUserRoles(userId, roleIds);
|
|
return result;
|
|
}
|
|
|
|
case "createRole": {
|
|
const roleData = JSON.parse(formData.get("roleData") as string);
|
|
const result = await createRole(roleData);
|
|
return result;
|
|
}
|
|
|
|
case "updateRole": {
|
|
const roleId = parseInt(formData.get("roleId") as string);
|
|
const roleData = JSON.parse(formData.get("roleData") as string);
|
|
const result = await updateRole(roleId, roleData);
|
|
return result;
|
|
}
|
|
|
|
case "deleteRole": {
|
|
const roleId = parseInt(formData.get("roleId") as string);
|
|
const result = await deleteRole(roleId);
|
|
return result;
|
|
}
|
|
|
|
default:
|
|
return { success: false, message: "未知操作" };
|
|
}
|
|
} catch (error) {
|
|
console.error("操作失败:", error);
|
|
return {
|
|
success: false,
|
|
message: error instanceof Error ? error.message : "操作失败"
|
|
};
|
|
}
|
|
}
|
|
|
|
// 主组件
|
|
export default function RolePermissions() {
|
|
const [roles, setRoles] = useState<RoleInfo[]>([]);
|
|
const [routes, setRoutes] = useState<RouteInfo[]>([]);
|
|
const [users, setUsers] = useState<UserInfo[]>([]);
|
|
const [selectedRole, setSelectedRole] = useState<RoleInfo | null>(null);
|
|
const [activeTab, setActiveTab] = useState<'permissions' | 'users'>('permissions');
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
// 路由权限相关状态
|
|
const [selectedRouteIds, setSelectedRouteIds] = useState<number[]>([]);
|
|
const [roleUsers, setRoleUsers] = useState<UserInfo[]>([]);
|
|
|
|
// 加载初始数据
|
|
useEffect(() => {
|
|
loadData();
|
|
}, []);
|
|
|
|
const loadData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [rolesData, routesData, usersData] = await Promise.all([
|
|
getRoles(),
|
|
getRoutes(),
|
|
getAllUsers()
|
|
]);
|
|
|
|
setRoles(rolesData);
|
|
setRoutes(routesData);
|
|
setUsers(usersData);
|
|
|
|
// 默认选中第一个角色
|
|
if (rolesData.length > 0) {
|
|
handleSelectRole(rolesData[0]);
|
|
}
|
|
} catch (error) {
|
|
console.error("加载数据失败:", error);
|
|
toastService.error("加载数据失败");
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
// 选择角色
|
|
const handleSelectRole = async (role: RoleInfo) => {
|
|
setSelectedRole(role);
|
|
|
|
// 加载该角色的权限
|
|
const permissions = await getRoleRoutePermissions(role.id);
|
|
const routeIds = permissions.map(p => p.route_id);
|
|
setSelectedRouteIds(routeIds);
|
|
|
|
// 加载该角色的用户列表
|
|
const users = await getRoleUsers(role.id);
|
|
setRoleUsers(users);
|
|
};
|
|
|
|
// 递归获取所有路由ID(包括子路由)
|
|
const getAllRouteIds = (routes: RouteInfo[]): number[] => {
|
|
let ids: number[] = [];
|
|
routes.forEach(route => {
|
|
ids.push(route.id);
|
|
if (route.children && route.children.length > 0) {
|
|
ids = ids.concat(getAllRouteIds(route.children));
|
|
}
|
|
});
|
|
return ids;
|
|
};
|
|
|
|
// 切换路由权限
|
|
const handleToggleRoute = (routeId: number, checked: boolean) => {
|
|
if (checked) {
|
|
setSelectedRouteIds([...selectedRouteIds, routeId]);
|
|
} else {
|
|
setSelectedRouteIds(selectedRouteIds.filter(id => id !== routeId));
|
|
}
|
|
};
|
|
|
|
// 切换父路由(包括所有子路由)
|
|
const handleToggleParentRoute = (route: RouteInfo, checked: boolean) => {
|
|
const childIds = route.children ? getAllRouteIds(route.children) : [];
|
|
const allIds = [route.id, ...childIds];
|
|
|
|
if (checked) {
|
|
const newIds = [...selectedRouteIds, ...allIds].filter(
|
|
(id, index, self) => self.indexOf(id) === index
|
|
);
|
|
setSelectedRouteIds(newIds);
|
|
} else {
|
|
setSelectedRouteIds(
|
|
selectedRouteIds.filter(id => !allIds.includes(id))
|
|
);
|
|
}
|
|
};
|
|
|
|
// 保存权限
|
|
const handleSavePermissions = async () => {
|
|
if (!selectedRole) return;
|
|
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append("action", "updatePermissions");
|
|
formData.append("roleId", selectedRole.id.toString());
|
|
formData.append("routeIds", JSON.stringify(selectedRouteIds));
|
|
|
|
const response = await fetch("/role-permissions", {
|
|
method: "POST",
|
|
body: formData
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
toastService.success(result.message);
|
|
} else {
|
|
toastService.error(result.message);
|
|
}
|
|
} catch (error) {
|
|
console.error("保存权限失败:", error);
|
|
toastService.error("保存权限失败");
|
|
}
|
|
};
|
|
|
|
// 渲染路由树
|
|
const renderRouteTree = (routes: RouteInfo[], level = 0) => {
|
|
return routes.map(route => {
|
|
const hasChildren = route.children && route.children.length > 0;
|
|
const isChecked = selectedRouteIds.includes(route.id);
|
|
const allChildIds = hasChildren ? getAllRouteIds(route.children!) : [];
|
|
const checkedChildCount = allChildIds.filter(id =>
|
|
selectedRouteIds.includes(id)
|
|
).length;
|
|
const isIndeterminate =
|
|
hasChildren && checkedChildCount > 0 && checkedChildCount < allChildIds.length;
|
|
|
|
return (
|
|
<div key={route.id} className="route-item" style={{ paddingLeft: `${level * 20}px` }}>
|
|
<div className="route-item-content">
|
|
<input
|
|
type="checkbox"
|
|
id={`route-${route.id}`}
|
|
checked={isChecked}
|
|
ref={el => {
|
|
if (el) el.indeterminate = isIndeterminate;
|
|
}}
|
|
onChange={(e) => {
|
|
if (hasChildren) {
|
|
handleToggleParentRoute(route, e.target.checked);
|
|
} else {
|
|
handleToggleRoute(route.id, e.target.checked);
|
|
}
|
|
}}
|
|
className="route-checkbox"
|
|
/>
|
|
<label htmlFor={`route-${route.id}`} className="route-label">
|
|
{route.icon && <i className={`${route.icon} route-icon`}></i>}
|
|
<span className="route-title">{route.route_title}</span>
|
|
<span className="route-path">{route.route_path}</span>
|
|
</label>
|
|
</div>
|
|
|
|
{hasChildren && (
|
|
<div className="route-children">
|
|
{renderRouteTree(route.children!, level + 1)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
});
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="role-permissions-page">
|
|
<div className="loading-container">
|
|
<i className="ri-loader-4-line spin"></i>
|
|
<span>加载中...</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="role-permissions-page">
|
|
{/* 页面头部 */}
|
|
<div className="page-header">
|
|
<h2 className="page-title">
|
|
<i className="ri-shield-user-line"></i>
|
|
角色权限管理
|
|
</h2>
|
|
<div className="page-actions">
|
|
<Button
|
|
type="primary"
|
|
icon="ri-add-line"
|
|
onClick={() => {
|
|
toastService.info("创建角色功能开发中...");
|
|
}}
|
|
>
|
|
新建角色
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="permissions-container">
|
|
{/* 左侧:角色列表 */}
|
|
<Card className="roles-panel" title="角色列表" bodyClassName="p-0">
|
|
<div className="roles-list">
|
|
{roles.map(role => (
|
|
<div
|
|
key={role.id}
|
|
className={`role-item ${selectedRole?.id === role.id ? 'active' : ''}`}
|
|
onClick={() => handleSelectRole(role)}
|
|
>
|
|
<div className="role-info">
|
|
<div className="role-header">
|
|
<span className="role-name">{role.role_name}</span>
|
|
{role.is_system_role && (
|
|
<span className="system-badge">系统角色</span>
|
|
)}
|
|
</div>
|
|
<div className="role-key">{role.role_key}</div>
|
|
<div className="role-desc">{role.description}</div>
|
|
<div className="role-meta">
|
|
<span className="data-scope">
|
|
<i className="ri-database-line"></i>
|
|
{role.data_scope}
|
|
</span>
|
|
<span className="priority">
|
|
<i className="ri-sort-asc"></i>
|
|
优先级: {role.priority}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{!role.is_system_role && (
|
|
<div className="role-actions">
|
|
<button
|
|
className="btn-icon"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
toastService.info("编辑角色功能开发中...");
|
|
}}
|
|
title="编辑"
|
|
>
|
|
<i className="ri-edit-line"></i>
|
|
</button>
|
|
<button
|
|
className="btn-icon text-error"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
if (confirm(`确定要删除角色"${role.role_name}"吗?`)) {
|
|
toastService.info("删除角色功能开发中...");
|
|
}
|
|
}}
|
|
title="删除"
|
|
>
|
|
<i className="ri-delete-bin-line"></i>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
|
|
{/* 右侧:角色详情和权限设置 */}
|
|
<div className="permissions-detail">
|
|
{selectedRole ? (
|
|
<>
|
|
{/* Tab 切换 */}
|
|
<Card className="tabs-card">
|
|
<div className="tabs-header">
|
|
<button
|
|
className={`tab-btn ${activeTab === 'permissions' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('permissions')}
|
|
>
|
|
<i className="ri-shield-check-line"></i>
|
|
路由权限
|
|
</button>
|
|
<button
|
|
className={`tab-btn ${activeTab === 'users' ? 'active' : ''}`}
|
|
onClick={() => setActiveTab('users')}
|
|
>
|
|
<i className="ri-team-line"></i>
|
|
用户列表 ({roleUsers.length})
|
|
</button>
|
|
</div>
|
|
|
|
<div className="tabs-content">
|
|
{/* 路由权限Tab */}
|
|
{activeTab === 'permissions' && (
|
|
<div className="permissions-tab">
|
|
<div className="permissions-header">
|
|
<h3>为角色 "{selectedRole.role_name}" 分配路由权限</h3>
|
|
<Button
|
|
type="primary"
|
|
icon="ri-save-line"
|
|
onClick={handleSavePermissions}
|
|
>
|
|
保存权限
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="routes-tree">
|
|
{renderRouteTree(routes)}
|
|
</div>
|
|
|
|
<div className="permissions-summary">
|
|
<i className="ri-information-line"></i>
|
|
已选择 <strong>{selectedRouteIds.length}</strong> 个路由权限
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 用户列表Tab */}
|
|
{activeTab === 'users' && (
|
|
<div className="users-tab">
|
|
<div className="users-header">
|
|
<h3>拥有角色 "{selectedRole.role_name}" 的用户</h3>
|
|
<Button
|
|
type="primary"
|
|
icon="ri-user-add-line"
|
|
onClick={() => {
|
|
toastService.info("分配用户功能开发中...");
|
|
}}
|
|
>
|
|
分配用户
|
|
</Button>
|
|
</div>
|
|
|
|
<div className="users-list">
|
|
{roleUsers.length > 0 ? (
|
|
roleUsers.map(user => (
|
|
<div key={user.id} className="user-card">
|
|
<div className="user-avatar">
|
|
<i className="ri-user-line"></i>
|
|
</div>
|
|
<div className="user-info">
|
|
<div className="user-name">
|
|
{user.nick_name}
|
|
{user.is_leader && (
|
|
<span className="leader-badge">负责人</span>
|
|
)}
|
|
</div>
|
|
<div className="user-username">@{user.username}</div>
|
|
<div className="user-org">{user.ou_name}</div>
|
|
<div className="user-contact">
|
|
{user.phone_number && (
|
|
<span>
|
|
<i className="ri-phone-line"></i>
|
|
{user.phone_number}
|
|
</span>
|
|
)}
|
|
{user.email && (
|
|
<span>
|
|
<i className="ri-mail-line"></i>
|
|
{user.email}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="user-actions">
|
|
<button
|
|
className="btn-icon text-error"
|
|
onClick={() => {
|
|
if (confirm(`确定要移除用户"${user.nick_name}"的该角色吗?`)) {
|
|
toastService.info("移除角色功能开发中...");
|
|
}
|
|
}}
|
|
title="移除角色"
|
|
>
|
|
<i className="ri-user-unfollow-line"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="empty-state">
|
|
<i className="ri-user-line"></i>
|
|
<p>暂无用户拥有此角色</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</>
|
|
) : (
|
|
<Card>
|
|
<div className="empty-state">
|
|
<i className="ri-shield-line"></i>
|
|
<p>请选择一个角色查看详情</p>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|