fix: 1. 优化collabora的高亮效果,不固定主要页面。
2. 优化评查结果中的下载按钮,如果加载docx文件的话需要先保存再下载。 3. 交叉评查结果中添加返回按钮,并实现打开对应的任务的文档列表。 4. 文档类型的添加,添加绑定合同管理为入口的时候文档类型名称必须是要附带‘合同’字符。
This commit is contained in:
@@ -185,9 +185,10 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
}), [unoCommands, isDocumentLoaded, mode]);
|
}), [unoCommands, isDocumentLoaded, mode]);
|
||||||
|
|
||||||
// 5. 监听 targetPage 和 highlightText 变化,自动跳转并高亮
|
// 5. 监听 targetPage 和 highlightText 变化,自动跳转并高亮
|
||||||
// 根据是否有 targetPage 参数选择高亮方式:
|
// 新逻辑:从指定页(默认第1页)开始向下搜索第一个匹配项并高亮
|
||||||
// - 有 targetPage:使用 Python 脚本(支持跨页搜索和精确跳转)
|
// - 先跳转到指定页面
|
||||||
// - 无 targetPage:使用 UNO 命令(当前页面高亮,性能更好)
|
// - 使用 UNO 搜索从当前位置向下找第一个匹配项(UNO 搜索自带循环功能)
|
||||||
|
// - 对搜索到的文本设置高亮背景色
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 如果文档未加载完成,不执行跳转和高亮
|
// 如果文档未加载完成,不执行跳转和高亮
|
||||||
if (!isDocumentLoaded || !iframeRef.current?.contentWindow) {
|
if (!isDocumentLoaded || !iframeRef.current?.contentWindow) {
|
||||||
@@ -199,10 +200,12 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
const performHighlight = async () => {
|
const performHighlight = async () => {
|
||||||
const iframeWindow = iframeRef.current!.contentWindow!;
|
const iframeWindow = iframeRef.current!.contentWindow!;
|
||||||
const textToHighlight = highlightText.trim();
|
const textToHighlight = highlightText.trim();
|
||||||
|
// 确定起始页码:有 targetPage 则使用,否则默认从第1页开始
|
||||||
|
const startPage = (targetPage !== undefined && targetPage !== null) ? targetPage : 1;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 步骤1:清除之前的所有高亮
|
// 步骤1:清除之前的所有高亮
|
||||||
console.log('[CollaboraViewer] 步骤1:清除旧高亮...');
|
// console.log('[CollaboraViewer] 步骤1:清除旧高亮...');
|
||||||
await clearHighlights(iframeWindow, {
|
await clearHighlights(iframeWindow, {
|
||||||
color: 16776960, // 黄色
|
color: 16776960, // 黄色
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
@@ -211,36 +214,38 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
// 短暂延迟,确保清除操作完成
|
// 短暂延迟,确保清除操作完成
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
// 步骤2:根据是否有 targetPage 选择高亮方式
|
// 步骤2:跳转到起始页面
|
||||||
if (targetPage !== undefined && targetPage !== null) {
|
// console.log(`[CollaboraViewer] 步骤2:跳转到第 ${startPage} 页...`);
|
||||||
// 方案A:有 targetPage - 使用 Python 脚本(跨页搜索 + 高亮 + 跳转)
|
try {
|
||||||
console.log(`[CollaboraViewer] 步骤2A:使用 Python 脚本跳转到第 ${targetPage} 页并高亮 "${textToHighlight}"`);
|
await customGotoPage(iframeWindow, startPage);
|
||||||
|
// 等待页面跳转完成
|
||||||
const result = await pythonHighlightText(iframeWindow, textToHighlight, {
|
await new Promise(resolve => setTimeout(resolve, 300));
|
||||||
color: 16776960, // 黄色
|
} catch (gotoError) {
|
||||||
page: targetPage,
|
console.warn('[CollaboraViewer] 页面跳转失败,继续在当前位置搜索:', gotoError);
|
||||||
});
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
console.log(`[CollaboraViewer] ✓ Python 高亮成功: "${textToHighlight}" (第${targetPage}页, 共${result.highlightedCount}处)`);
|
|
||||||
} else {
|
|
||||||
console.error('[CollaboraViewer] ✗ Python 高亮失败:', result.message);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 方案B:无 targetPage - 使用 UNO 命令(当前页面高亮)
|
|
||||||
console.log(`[CollaboraViewer] 步骤2B:使用 UNO 命令在当前页高亮 "${textToHighlight}"`);
|
|
||||||
|
|
||||||
unoHighlightText(iframeWindow, textToHighlight, 16776960); // 黄色
|
|
||||||
|
|
||||||
// 短暂延迟,确保高亮操作完成
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100));
|
|
||||||
|
|
||||||
// 取消选中状态(避免高亮后文本仍被选中)
|
|
||||||
console.log('[CollaboraViewer] 步骤3:取消选中状态...');
|
|
||||||
sendUnoCommand(iframeWindow, '.uno:Escape', {});
|
|
||||||
|
|
||||||
console.log(`[CollaboraViewer] ✓ UNO 高亮完成: "${textToHighlight}"`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 步骤3:使用 UNO 搜索从当前位置向下找第一个匹配项
|
||||||
|
// UNO 搜索会自动跳转到匹配位置,如果到文档末尾没找到会循环回开头继续搜索
|
||||||
|
console.log(`[CollaboraViewer] 步骤3:从第 ${startPage} 页开始向下搜索...`);
|
||||||
|
unoSearchNext(iframeWindow, textToHighlight);
|
||||||
|
|
||||||
|
// 等待搜索完成
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
// 步骤4:对搜索到的文本(当前选中)设置高亮背景色
|
||||||
|
// console.log('[CollaboraViewer] 步骤4:设置高亮背景色...');
|
||||||
|
sendUnoCommand(iframeWindow, '.uno:BackColor', {
|
||||||
|
BackColor: { type: 'long', value: 16776960 }, // 黄色
|
||||||
|
});
|
||||||
|
|
||||||
|
// 短暂延迟,确保高亮操作完成
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// 步骤5:取消选中状态(避免高亮后文本仍被选中)
|
||||||
|
// console.log('[CollaboraViewer] 步骤5:取消选中状态...');
|
||||||
|
sendUnoCommand(iframeWindow, '.uno:Escape', {});
|
||||||
|
|
||||||
|
console.log(`[CollaboraViewer] ✓ 搜索高亮完成:(从第${startPage}页开始搜索)`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[CollaboraViewer] 高亮失败:', error);
|
console.error('[CollaboraViewer] 高亮失败:', error);
|
||||||
}
|
}
|
||||||
@@ -329,7 +334,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
// 步骤3:自动搜索下一个相同的占位符(如果还有的话)
|
// 步骤3:自动搜索下一个相同的占位符(如果还有的话)
|
||||||
console.log(`[CollaboraViewer] 步骤3:定位到下一个 "${newSearchText}"`);
|
// console.log(`[CollaboraViewer] 步骤3:定位到下一个 "${newSearchText}"`);
|
||||||
unoSearchNext(iframeRef.current.contentWindow, newSearchText);
|
unoSearchNext(iframeRef.current.contentWindow, newSearchText);
|
||||||
|
|
||||||
console.log('[CollaboraViewer] ✓ 静默替换完成,已定位到下一个占位符(如有)');
|
console.log('[CollaboraViewer] ✓ 静默替换完成,已定位到下一个占位符(如有)');
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function unoSearchNext(
|
|||||||
'Quiet': { type: 'boolean', value: quiet },
|
'Quiet': { type: 'boolean', value: quiet },
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[SearchReplace] 搜索下一个:', text, options);
|
// console.log('[SearchReplace] 搜索下一个:', text, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -650,7 +650,7 @@ export function DocumentListModal({
|
|||||||
placeholder="搜索文件名称或文档编号"
|
placeholder="搜索文件名称或文档编号"
|
||||||
value={searchKeyword}
|
value={searchKeyword}
|
||||||
onChange={(e) => handleSearchChange(e.target.value)}
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md text-sm w-64 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
className="pl-9 pr-4 py-2 border border-gray-300 rounded-md text-sm w-64 focus:outline-none focus:ring-0"
|
||||||
/>
|
/>
|
||||||
<i className="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
<i className="ri-search-line absolute left-3 top-1/2 -translate-y-1/2 text-gray-400"></i>
|
||||||
{searchKeyword && (
|
{searchKeyword && (
|
||||||
|
|||||||
@@ -1502,6 +1502,7 @@ export function ReviewPointsList({
|
|||||||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||||||
console.log("点击了其他评查点", mainTypeValue)
|
console.log("点击了其他评查点", mainTypeValue)
|
||||||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page), mainTypeValue.char_positions, mainTypeValue.value);
|
||||||
|
// onReviewPointSelect(reviewPoint.id, undefined, mainTypeValue.char_positions, mainTypeValue.value);
|
||||||
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
}else if(reviewPoint.contentPage && reviewPoint.contentPage[fieldKey]){
|
||||||
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
onReviewPointSelect(reviewPoint.id, Number(reviewPoint.contentPage[fieldKey]), mainTypeValue.char_positions, mainTypeValue.value);
|
||||||
}else{
|
}else{
|
||||||
|
|||||||
@@ -28,13 +28,16 @@ interface ReviewTabsProps {
|
|||||||
};
|
};
|
||||||
onConfirmResults: () => void;
|
onConfirmResults: () => void;
|
||||||
jwtToken?: string | null;
|
jwtToken?: string | null;
|
||||||
|
/** 下载前保存文档的回调,返回 true 表示保存成功可以继续下载 */
|
||||||
|
onSaveBeforeDownload?: () => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfirmResults, jwtToken }: ReviewTabsProps) {
|
export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfirmResults, jwtToken, onSaveBeforeDownload }: ReviewTabsProps) {
|
||||||
const [isNavigating, setIsNavigating] = useState(false);
|
const [isNavigating, setIsNavigating] = useState(false);
|
||||||
const [isReuploadModalOpen, setIsReuploadModalOpen] = useState(false);
|
const [isReuploadModalOpen, setIsReuploadModalOpen] = useState(false);
|
||||||
const [selectedTemplateFiles, setSelectedTemplateFiles] = useState<File[]>([]);
|
const [selectedTemplateFiles, setSelectedTemplateFiles] = useState<File[]>([]);
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [isSaving, setIsSaving] = useState(false); // 下载前保存文档的状态
|
||||||
const uploadAreaRef = useRef<UploadAreaRef>(null);
|
const uploadAreaRef = useRef<UploadAreaRef>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const revalidator = useRevalidator();
|
const revalidator = useRevalidator();
|
||||||
@@ -64,6 +67,23 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
|||||||
// 下载原文件
|
// 下载原文件
|
||||||
const handleDownloadFile = async () => {
|
const handleDownloadFile = async () => {
|
||||||
try {
|
try {
|
||||||
|
// 如果有保存回调,先执行保存(仅对 DOCX 文件有效)
|
||||||
|
if (onSaveBeforeDownload) {
|
||||||
|
setIsSaving(true);
|
||||||
|
toastService.info('正在保存文档...');
|
||||||
|
|
||||||
|
const saveSuccess = await onSaveBeforeDownload();
|
||||||
|
|
||||||
|
setIsSaving(false);
|
||||||
|
|
||||||
|
if (!saveSuccess) {
|
||||||
|
toastService.error('保存失败,下载已取消');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toastService.success('文档已保存,开始下载...');
|
||||||
|
}
|
||||||
|
|
||||||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||||||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(fileInfo.path || '')}`;
|
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(fileInfo.path || '')}`;
|
||||||
|
|
||||||
@@ -95,7 +115,7 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
|||||||
}, 100);
|
}, 100);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('下载文件失败:', error);
|
console.error('下载文件失败:', error);
|
||||||
alert(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -279,8 +299,17 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
|||||||
<button
|
<button
|
||||||
className="ant-btn ant-btn-default inline-flex items-center my-2"
|
className="ant-btn ant-btn-default inline-flex items-center my-2"
|
||||||
onClick={handleDownloadFile}
|
onClick={handleDownloadFile}
|
||||||
|
disabled={isSaving}
|
||||||
>
|
>
|
||||||
<i className="ri-file-download-line mr-1"></i> 下载
|
{isSaving ? (
|
||||||
|
<>
|
||||||
|
<i className="ri-loader-4-line mr-1 animate-spin"></i> 保存中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="ri-file-download-line mr-1"></i> 下载
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
{/* <button
|
{/* <button
|
||||||
className="ant-btn ant-btn-default flex items-center"
|
className="ant-btn ant-btn-default flex items-center"
|
||||||
|
|||||||
@@ -309,14 +309,14 @@ const getCurrentPort = (): string => {
|
|||||||
// 优先级:windowPort > NEXT_PUBLIC_API_PORT_CONFIG > NEXT_PUBLIC_PORT > API_PORT_CONFIG > PORT环境变量
|
// 优先级:windowPort > NEXT_PUBLIC_API_PORT_CONFIG > NEXT_PUBLIC_PORT > API_PORT_CONFIG > PORT环境变量
|
||||||
const result = windowPort || nextPublicApiPortConfig || nextPublicPort || apiPortConfig || portEnv || '';
|
const result = windowPort || nextPublicApiPortConfig || nextPublicPort || apiPortConfig || portEnv || '';
|
||||||
|
|
||||||
console.log('🔧 端口检测:', {
|
// console.log('🔧 端口检测:', {
|
||||||
windowPort: windowPort,
|
// windowPort: windowPort,
|
||||||
NEXT_PUBLIC_API_PORT_CONFIG: nextPublicApiPortConfig,
|
// NEXT_PUBLIC_API_PORT_CONFIG: nextPublicApiPortConfig,
|
||||||
NEXT_PUBLIC_PORT: nextPublicPort,
|
// NEXT_PUBLIC_PORT: nextPublicPort,
|
||||||
API_PORT_CONFIG: apiPortConfig,
|
// API_PORT_CONFIG: apiPortConfig,
|
||||||
PORT: portEnv,
|
// PORT: portEnv,
|
||||||
result: result
|
// result: result
|
||||||
});
|
// });
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -309,14 +309,14 @@ const getCurrentPort = (): string => {
|
|||||||
// 优先级:windowPort > NEXT_PUBLIC_API_PORT_CONFIG > NEXT_PUBLIC_PORT > API_PORT_CONFIG > PORT环境变量
|
// 优先级:windowPort > NEXT_PUBLIC_API_PORT_CONFIG > NEXT_PUBLIC_PORT > API_PORT_CONFIG > PORT环境变量
|
||||||
const result = windowPort || nextPublicApiPortConfig || nextPublicPort || apiPortConfig || portEnv || '';
|
const result = windowPort || nextPublicApiPortConfig || nextPublicPort || apiPortConfig || portEnv || '';
|
||||||
|
|
||||||
console.log('🔧 端口检测:', {
|
// console.log('🔧 端口检测:', {
|
||||||
windowPort: windowPort,
|
// windowPort: windowPort,
|
||||||
NEXT_PUBLIC_API_PORT_CONFIG: nextPublicApiPortConfig,
|
// NEXT_PUBLIC_API_PORT_CONFIG: nextPublicApiPortConfig,
|
||||||
NEXT_PUBLIC_PORT: nextPublicPort,
|
// NEXT_PUBLIC_PORT: nextPublicPort,
|
||||||
API_PORT_CONFIG: apiPortConfig,
|
// API_PORT_CONFIG: apiPortConfig,
|
||||||
PORT: portEnv,
|
// PORT: portEnv,
|
||||||
result: result
|
// result: result
|
||||||
});
|
// });
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
|||||||
|
|
||||||
// 提取占位符
|
// 提取占位符
|
||||||
const placeholders = await extractPlaceholdersFromDocx(filePath);
|
const placeholders = await extractPlaceholdersFromDocx(filePath);
|
||||||
console.log('[Loader] 提取到的占位符:', placeholders);
|
// console.log('[Loader] 提取到的占位符:', placeholders);
|
||||||
|
|
||||||
// 生成默认 schema
|
// 生成默认 schema
|
||||||
placeholderSchema = generateDefaultSchema(placeholders);
|
placeholderSchema = generateDefaultSchema(placeholders);
|
||||||
|
|||||||
@@ -144,7 +144,8 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
|||||||
return Response.json({ error: `文件复制失败: ${copyResponse.error}` }, { status: 500 });
|
return Response.json({ error: `文件复制失败: ${copyResponse.error}` }, { status: 500 });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Draft] 文件复制成功:', copyResponse.data);
|
console.log('[Draft] 文件复制成功:');
|
||||||
|
// console.log('[Draft] 文件复制成功:', copyResponse.data);
|
||||||
|
|
||||||
// 重定向到草稿编辑页面,通过 URL 参数传递文件路径和模板 ID
|
// 重定向到草稿编辑页面,通过 URL 参数传递文件路径和模板 ID
|
||||||
const draftUrl = `/contract-draft/1?filePath=${encodeURIComponent(newFilePath)}&templateId=${templateId}&title=${encodeURIComponent(title)}`;
|
const draftUrl = `/contract-draft/1?filePath=${encodeURIComponent(newFilePath)}&templateId=${templateId}&title=${encodeURIComponent(title)}`;
|
||||||
|
|||||||
@@ -438,7 +438,7 @@ export default function CrossCheckingIndex() {
|
|||||||
// 渲染进度条
|
// 渲染进度条
|
||||||
const renderProgress = (progress: number) => (
|
const renderProgress = (progress: number) => (
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div className="progress-bar w-16">
|
<div className="progress-bar w-16 mb-0">
|
||||||
<div
|
<div
|
||||||
className={`progress-bar-fill ${getProgressClass(progress)}`}
|
className={`progress-bar-fill ${getProgressClass(progress)}`}
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${progress}%` }}
|
||||||
|
|||||||
@@ -754,6 +754,24 @@ export default function CrossCheckingResult() {
|
|||||||
</div>
|
</div>
|
||||||
</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 && (
|
{hasTemplateForCompare && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -151,6 +151,20 @@ export async function action({ request }: ActionFunctionArgs) {
|
|||||||
errors.name = "文档类型名称不能为空";
|
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) {
|
if (selectedGroups.length === 0) {
|
||||||
errors.groups = "请至少选择一个关联的评查点分组";
|
errors.groups = "请至少选择一个关联的评查点分组";
|
||||||
}
|
}
|
||||||
@@ -268,6 +282,17 @@ export default function DocumentTypeNew() {
|
|||||||
console.log('返回的评查点分组数据',ruleGroups)
|
console.log('返回的评查点分组数据',ruleGroups)
|
||||||
}, [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初始化本地错误
|
// 从actionData初始化本地错误
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!actionData?.result) {
|
if (!actionData?.result) {
|
||||||
@@ -357,10 +382,19 @@ export default function DocumentTypeNew() {
|
|||||||
}, [documentType, ruleGroups]);
|
}, [documentType, ruleGroups]);
|
||||||
|
|
||||||
// 验证表单字段
|
// 验证表单字段
|
||||||
const validateField = (field: string, value: string | string[]): string => {
|
const validateField = (field: string, value: string | string[], allFormData?: typeof formData): string => {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case 'name':
|
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':
|
case 'llmExtractionTemplate':
|
||||||
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择llm抽取提示词模板" : "";
|
return !value || (typeof value === 'string' && value.trim() === "") ? "请选择llm抽取提示词模板" : "";
|
||||||
case 'vlmExtractionTemplate':
|
case 'vlmExtractionTemplate':
|
||||||
@@ -483,11 +517,13 @@ export default function DocumentTypeNew() {
|
|||||||
setTouchedFields(prev => ({...prev, name: true}));
|
setTouchedFields(prev => ({...prev, name: true}));
|
||||||
}
|
}
|
||||||
|
|
||||||
setFormData(prev => ({ ...prev, [fieldName]: value }));
|
// 先更新 formData
|
||||||
|
const updatedFormData = { ...formData, [fieldName]: value };
|
||||||
|
setFormData(updatedFormData);
|
||||||
|
|
||||||
// 实时验证
|
// 实时验证
|
||||||
if (name === 'name') {
|
if (name === 'name') {
|
||||||
const error = validateField(name, value);
|
const error = validateField(name, value, updatedFormData);
|
||||||
setFormErrors(prev => ({...prev, name: error}));
|
setFormErrors(prev => ({...prev, name: error}));
|
||||||
} else if (name === 'llm_extraction_template') {
|
} else if (name === 'llm_extraction_template') {
|
||||||
const error = validateField('llmExtractionTemplate', value);
|
const error = validateField('llmExtractionTemplate', value);
|
||||||
@@ -495,6 +531,12 @@ export default function DocumentTypeNew() {
|
|||||||
} else if (name === 'vlm_extraction_template') {
|
} else if (name === 'vlm_extraction_template') {
|
||||||
const error = validateField('vlmExtractionTemplate', value);
|
const error = validateField('vlmExtractionTemplate', value);
|
||||||
setFormErrors(prev => ({...prev, vlmExtractionTemplate: error}));
|
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 = {
|
const errors = {
|
||||||
name: validateField('name', formData.name),
|
name: validateField('name', formData.name, formData),
|
||||||
llmExtractionTemplate: validateField('llmExtractionTemplate', formData.llmExtractionTemplateId),
|
llmExtractionTemplate: validateField('llmExtractionTemplate', formData.llmExtractionTemplateId),
|
||||||
vlmExtractionTemplate: validateField('vlmExtractionTemplate', formData.vlmExtractionTemplateId),
|
vlmExtractionTemplate: validateField('vlmExtractionTemplate', formData.vlmExtractionTemplateId),
|
||||||
groups: validateField('groups', formData.selectedGroups)
|
groups: validateField('groups', formData.selectedGroups)
|
||||||
|
|||||||
+64
-2
@@ -26,8 +26,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
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 { useNavigate, useLoaderData, useFetcher } from "@remix-run/react";
|
||||||
|
import type { FilePreviewHandle } from "~/components/reviews/FilePreview";
|
||||||
import reviewsStyles from "~/styles/reviews.css?url";
|
import reviewsStyles from "~/styles/reviews.css?url";
|
||||||
import { getReviewPoints, getReviewPoints_fromApi, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
import { getReviewPoints, getReviewPoints_fromApi, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
||||||
import { toastService } from "~/components/ui/Toast";
|
import { toastService } from "~/components/ui/Toast";
|
||||||
@@ -321,6 +322,64 @@ export default function ReviewDetails() {
|
|||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
} | undefined>(undefined);
|
} | 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 返回的完整数据到浏览器控制台
|
// 🐛 调试:打印 loader 返回的完整数据到浏览器控制台
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// if (typeof window !== 'undefined') {
|
// if (typeof window !== 'undefined') {
|
||||||
@@ -450,7 +509,7 @@ export default function ReviewDetails() {
|
|||||||
// 短暂延迟后清除参数,以便下次可以重新触发
|
// 短暂延迟后清除参数,以便下次可以重新触发
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setAiSuggestionReplace(undefined);
|
setAiSuggestionReplace(undefined);
|
||||||
}, 1000);
|
}, 500);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 刷新评审数据
|
// 刷新评审数据
|
||||||
@@ -777,6 +836,7 @@ export default function ReviewDetails() {
|
|||||||
}}
|
}}
|
||||||
onConfirmResults={handleConfirmResults}
|
onConfirmResults={handleConfirmResults}
|
||||||
jwtToken={frontendJWT}
|
jwtToken={frontendJWT}
|
||||||
|
onSaveBeforeDownload={handleSaveBeforeDownload}
|
||||||
>
|
>
|
||||||
{/* 评查结果选项卡内容 */}
|
{/* 评查结果选项卡内容 */}
|
||||||
{activeTab === 'preview' && (
|
{activeTab === 'preview' && (
|
||||||
@@ -794,6 +854,8 @@ export default function ReviewDetails() {
|
|||||||
// });
|
// });
|
||||||
return (
|
return (
|
||||||
<FilePreview
|
<FilePreview
|
||||||
|
key={`file-preview-${collaboraKey}`}
|
||||||
|
ref={filePreviewRef}
|
||||||
fileContent={document}
|
fileContent={document}
|
||||||
reviewPoints={reviewData.reviewPoints}
|
reviewPoints={reviewData.reviewPoints}
|
||||||
activeReviewPointResultId={activeReviewPointResultId}
|
activeReviewPointResultId={activeReviewPointResultId}
|
||||||
|
|||||||
Reference in New Issue
Block a user