feat: 1. 完善评查点分组的删除逻辑,会涉及文档类型绑定的一级分组,分组绑定的评查点规则。新增一个评查点分组换绑的。

2. 修复交叉评查的任务中的文档列表的历史文档的查看跳转路径。
3. 修复评查点新增中评查点类型只能显示当前文档类型绑定的这几个一级分组。评查点类型=一级分组。
4. 修复文档列表关于pdf的下载失败的问题。
This commit is contained in:
2025-12-19 00:21:49 +08:00
parent 38f17fb3ed
commit 616f059f1e
14 changed files with 626 additions and 117 deletions
@@ -1,5 +1,4 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { Link } from "@remix-run/react";
import { Modal } from '../ui/Modal';
import { FileTag } from '../ui/FileTag';
import { FileTypeTag } from '../ui/FileTypeTag';
@@ -368,18 +367,20 @@ export function DocumentListModal({
</td>
<td className="px-4 py-3" style={{ width: '13%' }}>
<div className="flex flex-wrap gap-1">
{/* 查看按钮 */}
<Link
to={`/cross-checking/review?id=${historyDoc.id}&taskId=${taskId}`}
{/* 查看按钮 - 与主表格样式和行为一致 */}
<button
type="button"
className={`text-xs px-2 py-1 h-7 mr-1 ${
historyDoc.status === 'Processed'
? 'hover:underline text-primary'
: 'text-gray-400 pointer-events-none'
? 'hover:underline text-primary cursor-pointer'
: 'text-gray-400 cursor-not-allowed'
}`}
onClick={() => historyDoc.status === 'Processed' && handleViewClickDebounced(historyDoc.id.toString())}
disabled={historyDoc.status !== 'Processed'}
>
<i className="ri-eye-line mr-1"></i>
</Link>
{isNavigating ? '跳转中...' : '查看'}
</button>
{/* 追加附件按钮 - 仅当 type_name 包含"合同"时显示 */}
{historyDoc.status === 'Processed' && taskId && parentDoc.type_name?.includes('合同') && (
<button
+234
View File
@@ -0,0 +1,234 @@
import { useState } from 'react';
import type { RuleGroup, DocTypeInfo } from '~/api/evaluation_points/rule-groups';
interface RebindModalProps {
/** 是否显示弹窗 */
visible: boolean;
/** 要删除的分组ID */
groupId: string;
/** 要删除的分组名称 */
groupName: string;
/** 关联的评查点数量 */
pointsCount: number;
/** 只绑定当前分组的文档类型(换绑时会替换为新分组) */
singleBoundDocTypes: DocTypeInfo[];
/** 绑定多个分组的文档类型(换绑时会移除当前分组) */
multiBoundDocTypes: DocTypeInfo[];
/** 所有一级分组列表(用于选择新分组) */
allFirstLevelGroups: RuleGroup[];
/** 换绑并删除操作是否加载中 */
loading?: boolean;
/** 关闭弹窗 */
onClose: () => void;
/** 确认换绑并删除 */
onConfirm: (newGroupId: string) => void;
}
export function RebindModal({
visible,
groupId,
groupName,
pointsCount,
singleBoundDocTypes,
multiBoundDocTypes,
allFirstLevelGroups,
loading = false,
onClose,
onConfirm
}: RebindModalProps) {
// 选中的新一级分组ID
const [selectedGroupId, setSelectedGroupId] = useState<string>('');
// 过滤掉当前要删除的一级分组
const availableGroups = allFirstLevelGroups.filter(g => g.id !== groupId);
// 处理确认换绑
const handleConfirm = () => {
if (!selectedGroupId) {
return;
}
onConfirm(selectedGroupId);
};
// 重置状态
const handleClose = () => {
setSelectedGroupId('');
onClose();
};
if (!visible) return null;
return (
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
{/* 遮罩层 */}
<div
className="absolute inset-0 bg-black/50"
onClick={handleClose}
/>
{/* 弹窗内容 */}
<div className="relative bg-white rounded-lg shadow-xl w-[800px] min-h-[500px] max-h-[85vh] overflow-hidden flex flex-col">
{/* 标题栏 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-gray-900">
<i className="ri-exchange-line mr-2 text-primary"></i>
</h3>
<button
type="button"
onClick={handleClose}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<i className="ri-close-line text-xl"></i>
</button>
</div>
{/* 主体内容 */}
<div className="px-6 py-4 overflow-y-auto flex-1">
{/* 提示信息 */}
<div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg">
<div className="flex items-start">
<i className="ri-alert-line text-amber-500 text-xl mr-3 mt-0.5"></i>
<div>
<p className="text-amber-800 font-medium">
{groupName} <span className="text-amber-900 font-bold">{pointsCount}</span>
</p>
<p className="text-amber-700 text-sm mt-1">
</p>
</div>
</div>
</div>
{/* 两栏布局 */}
<div className="grid grid-cols-2 gap-6 mb-6">
{/* 左侧:需要更换绑定的文档类型 */}
<div className="border border-gray-200 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
<i className="ri-file-transfer-line mr-2 text-blue-500"></i>
</h4>
{singleBoundDocTypes.length > 0 ? (
<>
<p className="text-xs text-gray-500 mb-3">
</p>
<div className="flex flex-wrap gap-2">
{singleBoundDocTypes.map(docType => (
<span
key={docType.id}
className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800"
>
<i className="ri-file-text-line mr-1"></i>
{docType.name}
</span>
))}
</div>
</>
) : (
<p className="text-sm text-gray-400 italic"></p>
)}
</div>
{/* 右侧:选择新的一级分组 */}
<div className="border border-gray-200 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
<i className="ri-folder-transfer-line mr-2 text-primary"></i>
</h4>
<p className="text-xs text-gray-500 mb-3">
</p>
{availableGroups.length > 0 ? (
<div className="min-h-[280px] overflow-y-auto space-y-2">
{availableGroups.map(group => (
<label
key={group.id}
className={`flex items-center p-2 rounded-md cursor-pointer transition-colors ${
selectedGroupId === group.id
? 'bg-primary/10 border border-primary'
: 'hover:bg-gray-50 border border-transparent'
}`}
>
<input
type="radio"
name="newGroup"
value={group.id}
checked={selectedGroupId === group.id}
onChange={(e) => setSelectedGroupId(e.target.value)}
className="mr-3 text-primary focus:ring-primary"
/>
<i className="ri-folder-line mr-2 text-gray-400"></i>
<span className="text-sm text-gray-700">{group.name}</span>
{group.code && (
<span className="ml-2 text-xs text-gray-400">({group.code})</span>
)}
</label>
))}
</div>
) : (
<p className="text-sm text-gray-400 italic"></p>
)}
</div>
</div>
{/* 下方:会移除绑定的文档类型 */}
{multiBoundDocTypes.length > 0 && (
<div className="border border-gray-200 rounded-lg p-4">
<h4 className="text-sm font-medium text-gray-700 mb-3 flex items-center">
<i className="ri-delete-bin-line mr-2 text-orange-500"></i>
</h4>
<p className="text-xs text-gray-500 mb-3">
</p>
<div className="flex flex-wrap gap-2">
{multiBoundDocTypes.map(docType => (
<span
key={docType.id}
className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800"
>
<i className="ri-file-text-line mr-1"></i>
{docType.name}
</span>
))}
</div>
</div>
)}
</div>
{/* 底部按钮 */}
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-gray-200 bg-gray-50">
<button
type="button"
onClick={handleClose}
disabled={loading}
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
<button
type="button"
onClick={handleConfirm}
disabled={!selectedGroupId || loading}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-primary border border-transparent rounded-md hover:bg-primary-hover focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<>
<i className="ri-loader-4-line animate-spin mr-2"></i>
...
</>
) : (
<>
<i className="ri-exchange-line mr-2"></i>
</>
)}
</button>
</div>
</div>
</div>
);
}
export default RebindModal;
+90 -9
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import type { EvaluationPoint } from '~/models/evaluation_points';
import type { EvaluationPointGroup } from '~/models/evaluation_point_groups';
import { getRulesList } from '~/api/evaluation_points/rules';
import { getRulesList, getRuleTypes, type RuleType } from '~/api/evaluation_points/rules';
interface BasicInfoProps {
onChange?: (data: Record<string, unknown>) => void;
@@ -77,8 +77,15 @@ export function BasicInfo({
const getCheckpointTypeCode = () => {
if (!formData.evaluation_point_groups_pid) return "";
// 优先从 API 返回的 filteredRuleTypes 中查找
const fromApi = filteredRuleTypes.find(
ruleType => Number(ruleType.id) === formData.evaluation_point_groups_pid
);
if (fromApi?.code) return fromApi.code;
// 兜底:从 evaluationPointGroups 中查找
const typeGroup = evaluationPointGroups.find(
group => group.id === formData.evaluation_point_groups_pid && (!group.pid || group.pid === 0) // 🆕 NULL或0都表示顶级分组
group => group.id === formData.evaluation_point_groups_pid && (!group.pid || group.pid === 0)
);
return typeGroup?.code || "";
@@ -86,10 +93,53 @@ export function BasicInfo({
// 评查点描述与法律依据 展开状态
const [isDescExpanded, setIsDescExpanded] = useState(false);
// 条款号临时输入字符串(不会触发自动分割)
const [lawArticlesText, setLawArticlesText] = useState('');
// 从 API 获取的评查点类型列表(根据 documentTypeIds 过滤)
const [filteredRuleTypes, setFilteredRuleTypes] = useState<RuleType[]>([]);
const [ruleTypesLoading, setRuleTypesLoading] = useState(false);
// 从 Session Storage 获取 documentTypeIds 并调用 API 获取评查点类型
useEffect(() => {
const fetchRuleTypes = async () => {
try {
const storedIds = sessionStorage.getItem('documentTypeIds');
if (!storedIds) {
setFilteredRuleTypes([]);
return;
}
const parsedIds = JSON.parse(storedIds);
if (!Array.isArray(parsedIds) || parsedIds.length === 0) {
setFilteredRuleTypes([]);
return;
}
const documentTypeIds = parsedIds.map(id => Number(id));
setRuleTypesLoading(true);
// 调用 getRuleTypes API 获取过滤后的评查点类型
const response = await getRuleTypes(documentTypeIds, frontendJWT);
if (response.data) {
setFilteredRuleTypes(response.data);
} else {
console.error('获取评查点类型失败:', response.error);
setFilteredRuleTypes([]);
}
} catch (error) {
console.error('获取评查点类型失败:', error);
setFilteredRuleTypes([]);
} finally {
setRuleTypesLoading(false);
}
};
fetchRuleTypes();
}, [frontendJWT]);
// 根据选择的评查点类型筛选可用的规则组
const filteredRuleGroups = evaluationPointGroups.filter(group =>
formData.evaluation_point_groups_pid &&
@@ -97,8 +147,31 @@ export function BasicInfo({
group.is_enabled
);
// 🆕 获取评查点类型选项(pid为NULL或0的数据)
// 🆕 获取评查点类型选项(使用 API 返回的过滤后数据)
const getCheckpointTypeOptions = () => {
if (ruleTypesLoading) {
return (
<>
<option value="">...</option>
</>
);
}
// 如果 API 返回了数据,使用 API 数据
if (filteredRuleTypes.length > 0) {
return (
<>
<option value=""></option>
{filteredRuleTypes.map(ruleType => (
<option key={ruleType.id} value={ruleType.code}>
{ruleType.name}
</option>
))}
</>
);
}
// 兜底:如果 API 没有返回数据,使用 evaluationPointGroups 中的一级分组
if (!evaluationPointGroups || evaluationPointGroups.length === 0) {
return (
<>
@@ -107,7 +180,9 @@ export function BasicInfo({
);
}
const typeGroups = evaluationPointGroups.filter(group => (!group.pid || group.pid === 0) && group.is_enabled);
const typeGroups = evaluationPointGroups.filter(group =>
(!group.pid || group.pid === 0) && group.is_enabled
);
return (
<>
@@ -177,10 +252,16 @@ export function BasicInfo({
case 'checkpoint-type':
// 处理评查点类型选择
if (value) {
// 🆕 找到选中的类型组(pid为NULL或0表示顶级分组)
const selectedType = evaluationPointGroups.find(group => group.code === value && (!group.pid || group.pid === 0));
if (selectedType) {
newData.evaluation_point_groups_pid = selectedType.id;
// 优先从 API 返回的 filteredRuleTypes 中查找
const selectedFromApi = filteredRuleTypes.find(ruleType => ruleType.code === value);
if (selectedFromApi) {
newData.evaluation_point_groups_pid = Number(selectedFromApi.id);
} else {
// 兜底:从 evaluationPointGroups 中查找(pid为NULL或0表示顶级分组)
const selectedType = evaluationPointGroups.find(group => group.code === value && (!group.pid || group.pid === 0));
if (selectedType) {
newData.evaluation_point_groups_pid = selectedType.id;
}
}
} else {
newData.evaluation_point_groups_pid = null;