fix: 1. 优化collabora的高亮效果,不固定主要页面。

2. 优化评查结果中的下载按钮,如果加载docx文件的话需要先保存再下载。
3. 交叉评查结果中添加返回按钮,并实现打开对应的任务的文档列表。
4. 文档类型的添加,添加绑定合同管理为入口的时候文档类型名称必须是要附带‘合同’字符。
This commit is contained in:
2025-12-17 01:09:23 +08:00
parent d04882bf51
commit 6fa65ff156
13 changed files with 223 additions and 65 deletions
+1 -1
View File
@@ -92,7 +92,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
// 提取占位符
const placeholders = await extractPlaceholdersFromDocx(filePath);
console.log('[Loader] 提取到的占位符:', placeholders);
// console.log('[Loader] 提取到的占位符:', placeholders);
// 生成默认 schema
placeholderSchema = generateDefaultSchema(placeholders);
+2 -1
View File
@@ -144,7 +144,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
return Response.json({ error: `文件复制失败: ${copyResponse.error}` }, { status: 500 });
}
console.log('[Draft] 文件复制成功:', copyResponse.data);
console.log('[Draft] 文件复制成功:');
// console.log('[Draft] 文件复制成功:', copyResponse.data);
// 重定向到草稿编辑页面,通过 URL 参数传递文件路径和模板 ID
const draftUrl = `/contract-draft/1?filePath=${encodeURIComponent(newFilePath)}&templateId=${templateId}&title=${encodeURIComponent(title)}`;
+1 -1
View File
@@ -438,7 +438,7 @@ export default function CrossCheckingIndex() {
// 渲染进度条
const renderProgress = (progress: number) => (
<div className="flex items-center space-x-2">
<div className="progress-bar w-16">
<div className="progress-bar w-16 mb-0">
<div
className={`progress-bar-fill ${getProgressClass(progress)}`}
style={{ width: `${progress}%` }}
+18
View File
@@ -754,6 +754,24 @@ export default function CrossCheckingResult() {
</div>
</div>
{/* 返回按钮 */}
<button
type="button"
onClick={() => {
// 返回到交叉评查列表页,并带上任务信息以自动打开模态框
const params = new URLSearchParams({
openModal: 'true',
taskId: taskId || '',
taskName: taskName || '任务详情'
});
navigate(`/cross-checking?${params.toString()}`);
}}
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-600 border border-gray-400 rounded-md hover:bg-gray-100 hover:text-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 mr-3"
>
<i className="ri-arrow-left-line mr-1.5"></i>
</button>
{/* 结构比对/查看评查结果按钮 - 仅当文档类型包含"合同"且有模板时显示 */}
{hasTemplateForCompare && (
<button
+47 -5
View File
@@ -151,6 +151,20 @@ export async function action({ request }: ActionFunctionArgs) {
errors.name = "文档类型名称不能为空";
}
// 验证:如果入口模块为"合同管理",文档类型名称必须包含"合同"
if (entryModuleId && name) {
// 获取入口模块列表以验证模块名称
const entryModulesResponse = await getEntryModules(frontendJWT);
if (!entryModulesResponse.error && entryModulesResponse.data) {
const selectedModule = entryModulesResponse.data.find(
(m: { id: number; name: string }) => m.id.toString() === entryModuleId
);
if (selectedModule?.name === '合同管理' && !name.includes('合同')) {
errors.name = '入口模块为"合同管理"时,文档类型名称必须包含"合同"';
}
}
}
if (selectedGroups.length === 0) {
errors.groups = "请至少选择一个关联的评查点分组";
}
@@ -267,6 +281,17 @@ export default function DocumentTypeNew() {
useEffect(() => {
console.log('返回的评查点分组数据',ruleGroups)
}, [ruleGroups])
// 新增模式下,设置模板的默认值(选择第一个选项)
useEffect(() => {
if (!isEditMode) {
setFormData(prev => ({
...prev,
llmExtractionTemplateId: prev.llmExtractionTemplateId || (llmExtractionTemplates[0]?.id?.toString() || ""),
vlmExtractionTemplateId: prev.vlmExtractionTemplateId || (vlmExtractionTemplates[0]?.id?.toString() || "")
}));
}
}, [isEditMode, llmExtractionTemplates, vlmExtractionTemplates]);
// 从actionData初始化本地错误
useEffect(() => {
@@ -357,10 +382,19 @@ export default function DocumentTypeNew() {
}, [documentType, ruleGroups]);
// 验证表单字段
const validateField = (field: string, value: string | string[]): string => {
const validateField = (field: string, value: string | string[], allFormData?: typeof formData): string => {
switch (field) {
case 'name':
return !value || (typeof value === 'string' && value.trim() === "") ? "文档类型名称不能为空" : "";
if (!value || (typeof value === 'string' && value.trim() === "")) {
return "文档类型名称不能为空";
}
// 检查入口模块是否为"合同管理",如果是则名称必须包含"合同"
const currentEntryModuleId = allFormData?.entryModuleId || formData.entryModuleId;
const selectedModule = entryModules.find((m: { id: number; name: string }) => m.id.toString() === currentEntryModuleId);
if (selectedModule?.name === '合同管理' && typeof value === 'string' && !value.includes('合同')) {
return '入口模块为"合同管理"时,文档类型名称必须包含"合同"';
}
return "";
case 'llmExtractionTemplate':
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择llm抽取提示词模板" : "";
case 'vlmExtractionTemplate':
@@ -483,11 +517,13 @@ export default function DocumentTypeNew() {
setTouchedFields(prev => ({...prev, name: true}));
}
setFormData(prev => ({ ...prev, [fieldName]: value }));
// 先更新 formData
const updatedFormData = { ...formData, [fieldName]: value };
setFormData(updatedFormData);
// 实时验证
if (name === 'name') {
const error = validateField(name, value);
const error = validateField(name, value, updatedFormData);
setFormErrors(prev => ({...prev, name: error}));
} else if (name === 'llm_extraction_template') {
const error = validateField('llmExtractionTemplate', value);
@@ -495,6 +531,12 @@ export default function DocumentTypeNew() {
} else if (name === 'vlm_extraction_template') {
const error = validateField('vlmExtractionTemplate', value);
setFormErrors(prev => ({...prev, vlmExtractionTemplate: error}));
} else if (name === 'entry_module_id') {
// 入口模块变更时,重新验证名称字段(如果名称已被触摸过)
if (touchedFields.name) {
const nameError = validateField('name', updatedFormData.name, updatedFormData);
setFormErrors(prev => ({...prev, name: nameError}));
}
}
};
@@ -522,7 +564,7 @@ export default function DocumentTypeNew() {
// 验证所有字段
const errors = {
name: validateField('name', formData.name),
name: validateField('name', formData.name, formData),
llmExtractionTemplate: validateField('llmExtractionTemplate', formData.llmExtractionTemplateId),
vlmExtractionTemplate: validateField('vlmExtractionTemplate', formData.vlmExtractionTemplateId),
groups: validateField('groups', formData.selectedGroups)
+64 -2
View File
@@ -26,8 +26,9 @@
*/
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
import { useState, useEffect } from "react";
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 { toastService } from "~/components/ui/Toast";
@@ -321,6 +322,64 @@ export default function ReviewDetails() {
pageNumber: number;
} | undefined>(undefined);
// FilePreview 组件的 ref,用于在下载前保存文档
const filePreviewRef = useRef<FilePreviewHandle>(null);
// CollaboraViewer 组件的 key,用于强制重新加载触发保存
const [collaboraKey, setCollaboraKey] = useState<number>(0);
// 保存文档的回调函数,传递给 ReviewTabs
// 通过改变 key 强制重新加载 CollaboraViewer 组件,触发组件卸载时的保存逻辑
const handleSaveBeforeDownload = useCallback(async (): Promise<boolean> => {
// 检查文件类型是否为 DOCX(需要 Collabora 保存)
const fileExtension = document?.path?.split('.').pop()?.toLowerCase();
if (fileExtension !== 'docx') {
// 非 DOCX 文件不需要保存
return true;
}
const collaboraRef = filePreviewRef.current?.collaboraViewerRef?.current;
if (!collaboraRef?.isReady) {
console.log('[Reviews] Collabora 未就绪,跳过保存');
return true;
}
try {
console.log('[Reviews] 通过重新加载 CollaboraViewer 保存文档...');
// 改变 key 触发组件卸载(会执行保存)和重新挂载
setCollaboraKey(prev => prev + 1);
// 等待组件重新加载完成
// 先等待组件卸载和重新挂载
await new Promise(resolve => setTimeout(resolve, 500));
// 轮询检查组件是否重新加载完成
const maxWaitTime = 30000; // 最大等待30秒
const checkInterval = 500; // 每500ms检查一次
let waitedTime = 0;
while (waitedTime < maxWaitTime) {
const newCollaboraRef = filePreviewRef.current?.collaboraViewerRef?.current;
if (newCollaboraRef?.isReady) {
console.log('[Reviews] CollaboraViewer 重新加载完成');
// 额外等待一小段时间确保文档完全就绪
await new Promise(resolve => setTimeout(resolve, 500));
return true;
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
waitedTime += checkInterval;
}
console.warn('[Reviews] 等待 CollaboraViewer 重新加载超时');
return true; // 超时也允许下载
} catch (error) {
console.error('[Reviews] 保存文档失败:', error);
toastService.error('保存文档失败,请重试');
return false;
}
}, [document?.path]);
// 🐛 调试:打印 loader 返回的完整数据到浏览器控制台
// useEffect(() => {
// if (typeof window !== 'undefined') {
@@ -450,7 +509,7 @@ export default function ReviewDetails() {
// 短暂延迟后清除参数,以便下次可以重新触发
setTimeout(() => {
setAiSuggestionReplace(undefined);
}, 1000);
}, 500);
};
// 刷新评审数据
@@ -777,6 +836,7 @@ export default function ReviewDetails() {
}}
onConfirmResults={handleConfirmResults}
jwtToken={frontendJWT}
onSaveBeforeDownload={handleSaveBeforeDownload}
>
{/* 评查结果选项卡内容 */}
{activeTab === 'preview' && (
@@ -794,6 +854,8 @@ export default function ReviewDetails() {
// });
return (
<FilePreview
key={`file-preview-${collaboraKey}`}
ref={filePreviewRef}
fileContent={document}
reviewPoints={reviewData.reviewPoints}
activeReviewPointResultId={activeReviewPointResultId}