feat: 1. 本地化思源黑体的字体包并优先使用。
2. 添加权限映射表和全局查看权限的hook,便于路由控制不同权限按钮显示/隐藏。 3. 删除评查点分组的部分旧api方法。 4. 对接评查点分组接口,文档类型接口, 提示词管理接口, 入口模块管理的接口。 5. 优化角色权限管理的接口,完善不用地区的访问权限认证。 6. 优化主页交叉评查和设置的入口样式和布局。 7. 优化评查点分组,评查规则的功能权限校验。
This commit is contained in:
+82
-56
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate, Form, useLoaderData } from '@remix-run/react';
|
||||
import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import styles from "~/styles/pages/home.css?url";
|
||||
@@ -52,6 +52,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 🔑 检查用户是否有系统设置权限
|
||||
let hasSettingsAccess = false;
|
||||
let hasCrossCheckingAccess = false;
|
||||
let hasChatLLMAccess = false;
|
||||
let settingsChildren: { path: string; title: string }[] = [];
|
||||
|
||||
if (userRole && frontendJWT) {
|
||||
@@ -74,14 +75,19 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 检查是否存在顶级路由 '/cross-checking'
|
||||
hasCrossCheckingAccess = routesResult.data.some(route => route.path === '/cross-checking');
|
||||
|
||||
// 检查是否存在顶级路由 '/chat-with-llm'
|
||||
hasChatLLMAccess = routesResult.data.some(route => route.path === '/chat-with-llm');
|
||||
|
||||
// console.log(`🔑 [Index Loader] 用户${hasSettingsAccess ? '有' : '没有'}系统设置权限`);
|
||||
// console.log(`🔑 [Index Loader] 系统设置子路由数量: ${settingsChildren.length}`);
|
||||
// console.log(`🔑 [Index Loader] 用户${hasCrossCheckingAccess ? '有' : '没有'}交叉评查权限`);
|
||||
// console.log(`🔑 [Index Loader] 用户${hasChatLLMAccess ? '有' : '没有'}智慧法务大模型权限`);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回用户信息、入口模块和权限给客户端
|
||||
return Response.json({ userRole, userInfo, entryModules, hasSettingsAccess, hasCrossCheckingAccess, settingsChildren });
|
||||
return Response.json({ userRole, userInfo, entryModules, hasSettingsAccess, hasCrossCheckingAccess, hasChatLLMAccess, settingsChildren });
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
@@ -310,6 +316,17 @@ export default function Index() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="user-info">
|
||||
{/* 系统设置按钮 - 只在有权限时显示 */}
|
||||
{loaderData.hasSettingsAccess && (
|
||||
<button
|
||||
onClick={handleEnterSettings}
|
||||
className="settings-button"
|
||||
aria-label="系统设置"
|
||||
title="系统设置"
|
||||
>
|
||||
<i className="ri-settings-4-line"></i>
|
||||
</button>
|
||||
)}
|
||||
<span className="datetime">{currentDateTime.date} {currentDateTime.time}</span>
|
||||
<div className="user">
|
||||
{(() => {
|
||||
@@ -340,67 +357,76 @@ export default function Index() {
|
||||
<div className="index-main-content-container">
|
||||
<h1 className="welcome-text">- 欢迎来到智慧法务平台 -</h1>
|
||||
|
||||
{/* 模块网格区域 */}
|
||||
<div className="modules-container">
|
||||
{/* 动态渲染入口模块 */}
|
||||
{loaderData.entryModules && loaderData.entryModules.length > 0 ? (
|
||||
<>
|
||||
{loaderData.entryModules.map((module) => (
|
||||
<div
|
||||
key={module.id}
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick(module)}
|
||||
onKeyDown={(e) => handleKeyDown(module, e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={module.name}
|
||||
>
|
||||
<img
|
||||
src={getModuleIcon(module)}
|
||||
alt={module.name}
|
||||
className="w-12 h-12 mx-1"
|
||||
/>
|
||||
<span className="module-name">{module.name}</span>
|
||||
</div>
|
||||
))}
|
||||
{loaderData.entryModules.map((module) => {
|
||||
// 判断是否为智慧法务大模型,如果是且有交叉评查权限,则在其之前插入交叉评查卡片
|
||||
const isLLMModule = module.name === '智慧法务大模型';
|
||||
|
||||
{/* 🔑 交叉评查入口 - 只有有权限的用户才能看到 */}
|
||||
{loaderData.hasCrossCheckingAccess && (
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={handleEnterCrossChecking}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleEnterCrossChecking();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="交叉评查"
|
||||
>
|
||||
<i className="ri-shuffle-line text-5xl text-primary"></i>
|
||||
<span className="module-name">交叉评查</span>
|
||||
</div>
|
||||
)}
|
||||
// 🔑 如果是智慧法务大模型且用户没有访问权限,则不渲染该模块
|
||||
if (isLLMModule && !loaderData.hasChatLLMAccess) {
|
||||
return null;
|
||||
}
|
||||
|
||||
{/* 🔑 系统设置入口 - 只有有权限的用户才能看到 */}
|
||||
{loaderData.hasSettingsAccess && (
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={handleEnterSettings}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleEnterSettings();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="系统设置"
|
||||
>
|
||||
<i className="ri-settings-4-line text-5xl text-primary"></i>
|
||||
<span className="module-name">系统设置</span>
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<React.Fragment key={module.id}>
|
||||
{/* 在智慧法务大模型之前插入交叉评查入口 */}
|
||||
{isLLMModule && loaderData.hasCrossCheckingAccess && (
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={handleEnterCrossChecking}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleEnterCrossChecking();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="交叉评查"
|
||||
>
|
||||
<img
|
||||
src="/images/icon_cross_checking.png"
|
||||
alt="交叉评查"
|
||||
className="w-12 h-12 mx-1"
|
||||
onError={(e) => {
|
||||
// 如果图片加载失败,使用 icon
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
const parent = (e.target as HTMLImageElement).parentElement;
|
||||
if (parent) {
|
||||
const icon = document.createElement('i');
|
||||
icon.className = 'ri-shuffle-line';
|
||||
icon.style.fontSize = '48px';
|
||||
icon.style.color = 'var(--color-primary)';
|
||||
parent.insertBefore(icon, parent.firstChild);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span className="module-name">交叉评查</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 渲染原有模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick(module)}
|
||||
onKeyDown={(e) => handleKeyDown(module, e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={module.name}
|
||||
>
|
||||
<img
|
||||
src={getModuleIcon(module)}
|
||||
alt={module.name}
|
||||
className="w-12 h-12 mx-1"
|
||||
/>
|
||||
<span className="module-name">{module.name}</span>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSearchParams, useNavigate, useLoaderData, useRouteLoaderData } from "@remix-run/react";
|
||||
import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
@@ -8,6 +8,7 @@ import { Pagination } from "~/components/ui/Pagination";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import { usePermission } from "~/hooks/usePermission";
|
||||
import {
|
||||
getDocumentTypes,
|
||||
deleteDocumentType,
|
||||
@@ -51,39 +52,50 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
|
||||
const url = new URL(request.url);
|
||||
const name = url.searchParams.get('name') || undefined;
|
||||
const ruleType = url.searchParams.get('ruleType') || undefined;
|
||||
const groupId = url.searchParams.get('groupId') || undefined;
|
||||
const groupId = url.searchParams.get('group_id') || undefined;
|
||||
const entryModuleId = url.searchParams.get('entry_module_id') || undefined;
|
||||
const page = parseInt(url.searchParams.get('page') || '1', 10);
|
||||
const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10);
|
||||
|
||||
|
||||
// 构建搜索参数
|
||||
const searchParams: DocumentTypeSearchParams = {
|
||||
name,
|
||||
ruleType,
|
||||
groupId,
|
||||
group_id: groupId ? parseInt(groupId, 10) : undefined,
|
||||
entry_module_id: entryModuleId ? parseInt(entryModuleId, 10) : undefined,
|
||||
page,
|
||||
pageSize
|
||||
};
|
||||
|
||||
|
||||
// 并行获取文档类型数据和父级评查点分组
|
||||
const parentGroupsResponse = await getParentEvaluationPointGroups(frontendJWT);
|
||||
const [parentGroupsResponse, typesResponse] = await Promise.all([
|
||||
getParentEvaluationPointGroups(frontendJWT),
|
||||
getDocumentTypes(searchParams, frontendJWT)
|
||||
]);
|
||||
|
||||
// 如果获取父级评查点分组失败,返回空数组(不阻塞页面加载)
|
||||
if(parentGroupsResponse.error){
|
||||
console.error("获取父级评查点分组失败:", parentGroupsResponse.error);
|
||||
}
|
||||
const parentGroups = parentGroupsResponse.error ? [] : (parentGroupsResponse.data || []);
|
||||
|
||||
const typesResponse = await getDocumentTypes(searchParams, frontendJWT);
|
||||
// 如果获取文档类型失败(如403无权限),返回空数组和错误信息
|
||||
if(typesResponse.error){
|
||||
console.error("获取文档类型失败:", typesResponse.error);
|
||||
throw new Error(typesResponse.error);
|
||||
return Response.json({
|
||||
types: [],
|
||||
total: 0,
|
||||
pageSize,
|
||||
currentPage: page,
|
||||
parentGroups: [],
|
||||
frontendJWT,
|
||||
error: typesResponse.error
|
||||
});
|
||||
}
|
||||
const typesResult = typesResponse.data?.types || [];
|
||||
|
||||
// console.log('文档类型数据:', typesResult);
|
||||
// console.log('父级评查点分组:', parentGroups);
|
||||
const typesResult = typesResponse.data?.types || [];
|
||||
|
||||
return Response.json({
|
||||
types: typesResult,
|
||||
@@ -95,12 +107,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载文档类型列表失败:", error);
|
||||
return Response.json(
|
||||
{
|
||||
error: error || "加载文档类型列表失败",
|
||||
status: 500
|
||||
}
|
||||
);
|
||||
const errorMessage = error instanceof Error ? error.message : "加载文档类型列表失败";
|
||||
return Response.json({
|
||||
types: [],
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
parentGroups: [],
|
||||
frontendJWT: null,
|
||||
error: errorMessage
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,20 +158,32 @@ export default function DocumentTypesList() {
|
||||
// 获取加载器数据
|
||||
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 { canCreate, canUpdate, canDelete, canView } = usePermission();
|
||||
const canCreateType = canCreate('document_type');
|
||||
const canUpdateType = canUpdate('document_type');
|
||||
const canDeleteType = canDelete('document_type');
|
||||
const canViewType = canView('document_type');
|
||||
|
||||
// 获取搜索参数
|
||||
const name = searchParams.get('name') || '';
|
||||
const currentPage = parseInt(searchParams.get('page') || String(1), 10);
|
||||
const pageSize = parseInt(searchParams.get('pageSize') || String(10), 10);
|
||||
|
||||
// 处理测试loader返回的信息
|
||||
useEffect(() => {
|
||||
console.log('返回的父级评查点分组数据',parentGroups)
|
||||
}, [parentGroups])
|
||||
|
||||
// 处理loader加载数据的时候的错误
|
||||
useEffect(() => {
|
||||
if(error){
|
||||
toastService.error(error);
|
||||
// 如果是无权限错误,显示友好提示
|
||||
if(error.includes('Permission denied') || error.includes('无权限') || error.includes('权限不足')){
|
||||
toastService.error('无权限访问文档类型管理,请联系系统管理员');
|
||||
} else {
|
||||
toastService.error(error);
|
||||
}
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
@@ -202,6 +230,12 @@ export default function DocumentTypesList() {
|
||||
|
||||
// 处理删除文档类型
|
||||
const handleDelete = async (id: number) => {
|
||||
// 权限检查
|
||||
if (!canDeleteType) {
|
||||
toastService.warning('您没有删除权限');
|
||||
return;
|
||||
}
|
||||
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: "确定要删除该文档类型吗?此操作不会影响关联的评查点分组,但可能会影响使用该类型的文档评查。",
|
||||
@@ -259,7 +293,7 @@ export default function DocumentTypesList() {
|
||||
newParams.set('page', '1');
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
{
|
||||
@@ -331,14 +365,19 @@ export default function DocumentTypesList() {
|
||||
key: "operation",
|
||||
width: "150px",
|
||||
render: (_: unknown, record: DocumentTypeUI) => (
|
||||
<>
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleEdit(record.id)}
|
||||
>
|
||||
<i className="ri-edit-line"></i> {hasEditPermission ? '编辑' : '查看'}
|
||||
</button>
|
||||
{hasEditPermission && (
|
||||
<div className="operations-cell">
|
||||
{canViewType && (
|
||||
<>
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleEdit(record.id)}
|
||||
>
|
||||
<i className={canUpdateType ? "ri-edit-line" : "ri-eye-line"}></i>
|
||||
{canUpdateType ? '编辑' : '查看'}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{canDeleteType && (
|
||||
<button
|
||||
className="operation-btn text-error !hidden"
|
||||
onClick={() => handleDelete(record.id)}
|
||||
@@ -347,7 +386,10 @@ export default function DocumentTypesList() {
|
||||
<i className="ri-delete-bin-line"></i> 删除
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
{!canViewType && !canDeleteType && (
|
||||
<span className="text-gray-400">-</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
@@ -364,7 +406,7 @@ export default function DocumentTypesList() {
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">文档类型管理</h2>
|
||||
<div>
|
||||
{hasEditPermission && (
|
||||
{canCreateType && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
@@ -394,12 +436,12 @@ export default function DocumentTypesList() {
|
||||
noActionDivider={true}
|
||||
>
|
||||
<FilterSelect
|
||||
label="父级评查分组"
|
||||
name="ruleType"
|
||||
value={searchParams.get('ruleType') || ''}
|
||||
label="评查点分组"
|
||||
name="group_id"
|
||||
value={searchParams.get('group_id') || ''}
|
||||
options={[
|
||||
...(parentGroups || []).map(group => ({
|
||||
value: group.id,
|
||||
value: group.id.toString(),
|
||||
label: group.name
|
||||
}))
|
||||
]}
|
||||
|
||||
+247
-214
@@ -1,13 +1,21 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Form, useActionData, useLoaderData, useNavigate, useSearchParams, useRouteLoaderData } from "@remix-run/react";
|
||||
import { Form, useActionData, useLoaderData, useNavigate, useSearchParams } from "@remix-run/react";
|
||||
import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import documentTypesNewStyles from "~/styles/pages/document-types_new.css?url";
|
||||
import { getAllEvaluationPointGroups, type RuleGroup } from "~/api/evaluation_points/rule-groups";
|
||||
import { getDocumentType, createDocumentType, updateDocumentType, getEntryModules } from "~/api/document-types/document-types";
|
||||
import { getPromptTemplates, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
import { type RuleGroup } from "~/api/evaluation_points/rule-groups";
|
||||
import {
|
||||
getDocumentType,
|
||||
createDocumentType,
|
||||
updateDocumentType,
|
||||
getEntryModules,
|
||||
getRootEvaluationPointGroups_ForDocTypes,
|
||||
getPromptTemplateOptions
|
||||
} from "~/api/document-types/document-types";
|
||||
import { getEvaluationPointGroupChildren } from "~/api/evaluation_points/rule-groups";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { usePermission } from "~/hooks/usePermission";
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: documentTypesNewStyles }];
|
||||
@@ -38,9 +46,7 @@ export const meta: MetaFunction = ({ location }) => {
|
||||
// 定义模板类型
|
||||
const TEMPLATE_TYPES = {
|
||||
LLM_EXTRACTION: "LLM_Extraction",
|
||||
VLM_EXTRACTION: "VLM_Extraction",
|
||||
EVALUATION: "Evaluation",
|
||||
SUMMARY: "Summary"
|
||||
VLM_EXTRACTION: "VLM_Extraction"
|
||||
};
|
||||
|
||||
// 定义动作返回的数据类型
|
||||
@@ -52,8 +58,6 @@ interface ActionData {
|
||||
general?: string;
|
||||
llmExtractionTemplate?: string;
|
||||
vlmExtractionTemplate?: string;
|
||||
evaluationTemplate?: string;
|
||||
summaryTemplate?: string;
|
||||
};
|
||||
values?: Record<string, string | string[]>;
|
||||
}
|
||||
@@ -65,22 +69,18 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get("id");
|
||||
const isEdit = id ? true : false;
|
||||
|
||||
// 1. 获取评查点分组 - 使用 FastAPI v3 的 getAllEvaluationPointGroups 获取所有分组
|
||||
const ruleGroupsResponse = await getAllEvaluationPointGroups(false, true, frontendJWT);
|
||||
|
||||
// 1. 获取一级评查点分组(后续通过点击展开按钮动态加载子分组)
|
||||
const ruleGroupsResponse = await getRootEvaluationPointGroups_ForDocTypes(frontendJWT);
|
||||
if (ruleGroupsResponse.error) {
|
||||
console.error("获取评查点分组失败:", ruleGroupsResponse.error);
|
||||
// throw new Error(ruleGroupsResponse.error);
|
||||
console.error("获取一级评查点分组失败:", ruleGroupsResponse.error);
|
||||
}
|
||||
const rootGroups = ruleGroupsResponse.error ? [] : ruleGroupsResponse.data || [];
|
||||
|
||||
// ruleGroupsResponse.data已经是树形结构数据,getAllEvaluationPointGroups内部已处理好parent-children关系
|
||||
const groupsTree = ruleGroupsResponse.error ? [] : ruleGroupsResponse.data || [];
|
||||
|
||||
|
||||
// 2. 获取入口模块列表
|
||||
const entryModulesResponse = await getEntryModules(frontendJWT);
|
||||
if (entryModulesResponse.error) {
|
||||
@@ -88,16 +88,14 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
const entryModules = entryModulesResponse.error ? [] : (entryModulesResponse.data || []);
|
||||
|
||||
// 3. 获取各类型的提示词模板
|
||||
const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse, evaluationTemplatesResponse, summaryTemplatesResponse] =
|
||||
// 3. 获取提示词模板(只获取 LLM 和 VLM)
|
||||
const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse] =
|
||||
await Promise.all([
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.LLM_EXTRACTION }, frontendJWT),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.VLM_EXTRACTION }, frontendJWT),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.EVALUATION }, frontendJWT),
|
||||
getPromptTemplates({ type: TEMPLATE_TYPES.SUMMARY }, frontendJWT)
|
||||
getPromptTemplateOptions(TEMPLATE_TYPES.LLM_EXTRACTION, frontendJWT),
|
||||
getPromptTemplateOptions(TEMPLATE_TYPES.VLM_EXTRACTION, frontendJWT)
|
||||
]);
|
||||
|
||||
// 3. 如果是编辑模式,获取文档类型详情
|
||||
|
||||
// 4. 如果是编辑模式,获取文档类型详情
|
||||
let documentType = undefined;
|
||||
if (id) {
|
||||
const typeResponse = await getDocumentType(id, frontendJWT);
|
||||
@@ -105,16 +103,14 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
documentType = typeResponse.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return Response.json({
|
||||
isEdit,
|
||||
documentType,
|
||||
ruleGroups: groupsTree,
|
||||
ruleGroups: rootGroups,
|
||||
entryModules,
|
||||
llmExtractionTemplates: llmExtractionTemplatesResponse.data?.templates || [],
|
||||
vlmExtractionTemplates: vlmExtractionTemplatesResponse.data?.templates || [],
|
||||
evaluationTemplates: evaluationTemplatesResponse.data?.templates || [],
|
||||
summaryTemplates: summaryTemplatesResponse.data?.templates || []
|
||||
llmExtractionTemplates: llmExtractionTemplatesResponse.data || [],
|
||||
vlmExtractionTemplates: vlmExtractionTemplatesResponse.data || []
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
@@ -125,8 +121,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
entryModules: [],
|
||||
llmExtractionTemplates: [],
|
||||
vlmExtractionTemplates: [],
|
||||
evaluationTemplates: [],
|
||||
summaryTemplates: [],
|
||||
error: error || "加载数据失败"
|
||||
});
|
||||
}
|
||||
@@ -136,7 +130,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string | null;
|
||||
const name = formData.get("name") as string;
|
||||
@@ -144,24 +138,22 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
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;
|
||||
const summaryTemplateId = formData.get("summary_template") as string;
|
||||
|
||||
|
||||
// 获取选中的评查点分组ID列表
|
||||
const selectedGroups = formData.getAll("checkpoint_group_ids") as string[];
|
||||
|
||||
|
||||
// 表单验证
|
||||
const errors: ActionData["errors"] = {};
|
||||
|
||||
|
||||
// 收集所有错误
|
||||
if (!name || name.trim() === "") {
|
||||
errors.name = "文档类型名称不能为空";
|
||||
}
|
||||
|
||||
|
||||
if (selectedGroups.length === 0) {
|
||||
errors.groups = "请至少选择一个关联的评查点分组";
|
||||
}
|
||||
|
||||
|
||||
if (!llmExtractionTemplateId) {
|
||||
errors.llmExtractionTemplate = "请选择llm抽取提示词模板";
|
||||
}
|
||||
@@ -170,33 +162,22 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
errors.vlmExtractionTemplate = "请选择vlm抽取提示词模板";
|
||||
}
|
||||
|
||||
if (!evaluationTemplateId) {
|
||||
errors.evaluationTemplate = "请选择评查提示词模板";
|
||||
}
|
||||
|
||||
if (!summaryTemplateId) {
|
||||
errors.summaryTemplate = "请选择总结提示词模板";
|
||||
}
|
||||
|
||||
// 如果有错误,返回错误信息
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return Response.json({ errors, result: false });
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 构建文档类型数据
|
||||
// 构建文档类型数据 - group_ids 转换为 number[]
|
||||
const documentTypeData = {
|
||||
name,
|
||||
description,
|
||||
group_ids: selectedGroups,
|
||||
group_ids: selectedGroups.map(id => parseInt(id, 10)),
|
||||
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,
|
||||
evaluation_template_id: evaluationTemplateId ? parseInt(evaluationTemplateId) : null,
|
||||
summary_template_id: summaryTemplateId ? parseInt(summaryTemplateId) : null
|
||||
vlm_extraction_template_id: vlmExtractionTemplateId ? parseInt(vlmExtractionTemplateId) : null
|
||||
};
|
||||
|
||||
|
||||
// 调用API创建或更新文档类型
|
||||
let response;
|
||||
if (id) {
|
||||
@@ -209,12 +190,12 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// 创建新文档类型
|
||||
response = await createDocumentType(documentTypeData, frontendJWT);
|
||||
}
|
||||
|
||||
|
||||
if (response.error) {
|
||||
console.error("保存/更新文档类型失败:", response.error);
|
||||
throw new Error(response.error);
|
||||
}
|
||||
|
||||
|
||||
// 操作成功,重定向到列表页
|
||||
return redirect("/document-types");
|
||||
} catch (error) {
|
||||
@@ -232,24 +213,27 @@ export default function DocumentTypeNew() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const isEditMode = searchParams.has("id");
|
||||
|
||||
|
||||
const {
|
||||
documentType,
|
||||
ruleGroups,
|
||||
entryModules,
|
||||
llmExtractionTemplates,
|
||||
vlmExtractionTemplates,
|
||||
evaluationTemplates,
|
||||
summaryTemplates
|
||||
vlmExtractionTemplates
|
||||
} = useLoaderData<typeof loader>();
|
||||
|
||||
const actionData = useActionData<ActionData>();
|
||||
|
||||
// 获取用户角色并判断权限
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
const isReadOnly = !hasEditPermission;
|
||||
// 权限控制
|
||||
const { canCreate, canUpdate } = usePermission();
|
||||
const canCreateType = canCreate('document_type');
|
||||
const canUpdateType = canUpdate('document_type');
|
||||
|
||||
const urlMode = searchParams.get('mode');
|
||||
const isViewMode = urlMode === 'view';
|
||||
|
||||
const hasEditPermission = isEditMode ? canUpdateType : canCreateType;
|
||||
const isReadOnly = isViewMode || !hasEditPermission;
|
||||
|
||||
// 状态管理
|
||||
const [formData, setFormData] = useState({
|
||||
@@ -257,11 +241,9 @@ export default function DocumentTypeNew() {
|
||||
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 || "",
|
||||
summaryTemplateId: documentType?.summary_template_id || "",
|
||||
selectedGroups: documentType?.groups?.map((g: { id: string }) => g.id) || []
|
||||
llmExtractionTemplateId: documentType?.llm_extraction_template_id?.toString() || "",
|
||||
vlmExtractionTemplateId: documentType?.vlm_extraction_template_id?.toString() || "",
|
||||
selectedGroups: documentType?.groups?.map((g: { id: string | number }) => g.id.toString()) || []
|
||||
});
|
||||
|
||||
// 添加本地验证错误状态
|
||||
@@ -275,10 +257,13 @@ export default function DocumentTypeNew() {
|
||||
name: false,
|
||||
llmExtractionTemplate: false,
|
||||
vlmExtractionTemplate: false,
|
||||
evaluationTemplate: false,
|
||||
summaryTemplate: false,
|
||||
groups: false
|
||||
});
|
||||
|
||||
// 客户端调试信息ruleGroups
|
||||
useEffect(() => {
|
||||
console.log('返回的评查点分组数据',ruleGroups)
|
||||
}, [ruleGroups])
|
||||
|
||||
// 从actionData初始化本地错误
|
||||
useEffect(() => {
|
||||
@@ -292,6 +277,12 @@ export default function DocumentTypeNew() {
|
||||
|
||||
// 分组展开状态
|
||||
const [expandedGroups, setExpandedGroups] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 动态加载的子分组数据(groupId -> children[])
|
||||
const [groupChildrenMap, setGroupChildrenMap] = useState<Record<string, RuleGroup[]>>({});
|
||||
|
||||
// 子分组加载状态
|
||||
const [loadingChildren, setLoadingChildren] = useState<Record<string, boolean>>({});
|
||||
|
||||
// 当文档类型数据加载完成时更新表单
|
||||
useEffect(() => {
|
||||
@@ -301,30 +292,62 @@ export default function DocumentTypeNew() {
|
||||
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 || "",
|
||||
summaryTemplateId: documentType.summary_template_id || "",
|
||||
selectedGroups: documentType.groups.map((g: { id: string }) => g.id)
|
||||
llmExtractionTemplateId: documentType.llm_extraction_template_id?.toString() || "",
|
||||
vlmExtractionTemplateId: documentType.vlm_extraction_template_id?.toString() || "",
|
||||
selectedGroups: documentType.groups.map((g: { id: string | number }) => g.id.toString())
|
||||
});
|
||||
|
||||
// 初始化展开状态 - 对于有选中子分组的父分组,默认展开
|
||||
const newExpandedGroups: Record<string, boolean> = {};
|
||||
|
||||
ruleGroups.forEach((parentGroup: RuleGroup) => {
|
||||
// 如果父分组被选中或者有子分组被选中,则展开
|
||||
const isParentSelected = documentType.groups.some((g: { id: string }) => g.id === parentGroup.id);
|
||||
const hasSelectedChild = parentGroup.children &&
|
||||
parentGroup.children.some(child =>
|
||||
documentType.groups.some((g: { id: string }) => g.id === child.id)
|
||||
);
|
||||
|
||||
if (isParentSelected || hasSelectedChild) {
|
||||
newExpandedGroups[parentGroup.id] = true;
|
||||
|
||||
// 初始化展开状态 - 如果选中的是一级分组,需要加载子分组并展开
|
||||
const loadInitialChildren = async () => {
|
||||
const newExpandedGroups: Record<string, boolean> = {};
|
||||
const newChildrenMap: Record<string, RuleGroup[]> = {};
|
||||
|
||||
// 获取 frontendJWT
|
||||
let frontendJWT: string | undefined = undefined;
|
||||
if (typeof window !== 'undefined') {
|
||||
const userInfoStr = localStorage.getItem('user_info');
|
||||
if (userInfoStr) {
|
||||
try {
|
||||
const userInfo = JSON.parse(userInfoStr);
|
||||
frontendJWT = userInfo.frontendJWT;
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
setExpandedGroups(newExpandedGroups);
|
||||
|
||||
// 遍历所有一级分组,检查是否被选中
|
||||
for (const parentGroup of ruleGroups) {
|
||||
const isParentSelected = documentType.groups.some((g: { id: string | number }) =>
|
||||
g.id.toString() === parentGroup.id.toString()
|
||||
);
|
||||
|
||||
if (isParentSelected) {
|
||||
// 标记为展开
|
||||
newExpandedGroups[parentGroup.id] = true;
|
||||
|
||||
// 加载子分组
|
||||
try {
|
||||
const response = await getEvaluationPointGroupChildren(
|
||||
parentGroup.id,
|
||||
{ pageSize: 1000 },
|
||||
frontendJWT
|
||||
);
|
||||
|
||||
if (!response.error && response.data) {
|
||||
newChildrenMap[parentGroup.id] = response.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`加载分组 ${parentGroup.id} 的子分组失败:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setExpandedGroups(newExpandedGroups);
|
||||
setGroupChildrenMap(newChildrenMap);
|
||||
};
|
||||
|
||||
loadInitialChildren();
|
||||
}
|
||||
}, [documentType, ruleGroups]);
|
||||
|
||||
@@ -337,10 +360,6 @@ export default function DocumentTypeNew() {
|
||||
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择llm抽取提示词模板" : "";
|
||||
case 'vlmExtractionTemplate':
|
||||
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择vlm抽取提示词模板" : "";
|
||||
case 'evaluationTemplate':
|
||||
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择评查提示词模板" : "";
|
||||
case 'summaryTemplate':
|
||||
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择总结提示词模板" : "";
|
||||
case 'groups':
|
||||
return Array.isArray(value) && value.length === 0 ? "请至少选择一个关联的评查点分组" : "";
|
||||
default:
|
||||
@@ -350,68 +369,115 @@ export default function DocumentTypeNew() {
|
||||
|
||||
// 处理分组勾选
|
||||
const handleGroupCheckChange = (
|
||||
groupId: string,
|
||||
groupId: string,
|
||||
isChecked: boolean
|
||||
) => {
|
||||
// 单选模式:清空之前所有选择,只保留当前选中的
|
||||
// 多选模式:添加或移除选中的分组
|
||||
let newSelectedGroups: string[] = [];
|
||||
|
||||
|
||||
if (isChecked) {
|
||||
// 只添加当前选中的分组
|
||||
newSelectedGroups = [groupId];
|
||||
// 添加当前选中的分组(避免重复)
|
||||
newSelectedGroups = [...formData.selectedGroups, groupId];
|
||||
} else {
|
||||
// 移除取消选中的分组
|
||||
newSelectedGroups = formData.selectedGroups.filter(id => id !== groupId);
|
||||
}
|
||||
|
||||
|
||||
setFormData(prev => ({ ...prev, selectedGroups: newSelectedGroups }));
|
||||
|
||||
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, groups: true}));
|
||||
|
||||
|
||||
// 实时验证
|
||||
const error = validateField('groups', newSelectedGroups);
|
||||
setFormErrors(prev => ({...prev, groups: error}));
|
||||
};
|
||||
|
||||
// 修复展开/折叠功能
|
||||
const handleGroupExpand = (groupId: string, event: React.MouseEvent) => {
|
||||
// 修复展开/折叠功能 - 动态加载子分组
|
||||
const handleGroupExpand = async (groupId: string, event: React.MouseEvent) => {
|
||||
// 阻止事件冒泡,避免触发checkbox选中
|
||||
event.stopPropagation();
|
||||
|
||||
|
||||
const isCurrentlyExpanded = expandedGroups[groupId];
|
||||
|
||||
// 如果当前是折叠状态,准备展开
|
||||
if (!isCurrentlyExpanded) {
|
||||
// 检查是否已经加载过子分组
|
||||
if (!groupChildrenMap[groupId]) {
|
||||
// 还未加载,开始加载
|
||||
setLoadingChildren(prev => ({ ...prev, [groupId]: true }));
|
||||
|
||||
try {
|
||||
// 获取用户 token
|
||||
let frontendJWT: string | undefined = undefined;
|
||||
if (typeof window !== 'undefined') {
|
||||
const userInfoStr = localStorage.getItem('user_info');
|
||||
if (userInfoStr) {
|
||||
try {
|
||||
const userInfo = JSON.parse(userInfoStr);
|
||||
frontendJWT = userInfo.frontendJWT;
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 调用 API 获取子分组
|
||||
const response = await getEvaluationPointGroupChildren(
|
||||
groupId,
|
||||
{ pageSize: 1000 }, // 获取所有子分组
|
||||
frontendJWT
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
console.error('获取子分组失败:', response.error);
|
||||
toastService.error('获取子分组失败');
|
||||
setLoadingChildren(prev => ({ ...prev, [groupId]: false }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存子分组数据
|
||||
setGroupChildrenMap(prev => ({
|
||||
...prev,
|
||||
[groupId]: response.data || []
|
||||
}));
|
||||
|
||||
setLoadingChildren(prev => ({ ...prev, [groupId]: false }));
|
||||
} catch (error) {
|
||||
console.error('获取子分组异常:', error);
|
||||
toastService.error('获取子分组失败');
|
||||
setLoadingChildren(prev => ({ ...prev, [groupId]: false }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 切换展开/折叠状态
|
||||
setExpandedGroups(prev => ({
|
||||
...prev,
|
||||
[groupId]: !prev[groupId]
|
||||
[groupId]: !isCurrentlyExpanded
|
||||
}));
|
||||
};
|
||||
|
||||
// 处理表单输入变化
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
|
||||
|
||||
// 根据name属性映射到对应的formData字段
|
||||
let fieldName = name;
|
||||
|
||||
|
||||
if (name === 'llm_extraction_template') {
|
||||
fieldName = 'llmExtractionTemplateId';
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, llmExtractionTemplate: true}));
|
||||
} else if (name === 'vlm_extraction_template') {
|
||||
fieldName = 'vlmExtractionTemplateId';
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, vlmExtractionTemplate: true}));
|
||||
} else if (name === 'evaluation_template') {
|
||||
fieldName = 'evaluationTemplateId';
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, evaluationTemplate: true}));
|
||||
} else if (name === 'summary_template') {
|
||||
fieldName = 'summaryTemplateId';
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, summaryTemplate: true}));
|
||||
} else if (name === 'name') {
|
||||
// 标记字段为已触摸
|
||||
setTouchedFields(prev => ({...prev, name: true}));
|
||||
}
|
||||
|
||||
|
||||
setFormData(prev => ({ ...prev, [fieldName]: value }));
|
||||
|
||||
|
||||
// 实时验证
|
||||
if (name === 'name') {
|
||||
const error = validateField(name, value);
|
||||
@@ -422,42 +488,43 @@ export default function DocumentTypeNew() {
|
||||
} else if (name === 'vlm_extraction_template') {
|
||||
const error = validateField('vlmExtractionTemplate', value);
|
||||
setFormErrors(prev => ({...prev, vlmExtractionTemplate: error}));
|
||||
} else if (name === 'evaluation_template') {
|
||||
const error = validateField('evaluationTemplate', value);
|
||||
setFormErrors(prev => ({...prev, evaluationTemplate: error}));
|
||||
} else if (name === 'summary_template') {
|
||||
const error = validateField('summaryTemplate', value);
|
||||
setFormErrors(prev => ({...prev, summaryTemplate: error}));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理表单提交前验证
|
||||
const handleBeforeSubmit = (e: React.FormEvent) => {
|
||||
// 权限检查
|
||||
if (isEditMode && !canUpdateType) {
|
||||
toastService.warning('您没有修改权限,无法保存更改');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (!isEditMode && !canCreateType) {
|
||||
toastService.warning('您没有创建权限,无法新增文档类型');
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
// 标记所有字段为已触摸
|
||||
setTouchedFields({
|
||||
name: true,
|
||||
llmExtractionTemplate: true,
|
||||
vlmExtractionTemplate: true,
|
||||
evaluationTemplate: true,
|
||||
summaryTemplate: true,
|
||||
groups: true
|
||||
});
|
||||
|
||||
|
||||
// 验证所有字段
|
||||
const errors = {
|
||||
name: validateField('name', formData.name),
|
||||
llmExtractionTemplate: validateField('llmExtractionTemplate', formData.llmExtractionTemplateId),
|
||||
vlmExtractionTemplate: validateField('vlmExtractionTemplate', formData.vlmExtractionTemplateId),
|
||||
evaluationTemplate: validateField('evaluationTemplate', formData.evaluationTemplateId),
|
||||
summaryTemplate: validateField('summaryTemplate', formData.summaryTemplateId),
|
||||
groups: validateField('groups', formData.selectedGroups)
|
||||
};
|
||||
|
||||
|
||||
setFormErrors(errors);
|
||||
|
||||
|
||||
// 如果有错误,阻止提交
|
||||
if (errors.name || errors.llmExtractionTemplate || errors.vlmExtractionTemplate ||
|
||||
errors.evaluationTemplate || errors.summaryTemplate || errors.groups) {
|
||||
if (errors.name || errors.llmExtractionTemplate || errors.vlmExtractionTemplate || errors.groups) {
|
||||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
@@ -576,8 +643,8 @@ export default function DocumentTypeNew() {
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">请选择llm抽取提示词模板</option>
|
||||
{llmExtractionTemplates.map((template: PromptTemplateUI) => (
|
||||
{/* <option value="">请选择llm抽取提示词模板</option> */}
|
||||
{llmExtractionTemplates.map((template: { id: number; template_name: string }) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.template_name}
|
||||
</option>
|
||||
@@ -600,8 +667,8 @@ export default function DocumentTypeNew() {
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">请选择vlm抽取提示词模板</option>
|
||||
{vlmExtractionTemplates.map((template: PromptTemplateUI) => (
|
||||
{/* <option value="">请选择vlm抽取提示词模板</option> */}
|
||||
{vlmExtractionTemplates.map((template: { id: number; template_name: string }) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.template_name}
|
||||
</option>
|
||||
@@ -612,54 +679,6 @@ export default function DocumentTypeNew() {
|
||||
)}
|
||||
<div className="form-tip">选择用于从此类文档中抽取信息的vlm提示词模板</div>
|
||||
</div>
|
||||
|
||||
{/* 评查提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="evaluation-template" className="form-label">评查提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="evaluation-template"
|
||||
name="evaluation_template"
|
||||
className={`form-select ${touchedFields.evaluationTemplate && formErrors?.evaluationTemplate ? 'input-error' : ''}`}
|
||||
value={formData.evaluationTemplateId}
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">请选择评查提示词模板</option>
|
||||
{evaluationTemplates.map((template: PromptTemplateUI) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.template_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{touchedFields.evaluationTemplate && formErrors?.evaluationTemplate && (
|
||||
<div className="error-message">{formErrors.evaluationTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于评估此类文档内容的提示词模板</div>
|
||||
</div>
|
||||
|
||||
{/* 总结提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="summary-template" className="form-label">总结提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="summary-template"
|
||||
name="summary_template"
|
||||
className={`form-select ${touchedFields.summaryTemplate && formErrors?.summaryTemplate ? 'input-error' : ''}`}
|
||||
value={formData.summaryTemplateId}
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">请选择总结提示词模板</option>
|
||||
{summaryTemplates.map((template: PromptTemplateUI) => (
|
||||
<option key={template.id} value={template.id}>
|
||||
{template.template_name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{touchedFields.summaryTemplate && formErrors?.summaryTemplate && (
|
||||
<div className="error-message">{formErrors.summaryTemplate}</div>
|
||||
)}
|
||||
<div className="form-tip">选择用于生成此类文档摘要的提示词模板</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 关联评查点分组 */}
|
||||
@@ -689,13 +708,13 @@ export default function DocumentTypeNew() {
|
||||
<i className={`ri-arrow-${expandedGroups[group.id] ? 'down' : 'right'}-s-line text-primary`}></i>
|
||||
</button>
|
||||
<input
|
||||
type="radio"
|
||||
type="checkbox"
|
||||
id={`group-${group.id}`}
|
||||
name="checkpoint_group_ids"
|
||||
value={group.id}
|
||||
checked={formData.selectedGroups.includes(group.id)}
|
||||
onChange={(e) => handleGroupCheckChange(group.id, e.target.checked)}
|
||||
className="radio-input"
|
||||
className="checkbox-input"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
<label
|
||||
@@ -707,21 +726,35 @@ 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"
|
||||
style={{ paddingLeft: '2.5rem', opacity: 0.9 }}
|
||||
>
|
||||
<i className="ri-subtract-line" style={{ marginRight: '8px', color: '#9ca3af' }}></i>
|
||||
<span className="checkbox-label">
|
||||
{child.name}
|
||||
<span className="group-badge child-badge">二级分组</span>
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
{/* 子分组 - 动态加载并展示 */}
|
||||
{expandedGroups[group.id] && (
|
||||
<>
|
||||
{loadingChildren[group.id] ? (
|
||||
<div
|
||||
className="checkbox-item child-checkbox-item"
|
||||
style={{ paddingLeft: '2.5rem', opacity: 0.9 }}
|
||||
>
|
||||
<i className="ri-loader-4-line spin" style={{ marginRight: '8px', color: '#9ca3af' }}></i>
|
||||
<span className="checkbox-label" style={{ color: '#9ca3af' }}>
|
||||
加载中...
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
groupChildrenMap[group.id]?.map((child: RuleGroup) => (
|
||||
<div
|
||||
key={child.id}
|
||||
className="checkbox-item child-checkbox-item"
|
||||
style={{ paddingLeft: '2.5rem', opacity: 0.9 }}
|
||||
>
|
||||
<i className="ri-subtract-line" style={{ marginRight: '8px', color: '#9ca3af' }}></i>
|
||||
<span className="checkbox-label">
|
||||
{child.name}
|
||||
<span className="group-badge child-badge">二级分组</span>
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
|
||||
@@ -1008,14 +1008,18 @@ export default function DocumentsIndex() {
|
||||
})()}
|
||||
</td>
|
||||
<td className="px-4 py-3" style={{ width: '8%' }}>
|
||||
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs ${
|
||||
historyDoc.auditStatus === 1 ? 'bg-green-100 text-green-800' :
|
||||
historyDoc.auditStatus === -1 ? 'bg-red-100 text-red-800' :
|
||||
'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
<i className={`${auditStatusMapping[historyDoc.auditStatus]?.icon || 'ri-time-line'} mr-1`}></i>
|
||||
<span>{auditStatusMapping[historyDoc.auditStatus]?.label || '待审核'}</span>
|
||||
</div>
|
||||
{(() => {
|
||||
// 处理auditStatus为null或undefined的情况,默认为0(待审核)
|
||||
const auditStatus = historyDoc.auditStatus != null ? historyDoc.auditStatus : 0;
|
||||
const statusKey = auditStatus.toString();
|
||||
const statusInfo = auditStatusMapping[statusKey] || auditStatusMapping["0"];
|
||||
return (
|
||||
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${statusInfo.color}-100 text-${statusInfo.color}-800`}>
|
||||
<i className={`${statusInfo.icon} mr-1`}></i>
|
||||
<span>{statusInfo.label}</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</td>
|
||||
<td className="px-4 py-3" style={{ width: '15%' }}>
|
||||
<ResultStats
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSearchParams, useNavigate, useLoaderData, useRouteLoaderData, useRevalidator } from "@remix-run/react";
|
||||
import { useSearchParams, useNavigate, useLoaderData, useRevalidator } from "@remix-run/react";
|
||||
import { ClientLoaderFunctionArgs, MetaFunction } from "@remix-run/react";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
@@ -8,6 +8,7 @@ import { Pagination } from "~/components/ui/Pagination";
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import { usePermission } from "~/hooks/usePermission";
|
||||
import {
|
||||
getEntryModules,
|
||||
deleteEntryModule,
|
||||
@@ -113,16 +114,12 @@ export default function EntryModulesList() {
|
||||
const loaderData = useLoaderData<LoaderData>();
|
||||
const { modules, total, error } = loaderData;
|
||||
|
||||
// 获取用户角色并判断权限
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('admin') || userRole.toLowerCase().includes('developer');
|
||||
|
||||
// 调试信息
|
||||
useEffect(() => {
|
||||
console.log('📋 [EntryModules] 用户角色:', userRole);
|
||||
console.log('📋 [EntryModules] 是否有编辑权限:', hasEditPermission);
|
||||
}, [userRole, hasEditPermission]);
|
||||
// ✅ 使用权限 Hook
|
||||
const { canCreate, canUpdate, canDelete, canView } = usePermission();
|
||||
const canCreateModule = canCreate('entry_module');
|
||||
const canUpdateModule = canUpdate('entry_module');
|
||||
const canDeleteModule = canDelete('entry_module');
|
||||
const canViewModule = canView('entry_module');
|
||||
|
||||
// 获取搜索参数
|
||||
const name = searchParams.get('name') || '';
|
||||
@@ -179,6 +176,12 @@ export default function EntryModulesList() {
|
||||
|
||||
// 处理删除入口模块
|
||||
const handleDelete = async (id: number) => {
|
||||
// ✅ 检查删除权限
|
||||
if (!canDeleteModule) {
|
||||
toastService.warning('您没有删除权限');
|
||||
return;
|
||||
}
|
||||
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: "确定要删除该入口模块吗?此操作不可撤销。",
|
||||
@@ -317,15 +320,16 @@ export default function EntryModulesList() {
|
||||
width: '180px',
|
||||
render: (_: any, record: EntryModule) => (
|
||||
<div className="operations-cell">
|
||||
<button
|
||||
onClick={() => handleEdit(record.id!)}
|
||||
className="operation-btn"
|
||||
disabled={!hasEditPermission}
|
||||
title={hasEditPermission ? "编辑入口模块" : "无权限"}
|
||||
>
|
||||
<i className="ri-edit-line"></i> {hasEditPermission ? '编辑' : '查看'}
|
||||
</button>
|
||||
{hasEditPermission && (
|
||||
{ canViewModule &&
|
||||
<button
|
||||
onClick={() => handleEdit(record.id!)}
|
||||
className="operation-btn"
|
||||
title="查看/编辑入口模块"
|
||||
>
|
||||
<i className="ri-edit-line"></i> {canUpdateModule ? '编辑' : '查看'}
|
||||
</button>
|
||||
}
|
||||
{canDeleteModule && (
|
||||
<button
|
||||
type="button"
|
||||
className="operation-btn !text-[--color-error]"
|
||||
@@ -350,7 +354,8 @@ export default function EntryModulesList() {
|
||||
<h1 className="text-2xl font-bold text-gray-900">入口模块管理</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">管理系统入口模块,包括Logo图片和适用地区设置</p>
|
||||
</div>
|
||||
{hasEditPermission && (
|
||||
{/* ✅ 仅在有创建权限时显示新建按钮 */}
|
||||
{canCreateModule && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { Modal } from "~/components/ui/Modal";
|
||||
import { usePermission } from "~/hooks/usePermission";
|
||||
import {
|
||||
getEntryModuleById,
|
||||
createEntryModule,
|
||||
@@ -88,6 +89,15 @@ export default function EntryModuleNew() {
|
||||
const id = searchParams.get('id');
|
||||
const isEditMode = !!id;
|
||||
|
||||
// ✅ 使用权限 Hook
|
||||
const { canCreate, canUpdate } = usePermission();
|
||||
const canCreateModule = canCreate('entry_module');
|
||||
const canUpdateModule = canUpdate('entry_module');
|
||||
|
||||
// ✅ 根据当前操作类型判断权限
|
||||
const hasEditPermission = isEditMode ? canUpdateModule : canCreateModule;
|
||||
const isReadOnly = !hasEditPermission;
|
||||
|
||||
// 表单状态
|
||||
const [name, setName] = useState(module?.name || '');
|
||||
const [description, setDescription] = useState(module?.description || '');
|
||||
@@ -104,6 +114,17 @@ export default function EntryModuleNew() {
|
||||
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// ✅ 页面加载时检查权限并提示(仅在只读模式下提示)
|
||||
useEffect(() => {
|
||||
if (isReadOnly) {
|
||||
if (isEditMode) {
|
||||
toastService.info('当前为查看模式,您没有编辑权限');
|
||||
} else {
|
||||
toastService.warning('您没有创建入口模块的权限');
|
||||
}
|
||||
}
|
||||
}, [isReadOnly, isEditMode]);
|
||||
|
||||
// 处理loader加载数据的时候的错误
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
@@ -208,6 +229,17 @@ export default function EntryModuleNew() {
|
||||
|
||||
// 处理表单提交
|
||||
const handleSubmit = async () => {
|
||||
// ✅ Runtime permission check
|
||||
if (isEditMode && !canUpdateModule) {
|
||||
toastService.warning('您没有修改权限,无法保存更改');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEditMode && !canCreateModule) {
|
||||
toastService.warning('您没有创建权限,无法新增入口模块');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
@@ -270,10 +302,10 @@ export default function EntryModuleNew() {
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">
|
||||
{isEditMode ? '编辑入口模块' : '新建入口模块'}
|
||||
{isEditMode ? (isReadOnly ? '查看入口模块' : '编辑入口模块') : '新建入口模块'}
|
||||
</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
{isEditMode ? '修改入口模块信息' : '创建新的入口模块'}
|
||||
{isEditMode ? (isReadOnly ? '查看入口模块信息' : '修改入口模块信息') : '创建新的入口模块'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -293,6 +325,7 @@ export default function EntryModuleNew() {
|
||||
placeholder="请输入模块名称,如:合同管理"
|
||||
maxLength={255}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -305,6 +338,7 @@ export default function EntryModuleNew() {
|
||||
placeholder="请输入模块描述"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-md"
|
||||
rows={4}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -317,6 +351,7 @@ export default function EntryModuleNew() {
|
||||
type="default"
|
||||
icon="ri-upload-line"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
{logoPreview ? '更换图片' : '上传图片'}
|
||||
</Button>
|
||||
@@ -330,6 +365,7 @@ export default function EntryModuleNew() {
|
||||
accept="image/*"
|
||||
onChange={handleLogoChange}
|
||||
className="hidden"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
{logoPreview && (
|
||||
<div className="mt-3">
|
||||
@@ -355,13 +391,14 @@ export default function EntryModuleNew() {
|
||||
{AREA_OPTIONS.map(option => (
|
||||
<label
|
||||
key={option.value}
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
className={`flex items-center space-x-2 ${isReadOnly ? 'cursor-not-allowed' : 'cursor-pointer'}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAreas.includes(option.value)}
|
||||
onChange={() => handleAreaToggle(option.value)}
|
||||
className="w-4 h-4 border-gray-300 rounded cursor-pointer"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
<span className="text-sm text-gray-700">{option.label}</span>
|
||||
</label>
|
||||
@@ -379,14 +416,17 @@ export default function EntryModuleNew() {
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '提交中...' : (isEditMode ? '保存' : '创建')}
|
||||
</Button>
|
||||
{/* ✅ 仅在有对应权限时显示保存/创建按钮 */}
|
||||
{hasEditPermission && (
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSubmit}
|
||||
loading={isSubmitting}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? '提交中...' : (isEditMode ? '保存' : '创建')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { Table } from "~/components/ui/Table";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { getPromptTemplates, deletePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
import { toastService, messageService } from "~/components/ui";
|
||||
import { usePermission, PermissionGuard } from "~/hooks/usePermission";
|
||||
|
||||
// 样式链接
|
||||
export function links() {
|
||||
@@ -41,7 +42,7 @@ interface ActionData {
|
||||
// 数据加载器
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
// 获取用户会话信息
|
||||
// 获取用户会话信息(服务端需要获取 JWT token)
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
@@ -102,14 +103,14 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// Action函数 - 处理删除请求
|
||||
export async function action({ request }: ActionFunctionArgs) {
|
||||
// 获取用户会话信息(服务端需要获取 JWT token)
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
const formData = await request.formData();
|
||||
const id = formData.get("id") as string;
|
||||
const intent = formData.get("intent") as string;
|
||||
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (intent === "delete" && id) {
|
||||
try {
|
||||
const result = await deletePromptTemplate(id, frontendJWT);
|
||||
@@ -138,16 +139,35 @@ export default function PromptsIndex() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const fetcher = useFetcher<ActionData>();
|
||||
|
||||
// 获取用户角色并判断权限
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
// 🔐 使用新的权限检查Hook
|
||||
const {
|
||||
canCreate,
|
||||
canUpdate,
|
||||
canDelete,
|
||||
canView,
|
||||
hasPermission,
|
||||
permissions,
|
||||
userRole
|
||||
} = usePermission();
|
||||
|
||||
// 检查各项权限
|
||||
const canCreateTemplate = canCreate('prompt_template');
|
||||
const canEditTemplate = canUpdate('prompt_template');
|
||||
const canDeleteTemplate = canDelete('prompt_template');
|
||||
const canViewTemplate = canView('prompt_template');
|
||||
|
||||
// 调试信息
|
||||
useEffect(() => {
|
||||
console.log('📋 [Prompts] 用户角色:', userRole);
|
||||
console.log('📋 [Prompts] 是否有编辑权限:', hasEditPermission);
|
||||
}, [userRole, hasEditPermission]);
|
||||
// useEffect(() => {
|
||||
// console.log('📋 [Prompts] 模板数据:', templates);
|
||||
// console.log('📋 [Prompts] 用户角色:', userRole);
|
||||
// console.log('📋 [Prompts] 权限列表:', permissions);
|
||||
// console.log('📋 [Prompts] 权限检查结果:', {
|
||||
// canCreate: canCreateTemplate,
|
||||
// canEdit: canEditTemplate,
|
||||
// canDelete: canDeleteTemplate,
|
||||
// canView: canViewTemplate
|
||||
// });
|
||||
// }, [userRole, permissions, templates, canCreateTemplate, canEditTemplate, canDeleteTemplate, canViewTemplate]);
|
||||
|
||||
// 处理搜索名称
|
||||
const handleNameSearch = (value: string) => {
|
||||
@@ -234,6 +254,13 @@ export default function PromptsIndex() {
|
||||
});
|
||||
};
|
||||
|
||||
// 监听 loader 错误
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toastService.error(error);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
// 监听 fetcher 状态变化
|
||||
useEffect(() => {
|
||||
if (fetcher.state === 'idle' && fetcher.data) {
|
||||
@@ -366,6 +393,7 @@ export default function PromptsIndex() {
|
||||
render: (_: unknown, record: PromptTemplateUI) => (
|
||||
<div>
|
||||
{record.status === 'system' ? (
|
||||
// 系统预设模板:只能查看,有编辑权限的可以复制
|
||||
<>
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
@@ -373,7 +401,8 @@ export default function PromptsIndex() {
|
||||
>
|
||||
<i className="ri-eye-line"></i> 查看
|
||||
</button>
|
||||
{hasEditPermission && (
|
||||
{/* 🔐 复制按钮需要创建权限 */}
|
||||
{canCreateTemplate && (
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleCloneTemplate(record.id)}
|
||||
@@ -383,14 +412,27 @@ export default function PromptsIndex() {
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 自定义模板:根据权限显示编辑/查看/删除
|
||||
<>
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => hasEditPermission ? handleEditTemplate(record.id) : handleViewTemplate(record.id)}
|
||||
>
|
||||
<i className={hasEditPermission ? "ri-edit-line" : "ri-eye-line"}></i> {hasEditPermission ? '编辑' : '查看'}
|
||||
</button>
|
||||
{hasEditPermission && (
|
||||
{/* 🔐 有编辑权限显示编辑按钮,否则显示查看按钮 */}
|
||||
{canEditTemplate ? (
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleEditTemplate(record.id)}
|
||||
>
|
||||
<i className="ri-edit-line"></i> 编辑
|
||||
</button>
|
||||
) : canViewTemplate ? (
|
||||
<button
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleViewTemplate(record.id)}
|
||||
>
|
||||
<i className="ri-eye-line"></i> 查看
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
{/* 🔐 删除按钮需要删除权限 */}
|
||||
{canDeleteTemplate && (
|
||||
<button
|
||||
className="operation-btn text-error"
|
||||
onClick={() => handleDeleteTemplate(record.id)}
|
||||
@@ -412,7 +454,8 @@ export default function PromptsIndex() {
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">提示词模板管理</h2>
|
||||
<div>
|
||||
{hasEditPermission && (
|
||||
{/* 🔐 使用权限控制显示新增按钮 */}
|
||||
<PermissionGuard permission="prompt_template:create:write">
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
@@ -420,7 +463,7 @@ export default function PromptsIndex() {
|
||||
>
|
||||
新增提示词模板
|
||||
</Button>
|
||||
)}
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -485,14 +528,7 @@ export default function PromptsIndex() {
|
||||
className="flex-1 min-w-[200px]"
|
||||
/>
|
||||
</FilterPanel>
|
||||
|
||||
{/* 错误信息 */}
|
||||
{error && (
|
||||
<div className="error-alert mb-4 p-4 bg-red-50 text-red-700 rounded-md">
|
||||
<i className="ri-error-warning-line mr-2"></i> {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* 数据表格 */}
|
||||
<Card bodyClassName="px-4 py-4">
|
||||
<Table
|
||||
|
||||
+17
-18
@@ -4,7 +4,7 @@ import { Link, useLoaderData, useNavigation, useActionData, Form } from "@remix-
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import newStyles from "~/styles/pages/prompts_new.css?url";
|
||||
import { getPromptTemplate, createPromptTemplate, updatePromptTemplate, type PromptTemplateUI } from "~/api/prompts/prompts";
|
||||
// import { toastService } from "~/components/ui";
|
||||
import { toastService } from "~/components/ui";
|
||||
|
||||
// 样式链接
|
||||
export function links() {
|
||||
@@ -352,7 +352,21 @@ export default function PromptsNew() {
|
||||
setPageTitle("新增提示词模板");
|
||||
}
|
||||
}, [template, mode, actionData?.formData]);
|
||||
|
||||
|
||||
// 监听 loader 错误
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
toastService.error(error);
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
// 监听 action 错误
|
||||
useEffect(() => {
|
||||
if (actionData?.errors?.general) {
|
||||
toastService.error(actionData.errors.general);
|
||||
}
|
||||
}, [actionData?.errors?.general]);
|
||||
|
||||
// 处理输入变化
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => {
|
||||
const { name, value } = e.target;
|
||||
@@ -462,22 +476,7 @@ export default function PromptsNew() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 错误信息 */}
|
||||
{error && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<i className="ri-error-warning-line"></i>
|
||||
<div>{error}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actionData?.errors?.general && (
|
||||
<div className="alert alert-error mb-4">
|
||||
<i className="ri-error-warning-line"></i>
|
||||
<div>{actionData.errors.general}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
{/* 查看模式提示 */}
|
||||
{isViewMode && (
|
||||
<div className="alert alert-info">
|
||||
|
||||
@@ -529,9 +529,11 @@ interface AssignUserModalProps {
|
||||
onClose: () => void;
|
||||
onSuccess: () => void;
|
||||
role: RoleInfo | null;
|
||||
isCityAdmin?: boolean;
|
||||
currentUserArea?: string;
|
||||
}
|
||||
|
||||
function AssignUserModal({ isOpen, onClose, onSuccess, role }: AssignUserModalProps) {
|
||||
function AssignUserModal({ isOpen, onClose, onSuccess, role, isCityAdmin, currentUserArea }: AssignUserModalProps) {
|
||||
const [allUsers, setAllUsers] = useState<UserInfo[]>([]);
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<number[]>([]);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
@@ -552,12 +554,24 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role }: AssignUserModalPr
|
||||
setLoadingUsers(true);
|
||||
try {
|
||||
const users = await getAllUsers();
|
||||
setAllUsers(users);
|
||||
|
||||
// v3.3: 市级管理员只能看到同地区的用户(使用 area 字段)
|
||||
let filteredUsers = users;
|
||||
if (isCityAdmin && currentUserArea) {
|
||||
filteredUsers = users.filter(user => user.area === currentUserArea);
|
||||
console.log('🔒 [AssignUserModal v3.3] 市级管理员用户过滤:', {
|
||||
当前地区: currentUserArea,
|
||||
原始用户数: users.length,
|
||||
过滤后用户数: filteredUsers.length
|
||||
});
|
||||
}
|
||||
|
||||
setAllUsers(filteredUsers);
|
||||
|
||||
// 批量获取每个用户的角色
|
||||
const rolesMap = new Map<number, RoleInfo[]>();
|
||||
await Promise.all(
|
||||
users.map(async (user) => {
|
||||
filteredUsers.map(async (user) => {
|
||||
const roles = await getUserRoles(user.id);
|
||||
rolesMap.set(user.id, roles);
|
||||
})
|
||||
@@ -678,6 +692,14 @@ function AssignUserModal({ isOpen, onClose, onSuccess, role }: AssignUserModalPr
|
||||
}
|
||||
>
|
||||
<div className="assign-user-modal">
|
||||
{/* v3.3: 市级管理员地区过滤提示 */}
|
||||
{isCityAdmin && currentUserArea && (
|
||||
<div className="form-notice info" style={{ marginBottom: '12px' }}>
|
||||
<i className="ri-information-line"></i>
|
||||
<span>市级管理员权限:仅显示 {currentUserArea} 地区的用户</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 搜索框 */}
|
||||
<div className="search-box">
|
||||
<i className="ri-search-line"></i>
|
||||
@@ -784,6 +806,12 @@ export default function RolePermissions() {
|
||||
const [activeTab, setActiveTab] = useState<'permissions' | 'users'>('permissions');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// v3.3: 检查当前用户角色和地区
|
||||
const [currentUserRole, setCurrentUserRole] = useState('');
|
||||
const [currentUserArea, setCurrentUserArea] = useState('');
|
||||
const [isProvincialAdmin, setIsProvincialAdmin] = useState(false);
|
||||
const [isCityAdmin, setIsCityAdmin] = useState(false);
|
||||
|
||||
// 模态框状态
|
||||
const [showCreateModal, setShowCreateModal] = useState(false);
|
||||
const [showEditModal, setShowEditModal] = useState(false);
|
||||
@@ -800,6 +828,13 @@ export default function RolePermissions() {
|
||||
} | null>(null);
|
||||
const [deleteCountdown, setDeleteCountdown] = useState(3);
|
||||
|
||||
// 权限警告Modal状态
|
||||
const [showPermissionWarning, setShowPermissionWarning] = useState(false);
|
||||
const [pendingRouteChange, setPendingRouteChange] = useState<{
|
||||
routeId: number;
|
||||
checked: boolean;
|
||||
} | null>(null);
|
||||
|
||||
// 路由权限相关状态
|
||||
const [selectedRouteIds, setSelectedRouteIds] = useState<number[]>([]);
|
||||
const [roleUsers, setRoleUsers] = useState<UserInfo[]>([]);
|
||||
@@ -829,19 +864,62 @@ export default function RolePermissions() {
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// v3.3: 检查当前用户角色和地区
|
||||
if (typeof window !== 'undefined') {
|
||||
const userInfoStr = localStorage.getItem('user_info');
|
||||
if (userInfoStr) {
|
||||
try {
|
||||
const userInfo = JSON.parse(userInfoStr);
|
||||
const userRole = userInfo.user_role || '';
|
||||
const userArea = userInfo.area || ''; // v3.3: 使用 area 字段进行地区隔离
|
||||
|
||||
setCurrentUserRole(userRole);
|
||||
setCurrentUserArea(userArea);
|
||||
setIsProvincialAdmin(userRole === 'provincial_admin');
|
||||
setIsCityAdmin(userRole === 'admin');
|
||||
|
||||
console.log('🔑 [RolePermissions v3.3] 当前用户信息:', {
|
||||
role: userRole,
|
||||
area: userArea,
|
||||
isProvincialAdmin: userRole === 'provincial_admin',
|
||||
isCityAdmin: userRole === 'admin'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('❌ [RolePermissions] 解析用户信息失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [rolesData, routesData, usersData] = await Promise.all([
|
||||
getRoles(),
|
||||
getRoutes(),
|
||||
getAllUsers()
|
||||
]);
|
||||
|
||||
setRoles(rolesData);
|
||||
setRoutes(routesData);
|
||||
setUsers(usersData);
|
||||
// v3.3: 角色列表对所有人可见(不过滤)
|
||||
const filteredRoles = rolesData;
|
||||
|
||||
// 默认选中第一个角色
|
||||
if (rolesData.length > 0) {
|
||||
handleSelectRole(rolesData[0]);
|
||||
// v3.3: 根据用户地区过滤可见的用户列表
|
||||
let filteredUsers = usersData;
|
||||
if (isCityAdmin && currentUserArea) {
|
||||
// 市级管理员只能看到同地区的用户(使用 area 字段)
|
||||
filteredUsers = usersData.filter(user =>
|
||||
user.area === currentUserArea
|
||||
);
|
||||
console.log('🔒 [RolePermissions v3.3] 市级管理员用户过滤:', {
|
||||
当前地区: currentUserArea,
|
||||
原始用户数: usersData.length,
|
||||
过滤后用户数: filteredUsers.length
|
||||
});
|
||||
}
|
||||
|
||||
setRoles(filteredRoles);
|
||||
setRoutes(routesData);
|
||||
setUsers(filteredUsers);
|
||||
|
||||
// 默认选中第一个角色(使用过滤后的列表)
|
||||
if (filteredRoles.length > 0) {
|
||||
handleSelectRole(filteredRoles[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("加载数据失败:", error);
|
||||
@@ -888,6 +966,20 @@ export default function RolePermissions() {
|
||||
setRoleUsers(users);
|
||||
};
|
||||
|
||||
// 递归查找路由
|
||||
const findRouteById = (routes: RouteInfo[], routeId: number): RouteInfo | null => {
|
||||
for (const route of routes) {
|
||||
if (route.id === routeId) {
|
||||
return route;
|
||||
}
|
||||
if (route.children && route.children.length > 0) {
|
||||
const found = findRouteById(route.children, routeId);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// 递归获取所有路由ID(包括子路由)
|
||||
const getAllRouteIds = (routes: RouteInfo[]): number[] => {
|
||||
let ids: number[] = [];
|
||||
@@ -900,8 +992,34 @@ export default function RolePermissions() {
|
||||
return ids;
|
||||
};
|
||||
|
||||
// 递归检查路由树中是否包含指定路径的路由
|
||||
const containsRoutePath = (routes: RouteInfo[], targetPath: string): boolean => {
|
||||
for (const route of routes) {
|
||||
if (route.route_path === targetPath) {
|
||||
return true;
|
||||
}
|
||||
if (route.children && route.children.length > 0) {
|
||||
if (containsRoutePath(route.children, targetPath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// 切换路由权限
|
||||
const handleToggleRoute = (routeId: number, checked: boolean) => {
|
||||
// 检查是否正在取消勾选 /role-permissions 路由
|
||||
if (!checked) {
|
||||
const route = findRouteById(routes, routeId);
|
||||
if (route && route.route_path === '/role-permissions') {
|
||||
// 显示警告模态框
|
||||
setPendingRouteChange({ routeId, checked });
|
||||
setShowPermissionWarning(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
setSelectedRouteIds([...selectedRouteIds, routeId]);
|
||||
} else {
|
||||
@@ -911,6 +1029,20 @@ export default function RolePermissions() {
|
||||
|
||||
// 切换父路由(包括所有子路由)
|
||||
const handleToggleParentRoute = (route: RouteInfo, checked: boolean) => {
|
||||
// 检查是否正在取消勾选包含 /role-permissions 的父路由
|
||||
if (!checked) {
|
||||
const allRoutes = route.children ? [route, ...route.children] : [route];
|
||||
const hasRolePermissionsRoute = allRoutes.some(r => r.route_path === '/role-permissions') ||
|
||||
(route.children && containsRoutePath(route.children, '/role-permissions'));
|
||||
|
||||
if (route.route_path === '/role-permissions' || hasRolePermissionsRoute) {
|
||||
// 显示警告模态框,传递 route 对象表示是父路由操作
|
||||
setPendingRouteChange({ routeId: route.id, checked });
|
||||
setShowPermissionWarning(true);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const childIds = route.children ? getAllRouteIds(route.children) : [];
|
||||
const allIds = [route.id, ...childIds];
|
||||
|
||||
@@ -926,6 +1058,32 @@ export default function RolePermissions() {
|
||||
}
|
||||
};
|
||||
|
||||
// 确认取消角色权限管理路由
|
||||
const confirmRemovePermissionRoute = () => {
|
||||
if (!pendingRouteChange) return;
|
||||
|
||||
const { routeId, checked } = pendingRouteChange;
|
||||
const route = findRouteById(routes, routeId);
|
||||
|
||||
if (route) {
|
||||
// 如果是父路由,取消所有子路由
|
||||
if (route.children && route.children.length > 0) {
|
||||
const childIds = getAllRouteIds(route.children);
|
||||
const allIds = [route.id, ...childIds];
|
||||
setSelectedRouteIds(selectedRouteIds.filter(id => !allIds.includes(id)));
|
||||
} else {
|
||||
// 单个路由
|
||||
setSelectedRouteIds(selectedRouteIds.filter(id => id !== routeId));
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭模态框并重置状态
|
||||
setShowPermissionWarning(false);
|
||||
setPendingRouteChange(null);
|
||||
|
||||
toastService.warning('已取消角色权限管理路由,请谨慎保存权限配置');
|
||||
};
|
||||
|
||||
// v3.0: 切换路由展开状态(显示/隐藏权限列表)
|
||||
const handleToggleRouteExpand = (routeId: number) => {
|
||||
setExpandedRouteIds(prev =>
|
||||
@@ -1070,16 +1228,27 @@ export default function RolePermissions() {
|
||||
}
|
||||
};
|
||||
|
||||
// 保存权限 - v3.0: 同时保存路由权限和API权限
|
||||
// 保存权限 - v3.3: 同时保存路由权限和API权限,仅省级管理员可操作
|
||||
const handleSavePermissions = async () => {
|
||||
if (!selectedRole) return;
|
||||
|
||||
// v3.3: 前置权限检查(仅省级管理员)
|
||||
if (!isProvincialAdmin) {
|
||||
toastService.error('权限不足:仅省级管理员可以修改角色路由权限');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 保存路由权限
|
||||
const routeResult = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
|
||||
|
||||
// v3.3: 处理权限不足错误
|
||||
if (!routeResult.success) {
|
||||
toastService.error(routeResult.message);
|
||||
if (routeResult.code === 4003) {
|
||||
toastService.error('权限不足:仅省级管理员可以修改角色路由权限');
|
||||
} else {
|
||||
toastService.error(routeResult.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1146,6 +1315,7 @@ export default function RolePermissions() {
|
||||
}
|
||||
}}
|
||||
className="route-checkbox"
|
||||
disabled={!isProvincialAdmin}
|
||||
/>
|
||||
<label htmlFor={`route-${route.id}`} className="route-label">
|
||||
{route.icon && <i className={`${route.icon} route-icon`}></i>}
|
||||
@@ -1212,6 +1382,7 @@ export default function RolePermissions() {
|
||||
checked={selectedPermissionIds.includes(permission.id)}
|
||||
onChange={(e) => handleTogglePermission(permission.id, e.target.checked)}
|
||||
style={{ margin: 0 }}
|
||||
disabled={!isProvincialAdmin}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
@@ -1398,12 +1569,20 @@ export default function RolePermissions() {
|
||||
{/* 路由权限Tab */}
|
||||
{activeTab === 'permissions' && (
|
||||
<div className="permissions-tab">
|
||||
{/* v3.3: 权限提示(仅省级管理员可修改) */}
|
||||
{!isProvincialAdmin && (
|
||||
<div className="form-notice warning" style={{ marginBottom: '16px' }}>
|
||||
<i className="ri-information-line"></i>
|
||||
<span>您当前为只读模式,仅省级管理员可以修改角色路由权限</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="permissions-header">
|
||||
<h3>为角色 "{selectedRole.role_name}" 分配路由权限</h3>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-save-line"
|
||||
onClick={handleSavePermissions}
|
||||
disabled={!isProvincialAdmin}
|
||||
>
|
||||
保存权限
|
||||
</Button>
|
||||
@@ -1536,6 +1715,8 @@ export default function RolePermissions() {
|
||||
}
|
||||
}}
|
||||
role={selectedRole}
|
||||
isCityAdmin={isCityAdmin}
|
||||
currentUserArea={currentUserArea}
|
||||
/>
|
||||
|
||||
{/* 确认删除模态框 */}
|
||||
@@ -1614,6 +1795,47 @@ export default function RolePermissions() {
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* 权限警告模态框 */}
|
||||
<Modal
|
||||
isOpen={showPermissionWarning}
|
||||
onClose={() => {
|
||||
setShowPermissionWarning(false);
|
||||
setPendingRouteChange(null);
|
||||
}}
|
||||
title="⚠️ 警告:取消角色权限管理路由"
|
||||
size="medium"
|
||||
>
|
||||
<div style={{ padding: '20px 0' }}>
|
||||
<p style={{ marginBottom: '16px', fontSize: '15px', lineHeight: '1.6', color: '#ff6b00' }}>
|
||||
您正在尝试取消勾选 <strong>"/role-permissions"</strong> 路由权限。
|
||||
</p>
|
||||
<p style={{ marginBottom: '16px', fontSize: '14px', lineHeight: '1.6' }}>
|
||||
<strong>请注意:</strong>如果取消此路由权限,该角色的用户将无法访问角色权限管理页面,这可能导致无法管理系统权限。
|
||||
</p>
|
||||
<p style={{ marginBottom: '16px', fontSize: '14px', color: '#666' }}>
|
||||
请谨慎操作,确认后需要点击"保存权限"才会生效。
|
||||
</p>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', marginTop: '24px' }}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setShowPermissionWarning(false);
|
||||
setPendingRouteChange(null);
|
||||
}}
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={confirmRemovePermissionRoute}
|
||||
>
|
||||
确认取消勾选
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { type MetaFunction } from "@remix-run/node";
|
||||
import { useLoaderData, Link, useNavigate, useSearchParams, useRouteLoaderData } from "@remix-run/react";
|
||||
import { useLoaderData, Link, useNavigate, useSearchParams } from "@remix-run/react";
|
||||
import { useState, useEffect } from "react";
|
||||
import indexStyles from "~/styles/pages/rule-groups_index.css?url";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
batchDeleteEvaluationPointGroups
|
||||
} from "~/api/evaluation_points/rule-groups";
|
||||
import { toastService, messageService } from "~/components/ui";
|
||||
import { usePermission } from "~/hooks/usePermission";
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: indexStyles }];
|
||||
@@ -78,8 +79,7 @@ export async function loader({ request }: { request: Request }) {
|
||||
|
||||
export default function RuleGroupsIndex() {
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { groups: initialGroups, totalCount = 0, page = 1, pageSize = 50, frontendJWT } = loaderData;
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const { groups: initialGroups, frontendJWT } = loaderData;
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [expandedGroups, setExpandedGroups] = useState<string[]>([]);
|
||||
@@ -88,8 +88,13 @@ export default function RuleGroupsIndex() {
|
||||
const [filteredChildrenMap, setFilteredChildrenMap] = useState<Record<string, RuleGroup[]>>({});
|
||||
const [initialLoading, setInitialLoading] = useState<boolean>(true);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]); // 🆕 批量选择状态
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
|
||||
// ✅ 使用权限 Hook
|
||||
const { canCreate, canUpdate, canDelete, canBatch } = usePermission();
|
||||
const canCreateGroup = canCreate('evaluation_group');
|
||||
const canUpdateGroup = canUpdate('evaluation_group');
|
||||
const canDeleteGroup = canDelete('evaluation_group');
|
||||
const canBatchOperation = canBatch('evaluation_group'); // ✅ 批量操作权限
|
||||
|
||||
// 初始加载时自动加载所有子分组
|
||||
useEffect(() => {
|
||||
@@ -230,6 +235,12 @@ export default function RuleGroupsIndex() {
|
||||
|
||||
// 处理删除分组
|
||||
const handleDeleteGroup = async (groupId: string) => {
|
||||
// ✅ 检查删除权限
|
||||
if (!canDeleteGroup) {
|
||||
toastService.warning('您没有删除权限');
|
||||
return;
|
||||
}
|
||||
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: "确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。",
|
||||
@@ -277,6 +288,12 @@ export default function RuleGroupsIndex() {
|
||||
|
||||
// 🆕 批量启用/禁用
|
||||
const handleBatchEnable = async (enable: boolean) => {
|
||||
// ✅ 检查更新权限
|
||||
if (!canUpdateGroup) {
|
||||
toastService.warning('您没有更新权限');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要操作的分组');
|
||||
return;
|
||||
@@ -299,6 +316,12 @@ export default function RuleGroupsIndex() {
|
||||
|
||||
// 🆕 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
// ✅ 检查删除权限
|
||||
if (!canDeleteGroup) {
|
||||
toastService.warning('您没有删除权限');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要删除的分组');
|
||||
return;
|
||||
@@ -569,8 +592,8 @@ export default function RuleGroupsIndex() {
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
// 🆕 复选框列
|
||||
...(hasEditPermission ? [{
|
||||
// 🆕 复选框列 - 仅在有批量操作权限时显示
|
||||
...(canBatchOperation ? [{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -676,9 +699,9 @@ export default function RuleGroupsIndex() {
|
||||
onClick={() => navigate(`/rule-groups/new?id=${record.id}`)}
|
||||
className="operation-btn"
|
||||
>
|
||||
<i className="ri-edit-line"></i> {hasEditPermission ? '编辑' : '查看'}
|
||||
<i className="ri-edit-line"></i> {canUpdateGroup ? '编辑' : '查看'}
|
||||
</button>
|
||||
{hasEditPermission && (
|
||||
{canDeleteGroup && (
|
||||
<button
|
||||
type="button"
|
||||
className="operation-btn !text-[--color-error]"
|
||||
@@ -720,7 +743,8 @@ export default function RuleGroupsIndex() {
|
||||
>
|
||||
收起全部
|
||||
</Button>
|
||||
{hasEditPermission && selectedIds.length > 0 && (
|
||||
{/* ✅ 批量启用/禁用按钮:仅当有更新权限且有选中项时显示 */}
|
||||
{canUpdateGroup && selectedIds.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
type="default"
|
||||
@@ -738,17 +762,20 @@ export default function RuleGroupsIndex() {
|
||||
>
|
||||
批量禁用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={handleBatchDelete}
|
||||
className="mr-2"
|
||||
>
|
||||
批量删除 ({selectedIds.length})
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{hasEditPermission && (
|
||||
{/* ✅ 批量删除按钮:仅当有删除权限且有选中项时显示 */}
|
||||
{canDeleteGroup && selectedIds.length > 0 && (
|
||||
<Button
|
||||
type="danger"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={handleBatchDelete}
|
||||
className="mr-2"
|
||||
>
|
||||
批量删除 ({selectedIds.length})
|
||||
</Button>
|
||||
)}
|
||||
{canCreateGroup && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
|
||||
import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
|
||||
import { useLoaderData, useActionData, useNavigation, Form, useRouteLoaderData } from "@remix-run/react";
|
||||
import { useLoaderData, useActionData, useNavigation, Form } from "@remix-run/react";
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
import { usePermission } from "~/hooks/usePermission";
|
||||
import ruleGroupsNewStyles from "~/styles/pages/rule-groups_new.css?url";
|
||||
import {
|
||||
getEvaluationPointGroups,
|
||||
@@ -246,16 +247,19 @@ export default function RuleGroupNew() {
|
||||
const actionData = useActionData<typeof action>();
|
||||
const navigation = useNavigation();
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
|
||||
// 判断表单是否为只读模式(只有包含'provin'的用户才有编辑权限)
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
const isReadOnly = !hasEditPermission;
|
||||
// ✅ Use permission Hook
|
||||
const { canCreate, canUpdate } = usePermission();
|
||||
const canCreateGroup = canCreate('evaluation_group');
|
||||
const canUpdateGroup = canUpdate('evaluation_group');
|
||||
|
||||
// 解构数据
|
||||
const { group, parentGroups, isEdit, error } = data;
|
||||
|
||||
// ✅ 根据当前操作类型判断权限
|
||||
const hasEditPermission = isEdit ? canUpdateGroup : canCreateGroup;
|
||||
const isReadOnly = !hasEditPermission;
|
||||
|
||||
// 表单状态管理 - 使用受控组件
|
||||
const [formValues, setFormValues] = useState<{
|
||||
groupType: "primary" | "secondary";
|
||||
@@ -299,13 +303,29 @@ export default function RuleGroupNew() {
|
||||
parentId: false
|
||||
});
|
||||
|
||||
// ✅ 页面加载时检查权限并提示(仅在只读模式下提示)
|
||||
// useEffect(() => {
|
||||
// console.log("权限",canCreateGroup,canUpdateGroup)
|
||||
// if (isReadOnly) {
|
||||
// if (isEdit) {
|
||||
// toastService.info('当前为查看模式,您没有编辑权限');
|
||||
// } else {
|
||||
// toastService.warning('您没有创建分组的权限');
|
||||
// }
|
||||
// }
|
||||
// }, [isReadOnly, isEdit]);
|
||||
|
||||
// 从 actionData 初始化表单错误
|
||||
useEffect(() => {
|
||||
if (actionData?.errors) {
|
||||
setFormErrors(actionData.errors);
|
||||
// ✅ 如果有通用错误,使用 toast 显示
|
||||
if (actionData.errors.general) {
|
||||
toastService.error(actionData.errors.general);
|
||||
}
|
||||
}
|
||||
}, [actionData]);
|
||||
|
||||
|
||||
// 根据加载的组数据初始化表单
|
||||
useEffect(() => {
|
||||
if (group) {
|
||||
@@ -452,31 +472,53 @@ export default function RuleGroupNew() {
|
||||
|
||||
// 处理表单提交前验证
|
||||
const handleBeforeSubmit = (e: React.FormEvent) => {
|
||||
// ✅ Runtime permission check
|
||||
if (isEdit && !canUpdateGroup) {
|
||||
e.preventDefault();
|
||||
toastService.warning('您没有修改权限,无法保存更改');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEdit && !canCreateGroup) {
|
||||
e.preventDefault();
|
||||
toastService.warning('您没有创建权限,无法新增分组');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果是只读模式,阻止提交
|
||||
if (isReadOnly) {
|
||||
e.preventDefault();
|
||||
toastService.info('当前为只读模式,无法提交');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 标记所有字段为已触摸
|
||||
setTouchedFields({
|
||||
name: true,
|
||||
code: true,
|
||||
parentId: true
|
||||
});
|
||||
|
||||
|
||||
// 验证所有字段
|
||||
const errors = {
|
||||
name: validateField('name', formValues.name),
|
||||
code: validateField('code', formValues.code),
|
||||
parentId: validateField('parentId', formValues.parentId)
|
||||
};
|
||||
|
||||
|
||||
setFormErrors(errors);
|
||||
|
||||
// 如果有错误,阻止提交
|
||||
if (errors.name || errors.code || (formValues.groupType === "secondary" && errors.parentId)) {
|
||||
|
||||
// 如果有错误,阻止提交并提示
|
||||
const hasErrors = errors.name || errors.code || (formValues.groupType === "secondary" && errors.parentId);
|
||||
if (hasErrors) {
|
||||
e.preventDefault();
|
||||
// ✅ 收集所有错误信息并提示
|
||||
const errorMessages = [];
|
||||
if (errors.name) errorMessages.push(errors.name);
|
||||
if (errors.code) errorMessages.push(errors.code);
|
||||
if (errors.parentId) errorMessages.push(errors.parentId);
|
||||
|
||||
toastService.error(`表单验证失败:${errorMessages[0]}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -502,16 +544,17 @@ export default function RuleGroupNew() {
|
||||
<p className="page-subtitle">创建新的评查点分组,用于组织管理评查点</p>
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<Button
|
||||
type="default"
|
||||
<Button
|
||||
type="default"
|
||||
to="/rule-groups"
|
||||
className="mr-3"
|
||||
>
|
||||
<i className="ri-arrow-left-line"></i> 返回列表
|
||||
</Button>
|
||||
{!isReadOnly && (
|
||||
<Button
|
||||
type="primary"
|
||||
{/* ✅ 仅在有对应权限时显示保存按钮 */}
|
||||
{hasEditPermission && (
|
||||
<Button
|
||||
type="primary"
|
||||
form="group-form"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
|
||||
+66
-42
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams, Link, useNavigate, useFetcher, useRouteLoaderData, useLocation } from "@remix-run/react";
|
||||
import { useLoaderData, useSearchParams, Link, useNavigate, useFetcher, useLocation } from "@remix-run/react";
|
||||
import { Button } from '~/components/ui/Button';
|
||||
import { Card } from '~/components/ui/Card';
|
||||
import { Tag } from '~/components/ui/Tag';
|
||||
@@ -15,6 +15,7 @@ import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterP
|
||||
import { Pagination } from '~/components/ui/Pagination';
|
||||
import { messageService } from '~/components/ui/MessageModal';
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import {
|
||||
getRulesList,
|
||||
deleteRule,
|
||||
@@ -25,7 +26,6 @@ import {
|
||||
type RuleType as ApiRuleType,
|
||||
type RuleGroup
|
||||
} from '~/api/evaluation_points/rules';
|
||||
import type { UserRole } from '~/root';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: rulesStyles }
|
||||
@@ -193,13 +193,20 @@ const priorityLabels = {
|
||||
|
||||
export default function RulesIndex() {
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const rootData = useRouteLoaderData("root") as { userRole: UserRole };
|
||||
const { rules: initialRules, totalCount: initialTotalCount, currentPage, pageSize, ruleTypes: initialRuleTypes, initialLoad } = loaderData;
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const navigate = useNavigate();
|
||||
const fetcher = useFetcher<ActionResponse>();
|
||||
const location = useLocation();
|
||||
|
||||
// ✅ 使用权限 Hook
|
||||
const { canCreate, canUpdate, canDelete, canBatch, canView } = usePermission();
|
||||
const canCreateRule = canCreate('evaluation_point');
|
||||
const canUpdateRule = canUpdate('evaluation_point');
|
||||
const canDeleteRule = canDelete('evaluation_point');
|
||||
const canBatchRule = canBatch('evaluation_point');
|
||||
const canViewRule = canView('evaluation_point');
|
||||
|
||||
// 状态管理
|
||||
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
|
||||
const [loadingGroups, setLoadingGroups] = useState(false);
|
||||
@@ -243,15 +250,6 @@ export default function RulesIndex() {
|
||||
// 判断是否禁用规则组选择
|
||||
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
|
||||
|
||||
// 检查用户是否为开发者角色
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const isDeveloper = userRole.includes('admin');
|
||||
|
||||
// 调试日志
|
||||
// console.log("🔑 [Rules List] rootData:", rootData);
|
||||
// console.log("🔑 [Rules List] 用户角色:", userRole);
|
||||
// console.log("🔑 [Rules List] 是否为管理员:", isDeveloper);
|
||||
|
||||
// 在组件渲染时初始化状态
|
||||
// useEffect(() => {
|
||||
// setFilteredRules(initialRules);
|
||||
@@ -523,6 +521,12 @@ export default function RulesIndex() {
|
||||
|
||||
// 删除评查点
|
||||
const handleDeleteClick = (rule: Rule) => {
|
||||
// ✅ 检查删除权限
|
||||
if (!canDeleteRule) {
|
||||
toastService.warning('您没有删除权限');
|
||||
return;
|
||||
}
|
||||
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: `确认删除评查点【${rule.name}】吗?`,
|
||||
@@ -535,7 +539,7 @@ export default function RulesIndex() {
|
||||
const form = new FormData();
|
||||
form.append("_action", "delete");
|
||||
form.append("ruleId", rule.id);
|
||||
|
||||
|
||||
fetcher.submit(form, { method: "post" });
|
||||
}
|
||||
});
|
||||
@@ -563,6 +567,12 @@ export default function RulesIndex() {
|
||||
|
||||
// 批量启用/禁用
|
||||
const handleBatchEnable = async (isEnabled: boolean) => {
|
||||
// ✅ 检查批量操作权限
|
||||
if (!canBatchRule) {
|
||||
toastService.warning('您没有批量操作权限');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要操作的评查点');
|
||||
return;
|
||||
@@ -594,6 +604,12 @@ export default function RulesIndex() {
|
||||
|
||||
// 批量删除
|
||||
const handleBatchDelete = async () => {
|
||||
// ✅ 检查批量删除权限
|
||||
if (!canBatchRule || !canDeleteRule) {
|
||||
toastService.warning('您没有批量删除权限');
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectedIds.length === 0) {
|
||||
toastService.warning('请先选择要删除的评查点');
|
||||
return;
|
||||
@@ -664,8 +680,8 @@ export default function RulesIndex() {
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
// 添加复选框列(仅开发者可见)
|
||||
...(isDeveloper ? [{
|
||||
// ✅ 添加复选框列(有批量操作权限时可见)
|
||||
...(canBatchRule ? [{
|
||||
title: (
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -762,24 +778,30 @@ export default function RulesIndex() {
|
||||
width: "10%",
|
||||
render: (_: unknown, record: Rule) => (
|
||||
<div className="operations-cell">
|
||||
{isDeveloper ? (
|
||||
// 开发者可以看到编辑、复制、删除
|
||||
{/* ✅ 查看/编辑和复制按钮 - 需要查看权限 */}
|
||||
{canViewRule && (
|
||||
<>
|
||||
<Link to={`/rules/new?id=${record.id}`} className="operation-btn">
|
||||
<i className="ri-edit-line"></i> 编辑
|
||||
{/* ✅ 编辑/查看按钮 - 根据权限显示编辑或查看 */}
|
||||
<Link to={`/rules/new?id=${record.id}${!canUpdateRule ? '&mode=view' : ''}`} className="operation-btn">
|
||||
<i className={canUpdateRule ? "ri-edit-line" : "ri-eye-line"}></i> {canUpdateRule ? '编辑' : '查看'}
|
||||
</Link>
|
||||
<button className="operation-btn" onClick={() => handleCopy(record)}>
|
||||
<i className="ri-file-copy-line"></i> 复制
|
||||
</button>
|
||||
<button className="operation-btn operation-btn-danger" onClick={() => handleDeleteClick(record)}>
|
||||
<i className="ri-delete-bin-line"></i> 删除
|
||||
</button>
|
||||
{/* ✅ 复制按钮 - 有创建权限时显示 */}
|
||||
{canCreateRule && (
|
||||
<button className="operation-btn" onClick={() => handleCopy(record)}>
|
||||
<i className="ri-file-copy-line"></i> 复制
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// 普通用户只能查看
|
||||
<Link to={`/rules/new?id=${record.id}&mode=view`} className="operation-btn">
|
||||
<i className="ri-eye-line"></i> 查看
|
||||
</Link>
|
||||
)}
|
||||
{/* ✅ 删除按钮 - 只需要删除权限 */}
|
||||
{canDeleteRule && (
|
||||
<button className="operation-btn operation-btn-danger" onClick={() => handleDeleteClick(record)}>
|
||||
<i className="ri-delete-bin-line"></i> 删除
|
||||
</button>
|
||||
)}
|
||||
{/* 如果什么权限都没有,显示 - */}
|
||||
{!canViewRule && !canDeleteRule && (
|
||||
<span className="text-gray-400">-</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -805,8 +827,8 @@ export default function RulesIndex() {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 批量操作按钮(仅在有选择时显示) */}
|
||||
{isDeveloper && selectedIds.length > 0 && (
|
||||
{/* ✅ 批量操作按钮(有批量权限且有选择时显示) */}
|
||||
{canBatchRule && selectedIds.length > 0 && (
|
||||
<>
|
||||
<Button
|
||||
type="default"
|
||||
@@ -824,18 +846,20 @@ export default function RulesIndex() {
|
||||
>
|
||||
批量禁用 ({selectedIds.length})
|
||||
</Button>
|
||||
<Button
|
||||
type="danger"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={handleBatchDelete}
|
||||
className="btn-batch-delete"
|
||||
>
|
||||
批量删除 ({selectedIds.length})
|
||||
</Button>
|
||||
{canDeleteRule && (
|
||||
<Button
|
||||
type="danger"
|
||||
icon="ri-delete-bin-line"
|
||||
onClick={handleBatchDelete}
|
||||
className="btn-batch-delete"
|
||||
>
|
||||
批量删除 ({selectedIds.length})
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* 新增按钮 */}
|
||||
{isDeveloper && (
|
||||
{/* ✅ 新增按钮 - 有创建权限时显示 */}
|
||||
{canCreateRule && (
|
||||
<Button type="primary" icon="ri-add-line" to="/rules/new" className="btn-add-rule">
|
||||
新增评查点
|
||||
</Button>
|
||||
|
||||
@@ -49,7 +49,7 @@ import type { EvaluationPointGroup } from "~/models/evaluation_point_groups";
|
||||
// 导入RuleContext上下文
|
||||
import { RuleContext } from "~/contexts/RuleContext";
|
||||
import { toastService } from '~/components/ui/Toast';
|
||||
import type { UserRole } from '~/root';
|
||||
import { usePermission } from '~/hooks/usePermission';
|
||||
import { getPromptTemplateOptions } from '~/api/prompts/prompts';
|
||||
import {
|
||||
createEvaluationPoint,
|
||||
@@ -148,26 +148,50 @@ export default function RuleNew() {
|
||||
const [isCopyMode, setIsCopyMode] = useState(false); // 添加复制模式状态
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [instanceKey, setInstanceKey] = useState<string>('new');
|
||||
// 从root路由获取用户角色和JWT token
|
||||
const rootData = useRouteLoaderData("root") as { userRole: UserRole; frontendJWT?: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
// 从root路由获取JWT token
|
||||
const rootData = useRouteLoaderData("root") as { frontendJWT?: string };
|
||||
const frontendJWT = rootData?.frontendJWT;
|
||||
|
||||
// ✅ 使用权限 Hook
|
||||
const { canCreate, canUpdate } = usePermission();
|
||||
const canCreateRule = canCreate('evaluation_point');
|
||||
const canUpdateRule = canUpdate('evaluation_point');
|
||||
|
||||
// ✅ 判断表单是否为只读模式
|
||||
// 从 URL 检查是否为查看模式
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const urlMode = searchParams.get('mode');
|
||||
const isViewMode = urlMode === 'view';
|
||||
|
||||
// 根据模式和权限决定是否只读
|
||||
const hasEditPermission = isEditMode ? canUpdateRule : canCreateRule;
|
||||
const isReadOnly = isViewMode || !hasEditPermission;
|
||||
|
||||
// 使用 ref 跟踪当前加载的 URL,避免重复加载
|
||||
const loadedUrlRef = useRef<string>('');
|
||||
|
||||
const [formData, setFormData] = useState<EvaluationPoint>({});
|
||||
const [evaluationPointGroups, setEvaluationPointGroups] = useState<EvaluationPointGroup[]>([]);
|
||||
|
||||
// 判断表单是否为只读模式
|
||||
const isReadOnly = userRole === 'common';
|
||||
|
||||
// 添加用于共享的字段数据状态
|
||||
const [extractionFields, setExtractionFields] = useState<string[]>([]);
|
||||
|
||||
// VLM字段类型选项
|
||||
const [vlmFieldTypeOptions, setVlmFieldTypeOptions] = useState<Array<{ value: string; label: string }>>([]);
|
||||
|
||||
// ✅ 页面加载时检查权限并提示(仅在只读模式下提示)
|
||||
useEffect(() => {
|
||||
if (isReadOnly && !isLoading) {
|
||||
if (isViewMode) {
|
||||
// toastService.info('当前为查看模式');
|
||||
} else if (isEditMode && !canUpdateRule) {
|
||||
toastService.info('当前为查看模式,您没有编辑权限');
|
||||
} else if (!isEditMode && !canCreateRule) {
|
||||
toastService.warning('您没有创建评查点的权限');
|
||||
}
|
||||
}
|
||||
}, [isReadOnly, isViewMode, isEditMode, canUpdateRule, canCreateRule, isLoading]);
|
||||
|
||||
/**
|
||||
* 从表单数据中提取所有字段
|
||||
* 用于编辑模式下初始化字段数据
|
||||
@@ -417,6 +441,17 @@ export default function RuleNew() {
|
||||
const handleSave = async () => {
|
||||
// console.log("保存评查点", formData);
|
||||
|
||||
// ✅ Runtime permission check
|
||||
if (isEditMode && !canUpdateRule) {
|
||||
toastService.warning('您没有修改权限,无法保存更改');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isEditMode && !canCreateRule) {
|
||||
toastService.warning('您没有创建权限,无法新增评查点');
|
||||
return;
|
||||
}
|
||||
|
||||
// ========== 验证必填字段 ==========
|
||||
|
||||
// 1. 验证评查点名称
|
||||
|
||||
Reference in New Issue
Block a user