# 管理后台架构

## 概述

管理后台是一个基于 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 /api/v1/admin/dashboard`, `GET /api/v1/admin/records?page=1&page_size=5` |
| 功能 | 4 个统计卡片 + 最近护理表格 + 快捷导航 |

4 个统计指标：

| 指标 | 字段 | 图标 |
|------|------|------|
| 绑定设备数 | device_count | 📱 |
| 注册用户数 | user_count | 👥 |
| 护理次数 | treatment_count | 💆 |
| 活跃订阅 | subscription_count | 💳 |

### 设备管理

| 项目 | 说明 |
|------|------|
| 路径 | `/pages/device/index` |
| API | `GET /api/v1/admin/devices`, `POST /api/v1/admin/devices` |
| 功能 | 设备列表（分页、搜索）、预生成产品码、CSV 导出 |

状态显示：未激活、在线、离线、禁用

### 设备详情

| 项目 | 说明 |
|------|------|
| 路径 | `/pages/device-detail/index` |
| API | `GET /api/v1/admin/devices/:id`, `POST .../unbind`, `POST .../command` |
| 功能 | 设备信息网格、绑定历史、护理记录、解绑、远程指令 |

### 用户管理

| 项目 | 说明 |
|------|------|
| 路径 | `/pages/user/index` |
| API | `GET /api/v1/admin/users` |
| 功能 | 用户列表（分页、搜索）、订阅状态徽标、CSV 导出 |

### 用户详情

| 项目 | 说明 |
|------|------|
| 路径 | `/pages/user-detail/index` |
| API | `GET /api/v1/admin/users/:user_id` |
| 功能 | 用户信息、统计行（护理次数/时长/消费）、最近护理记录 |

### 订阅管理

| 项目 | 说明 |
|------|------|
| 路径 | `/pages/subscription/index` |
| API | `GET /api/v1/admin/subscriptions`, `POST /api/v1/admin/subscriptions` |
| 功能 | 统计行、Tab 过滤（全部/月度/年度/试用/过期）、创建订阅弹窗、CSV 导出 |

创建订阅表单字段：user_id, plan, amount, days

### 护理记录

| 项目 | 说明 |
|------|------|
| 路径 | `/pages/record/index` |
| API | `GET /api/v1/admin/records` |
| 功能 | 记录列表（分页、日期范围筛选、关键字搜索）、区域位掩码解码显示、CSV 导出 |

区域解码映射：1=左脸 2=右脸 4=额头 8=下巴 16=鼻 32=左眼 64=右眼

### 操作日志

| 项目 | 说明 |
|------|------|
| 路径 | `/pages/log/index` |
| API | `GET /api/v1/admin/logs` |
| 功能 | 时间线样式日志列表、类型过滤、角色徽标（管理员/系统/用户）、CSV 导出 |

### 系统设置

| 项目 | 说明 |
|------|------|
| 路径 | `/pages/settings/index` |
| API | `GET /api/v1/admin/settings`, `POST /api/v1/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'`
