From de923f6521f427ed22936877deb7b8e59d07f607 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Tue, 9 Dec 2025 14:46:07 +0800 Subject: [PATCH] =?UTF-8?q?fix:=201.=20=E4=BF=AE=E6=94=B9dockerFile=202.?= =?UTF-8?q?=20=E4=BF=AE=E5=A4=8D=E4=B8=80=E4=BA=9B=E5=90=88=E5=90=8C?= =?UTF-8?q?=E8=B5=B7=E8=8D=89=E7=9A=84=E5=88=B7=E6=96=B0=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 37 +++---- app/api/files/files-upload.ts | 2 +- app/components/collabora/CollaboraViewer.tsx | 64 ++++++++--- app/components/dify-chat/chat-input.tsx | 2 +- app/components/reviews/FilePreview.tsx | 2 +- app/config/api-config.ts | 28 +++-- app/routes/contract-draft.$draftId.tsx | 96 ++++++++++++----- app/routes/contract-template.detail.$id.tsx | 2 +- app/routes/documents.list.tsx | 102 ++++++++++++++++-- .../components/chat-with-llm/chat-input.css | 2 + 10 files changed, 251 insertions(+), 86 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9459d36..6f36246 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,25 @@ -# 使用 Node.js 24 作为基础镜像(Alpine 版) -FROM node:24-alpine +FROM node:24 -# 设置工作目录 WORKDIR /app -# 安装 PM2 全局 -RUN npm install -g pm2 +# 复制 package.json 和 package-lock.json +COPY package*.json ./ -# 安装 pnpm -RUN npm install -g pnpm +# 安装依赖(包含 devDependencies 用于构建,包含可选依赖以获取 Linux 原生绑定) +RUN npm ci --include=dev -# 复制项目文件 +# 复制源代码 COPY . . -# 安装项目依赖(建议使用 pnpm,但你写的是 npm install .) -# 如果项目用 pnpm,请改为:RUN pnpm install --frozen-lockfile -RUN npm install . +# 构建应用 +RUN npm run build:production:multi -# 设置 node_modules/.bin 目录的执行权限(通常不需要,但保留) -RUN chmod -R +x node_modules/.bin +# 安装 PM2 +RUN npm install -g pm2 -# 创建 logs 目录 -RUN mkdir -p logs +EXPOSE 51703-51708 -# 暴露端口 -EXPOSE 51703 51704 51705 51706 51707 51708 - -# 设置环境变量 ENV NODE_ENV=production -# 确保 start.sh 可执行(777 权限过大,建议 755) -RUN chmod +x ./start.sh - -# 启动命令 -CMD ["./start.sh"] \ No newline at end of file +# 直接启动 PM2,不需要重新构建(构建已在上面完成) +CMD ["pm2-runtime", "start", "ecosystem.config.cjs", "--env", "production"] \ No newline at end of file diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index 89ffe83..6c94b0c 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -552,7 +552,7 @@ export async function getDocumentTypes(token?: string): Promise<{data: DocumentT } // 如果没有 documentTypeIds,返回所有文档类型(不添加过滤条件) - const response = await postgrestGet('/api/postgrest/proxy/ocument_types', { ...params, token }); + const response = await postgrestGet('/api/postgrest/proxy/document_types', { ...params, token }); if (response.error) { return { error: response.error, status: response.status }; diff --git a/app/components/collabora/CollaboraViewer.tsx b/app/components/collabora/CollaboraViewer.tsx index d9d8c30..43b5663 100644 --- a/app/components/collabora/CollaboraViewer.tsx +++ b/app/components/collabora/CollaboraViewer.tsx @@ -283,24 +283,51 @@ export const CollaboraViewer = forwardRef { + if (!iframeRef.current?.contentWindow) { + console.error('[CollaboraViewer] iframe 未就绪,无法执行替换'); + return; + } + + try { + // 步骤1:搜索文本(确保文本被选中,会自动跳转到匹配位置) + console.log(`[CollaboraViewer] 步骤1:搜索文本 "${newSearchText}"`); + unoSearchNext(iframeRef.current.contentWindow, newSearchText); + + // 等待搜索完成 + await new Promise(resolve => setTimeout(resolve, 300)); + + // 步骤2:执行替换(替换后光标保留在当前位置) + console.log(`[CollaboraViewer] 步骤2:替换为 "${newReplaceText}"`); + unoReplaceCurrent(iframeRef.current.contentWindow, newSearchText, newReplaceText); + + // 等待替换完成 + await new Promise(resolve => setTimeout(resolve, 200)); + + // 步骤3:自动搜索下一个相同的占位符(如果还有的话) + console.log(`[CollaboraViewer] 步骤3:定位到下一个 "${newSearchText}"`); + unoSearchNext(iframeRef.current.contentWindow, newSearchText); + + console.log('[CollaboraViewer] ✓ 静默替换完成,已定位到下一个占位符(如有)'); + } catch (error) { + console.error('[CollaboraViewer] 静默替换失败:', error); + } + }, 300); + + return () => clearTimeout(timer); } else { // 显示搜索替换面板 setShowSearchReplacePanel(true); - } - // 设置搜索、替换和页码输入框的值 - setSearchText(newSearchText); - setReplaceText(newReplaceText); - setSearchReplacePageNumber(String(pageNumber)); + // 设置搜索、替换和页码输入框的值 + setSearchText(newSearchText); + setReplaceText(newReplaceText); + setSearchReplacePageNumber(String(pageNumber)); - // 根据模式设置对应的自动执行标志 - if (silentReplace) { - // 静默替换:自动执行替换 - shouldAutoReplaceRef.current = true; - console.log('[CollaboraViewer] 已设置自动替换标志'); - } else { // 普通模式:仅自动执行查找 shouldAutoSearchRef.current = true; console.log('[CollaboraViewer] 已设置搜索参数,等待状态更新后自动执行查找'); @@ -326,7 +353,7 @@ export const CollaboraViewer = forwardRef { if (shouldAutoReplaceRef.current && searchText && replaceText && isDocumentLoaded) { console.log('[CollaboraViewer] 静默替换模式:自动执行替换:', { searchText, replaceText }); @@ -353,7 +380,14 @@ export const CollaboraViewer = forwardRef setTimeout(resolve, 200)); + + // 步骤3:自动搜索下一个相同的占位符(如果还有的话) + console.log(`[CollaboraViewer] 步骤3:定位到下一个 "${searchText}"`); + unoSearchNext(iframeRef.current.contentWindow, searchText); + + console.log('[CollaboraViewer] ✓ 静默替换完成,已定位到下一个占位符(如有)'); } catch (error) { console.error('[CollaboraViewer] 静默替换失败:', error); } diff --git a/app/components/dify-chat/chat-input.tsx b/app/components/dify-chat/chat-input.tsx index 17499f7..7e3de95 100644 --- a/app/components/dify-chat/chat-input.tsx +++ b/app/components/dify-chat/chat-input.tsx @@ -214,7 +214,7 @@ export default function ChatInput({ onCompositionEnd={handleCompositionEnd} placeholder={placeholder} disabled={disabled} - autoSize={{ minRows: 1, maxRows: 6 }} + rows={3} className="chat-input-textarea focus:outline-0 focus:outline-offset-0 focus:shadow-none focus:border-none" /> diff --git a/app/components/reviews/FilePreview.tsx b/app/components/reviews/FilePreview.tsx index 456d7a6..2cf6753 100644 --- a/app/components/reviews/FilePreview.tsx +++ b/app/components/reviews/FilePreview.tsx @@ -419,7 +419,7 @@ export const FilePreview = forwardRef(funct fileId={real_path} mode={isTemplate ? "view" : "edit"} // mode={"edit"} - userId={userInfo?.sub || 'guest'} + userId={userInfo?.sub || 'unknown'} userName={userInfo?.nick_name || ''} targetPage={targetPage} highlightText={highlightText} diff --git a/app/config/api-config.ts b/app/config/api-config.ts index 59e5818..0eea0b8 100644 --- a/app/config/api-config.ts +++ b/app/config/api-config.ts @@ -44,12 +44,19 @@ const portConfigs: Record> = { // 主要 // 梅州 '51703': { - baseUrl: 'http://172.16.0.56:8073', - documentUrl: 'http://172.16.0.56:8073/docauditai/', - uploadUrl: 'http://172.16.0.56:8073/admin/documents', + // baseUrl: 'http://172.16.0.56:8073', + // documentUrl: 'http://172.16.0.56:8073/docauditai/', + // uploadUrl: 'http://172.16.0.56:8073/admin/documents', - collaboraUrl: 'http://172.16.0.81:9980', - appUrl: 'http://172.16.0.34:51703', + // collaboraUrl: 'http://172.16.0.81:9980', + // appUrl: 'http://172.16.0.34:51703', + + baseUrl: 'http://10.79.97.17:8000', + documentUrl: 'http://10.79.97.17:8000/docauditai/', + uploadUrl: 'http://10.79.97.17:8000/admin/documents', + + collaboraUrl: 'http://10.79.97.17:9980', + appUrl: 'http://10.79.97.17:51703', oauth: { redirectUri: 'http://10.79.97.17:51703/callback' @@ -146,11 +153,12 @@ const configs: Record = { // 测试环境 testing: { - baseUrl: 'http://172.16.0.56:8073', // FastAPI后端(包含/dify代理) - documentUrl: 'http://172.16.0.56:8073/docauditai/', - uploadUrl: 'http://172.16.0.56:8073/admin/documents', - collaboraUrl: 'http://10.79.97.17:9980', - appUrl: 'http://10.79.97.17:51703', + baseUrl: 'http://172.16.0.55:8073', // FastAPI后端(包含/dify代理) + documentUrl: 'http://172.16.0.55:8073/docauditai/', + uploadUrl: 'http://172.16.0.55:8073/admin/documents', + collaboraUrl: 'http://172.16.0.81:9980', + // appUrl: 'http://10.79.97.17:51703', + appUrl: 'http://172.16.0.34:5183', oauth: { serverUrl: 'http://10.79.112.85', // IDaaS服务器地址 clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO', diff --git a/app/routes/contract-draft.$draftId.tsx b/app/routes/contract-draft.$draftId.tsx index 4d5078f..46337dd 100644 --- a/app/routes/contract-draft.$draftId.tsx +++ b/app/routes/contract-draft.$draftId.tsx @@ -4,7 +4,8 @@ */ import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from '@remix-run/node'; -import { useFetcher, useLoaderData, useNavigate } from '@remix-run/react'; +import { redirect } from '@remix-run/node'; +import { isRouteErrorResponse, useFetcher, useLoaderData, useNavigate, useParams, useRouteError } from '@remix-run/react'; import { useEffect, useRef, useState } from 'react'; import { downloadFile } from '~/api/axios-client'; import type { ContractTemplate } from '~/api/contract-template/templates'; @@ -56,12 +57,16 @@ export async function loader({ params, request }: LoaderFunctionArgs) { const templateId = url.searchParams.get('templateId'); const title = url.searchParams.get('title'); - if (!filePath) { - throw new Response('文件路径参数缺失', { status: 400 }); - } + // 如果参数缺失(可能是刷新导致),重定向到模板列表 + if (!filePath || !templateId) { + console.log('[Loader] URL参数缺失,可能是刷新导致,重定向到模板列表'); + console.log('[Loader] filePath:', filePath, 'templateId:', templateId); - if (!templateId) { - throw new Response('模板ID参数缺失', { status: 400 }); + // 如果有 templateId,重定向到模板详情页;否则重定向到模板列表 + if (templateId) { + return redirect(`/contract-template/detail/${templateId}`); + } + return redirect('/contract-template'); } console.log('[Loader] 起草合同:', { filePath, templateId, title }); @@ -94,7 +99,12 @@ export async function loader({ params, request }: LoaderFunctionArgs) { // console.log('[Loader] 生成的 schema:', JSON.stringify(placeholderSchema, null, 2)); } catch (error) { console.error('[Loader] 提取占位符失败:', error); - placeholderSchema = null; + console.error('[Loader] 错误类型:', error instanceof Error ? 'Error' : typeof error); + console.error('[Loader] 错误消息:', error instanceof Error ? error.message : String(error)); + + // 无论什么错误,都重定向回模板详情页(因为文件可能已被刷新删除) + console.log('[Loader] 发生错误,重定向到模板详情页'); + return redirect(`/contract-template/detail/${templateId}`); } // 创建模板对象 @@ -254,20 +264,6 @@ export default function ContractDraftPage() { } }; - const handleBeforeUnload = () => { - deleteFileSync(); - }; - - // 监听页面卸载事件 - window.addEventListener('beforeunload', handleBeforeUnload); - - // 清理事件监听器 - return () => { - window.removeEventListener('beforeunload', handleBeforeUnload); - - // 组件卸载时(路由切换),执行删除 - deleteFileSync(); - }; }, [draft.file_path]); // 单个替换占位符 @@ -276,12 +272,14 @@ export default function ContractDraftPage() { console.log(`[Draft] 单个替换: ${placeholder} -> ${value}`); // 设置 AI 建议替换参数,触发 FilePreview 中的静默替换 + // 添加唯一的时间戳,确保每次点击都会触发新的替换操作(即使值相同) setAiSuggestionReplace({ searchText: placeholder, replaceText: value, pageNumber: 1, // 从第一页开始搜索 - silentReplace: true // 静默替换,不显示搜索替换面板 - }); + silentReplace: true, // 静默替换,不显示搜索替换面板 + timestamp: Date.now() // 添加时间戳,确保对象始终是新的 + } as any); // 短暂延迟后清除参数,以便下次可以重新触发 setTimeout(() => { @@ -368,8 +366,12 @@ export default function ContractDraftPage() { console.log('[Complete] 步骤3:下载文件'); await handleExportDocument(); - // 步骤4:删除 MinIO 文件 - console.log('[Complete] 步骤4:删除 MinIO 文件'); + // 步骤4:清除会话标记 + const sessionKey = `contract-draft-${draft.id}-loaded`; + sessionStorage.removeItem(sessionKey); + + // 步骤5:删除 MinIO 文件 + console.log('[Complete] 步骤5:删除 MinIO 文件'); const formData = new FormData(); formData.append('_action', 'deleteFile'); formData.append('filePath', draft.file_path); @@ -393,6 +395,10 @@ export default function ContractDraftPage() { confirmText: '确定返回', cancelText: '取消', onConfirm: () => { + // 清除会话标记 + const sessionKey = `contract-draft-${draft.id}-loaded`; + sessionStorage.removeItem(sessionKey); + // 删除 MinIO 文件 const formData = new FormData(); formData.append('_action', 'deleteFile'); @@ -427,6 +433,13 @@ export default function ContractDraftPage() {
+ {/* 刷新提醒 */} +
+ + 请勿刷新页面,否则临时文件将被删除 +
+ + {/* 草稿状态标签 */} {draft.status === 'draft' ? '草稿' : draft.status === 'completed' ? '已完成' : '已归档'} @@ -485,3 +498,36 @@ export default function ContractDraftPage() {
); } + +/** + * 错误边界组件 - 处理页面加载错误时自动重定向 + */ +export function ErrorBoundary() { + const error = useRouteError(); + const params = useParams(); + const navigate = useNavigate(); + + useEffect(() => { + console.error('[ErrorBoundary] 捕获到错误:', error); + + // 自动重定向到模板详情页 + const templateId = new URLSearchParams(window.location.search).get('templateId'); + if (templateId) { + console.log('[ErrorBoundary] 自动重定向到模板详情页:', templateId); + // 使用 replace 避免返回到错误页面 + navigate(`/contract-template/detail/${templateId}`, { replace: true }); + } else { + console.log('[ErrorBoundary] 无法获取 templateId,重定向到模板列表'); + navigate('/contract-template', { replace: true }); + } + }, [error, navigate]); + + // 显示一个简单的加载提示(用户几乎看不到,因为会立即重定向) + return ( +
+
+
正在返回...
+
+
+ ); +} diff --git a/app/routes/contract-template.detail.$id.tsx b/app/routes/contract-template.detail.$id.tsx index 9eb7d8b..aef43e7 100644 --- a/app/routes/contract-template.detail.$id.tsx +++ b/app/routes/contract-template.detail.$id.tsx @@ -163,7 +163,7 @@ export default function ContractTemplateDetail() { }, []); const handleBack = () => { - navigate(-1); + navigate('/contract-template/list'); }; // 使用统一的下载方法(与 rules-files.tsx 相同) diff --git a/app/routes/documents.list.tsx b/app/routes/documents.list.tsx index 21323cd..f8ebde2 100644 --- a/app/routes/documents.list.tsx +++ b/app/routes/documents.list.tsx @@ -231,6 +231,10 @@ export default function DocumentsIndex() { const [attachmentRemark, setAttachmentRemark] = useState(""); const [attachmentUploading, setAttachmentUploading] = useState(false); const [templateUploading, setTemplateUploading] = useState(false); + + // 拖拽状态 + const [isDraggingAttachment, setIsDraggingAttachment] = useState(false); + const [isDraggingTemplate, setIsDraggingTemplate] = useState(false); // 查询参数记忆 key 与保存/恢复方法 const SEARCH_PARAMS_STORAGE_KEY = 'documents.searchParams'; @@ -970,7 +974,7 @@ export default function DocumentsIndex() { try { setTemplateUploading(true); - + const result = await uploadContractTemplate( templateFile, selectedDocumentId, @@ -994,7 +998,7 @@ export default function DocumentsIndex() { if (documentTypeIds && documentTypeIds.length > 0) { fetchData(documentTypeIds); } - + } catch (error) { console.error('【合同模板上传】上传失败:', error); toastService.error(error instanceof Error ? error.message : '合同模板上传失败'); @@ -1003,6 +1007,64 @@ export default function DocumentsIndex() { } }; + // 处理附件拖拽事件 + const handleAttachmentDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingAttachment(true); + }; + + const handleAttachmentDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleAttachmentDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingAttachment(false); + }; + + const handleAttachmentDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingAttachment(false); + + const files = e.dataTransfer.files; + if (files && files.length > 0) { + handleAttachmentFilesSelected(files); + } + }; + + // 处理模板拖拽事件 + const handleTemplateDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingTemplate(true); + }; + + const handleTemplateDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleTemplateDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingTemplate(false); + }; + + const handleTemplateDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingTemplate(false); + + const files = e.dataTransfer.files; + if (files && files.length > 0) { + handleTemplateFileSelected(files); + } + }; + // 展开/折叠历史版本 const handleToggleExpand = async (doc: DocumentUI) => { const newExpanded = new Set(expandedRows); @@ -1725,7 +1787,17 @@ export default function DocumentsIndex() { -
+
@@ -1885,7 +1959,17 @@ export default function DocumentsIndex() { -
+
diff --git a/app/styles/components/chat-with-llm/chat-input.css b/app/styles/components/chat-with-llm/chat-input.css index 3a86b79..f3ece2a 100644 --- a/app/styles/components/chat-with-llm/chat-input.css +++ b/app/styles/components/chat-with-llm/chat-input.css @@ -38,6 +38,8 @@ box-shadow: none !important; padding: 8px 0 !important; background: transparent !important; + overflow-y: auto !important; + max-height: 150px !important; } .chat-input-button {