docs: add design for quote/contract alias and brand inline manage dialog.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-07 12:38:26 +08:00
1 parent 9788e3ac17
commit 22014ba3fa
1 file changed
+127
@@ -0,0 +1,127 @@
# 报价/合同明细:别名与品牌就地维护弹窗
**日期:** 2026-08-07
**状态:** 待实现
**选定方案:** 抽公共轻量维护弹窗;写回 CRM 产品主数据;弹窗内即时保存;报价单 + 合同同步接入
**前置:**
- `docs/superpowers/specs/2026-07-27-crm-product-brand-alias-design.md`(品牌/别名主数据)
- `docs/superpowers/specs/2026-07-30-quote-contract-brand-alias-ux-design.md`(明细行列交互:下拉 / `—`)
- `docs/superpowers/specs/2026-08-07-crm-product-selector-enhance-design.md`(选择器内可打开完整产品编辑;本需求为明细行旁的轻量入口,不替代完整产品表单)
## 1. 目标
在报价单、合同的新增/编辑「产品明细」中,别名列与品牌列旁增加 icon 按钮;点击弹出维护弹窗,对当前产品的别名或品牌做新增、编辑、删除;保存后写回产品主数据,并立即刷新明细行下拉选项,开单过程中无需跳转产品管理页。
## 2. 非目标
- 不新增后端接口、不新增库表/字段
- 不把别名/品牌做成报价行/合同行私有数据
- 不在只读查看弹窗、列表展开子表、打印模板中增加维护入口
- 不改 `contract/form.vue`(当前无别名/品牌明细列)
- 不打开完整 `ProductFormDialog`(避免误改价格、分类等);本需求仅维护别名或品牌子集
- 不做乐观锁/并发合并(与现有产品编辑一致:后写覆盖)
## 3. 已确认决策
| 决策点 | 结论 |
|--------|------|
| 数据归属 | 写回产品主数据(`crm_product_alias` / `crm_product_brand`) |
| 保存时机 | 弹窗内点确定 → 立即 `fetchUpdateProduct` → 成功后刷新下拉 |
| 范围 | 报价单 + 合同一起做 |
| 权限 | 能编辑报价单/合同即可点维护;不单独按产品菜单权限藏按钮;接口无权限时提示错误 |
| 实现路径 | 公共组件 + 复用 `products/detail`、`products/update` |
## 4. 架构
```
web-admin/src/components/business/
product-alias-brand-dialog/index.vue # 新增:别名或品牌轻量维护弹窗
web-admin/src/views/crm/quote/components/QuoteFormPanel.vue
→ 别名/品牌列旁 icon;打开弹窗;saved 后刷新同 productId 行 options
web-admin/src/views/crm/contract/components/ContractFormDialog.vue
→ 同上
```
### 4.1 ProductAliasBrandDialog
| 项 | 约定 |
|----|------|
| Props | `visible`(或 `v-model`)、`productId: string`、`mode: 'alias' \| 'brand'`、可选 `productName`(标题展示) |
| Emits | `update:visible`、`saved`(payload:更新后的产品详情或 `aliases`/`brands`,便于父组件重建 options) |
| 打开 | `fetchProductDetail({ id })` 填充对应列表;保留项上 `id` 以便更新替换 |
| 提交 | `fetchUpdateProduct({ id, aliases })` 或 `{ id, brands }`,**不传**其他产品字段(后端仅在 kwargs 含 `brands`/`aliases` 时分别 `_replace_*`) |
| UI | `dialog-form-layout`,宽约 560~640px;标题 `维护别名 - {产品名}` / `维护品牌 - {产品名}`;底栏 Close/Check |
列表交互对齐产品表单子卡片:
- **别名**:名称输入 + 行删;顶栏「新增别名」
- **品牌**:名称 + 状态(`crm_product_status`,默认 `on`)+ 行删;顶栏「新增品牌」
- 空态:`暂无别名/品牌,点击上方新增`
### 4.2 明细列布局(报价 + 合同)
单元格横向:
```
[ el-select(有 options)| 灰色 —(无 options) ] [ link icon 按钮 ]
```
- icon:`Setting` 或 `Edit`(Element Plus),`link`/`text` 小按钮
- **无论是否已有 options 都显示** icon(无数据时用于新增)
- 仅当行有 `productId` 且处于可编辑表单时显示
- 下拉选项规则不变:仅上架(`on`)品牌进入 `brandOptions`(`initLineBrandMetaFromProduct`)
## 5. 校验与错误处理
弹窗确定前:
- 名称必填,trim 后非空
- 同列表内名称不可重复
- 品牌须有状态
提交失败:
- 保留弹窗与编辑内容
- 展示后端错误(含品牌仍有库存 / 被未归档计划引用无法删除等)
行内删除:先本地移除,点弹窗「确定」才落库;取消/关闭不落库。
## 6. 父组件刷新规则
维护成功后,对当前表单中 **相同 `productId` 的所有行**:
| 场景 | 行为 |
|------|------|
| 别名 | 更新 `aliasOptions`;当前 `aliasName` 不在新列表 → 清空 |
| 品牌 | `initLineBrandMetaFromProduct` 重建 `brandOptions`;当前 `brandId` 不在上架选项 → `brandMode=any`,清空 `brandId`/`brandName` |
| 从无到有 | `—` → 下拉,可立即选择 |
| 从有到无 | 下拉 → `—`,并清空该行已选别名/品牌 |
**不**自动把新建项选中到当前行,避免误改已填内容。
## 7. 验收要点
1. 无别名/品牌时显示 `—` + icon;新增保存后下拉出现并可选用。
2. 已有选项时 icon 可增删改;保存后 options 立即更新。
3. 删除当前行已选别名/品牌并保存后,选择被清空或回退「不限品牌」。
4. 同一产品多行时,一行维护成功后同 `productId` 各行 options 同步。
5. 品牌删除受库存/计划约束时,弹窗保留并提示,主数据不变。
6. 报价单与合同行为一致。
7. 取消或关闭弹窗不落库。
## 8. 实现顺序
1. 新建 `product-alias-brand-dialog`
2. 接入 `QuoteFormPanel`(列 UI + saved 刷新)
3. 接入 `ContractFormDialog`(同上)
4. 按第 7 节手工验收
## 9. 相关代码锚点
- 明细列现状:`QuoteFormPanel.vue` / `ContractFormDialog.vue` 别名、品牌列(无 options 时 `—`)
- 选项构建:`web-admin/src/utils/crm-product-brand.ts`(`initLineBrandMetaFromProduct` 等)
- 产品子卡片参考:`web-admin/src/components/business/product-form-dialog/index.vue`
- 后端替换:`backend/app/crud/crm/product.py`(`_replace_product_brands` / `_replace_product_aliases`;品牌删除 `_assert_brand_deletable`)