import type { MetaFunction, LoaderFunctionArgs, ActionFunctionArgs } from '@remix-run/node'; import { redirect } from '@remix-run/node'; import { useLoaderData, useNavigate, useSubmit, useActionData } from '@remix-run/react'; import { useState, useEffect } from 'react'; import { getContractTemplate } from '~/api/contract-template/templates'; import type { ContractTemplate } from '~/api/contract-template/templates'; import styles from '~/styles/pages/contract-template.css?url'; import filePreviewStyles from '~/styles/components/file-preview-isolation.css?url'; import { getUserSession } from '~/api/login/auth.server'; import { createDraftContract } from '~/api/contracts/draft-service.server'; import { apiRequest, downloadFile } from '~/api/axios-client'; import { checkRoutePermission } from '~/api/auth/check-route-permission.server'; // 导入FilePreview组件 import { FilePreview } from '~/components/reviews'; import { toastService } from '~/components/ui/Toast'; export const links = () => [ { rel: 'stylesheet', href: styles }, { rel: 'stylesheet', href: filePreviewStyles }, ]; export const meta: MetaFunction = ({ data }) => { return [ { title: `${data?.template.title || '模板详情'} - 合同管理 - 智慧法务` }, { name: 'description', content: data?.template.description || '查看合同管理模块中的模板详细信息' } ]; }; // 面包屑导航配置 export const handle = { breadcrumb: (data: { template: { title: string } }) => { return data?.template?.title || "模板详情"; } }; export async function loader({ params, request }: LoaderFunctionArgs) { const templateId = params.id!; // 获取 JWT const { frontendJWT } = await getUserSession(request); const jwt = frontendJWT || undefined; try { const response = await getContractTemplate(templateId, jwt); if (response.error) { throw new Response(response.error, { status: response.status || 404 }); } if (!response.data) { throw new Response('模板未找到', { status: 404 }); } // 添加调试信息 // console.log('模板详情数据:', response.data); // console.log('分类信息:', response.data.category); return { template: response.data }; } catch (error) { console.error('加载模板详情失败:', error); throw new Response('模板未找到', { status: 404 }); } } /** * Action 函数:处理起草合同请求 */ export async function action({ request, params }: ActionFunctionArgs) { const templateId = params.id!; if (!templateId) { return Response.json({ error: '模板ID无效' }, { status: 400 }); } // 获取用户信息和JWT const { userInfo, frontendJWT, userRole } = await getUserSession(request); if (!userInfo?.sub) { return Response.json({ error: '未登录' }, { status: 401 }); } // 🔒 在执行任何操作之前,先检查用户是否有权限访问目标路由 const targetPath = '/contract-draft'; const permissionCheck = await checkRoutePermission(targetPath, userRole, frontendJWT || undefined); if (!permissionCheck.allowed) { console.warn(`[Action] 用户无权访问 ${targetPath}:`, permissionCheck.error); return Response.json({ error: permissionCheck.error || '您没有权限使用起草合同功能' }, { status: 403 }); } try { // 解析表单数据 const formData = await request.formData(); const title = formData.get('title') as string; const originalFilePath = formData.get('originalFilePath') as string; if (!title) { return Response.json({ error: '标题不能为空' }, { status: 400 }); } if (!originalFilePath) { return Response.json({ error: '文件路径不存在' }, { status: 400 }); } // 生成新文件路径 const area = userInfo.area || 'unknown'; const timestamp = Date.now(); const uuid = crypto.randomUUID(); // 提取文件目录和文件名 const lastSlashIndex = originalFilePath.lastIndexOf('/'); const directory = lastSlashIndex >= 0 ? originalFilePath.substring(0, lastSlashIndex) : ''; const fileName = lastSlashIndex >= 0 ? originalFilePath.substring(lastSlashIndex + 1) : originalFilePath; // 提取文件扩展名 const lastDotIndex = fileName.lastIndexOf('.'); const baseName = lastDotIndex >= 0 ? fileName.substring(0, lastDotIndex) : fileName; const extension = lastDotIndex >= 0 ? fileName.substring(lastDotIndex) : ''; // 构建新文件名 const newFileName = `${baseName}_${area}_${timestamp}_${uuid}${extension}`; const newFilePath = directory ? `${directory}/${newFileName}` : newFileName; console.log('[Draft] 复制文件:', { originalFilePath, newFilePath }); // 调用 MinIO 复制文件 API(需要传递 JWT) const jwt = frontendJWT || undefined; const copyResponse = await apiRequest('/api/v2/storage/files/copy', { method: 'POST', data: { source_path: originalFilePath, destination_path: newFilePath }, headers: { 'Authorization': jwt ? `Bearer ${jwt}` : '' } }); if (copyResponse.error) { console.error('[Draft] 文件复制失败:', copyResponse.error); return Response.json({ error: `文件复制失败: ${copyResponse.error}` }, { status: 500 }); } console.log('[Draft] 文件复制成功:'); // console.log('[Draft] 文件复制成功:', copyResponse.data); // 重定向到草稿编辑页面,通过 URL 参数传递文件路径和模板 ID const draftUrl = `/contract-draft/1?filePath=${encodeURIComponent(newFilePath)}&templateId=${templateId}&title=${encodeURIComponent(title)}`; return redirect(draftUrl); } catch (error) { console.error('[Template Detail] 创建草稿失败:', error); return Response.json( { error: error instanceof Error ? error.message : '创建草稿失败' }, { status: 500 } ); } } // Action 返回的数据类型 interface ActionData { error?: string; } export default function ContractTemplateDetail() { const { template }: { template: ContractTemplate } = useLoaderData(); const actionData = useActionData(); const navigate = useNavigate(); const submit = useSubmit(); const [isCreatingDraft, setIsCreatingDraft] = useState(false); // 注释掉收藏功能 // const [isFavorited, setIsFavorited] = useState(false); // 防止页面加载时自动滚动到预览区域(由 Collabora iframe 的 tabIndex 导致) useEffect(() => { // 页面加载后立即滚动回顶部 window.scrollTo({ top: 0, behavior: 'instant' }); }, []); // 处理 action 返回的错误 useEffect(() => { if (actionData?.error) { toastService.error(actionData.error); setIsCreatingDraft(false); } }, [actionData]); const handleBack = () => { navigate('/contract-template/list'); }; // 使用统一的下载方法(与 rules-files.tsx 相同) const handleDownload = async () => { if (!template.file_path) { toastService.error('文件路径不存在,无法下载'); return; } try { // 使用axios封装的下载方法 const blob = await downloadFile(template.file_path); // 创建Blob URL const blobUrl = URL.createObjectURL(blob); // 清理文件名,移除可能导致问题的字符 const fileExtension = template.file_format || 'docx'; const fileName = `${template.title}.${fileExtension}`; const cleanFileName = fileName.replace(/[<>:"/\\|?*]/g, '_'); // 创建一个隐藏的a标签并点击它 const a = document.createElement('a'); a.style.display = 'none'; a.href = blobUrl; a.download = cleanFileName; document.body.appendChild(a); a.click(); // 清理 setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); }, 100); } catch (error) { console.error('下载文件失败:', error); toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; const handlePreview = () => { // console.log('预览模板:', template.id); // 页面内预览,滚动到预览区域 const previewElement = document.getElementById('template-preview'); if (previewElement) { previewElement.scrollIntoView({ behavior: 'smooth' }); } }; // 起草合同 const handleStartDraft = () => { if (isCreatingDraft) return; if (!template.file_path) { toastService.error('模板文件路径不存在,无法起草'); return; } // 生成默认标题 const defaultTitle = `${template.title}-${new Date().toLocaleDateString('zh-CN').replace(/\//g, '')}`; setIsCreatingDraft(true); // 使用 Remix 的 submit 提交表单 const formData = new FormData(); formData.append('title', defaultTitle); formData.append('originalFilePath', template.file_path); submit(formData, { method: 'post' }); }; /* const handleFavorite = () => { setIsFavorited(!isFavorited); console.log('收藏状态:', !isFavorited); }; */ /* const handleShare = () => { // 复制当前页面URL到剪贴板 navigator.clipboard.writeText(window.location.href).then(() => { alert('链接已复制到剪贴板'); }).catch(() => { alert('复制失败,请手动复制链接'); }); }; */ // 注释掉评分相关功能 /* const renderStars = (rating: number) => { const stars = []; const fullStars = Math.floor(rating); for (let i = 0; i < 5; i++) { if (i < fullStars) { stars.push(); } else { stars.push(); } } return stars; }; */ // 创建文件内容对象用于FilePreview组件 // 优先使用原始文件路径(支持docx),如果没有则使用pdf_file_path const previewPath = template.file_path || template.pdf_file_path; const fileContent = previewPath ? { title: template.title, contractNumber: template.template_code, // 使用file_path以支持多种格式(docx/pdf) path: previewPath, parties: { partyA: { name: '', address: '', representative: '', phone: '' }, partyB: { name: '', address: '', representative: '', phone: '' } }, sections: [] } : null; return (
{/* 返回按钮 */}
{/* 模板详情 */}
{/* 详情头部 */}
{/* 注释掉类型和评分显示 */} {/*
{template.type}
{renderStars(template.rating)} {template.rating} (156评价)
*/}

