Files
leaudit-platform-frontend/app/api/files/documents.ts
T
LiangShiyong 6dc9b4e468 feat: 1. 完善文档列表的显示效果,数据对接后端接口返回。
2. 对评查点分组和文档类型的编辑删除新增操作进行限制。
2025-11-20 15:26:11 +08:00

789 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 响应中提取数据
* @param responseData API 响应数据
* @returns 提取后的数据或 null
*/
function extractApiData<T>(responseData: unknown): T | null {
if (!responseData) return null;
// 格式1: { code: number, msg: string, data: T }
if (typeof responseData === 'object' && responseData !== null &&
'code' in responseData &&
'data' in responseData &&
(responseData as { data: unknown }).data) {
return (responseData as { data: T }).data;
}
// 格式2: 直接是数据对象
return responseData as T;
}
/**
* 数据库文档结构
*/
export interface Document {
id: number;
user_id: number | null;
type_id: number;
name: string;
document_number: string;
path: string;
storage_type: string;
file_size: number;
upload_time: string;
is_test_document: boolean;
evaluation_level: string;
status: 'pass' | 'warning' | 'waiting' | 'processing' | 'fail';
file_status: 'Waiting' | 'Cutting' | 'Extractioning' | 'Evaluationing' | 'Processed';
audit_status: number; // -1: 不通过, 0: 待审核, 1: 通过, 2: 警告, 3: 审核中
ocr_result?: {
__meta?: {
page_count?: number;
}
};
extracted_results?: unknown;
summary?: unknown;
remark?: string;
created_at: string;
updated_at: string;
}
/**
* 前端UI文档结构
*/
export interface DocumentUI {
id: number;
name: string;
documentNumber: string;
type: string;
typeName: string;
size: number;
auditStatus: number; // -1: 不通过, 0: 待审核, 1: 通过, 2: 警告, 3: 审核中
fileStatus: string; // Waiting, Cutting, Extractioning, Failed, Evaluationing, Processed
issues: number | null;
uploadTime: string;
fileType: string;
path: string;
isTest: boolean;
remark?: string;
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[]; // 历史版本列表
}
/**
* 文档历史版本结构
*/
export interface DocumentVersionUI {
id: number;
name: string;
documentNumber: string;
type: string;
typeName: string;
size: number;
auditStatus: number;
fileStatus: string;
issues: number | null;
issuesDiff?: number; // 与上一个版本的问题数量差异(绝对值)
issuesDiffType?: 'increase' | 'decrease' | 'same'; // 差异类型
uploadTime: string;
fileType: string;
path: string;
isTest: boolean;
updatedAt?: string;
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; // 上一版本人工数量
}
/**
* 获取文件扩展名
* @param filename 文件名
* @returns 文件扩展名
*/
function getFileExtension(filename: string): string {
const parts = filename.split('.');
return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : '';
}
/**
* 获取评查结果
* @param id 评查结果ID
* @returns 评查结果
*/
async function getEvaluationResults(id: number, frontendJWT?: string) {
const response = await postgrestGet<[]>('evaluation_results', {
filter: {
'document_id': `eq.${id}`
},
token: frontendJWT
});
if (response.error) {
return { error: response.error, status: response.status };
}
const evaluationResult = extractApiData<[]>(response.data);
return evaluationResult;
}
/**
* 将API文档转换为UI文档
*/
async function convertToUIDocument(doc: Document, frontendJWT?: string): Promise<DocumentUI> {
// 获取文档类型信息
const typeResponse = await getDocumentTypes(undefined, frontendJWT);
const documentTypes = typeResponse.data?.types || [];
const docType = documentTypes.find(type => type.id.toString() === doc.type_id.toString());
const evaluationResult = await getEvaluationResults(doc.id, frontendJWT);
let issues = 0;
interface EvaluationResultItem {
evaluated_results?: {
result?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
if (evaluationResult && Array.isArray(evaluationResult)) {
evaluationResult.forEach((result: EvaluationResultItem) => {
if(result && result.evaluated_results && !result.evaluated_results.result){
issues++;
}
});
}
return {
id: doc.id,
name: doc.name,
documentNumber: doc.document_number,
type: doc.type_id.toString(),
typeName: docType?.name || '未知类型',
size: doc.file_size,
auditStatus: doc.audit_status || 0,
fileStatus: doc.status || '', // 默认为''
issues: issues, // 使用计算得到的issues
uploadTime: formatDate(doc.updated_at),
fileType: getFileExtension(doc.name),
path: doc.path,
isTest: doc.is_test_document,
remark: doc.remark,
updatedAt: formatDate(doc.updated_at),
pageCount: doc.ocr_result?.__meta?.page_count || 0,
ocrResult: doc.ocr_result
};
}
/**
* 后端SQL函数返回的文档结构
*/
interface DocumentFromSQL {
id: number;
name: string;
document_number: string;
type_id: number;
type_name: string;
file_size: number;
audit_status: number;
status: string;
false_count: number;
updated_at: string;
path: string;
is_test_document: boolean;
ocr_result: {
__meta?: {
page_count?: number;
}
};
}
/**
* 删除文档
* @param id 文档ID
* @param userId 用户ID
* @param token JWT token (可选)
* @returns 删除结果
*/
export async function deleteDocument(id: string, userId: string, token?: string): Promise<{
success?: boolean;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '文档ID不能为空', status: 400 };
}
if (!userId) {
return { error: '用户身份验证失败', status: 401 };
}
const response = await postgrestDelete(
'documents',
{
filter: {
'id': `eq.${id}`,
'user_id': `eq.${userId}` // 确保只能删除自己的文档
},
token
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
return { success: true };
} catch (error) {
console.error('删除文档失败:', error);
return {
error: error instanceof Error ? error.message : '删除文档失败',
status: 500
};
}
}
/**
* 获取单个文档详情
* @param id 文档ID
* @returns 文档详情
*/
export async function getDocument(id: string, userId: string, frontendJWT?: string): Promise<{
data?: DocumentUI;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '文档ID不能为空', status: 400 };
}
if (!userId) {
return { error: '用户身份验证失败', status: 401 };
}
const response = await postgrestGet<Document[]>(
'documents',
{
filter: {
'id': `eq.${id}`,
'user_id': `eq.${userId}`
},
limit: 1,
token: frontendJWT
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
const extractedData = extractApiData<Document[]>(response.data);
if (!extractedData || extractedData.length === 0) {
return { error: '文档不存在', status: 404 };
}
const documentUI = await convertToUIDocument(extractedData[0], frontendJWT);
return { data: documentUI };
} catch (error) {
console.error('获取文档详情失败:', error);
return {
error: error instanceof Error ? error.message : '获取文档详情失败',
status: 500
};
}
}
/**
* 获取单个文档详情
* @param id 文档ID
* @returns 文档详情
*/
export async function getDocumentWithNoUserId(id: string, frontendJWT?: string): Promise<{
data?: DocumentUI;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '文档ID不能为空', status: 400 };
}
// console.log("get单个文档id", id)
const response = await postgrestGet<Document[]>(
'documents',
{
filter: {
'id': `eq.${id}`,
},
limit: 1,
token: frontendJWT
}
);
if (response.error) {
return { error: response.error, status: response.status };
}
// console.log("respose", response)
const extractedData = extractApiData<Document[]>(response.data);
if (!extractedData || extractedData.length === 0) {
return { error: '文档不存在', status: 404 };
}
// console.log('extractedData', extractedData);
const documentUI = await convertToUIDocument(extractedData[0], frontendJWT);
return { data: documentUI };
} catch (error) {
console.error('获取文档详情失败:', error);
return {
error: error instanceof Error ? error.message : '获取文档详情失败',
status: 500
};
}
}
/**
* 获取文件下载链接
* @param filePath 文件路径
* @returns 下载链接
*/
export async function getFileDownloadUrl(filePath: string): Promise<{
data?: { downloadUrl: string };
error?: string;
status?: number;
}> {
try {
if (!filePath) {
return { error: '文件路径不能为空', status: 400 };
}
// 这里应该调用获取文件下载链接的API
// 假设后端有这样的端点:/api/files/generate-download-url?path=xxx
// 实际项目中需要根据你的后端API调整
// 临时解决方案:返回Remix路由路径
// 这将通过Remix服务器代理对文件的访问
return {
data: {
downloadUrl: `/documents/download?path=${encodeURIComponent(filePath)}`
}
};
} catch (error) {
console.error('获取文件下载链接失败:', error);
return {
error: error instanceof Error ? error.message : '获取文件下载链接失败',
status: 500
};
}
}
/**
* 更新文档信息
* @param id 文档ID
* @param document 部分文档数据
* @returns 更新结果
*/
export async function updateDocument(id: string, document: Partial<DocumentUI> & { remark?: string }, userId: string, frontendJWT?: string): Promise<{
data?: DocumentUI;
error?: string;
status?: number;
}> {
try {
if (!id) {
return { error: '文档ID不能为空', status: 400 };
}
if (!userId) {
return { error: '用户身份验证失败', status: 401 };
}
// 准备API数据 - 将UI数据转换为API格式
const apiDocument: Partial<Document> = {};
if (document.documentNumber !== undefined) {
apiDocument.document_number = document.documentNumber;
}
// if (document.type !== undefined) {
// apiDocument.type_id = parseInt(document.type);
// }
if (document.auditStatus !== undefined) {
apiDocument.audit_status = document.auditStatus;
}
if (document.isTest !== undefined) {
apiDocument.is_test_document = document.isTest;
}
if (document.remark !== undefined) {
apiDocument.remark = document.remark;
}
// console.log('更新文档API数据:', apiDocument);
const response = await postgrestPut<Document, Partial<Document>>(
'documents',
apiDocument,
{
id: parseInt(id),
user_id: parseInt(userId) // 确保只能更新自己的文档
},
frontendJWT
);
if (response.error) {
console.error('更新文档API错误:', response.error);
return { error: response.error, status: response.status };
}
// 获取更新后的完整文档数据
const updatedResponse = await getDocument(id, userId, frontendJWT);
return updatedResponse;
} catch (error) {
console.error('更新文档信息失败:', error);
return {
error: error instanceof Error ? error.message : '更新文档信息失败',
status: 500
};
}
}
/**
* 获取文档列表(使用新的后端API)
* @param searchParams 搜索参数
* @returns 文档列表和总数
*/
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;
}> {
try {
const {
page = 1,
pageSize = 10,
name,
documentNumber,
documentTypeIds,
auditStatus,
fileStatus,
dateFrom,
dateTo,
token
} = searchParams;
// 构建查询参数
const params: Record<string, any> = {
page,
page_size: pageSize
};
// 添加可选参数
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;
// 处理文档类型ID数组 - 传递为数组或单个值
if (documentTypeIds && documentTypeIds.length > 0) {
params.type_id = documentTypeIds;
}
console.log('📤 [getDocumentsListFromAPI] 请求参数:', params);
// 调用后端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'
}
});
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: convertedDocuments,
total: totalCount,
page: data.page || page,
totalPages
}
};
} catch (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
};
}
}
/**
* 获取文档历史版本列表
* @param documentName 文档名称
* @param userId 用户ID
* @param excludeId 排除的文档ID(当前最新版本的ID)
* @param token JWT token
* @returns 历史版本列表
*/
export async function getDocumentHistory(
documentName: string,
userId: string,
excludeId: number,
token?: string
): Promise<{
data?: DocumentVersionUI[];
error?: string;
status?: number;
}> {
try {
if (!documentName) {
return { error: '文档名称不能为空', status: 400 };
}
if (!userId) {
return { error: '用户身份验证失败', status: 401 };
}
// 调用 RPC 函数获取历史版本
const response = await postgrestPost<any[], unknown>(
'rpc/documents_get_document_history',
{
p_document_name: documentName,
p_user_id: parseInt(userId, 10),
p_exclude_id: excludeId
},
token
);
if (response.error || !response.data) {
return { error: response.error || '获取历史版本失败', status: response.status || 500 };
}
const historyDocs = response.data;
// 转换为 UI 格式,并计算问题数量差异
const documents: DocumentVersionUI[] = historyDocs.map((doc: any, index: number) => {
// 计算与下一个版本(更早的版本)的问题数量差异
let issuesDiff: number | undefined;
let issuesDiffType: 'increase' | 'decrease' | 'same' | undefined;
if (index < historyDocs.length - 1) {
const olderDoc = historyDocs[index + 1];
if (doc.false_count != null && olderDoc.false_count != null) {
const diff = doc.false_count - olderDoc.false_count;
issuesDiff = Math.abs(diff);
if (diff > 0) {
issuesDiffType = 'increase';
} else if (diff < 0) {
issuesDiffType = 'decrease';
} else {
issuesDiffType = 'same';
}
}
}
return {
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,
issuesDiff,
issuesDiffType,
uploadTime: formatDate(doc.created_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,
versionNumber: historyDocs.length - index
};
});
return { data: documents };
} catch (error) {
console.error('获取文档历史版本失败:', error);
return {
error: error instanceof Error ? error.message : '获取文档历史版本失败',
status: 500
};
}
}