/** * 合同起草页面 * 路由: /contract-draft/:draftId */ import type { LoaderFunctionArgs, MetaFunction, ActionFunctionArgs } from '@remix-run/node'; import { redirect } from '@remix-run/node'; import { useLoaderData, useNavigate, useFetcher } from '@remix-run/react'; import { useState, useRef, useEffect } from 'react'; import { FilePreview, type FilePreviewHandle } from '~/components/reviews/FilePreview'; import { PlaceholderForm } from '~/components/contracts/PlaceholderForm'; import { getDraftById, deleteDraft } from '~/api/contracts/draft-service.server'; import { getUserSession } from '~/api/login/auth.server'; import { toastService } from '~/components/ui/Toast'; import { downloadFile } from '~/api/axios-client'; import { extractPlaceholdersFromDocx, generateDefaultSchema } from '~/api/contracts/docx-parser.server'; import path from 'path'; import type { DraftedContract, PlaceholderSchema } from '~/types/contract-draft'; import type { ContractTemplate } from '~/api/contract-template/templates'; export const meta: MetaFunction = ({ data }) => { return [ { title: `${data?.draft.title || '合同起草'} - 智慧法务` }, { name: 'description', content: '起草合同文档' } ]; }; interface LoaderData { draft: DraftedContract; template: ContractTemplate; } interface ActionData { success?: boolean; message?: string; error?: string; } export async function loader({ params, request }: LoaderFunctionArgs) { const draftId = parseInt(params.draftId || '0'); if (!draftId) { throw new Response('草稿ID无效', { status: 400 }); } // 获取用户信息和JWT const { userInfo, frontendJWT } = await getUserSession(request); if (!userInfo?.sub) { throw new Response('未登录', { status: 401 }); } const jwt = frontendJWT || undefined; // 【临时测试】使用测试文档和模拟数据 // const testDocPath = path.join(process.cwd(), 'public', 'testWork', '买卖合同 (1).docx'); const testDocPath = 'contract-template/买卖/买卖合同范本.docx'; // 创建临时的草稿对象(用于测试) const draft: DraftedContract = { id: draftId, template_id: 1, file_path: testDocPath, title: '买卖合同-测试草稿', placeholder_values: {}, status: 'draft', created_by: parseInt(userInfo.sub), created_at: new Date().toISOString(), updated_at: new Date().toISOString() }; // 从 docx 文件中提取占位符 let placeholderSchema: PlaceholderSchema | null = null; try { console.log('[Loader] 使用测试文档:', testDocPath); // 提取占位符 const placeholders = await extractPlaceholdersFromDocx(testDocPath); console.log('[Loader] 提取到的占位符:', placeholders); // 生成默认 schema placeholderSchema = generateDefaultSchema(placeholders); // console.log('[Loader] 生成的 schema:', JSON.stringify(placeholderSchema, null, 2)); } catch (error) { console.error('[Loader] 提取占位符失败:', error); placeholderSchema = null; } // 创建临时的模板对象(用于测试) const template: ContractTemplate = { id: 1, title: '买卖合同模板', template_code: 'TEST-001', category_id: 1, file_path: testDocPath, file_format: 'docx', description: '测试用买卖合同模板', is_featured: false, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), placeholder_schema: placeholderSchema as any }; return Response.json({ draft, template }); } /** * Action 函数:处理删除草稿 */ export async function action({ request, params }: ActionFunctionArgs) { const draftId = parseInt(params.draftId || '0'); if (!draftId) { return Response.json({ error: '草稿ID无效' }, { status: 400 }); } // 获取用户信息和JWT const { userInfo, frontendJWT } = await getUserSession(request); if (!userInfo?.sub) { return Response.json({ error: '未登录' }, { status: 401 }); } const userId = parseInt(userInfo.sub); const jwt = frontendJWT || undefined; try { // 解析表单数据 const formData = await request.formData(); const actionType = formData.get('_action') as string; if (actionType === 'delete') { // 删除草稿记录 await deleteDraft(draftId, userId, jwt); return Response.json({ success: true, message: '草稿已删除' }); } return Response.json({ error: '无效的操作类型' }, { status: 400 }); } catch (error) { console.error('[Action] 操作失败:', error); return Response.json( { error: error instanceof Error ? error.message : '操作失败' }, { status: 500 } ); } } export default function ContractDraftPage() { const { draft, template } = useLoaderData(); const navigate = useNavigate(); const fetcher = useFetcher(); const [placeholderValues, setPlaceholderValues] = useState>( draft.placeholder_values || {} ); const [isReplacing, setIsReplacing] = useState(false); const [highlightValue, setHighlightValue] = useState(undefined); const [aiSuggestionReplace, setAiSuggestionReplace] = useState<{ searchText: string; replaceText: string; pageNumber: number; } | undefined>(undefined); const filePreviewRef = useRef(null); // 从 fetcher.state 判断是否正在操作 const isDeleting = fetcher.state !== 'idle'; // 处理 fetcher 响应(删除草稿) useEffect(() => { if (fetcher.data?.success && fetcher.data.message === '草稿已删除') { // 删除成功,跳转到模板列表 navigate('/contract-template'); } else if (fetcher.data?.error) { toastService.error(fetcher.data.error); } }, [fetcher.data, navigate]); // 监听页面关闭事件 - 自动删除草稿 useEffect(() => { const handleBeforeUnload = (e: BeforeUnloadEvent) => { // 发送删除请求(使用 sendBeacon 确保请求发送) const formData = new FormData(); formData.append('_action', 'delete'); navigator.sendBeacon( `/contract-draft/${draft.id}`, formData ); }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); }; }, [draft.id]); // 组件卸载时删除草稿(处理路由跳转的情况) useEffect(() => { return () => { // 组件卸载时删除草稿记录 const formData = new FormData(); formData.append('_action', 'delete'); fetch(`/contract-draft/${draft.id}`, { method: 'POST', body: formData, keepalive: true // 确保请求在页面关闭后仍然发送 }).catch(err => { console.error('[Draft] 删除草稿失败:', err); }); }; }, [draft.id]); // 单个替换占位符 const handleSingleReplace = async (key: string, value: string) => { const placeholder = `{{${key}}}`; console.log(`[Draft] 单个替换: ${placeholder} -> ${value}`); // 设置 AI 建议替换参数,触发 FilePreview 中的替换 setAiSuggestionReplace({ searchText: placeholder, replaceText: value, pageNumber: 1 // 从第一页开始搜索 }); // 短暂延迟后清除参数,以便下次可以重新触发 setTimeout(() => { setAiSuggestionReplace(undefined); toastService.success(`已替换 ${key}`); }, 1000); }; // 字段聚焦时高亮对应占位符 const handleFieldFocus = (key: string) => { const placeholder = `{{${key}}}`; console.log(`[Draft] 高亮占位符: ${placeholder}`); // 设置高亮值,触发 FilePreview 中的高亮 setHighlightValue(placeholder); // 短暂延迟后清除高亮,以便下次点击可以重新触发 setTimeout(() => { setHighlightValue(undefined); }, 100); }; // 批量替换占位符 const handleBatchReplace = async () => { setIsReplacing(true); try { // 获取 CollaboraViewer 引用 const collaboraRef = filePreviewRef.current?.collaboraViewerRef.current; if (!collaboraRef?.isReady) { toastService.warning('文档尚未加载完成,请稍候...'); setIsReplacing(false); return; } console.log('[Draft] 开始批量替换占位符:', placeholderValues); // 批量替换所有占位符 let replaceCount = 0; for (const [key, value] of Object.entries(placeholderValues)) { if (value) { // 只替换有值的字段 const placeholder = `{{${key}}}`; console.log(`[Draft] 替换: ${placeholder} -> ${value}`); // 调用 unoCommands.replaceAll 方法 if (collaboraRef.unoCommands?.replaceAll) { await collaboraRef.unoCommands.replaceAll(placeholder, value); replaceCount++; // 添加延迟避免 Collabora 响应不过来 await new Promise(resolve => setTimeout(resolve, 100)); } else { console.warn('[Draft] unoCommands.replaceAll 方法不可用'); } } } console.log(`[Draft] 替换完成,共替换 ${replaceCount} 个占位符`); toastService.success(`占位符替换完成(${replaceCount}个)`); } catch (error) { console.error('[Draft] 替换失败:', error); toastService.error(`替换失败: ${error instanceof Error ? error.message : '未知错误'}`); } finally { setIsReplacing(false); } }; // 导出文档(下载文件) const handleExportDocument = async () => { if (!draft.file_path) { toastService.error('文件路径不存在,无法下载'); return; } try { toastService.info('正在下载文件...'); // 使用统一的下载方法 const blob = await downloadFile(draft.file_path); // 创建Blob URL const blobUrl = URL.createObjectURL(blob); // 清理文件名 const fileExtension = draft.file_path.split('.').pop() || 'docx'; const fileName = `${draft.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); toastService.success('文件下载成功'); } catch (error) { console.error('[Draft] 下载文件失败:', error); toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; // 完成起草(下载文件 + 删除草稿记录) const handleComplete = async () => { // 1. 先下载文件 await handleExportDocument(); // 2. 延迟后删除草稿记录并跳转 setTimeout(() => { const formData = new FormData(); formData.append('_action', 'delete'); fetcher.submit(formData, { method: 'post' }); }, 500); }; // 返回模板详情页(删除草稿) const handleBack = () => { if (confirm('确定要返回吗?草稿将被删除。')) { // 删除草稿记录 const formData = new FormData(); formData.append('_action', 'delete'); fetcher.submit(formData, { method: 'post' }); // 删除成功后会自动跳转(通过 useEffect) } }; return (
{/* 顶部工具栏 */}

{draft.title}

基于模板:{template.title}

{draft.status === 'draft' ? '草稿' : draft.status === 'completed' ? '已完成' : '已归档'}
{/* 主内容区域:左右分栏 */}
{/* 左侧:文档预览(60%) */}
{/* 右侧:占位符表单(40%) */}
); }