feat: 1. 完善文档列表的显示效果,数据对接后端接口返回。

2. 对评查点分组和文档类型的编辑删除新增操作进行限制。
This commit is contained in:
2025-11-20 15:26:11 +08:00
parent 2edde8a8ab
commit 6dc9b4e468
11 changed files with 549 additions and 575 deletions
+14 -47
View File
@@ -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 {
+27 -30
View File
@@ -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
// 🆕 如果需要更新父分组,添加 pidNULL表示顶级分组)
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
View File
@@ -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