diff --git a/openspec/changes/dict-driven-enums/.openspec.yaml b/openspec/changes/dict-driven-enums/.openspec.yaml new file mode 100644 index 0000000..a369219 --- /dev/null +++ b/openspec/changes/dict-driven-enums/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-11 diff --git a/openspec/changes/dict-driven-enums/design.md b/openspec/changes/dict-driven-enums/design.md new file mode 100644 index 0000000..4f844b3 --- /dev/null +++ b/openspec/changes/dict-driven-enums/design.md @@ -0,0 +1,70 @@ +## Context + +系统枚举的「值」目前是代码里的魔法字符串:后端收敛在 `src/common/enums.ts`(CommonStatus/UserStatus/ResourceType/DataScope/LoginStatus),前端则散布在 6+ 页面的 `STATUS_MAP`/`TYPE_MAP`/`DATA_SCOPE_MAP`。字典表(sys_dict_type / sys_dict_item)与字典管理页已存在但 seed 为空。见 proposal.md 动机。 + +关键约束: +- 枚举的 **value 被业务逻辑分支依赖**(登录校验 `status==='enabled'`、菜单树取 `type='menu'`、`resolveDataScope` 分支),因此 value 必须稳定,字典只承载「展示层」的 label/颜色/排序。 +- 后端 `enums.ts` 仍是代码逻辑唯一枚举值源;dict 是展示层映射,两者靠 value 字符串对齐。改值要两边同步,但本次只新增不改值。 + +## Goals / Non-Goals + +**Goals:** +- 让前端枚举渲染完全字典驱动,消除本地硬编码中文/颜色映射。 +- 内置字典值受保护(不可删、value 不可改),label/排序/颜色可定制。 +- 组件库支持 `dictCode` 声明,降低页面配置成本。 + +**Non-Goals:** +- 不改任何枚举的现有 value(避免破坏既有登录/鉴权/菜单逻辑)。 +- 不做「纯业务字典」(性别/学历等)——当前模型无对应字段,等业务来了再加。 +- `http_method` 不进字典(固定标准、后端枚举校验)。 +- 不做字典版本号/增量同步/多级缓存失效,够用即可。 + +## Decisions + +### D1. 保护标记:类型级 + 项级 `built_in` 各加一列 +`dict_type.built_in`(Boolean,默认 false,内置类型不可删)+ `dict_item.built_in`(Boolean,默认 false,内置项 value 不可改、项不可删)。 +- 备选:仅项级(空的内置类型会被误删)、仅硬编码保护清单(散在代码、不透明)。选两列,粒度最清晰,语义直白。 + +### D2. 颜色存独立列 `color_type` +`dict_item.color_type`(String?,取值 primary/success/warning/danger/info,可空)。 +- 备选:通用 `ext` JSON 列(弱类型、字典页要做自定义编辑)、按 sort 猜颜色(不可控)。选独立列,语义化、前后端都简单,且复用前端既有 `TagType` 联合类型。 + +### D3. 保护规则在后端 service 层强制,前端只做「降级展示 + 交互禁用」 +`dicts.service` 的 update/delete 前置校验:内置类型删除、内置项删除、内置项改 value 一律拒绝(返回 4xx 明确错误);label/sort/color_type 放行。前端字典管理页展示「内置」徽标、禁用 value 输入与删除入口,但**不把前端当安全边界**,后端兜底。 + +### D4. 聚合查询走新端点 `GET /dicts` +一次返回当前租户全部启用字典:`{ list: [{ code, name, items: [{ label, value, colorType, sort }] }] }`,item 按 sort 升序、仅启用。登录后拉一次缓存。 +- 备选:复用 `listDictTypes` + 逐类型 `listDictItems`(N+1 请求,5 个内置字典就 6 次)。选聚合端点,一次搞定。 + +### D5. 前端字典 store + `useDict` +- `dict.ts` Pinia store:`dicts: Record` + `loaded` + `fetch()`(调 GET /dicts)+ `reset()`(登出清空)。 +- `useDict(code)`:computed 返回该 code 的响应式字典项数组,附带 `tagMap`(value→{label,type})与 `options`(value→label,供下拉)两个衍生视图。 +- 加载时机:`layout/default.vue` 在菜单拉取后触发 `dictStore.fetch()`;`main.ts` 的 auth 失效回调加 `dictStore.reset()`。 + +### D6. 组件 `dictCode` 声明与优先级 +- `JsonSchemaProperty` 增 `dictCode?: string`:ty-form / ty-search-form 的 select 字段若声明 `dictCode`,忽略手写 `enum`/`enumNames`,用 `useDict` 的 options 渲染下拉。 +- `TableColumn` 增 `dictCode?: string`:ty-table 列若声明 `dictCode`,忽略 `tagMap`,用字典渲染标签(label + colorType)。 +- 优先级:`dictCode` > 手写 `enum`/`tagMap`。已声明 `dictCode` 的字段不再需要 `enum`/`enumNames`。 + +### D7. seed 幂等 +按 `(tenantCode, code)` upsert dict_type、按 `(tenantCode, dictTypeCode, value)` upsert dict_item;内置项置 `built_in=true`。5 个字典项的值与 `enums.ts` 完全一致。 + +## Risks / Trade-offs + +- **[内置项 value 被前端绕过]** → 后端 service 层强制校验兜底(D3),前端禁用只是体验优化。 +- **[字典缓存滞后:改 label 后需刷新才生效]** → 提供 `useDict.refresh`;字典管理页保存后调用 `dictStore.fetch()` 重拉。不追求实时推送。 +- **[停用某字典类型后 useDict 返回空]** → 组件降级:无映射时直接展示原始 value 字符串,避免空白。 +- **[enums.ts 与 seed 取值漂移]** → 两者值均由 spec(shared/data-model)锚定,本次不改值;后续改动需同步更新 seed 与 enums.ts,并在 spec 落一条。 +- **[加列迁移]** → 均为可空/带默认值的新增列,`prisma migrate` 增量即可,无回滚风险;向后兼容(旧数据 built_in 默认 false)。 + +## Migration Plan + +1. Prisma schema 加列 → `prisma migrate dev` 生成增量 migration。 +2. 更新 seed,跑 `pnpm seed` 预置 5 个内置字典。 +3. 后端加保护校验 + `GET /dicts`。 +4. 前端加 dict store/useDict + 组件 dictCode + 页面去硬编码。 +5. 回滚:加列可 `prisma migrate resolve --rolled-back`(无破坏性变更);前端无外部依赖,随代码回滚。 + +## Open Questions + +(无 —— 关键决策已在本 design 与用户确认中敲定) diff --git a/openspec/changes/dict-driven-enums/proposal.md b/openspec/changes/dict-driven-enums/proposal.md new file mode 100644 index 0000000..0a4e5f7 --- /dev/null +++ b/openspec/changes/dict-driven-enums/proposal.md @@ -0,0 +1,35 @@ +## Why + +系统里「启用/禁用」「菜单/按钮/接口」「数据权限档位」等枚举值以中文字面量硬编码在前端 6+ 个页面,后端虽已收敛在 `common/enums.ts`,但前端每个页面各自重复一份 `STATUS_MAP`/`TYPE_MAP`/`DATA_SCOPE_MAP`,改一处文案/颜色要改多处,且无法按租户定制展示。字典表(dict_type/dict_item)与字典管理页已存在但 seed 为空、无任何内置数据,等于「有字典能力却无字典可读」。本次把系统枚举真正字典化:内置字典 seed 预置、值受保护、前端用 `useDict` 读字典渲染,并让组件库支持按字典 code 声明下拉/枚举,从根上消除硬编码。 + +## What Changes + +- **数据模型**:`dict_type` 加 `built_in`(内置类型标记),`dict_item` 加 `built_in`(内置项标记)与 `color_type`(标签色 primary/success/warning/danger/info,可空)。 +- **内置字典**:seed 预置 5 个系统内置字典及其字典项(值 + 中文 label + 标签色 + 排序):`common_status`(启用/停用)、`user_status`(启用/停用/锁定)、`resource_type`(菜单/按钮/接口)、`data_scope`(全部/本部门/本部门及以下/仅本人/自定义)、`login_status`(成功/失败)。 +- **保护规则(后端)**:内置类型不可删;内置项不可删、`value` 不可改;`label`/`sort`/`color_type` 可改;内置类型下可新增「非内置」项(完全自由)。普通字典类型/项不受影响。 +- **聚合查询接口**:新增 `GET /dicts` 一次返回全部启用字典类型及其启用字典项(按 sort 升序),供前端缓存。 +- **前端字典机制**:新增 dict store + `useDict(dictCode)` Hook(登录后一次性拉取并缓存),提供「字典项数组 / 标签映射 / 下拉选项」三种形态。 +- **组件库扩展**:`ty-form` / `ty-search-form` 的 JSON Schema 字段支持 `dictCode`(替代手写 `enum`/`enumNames` 自动解析下拉选项);`ty-table` 的列支持 `dictCode`(自动渲染标签)。 +- **页面去硬编码**:用户/角色/资源/组织/租户/字典/审计/登录日志等页面,删除本地 `*_MAP`,改为 `useDict` 读字典。`http_method`(请求方法)为固定标准,不进字典。 + +## Capabilities + +### New Capabilities +- `backend/dict`: 字典保护规则(内置类型/项不可删、value 不可改)+ 内置字典 seed 预置 +- `frontend/AD/dict`: 前端字典驱动渲染(useDict + dict store + 组件 dictCode 支持 + 页面去硬编码) + +### Modified Capabilities +- `shared/data-model`: dict_type/dict_item 新增 built_in / color_type 字段,并锚定 5 个内置枚举字典的取值集合 +- `shared/api-contract`: 新增字典聚合查询接口 `GET /dicts` + +## Impact + +- 数据库 `sys_dict_type` / `sys_dict_item` 各加列(built_in、color_type),需 Prisma migration。 +- 后端 `sources/sysmgr-nest`: + - `dicts/`:新增保护规则校验(update/delete 时按 built_in 拦截)、新增 `GET /dicts` 聚合接口 + - `prisma/seed.ts`:预置 5 个内置字典类型 + 字典项 +- 前端 `sources/sysmgr-admin`: + - 新增 `src/stores/modules/dict.ts` + `src/composables/useDict.ts` + - `src/types/crud.ts`:JsonSchemaProperty 增加 `dictCode`、TableColumn 增加 `dictCode` + - `src/components/ty-form.vue`、`ty-search-form.vue`、`ty-table.vue`:支持 dictCode 解析 + - 字典管理页:内置标记展示、value 保护、颜色编辑;用户/角色/资源/组织/租户/审计/登录日志页去硬编码 diff --git a/openspec/changes/dict-driven-enums/specs/backend/dict/spec.md b/openspec/changes/dict-driven-enums/specs/backend/dict/spec.md new file mode 100644 index 0000000..bdea6ef --- /dev/null +++ b/openspec/changes/dict-driven-enums/specs/backend/dict/spec.md @@ -0,0 +1,37 @@ +## Purpose + +定义字典域的保护规则与内置字典种子:内置字典类型与字典项受保护(不可删、value 不可改),仅 label/排序/颜色可编辑,并预置 5 个系统内置枚举字典。 + +## ADDED Requirements + +### Requirement: 内置类型保护 +内置字典类型(built_in 为真)SHALL 不可被删除。 + +#### Scenario: 删除内置类型被拒 +- **WHEN** 管理员尝试删除内置字典类型 +- **THEN** 后端拒绝并返回错误(内置类型不可删除) + +### Requirement: 内置项保护 +内置字典项(built_in 为真)SHALL 不可删除、value 不可修改;label/sort/color_type 可修改。 + +#### Scenario: 修改内置项 value 被拒 +- **WHEN** 管理员尝试修改内置字典项的 value +- **THEN** 后端拒绝并返回错误(内置项 value 不可修改) + +#### Scenario: 修改内置项 label 与排序 +- **WHEN** 管理员修改内置字典项的 label 或 sort +- **THEN** 修改成功 + +### Requirement: 新增非内置项 +内置字典类型下 SHALL 允许新增非内置项(built_in 为假),新项可自由编辑与删除。 + +#### Scenario: 内置类型下新增项 +- **WHEN** 管理员在内置类型下新增字典项 +- **THEN** 新项 built_in 为假,可正常增删改 + +### Requirement: 内置字典种子 +系统 seed SHALL 预置 5 个内置字典类型及字典项(含 label、color_type、sort):common_status、user_status、resource_type、data_scope、login_status。 + +#### Scenario: 幂等初始化 +- **WHEN** 运行 seed 脚本 +- **THEN** 5 个内置字典类型与字典项被创建,重复运行不重复插入 diff --git a/openspec/changes/dict-driven-enums/specs/frontend/AD/dict/spec.md b/openspec/changes/dict-driven-enums/specs/frontend/AD/dict/spec.md new file mode 100644 index 0000000..44ed4aa --- /dev/null +++ b/openspec/changes/dict-driven-enums/specs/frontend/AD/dict/spec.md @@ -0,0 +1,48 @@ +## Purpose + +定义前端字典驱动渲染机制:登录后缓存全部启用字典,组件按字典 code 声明下拉/标签,页面不再硬编码枚举中文映射,且字典管理页对内置项施加保护交互。 + +## ADDED Requirements + +### Requirement: 字典缓存 +前端 SHALL 在登录后一次性拉取全部启用字典(GET /dicts)并缓存于全局字典 store,登录失效时清空。 + +#### Scenario: 登录后缓存 +- **WHEN** 用户登录进入后台 +- **THEN** 前端拉取并缓存全部启用字典 + +#### Scenario: 登出清空 +- **WHEN** 用户登出 +- **THEN** 字典缓存被清空 + +### Requirement: useDict Hook +前端 SHALL 提供 useDict(dictCode) Hook,返回该字典类型的响应式字典项数组(label/value/color_type/sort)。 + +#### Scenario: 读取字典项 +- **WHEN** 组件调用 useDict('common_status') +- **THEN** 返回 启用(enabled)/停用(disabled) 等字典项 + +### Requirement: 组件字典 code 声明 +JSON Schema 字段 SHALL 支持 dictCode 声明:ty-form / ty-search-form 的 select/enum 字段用 dictCode 自动解析下拉选项(label/value);ty-table 列用 dictCode 自动渲染标签(按 color_type 着色)。 + +#### Scenario: 下拉按字典渲染 +- **WHEN** 搜索/表单字段声明 dictCode='resource_type' +- **THEN** 下拉选项按字典项渲染为 菜单/按钮/接口 + +#### Scenario: 表格标签按字典渲染 +- **WHEN** 表格列声明 dictCode='common_status' +- **THEN** 单元格按字典项渲染启用/停用标签并按 color_type 着色 + +### Requirement: 字典管理保护交互 +字典管理页 SHALL 对内置类型/项展示「内置」标记,并禁用内置项的 value 编辑与删除(仅 label/排序/颜色可编辑)。 + +#### Scenario: 内置项 value 只读 +- **WHEN** 管理员在字典管理页编辑内置字典项 +- **THEN** value 输入框禁用,label/排序/颜色可编辑,且不提供删除入口 + +### Requirement: 字典化渲染生效 +各页面枚举字段 SHALL 按字典渲染:修改某字典项的 label 或 color_type 后,引用该字典的页面 SHALL 刷新后按新 label/颜色展示。 + +#### Scenario: 改 label 全局生效 +- **WHEN** 管理员把 common_status 中 enabled 的 label 由「启用」改为「可用」 +- **THEN** 所有引用 common_status 的页面(租户/角色/资源/组织等)刷新后展示「可用」 diff --git a/openspec/changes/dict-driven-enums/specs/shared/api-contract/spec.md b/openspec/changes/dict-driven-enums/specs/shared/api-contract/spec.md new file mode 100644 index 0000000..8cdcdc4 --- /dev/null +++ b/openspec/changes/dict-driven-enums/specs/shared/api-contract/spec.md @@ -0,0 +1,12 @@ +## ADDED Requirements + +### Requirement: 字典聚合查询接口 +系统 SHALL 提供 `GET /dicts` 接口,需登录态,按当前租户一次返回全部启用状态的字典类型及其启用状态的字典项(字典项按 sort 升序),供前端登录后缓存渲染。 + +#### Scenario: 一次性拉取 +- **WHEN** 前端调用 GET /dicts +- **THEN** 返回全部启用字典类型,每个类型含其启用字典项(label/value/color_type/sort) + +#### Scenario: 停用过滤 +- **WHEN** 某字典类型或字典项被停用 +- **THEN** GET /dicts 不返回该类型或该项 diff --git a/openspec/changes/dict-driven-enums/specs/shared/data-model/spec.md b/openspec/changes/dict-driven-enums/specs/shared/data-model/spec.md new file mode 100644 index 0000000..78b05ca --- /dev/null +++ b/openspec/changes/dict-driven-enums/specs/shared/data-model/spec.md @@ -0,0 +1,19 @@ +## ADDED Requirements + +### Requirement: 字典实体扩展 +dict_type 实体 SHALL 新增 built_in 字段(布尔,标记内置字典类型);dict_item 实体 SHALL 新增 built_in(布尔,标记内置字典项)与 color_type(字符串,取值 primary/success/warning/danger/info,可空)字段,用于前端标签渲染与内置保护。 + +#### Scenario: 内置类型标记 +- **WHEN** 某字典类型由系统 seed 预置 +- **THEN** 其 built_in 为真,后端据此保护其不被删除 + +#### Scenario: 内置项颜色 +- **WHEN** 内置字典项需要区分展示 +- **THEN** 其 color_type 指定标签色(如启用=success、锁定=danger、停用=info) + +### Requirement: 内置枚举字典取值 +系统 SHALL 预置 5 个内置枚举字典并锚定其取值集合:common_status={enabled,disabled}、user_status={enabled,disabled,locked}、resource_type={menu,button,api}、data_scope={all,dept,dept_and_child,self,custom}、login_status={success,fail}。 + +#### Scenario: 前端按字典渲染枚举 +- **WHEN** 前端渲染用户/角色/资源等页面的枚举字段 +- **THEN** 从对应内置字典读取 label 与 color_type,取值不脱离上述集合 diff --git a/openspec/changes/dict-driven-enums/tasks.md b/openspec/changes/dict-driven-enums/tasks.md new file mode 100644 index 0000000..85e63a8 --- /dev/null +++ b/openspec/changes/dict-driven-enums/tasks.md @@ -0,0 +1,36 @@ +## 1. 数据模型 + +- [x] 1.1 Prisma schema:`dict_type` 加 `built_in`(Boolean 默认 false),`dict_item` 加 `built_in`(Boolean 默认 false)与 `color_type`(String? 可空) +- [x] 1.2 生成并应用 Prisma migration(增量加列,无破坏) +- [x] 1.3 seed 预置 5 个内置字典类型 + 字典项(含 label / color_type / sort,按 `(tenantCode, code)` 与 `(tenantCode, dictTypeCode, value)` 幂等 upsert,内置项 `built_in=true`) + +## 2. 后端字典保护与聚合接口 + +- [x] 2.1 dicts.service:删除内置类型 / 删除内置项 拦截并返回明确错误 +- [x] 2.2 dicts.service:修改内置项 value 拦截(label / sort / color_type 放行) +- [x] 2.3 新增 `GET /dicts` 聚合接口(当前租户启用类型 + 启用项,项按 sort 升序) +- [x] 2.4 字典 DTO 补充 `built_in` / `color_type` 字段映射与校验 + +## 3. 前端字典机制 + +- [x] 3.1 新增 `src/stores/modules/dict.ts`(dicts / loaded / fetch / reset) +- [x] 3.2 新增 `src/composables/useDict.ts`(字典项数组 + tagMap + options 三种视图) +- [x] 3.3 `layout/default.vue` 登录后拉取字典;`main.ts` 登出/auth 失效清空字典缓存 + +## 4. 组件库 dictCode 支持 + +- [x] 4.1 `src/types/crud.ts`:JsonSchemaProperty 增 `dictCode`、TableColumn 增 `dictCode` +- [x] 4.2 ty-form / ty-search-form:字段声明 `dictCode` 时自动解析下拉选项(忽略手写 enum/enumNames) +- [x] 4.3 ty-table:列声明 `dictCode` 时按字典渲染标签(label + color_type 着色) + +## 5. 页面去硬编码与保护交互 + +- [x] 5.1 字典管理页:内置类型/项「内置」标记 + value 只读/禁删 + color_type 编辑 +- [x] 5.2 用户 / 角色 / 资源 / 组织 / 租户页删除本地 `STATUS_MAP` / `TYPE_MAP` / `DATA_SCOPE_MAP`,改用 dictCode / useDict +- [x] 5.3 审计 / 登录日志页删除 success/fail 硬编码映射,改用 dictCode + +## 6. 验证 + +- [x] 6.1 后端 `pnpm build` + 保护规则与 `GET /dicts` 冒烟验证 +- [x] 6.2 前端 `pnpm type-check` + `pnpm build` 通过 +- [x] 6.3 联调验收:改字典 label 全局生效、内置项 value 受保护、下拉/标签字典驱动渲染 diff --git a/sources/sysmgr-admin b/sources/sysmgr-admin index c96151d..23e2287 160000 --- a/sources/sysmgr-admin +++ b/sources/sysmgr-admin @@ -1 +1 @@ -Subproject commit c96151d2ae0523daf57abbf93ff710b506a6f107 +Subproject commit 23e228783cff43f59157e3b8975a57d11cda315d diff --git a/sources/sysmgr-nest b/sources/sysmgr-nest index 0823c75..73ed067 160000 --- a/sources/sysmgr-nest +++ b/sources/sysmgr-nest @@ -1 +1 @@ -Subproject commit 0823c750b2d4f1f38a6508d81e053379ca34886c +Subproject commit 73ed067a82a52dce7ecad1be30d7f4e1be88092b