# CRM 客户开票信息 · 粘贴快速识别 · 设计说明 > Demo:`docs/demo-crm-invoice-paste-recognize.html`(方案 A) > 日期:2026-08-20 > 状态:已实现(待联调验收) ## 1. 背景与目标 客户管理详情弹窗右侧「开票信息」新增/编辑时,业务常从微信/邮件粘贴整段「开票资料」文本,再手工逐项填入,效率低且易错。 **目标:** 在新增/编辑弹窗顶部提供「粘贴识别」区,用**纯前端规则**解析常见开票资料文本,并将结果填入对应表单字段;用户可再微调后保存。不依赖后端、不调用大模型。 **非目标:** - 不新增数据库字段(银行行号、邮编、邮寄地址等仍无独立列) - 不接阿里云 / OCR / 大模型 - 不改开票信息保存 API 与权限 ## 2. 已确认决策 | 项 | 结论 | |---|---| | 入口布局 | 方案 A:新增/编辑弹窗表单顶部「粘贴识别」区 | | 识别方式 | 纯前端规则(标签别名 + 行解析),零成本、离线 | | 回填策略 | **识别后覆盖对应字段**(已有值一并覆盖) | | 无表单字段的信息 | 写入 `remark`(银行行号、邮编、发票邮寄信息等) | | 备注合并 | 识别产生的备注内容**追加到 remark 末尾**;若与已有内容重复则不重复追加 | | 范围 | 仅 `web-admin` 客户详情 → `InvoiceInfo.vue`;**以及**客户新增弹窗 `CustomerFormDialog.vue` 底部开票信息(二级粘贴弹窗) | ## 3. 交互设计 组件:`web-admin/src/views/crm/customer/components/InvoiceInfo.vue` 在现有 `el-form` 顶部、`.form-grid-2` 字段区之前增加粘贴区: ``` el-dialog「添加/修改开票信息」 └─ el-form ├─ 粘贴识别面板(虚线边框 + primary-bg) │ ├─ 标题「粘贴识别」+ 简短提示 │ ├─ el-input type=textarea(粘贴原文) │ ├─ [清空] [识别并填入] │ └─ 结果提示文案(成功/部分成功/失败) └─ 原有字段栅格(单位名称、税号…) ``` 行为: 1. 打开新增/编辑弹窗时,粘贴区默认**空**(不强制预填样例,避免误覆盖业务数据)。 2. 点击「识别并填入」→ 调用解析工具 → **覆盖**已映射字段;未映射信息写入/追加 `remark`。 3. 点击「清空」仅清空粘贴框与提示,**不**清空表单字段。 4. 关闭弹窗时重置粘贴区与表单(沿用现有 `reset`)。 5. 识别失败或零字段:`ElMessage.warning`,不改表单。 6. 部分成功:照常覆盖已识别字段,并用消息列出未识别的关键项(如缺单位名称)。 视觉对齐 Fresh Mint:主色 `#10b981` / `#059669` / `#ecfdf5`;按钮带图标(DocumentCopy / Delete / Close / Check 等既有习惯)。 ## 4. 解析规则 新增工具:`web-admin/src/utils/crm-invoice-paste-recognize.ts` 参考既有模式:`web-admin/src/utils/pl-paste-recognize.ts`(纯函数、导出样例常量、返回 `warnings`)。 ### 4.1 输入输出 ```ts interface CrmInvoicePasteResult { companyName?: string taxNo?: string companyAddress?: string legalRepresentative?: string agent?: string phoneFax?: string bankName?: string bankAccount?: string /** 无独立字段的附加信息,供写入 remark */ remarkParts: string[] warnings: string[] /** 成功写入表单的字段数(不含 remarkParts) */ filledCount: number } ``` ### 4.2 标签别名 → 字段 | 字段 | 标签别名(行首,支持全/半角冒号、标签内空格) | |---|---| | companyName | 公司名称、单位名称、单位名、名称 | | taxNo | 纳税人识别号、纳税识别号、统一社会信用代码、税号 | | companyAddress | 单位地址、地址(见 4.3 邮寄段排除) | | phoneFax | 电话/传真、电话传真、传真、电话 | | bankName | 银行开户行、开户银行、开户行 | | bankAccount | 银行账号、账号、帐号(去除内部空格) | | legalRepresentative | 法定代表人、法人代表、法人 | | agent | 代理人、委托代理人 | 「地址及电话」单独规则:整行取值后,若末尾匹配固话/手机则拆到 `phoneFax`,其余为 `companyAddress`。 ### 4.3 段落与冲突 - 若存在「发票邮寄地址」标题,其后块内的「姓名 / 电话 / 地址」**不**写入 `companyName` / `phoneFax` / `companyAddress`,而是汇总进 `remarkParts`(如 `邮寄:汪璇 / 188… / 地址…`)。 - 「开票资料」等总标题行忽略。 - 银行行号 / 联行号 / 行号 → `remarkParts`:`银行行号:…`(去空格)。 - 邮编 → `remarkParts`:`邮编:…`。 ### 4.4 验收样例(必须通过) **样例 1(安庆山峡):** 公司名称、税号、开户行、帐号、地址、电话/传真正确回填;银行行号、邮编、邮寄信息进 remark。 **样例 2(江峰生物):** 单位名、税号、地址及电话拆分、开户行、带空格账号去空格、电话正确;银行行号进 remark。 单测:`web-admin/src/utils/crm-invoice-paste-recognize.test.ts`(与现有 vitest 风格一致,如 `pl-cert-slots.test.ts`)。 ## 5. 数据流 ``` 用户粘贴文本 → parseCrmInvoicePaste(text) → 覆盖 form 对应键(仅当解析值非空) → remark:将 remarkParts 用「;」拼接后追加到现有 remark → ElMessage 提示 filledCount / warnings → 用户点确定 → 现有 /web/crm/invoice-info/add|update(不变) ``` 覆盖规则细节:某字段解析值为空字符串时,**不**清空该表单字段(只覆盖「识别到了」的字段)。 ## 6. 风险与后续 | 风险 | 应对 | |---|---| | 新标签写法未覆盖 | 补别名;UI 提示可手动改 | | 无标签散文文本 | 识别失败,手动填写 | | 长期需行号等独立字段 | 另开变更加列;本设计不扩表 | 可选后续(本变更不做):规则失败时降级调大模型。 ## 7. 实现清单(实现阶段) 1. 新增 `crm-invoice-paste-recognize.ts` + 单测(含两段业务样例) 2. 改 `InvoiceInfo.vue`:顶部粘贴区 + 识别回填 3. 保留/更新 Demo:`docs/demo-crm-invoice-paste-recognize.html`(与线上规则一致即可) 4. 更新 graphify(web-admin) 不改 backend、不改 SQL、不改 OpenSpec 基线(纯前端体验增强)。