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 axios from 'axios';
|
||||
import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE } from '~/constants/contractTypes';
|
||||
|
||||
/**
|
||||
* 从不同格式的 API 响应中提取数据
|
||||
@@ -224,7 +225,8 @@ export async function batchUploadAndAssignCrossCheckingFiles(
|
||||
docType: string,
|
||||
taskType: string = '市局间交叉评查',
|
||||
token: string | null = null,
|
||||
principalUserIds: number[] = []
|
||||
principalUserIds: number[] = [],
|
||||
attributeType?: string
|
||||
): Promise<{
|
||||
successes: Array<{file: CrossCheckingUploadedFile; result: Record<string, unknown>}>;
|
||||
failures: Array<{file: CrossCheckingUploadedFile; error: string}>;
|
||||
@@ -249,7 +251,8 @@ export async function batchUploadAndAssignCrossCheckingFiles(
|
||||
is_test_document: isTestDocument,
|
||||
task_name: taskName,
|
||||
doc_type: typeof docType === 'string' ? docType.toUpperCase() : docType,
|
||||
task_type: taskType
|
||||
task_type: taskType,
|
||||
attribute_type: attributeType || null
|
||||
};
|
||||
// console.log('fileInfo', fileInfo)
|
||||
|
||||
|
||||
@@ -94,6 +94,53 @@ interface StatsData {
|
||||
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 OcrDataResult {
|
||||
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 { UPLOAD_URL, API_BASE_URL } from '../../config/api-config';
|
||||
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(
|
||||
binaryData: ArrayBuffer,
|
||||
fileName: string,
|
||||
fileType: string,
|
||||
binaryData: ArrayBuffer,
|
||||
fileName: string,
|
||||
fileType: string,
|
||||
typeId: string | number,
|
||||
priority: string,
|
||||
documentNumber?: string | null,
|
||||
@@ -382,7 +383,8 @@ export async function uploadDocumentToServer(
|
||||
documentId?: number | null,
|
||||
isReupload: boolean = false,
|
||||
jwtToken?: string,
|
||||
attachments?: File[]
|
||||
attachments?: File[],
|
||||
attributeType?: string
|
||||
): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> {
|
||||
try {
|
||||
// console.log('【调试】开始上传文档:', { fileName, fileSize: binaryData.byteLength });
|
||||
@@ -397,13 +399,14 @@ export async function uploadDocumentToServer(
|
||||
|
||||
// 将信息添加到一个JSON对象中
|
||||
const uploadInfo = {
|
||||
type_id: Number(typeId),
|
||||
type_id: Number(typeId),
|
||||
evaluation_level: priority,
|
||||
document_number: documentNumber || null,
|
||||
remark: remark || null,
|
||||
is_test_document: isTestDocument,
|
||||
document_id: documentId || null,
|
||||
is_reupload: isReupload
|
||||
is_reupload: isReupload,
|
||||
attribute_type: attributeType || null
|
||||
};
|
||||
|
||||
// 添加JSON字符串到FormData
|
||||
|
||||
@@ -93,6 +93,32 @@ export interface CharPosition {
|
||||
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) {
|
||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1526,7 +1552,7 @@ export function ReviewPointsList({
|
||||
// 遍历chain找到第一个有效的page
|
||||
for (const item of chain) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1566,11 +1592,11 @@ export function ReviewPointsList({
|
||||
// 假设onReviewPointSelect在作用域内可用
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
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]){
|
||||
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{
|
||||
toastService.error(`没有找到${item.field}对应的索引内容`);
|
||||
@@ -1649,11 +1675,11 @@ export function ReviewPointsList({
|
||||
if (chain[0].data.page) {
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
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]){
|
||||
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{
|
||||
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
|
||||
@@ -1675,11 +1701,11 @@ export function ReviewPointsList({
|
||||
if (chain[1].data.page) {
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
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]){
|
||||
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{
|
||||
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
|
||||
@@ -1815,9 +1841,9 @@ export function ReviewPointsList({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||
}
|
||||
@@ -1826,9 +1852,9 @@ export function ReviewPointsList({
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
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]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||
}
|
||||
@@ -1960,9 +1986,9 @@ export function ReviewPointsList({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value), value.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${key}对应的索引内容`);
|
||||
}
|
||||
@@ -1972,9 +1998,9 @@ export function ReviewPointsList({
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
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]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions, value.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value), value.value);
|
||||
}else{
|
||||
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;
|
||||
targetPage?: number; // 新增目标页码参数
|
||||
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)
|
||||
isStructuredView?: boolean; // 是否显示结构化视图
|
||||
userInfo?: {
|
||||
@@ -74,7 +75,7 @@ export interface FilePreviewHandle {
|
||||
}
|
||||
|
||||
// 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 fileExtension = real_path.split('.').pop()?.toLowerCase();
|
||||
@@ -236,6 +237,7 @@ export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(funct
|
||||
filePath={real_path}
|
||||
targetPage={targetPage}
|
||||
charPositions={charPositions}
|
||||
textBbox={textBbox}
|
||||
isStructuredView={isStructuredView}
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
pageOffset={pageOffset}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { Tooltip } from '../ui/Tooltip';
|
||||
import { CorporateInfoModal } from '../corporate-information';
|
||||
import type { BusinessInfoResult, DishonestyResult } from '../corporate-information';
|
||||
import { queryCompanyInfo } from '~/api/corporate-information/qichacha';
|
||||
import { ScoredResultCard } from '~/components/evaluation';
|
||||
// import '../../styles/components/TooltipStyles.css';
|
||||
|
||||
/**
|
||||
@@ -81,6 +82,32 @@ export interface CharPosition {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
reviewPoints: ReviewPoint[];
|
||||
statistics: Statistics;
|
||||
@@ -153,6 +219,10 @@ interface ReviewPointsListProps {
|
||||
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
|
||||
fileFormat?: string; // 文档格式类型(PDF、DOCX等)
|
||||
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,
|
||||
onStatusChange,
|
||||
fileFormat,
|
||||
onAiSuggestionReplace
|
||||
onAiSuggestionReplace,
|
||||
flowType,
|
||||
scoredResults,
|
||||
scoredSummary
|
||||
}: ReviewPointsListProps) {
|
||||
// 状态管理
|
||||
const [editingReviewPoint, setEditingReviewPoint] = useState<string | null>(null); // 当前正在编辑的评查点ID
|
||||
@@ -587,6 +660,9 @@ export function ReviewPointsList({
|
||||
} else if (statusFilter === 'error') {
|
||||
// 过滤"错误"状态
|
||||
matchesStatus = point.result === false && point.status === 'error';
|
||||
} else if (statusFilter === 'notApplicable') {
|
||||
// 过滤"未涉及"状态
|
||||
matchesStatus = point.status === 'notApplicable' || point.status === 'not_applicable';
|
||||
}
|
||||
// console.log('筛选point', point);
|
||||
|
||||
@@ -618,6 +694,7 @@ export function ReviewPointsList({
|
||||
success: 0,
|
||||
warning: 0,
|
||||
error: 0,
|
||||
notApplicable: 0,
|
||||
score: 0
|
||||
};
|
||||
|
||||
@@ -639,6 +716,10 @@ export function ReviewPointsList({
|
||||
const successToShow = successCount || statsToUse.success;
|
||||
const warningToShow = warningCount || statsToUse.warning;
|
||||
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 (
|
||||
<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>
|
||||
</button>
|
||||
</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>
|
||||
);
|
||||
@@ -1194,7 +1288,7 @@ export function ReviewPointsList({
|
||||
for (const item of chain) {
|
||||
if (item.data.page && typeof onReviewPointSelect === 'function') {
|
||||
hasPage = true;
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), item.data.char_positions);
|
||||
onReviewPointSelect(reviewPoint.id, Number(item.data.page), resolveCharPositions(item.data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1208,7 +1302,7 @@ export function ReviewPointsList({
|
||||
// 遍历chain找到第一个有效的page
|
||||
for (const item of chain) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1244,15 +1338,15 @@ export function ReviewPointsList({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (item.data.page) {
|
||||
console.log('点击了长链条评查点', item.data.char_positions, item.data);
|
||||
console.log('点击了长链条评查点', resolveCharPositions(item.data), item.data);
|
||||
// 假设onReviewPointSelect在作用域内可用
|
||||
const reviewPointId = reviewPoint.id as string;
|
||||
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]){
|
||||
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{
|
||||
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`}
|
||||
onClick={(e) => {
|
||||
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) {
|
||||
// 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;
|
||||
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]){
|
||||
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{
|
||||
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
|
||||
@@ -1357,14 +1451,14 @@ export function ReviewPointsList({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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;
|
||||
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]){
|
||||
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{
|
||||
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
|
||||
@@ -1501,10 +1595,10 @@ export function ReviewPointsList({
|
||||
e.stopPropagation();
|
||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||
console.log("点击了其他评查点", mainTypeValue)
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
||||
// onReviewPointSelect(reviewPoint.id, undefined, mainTypeValue.char_positions, mainTypeValue.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||
// onReviewPointSelect(reviewPoint.id, undefined, resolveCharPositions(mainTypeValue), mainTypeValue.value);
|
||||
}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{
|
||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||
}
|
||||
@@ -1513,7 +1607,7 @@ export function ReviewPointsList({
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
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{
|
||||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||||
}
|
||||
@@ -1583,6 +1677,7 @@ export function ReviewPointsList({
|
||||
value: string;
|
||||
res: boolean;
|
||||
char_positions?: CharPosition[];
|
||||
res?: boolean;
|
||||
}>;
|
||||
ai_suggestion?: {
|
||||
summary?: string;
|
||||
@@ -1635,7 +1730,8 @@ export function ReviewPointsList({
|
||||
// 遍历fields,获取每个字段的值并生成对应的JSX元素
|
||||
if (config.fields) {
|
||||
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(
|
||||
<button
|
||||
key={`field-${index}`}
|
||||
@@ -1645,10 +1741,10 @@ export function ReviewPointsList({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (value.page && typeof onReviewPointSelect === 'function') {
|
||||
console.log("点击了大模型的评查点", value.char_positions, value)
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), value.char_positions, value.value);
|
||||
console.log("点击了大模型的评查点", resolveCharPositions(value), value)
|
||||
onReviewPointSelect(reviewPoint.id, Number(value.page), resolveCharPositions(value), value.value);
|
||||
}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{
|
||||
toastService.error(`没有找到${key}对应的索引内容`);
|
||||
}
|
||||
@@ -1658,9 +1754,9 @@ export function ReviewPointsList({
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
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]){
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), value.char_positions,value.value);
|
||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[key]), resolveCharPositions(value),value.value);
|
||||
}else{
|
||||
toastService.error(`没有找到${key}对应的索引内容`);
|
||||
}
|
||||
@@ -1677,16 +1773,16 @@ export function ReviewPointsList({
|
||||
{!value.page && (reviewPoint.contentPage && !reviewPoint.contentPage[key]) && (
|
||||
<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>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 主要值显示 */}
|
||||
{res && (
|
||||
|
||||
{/* 主要值显示:有值就显示,不受res影响 */}
|
||||
{value.value && (
|
||||
<ReactTableTooltip content={value.value} />
|
||||
)}
|
||||
</div>
|
||||
@@ -2476,13 +2572,18 @@ export function ReviewPointsList({
|
||||
|
||||
{/* 评查点列表 */}
|
||||
<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 => (
|
||||
<div
|
||||
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
|
||||
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`}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
|
||||
@@ -38,6 +38,7 @@ interface PdfPreviewProps {
|
||||
filePath: string; // PDF 文件路径
|
||||
targetPage?: 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; // 是否结构化视图
|
||||
activeReviewPointResultId?: string | null; // 激活的评查点结果ID
|
||||
pageOffset?: number; // 页码偏移量(用于调整 OCR 结果的页码)
|
||||
@@ -49,6 +50,7 @@ export function PdfPreview({
|
||||
filePath,
|
||||
targetPage,
|
||||
charPositions,
|
||||
textBbox,
|
||||
isStructuredView = false,
|
||||
activeReviewPointResultId,
|
||||
pageOffset = 0,
|
||||
@@ -227,6 +229,18 @@ export function PdfPreview({
|
||||
|
||||
// ============ 处理字符位置数据,转换为高亮矩形 ============
|
||||
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) {
|
||||
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
|
||||
} from "~/api/user/user-management";
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE } from '~/constants/contractTypes';
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
@@ -173,6 +174,7 @@ export default function CrossCheckingUpload() {
|
||||
const [documentNumber] = useState<string>("");
|
||||
const [remark] = useState<string>("");
|
||||
const [isTestDocument] = useState<boolean>(false);
|
||||
const [attributeType, setAttributeType] = useState<string>(DEFAULT_CONTRACT_TYPE);
|
||||
|
||||
// 文件管理状态 - 简化为单文件上传
|
||||
const [uploadedFile, setUploadedFile] = useState<CrossCheckingUploadedFile | null>(null);
|
||||
@@ -358,7 +360,8 @@ export default function CrossCheckingUpload() {
|
||||
selectedDocType.code, // 使用文档类型code
|
||||
taskInfo.type, // 使用任务类型(市局间交叉评查 或 区局间交叉评查)
|
||||
frontendJWT,
|
||||
principalUserIds // 负责人ID数组
|
||||
principalUserIds, // 负责人ID数组
|
||||
attributeType // 合同类型
|
||||
);
|
||||
|
||||
|
||||
@@ -987,6 +990,32 @@ export default function CrossCheckingUpload() {
|
||||
</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 || ''} />
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
||||
import { links as fileTypeTagLinks } from "~/components/ui/FileTypeTag";
|
||||
import { getQueueStatus, type QueueStatus } from "~/api/queue";
|
||||
import { CONTRACT_TYPES, DEFAULT_CONTRACT_TYPE } from "~/constants/contractTypes";
|
||||
|
||||
export function links() {
|
||||
return [
|
||||
@@ -131,7 +132,8 @@ async function handleFileUpload(
|
||||
documentId?: number | null,
|
||||
isReupload: boolean = false,
|
||||
jwtToken?: string,
|
||||
attachments?: File[]
|
||||
attachments?: File[],
|
||||
attributeType?: string
|
||||
): Promise<FileUploadResponse> {
|
||||
// console.log('【handleFileUpload】开始上传:', {
|
||||
// fileName,
|
||||
@@ -153,7 +155,8 @@ async function handleFileUpload(
|
||||
documentId,
|
||||
isReupload,
|
||||
jwtToken,
|
||||
attachments
|
||||
attachments,
|
||||
attributeType
|
||||
);
|
||||
|
||||
// console.log('【handleFileUpload】uploadDocumentToServer返回:', {
|
||||
@@ -349,6 +352,7 @@ export default function FilesUpload() {
|
||||
const [documentNumber, setDocumentNumber] = useState<string>("");
|
||||
const [remark, setRemark] = useState<string>("");
|
||||
const [currentFiles, setCurrentFiles] = useState<File[]>([]);
|
||||
const [attributeType, setAttributeType] = useState<string>(DEFAULT_CONTRACT_TYPE);
|
||||
|
||||
// 合同文件上传状态
|
||||
// 这些变量暂时未使用,但保留以备将来扩展
|
||||
@@ -1193,7 +1197,8 @@ export default function FilesUpload() {
|
||||
null,
|
||||
false,
|
||||
loaderData.frontendJWT || undefined,
|
||||
attachmentFiles
|
||||
attachmentFiles,
|
||||
attributeType
|
||||
);
|
||||
|
||||
// console.log('【合同上传】服务器响应数据:', uploadResp);
|
||||
@@ -1550,7 +1555,9 @@ export default function FilesUpload() {
|
||||
isTestDocument,
|
||||
temp_n > 1 ? firstFileDocumentId : null, // 第二个文件及以后使用第一个文件的document_id
|
||||
false,
|
||||
loaderData.frontendJWT || undefined
|
||||
loaderData.frontendJWT || undefined,
|
||||
undefined,
|
||||
attributeType
|
||||
);
|
||||
|
||||
const timeoutPromise = new Promise<FileUploadResponse>((_, reject) => {
|
||||
@@ -2326,6 +2333,28 @@ export default function FilesUpload() {
|
||||
</select>
|
||||
<div className="form-tip">优先级影响文档在队列中的处理顺序</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">
|
||||
<label htmlFor="docNumber" className="form-label">文档编号</label>
|
||||
<input
|
||||
|
||||
+100
-15
@@ -30,7 +30,7 @@ import { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
||||
import type { FilePreviewHandle } from "~/components/reviews/FilePreview";
|
||||
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";
|
||||
|
||||
// 导入评查详情页面组件
|
||||
@@ -72,6 +72,7 @@ interface Statistics {
|
||||
success: number;
|
||||
warning: number;
|
||||
error: number;
|
||||
notApplicable: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
@@ -193,19 +194,94 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const { getUserSession } = await import("~/api/login/auth.server");
|
||||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||
|
||||
// 🆕 使用新的后端API获取评查点数据(单次请求替代原7次请求)
|
||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||
// 🆕 使用新的统一API获取评查点数据
|
||||
// 先尝试新的统一评查接口
|
||||
const unifiedData = await getUnifiedEvaluationResults(id, request);
|
||||
|
||||
// ⚠️ 原方法已注释(保留以备回退)
|
||||
// const reviewData = await getReviewPoints(id, request);
|
||||
// 如果统一接口返回错误或flow_type为legacy,使用原有API
|
||||
if ('error' in unifiedData || !unifiedData.flow_type) {
|
||||
console.log("[Reviews Loader] 统一接口不可用,使用旧接口...");
|
||||
const reviewData = await getReviewPoints_fromApi(id, request);
|
||||
|
||||
if ('error' in reviewData && reviewData.error) {
|
||||
console.error("[Reviews Loader] 获取评查点数据错误:", reviewData.error);
|
||||
return Response.json({ result: false, message: reviewData.error });
|
||||
if ('error' in reviewData && reviewData.error) {
|
||||
console.error("[Reviews Loader] 获取评查点数据错误:", 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({
|
||||
previousRoute: previousRoute,
|
||||
document: reviewData.document,
|
||||
@@ -214,11 +290,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
statistics: reviewData.stats,
|
||||
comparison_document: reviewData.comparison_document,
|
||||
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) {
|
||||
console.error('[Reviews Loader] 获取评查数据失败:', error);
|
||||
@@ -310,6 +386,7 @@ export default function ReviewDetails() {
|
||||
const [targetPage, setTargetPage] = 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 [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 [pendingUpdate, setPendingUpdate] = useState<{
|
||||
reviewPointResultId: string;
|
||||
@@ -475,17 +552,19 @@ export default function ReviewDetails() {
|
||||
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能够触发
|
||||
if (reviewPointId === activeReviewPointResultId && page) {
|
||||
setTargetPage(undefined);
|
||||
setCharPositions(undefined);
|
||||
setTextBbox(undefined);
|
||||
setHighlightValue(undefined);
|
||||
// 使用setTimeout确保状态更新后再设置新的targetPage、charPositions和highlightValue
|
||||
setTimeout(() => {
|
||||
setActiveReviewPointResultId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
setCharPositions(charPos);
|
||||
setTextBbox(bbox);
|
||||
setHighlightValue(value);
|
||||
}, 0);
|
||||
} else {
|
||||
@@ -493,6 +572,7 @@ export default function ReviewDetails() {
|
||||
setActiveReviewPointResultId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
setCharPositions(charPos);
|
||||
setTextBbox(bbox);
|
||||
setHighlightValue(value);
|
||||
}
|
||||
};
|
||||
@@ -890,6 +970,7 @@ export default function ReviewDetails() {
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
targetPage={targetPage}
|
||||
charPositions={charPositions}
|
||||
textBbox={textBbox}
|
||||
highlightValue={highlightValue}
|
||||
userInfo={loaderData.userInfo}
|
||||
aiSuggestionReplace={aiSuggestionReplace}
|
||||
@@ -909,6 +990,9 @@ export default function ReviewDetails() {
|
||||
onStatusChange={handleReviewPointStatusChange}
|
||||
fileFormat={reviewData.fileInfo.fileFormat}
|
||||
onAiSuggestionReplace={handleAiSuggestionReplace}
|
||||
flowType={reviewData.flowType}
|
||||
scoredResults={reviewData.scoredResults}
|
||||
scoredSummary={reviewData.scoredSummary}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -940,6 +1024,7 @@ export default function ReviewDetails() {
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
targetPage={targetPage}
|
||||
charPositions={charPositions}
|
||||
textBbox={textBbox}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user