feat: 1. 完善评查点分组的删除逻辑,会涉及文档类型绑定的一级分组,分组绑定的评查点规则。新增一个评查点分组换绑的。
2. 修复交叉评查的任务中的文档列表的历史文档的查看跳转路径。 3. 修复评查点新增中评查点类型只能显示当前文档类型绑定的这几个一级分组。评查点类型=一级分组。 4. 修复文档列表关于pdf的下载失败的问题。
This commit is contained in:
@@ -45,6 +45,39 @@ interface ApiResponse<T> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
/** 文档类型信息(用于删除换绑) */
|
||||
export interface DocTypeInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
/** 删除分组响应 */
|
||||
export interface DeleteGroupResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
deleted_count?: number;
|
||||
deleted_groups?: number;
|
||||
deleted_points?: number;
|
||||
/** 是否需要换绑(仅删除一级分组时) */
|
||||
need_rebind?: boolean;
|
||||
/** 关联的评查点数量 */
|
||||
points_count?: number;
|
||||
/** 只绑定当前分组的文档类型(换绑时替换) */
|
||||
single_bound_doc_types?: DocTypeInfo[];
|
||||
/** 绑定多个分组的文档类型(换绑时移除) */
|
||||
multi_bound_doc_types?: DocTypeInfo[];
|
||||
error?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/** 换绑响应 */
|
||||
export interface RebindResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
rebind_count: number;
|
||||
doc_types_updated: number;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
@@ -520,13 +553,18 @@ export async function updateEvaluationPointGroup(
|
||||
export async function deleteEvaluationPointGroup(
|
||||
id: string,
|
||||
token?: string
|
||||
): Promise<{success: boolean; message?: string; deleted_groups?: number; deleted_points?: number; error?: never} | {success: false; error: string; status?: number}> {
|
||||
): Promise<DeleteGroupResponse> {
|
||||
try {
|
||||
const response = await apiRequest<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
deleted_groups: number;
|
||||
deleted_points: number;
|
||||
deleted_count?: number;
|
||||
deleted_groups?: number;
|
||||
deleted_points?: number;
|
||||
need_rebind?: boolean;
|
||||
points_count?: number;
|
||||
single_bound_doc_types?: DocTypeInfo[];
|
||||
multi_bound_doc_types?: DocTypeInfo[];
|
||||
}>(
|
||||
`/api/v3/evaluation-point-groups/${id}`,
|
||||
{
|
||||
@@ -543,8 +581,13 @@ export async function deleteEvaluationPointGroup(
|
||||
return {
|
||||
success: response.data.success,
|
||||
message: response.data.message,
|
||||
deleted_groups: response.data.deleted_groups,
|
||||
deleted_points: response.data.deleted_points
|
||||
deleted_count: response.data.deleted_count || 0,
|
||||
deleted_groups: response.data.deleted_groups || 0,
|
||||
deleted_points: response.data.deleted_points || 0,
|
||||
need_rebind: response.data.need_rebind || false,
|
||||
points_count: response.data.points_count || 0,
|
||||
single_bound_doc_types: response.data.single_bound_doc_types || [],
|
||||
multi_bound_doc_types: response.data.multi_bound_doc_types || []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -559,6 +602,55 @@ export async function deleteEvaluationPointGroup(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 7.1 换绑一级分组(FastAPI v3)
|
||||
* @param oldGroupId 要删除的一级分组ID
|
||||
* @param newParentId 新的一级分组ID
|
||||
* @param token JWT token
|
||||
* @returns 换绑结果
|
||||
*/
|
||||
export async function rebindEvaluationPointGroup(
|
||||
oldGroupId: string,
|
||||
newParentId: string,
|
||||
token?: string
|
||||
): Promise<{success: boolean; message?: string; rebind_count?: number; doc_types_updated?: number; error?: never} | {success: false; error: string; status?: number}> {
|
||||
try {
|
||||
const response = await apiRequest<RebindResponse>(
|
||||
`/api/v3/evaluation-point-groups/${oldGroupId}/rebind`,
|
||||
{
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ new_parent_id: Number(newParentId) }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {})
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
if (response.error) {
|
||||
return { success: false, error: response.error, status: response.status };
|
||||
}
|
||||
|
||||
if (response.data) {
|
||||
return {
|
||||
success: response.data.success,
|
||||
message: response.data.message,
|
||||
rebind_count: response.data.rebind_count,
|
||||
doc_types_updated: response.data.doc_types_updated
|
||||
};
|
||||
}
|
||||
|
||||
return { success: false, error: '换绑失败:返回数据格式不正确', status: 500 };
|
||||
} catch (error) {
|
||||
console.error('❌ 换绑分组出错:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : '换绑分组失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 8. 批量更新分组启用状态(FastAPI v3)
|
||||
* @param ids 分组ID列表
|
||||
|
||||
@@ -314,7 +314,7 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul
|
||||
area: point.area || ''
|
||||
};
|
||||
});
|
||||
console.log('✅ [getRulesList] 成功映射评查点列表数据', response.data.data[0]);
|
||||
// console.log('✅ [getRulesList] 成功映射评查点列表数据', response.data.data[0]);
|
||||
|
||||
return {
|
||||
data: {
|
||||
@@ -486,6 +486,8 @@ export async function deleteRule(id: string, token?: string): Promise<{data: {su
|
||||
*/
|
||||
export interface RuleType {
|
||||
id: string;
|
||||
pid?: string;
|
||||
code?: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
isEnabled: boolean;
|
||||
@@ -703,6 +705,7 @@ export async function getRuleGroupsByType(typeId: string, token?: string): Promi
|
||||
if(response.data && 'code' in response.data && response.data.data){
|
||||
if(Array.isArray(response.data.data) && response.data.data.length > 0){
|
||||
// 将API返回的数据映射到前端模型
|
||||
// console.log("评查点类型列表",response.data);
|
||||
const ruleGroups = response.data.data.map(item => ({
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -147,7 +147,7 @@ const configs: Record<string, ApiConfig> = {
|
||||
documentUrl: 'http://172.16.0.55:8073/docauditai/',
|
||||
uploadUrl: 'http://172.16.0.55:8073/api/v2/documents',
|
||||
|
||||
collaboraUrl: 'http://172.16.0.81:9980',
|
||||
collaboraUrl: 'http://172.16.0.58:9980',
|
||||
appUrl: 'http://172.16.0.34:5173',
|
||||
|
||||
oauth: {
|
||||
|
||||
@@ -28,6 +28,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 构建完整的文件 URL
|
||||
const fileUrl = `${DOCUMENT_URL}${filePath}`;
|
||||
// console.log('fileUrl:', fileUrl)
|
||||
|
||||
// 使用 JWT 认证获取文件
|
||||
const response = await fetch(fileUrl, {
|
||||
|
||||
@@ -173,7 +173,7 @@ export default function DocumentTypesList() {
|
||||
|
||||
// 处理测试loader返回的信息
|
||||
useEffect(() => {
|
||||
console.log('返回的父级评查点分组数据',parentGroups)
|
||||
// console.log('返回的父级评查点分组数据',parentGroups)
|
||||
}, [parentGroups])
|
||||
|
||||
// 处理loader加载数据的时候的错误
|
||||
|
||||
@@ -575,42 +575,24 @@ export default function DocumentsIndex() {
|
||||
};
|
||||
|
||||
// 下载文档
|
||||
const handleDownload = async (path: string) => {
|
||||
try {
|
||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`;
|
||||
const handleDownload = (path: string) => {
|
||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(path)}`;
|
||||
|
||||
// 使用fetch获取文件内容
|
||||
const response = await fetch(downloadUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
// 直接使用链接下载,避免 fetch + blob 在生产环境下对 PDF 的兼容问题
|
||||
const a = document.createElement('a');
|
||||
a.style.display = 'none';
|
||||
a.href = downloadUrl;
|
||||
// 从路径中获取文件名
|
||||
const fileName = path.split('/').pop() || 'document';
|
||||
a.download = decodeURIComponent(fileName);
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// 将响应转换为Blob
|
||||
const blob = await response.blob();
|
||||
|
||||
// 创建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 : '未知错误'}`);
|
||||
}
|
||||
// 清理
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(a);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 删除文档
|
||||
|
||||
@@ -12,10 +12,13 @@ import {
|
||||
getEvaluationPointGroups,
|
||||
getChildGroups,
|
||||
type RuleGroup,
|
||||
type DocTypeInfo,
|
||||
deleteEvaluationPointGroup,
|
||||
rebindEvaluationPointGroup,
|
||||
batchUpdateEvaluationPointGroupStatus,
|
||||
batchDeleteEvaluationPointGroups
|
||||
} from "~/api/evaluation_points/rule-groups";
|
||||
import { RebindModal } from "~/components/rule-groups/RebindModal";
|
||||
import { toastService, messageService } from "~/components/ui";
|
||||
import { usePermission } from "~/hooks/usePermission";
|
||||
|
||||
@@ -90,6 +93,17 @@ export default function RuleGroupsIndex() {
|
||||
const [initialLoading, setInitialLoading] = useState<boolean>(true);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]); // 🆕 批量选择状态
|
||||
|
||||
// 换绑弹窗状态
|
||||
const [rebindModalVisible, setRebindModalVisible] = useState(false);
|
||||
const [rebindModalData, setRebindModalData] = useState<{
|
||||
groupId: string;
|
||||
groupName: string;
|
||||
pointsCount: number;
|
||||
singleBoundDocTypes: DocTypeInfo[];
|
||||
multiBoundDocTypes: DocTypeInfo[];
|
||||
} | null>(null);
|
||||
const [rebindLoading, setRebindLoading] = useState(false);
|
||||
|
||||
// ✅ 使用权限 Hook
|
||||
const { canCreate, canUpdate, canDelete, canBatch } = usePermission();
|
||||
const canCreateGroup = canCreate('evaluation_group');
|
||||
@@ -252,13 +266,29 @@ export default function RuleGroupsIndex() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否是一级分组
|
||||
const parentGroup = groups.find(g => g.id === groupId);
|
||||
const isFirstLevel = !!parentGroup;
|
||||
|
||||
// 如果是一级分组,检查是否还有二级分组
|
||||
if (isFirstLevel && parentGroup.children && parentGroup.children.length > 0) {
|
||||
messageService.show({
|
||||
title: "无法删除",
|
||||
message: "尚未完全删除该一级分组下的所有二级分组,不可进行删除操作,请检查。",
|
||||
type: "error",
|
||||
confirmText: "知道了"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 一级分组无子分组 或 二级分组,显示确认删除提示
|
||||
messageService.show({
|
||||
title: "确认删除",
|
||||
message: "确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。",
|
||||
message: "确定要删除该分组吗?此操作将同时解除该分组下的已绑定的所有评查点。",
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
confirmDelay: 4,
|
||||
confirmDelay: 2,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const result = await deleteEvaluationPointGroup(groupId, frontendJWT);
|
||||
@@ -285,9 +315,19 @@ export default function RuleGroupsIndex() {
|
||||
|
||||
// 显示成功消息
|
||||
toastService.success('删除成功')
|
||||
} else if (result.need_rebind) {
|
||||
// 需要换绑,打开换绑弹窗
|
||||
setRebindModalData({
|
||||
groupId: groupId,
|
||||
groupName: parentGroup?.name || '未知分组',
|
||||
pointsCount: result.points_count || 0,
|
||||
singleBoundDocTypes: result.single_bound_doc_types || [],
|
||||
multiBoundDocTypes: result.multi_bound_doc_types || []
|
||||
});
|
||||
setRebindModalVisible(true);
|
||||
} else {
|
||||
toastService.error(`删除失败: ${result.error}`);
|
||||
console.error(`删除失败: ${result.error}`);
|
||||
// 其他失败情况(如有二级分组)
|
||||
toastService.error(result.message || result.error || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除分组失败:', error);
|
||||
@@ -297,6 +337,49 @@ export default function RuleGroupsIndex() {
|
||||
});
|
||||
};
|
||||
|
||||
// 处理换绑确认
|
||||
const handleRebindConfirm = async (newGroupId: string) => {
|
||||
if (!rebindModalData) return;
|
||||
|
||||
setRebindLoading(true);
|
||||
try {
|
||||
// 1. 执行换绑
|
||||
const rebindResult = await rebindEvaluationPointGroup(
|
||||
rebindModalData.groupId,
|
||||
newGroupId,
|
||||
frontendJWT
|
||||
);
|
||||
|
||||
if (!rebindResult.success) {
|
||||
toastService.error(rebindResult.error || '换绑失败');
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.success(rebindResult.message || '换绑成功');
|
||||
|
||||
// 2. 换绑成功后,再次调用删除
|
||||
const deleteResult = await deleteEvaluationPointGroup(rebindModalData.groupId, frontendJWT);
|
||||
|
||||
if (deleteResult.success) {
|
||||
toastService.success(deleteResult.message || '删除成功');
|
||||
|
||||
// 关闭弹窗
|
||||
setRebindModalVisible(false);
|
||||
setRebindModalData(null);
|
||||
|
||||
// 刷新页面
|
||||
window.location.reload();
|
||||
} else {
|
||||
toastService.error(deleteResult.message || deleteResult.error || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('换绑并删除失败:', error);
|
||||
toastService.error('操作失败,请稍后重试');
|
||||
} finally {
|
||||
setRebindLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 🆕 批量启用/禁用
|
||||
const handleBatchEnable = async (enable: boolean) => {
|
||||
// ✅ 检查更新权限
|
||||
@@ -317,7 +400,8 @@ export default function RuleGroupsIndex() {
|
||||
// 刷新页面以重新加载数据
|
||||
window.location.reload();
|
||||
} else {
|
||||
toastService.error(`批量操作失败:${result.failed_ids.length} 个分组操作失败`);
|
||||
// toastService.error(`批量操作失败:${result.failed_ids.length} 个分组操作失败`);
|
||||
toastService.error(`批量操作失败`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量操作失败:', error);
|
||||
@@ -338,9 +422,23 @@ export default function RuleGroupsIndex() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查选中的分组中是否存在一级分组 - 完全禁止批量删除一级分组
|
||||
const selectedFirstLevelGroups = groups.filter(g => selectedIds.includes(g.id));
|
||||
|
||||
if (selectedFirstLevelGroups.length > 0) {
|
||||
messageService.show({
|
||||
title: "无法批量删除",
|
||||
message: "批量删除不支持删除一级分组,请单独删除一级分组或仅选择二级分组进行批量删除。",
|
||||
type: "error",
|
||||
confirmText: "知道了"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 只允许批量删除二级分组
|
||||
messageService.show({
|
||||
title: "确认批量删除",
|
||||
message: `确定要删除选中的 ${selectedIds.length} 个分组吗?此操作不可恢复。`,
|
||||
message: `确定要删除选中的 ${selectedIds.length} 个二级分组吗?此操作将同时解除这些分组下的已绑定的所有评查点。`,
|
||||
type: "warning",
|
||||
confirmText: "删除",
|
||||
cancelText: "取消",
|
||||
@@ -872,6 +970,23 @@ export default function RuleGroupsIndex() {
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 换绑弹窗 */}
|
||||
<RebindModal
|
||||
visible={rebindModalVisible}
|
||||
groupId={rebindModalData?.groupId || ''}
|
||||
groupName={rebindModalData?.groupName || ''}
|
||||
pointsCount={rebindModalData?.pointsCount || 0}
|
||||
singleBoundDocTypes={rebindModalData?.singleBoundDocTypes || []}
|
||||
multiBoundDocTypes={rebindModalData?.multiBoundDocTypes || []}
|
||||
allFirstLevelGroups={groups}
|
||||
loading={rebindLoading}
|
||||
onClose={() => {
|
||||
setRebindModalVisible(false);
|
||||
setRebindModalData(null);
|
||||
}}
|
||||
onConfirm={handleRebindConfirm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -293,7 +293,7 @@ export default function RulesIndex() {
|
||||
if (typeResponse.data) {
|
||||
loadedRuleTypes = typeResponse.data;
|
||||
setRuleTypes(loadedRuleTypes);
|
||||
console.log("📋 [fetchData] 获取到评查点类型:", loadedRuleTypes);
|
||||
// console.log("📋 [fetchData] 获取到评查点类型:", loadedRuleTypes);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载评查点类型失败:', error);
|
||||
@@ -308,7 +308,7 @@ export default function RulesIndex() {
|
||||
} else if (loadedRuleTypes && loadedRuleTypes.length > 0) {
|
||||
// 选择"全部"或未选择,使用刚加载的评查点类型的 id
|
||||
finalRuleType = loadedRuleTypes.map(type => type.id).join(',');
|
||||
console.log("📋 [fetchData] 选择全部类型,使用 loadedRuleTypes 的 id 组合:", finalRuleType);
|
||||
// console.log("📋 [fetchData] 选择全部类型,使用 loadedRuleTypes 的 id 组合:", finalRuleType);
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
z-index: 99999; /* 比 Modal (9999) 更高,确保提示框显示在最顶层 */
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.3s ease forwards;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user