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
12 KiB
12 KiB
管理后台架构
概述
管理后台是一个基于 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 |
| 功能 | 用户名+密码登录,成功后跳转到仪表盘 |
登录成功后:
userStore.login()保存 token 和管理员信息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
// 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) :
- 构建 CSV 字符串(带 BOM
确保 Excel 正确识别编码) - 创建 Blob → 临时
<a>元素 → 触发下载 - 仅 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 页面注册顺序
- login(入口页)
- dashboard
- device → device-detail
- user → user-detail
- subscription
- record
- log
- 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 登录
部署
构建
npm run build:h5 # uni build -p h5,输出到 dist/build/h5/
部署到 COS
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'