fix: 1.接入ai_suggestion.
2. 接入合同起草功能。
This commit is contained in:
@@ -44,6 +44,7 @@ export interface ContractTemplate {
|
|||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
pdf_file_path?: string;
|
pdf_file_path?: string;
|
||||||
|
placeholder_schema?: Record<string, any>; // 占位符配置 (JSONB)
|
||||||
// 关联的分类信息
|
// 关联的分类信息
|
||||||
category?: ContractCategory;
|
category?: ContractCategory;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -317,11 +317,11 @@ export async function getEntryModules(userRole: string | null | undefined, userA
|
|||||||
|
|
||||||
// console.log('✅ [getEntryModules] 入口模块数据(含文档类型):', JSON.stringify(modulesWithTypes));
|
// console.log('✅ [getEntryModules] 入口模块数据(含文档类型):', JSON.stringify(modulesWithTypes));
|
||||||
|
|
||||||
// 默认会多加一个 智慧法务大模型 入口 默认所有人都可以用,看到
|
// 默认会多加一个 智慧法务助手 入口 默认所有人都可以用,看到
|
||||||
modulesWithTypes.push({
|
modulesWithTypes.push({
|
||||||
"id": 0,
|
"id": 0,
|
||||||
"name": "智慧法务大模型",
|
"name": "智慧法务助手",
|
||||||
"description": "智慧法务大模型",
|
"description": "智慧法务助手",
|
||||||
"path": "entryModule/assistant",
|
"path": "entryModule/assistant",
|
||||||
"areas": [],
|
"areas": [],
|
||||||
"created_at": "2025-11-18T21:33:33.857417+08:00",
|
"created_at": "2025-11-18T21:33:33.857417+08:00",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface PromptTemplate {
|
|||||||
status: number;
|
status: number;
|
||||||
version: string;
|
version: string;
|
||||||
created_by: number | null;
|
created_by: number | null;
|
||||||
|
created_by_username?: string; // 创建者用户名
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
is_active?: boolean;
|
is_active?: boolean;
|
||||||
@@ -151,6 +152,7 @@ export function convertToUITemplate(template: PromptTemplate): PromptTemplateUI
|
|||||||
status: mapStatusToUI(template.status),
|
status: mapStatusToUI(template.status),
|
||||||
version: template.version,
|
version: template.version,
|
||||||
created_by: template.created_by || 0,
|
created_by: template.created_by || 0,
|
||||||
|
created_by_username: template.created_by_username,
|
||||||
created_at: formatDate(template.created_at),
|
created_at: formatDate(template.created_at),
|
||||||
updated_at: formatDate(template.updated_at),
|
updated_at: formatDate(template.updated_at),
|
||||||
template_code: template.template_code || undefined,
|
template_code: template.template_code || undefined,
|
||||||
|
|||||||
@@ -196,7 +196,7 @@ export async function getRoles(params?: {
|
|||||||
description: role.description || '',
|
description: role.description || '',
|
||||||
parent_role_id: role.parent_role_id || null,
|
parent_role_id: role.parent_role_id || null,
|
||||||
priority: role.priority || 0,
|
priority: role.priority || 0,
|
||||||
is_system_role: role.is_system || false,
|
is_system_role: (role.role_key == 'admin' || role.role_key == 'common') ? true : role.is_system || false,
|
||||||
created_at: role.created_at,
|
created_at: role.created_at,
|
||||||
updated_at: role.updated_at
|
updated_at: role.updated_at
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -279,8 +279,8 @@ export const CollaboraViewer = forwardRef<CollaboraViewerHandle, CollaboraViewer
|
|||||||
<div className="flex justify-center items-center h-full min-h-[600px]">
|
<div className="flex justify-center items-center h-full min-h-[600px]">
|
||||||
<div className="text-center text-red-500">
|
<div className="text-center text-red-500">
|
||||||
<i className="ri-error-warning-line text-4xl mb-2"></i>
|
<i className="ri-error-warning-line text-4xl mb-2"></i>
|
||||||
<p className="text-lg">{error || '加载配置失败'}</p>
|
<p className="text-lg">{error}</p>
|
||||||
<p className="text-sm text-gray-500 mt-2">请刷新页面重试或联系管理员</p>
|
{/* <p className="text-sm text-gray-500 mt-2">请刷新页面重试或联系管理员</p> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import { COLLABORA_URL } from '~/config/api-config';
|
|||||||
import { toastService } from '../ui/Toast';
|
import { toastService } from '../ui/Toast';
|
||||||
import {
|
import {
|
||||||
unoScrollToTop,
|
unoScrollToTop,
|
||||||
|
unoReplaceAll
|
||||||
} from './lib';
|
} from './lib';
|
||||||
import type { CollaboraConfig } from './types';
|
import type { CollaboraConfig } from './types';
|
||||||
|
|
||||||
@@ -131,14 +132,30 @@ export function useCollaboraUnoCommands(iframeRef: RefObject<HTMLIFrameElement>)
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
}, [iframeRef]);
|
}, [iframeRef]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 替换所有匹配项
|
||||||
|
* @param searchText 要搜索的文本
|
||||||
|
* @param replaceText 替换后的文本
|
||||||
|
*/
|
||||||
|
const replaceAll = useCallback(async (searchText: string, replaceText: string) => {
|
||||||
|
if (!iframeRef.current?.contentWindow) {
|
||||||
|
console.warn('[UNO] iframe 不可用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[UNO] 替换全部:', searchText, '->', replaceText);
|
||||||
|
await unoReplaceAll(iframeRef.current.contentWindow, searchText, replaceText);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
}, [iframeRef]);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
scrollToTop
|
scrollToTop,
|
||||||
|
replaceAll
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
scrollToTop
|
scrollToTop,
|
||||||
|
replaceAll
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export interface HighlightOptions {
|
|||||||
/** 高亮颜色 (LibreOffice Decimal 格式), 默认 16776960 = 黄色 */
|
/** 高亮颜色 (LibreOffice Decimal 格式), 默认 16776960 = 黄色 */
|
||||||
color?: number;
|
color?: number;
|
||||||
/** 目标页码 (从1开始), 默认第1页 */
|
/** 目标页码 (从1开始), 默认第1页 */
|
||||||
page?: number;
|
page?: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,7 +32,7 @@ export interface HighlightResponse {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
highlightedCount?: number;
|
highlightedCount?: number;
|
||||||
page?: number;
|
page?: number | null;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ export async function highlightText(
|
|||||||
options?: HighlightOptions
|
options?: HighlightOptions
|
||||||
): Promise<HighlightResponse> {
|
): Promise<HighlightResponse> {
|
||||||
const color = options?.color ?? 16776960; // 默认黄色
|
const color = options?.color ?? 16776960; // 默认黄色
|
||||||
const page = options?.page ?? 1; // 默认第1页
|
const page = options?.page ?? null; // 默认第1页
|
||||||
|
|
||||||
console.log('[HighlightSelectText] 调用 Python 脚本高亮文本:', {
|
console.log('[HighlightSelectText] 调用 Python 脚本高亮文本:', {
|
||||||
text,
|
text,
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ export interface CollaboraViewerHandle {
|
|||||||
/** UNO 命令方法集合 */
|
/** UNO 命令方法集合 */
|
||||||
unoCommands: {
|
unoCommands: {
|
||||||
scrollToTop: () => Promise<void>;
|
scrollToTop: () => Promise<void>;
|
||||||
|
replaceAll?: (searchText: string, replaceText: string) => Promise<void>;
|
||||||
|
find?: (searchText: string) => Promise<void>;
|
||||||
|
search?: (searchText: string) => Promise<void>;
|
||||||
};
|
};
|
||||||
/** 文档是否已加载完成 */
|
/** 文档是否已加载完成 */
|
||||||
isReady: boolean;
|
isReady: boolean;
|
||||||
|
|||||||
@@ -71,13 +71,13 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
|||||||
|
|
||||||
{/* 操作按钮区域 */}
|
{/* 操作按钮区域 */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* 下载原文件按钮 */}
|
{/* 下载文件按钮 */}
|
||||||
<button
|
<button
|
||||||
onClick={handleDownloadFile}
|
onClick={handleDownloadFile}
|
||||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||||
>
|
>
|
||||||
<i className="fas fa-download mr-1.5"></i>
|
<i className="fas fa-download mr-1.5"></i>
|
||||||
下载原文件
|
下载
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* 导出评查报告按钮 */}
|
{/* 导出评查报告按钮 */}
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
<span className="ml-2 text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap flex-shrink-0">
|
<span className="ml-2 text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap flex-shrink-0">
|
||||||
比例:{zoomLevel}%
|
比例:{zoomLevel}%
|
||||||
</span>
|
</span>
|
||||||
<button
|
{/* <button
|
||||||
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-2 flex-shrink-0 ${dragMode ? 'active bg-green-300' : ''}`}
|
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-2 flex-shrink-0 ${dragMode ? 'active bg-green-300' : ''}`}
|
||||||
title="切换拖拽模式"
|
title="切换拖拽模式"
|
||||||
aria-pressed={dragMode}
|
aria-pressed={dragMode}
|
||||||
@@ -537,7 +537,7 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline truncate max-w-[80px]">
|
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline truncate max-w-[80px]">
|
||||||
拖拽模式{dragMode ? '(已激活)' : ''}
|
拖拽模式{dragMode ? '(已激活)' : ''}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1809,6 +1809,26 @@ export function ReviewPointsList({
|
|||||||
value: string;
|
value: string;
|
||||||
char_positions?: CharPosition[];
|
char_positions?: CharPosition[];
|
||||||
}>;
|
}>;
|
||||||
|
ai_suggestion?: {
|
||||||
|
summary?: string;
|
||||||
|
analysis?: {
|
||||||
|
failure_reason?: string;
|
||||||
|
solution_approach?: string;
|
||||||
|
rule_understanding?: string;
|
||||||
|
};
|
||||||
|
suggestions?: Record<string, {
|
||||||
|
reason: string;
|
||||||
|
source: {
|
||||||
|
page: number | null;
|
||||||
|
type: string;
|
||||||
|
field: string | null;
|
||||||
|
};
|
||||||
|
priority: string;
|
||||||
|
confidence: number;
|
||||||
|
suggested_value: string | null;
|
||||||
|
}>;
|
||||||
|
generated_at?: string;
|
||||||
|
};
|
||||||
message?: string;
|
message?: string;
|
||||||
res?: boolean;
|
res?: boolean;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
@@ -1934,10 +1954,10 @@ export function ReviewPointsList({
|
|||||||
// 渲染AI模型返回的评估消息
|
// 渲染AI模型返回的评估消息
|
||||||
if (config.message) {
|
if (config.message) {
|
||||||
// 检查message是否为对象,如果是则转换为字符串
|
// 检查message是否为对象,如果是则转换为字符串
|
||||||
const messageContent = typeof config.message === 'object'
|
const messageContent = typeof config.message === 'object'
|
||||||
? JSON.stringify(config.message)
|
? JSON.stringify(config.message)
|
||||||
: String(config.message);
|
: String(config.message);
|
||||||
|
|
||||||
// 添加模型评估消息区域,使用蓝色背景突出显示
|
// 添加模型评估消息区域,使用蓝色背景突出显示
|
||||||
fieldElements.push(
|
fieldElements.push(
|
||||||
<div key="message" className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
<div key="message" className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
||||||
@@ -1949,6 +1969,57 @@ export function ReviewPointsList({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 渲染AI建议(ai_suggestion)
|
||||||
|
if (config.ai_suggestion?.suggestions && Object.keys(config.ai_suggestion.suggestions).length > 0) {
|
||||||
|
// 遍历suggestions对象的key-value对
|
||||||
|
Object.entries(config.ai_suggestion.suggestions).forEach(([key, suggestionValue], index) => {
|
||||||
|
// 检查建议值是否存在(null 或有值都要渲染)
|
||||||
|
const hasSuggestedValue = suggestionValue.suggested_value !== null && suggestionValue.suggested_value.trim() !== '';
|
||||||
|
|
||||||
|
fieldElements.push(
|
||||||
|
<div key={`ai-suggestion-${index}`} className="mb-3">
|
||||||
|
{/* 字段名称标签 */}
|
||||||
|
<div className="text-xs text-gray-600 mb-2 font-medium">
|
||||||
|
<i className="ri-lightbulb-line text-yellow-500 mr-1"></i>
|
||||||
|
AI建议修改 - {key}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 原因说明 */}
|
||||||
|
<div className="mb-2 p-2 bg-amber-50 rounded border border-amber-200 text-xs text-gray-700">
|
||||||
|
<div className="flex items-start">
|
||||||
|
<i className="ri-information-line text-amber-600 mr-1 mt-0.5"></i>
|
||||||
|
<div>
|
||||||
|
<span>{suggestionValue.reason}</span>
|
||||||
|
{suggestionValue.source.page !== null && (
|
||||||
|
<span className="ml-2 text-gray-500">
|
||||||
|
(来源页码: {suggestionValue.source.page})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 建议内容显示 */}
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{/* 文本输入框 */}
|
||||||
|
<textarea
|
||||||
|
value={suggestionValue.suggested_value || ''}
|
||||||
|
readOnly
|
||||||
|
disabled={!hasSuggestedValue}
|
||||||
|
className={`flex-1 p-2 border rounded text-xs resize-none overflow-y-auto ${
|
||||||
|
hasSuggestedValue
|
||||||
|
? 'border-gray-200 bg-gray-50 text-gray-700 cursor-not-allowed'
|
||||||
|
: 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
aria-label={`${key}的AI建议内容`}
|
||||||
|
placeholder={!hasSuggestedValue ? '暂无建议值' : ''}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 返回包含所有元素的React片段
|
// 返回包含所有元素的React片段
|
||||||
return <>{fieldElements}</>;
|
return <>{fieldElements}</>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -207,20 +207,20 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
|||||||
|
|
||||||
// 如果是省局访问
|
// 如果是省局访问
|
||||||
// if(isPort51707){
|
// if(isPort51707){
|
||||||
// if (selectedModuleName === '智慧法务大模型'){
|
// if (selectedModuleName === '智慧法务助手'){
|
||||||
// return item.path && item.path.startsWith('/chat-with-llm')
|
// return item.path && item.path.startsWith('/chat-with-llm')
|
||||||
// }
|
// }
|
||||||
// return item.path && item.path.startsWith('/cross-checking')
|
// return item.path && item.path.startsWith('/cross-checking')
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// 🔑 如果选择了"智慧法务大模型",显示 /chat-with-llm 和 /dataset-manager 相关菜单
|
// 🔑 如果选择了"智慧法务助手",显示 /chat-with-llm 和 /dataset-manager 相关菜单
|
||||||
if (selectedModuleName === '智慧法务大模型') {
|
if (selectedModuleName === '智慧法务助手') {
|
||||||
return item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')
|
return item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔑 如果选择了包含"合同"的模块
|
// 🔑 如果选择了包含"合同"的模块
|
||||||
if (selectedModuleName.includes('合同')) {
|
if (selectedModuleName.includes('合同')) {
|
||||||
// 排除智慧法务大模型专属菜单
|
// 排除智慧法务助手专属菜单
|
||||||
if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) {
|
if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 🔑 其他模块:排除特殊菜单
|
// 🔑 其他模块:排除特殊菜单
|
||||||
// 排除智慧法务大模型专属菜单
|
// 排除智慧法务助手专属菜单
|
||||||
if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) {
|
if (item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/')) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -321,7 +321,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid
|
|||||||
<div className={`flex items-center ${collapsed ? 'justify-center' : ''}`}>
|
<div className={`flex items-center ${collapsed ? 'justify-center' : ''}`}>
|
||||||
{selectedModulePicPath && (
|
{selectedModulePicPath && (
|
||||||
<img
|
<img
|
||||||
src={selectedModuleName === '智慧法务大模型' || selectedModuleName === '交叉评查' ? selectedModulePicPath : `${DOCUMENT_URL}${selectedModulePicPath}`}
|
src={selectedModuleName === '智慧法务助手' || selectedModuleName === '交叉评查' ? selectedModulePicPath : `${DOCUMENT_URL}${selectedModulePicPath}`}
|
||||||
alt={selectedModuleName}
|
alt={selectedModuleName}
|
||||||
className={`${collapsed ? 'w-8 h-8' : 'w-6 h-6 mr-3'}`}
|
className={`${collapsed ? 'w-8 h-8' : 'w-6 h-6 mr-3'}`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* 文件预览组件
|
* 文件预览组件
|
||||||
* 显示文档内容和评查点高亮
|
* 显示文档内容和评查点高亮
|
||||||
*/
|
*/
|
||||||
import { useState, useEffect, useRef, ChangeEvent } from 'react';
|
import { useState, useEffect, useRef, forwardRef, useImperativeHandle, ChangeEvent } from 'react';
|
||||||
import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer';
|
import { CollaboraViewer, type CollaboraViewerHandle } from '~/components/collabora/CollaboraViewer';
|
||||||
import { requestPageInfo, customGotoPage } from '~/components/collabora/lib';
|
import { requestPageInfo, customGotoPage } from '~/components/collabora/lib';
|
||||||
import { PdfPreview } from './previewComponents/PdfPreview';
|
import { PdfPreview } from './previewComponents/PdfPreview';
|
||||||
@@ -64,14 +64,22 @@ interface FilePreviewProps {
|
|||||||
replaceText: string;
|
replaceText: string;
|
||||||
pageNumber: number;
|
pageNumber: number;
|
||||||
}; // AI建议替换参数
|
}; // AI建议替换参数
|
||||||
|
isTemplate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 暴露给父组件的接口
|
||||||
|
export interface FilePreviewHandle {
|
||||||
|
collaboraViewerRef: React.RefObject<CollaboraViewerHandle>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
||||||
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace }: FilePreviewProps) {
|
export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>(function FilePreview({ fileContent, activeReviewPointResultId, targetPage, charPositions, highlightValue, isStructuredView = false, userInfo, aiSuggestionReplace, isTemplate = false }, ref) {
|
||||||
// 获取文件类型
|
// 获取文件类型
|
||||||
const real_path = fileContent.path || fileContent.template_contract_path || '';
|
const real_path = fileContent.path || fileContent.template_contract_path || '';
|
||||||
const fileExtension = real_path.split('.').pop()?.toLowerCase();
|
const fileExtension = real_path.split('.').pop()?.toLowerCase();
|
||||||
|
// 是文档类型 并且 是模板文件
|
||||||
const isDocx = fileExtension === 'docx';
|
const isDocx = fileExtension === 'docx';
|
||||||
|
// 是模板文件 或 是pdf文件就用pdf渲染
|
||||||
const isPdf = fileExtension === 'pdf';
|
const isPdf = fileExtension === 'pdf';
|
||||||
|
|
||||||
// ✅ 将所有hooks移到条件return之前,确保遵守React Hooks规则
|
// ✅ 将所有hooks移到条件return之前,确保遵守React Hooks规则
|
||||||
@@ -80,6 +88,11 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
const collaboraViewerRef = useRef<CollaboraViewerHandle>(null);
|
const collaboraViewerRef = useRef<CollaboraViewerHandle>(null);
|
||||||
const prevTargetPageRef = useRef<number | undefined>(undefined);
|
const prevTargetPageRef = useRef<number | undefined>(undefined);
|
||||||
|
|
||||||
|
// 暴露 collaboraViewerRef 给父组件
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
collaboraViewerRef
|
||||||
|
}));
|
||||||
|
|
||||||
// States
|
// States
|
||||||
const [numPages, setNumPages] = useState<number | null>(null);
|
const [numPages, setNumPages] = useState<number | null>(null);
|
||||||
const [pageInputValue, setPageInputValue] = useState<string>('');
|
const [pageInputValue, setPageInputValue] = useState<string>('');
|
||||||
@@ -265,17 +278,20 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if (targetPageNum > 0 && (!numPages || targetPageNum <= numPages)) {
|
||||||
if (targetPageNum > 0) {
|
if (targetPageNum > 0) {
|
||||||
try {
|
try {
|
||||||
await customGotoPage(iframeWindow, targetPageNum);
|
await customGotoPage(iframeWindow, targetPageNum);
|
||||||
setPageInputValue('');
|
setPageInputValue('');
|
||||||
|
// toastService.success(`已跳转至第 ${targetPageNum} 页`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||||
toastService.error(`跳转失败: ${errorMessage}`);
|
toastService.error(`跳转失败: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
|
// } else if (numPages && targetPageNum > numPages) {
|
||||||
|
// toastService.warning(`页码不能超过总页数 ${numPages}`);
|
||||||
} else {
|
} else {
|
||||||
toastService.warning('请输入有效页码');
|
toastService.warning('请输入有效页码');
|
||||||
setPageInputValue('');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -286,17 +302,55 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 滚动到顶部(仅用于 DOCX)
|
// 滚动到顶部(支持 PDF 和 DOCX)
|
||||||
const handleScrollToTop = async () => {
|
const handleScrollToTop = async () => {
|
||||||
if (!collaboraViewerRef.current?.isReady) {
|
|
||||||
toastService.warning('文档尚未加载完成,请稍候...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsScrollingToTop(true);
|
setIsScrollingToTop(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await collaboraViewerRef.current?.unoCommands.scrollToTop();
|
if (isPdf) {
|
||||||
console.log('[FilePreview] 已返回顶部');
|
// PDF文件:滚动到第一个页面元素
|
||||||
|
const firstPage = document.querySelector('[data-page-number="1"]');
|
||||||
|
if (firstPage) {
|
||||||
|
firstPage.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||||
|
// console.log('[FilePreview] PDF已返回顶部');
|
||||||
|
} else if (contentRef.current) {
|
||||||
|
// 如果找不到页面元素,则滚动容器到顶部
|
||||||
|
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
// console.log('[FilePreview] PDF容器已返回顶部');
|
||||||
|
}
|
||||||
|
} else if (isDocx) {
|
||||||
|
// DOCX文件:模板预览模式尝试多种滚动方式,编辑模式使用Collabora命令
|
||||||
|
if (isTemplate) {
|
||||||
|
// 模板预览模式:尝试多种滚动方式
|
||||||
|
console.log('[FilePreview] 尝试返回顶部...');
|
||||||
|
|
||||||
|
// 1. 尝试滚动 contentRef 容器
|
||||||
|
if (contentRef.current) {
|
||||||
|
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
console.log('[FilePreview] 已滚动 contentRef 容器');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试滚动外层的 file-preview-isolation 容器
|
||||||
|
const isolationContainer = document.querySelector('.file-preview-isolation');
|
||||||
|
if (isolationContainer) {
|
||||||
|
isolationContainer.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
console.log('[FilePreview] 已滚动 isolation 容器');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 最后滚动整个窗口
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
console.log('[FilePreview] 已滚动窗口');
|
||||||
|
} else {
|
||||||
|
// 编辑模式:使用Collabora UNO命令
|
||||||
|
if (!collaboraViewerRef.current?.isReady) {
|
||||||
|
toastService.warning('文档尚未加载完成,请稍候...');
|
||||||
|
setIsScrollingToTop(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await collaboraViewerRef.current?.unoCommands.scrollToTop();
|
||||||
|
console.log('[FilePreview] DOCX已返回顶部(UNO命令)');
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[FilePreview] 返回顶部失败:', error);
|
console.error('[FilePreview] 返回顶部失败:', error);
|
||||||
toastService.error('返回顶部失败');
|
toastService.error('返回顶部失败');
|
||||||
@@ -357,11 +411,13 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
const highlightText = highlightValue;
|
const highlightText = highlightValue;
|
||||||
|
|
||||||
// DOCX文件使用Collabora Online预览
|
// DOCX文件使用Collabora Online预览
|
||||||
|
// 如果是模板预览,使用只读模式;否则使用编辑模式
|
||||||
return (
|
return (
|
||||||
<CollaboraViewer
|
<CollaboraViewer
|
||||||
ref={collaboraViewerRef}
|
ref={collaboraViewerRef}
|
||||||
fileId={real_path}
|
fileId={real_path}
|
||||||
mode="edit"
|
mode={isTemplate ? "view" : "edit"}
|
||||||
|
// mode={"edit"}
|
||||||
userId={userInfo?.sub || 'guest'}
|
userId={userInfo?.sub || 'guest'}
|
||||||
userName={userInfo?.nick_name || ''}
|
userName={userInfo?.nick_name || ''}
|
||||||
targetPage={targetPage}
|
targetPage={targetPage}
|
||||||
@@ -380,8 +436,8 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="file-preview">
|
<div className="file-preview h-full flex flex-col">
|
||||||
<div className="file-preview-header px-2 text-xs sm:text-xs md:text-sm max-w-full flex items-center justify-between min-w-0">
|
<div className="file-preview-header px-2 text-xs sm:text-xs md:text-sm max-w-full flex items-center justify-between min-w-0 flex-shrink-0">
|
||||||
<div className="flex items-center min-w-0 flex-shrink-0">
|
<div className="flex items-center min-w-0 flex-shrink-0">
|
||||||
<i className={`${isStructuredView ? 'ri-file-list-line' : 'ri-file-text-line'} text-primary mr-2 flex-shrink-0`}></i>
|
<i className={`${isStructuredView ? 'ri-file-list-line' : 'ri-file-text-line'} text-primary mr-2 flex-shrink-0`}></i>
|
||||||
<span className="font-medium text-primary truncate max-w-[120px]" title={isStructuredView ? '模板预览' : '文件预览'}>
|
<span className="font-medium text-primary truncate max-w-[120px]" title={isStructuredView ? '模板预览' : '文件预览'}>
|
||||||
@@ -389,27 +445,30 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="file-preview-actions flex items-center ml-2 min-w-0 flex-1 justify-end overflow-hidden gap-2">
|
<div className="file-preview-actions flex items-center ml-2 min-w-0 flex-1 justify-end overflow-hidden gap-2">
|
||||||
<button
|
{/* 返回顶部按钮 - 不在模板预览时显示 */}
|
||||||
className={`flex items-center justify-center px-2 py-1 text-xs text-gray-700 bg-white border border-gray-300 rounded transition-colors duration-200 flex-shrink-0 outline-none ${
|
{!isTemplate && (
|
||||||
isScrollingToTop || isDocumentLoading
|
<button
|
||||||
? 'opacity-50 cursor-not-allowed'
|
className={`flex items-center justify-center px-2 py-1 text-xs text-gray-700 bg-white border border-gray-300 rounded transition-colors duration-200 flex-shrink-0 outline-none ${
|
||||||
: 'hover:bg-gray-50 hover:border-primary hover:text-primary'
|
isScrollingToTop || isDocumentLoading
|
||||||
}`}
|
? 'opacity-50 cursor-not-allowed'
|
||||||
onClick={handleScrollToTop}
|
: 'hover:bg-gray-50 hover:border-primary hover:text-primary'
|
||||||
disabled={isScrollingToTop || isDocumentLoading}
|
}`}
|
||||||
title="返回顶部"
|
onClick={handleScrollToTop}
|
||||||
>
|
disabled={isScrollingToTop || isDocumentLoading}
|
||||||
{isScrollingToTop ? (
|
title="返回顶部"
|
||||||
<i className="ri-loader-4-line text-sm animate-spin"></i>
|
>
|
||||||
) : (
|
{isScrollingToTop ? (
|
||||||
<i className="ri-arrow-up-double-line text-sm"></i>
|
<i className="ri-loader-4-line text-sm animate-spin"></i>
|
||||||
)}
|
) : (
|
||||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
<i className="ri-arrow-up-double-line text-sm"></i>
|
||||||
{isScrollingToTop ? '返回中...' : '返回顶部'}
|
)}
|
||||||
</span>
|
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
||||||
</button>
|
{isScrollingToTop ? '返回中...' : '返回顶部'}
|
||||||
{/* 清除高亮按钮 - 仅在DOCX文档时显示 */}
|
</span>
|
||||||
{isDocx && (
|
</button>
|
||||||
|
)}
|
||||||
|
{/* 清除高亮按钮 - 仅在DOCX文档时显示,且不是模板预览 */}
|
||||||
|
{isDocx && !isTemplate && (
|
||||||
<button
|
<button
|
||||||
className={`flex items-center justify-center px-2 py-1 text-xs text-white bg-red-500 border border-red-500 rounded transition-colors duration-200 flex-shrink-0 outline-none ${
|
className={`flex items-center justify-center px-2 py-1 text-xs text-white bg-red-500 border border-red-500 rounded transition-colors duration-200 flex-shrink-0 outline-none ${
|
||||||
isClearingHighlights || isDocumentLoading
|
isClearingHighlights || isDocumentLoading
|
||||||
@@ -430,31 +489,39 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{/* 页码跳转控件 */}
|
{/* 页码跳转控件 - 不在模板预览时显示 */}
|
||||||
<div className="inline-flex items-center flex-shrink-0 gap-1">
|
{!isTemplate && (
|
||||||
<input
|
<div className="inline-flex items-center bg-white border border-gray-300 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 flex-shrink-0 overflow-hidden">
|
||||||
type="text"
|
<div className="flex items-center px-2 py-1">
|
||||||
className="w-12 h-7 px-2 text-xs text-center text-gray-700 bg-white border border-gray-300 rounded outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
<i className="ri-file-list-line text-sm text-gray-400 mr-1.5"></i>
|
||||||
placeholder="页码"
|
<input
|
||||||
value={pageInputValue}
|
type="text"
|
||||||
onChange={handlePageInputChange}
|
className="w-10 h-5 px-1 text-xs text-center text-gray-700 bg-transparent border-0 outline-none focus:text-primary transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
onKeyDown={handlePageInputKeyDown}
|
placeholder="页码"
|
||||||
disabled={isDocumentLoading}
|
value={pageInputValue}
|
||||||
/>
|
onChange={handlePageInputChange}
|
||||||
<button
|
onKeyDown={handlePageInputKeyDown}
|
||||||
className="flex items-center justify-center w-7 h-7 text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:border-primary hover:text-primary transition-colors duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white disabled:hover:border-gray-300 disabled:hover:text-gray-700"
|
disabled={isDocumentLoading}
|
||||||
onClick={handlePageJump}
|
/>
|
||||||
disabled={!numPages || isDocumentLoading}
|
{/* {numPages && (
|
||||||
title="跳转到页面"
|
<span className="text-xs text-gray-400 mx-0.5">/</span>
|
||||||
>
|
)}
|
||||||
<i className="ri-arrow-right-line text-sm"></i>
|
{numPages && (
|
||||||
</button>
|
<span className="text-xs text-gray-500 font-medium min-w-[1.5rem] text-center">
|
||||||
{numPages && (
|
{numPages}
|
||||||
<span className="ml-1 text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
</span>
|
||||||
/ {numPages}
|
)} */}
|
||||||
</span>
|
</div>
|
||||||
)}
|
<button
|
||||||
</div>
|
className="flex items-center justify-center h-7 px-2.5 text-white bg-primary hover:bg-primary-hover transition-colors duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary border-l border-primary-hover/20"
|
||||||
|
onClick={handlePageJump}
|
||||||
|
disabled={!numPages || isDocumentLoading}
|
||||||
|
title="跳转"
|
||||||
|
>
|
||||||
|
<i className="ri-skip-forward-mini-line text-sm"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* 缩放提示 - 仅在DOCX文档时显示 */}
|
{/* 缩放提示 - 仅在DOCX文档时显示 */}
|
||||||
{isDocx && (
|
{isDocx && (
|
||||||
<div className="flex items-center px-2 py-1 text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded flex-shrink-0">
|
<div className="flex items-center px-2 py-1 text-xs text-gray-600 bg-gray-50 border border-gray-200 rounded flex-shrink-0">
|
||||||
@@ -465,13 +532,8 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="file-preview-content"
|
className="file-preview-content flex-1 overflow-auto"
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
style={{
|
|
||||||
maxHeight: 'calc(100vh - 150px)',
|
|
||||||
overflowY: 'auto',
|
|
||||||
overflowX: 'auto',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="pdf-interactive-container"
|
className="pdf-interactive-container"
|
||||||
@@ -489,4 +551,4 @@ export function FilePreview({ fileContent, activeReviewPointResultId, targetPage
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
@@ -1251,8 +1251,9 @@ 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`}
|
${res ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'} transition-colors flex flex-col`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
console.log('点击了短链1左', chain[0].data.char_positions, chain[0].data)
|
||||||
if (chain[0].data.page) {
|
if (chain[0].data.page) {
|
||||||
console.log('点击了短链1左', chain[0].data.char_positions, chain[0].data)
|
// console.log('点击了短链1左', chain[0].data.char_positions, chain[0].data)
|
||||||
const reviewPointId = reviewPoint.id as string;
|
const reviewPointId = reviewPoint.id as string;
|
||||||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
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, chain[0].data.char_positions, chain[0].data.value);
|
||||||
@@ -1504,7 +1505,26 @@ export function ReviewPointsList({
|
|||||||
value: string;
|
value: string;
|
||||||
char_positions?: CharPosition[];
|
char_positions?: CharPosition[];
|
||||||
}>;
|
}>;
|
||||||
ai_suggestion?: Record<string,string>;
|
ai_suggestion?: {
|
||||||
|
summary?: string;
|
||||||
|
analysis?: {
|
||||||
|
failure_reason?: string;
|
||||||
|
solution_approach?: string;
|
||||||
|
rule_understanding?: string;
|
||||||
|
};
|
||||||
|
suggestions?: Record<string, {
|
||||||
|
reason: string;
|
||||||
|
source: {
|
||||||
|
page: number | null;
|
||||||
|
type: string;
|
||||||
|
field: string | null;
|
||||||
|
};
|
||||||
|
priority: string;
|
||||||
|
confidence: number;
|
||||||
|
suggested_value: string | null;
|
||||||
|
}>;
|
||||||
|
generated_at?: string;
|
||||||
|
};
|
||||||
message?: string;
|
message?: string;
|
||||||
res?: boolean;
|
res?: boolean;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
@@ -1536,8 +1556,6 @@ export function ReviewPointsList({
|
|||||||
// 遍历fields,获取每个字段的值并生成对应的JSX元素
|
// 遍历fields,获取每个字段的值并生成对应的JSX元素
|
||||||
if (config.fields) {
|
if (config.fields) {
|
||||||
Object.entries(config.fields).forEach(([key, value], index) => {
|
Object.entries(config.fields).forEach(([key, value], index) => {
|
||||||
if (key == '合同正文-附件序号、标题') {value.value = '签订', value.page = 1}
|
|
||||||
if (key == '合同附件-序号、标题') {value.value = '电话', value.page = 1}
|
|
||||||
const res = value.value.trim() !== '';
|
const res = value.value.trim() !== '';
|
||||||
fieldElements.push(
|
fieldElements.push(
|
||||||
<button
|
<button
|
||||||
@@ -1648,85 +1666,99 @@ export function ReviewPointsList({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
config.ai_suggestion = {
|
|
||||||
'合同正文-附件序号、标题': '签订-一致啊一致',
|
|
||||||
'合同附件-序号、标题': '电话-明确啊明确'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染AI建议(ai_suggestion)
|
// 渲染AI建议(ai_suggestion)
|
||||||
if (config.ai_suggestion) {
|
if (config.ai_suggestion?.suggestions && Object.keys(config.ai_suggestion.suggestions).length > 0) {
|
||||||
// 遍历ai_suggestion对象
|
// 判断是否为PDF文档(禁用替换按钮)
|
||||||
Object.entries(config.ai_suggestion).forEach(([key, value], index) => {
|
fileFormat = fileFormat?.replace(/\./g,'')
|
||||||
// 只渲染value不为空的项
|
const isPDF = fileFormat?.toUpperCase() === 'PDF';
|
||||||
if (value && value.trim() !== '') {
|
|
||||||
// 判断是否为PDF文档(禁用替换按钮)
|
|
||||||
fileFormat = fileFormat?.replace(/\./g,'')
|
|
||||||
const isPDF = fileFormat?.toUpperCase() === 'PDF';
|
|
||||||
|
|
||||||
fieldElements.push(
|
// 遍历suggestions对象的key-value对
|
||||||
<div key={`ai-suggestion-${index}`} className="mb-3">
|
Object.entries(config.ai_suggestion.suggestions).forEach(([key, suggestionValue], index) => {
|
||||||
{/* 字段名称标签 */}
|
// 检查建议值是否存在(null 或有值都要渲染)
|
||||||
<div className="text-xs text-gray-600 mb-2 font-medium">
|
const hasSuggestedValue = suggestionValue.suggested_value !== null && suggestionValue.suggested_value.trim() !== '';
|
||||||
<i className="ri-lightbulb-line text-yellow-500 mr-1"></i>
|
const isReplaceDisabled = !hasSuggestedValue || isPDF;
|
||||||
AI建议修改 - {key}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 建议内容和替换按钮 */}
|
fieldElements.push(
|
||||||
<div className="flex gap-2 items-center">
|
<div key={`ai-suggestion-${index}`} className="mb-3">
|
||||||
{/* 禁用编辑的文本输入框 */}
|
{/* 字段名称标签 */}
|
||||||
<textarea
|
<div className="text-xs text-gray-600 mb-2 font-medium">
|
||||||
value={value}
|
<i className="ri-lightbulb-line text-yellow-500 mr-1"></i>
|
||||||
readOnly
|
AI建议修改 - {key}
|
||||||
className="flex-1 p-2 border border-gray-200 rounded text-xs bg-gray-50 text-gray-700 resize-none cursor-not-allowed overflow-y-auto"
|
|
||||||
aria-label={`${key}的AI建议内容`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 意见替换按钮 */}
|
|
||||||
{ !isPDF &&
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (!isPDF && onAiSuggestionReplace && config.fields) {
|
|
||||||
// 从 config.fields[key] 中获取对应的字段信息
|
|
||||||
const fieldData = config.fields[key];
|
|
||||||
if (fieldData) {
|
|
||||||
// 调用回调函数,传递搜索文本、替换文本和页码
|
|
||||||
onAiSuggestionReplace(
|
|
||||||
fieldData.value || '', // 搜索文本(原文)
|
|
||||||
value || '', // 替换文本(AI建议)
|
|
||||||
Number(fieldData.page) || 1 // 页码
|
|
||||||
);
|
|
||||||
// toastService.success(`已触发替换操作: ${key}`);
|
|
||||||
} else {
|
|
||||||
toastService.error(`未找到字段 ${key} 的原始数据`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
disabled={isPDF}
|
|
||||||
className={`px-3 py-2 text-xs rounded whitespace-nowrap transition-colors
|
|
||||||
${isPDF
|
|
||||||
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
|
||||||
: 'bg-primary text-white hover:bg-primary-hover active:bg-primary'
|
|
||||||
}`}
|
|
||||||
title={isPDF ? 'PDF文档不支持替换' : '点击执行一键替换'}
|
|
||||||
aria-label={`替换${key}的内容`}
|
|
||||||
>
|
|
||||||
<i className="ri-exchange-line mr-1"></i>
|
|
||||||
替换
|
|
||||||
</button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* PDF禁用提示 */}
|
|
||||||
{/* {isPDF && (
|
|
||||||
<div className="mt-1 text-xs text-gray-500 flex items-center">
|
|
||||||
<i className="ri-information-line mr-1"></i>
|
|
||||||
PDF文档不支持替换功能
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
}
|
{/* 原因说明 */}
|
||||||
|
<div className="mb-2 p-2 bg-amber-50 rounded border border-amber-200 text-xs text-gray-700">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<i className="ri-information-line text-amber-600 mr-1 mt-0.5"></i>
|
||||||
|
<div>
|
||||||
|
<span>{suggestionValue.reason}</span>
|
||||||
|
{suggestionValue.source.page !== null && (
|
||||||
|
<span className="ml-2 text-gray-500">
|
||||||
|
(来源页码: {suggestionValue.source.page})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 建议内容和替换按钮 */}
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{/* 文本输入框 */}
|
||||||
|
<textarea
|
||||||
|
value={suggestionValue.suggested_value || ''}
|
||||||
|
readOnly
|
||||||
|
disabled={!hasSuggestedValue}
|
||||||
|
className={`flex-1 p-2 border rounded text-xs resize-none overflow-y-auto ${
|
||||||
|
hasSuggestedValue
|
||||||
|
? 'border-gray-200 bg-gray-50 text-gray-700 cursor-not-allowed'
|
||||||
|
: 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
aria-label={`${key}的AI建议内容`}
|
||||||
|
placeholder={!hasSuggestedValue ? '暂无建议值' : ''}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 意见替换按钮 */}
|
||||||
|
{ !isPDF &&
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
if (!isReplaceDisabled && onAiSuggestionReplace && config.fields) {
|
||||||
|
// 从 config.fields[key] 中获取对应的字段信息
|
||||||
|
const fieldData = config.fields[key];
|
||||||
|
if (fieldData) {
|
||||||
|
// 调用回调函数,传递搜索文本(原文)、替换文本(AI建议)和页码
|
||||||
|
onAiSuggestionReplace(
|
||||||
|
key, // 搜索文本(使用 suggestions 的 key)
|
||||||
|
suggestionValue.suggested_value || '', // 替换文本(AI建议的 suggested_value)
|
||||||
|
Number(fieldData.page) || 1 // 页码
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
toastService.error(`未找到字段 ${key} 的原始数据`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={isReplaceDisabled}
|
||||||
|
className={`px-3 py-2 text-xs rounded whitespace-nowrap transition-colors
|
||||||
|
${isReplaceDisabled
|
||||||
|
? 'bg-gray-200 text-gray-400 cursor-not-allowed'
|
||||||
|
: 'bg-primary text-white hover:bg-primary-hover active:bg-primary'
|
||||||
|
}`}
|
||||||
|
title={
|
||||||
|
isPDF
|
||||||
|
? 'PDF文档不支持替换'
|
||||||
|
: !hasSuggestedValue
|
||||||
|
? '暂无建议值,无法替换'
|
||||||
|
: '点击执行一键替换'
|
||||||
|
}
|
||||||
|
aria-label={`替换${key}的内容`}
|
||||||
|
>
|
||||||
|
<i className="ri-exchange-line mr-1"></i>
|
||||||
|
替换
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -277,10 +277,10 @@ export function ReviewTabs({ activeTab, onTabChange, children, fileInfo, onConfi
|
|||||||
<i className="ri-arrow-left-line mr-1"></i> {isNavigating ? '返回中...' : '返回'}
|
<i className="ri-arrow-left-line mr-1"></i> {isNavigating ? '返回中...' : '返回'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="ant-btn ant-btn-default flex items-center my-2"
|
className="ant-btn ant-btn-default inline-flex items-center my-2"
|
||||||
onClick={handleDownloadFile}
|
onClick={handleDownloadFile}
|
||||||
>
|
>
|
||||||
<i className="ri-file-download-line mr-1"></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"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from "react";
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
import { DiffEditor } from "@monaco-editor/react";
|
import { DiffEditor, loader } from "@monaco-editor/react";
|
||||||
import type { editor } from "monaco-editor";
|
import type { editor } from "monaco-editor";
|
||||||
import { pdfjs } from 'react-pdf';
|
import { pdfjs } from 'react-pdf';
|
||||||
import mammoth from 'mammoth';
|
import mammoth from 'mammoth';
|
||||||
@@ -23,6 +23,33 @@ import { DOCUMENT_URL } from '~/config/api-config';
|
|||||||
// Setup PDF.js worker
|
// Setup PDF.js worker
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||||
|
|
||||||
|
// 配置 Monaco Editor 使用本地资源(避免 CDN 加载超时)
|
||||||
|
// Monaco Editor 资源已通过 npm run copy-monaco 复制到 public/monaco-editor
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
console.log('[Monaco] 使用本地资源加载');
|
||||||
|
|
||||||
|
loader.config({
|
||||||
|
paths: {
|
||||||
|
vs: '/monaco-editor/vs'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加加载超时监控和错误处理
|
||||||
|
const initTimeout = setTimeout(() => {
|
||||||
|
console.error('[Monaco] 加载超时(30秒)');
|
||||||
|
toastService.error('代码编辑器加载超时,请刷新页面重试');
|
||||||
|
}, 30000);
|
||||||
|
|
||||||
|
loader.init().then(() => {
|
||||||
|
clearTimeout(initTimeout);
|
||||||
|
console.log('[Monaco] ✅ 加载成功');
|
||||||
|
}).catch((error: Error) => {
|
||||||
|
clearTimeout(initTimeout);
|
||||||
|
console.error('[Monaco] ❌ 加载失败:', error);
|
||||||
|
toastService.error(`代码编辑器加载失败: ${error.message}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Document type enum
|
// Document type enum
|
||||||
type DocumentType = 'pdf' | 'docx' | 'unknown';
|
type DocumentType = 'pdf' | 'docx' | 'unknown';
|
||||||
|
|
||||||
@@ -600,7 +627,7 @@ export function ComparePreview({ doc1Path, doc2Path }: ComparePreviewProps): JSX
|
|||||||
<strong>差异高亮说明:</strong>
|
<strong>差异高亮说明:</strong>
|
||||||
<span style={{ marginLeft: '8px' }}>
|
<span style={{ marginLeft: '8px' }}>
|
||||||
<span style={{ color: '#dc3545', fontWeight: 'bold' }}>左侧红色</span>:原始版本 |
|
<span style={{ color: '#dc3545', fontWeight: 'bold' }}>左侧红色</span>:原始版本 |
|
||||||
<span style={{ color: '#28a745', fontWeight: 'bold', marginLeft: '8px' }}>右侧绿色</span>:修改版本 |
|
<span style={{ color: '#28a745', fontWeight: 'bold', marginLeft: '8px' }}>右侧绿色</span>:比对版本 |
|
||||||
<span style={{ color: '#666', fontWeight: 'bold', marginLeft: '8px' }}>深色高亮</span>:字符差异
|
<span style={{ color: '#666', fontWeight: 'bold', marginLeft: '8px' }}>深色高亮</span>:字符差异
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ export function PdfPreview({
|
|||||||
>
|
>
|
||||||
<i className="ri-zoom-out-line text-sm"></i>
|
<i className="ri-zoom-out-line text-sm"></i>
|
||||||
</button>
|
</button>
|
||||||
{/* 页码跳转控件 */}
|
{/* 页码跳转控件
|
||||||
<div className="inline-flex items-center flex-shrink-0 gap-1">
|
<div className="inline-flex items-center flex-shrink-0 gap-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -412,7 +412,38 @@ export function PdfPreview({
|
|||||||
/ {numPages}
|
/ {numPages}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
</div> */}
|
||||||
|
{/* 页码跳转控件 */}
|
||||||
|
<div className="inline-flex items-center bg-white border border-gray-300 rounded-lg shadow-sm hover:shadow-md transition-shadow duration-200 flex-shrink-0 overflow-hidden">
|
||||||
|
<div className="flex items-center px-2 py-1">
|
||||||
|
<i className="ri-file-list-line text-sm text-gray-400 mr-1.5"></i>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="w-10 h-5 px-1 text-xs text-center text-gray-700 bg-transparent border-0 outline-none focus:text-primary transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
placeholder="页码"
|
||||||
|
value={pageInputValue}
|
||||||
|
onChange={handlePageInputChange}
|
||||||
|
onKeyDown={handlePageInputKeyDown}
|
||||||
|
/>
|
||||||
|
{numPages && (
|
||||||
|
<span className="text-xs text-gray-400 mx-0.5">/</span>
|
||||||
|
)}
|
||||||
|
{numPages && (
|
||||||
|
<span className="text-xs text-gray-500 font-medium min-w-[1.5rem] text-center">
|
||||||
|
{numPages}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="flex items-center justify-center h-7 px-2.5 text-white bg-primary hover:bg-primary-hover transition-colors duration-200 outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-primary border-l border-primary-hover/20"
|
||||||
|
onClick={handlePageJump}
|
||||||
|
disabled={!numPages}
|
||||||
|
title="跳转"
|
||||||
|
>
|
||||||
|
<i className="ri-skip-forward-mini-line text-sm"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap flex-shrink-0">
|
<span className="text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap flex-shrink-0">
|
||||||
比例:{zoomLevel}%
|
比例:{zoomLevel}%
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
+5
-5
@@ -177,11 +177,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
// 检查当前路径是否在允许列表中
|
// 检查当前路径是否在允许列表中
|
||||||
const isAllowedPath = isPathAllowed(pathname, allowedPaths);
|
const isAllowedPath = isPathAllowed(pathname, allowedPaths);
|
||||||
|
|
||||||
if (!isAllowedPath) {
|
// if (!isAllowedPath) {
|
||||||
console.warn(`⚠️ [Root Loader] 用户尝试访问未授权路由: ${pathname}`);
|
// console.warn(`⚠️ [Root Loader] 用户尝试访问未授权路由: ${pathname}`);
|
||||||
// 返回 403 错误,而不是 redirect(避免循环)
|
// // 返回 403 错误,而不是 redirect(避免循环)
|
||||||
throw new Response("无权访问此页面", { status: 403 });
|
// throw new Response("无权访问此页面", { status: 403 });
|
||||||
}
|
// }
|
||||||
} else {
|
} else {
|
||||||
// 🔑 检查是否因为认证失败需要重定向到登录页
|
// 🔑 检查是否因为认证失败需要重定向到登录页
|
||||||
if (routesResult.shouldRedirectToHome) {
|
if (routesResult.shouldRedirectToHome) {
|
||||||
|
|||||||
+10
-10
@@ -83,7 +83,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
|||||||
// console.log(`🔑 [Index Loader] 用户${hasSettingsAccess ? '有' : '没有'}系统设置权限`);
|
// console.log(`🔑 [Index Loader] 用户${hasSettingsAccess ? '有' : '没有'}系统设置权限`);
|
||||||
// console.log(`🔑 [Index Loader] 系统设置子路由数量: ${settingsChildren.length}`);
|
// console.log(`🔑 [Index Loader] 系统设置子路由数量: ${settingsChildren.length}`);
|
||||||
// console.log(`🔑 [Index Loader] 用户${hasCrossCheckingAccess ? '有' : '没有'}交叉评查权限`);
|
// console.log(`🔑 [Index Loader] 用户${hasCrossCheckingAccess ? '有' : '没有'}交叉评查权限`);
|
||||||
// console.log(`🔑 [Index Loader] 用户${hasChatLLMAccess ? '有' : '没有'}智慧法务大模型权限`);
|
// console.log(`🔑 [Index Loader] 用户${hasChatLLMAccess ? '有' : '没有'}智慧法务助手权限`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,8 +176,8 @@ export default function Index() {
|
|||||||
// 提取文档类型 IDs
|
// 提取文档类型 IDs
|
||||||
const typeIds = module.document_types?.map(dt => dt.id) || [];
|
const typeIds = module.document_types?.map(dt => dt.id) || [];
|
||||||
|
|
||||||
// 🔑 验证文档类型(智慧法务大模型除外)
|
// 🔑 验证文档类型(智慧法务助手除外)
|
||||||
if (module.name !== '智慧法务大模型' && typeIds.length === 0) {
|
if (module.name !== '智慧法务助手' && typeIds.length === 0) {
|
||||||
toastService.error('该入口尚未关联文档类型,无法进入');
|
toastService.error('该入口尚未关联文档类型,无法进入');
|
||||||
console.warn('⚠️ [Index] 模块未关联文档类型:', module.name);
|
console.warn('⚠️ [Index] 模块未关联文档类型:', module.name);
|
||||||
return; // 阻止进入
|
return; // 阻止进入
|
||||||
@@ -207,11 +207,11 @@ export default function Index() {
|
|||||||
// 合同相关模块 → 跳转到合同模板搜索
|
// 合同相关模块 → 跳转到合同模板搜索
|
||||||
targetPath = '/contract-template/search';
|
targetPath = '/contract-template/search';
|
||||||
// console.log('📌 [Index] 合同模块,跳转到:', targetPath);
|
// console.log('📌 [Index] 合同模块,跳转到:', targetPath);
|
||||||
} else if (module.name === '智慧法务大模型') {
|
} else if (module.name === '智慧法务助手') {
|
||||||
// 智慧法务大模型 → 跳转到 AI 对话
|
// 智慧法务助手 → 跳转到 AI 对话
|
||||||
targetPath = '/chat-with-llm/chat';
|
targetPath = '/chat-with-llm/chat';
|
||||||
sessionStorage.setItem('selectedModulePicPath', '/images/icon_assistant.png')
|
sessionStorage.setItem('selectedModulePicPath', '/images/icon_assistant.png')
|
||||||
// console.log('📌 [Index] 智慧法务大模型,跳转到:', targetPath);
|
// console.log('📌 [Index] 智慧法务助手,跳转到:', targetPath);
|
||||||
} else {
|
} else {
|
||||||
// console.log('📌 [Index] 其他模块,跳转到:', targetPath);
|
// console.log('📌 [Index] 其他模块,跳转到:', targetPath);
|
||||||
}
|
}
|
||||||
@@ -370,17 +370,17 @@ export default function Index() {
|
|||||||
{loaderData.entryModules && loaderData.entryModules.length > 0 ? (
|
{loaderData.entryModules && loaderData.entryModules.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{loaderData.entryModules.map((module) => {
|
{loaderData.entryModules.map((module) => {
|
||||||
// 判断是否为智慧法务大模型,如果是且有交叉评查权限,则在其之前插入交叉评查卡片
|
// 判断是否为智慧法务助手,如果是且有交叉评查权限,则在其之前插入交叉评查卡片
|
||||||
const isLLMModule = module.name === '智慧法务大模型';
|
const isLLMModule = module.name === '智慧法务助手';
|
||||||
|
|
||||||
// 🔑 如果是智慧法务大模型且用户没有访问权限,则不渲染该模块
|
// 🔑 如果是智慧法务助手且用户没有访问权限,则不渲染该模块
|
||||||
if (isLLMModule && !loaderData.hasChatLLMAccess) {
|
if (isLLMModule && !loaderData.hasChatLLMAccess) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={module.id}>
|
<React.Fragment key={module.id}>
|
||||||
{/* 在智慧法务大模型之前插入交叉评查入口 */}
|
{/* 在智慧法务助手之前插入交叉评查入口 */}
|
||||||
{isLLMModule && loaderData.hasCrossCheckingAccess && (
|
{isLLMModule && loaderData.hasCrossCheckingAccess && (
|
||||||
<div
|
<div
|
||||||
className="module-card"
|
className="module-card"
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import type { MetaFunction, LoaderFunctionArgs } from '@remix-run/node';
|
import type { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node';
|
||||||
import { useLoaderData, useNavigate } from '@remix-run/react';
|
import { redirect } from '@remix-run/node';
|
||||||
|
import { useLoaderData, useNavigate, useSubmit } from '@remix-run/react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { getContractTemplate } from '~/api/contract-template/templates';
|
import { getContractTemplate } from '~/api/contract-template/templates';
|
||||||
import type { ContractTemplate } from '~/api/contract-template/templates';
|
import type { ContractTemplate } from '~/api/contract-template/templates';
|
||||||
import styles from '~/styles/pages/contract-template.css?url';
|
import styles from '~/styles/pages/contract-template.css?url';
|
||||||
import filePreviewStyles from '~/styles/components/file-preview-isolation.css?url';
|
import filePreviewStyles from '~/styles/components/file-preview-isolation.css?url';
|
||||||
import { getUserSession } from '~/api/login/auth.server';
|
import { getUserSession } from '~/api/login/auth.server';
|
||||||
|
import { createDraftContract } from '~/api/contracts/draft-service.server';
|
||||||
|
|
||||||
// 导入FilePreview组件
|
// 导入FilePreview组件
|
||||||
import { FilePreview } from '~/components/reviews';
|
import { FilePreview } from '~/components/reviews';
|
||||||
@@ -36,26 +39,26 @@ export const handle = {
|
|||||||
|
|
||||||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||||||
const templateId = params.id!;
|
const templateId = params.id!;
|
||||||
|
|
||||||
// 获取 JWT
|
// 获取 JWT
|
||||||
const { frontendJWT } = await getUserSession(request);
|
const { frontendJWT } = await getUserSession(request);
|
||||||
const jwt = frontendJWT || undefined;
|
const jwt = frontendJWT || undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await getContractTemplate(templateId, jwt);
|
const response = await getContractTemplate(templateId, jwt);
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw new Response(response.error, { status: response.status || 404 });
|
throw new Response(response.error, { status: response.status || 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.data) {
|
if (!response.data) {
|
||||||
throw new Response('模板未找到', { status: 404 });
|
throw new Response('模板未找到', { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加调试信息
|
// 添加调试信息
|
||||||
// console.log('模板详情数据:', response.data);
|
// console.log('模板详情数据:', response.data);
|
||||||
// console.log('分类信息:', response.data.category);
|
// console.log('分类信息:', response.data.category);
|
||||||
|
|
||||||
return { template: response.data };
|
return { template: response.data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载模板详情失败:', error);
|
console.error('加载模板详情失败:', error);
|
||||||
@@ -63,9 +66,61 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action 函数:处理起草合同请求
|
||||||
|
*/
|
||||||
|
export async function action({ request, params }: ActionFunctionArgs) {
|
||||||
|
const templateId = parseInt(params.id || '0');
|
||||||
|
|
||||||
|
if (!templateId) {
|
||||||
|
return Response.json({ error: '模板ID无效' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户信息和JWT
|
||||||
|
const { userInfo, frontendJWT } = await getUserSession(request);
|
||||||
|
if (!userInfo?.sub) {
|
||||||
|
return Response.json({ error: '未登录' }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解析表单数据
|
||||||
|
const formData = await request.formData();
|
||||||
|
const title = formData.get('title') as string;
|
||||||
|
const draftFilePath = formData.get('draftFilePath') as string | null;
|
||||||
|
|
||||||
|
if (!title) {
|
||||||
|
return Response.json({ error: '标题不能为空' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建草稿记录(到时候可以换成接口,使用接口来在minio中生成备份文件:备份文件可以用时间戳+uuid来保证唯一性。)
|
||||||
|
// const draft = await createDraftContract(
|
||||||
|
// {
|
||||||
|
// templateId,
|
||||||
|
// title,
|
||||||
|
// draftFilePath: draftFilePath || undefined
|
||||||
|
// },
|
||||||
|
// parseInt(userInfo.sub),
|
||||||
|
// draftFilePath || undefined,
|
||||||
|
// frontendJWT || undefined
|
||||||
|
// );
|
||||||
|
|
||||||
|
// 重定向到草稿编辑页面
|
||||||
|
// return redirect(`/contract-draft/${draft.id}`);
|
||||||
|
return redirect(`/contract-draft/1`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Template Detail] 创建草稿失败:', error);
|
||||||
|
return Response.json(
|
||||||
|
{ error: error instanceof Error ? error.message : '创建草稿失败' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function ContractTemplateDetail() {
|
export default function ContractTemplateDetail() {
|
||||||
const { template }: { template: ContractTemplate } = useLoaderData<typeof loader>();
|
const { template }: { template: ContractTemplate } = useLoaderData<typeof loader>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const submit = useSubmit();
|
||||||
|
const [isCreatingDraft, setIsCreatingDraft] = useState(false);
|
||||||
// 注释掉收藏功能
|
// 注释掉收藏功能
|
||||||
// const [isFavorited, setIsFavorited] = useState(false);
|
// const [isFavorited, setIsFavorited] = useState(false);
|
||||||
|
|
||||||
@@ -75,6 +130,7 @@ export default function ContractTemplateDetail() {
|
|||||||
|
|
||||||
// 使用统一的下载方法(与 rules-files.tsx 相同)
|
// 使用统一的下载方法(与 rules-files.tsx 相同)
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
|
|
||||||
if (!template.file_path) {
|
if (!template.file_path) {
|
||||||
toastService.error('文件路径不存在,无法下载');
|
toastService.error('文件路径不存在,无法下载');
|
||||||
return;
|
return;
|
||||||
@@ -84,6 +140,7 @@ export default function ContractTemplateDetail() {
|
|||||||
// 使用axios封装的下载方法
|
// 使用axios封装的下载方法
|
||||||
const blob = await downloadFile(template.file_path);
|
const blob = await downloadFile(template.file_path);
|
||||||
|
|
||||||
|
|
||||||
// 创建Blob URL
|
// 创建Blob URL
|
||||||
const blobUrl = URL.createObjectURL(blob);
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
|
||||||
@@ -120,6 +177,29 @@ export default function ContractTemplateDetail() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 起草合同
|
||||||
|
const handleStartDraft = () => {
|
||||||
|
if (isCreatingDraft) return;
|
||||||
|
|
||||||
|
// 生成默认标题
|
||||||
|
// const defaultTitle = `${template.title}-${new Date().toLocaleDateString('zh-CN').replace(/\//g, '')}`;
|
||||||
|
|
||||||
|
// // 提示用户输入标题
|
||||||
|
// const title = prompt('请输入合同标题:', defaultTitle);
|
||||||
|
// if (!title) return;
|
||||||
|
|
||||||
|
setIsCreatingDraft(true);
|
||||||
|
|
||||||
|
// 使用 Remix 的 submit 提交表单
|
||||||
|
const formData = new FormData();
|
||||||
|
// formData.append('title', title.trim());
|
||||||
|
formData.append('title', '买卖合同-拟起草合同');
|
||||||
|
// 可选:如果需要复制文件,可以先调用文件复制服务,然后传递 draftFilePath
|
||||||
|
// formData.append('draftFilePath', draftFilePath);
|
||||||
|
|
||||||
|
submit(formData, { method: 'post' });
|
||||||
|
};
|
||||||
|
|
||||||
/* const handleFavorite = () => {
|
/* const handleFavorite = () => {
|
||||||
setIsFavorited(!isFavorited);
|
setIsFavorited(!isFavorited);
|
||||||
console.log('收藏状态:', !isFavorited);
|
console.log('收藏状态:', !isFavorited);
|
||||||
@@ -247,15 +327,32 @@ export default function ContractTemplateDetail() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="detail-actions flex gap-3">
|
<div className="detail-actions flex gap-3">
|
||||||
<button
|
<button
|
||||||
className="detail-btn primary bg-primary text-white px-6 py-3 rounded-lg flex items-center gap-2 hover:bg-primary-hover"
|
className="detail-btn primary bg-primary text-white px-6 py-3 rounded-lg flex items-center gap-2 hover:bg-primary-hover"
|
||||||
|
onClick={handleStartDraft}
|
||||||
|
disabled={isCreatingDraft}
|
||||||
|
>
|
||||||
|
{isCreatingDraft ? (
|
||||||
|
<>
|
||||||
|
<i className="ri-loader-4-line animate-spin"></i>
|
||||||
|
创建中...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="ri-edit-line"></i>
|
||||||
|
起草合同
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="detail-btn secondary bg-white border border-gray-200 px-6 py-3 rounded-lg flex items-center gap-2 hover:border-primary"
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
>
|
>
|
||||||
<i className="ri-download-line"></i>
|
<i className="ri-download-line"></i>
|
||||||
立即下载使用
|
下载模板
|
||||||
</button>
|
</button>
|
||||||
{template.pdf_file_path && (
|
{template.pdf_file_path && (
|
||||||
<button
|
<button
|
||||||
className="detail-btn secondary bg-white border border-gray-200 px-6 py-3 rounded-lg flex items-center gap-2 hover:border-primary"
|
className="detail-btn secondary bg-white border border-gray-200 px-6 py-3 rounded-lg flex items-center gap-2 hover:border-primary"
|
||||||
onClick={handlePreview}
|
onClick={handlePreview}
|
||||||
>
|
>
|
||||||
@@ -336,38 +433,15 @@ export default function ContractTemplateDetail() {
|
|||||||
<div className="content-section mb-8" id="template-preview">
|
<div className="content-section mb-8" id="template-preview">
|
||||||
<h3 className="section-title text-xl font-semibold mb-4">合同预览</h3>
|
<h3 className="section-title text-xl font-semibold mb-4">合同预览</h3>
|
||||||
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
<div className="border border-gray-200 rounded-lg overflow-hidden">
|
||||||
{/* 使用更强的样式隔离 */}
|
<div
|
||||||
<div
|
className="file-preview-isolation w-full"
|
||||||
className="file-preview-isolation"
|
|
||||||
style={{
|
|
||||||
// 使用CSS变量避免继承
|
|
||||||
'--font-family': '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
|
||||||
'--font-size': '14px',
|
|
||||||
'--line-height': '1.5',
|
|
||||||
'--text-color': '#333333',
|
|
||||||
'--bg-color': '#ffffff',
|
|
||||||
|
|
||||||
// 强制重置所有可能的样式
|
|
||||||
all: 'unset',
|
|
||||||
display: 'block',
|
|
||||||
fontFamily: 'var(--font-family)',
|
|
||||||
fontSize: 'var(--font-size)',
|
|
||||||
lineHeight: 'var(--line-height)',
|
|
||||||
color: 'var(--text-color)',
|
|
||||||
backgroundColor: 'var(--bg-color)',
|
|
||||||
width: '100%',
|
|
||||||
minHeight: '600px',
|
|
||||||
position: 'relative',
|
|
||||||
isolation: 'isolate', // 创建新的层叠上下文
|
|
||||||
contain: 'layout style', // CSS容器化
|
|
||||||
zIndex: 0
|
|
||||||
} as React.CSSProperties}
|
|
||||||
>
|
>
|
||||||
<FilePreview
|
<FilePreview
|
||||||
fileContent={fileContent}
|
fileContent={fileContent}
|
||||||
activeReviewPointResultId={null}
|
activeReviewPointResultId={null}
|
||||||
targetPage={undefined}
|
targetPage={undefined}
|
||||||
isStructuredView={false}
|
isStructuredView={false}
|
||||||
|
isTemplate={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+151
-24
@@ -223,9 +223,11 @@ export default function DocumentsIndex() {
|
|||||||
const [showAttachmentUpload, setShowAttachmentUpload] = useState<boolean>(false);
|
const [showAttachmentUpload, setShowAttachmentUpload] = useState<boolean>(false);
|
||||||
const [showTemplateUpload, setShowTemplateUpload] = useState<boolean>(false);
|
const [showTemplateUpload, setShowTemplateUpload] = useState<boolean>(false);
|
||||||
const [selectedDocumentId, setSelectedDocumentId] = useState<number | null>(null);
|
const [selectedDocumentId, setSelectedDocumentId] = useState<number | null>(null);
|
||||||
|
const [selectedDocumentName, setSelectedDocumentName] = useState<string | null>(null);
|
||||||
|
const [selectedDocumentVersion, setSelectedDocumentVersion] = useState<number | null>(null);
|
||||||
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
||||||
const [templateFile, setTemplateFile] = useState<File | null>(null);
|
const [templateFile, setTemplateFile] = useState<File | null>(null);
|
||||||
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('overwrite');
|
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('new');
|
||||||
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);
|
||||||
@@ -356,6 +358,38 @@ export default function DocumentsIndex() {
|
|||||||
}
|
}
|
||||||
}, [searchParams, fetchData, documentTypeIds]);
|
}, [searchParams, fetchData, documentTypeIds]);
|
||||||
|
|
||||||
|
// 监听 documents 数据变化,自动修正不一致的展开状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (documents.length === 0) return;
|
||||||
|
|
||||||
|
const newExpandedRows = new Set(expandedRows);
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
// 检查每个展开的行
|
||||||
|
expandedRows.forEach(docId => {
|
||||||
|
const doc = documents.find(d => d.id === docId);
|
||||||
|
|
||||||
|
// 如果文档不存在或没有历史版本数据,自动折叠
|
||||||
|
if (!doc || !doc.historyVersions || doc.historyVersions.length === 0) {
|
||||||
|
console.warn(`自动折叠文档 ${docId}:数据不完整`);
|
||||||
|
newExpandedRows.delete(docId);
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果有变化,更新状态
|
||||||
|
if (hasChanges) {
|
||||||
|
setExpandedRows(newExpandedRows);
|
||||||
|
|
||||||
|
// 同时更新 documents 中的 isExpanded 状态
|
||||||
|
setDocuments(prevDocs =>
|
||||||
|
prevDocs.map(d =>
|
||||||
|
newExpandedRows.has(d.id) ? { ...d, isExpanded: true } : { ...d, isExpanded: false }
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [documents, expandedRows]);
|
||||||
|
|
||||||
// 使用并更新缓存数据
|
// 使用并更新缓存数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 如果有缓存数据,先显示缓存,再在后台加载新数据
|
// 如果有缓存数据,先显示缓存,再在后台加载新数据
|
||||||
@@ -881,6 +915,8 @@ export default function DocumentsIndex() {
|
|||||||
setAttachmentRemark("");
|
setAttachmentRemark("");
|
||||||
setShowAttachmentUpload(false);
|
setShowAttachmentUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null);
|
||||||
|
setSelectedDocumentVersion(null);
|
||||||
|
|
||||||
// 刷新文档列表
|
// 刷新文档列表
|
||||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||||
@@ -951,6 +987,8 @@ export default function DocumentsIndex() {
|
|||||||
setTemplateFile(null);
|
setTemplateFile(null);
|
||||||
setShowTemplateUpload(false);
|
setShowTemplateUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null);
|
||||||
|
setSelectedDocumentVersion(null);
|
||||||
|
|
||||||
// 刷新文档列表
|
// 刷新文档列表
|
||||||
if (documentTypeIds && documentTypeIds.length > 0) {
|
if (documentTypeIds && documentTypeIds.length > 0) {
|
||||||
@@ -981,11 +1019,28 @@ export default function DocumentsIndex() {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
// 展开前检查是否有历史版本数据
|
||||||
|
const hasHistoryData = doc.historyVersions && doc.historyVersions.length > 0;
|
||||||
|
const hasHistoryCount = doc.historyCount && doc.historyCount > 0;
|
||||||
|
|
||||||
|
// 如果有历史版本计数但没有数据,可能是数据加载失败
|
||||||
|
if (hasHistoryCount && !hasHistoryData) {
|
||||||
|
console.warn(`文档 ${doc.id} 有 ${doc.historyCount} 个历史版本,但数据为空`);
|
||||||
|
toastService.warning('历史版本数据加载失败,请刷新页面重试');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有历史版本,不允许展开
|
||||||
|
if (!hasHistoryCount) {
|
||||||
|
console.log(`文档 ${doc.id} 没有历史版本`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 展开:显示历史版本
|
// 展开:显示历史版本
|
||||||
newExpanded.add(doc.id);
|
newExpanded.add(doc.id);
|
||||||
setExpandedRows(newExpanded);
|
setExpandedRows(newExpanded);
|
||||||
|
|
||||||
// 更新展开状态(历史版本数据已经在主数据中了)
|
// 更新展开状态
|
||||||
setDocuments(prevDocs =>
|
setDocuments(prevDocs =>
|
||||||
prevDocs.map(d =>
|
prevDocs.map(d =>
|
||||||
d.id === doc.id ? { ...d, isExpanded: true } : d
|
d.id === doc.id ? { ...d, isExpanded: true } : d
|
||||||
@@ -1077,6 +1132,36 @@ export default function DocumentsIndex() {
|
|||||||
<i className="ri-download-line"></i>
|
<i className="ri-download-line"></i>
|
||||||
下载
|
下载
|
||||||
</button>
|
</button>
|
||||||
|
{parentDoc.type === '1' && historyDoc.fileStatus === 'Processed' && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs px-2 py-1 h-7 mr-1 hover:underline hover:text-primary"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDocumentId(historyDoc.id);
|
||||||
|
setSelectedDocumentName(historyDoc.name);
|
||||||
|
setSelectedDocumentVersion(historyDoc.versionNumber || null);
|
||||||
|
setShowAttachmentUpload(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="ri-attachment-line"></i>
|
||||||
|
追加附件
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedDocumentId(historyDoc.id);
|
||||||
|
setSelectedDocumentName(historyDoc.name);
|
||||||
|
setSelectedDocumentVersion(historyDoc.versionNumber || null);
|
||||||
|
setShowTemplateUpload(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<i className="ri-file-copy-line"></i>
|
||||||
|
上传模板
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs px-2 py-1 h-7 text-error hover:underline hover:text-red-700"
|
className="text-xs px-2 py-1 h-7 text-error hover:underline hover:text-red-700"
|
||||||
@@ -1294,22 +1379,26 @@ export default function DocumentsIndex() {
|
|||||||
</button>
|
</button>
|
||||||
{record.type === '1' && record.fileStatus === 'Processed' && (
|
{record.type === '1' && record.fileStatus === 'Processed' && (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs px-2 py-1 h-7 mr-1 hover:underline hover:text-primary"
|
className="text-xs px-2 py-1 h-7 mr-1 hover:underline hover:text-primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDocumentId(record.id);
|
setSelectedDocumentId(record.id);
|
||||||
|
setSelectedDocumentName(record.name);
|
||||||
|
setSelectedDocumentVersion(record.historyCount !== undefined && record.historyCount > 0 ? record.historyCount + 1 : null);
|
||||||
setShowAttachmentUpload(true);
|
setShowAttachmentUpload(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<i className="ri-attachment-line"></i>
|
<i className="ri-attachment-line"></i>
|
||||||
追加附件
|
追加附件
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDocumentId(record.id);
|
setSelectedDocumentId(record.id);
|
||||||
|
setSelectedDocumentName(record.name);
|
||||||
|
setSelectedDocumentVersion(record.historyCount !== undefined && record.historyCount > 0 ? record.historyCount + 1 : null);
|
||||||
setShowTemplateUpload(true);
|
setShowTemplateUpload(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -1531,22 +1620,34 @@ export default function DocumentsIndex() {
|
|||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
{/* 历史版本行 */}
|
{/* 历史版本行 */}
|
||||||
{doc.isExpanded && doc.historyVersions && doc.historyVersions.length > 0 && (
|
{doc.isExpanded && (
|
||||||
<>
|
<>
|
||||||
{doc.historyVersions.map((historyDoc) => renderHistoryRow(historyDoc, doc))}
|
{/* 正在加载历史版本 */}
|
||||||
|
{loadingHistory.has(doc.id) ? (
|
||||||
|
<tr key={`loading-${doc.id}`} className="history-row">
|
||||||
|
<td colSpan={columns.length} className="px-4 py-3">
|
||||||
|
<div className="version-loading">
|
||||||
|
<i className="ri-loader-4-line"></i>
|
||||||
|
加载历史版本中...
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : doc.historyVersions && doc.historyVersions.length > 0 ? (
|
||||||
|
/* 显示历史版本数据 */
|
||||||
|
doc.historyVersions.map((historyDoc) => renderHistoryRow(historyDoc, doc))
|
||||||
|
) : (
|
||||||
|
/* 数据为空时的提示 */
|
||||||
|
<tr key={`empty-${doc.id}`} className="history-row">
|
||||||
|
<td colSpan={columns.length} className="px-4 py-3">
|
||||||
|
<div className="text-center text-gray-500 text-sm py-2">
|
||||||
|
<i className="ri-information-line mr-1"></i>
|
||||||
|
暂无历史版本数据
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* 正在加载历史版本 */}
|
|
||||||
{doc.isExpanded && loadingHistory.has(doc.id) && (
|
|
||||||
<tr key={`loading-${doc.id}`} className="history-row">
|
|
||||||
<td colSpan={columns.length} className="px-4 py-3">
|
|
||||||
<div className="version-loading">
|
|
||||||
<i className="ri-loader-4-line"></i>
|
|
||||||
加载历史版本中...
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -1573,6 +1674,8 @@ export default function DocumentsIndex() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAttachmentUpload(false);
|
setShowAttachmentUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null);
|
||||||
|
setSelectedDocumentVersion(null);
|
||||||
setAttachmentFiles([]);
|
setAttachmentFiles([]);
|
||||||
setAttachmentRemark("");
|
setAttachmentRemark("");
|
||||||
}}
|
}}
|
||||||
@@ -1587,6 +1690,8 @@ export default function DocumentsIndex() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAttachmentUpload(false);
|
setShowAttachmentUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null);
|
||||||
|
setSelectedDocumentVersion(null);
|
||||||
setAttachmentFiles([]);
|
setAttachmentFiles([]);
|
||||||
setAttachmentRemark("");
|
setAttachmentRemark("");
|
||||||
}}
|
}}
|
||||||
@@ -1600,10 +1705,18 @@ export default function DocumentsIndex() {
|
|||||||
{/* 文档信息 */}
|
{/* 文档信息 */}
|
||||||
<div className="bg-gray-50 p-3 rounded">
|
<div className="bg-gray-50 p-3 rounded">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
{/* 目标文档ID: <span className="font-medium">{selectedDocumentId}</span> */}
|
||||||
|
目标文档名称: <span className="font-medium">{selectedDocumentName}</span>
|
||||||
|
{selectedDocumentVersion && (
|
||||||
|
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||||
|
v{selectedDocumentVersion}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
支持.pdf、.docx、ZIP、RAR格式。ZIP/RAR内需要保证文件格式一致,否则报错
|
支持.pdf、.docx、ZIP、RAR格式。
|
||||||
|
<i className="ri-information-line mr-1"></i>
|
||||||
|
ZIP/RAR内需要保证文件格式一致,否则报错
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1624,7 +1737,7 @@ export default function DocumentsIndex() {
|
|||||||
<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 text-gray-400 mb-2 block"></i>
|
||||||
<p className="text-sm text-gray-600">点击选择文件或拖拽文件到此处</p>
|
<p className="text-sm text-gray-600">点击选择文件或拖拽文件到此处</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">支持PDF、Word、ZIP、RAR格式,可多选</p>
|
<p className="text-xs text-gray-500 mt-1">支持.pdf、.docx、.zip、.rar格式,可多选</p>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{attachmentFiles.length > 0 && (
|
{attachmentFiles.length > 0 && (
|
||||||
@@ -1650,7 +1763,7 @@ export default function DocumentsIndex() {
|
|||||||
合并模式
|
合并模式
|
||||||
</label>
|
</label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center">
|
{/* <label className="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="mergeMode"
|
name="mergeMode"
|
||||||
@@ -1660,7 +1773,7 @@ export default function DocumentsIndex() {
|
|||||||
className="mr-2"
|
className="mr-2"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">覆盖原文档(推荐)</span>
|
<span className="text-sm">覆盖原文档(推荐)</span>
|
||||||
</label>
|
</label> */}
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -1696,6 +1809,8 @@ export default function DocumentsIndex() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAttachmentUpload(false);
|
setShowAttachmentUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null);
|
||||||
|
setSelectedDocumentVersion(null);
|
||||||
setAttachmentFiles([]);
|
setAttachmentFiles([]);
|
||||||
setAttachmentRemark("");
|
setAttachmentRemark("");
|
||||||
}}
|
}}
|
||||||
@@ -1723,11 +1838,13 @@ export default function DocumentsIndex() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowTemplateUpload(false);
|
setShowTemplateUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null);
|
||||||
|
setSelectedDocumentVersion(null);
|
||||||
setTemplateFile(null);
|
setTemplateFile(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="bg-white rounded-lg p-6 w-full max-w-lg mx-4"
|
className="bg-white rounded-lg p-6 w-full max-w-xl mx-4"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
@@ -1736,6 +1853,8 @@ export default function DocumentsIndex() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowTemplateUpload(false);
|
setShowTemplateUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null);
|
||||||
|
setSelectedDocumentVersion(null);
|
||||||
setTemplateFile(null);
|
setTemplateFile(null);
|
||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-gray-400 hover:text-gray-600"
|
||||||
@@ -1748,7 +1867,13 @@ export default function DocumentsIndex() {
|
|||||||
{/* 文档信息 */}
|
{/* 文档信息 */}
|
||||||
<div className="bg-gray-50 p-3 rounded">
|
<div className="bg-gray-50 p-3 rounded">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
{/* 目标文档ID: <span className="font-medium">{selectedDocumentId}</span> */}
|
||||||
|
目标文档名称: <span className="font-medium">{selectedDocumentName}</span>
|
||||||
|
{selectedDocumentVersion && (
|
||||||
|
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||||
|
v{selectedDocumentVersion}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
支持.pdf、.docx格式,用于与合同文档进行结构对比
|
支持.pdf、.docx格式,用于与合同文档进行结构对比
|
||||||
@@ -1794,6 +1919,8 @@ export default function DocumentsIndex() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowTemplateUpload(false);
|
setShowTemplateUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null);
|
||||||
|
setSelectedDocumentVersion(null);
|
||||||
setTemplateFile(null);
|
setTemplateFile(null);
|
||||||
}}
|
}}
|
||||||
disabled={templateUploading}
|
disabled={templateUploading}
|
||||||
|
|||||||
+28
-14
@@ -336,8 +336,9 @@ export default function FilesUpload() {
|
|||||||
// 附件追加状态
|
// 附件追加状态
|
||||||
const [showAttachmentUpload, setShowAttachmentUpload] = useState<boolean>(false);
|
const [showAttachmentUpload, setShowAttachmentUpload] = useState<boolean>(false);
|
||||||
const [selectedDocumentId, setSelectedDocumentId] = useState<number | null>(null);
|
const [selectedDocumentId, setSelectedDocumentId] = useState<number | null>(null);
|
||||||
|
const [selectedDocumentName, setSelectedDocumentName] = useState<string | null>(null);
|
||||||
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
||||||
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('overwrite');
|
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('new');
|
||||||
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
||||||
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
||||||
|
|
||||||
@@ -887,7 +888,7 @@ export default function FilesUpload() {
|
|||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
// 检查主文件类型
|
// 检查主文件类型
|
||||||
const selectedDocument = queueFiles.find(doc => doc.id === selectedDocumentId);
|
const selectedDocument = queueFiles.find(doc => doc.id === selectedDocumentId);
|
||||||
const isMainFileDocx = selectedDocument?.path.toLowerCase().endsWith('.docx');
|
const isMainFileDocx = selectedDocument?.path?.toLowerCase().endsWith('.docx');
|
||||||
|
|
||||||
// 验证文件类型,支持PDF、Word、ZIP、RAR
|
// 验证文件类型,支持PDF、Word、ZIP、RAR
|
||||||
const validFiles: File[] = [];
|
const validFiles: File[] = [];
|
||||||
@@ -899,7 +900,7 @@ export default function FilesUpload() {
|
|||||||
const isPdf = file.type === 'application/pdf' || fileName.endsWith('.pdf');
|
const isPdf = file.type === 'application/pdf' || fileName.endsWith('.pdf');
|
||||||
const isValidType =
|
const isValidType =
|
||||||
isPdf ||
|
isPdf ||
|
||||||
file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
// file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||||||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
|
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
|
||||||
file.type === 'application/zip' || fileName.endsWith('.zip') ||
|
file.type === 'application/zip' || fileName.endsWith('.zip') ||
|
||||||
file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar');
|
file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar');
|
||||||
@@ -973,6 +974,7 @@ export default function FilesUpload() {
|
|||||||
setAttachmentRemark("");
|
setAttachmentRemark("");
|
||||||
setShowAttachmentUpload(false);
|
setShowAttachmentUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null)
|
||||||
|
|
||||||
// 刷新文档列表
|
// 刷新文档列表
|
||||||
await filterDocuments(documentTypeIds);
|
await filterDocuments(documentTypeIds);
|
||||||
@@ -1042,6 +1044,7 @@ export default function FilesUpload() {
|
|||||||
setTemplateFile(null);
|
setTemplateFile(null);
|
||||||
setShowTemplateUpload(false);
|
setShowTemplateUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null)
|
||||||
|
|
||||||
// 刷新文档列表
|
// 刷新文档列表
|
||||||
await filterDocuments(documentTypeIds);
|
await filterDocuments(documentTypeIds);
|
||||||
@@ -2114,6 +2117,7 @@ export default function FilesUpload() {
|
|||||||
icon="ri-attachment-line"
|
icon="ri-attachment-line"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDocumentId(record.id);
|
setSelectedDocumentId(record.id);
|
||||||
|
setSelectedDocumentName(record.name)
|
||||||
setShowAttachmentUpload(true);
|
setShowAttachmentUpload(true);
|
||||||
}}
|
}}
|
||||||
className="text-xs px-2 py-1 h-7"
|
className="text-xs px-2 py-1 h-7"
|
||||||
@@ -2126,6 +2130,7 @@ export default function FilesUpload() {
|
|||||||
icon="ri-file-copy-line"
|
icon="ri-file-copy-line"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedDocumentId(record.id);
|
setSelectedDocumentId(record.id);
|
||||||
|
setSelectedDocumentName(record.name)
|
||||||
setShowTemplateUpload(true);
|
setShowTemplateUpload(true);
|
||||||
}}
|
}}
|
||||||
className="text-xs px-2 py-1 h-7"
|
className="text-xs px-2 py-1 h-7"
|
||||||
@@ -2290,7 +2295,7 @@ export default function FilesUpload() {
|
|||||||
ref={contractMainFileRef}
|
ref={contractMainFileRef}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
accept=".pdf,.docx"
|
accept=".pdf,.docx"
|
||||||
tipText="请上传合同主文件,格式:PDF/Word"
|
tipText="请上传合同主文件,格式:.pdf/.docx"
|
||||||
mainText="上传合同主文件"
|
mainText="上传合同主文件"
|
||||||
buttonText="选择主文件"
|
buttonText="选择主文件"
|
||||||
icon="ri-file-text-line"
|
icon="ri-file-text-line"
|
||||||
@@ -2327,7 +2332,7 @@ export default function FilesUpload() {
|
|||||||
ref={contractAttachmentFileRef}
|
ref={contractAttachmentFileRef}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
accept=".pdf,.docx"
|
accept=".pdf,.docx"
|
||||||
tipText="请上传合同附件,格式:PDF/Word"
|
tipText="请上传合同附件,格式:.pdf/.docx"
|
||||||
mainText="上传合同附件"
|
mainText="上传合同附件"
|
||||||
buttonText="选择附件"
|
buttonText="选择附件"
|
||||||
icon="ri-file-copy-line"
|
icon="ri-file-copy-line"
|
||||||
@@ -2362,7 +2367,7 @@ export default function FilesUpload() {
|
|||||||
onFilesSelected={handleContractTemplateFilesSelected}
|
onFilesSelected={handleContractTemplateFilesSelected}
|
||||||
multiple={false}
|
multiple={false}
|
||||||
accept=".pdf,.docx"
|
accept=".pdf,.docx"
|
||||||
tipText="请上传合同模板,格式:PDF/Word"
|
tipText="请上传合同模板,格式:.pdf/.docx"
|
||||||
mainText="上传合同模板"
|
mainText="上传合同模板"
|
||||||
buttonText="选择模板"
|
buttonText="选择模板"
|
||||||
icon="ri-file-copy-line"
|
icon="ri-file-copy-line"
|
||||||
@@ -2600,11 +2605,13 @@ export default function FilesUpload() {
|
|||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowAttachmentUpload(false);
|
setShowAttachmentUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null)
|
||||||
setAttachmentFiles([]);
|
setAttachmentFiles([]);
|
||||||
setAttachmentRemark("");
|
setAttachmentRemark("");
|
||||||
setAttachmentMergeMode('overwrite');
|
setAttachmentMergeMode('overwrite');
|
||||||
}
|
}
|
||||||
}}
|
}
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
<div className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
@@ -2613,6 +2620,7 @@ export default function FilesUpload() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAttachmentUpload(false);
|
setShowAttachmentUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null)
|
||||||
setAttachmentFiles([]);
|
setAttachmentFiles([]);
|
||||||
setAttachmentRemark("");
|
setAttachmentRemark("");
|
||||||
}}
|
}}
|
||||||
@@ -2626,10 +2634,11 @@ export default function FilesUpload() {
|
|||||||
{/* 文档信息 */}
|
{/* 文档信息 */}
|
||||||
<div className="bg-gray-50 p-3 rounded">
|
<div className="bg-gray-50 p-3 rounded">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
{/* 目标文档ID: <span className="font-medium">{selectedDocumentId}</span> */}
|
||||||
|
目标文档名称: <span className="font-medium">{selectedDocumentName}</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
支持PDF、Word、ZIP、RAR格式,ZIP/RAR内仅合并其中的PDF文件
|
支持.pdf、.docx、.zip、.rar格式。<i className="ri-information-2-line mr-1"></i>ZIP/RAR内需要保证文件格式一致,否则报错
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -2642,7 +2651,7 @@ export default function FilesUpload() {
|
|||||||
onFilesSelected={handleAttachmentFilesSelected}
|
onFilesSelected={handleAttachmentFilesSelected}
|
||||||
multiple={true}
|
multiple={true}
|
||||||
accept=".pdf,.docx,.zip,.rar"
|
accept=".pdf,.docx,.zip,.rar"
|
||||||
tipText="支持PDF、Word、ZIP、RAR格式,可多选"
|
tipText="支持.pdf、.docx、.zip、.rar格式,可多选"
|
||||||
mainText="选择附件文件"
|
mainText="选择附件文件"
|
||||||
buttonText="选择文件"
|
buttonText="选择文件"
|
||||||
icon="ri-attachment-line"
|
icon="ri-attachment-line"
|
||||||
@@ -2652,7 +2661,7 @@ export default function FilesUpload() {
|
|||||||
<p className="text-sm text-green-600 mb-2">
|
<p className="text-sm text-green-600 mb-2">
|
||||||
<i className="ri-checkbox-circle-line"></i> 已选择 {attachmentFiles.length} 个文件
|
<i className="ri-checkbox-circle-line"></i> 已选择 {attachmentFiles.length} 个文件
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||||
{attachmentFiles.map((file, index) => (
|
{attachmentFiles.map((file, index) => (
|
||||||
<div key={index} className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
<div key={index} className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
||||||
<i className="ri-file-line mr-1"></i>
|
<i className="ri-file-line mr-1"></i>
|
||||||
@@ -2670,7 +2679,7 @@ export default function FilesUpload() {
|
|||||||
合并模式
|
合并模式
|
||||||
</label>
|
</label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="flex items-center">
|
{/* <label className="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name="mergeMode"
|
name="mergeMode"
|
||||||
@@ -2680,7 +2689,7 @@ export default function FilesUpload() {
|
|||||||
className="mr-2"
|
className="mr-2"
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">覆盖原文档(推荐)</span>
|
<span className="text-sm">覆盖原文档(推荐)</span>
|
||||||
</label>
|
</label> */}
|
||||||
<label className="flex items-center">
|
<label className="flex items-center">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -2716,6 +2725,7 @@ export default function FilesUpload() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowAttachmentUpload(false);
|
setShowAttachmentUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null)
|
||||||
setAttachmentFiles([]);
|
setAttachmentFiles([]);
|
||||||
setAttachmentRemark("");
|
setAttachmentRemark("");
|
||||||
}}
|
}}
|
||||||
@@ -2746,6 +2756,7 @@ export default function FilesUpload() {
|
|||||||
if (e.target === e.currentTarget) {
|
if (e.target === e.currentTarget) {
|
||||||
setShowTemplateUpload(false);
|
setShowTemplateUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null)
|
||||||
setTemplateFile(null);
|
setTemplateFile(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -2757,6 +2768,7 @@ export default function FilesUpload() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowTemplateUpload(false);
|
setShowTemplateUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null)
|
||||||
setTemplateFile(null);
|
setTemplateFile(null);
|
||||||
}}
|
}}
|
||||||
className="text-gray-400 hover:text-gray-600"
|
className="text-gray-400 hover:text-gray-600"
|
||||||
@@ -2769,7 +2781,8 @@ export default function FilesUpload() {
|
|||||||
{/* 文档信息 */}
|
{/* 文档信息 */}
|
||||||
<div className="bg-gray-50 p-3 rounded">
|
<div className="bg-gray-50 p-3 rounded">
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
目标文档ID: <span className="font-medium">{selectedDocumentId}</span>
|
{/* 目标文档ID: <span className="font-medium">{selectedDocumentId}</span> */}
|
||||||
|
目标文档名称: <span className="font-medium">{selectedDocumentName}</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
支持.pdf、.docx格式,用于与合同文档进行结构对比
|
支持.pdf、.docx格式,用于与合同文档进行结构对比
|
||||||
@@ -2810,6 +2823,7 @@ export default function FilesUpload() {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowTemplateUpload(false);
|
setShowTemplateUpload(false);
|
||||||
setSelectedDocumentId(null);
|
setSelectedDocumentId(null);
|
||||||
|
setSelectedDocumentName(null)
|
||||||
setTemplateFile(null);
|
setTemplateFile(null);
|
||||||
}}
|
}}
|
||||||
disabled={templateUploading}
|
disabled={templateUploading}
|
||||||
|
|||||||
@@ -157,17 +157,17 @@ export default function PromptsIndex() {
|
|||||||
const canViewTemplate = canView('prompt_template');
|
const canViewTemplate = canView('prompt_template');
|
||||||
|
|
||||||
// 调试信息
|
// 调试信息
|
||||||
// useEffect(() => {
|
useEffect(() => {
|
||||||
// console.log('📋 [Prompts] 模板数据:', templates);
|
console.log('📋 [Prompts] 模板数据:', templates);
|
||||||
// console.log('📋 [Prompts] 用户角色:', userRole);
|
// console.log('📋 [Prompts] 用户角色:', userRole);
|
||||||
// console.log('📋 [Prompts] 权限列表:', permissions);
|
// console.log('📋 [Prompts] 权限列表:', permissions);
|
||||||
// console.log('📋 [Prompts] 权限检查结果:', {
|
// console.log('📋 [Prompts] 权限检查结果:', {
|
||||||
// canCreate: canCreateTemplate,
|
// canCreate: canCreateTemplate,
|
||||||
// canEdit: canEditTemplate,
|
// canEdit: canEditTemplate,
|
||||||
// canDelete: canDeleteTemplate,
|
// canDelete: canDeleteTemplate,
|
||||||
// canView: canViewTemplate
|
// canView: canViewTemplate
|
||||||
// });
|
// });
|
||||||
// }, [userRole, permissions, templates, canCreateTemplate, canEditTemplate, canDeleteTemplate, canViewTemplate]);
|
}, [userRole, permissions, templates, canCreateTemplate, canEditTemplate, canDeleteTemplate, canViewTemplate]);
|
||||||
|
|
||||||
// 处理搜索名称
|
// 处理搜索名称
|
||||||
const handleNameSearch = (value: string) => {
|
const handleNameSearch = (value: string) => {
|
||||||
|
|||||||
@@ -1361,9 +1361,18 @@ export default function RolePermissions() {
|
|||||||
marginLeft: '8px',
|
marginLeft: '8px',
|
||||||
padding: '2px 8px',
|
padding: '2px 8px',
|
||||||
fontSize: '12px',
|
fontSize: '12px',
|
||||||
backgroundColor: selectedPermCount > 0 ? '#e6f7ed' : '#f5f5f5',
|
backgroundColor:
|
||||||
color: selectedPermCount > 0 ? '#52c41a' : '#666',
|
selectedPermCount === permissions.length ? '#e6f7ed' : // 全部选中:绿色
|
||||||
border: selectedPermCount > 0 ? '1px solid #b7eb8f' : '1px solid #d9d9d9',
|
selectedPermCount > 0 ? '#fff7e6' : // 部分选中:浅橙色
|
||||||
|
'#f5f5f5', // 未选中:灰色
|
||||||
|
color:
|
||||||
|
selectedPermCount === permissions.length ? '#52c41a' : // 全部选中:绿色
|
||||||
|
selectedPermCount > 0 ? '#fa8c16' : // 部分选中:橙色
|
||||||
|
'#666', // 未选中:灰色
|
||||||
|
border:
|
||||||
|
selectedPermCount === permissions.length ? '1px solid #b7eb8f' : // 全部选中:绿色
|
||||||
|
selectedPermCount > 0 ? '1px solid #ffd591' : // 部分选中:浅橙色
|
||||||
|
'1px solid #d9d9d9', // 未选中:灰色
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
display: 'inline-flex',
|
display: 'inline-flex',
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ export class WopiService {
|
|||||||
throw new Error(`保存文件失败: ${sanitizedFileId}`);
|
throw new Error(`保存文件失败: ${sanitizedFileId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`PutFile 成功: ${sanitizedFileId}, Size: ${fileBuffer.byteLength} bytes`);
|
// console.log(`PutFile 成功: ${sanitizedFileId}, Size: ${fileBuffer.byteLength} bytes`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PutFile 失败:', error);
|
console.error('PutFile 失败:', error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
/* 文件预览样式隔离 */
|
/* 文件预览样式隔离 */
|
||||||
.file-preview-isolation {
|
.file-preview-isolation {
|
||||||
all: unset !important;
|
all: unset !important;
|
||||||
display: block !important;
|
display: flex !important;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important;
|
flex-direction: column !important;
|
||||||
|
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; */
|
||||||
font-size: 14px !important;
|
font-size: 14px !important;
|
||||||
line-height: 1.5 !important;
|
line-height: 1.5 !important;
|
||||||
color: #333 !important;
|
color: #333 !important;
|
||||||
background: #fff !important;
|
background: #fff !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
min-height: 600px !important;
|
height: 800px !important;
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
isolation: isolate !important;
|
isolation: isolate !important;
|
||||||
contain: layout style !important;
|
contain: layout style !important;
|
||||||
@@ -94,9 +95,22 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.file-preview-isolation .file-preview-content {
|
.file-preview-isolation .file-preview-content {
|
||||||
max-height: calc(100vh - 150px) !important;
|
flex: 1 !important;
|
||||||
overflow: auto !important;
|
overflow: auto !important;
|
||||||
background: #f8f9fa !important;
|
background: #f8f9fa !important;
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 为了保持内部容器也能填充高度 */
|
||||||
|
.file-preview-isolation .file-preview {
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-preview-isolation .pdf-interactive-container {
|
||||||
|
flex: 1 !important;
|
||||||
|
min-height: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
/* 角色权限管理页面样式 */
|
/* 角色权限管理页面样式 */
|
||||||
|
|
||||||
.role-permissions-page {
|
.role-permissions-page {
|
||||||
padding: 20px;
|
/* padding: 20px; */
|
||||||
background: #f5f7fa;
|
padding:0 1rem;
|
||||||
|
/* background: #f5f7fa; */
|
||||||
min-height: calc(100vh - 60px);
|
min-height: calc(100vh - 60px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
.role-permissions-page .page-title {
|
.role-permissions-page .page-title {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #303133;
|
/* color: #303133; */
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
@@ -44,8 +45,8 @@
|
|||||||
|
|
||||||
/* 左侧角色面板 */
|
/* 左侧角色面板 */
|
||||||
.roles-panel {
|
.roles-panel {
|
||||||
height: calc(100vh - 140px);
|
height: calc(100vh - 100px);
|
||||||
overflow: hidden;
|
overflow: auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
@@ -173,11 +174,14 @@
|
|||||||
|
|
||||||
/* 右侧详情面板 */
|
/* 右侧详情面板 */
|
||||||
.permissions-detail {
|
.permissions-detail {
|
||||||
min-height: calc(100vh - 140px);
|
height: calc(100vh - 100px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tab 样式 */
|
/* Tab 样式 */
|
||||||
.tabs-card {
|
.tabs-card {
|
||||||
|
height: calc(100vh - 100px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,21 +224,28 @@
|
|||||||
|
|
||||||
.tabs-content {
|
.tabs-content {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
min-height: 500px;
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 170px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 权限Tab */
|
/* 权限Tab */
|
||||||
.permissions-tab {
|
.permissions-tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 10px;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.permissions-header {
|
.permissions-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-bottom: 16px;
|
padding-bottom: 6px;
|
||||||
border-bottom: 1px solid #e4e7ed;
|
border-bottom: 1px solid #e4e7ed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,16 +258,18 @@
|
|||||||
|
|
||||||
/* 路由树 */
|
/* 路由树 */
|
||||||
.routes-tree {
|
.routes-tree {
|
||||||
max-height: 600px;
|
/* max-height: calc(100vh - 240px); */
|
||||||
|
/* height: 400px; */
|
||||||
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
border: 1px solid #e4e7ed;
|
border: 1px solid #e4e7ed;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 16px;
|
padding: 10px;
|
||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.route-item {
|
.route-item {
|
||||||
margin-bottom: 8px;
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.route-item-content {
|
.route-item-content {
|
||||||
@@ -334,7 +347,9 @@
|
|||||||
.users-tab {
|
.users-tab {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 20px;
|
gap: 10px;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.users-header {
|
.users-header {
|
||||||
@@ -356,7 +371,6 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
max-height: 600px;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+2386
-683
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,7 @@
|
|||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"diff": "^7.0.0",
|
"diff": "^7.0.0",
|
||||||
"docx-preview": "^0.3.5",
|
"docx-preview": "^0.3.5",
|
||||||
|
"docxtemplater": "^3.67.5",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
"html-docx-js": "^0.3.1",
|
"html-docx-js": "^0.3.1",
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
"monaco-editor": "^0.55.1",
|
"monaco-editor": "^0.55.1",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
"pg": "^8.14.1",
|
"pg": "^8.14.1",
|
||||||
|
"pizzip": "^3.2.0",
|
||||||
"pm2": "^6.0.8",
|
"pm2": "^6.0.8",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user