feat: 1. 完善全局路由的访问权限的验证。 2. 完善接口返回的树形路由结构 3.优化评查点列表的查询,改用表连接的方式,废弃使用数据库的rpc函数,同时进行地区隔离和权限隔离。
4. 删除冗余的评查文件列表。 5.完善上传文档 页面初始化查询数据的时候 查询文件类型(改成动态指定) 6. 添加获取入口模块的查询接口。 7.完善服务端中判断token的有效性,失效则跳转到登录页。 8. 重构layout和sidebar的页面,改成由动态权限路由来渲染对应的菜单栏。 9.重构入口页面,通过动态查询根据不同地区的人返回不同的入口。
This commit is contained in:
+102
-51
@@ -4,6 +4,7 @@ import { type MetaFunction, type ActionFunctionArgs, LoaderFunctionArgs, redirec
|
||||
import styles from "~/styles/pages/home.css?url";
|
||||
import dayjs from 'dayjs';
|
||||
import { getUserSession, logout } from "~/api/login/auth.server";
|
||||
import { toastService } from '~/components/ui';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: styles }
|
||||
@@ -28,15 +29,28 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 获取用户信息(不再检查服务端认证)
|
||||
// 获取用户信息
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// ⚠️ 不再检查服务端 session 认证
|
||||
// 认证检查由 ClientAuthGuard 在客户端进行
|
||||
// 🔒 认证检查已在 getUserSession() 中统一处理
|
||||
// 如果未认证,会自动重定向到登录页,不会执行到这里
|
||||
const { userRole, userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
const { userRole, userInfo } = await getUserSession(request);
|
||||
// 🔑 获取用户地区并查询入口模块
|
||||
const userArea = userInfo?.area || null;
|
||||
// console.log('🔍 [Index Loader] 用户地区:', userArea);
|
||||
// console.log('🔍 [Index Loader] 用户角色:', userRole);
|
||||
|
||||
// 返回用户信息给客户端(可能为空)
|
||||
return Response.json({ userRole, userInfo });
|
||||
let entryModules = [];
|
||||
if (userRole && frontendJWT) {
|
||||
const { getEntryModules } = await import('~/api/home/home');
|
||||
entryModules = await getEntryModules(userRole,userArea, frontendJWT);
|
||||
console.log(`📦 [Index Loader] 获取到 ${entryModules.length} 个入口模块`);
|
||||
} else {
|
||||
console.warn('⚠️ [Index Loader] 用户角色为空,返回空模块列表');
|
||||
}
|
||||
|
||||
// 返回用户信息和入口模块给客户端
|
||||
return Response.json({ userRole, userInfo, entryModules });
|
||||
}
|
||||
|
||||
export default function Index() {
|
||||
@@ -102,21 +116,73 @@ export default function Index() {
|
||||
}, []);
|
||||
|
||||
// 处理模块点击
|
||||
const handleModuleClick = (path: string, reviewType: string) => {
|
||||
// 将reviewType存入sessionStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem('reviewType', reviewType);
|
||||
const handleModuleClick = (module: typeof loaderData.entryModules[0]) => {
|
||||
// 提取文档类型 IDs
|
||||
const typeIds = module.document_types?.map(dt => dt.id) || [];
|
||||
|
||||
// 🔑 验证文档类型(智慧法务大模型除外)
|
||||
if (module.name !== '智慧法务大模型' && typeIds.length === 0) {
|
||||
toastService.error('该入口尚未关联文档类型,无法进入');
|
||||
console.warn('⚠️ [Index] 模块未关联文档类型:', module.name);
|
||||
return; // 阻止进入
|
||||
}
|
||||
navigate(path);
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
// 🔑 存储到 sessionStorage(用于客户端请求)
|
||||
if (typeIds.length > 0) {
|
||||
sessionStorage.setItem('documentTypeIds', JSON.stringify(typeIds));
|
||||
|
||||
// console.log('📝 [Index] 存储到客户端 sessionStorage:', typeIds);
|
||||
} else {
|
||||
// 清空文档类型数据
|
||||
sessionStorage.removeItem('documentTypeIds');
|
||||
}
|
||||
|
||||
// 存储模块信息
|
||||
sessionStorage.setItem('selectedModuleId', String(module.id));
|
||||
sessionStorage.setItem('selectedModuleName', module.name);
|
||||
sessionStorage.setItem('selectedModulePicPath', module.path)
|
||||
}
|
||||
|
||||
// 🔑 根据模块名称决定跳转路径
|
||||
let targetPath = '/home'; // 默认跳转到首页
|
||||
|
||||
if (module.name.includes('合同')) {
|
||||
// 合同相关模块 → 跳转到合同模板搜索
|
||||
targetPath = '/contract-template/search';
|
||||
// console.log('📌 [Index] 合同模块,跳转到:', targetPath);
|
||||
} else if (module.name === '智慧法务大模型') {
|
||||
// 智慧法务大模型 → 跳转到 AI 对话
|
||||
targetPath = '/chat-with-llm';
|
||||
// console.log('📌 [Index] 智慧法务大模型,跳转到:', targetPath);
|
||||
} else {
|
||||
// console.log('📌 [Index] 其他模块,跳转到:', targetPath);
|
||||
}
|
||||
|
||||
navigate(targetPath);
|
||||
};
|
||||
|
||||
// 处理键盘事件
|
||||
const handleKeyDown = (path: string, reviewType: string, e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
const handleKeyDown = (module: typeof loaderData.entryModules[0], e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
handleModuleClick(path, reviewType);
|
||||
handleModuleClick(module);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取模块图标(根据模块 path 或 id)
|
||||
const getModuleIcon = (module: typeof loaderData.entryModules[0]) => {
|
||||
// 根据 path 判断图标
|
||||
if (module.path?.includes('ht')) {
|
||||
return '/images/icon_hetong.png';
|
||||
} else if (module.path?.includes('aj')) {
|
||||
return '/images/icon_anjuan.png';
|
||||
} else if (module.path?.includes('nw')) {
|
||||
return '/images/icon_assistant.png';
|
||||
}
|
||||
// 默认图标
|
||||
return '/images/icon_assistant.png';
|
||||
};
|
||||
|
||||
// 处理登出
|
||||
const handleLogout = () => {
|
||||
// 清除sessionStorage中的所有数据
|
||||
@@ -182,46 +248,31 @@ export default function Index() {
|
||||
<h1 className="welcome-text">- 欢迎来到智慧法务平台 -</h1>
|
||||
|
||||
<div className="modules-container">
|
||||
{/* 合同管理模块 - 51708端口时隐藏 */}
|
||||
{!isPort51707 && (
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/contract-template/search', 'contract')}
|
||||
onKeyDown={(e) => handleKeyDown('/contract-template/search', 'contract', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="合同管理"
|
||||
>
|
||||
<img src="/images/icon_hetong.png" alt="合同管理" className="w-12 h-12 mx-1" />
|
||||
<span className="module-name">合同管理</span>
|
||||
{/* 动态渲染入口模块 */}
|
||||
{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>
|
||||
))
|
||||
) : (
|
||||
<div className="text-center text-gray-500 py-8">
|
||||
暂无可用模块
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 案卷智能评查模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/home', 'record')}
|
||||
onKeyDown={(e) => handleKeyDown('/home', 'record', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="案卷智能评查"
|
||||
>
|
||||
<img src="/images/icon_anjuan.png" alt="案卷智能评查" className="w-12 h-12" />
|
||||
<span className="module-name">案卷智能评查</span>
|
||||
</div>
|
||||
|
||||
{/* 智慧法务大模型模块 */}
|
||||
<div
|
||||
className="module-card"
|
||||
onClick={() => handleModuleClick('/chat-with-llm', 'model')}
|
||||
onKeyDown={(e) => handleKeyDown('/chat-with-llm', 'model', e)}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="智慧法务大模型"
|
||||
>
|
||||
<img src="/images/icon_assistant.png" alt="智慧法务大模型" className="w-12 h-12" />
|
||||
<span className="module-name">智慧法务大模型</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -205,6 +205,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
is_leader: savedUserInfo.is_leader,
|
||||
user_id: savedUserInfo.user_id,
|
||||
user_role: savedUserInfo.user_role, // 使用后端返回的角色
|
||||
area: savedUserInfo.area, // 🔑 用户所属地区
|
||||
frontend_jwt: frontendJWT
|
||||
};
|
||||
|
||||
@@ -222,6 +223,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
ou_name: savedUserInfo.ou_name,
|
||||
is_leader: savedUserInfo.is_leader,
|
||||
user_role: savedUserInfo.user_role,
|
||||
area: savedUserInfo.area, // 🔑 用户所属地区
|
||||
sub: userInfo.data.sub
|
||||
})));
|
||||
callbackUrl.searchParams.set('redirectTo', redirectTo);
|
||||
|
||||
@@ -56,7 +56,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
label: type.name
|
||||
}));
|
||||
|
||||
// 初始返回空数据,将在客户端根据 sessionStorage 中的 reviewType 加载实际数据
|
||||
// 初始返回空数据,将在客户端根据 sessionStorage 中的 documentTypeIds 加载实际数据
|
||||
return Response.json({
|
||||
documents: [],
|
||||
total: 0,
|
||||
@@ -64,6 +64,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
pageSize,
|
||||
documentTypeOptions,
|
||||
userInfo, // 传递用户信息到客户端
|
||||
frontendJWT, // 传递 JWT 到客户端
|
||||
initialLoad: true // 标记这是初始加载
|
||||
});
|
||||
};
|
||||
@@ -295,16 +296,22 @@ export default function DocumentsIndex() {
|
||||
throw new Error(documentsResponse.error);
|
||||
}
|
||||
|
||||
// 🔑 从 sessionStorage 读取文档类型 IDs
|
||||
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
|
||||
const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : undefined;
|
||||
|
||||
// 获取经过过滤的文档类型列表(token 由 axios 拦截器自动获取)
|
||||
const filteredTypesResponse = await getDocumentTypes({
|
||||
pageSize: 500,
|
||||
reviewType: storedReviewType || undefined
|
||||
documentTypeIds: documentTypeIds // 使用动态的文档类型 IDs
|
||||
});
|
||||
const filteredDocumentTypes = filteredTypesResponse.data?.types || [];
|
||||
const filteredOptions = filteredDocumentTypes.map(type => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
}));
|
||||
|
||||
// console.log('文档列表',documentsResponse)
|
||||
|
||||
// 更新状态
|
||||
setDocuments(documentsResponse.data?.documents || []);
|
||||
@@ -824,7 +831,8 @@ export default function DocumentsIndex() {
|
||||
attachmentFiles,
|
||||
attachmentMergeMode,
|
||||
true, // isReprocess
|
||||
attachmentRemark || undefined
|
||||
attachmentRemark || undefined,
|
||||
loaderData.frontendJWT
|
||||
);
|
||||
|
||||
if (result.error) {
|
||||
@@ -1153,7 +1161,7 @@ export default function DocumentsIndex() {
|
||||
{record.historyCount !== undefined && record.historyCount > 0 ?
|
||||
<span className="version-badge">
|
||||
<i className="ri-history-line"></i>
|
||||
v{record.historyCount + 1} {record.historyCount !== undefined && `(共${record.historyCount}个历史版本)`}
|
||||
v{record.historyCount + 1} {record.historyCount !== undefined && `(有${record.historyCount}个历史版本)`}
|
||||
</span> : ""
|
||||
}
|
||||
</div>
|
||||
|
||||
+66
-72
@@ -259,11 +259,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
const mode = url.searchParams.get("mode") || "create";
|
||||
|
||||
// 我们不能在服务器端访问 sessionStorage,所以在客户端组件中处理 reviewType 过滤
|
||||
// 我们不能在服务器端访问 sessionStorage,所以在客户端组件中处理 documentTypeIds 过滤
|
||||
// 并行加载文档和文档类型
|
||||
const [documentsResponse, typesResponse] = await Promise.all([
|
||||
getTodayDocuments(userInfo, undefined, frontendJWT),
|
||||
getDocumentTypes(undefined, frontendJWT)
|
||||
getTodayDocuments(userInfo, frontendJWT),
|
||||
getDocumentTypes(frontendJWT)
|
||||
]);
|
||||
|
||||
// console.log('loader: 文档加载结果:', documentsResponse);
|
||||
@@ -308,9 +308,9 @@ export default function FilesUpload() {
|
||||
const [isNavigating, setIsNavigating] = useState(false)
|
||||
const revalidator = useRevalidator()
|
||||
|
||||
// 获取 sessionStorage 中的 reviewType 值
|
||||
// 获取 sessionStorage 中的 documentTypeIds 值
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [reviewType, setReviewType] = useState<string | null>(null);
|
||||
const [documentTypeIds, setDocumentTypeIds] = useState<number[] | null>(null);
|
||||
|
||||
// 使用 useLoaderData 获取初始数据
|
||||
const loaderData = useLoaderData<LoaderData>();
|
||||
@@ -361,19 +361,20 @@ export default function FilesUpload() {
|
||||
const [queueFiles, setQueueFiles] = useState<Document[]>([]);
|
||||
const [documentTypesState, setDocumentTypesState] = useState<DocumentType[]>([]);
|
||||
|
||||
// 在组件挂载时从 sessionStorage 获取 reviewType
|
||||
// 在组件挂载时从 sessionStorage 获取 documentTypeIds
|
||||
useEffect(() => {
|
||||
try {
|
||||
// 在客户端环境中执行
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedReviewType = sessionStorage.getItem('reviewType');
|
||||
setReviewType(storedReviewType);
|
||||
// 根据 reviewType 过滤文档类型和文档列表
|
||||
filterDocumentTypes(storedReviewType, loaderData.documentTypes);
|
||||
filterDocuments(storedReviewType);
|
||||
|
||||
// 如果reviewType是contract,自动选择合同文档类型
|
||||
if (storedReviewType === 'contract') {
|
||||
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
|
||||
const typeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
setDocumentTypeIds(typeIds);
|
||||
// 根据 documentTypeIds 过滤文档类型和文档列表
|
||||
filterDocumentTypes(typeIds, loaderData.documentTypes);
|
||||
filterDocuments(typeIds);
|
||||
|
||||
// 如果包含合同类型(ID=1),自动选择合同文档类型
|
||||
if (typeIds && typeIds.includes(1)) {
|
||||
setIsContractType(true);
|
||||
// 查找ID为1的合同文档类型
|
||||
const contractType = loaderData.documentTypes.find(type => type.id === 1);
|
||||
@@ -385,49 +386,39 @@ export default function FilesUpload() {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取 sessionStorage 中的 reviewType 失败:', error);
|
||||
console.error('获取 sessionStorage 中的 documentTypeIds 失败:', error);
|
||||
}
|
||||
}, [loaderData]);
|
||||
|
||||
// 过滤文档类型列表
|
||||
const filterDocumentTypes = (reviewType: string | null, types: DocumentType[]) => {
|
||||
if (!reviewType) {
|
||||
// 如果没有特定的 reviewType,使用原始数据
|
||||
const filterDocumentTypes = (documentTypeIds: number[] | null, types: DocumentType[]) => {
|
||||
if (!documentTypeIds || documentTypeIds.length === 0) {
|
||||
// 如果没有特定的 documentTypeIds,使用原始数据
|
||||
setDocumentTypesState(types);
|
||||
return;
|
||||
}
|
||||
|
||||
let filteredTypes: DocumentType[] = [];
|
||||
|
||||
if (reviewType === 'contract') {
|
||||
// 只保留 id=1 的选项
|
||||
filteredTypes = types.filter(type => type.id === 1);
|
||||
} else if (reviewType === 'record') {
|
||||
// 只保留 id=2 和 id=3 的选项
|
||||
filteredTypes = types.filter(type => type.id === 2 || type.id === 3 || type.id === 155);
|
||||
} else {
|
||||
// 如果reviewType不匹配任何条件,使用原始数据
|
||||
filteredTypes = types;
|
||||
}
|
||||
|
||||
|
||||
// 根据 documentTypeIds 过滤文档类型
|
||||
const filteredTypes = types.filter(type => documentTypeIds.includes(type.id));
|
||||
|
||||
setDocumentTypesState(filteredTypes);
|
||||
};
|
||||
|
||||
// 过滤文档列表
|
||||
const filterDocuments = async (reviewType: string | null) => {
|
||||
if (!reviewType) {
|
||||
// 如果没有特定的 reviewType,使用原始数据
|
||||
const filterDocuments = async (documentTypeIds: number[] | null) => {
|
||||
if (!documentTypeIds || documentTypeIds.length === 0) {
|
||||
// 如果没有特定的 documentTypeIds,使用原始数据
|
||||
const documents = loaderData.documents;
|
||||
setQueueFiles(documents);
|
||||
|
||||
|
||||
// 启动状态检查定时器
|
||||
startStatusChecker(documents);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 使用 reviewType 获取过滤后的文档列表
|
||||
const response = await getTodayDocuments(loaderData.userInfo || undefined, reviewType, loaderData.frontendJWT || undefined);
|
||||
// 使用 documentTypeIds 获取过滤后的文档列表
|
||||
const response = await getTodayDocuments(loaderData.userInfo || undefined, loaderData.frontendJWT || undefined, documentTypeIds);
|
||||
|
||||
if (response.error) {
|
||||
console.error('过滤文档列表失败:', response.error);
|
||||
@@ -559,45 +550,48 @@ export default function FilesUpload() {
|
||||
const checkQueueStatusWithFiles = async (files: Document[]) => {
|
||||
try {
|
||||
// console.log('开始检查队列状态,当前队列文件:', files);
|
||||
|
||||
// 直接从sessionStorage读取reviewType,避免异步状态更新问题
|
||||
const currentReviewType = typeof window !== 'undefined' ? sessionStorage.getItem('reviewType') : null;
|
||||
// console.log('从sessionStorage读取的reviewType:', currentReviewType);
|
||||
|
||||
|
||||
// 直接从sessionStorage读取documentTypeIds,避免异步状态更新问题
|
||||
const typeIdsStr = typeof window !== 'undefined' ? sessionStorage.getItem('documentTypeIds') : null;
|
||||
const currentDocumentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
// console.log('从sessionStorage读取的documentTypeIds:', currentDocumentTypeIds);
|
||||
|
||||
// 获取所有未完成的文档
|
||||
const incompleteFiles = files.filter(file =>
|
||||
const incompleteFiles = files.filter(file =>
|
||||
file.status !== DocumentStatus.PROCESSED && file.id
|
||||
);
|
||||
|
||||
|
||||
if (incompleteFiles.length === 0) {
|
||||
console.log('没有未完成的文档,跳过状态检查');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let statusResponse;
|
||||
|
||||
// 如果是合同类型,需要分类处理
|
||||
console.log('当前reviewType:', currentReviewType);
|
||||
if (currentReviewType === 'contract') {
|
||||
// 分类文档ID
|
||||
const mainDocumentIds: number[] = [];
|
||||
const attachmentIds: number[] = [];
|
||||
|
||||
// 如果是合同类型(ID=1),需要分类处理
|
||||
// console.log('当前documentTypeIds:', currentDocumentTypeIds);
|
||||
// if (currentDocumentTypeIds && currentDocumentTypeIds.includes(1)) {
|
||||
// // 分类文档ID
|
||||
// const mainDocumentIds: number[] = [];
|
||||
// const attachmentIds: number[] = [];
|
||||
|
||||
incompleteFiles.forEach(file => {
|
||||
// 检查是否存在template_contract_path属性来判断是否为合同附件
|
||||
if ('template_contract_path' in file && file.template_contract_path) {
|
||||
attachmentIds.push(file.id);
|
||||
} else {
|
||||
mainDocumentIds.push(file.id);
|
||||
}
|
||||
});
|
||||
// incompleteFiles.forEach(file => {
|
||||
// // 检查是否存在template_contract_path属性来判断是否为合同附件
|
||||
// if ('template_contract_path' in file && file.template_contract_path) {
|
||||
// attachmentIds.push(file.id);
|
||||
// } else {
|
||||
// mainDocumentIds.push(file.id);
|
||||
// }
|
||||
// });
|
||||
|
||||
// console.log('合同主文件ID:', mainDocumentIds);
|
||||
// console.log('合同附件ID:', attachmentIds);
|
||||
// // console.log('合同主文件ID:', mainDocumentIds);
|
||||
// // console.log('合同附件ID:', attachmentIds);
|
||||
|
||||
// 分别查询状态
|
||||
statusResponse = await getDocumentsStatus(mainDocumentIds, attachmentIds, loaderData.frontendJWT || undefined);
|
||||
} else {
|
||||
// // 分别查询状态
|
||||
// statusResponse = await getDocumentsStatus(mainDocumentIds, attachmentIds, loaderData.frontendJWT || undefined);
|
||||
// }
|
||||
// else
|
||||
{
|
||||
// 非合同类型,使用原有逻辑
|
||||
const incompleteIds = incompleteFiles.map(file => file.id);
|
||||
// console.log('未完成的文档ID:', incompleteIds);
|
||||
@@ -959,9 +953,9 @@ export default function FilesUpload() {
|
||||
setAttachmentRemark("");
|
||||
setShowAttachmentUpload(false);
|
||||
setSelectedDocumentId(null);
|
||||
|
||||
|
||||
// 刷新文档列表
|
||||
await filterDocuments(reviewType);
|
||||
await filterDocuments(documentTypeIds);
|
||||
|
||||
} catch (error) {
|
||||
console.error('【附件追加】上传失败:', error);
|
||||
@@ -1028,9 +1022,9 @@ export default function FilesUpload() {
|
||||
setTemplateFile(null);
|
||||
setShowTemplateUpload(false);
|
||||
setSelectedDocumentId(null);
|
||||
|
||||
|
||||
// 刷新文档列表
|
||||
await filterDocuments(reviewType);
|
||||
await filterDocuments(documentTypeIds);
|
||||
|
||||
} catch (error) {
|
||||
console.error('【合同模板上传】上传失败:', error);
|
||||
@@ -1163,7 +1157,7 @@ export default function FilesUpload() {
|
||||
setCompletedFiles(uploadedFiles);
|
||||
startProcessing(uploadedFiles);
|
||||
// 刷新队列
|
||||
await filterDocuments(reviewType);
|
||||
await filterDocuments(documentTypeIds);
|
||||
} catch (error) {
|
||||
console.error('合同首传上传失败:', error);
|
||||
messageService.error(`合同上传失败:${error instanceof Error ? error.message : '未知错误'}`);
|
||||
|
||||
+43
-6
@@ -24,21 +24,41 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// ⚠️ 不再检查服务端 session 认证
|
||||
// 认证检查改为在客户端通过 localStorage 进行
|
||||
|
||||
// 获取重定向URL
|
||||
// 获取重定向URL和错误参数
|
||||
const url = new URL(request.url);
|
||||
const redirectTo = url.searchParams.get("redirect") || "/";
|
||||
const urlError = url.searchParams.get("error");
|
||||
|
||||
const session = await getSession(request);
|
||||
|
||||
// 读取 flash 消息(来自 callback 的错误)
|
||||
const loginError = session.get("loginError");
|
||||
|
||||
// 将URL错误参数转换为友好的错误消息
|
||||
let urlErrorMessage: string | null = null;
|
||||
if (urlError) {
|
||||
switch (urlError) {
|
||||
case 'no_role':
|
||||
urlErrorMessage = '用户角色信息缺失,请重新登录';
|
||||
break;
|
||||
case 'no_token':
|
||||
urlErrorMessage = '认证令牌缺失,请重新登录';
|
||||
break;
|
||||
case 'session_expired':
|
||||
urlErrorMessage = '会话已过期,请重新登录';
|
||||
break;
|
||||
default:
|
||||
urlErrorMessage = '登录状态异常,请重新登录';
|
||||
}
|
||||
}
|
||||
|
||||
// 提交 session 以清除 flash 消息
|
||||
if (loginError) {
|
||||
const { sessionStorage } = await import("~/api/login/auth.server");
|
||||
return Response.json({
|
||||
redirectTo,
|
||||
flashError: loginError
|
||||
flashError: loginError,
|
||||
urlError: urlErrorMessage
|
||||
}, {
|
||||
headers: {
|
||||
"Set-Cookie": await sessionStorage.commitSession(session)
|
||||
@@ -48,7 +68,8 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
return Response.json({
|
||||
redirectTo,
|
||||
flashError: null
|
||||
flashError: null,
|
||||
urlError: urlErrorMessage
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,6 +128,11 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
}, { status: 500 });
|
||||
}
|
||||
|
||||
// 测试,管理员账密返回的时候默认给没有area信息
|
||||
// if(!user_info.area){
|
||||
// user_info.area = '梅州'
|
||||
// }
|
||||
|
||||
// 🔑 将后端返回的 issued_time 转换为时间戳(毫秒)
|
||||
let tokenIssuedAt = Date.now(); // 默认使用当前时间
|
||||
if (issued_time) {
|
||||
@@ -141,6 +167,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
ou_name: user_info.ou_name,
|
||||
is_leader: user_info.is_leader,
|
||||
user_role: user_info.user_role,
|
||||
area: user_info.area, // 🔑 用户所属地区
|
||||
sub: user_info.sub
|
||||
})));
|
||||
callbackUrl.searchParams.set('redirectTo', redirectTo);
|
||||
@@ -163,6 +190,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
ou_name: user_info.ou_name,
|
||||
is_leader: user_info.is_leader,
|
||||
user_role: user_info.user_role,
|
||||
area: user_info.area, // 🔑 用户所属地区
|
||||
sub: user_info.sub
|
||||
}
|
||||
});
|
||||
@@ -185,11 +213,12 @@ export default function Login() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [passwordLoginError, setPasswordLoginError] = useState<string | null>(null);
|
||||
|
||||
// 从 loaderData 中获取 OAuth 回调的错误信息
|
||||
// 从 loaderData 中获取错误信息
|
||||
const oauthError = loaderData?.flashError;
|
||||
const urlError = loaderData?.urlError;
|
||||
|
||||
// 显示的错误信息:密码登录错误优先,其次是 OAuth 错误
|
||||
const error = passwordLoginError || oauthError;
|
||||
// 显示的错误信息:密码登录错误优先,其次是 URL 错误,最后是 OAuth 错误
|
||||
const error = passwordLoginError || urlError || oauthError;
|
||||
const isLocked = false; // 可以从后端响应中获取
|
||||
const retryCount = 0;
|
||||
const remainingAttempts = 5;
|
||||
@@ -283,6 +312,14 @@ export default function Login() {
|
||||
}
|
||||
}, [fetcher.data]);
|
||||
|
||||
// 显示URL错误参数的Toast提示
|
||||
useEffect(() => {
|
||||
if (urlError) {
|
||||
console.warn("⚠️ [Login] 检测到URL错误参数:", urlError);
|
||||
toastService.error(urlError);
|
||||
}
|
||||
}, [urlError]);
|
||||
|
||||
useEffect(() => {
|
||||
// 🔑 只在 token 过期时清理客户端存储
|
||||
// 检查 URL 参数中是否有 expired=true 标识
|
||||
|
||||
@@ -1,985 +0,0 @@
|
||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useLoaderData, useSearchParams, useNavigate } from "@remix-run/react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { FileIcon } from "~/components/ui/FileIcon";
|
||||
import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel";
|
||||
import { Pagination } from "~/components/ui/Pagination";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { StatusBadge } from "~/components/ui/StatusBadge";
|
||||
import { FileTypeTag, links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
|
||||
import { NumberSkeleton, TableRowSkeleton, LoadingIndicator } from "~/components/ui/SkeletonScreen";
|
||||
import rulesFilesStyles from "~/styles/pages/rules-files.css?url";
|
||||
import {
|
||||
getReviewFiles,
|
||||
type ReviewFileUI,
|
||||
updateDocumentAuditStatus,
|
||||
type DocumentSearchParams
|
||||
} from "~/api/evaluation_points/rules-files";
|
||||
import { getDocumentTypes } from "~/api/document-types/document-types";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
// 导入axios下载文件方法
|
||||
import { downloadFile } from "~/api/axios-client";
|
||||
import { appendContractAttachments } from "~/api/files/files-upload";
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: rulesFilesStyles },
|
||||
...fileTypeTagLinks()
|
||||
];
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "评查文件列表"
|
||||
};
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "评查文件列表 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{ name: "description", content: "管理系统中所有上传的评查文件,支持按文件类型、评查状态进行筛选" },
|
||||
{ name: "keywords", content: "评查文件,合同审核,中国烟草,文件管理" }
|
||||
];
|
||||
};
|
||||
|
||||
// 日期范围枚举
|
||||
export enum DateRange {
|
||||
ALL = 'all',
|
||||
TODAY = 'today',
|
||||
WEEK = 'week',
|
||||
MONTH = 'month',
|
||||
CUSTOM = 'custom'
|
||||
}
|
||||
|
||||
// 评查状态标签映射
|
||||
export const REVIEW_STATUS_LABELS: Record<string, string> = {
|
||||
'pass': '通过',
|
||||
'warning': '警告',
|
||||
'fail': '不通过',
|
||||
'pending': '待人工确认'
|
||||
};
|
||||
|
||||
// 加载评查文件列表
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
// 获取用户会话信息
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 获取分页参数
|
||||
const url = new URL(request.url);
|
||||
const currentPage = parseInt(url.searchParams.get("page") || "1", 10);
|
||||
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
|
||||
|
||||
try {
|
||||
// 获取文档类型列表(服务端需要显式传递 token,客户端依赖 axios 拦截器)
|
||||
const typesResponse = await getDocumentTypes({pageSize:500}, frontendJWT);
|
||||
const documentTypes = typesResponse.data?.types || [];
|
||||
|
||||
// 返回初始空数据,客户端将根据 sessionStorage 中的 reviewType 加载实际数据
|
||||
return Response.json({
|
||||
files: [],
|
||||
documentTypes,
|
||||
totalCount: 0,
|
||||
currentPage,
|
||||
pageSize,
|
||||
userInfo, // 传递用户信息到客户端
|
||||
initialLoad: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载评查文件列表失败:', error);
|
||||
return Response.json({ result: false, message: error instanceof Error ? error.message : '加载评查文件列表失败' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export default function RulesFiles() {
|
||||
const navigate = useNavigate();
|
||||
const { files: initialFiles, documentTypes: allDocumentTypes, totalCount: initialTotal, currentPage, pageSize, userInfo, result, message } = useLoaderData<typeof loader>();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const dateFrom = searchParams.get('dateFrom') || '';
|
||||
const dateTo = searchParams.get('dateTo') || '';
|
||||
|
||||
// 添加状态管理
|
||||
const [files, setFiles] = useState<ReviewFileUI[]>(initialFiles);
|
||||
const [documentTypes, setDocumentTypes] = useState(allDocumentTypes);
|
||||
const [totalCount, setTotalCount] = useState(initialTotal);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [reviewType, setReviewType] = useState<string | null>(null);
|
||||
|
||||
// 附件追加相关状态
|
||||
const [showAttachmentUpload, setShowAttachmentUpload] = useState<boolean>(false);
|
||||
const [selectedDocumentId, setSelectedDocumentId] = useState<string | null>(null);
|
||||
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
||||
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('overwrite');
|
||||
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
||||
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
||||
|
||||
// 保存/恢复 查询参数 的 sessionStorage key
|
||||
const SEARCH_PARAMS_STORAGE_KEY = 'rulesFiles.searchParams';
|
||||
|
||||
const persistSearchParams = useCallback((params: URLSearchParams) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, params.toString());
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 首次进入列表页且 URL 无查询参数时,尝试恢复上次保存的参数
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
const hasAnyParam = Array.from(searchParams.keys()).length > 0;
|
||||
const stored = sessionStorage.getItem(SEARCH_PARAMS_STORAGE_KEY);
|
||||
if (!hasAnyParam && stored) {
|
||||
setSearchParams(new URLSearchParams(stored));
|
||||
}
|
||||
// 仅在初始渲染时检查
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// 处理初始加载数据loader的错误
|
||||
useEffect(() => {
|
||||
if(result === false && message) {
|
||||
toastService.error(message);
|
||||
}
|
||||
}, [result, message]);
|
||||
|
||||
// 辅助函数:从 localStorage 获取用户ID
|
||||
const getUserId = useCallback((): string | undefined => {
|
||||
if (typeof window === 'undefined') return undefined;
|
||||
|
||||
const userInfoStr = localStorage.getItem('user_info');
|
||||
if (!userInfoStr) return undefined;
|
||||
|
||||
try {
|
||||
const userInfoData = JSON.parse(userInfoStr);
|
||||
return userInfoData.user_id?.toString();
|
||||
} catch (error) {
|
||||
console.error('解析 localStorage 用户信息失败:', error);
|
||||
return undefined;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 客户端数据请求
|
||||
const fetchData = useCallback(async (params: Record<string, string>) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// 构建搜索参数(token 由 axios 拦截器自动从 localStorage 获取)
|
||||
const searchParams: DocumentSearchParams = {
|
||||
fileType: params.fileType || undefined,
|
||||
reviewStatus: params.reviewStatus || undefined,
|
||||
dateFrom: params.dateFrom || undefined,
|
||||
dateTo: params.dateTo || undefined,
|
||||
keyword: params.keyword || undefined,
|
||||
sortOrder: params.sortOrder || 'upload_time_desc',
|
||||
page: parseInt(params.page || "1", 10),
|
||||
pageSize: parseInt(params.pageSize || "10", 10)
|
||||
};
|
||||
|
||||
// 根据 reviewType 添加类型过滤
|
||||
if (reviewType === 'contract') {
|
||||
searchParams.fileType = 'contract';
|
||||
} else if (reviewType === 'record') {
|
||||
// 在 API 层处理 type_id 为 2 或 3 的过滤
|
||||
searchParams.fileType = 'record';
|
||||
}
|
||||
|
||||
// 如果用户手动选择了文件类型,优先使用用户选择的
|
||||
if (params.fileType) {
|
||||
searchParams.fileType = params.fileType;
|
||||
}
|
||||
|
||||
// 从 localStorage 获取用户ID(与 token 管理保持一致)
|
||||
const userId = getUserId();
|
||||
if (!userId) {
|
||||
throw new Error('用户身份验证失败,无法获取评查文件列表');
|
||||
}
|
||||
|
||||
// 获取文件列表(token 由 axios 拦截器自动添加)
|
||||
const filesResponse = await getReviewFiles(searchParams, null, userId);
|
||||
if (filesResponse.error) {
|
||||
throw new Error(filesResponse.error);
|
||||
}
|
||||
|
||||
setFiles(filesResponse.data?.files || []);
|
||||
setTotalCount(filesResponse.data?.total || 0);
|
||||
} catch (error) {
|
||||
console.error('获取评查文件列表失败:', error);
|
||||
toastService.error('获取评查文件列表失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [reviewType, getUserId]); // 使用 getUserId 辅助函数
|
||||
|
||||
// 在组件挂载时从 sessionStorage 获取 reviewType 并加载数据
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedReviewType = sessionStorage.getItem('reviewType');
|
||||
|
||||
// 根据 reviewType 过滤文档类型选项
|
||||
if (storedReviewType) {
|
||||
setReviewType(storedReviewType);
|
||||
|
||||
if (storedReviewType === 'contract') {
|
||||
// 只保留 id=1 的选项
|
||||
const filteredTypes = allDocumentTypes.filter((type: {id: number}) => type.id === 1);
|
||||
setDocumentTypes(filteredTypes);
|
||||
} else if (storedReviewType === 'record') {
|
||||
// 只保留 id=2 和 id=3 的选项
|
||||
const filteredTypes = allDocumentTypes.filter((type: {id: number}) => type.id === 2 || type.id === 3 || type.id === 155);
|
||||
setDocumentTypes(filteredTypes);
|
||||
}
|
||||
|
||||
// 直接使用 storedReviewType 构建搜索参数
|
||||
const currentParams = Object.fromEntries(searchParams.entries());
|
||||
const apiSearchParams: DocumentSearchParams = {
|
||||
fileType: currentParams.fileType || undefined,
|
||||
reviewStatus: currentParams.reviewStatus || undefined,
|
||||
dateFrom: currentParams.dateFrom || undefined,
|
||||
dateTo: currentParams.dateTo || undefined,
|
||||
keyword: currentParams.keyword || undefined,
|
||||
sortOrder: currentParams.sortOrder || 'upload_time_desc',
|
||||
page: parseInt(currentParams.page || "1", 10),
|
||||
pageSize: parseInt(currentParams.pageSize || "10", 10)
|
||||
};
|
||||
|
||||
// 根据 storedReviewType 添加类型过滤
|
||||
if (storedReviewType === 'contract') {
|
||||
apiSearchParams.fileType = '1';
|
||||
} else if (storedReviewType === 'record') {
|
||||
apiSearchParams.fileType = 'record';
|
||||
}
|
||||
|
||||
// 如果用户手动选择了文件类型,优先使用用户选择的
|
||||
if (currentParams.fileType) {
|
||||
apiSearchParams.fileType = currentParams.fileType;
|
||||
}
|
||||
|
||||
// 设置加载状态
|
||||
setIsLoading(true);
|
||||
|
||||
// 从 localStorage 获取用户ID
|
||||
const userId = getUserId();
|
||||
if (!userId) {
|
||||
toastService.error('用户身份验证失败,无法获取评查文件列表');
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取文件列表(token 由 axios 拦截器自动添加)
|
||||
getReviewFiles(apiSearchParams, null, userId)
|
||||
.then(filesResponse => {
|
||||
if (filesResponse.error) {
|
||||
throw new Error(filesResponse.error);
|
||||
}
|
||||
|
||||
setFiles(filesResponse.data?.files || []);
|
||||
setTotalCount(filesResponse.data?.total || 0);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('获取评查文件列表失败:', error);
|
||||
toastService.error('获取评查文件列表失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取 sessionStorage 中的 reviewType 失败:', error);
|
||||
}
|
||||
}, [allDocumentTypes, searchParams]);
|
||||
|
||||
// 监听 URL 参数变化,重新获取数据
|
||||
useEffect(() => {
|
||||
if (reviewType) {
|
||||
fetchData(Object.fromEntries(searchParams.entries()));
|
||||
}
|
||||
}, [searchParams, fetchData, reviewType]);
|
||||
|
||||
// 处理筛选条件变更
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
|
||||
if (value) {
|
||||
newParams.set(name, value);
|
||||
} else {
|
||||
newParams.delete(name);
|
||||
}
|
||||
|
||||
// 切换筛选条件时,重置到第一页
|
||||
newParams.set('page', '1');
|
||||
|
||||
persistSearchParams(newParams);
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理搜索操作
|
||||
const handleSearch = (keyword: string) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if (keyword) {
|
||||
newParams.set('keyword', keyword);
|
||||
} else {
|
||||
newParams.delete('keyword');
|
||||
}
|
||||
|
||||
// 搜索时,重置到第一页
|
||||
newParams.set('page', '1');
|
||||
|
||||
persistSearchParams(newParams);
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理页码变更
|
||||
const handlePageChange = (page: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('page', page.toString());
|
||||
persistSearchParams(newParams);
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理每页条数变更
|
||||
const handlePageSizeChange = (size: number) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
newParams.set('pageSize', size.toString());
|
||||
newParams.set('page', '1'); // 改变每页条数时重置为第一页
|
||||
persistSearchParams(newParams);
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 查看评查文件
|
||||
const handleReviewFileClick = async (fileId: string, auditStatus: number | null) => {
|
||||
// 检查audit_status是否为0,如果是则更新为2
|
||||
if (auditStatus === 0 || auditStatus === null) {
|
||||
try {
|
||||
// 从 localStorage 获取用户ID
|
||||
const userId = getUserId();
|
||||
if (!userId) {
|
||||
toastService.error('用户身份验证失败');
|
||||
return;
|
||||
}
|
||||
|
||||
// token 由 axios 拦截器自动添加
|
||||
const response = await updateDocumentAuditStatus(fileId, 2, userId);
|
||||
if (response.error) {
|
||||
throw new Error(response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新文件审核状态时出错:', error);
|
||||
toastService.error(`更新文件审核状态时出错:${error instanceof Error ? error.message : '未知错误'}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 导航到评查详情页
|
||||
// 在离开当前页前保存当前查询参数,返回时可恢复
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, searchParams.toString());
|
||||
}
|
||||
navigate(`/reviews?id=${fileId}&previousRoute=rulesFiles`);
|
||||
};
|
||||
|
||||
// 渲染问题摘要
|
||||
const renderIssues = (file: ReviewFileUI) => {
|
||||
// 如果文件状态为完成
|
||||
if (file.status === 'Processed') {
|
||||
// 如果没有问题,显示"所有评查点均通过"
|
||||
if (file.warningCount <= 0 && file.failCount <= 0) {
|
||||
return (
|
||||
<div className="text-sm text-success">
|
||||
<i className="ri-check-double-line mr-1"></i>所有评查点均通过
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果评查状态为不通过,显示"统计分数为:{file.score || 0}。分数低于80分。"
|
||||
// if (file.reviewStatus === 'fail') {
|
||||
// return (
|
||||
// <div className="text-sm text-error">
|
||||
// <i className="ri-error-warning-line mr-1"></i>统计分数为:{file.score || 0}。分数低于80分。
|
||||
// </div>
|
||||
// );
|
||||
// }
|
||||
|
||||
// 显示问题列表
|
||||
if (file.issues && file.issues.length > 0) {
|
||||
// 最多显示2个问题
|
||||
const displayIssues = file.issues.slice(0, 2);
|
||||
|
||||
return (
|
||||
<div className="text-sm">
|
||||
{displayIssues.map((issue, index) => (
|
||||
<div key={index} className="mb-1">
|
||||
<i className="ri-circle-fill mr-1 text-warning"></i>
|
||||
{issue.message}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{file.issues.length > 2 && (
|
||||
<div className="text-secondary mt-1">
|
||||
还有 {file.issues.length - 2} 个问题...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
// 其他状态显示占位符
|
||||
return <div className="text-sm text-secondary">-</div>;
|
||||
};
|
||||
|
||||
|
||||
// 下载文件
|
||||
const handleDownload = async (path: string) => {
|
||||
try {
|
||||
// 使用axios封装的下载方法
|
||||
const blob = await downloadFile(path);
|
||||
|
||||
// 创建Blob URL
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
// 创建一个隐藏的a标签并点击它
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = blobUrl;
|
||||
// 从路径中获取文件名
|
||||
const fileName = path.split('/').pop() || 'document';
|
||||
a.download = decodeURIComponent(fileName);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// 清理
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(blobUrl);
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('下载文件失败:', error);
|
||||
toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理时间范围变更
|
||||
const handleDateChange = (field: 'dateFrom' | 'dateTo', value: string) => {
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
if(value) {
|
||||
newParams.set(field, value);
|
||||
} else {
|
||||
newParams.delete(field);
|
||||
}
|
||||
newParams.set('page', '1');
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
const newParams = new URLSearchParams();
|
||||
const searchInput = document.querySelector('input[name="keyword"]');
|
||||
if(searchInput) {
|
||||
(searchInput as HTMLInputElement).value = '';
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.removeItem(SEARCH_PARAMS_STORAGE_KEY);
|
||||
}
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 辅助:格式化文件大小
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// 选择附件文件
|
||||
const handleAttachmentFilesSelected = (files: FileList) => {
|
||||
try {
|
||||
if (files.length > 0) {
|
||||
const validFiles: File[] = [];
|
||||
let hasInvalidFiles = false;
|
||||
Array.from(files).forEach(file => {
|
||||
const fileName = file.name.toLowerCase();
|
||||
const isValidType =
|
||||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
|
||||
file.type === 'application/zip' || fileName.endsWith('.zip') ||
|
||||
file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar');
|
||||
if (isValidType) {
|
||||
validFiles.push(file);
|
||||
} else {
|
||||
hasInvalidFiles = true;
|
||||
}
|
||||
});
|
||||
if (hasInvalidFiles) {
|
||||
messageService.error('只支持PDF、Word、ZIP、RAR格式的文件', {
|
||||
title: '文件类型错误',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
});
|
||||
}
|
||||
if (validFiles.length > 0) {
|
||||
setAttachmentFiles(validFiles);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('【附件追加】处理文件选择时发生错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 执行附件追加
|
||||
const handleAttachmentUpload = async () => {
|
||||
if (!selectedDocumentId || attachmentFiles.length === 0) {
|
||||
toastService.error('请选择文档和附件文件');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setAttachmentUploading(true);
|
||||
const docId = parseInt(selectedDocumentId, 10);
|
||||
const result = await appendContractAttachments(
|
||||
docId,
|
||||
attachmentFiles,
|
||||
attachmentMergeMode,
|
||||
true,
|
||||
attachmentRemark || undefined
|
||||
);
|
||||
if (result.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
toastService.success('附件追加成功!');
|
||||
// 重置并关闭
|
||||
setAttachmentFiles([]);
|
||||
setAttachmentRemark("");
|
||||
setShowAttachmentUpload(false);
|
||||
setSelectedDocumentId(null);
|
||||
// 刷新列表
|
||||
fetchData(Object.fromEntries(searchParams.entries()));
|
||||
} catch (error) {
|
||||
console.error('【附件追加】上传失败:', error);
|
||||
toastService.error(error instanceof Error ? error.message : '附件追加失败');
|
||||
} finally {
|
||||
setAttachmentUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 文件类型选项
|
||||
const fileTypeOptions = documentTypes.map((type: {id: number, name: string}) => ({
|
||||
value: type.id.toString(),
|
||||
label: type.name
|
||||
}));
|
||||
|
||||
// 定义表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: "文件名称",
|
||||
key: "fileName",
|
||||
width: "30%",
|
||||
render: (_: unknown, file: ReviewFileUI) => (
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center self-center">
|
||||
<FileIcon fileName={file.fileName} className="text-lg w-10 h-10" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 flex flex-col py-2 ml-2">
|
||||
<div className="font-normal text-base break-words whitespace-normal leading-normal" title={file.fileName}>{file.fileName}</div>
|
||||
<div className="text-xs text-secondary mt-2">
|
||||
文件编号:{file.fileCode}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "文件类型",
|
||||
key: "fileType",
|
||||
width: "12%",
|
||||
render: (_: unknown, file: ReviewFileUI) => (
|
||||
<FileTypeTag
|
||||
type="other"
|
||||
typeName={file.fileType}
|
||||
text={file.fileType}
|
||||
size="sm"
|
||||
showIcon={false}
|
||||
colorMode="light"
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
key: "uploadTime",
|
||||
width: "12%",
|
||||
render: (_: unknown, file: ReviewFileUI) => {
|
||||
const [date, time] = file.uploadTime.split(' ');
|
||||
return (
|
||||
<div>
|
||||
<span className="text-base">{date}</span>
|
||||
<br />
|
||||
<span className="text-xs text-secondary">{time}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "评查统计",
|
||||
key: "reviewStatus",
|
||||
width: "12%",
|
||||
render: (_: unknown, file: ReviewFileUI) =>
|
||||
// 要文件切分处理完之后,再显示评查统计
|
||||
file.status === 'Processed' ? (
|
||||
<div>
|
||||
{file.passCount > 0 && (
|
||||
<StatusBadge
|
||||
status="pass"
|
||||
text={`通过(${file.passCount})`}
|
||||
showIcon={true}
|
||||
className="my-2"
|
||||
/>
|
||||
)}
|
||||
{file.warningCount > 0 && (
|
||||
<StatusBadge
|
||||
status="warning"
|
||||
text={`警告(${file.warningCount})`}
|
||||
showIcon={true}
|
||||
className="my-2"
|
||||
/>
|
||||
)}
|
||||
{file.failCount > 0 && (
|
||||
<StatusBadge
|
||||
status="fail"
|
||||
text={`不通过(${file.failCount})`}
|
||||
showIcon={true}
|
||||
className="my-2"
|
||||
/>
|
||||
)}
|
||||
{file.manualCount > 0 && (
|
||||
<StatusBadge
|
||||
status="pending"
|
||||
text={`需人工(${file.manualCount})`}
|
||||
showIcon={true}
|
||||
className="my-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
) : (
|
||||
<div className="text-sm">
|
||||
-
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "问题摘要",
|
||||
key: "issues",
|
||||
width: "20%",
|
||||
render: (_: unknown, file: ReviewFileUI) => renderIssues(file)
|
||||
},
|
||||
{
|
||||
title: "操作",
|
||||
key: "operation",
|
||||
width: "20%",
|
||||
render: (_: unknown, file: ReviewFileUI) => (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReviewFileClick(file.id, file.auditStatus)}
|
||||
disabled={file.status !== 'Processed'}
|
||||
className={`text-xs px-2 py-1 h-7 mr-1 ${file.status === 'Processed' ? 'hover:underline hover:text-primary' : 'opacity-60 cursor-not-allowed pointer-events-none'}`}
|
||||
>
|
||||
<i className="ri-eye-line"></i>
|
||||
查看
|
||||
</button>
|
||||
{file.fileTypeId === 1 && file.status === 'Processed' && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs px-2 py-1 h-7 mr-1 hover:underline hover:text-primary"
|
||||
onClick={() => {
|
||||
setSelectedDocumentId(file.id);
|
||||
setShowAttachmentUpload(true);
|
||||
}}
|
||||
>
|
||||
<i className="ri-attachment-line"></i>
|
||||
追加附件
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||||
onClick={() => handleDownload(file.path)}
|
||||
>
|
||||
<i className="ri-download-2-line"></i>
|
||||
下载
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="review-files-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-xl font-normal">评查文件列表</h2>
|
||||
<div className="flex items-center ml-4 bg-white px-3 py-1 rounded-md">
|
||||
<i className="ri-file-list-3-line text-primary text-lg mr-1"></i>
|
||||
<span className="text-sm text-secondary">总文件数:</span>
|
||||
{isLoading ? (
|
||||
<NumberSkeleton className="ml-1" />
|
||||
) : (
|
||||
<span className="text-base font-normal text-primary ml-1">{totalCount}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="primary" icon="ri-file-upload-line" to="/files/upload">
|
||||
上传新文件
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 筛选区域 */}
|
||||
<FilterPanel className="px-3 py-3" noActionDivider={true}
|
||||
actions={
|
||||
<>
|
||||
<Button type="default" icon="ri-refresh-line" onClick={handleReset} className="mr-2 hover:!border-gray-300">
|
||||
重置
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FilterSelect
|
||||
label="文件类型"
|
||||
name="fileType"
|
||||
value={searchParams.get('fileType') || ''}
|
||||
options={fileTypeOptions}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-2 w-40"
|
||||
/>
|
||||
|
||||
{/* <FilterSelect
|
||||
label="评查状态"
|
||||
name="reviewStatus"
|
||||
value={searchParams.get('reviewStatus') || ''}
|
||||
options={reviewStatusOptions}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-2 w-40"
|
||||
/> */}
|
||||
|
||||
{/* <FilterSelect
|
||||
label="时间范围"
|
||||
name="dateRange"
|
||||
value={searchParams.get('dateRange') || ''}
|
||||
options={dateRangeOptions}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-2 w-40"
|
||||
/> */}
|
||||
|
||||
<DateRangeFilter
|
||||
label="时间范围"
|
||||
startDate={dateFrom}
|
||||
endDate={dateTo}
|
||||
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
|
||||
onEndDateChange={(value) => handleDateChange('dateTo', value)}
|
||||
simple={true}
|
||||
colorMode="light"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="排序方式"
|
||||
name="sortOrder"
|
||||
value={searchParams.get('sortOrder') || 'upload_time_desc'}
|
||||
onChange={handleFilterChange}
|
||||
className="w-32"
|
||||
options={[
|
||||
{ value: "upload_time_desc", label: "上传时间 ↓" },
|
||||
{ value: "upload_time_asc", label: "上传时间 ↑" },
|
||||
// { value: "issue_count_desc", label: "问题数量 ↓" },
|
||||
// { value: "issue_count_asc", label: "问题数量 ↑" }
|
||||
]}
|
||||
/>
|
||||
<SearchFilter
|
||||
label="搜索"
|
||||
placeholder="搜索文件名、合同编号"
|
||||
value={searchParams.get('keyword') || ''}
|
||||
onSearch={handleSearch}
|
||||
buttonText=""
|
||||
className="mr-2 flex-1"
|
||||
/>
|
||||
</FilterPanel>
|
||||
|
||||
{/* 文件列表 */}
|
||||
<Card>
|
||||
<div className={isLoading ? "opacity-70 pointer-events-none transition-opacity" : ""}>
|
||||
{isLoading && <LoadingIndicator />}
|
||||
|
||||
{isLoading && files.length === 0 ? (
|
||||
<TableRowSkeleton count={5} />
|
||||
) : (
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={files}
|
||||
rowKey="id"
|
||||
emptyText={isLoading ? "加载中..." : "暂无文件数据"}
|
||||
className="files-table table-auto-height"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 分页组件 */}
|
||||
{totalCount > 0 && (
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
total={totalCount}
|
||||
pageSize={pageSize}
|
||||
onChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
showTotal={true}
|
||||
showPageSizeChanger={true}
|
||||
pageSizeOptions={[10, 20, 30, 50]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* 附件追加模态框 */}
|
||||
{showAttachmentUpload && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold">追加合同附件</h3>
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowAttachmentUpload(false);
|
||||
setSelectedDocumentId(null);
|
||||
setAttachmentFiles([]);
|
||||
setAttachmentRemark("");
|
||||
}}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<i className="ri-close-line text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-gray-50 p-3 rounded">
|
||||
<p className="text-sm text-gray-600">
|
||||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
支持PDF、Word、ZIP、RAR格式,ZIP/RAR内仅合并其中的PDF文件
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
选择附件文件 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors">
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
accept=".pdf,.doc,.docx,.zip,.rar"
|
||||
onChange={(e) => e.target.files && handleAttachmentFilesSelected(e.target.files)}
|
||||
className="hidden"
|
||||
id="attachment-file-input"
|
||||
/>
|
||||
<label htmlFor="attachment-file-input" className="cursor-pointer">
|
||||
<i className="ri-attachment-line text-3xl text-gray-400 mb-2 block"></i>
|
||||
<p className="text-sm text-gray-600">点击选择文件或拖拽文件到此处</p>
|
||||
<p className="text-xs text-gray-500 mt-1">支持PDF、Word、ZIP、RAR格式,可多选</p>
|
||||
</label>
|
||||
</div>
|
||||
{attachmentFiles.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="text-sm text-green-600 mb-2">
|
||||
<i className="ri-checkbox-circle-line"></i> 已选择 {attachmentFiles.length} 个文件
|
||||
</p>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{attachmentFiles.map((file, index) => (
|
||||
<div key={index} className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
||||
<i className="ri-file-line mr-1"></i>
|
||||
{file.name} ({formatFileSize(file.size)})
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
合并模式
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="mergeMode"
|
||||
value="overwrite"
|
||||
checked={attachmentMergeMode === 'overwrite'}
|
||||
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm">覆盖原文档(推荐)</span>
|
||||
</label>
|
||||
<label className="flex items-center">
|
||||
<input
|
||||
type="radio"
|
||||
name="mergeMode"
|
||||
value="new"
|
||||
checked={attachmentMergeMode === 'new'}
|
||||
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
|
||||
className="mr-2"
|
||||
/>
|
||||
<span className="text-sm">新建文档记录</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
备注(可选)
|
||||
</label>
|
||||
<textarea
|
||||
value={attachmentRemark}
|
||||
onChange={(e) => setAttachmentRemark(e.target.value)}
|
||||
className="w-full p-2 border border-gray-300 rounded-md text-sm"
|
||||
rows={3}
|
||||
placeholder="请输入备注信息..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||
<button
|
||||
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50"
|
||||
onClick={() => {
|
||||
setShowAttachmentUpload(false);
|
||||
setSelectedDocumentId(null);
|
||||
setAttachmentFiles([]);
|
||||
setAttachmentRemark("");
|
||||
}}
|
||||
disabled={attachmentUploading}
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary-dark disabled:opacity-50"
|
||||
onClick={handleAttachmentUpload}
|
||||
disabled={attachmentFiles.length === 0 || attachmentUploading}
|
||||
>
|
||||
{attachmentUploading ? '上传中...' : '开始追加'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 错误边界
|
||||
export function ErrorBoundary() {
|
||||
return (
|
||||
<div className="error-container p-6">
|
||||
<h1 className="text-xl font-normal text-red-500 mb-4">出错了</h1>
|
||||
<p className="mb-4">加载评查文件列表时发生错误。请稍后再试,或联系管理员。</p>
|
||||
<Button type="primary" to="/">返回首页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+113
-79
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
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 { Button } from '~/components/ui/Button';
|
||||
@@ -38,6 +38,10 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "评查点列表"
|
||||
};
|
||||
|
||||
// 声明loader返回的数据类型
|
||||
export type LoaderData = {
|
||||
rules: Rule[];
|
||||
@@ -70,9 +74,17 @@ interface ActionResponse {
|
||||
}
|
||||
|
||||
function mapApiRuleToModel(apiRule: ApiRule): Rule {
|
||||
// 🔑 清洗评查点编码:移除最后一个 '--' 及其后面的字符
|
||||
// 例如:'code-mis--mz' --> 'code-mis', 'code-mbs--alsi--gz' --> 'code-mbs--alsi'
|
||||
let cleanedCode = apiRule.code;
|
||||
const lastDoubleHyphenIndex = cleanedCode.lastIndexOf('--');
|
||||
if (lastDoubleHyphenIndex !== -1) {
|
||||
cleanedCode = cleanedCode.substring(0, lastDoubleHyphenIndex);
|
||||
}
|
||||
|
||||
return {
|
||||
id: apiRule.id,
|
||||
code: apiRule.code,
|
||||
code: cleanedCode,
|
||||
name: apiRule.name,
|
||||
ruleType: apiRule.ruleType as RuleType, // 类型转换
|
||||
ruleGroupId: apiRule.groupId,
|
||||
@@ -105,22 +117,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 获取评查点类型列表,供前端筛选使用
|
||||
const typeResponse = await getRuleTypes(undefined, frontendJWT);
|
||||
|
||||
if (typeResponse.error) {
|
||||
console.error('获取评查点类型失败:', typeResponse.error);
|
||||
}
|
||||
|
||||
const ruleTypes = typeResponse.error ? [] : typeResponse.data;
|
||||
|
||||
// 返回初始空数据,客户端将根据 sessionStorage 中的 reviewType 加载实际数据
|
||||
// 返回初始空数据,客户端将根据 sessionStorage 中的 documentTypeIds 加载实际数据
|
||||
return Response.json({
|
||||
rules: [],
|
||||
totalCount: 0,
|
||||
currentPage: params.page,
|
||||
pageSize: params.pageSize,
|
||||
ruleTypes,
|
||||
ruleTypes: [], // 服务端无法访问 sessionStorage,客户端加载
|
||||
initialLoad: true,
|
||||
frontendJWT
|
||||
}, {
|
||||
@@ -198,16 +201,16 @@ export default function RulesIndex() {
|
||||
// 状态管理
|
||||
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
|
||||
const [loadingGroups, setLoadingGroups] = useState(false);
|
||||
const [reviewType, setReviewType] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filteredRules, setFilteredRules] = useState<Rule[]>(initialRules);
|
||||
const [filteredTotalCount, setFilteredTotalCount] = useState<number>(initialTotalCount);
|
||||
const [ruleTypes, setRuleTypes] = useState<ApiRuleType[]>(initialRuleTypes);
|
||||
|
||||
// 添加一个路由变化计数器
|
||||
const [routeChangeCount, setRouteChangeCount] = useState(0);
|
||||
|
||||
// 添加一个状态来跟踪是否执行了删除操作
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
// 使用 ref 跟踪是否正在加载数据,避免重复加载
|
||||
const isLoadingRef = useRef(false);
|
||||
|
||||
// 查询参数记忆 key 与保存/恢复
|
||||
const SEARCH_PARAMS_STORAGE_KEY = 'rules.searchParams';
|
||||
@@ -231,19 +234,13 @@ export default function RulesIndex() {
|
||||
|
||||
// 获取当前的ruleType值
|
||||
const ruleTypeParam = searchParams.get('ruleType');
|
||||
|
||||
// 追踪路由变化
|
||||
useEffect(() => {
|
||||
// console.log("路由变化:", location.key);
|
||||
setRouteChangeCount(prev => prev + 1);
|
||||
}, [location]);
|
||||
|
||||
|
||||
// 判断是否禁用规则组选择
|
||||
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
|
||||
|
||||
// 检查用户是否为开发者角色
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const isDeveloper = userRole === 'admin';
|
||||
const isDeveloper = userRole.includes('admin');
|
||||
|
||||
// 调试日志
|
||||
// console.log("🔑 [Rules List] rootData:", rootData);
|
||||
@@ -261,56 +258,93 @@ export default function RulesIndex() {
|
||||
useEffect(() => {
|
||||
if(loaderData.error) {
|
||||
toastService.error(loaderData.error);
|
||||
}else if(loaderData.ruleTypes.length === 0){
|
||||
toastService.error("评查点类型数据为空");
|
||||
}
|
||||
}, [loaderData.error,loaderData.ruleTypes]);
|
||||
// ❌ 不再检查 loaderData.ruleTypes,因为服务端永远返回空数组
|
||||
// 如果需要检查评查点类型数据,应该在 fetchData 完成后检查状态 ruleTypes
|
||||
}, [loaderData.error]);
|
||||
|
||||
// 客户端数据加载函数
|
||||
const fetchData = useCallback(async () => {
|
||||
try {
|
||||
// 从sessionStorage获取reviewType
|
||||
const storedReviewType = typeof window !== 'undefined' ? sessionStorage.getItem('reviewType') : null;
|
||||
const typeToUse = reviewType || storedReviewType;
|
||||
|
||||
if (!typeToUse) {
|
||||
console.warn('无法加载评查点数据:未找到reviewType');
|
||||
// 🔑 如果正在加载,避免重复调用
|
||||
if (isLoadingRef.current) {
|
||||
console.log('📋 [fetchData] 正在加载中,跳过重复调用');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log("fetchData被调用,加载评查点数据", typeToUse);
|
||||
|
||||
// 🔑 从 sessionStorage 获取 documentTypeIds
|
||||
const typeIdsStr = typeof window !== 'undefined' ? sessionStorage.getItem('documentTypeIds') : null;
|
||||
const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
|
||||
if (!documentTypeIds || documentTypeIds.length === 0) {
|
||||
console.warn('无法加载评查点数据:未找到 documentTypeIds');
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingRef.current = true;
|
||||
|
||||
// 🔑 从 localStorage 获取 user_info 中的 area
|
||||
let userArea: string | undefined = undefined;
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const userInfoStr = localStorage.getItem('user_info');
|
||||
if (userInfoStr) {
|
||||
const userInfo = JSON.parse(userInfoStr);
|
||||
userArea = userInfo.area;
|
||||
console.log("📋 [fetchData] 从 localStorage 获取到用户地区:", userArea);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析 user_info 失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("📋 [fetchData] 开始加载评查点数据, documentTypeIds:", documentTypeIds, "area:", userArea);
|
||||
setLoading(true);
|
||||
|
||||
// 获取评查点类型
|
||||
|
||||
// 🔑 获取评查点类型(通过 documentTypeIds)
|
||||
let loadedRuleTypes: ApiRuleType[] = [];
|
||||
try {
|
||||
const typeResponse = await getRuleTypes(typeToUse, loaderData.frontendJWT);
|
||||
const typeResponse = await getRuleTypes(documentTypeIds, loaderData.frontendJWT);
|
||||
if (typeResponse.data) {
|
||||
setRuleTypes(typeResponse.data);
|
||||
loadedRuleTypes = typeResponse.data;
|
||||
setRuleTypes(loadedRuleTypes);
|
||||
console.log("📋 [fetchData] 获取到评查点类型:", loadedRuleTypes);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载评查点类型失败:', error);
|
||||
}
|
||||
|
||||
|
||||
// 构建查询参数
|
||||
// 🔑 当选择"全部"或未选择评查点类型时,使用下拉框中所有评查点类型的 id 组合
|
||||
let finalRuleType: string | undefined = undefined;
|
||||
if (ruleTypeParam && ruleTypeParam !== 'all') {
|
||||
// 选择了具体的评查点类型
|
||||
finalRuleType = ruleTypeParam;
|
||||
} else if (loadedRuleTypes && loadedRuleTypes.length > 0) {
|
||||
// 选择"全部"或未选择,使用刚加载的评查点类型的 id
|
||||
finalRuleType = loadedRuleTypes.map(type => type.id).join(',');
|
||||
console.log("📋 [fetchData] 选择全部类型,使用 loadedRuleTypes 的 id 组合:", finalRuleType);
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
ruleType: ruleTypeParam || undefined,
|
||||
ruleType: finalRuleType,
|
||||
groupId: searchParams.get('groupId') || undefined,
|
||||
isActive: searchParams.get('isActive') ? searchParams.get('isActive') === 'true' : undefined,
|
||||
keyword: searchParams.get('keyword') || undefined,
|
||||
area: userArea, // 添加地区过滤
|
||||
page: currentPage,
|
||||
pageSize,
|
||||
reviewType: typeToUse,
|
||||
token: loaderData.frontendJWT
|
||||
};
|
||||
|
||||
|
||||
// 调用 API 获取数据
|
||||
const response = await getRulesList(queryParams);
|
||||
|
||||
|
||||
if (response.data) {
|
||||
const apiRules = response.data.rules || [];
|
||||
const total = response.data.totalCount || 0;
|
||||
const mappedRules = apiRules.map((apiRule: ApiRule) => mapApiRuleToModel(apiRule));
|
||||
|
||||
|
||||
setFilteredRules(mappedRules);
|
||||
setFilteredTotalCount(total);
|
||||
}
|
||||
@@ -319,8 +353,9 @@ export default function RulesIndex() {
|
||||
toastService.error('加载评查点列表失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
isLoadingRef.current = false;
|
||||
}
|
||||
}, [reviewType, ruleTypeParam, searchParams, currentPage, pageSize]);
|
||||
}, [ruleTypeParam, searchParams, currentPage, pageSize, loaderData.frontendJWT]);
|
||||
|
||||
// 当评查点类型变化时,加载对应的规则组
|
||||
useEffect(() => {
|
||||
@@ -380,55 +415,54 @@ export default function RulesIndex() {
|
||||
}
|
||||
}, [fetcher.data, fetcher.state, fetchData, isDeleting]);
|
||||
|
||||
// 在组件挂载时从 sessionStorage 获取 reviewType 并加载数据
|
||||
// 在组件挂载时从 sessionStorage 获取 documentTypeIds 并加载数据
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedReviewType = sessionStorage.getItem('reviewType');
|
||||
// console.log("组件挂载,从sessionStorage获取reviewType:", storedReviewType);
|
||||
|
||||
if (storedReviewType !== reviewType) {
|
||||
setReviewType(storedReviewType);
|
||||
}
|
||||
|
||||
// 无论如何,都加载数据,不依赖于reviewType的变化
|
||||
if (storedReviewType) {
|
||||
// 使用setTimeout确保该操作在其他状态更新之后执行
|
||||
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
|
||||
const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
console.log("📋 组件挂载,从 sessionStorage 获取 documentTypeIds:", documentTypeIds);
|
||||
|
||||
// 如果有 documentTypeIds,加载数据
|
||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||
// 使用 setTimeout 确保该操作在其他状态更新之后执行
|
||||
setTimeout(() => {
|
||||
fetchData();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取 sessionStorage 中的 reviewType 失败:', error);
|
||||
console.error('获取 sessionStorage 中的 documentTypeIds 失败:', error);
|
||||
}
|
||||
}, [initialLoad, fetchData]);
|
||||
|
||||
// 监听路由变化,每次路由到此页面时刷新数据
|
||||
useEffect(() => {
|
||||
if (routeChangeCount > 0) {
|
||||
// console.log("路由变化触发数据刷新,计数:", routeChangeCount);
|
||||
const storedReviewType = typeof window !== 'undefined' ? sessionStorage.getItem('reviewType') : null;
|
||||
// console.log("storedReviewType:", storedReviewType);
|
||||
if (storedReviewType) {
|
||||
if (storedReviewType !== reviewType) {
|
||||
setReviewType(storedReviewType);
|
||||
}
|
||||
|
||||
// 使用setTimeout确保该操作在其他状态更新之后执行
|
||||
setTimeout(() => {
|
||||
fetchData();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}, [routeChangeCount, fetchData]);
|
||||
// 注释掉重复的路由监听逻辑,避免与searchParams监听重复触发
|
||||
// useEffect(() => {
|
||||
// if (routeChangeCount > 0) {
|
||||
// console.log("📋 路由变化触发数据刷新,计数:", routeChangeCount);
|
||||
// const typeIdsStr = typeof window !== 'undefined' ? sessionStorage.getItem('documentTypeIds') : null;
|
||||
// const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
// console.log("📋 documentTypeIds:", documentTypeIds);
|
||||
|
||||
// if (documentTypeIds && documentTypeIds.length > 0) {
|
||||
// // 使用 setTimeout 确保该操作在其他状态更新之后执行
|
||||
// setTimeout(() => {
|
||||
// fetchData();
|
||||
// }, 0);
|
||||
// }
|
||||
// }
|
||||
// }, [routeChangeCount, fetchData]);
|
||||
|
||||
// 监听 URL 参数变化,重新获取数据
|
||||
useEffect(() => {
|
||||
if (reviewType) {
|
||||
// 检查是否有 documentTypeIds
|
||||
const typeIdsStr = typeof window !== 'undefined' ? sessionStorage.getItem('documentTypeIds') : null;
|
||||
const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
|
||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||
fetchData();
|
||||
}
|
||||
}, [searchParams, fetchData, reviewType]);
|
||||
}, [searchParams, fetchData]);
|
||||
|
||||
// 筛选评查点
|
||||
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
|
||||
|
||||
+110
-26
@@ -26,7 +26,7 @@
|
||||
*/
|
||||
|
||||
import { type MetaFunction } from "@remix-run/node";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { BasicInfo } from "~/components/rules/new/BasicInfo";
|
||||
import { ExtractionSettings } from "~/components/rules/new/ExtractionSettings";
|
||||
import { ReviewSettings } from "~/components/rules/new/ReviewSettings";
|
||||
@@ -138,13 +138,17 @@ export default function RuleNew() {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
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';
|
||||
const frontendJWT = rootData?.frontendJWT;
|
||||
|
||||
|
||||
// 使用 ref 跟踪当前加载的 URL,避免重复加载
|
||||
const loadedUrlRef = useRef<string>('');
|
||||
|
||||
const [formData, setFormData] = useState<EvaluationPoint>({});
|
||||
const [evaluationPointGroups, setEvaluationPointGroups] = useState<EvaluationPointGroup[]>([]);
|
||||
|
||||
@@ -266,11 +270,12 @@ export default function RuleNew() {
|
||||
* 获取评查点数据
|
||||
* 编辑模式下从API获取指定ID的评查点数据
|
||||
* @param id 评查点ID
|
||||
* @param isCopy 是否为复制模式
|
||||
*/
|
||||
const fetchEvaluationPoint = useCallback(async (id: number) => {
|
||||
const fetchEvaluationPoint = useCallback(async (id: number, isCopy = false) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
// console.log(`获取评查点数据,ID: ${id}`);
|
||||
// console.log(`获取评查点数据,ID: ${id}, 复制模式: ${isCopy}`);
|
||||
// 使用 postgrestGet 替代直接调用 fetch
|
||||
const postgrestParams = {
|
||||
filter: {
|
||||
@@ -283,23 +288,43 @@ export default function RuleNew() {
|
||||
if (response.data) {
|
||||
// 使用extractApiData从响应中提取数据
|
||||
const evaluationPoints = extractApiData<EvaluationPoint[]>(response.data);
|
||||
|
||||
|
||||
if (evaluationPoints && Array.isArray(evaluationPoints) && evaluationPoints.length > 0) {
|
||||
try {
|
||||
// 使用JSON序列化和反序列化来进行深拷贝,避免浏览器差异
|
||||
const originalData = evaluationPoints[0];
|
||||
const jsonString = JSON.stringify(originalData);
|
||||
const data = JSON.parse(jsonString);
|
||||
|
||||
|
||||
// 🔄 复制模式:删除不应该复制的字段
|
||||
if (isCopy) {
|
||||
delete data.id;
|
||||
delete data.created_at;
|
||||
delete data.updated_at;
|
||||
delete data.usage_count;
|
||||
|
||||
// console.log('📋 复制模式:已清除不应复制的字段(id, created_at, updated_at, usage_count)');
|
||||
}
|
||||
|
||||
// 🔑 清洗评查点编码:移除最后一个 '--' 及其后面的字符
|
||||
// 例如:'code-mis--mz' --> 'code-mis', 'code-mbs--alsi--gz' --> 'code-mbs--alsi'
|
||||
if (data.code) {
|
||||
const lastDoubleHyphenIndex = data.code.lastIndexOf('--');
|
||||
if (lastDoubleHyphenIndex !== -1) {
|
||||
data.code = data.code.substring(0, lastDoubleHyphenIndex);
|
||||
// console.log('🔑 已清洗评查点编码:', data.code);
|
||||
}
|
||||
}
|
||||
|
||||
// 设置表单数据
|
||||
setFormData(data);
|
||||
|
||||
|
||||
// 初始化extractionFields
|
||||
const extractedFields = extractFieldsFromFormData(data);
|
||||
setExtractionFields(extractedFields);
|
||||
|
||||
// 设置编辑模式的实例键
|
||||
setInstanceKey(`edit_${id}_${Date.now()}`);
|
||||
|
||||
// 设置实例键
|
||||
setInstanceKey(isCopy ? `copy_${id}_${Date.now()}` : `edit_${id}_${Date.now()}`);
|
||||
} catch (jsonError) {
|
||||
console.error('JSON处理错误:', jsonError);
|
||||
toastService.error(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`);
|
||||
@@ -332,14 +357,30 @@ export default function RuleNew() {
|
||||
*/
|
||||
const fetchEvaluationPointGroups = useCallback(async () => {
|
||||
try {
|
||||
// console.log("获取评查点组数据");
|
||||
// console.log("🔍 [fetchEvaluationPointGroups] 开始获取评查点组数据");
|
||||
const response = await postgrestGet('evaluation_point_groups', { token: frontendJWT });
|
||||
|
||||
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
|
||||
setEvaluationPointGroups(response.data);
|
||||
// console.log("🔍 [fetchEvaluationPointGroups] API响应:", response);
|
||||
|
||||
if (response.data) {
|
||||
// 使用 extractApiData 提取数据(处理可能的包装格式)
|
||||
const extractedData = extractApiData<EvaluationPointGroup[]>(response.data);
|
||||
// console.log("🔍 [fetchEvaluationPointGroups] 提取后的数据:", extractedData);
|
||||
|
||||
if (extractedData && Array.isArray(extractedData) && extractedData.length > 0) {
|
||||
setEvaluationPointGroups(extractedData);
|
||||
// console.log(`✅ [fetchEvaluationPointGroups] 成功加载 ${extractedData.length} 个评查点组`);
|
||||
} else {
|
||||
console.warn("⚠️ [fetchEvaluationPointGroups] 提取的数据为空或格式不正确");
|
||||
setEvaluationPointGroups([]);
|
||||
}
|
||||
} else if (response.error) {
|
||||
console.error('❌ [fetchEvaluationPointGroups] API返回错误:', response.error);
|
||||
setEvaluationPointGroups([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评查点组数据失败:', error);
|
||||
console.error('❌ [fetchEvaluationPointGroups] 获取评查点组数据失败:', error);
|
||||
setEvaluationPointGroups([]);
|
||||
// 显示错误提示但不影响应用继续使用
|
||||
toastService.error(`获取评查点组数据失败: ${error instanceof Error ? error.message : '未知错误'}\n将使用默认数据`);
|
||||
}
|
||||
@@ -761,6 +802,7 @@ export default function RuleNew() {
|
||||
let response;
|
||||
if (isEditMode) {
|
||||
response = await postgrestPut('evaluation_points', finalData, {id: formData.id!}, frontendJWT);
|
||||
// console.log("最终提交的数据", finalData)
|
||||
} else {
|
||||
response = await postgrestPost('evaluation_points', finalData, frontendJWT);
|
||||
}
|
||||
@@ -909,46 +951,85 @@ export default function RuleNew() {
|
||||
* 3. 获取评查点组数据(用于表单选择项)
|
||||
*/
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const id = searchParams.get('id');
|
||||
const currentUrl = location.search;
|
||||
const currentPathname = location.pathname;
|
||||
const fullUrl = `${currentPathname}${currentUrl}`;
|
||||
|
||||
// 🔑 如果 URL 没有变化,不重复加载(避免无限循环)
|
||||
// 使用完整路径(pathname + search)进行比较,避免新增页面被拦截
|
||||
if (loadedUrlRef.current === fullUrl) {
|
||||
console.log('🔄 [useEffect] URL未变化,跳过重复加载:', fullUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('🔄 [useEffect] URL变化,开始加载数据:', { previous: loadedUrlRef.current, current: fullUrl });
|
||||
|
||||
const searchParams = new URLSearchParams(currentUrl);
|
||||
const id = searchParams.get('id');
|
||||
const mode = searchParams.get('mode');
|
||||
|
||||
|
||||
// 判断是否为复制模式
|
||||
const isCopy = mode === 'copy';
|
||||
|
||||
// 编辑或复制模式下设置加载状态
|
||||
if (id || mode === 'copy') {
|
||||
if (id || isCopy) {
|
||||
setIsLoading(true);
|
||||
}
|
||||
|
||||
// 设置编辑模式
|
||||
if (mode && mode === 'copy') {
|
||||
|
||||
// 设置复制模式状态
|
||||
setIsCopyMode(isCopy);
|
||||
|
||||
// 设置编辑模式(复制模式不是编辑模式)
|
||||
if (isCopy) {
|
||||
setIsEditMode(false);
|
||||
} else {
|
||||
setIsEditMode(!!id);
|
||||
}
|
||||
|
||||
|
||||
if (id) {
|
||||
// 编辑模式:获取数据
|
||||
fetchEvaluationPoint(parseInt(id));
|
||||
// 编辑或复制模式:获取数据(传入复制模式标志)
|
||||
console.log('📝 [useEffect] 编辑/复制模式,加载评查点数据,ID:', id);
|
||||
fetchEvaluationPoint(parseInt(id), isCopy);
|
||||
} else {
|
||||
// 新建模式:重置表单数据
|
||||
console.log('📝 [useEffect] 新建模式,重置表单数据');
|
||||
resetFormData();
|
||||
}
|
||||
|
||||
// 获取评查点组数据
|
||||
// console.log('📝 [useEffect] 获取评查点组数据');
|
||||
fetchEvaluationPointGroups();
|
||||
// 获取VLM字段类型选项
|
||||
// console.log('📝 [useEffect] 获取VLM字段类型选项');
|
||||
fetchVlmFieldTypeOptions();
|
||||
}, [location.search, fetchEvaluationPoint, fetchEvaluationPointGroups, fetchVlmFieldTypeOptions, resetFormData]);
|
||||
|
||||
// 记录已加载的 URL(使用完整路径)
|
||||
loadedUrlRef.current = fullUrl;
|
||||
}, [location.search, location.pathname, fetchEvaluationPoint, fetchEvaluationPointGroups, fetchVlmFieldTypeOptions, resetFormData]);
|
||||
|
||||
// 渲染页面内容
|
||||
return (
|
||||
<div className="container">
|
||||
{/* 页面标题和右上角保存按钮 */}
|
||||
<PageHeader
|
||||
title={isEditMode ? (isReadOnly ? "查看评查点" : "编辑评查点") : "新增评查点"}
|
||||
title={isCopyMode ? "复制评查点" : (isEditMode ? (isReadOnly ? "查看评查点" : "编辑评查点") : "新增评查点")}
|
||||
onSave={handleSave}
|
||||
showSaveButton={!isReadOnly}
|
||||
/>
|
||||
|
||||
{/* 复制模式提示 */}
|
||||
{isCopyMode && !isLoading && (
|
||||
<div className="mb-4 p-1 bg-blue-50 border border-blue-200 rounded-md">
|
||||
<div className="flex items-center">
|
||||
<i className="ri-information-line text-blue-500 text-xl mr-2"></i>
|
||||
<div className="text-sm text-blue-800">
|
||||
<span className="font-medium">复制模式:</span>
|
||||
请检查并修改评查点信息后保存。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 加载状态显示 */}
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center p-12">
|
||||
@@ -965,6 +1046,7 @@ export default function RuleNew() {
|
||||
{/* 评查点基本信息设置 */}
|
||||
<div className="mb-8">
|
||||
<BasicInfo
|
||||
key={instanceKey}
|
||||
onChange={handleBasicInfoChange}
|
||||
initialData={formData}
|
||||
evaluationPointGroups={evaluationPointGroups}
|
||||
@@ -975,6 +1057,7 @@ export default function RuleNew() {
|
||||
{/* 抽取设置 - 配置从文档中提取的字段 */}
|
||||
<div className="mb-8">
|
||||
<ExtractionSettings
|
||||
key={instanceKey}
|
||||
onChange={handleExtractionSettingsChange}
|
||||
initialData={formData}
|
||||
promptTypeOptions={EVALUATION_OPTIONS.llmPromptTypeOptions}
|
||||
@@ -985,6 +1068,7 @@ export default function RuleNew() {
|
||||
{/* 评查设置 - 配置评查规则、消息等 */}
|
||||
<div className="mb-8">
|
||||
<ReviewSettings
|
||||
key={instanceKey}
|
||||
onChange={handleReviewSettingsChange}
|
||||
initialData={{
|
||||
rules: formData.evaluation_config?.rules || [],
|
||||
|
||||
Reference in New Issue
Block a user