fix: 1. 修改dockerFile
2. 修复一些合同起草的刷新报错问题
This commit is contained in:
+13
-24
@@ -1,36 +1,25 @@
|
|||||||
# 使用 Node.js 24 作为基础镜像(Alpine 版)
|
FROM node:24
|
||||||
FROM node:24-alpine
|
|
||||||
|
|
||||||
# 设置工作目录
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 安装 PM2 全局
|
# 复制 package.json 和 package-lock.json
|
||||||
RUN npm install -g pm2
|
COPY package*.json ./
|
||||||
|
|
||||||
# 安装 pnpm
|
# 安装依赖(包含 devDependencies 用于构建,包含可选依赖以获取 Linux 原生绑定)
|
||||||
RUN npm install -g pnpm
|
RUN npm ci --include=dev
|
||||||
|
|
||||||
# 复制项目文件
|
# 复制源代码
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# 安装项目依赖(建议使用 pnpm,但你写的是 npm install .)
|
# 构建应用
|
||||||
# 如果项目用 pnpm,请改为:RUN pnpm install --frozen-lockfile
|
RUN npm run build:production:multi
|
||||||
RUN npm install .
|
|
||||||
|
|
||||||
# 设置 node_modules/.bin 目录的执行权限(通常不需要,但保留)
|
# 安装 PM2
|
||||||
RUN chmod -R +x node_modules/.bin
|
RUN npm install -g pm2
|
||||||
|
|
||||||
# 创建 logs 目录
|
EXPOSE 51703-51708
|
||||||
RUN mkdir -p logs
|
|
||||||
|
|
||||||
# 暴露端口
|
|
||||||
EXPOSE 51703 51704 51705 51706 51707 51708
|
|
||||||
|
|
||||||
# 设置环境变量
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# 确保 start.sh 可执行(777 权限过大,建议 755)
|
# 直接启动 PM2,不需要重新构建(构建已在上面完成)
|
||||||
RUN chmod +x ./start.sh
|
CMD ["pm2-runtime", "start", "ecosystem.config.cjs", "--env", "production"]
|
||||||
|
|
||||||
# 启动命令
|
|
||||||
CMD ["./start.sh"]
|
|
||||||
@@ -552,7 +552,7 @@ export async function getDocumentTypes(token?: string): Promise<{data: DocumentT
|
|||||||
}
|
}
|
||||||
// 如果没有 documentTypeIds,返回所有文档类型(不添加过滤条件)
|
// 如果没有 documentTypeIds,返回所有文档类型(不添加过滤条件)
|
||||||
|
|
||||||
const response = await postgrestGet<DocumentType[]>('/api/postgrest/proxy/ocument_types', { ...params, token });
|
const response = await postgrestGet<DocumentType[]>('/api/postgrest/proxy/document_types', { ...params, token });
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
return { error: response.error, status: response.status };
|
return { error: response.error, status: response.status };
|
||||||
|
|||||||
@@ -283,24 +283,51 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
|
|
||||||
// 根据 silentReplace 标志决定是否显示面板
|
// 根据 silentReplace 标志决定是否显示面板
|
||||||
if (silentReplace) {
|
if (silentReplace) {
|
||||||
// 静默替换模式:不显示面板
|
// 静默替换模式:不显示面板,直接执行替换
|
||||||
console.log('[CollaboraViewer] 静默替换模式,不显示面板');
|
console.log('[CollaboraViewer] 静默替换模式,不显示面板,直接执行替换');
|
||||||
|
|
||||||
|
// 延迟执行替换,确保 DOM 更新完成
|
||||||
|
const timer = setTimeout(async () => {
|
||||||
|
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 {
|
} else {
|
||||||
// 显示搜索替换面板
|
// 显示搜索替换面板
|
||||||
setShowSearchReplacePanel(true);
|
setShowSearchReplacePanel(true);
|
||||||
}
|
|
||||||
|
|
||||||
// 设置搜索、替换和页码输入框的值
|
// 设置搜索、替换和页码输入框的值
|
||||||
setSearchText(newSearchText);
|
setSearchText(newSearchText);
|
||||||
setReplaceText(newReplaceText);
|
setReplaceText(newReplaceText);
|
||||||
setSearchReplacePageNumber(String(pageNumber));
|
setSearchReplacePageNumber(String(pageNumber));
|
||||||
|
|
||||||
// 根据模式设置对应的自动执行标志
|
|
||||||
if (silentReplace) {
|
|
||||||
// 静默替换:自动执行替换
|
|
||||||
shouldAutoReplaceRef.current = true;
|
|
||||||
console.log('[CollaboraViewer] 已设置自动替换标志');
|
|
||||||
} else {
|
|
||||||
// 普通模式:仅自动执行查找
|
// 普通模式:仅自动执行查找
|
||||||
shouldAutoSearchRef.current = true;
|
shouldAutoSearchRef.current = true;
|
||||||
console.log('[CollaboraViewer] 已设置搜索参数,等待状态更新后自动执行查找');
|
console.log('[CollaboraViewer] 已设置搜索参数,等待状态更新后自动执行查找');
|
||||||
@@ -326,7 +353,7 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
}, [searchText, searchReplacePageNumber, isDocumentLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [searchText, searchReplacePageNumber, isDocumentLoaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
// 9. 当搜索参数更新完成后,自动执行替换(静默模式)
|
// 9. 当搜索参数更新完成后,自动执行替换(静默模式)
|
||||||
// 不跳转页面,直接在当前位置搜索并替换,替换后保留在替换位置
|
// 不跳转页面,直接在当前位置搜索并替换,替换后自动定位到下一个相同占位符
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldAutoReplaceRef.current && searchText && replaceText && isDocumentLoaded) {
|
if (shouldAutoReplaceRef.current && searchText && replaceText && isDocumentLoaded) {
|
||||||
console.log('[CollaboraViewer] 静默替换模式:自动执行替换:', { searchText, replaceText });
|
console.log('[CollaboraViewer] 静默替换模式:自动执行替换:', { searchText, replaceText });
|
||||||
@@ -353,7 +380,14 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
console.log(`[CollaboraViewer] 步骤2:替换为 "${replaceText}"`);
|
console.log(`[CollaboraViewer] 步骤2:替换为 "${replaceText}"`);
|
||||||
unoReplaceCurrent(iframeRef.current.contentWindow, searchText, replaceText);
|
unoReplaceCurrent(iframeRef.current.contentWindow, searchText, replaceText);
|
||||||
|
|
||||||
console.log('[CollaboraViewer] ✓ 静默替换完成');
|
// 等待替换完成
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 200));
|
||||||
|
|
||||||
|
// 步骤3:自动搜索下一个相同的占位符(如果还有的话)
|
||||||
|
console.log(`[CollaboraViewer] 步骤3:定位到下一个 "${searchText}"`);
|
||||||
|
unoSearchNext(iframeRef.current.contentWindow, searchText);
|
||||||
|
|
||||||
|
console.log('[CollaboraViewer] ✓ 静默替换完成,已定位到下一个占位符(如有)');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[CollaboraViewer] 静默替换失败:', error);
|
console.error('[CollaboraViewer] 静默替换失败:', error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export default function ChatInput({
|
|||||||
onCompositionEnd={handleCompositionEnd}
|
onCompositionEnd={handleCompositionEnd}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
disabled={disabled}
|
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"
|
className="chat-input-textarea focus:outline-0 focus:outline-offset-0 focus:shadow-none focus:border-none"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -419,7 +419,7 @@ export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(funct
|
|||||||
fileId={real_path}
|
fileId={real_path}
|
||||||
mode={isTemplate ? "view" : "edit"}
|
mode={isTemplate ? "view" : "edit"}
|
||||||
// mode={"edit"}
|
// mode={"edit"}
|
||||||
userId={userInfo?.sub || 'guest'}
|
userId={userInfo?.sub || 'unknown'}
|
||||||
userName={userInfo?.nick_name || ''}
|
userName={userInfo?.nick_name || ''}
|
||||||
targetPage={targetPage}
|
targetPage={targetPage}
|
||||||
highlightText={highlightText}
|
highlightText={highlightText}
|
||||||
|
|||||||
+18
-10
@@ -44,12 +44,19 @@ const portConfigs: Record<string, Partial<ApiConfig>> = {
|
|||||||
// 主要
|
// 主要
|
||||||
// 梅州
|
// 梅州
|
||||||
'51703': {
|
'51703': {
|
||||||
baseUrl: 'http://172.16.0.56:8073',
|
// baseUrl: 'http://172.16.0.56:8073',
|
||||||
documentUrl: 'http://172.16.0.56:8073/docauditai/',
|
// documentUrl: 'http://172.16.0.56:8073/docauditai/',
|
||||||
uploadUrl: 'http://172.16.0.56:8073/admin/documents',
|
// uploadUrl: 'http://172.16.0.56:8073/admin/documents',
|
||||||
|
|
||||||
collaboraUrl: 'http://172.16.0.81:9980',
|
// collaboraUrl: 'http://172.16.0.81:9980',
|
||||||
appUrl: 'http://172.16.0.34:51703',
|
// 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: {
|
oauth: {
|
||||||
redirectUri: 'http://10.79.97.17:51703/callback'
|
redirectUri: 'http://10.79.97.17:51703/callback'
|
||||||
@@ -146,11 +153,12 @@ const configs: Record<string, ApiConfig> = {
|
|||||||
|
|
||||||
// 测试环境
|
// 测试环境
|
||||||
testing: {
|
testing: {
|
||||||
baseUrl: 'http://172.16.0.56:8073', // FastAPI后端(包含/dify代理)
|
baseUrl: 'http://172.16.0.55:8073', // FastAPI后端(包含/dify代理)
|
||||||
documentUrl: 'http://172.16.0.56:8073/docauditai/',
|
documentUrl: 'http://172.16.0.55:8073/docauditai/',
|
||||||
uploadUrl: 'http://172.16.0.56:8073/admin/documents',
|
uploadUrl: 'http://172.16.0.55:8073/admin/documents',
|
||||||
collaboraUrl: 'http://10.79.97.17:9980',
|
collaboraUrl: 'http://172.16.0.81:9980',
|
||||||
appUrl: 'http://10.79.97.17:51703',
|
// appUrl: 'http://10.79.97.17:51703',
|
||||||
|
appUrl: 'http://172.16.0.34:5183',
|
||||||
oauth: {
|
oauth: {
|
||||||
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
serverUrl: 'http://10.79.112.85', // IDaaS服务器地址
|
||||||
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
clientId: '54d2a619fe5c81ae1250434c441fccccqMtKwh7H4fO',
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from '@remix-run/node';
|
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 { useEffect, useRef, useState } from 'react';
|
||||||
import { downloadFile } from '~/api/axios-client';
|
import { downloadFile } from '~/api/axios-client';
|
||||||
import type { ContractTemplate } from '~/api/contract-template/templates';
|
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 templateId = url.searchParams.get('templateId');
|
||||||
const title = url.searchParams.get('title');
|
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) {
|
// 如果有 templateId,重定向到模板详情页;否则重定向到模板列表
|
||||||
throw new Response('模板ID参数缺失', { status: 400 });
|
if (templateId) {
|
||||||
|
return redirect(`/contract-template/detail/${templateId}`);
|
||||||
|
}
|
||||||
|
return redirect('/contract-template');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[Loader] 起草合同:', { filePath, templateId, title });
|
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));
|
// console.log('[Loader] 生成的 schema:', JSON.stringify(placeholderSchema, null, 2));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Loader] 提取占位符失败:', 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]);
|
}, [draft.file_path]);
|
||||||
|
|
||||||
// 单个替换占位符
|
// 单个替换占位符
|
||||||
@@ -276,12 +272,14 @@ export default function ContractDraftPage() {
|
|||||||
console.log(`[Draft] 单个替换: ${placeholder} -> ${value}`);
|
console.log(`[Draft] 单个替换: ${placeholder} -> ${value}`);
|
||||||
|
|
||||||
// 设置 AI 建议替换参数,触发 FilePreview 中的静默替换
|
// 设置 AI 建议替换参数,触发 FilePreview 中的静默替换
|
||||||
|
// 添加唯一的时间戳,确保每次点击都会触发新的替换操作(即使值相同)
|
||||||
setAiSuggestionReplace({
|
setAiSuggestionReplace({
|
||||||
searchText: placeholder,
|
searchText: placeholder,
|
||||||
replaceText: value,
|
replaceText: value,
|
||||||
pageNumber: 1, // 从第一页开始搜索
|
pageNumber: 1, // 从第一页开始搜索
|
||||||
silentReplace: true // 静默替换,不显示搜索替换面板
|
silentReplace: true, // 静默替换,不显示搜索替换面板
|
||||||
});
|
timestamp: Date.now() // 添加时间戳,确保对象始终是新的
|
||||||
|
} as any);
|
||||||
|
|
||||||
// 短暂延迟后清除参数,以便下次可以重新触发
|
// 短暂延迟后清除参数,以便下次可以重新触发
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -368,8 +366,12 @@ export default function ContractDraftPage() {
|
|||||||
console.log('[Complete] 步骤3:下载文件');
|
console.log('[Complete] 步骤3:下载文件');
|
||||||
await handleExportDocument();
|
await handleExportDocument();
|
||||||
|
|
||||||
// 步骤4:删除 MinIO 文件
|
// 步骤4:清除会话标记
|
||||||
console.log('[Complete] 步骤4:删除 MinIO 文件');
|
const sessionKey = `contract-draft-${draft.id}-loaded`;
|
||||||
|
sessionStorage.removeItem(sessionKey);
|
||||||
|
|
||||||
|
// 步骤5:删除 MinIO 文件
|
||||||
|
console.log('[Complete] 步骤5:删除 MinIO 文件');
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('_action', 'deleteFile');
|
formData.append('_action', 'deleteFile');
|
||||||
formData.append('filePath', draft.file_path);
|
formData.append('filePath', draft.file_path);
|
||||||
@@ -393,6 +395,10 @@ export default function ContractDraftPage() {
|
|||||||
confirmText: '确定返回',
|
confirmText: '确定返回',
|
||||||
cancelText: '取消',
|
cancelText: '取消',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
|
// 清除会话标记
|
||||||
|
const sessionKey = `contract-draft-${draft.id}-loaded`;
|
||||||
|
sessionStorage.removeItem(sessionKey);
|
||||||
|
|
||||||
// 删除 MinIO 文件
|
// 删除 MinIO 文件
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('_action', 'deleteFile');
|
formData.append('_action', 'deleteFile');
|
||||||
@@ -427,6 +433,13 @@ export default function ContractDraftPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{/* 刷新提醒 */}
|
||||||
|
<div className="flex items-center gap-2 px-3 py-2 text-sm text-yellow-700 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
<i className="ri-alert-line text-base"></i>
|
||||||
|
<span>请勿刷新页面,否则临时文件将被删除</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 草稿状态标签 */}
|
||||||
<span className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-gradient-to-r from-blue-50 to-blue-100 text-blue-700 border border-blue-200 shadow-sm">
|
<span className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium rounded-lg bg-gradient-to-r from-blue-50 to-blue-100 text-blue-700 border border-blue-200 shadow-sm">
|
||||||
<i className="ri-draft-line text-base"></i>
|
<i className="ri-draft-line text-base"></i>
|
||||||
<span>{draft.status === 'draft' ? '草稿' : draft.status === 'completed' ? '已完成' : '已归档'}</span>
|
<span>{draft.status === 'draft' ? '草稿' : draft.status === 'completed' ? '已完成' : '已归档'}</span>
|
||||||
@@ -485,3 +498,36 @@ export default function ContractDraftPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误边界组件 - 处理页面加载错误时自动重定向
|
||||||
|
*/
|
||||||
|
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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-gray-600">正在返回...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export default function ContractTemplateDetail() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
navigate(-1);
|
navigate('/contract-template/list');
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用统一的下载方法(与 rules-files.tsx 相同)
|
// 使用统一的下载方法(与 rules-files.tsx 相同)
|
||||||
|
|||||||
@@ -231,6 +231,10 @@ export default function DocumentsIndex() {
|
|||||||
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
||||||
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
||||||
const [templateUploading, setTemplateUploading] = useState<boolean>(false);
|
const [templateUploading, setTemplateUploading] = useState<boolean>(false);
|
||||||
|
|
||||||
|
// 拖拽状态
|
||||||
|
const [isDraggingAttachment, setIsDraggingAttachment] = useState<boolean>(false);
|
||||||
|
const [isDraggingTemplate, setIsDraggingTemplate] = useState<boolean>(false);
|
||||||
|
|
||||||
// 查询参数记忆 key 与保存/恢复方法
|
// 查询参数记忆 key 与保存/恢复方法
|
||||||
const SEARCH_PARAMS_STORAGE_KEY = 'documents.searchParams';
|
const SEARCH_PARAMS_STORAGE_KEY = 'documents.searchParams';
|
||||||
@@ -970,7 +974,7 @@ export default function DocumentsIndex() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setTemplateUploading(true);
|
setTemplateUploading(true);
|
||||||
|
|
||||||
const result = await uploadContractTemplate(
|
const result = await uploadContractTemplate(
|
||||||
templateFile,
|
templateFile,
|
||||||
selectedDocumentId,
|
selectedDocumentId,
|
||||||
@@ -994,7 +998,7 @@ export default function DocumentsIndex() {
|
|||||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||||
fetchData(documentTypeIds);
|
fetchData(documentTypeIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('【合同模板上传】上传失败:', error);
|
console.error('【合同模板上传】上传失败:', error);
|
||||||
toastService.error(error instanceof Error ? error.message : '合同模板上传失败');
|
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 handleToggleExpand = async (doc: DocumentUI) => {
|
||||||
const newExpanded = new Set(expandedRows);
|
const newExpanded = new Set(expandedRows);
|
||||||
@@ -1725,7 +1787,17 @@ export default function DocumentsIndex() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
选择附件文件 <span className="text-red-500">*</span>
|
选择附件文件 <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors">
|
<div
|
||||||
|
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
|
||||||
|
isDraggingAttachment
|
||||||
|
? 'border-primary bg-primary-light'
|
||||||
|
: 'border-gray-300 hover:border-gray-400'
|
||||||
|
}`}
|
||||||
|
onDragEnter={handleAttachmentDragEnter}
|
||||||
|
onDragOver={handleAttachmentDragOver}
|
||||||
|
onDragLeave={handleAttachmentDragLeave}
|
||||||
|
onDrop={handleAttachmentDrop}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
multiple
|
multiple
|
||||||
@@ -1735,8 +1807,10 @@ export default function DocumentsIndex() {
|
|||||||
id="attachment-file-input"
|
id="attachment-file-input"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="attachment-file-input" className="cursor-pointer">
|
<label htmlFor="attachment-file-input" className="cursor-pointer">
|
||||||
<i className="ri-attachment-line text-3xl text-gray-400 mb-2 block"></i>
|
<i className={`ri-attachment-line text-3xl mb-2 block ${isDraggingAttachment ? 'text-primary' : 'text-gray-400'}`}></i>
|
||||||
<p className="text-sm text-gray-600">点击选择文件或拖拽文件到此处</p>
|
<p className={`text-sm ${isDraggingAttachment ? 'text-primary font-medium' : 'text-gray-600'}`}>
|
||||||
|
{isDraggingAttachment ? '松开鼠标上传文件' : '点击选择文件或拖拽文件到此处'}
|
||||||
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">支持.pdf、.docx、.zip、.rar格式,可多选</p>
|
<p className="text-xs text-gray-500 mt-1">支持.pdf、.docx、.zip、.rar格式,可多选</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@@ -1885,7 +1959,17 @@ export default function DocumentsIndex() {
|
|||||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
选择模板文件 <span className="text-red-500">*</span>
|
选择模板文件 <span className="text-red-500">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors">
|
<div
|
||||||
|
className={`border-2 border-dashed rounded-lg p-6 text-center transition-colors ${
|
||||||
|
isDraggingTemplate
|
||||||
|
? 'border-primary bg-primary-light'
|
||||||
|
: 'border-gray-300 hover:border-gray-400'
|
||||||
|
}`}
|
||||||
|
onDragEnter={handleTemplateDragEnter}
|
||||||
|
onDragOver={handleTemplateDragOver}
|
||||||
|
onDragLeave={handleTemplateDragLeave}
|
||||||
|
onDrop={handleTemplateDrop}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".pdf,.docx"
|
accept=".pdf,.docx"
|
||||||
@@ -1894,8 +1978,10 @@ export default function DocumentsIndex() {
|
|||||||
id="template-file-input"
|
id="template-file-input"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="template-file-input" className="cursor-pointer">
|
<label htmlFor="template-file-input" className="cursor-pointer">
|
||||||
<i className="ri-file-copy-line text-3xl text-gray-400 mb-2 block"></i>
|
<i className={`ri-file-copy-line text-3xl mb-2 block ${isDraggingTemplate ? 'text-primary' : 'text-gray-400'}`}></i>
|
||||||
<p className="text-sm text-gray-600">点击选择文件或拖拽文件到此处</p>
|
<p className={`text-sm ${isDraggingTemplate ? 'text-primary font-medium' : 'text-gray-600'}`}>
|
||||||
|
{isDraggingTemplate ? '松开鼠标上传文件' : '点击选择文件或拖拽文件到此处'}
|
||||||
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">支持.pdf、.docx格式</p>
|
<p className="text-xs text-gray-500 mt-1">支持.pdf、.docx格式</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -38,6 +38,8 @@
|
|||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
padding: 8px 0 !important;
|
padding: 8px 0 !important;
|
||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
|
overflow-y: auto !important;
|
||||||
|
max-height: 150px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-input-button {
|
.chat-input-button {
|
||||||
|
|||||||
Reference in New Issue
Block a user