Merge branch 'shiy' into awen
This commit is contained in:
-131
@@ -1,131 +0,0 @@
|
||||
/**
|
||||
* API基础服务
|
||||
*/
|
||||
|
||||
// 默认API基础URL
|
||||
const API_BASE_URL = process.env.API_BASE_URL || 'http://localhost:8000';
|
||||
|
||||
/**
|
||||
* 通用API响应格式
|
||||
*/
|
||||
export interface ApiResponse<T> {
|
||||
status: 'success' | 'error';
|
||||
data?: T;
|
||||
message?: string;
|
||||
error?: {
|
||||
code: string;
|
||||
details?: any;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页请求参数
|
||||
*/
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页响应数据
|
||||
*/
|
||||
export interface PaginatedResponse<T> {
|
||||
items: T[];
|
||||
totalItems: number;
|
||||
totalPages: number;
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一错误处理
|
||||
*/
|
||||
export class ApiError extends Error {
|
||||
statusCode: number;
|
||||
errorCode: string;
|
||||
details?: any;
|
||||
|
||||
constructor(message: string, statusCode: number, errorCode: string, details?: any) {
|
||||
super(message);
|
||||
this.name = 'ApiError';
|
||||
this.statusCode = statusCode;
|
||||
this.errorCode = errorCode;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建请求URL
|
||||
*/
|
||||
export function buildUrl(path: string, params?: Record<string, any>): string {
|
||||
// 确保API_BASE_URL末尾没有斜杠,而path开头有斜杠
|
||||
const baseUrl = API_BASE_URL.endsWith('/') ? API_BASE_URL.slice(0, -1) : API_BASE_URL;
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
|
||||
try {
|
||||
const url = new URL(`${baseUrl}${normalizedPath}`);
|
||||
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
url.searchParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
} catch (error) {
|
||||
console.error('URL构建错误:', error);
|
||||
throw new Error(`无法构建URL: ${baseUrl}${normalizedPath}, 错误: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用API请求函数
|
||||
*/
|
||||
export async function apiRequest<T>(
|
||||
url: string,
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
|
||||
body?: any,
|
||||
headers?: Record<string, string>
|
||||
): Promise<T> {
|
||||
const requestInit: RequestInit = {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
...headers
|
||||
}
|
||||
};
|
||||
|
||||
if (body) {
|
||||
requestInit.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(url, requestInit);
|
||||
|
||||
// 获取响应数据
|
||||
const data = await response.json() as ApiResponse<T>;
|
||||
|
||||
// 检查状态码
|
||||
if (!response.ok) {
|
||||
throw new ApiError(
|
||||
data.message || '请求失败',
|
||||
response.status,
|
||||
data.error?.code || 'UNKNOWN_ERROR',
|
||||
data.error?.details
|
||||
);
|
||||
}
|
||||
|
||||
// 检查业务状态
|
||||
if (data.status === 'error') {
|
||||
throw new ApiError(
|
||||
data.message || '请求失败',
|
||||
response.status,
|
||||
data.error?.code || 'BUSINESS_ERROR',
|
||||
data.error?.details
|
||||
);
|
||||
}
|
||||
|
||||
return data.data as T;
|
||||
}
|
||||
@@ -57,6 +57,7 @@ interface EvaluationPoint {
|
||||
suggestion_message_type?: string;
|
||||
suggestion_message?: string;
|
||||
score?: number;
|
||||
updated_at?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ interface StatsData {
|
||||
* 获取当前评查文件的所有评查点结果
|
||||
* @param fileId 评查文件ID
|
||||
* @returns 评查点结果列表和统计数据
|
||||
*/
|
||||
*/
|
||||
export async function getReviewPoints(fileId: string) {
|
||||
// 步骤1:根据fileId查询evaluation_results表
|
||||
const evaluationResultsParams: PostgrestParams = {
|
||||
@@ -197,12 +198,24 @@ export async function getReviewPoints(fileId: string) {
|
||||
return {
|
||||
id: result.id,
|
||||
title: message,
|
||||
pointName: point.name || '',
|
||||
groupName: group.name || '',
|
||||
status: point.suggestion_message_type || '',
|
||||
content: data,
|
||||
suggestion: point.suggestion_message || '',
|
||||
result: result.evaluated_results?.result, // 记录评查结果,用于统计
|
||||
score: point.score || 0
|
||||
score: point.score || 0,
|
||||
legalBasis: point.references_laws || {}
|
||||
// legalBasis: {
|
||||
// name: '中华人民共和国食品安全法',
|
||||
// content: '中华人民共和国食品安全法',
|
||||
// article: [
|
||||
// {
|
||||
// name: '中华人民共和国食品安全法',
|
||||
// content: '中华人民共和国食品安全法'
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
};
|
||||
});
|
||||
|
||||
@@ -233,5 +246,30 @@ export async function getReviewPoints(fileId: string) {
|
||||
stats.score += item.score || 0;
|
||||
});
|
||||
|
||||
return { data: resultData, stats };
|
||||
// 构建文件信息-评查信息的数据
|
||||
// 找出最新的评查时间
|
||||
let latestUpdatedAt = '';
|
||||
evaluationResultsData.forEach(result => {
|
||||
if (result.updated_at && (!latestUpdatedAt || result.updated_at > latestUpdatedAt)) {
|
||||
latestUpdatedAt = result.updated_at.toString();
|
||||
}
|
||||
});
|
||||
|
||||
// 提取不重复的规则组名称
|
||||
const uniqueGroups = Array.from(new Set(resultData.map(item => item.groupName))).filter(Boolean);
|
||||
|
||||
// 计算问题数量
|
||||
const issueCount = stats.warning + stats.error;
|
||||
|
||||
// 构建评查信息对象
|
||||
const reviewInfo = {
|
||||
reviewTime: formatDate(latestUpdatedAt),
|
||||
reviewModel: 'DeepSeek',
|
||||
ruleGroup: uniqueGroups.join('、'),
|
||||
result: issueCount > 0 ? 'warning' : 'success',
|
||||
issueCount: issueCount
|
||||
};
|
||||
// console.log("reviewInfo-------",JSON.stringify(reviewInfo,null,2));
|
||||
|
||||
return { data: resultData, stats, reviewInfo };
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ export interface DocumentSearchParams {
|
||||
function formatDate(dateString: string): string {
|
||||
if (!dateString) return '';
|
||||
try {
|
||||
return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss');
|
||||
return dayjs(dateString).format('YYYY-MM-DD');
|
||||
} catch (error) {
|
||||
console.error('日期格式化失败:', error);
|
||||
return dateString;
|
||||
@@ -215,7 +215,12 @@ export async function getReviewFiles(searchParams: DocumentSearchParams = {}): P
|
||||
case 'upload_time_asc':
|
||||
params.order = 'created_at.asc';
|
||||
break;
|
||||
// 其他排序方式可以在这里添加
|
||||
// case 'issue_count_desc':
|
||||
// params.order = 'issue_count.desc';
|
||||
// break;
|
||||
// case 'issue_count_asc':
|
||||
// params.order = 'issue_count.asc';
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,25 @@ function getFileExtension(filename: string): string {
|
||||
return parts.length > 1 ? parts.pop()?.toLowerCase() || '' : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评查结果
|
||||
* @param id 评查结果ID
|
||||
* @returns 评查结果
|
||||
*/
|
||||
async function getEvaluationResults(id: number) {
|
||||
const response = await postgrestGet<[]>('evaluation_results', {
|
||||
filter: {
|
||||
'document_id': `eq.${id}`
|
||||
}
|
||||
});
|
||||
if (response.error) {
|
||||
return { error: response.error, status: response.status };
|
||||
}
|
||||
const evaluationResult = extractApiData<[]>(response.data);
|
||||
return evaluationResult;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将API文档转换为UI文档
|
||||
*/
|
||||
@@ -122,6 +141,24 @@ async function convertToUIDocument(doc: Document): Promise<DocumentUI> {
|
||||
const typeResponse = await getDocumentTypes();
|
||||
const documentTypes = typeResponse.data?.types || [];
|
||||
const docType = documentTypes.find(type => type.id.toString() === doc.type_id.toString());
|
||||
const evaluationResult = await getEvaluationResults(doc.id);
|
||||
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,
|
||||
@@ -132,7 +169,7 @@ async function convertToUIDocument(doc: Document): Promise<DocumentUI> {
|
||||
size: doc.file_size,
|
||||
auditStatus: doc.audit_status || 0,
|
||||
fileStatus: doc.status || '', // 默认为''
|
||||
issues: 0, // 固定为0
|
||||
issues: issues, // 使用计算得到的issues
|
||||
uploadTime: formatDate(doc.updated_at),
|
||||
fileType: getFileExtension(doc.name),
|
||||
path: doc.path,
|
||||
|
||||
@@ -40,6 +40,7 @@ function extractApiData<T>(responseData: unknown): T | null {
|
||||
|
||||
// 文档状态枚举
|
||||
export enum DocumentStatus {
|
||||
waiting = 'waiting',
|
||||
WAITING = "Waiting",
|
||||
CUTTING = "Cutting",
|
||||
EXTRACTIONING = "Extractioning",
|
||||
@@ -175,7 +176,8 @@ export async function uploadDocumentToServer(
|
||||
|
||||
// 发送请求
|
||||
// const response = await fetch(`${API_BASE_URL}/admin/documents/upload`, {
|
||||
const response = await fetch('http://172.16.0.119:8000/admin/documents/upload', {
|
||||
const response = await fetch('http://172.16.0.55:8000/admin/documents/upload', {
|
||||
// const response = await fetch('http://172.16.0.119:8000/admin/documents/upload', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-File-Name': encodeURIComponent(fileName)
|
||||
|
||||
@@ -44,6 +44,7 @@ interface FileDetailsProps {
|
||||
|
||||
export function FileDetails({ fileInfo, contractInfo, reviewInfo }: FileDetailsProps) {
|
||||
// 情况状态对应的标签
|
||||
const controlContractShow = false
|
||||
const renderResultBadge = (result: string) => {
|
||||
switch (result) {
|
||||
case 'success':
|
||||
@@ -75,11 +76,39 @@ export function FileDetails({ fileInfo, contractInfo, reviewInfo }: FileDetailsP
|
||||
|
||||
// 渲染信息区块
|
||||
const renderInfoSection = (title: string, icon: string, color: string, children: ReactNode) => {
|
||||
// 根据color参数返回对应的具体类名
|
||||
const getBgClass = (colorName: string) => {
|
||||
switch(colorName) {
|
||||
case 'blue': return 'bg-blue-50';
|
||||
case 'green': return 'bg-green-50';
|
||||
case 'purple': return 'bg-purple-50';
|
||||
default: return 'bg-gray-50';
|
||||
}
|
||||
};
|
||||
|
||||
const getTextClass = (colorName: string) => {
|
||||
switch(colorName) {
|
||||
case 'blue': return 'text-blue-500';
|
||||
case 'green': return 'text-green-500';
|
||||
case 'purple': return 'text-purple-500';
|
||||
default: return 'text-gray-500';
|
||||
}
|
||||
};
|
||||
|
||||
const getTextTitleClass = (colorName: string) => {
|
||||
switch(colorName) {
|
||||
case 'blue': return 'text-blue-700';
|
||||
case 'green': return 'text-green-700';
|
||||
case 'purple': return 'text-purple-700';
|
||||
default: return 'text-gray-700';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="info-section">
|
||||
<div className={`info-header bg-${color}-50`}>
|
||||
<i className={`${icon} text-${color}-500 text-lg mr-2`}></i>
|
||||
<h3 className={`font-medium text-${color}-700`}>{title}</h3>
|
||||
<div className={`info-header ${getBgClass(color)}`}>
|
||||
<i className={`${icon} ${getTextClass(color)} text-lg mr-2`}></i>
|
||||
<h3 className={`font-medium ${getTextTitleClass(color)}`}>{title}</h3>
|
||||
</div>
|
||||
<div className="info-content">
|
||||
{children}
|
||||
@@ -114,20 +143,22 @@ export function FileDetails({ fileInfo, contractInfo, reviewInfo }: FileDetailsP
|
||||
))}
|
||||
|
||||
{/* 合同信息 */}
|
||||
{renderInfoSection('合同信息', 'ri-file-paper-2-line', 'green', (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{renderInfoRow('合同类型', contractInfo.contractType)}
|
||||
{renderInfoRow('签约日期', contractInfo.signDate)}
|
||||
{renderInfoRow('合同当事人', (
|
||||
<div>
|
||||
<div>甲方:{contractInfo.parties.partyA}</div>
|
||||
<div>乙方:{contractInfo.parties.partyB}</div>
|
||||
</div>
|
||||
))}
|
||||
{renderInfoRow('合同金额', contractInfo.amount)}
|
||||
{renderInfoRow('履行期限', contractInfo.period)}
|
||||
</div>
|
||||
))}
|
||||
{ controlContractShow && (
|
||||
renderInfoSection('合同信息', 'ri-file-paper-2-line', 'green', (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{renderInfoRow('合同类型', contractInfo.contractType)}
|
||||
{renderInfoRow('签约日期', contractInfo.signDate)}
|
||||
{renderInfoRow('合同当事人', (
|
||||
<div>
|
||||
<div>甲方:{contractInfo.parties.partyA}</div>
|
||||
<div>乙方:{contractInfo.parties.partyB}</div>
|
||||
</div>
|
||||
))}
|
||||
{renderInfoRow('合同金额', contractInfo.amount)}
|
||||
{renderInfoRow('履行期限', contractInfo.period)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* 评查信息 */}
|
||||
{renderInfoSection('评查信息', 'ri-search-eye-line', 'purple', (
|
||||
@@ -139,7 +170,7 @@ export function FileDetails({ fileInfo, contractInfo, reviewInfo }: FileDetailsP
|
||||
{renderInfoRow('评查结果', (
|
||||
<div className="flex items-center">
|
||||
{renderResultBadge(reviewInfo.result)}
|
||||
<span className="text-sm ml-2">(共发现{reviewInfo.issueCount}个问题)</span>
|
||||
{reviewInfo.issueCount > 0 && <span className="text-sm ml-2">(共发现{reviewInfo.issueCount}个问题)</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -26,17 +26,17 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
||||
<div className="mb-4 file-info-header">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-xl font-medium">
|
||||
<h2 className="text-xl font-medium max-w-xl">
|
||||
{fileInfo.fileName}
|
||||
<span className="text-sm text-secondary font-normal ml-2">
|
||||
合同编号:{fileInfo.contractNumber}
|
||||
</span>
|
||||
{fileInfo.fileSize && (
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
{fileInfo.fileSize} | {fileInfo.fileFormat} | {fileInfo.pageCount}页
|
||||
</span>
|
||||
)}
|
||||
</h2>
|
||||
<span className="text-xs text-gray-500">
|
||||
合同编号:{fileInfo.contractNumber}
|
||||
</span>
|
||||
{fileInfo.fileSize && (
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
{fileInfo.fileSize} | {fileInfo.fileFormat} | {fileInfo.pageCount}页
|
||||
</span>
|
||||
)}
|
||||
{fileInfo.uploadTime && (
|
||||
<div className="text-xs text-gray-500 mt-1">
|
||||
上传时间:{fileInfo.uploadTime} | 上传用户:{fileInfo.uploadUser}
|
||||
|
||||
@@ -4,18 +4,8 @@
|
||||
*/
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
// 定义评查点类型
|
||||
interface ReviewPoint {
|
||||
id: string;
|
||||
title: string;
|
||||
status: string;
|
||||
content: string;
|
||||
suggestion: string;
|
||||
position?: {
|
||||
section: string;
|
||||
index: number;
|
||||
};
|
||||
}
|
||||
// 导入统一的ReviewPoint类型
|
||||
import { type ReviewPoint } from './';
|
||||
|
||||
// 定义文档内容类型
|
||||
interface FileContent {
|
||||
|
||||
@@ -39,6 +39,12 @@ export interface ReviewPoint {
|
||||
index: number;
|
||||
};
|
||||
result?: boolean;
|
||||
legalBasis?: {
|
||||
name?: string;
|
||||
content?: string;
|
||||
articles?: Array<string | { name?: string; content?: string; [key: string]: unknown }>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
// 统计数据类型
|
||||
@@ -70,6 +76,7 @@ export function ReviewPointsList({
|
||||
const [userInputText, setUserInputText] = useState(''); // 用户输入的审核意见文本
|
||||
const [searchText, setSearchText] = useState(''); // 搜索文本
|
||||
const [statusFilter, setStatusFilter] = useState<string | null>(null); // 状态过滤
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const [suggestionTexts, setSuggestionTexts] = useState<Record<string, string>>({}); // 存储每个评查点的建议文本
|
||||
|
||||
// 初始化建议文本
|
||||
@@ -83,6 +90,7 @@ export function ReviewPointsList({
|
||||
}, [reviewPoints]);
|
||||
|
||||
// 处理建议文本变更
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const handleSuggestionChange = (reviewPointId: string, text: string) => {
|
||||
setSuggestionTexts(prev => ({
|
||||
...prev,
|
||||
@@ -135,7 +143,7 @@ export function ReviewPointsList({
|
||||
alert(`将为评查点 ${reviewPointId} 执行一键替换操作`);
|
||||
|
||||
// 更新评查点状态为成功
|
||||
onStatusChange(reviewPointId, 'success');
|
||||
// onStatusChange(reviewPointId, 'success');
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -206,9 +214,17 @@ export function ReviewPointsList({
|
||||
<div className="flex justify-between items-center">
|
||||
{/* 总计数量 */}
|
||||
<div className="flex items-center">
|
||||
<div className="w-7 h-7 bg-gray-100 rounded-md flex items-center justify-center">
|
||||
<button
|
||||
className={`w-7 h-7 bg-gray-100 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === null && searchText === '' ? 'ring-2 ring-gray-400' : ''}`}
|
||||
onClick={() => {
|
||||
setStatusFilter(null);
|
||||
setSearchText('');
|
||||
}}
|
||||
aria-label="显示所有评查点"
|
||||
type="button"
|
||||
>
|
||||
<span className="text-sm font-semibold text-gray-600">{totalToShow}</span>
|
||||
</div>
|
||||
</button>
|
||||
<span className="text-xs text-gray-500 ml-1">总计</span>
|
||||
</div>
|
||||
<div className="h-8 border-r border-gray-200"></div>
|
||||
@@ -422,75 +438,120 @@ export function ReviewPointsList({
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
{/* 内容显示区域 */}
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3">
|
||||
{/* 移除顶部的"当前值"标题,在每个内容项中显示 */}
|
||||
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
||||
// 当 content 是对象时的渲染方式
|
||||
<div>
|
||||
{Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
<div key={index} className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0">
|
||||
{/* 使用flex布局使key和状态标签左右对齐 */}
|
||||
{reviewPoint.content !== null && (
|
||||
(typeof reviewPoint.content === 'string' && reviewPoint.content !== '') ||
|
||||
(typeof reviewPoint.content === 'object' && Object.keys(reviewPoint.content).length > 0)
|
||||
) && (
|
||||
<>
|
||||
{/* 内容显示区域 */}
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3">
|
||||
{/* 移除顶部的"当前值"标题,在每个内容项中显示 */}
|
||||
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
||||
// 当 content 是对象时的渲染方式
|
||||
<div>
|
||||
{Object.entries(reviewPoint.content).map(([key, value], index) => (
|
||||
<div key={index} className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-0">
|
||||
{/* 使用flex布局使key和状态标签左右对齐 */}
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">{key}</span>
|
||||
{/* <span className={`text-xs ${isErrorStatus ? 'text-error' : 'text-warning'}`}>
|
||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||
</span> */}
|
||||
<span className={`text-xs ${isErrorStatus ? 'text-error' : 'text-warning'}`}>
|
||||
{value ? '' : '缺失'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-left">{value || ' '}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
// 当 content 是字符串时的渲染方式
|
||||
<>
|
||||
{/* 为字符串内容也添加标题和状态 */}
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">{key}</span>
|
||||
<span className="text-xs">当前值</span>
|
||||
<span className={`text-xs ${isErrorStatus ? 'text-error' : 'text-warning'}`}>
|
||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-left">{value || '(内容为空)'}</p>
|
||||
</div>
|
||||
))}
|
||||
<p className="text-xs text-left">{reviewPoint.content || '(内容为空)'}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 当 content 是字符串时的渲染方式
|
||||
<>
|
||||
{/* 为字符串内容也添加标题和状态 */}
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs">当前值</span>
|
||||
<span className={`text-xs ${isErrorStatus ? 'text-error' : 'text-warning'}`}>
|
||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||
</span>
|
||||
|
||||
{/* 法律依据内容 */}
|
||||
{reviewPoint.legalBasis && (typeof reviewPoint.legalBasis === 'object') && (
|
||||
(reviewPoint.legalBasis.name || reviewPoint.legalBasis.content ||
|
||||
(reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && (
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<span className="text-xs font-medium">法律依据</span>
|
||||
</div>
|
||||
{reviewPoint.legalBasis.name && (
|
||||
<p className="text-xs text-left mb-1">{reviewPoint.legalBasis.name}</p>
|
||||
)}
|
||||
{reviewPoint.legalBasis.content && (
|
||||
<p className="text-xs text-left mb-1"><span className="font-medium">条款内容:</span>{reviewPoint.legalBasis.content}</p>
|
||||
)}
|
||||
{reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-left font-medium mb-1">相关条款:</p>
|
||||
<ul className="list-disc pl-4">
|
||||
{reviewPoint.legalBasis.articles.map((item, index) => (
|
||||
<li key={index} className="text-xs text-left">
|
||||
{typeof item === 'string' ? item :
|
||||
typeof item === 'object' && item !== null ?
|
||||
(item.name ? `${item.name}: ${item.content || ''}` :
|
||||
item.content || JSON.stringify(item)) :
|
||||
String(item)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
|
||||
{/* 建议修改区域 */}
|
||||
<div className="mb-2">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-700">建议修改为:</span>
|
||||
{/* <span className="text-green-500">符合规范</span> */}
|
||||
</div>
|
||||
<p className="text-xs text-left">{reviewPoint.content || '(内容为空)'}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<textarea
|
||||
value={suggestionTexts[reviewPoint.id] || ''}
|
||||
onChange={(e) => handleSuggestionChange(reviewPoint.id, e.target.value)}
|
||||
className="w-full p-2 border rounded bg-white min-h-[100px] focus:outline-none focus:border-[#00684a] focus:shadow-[0_0_0_2px_rgba(0,104,74,0.2)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 建议修改区域 */}
|
||||
<div className="mb-2">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-700">建议修改为:</span>
|
||||
<span className="text-green-500">符合规范</span>
|
||||
</div>
|
||||
<textarea
|
||||
value={suggestionTexts[reviewPoint.id] || ''}
|
||||
onChange={(e) => handleSuggestionChange(reviewPoint.id, e.target.value)}
|
||||
className="w-full p-2 border rounded bg-gray-50 min-h-[100px] focus:outline-none focus:border-[#00684a] focus:shadow-[0_0_0_2px_rgba(0,104,74,0.2)]"
|
||||
/>
|
||||
</div>
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex space-x-2 mt-2">
|
||||
{/* 一键替换按钮 - 只有非人工审核的点或未通过的人工审核点才显示 */}
|
||||
{(!reviewPoint.needsHumanReview || (!reviewPoint.result && reviewPoint.status !== 'success')) && (
|
||||
<button
|
||||
className="replace-action flex-1 justify-center"
|
||||
onClick={() => handleReplace(reviewPoint.id)}
|
||||
>
|
||||
<i className="ri-replace-line"></i> 一键替换
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 人工审核按钮 */}
|
||||
{reviewPoint.needsHumanReview && !reviewPoint.result && reviewPoint.status !== 'success' && (
|
||||
<button
|
||||
className="replace-action flex-1 justify-center human-review-request"
|
||||
onClick={() => handleEditReviewPoint(reviewPoint.id)}
|
||||
>
|
||||
<i className="ri-edit-line"></i> 人工审核
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex space-x-2 mt-2">
|
||||
{/* 一键替换按钮 - 只有非人工审核的点或未通过的人工审核点才显示 */}
|
||||
{(!reviewPoint.needsHumanReview || (!reviewPoint.result && reviewPoint.status !== 'success')) && (
|
||||
<button
|
||||
className="replace-action flex-1 justify-center"
|
||||
onClick={() => handleReplace(reviewPoint.id)}
|
||||
>
|
||||
<i className="ri-replace-line"></i> 一键替换
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 人工审核按钮 */}
|
||||
{reviewPoint.needsHumanReview && !reviewPoint.result && reviewPoint.status !== 'success' && (
|
||||
<button
|
||||
className="replace-action flex-1 justify-center human-review-request"
|
||||
onClick={() => handleEditReviewPoint(reviewPoint.id)}
|
||||
>
|
||||
<i className="ri-edit-line"></i> 人工审核
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -105,4 +105,4 @@ export function Table<T extends Record<string, any>>({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-3
@@ -3,15 +3,23 @@ import { type MetaFunction } from "@remix-run/node";
|
||||
import { useLoaderData } from "@remix-run/react";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { StatusBadge, links as statusBadgeLinks } from "~/components/ui/StatusBadge";
|
||||
import { FileTag, links as fileTagLinks } from "~/components/ui/FileTag";
|
||||
// import { FileTypeTag, links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
|
||||
import { Tag } from "~/components/ui/Tag";
|
||||
import homeStyles from "~/styles/pages/home.css?url";
|
||||
import { getDocuments, type DocumentUI } from "~/api/files/documents";
|
||||
|
||||
// 文件处理状态选项
|
||||
const fileProcessingStatusOptions = [
|
||||
{ value: "Waiting", label: "上传中", icon: "ri-loader-line", color: "blue" },
|
||||
{ value: "Cutting", label: "切分中", icon: "ri-loader-line", color: "purple" },
|
||||
{ value: "Extractioning", label: "抽取中", icon: "ri-loader-line", color: "cyan" },
|
||||
{ value: "Evaluationing", label: "评查中", icon: "ri-loader-line", color: "teal" },
|
||||
{ value: "Processed", label: "已完成", icon: "ri-check-line", color: "green" },
|
||||
];
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: homeStyles },
|
||||
...statusBadgeLinks(),
|
||||
...fileTagLinks()
|
||||
];
|
||||
|
||||
@@ -158,7 +166,18 @@ export default function Index() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="doc-status">
|
||||
<StatusBadge status={file.fileStatus} />
|
||||
{(() => {
|
||||
const fileStatus = file.fileStatus || "-";
|
||||
const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) ||
|
||||
fileProcessingStatusOptions[0];
|
||||
const isSpinning = fileStatus !== "Processed";
|
||||
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>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -56,6 +56,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||||
|
||||
// 获取文档列表
|
||||
const documentsResponse = await getDocuments(searchParams);
|
||||
// console.log('documentsResponse---1--',JSON.stringify(documentsResponse,null,2));
|
||||
if (documentsResponse.error) {
|
||||
throw new Error(documentsResponse.error);
|
||||
}
|
||||
@@ -436,17 +437,17 @@ export default function DocumentsIndex() {
|
||||
key: "name",
|
||||
width:'25%',
|
||||
render: (_: unknown, record: DocumentUI) => (
|
||||
<div className="flex items-center m-1">
|
||||
<div className="flex m-1">
|
||||
<FileTag
|
||||
extension={record.fileType}
|
||||
showIcon={true}
|
||||
showText={false}
|
||||
showBackground={false}
|
||||
size="lg"
|
||||
className="mr-2 flex-shrink-0"
|
||||
className="mr-2 flex-shrink-0 self-center"
|
||||
/>
|
||||
<div className="overflow-hidden">
|
||||
<span className="file-name break-words block" title={record.name}>{record.name}</span>
|
||||
<span className="file-name break-words block whitespace-normal leading-normal" title={record.name}>{record.name}</span>
|
||||
<div className="mt-2 flex inline-block">
|
||||
<FileTypeTag
|
||||
type={record.type}
|
||||
@@ -484,7 +485,7 @@ export default function DocumentsIndex() {
|
||||
render: (_: unknown, record: DocumentUI) => {
|
||||
// 处理fileStatus为null或undefined的情况
|
||||
// console.log('fileStatus',record.fileStatus)
|
||||
const fileStatus = record.fileStatus || "Processed";
|
||||
const fileStatus = record.fileStatus || "-";
|
||||
const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) ||
|
||||
fileProcessingStatusOptions[0];
|
||||
const isSpinning = fileStatus !== "Processed";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { MetaFunction, ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { Form, useActionData, useLoaderData } from "@remix-run/react";
|
||||
import { Form, useActionData, useLoaderData, useNavigate } from "@remix-run/react";
|
||||
import { Card } from "~/components/ui/Card";
|
||||
import { Button } from "~/components/ui/Button";
|
||||
import { Table } from "~/components/ui/Table";
|
||||
@@ -288,6 +288,7 @@ export default function FilesUpload() {
|
||||
{ title: "评查点审核", description: "DeepSeek 评查中", status: "waiting" },
|
||||
{ title: "审核准备", description: "文档已准备就绪,等待审核", status: "waiting" }
|
||||
]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 队列文件状态
|
||||
const [queueFiles, setQueueFiles] = useState<Document[]>(documents);
|
||||
@@ -823,6 +824,11 @@ export default function FilesUpload() {
|
||||
let statusText = "";
|
||||
|
||||
switch(record.status) {
|
||||
case 'waiting':
|
||||
statusClass = "status-processing";
|
||||
statusIcon = "ri-loader-4-line";
|
||||
statusText = "等待中";
|
||||
break;
|
||||
case DocumentStatus.WAITING:
|
||||
statusClass = "status-processing";
|
||||
statusIcon = "ri-loader-4-line";
|
||||
@@ -854,6 +860,7 @@ export default function FilesUpload() {
|
||||
statusText = "已完成";
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<span className={`status-badge ${statusClass}`}>
|
||||
@@ -873,7 +880,7 @@ export default function FilesUpload() {
|
||||
size="small"
|
||||
disabled={record.status !== DocumentStatus.PROCESSED}
|
||||
icon="ri-eye-line"
|
||||
onClick={() => alert(`查看文件详情: ${record.name}`)}
|
||||
onClick={() => navigate(`/reviews?id=${record.id}`)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
@@ -1144,14 +1151,15 @@ export default function FilesUpload() {
|
||||
<p className="text-sm text-green-700">文件已成功上传并评查完成,请查看结果</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
{/* <div className="flex justify-end">
|
||||
<Button
|
||||
type="primary"
|
||||
icon="ri-file-search-line"
|
||||
|
||||
>
|
||||
查看详情并审核
|
||||
</Button>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
}
|
||||
const reviewData = await getReviewPoints(id);
|
||||
|
||||
console.log("reviewData-------",reviewData);
|
||||
console.log("reviewData-------",JSON.stringify(reviewData.data,null,2));
|
||||
if (reviewData.error) {
|
||||
console.error("获取评查点数据错误:", reviewData.error);
|
||||
return Response.json({ error: reviewData.error }, { status: reviewData.status || 500 });
|
||||
@@ -192,6 +192,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
return Response.json({
|
||||
document: documentData.data,
|
||||
reviewPoints: reviewData.data,
|
||||
reviewInfo: reviewData.reviewInfo,
|
||||
statistics: reviewData.stats
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -202,7 +203,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
export default function ReviewDetails() {
|
||||
const navigate = useNavigate();
|
||||
const { document, reviewPoints, statistics } = useLoaderData<typeof loader>();
|
||||
const { document, reviewPoints, statistics, reviewInfo } = useLoaderData<typeof loader>();
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
const [activeTab, setActiveTab] = useState<string>('preview'); // 'preview', 'analysis', 'fileinfo'
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||
@@ -222,6 +223,7 @@ export default function ReviewDetails() {
|
||||
uploadTime: document.uploadTime || "未知时间",
|
||||
uploadUser: document.uploadUser || "未知用户",
|
||||
auditStatus: document.auditStatus || 0,
|
||||
legalBasis: document.legalBasis || {}
|
||||
};
|
||||
|
||||
// 创建包含真实文档数据的评查数据对象
|
||||
@@ -230,16 +232,17 @@ export default function ReviewDetails() {
|
||||
fileInfo: fileInfo,
|
||||
// 其他字段暂时使用默认值
|
||||
contractInfo: getMockReviewData().contractInfo,
|
||||
reviewInfo: getMockReviewData().reviewInfo,
|
||||
reviewInfo: reviewInfo,
|
||||
statistics: statistics,
|
||||
fileContent: getMockReviewData().fileContent,
|
||||
reviewPoints: reviewPoints,
|
||||
aiAnalysis: getMockReviewData().aiAnalysis,
|
||||
};
|
||||
console.log("reviewDataObj-------",reviewDataObj);
|
||||
|
||||
setReviewData(reviewDataObj);
|
||||
setIsLoading(false);
|
||||
}, [document, reviewPoints, statistics]);
|
||||
}, [document, reviewPoints, statistics, reviewInfo]);
|
||||
|
||||
const handleTabChange = (tabKey: string) => {
|
||||
setActiveTab(tabKey);
|
||||
|
||||
+26
-50
@@ -163,19 +163,6 @@ export default function RulesFiles() {
|
||||
setSearchParams(newParams);
|
||||
};
|
||||
|
||||
// 处理确认评查状态
|
||||
const handleConfirmStatus = async (id: string, status: string) => {
|
||||
try {
|
||||
await updateReviewStatus(id, status);
|
||||
// 刷新页面获取最新数据
|
||||
const newParams = new URLSearchParams(searchParams);
|
||||
setSearchParams(newParams);
|
||||
} catch (error) {
|
||||
console.error('更新评查状态失败:', error);
|
||||
// 可以在这里添加错误提示
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染问题摘要
|
||||
const renderIssues = (file: ReviewFileUI) => {
|
||||
// 如果评查状态为通过,显示"所有评查点均通过"
|
||||
@@ -220,11 +207,13 @@ export default function RulesFiles() {
|
||||
key: "fileName",
|
||||
width: "30%",
|
||||
render: (_: unknown, file: ReviewFileUI) => (
|
||||
<div className="flex items-center">
|
||||
<FileIcon fileName={file.fileName} className="mr-2 text-lg flex-shrink-0 w-10 h-10" />
|
||||
<div>
|
||||
<div className="font-normal text-base break-words" title={file.fileName}>{file.fileName}</div>
|
||||
<div className="text-xs text-secondary mt-1">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0 flex items-center self-center">
|
||||
<FileIcon fileName={file.fileName} className="text-lg w-10 h-10" />
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 flex flex-col py-2 ml-2">
|
||||
<div className="font-normal text-base break-words whitespace-normal leading-normal" title={file.fileName}>{file.fileName}</div>
|
||||
<div className="text-xs text-secondary mt-2">
|
||||
文件编号:{file.fileCode}
|
||||
</div>
|
||||
</div>
|
||||
@@ -283,27 +272,16 @@ export default function RulesFiles() {
|
||||
width: "14%",
|
||||
render: (_: unknown, file: ReviewFileUI) => (
|
||||
<>
|
||||
{file.reviewStatus === 'pending' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon="ri-check-double-line"
|
||||
className="mr-2"
|
||||
onClick={() => handleConfirmStatus(file.id, 'pass')}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-eye-line"
|
||||
to={`/files/${file.id}`}
|
||||
className="mr-2"
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="default"
|
||||
size="small"
|
||||
icon="ri-eye-line"
|
||||
to={`/reviews?id=${file.id}`}
|
||||
className="mr-2"
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
|
||||
<Button type="default" size="small" icon="ri-download-2-line">
|
||||
下载
|
||||
</Button>
|
||||
@@ -357,16 +335,6 @@ export default function RulesFiles() {
|
||||
onChange={handleFilterChange}
|
||||
className="mr-2 w-40"
|
||||
/>
|
||||
|
||||
<SearchFilter
|
||||
label="搜索"
|
||||
placeholder="搜索文件名、合同编号或关键词"
|
||||
value={searchParams.get('keyword') || ''}
|
||||
onSearch={handleSearch}
|
||||
buttonText=""
|
||||
className="mr-2 w-64"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="排序方式"
|
||||
name="sortOrder"
|
||||
@@ -380,6 +348,14 @@ export default function RulesFiles() {
|
||||
{ value: "issue_count_asc", label: "问题数量 ↑" }
|
||||
]}
|
||||
/>
|
||||
<SearchFilter
|
||||
label="搜索"
|
||||
placeholder="搜索文件名、合同编号或关键词"
|
||||
value={searchParams.get('keyword') || ''}
|
||||
onSearch={handleSearch}
|
||||
buttonText=""
|
||||
className="mr-2 flex-1"
|
||||
/>
|
||||
</FilterPanel>
|
||||
|
||||
{/* 文件列表 */}
|
||||
@@ -389,7 +365,7 @@ export default function RulesFiles() {
|
||||
dataSource={files}
|
||||
rowKey="id"
|
||||
emptyText="暂无文件数据"
|
||||
className="files-table"
|
||||
className="files-table table-auto-height"
|
||||
/>
|
||||
|
||||
{/* 分页组件 */}
|
||||
|
||||
@@ -27,6 +27,17 @@
|
||||
@apply bg-white hover:bg-gray-50 transition-colors duration-150;
|
||||
}
|
||||
|
||||
/* 自动高度表格样式 - 全局可用 */
|
||||
.table-auto-height tr {
|
||||
height: auto !important;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.table-auto-height td {
|
||||
height: auto !important;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.ant-table-empty {
|
||||
@apply py-12 text-center text-gray-500;
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
max-width: 100%;
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
line-height: 1.4;
|
||||
white-space: normal;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.document-number {
|
||||
|
||||
@@ -477,3 +477,25 @@
|
||||
text-align: center !important;
|
||||
vertical-align: middle !important;
|
||||
} */
|
||||
|
||||
/* 新增:确保表格行能根据内容自动调整高度 */
|
||||
/* .review-files-page .files-table tr {
|
||||
height: auto;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.review-files-page .files-table td {
|
||||
height: auto;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.review-files-page .files-table .break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.review-files-page .files-table .whitespace-pre-wrap {
|
||||
white-space: pre-wrap;
|
||||
} */
|
||||
|
||||
|
||||
Reference in New Issue
Block a user