feat: 1. 重构交叉评查任务的文档列表的显示,对接接口查询当前任务的文档相关信息。

2.文档上传通过接口去查询是否存在同名的文件,做上传前拦截提示。
3.交叉评查的评查结果也同步添加企查查的企业信息查询模块。
4. 封装上传附件和上传模板的模态框的组件,在交叉评查的文档列表中引入显示。
5. 交叉评查的评查结果中关于合同类型的文档同步加入结构比对的功能。
This commit is contained in:
2025-12-13 07:18:37 +08:00
parent daa53289af
commit 1658bb1c6f
11 changed files with 3368 additions and 363 deletions
+1 -1
View File
@@ -113,7 +113,7 @@ export async function findIsProposer(taskId: string | number, userId: number | u
);
const data = response.data;
console.log('[findIsProposer] 检查权限响应:', data);
// console.log('[findIsProposer] 检查权限响应:', data);
// 返回 can_confirm 字段,表示是否有权确认完成
// 有权限的用户:任务创建者(assigner_id) 或 主要负责人(principal_user_ids)
+468 -2
View File
@@ -75,7 +75,7 @@ export interface UserTaskApiResponse {
items: UserTaskInfo[];
}
// 任务文档接口类型定义(新增
// 任务文档接口类型定义(旧版,保留兼容
export interface TaskDocument {
document_id: number;
file_name: string;
@@ -103,6 +103,174 @@ export interface TaskDocument {
manual_count: number;
}
// ==================== 新版接口类型定义(支持版本归纳)====================
/**
* 历史版本信息
* 每个历史版本都有独立的评查统计、消息列表、分数信息
*/
export interface CrossReviewHistoryVersion {
/** 历史版本的文档ID */
id: number;
/** 版本号(从1开始,数字越小越旧) */
version_number: number;
/** 创建时间(ISO 8601格式) */
created_at: string;
/** 文件大小(字节) */
file_size: number;
/** 文件存储路径 */
path: string;
/** 文档处理状态 */
status: "Waiting" | "Cutting" | "Extractioning" | "Evaluationing" | "Failed" | "Processed";
/** 文书号/文档编号(可为null) */
document_number: string | null;
/** 文档类型ID */
type_id: number;
/** 文档类型名称 */
type_name: string;
/** 上传时间(ISO 8601格式) */
upload_time: string;
/** 任务内评查完成状态:0=未评查, 1=已评查 */
audit_status: 0 | 1;
/** 总评查点数 */
total_evaluation_points: number;
/** 通过的评查点数量 */
pass_count: number;
/** 警告的评查点数量 */
warning_count: number;
/** 错误的评查点数量 */
error_count: number;
/** 需人工审核的评查点数量 */
manual_count: number;
/** 问题总数 */
issue_count: number;
/** 警告消息列表 */
warning_messages: string[];
/** 错误消息列表 */
error_messages: string[];
/** 问题消息列表(综合:警告+错误) */
issue_messages: string[];
/** 需人工确认的消息列表 */
manual_messages: string[];
/** 最终得分 */
final_score: number;
/** 满分 */
full_score: number;
/** 得分摘要(如 "85.5/100" */
score_summary: string;
/** 得分百分比(0-100 */
score_percent: number;
}
/**
* 文档信息(含版本和评查统计)- 新版接口
*/
export interface CrossReviewDocumentWithVersion {
// ========== 基本信息 ==========
/** 当前版本的文档ID */
id: number;
/** 文档名称 */
name: string;
/** 文件存储路径 */
path: string;
/** 当前版本号(最大值,从1开始) */
version_number: number;
/** 创建时间(ISO 8601格式) */
created_at: string;
/** 文档处理状态 */
status: "Waiting" | "Cutting" | "Extractioning" | "Evaluationing" | "Failed" | "Processed";
/** 文件大小(字节) */
file_size: number;
/** 文书号/文档编号(可为null) */
document_number: string | null;
/** 文档类型ID */
type_id: number;
/** 文档类型名称 */
type_name: string;
/** 上传时间(ISO 8601格式) */
upload_time: string;
// ========== 任务内评查状态 ==========
/** 任务内评查完成状态:0=未评查, 1=已评查 */
audit_status: 0 | 1;
// ========== 评查统计 ==========
/** 总评查点数 */
total_evaluation_points: number;
/** 通过的评查点数量 */
pass_count: number;
/** 警告的评查点数量 */
warning_count: number;
/** 错误的评查点数量 */
error_count: number;
/** 需人工审核的评查点数量 */
manual_count: number;
/** 问题总数 */
issue_count: number;
// ========== 评查消息列表 ==========
/** 警告消息列表 */
warning_messages: string[];
/** 错误消息列表 */
error_messages: string[];
/** 问题消息列表(综合) */
issue_messages: string[];
/** 需人工确认的消息列表 */
manual_messages: string[];
// ========== 交叉评查特有:分数信息 ==========
/** 最终得分 */
final_score: number;
/** 满分 */
full_score: number;
/** 得分摘要(如 "85.5/100" */
score_summary: string;
/** 得分百分比(0-100 */
score_percent: number;
// ========== 版本信息 ==========
/** 总版本数 */
total_versions: number;
/** 历史版本列表(按created_at降序,不包含当前版本) */
history_versions: CrossReviewHistoryVersion[];
// ========== 前端扩展字段 ==========
/** 是否已展开历史版本(前端状态) */
isExpanded?: boolean;
}
/**
* 交叉评查任务文档列表响应
*/
export interface CrossReviewDocumentListResponse {
/** 总文档数(按版本分组后的唯一文档数) */
total: number;
/** 当前页码 */
page: number;
/** 每页数量 */
page_size: number;
/** 总页数 */
total_pages: number;
/** 文档列表 */
documents: CrossReviewDocumentWithVersion[];
}
/**
* 获取任务文档列表请求参数(支持版本归纳)
*/
export interface GetTaskDocumentsWithVersionsParams {
/** 任务ID */
taskId: number;
/** 页码(从1开始) */
page?: number;
/** 每页数量(最大100 */
pageSize?: number;
/** 模糊搜索关键字(匹配文件名称或文档编号) */
keyword?: string;
/** JWT token */
jwtToken?: string;
}
// 任务文档API响应格式(新增)
export interface TaskDocumentApiResponse {
total: number;
@@ -434,7 +602,7 @@ export async function getUserTaskDocuments(page: number = 1, pageSize: number =
}
/**
* 获取指定任务的文档列表(新增接口
* 获取指定任务的文档列表(旧版接口,保留兼容
* @param taskId 任务ID
* @param page 页码
* @param pageSize 每页大小
@@ -476,6 +644,75 @@ export async function getTaskDocuments(taskId: number, page: number = 1, pageSiz
}
}
/**
* 获取任务下文档列表(支持版本归纳)- 新版接口
*
* POST /api/v2/cross_review/tasks/{task_id}/documents
*
* 同一任务内同名且同类型的文档会被归纳为版本组,最新上传的为当前版本,其余为历史版本。
*
* @param params 请求参数
* @returns 文档列表响应(含版本信息)
*/
export async function getTaskDocumentsWithVersions(
params: GetTaskDocumentsWithVersionsParams
): Promise<ApiResponse<CrossReviewDocumentListResponse>> {
const { taskId, page = 1, pageSize = 10, keyword, jwtToken } = params;
try {
// 拼接绝对路径,去除多余斜杠
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
const url = `${base}/api/v2/cross_review/tasks/${taskId}/documents`;
// 构建请求体
const queryParams: {
page: number;
page_size: number;
keyword?: string;
} = {
page,
page_size: pageSize
};
// 只有当 keyword 有值时才添加
if (keyword && keyword.trim()) {
queryParams.keyword = keyword.trim();
}
const response = await axios.get<CrossReviewDocumentListResponse>(url, {
params: queryParams,
headers: {
'Authorization': `Bearer ${jwtToken || ''}`
}
});
return {
success: true,
data: response.data
};
} catch (error) {
if (axios.isAxiosError(error)) {
// 处理特定错误码
if (error.response?.status === 403) {
return {
success: false,
error: '无权访问任务:您不是该任务的参与者',
status: 403
};
}
return {
success: false,
error: `HTTP ${error.response?.status}: ${error.response?.statusText || error.message}`,
status: error.response?.status
};
}
return {
success: false,
error: error instanceof Error ? error.message : '获取任务文档列表失败'
};
}
}
/**
* 更新文件的审核状态
@@ -563,4 +800,233 @@ export async function getCrossCheckingDocumentTypes(jwtToken?: string): Promise<
error: error instanceof Error ? error.message : '获取文档类型失败'
};
}
}
// ==================== 追加附件 API ====================
/**
* 追加附件响应接口
*/
export interface AppendAttachmentsResponse {
success: boolean;
result?: {
original_document_id: number;
new_document_id: number;
new_version_number: number;
task_id: number;
message: string;
background_processing: boolean;
};
error?: string;
}
/**
* 追加附件参数接口
*/
export interface AppendAttachmentsParams {
/** 任务ID */
taskId: number;
/** 原文档ID */
documentId: number;
/** 附件文件列表 */
files: File[];
/** 备注说明(可选) */
remark?: string;
/** Word附件是否使用Markdown处理(可选,默认false */
useMarkdown?: boolean;
/** JWT Token */
jwtToken?: string;
}
/**
* 为交叉评查任务文档追加附件
*
* POST /api/v2/cross_review/tasks/{task_id}/documents/{document_id}/append_attachments
*
* 追加附件后创建新文档(新版本),原文档保留。
* 新文档自动关联到当前任务,audit_status = 0(需重新评查)
*
* @param params 追加附件参数
* @returns 追加结果
*/
export async function appendTaskDocumentAttachments(
params: AppendAttachmentsParams
): Promise<ApiResponse<AppendAttachmentsResponse>> {
const { taskId, documentId, files, remark, useMarkdown = false, jwtToken } = params;
try {
if (!taskId || taskId <= 0) {
return { success: false, error: '任务ID无效' };
}
if (!documentId || documentId <= 0) {
return { success: false, error: '文档ID无效' };
}
if (!files || files.length === 0) {
return { success: false, error: '请选择附件文件' };
}
// 构建 FormData
const formData = new FormData();
files.forEach(file => {
formData.append('files', file);
});
if (remark) {
formData.append('remark', remark);
}
formData.append('use_markdown', useMarkdown.toString());
// 构建请求 URL
const base = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
const url = `${base}/api/v2/cross_review/tasks/${taskId}/documents/${documentId}/append_attachments`;
const response = await axios.post<AppendAttachmentsResponse>(url, formData, {
headers: {
'Content-Type': 'multipart/form-data',
'Authorization': `Bearer ${jwtToken || ''}`
}
});
return {
success: true,
data: response.data
};
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 403) {
return {
success: false,
error: '无权追加附件:您不是任务创建者或负责人',
status: 403
};
}
if (error.response?.status === 400) {
return {
success: false,
error: error.response.data?.error || '请求参数错误',
status: 400
};
}
return {
success: false,
error: `HTTP ${error.response?.status}: ${error.response?.statusText || error.message}`,
status: error.response?.status
};
}
return {
success: false,
error: error instanceof Error ? error.message : '追加附件失败'
};
}
}
// ==================== 上传模板 API ====================
/**
* 上传模板响应接口(文档级别)
*/
export interface UploadDocumentTemplateResponse {
success: boolean;
result?: {
document_id: number;
comparison_id: number;
template_name: string;
template_path: string;
status: string;
message: string;
};
error?: string;
}
/**
* 上传模板参数接口(文档级别)
*/
export interface UploadDocumentTemplateParams {
/** 文档ID */
documentId: number;
/** 模板文件 */
file: File;
/** 对比记录ID(可选,用于更新已有模板) */
comparisonId?: number;
/** JWT Token */
jwtToken?: string;
}
/**
* 为交叉评查任务中的文档上传模板(文档级别)
*
* 复用现有的 /upload_contract_template 接口,与 files-upload.ts 中的 uploadContractTemplate 保持一致
*
* @param params 上传模板参数
* @returns 上传结果
*/
export async function uploadCrossReviewDocumentTemplate(
params: UploadDocumentTemplateParams
): Promise<ApiResponse<UploadDocumentTemplateResponse>> {
const { documentId, file, comparisonId, jwtToken } = params;
try {
if (!documentId || documentId <= 0) {
return { success: false, error: '文档ID无效' };
}
if (!file) {
return { success: false, error: '请选择模板文件' };
}
// 构建 FormData,与 files-upload.ts 中的 uploadContractTemplate 保持一致
const formData = new FormData();
formData.append('file', file);
// upload_info 作为 JSON 字符串
const uploadInfo: { document_id: number; comparison_id?: number } = {
document_id: documentId
};
if (comparisonId) {
uploadInfo.comparison_id = comparisonId;
}
formData.append('upload_info', JSON.stringify(uploadInfo));
// 使用与 files-upload.ts 相同的上传接口
const { UPLOAD_URL } = await import('~/config/api-config');
const url = `${UPLOAD_URL}/upload_contract_template`;
const response = await axios.post<UploadDocumentTemplateResponse>(url, formData, {
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${jwtToken || ''}`
}
});
return {
success: true,
data: response.data
};
} catch (error) {
if (axios.isAxiosError(error)) {
if (error.response?.status === 403) {
return {
success: false,
error: '无权上传模板',
status: 403
};
}
if (error.response?.status === 400) {
return {
success: false,
error: error.response.data?.error || '请求参数错误',
status: 400
};
}
return {
success: false,
error: `HTTP ${error.response?.status}: ${error.response?.statusText || error.message}`,
status: error.response?.status
};
}
return {
success: false,
error: error instanceof Error ? error.message : '上传模板失败'
};
}
}
+58 -1
View File
@@ -1,7 +1,64 @@
import { postgrestGet, type PostgrestParams } from '../postgrest-client';
import dayjs from 'dayjs';
import { UPLOAD_URL } from '../../config/api-config';
import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config';
import axios from 'axios';
/**
* 检查文档名称是否重复
* @param name 文档名称
* @param typeId 文档类型ID
* @returns 重复检查结果
*/
export async function checkDocumentDuplicate(
name: string,
typeId: number
): Promise<{ is_duplicate: boolean; count: number }> {
try {
// 获取 token
let token: string | null = null;
if (typeof window !== 'undefined') {
token = localStorage.getItem('access_token');
}
const headers: Record<string, string> = {
'Accept': 'application/json'
};
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
const response = await axios.get(
`${API_BASE_URL}/api/v2/documents/check-duplicate`,
{
params: { name, type_id: typeId },
headers
}
);
// 解析响应数据
const data = response.data;
if (data && typeof data === 'object') {
// 处理标准响应格式 { code, msg, data }
if ('data' in data && data.data) {
return {
is_duplicate: data.data.is_duplicate ?? false,
count: data.data.count ?? 0
};
}
// 直接返回数据格式
return {
is_duplicate: data.is_duplicate ?? false,
count: data.count ?? 0
};
}
return { is_duplicate: false, count: 0 };
} catch (error) {
console.error('【文档重名检查】检查失败:', error);
// 检查失败时默认允许上传
return { is_duplicate: false, count: 0 };
}
}
// import { API_BASE_URL } from '../client';
/**