feat: 1. 完善交叉评查上传创建任务,改成动态加载文档类型。
2. 重新对齐交叉评查的接口。
This commit is contained in:
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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: "评查进度",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -857,6 +857,9 @@ export default function RolePermissions() {
|
||||
// 存储每个路由的 permissions(routeId -> 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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user