Compare commits

...

48 Commits

Author SHA1 Message Date
liuyun
a397981b90 docs: 补 backend users/roles/orgs/tenants/configs 主 spec + 审计清单收尾
Some checks are pending
CI / backend (push) Waiting to run
CI / frontend (push) Waiting to run
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 17:03:06 +08:00
liuyun
7fbc0eae60 chore: track Dockerfiles
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 17:02:18 +08:00
liuyun
380195fffc ci: 补 CI + docker-compose + 前后端 Dockerfile
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 17:02:08 +08:00
liuyun
0d615be363 docs: 审计清单 P3 小项全部完成
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:58:29 +08:00
liuyun
bedd964dc6 docs: README 克隆命令对齐子模块端口 + 补 MIT LICENSE + 审计清单 P3 小项
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:58:07 +08:00
liuyun
317cf2f575 docs: 审计清单 M2/M7/L8 完成(track 前后端)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:54:19 +08:00
liuyun
e951b70b1a docs: 审计清单 M5/M12/L7 完成(track 前后端)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:46:13 +08:00
liuyun
1edd78326a docs: 审计清单 M3/M14/M15/L5 完成(track 前后端)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:43:31 +08:00
liuyun
5038b43d18 docs: 审计清单 M20 完成(track 前后端)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:41:26 +08:00
liuyun
a7567f2908 docs: 审计清单 P1 全部完成(track 前后端)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:34:28 +08:00
liuyun
bb7c79b981 docs: 审计清单 H8/M21 完成 + guide 修正 JWT_SECRET
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:29:19 +08:00
liuyun
207999f061 docs: 审计清单 M18 完成(track 前端)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:22:19 +08:00
liuyun
38bf9e8ebc docs: 审计清单 H7/H9 完成(track 前后端)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:20:43 +08:00
liuyun
72a35c189e docs: 审计整改清单 + P0 完成(track 前后端)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 16:11:11 +08:00
liuyun
0e257d444d docs: add developer guide for the scaffold
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 15:01:25 +08:00
liuyun
8743e68451 chore: archive dict-driven-enums change + sync specs to main specs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 14:44:56 +08:00
liuyun
7a5af292fb chore: track dict-driven-enums change
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 14:30:50 +08:00
liuyun
60501eac20 chore: archive fe-admin-user-page change + sync user spec to main specs
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 13:26:18 +08:00
liuyun
1bb89ce437 chore: track sysmgr-admin Tailwind style refactor
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 13:23:30 +08:00
liuyun
2818c8108d chore: track sysmgr-admin home welcome page
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 13:03:48 +08:00
liuyun
60f2ef9b4f chore: track sysmgr-admin + sysmgr-nest (tenant management)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 12:50:32 +08:00
liuyun
5283bd8bb3 chore: track sysmgr-admin + sysmgr-nest (all management pages wired)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 12:46:21 +08:00
liuyun
b2771608cc chore: track sysmgr-admin + sysmgr-nest (menu/axios/useRequest integration)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 12:19:49 +08:00
liuyun
b321327789 chore: track sysmgr-admin login/register/forgot pages
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 11:34:48 +08:00
liuyun
2390934150 chore: track sysmgr-admin resource page + 404 + header fixes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 11:06:28 +08:00
liuyun
d86b05ea18 chore: track sysmgr-admin dictionary & organization pages
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 10:44:12 +08:00
liuyun
49080d0fa8 chore: track sysmgr-admin unified dialog manager
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 10:17:09 +08:00
liuyun
30bfe5bdb2 chore: track sysmgr-admin config management page
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 10:06:47 +08:00
liuyun
28c23f3b78 chore: track sysmgr-admin multi-tab completion
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 10:04:06 +08:00
liuyun
ef0c1f1a15 chore: track sysmgr-admin Element Plus CSS + formatting tweaks
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 09:59:43 +08:00
liuyun
78e8e137ba chore: track sysmgr-admin search enter-to-submit
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 09:53:26 +08:00
liuyun
9d8381f2a0 chore: track sysmgr-admin search buttons align fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 09:50:45 +08:00
liuyun
ddeb15c9ad chore(openspec): track toolbar/selection/scroll in fe-admin-user-page
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 09:46:39 +08:00
liuyun
b181d2cd0c chore(openspec): track search width + action collapse in fe-admin-user-page
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 09:34:39 +08:00
liuyun
5a941f8ee6 chore(openspec): track ty-form rename + search collapse in fe-admin-user-page
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 09:28:00 +08:00
liuyun
1272b48fc1 chore(openspec): track fe-admin-user-page component extraction
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 01:26:11 +08:00
liuyun
ad002003fe chore(openspec): track fe-admin-user-page implementation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 01:16:08 +08:00
liuyun
5e002cac74 chore(openspec): archive fe-admin-layout, sync frontend layout spec to main
- 归档 fe-admin-layout(管理后台布局壳 + 主题系统)
- 新增主 spec:frontend/AD/layout

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 01:09:15 +08:00
liuyun
a53ce4c196 chore: track sysmgr-admin sidebar auto-hide
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 01:07:49 +08:00
liuyun
9a7833c198 chore: track sysmgr-admin header nav height fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:57:08 +08:00
liuyun
f1db2aba57 chore: track sysmgr-admin sidebar margin/popup fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:54:41 +08:00
liuyun
eb8bdddfa6 chore: track sysmgr-admin sidebar icon fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:49:10 +08:00
liuyun
c68168c026 chore: track sysmgr-admin logo centering + dark theme fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:43:46 +08:00
liuyun
2b2792b433 chore: track sysmgr-admin layout visual polish
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:40:16 +08:00
liuyun
b8fdb6cf36 chore(openspec): refine fe-admin-layout (mixbar scope, logo area, footer)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:35:56 +08:00
liuyun
d9178624b1 chore: track sysmgr-admin footer fill fix
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:26:05 +08:00
liuyun
f7f9315a5e chore(openspec): correct layout header semantics in fe-admin-layout
左右布局顶栏仅不含菜单,固定顶栏对所有布局生效。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:23:33 +08:00
liuyun
6bb227d635 chore(openspec): track fe-admin-layout implementation
sysmgr-admin 布局壳:Element Plus + 可配置布局/主题系统(5 种模式 + 主题色 + 暗黑 + 折叠/宽度 + 固定顶栏/底栏 + 面包屑/多页签/全屏/手风琴)。任务全部完成,待归档。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-11 00:13:10 +08:00
37 changed files with 1853 additions and 3 deletions

48
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,48 @@
name: CI
on:
push:
pull_request:
jobs:
backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: sources/sysmgr-nest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: pnpm/action-setup@v4
with:
version: 12
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: sources/sysmgr-nest/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm test
frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: sources/sysmgr-admin
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: pnpm/action-setup@v4
with:
version: 12
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
cache-dependency-path: sources/sysmgr-admin/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm type-check
- run: pnpm build-only

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 云南投云科技有限公司
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -32,7 +32,7 @@ openspec view # 交互式看板
克隆(含子模块): 克隆(含子模块):
```bash ```bash
git clone --recurse-submodules git@git.dianyunshouhu.com:touyun_liuyun/System_Management.git git clone --recurse-submodules ssh://git@git.dianyunshouhu.com:2222/touyun_liuyun/System_Management.git
``` ```
## 目录 ## 目录

19
docker-compose.yml Normal file
View File

@ -0,0 +1,19 @@
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: Mysql@123
MYSQL_DATABASE: sysmgr
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
command: --default-authentication-plugin=mysql_native_password
redis:
image: redis:7
ports:
- "6379:6379"
volumes:
mysql-data:

63
docs/audit-checklist.md Normal file
View File

