feat: 1. 将大部分的请求从fetch改成axios方便管理。
2. 给文档类型添加入口模块和相关数据的渲染。并且给文档类型进行功能上的角色权限区分 3. 新增角色权限管理页面
This commit is contained in:
@@ -0,0 +1,513 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user