--- name: project-guide description: 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`); 不选就不发请求,否则会把各租户数据混在一起且看起来正常。 ## 十条最容易写错的约定 1. **没有 auto-import**:组件与 API 全显式 import,标签写 **PascalCase**。 漏 import 是**静默故障**(未知自定义元素 → 样式整块丢掉,typecheck/lint 全绿); `python scripts/check-wiring.py` 第 ⑧ 项会核对。 2. **没有 `v-hasPerm`**:权限判断写 `user.has('码')`。 3. **`n-tag` 的 `type` 必须过 `@/utils/tag.ts`**(Arco 色词直接透传会静默变灰)。 4. **图标要重跑脚本**:`scripts/extract-pc-icons.py` → `src/icons/icon-data.ts`(37 个); 缺图标是空占位、不报错 → 图标集升级后必须重跑。 5. **`n-tab-pane` 的标签用 `#tab` 插槽**,不要用 `tab="文字"` 属性 —— naive-ui 2.45.3 的属性写法只渲染出空注释节点,整排 tab 一个字都不显示(不报错)。 6. **卡片式版面**:白卡片靠全局 `--xt-bg` 灰底浮起来;卡片外观由 `styles/index.scss` 统一给, 页面不要再自己写;卡片内部分区用 `.section-title` + 浅灰小格子,不要卡里套卡。 详见 [code-css](../code-css/SKILL.md) 的「设计基线」一节。 7. **品牌标是几何标**:`src/assets/images/logo.svg`(方 = 卷宗,环 = 在线)。 **不要换回文字字形标**;页签用 `public/favicon.svg`(加粗版),改一个要同步另一个。 ⚠️ SVG 注释里**不能出现连续两个减号**(XML 规范禁止)→ 整个文件 not well-formed, 图片裂开而控制台一句话都不报;`python scripts/check-wiring.py` 第 ⑨ 项会核对。 8. **详情用抽屉、不新开路由**:`NDrawer` + `NDrawerContent` + `NDescriptions`, 抄 `src/views/audit/AuditDetailDrawer.vue`;入口是操作列里的 `text` 按钮(`size="small"`)。 ⚠️ 先确认**列表接口给没给详情要的字段** —— 例:`/tenants` 的 `contactPhone` 恒为 `null`, 脱敏值只有 `/tenants/{id}` 给;拿行数据凑详情会显示成"没有联系电话"。 9. **页面内的页签用工具条分段按钮,不要 `NTabs`**:`NTabPane` 要额外 flex 化才能维持 `.page-card` 高度链,否则表格高度算成 0、**一行都看不见**(本仓踩过); 且第 5 条那个 `tab="..."` 的坑也在这条路上。各页签的 list/page 状态分开维护。 10. **`#feedback` 会遮住校验错误**:`NFormItem` 只要用了这个插槽, `renderExplains`(校验错误)就**永不渲染**(`children || feedback ? feedback : …`), 用户看到的是「红框 + 一句无关的正常提示」=「点了保存没反应」。 所以带 `#feedback` 的表单,`validate()` 失败必须自己 `message.error(firstError(errors) ?? '请检查表单填写')`。 ⚠️ `firstError` **必须摊平后再取**(naive reject 的是**嵌套数组**,`errors[0].message` 恒为 `undefined`);写法抄 `src/views/tenant/TenantEditModal.vue`。 ⚠️ 该插槽**不接收任何参数**,没有 `#feedback="{ status }"` 这种写法 —— 想内联渲染错误只能给 `feedback` **prop** 传渲染函数(要自写包装组件)。 ## 开发 ```bash pnpm install pnpm dev # → http://localhost:5173,/api 代理到 8080(后端不起着就没有数据) pnpm typecheck && pnpm lint ``` 演示账号:`admin / Admin@123` + 租户码 `demo`(**不是** Gi README 里的 `admin/123456`)。