@ -0,0 +1,63 @@
# 脚手架审计整改清单
> 状态标记:`⬜ 待处理` · `🔶 需讨论` · `✅ 已决定` · `✔ 已完成`
> 顺序按批次(P0 安全堵漏 → P1 RBAC 闭环 → P2 会话加固 → P3 工程化契约)推进,序号即处理顺序,可调。
## P0 安全越权堵漏
- [x] **C1** 跨租户越权 —— Tenant 表无租户过滤,任意租户超管可管全平台租户。✔ 已完成:`super_admin`=平台超管(保留)、`tenant_super_admin`=租户顶层(保留),`tenant:*` 仅平台租户可授予(constants.ts + roles.service 保留码校验 + 平台级权限剥离)
- [x] **H1** 软删与唯一约束冲突 —— 删后无法重建同名 → P2002 → 500。✔ 已完成:方案 B(BaseCrudService.softRemove 改名回收 `#id`,按列长截断)+ P2002→409
- [x] **H2** 删除角色不清理关联与缓存 —— user_role/role_resource/role_org 残留,聚合不校验已删角色。✔ 已完成:removeRole 级联清理 + 清缓存 + 保留角色禁删 + resolveAuth 过滤已删角色
- [x] **H3** 权限聚合缺 status 过滤 —— 停用资源仍被授权。✔ 已完成:resolveAuth 资源查询补 `status='enabled'`
- [x] **H4** 用户停用/删除后旧 token 仍可用 —— 守卫不查用户状态,缓存 TTL=7 天。✔ 已完成:方案 A(tokenVersion)+ JwtAuthGuard 每请求校验 + Redis 用户状态缓存
- [x] **H5** 重置密码不吊销会话 —— refresh token 不撤销。✔ 已完成:并入 H4,resetPassword/停用/删除 → invalidateSessions(tokenVersion++ + 删 refresh + 清缓存)
- [x] **H6** 角色资源树半选回显导致权限放大 —— 半选父节点被持久化 + 回显级联全选。✔ 已完成:只保存叶子键 `getCheckedKeys(true)` + 回显 `setCheckedKeys(assigned, true)`
- [x] **H10** 401 自动刷新无限循环 —— login/refresh 自身 401 也进刷新重放。✔ 已完成:`_retry` 标记 + 排除 login/refresh + 失败 reject
- [x] **H11** data_scope self 语义错误 —— self 等同本部门;详情接口无数据权限。✔ 已完成:self 按 userId 过滤 + 详情 assertCanView 403
- [x] **H13** seed 幂等副作用 —— 重跑重置管理员密码/启用账号。✔ 已完成:update 分支剥离 password/status
- [x] **M1** 日志脱敏不含 phone/email —— 违反 spec 安全红线。✔ 已完成:mask.ts 补充 phone/mobile/email
- [x] **M6** JWT 密钥默认兜底值 —— 生产忘配置即可伪造 token。✔ 已完成:缺失/生产弱密钥直接拒绝启动
- [x] **M8** pageSize 无上限 —— 可拉全表。✔ 已完成:`@Max(200)`
- [x] **M9** DTO 枚举缺校验 —— status 裸 `@IsString`。✔ 已完成:全 DTO 加 `@IsIn`(UserStatus/CommonStatus/DataScope/ResourceType)
- [x] **M10** Prisma 错误未映射 —— P2002/P2025 原文泄漏且 500。✔ 已完成:P2002→409、P2025→404、未知异常不泄漏内部细节
- [x] **M11** assignResources 非事务 + 不校验 + :id 语义混淆 —— ✔ 已完成:事务化 + 角色存在性校验(:id 实为 roleCode,已注释说明)
- [x] **M22** super_admin 可被租户自建 —— ✔ 已完成:并入 C1,`RESERVED_ROLE_CODES` 校验(super_admin / tenant_super_admin)
- [x] **M4** 组织树可成环 —— collectDescendants 死循环。✔ 已完成:create/update 防环校验 + collectDescendants visited 集合
## P1 RBAC 闭环
- [x] **H7** 用户-角色分配缺失 —— 无端点/DTO/UI。✔ 已完成:`GET/PUT /users/:id/roles` + DTO + 前端分配角色表单 + 用户页入口
- [x] **H9** 按钮级权限未实现 —— 无下发接口/指令,导出按钮不存在。✔ 核心已完成:`/auth/permissions` 下发 + permission store + `v-permission` 指令 + ty-crud 权限过滤 + 用户页导出演示(role/audit/login-log 3 个导出按钮为同模式,待补)
- [x] **M18** 前端无路由级权限过滤 + 403 页 —— ✔ 已完成:路由守卫按可访问菜单 path 过滤 + 403 页(/403)
- [x] **M21** 数据权限覆盖面过窄 —— 仅用户列表。✔ 已完成(方案 A):组织树按可见部门过滤 + 审计/登录日志按操作人部门过滤;角色/字典/配置保持租户级
- [x] **H8** 注册/忘记密码是假功能 —— mock 假成功。✔ 已完成:注册真实化(POST /auth/register),注册/忘记密码默认隐藏(showRegister/showForgetPassword=false,fork 可开启)
- [x] **M17** 租户 onboarding 断裂 —— 新建租户不可用、删除留孤儿。✔ 已完成:initTenant 模板(根组织+tenant_super_admin+管理员+资源+字典+授权)+ 删除级联清理;顺带修复 BaseCrudService tenant 大小写注入 bug
- [x] **新增** 用户自助修改密码/个人资料 —— 缺失清单。✔ 已完成:PATCH /auth/me/password(校验原密码 + 吊销其它会话)+ 顶栏「修改密码」入口
## P2 会话与安全加固
- [x] **M5** 登录安全基线缺失 —— 无锁定/后端验证码/密码策略。✔ 已完成:失败锁定(5 次/15 分钟)+ 密码长度策略(验证码后置)
- [x] **M7** Swagger 无鉴权 + CORS 全开 + 无 helmet/限流 —— ✔ 已完成:helmet + CORS 白名单(CORS_ORIGIN)+ Swagger 仅非生产 + @nestjs/throttler 限流
- [x] **M12** RefreshToken 哈希未校验 + 无清理 —— ✔ 已完成:refresh 校验 tokenHash(清理并入 L8)
- [x] **M14** 401 强退后 authStore 失真 —— ✔ 已完成:auth store 增 reset,handler 同步清空
- [x] **M15** dictStore loaded 守卫无法刷新 —— ✔ 已完成:fetch 加 force 参数,字典页增删改后 force 刷新
- [x] **M20** 登录租户写死 touyun —— ✔ 已完成:登录按用户名自动探测租户,多租户返回候选由前端弹窗选择
- [x] **M3** created_by 全项目零写入 —— ✔ 已完成:BaseCrudService.create 注入 createdBy
- [x] **L5** 登出审计 username 写 null —— ✔ 已完成:按 userId 回查真实 username
- [x] **L7** useRequest loading 竞态 + ty-crud 重复请求 —— ✔ 已完成:useRequest 请求序号防竞态 + ty-crud 搜索/重置去重
- [x] **L8** 审计/登录日志、refresh_token 无清理任务 —— ✔ 已完成:@nestjs/schedule 定时清理(过期 token + 90 天前日志)
## P3 工程化契约
- [x] **H12** e2e 必然失败 + 无业务测试 —— ✔ 已完成:e2e 改为登录链路(成功/失败),补 mask/data-scope 单测
- [x] **M2** 时间字段 ISO vs Unix 毫秒 —— ✔ 已完成:API 层统一 Unix 毫秒(TransformInterceptor 递归转换 + 前端类型 number)
- [x] **M16** vite `olldownOptions` 拼写错误 —— ✔ 已完成:改 `rolldownOptions`
- [x] **M19** 前端 .env/.env.dev 入库 + .env.example 带像真密码 —— ✔ 已完成:gitignore .env + .env.example 模板 + untrack
- [x] **L1** 模板残留/无 LICENSE/stats.html 入库 —— ✔ 已完成:package 元数据 + MIT LICENSE + untrack stats.html
- [x] **L2** 启动 console.log 噪音 —— ✔ 已完成:移除
- [x] **L3** docs/requirements 空目录 + config/menu.ts 死代码 —— ✔ 已完成:删 config/menu.ts(docs/requirements 为占位保留)
- [x] **L4** README 克隆命令与子模块风格不一致 —— ✔ 已完成:统一 ssh:// 端口
- [x] **L6** ty-crud 取消确认 rejection 未捕获 —— ✔ 已完成:catch 取消
- [x] **L9** @nestjs/mau 非常规依赖 —— ✔ 已完成:核实为 NestJS Mau 部署 CLI,已移除 + 删 deploy 脚本
- [x] **L10** 无 CI/Dockerfile/compose 部署编排 —— ✔ 已完成:docker-compose(MySQL/Redis)+ CI(前后端 build/test)+ 前后端 Dockerfile
- [x] **新增** spec 覆盖补全 —— users/roles/orgs/tenants/configs 与前端多页无 spec 锚点。✔ 已完成:backend 5 域 spec 已补(users/roles/orgs/tenants/configs);前端页面为薄容器,后续按需补

544
docs/guide.md Normal file
View File

