feat: 1. 完善交叉评查上传创建任务,改成动态加载文档类型。

2. 重新对齐交叉评查的接口。
This commit is contained in:
2025-12-02 10:10:03 +08:00
parent c9e0d5abba
commit 88466b7a8b
21 changed files with 561 additions and 174 deletions
+5 -2
View File
@@ -210,6 +210,7 @@ export default function Index() {
} else if (module.name === '智慧法务大模型') {
// 智慧法务大模型 → 跳转到 AI 对话
targetPath = '/chat-with-llm/chat';
sessionStorage.setItem('selectedModulePicPath', '/images/icon_assistant.png')
// console.log('📌 [Index] 智慧法务大模型,跳转到:', targetPath);
} else {
// console.log('📌 [Index] 其他模块,跳转到:', targetPath);
@@ -291,10 +292,12 @@ export default function Index() {
if (typeof window !== 'undefined') {
// 🔑 设置标志:表示用户通过交叉评查入口进入
sessionStorage.setItem('crossCheckingMode', 'true');
sessionStorage.setItem('selectedModuleName', '交叉评查')
sessionStorage.setItem('selectedModulePicPath', '/images/icon_cross@2x.png')
// 清除模块相关的标志(因为不是从入口模块进入)
sessionStorage.removeItem('selectedModuleId');
sessionStorage.removeItem('selectedModuleName');
sessionStorage.removeItem('selectedModulePicPath');
// sessionStorage.removeItem('selectedModuleName');
// sessionStorage.removeItem('selectedModulePicPath');
// 清除系统设置模式标志
sessionStorage.removeItem('settingsMode');
}
+37 -6
View File
@@ -206,9 +206,9 @@ const statusConfig = {
};
// 任务类型标签配置
const taskTypeConfig = {
[CrossCheckingTaskType.CITY]: { label: '市交叉评查', color: 'green' as const },
[CrossCheckingTaskType.COUNTY]: { label: '下级交叉评查', color: 'orange' as const }
const taskTypeConfig: Record<string, { label: string; color: 'green' | 'orange' }> = {
[CrossCheckingTaskType.CITY]: { label: '市局间交叉评查', color: 'green' as const },
[CrossCheckingTaskType.DISTRICT]: { label: '区局间交叉评查', color: 'orange' as const }
};
// 案卷类型标签配置
@@ -248,6 +248,11 @@ export default function CrossCheckingIndex() {
total: 0
});
// 客户端调式日志
// useEffect(()=>{
// console.log('[CrossCheckingIndex] loaderData.tasks', loaderData.tasks)
// },[loaderData])
// 获取进度条样式类
const getProgressClass = (progress: number) => {
if (progress === 0) return 'low';
@@ -603,7 +608,7 @@ export default function CrossCheckingIndex() {
align: "center" as const,
width: "10%",
render: (_: unknown, record: CrossCheckingTask) => {
const config = taskTypeConfig[record.taskType];
const config = taskTypeConfig[record.taskType] || { label: record.taskType, color: 'gray' as const };
return (
<Tag color={config.color}>
{config.label}
@@ -613,10 +618,36 @@ export default function CrossCheckingIndex() {
},
{
title: "评查地区",
dataIndex: "evaluationRegion" as keyof CrossCheckingTask,
key: "evaluationRegion",
align: "left" as const,
width: "16%"
width: "16%",
render: (_: unknown, record: CrossCheckingTask) => {
const regions = record.evaluationRegion;
// 如果不是数组,直接显示字符串
if (!Array.isArray(regions)) {
return <span className="text-sm">{regions || '-'}</span>;
}
// 如果是空数组
if (regions.length === 0) {
return <span className="text-sm text-gray-400">-</span>;
}
// 渲染为标签列表
return (
<div className="flex flex-wrap gap-1 items-start">
{regions.map((region, index) => (
<Tag
key={`${region}-${index}`}
color="cyan"
>
{region}
</Tag>
))}
</div>
);
}
},
{
title: "评查进度",
+41 -15
View File
@@ -26,7 +26,7 @@ import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } f
import React, { useState, useEffect, useCallback, useRef, useMemo } from "react";
import { useNavigate, useLoaderData } from "@remix-run/react";
import crossCheckingStyles from "~/styles/cross-checking-result.css?url";
import { getReviewPoints, updateReviewResult} from "~/api/evaluation_points/reviews";
import { getReviewPoints, updateReviewResult, getReviewPoints_fromApi} from "~/api/evaluation_points/reviews";
import { toastService } from "~/components/ui/Toast";
import { confirmReviewResults, checkProposalVotes, findIsProposer } from "~/api/cross-checking/cross-file-result";
@@ -199,8 +199,37 @@ export async function loader({ request }: LoaderFunctionArgs) {
const { getUserSession } = await import("~/api/login/auth.server");
const { userInfo, frontendJWT } = await getUserSession(request);
// 获取评查点数据,传递request对象
const reviewData = await getReviewPoints(id, request);
// 🔒 安全验证:检查用户是否有权限访问该文档
if (!userInfo?.user_id) {
return Response.json({
result: false,
message: '用户身份验证失败,请重新登录'
}, { status: 401 });
}
// const { verifyDocumentAccess } = await import("~/api/cross-checking/verify-document-access");
// const accessCheck = await verifyDocumentAccess({
// documentId: id,
// taskId: taskId,
// userId: userInfo.user_id,
// jwtToken: frontendJWT
// });
// if (!accessCheck.hasAccess) {
// console.warn(`⚠️ [Loader] 用户 ${userInfo.user_id} 尝试访问未授权文档 ${id},原因: ${accessCheck.reason}`);
// return Response.json({
// result: false,
// message: accessCheck.reason || '您没有权限访问该文档'
// }, { status: 403 });
// }
// console.log(`✅ [Loader] 用户 ${userInfo.user_id} (${accessCheck.userRole}) 访问文档 ${id} - 权限验证通过`);
// 对接接口,新的获取评查点结果的方法
const reviewData = await getReviewPoints_fromApi(id, request)
// 获取评查点数据,传递request对象 旧获取评查点结果的方法
// const reviewData = await getReviewPoints(id, request);
// 获取当前登录用户是否是发起人
const isProposer = await findIsProposer(taskId, userInfo?.user_id, frontendJWT);
@@ -313,12 +342,9 @@ export default function CrossCheckingResult() {
const isProcessingRef = useRef(false);
// 添加组件挂载/卸载日志
// useEffect(() => {
// console.log('[组件] CrossCheckingResult 挂载');
// return () => {
// console.log('[组件] CrossCheckingResult 卸载');
// };
// }, []);
useEffect(() => {
console.log('[组件] CrossCheckingResult', isProposer);
}, [isProposer]);
// 同步外部scoring_proposals到本地状态
useEffect(() => {
@@ -536,18 +562,18 @@ export default function CrossCheckingResult() {
// 使用ref防止重复点击,避免触发状态更新
if (isProcessingRef.current) {
console.log('[完成评查] 正在处理中,跳过');
// console.log('[完成评查] 正在处理中,跳过');
return;
}
try {
console.log('[完成评查] 标记为处理中');
// console.log('[完成评查] 标记为处理中');
isProcessingRef.current = true;
// 1. 先检查未投票(不触发loading状态更新,避免重新渲染)
console.log('[完成评查] 开始检查未投票提案');
// console.log('[完成评查] 开始检查未投票提案');
const checkRes = await checkProposalVotes(document.id, jwtToken);
console.log("[完成评查] 检查结果:", checkRes);
// console.log("[完成评查] 检查结果:", checkRes);
if (checkRes.error) {
toastService.error(checkRes.error);
@@ -582,11 +608,11 @@ export default function CrossCheckingResult() {
}
// 4. 重置处理状态标记,准备显示模态框(不触发状态更新)
console.log('[完成评查] 重置处理标记,准备显示模态框');
// console.log('[完成评查] 重置处理标记,准备显示模态框');
isProcessingRef.current = false;
// 5. 弹出模态框
console.log('[完成评查] 显示确认模态框');
// console.log('[完成评查] 显示确认模态框');
messageService.show({
title: '提示',
message: modalMessage,
+28 -47
View File
@@ -14,7 +14,8 @@ import {
type CrossCheckingUploadedFile,
generateFileId,
formatFileSize,
batchUploadAndAssignCrossCheckingFiles
batchUploadAndAssignCrossCheckingFiles,
createCrossReviewTask
} from "~/api/cross-checking/cross-files-upload";
import {
getCrossCheckingDocumentTypes,
@@ -147,48 +148,6 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
});
};
/**
* 创建交叉评查任务
* @param taskData 任务数据
* @param token JWT Token
* @returns 创建结果
*/
export async function createCrossReviewTask(taskData: {
documentIds: number[];
userIds: number[];
assignerId: number;
taskName: string;
docType: string;
}, token: string) {
try {
const response = await fetch(`${API_BASE_URL}/admin/cross_review/tasks/assign`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
document_ids: taskData.documentIds,
user_ids: taskData.userIds,
assigner_id: taskData.assignerId,
task_name: taskData.taskName,
doc_type: taskData.docType
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
console.log('任务创建成功:', result);
return result;
} catch (error) {
console.error('创建任务失败:', error);
throw error;
}
}
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const caseType = formData.get("caseType") as string;
@@ -217,7 +176,7 @@ export default function CrossCheckingUpload() {
const [taskInfo, setTaskInfo] = useState({
name: '',
date: '',
type: '市局间交叉评查',
type: 'CITY', // 使用枚举值,默认为市局间交叉评查
});
// 步骤2状态
const [groupChecked, setGroupChecked] = useState<string[]>(userInfo?.user_id ? [`user_${userInfo.user_id}`] : []);
@@ -441,6 +400,7 @@ export default function CrossCheckingUpload() {
try {
// 获取选中的文档类型信息
const selectedDocType = documentTypes?.find((dt: DocumentType) => dt.id === selectedDocTypeId);
if (!selectedDocType) {
toastService.error("无效的案卷类型");
return;
@@ -460,6 +420,23 @@ export default function CrossCheckingUpload() {
return;
}
// const requireParam = {
// filesToUpload: filesToUpload,
// selectedDocTypeId: selectedDocTypeId,
// priority: priority,
// documentNumber: documentNumber,
// remark: remark,
// isTestDocument: isTestDocument,
// userIds: userIds,
// taskInfo_name: taskInfo.name,
// selectedDocType_name: selectedDocType.code,
// taskInfo_type: taskInfo.type,
// frontendJWT
// }
// console.log("requireParam", requireParam)
// return;
// 使用文档类型名称作为 doc_type
const uploadResult = await batchUploadAndAssignCrossCheckingFiles(
filesToUpload,
@@ -470,10 +447,14 @@ export default function CrossCheckingUpload() {
isTestDocument,
userIds,
taskInfo.name,
selectedDocType.name, // 使用文档类型名称
selectedDocType.code, // 使用文档类型code
taskInfo.type, // 使用任务类型(市局间交叉评查 或 区局间交叉评查)
frontendJWT
);
// return;
const { successes, failures } = uploadResult;
if (failures.length > 0) {
@@ -710,8 +691,8 @@ export default function CrossCheckingUpload() {
value={taskInfo.type}
onChange={e => setTaskInfo({ ...taskInfo, type: e.target.value })}
>
<option value="市局间交叉评查"></option>
<option value="区局间交叉评查"></option>
<option value="CITY"></option>
<option value="DISTRICT"></option>
</select>
</div>
<div className="flex justify-between items-center mt-6">
+1 -1
View File
@@ -324,7 +324,7 @@ export default function DocumentTypesList() {
render: (_: unknown, record: DocumentTypeUI) => (
<div className="flex items-center">
{record.entry_module ? (
<span className="type-badge">{record.entry_module.name}</span>
<span className="entry-module-badge">{record.entry_module.name}</span>
) : (
<span className="text-gray-400"></span>
)}
+23
View File
@@ -134,6 +134,7 @@ export async function action({ request }: ActionFunctionArgs) {
const formData = await request.formData();
const id = formData.get("id") as string | null;
const name = formData.get("name") as string;
const code = formData.get("code") as string;
const description = formData.get("description") as string;
const entryModuleId = formData.get("entry_module_id") as string;
const llmExtractionTemplateId = formData.get("llm_extraction_template") as string;
@@ -171,6 +172,7 @@ export async function action({ request }: ActionFunctionArgs) {
// 构建文档类型数据 - group_ids 转换为 number[]
const documentTypeData = {
name,
code: code || null,
description,
group_ids: selectedGroups.map(id => parseInt(id, 10)),
entry_module_id: entryModuleId ? parseInt(entryModuleId) : null,
@@ -239,6 +241,7 @@ export default function DocumentTypeNew() {
const [formData, setFormData] = useState({
id: documentType?.id || "",
name: documentType?.name || "",
code: documentType?.code || "",
description: documentType?.description || "",
entryModuleId: documentType?.entry_module?.id?.toString() || "",
llmExtractionTemplateId: documentType?.llm_extraction_template_id?.toString() || "",
@@ -287,9 +290,11 @@ export default function DocumentTypeNew() {
// 当文档类型数据加载完成时更新表单
useEffect(() => {
if (documentType) {
console.log('documentType', documentType)
setFormData({
id: documentType.id,
name: documentType.name,
code: documentType.code || "",
description: documentType.description,
entryModuleId: documentType.entry_module?.id?.toString() || "",
llmExtractionTemplateId: documentType.llm_extraction_template_id?.toString() || "",
@@ -592,6 +597,24 @@ export default function DocumentTypeNew() {
<div className="form-tip"></div>
</div>
{/* 文档类型编码 */}
<div className="form-group">
<label htmlFor="type-code" className="form-label">
</label>
<input
type="text"
id="type-code"
name="code"
className="form-input"
placeholder="请输入文档类型编码"
value={formData.code}
onChange={handleInputChange}
readOnly={isReadOnly}
/>
<div className="form-tip"></div>
</div>
{/* 入口模块 */}
<div className="form-group">
<label htmlFor="entry-module" className="form-label">
+16 -4
View File
@@ -857,6 +857,9 @@ export default function RolePermissions() {
// 存储每个路由的 permissionsrouteId -> permissions[]
const [routePermissionsMap, setRoutePermissionsMap] = useState<Map<number, ApiPermission[]>>(new Map());
// 保存权限的 loading 状态
const [savingPermissions, setSavingPermissions] = useState(false);
// 加载初始数据
useEffect(() => {
loadData();
@@ -1130,7 +1133,7 @@ export default function RolePermissions() {
};
// v3.0: 获取HTTP方法对应的标签样式
const getMethodTagStyle = (method: string): React.CSSProperties => {
const getMethodTagStyle = (method: string | null | undefined): React.CSSProperties => {
const styles: Record<string, React.CSSProperties> = {
'GET': { backgroundColor: '#e6f7ed', color: '#52c41a', border: '1px solid #b7eb8f' },
'POST': { backgroundColor: '#e6f0ff', color: '#1890ff', border: '1px solid #91caff' },
@@ -1138,6 +1141,12 @@ export default function RolePermissions() {
'DELETE': { backgroundColor: '#fff1f0', color: '#f5222d', border: '1px solid #ffa39e' },
'PATCH': { backgroundColor: '#f0f5ff', color: '#722ed1', border: '1px solid #d3adf7' }
};
// 空值检查:如果 method 为 null 或 undefined,返回默认样式
if (!method) {
return { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #d9d9d9' };
}
return styles[method.toUpperCase()] || { backgroundColor: '#f5f5f5', color: '#666', border: '1px solid #d9d9d9' };
};
@@ -1251,6 +1260,7 @@ export default function RolePermissions() {
return;
}
setSavingPermissions(true);
try {
// 1. 保存路由权限
const routeResult = await updateRoleRoutePermissions(selectedRole.id, selectedRouteIds);
@@ -1284,6 +1294,8 @@ export default function RolePermissions() {
} catch (error) {
console.error("保存权限失败:", error);
toastService.error("保存权限失败");
} finally {
setSavingPermissions(false);
}
};
@@ -1593,11 +1605,11 @@ export default function RolePermissions() {
<h3> "{selectedRole.role_name}" </h3>
<Button
type="primary"
icon="ri-save-line"
icon={savingPermissions ? "ri-loader-4-line spin" : "ri-save-line"}
onClick={handleSavePermissions}
disabled={!isProvincialAdmin}
disabled={!isProvincialAdmin || savingPermissions}
>
{savingPermissions ? '保存中...' : '保存权限'}
</Button>
</div>