feat: 1. 将大部分的请求从fetch改成axios方便管理。
2. 给文档类型添加入口模块和相关数据的渲染。并且给文档类型进行功能上的角色权限区分 3. 新增角色权限管理页面
This commit is contained in:
@@ -179,6 +179,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
console.log("✅ [Callback] 后端登录成功,JWT token 已获取");
|
||||
const frontendJWT = loginResponse.data.access_token;
|
||||
const savedUserInfo = loginResponse.data.user_info;
|
||||
const backExpiresIn = loginResponse.data.expires_in || (60 * 60 * 8)
|
||||
|
||||
// 🔑 提取后端返回的签发时间并转换为时间戳
|
||||
let tokenIssuedAt = Date.now(); // 默认使用当前时间
|
||||
@@ -235,7 +236,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
redirectTo: callbackUrl.toString(), // 先跳转到 callback 页面保存 token
|
||||
accessToken: tokenResponse.access_token,
|
||||
refreshToken: tokenResponse.refresh_token,
|
||||
tokenExpiresIn: tokenResponse.expires_in,
|
||||
tokenExpiresIn: backExpiresIn,
|
||||
tokenIssuedAt: tokenIssuedAt, // 🔑 传递后端返回的签发时间
|
||||
userInfo: enhancedUserInfo,
|
||||
frontendJWT
|
||||
|
||||
@@ -6,14 +6,14 @@ import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import { getRuleTypes, getRuleGroupsByType, type RuleType, type RuleGroup } from "~/api/evaluation_points/rules";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import {
|
||||
getDocumentTypes,
|
||||
deleteDocumentType,
|
||||
type DocumentTypeUI,
|
||||
import {
|
||||
getDocumentTypes,
|
||||
deleteDocumentType,
|
||||
type DocumentTypeUI,
|
||||
type DocumentTypeSearchParams,
|
||||
type DocumentTypeGroup
|
||||
type DocumentTypeGroup,
|
||||
getParentEvaluationPointGroups
|
||||
} from "~/api/document-types/document-types";
|
||||
import documentTypesStyles from "~/styles/pages/document-types_index.css?url";
|
||||
|
||||
@@ -40,8 +40,7 @@ interface LoaderData {
|
||||
pageSize: number;
|
||||
currentPage: number;
|
||||
error?: string;
|
||||
groups: DocumentTypeGroup[];
|
||||
ruleTypes: RuleType[];
|
||||
parentGroups: DocumentTypeGroup[];
|
||||
frontendJWT?: string | null;
|
||||
}
|
||||
|
||||
@@ -69,11 +68,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
};
|
||||
|
||||
// 并行获取文档类型数据和父级评查点分组
|
||||
const ruleTypesResponse = await getRuleTypes(undefined, frontendJWT);
|
||||
if(ruleTypesResponse.error){
|
||||
console.error("获取父级评查点分组失败:", ruleTypesResponse.error);
|
||||
const parentGroupsResponse = await getParentEvaluationPointGroups(frontendJWT);
|
||||
if(parentGroupsResponse.error){
|
||||
console.error("获取父级评查点分组失败:", parentGroupsResponse.error);
|
||||
}
|
||||
const ruleTypes = ruleTypesResponse.error ? [] : ruleTypesResponse.data;
|
||||
const parentGroups = parentGroupsResponse.error ? [] : (parentGroupsResponse.data || []);
|
||||
|
||||
const typesResponse = await getDocumentTypes(searchParams, frontendJWT);
|
||||
if(typesResponse.error){
|
||||
@@ -81,16 +80,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
throw new Error(typesResponse.error);
|
||||
}
|
||||
const typesResult = typesResponse.data?.types || [];
|
||||
|
||||
// console.log('文档类型数据:', typesResult.data?.types);
|
||||
// console.log('父级评查点分组:', groupsResult.data);
|
||||
|
||||
|
||||
// console.log('文档类型数据:', typesResult);
|
||||
// console.log('父级评查点分组:', parentGroups);
|
||||
|
||||
return Response.json({
|
||||
types: typesResult,
|
||||
total: typesResponse.data?.total || typesResult.length,
|
||||
pageSize,
|
||||
currentPage: page,
|
||||
ruleTypes,
|
||||
parentGroups,
|
||||
frontendJWT
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -140,58 +139,18 @@ export default function DocumentTypesList() {
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 获取加载器数据
|
||||
const { types, total, error, ruleTypes, frontendJWT } = useLoaderData<LoaderData>();
|
||||
const { types, total, error, parentGroups, frontendJWT } = useLoaderData<LoaderData>();
|
||||
|
||||
// 获取用户角色并判断权限
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
|
||||
// 状态管理
|
||||
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
|
||||
const [loadingGroups, setLoadingGroups] = useState(false);
|
||||
|
||||
// 获取当前的ruleType值
|
||||
const ruleTypeParam = searchParams.get('ruleType');
|
||||
|
||||
// 获取搜索参数
|
||||
const name = searchParams.get('name') || '';
|
||||
const currentPage = parseInt(searchParams.get('page') || String(1), 10);
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || String(10), 10);
|
||||
|
||||
// 判断是否禁用子级评查分组选择,true表示禁用,false表示不禁用
|
||||
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
|
||||
|
||||
// 当评查点类型变化时,加载对应的子级评查分组
|
||||
useEffect(() => {
|
||||
// 如果选择了"全部"或未选择,则清空子级评查分组
|
||||
if (!ruleTypeParam || ruleTypeParam === 'all') {
|
||||
setRuleGroups([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 加载当前类型的子级评查分组
|
||||
const loadRuleGroups = async () => {
|
||||
setLoadingGroups(true);
|
||||
try {
|
||||
const response = await getRuleGroupsByType(ruleTypeParam, frontendJWT || undefined);
|
||||
if (response.data) {
|
||||
setRuleGroups(response.data);
|
||||
} else if (response.error) {
|
||||
console.error('加载子级规则组失败:', response.error);
|
||||
setRuleGroups([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载子级规则组出错:', error);
|
||||
toastService.error('加载子级规则组出错:' + error);
|
||||
setRuleGroups([]);
|
||||
} finally {
|
||||
setLoadingGroups(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadRuleGroups();
|
||||
}, [ruleTypeParam]);
|
||||
|
||||
// 处理loader加载数据的时候的错误
|
||||
useEffect(() => {
|
||||
if(error){
|
||||
@@ -216,36 +175,16 @@ export default function DocumentTypesList() {
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
|
||||
// 如果是子级评查分组选择,但是当前应该被禁用,则不处理
|
||||
if (name === 'groupId' && isRuleGroupSelectDisabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (value) {
|
||||
newParams.set(name, value);
|
||||
|
||||
// 如果是评查点类型变更,清空子级评查分组选择
|
||||
if (name === 'ruleType') {
|
||||
newParams.delete('groupId');
|
||||
// 如果选择了"全部"或空值,也清空子级评查分组选择
|
||||
if (value === '' || value === 'all') {
|
||||
setRuleGroups([]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newParams.delete(name);
|
||||
|
||||
// 如果清除评查点类型,也清除规则组
|
||||
if (name === 'ruleType') {
|
||||
newParams.delete('groupId');
|
||||
setRuleGroups([]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 切换筛选条件时,重置到第一页
|
||||
newParams.set('page', '1');
|
||||
|
||||
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
@@ -317,7 +256,7 @@ export default function DocumentTypesList() {
|
||||
{
|
||||
title: "文档类型名称",
|
||||
key: "name",
|
||||
width: "200px",
|
||||
width: "180px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="flex items-center">
|
||||
<i className="ri-file-text-line text-primary mr-2"></i>
|
||||
@@ -328,13 +267,27 @@ export default function DocumentTypesList() {
|
||||
{
|
||||
title: "描述",
|
||||
key: "description",
|
||||
width: "300px",
|
||||
width: "250px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="text-secondary text-sm truncate max-w-xs" title={record.description}>
|
||||
{record.description}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "入口模块",
|
||||
key: "entry_module",
|
||||
width: "150px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<div className="flex items-center">
|
||||
{record.entry_module ? (
|
||||
<span className="type-badge">{record.entry_module.name}</span>
|
||||
) : (
|
||||
<span className="text-gray-400">暂无关联入口</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "关联的评查点分组",
|
||||
key: "groups",
|
||||
@@ -436,29 +389,14 @@ export default function DocumentTypesList() {
|
||||
name="ruleType"
|
||||
value={searchParams.get('ruleType') || ''}
|
||||
options={[
|
||||
...(ruleTypes || []).map(type => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
...(parentGroups || []).map(group => ({
|
||||
value: group.id,
|
||||
label: group.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-3 w-[20%]"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="所属子级评查分组"
|
||||
name="groupId"
|
||||
value={searchParams.get('groupId') || ''}
|
||||
options={[
|
||||
...(isRuleGroupSelectDisabled ? [{ value: "", label: "请先选择评查点类型" }] : []),
|
||||
...ruleGroups.map(group => ({
|
||||
value: group.id,
|
||||
label: group.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className={`mr-3 w-[20%] ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`}
|
||||
/>
|
||||
|
||||
<SearchFilter
|
||||
label="类型名称"
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import documentTypesNewStyles from "~/styles/pages/document-types_new.css?url";
|
||||
import { getAllRuleGroups, type RuleGroup } from "~/api/evaluation_points/rule-groups";
|
||||
import { getDocumentType, createDocumentType, updateDocumentType } from "~/api/document-types/document-types";
|
||||
import { getDocumentType, createDocumentType, updateDocumentType, getEntryModules } from "~/api/document-types/document-types";
|
||||
import { getPromptTemplates, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
|
||||
@@ -81,8 +81,15 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const groupsTree = ruleGroupsResponse.error ? [] : ruleGroupsResponse.data || [];
|
||||
|
||||
|
||||
// 2. 获取各类型的提示词模板
|
||||
const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse, evaluationTemplatesResponse, summaryTemplatesResponse] =
|
||||
// 2. 获取入口模块列表
|
||||
const entryModulesResponse = await getEntryModules(frontendJWT);
|
||||
if (entryModulesResponse.error) {
|
||||
console.error("获取入口模块失败:", entryModulesResponse.error);
|
||||
}
|
||||
const entryModules = entryModulesResponse.error ? [] : (entryModulesResponse.data || []);
|
||||
|
||||
// 3. 获取各类型的提示词模板
|
||||
const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse, evaluationTemplatesResponse, summaryTemplatesResponse] =
|
||||
await Promise.all([
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.LLM_EXTRACTION }, frontendJWT),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.VLM_EXTRACTION }, frontendJWT),
|
||||
@@ -103,6 +110,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
isEdit,
|
||||
documentType,
|
||||
ruleGroups: groupsTree,
|
||||
entryModules,
|
||||
llmExtractionTemplates: llmExtractionTemplatesResponse.data?.templates || [],
|
||||
vlmExtractionTemplates: vlmExtractionTemplatesResponse.data?.templates || [],
|
||||
evaluationTemplates: evaluationTemplatesResponse.data?.templates || [],
|
||||
@@ -114,6 +122,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
isEdit: false,
|
||||
documentType: undefined,
|
||||
ruleGroups: [],
|
||||
entryModules: [],
|
||||
llmExtractionTemplates: [],
|
||||
vlmExtractionTemplates: [],
|
||||
evaluationTemplates: [],
|
||||
@@ -132,6 +141,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const id = formData.get("id") as string | null;
|
||||
const name = formData.get("name") as string;
|
||||
const description = formData.get("description") as string;
|
||||
const entryModuleId = formData.get("entry_module_id") as string;
|
||||
const llmExtractionTemplateId = formData.get("llm_extraction_template") as string;
|
||||
const vlmExtractionTemplateId = formData.get("vlm_extraction_template") as string;
|
||||
const evaluationTemplateId = formData.get("evaluation_template") as string;
|
||||
@@ -179,6 +189,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
name,
|
||||
description,
|
||||
group_ids: selectedGroups,
|
||||
entry_module_id: entryModuleId ? parseInt(entryModuleId) : null,
|
||||
// 确保映射关系与prompt_config字段对应正确
|
||||
llm_extraction_template_id: llmExtractionTemplateId ? parseInt(llmExtractionTemplateId) : null,
|
||||
vlm_extraction_template_id: vlmExtractionTemplateId ? parseInt(vlmExtractionTemplateId) : null,
|
||||
@@ -222,9 +233,10 @@ export default function DocumentTypeNew() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const isEditMode = searchParams.has("id");
|
||||
|
||||
const {
|
||||
documentType,
|
||||
ruleGroups,
|
||||
const {
|
||||
documentType,
|
||||
ruleGroups,
|
||||
entryModules,
|
||||
llmExtractionTemplates,
|
||||
vlmExtractionTemplates,
|
||||
evaluationTemplates,
|
||||
@@ -244,6 +256,7 @@ export default function DocumentTypeNew() {
|
||||
id: documentType?.id || "",
|
||||
name: documentType?.name || "",
|
||||
description: documentType?.description || "",
|
||||
entryModuleId: documentType?.entry_module?.id?.toString() || "",
|
||||
llmExtractionTemplateId: documentType?.llm_extraction_template_id || "",
|
||||
vlmExtractionTemplateId: documentType?.vlm_extraction_template_id || "",
|
||||
evaluationTemplateId: documentType?.evaluation_template_id || "",
|
||||
@@ -287,6 +300,7 @@ export default function DocumentTypeNew() {
|
||||
id: documentType.id,
|
||||
name: documentType.name,
|
||||
description: documentType.description,
|
||||
entryModuleId: documentType.entry_module?.id?.toString() || "",
|
||||
llmExtractionTemplateId: documentType.llm_extraction_template_id || "",
|
||||
vlmExtractionTemplateId: documentType.vlm_extraction_template_id || "",
|
||||
evaluationTemplateId: documentType.evaluation_template_id || "",
|
||||
@@ -510,7 +524,30 @@ export default function DocumentTypeNew() {
|
||||
)}
|
||||
<div className="form-tip">例如:销售合同、采购合同、专卖许可证等</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* 入口模块 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="entry-module" className="form-label">
|
||||
入口模块
|
||||
</label>
|
||||
<select
|
||||
id="entry-module"
|
||||
name="entry_module_id"
|
||||
className="form-select"
|
||||
value={formData.entryModuleId}
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">无</option>
|
||||
{entryModules.map((module: { id: number; name: string }) => (
|
||||
<option key={module.id} value={module.id}>
|
||||
{module.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="form-tip">选择此文档类型对应的入口模块(可选)</div>
|
||||
</div>
|
||||
|
||||
{/* 类型描述 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="type-description" className="form-label">类型描述</label>
|
||||
@@ -670,30 +707,19 @@ export default function DocumentTypeNew() {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 子分组 */}
|
||||
{/* 子分组 - 仅展示,不可选 */}
|
||||
{group.children && group.children.length > 0 && expandedGroups[group.id] && (
|
||||
group.children.map((child: RuleGroup) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className={`checkbox-item child-checkbox-item ${formData.selectedGroups.includes(child.id) ? 'checked' : ''}`}
|
||||
className="checkbox-item child-checkbox-item"
|
||||
style={{ paddingLeft: '2.5rem', opacity: 0.9 }}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id={`group-${child.id}`}
|
||||
name="checkpoint_group_ids"
|
||||
value={child.id}
|
||||
checked={formData.selectedGroups.includes(child.id)}
|
||||
onChange={(e) => handleGroupCheckChange(child.id, e.target.checked)}
|
||||
className="radio-input"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`group-${child.id}`}
|
||||
className="checkbox-label"
|
||||
>
|
||||
<i className="ri-subtract-line" style={{ marginRight: '8px', color: '#9ca3af' }}></i>
|
||||
<span className="checkbox-label">
|
||||
{child.name}
|
||||
<span className="group-badge child-badge">二级分组</span>
|
||||
</label>
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { postgrestGet } from "~/api/postgrest-client";
|
||||
import { getUserSession } from "~/api/login/auth.server";
|
||||
|
||||
/**
|
||||
* 文档下载路由 - 处理文档下载请求
|
||||
* 通过重定向到带有授权的连接来允许下载文件
|
||||
*/
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
// 获取文件路径参数
|
||||
const url = new URL(request.url);
|
||||
const filePath = url.searchParams.get("path");
|
||||
|
||||
if (!filePath) {
|
||||
return new Response("缺少文件路径参数", { status: 400 });
|
||||
}
|
||||
|
||||
// 调用Minio API获取带有授权的预签名URL
|
||||
// 这里假设后端有一个生成预签名URL的API
|
||||
const response = await postgrestGet<{ presignedUrl: string }>(
|
||||
'/minio/presign',
|
||||
{
|
||||
filter: {
|
||||
'object_path': `eq.${filePath}`,
|
||||
'expires_in': 'eq.300' // 5分钟有效期
|
||||
},
|
||||
token: frontendJWT
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
console.error("获取文件下载链接失败:", response.error);
|
||||
return new Response("获取文件下载链接失败", { status: 500 });
|
||||
}
|
||||
|
||||
if (!response.data?.presignedUrl) {
|
||||
return new Response("无法获取文件下载链接", { status: 404 });
|
||||
}
|
||||
|
||||
// 重定向到预签名URL,这样浏览器就能直接下载文件
|
||||
return Response.redirect(response.data.presignedUrl);
|
||||
} catch (error) {
|
||||
console.error("文件下载处理失败:", error);
|
||||
return new Response(
|
||||
"文件下载处理失败: " + (error instanceof Error ? error.message : "未知错误"),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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