7.6 KiB
7.6 KiB
字典下拉手输即入库(DictSelect + ensure)— 设计文档
日期: 2026-08-21
状态: 已实现
仓库: shanxia2.0(backend/+web-admin/)
方案: 方案 1 — 通用DictSelect+ 通用dict-data/ensureAPI
影响范围: 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 等) |
行为:
dictType不在白名单 → 400- 同租户、未删除、同
dict_type下已有相同dict_label→ 返回已有项,created: false - 否则插入
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 |
行为:
- 白名单校验
- 仅当记录存在、未删除、且
create_by == 当前用户时允许软删(del_flag=1) - 否则 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 |
透传 |
行为:
- 选项来自
dictStore.getOptions(dictType) - change 时若值不在已有 options → 确认框;取消则不更新 v-model
- 确认后 ensure;成功则 v-model = 返回 value,并刷新 options
sessionCreated中的项在 option 槽显示删除图标;确认后 ensureRemove- ensure/删除期间 loading,防重复提交
- 首期不做 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 改造一致
- 单一入口:仅
DictSelect - 白名单双检:前端常量 + 后端拒绝
- 对照表验收:上表每个字段走一遍手输 → 确认 → 可选 → 会话可删 → 刷新无删按钮
- Code review 门禁:新增字典可创建下拉必须用
DictSelect,禁止再抄allow-create空壳 handler
测试要点
- 手输新包装规格 → 确认 → 立刻在下拉与字典中可见
- 取消确认 → 不入库
- 报价/合同/运输计划互开 → 能选到同一新值
- 本会话删除 → 软删且下拉消失
- 预置「自提」「一票制送到」无删除按钮;接口无法删预置
- 刷新后会话删除按钮消失
- 非白名单 dictType → 400
- 客户/线索园区字段行为一致
非目标
- 不做跨用户删除治理 UI
- 不做 DictSelect 多选首期
- 不清理历史已手输但未入字典的脏业务数据(可另开任务)