407 lines
8.6 KiB
Markdown
407 lines
8.6 KiB
Markdown
# 从 DOCX 文件自动提取占位符
|
||
|
||
## 功能说明
|
||
|
||
系统会自动从 docx 文档中提取 `{{占位符}}` 格式的变量,并生成表单字段,无需在数据库中手动配置。
|
||
|
||
---
|
||
|
||
## 实现原理
|
||
|
||
### 技术栈
|
||
|
||
- **pizzip** - 用于解压 docx 文件(docx 本质是 zip 格式)
|
||
- **正则表达式** - 提取占位符(避免 docxtemplater 编译时的格式化标签分割问题)
|
||
|
||
### 工作流程
|
||
|
||
```
|
||
1. 用户点击"起草合同"
|
||
↓
|
||
2. Loader 读取 docx 文件
|
||
↓
|
||
3. PizZip 解压文件,读取 word/document.xml
|
||
↓
|
||
4. 移除 XML 标签,提取纯文本
|
||
↓
|
||
5. 正则匹配所有 {{...}} 占位符
|
||
↓
|
||
6. 根据占位符名称自动推测:
|
||
- 字段类型(text/number/date/textarea)
|
||
- 字段分组(甲方信息/乙方信息/合同条款)
|
||
- 是否必填
|
||
↓
|
||
7. 生成 PlaceholderSchema
|
||
↓
|
||
8. 渲染表单
|
||
```
|
||
|
||
---
|
||
|
||
## 核心代码
|
||
|
||
### docx-parser.server.ts
|
||
|
||
```typescript
|
||
import PizZip from 'pizzip';
|
||
import fs from 'fs';
|
||
|
||
/**
|
||
* 从 docx 文件中提取占位符
|
||
*/
|
||
export async function extractPlaceholdersFromDocx(
|
||
filePath: string
|
||
): Promise<string[]> {
|
||
// 1. 读取文件
|
||
const content = fs.readFileSync(filePath, 'binary');
|
||
|
||
// 2. 使用 PizZip 解压
|
||
const zip = new PizZip(content);
|
||
|
||
// 3. 读取 document.xml 文件(不使用 docxtemplater,避免格式化文本的标签分割问题)
|
||
const documentXml = zip.file('word/document.xml');
|
||
if (!documentXml) {
|
||
throw new Error('无法找到 word/document.xml 文件');
|
||
}
|
||
|
||
// 4. 获取 XML 文本内容
|
||
const xmlContent = documentXml.asText();
|
||
|
||
// 5. 移除所有 XML 标签,只保留纯文本
|
||
const fullText = xmlContent.replace(/<[^>]+>/g, '');
|
||
|
||
// 6. 使用正则表达式提取所有 {{...}} 占位符
|
||
const placeholderRegex = /\{\{([^}]+)\}\}/g;
|
||
const matches = fullText.matchAll(placeholderRegex);
|
||
|
||
// 7. 去重并返回
|
||
const placeholders = new Set<string>();
|
||
for (const match of matches) {
|
||
placeholders.add(match[1].trim());
|
||
}
|
||
|
||
return Array.from(placeholders);
|
||
}
|
||
|
||
/**
|
||
* 生成默认的 PlaceholderSchema
|
||
*/
|
||
export function generateDefaultSchema(
|
||
placeholders: string[]
|
||
): PlaceholderSchema {
|
||
return {
|
||
fields: placeholders.map(placeholder => ({
|
||
key: placeholder,
|
||
label: placeholder,
|
||
type: inferType(placeholder), // 推测类型
|
||
required: inferRequired(placeholder), // 推测是否必填
|
||
group: inferGroup(placeholder) // 推测分组
|
||
}))
|
||
};
|
||
}
|
||
```
|
||
|
||
### contract-draft.$draftId.tsx (loader)
|
||
|
||
```typescript
|
||
export async function loader({ params, request }: LoaderFunctionArgs) {
|
||
// ... 获取用户信息和草稿信息 ...
|
||
|
||
// 从 docx 文件中提取占位符
|
||
const testDocPath = path.join(process.cwd(), 'public', 'testWork', '买卖合同 (1).docx');
|
||
|
||
// 提取占位符
|
||
const placeholders = await extractPlaceholdersFromDocx(testDocPath);
|
||
console.log('提取到的占位符:', placeholders);
|
||
|
||
// 生成默认 schema
|
||
const placeholderSchema = generateDefaultSchema(placeholders);
|
||
|
||
return json({
|
||
draft,
|
||
template: {
|
||
...template,
|
||
placeholder_schema: placeholderSchema
|
||
}
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 自动推测规则
|
||
|
||
### 字段类型推测
|
||
|
||
| 占位符名称包含 | 推测类型 | 示例 |
|
||
|--------------|---------|------|
|
||
| 金额、价格、数量、amount、price | `number` | {{合同金额}}、{{price}} |
|
||
| 日期、时间、date、time | `date` | {{签订日期}}、{{date}} |
|
||
| 地址、说明、备注、address、description | `textarea` | {{甲方地址}}、{{remark}} |
|
||
| 其他 | `text` | {{甲方名称}}、{{项目名称}} |
|
||
|
||
### 字段分组推测
|
||
|
||
| 占位符名称包含 | 推测分组 | 示例 |
|
||
|--------------|---------|------|
|
||
| 甲方、partyA | 甲方信息 | {{甲方名称}}、{{partyA_address}} |
|
||
| 乙方、partyB | 乙方信息 | {{乙方代表}}、{{partyB_phone}} |
|
||
| 金额、价格、数量 | 合同条款 | {{合同金额}}、{{数量}} |
|
||
| 日期、时间 | 日期信息 | {{签订日期}}、{{生效日期}} |
|
||
| 其他 | 基本信息 | {{项目名称}}、{{编号}} |
|
||
|
||
### 必填字段推测
|
||
|
||
| 占位符名称包含 | 是否必填 |
|
||
|--------------|---------|
|
||
| "可选"、"optional" | ❌ 否 |
|
||
| 其他 | ✅ 是 |
|
||
|
||
---
|
||
|
||
## 使用示例
|
||
|
||
### 1. 准备模板文档
|
||
|
||
在 Word 文档中添加占位符:
|
||
|
||
```
|
||
买卖合同
|
||
|
||
甲方名称:{{甲方名称}}
|
||
甲方地址:{{甲方地址}}
|
||
甲方代表:{{甲方代表}}
|
||
|
||
乙方名称:{{乙方名称}}
|
||
乙方地址:{{乙方地址}}
|
||
乙方代表:{{乙方代表}}
|
||
|
||
合同金额:{{合同金额}} 元
|
||
签订日期:{{签订日期}}
|
||
|
||
备注:{{备注说明(可选)}}
|
||
```
|
||
|
||
### 2. 系统自动提取
|
||
|
||
提取结果:
|
||
```json
|
||
[
|
||
"甲方名称",
|
||
"甲方地址",
|
||
"甲方代表",
|
||
"乙方名称",
|
||
"乙方地址",
|
||
"乙方代表",
|
||
"合同金额",
|
||
"签订日期",
|
||
"备注说明(可选)"
|
||
]
|
||
```
|
||
|
||
### 3. 自动生成表单配置
|
||
|
||
```json
|
||
{
|
||
"fields": [
|
||
{
|
||
"key": "甲方名称",
|
||
"label": "甲方名称",
|
||
"type": "text",
|
||
"required": true,
|
||
"group": "甲方信息"
|
||
},
|
||
{
|
||
"key": "甲方地址",
|
||
"label": "甲方地址",
|
||
"type": "textarea",
|
||
"required": true,
|
||
"group": "甲方信息"
|
||
},
|
||
{
|
||
"key": "合同金额",
|
||
"label": "合同金额",
|
||
"type": "number",
|
||
"required": true,
|
||
"group": "合同条款"
|
||
},
|
||
{
|
||
"key": "签订日期",
|
||
"label": "签订日期",
|
||
"type": "date",
|
||
"required": true,
|
||
"group": "日期信息"
|
||
},
|
||
{
|
||
"key": "备注说明(可选)",
|
||
"label": "备注说明(可选)",
|
||
"type": "textarea",
|
||
"required": false,
|
||
"group": "基本信息"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
### 4. 渲染表单
|
||
|
||
表单自动分组显示:
|
||
|
||
```
|
||
【甲方信息】
|
||
甲方名称: [_________] *
|
||
甲方地址: [_________] *
|
||
甲方代表: [_________] *
|
||
|
||
【乙方信息】
|
||
乙方名称: [_________] *
|
||
乙方地址: [_________] *
|
||
乙方代表: [_________] *
|
||
|
||
【合同条款】
|
||
合同金额: [_________] 元 *
|
||
|
||
【日期信息】
|
||
签订日期: [日期选择器] *
|
||
|
||
【基本信息】
|
||
备注说明(可选): [_________]
|
||
```
|
||
|
||
---
|
||
|
||
## 测试验证
|
||
|
||
### 当前测试文档
|
||
|
||
文件路径:`public/testWork/买卖合同 (1).docx`
|
||
|
||
### 测试步骤
|
||
|
||
1. **启动开发服务器**
|
||
```bash
|
||
npm run dev
|
||
```
|
||
|
||
2. **创建测试草稿**
|
||
- 访问模板详情页
|
||
- 点击"起草合同"
|
||
- 输入标题
|
||
|
||
3. **查看控制台输出**
|
||
```
|
||
[DOCX Parser] 开始读取文件: D:\remix_project\docreview\public\testWork\买卖合同 (1).docx
|
||
[DOCX Parser] 文档文本长度: 1234
|
||
[DOCX Parser] 提取到的占位符: ["甲方名称", "乙方名称", "合同金额", ...]
|
||
[Loader] 生成的 schema: {...}
|
||
```
|
||
|
||
4. **验证表单渲染**
|
||
- 检查右侧表单是否正确渲染
|
||
- 检查字段分组是否合理
|
||
- 检查字段类型是否正确
|
||
|
||
---
|
||
|
||
## 优势总结
|
||
|
||
### 1. 无需手动配置
|
||
|
||
- ✅ 自动提取占位符
|
||
- ✅ 自动推测字段类型
|
||
- ✅ 自动分组
|
||
- ✅ 节省配置时间
|
||
|
||
### 2. 灵活性高
|
||
|
||
- ✅ 修改文档即可,无需改代码
|
||
- ✅ 支持任意数量的占位符
|
||
- ✅ 支持任意命名规则
|
||
|
||
### 3. 用户友好
|
||
|
||
- ✅ 表单自动生成
|
||
- ✅ 智能分组展示
|
||
- ✅ 类型自动匹配
|
||
|
||
### 4. 维护简单
|
||
|
||
- ✅ 模板即配置
|
||
- ✅ 无需同步数据库
|
||
- ✅ 减少维护成本
|
||
|
||
---
|
||
|
||
## 扩展功能
|
||
|
||
### 1. 支持更复杂的占位符
|
||
|
||
```
|
||
{{甲方名称:text:required:甲方信息}}
|
||
{{合同金额:number:required:合同条款}}
|
||
{{备注:textarea:optional:基本信息}}
|
||
```
|
||
|
||
可以解析占位符中的元数据:
|
||
```typescript
|
||
const [key, type, required, group] = placeholder.split(':');
|
||
```
|
||
|
||
### 2. 支持默认值
|
||
|
||
```
|
||
{{甲方名称|默认公司}}
|
||
{{合同金额|100000}}
|
||
```
|
||
|
||
### 3. 支持枚举选项
|
||
|
||
```
|
||
{{付款方式|现金,转账,支票}}
|
||
```
|
||
|
||
渲染为下拉选择框。
|
||
|
||
### 4. 支持条件显示
|
||
|
||
```
|
||
{{#if 需要担保人}}
|
||
担保人姓名:{{担保人姓名}}
|
||
{{/if}}
|
||
```
|
||
|
||
---
|
||
|
||
## 注意事项
|
||
|
||
### 1. 占位符命名规范
|
||
|
||
- ✅ 使用有意义的名称
|
||
- ✅ 避免特殊字符
|
||
- ✅ 保持一致的命名风格
|
||
|
||
### 2. 性能考虑
|
||
|
||
- ✅ 提取占位符很快(毫秒级)
|
||
- ✅ 结果可以缓存
|
||
- ⚠️ 大文件(>10MB)可能较慢
|
||
|
||
### 3. 错误处理
|
||
|
||
- ✅ 文件不存在 → 使用空 schema
|
||
- ✅ 解析失败 → 回退到数据库配置
|
||
- ✅ 无占位符 → 显示提示信息
|
||
|
||
---
|
||
|
||
## 总结
|
||
|
||
使用 docxtemplater 自动提取占位符是一个优雅的解决方案:
|
||
|
||
1. ✅ **零配置** - 模板即配置
|
||
2. ✅ **自动化** - 智能推测字段属性
|
||
3. ✅ **灵活** - 修改文档即可
|
||
4. ✅ **可靠** - 专业工具,提取准确
|
||
|
||
功能已实现,可以开始测试!🎉
|