@ -0,0 +1,544 @@
# 通用系统管理脚手架 · 开发帮助手册
> 面向 fork 本工程进行二次开发的工程师。读完能跑起来、能看懂结构、能照着新增功能。
> 更底层的架构与 spec 约定见 [`docs/architecture.md`](./architecture.md)、[`docs/spec-conventions.md`](./spec-conventions.md),工作流见根目录 [`CLAUDE.md`](../CLAUDE.md)。
---
## 0. 功能介绍与功能清单
一套可 fork 的「通用系统管理脚手架」:开箱即用后台基础能力,fork 后只改业务、不重复造轮子。后端 NestJS + Prisma + MySQL,前端 Vue 3 + Vite + Pinia + UnoCSS。
| 功能块 | 说明 |
|---|---|
| **认证** | 登录 / 登出;JWT(access 2h + refresh 7d),401 自动刷新;注册/忘记密码默认隐藏(`showRegister`/`showForgetPassword` 可开启) |
| **权限管理** | 用户(增删改/重置密码/启停/锁定)、角色(分配资源 + 数据权限)、资源(菜单/按钮/接口统一资源树)、组织(部门树) |
| **系统管理** | 租户、字典(类型 + 项,支持内置保护)、参数配置(键值) |
| **系统监控** | 操作日志、登录日志(只读查询) |
| **布局与主题** | 多布局(左右 / 上下 / 纯上下 + MixBar)、暗黑模式、主题色、多页签、面包屑、全屏 |
| **平台能力** | 多租户隔离(tenant_id)、RBAC(资源表统一授权 + 数据权限 data_scope)、审计日志落库、字典驱动渲染 |
**代码规范**:本工程用 openspec 追踪需求与变更(spec-driven),新增功能走 `propose → apply → archive` 流程,见 [`CLAUDE.md`](../CLAUDE.md)。本文只讲「代码怎么改」,不讲「流程怎么走」。
---
## 1. 快速开始
### 1.1 环境要求
| 依赖 | 版本 |
|---|---|
| Node | 22.23.1(以 `.nvmrc` / `engines` 为准) |
| pnpm | 12.10.1(以 `packageManager` 为准) |
| MySQL | 8.x |
| Redis | 6+ |
### 1.2 拉取与安装
```bash
git clone <仓库地址> && cd <目录>
git submodule update --init --recursive # 拉取 sysmgr-nest / sysmgr-admin
# 后端
cd sources/sysmgr-nest
cp .env.example .env # 若无 .env.example 则手建,见下
pnpm install
npx prisma migrate deploy # 应用已有迁移(开发期可用 migrate dev)
pnpm seed # 初始化租户/用户/角色/资源/内置字典
# 前端
cd ../sysmgr-admin
pnpm install
```
### 1.3 `.env`(后端)
```env
DATABASE_URL="mysql://root:密码@127.0.0.1:3306/sysmgr"
REDIS_HOST="127.0.0.1"
REDIS_PORT=6379
REDIS_DB=0
REDIS_KEY_PREFIX="sysmgr:"
JWT_SECRET="请替换为强密钥(生产至少 32 位)"
JWT_ACCESS_TTL=7200
JWT_REFRESH_TTL=604800
PORT=3000
```
### 1.4 启动
```bash
# 后端(:3000,全局前缀 /api/v1)
cd sources/sysmgr-nest && pnpm start:dev
# 前端(Vite,直连 VITE_API_BASE_URL,默认 http://localhost:3000/api/v1,见 .env)
cd sources/sysmgr-admin && pnpm dev:dev
```
### 1.5 默认账号
| 项 | 值 |
|---|---|
| 租户 | `touyun` |
| 用户名 | `touyun` |
| 密码 | `Touyun@123` |
| 角色 | `super_admin`(超管,绕过权限校验、数据权限看全量) |
---
## 2. 后端开发指南(sysmgr-nest)
### 2.1 目录与分层
```
src/
├── auth/ # 认证:登录/刷新/登出、菜单树、JWT 守卫、权限守卫、装饰器、请求上下文
├── users/ roles/ resources/ orgs/ dicts/ configs/ tenants/ audit-logs/ login-logs/
│ # 每个域一个目录:<name>.module.ts / controller / service / dto
├── prisma/ # PrismaService:统一注入多租户过滤 + 软删过滤
├── common/ # BaseCrudService、enums、permissions、page.dto、拦截器/过滤器、Redis
└── logging/ # pino 日志、请求体日志拦截器
```
每个域遵循同构结构(以 `tenants` 为例):
```
tenants/
├── tenants.module.ts # 声明 controller + service
├── tenants.controller.ts # 路由 + @RequirePermissions + @AuditLog
├── tenants.service.ts # extends BaseCrudService,写业务
└── dto.ts # class-validator DTO(创建/更新/查询)
```
### 2.2 Prisma 约定
- 表名 `sys_` 前缀,列名 snake_case(模型 camelCase + `@map`)。
- 主键 UUID v7:`id String @id @default(uuid(7)) @db.VarChar(36)`。
- 业务键关联用 `code`(角色/资源/组织/字典/租户),引用用户用 `id`,树引用用 `parent_code`。
- 业务软删实体带 `created_at / created_by / updated_at / deleted_at`;日志/会话实体无 `deleted_at`。
- 多租户:业务实体一律带 `tenant_code`。
### 2.3 BaseCrudService + PrismaService
所有域 service 继承 `BaseCrudService(prisma, '模型名')`,直接获得:
| 方法 | 说明 |
|---|---|
| `paginate(page, pageSize, where?, orderBy?)` | 分页,返回 `{ list, total, page, pageSize }` |
| `list(where?, orderBy?)` | 列表 |
| `findById(id)` | 按 id 查 |
| `create(data)` | 创建,自动注入 `tenantCode`(Tenant 除外) |
| `update(id, data)` | 更新 |
| `softRemove(id)` | 软删(置 `deletedAt`) |
`PrismaService` 通过 `$extends` 全局拦截,**无需手写**:
- 读操作自动追加 `tenantCode` 过滤(`Tenant` 模型除外);
- 软删模型(User/Role/Resource/Org/DictType/DictItem/Config)读操作自动追加 `deletedAt: null`。
> 因此业务 service 里写 `this.list({ status: 'enabled' })` 即可,租户与软删已由框架兜底。
### 2.4 RBAC:资源表统一授权
- 菜单、按钮、接口统一在 `resource` 表,`type ∈ { menu, button, api }`。
- `api`/`button` 型资源的 `code` 即**权限码**,形如 `domain:action`(`user:list`、`user:create`)。
- 权限码清单在 [`src/common/permissions.ts`](../sources/sysmgr-nest/src/common/permissions.ts),控制器与 seed 共用。
**三级鉴权**(由全局 `JwtAuthGuard` + `PermissionsGuard` 实现):
| 层级 | 写法 | 效果 |
|---|---|---|
| 公开 | `@Public()` | 无需 token |
| 仅登录 | 不加权限装饰器 | 只要登录态 |
| 权限码 | `@RequirePermissions(Permissions.user.list)` | 登录 + 拥有该权限码 |
```ts
@Get()
@RequirePermissions(Permissions.user.list) // 登录 + user:list
list(@Query() q: PageQuery) { return this.service.paginate(q.page, q.pageSize) }
```
- 角色 `code === 'super_admin'` 自动绕过权限码校验、`data_scope` 看全量。
- 数据权限 `role.data_scope ∈ { all, dept, dept_and_child, self, custom }`,`custom` 时经 `role_org` 关联可见部门。
### 2.5 装饰器与拦截器
| 项 | 用途 |
|---|---|
| `@Public()` | 跳过登录校验 |
| `@RequirePermissions(...codes)` | 要求权限码 |
| `@AuditLog('模块', '动作')` | 写操作自动落 `sys_audit_log`(异步,失败不阻塞业务) |
| `@CurrentUser` | 取当前登录用户 |
全局拦截器/过滤器(`app.module.ts` 注册):`TransformInterceptor`(响应封装 `{code,message,data}`)、`AuditLogInterceptor`(审计)、`AllExceptionsFilter`(异常→`code = HTTP*100`)。
### 2.6 字典域(内置字典 + 保护)
- 字典类型/字典项在 `dicts/`;内置字典 `built_in=true`。
- **保护规则**:内置类型不可删、内置项不可删、内置项 `value` 不可改;`label/sort/color_type` 可改;内置类型下可新增非内置项。
- `GET /dicts`(仅登录态)一次返回当前租户全部启用类型 + 启用项,供前端缓存。
- 内置字典取值与 [`src/common/enums.ts`](../sources/sysmgr-nest/src/common/enums.ts) 一一对应,由 seed 预置(`common_status` / `user_status` / `resource_type` / `data_scope` / `login_status`)。
### 2.7 seed 脚本
`prisma/seed.ts`:幂等 upsert 租户 `touyun`、用户、`super_admin` 角色、全量资源树(菜单+接口+按钮)、内置字典。新增资源/字典/权限码时同步补进 seed。
---
## 3. 前端开发指南(sysmgr-admin)
### 3.1 目录与自动导入
```
src/
├── api/ # http.ts(axios 封装)+ 每域 api(user/role/resource/org/dict/...)
├── components/ # ty-* 组件库(全局自动导入,无需 import)
├── composables/ # useRequest、useDict
├── config/ # layout.ts(布局设置)、login.ts(登录页配置)、menu.ts(兜底菜单)
├── layouts/ # default.vue(后台壳)、blank.vue(登录页等)
├── pages/ # 页面(unplugin-vue-router 按目录自动生成路由)
├── stores/modules/ # Pinia:app/auth/menu/tabs/dict
├── types/ # crud.ts(JsonSchema/TableColumn 等)、resource.ts
└── utils/ # dialog.ts(showDialog)、tree.ts、token.ts、theme.ts
```
已配置 `unplugin-auto-import` + `unplugin-vue-components`:Vue API、Element Plus、`@/components` 下的组件**均无需手动 import**。
### 3.2 网络请求:axios 封装 + useRequest
统一用全局 `http`(`@/api/http`):
```ts
const data = await http.get<User[]>('/users', { params: { page: 1 } }) // 自动解包,直接拿 data
```
- 自动注入 `Authorization`;响应自动拆 `{code,message,data}`(`code=0` 返回 `data`);`401` 自动刷新 token 并重放;失败抛 `ApiError`。
- 方法:`http.get/post/put/patch/delete/request<T>(url, data?, config?)`。
`useRequest`(`@/composables/useRequest`)管理请求状态:
```ts
const { data, loading, run, refresh } = useRequest((signal: AbortSignal) => listUsers(q, signal))
// 或手动触发:useRequest(fetcher, { manual: true }) → run(...args)
```
### 3.3 字典:dict store + useDict + dictCode
登录后自动缓存全部启用字典,页面/组件通过 `dictCode` 或 `useDict` 读取,**不要硬编码中文/颜色映射**。
```ts
const { items, tagMap, options } = useDict('common_status')
// items : [{ label:'启用', value:'enabled', colorType:'success', sort:1 }, ...]
// tagMap : { enabled: { label:'启用', type:'success' }, ... }
// options: [{ label:'启用', value:'enabled' }, ...]
```
在 JSON Schema / 表格列里直接写 `dictCode` 即可(见 §3.7 组件 API)。
### 3.4 菜单 / 路由 / 布局
- 路由由 `unplugin-vue-router` 按 `src/pages/**` 目录自动生成;页面用 `<route lang="yaml">` 自定义块声明 `meta.layout`(`default` | `blank`)选择布局:
```vue
<route lang="yaml">
meta:
layout: blank
requiresAuth: false
</route>
```
- 菜单从后端 `GET /auth/menus` 拉取(超管=全部启用菜单,普通用户=角色可访问菜单+祖先),存 `menu` store。
- 页签存 `tabs` store,登出/401 清空。
### 3.5 样式:UnoCSS @apply
优先用 `@apply` 工具类,少写裸 CSS。注意主题色工具类写法:
```scss
.foo {
@apply bg-bg-base text-text-primary; // 背景/文字主题色:bg-bg-* / text-text-*
@apply border-solid rounded-base; // 语义色:primary/success/warning/danger/info
}
```
### 3.6 组件 API 文档
下面 5 个业务组件 + 2 个 composable 是日常开发主力。布局类组件(ty-header/ty-sidebar/ty-tabs 等)由 `layouts/default.vue` 组装,一般业务页无需直接使用。
#### 3.6.1 ty-form(JSON Schema 表单)
**Props**
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `schema` | `JsonSchema` | — | 必填,表单结构描述 |
| `modelValue` | `Record<string, any>` | — | 必填,表单数据(就地绑定) |
| `inline` | `boolean` | `false` | 行内布局(搜索用) |
| `mode` | `'create' \| 'edit'` | `undefined` | 配合字段 `showOn` 过滤 |
| `enterToSubmit` | `boolean` | `false` | 输入框回车触发 `submit` |
| `readonlyKeys` | `string[]` | `[]` | 强制只读的字段名 |
**Expose(通过 `ref` 调用)**
| 方法 | 说明 |
|---|---|
| `submit()` | 校验并返回表单数据(校验失败抛错) |
| `validate()` | 仅校验 |
| `resetFields()` | 重置 |
| `clearValidate()` | 清空校验状态 |
**Emits**:`submit`。
**JsonSchemaProperty 字段速查**
| 字段 | 类型 | 说明 |
|---|---|---|
| `type` | `'string'\|'integer'\|'number'\|'boolean'` | 控件类型 |
| `title` | `string` | 标签 |
| `placeholder` | `string` | 占位 |
| `enum` / `enumNames` | `(string\|number\|boolean)[]` / `string[]` | 下拉选项(一一对应) |
| `dictCode` | `string` | 字典 code,声明后自动生成下拉(优先于 enum) |
| `format` | `'password'\|'email'\|'textarea'\|'tree-select'\|'date'\|'datetime'` | 特殊控件;`date/datetime` 目前按普通输入框渲染 |
| `default` | `unknown` | 默认值 |
| `readOnly` | `boolean` | 只读 |
| `showOn` | `('create'\|'edit')[]` | 仅指定模式显示 |
| `when` | `{ field; equals?; in? }` | 依赖字段值联动显隐 |
| `minLength`/`maxLength`/`minimum`/`maximum` | `number` | 校验 |
| `prefixIcon` | `string` | 输入框前缀 iconify 图标 |
| `width` | `string` | 控件宽度 |
| `treeData`/`treeProps` | — | `format:'tree-select'` 时的树数据与字段映射 |
```ts
const schema: JsonSchema = {
type: 'object',
properties: {
status: { type: 'string', title: '状态', dictCode: 'common_status', default: 'enabled' },
path: { type: 'string', title: '路由', when: { field: 'type', equals: 'menu' } },
},
required: ['status'],
}
```
#### 3.6.2 ty-search-form(搜索 + 折叠)
**Props**
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `schema` | `JsonSchema` | — | 搜索字段 |
| `modelValue` | `Record<string, any>` | — | 搜索条件 |
| `collapseCount` | `number` | `3` | 折叠时显示字段数,超出「展开」 |
**Emits**:`search`、`reset`。
#### 3.6.3 ty-table(表格)
**Props**
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `columns` | `TableColumn[]` | — | 列配置 |
| `data` | `Record<string, any>[]` | — | 行数据 |
| `loading` | `boolean` | `false` | 加载态 |
| `selection` | `boolean` | `false` | 多选列 |
| `total` / `page` / `pageSize` / `pageSizes` | `number`/`number[]` | `0`/`1`/`10`/`[10,20,50]` | 分页 |
| `actionsWidth` | `number\|string` | `200` | 操作列宽 |
| `rowKey` | `string` | `'id'` | 行唯一键(树形用 code) |
| `treeProps` | `Record<string,string>` | `{children:'children'}` | 树形字段映射 |
| `defaultExpandAll` | `boolean` | `false` | 树形默认展开 |
**Emits**:`update:page`、`update:pageSize`、`selection-change`。
**Slots**:`actions`(作用域 `{ row }`)。
**TableColumn 字段**
| 字段 | 类型 | 说明 |
|---|---|---|
| `prop` | `string` | 字段名 |
| `label` | `string` | 列标题 |
| `width` / `minWidth` | `number\|string` | 列宽 |
| `align` | `'left'\|'center'\|'right'` | 对齐 |
| `type` | `'text'\|'tag'\|'datetime'` | 渲染类型(默认 text) |
| `tagMap` | `Record<string, {label,type}>` | `type:'tag'` 的值→标签映射 |
| `dictCode` | `string` | `type:'tag'` 时按字典渲染(优先于 tagMap) |
| `formatter` | `(value, row) => string` | 自定义格式化 |
```ts
const columns: TableColumn[] = [
{ prop: 'status', label: '状态', width: 90, type: 'tag', dictCode: 'common_status' },
{ prop: 'createdAt', label: '创建时间', width: 160, type: 'datetime' },
]
```
#### 3.6.4 ty-crud(整页 CRUD)
组合「搜索 + 工具栏 + 表格 + 弹窗表单 + 删除」,多数单表页面用它几行搞定。
**Props**
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
| `entityName` | `string` | — | 实体名(标题/确认文案) |
| `columns` | `TableColumn[]` | — | 列 |
| `formSchema` | `JsonSchema` | — | 新增/编辑表单 |
| `searchSchema` | `JsonSchema` | — | 搜索(可选) |
| `list` | `(query, signal?) => Promise<PageResult>` | — | 分页查询 |
| `create` | `(data) => Promise<unknown>` | — | 新增 |
| `update` | `(id, data) => Promise<unknown>` | — | 更新 |
| `remove` | `(id) => Promise<unknown>` | — | 删除 |
| `idKey` / `nameKey` | `string` | `'id'`/`'name'` | 主键/删除确认显示字段 |
| `editReadonlyKeys` | `string[]` | `[]` | 编辑只读字段 |
| `getEditReadonlyKeys` | `(row) => string[]` | — | 按行计算只读(优先) |
| `actions` | `CrudAction[]` | `['edit','delete']` | 操作列(内置或自定义 `{label,type,onClick,show}`) |
| `collapseCount` | `number` | `2` | 操作按钮折叠数 |
| `selection` | `boolean` | `false` | 多选(配合 `#toolbar-right`) |
| `actionsWidth` | `number\|string` | `140` | 操作列宽 |
**Slots**:`toolbar`、`toolbar-right`(作用域 `{ selectedRows }`)。
**Expose**:`reload()`。
```vue
<ty-crud
entity-name="用户"
:columns="columns"
:search-schema="searchSchema"
:form-schema="formSchema"
:list="list" :create="create" :update="update" :remove="remove"
id-key="id" name-key="username" :edit-readonly-keys="['username']"
/>
```
#### 3.6.5 showDialog / ty-dialog(弹窗)
`showDialog(options)` 打开弹窗,返回 Promise(确认 resolve 内容组件 `submit()` 数据,取消/关闭 resolve `null`)。`<ty-dialog />` 需挂载一次作为宿主(通常已在根组件)。
```ts
const data = await showDialog({
component: TyForm, // 内容组件(通常传 TyForm)
title: '新增用户',
width: '520px',
props: { schema, modelValue: model, mode: 'create' },
})
if (data == null) return // 取消
await createUser(data)
```
**DialogOptions**
| 字段 | 类型 | 说明 |
|---|---|---|
| `component` | `Component` | 内容组件(须 expose `submit()`) |
| `title` / `width` | `string` | 标题/宽度(默认 480px) |
| `props` | `Record<string, any>` | 传给内容组件的 props |
| `confirmText` / `cancelText` | `string` | 按钮文案 |
| `showFooter` | `boolean` | 默认显示取消/确定;`false` 时内容组件自带底部 |
| `onConfirm` | `(data) => void\|Promise` | 确认回调 |
#### 3.6.6 useRequest
```ts
useRequest<TData, TParams>(fetcher, options?)
// fetcher: (...args: [...TParams, AbortSignal]) => Promise<TData>
```
**Options**:`manual`(默认 false 自动请求)、`defaultParams`、`refreshDeps`(依赖变化重请求)、`onBefore/onSuccess/onError/onFinally`。
**返回**:`data`、`loading`、`error`、`run(...args)`、`refresh()`、`cancel()`、`mutate(data|fn)`。
#### 3.6.7 useDict
```ts
useDict(code) // → { items, tagMap, options },均响应式
```
---
## 4. 联调契约
### 4.1 响应封装
```json
{ "code": 0, "message": "ok", "data": { } }
```
- `code=0` 成功;失败 `code = HTTP状态码 * 100`(401→40100,403→40300,400→40000,500→50000)。
- 前端 `http` 已自动拆包:成功拿 `data`,失败抛 `ApiError(code, message)`。
### 4.2 分页
列表接口 `page/pageSize`,返回 `{ list, total, page, pageSize }`。
### 4.3 三级鉴权
公开(`@Public`)→ 仅登录(无装饰器)→ 权限码(`@RequirePermissions`),见 §2.4。
### 4.4 契约锚点
`shared/api-contract`(响应/错误码/分页/鉴权/接口目录)与 `shared/data-model`(字段字典)是前后端唯一锚点,字段/接口变更先改 `shared/`。
---
## 5. 常用操作手册(Cookbook)
### 5.1 新增一个业务实体(含权限码 + 页面)完整走查
以新增「岗位 post」为例:
**后端**
1. `prisma/schema.prisma` 加 `Post` 模型(`sys_post`,带 `tenant_code` + 公共字段)。
2. `npx prisma migrate dev --name add_post` + `prisma generate`。
3. `src/common/permissions.ts` 加 `post: { list, create, update, delete }`。
4. 新建 `src/posts/`(module/controller/service/dto),service `extends BaseCrudService`。
5. controller 挂 `@RequirePermissions(Permissions.post.xxx)` + 写操作 `@AuditLog('post', 'xxx')`。
6. `app.module.ts` 注册 `PostsModule`。
7. `prisma/seed.ts` 的 `APIS` 加 4 条接口资源(method/path/code),超管即全量授权。
**前端**
1. `src/api/post.ts` 加类型 + `listPosts/createPost/...`。
2. `src/pages/system/post/index.vue` 用 `ty-crud` 组装(列用 `dictCode`,表单用 JSON Schema)。
3. seed 的 `MENUS` 加菜单节点(path/component/icon),路由自动生成。
### 5.2 新增一个字典类型
1. 登录进入「系统管理 → 字典管理」→ 新增字典类型(填 code/name)。
2. 选中类型 → 新增字典项(label/value/标签色/排序)。
3. 页面/组件里用 `dictCode: '<code>'` 或 `useDict('<code>')` 引用。
> 内置字典(seed 预置)的 `value` 与内置项受保护,只能改 label/排序/颜色;普通字典完全自由。
### 5.3 新增一个权限码 / 按钮
1. `common/permissions.ts` 加 `domain.action`。
2. `seed.ts` 的 `APIS`(接口)或 `BUTTONS`(纯前端按钮)加对应资源,跑 `pnpm seed`。
3. 后端 controller 用 `@RequirePermissions(...)`;前端按钮权限后续按资源 code 下发渲染。
### 5.4 新增租户
「系统管理 → 租户管理」新增;或 seed 里 upsert。租户之间 `tenant_code` 隔离,登录时按账号+租户识别当前租户。
### 5.5 扩展 seed
改 `prisma/seed.ts` 的 `MENUS` / `APIS` / `BUTTONS` / `DICTS` 常量,跑 `pnpm seed`(幂等,重复运行不重复插入)。
---
## 6. 规范与约定
- **提交信息**:Conventional Commits(`feat`/`fix`/`chore`/`docs`/`refactor`)。
- **命名**:后端模型 camelCase + `@map` snake_case;业务键统一 `code`;权限码 `domain:action`。
- **样式**:UnoCSS `@apply`,少写裸 CSS;主题色用 `bg-bg-*` / `text-text-*`。
- **自测后停服务**:冒烟测试临时起的服务,测完立即停掉。
- **单一事实源**:字段/接口以 `shared/data-model`、`shared/api-contract` 为准,不各自命名。
---
## 7. 排障 FAQ
| 现象 | 原因 / 处理 |
|---|---|
| 启动报 `EADDRINUSE :3000` | 端口被占,换 `PORT=3100` 或停掉旧进程 |
| `prisma migrate dev` 非交互报错 | 用 `prisma migrate diff` 生成 SQL + `prisma migrate deploy`,或开发期用 `prisma db push` |
| seed 重复插入报唯一约束 | 检查 upsert 的 `where` 是否用了唯一键(如 `tenantCode_code`) |
| 接口返回 401 | token 缺失/过期;前端会自动刷新,刷新失败跳登录 |
| 接口返回 403 | 权限码未授权;确认 resource 表登记了该接口、角色已分配 |
| 字典下拉/标签不显示 | 确认登录后 `dictStore.fetch()` 成功;字典类型/项 `status=enabled`;`dictCode` 拼写一致 |
| 改字典 label 页面没变 | 字典缓存未刷新:重新登录,或字典页保存后 `dictStore.fetch()` |
| 登录失败 | 确认 seed 已跑、账号 `touyun / Touyun@123`、租户 `touyun` 存在且 enabled |
| 内置项 value 改不动 | 内置项受保护,仅 label/排序/颜色可改(有意为之) |

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-11

