Miniprogram: - Add back buttons to all custom-nav pages - Add debug/mock buttons (BLE connect, wear check, treatment) controlled by __DEV__ flag (hidden in prod) Admin console: - Fix log page using nonexistent fields (operator_type, target_type) now correctly reads admin_id/user_id/action/detail from API - Fix subscription date fields (started_at→start_time, expired_at→expire_time) - Wire up subscription detail/extend/renew action buttons - Wire up device detail "查看完整日志" button - Add "创建订阅" button to subscription toolbar - Fix subscription status mapping (2=expired, 3=cancelled) Docs: - Add detailed architecture docs for server, miniprogram, admin console
387 行
12 KiB
Markdown
387 行
12 KiB
Markdown
# 管理后台架构
|
||
|
||
## 概述
|
||
|
||
管理后台是一个基于 uni-app + Vue 3 的单页应用,编译目标为 H5(浏览器),部署在腾讯云 COS 上作为静态站点。通过调用服务端 Admin API 实现设备管理、用户管理、订阅管理等后台功能。
|
||
|
||
**技术栈:** uni-app + Vue 3 + Pinia + Vite
|
||
**AppID:** `__UNI__HOXADMIN`
|
||
**路由模式:** Hash(`#/pages/dashboard/index`)
|
||
|
||
---
|
||
|
||
## 目录结构
|
||
|
||
```
|
||
admin-console/
|
||
├── index.html # SPA 入口 HTML
|
||
├── package.json
|
||
├── vite.config.js # Vite 配置(仅引入 uni 插件)
|
||
├── scripts/
|
||
│ └── deploy-cos.js # COS 静态站点部署脚本
|
||
└── src/
|
||
├── App.vue # 根组件(登录状态检查)
|
||
├── main.js # 应用入口(createSSRApp + Pinia)
|
||
├── manifest.json # uni-app 配置
|
||
├── pages.json # 页面路由定义
|
||
├── config/
|
||
│ └── env.js # 环境配置(API 地址)
|
||
├── components/
|
||
│ └── AdminLayout.vue # 侧边栏 + 顶栏布局组件
|
||
├── store/
|
||
│ └── user.js # Pinia 用户状态(token、管理员信息)
|
||
├── utils/
|
||
│ ├── request.js # HTTP 请求封装(Bearer token 注入)
|
||
│ ├── export.js # CSV 导出工具
|
||
│ └── format.js # 日期格式化
|
||
├── styles/
|
||
│ └── common.css # 表格、徽标、按钮等公共样式
|
||
└── pages/
|
||
├── login/index.vue # 登录页
|
||
├── dashboard/index.vue # 仪表盘
|
||
├── device/index.vue # 设备列表
|
||
├── device-detail/index.vue # 设备详情
|
||
├── user/index.vue # 用户列表
|
||
├── user-detail/index.vue # 用户详情
|
||
├── subscription/index.vue# 订阅管理
|
||
├── record/index.vue # 护理记录
|
||
├── log/index.vue # 操作日志
|
||
└── settings/index.vue # 系统设置
|
||
```
|
||
|
||
共 10 个页面,1 个全局组件,1 个 Store。
|
||
|
||
---
|
||
|
||
## 应用启动流程
|
||
|
||
```
|
||
index.html → main.js
|
||
│
|
||
├── createSSRApp(App)
|
||
├── app.use(createPinia())
|
||
└── App.vue onLaunch()
|
||
│
|
||
├── useUserStore().isLoggedIn()
|
||
│ ├── 检查 Storage 中 admin_token 是否存在
|
||
│ └── 检查 admin_token_expiry 是否未过期(7天有效)
|
||
│
|
||
├── 已登录 → 停留在当前页
|
||
└── 未登录 → uni.reLaunch('/pages/login/index')
|
||
```
|
||
|
||
---
|
||
|
||
## 页面一览
|
||
|
||
### 登录页
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/login/index` |
|
||
| 导航栏 | 自定义(无系统导航栏) |
|
||
| API | `POST /api/v1/admin/login` |
|
||
| 功能 | 用户名+密码登录,成功后跳转到仪表盘 |
|
||
|
||
登录成功后:
|
||
1. `userStore.login()` 保存 token 和管理员信息
|
||
2. `uni.reLaunch('/pages/dashboard/index')`
|
||
|
||
### 仪表盘
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/dashboard/index` |
|
||
| API | `GET /admin/dashboard`, `GET /admin/records?page=1&page_size=5` |
|
||
| 功能 | 4 个统计卡片 + 最近护理表格 + 快捷导航 |
|
||
|
||
4 个统计指标:
|
||
|
||
| 指标 | 字段 | 图标 |
|
||
|------|------|------|
|
||
| 绑定设备数 | device_count | 📱 |
|
||
| 注册用户数 | user_count | 👥 |
|
||
| 护理次数 | treatment_count | 💆 |
|
||
| 活跃订阅 | subscription_count | 💳 |
|
||
|
||
### 设备管理
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/device/index` |
|
||
| API | `GET /admin/devices`, `POST /admin/devices` |
|
||
| 功能 | 设备列表(分页、搜索)、预生成产品码、CSV 导出 |
|
||
|
||
状态显示:未激活、在线、离线、禁用
|
||
|
||
### 设备详情
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/device-detail/index` |
|
||
| API | `GET /admin/devices/:id`, `POST .../unbind`, `POST .../command` |
|
||
| 功能 | 设备信息网格、绑定历史、护理记录、解绑、远程指令 |
|
||
|
||
### 用户管理
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/user/index` |
|
||
| API | `GET /admin/users` |
|
||
| 功能 | 用户列表(分页、搜索)、订阅状态徽标、CSV 导出 |
|
||
|
||
### 用户详情
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/user-detail/index` |
|
||
| API | `GET /admin/users/:user_id` |
|
||
| 功能 | 用户信息、统计行(护理次数/时长/消费)、最近护理记录 |
|
||
|
||
### 订阅管理
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/subscription/index` |
|
||
| API | `GET /admin/subscriptions`, `POST /admin/subscriptions` |
|
||
| 功能 | 统计行、Tab 过滤(全部/月度/年度/试用/过期)、创建订阅弹窗、CSV 导出 |
|
||
|
||
创建订阅表单字段:user_id, plan, amount, days
|
||
|
||
### 护理记录
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/record/index` |
|
||
| API | `GET /admin/records` |
|
||
| 功能 | 记录列表(分页、日期范围筛选、关键字搜索)、区域位掩码解码显示、CSV 导出 |
|
||
|
||
区域解码映射:1=左脸 2=右脸 4=额头 8=下巴 16=鼻 32=左眼 64=右眼
|
||
|
||
### 操作日志
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/log/index` |
|
||
| API | `GET /admin/logs` |
|
||
| 功能 | 时间线样式日志列表、类型过滤、角色徽标(管理员/系统/用户)、CSV 导出 |
|
||
|
||
### 系统设置
|
||
|
||
| 项目 | 说明 |
|
||
|------|------|
|
||
| 路径 | `/pages/settings/index` |
|
||
| API | `GET /admin/settings`, `POST /admin/settings` |
|
||
| 功能 | 三组配置 + 保存/重置按钮 |
|
||
|
||
配置项分组:
|
||
|
||
| 分组 | 字段 |
|
||
|------|------|
|
||
| 基本配置 | system_name, admin_email, timezone |
|
||
| 订阅定价 | monthly_price, yearly_price, trial_days |
|
||
| 功能开关 | enable_register, enable_binding, enable_free_mode, maintenance_mode |
|
||
|
||
---
|
||
|
||
## 全局布局组件 — AdminLayout
|
||
|
||
每个页面(除登录页)都包裹在 `<AdminLayout>` 组件中:
|
||
|
||
```
|
||
┌─────────────────────────────────────────────┐
|
||
│ Sidebar (220px) │ Header │
|
||
│ │ 页面标题 管理员名 退出 │
|
||
│ 🌸 光子美容仪 ├──────────────────────────┤
|
||
│ 管理后台 │ │
|
||
│ │ Content (<slot>) │
|
||
│ ▸ 仪表盘 │ │
|
||
│ ▸ 设备管理 │ │
|
||
│ ▸ 用户管理 │ │
|
||
│ ▸ 订阅管理 │ │
|
||
│ ▸ 护理记录 │ │
|
||
│ ▸ 操作日志 │ │
|
||
│ ▸ 系统设置 │ │
|
||
│ │ │
|
||
└─────────────────────────────────────────────┘
|
||
```
|
||
|
||
- **Props**:`currentPage` — 当前页面路径,用于高亮侧边栏菜单项
|
||
- **侧边栏导航**:使用 `uni.redirectTo`(替换页面,不累积页面栈)
|
||
- **退出登录**:`store.clearToken()` → `uni.reLaunch('/pages/login/index')`
|
||
- **管理员名称**:从 `userStore.adminInfo.real_name` 读取
|
||
|
||
---
|
||
|
||
## 状态管理 — Pinia Store
|
||
|
||
### user.js
|
||
|
||
```js
|
||
// State
|
||
token: ref('') // 从 Storage 初始化
|
||
adminInfo: ref(null) // { admin_id, username, real_name, role }
|
||
|
||
// Actions
|
||
setToken(val) // 保存 token + 计算过期时间(7天后)
|
||
clearToken() // 清除 token + adminInfo + Storage
|
||
login({ username, password }) // POST /admin/login → setToken + adminInfo
|
||
isLoggedIn() // 检查 token 非空 && 未过期
|
||
```
|
||
|
||
Token 存储位置:`uni.setStorageSync('admin_token')`
|
||
过期时间存储:`uni.setStorageSync('admin_token_expiry')`
|
||
有效期:7 天(客户端计算)
|
||
|
||
---
|
||
|
||
## HTTP 请求层(request.js)
|
||
|
||
### 请求流程
|
||
|
||
```
|
||
页面调用 get/post(url, data)
|
||
│
|
||
▼
|
||
request(options)
|
||
│
|
||
├── 从 Storage 读取 admin_token
|
||
├── 注入 Authorization: Bearer <token>
|
||
├── 拼接 BASE_URL + url
|
||
│
|
||
▼
|
||
uni.request()
|
||
│
|
||
├── code === 0 → resolve(res.data.data)
|
||
├── code === 1001/1002 → 清除 token → reLaunch 到 login → reject
|
||
└── 其他 code → reject(res.data)
|
||
```
|
||
|
||
### 注意事项
|
||
|
||
- Token 直接从 `uni.getStorageSync('admin_token')` 读取,不通过 Pinia store
|
||
- 没有 token 刷新机制(不同于小程序端),token 过期后走 1001/1002 重定向登录
|
||
- PUT 方法已定义但目前没有页面使用
|
||
|
||
---
|
||
|
||
## CSV 导出(export.js)
|
||
|
||
所有列表页都有"导出"按钮,调用 `exportCSV(filename, headers, rows)` :
|
||
|
||
1. 构建 CSV 字符串(带 BOM `` 确保 Excel 正确识别编码)
|
||
2. 创建 Blob → 临时 `<a>` 元素 → 触发下载
|
||
3. 仅 H5 端可用(使用 `document.createElement`)
|
||
|
||
---
|
||
|
||
## 样式体系
|
||
|
||
### 品牌色
|
||
|
||
| 色值 | 用途 |
|
||
|------|------|
|
||
| #E6508C | 主色(按钮、活跃状态、链接) |
|
||
| #001529 | 侧边栏背景(深蓝) |
|
||
| #f0f2f5 | 页面背景 |
|
||
| #52c41a | 成功状态徽标 |
|
||
| #faad14 | 警告状态徽标 |
|
||
| #ff4d4f | 错误状态徽标 |
|
||
|
||
### 公共样式(common.css)
|
||
|
||
被 device / user / subscription / record / log / device-detail / user-detail 7 个页面引用,包含:
|
||
|
||
- **Flex 表格**:`.data-table`, `.t-header`, `.t-row`, `.t-th`, `.t-td`, `.flex1/.flex2/.flex3`
|
||
- **徽标**:`.badge`, `.badge-success/.warning/.error/.default/.blue`
|
||
- **按钮**:`.btn-primary`(粉色), `.btn-default`(白色边框), `.btn-sm`
|
||
- **分页**:`.pagination`, `.btn-page`, `.page-info`
|
||
- **工具栏**:`.toolbar`, `.header-actions`, `.search-input`
|
||
- **卡片**:`.page-card`
|
||
- **操作链接**:`.action-link`(粉色文字)
|
||
|
||
Dashboard 和 Login 各自独立定义样式,不引用 common.css。
|
||
|
||
---
|
||
|
||
## 路由与导航
|
||
|
||
### pages.json 页面注册顺序
|
||
|
||
1. login(入口页)
|
||
2. dashboard
|
||
3. device → device-detail
|
||
4. user → user-detail
|
||
5. subscription
|
||
6. record
|
||
7. log
|
||
8. settings
|
||
|
||
没有 tabBar。登录页使用自定义导航栏,其他页面使用系统导航栏(白底黑字)。
|
||
|
||
### 导航方式
|
||
|
||
| 方法 | 使用场景 |
|
||
|------|---------|
|
||
| `uni.reLaunch` | 登录跳转、退出登录(清空页面栈) |
|
||
| `uni.redirectTo` | 侧边栏菜单切换(替换当前页) |
|
||
| `uni.navigateTo` | 进入详情页(压栈,可返回) |
|
||
| `uni.navigateBack` | 详情页返回列表 |
|
||
|
||
---
|
||
|
||
## 鉴权流程
|
||
|
||
```
|
||
1. 启动 App.vue onLaunch()
|
||
└── isLoggedIn() → 检查 admin_token 存在 + admin_token_expiry 未过期
|
||
├── 否 → uni.reLaunch('/pages/login/index')
|
||
└── 是 → 正常显示
|
||
|
||
2. 登录 POST /admin/login
|
||
└── 成功 → setToken(token)
|
||
├── Storage: admin_token = token
|
||
├── Storage: admin_token_expiry = now + 7天
|
||
└── 内存: adminInfo = { admin_id, username, real_name, role }
|
||
|
||
3. 每次 API 请求
|
||
└── request.js 从 Storage 读取 admin_token → Bearer header
|
||
├── 服务端返回 1001/1002 → 清 token → 跳登录
|
||
└── 正常响应 → 继续
|
||
|
||
4. 退出
|
||
└── AdminLayout 退出按钮 → clearToken() → reLaunch 登录
|
||
```
|
||
|
||
---
|
||
|
||
## 部署
|
||
|
||
### 构建
|
||
|
||
```bash
|
||
npm run build:h5 # uni build -p h5,输出到 dist/build/h5/
|
||
```
|
||
|
||
### 部署到 COS
|
||
|
||
```bash
|
||
npm run deploy:cos # 构建 + 上传到 COS
|
||
```
|
||
|
||
`scripts/deploy-cos.js`:
|
||
- 从 `server/.env` 读取 COS 凭证
|
||
- 上传 `dist/build/h5/` 下所有文件到 COS
|
||
- 文件路径前缀:`admin/`(可通过 `ADMIN_COS_PREFIX` 环境变量配置)
|
||
- 自动设置正确的 `Content-Type`
|
||
|
||
### 环境配置
|
||
|
||
| 环境 | API 地址 |
|
||
|------|---------|
|
||
| local | `http://localhost:3000` |
|
||
| test | `https://1426323813-ilxkhlxf4p.ap-guangzhou.tencentscf.com` |
|
||
| prod | 待配置 |
|
||
|
||
当前设置:`ENV = 'test'`
|