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

483 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 合同起草功能 - 实施总结(V2 更新版)
## 业务逻辑变更
基于用户需求,功能从"保存草稿"模式调整为"临时编辑"模式:
### 变更对比
| 功能 | 旧逻辑(V1) | ✅ 新逻辑(V2) |
|------|------------|--------------|
| 保存草稿 | 保存占位符值到数据库 | 导出文档(下载 MinIO 文件) |
| 完成起草 | 标记为已完成,保留记录 | 下载文件 + 删除草稿记录 |
| 离开页面 | 草稿继续保留 | **自动删除草稿记录** |
| 草稿列表 | 可查看历史草稿 | 无草稿列表(临时存在) |
| 数据持久化 | 保存 placeholder_values | **不保存**,仅跟踪会话 |
---
## 实施的功能
### ✅ 已完成
1. **草稿创建**
- 用户点击"起草合同"创建临时草稿记录
- 记录包含:标题、文件路径、模板ID
- 不保存占位符值
2. **占位符表单**
- 动态渲染表单字段(基于 placeholder_schema
- 分组显示(甲方信息、乙方信息、合同条款)
- 本地状态管理(不提交到数据库)
3. **一键替换**
- 遍历占位符值
- 调用 Collabora `unoCommands.replaceAll()`
- 实时替换文档中的 `{{占位符}}`
4. **导出文档**(新)
- 下载 MinIO 上的当前文件
- 使用 `downloadFile()` 统一方法
- 清理文件名,触发浏览器下载
5. **完成起草**(更新)
- 先下载文件
- 延迟 500ms 后删除草稿记录
- 自动跳转到模板列表
6. **自动清理**(新)
- **页面关闭**:使用 `navigator.sendBeacon` 删除草稿
- **路由跳转**:使用 `fetch` + `keepalive` 删除草稿
- **点击返回**:弹窗确认后删除草稿
### ❌ 已移除
1. ~~`updatePlaceholders()`~~ - 不再保存占位符值
2. ~~`completeDraft()`~~ - 不再更新状态
3. ~~`getDraftsByUser()`~~ - 不需要草稿列表
### 新增功能
1. **`deleteDraft()`**
- 删除草稿记录
- 权限验证(只能删除自己的)
- 使用 `postgrestDelete`
2. **`handleExportDocument()`**
- 下载 MinIO 文件
- 创建 Blob URL
- 触发浏览器下载
3. **自动清理机制**
- `beforeunload` 事件监听
- 组件卸载清理
- `keepalive``sendBeacon` 技术
---
## 技术实现
### 1. Service 层(draft-service.server.ts
```typescript
// 保留的函数
createDraftContract() // 创建草稿记录
getDraftById() // 获取草稿详情
copyMinioFile() // 文件复制(预留)
generateDraftFilePath() // 生成文件路径
// 新增的函数
deleteDraft() // 删除草稿记录
// 删除的函数
updatePlaceholders() // 不再需要
completeDraft() // 不再需要
getDraftsByUser() // 不再需要
```
### 2. 路由层(contract-draft.$draftId.tsx
```typescript
// Loader
export async function loader({ params, request }) {
// 获取草稿和模板信息
const draft = await getDraftById(draftId, userId, jwt);
const template = await postgrestGet('contract_templates', ...);
return { draft, template };
}
// Action(只处理删除)
export async function action({ request, params }) {
const actionType = formData.get('_action');
if (actionType === 'delete') {
await deleteDraft(draftId, userId, jwt);
return json({ success: true, message: '草稿已删除' });
}
}
// 组件
export default function ContractDraftPage() {
// 自动删除机制
useEffect(() => {
window.addEventListener('beforeunload', handleBeforeUnload);
return () => {
// 组件卸载时删除草稿
fetch(`/contract-draft/${draft.id}`, {
method: 'POST',
body: formData,
keepalive: true
});
};
}, [draft.id]);
// 导出文档
const handleExportDocument = async () => {
const blob = await downloadFile(draft.file_path);
// 创建下载链接
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = fileName;
a.click();
};
// 完成起草
const handleComplete = async () => {
await handleExportDocument(); // 先下载
setTimeout(() => {
fetcher.submit({ _action: 'delete' }, { method: 'post' }); // 后删除
}, 500);
};
// 返回
const handleBack = () => {
if (confirm('确定要返回吗?草稿将被删除。')) {
fetcher.submit({ _action: 'delete' }, { method: 'post' });
}
};
}
```
### 3. 组件层(PlaceholderForm.tsx
```typescript
interface PlaceholderFormProps {
onBatchReplace: () => void;
onExportDocument: () => void; // 改名:导出文档
onComplete: () => void;
isReplacing: boolean;
isDeleting: boolean; // 改名:是否正在删除
}
// 按钮
<button onClick={onBatchReplace}></button>
<button onClick={onExportDocument}>
<i className="ri-download-line"></i>
</button>
<button onClick={onComplete}>
{isDeleting ? '处理中...' : '完成起草'}
</button>
```
---
## 关键技术点
### 1. 页面关闭时发送请求
```typescript
// 使用 sendBeacon 确保请求发送
const handleBeforeUnload = () => {
const formData = new FormData();
formData.append('_action', 'delete');
navigator.sendBeacon(`/contract-draft/${draft.id}`, formData);
};
window.addEventListener('beforeunload', handleBeforeUnload);
```
**特点**
- 异步发送,不阻塞页面
- 浏览器保证发送(即使页面已关闭)
- 适用于页面关闭场景
### 2. 组件卸载时清理
```typescript
useEffect(() => {
return () => {
const formData = new FormData();
formData.append('_action', 'delete');
fetch(`/contract-draft/${draft.id}`, {
method: 'POST',
body: formData,
keepalive: true // 关键:确保请求发送
});
};
}, [draft.id]);
```
**特点**
- `keepalive: true` 确保即使组件卸载也能发送
- 适用于 SPA 路由跳转场景
- 不阻塞路由跳转
### 3. 文件下载
```typescript
const handleExportDocument = async () => {
// 1. 从 MinIO 下载文件
const blob = await downloadFile(draft.file_path);
// 2. 创建 Blob URL
const blobUrl = URL.createObjectURL(blob);
// 3. 触发下载
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
// 4. 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
}, 100);
};
```
**特点**
- 使用 Blob URL 避免跨域问题
- 自动清理内存
- 兼容所有现代浏览器
---
## 文件变更清单
### 修改的文件
| 文件 | 变更内容 |
|------|---------|
| `app/api/contracts/draft-service.server.ts` | 添加 `deleteDraft()`<br>❌ 删除 `updatePlaceholders()`<br>❌ 删除 `completeDraft()`<br>❌ 删除 `getDraftsByUser()` |
| `app/routes/contract-draft.$draftId.tsx` | 🔄 action 只处理删除<br> 添加 `handleExportDocument()`<br>🔄 修改 `handleComplete()`<br>🔄 修改 `handleBack()`<br> 添加自动清理机制 |
| `app/components/contracts/PlaceholderForm.tsx` | 🔄 props: `onSaveDraft``onExportDocument`<br>🔄 props: `isSaving``isDeleting`<br>🔄 按钮文案:保存草稿 → 导出文档 |
### 删除的文件
| 文件 | 原因 |
|------|------|
| ❌ `app/routes/api.contracts.draft.tsx` | 已集成到页面路由 action |
| ❌ `app/routes/api.contracts.draft.$id.placeholders.tsx` | 不再保存占位符 |
| ❌ `app/routes/api.contracts.draft.$id.complete.tsx` | 改为删除操作 |
| ❌ `app/routes/api.files.copy.tsx` | 已集成到 service 层 |
### 新增的文档
| 文件 | 内容 |
|------|------|
| `docs/contract-drafting-updated-architecture.md` | 更新后的架构说明 |
| `docs/contract-drafting-implementation-summary-v2.md` | 本文档 |
---
## 测试清单
### 功能测试
- [ ] **创建草稿**
- [ ] 点击"起草合同"按钮
- [ ] 输入标题
- [ ] 成功跳转到编辑页
- [ ] **占位符表单**
- [ ] 表单字段正确渲染
- [ ] 分组显示正常
- [ ] 输入值能实时更新
- [ ] **一键替换**
- [ ] 点击"一键替换占位符"
- [ ] Collabora 中占位符被替换
- [ ] 显示替换数量提示
- [ ] **导出文档**
- [ ] 点击"导出文档"按钮
- [ ] 文件开始下载
- [ ] 文件名正确
- [ ] 文件内容包含已替换的内容
- [ ] **完成起草**
- [ ] 点击"完成起草"按钮
- [ ] 文件自动下载
- [ ] 草稿记录被删除
- [ ] 跳转到模板列表
- [ ] **返回按钮**
- [ ] 点击"返回"按钮
- [ ] 显示确认弹窗
- [ ] 确认后草稿被删除
- [ ] 跳转到模板列表
- [ ] **自动清理**
- [ ] 关闭浏览器标签页 → 草稿被删除
- [ ] 点击浏览器返回按钮 → 草稿被删除
- [ ] 在地址栏输入其他URL → 草稿被删除
### 边界测试
- [ ] **网络异常**
- [ ] 下载失败时显示错误提示
- [ ] 删除失败时显示错误提示
- [ ] **并发操作**
- [ ] 快速点击"完成起草"不会重复操作
- [ ] 正在删除时禁用所有按钮
- [ ] **权限验证**
- [ ] 只能查看自己的草稿
- [ ] 只能删除自己的草稿
---
## 部署步骤
### 1. 数据库迁移
```bash
# 连接数据库
psql -U postgres -d docreview
# 执行迁移脚本
\i database/migrations/001_create_drafted_contracts.sql
# 验证表结构
\d drafted_contracts
```
### 2. 配置测试模板
```sql
-- 更新模板的 placeholder_schema
UPDATE contract_templates
SET placeholder_schema = '{
"fields": [
{
"key": "甲方名称",
"label": "甲方名称",
"type": "text",
"required": true,
"group": "甲方信息"
},
{
"key": "乙方名称",
"label": "乙方名称",
"type": "text",
"required": true,
"group": "乙方信息"
},
{
"key": "合同金额",
"label": "合同金额(元)",
"type": "number",
"required": true,
"group": "合同条款"
},
{
"key": "签订日期",
"label": "签订日期",
"type": "date",
"required": true,
"group": "合同条款"
}
]
}'::jsonb
WHERE id = 1; -- 替换为实际的模板ID
```
### 3. 准备测试模板文档
在模板 Word 文档中添加占位符:
- `{{甲方名称}}`
- `{{乙方名称}}`
- `{{合同金额}}`
- `{{签订日期}}`
### 4. 构建和部署
```bash
# 类型检查
npm run typecheck
# 构建
npm run build
# 启动
npm start
```
---
## 常见问题
### Q1: 草稿记录什么时候被删除?
**A**: 在以下情况下自动删除:
1. 用户点击"完成起草"
2. 用户点击"返回"按钮并确认
3. 用户关闭浏览器标签页
4. 用户通过浏览器返回按钮离开
5. 用户在地址栏输入其他URL
### Q2: 占位符值保存在哪里?
**A**: 占位符值**仅保存在前端组件的本地状态**中,不会提交到数据库。数据库中的 `placeholder_values` 字段始终为空对象 `{}`
### Q3: 如果用户关闭页面前网络断开,草稿会被删除吗?
**A**: 使用了两种机制确保删除:
1. `navigator.sendBeacon` - 浏览器会排队等待网络恢复后发送
2. `fetch` + `keepalive` - 即使页面关闭也会尝试发送
但如果网络长时间不可用,草稿可能保留。可以通过定时任务清理超过一定时间(如 1 小时)的草稿记录。
### Q4: "导出文档"和"完成起草"有什么区别?
**A**:
- **导出文档**:只下载文件,草稿记录继续保留,用户可以继续编辑
- **完成起草**:下载文件 + 删除草稿记录 + 返回模板列表
### Q5: 如何启用文件复制功能?
**A**: 参考 `docs/minio-file-copy-implementation.md`,需要:
1. 安装 `npm install minio`
2. 配置环境变量(MINIO_ENDPOINT, MINIO_ACCESS_KEY 等)
3. 实现 `copyMinioFile()` 函数
4. 在创建草稿时先复制文件,然后传递 `draftFilePath`
---
## 总结
**功能已完成**
- 草稿临时管理
- 导出文档功能
- 自动清理机制
- 完整的 Remix 集成
**代码质量**
- TypeScript 类型检查通过
- 符合 Remix 最佳实践
- 代码结构清晰
- 注释完整
**用户体验**
- 操作流程简单
- 自动清理无需手动管理
- 随时导出当前编辑
- 错误提示友好
🎉 **可以开始测试和部署了!**