Files
shanxia2.0/docs/superpowers/specs/2026-08-21-dict-select-ensure-design.md
T
2026-08-21 16:50:47 +08:00

7.6 KiB
Raw Blame History

字典下拉手输即入库(DictSelect + ensure)— 设计文档

日期: 2026-08-21
状态: 已实现
仓库: shanxia2.0(backend/ + web-admin/)
方案: 方案 1 — 通用 DictSelect + 通用 dict-data/ensure API
影响范围: web-admin 报价/合同/运输计划/客户/线索等字典可创建下拉;系统字典 sys_dict_data

背景

客户希望:字典表驱动的下拉在支持手动输入新值时,能把新值写入字典,后续可直接选择,不必每次手输。

现状:

  • 包装规格、单位、园区等已开 allow-create,注释写「写入字典」,但 handlePackageSpecChange 等仅改行内字段,未落库。
  • 送货方式(crm_shfs)为主路径字典下拉,未开 allow-create。
  • 值班排班已有 POST /web/admin/duty/dict/ensure 可复用思路,但是值班专用白名单。

已确认决策

# 决策
D1 范围 C:所有「已开 allow-create 且选项来自字典」的字段,统一改造
D2 送货方式与包装规格同一套:补 allow-create + ensure
D3 写入时机:选中/确认后立刻写入(非等业务单据保存)
D4 新值须 二次确认 后再 ensure(A')
D5 权限:能打开该业务页的登录用户均可 ensure;后端 字典类型白名单 限权
D6 删除:仅本会话刚新增的项在下拉旁显示删除;预置项不可删;后端仅允许软删当前用户创建的项
D7 实现路径:通用 DictSelect + 通用 ensure/ensure-remove API(方案 1)
D8 合同单位字段现无 allow-create,与报价对齐时一并加上 DictSelect

白名单(首批)

dict_type 用途
crm_product_gg 包装规格
crm_shfs 送货方式
crm_product_unit 单位
crm_customer_park 所在园区

后续扩容:改前后端同一白名单常量即可。值班三类(duty_*)可后续迁入,本次不强迁值班页。

方案选择

采用方案 1:DictSelect + POST /web/admin/dict-data/ensure(及 remove)。

未采用:

  • 各页手写 el-select + 工具函数(易行为不一致)
  • 直接调字典管理「创建」接口(权限与白名单不匹配)

架构

业务页
  └── <DictSelect dict-type="crm_shfs" v-model="..." />
          │
          ├─ options ← dictStore.getOptions(dictType)
          ├─ 新值 → ElMessageBox 确认 → dictStore.ensure
          │            → POST /web/admin/dict-data/ensure
          │            → upsert 本地 dictMap + sessionCreated
          └─ 本会话新增 → option 旁删除 → dictStore.ensureRemove
                       → 软删 + 移出 dictMap / sessionCreated
层 职责
DictSelect 唯一 UI 入口;确认、loading、会话删除按钮
dictStore options、ensure、ensureRemove、sessionCreated、本地 upsert/remove
backend 白名单校验、按 label 复用或插入、软删且仅允许当前用户创建的项

后端接口

POST /web/admin/dict-data/ensure

鉴权:管理端已登录用户(与业务页一致),不要求字典管理菜单权限。

入参 说明
dictType 必须在白名单
label trim 后非空
preferredValue? 可选;缺省则 dict_value = dict_label(对齐现有 crm_shfs 等)

行为:

  1. dictType 不在白名单 → 400
  2. 同租户、未删除、同 dict_type 下已有相同 dict_label → 返回已有项,created: false
  3. 否则插入 sys_dict_data(tenant_no 取当前用户),返回 created: true

响应:{ label, value, created: boolean }

可抽取与 duty_schedule.ensure_dict_data 同类的通用服务(白名单入参化);值班接口可继续薄封装或后续切换。

POST /web/admin/dict-data/ensure-remove(或等价 DELETE)

入参 说明
dictType 白名单
value dict_value

行为:

  1. 白名单校验
  2. 仅当记录存在、未删除、且 create_by == 当前用户 时允许软删(del_flag=1)
  3. 否则 400(防止删预置项或他人创建项)

前端「仅本会话显示删除」是体验约束;后端以 create_by 兜底。

前端设计

dictStore 增补

  • sessionCreated: Record<string, Set<string>>(dictType → values)
  • ensure(dictType, label): Promise<{ label, value, created }>
  • ensureRemove(dictType, value): Promise<void>
  • upsertLocal / removeLocal:更新 dictMap,避免每次全量 loadAllDict
  • 仅当 created === true 时写入 sessionCreated
  • 刷新页面 / clearDict 时清空 sessionCreated

组件 web-admin/src/components/DictSelect/index.vue

Prop 说明
dictType 必填
modelValue v-model
allowCreate 默认 true
clearable / filterable / placeholder / size / disabled 透传

行为:

  1. 选项来自 dictStore.getOptions(dictType)
  2. change 时若值不在已有 options → 确认框;取消则不更新 v-model
  3. 确认后 ensure;成功则 v-model = 返回 value,并刷新 options
  4. sessionCreated 中的项在 option 槽显示删除图标;确认后 ensureRemove
  5. ensure/删除期间 loading,防重复提交
  6. 首期不做 multiple

业务页禁止直接调 ensure API;一律用 DictSelect。

改造清单

界面 文件 字段
报价单 QuoteFormPanel.vue 单位、包装规格、送货方式
合同弹窗 ContractFormDialog.vue 单位、包装规格、送货方式
合同独立页 contract/form.vue 同上
运输计划 PlPlanFormProductTable.vue 单位、包装规格、送货方式
客户 CustomerFormDialog.vue 所在园区
线索 clue/form.vue 所在园区

删除空壳 handlePackageSpecChange / handleUnitChange 中「仅赋值」逻辑(若无其它职责)。

不在范围

  • 非字典 allow-create:维保类型、尿素单位、加油站、证件名、DbCrmCustomerSelect
  • 旧版 pl/plan/form.vue 写死送货方式
  • 强制迁移值班 ensure UI
  • 字典管理页大改(仍可手动维护预置项)

错误与边界

  • 空串 / 纯空格:不调接口
  • 用户取消确认:不写入、不选中
  • ensure / 删除失败:ElMessage.error,状态回滚
  • 历史业务数据保留原文案;软删后下拉不再出现,展示回退为原文案
  • 合同「自提」条款仍按存库文案匹配现有逻辑,不因本功能改变匹配规则

如何保证 web-admin 改造一致

  1. 单一入口:仅 DictSelect
  2. 白名单双检:前端常量 + 后端拒绝
  3. 对照表验收:上表每个字段走一遍手输 → 确认 → 可选 → 会话可删 → 刷新无删按钮
  4. Code review 门禁:新增字典可创建下拉必须用 DictSelect,禁止再抄 allow-create 空壳 handler

测试要点

  1. 手输新包装规格 → 确认 → 立刻在下拉与字典中可见
  2. 取消确认 → 不入库
  3. 报价/合同/运输计划互开 → 能选到同一新值
  4. 本会话删除 → 软删且下拉消失
  5. 预置「自提」「一票制送到」无删除按钮;接口无法删预置
  6. 刷新后会话删除按钮消失
  7. 非白名单 dictType → 400
  8. 客户/线索园区字段行为一致

非目标

  • 不做跨用户删除治理 UI
  • 不做 DictSelect 多选首期
  • 不清理历史已手输但未入字典的脏业务数据(可另开任务)