View File

@ -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<code, DictItem[]>` + `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 与用户确认中敲定)

View File

@ -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 保护、颜色编辑;用户/角色/资源/组织/租户/审计/登录日志页去硬编码

View File

@ -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 个内置字典类型与字典项被创建,重复运行不重复插入

View File

@ -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 的页面(租户/角色/资源/组织等)刷新后展示「可用」

View File

@ -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 不返回该类型或该项

View File

@ -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,取值不脱离上述集合

View File

@ -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 受保护、下拉/标签字典驱动渲染

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-10

View File

@ -0,0 +1,109 @@
## Context
sysmgr-admin 基于 agarwood 模板,已具备:Vue 3 + Vite 8 + TypeScript、Pinia(`pinia-plugin-persistedstate`)、vue-router 5(`unplugin-vue-router` 自动路由 + `vite-plugin-vue-layouts`)、UnoCSS、`unplugin-auto-import` + `unplugin-vue-components`(`deep: true, directoryAsNamespace: false`)、@iconify/vue、vue-i18n、@vueuse(`useDark`)。现有 `src/layouts/default.vue` 是占位 header + router-view;登录页 `layout:false`。全局设计令牌在 `src/assets/scss/settings/_color.scss`(`--wm-color-*`,深色用 `[data-theme="dark"]`)。
动机见 proposal.md。
## Goals / Non-Goals
**Goals:**
- Element Plus 按需接入,中文 locale。
- 可配置布局壳(7 种模式)+ 主题色 + 暗黑 + 折叠 + 宽度 + 固定顶栏/底栏 + 面包屑/多页签/全屏/手风琴。
- 布局/主题偏好 localStorage 持久化;默认值可在代码中集中配置。
- 组件库友好:组件命名/放置/定义规范化,便于将来抽取独立组件库。
**Non-Goals:**
- 不接后端菜单数据(本任务用占位菜单,但字段结构对齐后端)。
- 不做水印、色弱模式、路由过渡动画(后续再议)。
- 不改后端。
## Decisions
### D1. 组件命名与放置(用户约束)
公共/布局/全局组件统一放 `src/components/`(自动注册目录),目录与文件名均中划线 `xx-xxx.vue`,组件名 `ty-` 前缀(投云科技),如 `ty-header.vue` → `<ty-header>`。模板直接写 kebab 标签,不手动 import。
- 组件清单:`ty-header` / `ty-mixbar` / `ty-sidebar` / `ty-tabs` / `ty-breadcrumb` / `ty-footer` / `ty-setting`(齿轮触发 + drawer 外壳)/ `ty-setting-panel`(面板)。
- 依据:`Components({ deep: true, directoryAsNamespace: false })` 按文件名注册,`ty-` 前缀避免与 Element Plus(`el-`)及业务组件撞名。
### D2. 组件库友好定义(用户约束:将来抽独立组件库)
每个组件显式 `defineProps` 类型化 + `defineEmits` + 需要时 `defineSlots`/`defineExpose`;组件 **props 驱动、与 Pinia store 解耦**——store 只在 `src/layouts/default.vue` 壳里读,通过 props 传给组件、组件通过 emits 回传交互。组件不直接 import store,便于整体抽出。
### D3. Element Plus 接入:on-demand resolver
`unplugin-vue-components` 加 `ElementPlusResolver({ importStyle: 'sass' })`;`unplugin-auto-import` 加 `ElementPlusResolver`(自动引入 `ElMessage` 等)。中文 locale 用 `<el-config-provider :locale="zhCn">` 包裹 App。
- 备选:全量引入。放弃:脚手架体积敏感,模板已有 on-demand 基础设施。
### D4. 布局状态模型 + 默认值集中配置
状态存 Pinia `app` store(persist)。**默认值不散落在 store 里,而集中在一个配置模块** `src/config/layout.ts` 导出 `defaultLayout`,store 用它作初始值。开发者改这一个文件即可统一调整默认值,用户在页面上的修改覆盖默认值并持久化到 localStorage。
`defaultLayout`(初始值):
```ts
{
layout: 'horizontal', // horizontal(上下) | top(纯上下) | sidebar(左右)
mixbar: false,
themeColor: '#2E7FA3',
isDark: false,
headerFixed: true,
sidebarWidth: 210, // 200–340
footerFixed: false,
sidebarCollapsed: false,
showBreadcrumb: true,
showTabs: true,
showFullscreen: true,
menuAccordion: false,
}
```
4 种呈现:`top`、`horizontal`、`sidebar`、`sidebar`+MixBar。MixBar 仅 `sidebar`(左右)布局可用,`horizontal`(上下)无 MixBar。**所有布局均有顶栏**(含折叠/面包屑/全屏/暗黑/设置/用户等工具),区别只在导航菜单位置:`top` 顶栏放全量导航、`horizontal` 顶栏放一级导航 + 侧栏放子菜单、`sidebar` 顶栏不放菜单(菜单全在侧栏)。顶栏 Logo 区与左侧菜单(或 MixBar+菜单)同宽,侧栏折叠时仅显 Logo。
### D5. 主题色运行时覆盖
由所选主色用 JS 计算派生色阶(light-3/5/7/8/9、dark-2),写入 `:root` 的 `--el-color-primary` 及派生变量;默认 `#2E7FA3` 预置。色值计算用 `colord` 或自写 mix(混白/黑)。
- 不用编译期 SCSS `$colors` 覆盖——无法运行时换色。
### D6. 暗黑模式与现有令牌对齐
暗黑开关同时:① `html` 加/去 `dark` class(Element Plus 深色 css-vars);② `html` 设 `data-theme="dark"/"light"`(现有 `--wm-color-*` 深色令牌)。统一由 `applyTheme()` 维护,避免两套状态漂移。
### D7. 菜单数据结构对齐后端
占位菜单**完全按后端 `sys_resource` / `Resource` 模型的字段**定义,不自造字段。类型 `src/types/resource.ts`:
```ts
export type ResourceType = 'menu' | 'button' | 'api'
export interface ResourceNode {
id: string
code: string
name: string
parentCode: string | null
type: ResourceType
path: string | null
component: string | null
icon: string | null
sort: number
children?: ResourceNode[] // 前端由 parentCode 组装,仅用于渲染
}
```
占位数据内置在 `src/config/menu.ts`(树已组装好 `children`),字段名与后端一致(`code/name/parentCode/type/path/component/icon/sort`),下个任务直接换成「当前用户菜单树」接口返回值。
- 菜单 `icon` 字段存 **iconify 图标名**(如 `mdi:home`),渲染时复用已有 `src/components/icon/icon.vue`(`type="iconify"`),不自建图标体系。
### D8. 布局壳结构(store 只在壳层)
`src/layouts/default.vue` 读 `app` store,按 `layout`/`mixbar` 条件渲染并接线:
- `ty-header`(一级导航或工具栏 + 折叠 + 面包屑 + 全屏 + 设置入口 + 用户下拉),props 接收模式/折叠/菜单/面包屑等
- `ty-mixbar`(一级图标)、`ty-sidebar`(`el-menu`:collapse + width + accordion)
- `ty-tabs`(多页签)、`ty-footer`(版权「云南投云科技有限公司」)、内容区 `<router-view>`
- `ty-setting` / `ty-setting-panel`(设置抽屉与面板)
布局切换用 Vue 条件渲染 + flex/grid,不引第三方布局库。
## Risks / Trade-offs
- [主题色派生色阶与 Element Plus 视觉一致性] → 用标准 mix 公式生成 light-3/5/7/8/9、dark-2。
- [props 驱动 vs 开发便利] → 组件更通用但壳层接线略多;为将来抽组件库值得。
- [暗黑两套令牌漂移] → 集中在 `applyTheme()` 维护 `dark` class 与 `data-theme`。
- [多页签 keep-alive 复杂度] → 本任务先做展示/切换/关闭,缓存后续再上。
## Migration Plan
- 纯前端新增,无迁移。
- 部署:合并 → `pnpm install`(装 element-plus)→ `pnpm dev:dev` 或构建。
- 回滚:`git revert`。
## Open Questions
- 占位菜单图标资源(@iconify vs 本地 svg)后续接后端时一并定。
- 多页签是否 keep-alive 缓存(后续再议)。

