15 KiB
15 KiB
合同起草功能实施清单
阶段一:数据库设计(1天)
1.1 创建数据库表
- 创建
drafted_contracts表CREATE TABLE drafted_contracts ( id SERIAL PRIMARY KEY, template_id INTEGER REFERENCES contract_templates(id), file_path TEXT NOT NULL, title TEXT NOT NULL, placeholder_values JSONB, status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'completed', 'archived')), created_by INTEGER REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_drafted_contracts_template_id ON drafted_contracts(template_id); CREATE INDEX idx_drafted_contracts_created_by ON drafted_contracts(created_by); CREATE INDEX idx_drafted_contracts_status ON drafted_contracts(status);
1.2 扩展模板表
- 为
contract_templates表添加占位符配置字段ALTER TABLE contract_templates ADD COLUMN placeholder_schema JSONB; -- 示例数据 UPDATE contract_templates SET placeholder_schema = '{ "fields": [ {"key": "甲方名称", "label": "甲方名称", "type": "text", "required": true, "group": "甲方信息"}, {"key": "甲方地址", "label": "甲方地址", "type": "text", "required": true, "group": "甲方信息"}, {"key": "甲方法定代表人", "label": "法定代表人", "type": "text", "required": true, "group": "甲方信息"}, {"key": "甲方联系电话", "label": "联系电话", "type": "tel", "required": true, "group": "甲方信息"}, {"key": "乙方名称", "label": "乙方名称", "type": "text", "required": true, "group": "乙方信息"}, {"key": "乙方地址", "label": "乙方地址", "type": "text", "required": true, "group": "乙方信息"}, {"key": "乙方法定代表人", "label": "法定代表人", "type": "text", "required": true, "group": "乙方信息"}, {"key": "乙方联系电话", "label": "联系电话", "type": "tel", "required": true, "group": "乙方信息"}, {"key": "合同金额", "label": "合同金额(元)", "type": "number", "required": true, "group": "合同条款"}, {"key": "签订日期", "label": "签订日期", "type": "date", "required": true, "group": "合同条款"}, {"key": "合同编号", "label": "合同编号", "type": "text", "required": false, "group": "基本信息"} ] }'::jsonb WHERE id = 1;
阶段二:后端 API 开发(2-3天)
2.1 文件复制服务
- 创建
app/api/contracts/draft-service.server.ts- 实现
copyTemplateFile函数- 从 MinIO 下载模板文件
- 重命名文件(添加时间戳、用户ID)
- 上传到草稿目录
- 返回新文件路径
- 实现
2.2 创建起草合同 API
- 创建路由
app/routes/api.contracts.draft.tsx- POST action:创建起草记录
export async function action({ request }: ActionFunctionArgs) { const { templateId, title } = await request.json(); // 1. 获取模板信息 // 2. 复制模板文件 // 3. 创建 drafted_contracts 记录 // 4. 返回新记录 ID 和文件路径 }
- POST action:创建起草记录
2.3 保存占位符值 API
- 创建路由
app/routes/api.contracts.draft.$id.placeholders.tsx- PUT action:更新占位符值
export async function action({ request, params }: ActionFunctionArgs) { const { placeholders } = await request.json(); // 更新 drafted_contracts 表的 placeholder_values 字段 }
- PUT action:更新占位符值
2.4 完成起草 API
- 创建路由
app/routes/api.contracts.draft.$id.complete.tsx- POST action:标记为已完成
export async function action({ request, params }: ActionFunctionArgs) { // 1. 检查是否还有未替换的占位符 // 2. 更新状态为 'completed' // 3. 可选:移动文件到正式合同目录 }
- POST action:标记为已完成
2.5 查询草稿列表 API
- 扩展
app/routes/api.contracts.drafts.tsx- GET loader:查询当前用户的草稿列表
- 支持分页和筛选
阶段三:前端组件开发(3-4天)
3.1 模板详情页增加按钮
- 修改
app/routes/contract-template.detail.$id.tsx- 在操作按钮区域添加"起草合同"按钮
<button className="detail-btn primary bg-primary text-white px-6 py-3 rounded-lg" onClick={handleStartDraft} > <i className="ri-edit-line"></i> 起草合同 </button> - 实现
handleStartDraft函数- 调用创建草稿 API
- 导航到起草页面
- 在操作按钮区域添加"起草合同"按钮
3.2 创建起草页面路由
- 创建
app/routes/contract-draft.$draftId.tsx- Loader:加载草稿信息和模板信息
export async function loader({ params, request }: LoaderFunctionArgs) { const draftId = params.draftId!; // 1. 获取草稿记录 // 2. 获取模板信息(占位符配置) // 3. 返回数据 return { draft, template }; }
- Loader:加载草稿信息和模板信息
3.3 创建占位符表单组件
- 创建
app/components/contracts/PlaceholderForm.tsx- Props 定义
interface PlaceholderFormProps { schema: PlaceholderSchema; values: Record<string, string>; onChange: (values: Record<string, string>) => void; onBatchReplace: () => void; onSaveDraft: () => void; onComplete: () => void; isReplacing: boolean; isSaving: boolean; } - 渲染表单字段
- 根据
schema.fields动态生成表单 - 按
group分组显示 - 支持不同字段类型(text, number, date, tel)
- 根据
- 表单验证
- 必填字段验证
- 数据格式验证
- 操作按钮
- "一键替换"按钮
- "保存草稿"按钮
- "完成起草"按钮
- Props 定义
3.4 创建起草页面布局
- 实现
app/routes/contract-draft.$draftId.tsx页面组件- 布局结构
<div className="flex h-screen"> {/* 左侧:文档预览(60%) */} <div className="w-[60%] border-r"> <FilePreview fileContent={{ path: draft.file_path, title: draft.title, // ... }} isTemplate={false} // 编辑模式 /> </div> {/* 右侧:占位符表单(40%) */} <div className="w-[40%] overflow-y-auto p-6"> <PlaceholderForm schema={template.placeholder_schema} values={draft.placeholder_values} onChange={handleValuesChange} onBatchReplace={handleBatchReplace} onSaveDraft={handleSaveDraft} onComplete={handleComplete} isReplacing={isReplacing} isSaving={isSaving} /> </div> </div>
- 布局结构
3.5 实现替换逻辑
-
在起草页面实现
handleBatchReplace函数const handleBatchReplace = async () => { setIsReplacing(true); try { // 获取 CollaboraViewer 引用 const collaboraRef = filePreviewRef.current?.collaboraViewerRef; if (!collaboraRef?.isReady) { toastService.warning('文档尚未加载完成'); return; } // 批量替换所有占位符 for (const [key, value] of Object.entries(placeholderValues)) { if (value) { // 只替换有值的字段 const placeholder = `{{${key}}}`; await collaboraRef.unoCommands.replaceAll(placeholder, value); } } toastService.success('占位符替换完成'); } catch (error) { console.error('替换失败:', error); toastService.error('替换失败'); } finally { setIsReplacing(false); } }; -
实现
handleSaveDraft函数const handleSaveDraft = async () => { setIsSaving(true); try { await fetch(`/api/contracts/draft/${draftId}/placeholders`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ placeholders: placeholderValues }) }); toastService.success('草稿已保存'); } catch (error) { toastService.error('保存失败'); } finally { setIsSaving(false); } }; -
实现
handleComplete函数const handleComplete = async () => { // 1. 检查必填字段 const missingFields = schema.fields .filter(f => f.required && !placeholderValues[f.key]) .map(f => f.label); if (missingFields.length > 0) { toastService.error(`请填写必填字段:${missingFields.join('、')}`); return; } // 2. 检查是否还有未替换的占位符 // (可选:调用 API 检查文档内容) // 3. 标记为完成 await fetch(`/api/contracts/draft/${draftId}/complete`, { method: 'POST' }); toastService.success('起草完成'); navigate('/contracts/drafts'); // 跳转到草稿列表 };
3.6 FilePreview 组件增强
- 修改
app/components/reviews/FilePreview.tsx- 导出
collaboraViewerRef引用export interface FilePreviewHandle { collaboraViewerRef: RefObject<CollaboraViewerHandle>; } export const FilePreview = forwardRef<FilePreviewHandle, FilePreviewProps>( function FilePreview(props, ref) { const collaboraViewerRef = useRef<CollaboraViewerHandle>(null); useImperativeHandle(ref, () => ({ collaboraViewerRef })); // ... } );
- 导出
阶段四:CollaboraViewer 替换功能完善(1-2天)
4.1 检查现有替换功能
- 查看
app/components/collabora/lib/中的替换相关方法unoReplaceAll- 全局替换unoReplaceCurrent- 替换当前匹配replaceTextInPage- 页面定点替换
4.2 增强 replaceAll 方法
- 修改
app/components/collabora/hooks.ts- 确保
replaceAll方法可用const replaceAll = useCallback(async (searchText: string, replaceText: string) => { if (!iframeRef.current?.contentWindow) { throw new Error('iframe 不可用'); } await unoReplaceAll(iframeRef.current.contentWindow, searchText, replaceText); }, [iframeRef]); return useMemo( () => ({ scrollToTop, replaceAll // 确保导出 }), [scrollToTop, replaceAll] );
- 确保
4.3 添加批量替换方法
- 在
CollaboraViewer.tsx中添加批量替换方法useImperativeHandle(ref, () => ({ isReady: documentReady, unoCommands: { scrollToTop, replaceAll }, // 新增批量替换方法 batchReplace: async (replacements: Array<{ search: string; replace: string }>) => { for (const { search, replace } of replacements) { await replaceAll(search, replace); // 添加延迟避免 Collabora 响应不过来 await new Promise(resolve => setTimeout(resolve, 100)); } }, clearAllHighlights: async () => { await clearHighlights(iframeWindowRef.current); } }));
阶段五:样式和体验优化(1-2天)
5.1 占位符表单样式
- 创建
app/styles/components/placeholder-form.css- 表单分组样式
- 字段输入框样式
- 按钮样式
- 响应式布局
5.2 起草页面样式
- 创建
app/styles/pages/contract-draft.css- 左右分栏布局
- 固定高度和滚动
- 响应式设计
5.3 交互优化
-
添加加载状态指示
- 文档加载中
- 替换进行中
- 保存中
-
添加进度提示
- "正在替换占位符... (3/10)"
- 替换完成提示
-
添加确认对话框
- 完成起草前确认
- 离开页面前提示未保存
5.4 错误处理
- 网络错误提示
- 文件加载失败处理
- 替换失败回滚
阶段六:草稿管理功能(1-2天)
6.1 草稿列表页面
- 创建
app/routes/contracts.drafts.tsx- 显示用户的所有草稿
- 支持筛选(模板、状态、日期)
- 支持搜索
- 操作按钮
- 继续编辑
- 删除草稿
- 查看详情
6.2 草稿卡片组件
- 创建
app/components/contracts/DraftCard.tsx- 显示草稿信息
- 标题
- 基于的模板
- 创建时间
- 状态
- 操作按钮
- 显示草稿信息
阶段七:测试和优化(2-3天)
7.1 功能测试
- 创建草稿流程测试
- 占位符替换测试
- 单个替换
- 批量替换
- 部分替换
- 重复替换
- 保存草稿测试
- 完成起草测试
- 边界情况测试
- 占位符不存在
- 替换值为空
- 特殊字符处理
7.2 性能测试
- 大文档替换性能
- 多占位符替换时间
- 文件复制速度
7.3 用户体验测试
- 操作流畅性
- 错误提示清晰度
- 响应式布局适配
阶段八:文档和部署(1天)
8.1 开发文档
- 编写功能使用说明
- 编写 API 文档
- 编写占位符配置说明
8.2 用户手册
- 如何创建模板(添加占位符)
- 如何起草合同
- 常见问题解答
8.3 部署准备
- 数据库迁移脚本
- 环境变量配置
- 测试环境部署
- 生产环境部署
预计总工期
- 阶段一:数据库设计 - 1天
- 阶段二:后端 API 开发 - 2-3天
- 阶段三:前端组件开发 - 3-4天
- 阶段四:CollaboraViewer 完善 - 1-2天
- 阶段五:样式优化 - 1-2天
- 阶段六:草稿管理 - 1-2天
- 阶段七:测试优化 - 2-3天
- 阶段八:文档部署 - 1天
总计:12-18天
依赖关系
阶段一(数据库) → 阶段二(后端 API) → 阶段三(前端开发)
↓
阶段四(Collabora 完善)
↓
阶段五(样式优化) + 阶段六(草稿管理)
↓
阶段七(测试) → 阶段八(部署)
关键里程碑
- 里程碑1:数据库和 API 完成(第3天)
- 里程碑2:基本起草功能完成(第7天)
- 里程碑3:完整功能开发完成(第12天)
- 里程碑4:测试和部署完成(第18天)
风险评估
| 风险 | 等级 | 应对措施 |
|---|---|---|
| CollaboraViewer 替换性能不足 | 中 | 提前测试,准备降级方案(使用 docxtemplater) |
| 文件复制失败 | 低 | 添加重试机制,记录详细日志 |
| 占位符格式不统一 | 中 | 制定严格的占位符规范,提供模板检查工具 |
| 用户数据丢失 | 中 | 实现自动保存,添加草稿历史记录 |
可选增强功能(后续迭代)
- 模板变量智能推荐
- 历史数据自动填充
- 合同模板市场
- 审批流程集成
- 电子签名集成
- 导出为 PDF
- 版本控制和对比