feat: 1. 完善文档列表的显示效果,数据对接后端接口返回。
2. 对评查点分组和文档类型的编辑删除新增操作进行限制。
This commit is contained in:
@@ -64,7 +64,7 @@ export interface DocumentTypeSearchParams {
|
||||
groupId?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
reviewType?: string;
|
||||
documentTypeIds?: number[]; // 文档类型 ID 数组
|
||||
}
|
||||
|
||||
|
||||
@@ -254,18 +254,15 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams =
|
||||
// 如果groupId存在,则将groupId作为子级评查点分组ID
|
||||
filter['evaluation_point_groups_ids'] = `cs.[${searchParams.groupId}]`;
|
||||
}
|
||||
|
||||
// 根据 reviewType 添加过滤条件
|
||||
if (searchParams.reviewType) {
|
||||
if (searchParams.reviewType === 'contract') {
|
||||
// 如果是合同类型,只显示id=1的文档类型
|
||||
filter['id'] = 'eq.1';
|
||||
} else if (searchParams.reviewType === 'record') {
|
||||
// 如果是卷宗类型,只显示id=2或id=3的文档类型
|
||||
filter['id'] = 'in.(2,3,155)';
|
||||
}
|
||||
|
||||
// 🔑 根据 documentTypeIds 添加过滤条件
|
||||
if (searchParams.documentTypeIds && searchParams.documentTypeIds.length > 0) {
|
||||
// 直接使用文档类型 ID 数组进行过滤
|
||||
const typeIdsStr = searchParams.documentTypeIds.join(',');
|
||||
filter['id'] = `in.(${typeIdsStr})`;
|
||||
// console.log('📋 [getDocumentTypes] 根据文档类型 IDs 过滤:', typeIdsStr);
|
||||
}
|
||||
|
||||
|
||||
params.filter = filter;
|
||||
|
||||
// console.log('获取文档类型列表,参数:', params);
|
||||
@@ -280,44 +277,14 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams =
|
||||
const documentTypes = extractedData || [];
|
||||
|
||||
// console.log('提取的文档类型数据:', documentTypes);
|
||||
|
||||
// 为每个文档类型获取关联的分组
|
||||
const typesWithGroups = await Promise.all(documentTypes.map(async (type) => {
|
||||
// 获取文档类型关联的分组IDs
|
||||
let groupIds: number[] = [];
|
||||
|
||||
try {
|
||||
// 尝试解析evaluation_point_groups_ids
|
||||
if (typeof type.evaluation_point_groups_ids === 'string') {
|
||||
// 如果是JSON字符串,解析它
|
||||
groupIds = JSON.parse(type.evaluation_point_groups_ids as unknown as string);
|
||||
} else if (Array.isArray(type.evaluation_point_groups_ids)) {
|
||||
// 如果已经是数组,直接使用
|
||||
groupIds = type.evaluation_point_groups_ids;
|
||||
} else if (type.evaluation_point_groups_ids) {
|
||||
// 其他情况,尝试将其转换为数组
|
||||
groupIds = [type.evaluation_point_groups_ids as unknown as number];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析分组ID失败:', error, '原始值:', type.evaluation_point_groups_ids);
|
||||
groupIds = [];
|
||||
}
|
||||
|
||||
// console.log(`文档类型 ${type.id} 的分组IDs:`, groupIds);
|
||||
|
||||
// 获取这些ID对应的分组信息
|
||||
const groupsResponse = await getEvaluationPointGroupsByIds(groupIds, frontendJWT);
|
||||
|
||||
// 返回包含分组信息的文档类型
|
||||
return {
|
||||
...type,
|
||||
groups: groupsResponse.data || []
|
||||
};
|
||||
// 🔧 优化:移除评查点分组查询(文档列表UI不需要此数据)
|
||||
// 直接转换为UI类型,不查询关联的分组信息
|
||||
const uiTypes = documentTypes.map(type => ({
|
||||
...convertToUIDocumentType(type),
|
||||
groups: [] // 保持接口兼容性,但不填充数据
|
||||
}));
|
||||
|
||||
// 转换为UI类型
|
||||
const uiTypes = typesWithGroups.map(convertToUIDocumentType);
|
||||
|
||||
// 获取总数
|
||||
let totalCount = 0;
|
||||
const responseWithHeaders = response as {
|
||||
|
||||
@@ -24,7 +24,6 @@ export interface ApiRuleGroup {
|
||||
code?: string;
|
||||
description?: string;
|
||||
is_enabled: boolean;
|
||||
m_type?: number; // 文档类型:0=合同,1=其他
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
@@ -36,7 +35,6 @@ export interface RuleGroupCreateUpdateDto {
|
||||
pid: string | null; // 父分组ID,如果是一级分组则为null或'0'
|
||||
description?: string;
|
||||
is_enabled: boolean;
|
||||
reviewType?: string; // 审核类型:'contract'=合同,其他为卷宗
|
||||
}
|
||||
|
||||
// 用于替换代码中的 any 类型
|
||||
@@ -289,8 +287,8 @@ export async function getAllRuleGroups(token?: string): Promise<{data: RuleGroup
|
||||
}));
|
||||
}
|
||||
|
||||
// 3. 构建树形结构
|
||||
const parentGroups = allGroups.filter(group => group.pid === '0');
|
||||
// 3. 构建树形结构(pid为NULL表示顶级分组)
|
||||
const parentGroups = allGroups.filter(group => !group.pid || group.pid === '0' || group.pid === null);
|
||||
|
||||
// 4. 为每个父分组添加子分组
|
||||
for (const parent of parentGroups) {
|
||||
@@ -396,8 +394,8 @@ export async function getRuleGroup(id: string, token?: string): Promise<{data: R
|
||||
return { error: '未找到指定分组', status: 404 };
|
||||
}
|
||||
|
||||
// 如果是父分组,获取评查点数量
|
||||
if (group.pid === '0') {
|
||||
// 如果是父分组(顶级分组,pid为NULL或'0'),获取评查点数量
|
||||
if (!group.pid || group.pid === '0' || group.pid === null) {
|
||||
const ruleCountParams: PostgrestParams = {
|
||||
select: 'id',
|
||||
filter: {
|
||||
@@ -436,30 +434,29 @@ export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto, token
|
||||
return { error: '分组名称和编码不能为空', status: 400 };
|
||||
}
|
||||
|
||||
// 确保 pid 是合法值
|
||||
let pidValue: number;
|
||||
// 🆕 确保 pid 是合法值(NULL表示顶级分组)
|
||||
let pidValue: number | null;
|
||||
try {
|
||||
pidValue = groupData.pid ? Number(groupData.pid) : 0;
|
||||
if (isNaN(pidValue)) {
|
||||
return { error: '父分组ID必须是有效的数字', status: 400 };
|
||||
if (!groupData.pid || groupData.pid === '0') {
|
||||
pidValue = null; // 顶级分组
|
||||
} else {
|
||||
pidValue = Number(groupData.pid);
|
||||
if (isNaN(pidValue)) {
|
||||
return { error: '父分组ID必须是有效的数字', status: 400 };
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('父分组ID转换失败:', error);
|
||||
return { error: '父分组ID格式错误', status: 400 };
|
||||
}
|
||||
|
||||
// 根据 reviewType 确定 m_type 的值
|
||||
// contract -> 0, 其他 -> 1
|
||||
const mType = groupData.reviewType === 'contract' ? 0 : 1;
|
||||
|
||||
// 构建API请求数据 - 确保字段类型符合数据库要求
|
||||
const apiGroup: ApiRuleGroup = {
|
||||
pid: pidValue,
|
||||
name: groupData.name.trim(),
|
||||
code: groupData.code.trim(),
|
||||
description: groupData.description || '',
|
||||
is_enabled: groupData.is_enabled,
|
||||
m_type: mType
|
||||
is_enabled: groupData.is_enabled
|
||||
};
|
||||
|
||||
// console.log('创建评查点分组请求数据:', JSON.stringify(apiGroup, null, 2));
|
||||
@@ -489,7 +486,7 @@ export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto, token
|
||||
// 构建返回对象
|
||||
const createdGroup: RuleGroup = {
|
||||
id: apiResponse.id?.toString() || '',
|
||||
pid: apiResponse.pid?.toString() || '0', // 兼容没有 pid 的情况
|
||||
pid: apiResponse.pid?.toString() || '', // 🆕 NULL 转换为空字符串(表示顶级分组)
|
||||
name: apiResponse.name || '',
|
||||
code: apiResponse.code?.toString() || '', // 处理可能的数字类型
|
||||
description: apiResponse.description,
|
||||
@@ -521,24 +518,24 @@ export async function updateRuleGroup(id: string, data: RuleGroupCreateUpdateDto
|
||||
return { error: '分组名称和编码不能为空', status: 400 };
|
||||
}
|
||||
|
||||
// 根据 reviewType 确定 m_type 的值
|
||||
// contract -> 0, 其他 -> 1
|
||||
const mType = data.reviewType === 'contract' ? 0 : 1;
|
||||
|
||||
// 构建符合数据库结构的对象
|
||||
const apiGroup: Partial<ApiRuleGroup> = {
|
||||
name: data.name.trim(),
|
||||
code: data.code.trim(),
|
||||
description: data.description || '',
|
||||
is_enabled: data.is_enabled,
|
||||
m_type: mType // 使用 m_type 而不是 reviewType
|
||||
is_enabled: data.is_enabled
|
||||
};
|
||||
|
||||
// 如果需要更新父分组,添加 pid
|
||||
// 🆕 如果需要更新父分组,添加 pid(NULL表示顶级分组)
|
||||
if (data.pid !== undefined) {
|
||||
const pidValue = data.pid ? Number(data.pid) : 0;
|
||||
if (isNaN(pidValue)) {
|
||||
return { error: '父分组ID必须是有效的数字', status: 400 };
|
||||
let pidValue: number | null;
|
||||
if (!data.pid || data.pid === '0') {
|
||||
pidValue = null; // 顶级分组
|
||||
} else {
|
||||
pidValue = Number(data.pid);
|
||||
if (isNaN(pidValue)) {
|
||||
return { error: '父分组ID必须是有效的数字', status: 400 };
|
||||
}
|
||||
}
|
||||
apiGroup.pid = pidValue;
|
||||
}
|
||||
@@ -590,8 +587,8 @@ export async function deleteRuleGroup(id: string, token?: string): Promise<{succ
|
||||
return { success: false, error: '未找到指定分组' };
|
||||
}
|
||||
|
||||
// 2. 如果是一级分组,需要先删除所有子分组
|
||||
if (group.pid === '0') {
|
||||
// 2. 如果是一级分组(顶级分组,pid为NULL或'0'),需要先删除所有子分组
|
||||
if (!group.pid || group.pid === '0' || group.pid === null) {
|
||||
// 获取所有子分组
|
||||
const childGroupsResponse = await getChildGroups(id, token);
|
||||
if (childGroupsResponse.error) {
|
||||
|
||||
+180
-208
@@ -1,6 +1,7 @@
|
||||
import { postgrestGet, postgrestDelete, postgrestPut, postgrestPost } from '../postgrest-client';
|
||||
import { getDocumentTypes } from '../document-types/document-types';
|
||||
import { formatDate } from '../../utils';
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
@@ -23,25 +24,6 @@ function extractApiData<T>(responseData: unknown): T | null {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询参数
|
||||
*/
|
||||
export interface DocumentSearchParams {
|
||||
name?: string;
|
||||
documentNumber?: string;
|
||||
documentType?: string;
|
||||
status?: string;
|
||||
auditStatus?: string;
|
||||
fileStatus?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
reviewType?: string;
|
||||
userId?: string; // 添加用户ID筛选
|
||||
token?: string; // JWT token
|
||||
}
|
||||
|
||||
/**
|
||||
* 数据库文档结构
|
||||
*/
|
||||
@@ -93,9 +75,22 @@ export interface DocumentUI {
|
||||
updatedAt?: string;
|
||||
pageCount?: number;
|
||||
ocrResult?: unknown;
|
||||
// 结果统计字段
|
||||
pass_count: number | null; // 通过数量
|
||||
warning_count: number | null; // 警告数量
|
||||
error_count: number | null; // 错误数量
|
||||
manual_count: number | null; // 人工审核数量
|
||||
// 消息详情字段
|
||||
warning_messages?: string[]; // 警告消息列表
|
||||
error_messages?: string[]; // 错误消息列表
|
||||
manual_messages?: string[]; // 人工审核消息列表
|
||||
// 版本管理相关字段
|
||||
historyCount?: number; // 历史版本数量(不含当前版本)
|
||||
previousIssues?: number | null; // 上一个版本的问题数量
|
||||
previous_pass_count?: number | null; // 上一版本通过数量
|
||||
previous_warning_count?: number | null; // 上一版本警告数量
|
||||
previous_error_count?: number | null; // 上一版本错误数量
|
||||
previous_manual_count?: number | null; // 上一版本人工数量
|
||||
isExpanded?: boolean; // 是否展开历史版本(前端状态)
|
||||
historyVersions?: DocumentVersionUI[]; // 历史版本列表
|
||||
}
|
||||
@@ -123,6 +118,15 @@ export interface DocumentVersionUI {
|
||||
pageCount?: number;
|
||||
ocrResult?: unknown;
|
||||
versionNumber?: number; // 版本号(v2, v3, v4...)
|
||||
// 结果统计字段
|
||||
pass_count: number | null; // 通过数量
|
||||
warning_count: number | null; // 警告数量
|
||||
error_count: number | null; // 错误数量
|
||||
manual_count: number | null; // 人工审核数量
|
||||
previous_pass_count?: number | null; // 上一版本通过数量
|
||||
previous_warning_count?: number | null; // 上一版本警告数量
|
||||
previous_error_count?: number | null; // 上一版本错误数量
|
||||
previous_manual_count?: number | null; // 上一版本人工数量
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -227,114 +231,6 @@ interface DocumentFromSQL {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文档列表
|
||||
* @param searchParams 搜索参数
|
||||
* @returns 文档列表和总数
|
||||
*/
|
||||
export async function getDocuments(searchParams: DocumentSearchParams = {}): Promise<{
|
||||
data?: { documents: DocumentUI[], total: number };
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
try {
|
||||
// 准备RPC调用参数
|
||||
const {
|
||||
page = 1,
|
||||
pageSize = 10,
|
||||
name,
|
||||
documentNumber,
|
||||
documentType,
|
||||
auditStatus,
|
||||
fileStatus,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
reviewType,
|
||||
userId,
|
||||
token
|
||||
} = searchParams;
|
||||
|
||||
let documentTypes: number[] | undefined;
|
||||
if (documentType) {
|
||||
documentTypes = [parseInt(documentType, 10)];
|
||||
} else if (reviewType) {
|
||||
if (reviewType === 'contract') {
|
||||
documentTypes = [1];
|
||||
} else if (reviewType === 'record') {
|
||||
documentTypes = [2, 3, 155];
|
||||
}
|
||||
}
|
||||
|
||||
// 确保userId必须存在,如果不存在则抛出错误
|
||||
if (!userId) {
|
||||
return { error: '用户身份验证失败,无法获取文档列表', status: 401 };
|
||||
}
|
||||
|
||||
const rpcParams = {
|
||||
search_name: name,
|
||||
search_document_number: documentNumber,
|
||||
search_document_types: documentTypes,
|
||||
search_audit_status: auditStatus !== undefined ? parseInt(auditStatus, 10) : undefined,
|
||||
search_file_status: fileStatus,
|
||||
search_date_from: dateFrom,
|
||||
search_date_to: dateTo,
|
||||
search_user_id: parseInt(userId, 10), // 强制要求传递用户ID
|
||||
};
|
||||
|
||||
// 并行执行获取数据和获取总数的请求
|
||||
const [documentsResponse, countResponse] = await Promise.all([
|
||||
postgrestPost<DocumentFromSQL[], unknown>('rpc/get_documents_with_filters', { ...rpcParams, page, page_size: pageSize }, token),
|
||||
postgrestPost<number, unknown>('rpc/count_documents_with_filters', rpcParams, token)
|
||||
]);
|
||||
|
||||
// 处理获取文档列表的错误
|
||||
if (documentsResponse.error || !documentsResponse.data) {
|
||||
return { error: documentsResponse.error || '获取文档数据失败', status: documentsResponse.status || 500 };
|
||||
}
|
||||
|
||||
// 处理获取总数的错误
|
||||
if (countResponse.error || typeof countResponse.data !== 'number') {
|
||||
// 如果计数失败,可以继续返回数据,但总数可能不准
|
||||
console.error('获取文档总数失败:', countResponse.error);
|
||||
}
|
||||
// console.log('countResponse.data', countResponse.data);
|
||||
|
||||
const totalCount = typeof countResponse.data === 'number' ? countResponse.data : 0;
|
||||
|
||||
// 将SQL返回的数据转换为UI格式
|
||||
const documents: DocumentUI[] = documentsResponse.data.map((doc: DocumentFromSQL) => ({
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
documentNumber: doc.document_number,
|
||||
type: doc.type_id.toString(),
|
||||
typeName: doc.type_name || '未知类型',
|
||||
size: doc.file_size,
|
||||
auditStatus: doc.audit_status ?? 0,
|
||||
fileStatus: doc.status || '',
|
||||
issues: doc.false_count ?? null,
|
||||
uploadTime: formatDate(doc.updated_at),
|
||||
fileType: getFileExtension(doc.name),
|
||||
path: doc.path,
|
||||
isTest: doc.is_test_document,
|
||||
updatedAt: formatDate(doc.updated_at),
|
||||
pageCount: doc.ocr_result?.__meta?.page_count || 0,
|
||||
ocrResult: doc.ocr_result
|
||||
}));
|
||||
|
||||
return {
|
||||
data: {
|
||||
documents,
|
||||
total: totalCount
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取文档列表失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取文档列表失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除文档
|
||||
@@ -597,13 +493,25 @@ export async function updateDocument(id: string, document: Partial<DocumentUI> &
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取文档列表(带版本信息)- 使用 RPC 函数
|
||||
* 获取文档列表(使用新的后端API)
|
||||
* @param searchParams 搜索参数
|
||||
* @returns 文档列表和总数
|
||||
*/
|
||||
export async function getDocumentsWithVersionInfo(searchParams: DocumentSearchParams = {}): Promise<{
|
||||
data?: { documents: DocumentUI[], total: number };
|
||||
export async function getDocumentsListFromAPI(searchParams: {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
name?: string;
|
||||
documentNumber?: string;
|
||||
documentTypeIds?: number[]; // 文档类型ID数组
|
||||
auditStatus?: string;
|
||||
fileStatus?: string;
|
||||
dateFrom?: string;
|
||||
dateTo?: string;
|
||||
token: string; // JWT token (必填)
|
||||
}): Promise<{
|
||||
data?: { documents: DocumentUI[], total: number, page: number, totalPages: number };
|
||||
error?: string;
|
||||
status?: number;
|
||||
}> {
|
||||
@@ -613,104 +521,168 @@ export async function getDocumentsWithVersionInfo(searchParams: DocumentSearchPa
|
||||
pageSize = 10,
|
||||
name,
|
||||
documentNumber,
|
||||
documentType,
|
||||
documentTypeIds,
|
||||
auditStatus,
|
||||
fileStatus,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
reviewType,
|
||||
userId,
|
||||
token
|
||||
} = searchParams;
|
||||
|
||||
// 确保userId必须存在
|
||||
if (!userId) {
|
||||
return { error: '用户身份验证失败,无法获取文档列表', status: 401 };
|
||||
}
|
||||
|
||||
// 处理文档类型
|
||||
let documentTypes: number[] | undefined;
|
||||
if (documentType) {
|
||||
documentTypes = [parseInt(documentType, 10)];
|
||||
} else if (reviewType) {
|
||||
if (reviewType === 'contract') {
|
||||
documentTypes = [1];
|
||||
} else if (reviewType === 'record') {
|
||||
documentTypes = [2, 3, 155];
|
||||
}
|
||||
}
|
||||
|
||||
// 准备RPC调用参数
|
||||
const rpcParams = {
|
||||
p_user_id: parseInt(userId, 10),
|
||||
p_page: page,
|
||||
p_page_size: pageSize,
|
||||
p_search_name: name || null,
|
||||
p_search_document_number: documentNumber || null,
|
||||
p_search_document_types: documentTypes || null,
|
||||
p_search_audit_status: auditStatus !== undefined ? parseInt(auditStatus, 10) : null,
|
||||
p_search_file_status: fileStatus || null,
|
||||
p_search_date_from: dateFrom || null,
|
||||
p_search_date_to: dateTo || null
|
||||
// 构建查询参数
|
||||
const params: Record<string, any> = {
|
||||
page,
|
||||
page_size: pageSize
|
||||
};
|
||||
|
||||
// 并行执行获取数据和获取总数的请求
|
||||
const [documentsResponse, countResponse] = await Promise.all([
|
||||
postgrestPost<any[], unknown>('rpc/documents_get_latest_documents_with_version_info', rpcParams, token),
|
||||
postgrestPost<number, unknown>('rpc/documents_count_latest_documents_with_filters', {
|
||||
p_user_id: rpcParams.p_user_id,
|
||||
p_search_name: rpcParams.p_search_name,
|
||||
p_search_document_number: rpcParams.p_search_document_number,
|
||||
p_search_document_types: rpcParams.p_search_document_types,
|
||||
p_search_audit_status: rpcParams.p_search_audit_status,
|
||||
p_search_file_status: rpcParams.p_search_file_status,
|
||||
p_search_date_from: rpcParams.p_search_date_from,
|
||||
p_search_date_to: rpcParams.p_search_date_to
|
||||
}, token)
|
||||
]);
|
||||
// 添加可选参数
|
||||
if (name) params.name = name;
|
||||
if (documentNumber) params.document_number = documentNumber;
|
||||
if (auditStatus) params.audit_status = parseInt(auditStatus, 10);
|
||||
if (fileStatus) params.status = fileStatus;
|
||||
if (dateFrom) params.start_time = dateFrom;
|
||||
if (dateTo) params.end_time = dateTo;
|
||||
|
||||
// 处理获取文档列表的错误
|
||||
if (documentsResponse.error || !documentsResponse.data) {
|
||||
return { error: documentsResponse.error || '获取文档数据失败', status: documentsResponse.status || 500 };
|
||||
// 处理文档类型ID数组 - 传递为数组或单个值
|
||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||
params.type_id = documentTypeIds;
|
||||
}
|
||||
|
||||
// 处理获取总数的错误
|
||||
if (countResponse.error || typeof countResponse.data !== 'number') {
|
||||
console.error('获取文档总数失败:', countResponse.error);
|
||||
}
|
||||
console.log('📤 [getDocumentsListFromAPI] 请求参数:', params);
|
||||
|
||||
const totalCount = typeof countResponse.data === 'number' ? countResponse.data : 0;
|
||||
// 调用后端API
|
||||
const axios = await import('axios').then(m => m.default);
|
||||
const response = await axios.get(`${API_BASE_URL}/admin/versions/documents-list`, {
|
||||
params,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// 将RPC返回的数据转换为UI格式
|
||||
const documents: DocumentUI[] = documentsResponse.data.map((doc: any) => ({
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
documentNumber: doc.document_number,
|
||||
type: doc.type_id.toString(),
|
||||
typeName: doc.type_name || '未知类型',
|
||||
size: doc.file_size,
|
||||
auditStatus: doc.audit_status ?? 0,
|
||||
fileStatus: doc.status || '',
|
||||
issues: doc.false_count ?? null,
|
||||
uploadTime: formatDate(doc.updated_at),
|
||||
fileType: getFileExtension(doc.name),
|
||||
path: doc.path,
|
||||
isTest: doc.is_test_document,
|
||||
updatedAt: formatDate(doc.updated_at),
|
||||
pageCount: doc.ocr_result?.__meta?.page_count || 0,
|
||||
ocrResult: doc.ocr_result,
|
||||
historyCount: doc.history_count || 0,
|
||||
previousIssues: doc.previous_issues
|
||||
}));
|
||||
const data = response.data;
|
||||
const backendDocuments = data.documents || [];
|
||||
const totalCount = data.total || 0;
|
||||
const totalPages = data.total_pages || 0;
|
||||
|
||||
console.log(`📥 [getDocumentsListFromAPI] 获取到 ${backendDocuments.length} 个文档,总数: ${totalCount}`);
|
||||
|
||||
// 转换后端数据为前端 DocumentUI 格式
|
||||
const convertedDocuments: DocumentUI[] = backendDocuments.map((doc: any) => {
|
||||
// 转换历史版本数据
|
||||
const historyVersions: DocumentVersionUI[] = (doc.history_versions || []).map((hv: any, index: number) => {
|
||||
// 计算与下一个版本(更早的版本)的问题数量差异
|
||||
let issuesDiff: number | undefined;
|
||||
let issuesDiffType: 'increase' | 'decrease' | 'same' | undefined;
|
||||
|
||||
if (index < doc.history_versions.length - 1) {
|
||||
const olderDoc = doc.history_versions[index + 1];
|
||||
if (hv.issue_count != null && olderDoc.issue_count != null) {
|
||||
const diff = hv.issue_count - olderDoc.issue_count;
|
||||
issuesDiff = Math.abs(diff);
|
||||
if (diff > 0) {
|
||||
issuesDiffType = 'increase';
|
||||
} else if (diff < 0) {
|
||||
issuesDiffType = 'decrease';
|
||||
} else {
|
||||
issuesDiffType = 'same';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取前一个版本的统计数据(如果存在)
|
||||
const prevVersion = index < doc.history_versions.length - 1 ? doc.history_versions[index + 1] : null;
|
||||
|
||||
return {
|
||||
id: hv.id,
|
||||
name: hv.name || doc.name,
|
||||
documentNumber: hv.document_number || doc.document_number || '',
|
||||
type: hv.type_id?.toString() || doc.type_id?.toString() || '',
|
||||
typeName: hv.type_name || doc.type_name || '未知类型',
|
||||
size: hv.file_size || 0,
|
||||
auditStatus: hv.audit_status ?? 0,
|
||||
fileStatus: hv.status || 'Processed',
|
||||
issues: hv.issue_count ?? null,
|
||||
issuesDiff,
|
||||
issuesDiffType,
|
||||
uploadTime: formatDate(hv.created_at),
|
||||
fileType: getFileExtension(hv.name || doc.name),
|
||||
path: hv.path || '',
|
||||
isTest: hv.is_test_document || false,
|
||||
updatedAt: formatDate(hv.updated_at || hv.created_at),
|
||||
pageCount: hv.ocr_result?.__meta?.page_count || 0,
|
||||
ocrResult: hv.ocr_result,
|
||||
versionNumber: hv.version_number,
|
||||
// 结果统计字段
|
||||
pass_count: hv.pass_count ?? null,
|
||||
warning_count: hv.warning_count ?? null,
|
||||
error_count: hv.error_count ?? null,
|
||||
manual_count: hv.manual_count ?? null,
|
||||
// 前一版本统计(用于差异对比)
|
||||
previous_pass_count: prevVersion?.pass_count ?? null,
|
||||
previous_warning_count: prevVersion?.warning_count ?? null,
|
||||
previous_error_count: prevVersion?.error_count ?? null,
|
||||
previous_manual_count: prevVersion?.manual_count ?? null
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
id: doc.id,
|
||||
name: doc.name,
|
||||
documentNumber: doc.document_number || '',
|
||||
type: doc.type_id?.toString() || '',
|
||||
typeName: doc.type_name || '未知类型',
|
||||
size: doc.file_size || 0,
|
||||
auditStatus: doc.audit_status ?? 0,
|
||||
fileStatus: doc.status || '',
|
||||
issues: doc.issue_count ?? null,
|
||||
uploadTime: formatDate(doc.upload_time || doc.created_at),
|
||||
fileType: getFileExtension(doc.name),
|
||||
path: doc.path || '',
|
||||
isTest: doc.is_test_document || false,
|
||||
updatedAt: formatDate(doc.updated_at || doc.created_at),
|
||||
pageCount: doc.ocr_result?.__meta?.page_count || 0,
|
||||
ocrResult: doc.ocr_result,
|
||||
// 结果统计字段
|
||||
pass_count: doc.pass_count ?? null,
|
||||
warning_count: doc.warning_count ?? null,
|
||||
error_count: doc.error_count ?? null,
|
||||
manual_count: doc.manual_count ?? null,
|
||||
// 消息详情字段
|
||||
warning_messages: doc.warning_messages || [],
|
||||
error_messages: doc.error_messages || [],
|
||||
manual_messages: doc.manual_messages || [],
|
||||
// 版本管理字段
|
||||
historyCount: (doc.total_versions || 1) - 1, // 总版本数 - 1 = 历史版本数
|
||||
previousIssues: doc.history_versions?.[0]?.issue_count ?? null,
|
||||
previous_pass_count: doc.history_versions?.[0]?.pass_count ?? null,
|
||||
previous_warning_count: doc.history_versions?.[0]?.warning_count ?? null,
|
||||
previous_error_count: doc.history_versions?.[0]?.error_count ?? null,
|
||||
previous_manual_count: doc.history_versions?.[0]?.manual_count ?? null,
|
||||
historyVersions: historyVersions.length > 0 ? historyVersions : undefined
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
data: {
|
||||
documents,
|
||||
total: totalCount
|
||||
documents: convertedDocuments,
|
||||
total: totalCount,
|
||||
page: data.page || page,
|
||||
totalPages
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取文档列表失败:', error);
|
||||
console.error('❌ [getDocumentsListFromAPI] 获取文档列表失败:', error);
|
||||
|
||||
// 处理axios错误
|
||||
if (error && typeof error === 'object' && 'response' in error) {
|
||||
const axiosError = error as { response?: { data?: any; status?: number; statusText?: string } };
|
||||
return {
|
||||
error: axiosError.response?.data?.message || axiosError.response?.statusText || '获取文档列表失败',
|
||||
status: axiosError.response?.status || 500
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取文档列表失败',
|
||||
status: 500
|
||||
|
||||
@@ -17,6 +17,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
const [isLoadingRoutes, setIsLoadingRoutes] = useState<boolean>(true); // 路由加载状态
|
||||
const [isMobile, setIsMobile] = useState<boolean>(false); // 移动端检测
|
||||
const [selectedModuleName, setSelectedModuleName] = useState<string>(''); // 当前选中的模块名称
|
||||
const [selectedModulePicPath, setSelectedModulePicPath] = useState<string>(''); // 当前选中的模块图片路径
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 移动端检测
|
||||
@@ -89,17 +90,24 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
fetchUserRoutes();
|
||||
}, [userRole, frontendJWT, navigate]);
|
||||
|
||||
// 从 sessionStorage 读取当前选中的模块名称
|
||||
// 从 sessionStorage 读取当前选中的模块名称和图片路径
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const moduleName = sessionStorage.getItem('selectedModuleName');
|
||||
const modulePicPath = sessionStorage.getItem('selectedModulePicPath');
|
||||
|
||||
if (moduleName) {
|
||||
setSelectedModuleName(moduleName);
|
||||
console.log('📌 [Sidebar] 当前选中模块:', moduleName);
|
||||
}
|
||||
|
||||
if (modulePicPath) {
|
||||
setSelectedModulePicPath(modulePicPath);
|
||||
console.log('🖼️ [Sidebar] 模块图片路径:', modulePicPath);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [Sidebar] 读取 selectedModuleName 失败:', error);
|
||||
console.error('❌ [Sidebar] 读取 sessionStorage 失败:', error);
|
||||
}
|
||||
}
|
||||
}, [location.pathname]); // 路由变化时重新读取
|
||||
@@ -260,6 +268,24 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 显示入口模块的名称 */}
|
||||
{selectedModuleName && (
|
||||
<div className="py-3 px-4 border-b border-gray-100">
|
||||
<div className={`flex items-center ${collapsed ? 'justify-center' : ''}`}>
|
||||
{selectedModulePicPath && (
|
||||
<img
|
||||
src={selectedModulePicPath}
|
||||
alt={selectedModuleName}
|
||||
className={`${collapsed ? 'w-8 h-8' : 'w-6 h-6 mr-3'}`}
|
||||
/>
|
||||
)}
|
||||
{!collapsed && (
|
||||
<span className="text-base font-medium text-green-700">{selectedModuleName}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="py-4 px-[10px] flex-1 overflow-y-auto sidebar-scroll-area">
|
||||
{isLoadingRoutes ? (
|
||||
// 加载中状态显示,保留菜单布局结构
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import { useSearchParams, useNavigate, useLoaderData, useRouteLoaderData } from "@remix-run/react";
|
||||
import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
@@ -138,9 +138,14 @@ export default function DocumentTypesList() {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
|
||||
// 获取加载器数据
|
||||
const { types, total, error, ruleTypes, frontendJWT } = useLoaderData<LoaderData>();
|
||||
|
||||
// 获取用户角色并判断权限
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
|
||||
// 状态管理
|
||||
const [ruleGroups, setRuleGroups] = useState<RuleGroup[]>([]);
|
||||
@@ -369,15 +374,17 @@ export default function DocumentTypesList() {
|
||||
className="operation-btn text-primary"
|
||||
onClick={() => handleEdit(record.id)}
|
||||
>
|
||||
<i className="ri-edit-line"></i> 编辑
|
||||
</button>
|
||||
<button
|
||||
className="operation-btn text-error !hidden"
|
||||
onClick={() => handleDelete(record.id)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i> 删除
|
||||
<i className="ri-edit-line"></i> {hasEditPermission ? '编辑' : '查看'}
|
||||
</button>
|
||||
{hasEditPermission && (
|
||||
<button
|
||||
className="operation-btn text-error !hidden"
|
||||
onClick={() => handleDelete(record.id)}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i> 删除
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -395,13 +402,15 @@ export default function DocumentTypesList() {
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">文档类型管理</h2>
|
||||
<div>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
onClick={() => navigate("/document-types/new")}
|
||||
>
|
||||
新增文档类型
|
||||
</Button>
|
||||
{hasEditPermission && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
onClick={() => navigate("/document-types/new")}
|
||||
>
|
||||
新增文档类型
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { Form, useActionData, useLoaderData, useNavigate, useSearchParams } from "@remix-run/react";
|
||||
import { Form, useActionData, useLoaderData, useNavigate, useSearchParams, useRouteLoaderData } from "@remix-run/react";
|
||||
import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
@@ -230,9 +230,15 @@ export default function DocumentTypeNew() {
|
||||
evaluationTemplates,
|
||||
summaryTemplates
|
||||
} = useLoaderData<typeof loader>();
|
||||
|
||||
|
||||
const actionData = useActionData<ActionData>();
|
||||
|
||||
|
||||
// 获取用户角色并判断权限
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
const isReadOnly = !hasEditPermission;
|
||||
|
||||
// 状态管理
|
||||
const [formData, setFormData] = useState({
|
||||
id: documentType?.id || "",
|
||||
@@ -446,23 +452,25 @@ export default function DocumentTypeNew() {
|
||||
<div className="document-type-new-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="page-header">
|
||||
<h2 className="page-title">{isEditMode ? "编辑文档类型" : "新增文档类型"}</h2>
|
||||
<h2 className="page-title">{isEditMode ? (isReadOnly ? "查看文档类型" : "编辑文档类型") : "新增文档类型"}</h2>
|
||||
<div>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => navigate("/document-types")}
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-arrow-left-line"
|
||||
onClick={() => navigate("/document-types")}
|
||||
className="mr-2"
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-save-line"
|
||||
form="type-form"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
{!isReadOnly && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-save-line"
|
||||
form="type-form"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -486,15 +494,16 @@ export default function DocumentTypeNew() {
|
||||
<label htmlFor="type-name" className="form-label">
|
||||
文档类型名称 <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="type-name"
|
||||
name="name"
|
||||
<input
|
||||
type="text"
|
||||
id="type-name"
|
||||
name="name"
|
||||
className={`form-input ${touchedFields.name && formErrors?.name ? 'input-error' : ''}`}
|
||||
placeholder="请输入文档类型名称"
|
||||
placeholder="请输入文档类型名称"
|
||||
value={formData.name}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
required
|
||||
readOnly={isReadOnly}
|
||||
/>
|
||||
{touchedFields.name && formErrors?.name && (
|
||||
<div className="error-message">{formErrors.name}</div>
|
||||
@@ -505,14 +514,15 @@ export default function DocumentTypeNew() {
|
||||
{/* 类型描述 */}
|
||||
<div className="form-group">
|
||||
<label htmlFor="type-description" className="form-label">类型描述</label>
|
||||
<textarea
|
||||
id="type-description"
|
||||
name="description"
|
||||
className="form-textarea"
|
||||
placeholder="请输入类型描述,介绍此类型文档的用途和特点"
|
||||
<textarea
|
||||
id="type-description"
|
||||
name="description"
|
||||
className="form-textarea"
|
||||
placeholder="请输入类型描述,介绍此类型文档的用途和特点"
|
||||
value={formData.description}
|
||||
onChange={handleInputChange}
|
||||
rows={3}
|
||||
readOnly={isReadOnly}
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@@ -521,12 +531,13 @@ export default function DocumentTypeNew() {
|
||||
{/* llm抽取提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="llm-extraction-template" className="form-label">llm抽取提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="llm-extraction-template"
|
||||
name="llm_extraction_template"
|
||||
<select
|
||||
id="llm-extraction-template"
|
||||
name="llm_extraction_template"
|
||||
className={`form-select ${touchedFields.llmExtractionTemplate && formErrors?.llmExtractionTemplate ? 'input-error' : ''}`}
|
||||
value={formData.llmExtractionTemplateId}
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">请选择llm抽取提示词模板</option>
|
||||
{llmExtractionTemplates.map((template: PromptTemplateUI) => (
|
||||
@@ -544,12 +555,13 @@ export default function DocumentTypeNew() {
|
||||
{/* vlm抽取提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="vlm-extraction-template" className="form-label">vlm抽取提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="vlm-extraction-template"
|
||||
name="vlm_extraction_template"
|
||||
<select
|
||||
id="vlm-extraction-template"
|
||||
name="vlm_extraction_template"
|
||||
className={`form-select ${touchedFields.vlmExtractionTemplate && formErrors?.vlmExtractionTemplate ? 'input-error' : ''}`}
|
||||
value={formData.vlmExtractionTemplateId}
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">请选择vlm抽取提示词模板</option>
|
||||
{vlmExtractionTemplates.map((template: PromptTemplateUI) => (
|
||||
@@ -567,12 +579,13 @@ export default function DocumentTypeNew() {
|
||||
{/* 评查提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="evaluation-template" className="form-label">评查提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="evaluation-template"
|
||||
name="evaluation_template"
|
||||
<select
|
||||
id="evaluation-template"
|
||||
name="evaluation_template"
|
||||
className={`form-select ${touchedFields.evaluationTemplate && formErrors?.evaluationTemplate ? 'input-error' : ''}`}
|
||||
value={formData.evaluationTemplateId}
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">请选择评查提示词模板</option>
|
||||
{evaluationTemplates.map((template: PromptTemplateUI) => (
|
||||
@@ -590,12 +603,13 @@ export default function DocumentTypeNew() {
|
||||
{/* 总结提示词模板 */}
|
||||
<div className="w-full">
|
||||
<label htmlFor="summary-template" className="form-label">总结提示词模板 <span className="text-red-500">*</span></label>
|
||||
<select
|
||||
id="summary-template"
|
||||
name="summary_template"
|
||||
<select
|
||||
id="summary-template"
|
||||
name="summary_template"
|
||||
className={`form-select ${touchedFields.summaryTemplate && formErrors?.summaryTemplate ? 'input-error' : ''}`}
|
||||
value={formData.summaryTemplateId}
|
||||
onChange={handleInputChange}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<option value="">请选择总结提示词模板</option>
|
||||
{summaryTemplates.map((template: PromptTemplateUI) => (
|
||||
@@ -625,7 +639,7 @@ export default function DocumentTypeNew() {
|
||||
{ruleGroups.map((group: RuleGroup) => (
|
||||
<React.Fragment key={group.id}>
|
||||
{/* 父分组 */}
|
||||
<div
|
||||
<div
|
||||
className={`checkbox-item parent-checkbox-item ${formData.selectedGroups.includes(group.id) ? 'checked' : ''}`}
|
||||
>
|
||||
<button
|
||||
@@ -633,17 +647,19 @@ export default function DocumentTypeNew() {
|
||||
className="expand-icon"
|
||||
onClick={(e) => handleGroupExpand(group.id, e)}
|
||||
aria-label={`${expandedGroups[group.id] ? '收起' : '展开'}${group.name}分组`}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<i className={`ri-arrow-${expandedGroups[group.id] ? 'down' : 'right'}-s-line text-primary`}></i>
|
||||
</button>
|
||||
<input
|
||||
type="radio"
|
||||
id={`group-${group.id}`}
|
||||
name="checkpoint_group_ids"
|
||||
<input
|
||||
type="radio"
|
||||
id={`group-${group.id}`}
|
||||
name="checkpoint_group_ids"
|
||||
value={group.id}
|
||||
checked={formData.selectedGroups.includes(group.id)}
|
||||
onChange={(e) => handleGroupCheckChange(group.id, e.target.checked)}
|
||||
className="radio-input"
|
||||
className="radio-input"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`group-${group.id}`}
|
||||
@@ -657,18 +673,19 @@ export default function DocumentTypeNew() {
|
||||
{/* 子分组 */}
|
||||
{group.children && group.children.length > 0 && expandedGroups[group.id] && (
|
||||
group.children.map((child: RuleGroup) => (
|
||||
<div
|
||||
key={child.id}
|
||||
<div
|
||||
key={child.id}
|
||||
className={`checkbox-item child-checkbox-item ${formData.selectedGroups.includes(child.id) ? 'checked' : ''}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
id={`group-${child.id}`}
|
||||
name="checkpoint_group_ids"
|
||||
<input
|
||||
type="radio"
|
||||
id={`group-${child.id}`}
|
||||
name="checkpoint_group_ids"
|
||||
value={child.id}
|
||||
checked={formData.selectedGroups.includes(child.id)}
|
||||
onChange={(e) => handleGroupCheckChange(child.id, e.target.checked)}
|
||||
className="radio-input"
|
||||
className="radio-input"
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`group-${child.id}`}
|
||||
|
||||
+184
-180
@@ -11,8 +11,9 @@ import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/comp
|
||||
import { TableRowSkeleton, LoadingIndicator, NumberSkeleton } from '~/components/ui/SkeletonScreen';
|
||||
import documentsIndexStyles from "~/styles/pages/documents_index.css?url";
|
||||
import documentVersionStyles from "~/styles/components/document-version.css?url";
|
||||
import { getDocuments, getDocumentsWithVersionInfo, getDocumentHistory, deleteDocument, type DocumentUI, type DocumentVersionUI } from "~/api/files/documents";
|
||||
import { IssuesDiff } from "~/components/ui/IssuesDiff";
|
||||
import { deleteDocument, getDocumentsListFromAPI, type DocumentUI, type DocumentVersionUI } from "~/api/files/documents";
|
||||
// import { IssuesDiff } from "~/components/ui/IssuesDiff";
|
||||
import { ResultStats } from "~/components/ui/ResultStats";
|
||||
import { getDocumentTypes } from "~/api/document-types/document-types";
|
||||
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
||||
import { appendContractAttachments, uploadContractTemplate } from "~/api/files/files-upload";
|
||||
@@ -184,8 +185,8 @@ export default function DocumentsIndex() {
|
||||
const fetcher = useFetcher<ActionResponse>();
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 存储从 sessionStorage 获取的 reviewType
|
||||
const [reviewType, setReviewType] = useState<string | null>(null);
|
||||
// 存储从 sessionStorage 获取的 documentTypeIds
|
||||
const [documentTypeIds, setDocumentTypeIds] = useState<number[] | null>(null);
|
||||
|
||||
// 添加页面加载状态管理
|
||||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||||
@@ -260,96 +261,100 @@ export default function DocumentsIndex() {
|
||||
const currentPage = parseInt(searchParams.get("page") || "1", 10);
|
||||
const pageSize = parseInt(searchParams.get("pageSize") || "10", 10);
|
||||
|
||||
|
||||
// 客户端数据请求
|
||||
const fetchData = useCallback(async (storedReviewType: string) => {
|
||||
const fetchData = useCallback(async (typeIds: number[]) => {
|
||||
setIsLoadingData(true);
|
||||
loadingBarService.show();
|
||||
|
||||
try {
|
||||
// 从 localStorage 获取用户ID(与 token 管理保持一致)
|
||||
const userId = getUserId();
|
||||
if (!userId) {
|
||||
// 从 loaderData 获取 JWT Token
|
||||
const jwtToken = loaderData.frontendJWT;
|
||||
if (!jwtToken) {
|
||||
toastService.error('用户身份验证失败,无法获取文档列表');
|
||||
setIsLoadingData(false);
|
||||
loadingBarService.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建搜索参数(token 由 axios 拦截器自动从 localStorage 获取)
|
||||
const searchParams = {
|
||||
console.log('🔑 [fetchData] 文档类型IDs:', typeIds);
|
||||
|
||||
// 调用新的 API 函数
|
||||
const result = await getDocumentsListFromAPI({
|
||||
page: currentPage,
|
||||
pageSize: pageSize,
|
||||
name: search || undefined,
|
||||
documentNumber: documentNumber || undefined,
|
||||
documentType: documentType || undefined,
|
||||
documentTypeIds: documentType ? [parseInt(documentType, 10)] : typeIds, // 如果有单独选择的类型,优先使用
|
||||
auditStatus: auditStatus || undefined,
|
||||
fileStatus: fileStatus || undefined,
|
||||
dateFrom: dateFrom || undefined,
|
||||
dateTo: dateTo || undefined,
|
||||
reviewType: storedReviewType || undefined,
|
||||
userId: userId, // 添加用户ID筛选
|
||||
page: currentPage,
|
||||
pageSize
|
||||
};
|
||||
token: jwtToken
|
||||
});
|
||||
|
||||
// 获取文档列表(带版本信息)
|
||||
const documentsResponse = await getDocumentsWithVersionInfo(searchParams);
|
||||
if (documentsResponse.error) {
|
||||
throw new Error(documentsResponse.error);
|
||||
if (result.error) {
|
||||
toastService.error('获取文档列表失败: ' + result.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 🔑 从 sessionStorage 读取文档类型 IDs
|
||||
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
|
||||
const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : undefined;
|
||||
if (!result.data) {
|
||||
toastService.error('获取文档列表失败: 返回数据为空');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取经过过滤的文档类型列表(token 由 axios 拦截器自动获取)
|
||||
// 更新状态
|
||||
setDocuments(result.data.documents);
|
||||
setTotal(result.data.total);
|
||||
|
||||
// 获取经过过滤的文档类型列表
|
||||
const filteredTypesResponse = await getDocumentTypes({
|
||||
pageSize: 500,
|
||||
documentTypeIds: documentTypeIds // 使用动态的文档类型 IDs
|
||||
});
|
||||
documentTypeIds: typeIds
|
||||
}, jwtToken);
|
||||
const filteredDocumentTypes = filteredTypesResponse.data?.types || [];
|
||||
const filteredOptions = filteredDocumentTypes.map(type => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
}));
|
||||
|
||||
// console.log('文档列表',documentsResponse)
|
||||
|
||||
// 更新状态
|
||||
setDocuments(documentsResponse.data?.documents || []);
|
||||
setTotal(documentsResponse.data?.total || 0);
|
||||
setFilteredDocumentTypeOptions(filteredOptions);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取文档列表失败:', error);
|
||||
console.error('❌ [fetchData] 获取文档列表失败:', error);
|
||||
toastService.error('获取文档列表失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||||
} finally {
|
||||
setIsLoadingData(false);
|
||||
loadingBarService.hide();
|
||||
}
|
||||
}, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize, getUserId]);
|
||||
}, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize, loaderData.frontendJWT]);
|
||||
|
||||
// 在组件挂载时从 sessionStorage 获取 reviewType 并加载数据
|
||||
// 在组件挂载时从 sessionStorage 获取 documentTypeIds 并加载数据
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedReviewType = sessionStorage.getItem('reviewType');
|
||||
setReviewType(storedReviewType);
|
||||
|
||||
// 如果有 reviewType,则加载数据
|
||||
if (storedReviewType) {
|
||||
fetchData(storedReviewType);
|
||||
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
|
||||
if (typeIdsStr) {
|
||||
const typeIds = JSON.parse(typeIdsStr) as number[];
|
||||
console.log('📋 [useEffect] 从 sessionStorage 获取文档类型IDs:', typeIds);
|
||||
setDocumentTypeIds(typeIds);
|
||||
|
||||
// 加载数据
|
||||
fetchData(typeIds);
|
||||
} else {
|
||||
console.warn('⚠️ [useEffect] sessionStorage 中没有 documentTypeIds');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取 sessionStorage 中的 reviewType 失败:', error);
|
||||
console.error('❌ [useEffect] 获取 sessionStorage 中的 documentTypeIds 失败:', error);
|
||||
}
|
||||
}, [fetchData]);
|
||||
|
||||
// 监听 URL 参数变化,重新获取数据
|
||||
useEffect(() => {
|
||||
if (reviewType) {
|
||||
fetchData(reviewType);
|
||||
if (documentTypeIds) {
|
||||
fetchData(documentTypeIds);
|
||||
}
|
||||
}, [searchParams, fetchData, reviewType]);
|
||||
}, [searchParams, fetchData, documentTypeIds]);
|
||||
|
||||
// 使用并更新缓存数据
|
||||
useEffect(() => {
|
||||
@@ -380,15 +385,15 @@ export default function DocumentsIndex() {
|
||||
if (fetcher.data.result) {
|
||||
toastService.success(fetcher.data.message);
|
||||
// 删除成功后重新加载数据
|
||||
if (reviewType) {
|
||||
fetchData(reviewType);
|
||||
if (documentTypeIds) {
|
||||
fetchData(documentTypeIds);
|
||||
}
|
||||
} else if (fetcher.data.message) {
|
||||
toastService.error(fetcher.data.message);
|
||||
// 删除失败只显示错误信息,不刷新数据
|
||||
}
|
||||
}
|
||||
}, [fetcher.data, fetcher.state, fetchData, reviewType, isDeleting]);
|
||||
}, [fetcher.data, fetcher.state, fetchData, documentTypeIds, isDeleting]);
|
||||
|
||||
// 分页处理函数
|
||||
const handlePageChange = (page: number) => {
|
||||
@@ -649,20 +654,29 @@ export default function DocumentsIndex() {
|
||||
|
||||
// 导出列表
|
||||
const handleExport = async () => {
|
||||
// 如果没有文档,显示提示信息
|
||||
if (documents.length === 0) {
|
||||
// alert('当前页面没有文档可供导出');
|
||||
toastService.error('当前页面没有文档可供导出');
|
||||
// 检查是否选中了文档
|
||||
if (selectedRowKeys.length === 0) {
|
||||
toastService.error('请至少选择一个文档');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 过滤出选中的文档(仅主文档,不包括历史版本)
|
||||
const selectedDocuments = documents.filter((doc: DocumentUI) =>
|
||||
selectedRowKeys.includes(doc.id.toString())
|
||||
);
|
||||
|
||||
if (selectedDocuments.length === 0) {
|
||||
toastService.error('没有可导出的文档');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建一个ZIP文件
|
||||
const JSZip = await import('jszip').then(module => module.default);
|
||||
const zip = new JSZip();
|
||||
|
||||
// 准备所有下载任务
|
||||
const downloadTasks = documents.map(async (doc: DocumentUI) => {
|
||||
|
||||
// 准备所有下载任务(仅选中的文档)
|
||||
const downloadTasks = selectedDocuments.map(async (doc: DocumentUI) => {
|
||||
try {
|
||||
if (!doc.path) {
|
||||
console.warn(`文档 ${doc.name} 没有有效的路径`);
|
||||
@@ -840,16 +854,16 @@ export default function DocumentsIndex() {
|
||||
}
|
||||
|
||||
toastService.success('附件追加成功!');
|
||||
|
||||
|
||||
// 重置状态
|
||||
setAttachmentFiles([]);
|
||||
setAttachmentRemark("");
|
||||
setShowAttachmentUpload(false);
|
||||
setSelectedDocumentId(null);
|
||||
|
||||
|
||||
// 刷新文档列表
|
||||
if (reviewType) {
|
||||
fetchData(reviewType);
|
||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||
fetchData(documentTypeIds);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -911,15 +925,15 @@ export default function DocumentsIndex() {
|
||||
}
|
||||
|
||||
toastService.success('合同模板上传成功!');
|
||||
|
||||
|
||||
// 重置状态
|
||||
setTemplateFile(null);
|
||||
setShowTemplateUpload(false);
|
||||
setSelectedDocumentId(null);
|
||||
|
||||
|
||||
// 刷新文档列表
|
||||
if (reviewType) {
|
||||
fetchData(reviewType);
|
||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||
fetchData(documentTypeIds);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -933,79 +947,29 @@ export default function DocumentsIndex() {
|
||||
// 展开/折叠历史版本
|
||||
const handleToggleExpand = async (doc: DocumentUI) => {
|
||||
const newExpanded = new Set(expandedRows);
|
||||
const newLoading = new Set(loadingHistory);
|
||||
|
||||
if (expandedRows.has(doc.id)) {
|
||||
// 折叠:移除展开状态
|
||||
newExpanded.delete(doc.id);
|
||||
setExpandedRows(newExpanded);
|
||||
|
||||
// 清空历史版本数据
|
||||
// 更新展开状态
|
||||
setDocuments(prevDocs =>
|
||||
prevDocs.map(d =>
|
||||
d.id === doc.id ? { ...d, historyVersions: undefined, isExpanded: false } : d
|
||||
d.id === doc.id ? { ...d, isExpanded: false } : d
|
||||
)
|
||||
);
|
||||
} else {
|
||||
// 展开:加载历史版本
|
||||
// 展开:显示历史版本
|
||||
newExpanded.add(doc.id);
|
||||
setExpandedRows(newExpanded);
|
||||
|
||||
// 如果还没有加载历史版本,则加载
|
||||
if (!doc.historyVersions && doc.historyCount && doc.historyCount > 0) {
|
||||
newLoading.add(doc.id);
|
||||
setLoadingHistory(newLoading);
|
||||
|
||||
try {
|
||||
// 从 localStorage 获取用户ID(与 token 管理保持一致)
|
||||
const userId = getUserId();
|
||||
if (!userId) {
|
||||
toastService.error('用户身份验证失败');
|
||||
newExpanded.delete(doc.id);
|
||||
setExpandedRows(newExpanded);
|
||||
newLoading.delete(doc.id);
|
||||
setLoadingHistory(newLoading);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await getDocumentHistory(
|
||||
doc.name,
|
||||
userId,
|
||||
doc.id
|
||||
);
|
||||
|
||||
if (result.data) {
|
||||
// 更新文档的历史版本数据
|
||||
setDocuments(prevDocs =>
|
||||
prevDocs.map(d =>
|
||||
d.id === doc.id
|
||||
? { ...d, historyVersions: result.data, isExpanded: true }
|
||||
: d
|
||||
)
|
||||
);
|
||||
} else if (result.error) {
|
||||
toastService.error(`加载历史版本失败: ${result.error}`);
|
||||
// 加载失败时取消展开
|
||||
newExpanded.delete(doc.id);
|
||||
setExpandedRows(newExpanded);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史版本失败:', error);
|
||||
toastService.error('加载历史版本失败');
|
||||
newExpanded.delete(doc.id);
|
||||
setExpandedRows(newExpanded);
|
||||
} finally {
|
||||
newLoading.delete(doc.id);
|
||||
setLoadingHistory(newLoading);
|
||||
}
|
||||
} else {
|
||||
// 已经加载过,只更新展开状态
|
||||
setDocuments(prevDocs =>
|
||||
prevDocs.map(d =>
|
||||
d.id === doc.id ? { ...d, isExpanded: true } : d
|
||||
)
|
||||
);
|
||||
}
|
||||
// 更新展开状态(历史版本数据已经在主数据中了)
|
||||
setDocuments(prevDocs =>
|
||||
prevDocs.map(d =>
|
||||
d.id === doc.id ? { ...d, isExpanded: true } : d
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1013,26 +977,35 @@ export default function DocumentsIndex() {
|
||||
const renderHistoryRow = (historyDoc: DocumentVersionUI, parentDoc: DocumentUI) => {
|
||||
return (
|
||||
<tr key={`history-${historyDoc.id}`} className="history-row">
|
||||
<td className="align-middle">
|
||||
<td className="align-middle px-4 py-3" style={{ width: '50px' }}>
|
||||
<input type="checkbox" disabled style={{ visibility: 'hidden' }} />
|
||||
</td>
|
||||
<td className="align-middle">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<td className="align-middle px-4 py-3" style={{ width: '25%' }}>
|
||||
<div className="flex items-center gap-3">
|
||||
<i className="ri-history-line text-gray-400 text-lg"></i>
|
||||
<span className="history-version-label">
|
||||
<span className="history-version-label text-sm">
|
||||
v{historyDoc.versionNumber} 版本
|
||||
</span>
|
||||
<span className="history-version-label text-sm">
|
||||
{historyDoc.documentNumber}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="text-xs text-gray-600 px-4">{historyDoc.documentNumber}</td>
|
||||
<td className="text-xs text-gray-600 px-4">{formatFileSize(historyDoc.size)}</td>
|
||||
<td className="px-4">
|
||||
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-green-100 text-green-800`}>
|
||||
<i className="ri-check-line mr-1"></i>
|
||||
<span>{fileProcessingStatusOptions.find(s => s.value === historyDoc.fileStatus)?.label || '已完成'}</span>
|
||||
</div>
|
||||
<td className="text-xs text-gray-600 px-4 py-3" style={{ width: '8%' }}>{formatFileSize(historyDoc.size)}</td>
|
||||
<td className="px-4 py-3" style={{ width: '8%' }}>
|
||||
{(() => {
|
||||
const fileStatus = historyDoc.fileStatus || "-";
|
||||
const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) || fileProcessingStatusOptions[0];
|
||||
const isSpinning = fileStatus !== "Processed" && fileStatus !== "Failed";
|
||||
return (
|
||||
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${status.color}-100 text-${status.color}-800`}>
|
||||
<i className={`${status.icon} ${isSpinning ? "animate-spin" : ""} mr-1`}></i>
|
||||
<span>{status.label}</span>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</td>
|
||||
<td className="px-4">
|
||||
<td className="px-4 py-3" style={{ width: '8%' }}>
|
||||
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs ${
|
||||
historyDoc.auditStatus === 1 ? 'bg-green-100 text-green-800' :
|
||||
historyDoc.auditStatus === -1 ? 'bg-red-100 text-red-800' :
|
||||
@@ -1042,17 +1015,21 @@ export default function DocumentsIndex() {
|
||||
<span>{auditStatusMapping[historyDoc.auditStatus]?.label || '待审核'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4">
|
||||
<IssuesDiff
|
||||
currentIssues={historyDoc.issues}
|
||||
previousIssues={undefined}
|
||||
issuesDiff={historyDoc.issuesDiff}
|
||||
issuesDiffType={historyDoc.issuesDiffType}
|
||||
<td className="px-4 py-3" style={{ width: '15%' }}>
|
||||
<ResultStats
|
||||
passCount={historyDoc.pass_count}
|
||||
warningCount={historyDoc.warning_count}
|
||||
errorCount={historyDoc.error_count}
|
||||
manualCount={historyDoc.manual_count}
|
||||
previousPassCount={historyDoc.previous_pass_count}
|
||||
previousWarningCount={historyDoc.previous_warning_count}
|
||||
previousErrorCount={historyDoc.previous_error_count}
|
||||
previousManualCount={historyDoc.previous_manual_count}
|
||||
/>
|
||||
</td>
|
||||
<td className="text-xs text-gray-600 px-4">{historyDoc.uploadTime}</td>
|
||||
<td className="">
|
||||
<div className="operations-cell flex flex-wrap gap-1 px-4">
|
||||
<td className="text-xs text-gray-600 px-4 py-3" style={{ width: '10%' }}>{historyDoc.uploadTime}</td>
|
||||
<td className="px-4 py-3" style={{ width: '25%' }}>
|
||||
<div className="operations-cell flex flex-wrap gap-1">
|
||||
<Link
|
||||
to={`/reviews?id=${historyDoc.id}&previousRoute=documents`}
|
||||
className="text-xs px-2 py-1 h-7 mr-1 hover:underline"
|
||||
@@ -1144,6 +1121,7 @@ export default function DocumentsIndex() {
|
||||
<span className="doc-name-text font-medium text-gray-900" title={record.name}>
|
||||
{record.name}
|
||||
</span>
|
||||
<span className="document-number">{record.documentNumber}</span>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<FileTypeTag
|
||||
type={record.type}
|
||||
@@ -1157,7 +1135,7 @@ export default function DocumentsIndex() {
|
||||
{record.isTest && (
|
||||
<span className="text-xs bg-gray-100 text-gray-500 px-1 rounded">测试</span>
|
||||
)}
|
||||
{/* 版本徽章 - 始终显示 */}
|
||||
{/* 版本徽章*/}
|
||||
{record.historyCount !== undefined && record.historyCount > 0 ?
|
||||
<span className="version-badge">
|
||||
<i className="ri-history-line"></i>
|
||||
@@ -1169,24 +1147,16 @@ export default function DocumentsIndex() {
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "文档编号",
|
||||
key: "documentNumber",
|
||||
width:'10%',
|
||||
render: (_: unknown, record: DocumentUI) => (
|
||||
<span className="document-number">{record.documentNumber}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "文件大小",
|
||||
key: "size",
|
||||
width: "10%",
|
||||
width: "8%",
|
||||
render: (_: unknown, record: DocumentUI) => formatFileSize(record.size)
|
||||
},
|
||||
{
|
||||
title: "文件状态",
|
||||
key: "fileStatus",
|
||||
width:'10%',
|
||||
width:'8%',
|
||||
render: (_: unknown, record: DocumentUI) => {
|
||||
// 处理fileStatus为null或undefined的情况
|
||||
// console.log('fileStatus',record.fileStatus)
|
||||
@@ -1205,7 +1175,7 @@ export default function DocumentsIndex() {
|
||||
{
|
||||
title: "审核状态",
|
||||
key: "auditStatus",
|
||||
width:"10%",
|
||||
width:"8%",
|
||||
render: (_: unknown, record: DocumentUI) => {
|
||||
// 处理auditStatus为null或undefined的情况,默认为0(待审核)
|
||||
const auditStatus = record.auditStatus != null ? record.auditStatus : 0;
|
||||
@@ -1220,34 +1190,29 @@ export default function DocumentsIndex() {
|
||||
}
|
||||
},
|
||||
{
|
||||
title: "问题数量",
|
||||
title: "结果统计",
|
||||
key: "issues",
|
||||
width:"10%",
|
||||
width:"18%",
|
||||
render: (_: unknown, record: DocumentUI) => (
|
||||
<IssuesDiff
|
||||
currentIssues={record.issues}
|
||||
previousIssues={record.previousIssues}
|
||||
issuesDiff={
|
||||
record.issues != null && record.previousIssues != null
|
||||
? Math.abs(record.issues - record.previousIssues)
|
||||
: undefined
|
||||
}
|
||||
issuesDiffType={
|
||||
record.issues != null && record.previousIssues != null
|
||||
? record.issues > record.previousIssues
|
||||
? 'increase'
|
||||
: record.issues < record.previousIssues
|
||||
? 'decrease'
|
||||
: 'same'
|
||||
: undefined
|
||||
}
|
||||
<ResultStats
|
||||
passCount={record.pass_count}
|
||||
warningCount={record.warning_count}
|
||||
errorCount={record.error_count}
|
||||
manualCount={record.manual_count}
|
||||
previousPassCount={record.previous_pass_count}
|
||||
previousWarningCount={record.previous_warning_count}
|
||||
previousErrorCount={record.previous_error_count}
|
||||
previousManualCount={record.previous_manual_count}
|
||||
warningMessages={record.warning_messages}
|
||||
errorMessages={record.error_messages}
|
||||
manualMessages={record.manual_messages}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: "上传时间",
|
||||
key: "uploadTime",
|
||||
width:"10%",
|
||||
width:"8%",
|
||||
render: (_: unknown, record: DocumentUI) => record.uploadTime
|
||||
},
|
||||
{
|
||||
@@ -1375,8 +1340,20 @@ export default function DocumentsIndex() {
|
||||
<FilterPanel
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
type="default"
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-search-line"
|
||||
onClick={() => {
|
||||
if (documentTypeIds) {
|
||||
fetchData(documentTypeIds);
|
||||
}
|
||||
}}
|
||||
className="mr-2"
|
||||
>
|
||||
搜索
|
||||
</Button>
|
||||
<Button
|
||||
type="default"
|
||||
icon="ri-refresh-line"
|
||||
onClick={handleReset}
|
||||
className="mr-2"
|
||||
@@ -1494,7 +1471,34 @@ export default function DocumentsIndex() {
|
||||
{documents.map((doc) => (
|
||||
<>
|
||||
{/* 主文档行 */}
|
||||
<tr key={doc.id} className="border-b hover:bg-gray-50 transition-colors">
|
||||
<tr
|
||||
key={doc.id}
|
||||
className={`border-b hover:bg-gray-50 transition-colors ${
|
||||
doc.historyCount && doc.historyCount > 0 ? 'cursor-pointer' : ''
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
// 只有有历史版本的行才可以点击
|
||||
if (!doc.historyCount || doc.historyCount === 0) return;
|
||||
|
||||
// 检查点击的是否是可交互元素(链接、按钮、输入框等)
|
||||
const target = e.target as HTMLElement;
|
||||
const isInteractiveElement =
|
||||
target.tagName === 'A' ||
|
||||
target.tagName === 'BUTTON' ||
|
||||
target.tagName === 'INPUT' ||
|
||||
target.closest('a') ||
|
||||
target.closest('button') ||
|
||||
target.closest('input') ||
|
||||
target.closest('.result-stats-wrapper') ||
|
||||
target.closest('.result-stat-item');
|
||||
|
||||
// 如果点击的是可交互元素,不触发展开/收起
|
||||
if (isInteractiveElement) return;
|
||||
|
||||
// 触发展开/收起
|
||||
handleToggleExpand(doc);
|
||||
}}
|
||||
>
|
||||
{columns.map((col, index) => (
|
||||
<td key={col.key || index} className="px-4 py-3 text-sm">
|
||||
{col.render ? col.render(null, doc, index) : (doc as any)[col.key]}
|
||||
|
||||
@@ -50,7 +50,8 @@ export default function RuleGroupsIndex() {
|
||||
const [filteredChildrenMap, setFilteredChildrenMap] = useState<Record<string, RuleGroup[]>>({});
|
||||
const [initialLoading, setInitialLoading] = useState<boolean>(true);
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
|
||||
// 初始加载时自动加载所有子分组
|
||||
useEffect(() => {
|
||||
const loadAllChildGroups = async () => {
|
||||
@@ -499,9 +500,10 @@ export default function RuleGroupsIndex() {
|
||||
key: "ruleCount",
|
||||
width: "12%",
|
||||
render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => (
|
||||
<button onClick={() => navigate(`/rules/list?${!record.isParent ? `ruleType=${record.parentId}&groupId=${record.id}` : `ruleType=${record.id}`}`)} className="badge bg-primary text-white">
|
||||
<span className="text-xs hover:underline">{calculateTotalRuleCount(record)}</span>
|
||||
</button>
|
||||
<div className="badge bg-primary text-white">
|
||||
{/* <button onClick={() => navigate(`/rules/list?${!record.isParent ? `ruleType=${record.parentId}&groupId=${record.id}` : `ruleType=${record.id}`}`)} className="badge bg-primary text-white"> */}
|
||||
<span className="text-xs">{calculateTotalRuleCount(record)}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
@@ -533,10 +535,10 @@ export default function RuleGroupsIndex() {
|
||||
onClick={() => navigate(`/rule-groups/new?id=${record.id}`)}
|
||||
className="operation-btn"
|
||||
>
|
||||
<i className="ri-edit-line"></i> {userRole === 'common' ? '查看' : '编辑'}
|
||||
<i className="ri-edit-line"></i> {hasEditPermission ? '编辑' : '查看'}
|
||||
</button>
|
||||
{userRole !== 'common' && (
|
||||
<button
|
||||
{hasEditPermission && (
|
||||
<button
|
||||
type="button"
|
||||
className="operation-btn !text-[--color-error]"
|
||||
onClick={() => handleDeleteGroup(record.id)}
|
||||
@@ -577,7 +579,7 @@ export default function RuleGroupsIndex() {
|
||||
>
|
||||
收起全部
|
||||
</Button>
|
||||
{userRole !== 'common' && (
|
||||
{hasEditPermission && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-add-line"
|
||||
|
||||
@@ -82,7 +82,7 @@ function mapApiToFrontend(apiGroup: ApiRuleGroup): RuleGroup {
|
||||
code: apiGroup.code || '',
|
||||
description: apiGroup.description,
|
||||
status: apiGroup.is_enabled ? 'active' : 'inactive',
|
||||
parentId: apiGroup.pid === '0' ? null : apiGroup.pid,
|
||||
parentId: (!apiGroup.pid || apiGroup.pid === '0') ? null : apiGroup.pid, // 🆕 NULL或'0'都表示顶级分组
|
||||
sortOrder: 0 // API中不存在sortOrder字段,使用默认值
|
||||
};
|
||||
}
|
||||
@@ -162,8 +162,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
const status = formData.get("status") as string || "active";
|
||||
const groupType = formData.get("groupType") as string;
|
||||
const parentId = groupType === "secondary" ? formData.get("parentId") as string : null;
|
||||
const reviewType = formData.get("reviewType") as string || undefined;
|
||||
|
||||
|
||||
// 表单验证
|
||||
// action是处于服务端的表单提交方法,这里再次验证表单数据也是出于安全考虑,防止客户端验证被绕过从而提交非法数据
|
||||
const errors: ActionData["errors"] = {};
|
||||
@@ -195,8 +194,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
code: code.trim(),
|
||||
description: description?.trim() || "",
|
||||
is_enabled: status === "active",
|
||||
pid: parentId === null ? "0" : parentId,
|
||||
reviewType: reviewType // 传递 reviewType
|
||||
pid: parentId || undefined // 🆕 NULL/undefined 表示顶级分组
|
||||
};
|
||||
|
||||
try {
|
||||
@@ -244,17 +242,14 @@ export default function RuleGroupNew() {
|
||||
const isSubmitting = navigation.state === "submitting";
|
||||
const rootData = useRouteLoaderData("root") as { userRole: string };
|
||||
const userRole = rootData?.userRole || 'common';
|
||||
|
||||
|
||||
// 判断表单是否为只读模式
|
||||
const isReadOnly = userRole === 'common';
|
||||
|
||||
// 判断表单是否为只读模式(只有包含'provin'的用户才有编辑权限)
|
||||
const hasEditPermission = userRole.toLowerCase().includes('provin');
|
||||
const isReadOnly = !hasEditPermission;
|
||||
|
||||
// 解构数据
|
||||
const { group, parentGroups, isEdit, error } = data;
|
||||
|
||||
// 从 sessionStorage 获取 reviewType
|
||||
const [reviewType, setReviewType] = useState<string | null>(null);
|
||||
|
||||
// 表单状态管理 - 使用受控组件
|
||||
const [formValues, setFormValues] = useState<{
|
||||
groupType: "primary" | "secondary";
|
||||
@@ -315,19 +310,6 @@ export default function RuleGroupNew() {
|
||||
}
|
||||
}, [group]);
|
||||
|
||||
// 在组件挂载时从 sessionStorage 获取 reviewType
|
||||
useEffect(() => {
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedReviewType = sessionStorage.getItem('reviewType');
|
||||
// console.log("从 sessionStorage 获取 reviewType:", storedReviewType);
|
||||
setReviewType(storedReviewType);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取 sessionStorage 中的 reviewType 失败:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 验证表单字段
|
||||
const validateField = (field: string, value: string) => {
|
||||
switch (field) {
|
||||
@@ -490,9 +472,6 @@ export default function RuleGroupNew() {
|
||||
{/* 如果是编辑模式,添加ID */}
|
||||
{group?.id && <input type="hidden" name="id" value={group.id} />}
|
||||
|
||||
{/* 传递 reviewType */}
|
||||
{reviewType && <input type="hidden" name="reviewType" value={reviewType} />}
|
||||
|
||||
{/* 基本信息区域 */}
|
||||
<Card className="form-section">
|
||||
<div className="form-section-header">
|
||||
|
||||
@@ -38,9 +38,9 @@ export const meta: MetaFunction = () => {
|
||||
];
|
||||
};
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "评查点列表"
|
||||
};
|
||||
// export const handle = {
|
||||
// breadcrumb: "评查点列表"
|
||||
// };
|
||||
|
||||
// 声明loader返回的数据类型
|
||||
export type LoaderData = {
|
||||
@@ -563,7 +563,7 @@ export default function RulesIndex() {
|
||||
if (input) {
|
||||
(input as HTMLInputElement).value = '';
|
||||
}
|
||||
// 保留reviewType的过滤条件,只重置其他条件
|
||||
|
||||
const newParams = new URLSearchParams();
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.removeItem(SEARCH_PARAMS_STORAGE_KEY);
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
@import './components/toast.css';
|
||||
@import './components/TooltipStyles.css';
|
||||
@import './components/document-version.css';
|
||||
@import './components/result-stats.css';
|
||||
|
||||
/* @import './components/modal.css'; */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user