View File

@ -0,0 +1,37 @@
## Why
Phase 0 前端只有登录页与路由守卫,登录后没有真正的后台布局壳。Phase 1 的管理后台需要一套可配置的布局框架——它是所有后台页面的容器,支持多种布局/导航模式、主题色、暗黑模式等。先搭好这个地基,后续用户/角色/资源等页面才有地方挂。
## What Changes
- **集成 Element Plus**:走 on-demand(`unplugin-vue-components` 的 `ElementPlusResolver` + 组件样式按需),配置中文 locale。
- **布局壳**:替换 `src/layouts/default.vue`;布局/全局组件统一放 `src/components/`(自动注册目录),命名 `ty-` 前缀 + 中划线(`ty-header.vue` → `<ty-header>`),充分复用 auto-import。
- **组件库友好**:组件 props/emits 显式类型化、props 驱动、与 Pinia store 解耦,为将来抽取独立组件库打基础。
- **布局与导航模式**:支持 3 种布局(上下/纯上下/左右)与 MixBar(仅左右布局可用),共 4 种呈现;MixBar 固定最左;所有布局均有顶栏(左右顶栏仅工具、不含菜单);左侧菜单支持 collapse/expand;顶栏 Logo 区与侧栏同宽、折叠仅显 Logo。
- **主题色**:颜色选择器自定义,默认 `#2E7FA3`,运行时覆盖 Element Plus 主色 CSS 变量。
- **暗黑模式**:明亮/暗黑切换。
- **顶栏固定**:可固定/不固定;纯左右布局(无顶栏)时该选项禁用。
- **侧边菜单宽度**:200–340 可调。
- **底部版权**:显示「云南投云科技有限公司」,可固定。
- **补充设置**:面包屑、多页签(tabs)、顶栏全屏、菜单手风琴。
- **持久化与默认值**:布局/主题偏好存 localStorage(Pinia `persistedstate`);默认值集中在代码配置模块(`src/config/layout.ts`),改一处统一生效。
- **导航菜单数据**:本任务用占位菜单,但字段结构严格对齐后端 `resource` 实体(`id/code/name/parentCode/type/path/component/icon/sort`),下个任务直接换接口数据。
## Capabilities
### New Capabilities
- `frontend/AD/layout`: 管理后台布局壳与主题系统(布局/导航模式、主题色、暗黑、折叠、宽度、固定顶栏/底栏、面包屑/多页签/全屏/手风琴、持久化)
### Modified Capabilities
(无)
## Impact
- 前端 `sources/sysmgr-admin`:
- 新增依赖 `element-plus`(+ `@element-plus/icons-vue` 如需)
- `vite.config.ts`:`Components`/`AutoImport` 加 `ElementPlusResolver`
- 新增 `src/components/ty-*.vue` 组件(ty-header/ty-sidebar/ty-mixbar/ty-tabs/ty-breadcrumb/ty-footer/ty-setting/ty-setting-panel)
- 新增 `src/stores/modules/app.ts`(布局/主题偏好,persist)+ `src/config/layout.ts`(默认值)+ `src/types/resource.ts`(菜单类型)
- 替换 `src/layouts/default.vue` 为布局壳
- 主题色/暗黑与现有 `--wm-color-*` 令牌对齐
- 后端无变更;本任务菜单为占位数据(字段对齐后端 resource)。

View File

