549 lines
15 KiB
Markdown
549 lines
15 KiB
Markdown
# 合同起草功能实施清单
|
||
|
||
## 阶段一:数据库设计(1天)
|
||
|
||
### 1.1 创建数据库表
|
||
|
||
- [ ] 创建 `drafted_contracts` 表
|
||
```sql
|
||
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` 表添加占位符配置字段
|
||
```sql
|
||
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:创建起草记录
|
||
```typescript
|
||
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:更新占位符值
|
||
```typescript
|
||
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:标记为已完成
|
||
```typescript
|
||
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`
|
||
- [ ] 在操作按钮区域添加"起草合同"按钮
|
||
```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:加载草稿信息和模板信息
|
||
```typescript
|
||
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 定义
|
||
```typescript
|
||
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` 页面组件
|
||
- [ ] 布局结构
|
||
```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` 函数
|
||
```typescript
|
||
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` 函数
|
||
```typescript
|
||
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` 函数
|
||
```typescript
|
||
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` 引用
|
||
```typescript
|
||
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` 方法可用
|
||
```typescript
|
||
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` 中添加批量替换方法
|
||
```typescript
|
||
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
|
||
- [ ] 版本控制和对比
|