Files
leaudit-platform-frontend/docs/contract-drafting-implementation-checklist.md
2025-12-05 00:09:32 +08:00

15 KiB
Raw Permalink Blame History

合同起草功能实施清单

阶段一:数据库设计(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 和文件路径
      }
      

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 字段
      }
      

2.4 完成起草 API

  • 创建路由 app/routes/api.contracts.draft.$id.complete.tsx
    • POST action:标记为已完成
      export async function action({ request, params }: ActionFunctionArgs) {
        // 1. 检查是否还有未替换的占位符
        // 2. 更新状态为 'completed'
        // 3. 可选:移动文件到正式合同目录
      }
      

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 };
      }
      

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
    • 表单验证
      • 必填字段验证
      • 数据格式验证
    • 操作按钮
      • "一键替换"按钮
      • "保存草稿"按钮
      • "完成起草"按钮

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. 里程碑1:数据库和 API 完成(第3天)
  2. 里程碑2:基本起草功能完成(第7天)
  3. 里程碑3:完整功能开发完成(第12天)
  4. 里程碑4:测试和部署完成(第18天)

风险评估

风险 等级 应对措施
CollaboraViewer 替换性能不足 提前测试,准备降级方案(使用 docxtemplater
文件复制失败 添加重试机制,记录详细日志
占位符格式不统一 制定严格的占位符规范,提供模板检查工具
用户数据丢失 实现自动保存,添加草稿历史记录

可选增强功能(后续迭代)

  • 模板变量智能推荐
  • 历史数据自动填充
  • 合同模板市场
  • 审批流程集成
  • 电子签名集成
  • 导出为 PDF
  • 版本控制和对比