# parking-cloud-ui — ZTEITS 智能停车云平台管理前端 停车平台 + 政务平台的**同一个管理前端**(一套框架、两套业务域)。前后端分离部署:后端只出一个 Jar,前端独立构建发布。 --- ## 1. 技术栈(版本已锁定,改动前先看第 6 节) | 依赖 | 版本 | 说明 | |---|---|---| | Vue | 3.3.8 | | | Vite | 4.5.0 | | | Element Plus | 2.4.2 | 组件 + `@element-plus/icons-vue` 2.3.1 | | TypeScript | 5.2.2 | `vue-tsc` 1.8.27(`pnpm type-check`) | | Pinia | 2.1.7 | | | Vue Router | 4.2.5 | | | VueUse | 10.6.1 | | | vue-i18n | 9.6.5 | zh-CN 起步 | | UnoCSS | 0.57.4 | + `@iconify-json/ep` 1.1.15 | | wangEditor | 5.1.23 | 富文本(`@wangeditor/editor-for-vue` 5.1.10) | | 其他 | axios 1.6.2 / dayjs 1.11.10 / echarts 5.4.3 / nprogress 0.2.0 / sortablejs 1.15.0 | | 包管理器 **pnpm**。注意 pnpm 11 已不再读 `package.json` 的 `pnpm` 字段,构建脚本白名单写在 `pnpm-workspace.yaml` 的 `allowBuilds` 里(`esbuild` / `vue-demi` 必须为 `true`,否则装完跑不起来)。 --- ## 2. 开发 ```bash pnpm install pnpm dev # http://localhost:5173 ,/admin-api、/app-api 代理到 127.0.0.1:48080 ``` 后端不在本机时的代理目标在 `.env.dev` 里改: ```properties VITE_PROXY_TARGET=http://127.0.0.1:48080 ``` 其他命令: ```bash pnpm build # 生产构建(mode=prod,terser + gzip) pnpm build:dev # 生产构建但用 dev 环境变量 pnpm type-check # vue-tsc --noEmit,CI 必过 pnpm preview # 本地预览 dist ``` --- ## 3. 环境变量 只从 `import.meta.env` 读,**任何后端地址/密钥都不写进代码**(见 `src/config/index.ts`)。 | 变量 | 用途 | |---|---| | `VITE_APP_TITLE` | 浏览器标题、登录页标题 | | `VITE_API_BASE_URL` | 后端基础地址;留空表示同源(开发走 Vite 代理,生产走 Nginx 反代) | | `VITE_BASE_PATH` | 部署子路径,如 `/parking/` | | `VITE_PROXY_TARGET` | 仅开发期的代理目标 | 生产用 Nginx 反代 `/admin-api`、`/app-api` 到后端 Jar,前端只发静态文件(`dist/`,已自带 `.gz`,Nginx 配 `gzip_static on;`)。 --- ## 4. 目录结构 ``` src/ ├── api/{module}/{entity}/index.ts # 一个实体一个文件,与后端模块同构 ├── views/{module}/{entity}/ # 页面(component 字段按相对 src/views 的路径给后端菜单表) ├── views/gov/ # ← 政务业务域(P6 迁入,原 PTMS-MANAGE 109 页) ├── layout/ # Layout / Sidebar / Navbar / TagsView / Breadcrumb / AppMain ├── router/ │ ├── routes.ts # 静态路由:登录 / 404 / redirect / 外壳(layoutRoute) │ └── index.ts ├── store/modules/ # app / user / permission / dict / tagsView / locale ├── utils/request.ts # axios 封装:Bearer + CommonResult 解包 + 401/403 ├── components/ directives/ styles/ locales/ types/ └── permission.ts # 路由守卫 ``` **菜单是后端驱动的**:`get-permission-info` 下发菜单树 → `store/modules/permission.ts` 把有 `component` 的叶子用 `import.meta.glob('/src/views/**/*.vue')` 转成动态路由;侧边栏直接渲染后端菜单树(不是路由表),所以**新增页面只要后端菜单表加一条 + 前端有对应组件文件,前端零改动**。 后端菜单的 `component` 字段填相对 `src/views` 的路径(不带 `.vue`),例如 `system/user/index` → `src/views/system/user/index.vue`。 `/` 外壳路由是**静态注册**的(`router/routes.ts` 的 `layoutRoute`,`redirect` 到 `HOME_PATH`),动态子路由用 `router.addRoute('Root', child)` 追加。这样即使某账号一个菜单都没有也不会白屏。 --- ## 5. 接口契约 | 项 | 约定 | |---|---| | 前缀 | `/admin-api/**`(管理后台)、`/app-api/**`(移动端/大屏),由后端包名自动加 | | 认证 | `Authorization: Bearer `,token 存 `localStorage.ACCESS_TOKEN` | | 返回 | `{code, msg, data}`,`code === 0` 为成功,`request.ts` 直接 resolve 出 `data` | | 未登录 | **HTTP 200 + body `code=401`** → 清 token、弹一次「重新登录」、跳登录页并带 `redirect` | | 无权限 | **HTTP 200 + body `code=403`** → toast「没有访问权限」,**不**登出 | ⚠️ 401/403 判定按**响应体 code**,不是 HTTP 状态码——这是与参考工程一致的约定,写新请求时不要改回按 HTTP 状态码判断。 --- ## 6. 系统管理页面(P1 已全部接通) 6 个系统页**已全部接真实接口**,取消所有骨架/开关写法(`P1_ENABLED`、`DICT_API_ENABLED` 已删除)。 | 页面 | 路径 | 列表端点 | 权限点 | |---|---|---|---| | 用户管理 | `/system/user` | `GET /admin-api/system/user/page` | `system:user:query` | | 角色管理 | `/system/role` | `GET /admin-api/system/role/page` | `system:role:query` | | 菜单管理 | `/system/menu` | `GET /admin-api/system/menu/list`(树,无分页) | `system:menu:query` | | 组织管理 | `/system/org` | `GET /admin-api/system/org/list`(树,无分页) | `system:org:query` | | 字典管理 | `/system/dict` | `GET /admin-api/system/dict-type/page` + `dict-data/page` | `system:dict:query` | | 日志管理 | `/system/log` | `GET /admin-api/system/operate-log/page` | `system:operate-log:query` | CRUD 的 URL 约定统一为 `${BASE}/create` `POST`、`/update` `PUT`、`/delete` `DELETE`、`/delete-list` `DELETE`(`BASE` 见各 `src/api/system/*/index.ts` 顶部常量)。 `src/views/system/user/index.vue` + `src/api/system/user/index.ts` 是**其余页面的参照样板**,新增页面直接照抄其结构。 ### 写新页面时最容易犯的三个错 1. **列表页必须 `onMounted(loadList)`**——漏了它页面能渲染但**一个请求都不发**,表现为「永远共 0 条、点刷新才出数据」(日志页当时就是这个症状,库里其实已有 30 条) 2. **表单里收的字段,后端 `RespVO` 必须下发**——否则编辑时拿不到原值、只能提交空串,而 `updateById` 覆盖整列,等于**静默清空**(组织 `remark` 的教训,详见 `../parking-cloud/AGENTS.md` §1 第 11 条) 3. **日期范围用 `value-format="YYYY-MM-DD"`**——操作日志的 `createTime` 是 `List`,传 ISO 串会 HTTP 400;数组查询参数靠 `paramsSerializer: { indexes: null }` 序列化成重复 key(`createTime=X&createTime=Y`) --- ## 7. 已知现象(不是 Bug) - **首次登录跳转时控制台有一条** `[Vue Router warn]: No match found for location with path "/dashboard"`。这是"菜单驱动动态路由"的固有现象:跳转发生在动态路由注册之前,守卫会立刻注册路由并重新导航,功能不受影响(vue-router 仅在 dev 模式告警)。 - **`504 (Outdated Optimize Dep)`**:Vite 首次预构建依赖时出现,刷新一次即消失;改动 `package.json` 依赖后 `rm -rf node_modules/.vite` 重启即可。 --- ## 8. 红线 1. 不改锁定版本(第 1 节表格);升级 Element Plus 前先确认 `el-tag type="primary"` 这类 2.5+ 才有的取值(本项目用 `type=""` + `effect="dark"` 表示选中态) 2. 不在代码里写死后端地址、AK、密钥,一律走环境变量 3. 不绕过 `src/utils/request.ts` 直接用 axios,否则会丢掉 Bearer 注入与 `CommonResult` 解包 4. 路由不写死在 `routes.ts`,业务页面一律走后端菜单下发 5. Portal 层不写业务逻辑;`views/` 只做展示与接口调用