@ -0,0 +1,122 @@
## Purpose
定义管理后台布局壳与主题系统的行为:可配置的布局/导航模式、主题色、暗黑模式、侧边栏折叠与宽度、固定顶栏/底栏,以及面包屑/多页签/全屏/手风琴等配套能力。
## ADDED Requirements
### Requirement: 布局与导航模式
系统 SHALL 支持 3 种布局(上下 / 纯上下 / 左右)与 MixBar 开关的组合,共 4 种呈现(MixBar 仅左右布局可用);MixBar 始终固定在布局最左侧;所有布局均渲染顶栏,仅导航菜单位置不同。
#### Scenario: 切换布局即时生效
- **WHEN** 用户在布局设置中切换布局或开启/关闭 MixBar
- **THEN** 主框架立即按新布局重新排布顶栏、侧边栏与内容区
#### Scenario: 左右顶栏不含菜单
- **WHEN** 布局为「左右」
- **THEN** 顶栏仍显示折叠/面包屑/全屏/暗黑/设置/用户等工具,但不渲染导航菜单,菜单全部在左侧侧边栏
#### Scenario: 上下/纯上下顶栏含导航
- **WHEN** 布局为「上下」或「纯上下」
- **THEN** 顶栏含导航:「上下」顶部放一级导航、左侧放子菜单;「纯上下」仅顶部放全量导航
### Requirement: 侧边菜单折叠
左侧导航菜单 SHALL 支持 collapse(折叠为图标)与 expand(展开显示文字+图标)。
#### Scenario: 折叠与展开
- **WHEN** 用户点击折叠按钮
- **THEN** 侧边栏在折叠(仅图标)与展开(文字+图标)之间切换
### Requirement: 顶栏 Logo 区
顶栏左上角 Logo 区 SHALL 与左侧菜单(或 MixBar+菜单)同宽;侧栏折叠时仅显示 Logo,展开时显示 Logo 与标题。
#### Scenario: Logo 区对齐
- **WHEN** 布局含左侧菜单
- **THEN** 顶栏 Logo 区宽度与左侧菜单(含 MixBar 时与 MixBar+菜单)一致
#### Scenario: 折叠仅显 Logo
- **WHEN** 侧栏折叠
- **THEN** 顶栏 Logo 区仅显示 Logo;展开时显示 Logo 与标题
### Requirement: 主题色
系统 SHALL 支持通过颜色选择器自定义主题色,默认 `#2E7FA3`;选择后运行时更新 Element Plus 主色及其派生色阶。
#### Scenario: 选择主题色
- **WHEN** 用户在设置面板用颜色选择器选取新主题色
- **THEN** 全站 Element Plus 主色(按钮/链接/高亮等)即时变为所选颜色,刷新后仍保留
### Requirement: 暗黑模式
系统 SHALL 支持明亮/暗黑模式切换;暗黑模式下切换 Element Plus 深色主题与全局深色令牌。
#### Scenario: 切换暗黑模式
- **WHEN** 用户切换暗黑模式开关
- **THEN** 全站即时切换为深色配色,刷新后仍保留
### Requirement: 顶栏固定
顶栏 SHALL 支持固定(内容滚动时顶栏常驻顶部)与不固定,所有布局均适用。
#### Scenario: 顶栏固定开关
- **WHEN** 用户切换「固定顶栏」
- **THEN** 顶栏在固定/随内容滚动之间切换
### Requirement: 侧边菜单宽度
左侧菜单宽度 SHALL 可调,范围 200–340(单位 px)。
#### Scenario: 调整宽度
- **WHEN** 用户拖动或输入侧边栏宽度
- **THEN** 宽度在 200–340 之间变化并即时生效,超界被限制在边界值
### Requirement: 底部版权固定
系统 SHALL 在底部显示版权信息「云南投云科技有限公司」,并支持固定/不固定。
#### Scenario: 底部版权
- **WHEN** 用户开启「固定底部」
- **THEN** 版权条常驻视口底部;关闭时随内容滚动
### Requirement: 布局设置持久化
所有布局与主题设置 SHALL 持久化到 localStorage,刷新页面后保持。
#### Scenario: 刷新保留
- **WHEN** 用户调整布局/主题/宽度等后刷新页面
- **THEN** 所有设置恢复为用户上次的配置
### Requirement: 面包屑
系统 SHALL 在内容区顶部显示面包屑,随当前路由与菜单层级展示当前位置。
#### Scenario: 面包屑导航
- **WHEN** 用户进入某个菜单页面
- **THEN** 面包屑显示当前菜单的层级路径
### Requirement: 多页签
系统 SHALL 支持多页签(tabs)视图:打开的页面保留为页签,可切换与关闭。
#### Scenario: 页签切换与关闭
- **WHEN** 用户打开多个菜单页面
- **THEN** 各页面以页签形式保留,可切换、可关闭
### Requirement: 顶栏全屏
系统 SHALL 在顶栏提供全屏切换按钮,进入/退出浏览器全屏。
#### Scenario: 全屏切换
- **WHEN** 用户点击全屏按钮
- **THEN** 页面进入浏览器全屏,再次点击退出
### Requirement: 菜单手风琴
侧边菜单 SHALL 支持手风琴模式:同一级只展开一个子菜单。
#### Scenario: 手风琴展开
- **WHEN** 开启手风琴且用户展开某个子菜单
- **THEN** 同级的其他已展开子菜单自动收起
### Requirement: 默认值集中配置
布局与主题的默认值 SHALL 在代码中集中配置(单一配置模块),开发者改一处即可统一调整;用户在页面上的修改覆盖默认值并持久化。
#### Scenario: 改默认值生效
- **WHEN** 开发者修改配置模块中的默认主题色或默认布局
- **THEN** 未手动改过的用户首次进入时按新默认值渲染;已手动改过的用户仍按自己的持久化设置
### Requirement: 菜单数据结构对齐后端
占位菜单数据 SHALL 采用后端资源实体(`resource`)的字段结构(`id/code/name/parentCode/type/path/component/icon/sort`),不自造字段名。
#### Scenario: 字段对齐
- **WHEN** 占位菜单被替换为后端「当前用户菜单树」接口数据
- **THEN** 前端无需改动数据结构即可直接渲染

View File

