624a05aa66
需求:「设计开发平台运营人员对租户的管理功能,还有日志的详情查看功能。」 ⚠️ **口径变更**(覆盖 2026-10 走查的旧结论):views/tenant/index.vue 原先写着「列表**没有操作列**: 不提供编辑 / 停用入口(客户 2026-10 走查确认的口径)」。本轮用户明确选了全量范围,所以列表现在 有操作列(详情 / 编辑 / 停用·启用)。旧注释是**改写而不是删掉**,并写明推翻理由 —— 运营的真实 诉求是:① 到期日变了要能自己改,而不是每次找开发;② 欠费 / 停止合作时要能当场停掉; ③ 每周核对"谁快到期了"时要能点进详情看用量。 1) 租户管理:操作列用 tenant:view 门控「详情」、tenant:manage 门控「编辑 / 停用 / 启用」, 拿不到管理权限时整列显示"只读",而不是留一个点了报 403 的按钮。停用走 NPopconfirm 二次确认 且文案写明后果("停用后该校所有学生与老师会立刻无法登录");只发 {status} 一个字段 —— PUT 是部分更新,回传整行会把列表里看不见的字段覆盖成空。 2) 详情抽屉 TenantDetailDrawer:打开时**同时**调 getTenantDetail 与 getTenantUsage —— 列表接口的 contactPhone **恒为 null**(电话以密文入库,列表不能成为批量拉取联系方式的通道), 脱敏值只有详情接口给,拿行数据凑详情会显示成"没有联系电话"。用量区写明"学生数与存储是**实时值**, 周期只影响计费项",否则会以为"2026Q3 的学生数"是历史值;notes 如实全列(它解释"为什么这几项是 0")。 统计周期只给本月 / 上月 / 本季三个固定选项,不做自由文本输入。 3) 编辑弹窗 TenantEditModal:类型 4 选项、状态 3 选项;数字下拉的校验规则必须带 type: 'number' (只写 required 会"看起来填了但校验不过",而且 #feedback 会把报错遮住);联系电话留空 = 不修改; 租户编码只读展示并注明"出现在扫码落地页与对接文档里,开通后不可修改"。不做"清空到期日" (MyBatis-Plus 忽略 null 更新,清不掉就不在界面上承诺)。 4) 日志页:操作日志 / 登录日志两个模式,各自维护 list / total / pageNum / pageSize / loading / 筛选条件,切换时重置到第 1 页。⚠️ 页签用工具条上的**分段按钮**而不是 NTabs / NTabPane: ① NTabPane 的内容要额外 flex 化才能维持 .page-card 的高度链,否则表格高度算成 0、 **一行数据都看不见**(本仓踩过);② naive 2.45.3 的 tab="..." 属性写法本身也是坏的, 必须用 #tab 插槽。 5) 日志详情 LogDetailDrawer:变更前后**完整不截断** + break-all + 各一个复制按钮; extraJson 格式化成 <pre>(解析失败原样展示);traceId 带复制;request_param / duration_ms 如实写"本版本未采集"(这两列从来没有写入点),不编。抽屉**不再打接口** —— 列表 SELECT 已经 把抽屉要的字段都带出来了。 6) 「单位」列只对平台显示(0 → 「平台」),两张表统一用「单位」这个词:这一列要装得下 tenant_id = 0 的「平台」,表头写「租户」而格子里写「平台」是自相矛盾的;且与筛选栏两个下拉的 占位符、租户管理页的「单位名称」一致。登录日志刻意**不要「归属地」列**(location 恒为 null, 整列空白看起来像功能坏了)。 7) 日志页的模块下拉换成导出的 MODULE_OPTIONS / TARGET_TYPE_OPTIONS:原先那 4 项里 AUDIT / EXPORT **后端一个写入点都没有**,而真会写的 FILE(代传材料)与 ROLE(6 个角色动作)**不在里面** —— 选中死项只会得到一张空表,让人以为"这段时间没有审核动作"。 8) 菜单显示名「操作日志」→「日志」(key 保持 'log' 不动:后端 BuiltinRole 的菜单键与 check-wiring 第 ⑥ 项都按它核对,改 key 会让平台角色的菜单静默少一项)。 同步 frontend-pc/AGENTS.md 与 .agents/:「最容易写错的约定」由 7 条扩到 **9 条** —— 新增 第 8 条「详情用抽屉、不要为详情新开路由」(含"列表接口可能刻意不返回某个字段"的坑)与 第 9 条「页面内的页签用工具条分段按钮」(写明高度链的原因)。 验证:pnpm typecheck **0 错**、pnpm lint **0 错 0 警告**、pnpm build 通过。 浏览器实测(平台账号,5173 + 8080):租户页出现操作列且三档到期配色正确、卡片上内边距仍是 16px、 高度链未破(表格 570px / 日志表 528px);详情抽屉显示脱敏电话 138****8000 —— 而库里存的是 **历史明文** 13800138000,也就是说这条同时验了"读路径容错"(否则是永久 500);停用二次确认文案 正确;清空单位名称提交后弹窗不关且错误可见;登录日志同一张表里同时出现「示范职业学校」 (tenant_id=1)与「平台」(tenant_id=0)两种映射;**学校管理员视角两张表都没有「单位」列**。
4.8 KiB
4.8 KiB
name, description
| name | description |
|---|---|
| project-guide | PC 管理端的技术栈、目录结构、菜单与权限模型,以及九条最容易写错的约定。开始任何 PC 端改动前加载本技能。 |
项目总览(PC 管理端)
技术栈
| 项 | 值 |
|---|---|
| 框架 | Vue 3.5 + TypeScript + Vite 7 |
| UI | Naive UI 2.42(2026-10 从 Gi Admin Pro + Arco 重搭,已无 Arco/Gi 依赖) |
| 状态 | Pinia(pinia-plugin-persistedstate) |
| 请求 | axios(src/utils/http.ts 统一封装) |
| 样式 | SCSS(无 Stylelint) |
| 检查 | pnpm typecheck(vue-tsc,当前 0 错)、pnpm lint(@antfu/eslint-config) |
目录
src/
├── apis/xt/ 接口模块(按业务域分文件)+ types.ts(通用类型)
├── composables/ pagination.ts(列表页分页器统一参数)
├── config/ xt-routes.ts(**业务菜单表:菜单 + 路由 + 分组**)
├── icons/ AppIcon.vue + icon-data.ts(由 scripts/extract-pc-icons.py 生成)
├── layout/ AppSider / AppHeader / index.vue
├── router/ index.ts(本地菜单表按后端 menuKeys 过滤后注册)
├── stores/ user.ts(登录态/权限/菜单)、acting-tenant.ts(平台代管学校)、app.ts
├── styles/ index.scss(只放跨页面样式)
├── utils/ http.ts / naive.ts / tag.ts / format.ts / auth.ts / rsa.ts
└── views/ 页面(每个菜单一个目录,行内抽屉与弹窗同目录)
菜单与权限模型(后端决定可见性)
- 前端
config/xt-routes.ts是唯一菜单表:key(菜单键)↔ 路由 ↔ 视图 ↔ 分组。 - 能不能看由后端决定:
/auth/me返回menus: string[],前端只按它过滤; 绕过前端也只会拿到40301。 - 能不能做看权限码:
useUserStore().has('码'),字符串必须与后端PermissionDef逐字一致 (progress:viewAll是驼峰,不是progress:view:all—— 写错只会恒为 false,不报错)。 - 平台运营要先选学校(
stores/acting-tenant.ts→ 请求头X-Tenant-Id); 不选就不发请求,否则会把各租户数据混在一起且看起来正常。
九条最容易写错的约定
- 没有 auto-import:组件与 API 全显式 import,标签写 PascalCase。
漏 import 是静默故障(未知自定义元素 → 样式整块丢掉,typecheck/lint 全绿);
python scripts/check-wiring.py第 ⑧ 项会核对。 - 没有
v-hasPerm:权限判断写user.has('码')。 n-tag的type必须过@/utils/tag.ts(Arco 色词直接透传会静默变灰)。- 图标要重跑脚本:
scripts/extract-pc-icons.py→src/icons/icon-data.ts(37 个); 缺图标是空占位、不报错 → 图标集升级后必须重跑。 n-tab-pane的标签用#tab插槽,不要用tab="文字"属性 —— naive-ui 2.45.3 的属性写法只渲染出空注释节点,整排 tab 一个字都不显示(不报错)。- 卡片式版面:白卡片靠全局
--xt-bg灰底浮起来;卡片外观由styles/index.scss统一给, 页面不要再自己写;卡片内部分区用.section-title+ 浅灰小格子,不要卡里套卡。 详见 code-css 的「设计基线」一节。 - 品牌标是几何标:
src/assets/images/logo.svg(方 = 卷宗,环 = 在线)。 不要换回文字字形标;页签用public/favicon.svg(加粗版),改一个要同步另一个。 ⚠️ SVG 注释里不能出现连续两个减号(XML 规范禁止)→ 整个文件 not well-formed, 图片裂开而控制台一句话都不报;python scripts/check-wiring.py第 ⑨ 项会核对。 - 详情用抽屉、不新开路由:
NDrawer+NDrawerContent+NDescriptions, 抄src/views/audit/AuditDetailDrawer.vue;入口是操作列里的text按钮(size="small")。 ⚠️ 先确认列表接口给没给详情要的字段 —— 例:/tenants的contactPhone恒为null, 脱敏值只有/tenants/{id}给;拿行数据凑详情会显示成"没有联系电话"。 - 页面内的页签用工具条分段按钮,不要
NTabs:NTabPane要额外 flex 化才能维持.page-card高度链,否则表格高度算成 0、一行都看不见(本仓踩过); 且第 5 条那个tab="..."的坑也在这条路上。各页签的 list/page 状态分开维护。
开发
pnpm install
pnpm dev # → http://localhost:5173,/api 代理到 8080(后端不起着就没有数据)
pnpm typecheck && pnpm lint
演示账号:admin / Admin@123 + 租户码 demo(不是 Gi README 里的 admin/123456)。