Merge branch 'Wren-dev' into shiy-login

This commit is contained in:
2026-03-23 16:51:38 +08:00
15 changed files with 641 additions and 76 deletions
+5 -2
View File
@@ -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)
+93
View File
@@ -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
};
}
}
+9 -6
View File
@@ -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>
);
}
+32
View File
@@ -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>
);
}
+3
View File
@@ -0,0 +1,3 @@
export { ScoreBar } from './ScoreBar';
export { FieldResultList } from './FieldResultList';
export { ScoredResultCard } from './ScoredResultCard';
+3 -1
View File
@@ -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}
+132 -31
View File
@@ -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 resfallback 到 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;
}
+43
View File
@@ -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 = "通用";
+30 -1
View File
@@ -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 || ''} />
+33 -4
View File
@@ -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
View File
@@ -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>