From 737040fc3a64133694cf869e833932d4db8c4826 Mon Sep 17 00:00:00 2001 From: wren Date: Wed, 18 Mar 2026 21:53:25 +0800 Subject: [PATCH 01/20] =?UTF-8?q?feat(constants):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=90=88=E5=90=8C=E7=B1=BB=E5=9E=8B=E5=B8=B8=E9=87=8F=E5=AE=9A?= =?UTF-8?q?=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/constants/contractTypes.ts | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/constants/contractTypes.ts diff --git a/app/constants/contractTypes.ts b/app/constants/contractTypes.ts new file mode 100644 index 0000000..15b09d5 --- /dev/null +++ b/app/constants/contractTypes.ts @@ -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 = "通用"; From 1a2ce367af9120feb30c616f1798ac2e6badef7e Mon Sep 17 00:00:00 2001 From: wren Date: Wed, 18 Mar 2026 21:55:04 +0800 Subject: [PATCH 02/20] =?UTF-8?q?feat(api):=20uploadDocumentToServer=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20attribute=5Ftype=20=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/files/files-upload.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index c01b84c..9ee3879 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -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 From 9fd222ef3d857e70f3f0250ee5b2f85e60e09261 Mon Sep 17 00:00:00 2001 From: wren Date: Wed, 18 Mar 2026 21:57:07 +0800 Subject: [PATCH 03/20] =?UTF-8?q?feat(upload):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=90=88=E5=90=8C=E7=B1=BB=E5=9E=8B=E9=80=89=E6=8B=A9=E5=99=A8?= =?UTF-8?q?=EF=BC=8C=E7=94=A8=E6=88=B7=E5=BF=85=E9=A1=BB=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/files.upload.tsx | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 09f4092..ac4daf5 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -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 { // console.log('【handleFileUpload】开始上传:', { // fileName, @@ -349,6 +351,7 @@ export default function FilesUpload() { const [documentNumber, setDocumentNumber] = useState(""); const [remark, setRemark] = useState(""); const [currentFiles, setCurrentFiles] = useState([]); + const [attributeType, setAttributeType] = useState(DEFAULT_CONTRACT_TYPE); // 合同文件上传状态 // 这些变量暂时未使用,但保留以备将来扩展 @@ -1193,7 +1196,8 @@ export default function FilesUpload() { null, false, loaderData.frontendJWT || undefined, - attachmentFiles + attachmentFiles, + attributeType ); // console.log('【合同上传】服务器响应数据:', uploadResp); @@ -1550,7 +1554,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((_, reject) => { @@ -2326,6 +2332,30 @@ export default function FilesUpload() {
优先级影响文档在队列中的处理顺序
+ {/* 只有选择合同类型时才显示合同子类型选择器 */} + {isContractType && ( +
+ + +
选择正确的合同类型以应用对应的审核规则
+
+ )}
Date: Wed, 18 Mar 2026 22:00:26 +0800 Subject: [PATCH 04/20] =?UTF-8?q?feat(cross-checking):=20=E4=BA=A4?= =?UTF-8?q?=E5=8F=89=E8=AF=84=E6=9F=A5=E4=B8=8A=E4=BC=A0=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=90=88=E5=90=8C=E7=B1=BB=E5=9E=8B=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/cross-checking/cross-files-upload.ts | 7 +++-- app/routes/cross-checking.upload.tsx | 31 +++++++++++++++++++- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/app/api/cross-checking/cross-files-upload.ts b/app/api/cross-checking/cross-files-upload.ts index d2b674a..1ef7341 100644 --- a/app/api/cross-checking/cross-files-upload.ts +++ b/app/api/cross-checking/cross-files-upload.ts @@ -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}>; 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) diff --git a/app/routes/cross-checking.upload.tsx b/app/routes/cross-checking.upload.tsx index 3fea53f..212d297 100644 --- a/app/routes/cross-checking.upload.tsx +++ b/app/routes/cross-checking.upload.tsx @@ -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(""); const [remark] = useState(""); const [isTestDocument] = useState(false); + const [attributeType, setAttributeType] = useState(DEFAULT_CONTRACT_TYPE); // 文件管理状态 - 简化为单文件上传 const [uploadedFile, setUploadedFile] = useState(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() {
+ {/* 合同类型选择器 - 仅在选择合同类型文档时显示 */} + {selectedDocTypeId === 1 && ( +
+
+
选择合同类型(可选)
+
+ {CONTRACT_TYPES.map((type) => ( + + ))} +
+
+
+ )} + {/* 文件上传区域 - 左右布局 */} From df02ed79c2f09b9af712ef604f95a19854ff0fd4 Mon Sep 17 00:00:00 2001 From: wren Date: Wed, 18 Mar 2026 22:51:19 +0800 Subject: [PATCH 05/20] =?UTF-8?q?fix(upload):=20=E5=AD=90=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E4=B8=8B=E6=8B=89=E6=A1=86=E5=A7=8B=E7=BB=88=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=EF=BC=8C=E4=B8=8D=E9=99=90=E4=BA=8E=E5=90=88=E5=90=8C?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/files.upload.tsx | 46 ++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index ac4daf5..3bf4e4a 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -2332,30 +2332,28 @@ export default function FilesUpload() {
优先级影响文档在队列中的处理顺序
- {/* 只有选择合同类型时才显示合同子类型选择器 */} - {isContractType && ( -
- - -
选择正确的合同类型以应用对应的审核规则
-
- )} + {/* 子类型(专属类型)- 始终显示 */} +
+ + +
选择文档专属类型以应用对应的审核规则(合同大类请选择技术/租赁/买卖等)
+
Date: Wed, 18 Mar 2026 23:15:05 +0800 Subject: [PATCH 06/20] =?UTF-8?q?fix(upload):=20=E4=BC=A0=E9=80=92=20attri?= =?UTF-8?q?buteType=20=E5=8F=82=E6=95=B0=E5=88=B0=20uploadDocumentToServer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/files.upload.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 3bf4e4a..a3028a8 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -155,7 +155,8 @@ async function handleFileUpload( documentId, isReupload, jwtToken, - attachments + attachments, + attributeType ); // console.log('【handleFileUpload】uploadDocumentToServer返回:', { From 11c00d34bc0a22ba591727735789a7914b74f48a Mon Sep 17 00:00:00 2001 From: wren Date: Thu, 19 Mar 2026 15:50:12 +0800 Subject: [PATCH 07/20] =?UTF-8?q?fix(ReviewPointsList):=20=E8=AF=BB?= =?UTF-8?q?=E5=8F=96=E5=90=8E=E7=AB=AF=E4=BC=A0=E5=85=A5=E7=9A=84=20per-fi?= =?UTF-8?q?eld=20res=20=E6=9B=BF=E4=BB=A3=E5=89=8D=E7=AB=AF=E8=87=AA?= =?UTF-8?q?=E8=A1=8C=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit renderModelRule 对 AI 评查点的 res 计算改为优先使用后端写入 config.fields[key].res 的值,fallback 到原来的 value 非空判定。 解决了评查点不通过但所有细项都显示绿色的问题。 Co-Authored-By: Claude Opus 4.6 --- app/components/reviews/ReviewPointsList.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 497956c..39a87fd 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -1582,6 +1582,7 @@ export function ReviewPointsList({ page: number | string; value: string; char_positions?: CharPosition[]; + res?: boolean; }>; ai_suggestion?: { summary?: string; @@ -1634,7 +1635,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(
+ {/* 未涉及数量(仅在有未涉及评查点时显示,仅统计展示不支持过滤) */} + {notApplicableToShow > 0 && ( + <> +
+
+
+ {notApplicableToShow} + 未涉及 +
+
+ + )} ); diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index abfe776..2460ad6 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -72,6 +72,7 @@ interface Statistics { success: number; warning: number; error: number; + notApplicable: number; score: number; } @@ -239,6 +240,7 @@ export async function loader({ request }: LoaderFunctionArgs) { success: unifiedData.summary?.passed_count || 0, error: unifiedData.summary?.failed_count || 0, warning: 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, From b2e8d3299c402633d7d65f0103572c45e3da8279 Mon Sep 17 00:00:00 2001 From: wren Date: Fri, 20 Mar 2026 18:53:31 +0800 Subject: [PATCH 11/20] fix(reviews): use legacy reviewPoints for graphrag mode to prevent content null crash - reviewPoints from unified API lacks 'content' field expected by ReviewPointsList - Use reviewData.reviewPoints (from /api/v3/review-points) which has proper content structure - Scored data still available via scoredResults prop Co-Authored-By: Claude Opus 4.6 (1M context) --- app/routes/reviews.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 2460ad6..c7dd40b 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -233,7 +233,7 @@ export async function loader({ request }: LoaderFunctionArgs) { return Response.json({ previousRoute: previousRoute, document: ('document' in reviewData && !('error' in reviewData)) ? reviewData.document : null, - reviewPoints: unifiedData.results, + reviewPoints: ('reviewPoints' in reviewData && !('error' in reviewData)) ? reviewData.reviewPoints : unifiedData.results, reviewInfo: { reviewTime: unifiedData.evaluated_at, reviewModel: 'GraphRAG', ruleGroup: '', result: '', issueCount: unifiedData.summary?.total_points || 0 }, statistics: { total: unifiedData.summary?.total_points || 0, From 401097536ed4a0ceaf4a5007a3e5de74429c9a36 Mon Sep 17 00:00:00 2001 From: wren Date: Fri, 20 Mar 2026 19:05:39 +0800 Subject: [PATCH 12/20] fix(reviews): use correct key 'data' from getReviewPoints_fromApi response - getReviewPoints_fromApi returns {data, stats, ...} not {reviewPoints, ...} - Fixed key check from 'reviewPoints' to 'data' Co-Authored-By: Claude Opus 4.6 (1M context) --- app/routes/reviews.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index c7dd40b..8f1ffc0 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -233,7 +233,7 @@ export async function loader({ request }: LoaderFunctionArgs) { return Response.json({ previousRoute: previousRoute, document: ('document' in reviewData && !('error' in reviewData)) ? reviewData.document : null, - reviewPoints: ('reviewPoints' in reviewData && !('error' in reviewData)) ? reviewData.reviewPoints : unifiedData.results, + reviewPoints: ('data' in reviewData && !('error' in reviewData)) ? reviewData.data : unifiedData.results, reviewInfo: { reviewTime: unifiedData.evaluated_at, reviewModel: 'GraphRAG', ruleGroup: '', result: '', issueCount: unifiedData.summary?.total_points || 0 }, statistics: { total: unifiedData.summary?.total_points || 0, From b13e758db15183cec0175fb8488a740d8688d01b Mon Sep 17 00:00:00 2001 From: wren Date: Fri, 20 Mar 2026 19:08:33 +0800 Subject: [PATCH 13/20] fix(reviews): map failed_count to warning, always show not-applicable badge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - failed_count should be 'warning' not 'error' for scored eval - Show '未涉及' badge always, not only when count > 0 Co-Authored-By: Claude Opus 4.6 (1M context) --- app/components/reviews/ReviewPointsList.tsx | 20 ++++++++------------ app/routes/reviews.tsx | 4 ++-- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index def63d9..7871ea4 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -746,18 +746,14 @@ export function ReviewPointsList({ 错误 - {/* 未涉及数量(仅在有未涉及评查点时显示,仅统计展示不支持过滤) */} - {notApplicableToShow > 0 && ( - <> -
-
-
- {notApplicableToShow} - 未涉及 -
-
- - )} + {/* 未涉及数量 */} +
+
+
+ {notApplicableToShow} + 未涉及 +
+
); diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 8f1ffc0..1e50eda 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -238,8 +238,8 @@ export async function loader({ request }: LoaderFunctionArgs) { statistics: { total: unifiedData.summary?.total_points || 0, success: unifiedData.summary?.passed_count || 0, - error: unifiedData.summary?.failed_count || 0, - warning: 0, + warning: unifiedData.summary?.failed_count || 0, + error: 0, notApplicable: unifiedData.summary?.not_applicable_count || 0, score: unifiedData.summary?.total_score || 0 }, From 0d8b9b1976c6da0d2f5a10bbf258aa1e3a9f139e Mon Sep 17 00:00:00 2001 From: wren Date: Fri, 20 Mar 2026 19:16:35 +0800 Subject: [PATCH 14/20] fix(reviews): make not-applicable button clickable with filter toggle Co-Authored-By: Claude Opus 4.6 (1M context) --- app/components/reviews/ReviewPointsList.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 7871ea4..d6f7766 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -634,6 +634,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); @@ -749,10 +752,15 @@ export function ReviewPointsList({ {/* 未涉及数量 */}
-
+
+
From c0f80042781fd81d9e64ff7fde5e424265bc6a4f Mon Sep 17 00:00:00 2001 From: wren Date: Fri, 20 Mar 2026 19:29:48 +0800 Subject: [PATCH 15/20] feat(reviews): display not-applicable evaluation points in review list - Create placeholder reviewPoints from not_applicable unified results - Merge with existing reviewPoints for display - Count notApplicable from reviewPoints for accurate statistics Co-Authored-By: Claude Opus 4.6 (1M context) --- app/components/reviews/ReviewPointsList.tsx | 5 +++- app/routes/reviews.tsx | 28 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index d6f7766..14e7577 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -690,7 +690,10 @@ export function ReviewPointsList({ const successToShow = successCount || statsToUse.success; const warningToShow = warningCount || statsToUse.warning; const errorToShow = errorCount || statsToUse.error; - const notApplicableToShow = statsToUse.notApplicable || 0; + const notApplicableCount = reviewPoints.filter( + point => point.status === 'notApplicable' || point.status === 'not_applicable' + ).length; + const notApplicableToShow = notApplicableCount || statsToUse.notApplicable || 0; return (
diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 1e50eda..0e64d6f 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -230,10 +230,36 @@ export async function loader({ request }: LoaderFunctionArgs) { // 先获取文档基本信息(统一接口不返回文档内容) 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: ('data' in reviewData && !('error' in reviewData)) ? reviewData.data : unifiedData.results, + reviewPoints: allReviewPoints, reviewInfo: { reviewTime: unifiedData.evaluated_at, reviewModel: 'GraphRAG', ruleGroup: '', result: '', issueCount: unifiedData.summary?.total_points || 0 }, statistics: { total: unifiedData.summary?.total_points || 0, From 32bee8799872185756929110da69157950a564a4 Mon Sep 17 00:00:00 2001 From: wren Date: Mon, 23 Mar 2026 01:05:48 +0800 Subject: [PATCH 16/20] =?UTF-8?q?fix(reviews):=20show=20field=20value=20wh?= =?UTF-8?q?en=20res=3Dfalse=20instead=20of=20showing=20'=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second rendering path (entity fields) incorrectly hid values when res=false, showing '缺失' even for fields with extracted values. Fixed to match first rendering path: only show '缺失' when both res=false AND value is empty. Values always display when present. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/components/reviews/ReviewPointsList.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 14e7577..69b5937 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -1746,16 +1746,16 @@ export function ReviewPointsList({ {!value.page && (reviewPoint.contentPage && !reviewPoint.contentPage[key]) && ( )} - {/* 缺失显示 */} - {!res && ( + {/* 缺失显示:仅在无值时显示 */} + {!res && !value.value && ( 缺失 )}
- - {/* 主要值显示 */} - {res && ( + + {/* 主要值显示:有值就显示,不受res影响 */} + {value.value && ( )} From 33fbd6b8608876b0e00af24f3f610ad44f40f58a Mon Sep 17 00:00:00 2001 From: DocAuditAI Dev Date: Mon, 23 Mar 2026 16:44:22 +0800 Subject: [PATCH 17/20] feat(pdf): support GraphRAG text_bbox highlighting in PDF viewer When documents are processed through GraphRAG pipeline, coordinate enrichment produces text_bbox (paragraph-level coordinates) instead of char_positions (character-level OCR coordinates). Added resolveCharPositions() helper that converts text_bbox to CharPosition[] format, enabling PDF highlight rendering for GraphRAG-processed documents. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../cross-checking/ReviewPointsList.tsx | 58 +++++++++++----- app/components/reviews/FilePreview.tsx | 4 +- app/components/reviews/ReviewPointsList.tsx | 68 +++++++++++++------ .../reviews/previewComponents/PdfPreview.tsx | 14 ++++ app/routes/reviews.tsx | 8 ++- 5 files changed, 113 insertions(+), 39 deletions(-) diff --git a/app/components/cross-checking/ReviewPointsList.tsx b/app/components/cross-checking/ReviewPointsList.tsx index 023e842..d212532 100644 --- a/app/components/cross-checking/ReviewPointsList.tsx +++ b/app/components/cross-checking/ReviewPointsList.tsx @@ -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}对应的索引内容`); } @@ -1959,9 +1985,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}对应的索引内容`); } @@ -1971,9 +1997,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}对应的索引内容`); } diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index adbd7d9..e692608 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -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(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) { +export const FilePreview = forwardRef(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(funct filePath={real_path} targetPage={targetPage} charPositions={charPositions} + textBbox={textBbox} isStructuredView={isStructuredView} activeReviewPointResultId={activeReviewPointResultId} pageOffset={pageOffset} diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index 69b5937..14dd505 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -82,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; +} + + /** * 评查点类型定义 * 用于展示单个评查结果 @@ -1262,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; } } @@ -1276,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; } } @@ -1312,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}对应的索引内容`); @@ -1396,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}对应的索引内容`); @@ -1425,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}对应的索引内容`); @@ -1569,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}对应的索引内容`); } @@ -1581,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}对应的索引内容`); } @@ -1714,10 +1740,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}对应的索引内容`); } @@ -1727,9 +1753,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}对应的索引内容`); } diff --git a/app/components/reviews/previewComponents/PdfPreview.tsx b/app/components/reviews/previewComponents/PdfPreview.tsx index 3bfbaf2..9b6833e 100644 --- a/app/components/reviews/previewComponents/PdfPreview.tsx +++ b/app/components/reviews/previewComponents/PdfPreview.tsx @@ -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; } diff --git a/app/routes/reviews.tsx b/app/routes/reviews.tsx index 0e64d6f..a04ba7d 100644 --- a/app/routes/reviews.tsx +++ b/app/routes/reviews.tsx @@ -386,6 +386,7 @@ export default function ReviewDetails() { const [targetPage, setTargetPage] = useState(undefined); const [templateTargetPage, setTemplateTargetPage] = useState(undefined); const [charPositions, setCharPositions] = useState | undefined>(undefined); + const [textBbox, setTextBbox] = useState<{ x_min: number; y_min: number; x_max: number; y_max: number } | undefined>(undefined); const [highlightValue, setHighlightValue] = useState(undefined); const [pendingUpdate, setPendingUpdate] = useState<{ reviewPointResultId: string; @@ -551,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 { @@ -569,6 +572,7 @@ export default function ReviewDetails() { setActiveReviewPointResultId(reviewPointId); setTargetPage(page); setCharPositions(charPos); + setTextBbox(bbox); setHighlightValue(value); } }; @@ -966,6 +970,7 @@ export default function ReviewDetails() { activeReviewPointResultId={activeReviewPointResultId} targetPage={targetPage} charPositions={charPositions} + textBbox={textBbox} highlightValue={highlightValue} userInfo={loaderData.userInfo} aiSuggestionReplace={aiSuggestionReplace} @@ -1019,6 +1024,7 @@ export default function ReviewDetails() { activeReviewPointResultId={activeReviewPointResultId} targetPage={targetPage} charPositions={charPositions} + textBbox={textBbox} /> From 4de16d66da833fea7596d94fe676f5a7804d184d Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Mon, 9 Mar 2026 17:54:11 +0800 Subject: [PATCH 18/20] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E6=B9=9B?= =?UTF-8?q?=E6=B1=9F=E7=9A=84=E5=9C=B0=E5=8C=BA=E9=80=89=E6=8B=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/routes/entry-modules._index.tsx | 17 +++++++++++++++++ app/routes/entry-modules.new.tsx | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/app/routes/entry-modules._index.tsx b/app/routes/entry-modules._index.tsx index cb695d0..61798ec 100644 --- a/app/routes/entry-modules._index.tsx +++ b/app/routes/entry-modules._index.tsx @@ -100,6 +100,23 @@ const AREA_OPTIONS = [ { 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: "汕头" }, + // { value: "河源", label: "河源" }, + // { value: "阳江", label: "阳江" }, + // { value: "清远", label: "清远" }, + // { value: "东莞", label: "东莞" }, + // { value: "中山", label: "中山" }, + // { value: "肇庆", label: "肇庆" }, + // { value: "韶关", label: "韶关" }, { value: "省局", label: "省局" } ]; diff --git a/app/routes/entry-modules.new.tsx b/app/routes/entry-modules.new.tsx index e7efb0e..f594ef4 100644 --- a/app/routes/entry-modules.new.tsx +++ b/app/routes/entry-modules.new.tsx @@ -76,6 +76,23 @@ const AREA_OPTIONS = [ { 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: "汕头" }, + // { value: "河源", label: "河源" }, + // { value: "阳江", label: "阳江" }, + // { value: "清远", label: "清远" }, + // { value: "东莞", label: "东莞" }, + // { value: "中山", label: "中山" }, + // { value: "肇庆", label: "肇庆" }, + // { value: "韶关", label: "韶关" }, { value: "省局", label: "省局" } ]; From 519287c7f4c6ef4524ec4a38d5fa772b9b008d2d Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Fri, 13 Mar 2026 14:10:55 +0800 Subject: [PATCH 19/20] =?UTF-8?q?feat:=20=E6=B8=B2=E6=9F=93=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E7=9A=84=E8=AF=84=E6=9F=A5=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=E4=B9=9F=E6=B7=BB=E5=8A=A0=E4=B8=8Atrue=E5=92=8Cfalse=E7=9A=84?= =?UTF-8?q?=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/cross-checking/ReviewPointsList.tsx | 9 +++++---- app/components/reviews/ReviewPointsList.tsx | 10 ++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/components/cross-checking/ReviewPointsList.tsx b/app/components/cross-checking/ReviewPointsList.tsx index d212532..e7908f3 100644 --- a/app/components/cross-checking/ReviewPointsList.tsx +++ b/app/components/cross-checking/ReviewPointsList.tsx @@ -1922,6 +1922,7 @@ export function ReviewPointsList({ fields?: Record; ai_suggestion?: { @@ -1980,8 +1981,8 @@ export function ReviewPointsList({ - )} - { reviewPoint.pointName === '签署甲方详细信息校验' && ( + ); + })()} + { reviewPoint.pointName === '签署甲方详细信息校验' && (() => { + const firstContentKey = reviewPoint.content ? Object.keys(reviewPoint.content)[0] : undefined; + const firstContentValue = firstContentKey ? reviewPoint.content![firstContentKey]?.value : undefined; + return ( - )} + ); + })()} {/*
{reviewPoint.title}
diff --git a/app/components/reviews/ReviewPointsList.tsx b/app/components/reviews/ReviewPointsList.tsx index eb89e18..59190e9 100644 --- a/app/components/reviews/ReviewPointsList.tsx +++ b/app/components/reviews/ReviewPointsList.tsx @@ -1902,6 +1902,7 @@ export function ReviewPointsList({ if (!isReplaceDisabled && onAiSuggestionReplace && config.fields) { // 从 config.fields[key] 中获取对应的字段信息 const fieldData = config.fields[key]; + console.log("替换原始数据", config, key) if (fieldData) { // 调用回调函数,传递搜索文本(原文)、替换文本(AI建议)和页码 onAiSuggestionReplace( @@ -2603,7 +2604,10 @@ export function ReviewPointsList({ {/*
*/}
{reviewPoint.pointName}
- { reviewPoint.pointName === '签署乙方详细信息校验' && ( + { reviewPoint.pointName === '签署乙方详细信息校验' && (() => { + const firstContentKey = reviewPoint.content ? Object.keys(reviewPoint.content)[0] : undefined; + const firstContentValue = firstContentKey ? reviewPoint.content![firstContentKey]?.value : undefined; + return ( - )} - { reviewPoint.pointName === '签署甲方详细信息校验' && ( + ); + })()} + { reviewPoint.pointName === '签署甲方详细信息校验' && (() => { + const firstContentKey = reviewPoint.content ? Object.keys(reviewPoint.content)[0] : undefined; + const firstContentValue = firstContentKey ? reviewPoint.content![firstContentKey]?.value : undefined; + return ( - )} + ); + })()}
{/*