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. 开发
pnpm install
pnpm dev # http://localhost:5173 ,/admin-api、/app-api 代理到 127.0.0.1:48080
后端不在本机时的代理目标在 .env.dev 里改:
VITE_PROXY_TARGET=http://127.0.0.1:48080
其他命令:
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>,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 是其余页面的参照样板,新增页面直接照抄其结构。
写新页面时最容易犯的三个错
- 列表页必须
onMounted(loadList)——漏了它页面能渲染但一个请求都不发,表现为「永远共 0 条、点刷新才出数据」(日志页当时就是这个症状,库里其实已有 30 条) - 表单里收的字段,后端
RespVO必须下发——否则编辑时拿不到原值、只能提交空串,而updateById覆盖整列,等于静默清空(组织remark的教训,详见../parking-cloud/AGENTS.md§1 第 11 条) - 日期范围用
value-format="YYYY-MM-DD"——操作日志的createTime是List<LocalDate>,传 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 节表格);升级 Element Plus 前先确认
el-tag type="primary"这类 2.5+ 才有的取值(本项目用type=""+effect="dark"表示选中态) - 不在代码里写死后端地址、AK、密钥,一律走环境变量
- 不绕过
src/utils/request.ts直接用 axios,否则会丢掉 Bearer 注入与CommonResult解包 - 路由不写死在
routes.ts,业务页面一律走后端菜单下发 - Portal 层不写业务逻辑;
views/只做展示与接口调用