{template.title}

模板编号: {template.template_code}
更新时间: {new Date(template.updated_at).toLocaleDateString('zh-CN')}
所属分类: {template.category?.name || '其他'}
文件格式: {template.file_format?.toUpperCase()}
{template.is_featured && (
特色推荐:
)} {/* 注释掉使用次数、文件大小、适用范围、法律依据等字段 */} {/*
使用次数: {template.useCount.toLocaleString()}次
文件大小: {template.fileSize}
适用范围: {template.scope}
法律依据: {template.legalBasis}
*/}
{template.pdf_file_path && ( )} {/* 注释掉收藏功能 */} {/* */} {/* */}
{/* 详情内容 */}
{/* 模板简介 */}

模板简介

{template.description || '该合同模板为标准格式,包含完整的合同条款结构,适用于相关业务场景的合同签署。'} {template.category?.description && ( <>

适用范围:{template.category.description} )}

{/* 注释掉主要特点模块 */} {/*

主要特点

{template.features.map((feature, index) => (
{feature.title}

{feature.description}

))}
*/} {/* 注释掉合同条款结构模块 */} {/*

合同条款结构

{template.structure.map((item) => (
{item.step}
{item.title}
{item.description}
))}
*/} {/* 合同预览 - 只有当存在pdf_file_path时才显示 */} {fileContent && (

合同预览

)} {/* 注释掉用户评价模块 */} {/*

用户评价

{template.reviews.map((review, index) => (
{review.user[0]}
{review.user}
{renderStars(review.rating)}
{review.date}

{review.comment}

))}
*/}
); }