@ -0,0 +1,45 @@
## 1. Element Plus 集成
- [x] 1.1 安装 element-plus(及 @element-plus/icons-vue)
- [x] 1.2 vite.config.ts:`Components`/`AutoImport` 增加 `ElementPlusResolver`(on-demand + sass 样式)
- [x] 1.3 App.vue 用 `<el-config-provider :locale="zhCn">` 包裹,配中文 locale
- [x] 1.4 冒烟:页面上 el-button 能渲染、ElMessage 可调用
## 2. 类型与默认值配置
- [x] 2.1 `src/types/resource.ts`:`ResourceType` 与 `ResourceNode` 类型,字段严格对齐后端 resource(id/code/name/parentCode/type/path/component/icon/sort + children)
- [x] 2.2 `src/config/layout.ts`:`defaultLayout` 集中配置默认值(themeColor `#2E7FA3`、sidebarWidth 210 等)
- [x] 2.3 `src/config/menu.ts`:占位菜单树(字段对齐后端,已组装 children)
## 3. 布局/主题状态(app store)
- [x] 3.1 `src/stores/modules/app.ts`:以 `defaultLayout` 作初始值,开启 persist;含 layout/mixbar/themeColor/isDark/headerFixed/sidebarWidth/footerFixed/sidebarCollapsed/showBreadcrumb/showTabs/showFullscreen/menuAccordion
## 4. 主题系统
- [x] 4.1 主题色工具:由主色计算 light-3/5/7/8/9、dark-2,写入 `:root` 的 `--el-color-primary*`
- [x] 4.2 `applyTheme()`:统一切换 `html.dark` class 与 `data-theme`(对齐现有 `--wm-color-*` 深色令牌)
- [x] 4.3 监听 store 的 themeColor/isDark 变化自动应用
## 5. 布局壳组件(src/components/ty-*.vue,kebab 命名 + 自动导入,props/emits 显式类型化)
- [x] 5.1 `ty-header`:顶栏(一级导航或工具栏 + 折叠 + 全屏 + 设置入口 + 用户下拉),响应 headerFixed;props 接收菜单/模式/折叠等,emits 折叠/打开设置
- [x] 5.2 `ty-mixbar`:固定最左一级图标栏
- [x] 5.3 `ty-sidebar`:左侧菜单(el-menu),支持 collapse/expand、width、accordion
- [x] 5.4 `ty-tabs`:多页签(展示/切换/关闭)
- [x] 5.5 `ty-breadcrumb`:面包屑
- [x] 5.6 `ty-footer`:底部版权「云南投云科技有限公司」,响应 footerFixed
- [x] 5.7 替换 `layouts/default.vue` 为布局壳:读 store,按 layout/mixbar 条件渲染并 props/emits 接线(组件不直接依赖 store)
## 6. 设置组件与面板
- [x] 6.1 `ty-setting`:设置组件(齿轮触发 + el-drawer 外壳)
- [x] 6.2 `ty-setting-panel`:设置面板——布局模式、MixBar、主题色选择器(默认 #2E7FA3)、暗黑、顶栏固定(纯左右禁用)、侧边宽度 slider(200–340)、底部固定、面包屑/多页签/全屏/手风琴开关
## 7. 验证
- [x] 7.1 `pnpm dev` 启动,登录后进入布局壳
- [x] 7.2 切换 4 种模式即时生效;「左右」顶栏不含菜单、导航全在左侧;MixBar 仅左右可用
- [x] 7.3 主题色、暗黑、侧边宽度、折叠、手风琴、多页签、全屏、面包屑、底部版权均生效
- [x] 7.4 刷新后设置保留(localStorage);改 `defaultLayout` 后未改过的用户按新默认值渲染
- [x] 7.5 `pnpm type-check` 与 `pnpm build:prod` 通过

View File

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-10-11

View File

@ -0,0 +1,42 @@
## Context
布局壳已完成(`frontend/AD/layout`)。用户管理页是第一个业务页,目标是**跑通并抽取一套可复用的 CRUD 公共组件**,作为后续角色/资源/组织/字典/配置/日志页面的底座。本阶段不接后端,页面内写死数据 + `setTimeout` 模拟异步。
## Goals / Non-Goals
**Goals:**
- 抽取公共组件:`ty-form`(JSON Schema 表单)、`ty-search-form`、`ty-table`、`ty-form-dialog`、`ty-crud`(整页组合)。
- 用 JSON Schema 描述表单(搜索 + 弹窗表单),声明式渲染字段与校验。
- 用户管理页用 `ty-crud` + JSON Schema + 内联数据(setTimeout)跑通。
**Non-Goals:**
- 不接后端;不做「关联角色/部门」跨域操作;不引入第三方表单库(自己实现轻量 JSON Schema 子集)。
## Decisions
### D1. JSON Schema 驱动表单(自实现轻量子集)
表单字段用 JSON Schema 描述:`type`(string/integer/number/boolean)、`title`(标签)、`enum`+`enumNames`(下拉)、`format`(password/email/textarea)、`default`、`readOnly`、`showOn`(仅 create/edit 显示,如 password)、`required`(必填)、`minLength`/`format:email`(校验)。
- `ty-form` 按 schema 渲染字段(enum→select、boolean→switch、number→input-number、format→password/textarea、其余→input),校验规则由 schema 生成,`defineExpose` 暴露 `validate`/`resetFields`。
- 不引第三方 JSON Schema 表单库(`vue-json-schema-form` 等)——脚手架追求少依赖,且只需常用子集。
### D2. 组件分层(基础组件 + 整页组合)
- 基础组件:`ty-form`(JSON Schema 表单,字段支持 `width`)、`ty-search-form`(搜索 + 折叠/展开,`collapseCount` 默认 2,查询/重置置右)、`ty-table`(列配置 + 分页 + 操作列插槽)、`ty-form-dialog`(弹窗 + schema 表单 + 校验提交)。
- 整页组件:`ty-crud`(组合搜索/工具栏/表格/弹窗/删除),props 传 `columns/searchSchema/formSchema/list/create/update/remove/actions` 等;操作列数据驱动(内置 `'edit'/'delete'` 或自定义动作),超过 `collapseCount`(默认 2)时折叠为「更多」下拉;工具条 justify-between(左新增/`#toolbar`、右批量 `#toolbar-right` + `selectedRows`),支持 `selection` 多选,卡片铺满剩余空间、列表内滚动。
- 类型放 `src/types/crud.ts`(JsonSchema/JsonSchemaProperty/TableColumn/PageResult/PageQuery/CrudAction)。
### D3. 数据:页面内联 + setTimeout
不建 mock 模块。用户管理页内联 `users` 数组,`list/create/update/remove` 用 `setTimeout` 模拟异步、返回 `PageResult`。后续接后端时,把这些函数替换为 `http` 调用即可(页面与 `ty-crud` 不动)。
## Risks / Trade-offs
- [JSON Schema 子集覆盖不全] → 只覆盖常用字段类型,复杂字段用 `readOnly`/`showOn`/`formatter` 扩展,必要时组件内补 widget。
- [内联数据刷新即重置] → 仅界面演示用;接后端后自然解决。
- [ty-crud 抽象过早] → 先用户管理一页验证,第二页复用时再迭代调整。
## Migration Plan
- 纯前端新增,无迁移。回滚 `git revert`。
## Open Questions
- 复杂字段(日期/级联/文件上传)的 schema widget 后置,用到再补。

View File

@ -0,0 +1,27 @@
## Why
布局壳已完成。Phase 1 需要开始搭管理后台的真实页面。用户管理是最具代表性的 CRUD 页——跑通「搜索 + 表格 + 分页 + 弹窗表单 + 删除 + 重置密码」的页面模式,作为后续角色/资源/组织/字典/配置/日志页面的模板。本阶段只做界面,占位数据字段对齐后端,后端对接放到后面。
## What Changes
- 新增「用户管理」页面(`src/pages/system/user/index.vue`,对应菜单 `/system/user`)。
- 页面功能:搜索(用户名/昵称/手机/状态)、表格(用户名/昵称/邮箱/手机/状态/组织/创建时间)、分页、新增/编辑弹窗、删除确认、重置密码。
- **抽取公共 CRUD 组件**(本次核心目标):`ty-form`(JSON Schema 驱动表单)、`ty-search-form`、`ty-table`、`ty-form-dialog`、`ty-crud`(整页组合),供后续 8 个域页面复用。
- **JSON Schema 描述表单**:搜索与弹窗表单字段用 JSON Schema 声明式配置(类型/标题/枚举/校验/showOn)。
- 数据:页面内联 + `setTimeout` 模拟异步(不建 mock 模块),字段对齐后端 `user` 模型,后续接后端只替换数据函数。
## Capabilities
### New Capabilities
- `frontend/AD/user`: 用户管理页面(列表查询/新增/编辑/删除/重置密码/状态展示)
### Modified Capabilities
(无)
## Impact
- 前端 `sources/sysmgr-admin`:
- 新增公共组件 `src/components/ty-form.vue`、`ty-search-form.vue`、`ty-table.vue`、`ty-form-dialog.vue`、`ty-crud.vue`
- 新增类型 `src/types/crud.ts`(JsonSchema/TableColumn/PageResult 等)
- 新增 `src/pages/system/user/index.vue`(用户管理页,ty-crud + JSON Schema + 内联数据)
- 后端无变更;不接真实接口。

View File

@ -0,0 +1,58 @@
## Purpose
定义用户管理页面的行为:按条件分页查询用户列表,支持新增、编辑、删除与重置密码,并展示用户状态。
## ADDED Requirements
### Requirement: 用户分页查询
系统 SHALL 在用户管理页按「用户名/昵称/手机/状态」条件分页查询用户列表,展示用户名、昵称、邮箱、手机、状态、所属组织与创建时间。
#### Scenario: 条件查询
- **WHEN** 用户输入用户名/昵称/手机或选择状态并点击查询
- **THEN** 表格按条件过滤并展示分页结果
#### Scenario: 分页
- **WHEN** 用户切换页码或每页条数
- **THEN** 表格展示对应页数据
### Requirement: 新增用户
系统 SHALL 支持新增用户:填写用户名、昵称、邮箱、手机、状态、所属组织与初始密码。
#### Scenario: 新增
- **WHEN** 用户点击新增并填写必填项(用户名、密码)后提交
- **THEN** 系统新增该用户并刷新列表
### Requirement: 编辑用户
系统 SHALL 支持编辑用户基本信息(昵称/邮箱/手机/状态/组织),用户名不可改。
#### Scenario: 编辑
- **WHEN** 用户编辑某用户并提交
- **THEN** 系统更新该用户并刷新列表
### Requirement: 重置密码
系统 SHALL 支持对某用户重置密码(填写新密码)。
#### Scenario: 重置密码
- **WHEN** 用户对某用户执行重置密码并填写新密码
- **THEN** 系统更新该用户密码
### Requirement: 删除用户
系统 SHALL 支持删除用户,删除前需确认。
#### Scenario: 删除确认
- **WHEN** 用户点击删除并确认
- **THEN** 系统删除该用户并刷新列表
### Requirement: 状态展示
用户状态 SHALL 以标签形式区分展示:启用 / 停用 / 锁定。
#### Scenario: 状态标签
- **WHEN** 列表展示用户
- **THEN** 状态以不同颜色的标签区分(启用=成功色、停用=信息色、锁定=危险色)
### Requirement: 字段对齐后端
占位用户数据 SHALL 采用后端 `user` 实体字段(`id/tenantCode/username/nickname/email/phone/status/orgCode/createdAt` 等),不自造字段名。
#### Scenario: 后续接后端
- **WHEN** 占位数据替换为后端用户接口
- **THEN** 页面无需改动数据结构即可直接渲染

View File

@ -0,0 +1,24 @@
## 1. 类型
- [x] 1.1 `src/types/crud.ts`:JsonSchema/JsonSchemaProperty/TableColumn/PageResult/PageQuery 类型
## 2. 基础组件(src/components/ty-*.vue,自动导入)
- [x] 2.1 `ty-form`:按 JSON Schema 渲染字段(select/switch/input-number/password/textarea/input)+ 校验 + expose validate/resetFields + readOnly/showOn
- [x] 2.2 `ty-search-form`:行内搜索 + 折叠/展开(collapseCount 默认 2)+ 查询/重置置右
- [x] 2.3 `ty-table`:列配置(text/tag/datetime/formatter)+ 分页 + `#actions` 操作列插槽
- [x] 2.4 `ty-form-dialog`:弹窗 + schema 表单 + 校验提交(v-model:visible + mode)
## 3. 整页组件
- [x] 3.1 `ty-crud`:组合搜索/工具栏/表格/弹窗/删除,操作列数据驱动(actions 内置/自定义),超过 collapseCount 折叠为「更多」
## 4. 用户管理页
- [x] 4.1 `src/pages/system/user/index.vue`:用 ty-crud + JSON Schema + 内联数据(setTimeout)实现搜索/列表/分页/新增/编辑/删除/重置密码
- [x] 4.2 删除 `src/api/user.ts`(不建 mock 模块)
## 5. 验证
- [x] 5.1 `pnpm type-check` 与 `pnpm build-only` 通过
- [x] 5.2 页面交互(搜索/分页/增删改/重置密码/状态标签)在 dev 下可验收

View File

@ -0,0 +1,14 @@
# Configs Specification
## Purpose
定义参数配置域的行为:键值配置的分页查询与增删改。
## Requirements
### Requirement: 配置 CRUD
系统 SHALL 支持配置项的增删改查;key 租户内唯一。
#### Scenario: 配置唯一性
- **WHEN** 创建重复 key 的配置
- **THEN** 返回 409 冲突

View File

@ -0,0 +1,39 @@
# Dict Specification
## Purpose
定义字典域的保护规则与内置字典种子:内置字典类型与字典项受保护(不可删、value 不可改),仅 label/排序/颜色可编辑,并预置 5 个系统内置枚举字典。
## 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 个内置字典类型与字典项被创建,重复运行不重复插入

View File

@ -0,0 +1,21 @@
# Orgs Specification
## Purpose
定义组织域的行为:组织树查询(按数据权限过滤)、增删改、防环校验。
## Requirements
### Requirement: 组织树查询
系统 SHALL 返回组织树列表,按数据权限过滤可见部门。
#### Scenario: 部门管理员查看组织
- **WHEN** 数据权限为「本部门及以下」的管理员查询组织
- **THEN** 仅返回本部门及下级组织
### Requirement: 组织增删改防环
创建/更新组织 SHALL 校验父组织存在且不构成环(父不能是自身或后代)。
#### Scenario: 防止成环
- **WHEN** 将组织挂到自身或其子组织下
- **THEN** 返回 400 拒绝

View File

@ -0,0 +1,28 @@
# Roles Specification
## Purpose
定义角色域的行为:分页查询、创建/编辑(保留角色码校验)、删除(级联清理关联)、资源分配(事务化、平台级权限隔离)。
## Requirements
### Requirement: 角色增删改
系统 SHALL 支持角色 CRUD;保留角色码(super_admin/tenant_super_admin)不可创建、不可删除。
#### Scenario: 保留角色保护
- **WHEN** 尝试创建或删除保留角色码
- **THEN** 返回 400 拒绝
### Requirement: 删除级联
删除角色 SHALL 级联清理 user_role/role_resource/role_org 关联,并清该租户权限缓存。
#### Scenario: 删除角色
- **WHEN** 删除某角色
- **THEN** 其关联与权限缓存一并清理
### Requirement: 资源分配
分配资源 SHALL 事务化替换;平台级权限(tenant:*)仅平台租户可授予。
#### Scenario: 业务租户分配
- **WHEN** 非平台租户分配含 tenant:* 的资源
- **THEN** 平台级权限被剥离

View File

@ -0,0 +1,28 @@
# Tenants Specification
## Purpose
定义租户域的行为:仅平台租户可管理租户;创建租户自动初始化(管理员/根组织/资源/字典),删除级联清理。
## Requirements
### Requirement: 平台级管理
租户管理 SHALL 仅平台租户(super_admin)可操作;tenant:* 权限仅平台授予。
#### Scenario: 业务租户越权
- **WHEN** 业务租户管理员尝试管理租户
- **THEN** 无 tenant:* 权限,返回 403
### Requirement: 租户 onboarding
创建租户 SHALL 自动初始化:根组织、tenant_super_admin 角色、管理员账号、全量资源(不含 tenant:*)、内置字典。
#### Scenario: 新租户可用
- **WHEN** 创建新租户
- **THEN** 其管理员可登录并访问菜单
### Requirement: 租户删除级联
删除租户 SHALL 级联清理其全部业务数据。
#### Scenario: 删除租户
- **WHEN** 删除租户
- **THEN** 其用户/角色/资源/组织/字典/日志一并清理

View File

@ -0,0 +1,35 @@
# Users Specification
## Purpose
定义用户域的行为:分页查询(含数据权限过滤)、新增/编辑/删除(软删回收唯一键)、重置密码、分配角色,以及会话吊销。
## Requirements
### Requirement: 用户分页查询
系统 SHALL 支持按用户名/昵称/手机/状态分页查询用户;查询按数据权限过滤(仅本人/本部门/本部门及以下/自定义)。
#### Scenario: 部门管理员查看用户
- **WHEN** 数据权限为「本部门」的管理员查询用户列表
- **THEN** 仅返回本部门用户
### Requirement: 用户增删改
系统 SHALL 支持新增/编辑用户;删除为软删并回收用户名唯一键(可重建同名)。
#### Scenario: 软删重建
- **WHEN** 删除某用户后再创建同名用户
- **THEN** 创建成功
### Requirement: 重置密码
系统 SHALL 支持管理员重置用户密码;重置后吊销该用户所有会话(旧 token 失效)。
#### Scenario: 重置后踢下线
- **WHEN** 管理员重置某用户密码
- **THEN** 该用户旧 access/refresh token 立即失效
### Requirement: 分配角色
系统 SHALL 支持为用户分配角色(整体替换),分配后清该用户权限缓存。
#### Scenario: 分配角色
- **WHEN** 管理员为用户分配角色
- **THEN** 该用户下次请求按新角色聚合权限

View File

@ -0,0 +1,50 @@
# Dictionary Rendering Specification
## Purpose
定义前端字典驱动渲染机制:登录后缓存全部启用字典,组件按字典 code 声明下拉/标签,页面不再硬编码枚举中文映射,且字典管理页对内置项施加保护交互。
## 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 的页面(租户/角色/资源/组织等)刷新后展示「可用」

View File

@ -0,0 +1,124 @@
# Layout Specification
## Purpose
定义管理后台布局壳与主题系统的行为:可配置的布局/导航模式、主题色、暗黑模式、侧边栏折叠与宽度、固定顶栏/底栏,以及面包屑/多页签/全屏/手风琴等配套能力。
## Requirements
### Requirement: 布局与导航模式
系统 SHALL 支持 3 种布局(上下 / 纯上下 / 左右)与 MixBar 开关的组合,共 4 种呈现(MixBar 仅左右布局可用);MixBar 始终固定在布局最左侧;所有布局均渲染顶栏,仅导航菜单位置不同。
#### Scenario: 切换布局即时生效
- **WHEN** 用户在布局设置中切换布局或开启/关闭 MixBar
- **THEN** 主框架立即按新布局重新排布顶栏、侧边栏与内容区
#### Scenario: 左右顶栏不含菜单
- **WHEN** 布局为「左右」
- **THEN** 顶栏仍显示折叠/面包屑/全屏/暗黑/设置/用户等工具,但不渲染导航菜单,菜单全部在左侧侧边栏
#### Scenario: 上下/纯上下顶栏含导航
- **WHEN** 布局为「上下」或「纯上下」
- **THEN** 顶栏含导航:「上下」顶部放一级导航、左侧放子菜单;「纯上下」仅顶部放全量导航
### Requirement: 侧边菜单折叠
左侧导航菜单 SHALL 支持 collapse(折叠为图标)与 expand(展开显示文字+图标)。
#### Scenario: 折叠与展开
- **WHEN** 用户点击折叠按钮
- **THEN** 侧边栏在折叠(仅图标)与展开(文字+图标)之间切换
### Requirement: 顶栏 Logo 区
顶栏左上角 Logo 区 SHALL 与左侧菜单(或 MixBar+菜单)同宽;侧栏折叠时仅显示 Logo,展开时显示 Logo 与标题。
#### Scenario: Logo 区对齐
- **WHEN** 布局含左侧菜单
- **THEN** 顶栏 Logo 区宽度与左侧菜单(含 MixBar 时与 MixBar+菜单)一致
#### Scenario: 折叠仅显 Logo
- **WHEN** 侧栏折叠
- **THEN** 顶栏 Logo 区仅显示 Logo;展开时显示 Logo 与标题
### Requirement: 主题色
系统 SHALL 支持通过颜色选择器自定义主题色,默认 `#2E7FA3`;选择后运行时更新 Element Plus 主色及其派生色阶。
#### Scenario: 选择主题色
- **WHEN** 用户在设置面板用颜色选择器选取新主题色
- **THEN** 全站 Element Plus 主色(按钮/链接/高亮等)即时变为所选颜色,刷新后仍保留
### Requirement: 暗黑模式
系统 SHALL 支持明亮/暗黑模式切换;暗黑模式下切换 Element Plus 深色主题与全局深色令牌。
#### Scenario: 切换暗黑模式
- **WHEN** 用户切换暗黑模式开关
- **THEN** 全站即时切换为深色配色,刷新后仍保留
### Requirement: 顶栏固定
顶栏 SHALL 支持固定(内容滚动时顶栏常驻顶部)与不固定,所有布局均适用。
#### Scenario: 顶栏固定开关
- **WHEN** 用户切换「固定顶栏」
- **THEN** 顶栏在固定/随内容滚动之间切换
### Requirement: 侧边菜单宽度
左侧菜单宽度 SHALL 可调,范围 200–340(单位 px)。
#### Scenario: 调整宽度
- **WHEN** 用户拖动或输入侧边栏宽度
- **THEN** 宽度在 200–340 之间变化并即时生效,超界被限制在边界值
### Requirement: 底部版权固定
系统 SHALL 在底部显示版权信息「云南投云科技有限公司」,并支持固定/不固定。
#### Scenario: 底部版权
- **WHEN** 用户开启「固定底部」
- **THEN** 版权条常驻视口底部;关闭时随内容滚动
### Requirement: 布局设置持久化
所有布局与主题设置 SHALL 持久化到 localStorage,刷新页面后保持。
#### Scenario: 刷新保留
- **WHEN** 用户调整布局/主题/宽度等后刷新页面
- **THEN** 所有设置恢复为用户上次的配置
### Requirement: 面包屑
系统 SHALL 在内容区顶部显示面包屑,随当前路由与菜单层级展示当前位置。
#### Scenario: 面包屑导航
- **WHEN** 用户进入某个菜单页面
- **THEN** 面包屑显示当前菜单的层级路径
### Requirement: 多页签
系统 SHALL 支持多页签(tabs)视图:打开的页面保留为页签,可切换与关闭。
#### Scenario: 页签切换与关闭
- **WHEN** 用户打开多个菜单页面
- **THEN** 各页面以页签形式保留,可切换、可关闭
### Requirement: 顶栏全屏
系统 SHALL 在顶栏提供全屏切换按钮,进入/退出浏览器全屏。
#### Scenario: 全屏切换
- **WHEN** 用户点击全屏按钮
- **THEN** 页面进入浏览器全屏,再次点击退出
### Requirement: 菜单手风琴
侧边菜单 SHALL 支持手风琴模式:同一级只展开一个子菜单。
#### Scenario: 手风琴展开
- **WHEN** 开启手风琴且用户展开某个子菜单
- **THEN** 同级的其他已展开子菜单自动收起
### Requirement: 默认值集中配置
布局与主题的默认值 SHALL 在代码中集中配置(单一配置模块),开发者改一处即可统一调整;用户在页面上的修改覆盖默认值并持久化。
#### Scenario: 改默认值生效
- **WHEN** 开发者修改配置模块中的默认主题色或默认布局
- **THEN** 未手动改过的用户首次进入时按新默认值渲染;已手动改过的用户仍按自己的持久化设置
### Requirement: 菜单数据结构对齐后端
占位菜单数据 SHALL 采用后端资源实体(`resource`)的字段结构(`id/code/name/parentCode/type/path/component/icon/sort`),不自造字段名。
#### Scenario: 字段对齐
- **WHEN** 占位菜单被替换为后端「当前用户菜单树」接口数据
- **THEN** 前端无需改动数据结构即可直接渲染

View File

@ -0,0 +1,60 @@
# User Management Specification
## Purpose
定义用户管理页面的行为:按条件分页查询用户列表,支持新增、编辑、删除与重置密码,并展示用户状态。
## Requirements
### Requirement: 用户分页查询
系统 SHALL 在用户管理页按「用户名/昵称/手机/状态」条件分页查询用户列表,展示用户名、昵称、邮箱、手机、状态、所属组织与创建时间。
#### Scenario: 条件查询
- **WHEN** 用户输入用户名/昵称/手机或选择状态并点击查询
- **THEN** 表格按条件过滤并展示分页结果
#### Scenario: 分页
- **WHEN** 用户切换页码或每页条数
- **THEN** 表格展示对应页数据
### Requirement: 新增用户
系统 SHALL 支持新增用户:填写用户名、昵称、邮箱、手机、状态、所属组织与初始密码。
#### Scenario: 新增
- **WHEN** 用户点击新增并填写必填项(用户名、密码)后提交
- **THEN** 系统新增该用户并刷新列表
### Requirement: 编辑用户
系统 SHALL 支持编辑用户基本信息(昵称/邮箱/手机/状态/组织),用户名不可改。
#### Scenario: 编辑
- **WHEN** 用户编辑某用户并提交
- **THEN** 系统更新该用户并刷新列表
### Requirement: 重置密码
系统 SHALL 支持对某用户重置密码(填写新密码)。
#### Scenario: 重置密码
- **WHEN** 用户对某用户执行重置密码并填写新密码
- **THEN** 系统更新该用户密码
### Requirement: 删除用户
系统 SHALL 支持删除用户,删除前需确认。
#### Scenario: 删除确认
- **WHEN** 用户点击删除并确认
- **THEN** 系统删除该用户并刷新列表
### Requirement: 状态展示
用户状态 SHALL 以标签形式区分展示:启用 / 停用 / 锁定。
#### Scenario: 状态标签
- **WHEN** 列表展示用户
- **THEN** 状态以不同颜色的标签区分(启用=成功色、停用=信息色、锁定=危险色)
### Requirement: 字段对齐后端
占位用户数据 SHALL 采用后端 `user` 实体字段(`id/tenantCode/username/nickname/email/phone/status/orgCode/createdAt` 等),不自造字段名。
#### Scenario: 后续接后端
- **WHEN** 占位数据替换为后端用户接口
- **THEN** 页面无需改动数据结构即可直接渲染

View File

@ -59,3 +59,14 @@
#### Scenario: 资源 CRUD #### Scenario: 资源 CRUD
- **WHEN** 管理端操作某核心域 - **WHEN** 管理端操作某核心域
- **THEN** 通过对应复数资源路径进行增删改查 - **THEN** 通过对应复数资源路径进行增删改查
### Requirement: 字典聚合查询接口
系统 SHALL 提供 `GET /dicts` 接口,需登录态,按当前租户一次返回全部启用状态的字典类型及其启用状态的字典项(字典项按 sort 升序),供前端登录后缓存渲染。
#### Scenario: 一次性拉取
- **WHEN** 前端调用 GET /dicts
- **THEN** 返回全部启用字典类型,每个类型含其启用字典项(label/value/color_type/sort)
#### Scenario: 停用过滤
- **WHEN** 某字典类型或字典项被停用
- **THEN** GET /dicts 不返回该类型或该项

View File

@ -58,3 +58,21 @@ user 实体 SHALL 支持启用/停用/锁定状态;停用与锁定的用户 SH
#### Scenario: 锁定登录 #### Scenario: 锁定登录
- **WHEN** 锁定用户尝试登录 - **WHEN** 锁定用户尝试登录
- **THEN** 系统拒绝登录并提示账号已锁定 - **THEN** 系统拒绝登录并提示账号已锁定
### 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,取值不脱离上述集合

@ -1 +1 @@
Subproject commit 7738abbbfd3ddad6401a53a4cdaab643b1cc7908 Subproject commit e1bc97303425f2a0a9f39abf2441d940601072cf

@ -1 +1 @@
Subproject commit ce2aa35a24ab351433ae7bb98aba2cab38c7e676 Subproject commit 9a9435e66565b0402a1246e35b7858cb3439f77b