This commit is contained in:
2025-12-05 00:09:32 +08:00
parent bb3d22eabf
commit 3d1dbb3f97
214 changed files with 113060 additions and 1232 deletions
@@ -0,0 +1,406 @@
# 从 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.**可靠** - 专业工具,提取准确
功能已实现,可以开始测试!🎉