Merge branch 'Wren-dev' into shiy-login
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config';
|
import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE } from '~/constants/contractTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从不同格式的 API 响应中提取数据
|
* 从不同格式的 API 响应中提取数据
|
||||||
@@ -224,7 +225,8 @@ export async function batchUploadAndAssignCrossCheckingFiles(
|
|||||||
docType: string,
|
docType: string,
|
||||||
taskType: string = '市局间交叉评查',
|
taskType: string = '市局间交叉评查',
|
||||||
token: string | null = null,
|
token: string | null = null,
|
||||||
principalUserIds: number[] = []
|
principalUserIds: number[] = [],
|
||||||
|
attributeType?: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
successes: Array<{file: CrossCheckingUploadedFile; result: Record<string, unknown>}>;
|
successes: Array<{file: CrossCheckingUploadedFile; result: Record<string, unknown>}>;
|
||||||
failures: Array<{file: CrossCheckingUploadedFile; error: string}>;
|
failures: Array<{file: CrossCheckingUploadedFile; error: string}>;
|
||||||
@@ -249,7 +251,8 @@ export async function batchUploadAndAssignCrossCheckingFiles(
|
|||||||
is_test_document: isTestDocument,
|
is_test_document: isTestDocument,
|
||||||
task_name: taskName,
|
task_name: taskName,
|
||||||
doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType,
|
doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType,
|
||||||
task_type: taskType
|
task_type: taskType,
|
||||||
|
attribute_type: attributeType || null
|
||||||
};
|
};
|
||||||
// console.log('fileInfo', fileInfo)
|
// console.log('fileInfo', fileInfo)
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,53 @@ interface StatsData {
|
|||||||
score: number;
|
score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphRAG Scored 评查结果类型
|
||||||
|
interface FieldScore {
|
||||||
|
field_path: string;
|
||||||
|
evaluation_as: string;
|
||||||
|
weight: number;
|
||||||
|
scored: number;
|
||||||
|
max_score: number;
|
||||||
|
status: string; // 'filled' | 'placeholder'
|
||||||
|
value: string;
|
||||||
|
page?: string;
|
||||||
|
ai_feedback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScoredEvaluationResult {
|
||||||
|
evaluation_point_id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
passed: boolean;
|
||||||
|
machine_score: number;
|
||||||
|
score: number;
|
||||||
|
percentage: number;
|
||||||
|
total_score: number;
|
||||||
|
total_weight: number;
|
||||||
|
pass_threshold: number;
|
||||||
|
result_type: 'scored';
|
||||||
|
field_results: FieldScore[];
|
||||||
|
missing_fields?: string[];
|
||||||
|
ai_suggestion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EvaluationSummary {
|
||||||
|
total_points: number;
|
||||||
|
passed_count: number;
|
||||||
|
failed_count: number;
|
||||||
|
total_score: number;
|
||||||
|
total_full_score: number;
|
||||||
|
average_percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UnifiedEvaluationResponse {
|
||||||
|
document_id: number;
|
||||||
|
flow_type: 'graphrag' | 'legacy';
|
||||||
|
results: ScoredEvaluationResult[];
|
||||||
|
summary: EvaluationSummary;
|
||||||
|
evaluated_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
// 在文件顶部添加的类型定义,在interface区块前添加
|
// 在文件顶部添加的类型定义,在interface区块前添加
|
||||||
interface OcrDataResult {
|
interface OcrDataResult {
|
||||||
pages?: number[];
|
pages?: number[];
|
||||||
@@ -1126,3 +1173,49 @@ export async function getReviewPoints_fromApi(fileId: string, request: Request)
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取统一评查结果(GraphRAG Scored 模式)
|
||||||
|
*
|
||||||
|
* @param fileId 文档ID
|
||||||
|
* @param request Remix请求对象
|
||||||
|
* @returns 统一评查结果
|
||||||
|
*/
|
||||||
|
export async function getUnifiedEvaluationResults(fileId: string, request: Request) {
|
||||||
|
try {
|
||||||
|
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||||
|
if (!userInfo?.user_id) {
|
||||||
|
return { error: '用户身份验证失败', status: 401 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await apiRequest<UnifiedEvaluationResponse>(
|
||||||
|
`/api/v2/evaluation/results-unified/${fileId}`,
|
||||||
|
{
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${frontendJWT}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.error) {
|
||||||
|
console.error('❌ [getUnifiedEvaluationResults] API调用失败:', response.error);
|
||||||
|
return { error: response.error, status: response.status || 500 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data) {
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('❌ [getUnifiedEvaluationResults] API响应数据为空');
|
||||||
|
return { error: 'API响应数据为空', status: 500 };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ [getUnifiedEvaluationResults] 调用失败:', error);
|
||||||
|
return {
|
||||||
|
error: error instanceof Error ? error.message : '获取评查结果失败',
|
||||||
|
status: 500
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { postgrestGet, type PostgrestParams } from '../postgrest-client';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config';
|
import { UPLOAD_URL, API_BASE_URL } from '../../config/api-config';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE, type ContractType } from '~/constants/contractTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查文档名称是否重复
|
* 检查文档名称是否重复
|
||||||
@@ -371,9 +372,9 @@ export async function appendContractAttachments(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadDocumentToServer(
|
export async function uploadDocumentToServer(
|
||||||
binaryData: ArrayBuffer,
|
binaryData: ArrayBuffer,
|
||||||
fileName: string,
|
fileName: string,
|
||||||
fileType: string,
|
fileType: string,
|
||||||
typeId: string | number,
|
typeId: string | number,
|
||||||
priority: string,
|
priority: string,
|
||||||
documentNumber?: string | null,
|
documentNumber?: string | null,
|
||||||
@@ -382,7 +383,8 @@ export async function uploadDocumentToServer(
|
|||||||
documentId?: number | null,
|
documentId?: number | null,
|
||||||
isReupload: boolean = false,
|
isReupload: boolean = false,
|
||||||
jwtToken?: string,
|
jwtToken?: string,
|
||||||
attachments?: File[]
|
attachments?: File[],
|
||||||
|
attributeType?: string
|
||||||
): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
|
): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
|
||||||
try {
|
try {
|
||||||
// console.log('【调试】开始上传文档:', { fileName, fileSize: binaryData.byteLength });
|
// console.log('【调试】开始上传文档:', { fileName, fileSize: binaryData.byteLength });
|
||||||
@@ -397,13 +399,14 @@ export async function uploadDocumentToServer(
|
|||||||
|
|
||||||
// 将信息添加到一个JSON对象中
|
// 将信息添加到一个JSON对象中
|
||||||
const uploadInfo = {
|
const uploadInfo = {
|
||||||
type_id: Number(typeId),
|
type_id: Number(typeId),
|
||||||
evaluation_level: priority,
|
evaluation_level: priority,
|
||||||
document_number: documentNumber || null,
|
document_number: documentNumber || null,
|
||||||
remark: remark || null,
|
remark: remark || null,
|
||||||
is_test_document: isTestDocument,
|
is_test_document: isTestDocument,
|
||||||
document_id: documentId || null,
|
document_id: documentId || null,
|
||||||
is_reupload: isReupload
|
is_reupload: isReupload,
|
||||||
|
attribute_type: attributeType || null
|
||||||
};
|
};
|
||||||
|
|
||||||
// 添加JSON字符串到FormData
|
// 添加JSON字符串到FormData
|
||||||
|
|||||||
@@ -93,6 +93,32 @@ export interface CharPosition {
|
|||||||
score: number; // OCR识别置信度
|
score: number; // OCR识别置信度
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* text_bbox -> CharPosition[] 转换
|
||||||
|
* GraphRAG 抽取结果只有 text_bbox (段落级坐标), 没有 char_positions (字符级坐标)。
|
||||||
|
* 将 text_bbox 转为单个 CharPosition 矩形框, 让 PdfPreview 的高亮逻辑复用。
|
||||||
|
*/
|
||||||
|
function resolveCharPositions(data: any): CharPosition[] | undefined {
|
||||||
|
// 优先用 char_positions
|
||||||
|
if (data?.char_positions && data.char_positions.length > 0) {
|
||||||
|
return data.char_positions;
|
||||||
|
}
|
||||||
|
// fallback: text_bbox -> CharPosition[]
|
||||||
|
if (data?.text_bbox) {
|
||||||
|
const b = data.text_bbox;
|
||||||
|
if (b.x_min != null && b.y_min != null && b.x_max != null && b.y_max != null
|
||||||
|
&& (b.x_max - b.x_min) > 0 && (b.y_max - b.y_min) > 0) {
|
||||||
|
return [{
|
||||||
|
box: [[b.x_min, b.y_min], [b.x_max, b.y_min], [b.x_max, b.y_max], [b.x_min, b.y_max]],
|
||||||
|
char: '',
|
||||||
|
score: 1
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 评查点类型定义
|
* 评查点类型定义
|
||||||
* 用于展示单个评查结果
|
* 用于展示单个评查结果
|
||||||
@@ -1512,7 +1538,7 @@ export function ReviewPointsList({
|
|||||||
for (const item of chain) {
|
for (const item of chain) {
|
||||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||||
hasPage = true;
|
hasPage = true;
|
||||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value);
|
onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data), item.data.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1526,7 +1552,7 @@ export function ReviewPointsList({
|
|||||||
// 遍历chain找到第一个有效的page
|
// 遍历chain找到第一个有效的page
|
||||||
for (const item of chain) {
|
for (const item of chain) {
|
||||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions, item.data.value);
|
onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data), item.data.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1566,11 +1592,11 @@ export function ReviewPointsList({
|
|||||||
// 假设onReviewPointSelect在作用域内可用
|
// 假设onReviewPointSelect在作用域内可用
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions, item.data.value);
|
onReviewPointSelect(reviewPointId, Number(item.data.page), resolveCharPositions(item.data), item.data.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){
|
else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), item.data.char_positions, item.data.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), resolveCharPositions(item.data), item.data.value);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
toastService.error(`没有找到${item.field}对应的索引内容`);
|
toastService.error(`没有找到${item.field}对应的索引内容`);
|
||||||
@@ -1649,11 +1675,11 @@ export function ReviewPointsList({
|
|||||||
if (chain[0].data.page) {
|
if (chain[0].data.page) {
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions, chain[0].data.value);
|
onReviewPointSelect(reviewPointId, chain[0].data.page, resolveCharPositions(chain[0].data), chain[0].data.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){
|
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), chain[0].data.char_positions, chain[0].data.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), resolveCharPositions(chain[0].data), chain[0].data.value);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
|
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
|
||||||
@@ -1675,11 +1701,11 @@ export function ReviewPointsList({
|
|||||||
if (chain[1].data.page) {
|
if (chain[1].data.page) {
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions, chain[1].data.value);
|
onReviewPointSelect(reviewPointId, chain[1].data.page, resolveCharPositions(chain[1].data), chain[1].data.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){
|
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), chain[1].data.char_positions, chain[1].data.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), resolveCharPositions(chain[1].data), chain[1].data.value);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
|
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
|
||||||
@@ -1815,9 +1841,9 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||||
}else{
|
}else{
|
||||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||||
}
|
}
|
||||||
@@ -1826,9 +1852,9 @@ export function ReviewPointsList({
|
|||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||||
}else{
|
}else{
|
||||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||||
}
|
}
|
||||||
@@ -1960,9 +1986,9 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
|
onReviewPointSelect(reviewPoint.id, Number(value.page), resolveCharPositions(value), value.value);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value), value.value);
|
||||||
}else{
|
}else{
|
||||||
toastService.error(`没有找到${key}对应的索引内容`);
|
toastService.error(`没有找到${key}对应的索引内容`);
|
||||||
}
|
}
|
||||||
@@ -1972,9 +1998,9 @@ export function ReviewPointsList({
|
|||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
|
onReviewPointSelect(reviewPoint.id, Number(value.page), resolveCharPositions(value), value.value);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value), value.value);
|
||||||
}else{
|
}else{
|
||||||
toastService.error(`没有找到${key}对应的索引内容`);
|
toastService.error(`没有找到${key}对应的索引内容`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface FieldResultItem {
|
||||||
|
field_path: string;
|
||||||
|
evaluation_as: string;
|
||||||
|
weight: number;
|
||||||
|
scored: number;
|
||||||
|
status: string; // 'filled' | 'placeholder'
|
||||||
|
value: string;
|
||||||
|
page?: string;
|
||||||
|
ai_feedback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FieldResultListProps {
|
||||||
|
items: FieldResultItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FieldResultList({ items }: FieldResultListProps) {
|
||||||
|
return (
|
||||||
|
<div className="field-result-list">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.field_path}
|
||||||
|
className={`field-result-item field-result-item--${item.status}`}
|
||||||
|
>
|
||||||
|
<div className="field-result-item__header">
|
||||||
|
<span className="field-result-item__name">
|
||||||
|
{item.status === 'filled' ? '✅' : '⚠️'} {item.evaluation_as}
|
||||||
|
</span>
|
||||||
|
<span className="field-result-item__score">
|
||||||
|
{item.scored}/{item.weight}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="field-result-item__value">
|
||||||
|
{item.value}
|
||||||
|
</div>
|
||||||
|
{item.ai_feedback && (
|
||||||
|
<div className="field-result-item__feedback">
|
||||||
|
💬 {item.ai_feedback}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ScoreBarProps {
|
||||||
|
score: number; // Actual score (e.g., 3 for 3/5)
|
||||||
|
fullScore: number; // Max score (e.g., 5)
|
||||||
|
percentage: number; // 0.0-1.0
|
||||||
|
passed: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScoreBar({ score, fullScore, percentage, passed }: ScoreBarProps) {
|
||||||
|
const pct = Math.round(percentage * 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="score-bar" data-passed={passed}>
|
||||||
|
<div className="score-bar__header">
|
||||||
|
<span className="score-bar__percentage">{pct}%</span>
|
||||||
|
<span className={`score-bar__status ${passed ? 'passed' : 'failed'}`}>
|
||||||
|
{passed ? '✅ 通过' : '❌ 未通过'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="score-bar__track">
|
||||||
|
<div
|
||||||
|
className={`score-bar__fill ${passed ? 'passed' : 'failed'}`}
|
||||||
|
style={{ width: `${pct}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="score-bar__footer">
|
||||||
|
得分 {score.toFixed(1)} / 满分 {fullScore.toFixed(1)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ScoreBar } from './ScoreBar';
|
||||||
|
import { FieldResultList } from './FieldResultList';
|
||||||
|
|
||||||
|
interface ScoredResultCardProps {
|
||||||
|
result: {
|
||||||
|
evaluation_point_id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
passed: boolean;
|
||||||
|
machine_score: number; // e.g., 3
|
||||||
|
score: number; // e.g., 5 (full score from evaluation_points)
|
||||||
|
percentage: number;
|
||||||
|
total_score: number; // e.g., 60
|
||||||
|
total_weight: number; // e.g., 100
|
||||||
|
field_results: Array<{
|
||||||
|
field_path: string;
|
||||||
|
evaluation_as: string;
|
||||||
|
weight: number;
|
||||||
|
scored: number;
|
||||||
|
status: string;
|
||||||
|
value: string;
|
||||||
|
page?: string;
|
||||||
|
ai_feedback?: string;
|
||||||
|
}>;
|
||||||
|
missing_fields?: string[];
|
||||||
|
ai_suggestion?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ScoredResultCard({ result }: ScoredResultCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="scored-result-card" data-passed={result.passed}>
|
||||||
|
<div className="scored-result-card__header">
|
||||||
|
<span className="scored-result-card__code">{result.code}</span>
|
||||||
|
<span className="scored-result-card__name">{result.name}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ScoreBar
|
||||||
|
score={result.machine_score}
|
||||||
|
fullScore={result.score}
|
||||||
|
percentage={result.percentage}
|
||||||
|
passed={result.passed}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FieldResultList items={result.field_results} />
|
||||||
|
|
||||||
|
{result.ai_suggestion && (
|
||||||
|
<div className="scored-result-card__suggestion">
|
||||||
|
<strong>💡 建议:</strong>
|
||||||
|
<span>{result.ai_suggestion}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { ScoreBar } from './ScoreBar';
|
||||||
|
export { FieldResultList } from './FieldResultList';
|
||||||
|
export { ScoredResultCard } from './ScoredResultCard';
|
||||||
@@ -53,6 +53,7 @@ interface FilePreviewProps {
|
|||||||
activeReviewPointResultId: string | null;
|
activeReviewPointResultId: string | null;
|
||||||
targetPage?: number; // 新增目标页码参数
|
targetPage?: number; // 新增目标页码参数
|
||||||
charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息(仅用于PDF)
|
charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息(仅用于PDF)
|
||||||
|
textBbox?: { x_min: number; y_min: number; x_max: number; y_max: number }; // GraphRAG段落级坐标
|
||||||
highlightValue?: string; // 高亮文本值(用于DOCX)
|
highlightValue?: string; // 高亮文本值(用于DOCX)
|
||||||
isStructuredView?: boolean; // 是否显示结构化视图
|
isStructuredView?: boolean; // 是否显示结构化视图
|
||||||
userInfo?: {
|
userInfo?: {
|
||||||
@@ -74,7 +75,7 @@ export interface FilePreviewHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
||||||
export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) {
|
export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, textBbox, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) {
|
||||||
// 获取文件类型
|
// 获取文件类型
|
||||||
const real_path = fileContent.path || fileContent.template_contract_path || '';
|
const real_path = fileContent.path || fileContent.template_contract_path || '';
|
||||||
const fileExtension = real_path.split('.').pop()?.toLowerCase();
|
const fileExtension = real_path.split('.').pop()?.toLowerCase();
|
||||||
@@ -236,6 +237,7 @@ export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(funct
|
|||||||
filePath={real_path}
|
filePath={real_path}
|
||||||
targetPage={targetPage}
|
targetPage={targetPage}
|
||||||
charPositions={charPositions}
|
charPositions={charPositions}
|
||||||
|
textBbox={textBbox}
|
||||||
isStructuredView={isStructuredView}
|
isStructuredView={isStructuredView}
|
||||||
activeReviewPointResultId={activeReviewPointResultId}
|
activeReviewPointResultId={activeReviewPointResultId}
|
||||||
pageOffset={pageOffset}
|
pageOffset={pageOffset}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import { Tooltip } from '../ui/Tooltip';
|
|||||||
import { CorporateInfoModal } from '../corporate-information';
|
import { CorporateInfoModal } from '../corporate-information';
|
||||||
import type { BusinessInfoResult, DishonestyResult } from '../corporate-information';
|
import type { BusinessInfoResult, DishonestyResult } from '../corporate-information';
|
||||||
import { queryCompanyInfo } from '~/api/corporate-information/qichacha';
|
import { queryCompanyInfo } from '~/api/corporate-information/qichacha';
|
||||||
|
import { ScoredResultCard } from '~/components/evaluation';
|
||||||
// import '../../styles/components/TooltipStyles.css';
|
// import '../../styles/components/TooltipStyles.css';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,6 +82,32 @@ export interface CharPosition {
|
|||||||
score: number; // OCR识别置信度
|
score: number; // OCR识别置信度
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* text_bbox -> CharPosition[] 转换
|
||||||
|
* GraphRAG 抽取结果只有 text_bbox (段落级坐标), 没有 char_positions (字符级坐标)。
|
||||||
|
* 将 text_bbox 转为单个 CharPosition 矩形框, 让 PdfPreview 的高亮逻辑复用。
|
||||||
|
*/
|
||||||
|
function resolveCharPositions(data: any): CharPosition[] | undefined {
|
||||||
|
// 优先用 char_positions
|
||||||
|
if (data?.char_positions && data.char_positions.length > 0) {
|
||||||
|
return data.char_positions;
|
||||||
|
}
|
||||||
|
// fallback: text_bbox -> CharPosition[]
|
||||||
|
if (data?.text_bbox) {
|
||||||
|
const b = data.text_bbox;
|
||||||
|
if (b.x_min != null && b.y_min != null && b.x_max != null && b.y_max != null
|
||||||
|
&& (b.x_max - b.x_min) > 0 && (b.y_max - b.y_min) > 0) {
|
||||||
|
return [{
|
||||||
|
box: [[b.x_min, b.y_min], [b.x_max, b.y_min], [b.x_max, b.y_max], [b.x_min, b.y_max]],
|
||||||
|
char: '',
|
||||||
|
score: 1
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 评查点类型定义
|
* 评查点类型定义
|
||||||
* 用于展示单个评查结果
|
* 用于展示单个评查结果
|
||||||
@@ -145,6 +172,45 @@ interface Statistics {
|
|||||||
score: number;
|
score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GraphRAG Scored 评查结果类型
|
||||||
|
interface FieldScore {
|
||||||
|
field_path: string;
|
||||||
|
evaluation_as: string;
|
||||||
|
weight: number;
|
||||||
|
scored: number;
|
||||||
|
max_score: number;
|
||||||
|
status: string;
|
||||||
|
value: string;
|
||||||
|
page?: string;
|
||||||
|
ai_feedback?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ScoredEvaluationResult {
|
||||||
|
evaluation_point_id: number;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
passed: boolean;
|
||||||
|
machine_score: number;
|
||||||
|
score: number;
|
||||||
|
percentage: number;
|
||||||
|
total_score: number;
|
||||||
|
total_weight: number;
|
||||||
|
pass_threshold: number;
|
||||||
|
result_type: 'scored';
|
||||||
|
field_results: FieldScore[];
|
||||||
|
missing_fields?: string[];
|
||||||
|
ai_suggestion?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EvaluationSummary {
|
||||||
|
total_points: number;
|
||||||
|
passed_count: number;
|
||||||
|
failed_count: number;
|
||||||
|
total_score: number;
|
||||||
|
total_full_score: number;
|
||||||
|
average_percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface ReviewPointsListProps {
|
interface ReviewPointsListProps {
|
||||||
reviewPoints: ReviewPoint[];
|
reviewPoints: ReviewPoint[];
|
||||||
statistics: Statistics;
|
statistics: Statistics;
|
||||||
@@ -153,6 +219,10 @@ interface ReviewPointsListProps {
|
|||||||
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||||
fileFormat?: string; // 文档格式类型(PDF、DOCX等)
|
fileFormat?: string; // 文档格式类型(PDF、DOCX等)
|
||||||
onAiSuggestionReplace?: (searchText: string, replaceText: string, pageNumber: number) => void; // AI建议替换回调
|
onAiSuggestionReplace?: (searchText: string, replaceText: string, pageNumber: number) => void; // AI建议替换回调
|
||||||
|
// GraphRAG Scored 模式支持
|
||||||
|
flowType?: 'graphrag' | 'legacy';
|
||||||
|
scoredResults?: ScoredEvaluationResult[];
|
||||||
|
scoredSummary?: EvaluationSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -411,7 +481,10 @@ export function ReviewPointsList({
|
|||||||
onReviewPointSelect,
|
onReviewPointSelect,
|
||||||
onStatusChange,
|
onStatusChange,
|
||||||
fileFormat,
|
fileFormat,
|
||||||
onAiSuggestionReplace
|
onAiSuggestionReplace,
|
||||||
|
flowType,
|
||||||
|
scoredResults,
|
||||||
|
scoredSummary
|
||||||
}: ReviewPointsListProps) {
|
}: ReviewPointsListProps) {
|
||||||
// 状态管理
|
// 状态管理
|
||||||
const [editingReviewPoint, setEditingReviewPoint] = useState<string | null>(null); // 当前正在编辑的评查点ID
|
const [editingReviewPoint, setEditingReviewPoint] = useState<string | null>(null); // 当前正在编辑的评查点ID
|
||||||
@@ -587,6 +660,9 @@ export function ReviewPointsList({
|
|||||||
} else if (statusFilter === 'error') {
|
} else if (statusFilter === 'error') {
|
||||||
// 过滤"错误"状态
|
// 过滤"错误"状态
|
||||||
matchesStatus = point.result === false && point.status === 'error';
|
matchesStatus = point.result === false && point.status === 'error';
|
||||||
|
} else if (statusFilter === 'notApplicable') {
|
||||||
|
// 过滤"未涉及"状态
|
||||||
|
matchesStatus = point.status === 'notApplicable' || point.status === 'not_applicable';
|
||||||
}
|
}
|
||||||
// console.log('筛选point', point);
|
// console.log('筛选point', point);
|
||||||
|
|
||||||
@@ -618,6 +694,7 @@ export function ReviewPointsList({
|
|||||||
success: 0,
|
success: 0,
|
||||||
warning: 0,
|
warning: 0,
|
||||||
error: 0,
|
error: 0,
|
||||||
|
notApplicable: 0,
|
||||||
score: 0
|
score: 0
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -639,6 +716,10 @@ export function ReviewPointsList({
|
|||||||
const successToShow = successCount || statsToUse.success;
|
const successToShow = successCount || statsToUse.success;
|
||||||
const warningToShow = warningCount || statsToUse.warning;
|
const warningToShow = warningCount || statsToUse.warning;
|
||||||
const errorToShow = errorCount || statsToUse.error;
|
const errorToShow = errorCount || statsToUse.error;
|
||||||
|
const notApplicableCount = reviewPoints.filter(
|
||||||
|
point => point.status === 'notApplicable' || point.status === 'not_applicable'
|
||||||
|
).length;
|
||||||
|
const notApplicableToShow = notApplicableCount || statsToUse.notApplicable || 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="review-statistics bg-white border-b border-gray-100 py-3 px-4">
|
<div className="review-statistics bg-white border-b border-gray-100 py-3 px-4">
|
||||||
@@ -697,6 +778,19 @@ export function ReviewPointsList({
|
|||||||
<span className="text-xs text-gray-500 ml-2">错误</span>
|
<span className="text-xs text-gray-500 ml-2">错误</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 未涉及数量 */}
|
||||||
|
<div className="h-8 border-r border-gray-200"></div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
className={`px-3 h-7 bg-blue-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'notApplicable' ? 'ring-2 ring-blue-400' : ''}`}
|
||||||
|
onClick={() => setStatusFilter(statusFilter === 'notApplicable' ? null : 'notApplicable')}
|
||||||
|
aria-label={`过滤未涉及项 ${statusFilter === 'notApplicable' ? '(已选中)' : ''}`}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span className="text-sm font-semibold text-blue-500">{notApplicableToShow}</span>
|
||||||
|
<span className="text-xs text-gray-500 ml-2">未涉及</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -1194,7 +1288,7 @@ export function ReviewPointsList({
|
|||||||
for (const item of chain) {
|
for (const item of chain) {
|
||||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||||
hasPage = true;
|
hasPage = true;
|
||||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions);
|
onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1208,7 +1302,7 @@ export function ReviewPointsList({
|
|||||||
// 遍历chain找到第一个有效的page
|
// 遍历chain找到第一个有效的page
|
||||||
for (const item of chain) {
|
for (const item of chain) {
|
||||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions);
|
onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1244,15 +1338,15 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (item.data.page) {
|
if (item.data.page) {
|
||||||
console.log('点击了长链条评查点', item.data.char_positions, item.data);
|
console.log('点击了长链条评查点', resolveCharPositions(item.data), item.data);
|
||||||
// 假设onReviewPointSelect在作用域内可用
|
// 假设onReviewPointSelect在作用域内可用
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPointId, Number(item.data.page), item.data.char_positions, item.data.value);
|
onReviewPointSelect(reviewPointId, Number(item.data.page), resolveCharPositions(item.data), item.data.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){
|
else if(reviewPoint.contentPage && reviewPoint.contentPage[item.field]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), item.data.char_positions, item.data.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[item.field]), resolveCharPositions(item.data), item.data.value);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
toastService.error(`没有找到${item.field}对应的索引内容`);
|
toastService.error(`没有找到${item.field}对应的索引内容`);
|
||||||
@@ -1328,16 +1422,16 @@ export function ReviewPointsList({
|
|||||||
${res ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'} transition-colors flex flex-col`}
|
${res ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'} transition-colors flex flex-col`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
console.log('点击了短链1左', chain[0].data.char_positions, chain[0].data)
|
console.log('点击了短链1左', resolveCharPositions(chain[0].data), chain[0].data)
|
||||||
if (chain[0].data.page) {
|
if (chain[0].data.page) {
|
||||||
// console.log('点击了短链1左', chain[0].data.char_positions, chain[0].data)
|
// console.log('点击了短链1左', resolveCharPositions(chain[0].data), chain[0].data)
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPointId, chain[0].data.page, chain[0].data.char_positions, chain[0].data.value);
|
onReviewPointSelect(reviewPointId, chain[0].data.page, resolveCharPositions(chain[0].data), chain[0].data.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){
|
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[0].field]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), chain[0].data.char_positions,chain[0].data.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[0].field]), resolveCharPositions(chain[0].data),chain[0].data.value);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
|
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
|
||||||
@@ -1357,14 +1451,14 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (chain[1].data.page) {
|
if (chain[1].data.page) {
|
||||||
console.log('点击了短链2右', chain[1].data.char_positions, chain[1].data)
|
console.log('点击了短链2右', resolveCharPositions(chain[1].data), chain[1].data)
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPointId, chain[1].data.page, chain[1].data.char_positions, chain[1].data.value);
|
onReviewPointSelect(reviewPointId, chain[1].data.page, resolveCharPositions(chain[1].data), chain[1].data.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){
|
else if(reviewPoint.contentPage && reviewPoint.contentPage[chain[1].field]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), chain[1].data.char_positions, chain[1].data.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[chain[1].field]), resolveCharPositions(chain[1].data), chain[1].data.value);
|
||||||
}
|
}
|
||||||
else{
|
else{
|
||||||
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
|
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
|
||||||
@@ -1501,10 +1595,10 @@ export function ReviewPointsList({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||||
console.log("点击了其他评查点", mainTypeValue)
|
console.log("点击了其他评查点", mainTypeValue)
|
||||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||||
// onReviewPointSelect(reviewPoint.id, undefined, mainTypeValue.char_positions, mainTypeValue.value);
|
// onReviewPointSelect(reviewPoint.id, undefined, resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||||
}else{
|
}else{
|
||||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||||
}
|
}
|
||||||
@@ -1513,7 +1607,7 @@ export function ReviewPointsList({
|
|||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||||
}else{
|
}else{
|
||||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||||
}
|
}
|
||||||
@@ -1583,6 +1677,7 @@ export function ReviewPointsList({
|
|||||||
value: string;
|
value: string;
|
||||||
res: boolean;
|
res: boolean;
|
||||||
char_positions?: CharPosition[];
|
char_positions?: CharPosition[];
|
||||||
|
res?: boolean;
|
||||||
}>;
|
}>;
|
||||||
ai_suggestion?: {
|
ai_suggestion?: {
|
||||||
summary?: string;
|
summary?: string;
|
||||||
@@ -1635,7 +1730,8 @@ export function ReviewPointsList({
|
|||||||
// 遍历fields,获取每个字段的值并生成对应的JSX元素
|
// 遍历fields,获取每个字段的值并生成对应的JSX元素
|
||||||
if (config.fields) {
|
if (config.fields) {
|
||||||
Object.entries(config.fields).forEach(([key, value], index) => {
|
Object.entries(config.fields).forEach(([key, value], index) => {
|
||||||
const res = value.value.trim() !== '';
|
// 优先使用后端传入的 per-field res,fallback 到 value 非空判定
|
||||||
|
const res = value.res !== undefined && value.res !== null ? value.res : value.value.trim() !== '';
|
||||||
fieldElements.push(
|
fieldElements.push(
|
||||||
<button
|
<button
|
||||||
key={`field-${index}`}
|
key={`field-${index}`}
|
||||||
@@ -1645,10 +1741,10 @@ export function ReviewPointsList({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||||
console.log("点击了大模型的评查点", value.char_positions, value)
|
console.log("点击了大模型的评查点", resolveCharPositions(value), value)
|
||||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
|
onReviewPointSelect(reviewPoint.id, Number(value.page), resolveCharPositions(value), value.value);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions,value.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value),value.value);
|
||||||
}else{
|
}else{
|
||||||
toastService.error(`没有找到${key}对应的索引内容`);
|
toastService.error(`没有找到${key}对应的索引内容`);
|
||||||
}
|
}
|
||||||
@@ -1658,9 +1754,9 @@ export function ReviewPointsList({
|
|||||||
if (e.key === 'Enter' || e.key === ' ') {
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
|
onReviewPointSelect(reviewPoint.id, Number(value.page), resolveCharPositions(value), value.value);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[key]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions,value.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value),value.value);
|
||||||
}else{
|
}else{
|
||||||
toastService.error(`没有找到${key}对应的索引内容`);
|
toastService.error(`没有找到${key}对应的索引内容`);
|
||||||
}
|
}
|
||||||
@@ -1677,16 +1773,16 @@ export function ReviewPointsList({
|
|||||||
{!value.page && (reviewPoint.contentPage && !reviewPoint.contentPage[key]) && (
|
{!value.page && (reviewPoint.contentPage && !reviewPoint.contentPage[key]) && (
|
||||||
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
|
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
|
||||||
)}
|
)}
|
||||||
{/* 缺失显示 */}
|
{/* 缺失显示:仅在无值时显示 */}
|
||||||
{!res && (
|
{!res && !value.value && (
|
||||||
<span className="ml-2 text-xs text-yellow-500">
|
<span className="ml-2 text-xs text-yellow-500">
|
||||||
缺失
|
缺失
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 主要值显示 */}
|
{/* 主要值显示:有值就显示,不受res影响 */}
|
||||||
{res && (
|
{value.value && (
|
||||||
<ReactTableTooltip content={value.value} />
|
<ReactTableTooltip content={value.value} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -2476,13 +2572,18 @@ export function ReviewPointsList({
|
|||||||
|
|
||||||
{/* 评查点列表 */}
|
{/* 评查点列表 */}
|
||||||
<div className="review-points-list">
|
<div className="review-points-list">
|
||||||
{filteredReviewPoints.length > 0 ? (
|
{/* GraphRAG Scored 模式渲染 */}
|
||||||
|
{flowType === 'graphrag' && scoredResults && scoredResults.length > 0 ? (
|
||||||
|
scoredResults.map(result => (
|
||||||
|
<ScoredResultCard key={result.evaluation_point_id} result={result} />
|
||||||
|
))
|
||||||
|
) : filteredReviewPoints.length > 0 ? (
|
||||||
filteredReviewPoints.map(reviewPoint => (
|
filteredReviewPoints.map(reviewPoint => (
|
||||||
<div
|
<div
|
||||||
key={reviewPoint.id}
|
key={reviewPoint.id}
|
||||||
className={`rounded-md review-point-item ${activeReviewPointResultId === reviewPoint.id ? 'active border-l-4 !border-l-[rgba(0,104,74,1)] shadow-md' : 'border-l-4 border-l-transparent'}
|
className={`rounded-md review-point-item ${activeReviewPointResultId === reviewPoint.id ? 'active border-l-4 !border-l-[rgba(0,104,74,1)] shadow-md' : 'border-l-4 border-l-transparent'}
|
||||||
transition-all duration-300 ease-in-out shadow-sm
|
transition-all duration-300 ease-in-out shadow-sm
|
||||||
hover:shadow-lg hover:border-l-4 hover:border-l-[rgba(0,104,74,0.3)]
|
hover:shadow-lg hover:border-l-4 hover:border-l-[rgba(0,104,74,0.3)]
|
||||||
hover:bg-[rgba(0,0,0,0.02)] my-2`}
|
hover:bg-[rgba(0,0,0,0.02)] my-2`}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ interface PdfPreviewProps {
|
|||||||
filePath: string; // PDF 文件路径
|
filePath: string; // PDF 文件路径
|
||||||
targetPage?: number; // 目标页码
|
targetPage?: number; // 目标页码
|
||||||
charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息(用于高亮显示)
|
charPositions?: Array<{ box: number[][], char: string, score: number }>; // 字符位置信息(用于高亮显示)
|
||||||
|
textBbox?: { x_min: number; y_min: number; x_max: number; y_max: number }; // GraphRAG段落级坐标
|
||||||
isStructuredView?: boolean; // 是否结构化视图
|
isStructuredView?: boolean; // 是否结构化视图
|
||||||
activeReviewPointResultId?: string | null; // 激活的评查点结果ID
|
activeReviewPointResultId?: string | null; // 激活的评查点结果ID
|
||||||
pageOffset?: number; // 页码偏移量(用于调整 OCR 结果的页码)
|
pageOffset?: number; // 页码偏移量(用于调整 OCR 结果的页码)
|
||||||
@@ -49,6 +50,7 @@ export function PdfPreview({
|
|||||||
filePath,
|
filePath,
|
||||||
targetPage,
|
targetPage,
|
||||||
charPositions,
|
charPositions,
|
||||||
|
textBbox,
|
||||||
isStructuredView = false,
|
isStructuredView = false,
|
||||||
activeReviewPointResultId,
|
activeReviewPointResultId,
|
||||||
pageOffset = 0,
|
pageOffset = 0,
|
||||||
@@ -227,6 +229,18 @@ export function PdfPreview({
|
|||||||
|
|
||||||
// ============ 处理字符位置数据,转换为高亮矩形 ============
|
// ============ 处理字符位置数据,转换为高亮矩形 ============
|
||||||
const processCharPositionsToHighlights = () => {
|
const processCharPositionsToHighlights = () => {
|
||||||
|
// GraphRAG fallback: charPositions 为空但有 textBbox 时,用段落级坐标画高亮
|
||||||
|
if ((!charPositions || charPositions.length === 0) && textBbox && targetPage) {
|
||||||
|
const scale = zoomLevel / 100;
|
||||||
|
return {
|
||||||
|
x: textBbox.x_min * coordinateScale * scale,
|
||||||
|
y: textBbox.y_min * coordinateScale * scale,
|
||||||
|
width: (textBbox.x_max - textBbox.x_min) * coordinateScale * scale,
|
||||||
|
height: (textBbox.y_max - textBbox.y_min) * coordinateScale * scale,
|
||||||
|
text: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (!charPositions || charPositions.length === 0 || !targetPage) {
|
if (!charPositions || charPositions.length === 0 || !targetPage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* 合同专属类型定义
|
||||||
|
* 用于用户上传时手动选择,后端据此加载对应评查点规则
|
||||||
|
*/
|
||||||
|
export interface ContractType {
|
||||||
|
value: string; // 类型代码,传递给后端
|
||||||
|
label: string; // 显示名称
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的合同类型列表
|
||||||
|
*
|
||||||
|
* 注意:"通用"类型会加载所有通用评查点
|
||||||
|
* 其他类型会加载 "通用 + 该类型" 的评查点
|
||||||
|
*/
|
||||||
|
export const CONTRACT_TYPES: ContractType[] = [
|
||||||
|
{ value: "通用", label: "通用合同" },
|
||||||
|
{ value: "技术", label: "技术合同" },
|
||||||
|
{ value: "租赁", label: "租赁合同" },
|
||||||
|
{ value: "买卖", label: "买卖合同" },
|
||||||
|
{ value: "服务", label: "服务合同" },
|
||||||
|
{ value: "委托", label: "委托合同" },
|
||||||
|
{ value: "建设工程", label: "建设工程合同" },
|
||||||
|
{ value: "培训", label: "培训合同" },
|
||||||
|
{ value: "赠与", label: "赠与合同" },
|
||||||
|
{ value: "运输", label: "运输合同" },
|
||||||
|
{ value: "仓储", label: "仓储合同" },
|
||||||
|
{ value: "合作", label: "合作合同" },
|
||||||
|
{ value: "承揽", label: "承揽合同" }
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据值获取标签
|
||||||
|
*/
|
||||||
|
export function getContractTypeLabel(value: string): string {
|
||||||
|
const type = CONTRACT_TYPES.find(t => t.value === value);
|
||||||
|
return type?.label || value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认合同类型(用于初始值)
|
||||||
|
*/
|
||||||
|
export const DEFAULT_CONTRACT_TYPE = "通用";
|
||||||
@@ -32,6 +32,7 @@ import {
|
|||||||
type UserInfo
|
type UserInfo
|
||||||
} from "~/api/user/user-management";
|
} from "~/api/user/user-management";
|
||||||
import { API_BASE_URL } from '~/config/api-config';
|
import { API_BASE_URL } from '~/config/api-config';
|
||||||
|
import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE } from '~/constants/contractTypes';
|
||||||
|
|
||||||
export const meta: MetaFunction = () => {
|
export const meta: MetaFunction = () => {
|
||||||
return [
|
return [
|
||||||
@@ -173,6 +174,7 @@ export default function CrossCheckingUpload() {
|
|||||||
const [documentNumber] = useState<string>("");
|
const [documentNumber] = useState<string>("");
|
||||||
const [remark] = useState<string>("");
|
const [remark] = useState<string>("");
|
||||||
const [isTestDocument] = useState<boolean>(false);
|
const [isTestDocument] = useState<boolean>(false);
|
||||||
|
const [attributeType, setAttributeType] = useState<string>(DEFAULT_CONTRACT_TYPE);
|
||||||
|
|
||||||
// 文件管理状态 - 简化为单文件上传
|
// 文件管理状态 - 简化为单文件上传
|
||||||
const [uploadedFile, setUploadedFile] = useState<CrossCheckingUploadedFile | null>(null);
|
const [uploadedFile, setUploadedFile] = useState<CrossCheckingUploadedFile | null>(null);
|
||||||
@@ -358,7 +360,8 @@ export default function CrossCheckingUpload() {
|
|||||||
selectedDocType.code, // 使用文档类型code
|
selectedDocType.code, // 使用文档类型code
|
||||||
taskInfo.type, // 使用任务类型(市局间交叉评查 或 区局间交叉评查)
|
taskInfo.type, // 使用任务类型(市局间交叉评查 或 区局间交叉评查)
|
||||||
frontendJWT,
|
frontendJWT,
|
||||||
principalUserIds // 负责人ID数组
|
principalUserIds, // 负责人ID数组
|
||||||
|
attributeType // 合同类型
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
@@ -987,6 +990,32 @@ export default function CrossCheckingUpload() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 合同类型选择器 - 仅在选择合同类型文档时显示 */}
|
||||||
|
{selectedDocTypeId === 1 && (
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<div className="w-full max-w-2xl">
|
||||||
|
<div className="text-sm font-medium text-gray-700 mb-3 text-center">选择合同类型(可选)</div>
|
||||||
|
<div className="grid grid-cols-4 gap-2">
|
||||||
|
{CONTRACT_TYPES.map((type) => (
|
||||||
|
<button
|
||||||
|
key={type.value}
|
||||||
|
type="button"
|
||||||
|
className={`px-3 py-2 text-sm rounded-md border transition-colors ${
|
||||||
|
attributeType === type.value
|
||||||
|
? 'bg-[var(--color-primary)] text-white border-[var(--color-primary)]'
|
||||||
|
: 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'
|
||||||
|
}`}
|
||||||
|
onClick={() => setAttributeType(type.value)}
|
||||||
|
disabled={isUploading}
|
||||||
|
>
|
||||||
|
{type.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 文件上传区域 - 左右布局 */}
|
{/* 文件上传区域 - 左右布局 */}
|
||||||
<input type="hidden" name="selectedDocTypeId" value={selectedDocTypeId || ''} />
|
<input type="hidden" name="selectedDocTypeId" value={selectedDocTypeId || ''} />
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
||||||
import { links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
|
import { links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
|
||||||
import { getQueueStatus, type QueueStatus } from "~/api/queue";
|
import { getQueueStatus, type QueueStatus } from "~/api/queue";
|
||||||
|
import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE } from "~/constants/contractTypes";
|
||||||
|
|
||||||
export function links() {
|
export function links() {
|
||||||
return [
|
return [
|
||||||
@@ -131,7 +132,8 @@ async function handleFileUpload(
|
|||||||
documentId?: number | null,
|
documentId?: number | null,
|
||||||
isReupload: boolean = false,
|
isReupload: boolean = false,
|
||||||
jwtToken?: string,
|
jwtToken?: string,
|
||||||
attachments?: File[]
|
attachments?: File[],
|
||||||
|
attributeType?: string
|
||||||
): Promise<FileUploadResponse> {
|
): Promise<FileUploadResponse> {
|
||||||
// console.log('【handleFileUpload】开始上传:', {
|
// console.log('【handleFileUpload】开始上传:', {
|
||||||
// fileName,
|
// fileName,
|
||||||
@@ -153,7 +155,8 @@ async function handleFileUpload(
|
|||||||
documentId,
|
documentId,
|
||||||
isReupload,
|
isReupload,
|
||||||
jwtToken,
|
jwtToken,
|
||||||
attachments
|
attachments,
|
||||||
|
attributeType
|
||||||
);
|
);
|
||||||
|
|
||||||
// console.log('【handleFileUpload】uploadDocumentToServer返回:', {
|
// console.log('【handleFileUpload】uploadDocumentToServer返回:', {
|
||||||
@@ -349,6 +352,7 @@ export default function FilesUpload() {
|
|||||||
const [documentNumber, setDocumentNumber] = useState<string>("");
|
const [documentNumber, setDocumentNumber] = useState<string>("");
|
||||||
const [remark, setRemark] = useState<string>("");
|
const [remark, setRemark] = useState<string>("");
|
||||||
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
|
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
|
||||||
|
const [attributeType, setAttributeType] = useState<string>(DEFAULT_CONTRACT_TYPE);
|
||||||
|
|
||||||
// 合同文件上传状态
|
// 合同文件上传状态
|
||||||
// 这些变量暂时未使用,但保留以备将来扩展
|
// 这些变量暂时未使用,但保留以备将来扩展
|
||||||
@@ -1193,7 +1197,8 @@ export default function FilesUpload() {
|
|||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
loaderData.frontendJWT || undefined,
|
loaderData.frontendJWT || undefined,
|
||||||
attachmentFiles
|
attachmentFiles,
|
||||||
|
attributeType
|
||||||
);
|
);
|
||||||
|
|
||||||
// console.log('【合同上传】服务器响应数据:', uploadResp);
|
// console.log('【合同上传】服务器响应数据:', uploadResp);
|
||||||
@@ -1550,7 +1555,9 @@ export default function FilesUpload() {
|
|||||||
isTestDocument,
|
isTestDocument,
|
||||||
temp_n > 1 ? firstFileDocumentId : null, // 第二个文件及以后使用第一个文件的document_id
|
temp_n > 1 ? firstFileDocumentId : null, // 第二个文件及以后使用第一个文件的document_id
|
||||||
false,
|
false,
|
||||||
loaderData.frontendJWT || undefined
|
loaderData.frontendJWT || undefined,
|
||||||
|
undefined,
|
||||||
|
attributeType
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeoutPromise = new Promise<FileUploadResponse>((_, reject) => {
|
const timeoutPromise = new Promise<FileUploadResponse>((_, reject) => {
|
||||||
@@ -2326,6 +2333,28 @@ export default function FilesUpload() {
|
|||||||
</select>
|
</select>
|
||||||
<div className="form-tip">优先级影响文档在队列中的处理顺序</div>
|
<div className="form-tip">优先级影响文档在队列中的处理顺序</div>
|
||||||
</div>
|
</div>
|
||||||
|
{/* 子类型(专属类型)- 始终显示 */}
|
||||||
|
<div className="form-group">
|
||||||
|
<label htmlFor="attribute-type-select" className="form-label">
|
||||||
|
子类型 <span className="required">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="attribute-type-select"
|
||||||
|
name="attributeType"
|
||||||
|
className="form-select"
|
||||||
|
value={attributeType}
|
||||||
|
onChange={(e) => setAttributeType(e.target.value)}
|
||||||
|
disabled={uploadStage !== "idle"}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
{CONTRACT_TYPES.map(type => (
|
||||||
|
<option key={type.value} value={type.value}>
|
||||||
|
{type.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="form-tip">选择文档专属类型以应用对应的审核规则(合同大类请选择技术/租赁/买卖等)</div>
|
||||||
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<label htmlFor="docNumber" className="form-label">文档编号</label>
|
<label htmlFor="docNumber" className="form-label">文档编号</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
+100
-15
@@ -30,7 +30,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
|
|||||||
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
||||||
import type { FilePreviewHandle } from "~/components/reviews/FilePreview";
|
import type { FilePreviewHandle } from "~/components/reviews/FilePreview";
|
||||||
import reviewsStyles from "~/styles/reviews.css?url";
|
import reviewsStyles from "~/styles/reviews.css?url";
|
||||||
import { getReviewPoints, getReviewPoints_fromApi, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
import { getReviewPoints, getReviewPoints_fromApi, getUnifiedEvaluationResults, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
||||||
import { toastService } from "~/components/ui/Toast";
|
import { toastService } from "~/components/ui/Toast";
|
||||||
|
|
||||||
// 导入评查详情页面组件
|
// 导入评查详情页面组件
|
||||||
@@ -72,6 +72,7 @@ interface Statistics {
|
|||||||
success: number;
|
success: number;
|
||||||
warning: number;
|
warning: number;
|
||||||
error: number;
|
error: number;
|
||||||
|
notApplicable: number;
|
||||||
score: number;
|
score: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,19 +194,94 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
const { getUserSession } = await import("~/api/login/auth.server");
|
const { getUserSession } = await import("~/api/login/auth.server");
|
||||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||||
|
|
||||||
// 🆕 使用新的后端API获取评查点数据(单次请求替代原7次请求)
|
// 🆕 使用新的统一API获取评查点数据
|
||||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
// 先尝试新的统一评查接口
|
||||||
|
const unifiedData = await getUnifiedEvaluationResults(id, request);
|
||||||
|
|
||||||
// ⚠️ 原方法已注释(保留以备回退)
|
// 如果统一接口返回错误或flow_type为legacy,使用原有API
|
||||||
// const reviewData = await getReviewPoints(id, request);
|
if ('error' in unifiedData || !unifiedData.flow_type) {
|
||||||
|
console.log("[Reviews Loader] 统一接口不可用,使用旧接口...");
|
||||||
|
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||||
|
|
||||||
if ('error' in reviewData && reviewData.error) {
|
if ('error' in reviewData && reviewData.error) {
|
||||||
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
|
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
|
||||||
return Response.json({ result: false, message: reviewData.error });
|
return Response.json({ result: false, message: reviewData.error });
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
|
||||||
|
return Response.json({
|
||||||
|
previousRoute: previousRoute,
|
||||||
|
document: reviewData.document,
|
||||||
|
reviewPoints: reviewData.data,
|
||||||
|
reviewInfo: reviewData.reviewInfo,
|
||||||
|
statistics: reviewData.stats,
|
||||||
|
comparison_document: reviewData.comparison_document,
|
||||||
|
userInfo,
|
||||||
|
frontendJWT,
|
||||||
|
flowType: 'legacy',
|
||||||
|
scoredResults: null,
|
||||||
|
scoredSummary: null
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保reviewData有效且具有预期的属性
|
// 统一接口成功返回,判断流程类型
|
||||||
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
|
if (unifiedData.flow_type === 'graphrag') {
|
||||||
|
// 先获取文档基本信息(统一接口不返回文档内容)
|
||||||
|
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||||
|
|
||||||
|
// 合并已评查的 reviewPoints + 未涉及的评查点
|
||||||
|
const existingPoints = ('data' in reviewData && !('error' in reviewData)) ? reviewData.data : [];
|
||||||
|
const notApplicablePoints = (unifiedData.results || [])
|
||||||
|
.filter((r: any) => r.result_type === 'not_applicable')
|
||||||
|
.map((r: any) => ({
|
||||||
|
id: `na-${r.evaluation_point_id}`,
|
||||||
|
documentId: id,
|
||||||
|
pointId: r.evaluation_point_id,
|
||||||
|
editAuditStatusId: '',
|
||||||
|
editAuditStatus: '',
|
||||||
|
editAuditStatusMessage: '',
|
||||||
|
title: '该评查点未涉及',
|
||||||
|
pointName: r.name || '',
|
||||||
|
groupName: '',
|
||||||
|
status: 'notApplicable',
|
||||||
|
content: {},
|
||||||
|
contentPage: {},
|
||||||
|
suggestion: r.ai_suggestion || '该评查点未涉及',
|
||||||
|
result: null,
|
||||||
|
score: r.score || 0,
|
||||||
|
finalScore: null,
|
||||||
|
machineScore: 0,
|
||||||
|
postAction: '',
|
||||||
|
}));
|
||||||
|
const allReviewPoints = [...existingPoints, ...notApplicablePoints];
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
previousRoute: previousRoute,
|
||||||
|
document: ('document' in reviewData && !('error' in reviewData)) ? reviewData.document : null,
|
||||||
|
reviewPoints: allReviewPoints,
|
||||||
|
reviewInfo: { reviewTime: unifiedData.evaluated_at, reviewModel: 'GraphRAG', ruleGroup: '', result: '', issueCount: unifiedData.summary?.total_points || 0 },
|
||||||
|
statistics: {
|
||||||
|
total: unifiedData.summary?.total_points || 0,
|
||||||
|
success: unifiedData.summary?.passed_count || 0,
|
||||||
|
warning: unifiedData.summary?.failed_count || 0,
|
||||||
|
error: 0,
|
||||||
|
notApplicable: unifiedData.summary?.not_applicable_count || 0,
|
||||||
|
score: unifiedData.summary?.total_score || 0
|
||||||
|
},
|
||||||
|
comparison_document: ('comparison_document' in reviewData && !('error' in reviewData)) ? reviewData.comparison_document : null,
|
||||||
|
userInfo,
|
||||||
|
frontendJWT,
|
||||||
|
flowType: 'graphrag',
|
||||||
|
scoredResults: unifiedData.results,
|
||||||
|
scoredSummary: unifiedData.summary
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// legacy 流程但统一接口可用,也走原有逻辑
|
||||||
|
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||||
|
if ('error' in reviewData && reviewData.error) {
|
||||||
|
return Response.json({ result: false, message: reviewData.error });
|
||||||
|
}
|
||||||
return Response.json({
|
return Response.json({
|
||||||
previousRoute: previousRoute,
|
previousRoute: previousRoute,
|
||||||
document: reviewData.document,
|
document: reviewData.document,
|
||||||
@@ -214,11 +290,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
statistics: reviewData.stats,
|
statistics: reviewData.stats,
|
||||||
comparison_document: reviewData.comparison_document,
|
comparison_document: reviewData.comparison_document,
|
||||||
userInfo,
|
userInfo,
|
||||||
frontendJWT
|
frontendJWT,
|
||||||
|
flowType: 'legacy',
|
||||||
|
scoredResults: null,
|
||||||
|
scoredSummary: null
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
console.error("[Reviews Loader] 返回的评查数据格式不正确,完整数据:", JSON.stringify(reviewData, null, 2));
|
|
||||||
return Response.json({ result: false, message: '返回的评查数据格式不正确' });
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Reviews Loader] 获取评查数据失败:', error);
|
console.error('[Reviews Loader] 获取评查数据失败:', error);
|
||||||
@@ -310,6 +386,7 @@ export default function ReviewDetails() {
|
|||||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||||
const [templateTargetPage, setTemplateTargetPage] = useState<number | undefined>(undefined);
|
const [templateTargetPage, setTemplateTargetPage] = useState<number | undefined>(undefined);
|
||||||
const [charPositions, setCharPositions] = useState<Array<{ box: number[][], char: string, score: number }> | undefined>(undefined);
|
const [charPositions, setCharPositions] = useState<Array<{ box: number[][], char: string, score: number }> | undefined>(undefined);
|
||||||
|
const [textBbox, setTextBbox] = useState<{ x_min: number; y_min: number; x_max: number; y_max: number } | undefined>(undefined);
|
||||||
const [highlightValue, setHighlightValue] = useState<string | undefined>(undefined);
|
const [highlightValue, setHighlightValue] = useState<string | undefined>(undefined);
|
||||||
const [pendingUpdate, setPendingUpdate] = useState<{
|
const [pendingUpdate, setPendingUpdate] = useState<{
|
||||||
reviewPointResultId: string;
|
reviewPointResultId: string;
|
||||||
@@ -475,17 +552,19 @@ export default function ReviewDetails() {
|
|||||||
setActiveTab(tabKey);
|
setActiveTab(tabKey);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>, value?: string) => {
|
const handleReviewPointSelect = (reviewPointId: string, page?: number, charPos?: Array<{ box: number[][], char: string, score: number }>, value?: string, bbox?: { x_min: number; y_min: number; x_max: number; y_max: number }) => {
|
||||||
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
||||||
if (reviewPointId === activeReviewPointResultId && page) {
|
if (reviewPointId === activeReviewPointResultId && page) {
|
||||||
setTargetPage(undefined);
|
setTargetPage(undefined);
|
||||||
setCharPositions(undefined);
|
setCharPositions(undefined);
|
||||||
|
setTextBbox(undefined);
|
||||||
setHighlightValue(undefined);
|
setHighlightValue(undefined);
|
||||||
// 使用setTimeout确保状态更新后再设置新的targetPage、charPositions和highlightValue
|
// 使用setTimeout确保状态更新后再设置新的targetPage、charPositions和highlightValue
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setActiveReviewPointResultId(reviewPointId);
|
setActiveReviewPointResultId(reviewPointId);
|
||||||
setTargetPage(page);
|
setTargetPage(page);
|
||||||
setCharPositions(charPos);
|
setCharPositions(charPos);
|
||||||
|
setTextBbox(bbox);
|
||||||
setHighlightValue(value);
|
setHighlightValue(value);
|
||||||
}, 0);
|
}, 0);
|
||||||
} else {
|
} else {
|
||||||
@@ -493,6 +572,7 @@ export default function ReviewDetails() {
|
|||||||
setActiveReviewPointResultId(reviewPointId);
|
setActiveReviewPointResultId(reviewPointId);
|
||||||
setTargetPage(page);
|
setTargetPage(page);
|
||||||
setCharPositions(charPos);
|
setCharPositions(charPos);
|
||||||
|
setTextBbox(bbox);
|
||||||
setHighlightValue(value);
|
setHighlightValue(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -890,6 +970,7 @@ export default function ReviewDetails() {
|
|||||||
activeReviewPointResultId={activeReviewPointResultId}
|
activeReviewPointResultId={activeReviewPointResultId}
|
||||||
targetPage={targetPage}
|
targetPage={targetPage}
|
||||||
charPositions={charPositions}
|
charPositions={charPositions}
|
||||||
|
textBbox={textBbox}
|
||||||
highlightValue={highlightValue}
|
highlightValue={highlightValue}
|
||||||
userInfo={loaderData.userInfo}
|
userInfo={loaderData.userInfo}
|
||||||
aiSuggestionReplace={aiSuggestionReplace}
|
aiSuggestionReplace={aiSuggestionReplace}
|
||||||
@@ -909,6 +990,9 @@ export default function ReviewDetails() {
|
|||||||
onStatusChange={handleReviewPointStatusChange}
|
onStatusChange={handleReviewPointStatusChange}
|
||||||
fileFormat={reviewData.fileInfo.fileFormat}
|
fileFormat={reviewData.fileInfo.fileFormat}
|
||||||
onAiSuggestionReplace={handleAiSuggestionReplace}
|
onAiSuggestionReplace={handleAiSuggestionReplace}
|
||||||
|
flowType={reviewData.flowType}
|
||||||
|
scoredResults={reviewData.scoredResults}
|
||||||
|
scoredSummary={reviewData.scoredSummary}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -940,6 +1024,7 @@ export default function ReviewDetails() {
|
|||||||
activeReviewPointResultId={activeReviewPointResultId}
|
activeReviewPointResultId={activeReviewPointResultId}
|
||||||
targetPage={targetPage}
|
targetPage={targetPage}
|
||||||
charPositions={charPositions}
|
charPositions={charPositions}
|
||||||
|
textBbox={textBbox}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user