1369 lines
42 KiB
Markdown
1369 lines
42 KiB
Markdown
# 健身房管理系统前端技术架构详细设计文档
|
||
|
||
> 文档编号: GYM-FE-ARCH-001
|
||
> 版本: v1.0
|
||
> 日期: 2026-03-04
|
||
> 作者: 张翔
|
||
> 状态: 初稿
|
||
|
||
---
|
||
|
||
## 文档修订历史
|
||
|
||
| 版本 | 日期 | 作者 | 修订内容 |
|
||
| ---- | ---------- | ---- | -------- |
|
||
| v1.0 | 2026-03-04 | 张翔 | 创建前端技术架构详细设计 |
|
||
|
||
---
|
||
|
||
## 参考文档
|
||
|
||
- 《健身房管理系统基础版产品设计文档》 GYM-PRD-BASIC-001
|
||
- 《健身房管理系统基础版系统概要设计文档》 GYM-HLD-BASIC-001
|
||
- 《健身房管理系统基础版系统详细设计文档》 GYM-LLD-BASIC-001
|
||
- Vue 3 官方文档
|
||
- uniapp 官方文档
|
||
- TypeScript 官方文档
|
||
|
||
---
|
||
|
||
## 一、架构概述
|
||
|
||
### 1.1 架构目标
|
||
|
||
- **跨平台覆盖**:支持小程序、App、PC多端,满足不同用户角色需求
|
||
- **高性能**:首屏加载时间 < 2s,交互响应时间 < 100ms
|
||
- **高安全性**:符合金融级安全标准,保障用户数据和交易安全
|
||
- **高可维护性**:代码结构清晰,模块化设计,便于团队协作
|
||
- **高扩展性**:支持订阅模块动态加载,适应业务快速变化
|
||
|
||
### 1.2 客户端架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ 前端客户端架构 │
|
||
├─────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ 会员小程序 (uniapp+Vue3) │ │
|
||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||
│ │ • 会员注册/登录 • 课程预约 • 扫码签到 • 会员卡管理 │ │
|
||
│ │ • 个人中心 • 消息通知 • 数据统计 │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ 教练端App (uniapp+Vue3) │ │
|
||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||
│ │ • 课程管理 • 排班管理 • 会员管理 • 签到管理 │ │
|
||
│ │ • 数据统计 • 消息通知 • 个人中心 │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ 管理后台PC (Vue3+Vite+Element Plus) │ │
|
||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||
│ │ • 会员管理 • 课程管理 • 预约管理 • 签到管理 │ │
|
||
│ │ • 财务管理 • 数据统计 • 系统管理 • 订阅管理 │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 1.3 技术栈选型
|
||
|
||
#### 1.3.1 核心框架
|
||
|
||
| 技术 | 版本 | 用途 | 选型理由 |
|
||
|------|------|------|----------|
|
||
| **Vue 3** | 3.4+ | 前端框架 | Composition API、响应式系统、生态成熟 |
|
||
| **uniapp** | 3.0+ | 跨平台框架 | 一次开发多端部署、性能优秀、生态完善 |
|
||
| **TypeScript** | 5.0+ | 类型系统 | 类型安全、开发体验、代码可维护性 |
|
||
| **Vite** | 5.0+ | 构建工具 | 快速热更新、优化的构建性能、插件生态 |
|
||
|
||
#### 1.3.2 状态管理
|
||
|
||
| 技术 | 版本 | 用途 | 选型理由 |
|
||
|------|------|------|----------|
|
||
| **Pinia** | 2.1+ | 状态管理 | Vue3官方推荐、API简洁、TypeScript支持好、模块化设计 |
|
||
|
||
#### 1.3.3 UI组件库
|
||
|
||
| 技术 | 版本 | 用途 | 选型理由 |
|
||
|------|------|------|----------|
|
||
| **Element Plus** | 2.5+ | PC端UI组件 | 功能完善、设计规范、TypeScript支持、文档齐全 |
|
||
| **uni-ui** | 1.5+ | 小程序/App UI组件 | 官方组件库、跨平台兼容、性能优化 |
|
||
|
||
#### 1.3.4 工具库
|
||
|
||
| 技术 | 版本 | 用途 | 选型理由 |
|
||
|------|------|------|----------|
|
||
| **Vue Router** | 4.2+ | 路由管理 | Vue官方路由、动态路由、路由守卫 |
|
||
| **Axios** | 1.6+ | HTTP客户端 | 拦截器、请求取消、TypeScript支持 |
|
||
| **Day.js** | 1.11+ | 日期处理 | 轻量级、API友好、国际化支持 |
|
||
| **Lodash-es** | 4.17+ | 工具函数 | 按需加载、性能优化、功能完善 |
|
||
|
||
#### 1.3.5 开发工具
|
||
|
||
| 技术 | 版本 | 用途 | 选型理由 |
|
||
|------|------|------|----------|
|
||
| **ESLint** | 8.56+ | 代码检查 | 规则完善、插件生态、自动修复 |
|
||
| **Prettier** | 3.1+ | 代码格式化 | 统一风格、配置简单、编辑器集成 |
|
||
| **Husky** | 8.0+ | Git钩子 | 提交前检查、自动化流程 |
|
||
| **Commitlint** | 18.4+ | 提交规范 | 规范提交信息、自动化生成Changelog |
|
||
|
||
---
|
||
|
||
## 二、架构设计
|
||
|
||
### 2.1 分层架构
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ 前端分层架构 │
|
||
├─────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ 表现层 (Presentation Layer) │ │
|
||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||
│ │ • 页面组件 • 业务组件 • 基础组件 • 布局组件 │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ 状态管理层 (State Management Layer) │ │
|
||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||
│ │ • 全局状态 • 模块状态 • 组件状态 • 持久化状态 │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ 业务逻辑层 (Business Logic Layer) │ │
|
||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||
│ │ • Composables • Hooks • Utils • Validators │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ 数据访问层 (Data Access Layer) │ │
|
||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||
│ │ • API Service • WebSocket • Cache • Storage │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
│ ┌─────────────────────────────────────────────────────────────────┐ │
|
||
│ │ 基础设施层 (Infrastructure Layer) │ │
|
||
│ ├─────────────────────────────────────────────────────────────────┤ │
|
||
│ │ • 路由 • 拦截器 • 错误处理 • 日志 • 监控 │ │
|
||
│ └─────────────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 2.2 模块划分
|
||
|
||
#### 2.2.1 会员端模块
|
||
|
||
```
|
||
会员端 (Member Mini Program)
|
||
├── 认证模块 (Auth)
|
||
│ ├── 登录/注册
|
||
│ ├── 手机号验证
|
||
│ ├── 微信授权
|
||
│ └── 忘记密码
|
||
├── 会员模块 (Member)
|
||
│ ├── 个人信息
|
||
│ ├── 会员卡管理
|
||
│ ├── 权益管理
|
||
│ └── 等级体系
|
||
├── 预约模块 (Booking)
|
||
│ ├── 团课列表
|
||
│ ├── 课程详情
|
||
│ ├── 预约操作
|
||
│ └── 预约记录
|
||
├── 签到模块 (CheckIn)
|
||
│ ├── 扫码签到
|
||
│ ├── 签到记录
|
||
│ └── 签到统计
|
||
├── 消息模块 (Message)
|
||
│ ├── 系统通知
|
||
│ ├── 预约提醒
|
||
│ └── 消息中心
|
||
└── 个人中心 (Profile)
|
||
├── 设置
|
||
├── 帮助
|
||
└── 关于
|
||
```
|
||
|
||
#### 2.2.2 教练端模块
|
||
|
||
```
|
||
教练端 (Coach App)
|
||
├── 认证模块 (Auth)
|
||
│ ├── 登录
|
||
│ └── 权限验证
|
||
├── 课程模块 (Course)
|
||
│ ├── 课程管理
|
||
│ ├── 排班管理
|
||
│ └── 课程统计
|
||
├── 会员模块 (Member)
|
||
│ ├── 会员列表
|
||
│ ├── 会员详情
|
||
│ └── 会员跟进
|
||
├── 签到模块 (CheckIn)
|
||
│ ├── 签到管理
|
||
│ ├── 代签操作
|
||
│ └── 签到统计
|
||
├── 数据模块 (Data)
|
||
│ ├── 课程数据
|
||
│ ├── 会员数据
|
||
│ └── 收入数据
|
||
└── 个人中心 (Profile)
|
||
├── 个人信息
|
||
├── 设置
|
||
└── 帮助
|
||
```
|
||
|
||
#### 2.2.3 管理后台模块
|
||
|
||
```
|
||
管理后台 (Admin PC)
|
||
├── 认证模块 (Auth)
|
||
│ ├── 登录
|
||
│ ├── 权限管理
|
||
│ └── 角色管理
|
||
├── 会员模块 (Member)
|
||
│ ├── 会员管理
|
||
│ ├── 会员卡管理
|
||
│ ├── 权益管理
|
||
│ └── 等级管理
|
||
├── 课程模块 (Course)
|
||
│ ├── 课程管理
|
||
│ ├── 时段管理
|
||
│ └── 排班管理
|
||
├── 预约模块 (Booking)
|
||
│ ├── 预约管理
|
||
│ ├── 候补管理
|
||
│ └── 预约统计
|
||
├── 签到模块 (CheckIn)
|
||
│ ├── 签到记录
|
||
│ ├── 设备管理
|
||
│ └── 签到统计
|
||
├── 财务模块 (Finance)
|
||
│ ├── 订单管理
|
||
│ ├── 收入统计
|
||
│ └── 财务报表
|
||
├── 数据模块 (Data)
|
||
│ ├── 数据统计
|
||
│ ├── 数据分析
|
||
│ └── 报表导出
|
||
├── 系统模块 (System)
|
||
│ ├── 用户管理
|
||
│ ├── 角色权限
|
||
│ ├── 系统配置
|
||
│ └── 操作日志
|
||
└── 订阅模块 (Subscription)
|
||
├── 订阅管理
|
||
├── 模块管理
|
||
└── 计费管理
|
||
```
|
||
|
||
### 2.3 目录结构
|
||
|
||
#### 2.3.1 会员端/教练端目录结构
|
||
|
||
```
|
||
src/
|
||
├── api/ # API接口
|
||
│ ├── modules/
|
||
│ │ ├── auth.ts
|
||
│ │ ├── member.ts
|
||
│ │ ├── booking.ts
|
||
│ │ └── checkin.ts
|
||
│ └── request.ts # Axios封装
|
||
├── assets/ # 静态资源
|
||
│ ├── images/
|
||
│ ├── icons/
|
||
│ └── styles/
|
||
├── components/ # 组件
|
||
│ ├── base/ # 基础组件
|
||
│ ├── business/ # 业务组件
|
||
│ └── layout/ # 布局组件
|
||
├── composables/ # Composables
|
||
│ ├── useAuth.ts
|
||
│ ├── useBooking.ts
|
||
│ └── useCheckIn.ts
|
||
├── config/ # 配置
|
||
│ ├── app.config.ts
|
||
│ └── env.config.ts
|
||
├── hooks/ # Hooks
|
||
│ ├── useRequest.ts
|
||
│ └── useWebSocket.ts
|
||
├── pages/ # 页面
|
||
│ ├── index/
|
||
│ ├── auth/
|
||
│ ├── member/
|
||
│ ├── booking/
|
||
│ └── checkin/
|
||
├── stores/ # 状态管理
|
||
│ ├── auth.ts
|
||
│ ├── member.ts
|
||
│ ├── booking.ts
|
||
│ └── checkin.ts
|
||
├── types/ # 类型定义
|
||
│ ├── api.ts
|
||
│ ├── models.ts
|
||
│ └── index.ts
|
||
├── utils/ # 工具函数
|
||
│ ├── validator.ts
|
||
│ ├── formatter.ts
|
||
│ └── storage.ts
|
||
├── App.vue
|
||
└── main.ts
|
||
```
|
||
|
||
#### 2.3.2 管理后台目录结构
|
||
|
||
```
|
||
src/
|
||
├── api/ # API接口
|
||
│ ├── modules/
|
||
│ │ ├── auth.ts
|
||
│ │ ├── member.ts
|
||
│ │ ├── course.ts
|
||
│ │ ├── booking.ts
|
||
│ │ ├── checkin.ts
|
||
│ │ ├── finance.ts
|
||
│ │ └── system.ts
|
||
│ └── request.ts # Axios封装
|
||
├── assets/ # 静态资源
|
||
│ ├── images/
|
||
│ ├── icons/
|
||
│ └── styles/
|
||
├── components/ # 组件
|
||
│ ├── base/ # 基础组件
|
||
│ ├── business/ # 业务组件
|
||
│ └── layout/ # 布局组件
|
||
├── composables/ # Composables
|
||
│ ├── useAuth.ts
|
||
│ ├── usePermission.ts
|
||
│ └── useTable.ts
|
||
├── config/ # 配置
|
||
│ ├── app.config.ts
|
||
│ └── env.config.ts
|
||
├── directives/ # 自定义指令
|
||
│ ├── permission.ts
|
||
│ └── loading.ts
|
||
├── hooks/ # Hooks
|
||
│ ├── useRequest.ts
|
||
│ └── useWebSocket.ts
|
||
├── layouts/ # 布局
|
||
│ ├── BasicLayout.vue
|
||
│ └── BlankLayout.vue
|
||
├── router/ # 路由
|
||
│ ├── index.ts
|
||
│ └── modules/
|
||
│ ├── auth.ts
|
||
│ ├── member.ts
|
||
│ ├── course.ts
|
||
│ └── system.ts
|
||
├── stores/ # 状态管理
|
||
│ ├── auth.ts
|
||
│ ├── permission.ts
|
||
│ ├── app.ts
|
||
│ └── user.ts
|
||
├── types/ # 类型定义
|
||
│ ├── api.ts
|
||
│ ├── models.ts
|
||
│ └── index.ts
|
||
├── utils/ # 工具函数
|
||
│ ├── validator.ts
|
||
│ ├── formatter.ts
|
||
│ ├── storage.ts
|
||
│ └── permission.ts
|
||
├── views/ # 页面
|
||
│ ├── auth/
|
||
│ ├── member/
|
||
│ ├── course/
|
||
│ ├── booking/
|
||
│ ├── checkin/
|
||
│ ├── finance/
|
||
│ ├── data/
|
||
│ ├── system/
|
||
│ └── subscription/
|
||
├── App.vue
|
||
└── main.ts
|
||
```
|
||
|
||
---
|
||
|
||
## 三、数据流设计
|
||
|
||
### 3.1 单向数据流
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────┐
|
||
│ 单向数据流设计 │
|
||
├─────────────────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||
│ │ 用户 │ ──▶│ 组件 │ ──▶│ Store │ ──▶│ API │ │
|
||
│ │ 操作 │ │ Action │ │ State │ │ Service │ │
|
||
│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │
|
||
│ │ │ │ │ │
|
||
│ │ │ │ │ │
|
||
│ │ │ │ ▼ │
|
||
│ │ │ │ ┌─────────┐ │
|
||
│ │ │ │ │ 后端API │ │
|
||
│ │ │ │ └─────────┘ │
|
||
│ │ │ │ │ │
|
||
│ │ │ │ ▼ │
|
||
│ │ │ │ ┌─────────┐ │
|
||
│ │ │ │ │ 数据库 │ │
|
||
│ │ │ │ └─────────┘ │
|
||
│ │ │ │ │ │
|
||
│ │ │ │ ▼ │
|
||
│ │ │ │ ┌─────────┐ │
|
||
│ │ │ │ │ 返回数据 │ │
|
||
│ │ │ │ └─────────┘ │
|
||
│ │ │ │ │ │
|
||
│ │ │ ▼ │ │
|
||
│ │ │ ┌─────────┐ │ │
|
||
│ │ │ │ 更新 │◀─────────┘ │
|
||
│ │ │ │ State │ │
|
||
│ │ │ └─────────┘ │
|
||
│ │ │ │ │
|
||
│ │ ▼ ▼ │
|
||
│ │ ┌─────────────────────────┐ │
|
||
│ │ │ 重新渲染组件 │ │
|
||
│ │ └─────────────────────────┘ │
|
||
│ │ │ │
|
||
│ ▼ ▼ │
|
||
│ ┌─────────────────────────┐ │
|
||
│ │ 更新UI显示 │ │
|
||
│ └─────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 3.2 状态管理策略
|
||
|
||
#### 3.2.1 状态分类
|
||
|
||
| 状态类型 | 存储位置 | 持久化 | 示例 |
|
||
|---------|----------|--------|------|
|
||
| **全局状态** | Pinia Store | 是 | 用户信息、权限、配置 |
|
||
| **模块状态** | Pinia Store | 否 | 当前页面数据、表单数据 |
|
||
| **组件状态** | Component State | 否 | 弹窗显示、加载状态 |
|
||
| **临时状态** | Component State | 否 | 输入框值、选择项 |
|
||
|
||
#### 3.2.2 状态持久化
|
||
|
||
```typescript
|
||
// stores/auth.ts
|
||
import { defineStore } from 'pinia'
|
||
import { ref } from 'vue'
|
||
|
||
export const useAuthStore = defineStore('auth', () => {
|
||
const token = ref<string>('')
|
||
const user = ref<User | null>(null)
|
||
|
||
// 持久化到localStorage
|
||
const $persist = () => {
|
||
localStorage.setItem('auth_token', token.value)
|
||
localStorage.setItem('auth_user', JSON.stringify(user.value))
|
||
}
|
||
|
||
const login = async (credentials: LoginRequest) => {
|
||
const response = await api.login(credentials)
|
||
token.value = response.token
|
||
user.value = response.user
|
||
$persist()
|
||
}
|
||
|
||
const logout = () => {
|
||
token.value = ''
|
||
user.value = null
|
||
localStorage.removeItem('auth_token')
|
||
localStorage.removeItem('auth_user')
|
||
}
|
||
|
||
return { token, user, login, logout }
|
||
})
|
||
```
|
||
|
||
### 3.3 实时数据处理
|
||
|
||
#### 3.3.1 WebSocket连接管理
|
||
|
||
```typescript
|
||
// hooks/useWebSocket.ts
|
||
import { ref, onUnmounted } from 'vue'
|
||
|
||
export function useWebSocket(url: string) {
|
||
const ws = ref<WebSocket | null>(null)
|
||
const connected = ref(false)
|
||
const messageHandler = ref<(data: any) => void>(() => {})
|
||
|
||
const connect = () => {
|
||
ws.value = new WebSocket(url)
|
||
|
||
ws.value.onopen = () => {
|
||
connected.value = true
|
||
startHeartbeat()
|
||
}
|
||
|
||
ws.value.onmessage = (event) => {
|
||
const data = JSON.parse(event.data)
|
||
messageHandler.value(data)
|
||
}
|
||
|
||
ws.value.onclose = () => {
|
||
connected.value = false
|
||
reconnect()
|
||
}
|
||
|
||
ws.value.onerror = (error) => {
|
||
console.error('WebSocket error:', error)
|
||
}
|
||
}
|
||
|
||
const disconnect = () => {
|
||
if (ws.value) {
|
||
ws.value.close()
|
||
ws.value = null
|
||
}
|
||
}
|
||
|
||
const send = (data: any) => {
|
||
if (ws.value && connected.value) {
|
||
ws.value.send(JSON.stringify(data))
|
||
}
|
||
}
|
||
|
||
const reconnect = () => {
|
||
setTimeout(() => {
|
||
connect()
|
||
}, 3000)
|
||
}
|
||
|
||
const startHeartbeat = () => {
|
||
setInterval(() => {
|
||
send({ type: 'ping' })
|
||
}, 30000)
|
||
}
|
||
|
||
onUnmounted(() => {
|
||
disconnect()
|
||
})
|
||
|
||
return {
|
||
connected,
|
||
connect,
|
||
disconnect,
|
||
send,
|
||
onMessage: (handler: (data: any) => void) => {
|
||
messageHandler.value = handler
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 3.3.2 数据更新优化
|
||
|
||
```typescript
|
||
// composables/useRealTimeData.ts
|
||
import { ref, computed } from 'vue'
|
||
import { useWebSocket } from '@/hooks/useWebSocket'
|
||
|
||
export function useRealTimeData<T>(initialData: T, wsUrl: string) {
|
||
const data = ref<T>(initialData)
|
||
const { connected, connect, send, onMessage } = useWebSocket(wsUrl)
|
||
|
||
const updateData = (newData: Partial<T>) => {
|
||
data.value = { ...data.value, ...newData }
|
||
}
|
||
|
||
const subscribe = (channel: string) => {
|
||
onMessage((message) => {
|
||
if (message.channel === channel) {
|
||
// 增量更新,减少重渲染
|
||
updateData(message.data)
|
||
}
|
||
})
|
||
}
|
||
|
||
return {
|
||
data,
|
||
connected,
|
||
connect,
|
||
subscribe
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 四、组件设计
|
||
|
||
### 4.1 组件分类
|
||
|
||
#### 4.1.1 基础组件
|
||
|
||
| 组件名称 | 功能 | 适用端 |
|
||
|---------|------|--------|
|
||
| Button | 按钮 | 全端 |
|
||
| Input | 输入框 | 全端 |
|
||
| Select | 选择器 | 全端 |
|
||
| DatePicker | 日期选择器 | 全端 |
|
||
| Modal | 弹窗 | 全端 |
|
||
| Loading | 加载 | 全端 |
|
||
| Empty | 空状态 | 全端 |
|
||
| ErrorPage | 错误页 | 全端 |
|
||
|
||
#### 4.1.2 业务组件
|
||
|
||
| 组件名称 | 功能 | 适用端 |
|
||
|---------|------|--------|
|
||
| MemberCard | 会员卡展示 | 全端 |
|
||
| CourseCard | 课程卡片 | 全端 |
|
||
| BookingCard | 预约卡片 | 全端 |
|
||
| CheckInCode | 签到码 | 会员端/教练端 |
|
||
| QRCodeScanner | 二维码扫描 | 会员端/教练端 |
|
||
| DataChart | 数据图表 | 管理后台 |
|
||
| DataTable | 数据表格 | 管理后台 |
|
||
|
||
#### 4.1.3 布局组件
|
||
|
||
| 组件名称 | 功能 | 适用端 |
|
||
|---------|------|--------|
|
||
| PageLayout | 页面布局 | 全端 |
|
||
| TabBar | 底部导航 | 会员端/教练端 |
|
||
| Sidebar | 侧边栏 | 管理后台 |
|
||
| Header | 顶部导航 | 管理后台 |
|
||
| Footer | 底部 | 管理后台 |
|
||
|
||
### 4.2 组件设计原则
|
||
|
||
#### 4.2.1 单一职责原则
|
||
|
||
每个组件只负责一个功能,保持组件的简洁和可复用性。
|
||
|
||
```vue
|
||
<!-- Bad: 职责过多 -->
|
||
<template>
|
||
<div>
|
||
<input v-model="form.name" />
|
||
<input v-model="form.phone" />
|
||
<button @click="submit">提交</button>
|
||
<div v-if="showChart">
|
||
<chart :data="chartData" />
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<!-- Good: 职责单一 -->
|
||
<template>
|
||
<member-form :model="form" @submit="handleSubmit" />
|
||
<data-chart v-if="showChart" :data="chartData" />
|
||
</template>
|
||
```
|
||
|
||
#### 4.2.2 Props设计
|
||
|
||
```typescript
|
||
// components/base/Button.vue
|
||
interface ButtonProps {
|
||
type?: 'primary' | 'secondary' | 'danger'
|
||
size?: 'small' | 'medium' | 'large'
|
||
disabled?: boolean
|
||
loading?: boolean
|
||
block?: boolean
|
||
}
|
||
|
||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||
type: 'primary',
|
||
size: 'medium',
|
||
disabled: false,
|
||
loading: false,
|
||
block: false
|
||
})
|
||
```
|
||
|
||
#### 4.2.3 事件设计
|
||
|
||
```typescript
|
||
// components/base/DatePicker.vue
|
||
const emit = defineEmits<{
|
||
change: [value: Date]
|
||
confirm: [value: Date]
|
||
cancel: []
|
||
}>()
|
||
|
||
// 使用
|
||
emit('change', new Date())
|
||
emit('confirm', new Date())
|
||
emit('cancel')
|
||
```
|
||
|
||
### 4.3 组件复用策略
|
||
|
||
#### 4.3.1 跨端组件
|
||
|
||
使用uniapp的条件编译实现跨端组件:
|
||
|
||
```vue
|
||
<template>
|
||
<view class="button">
|
||
<!-- #ifdef MP-WEIXIN -->
|
||
<button open-type="getUserInfo" @getuserinfo="handleGetUserInfo">
|
||
{{ text }}
|
||
</button>
|
||
<!-- #endif -->
|
||
|
||
<!-- #ifdef APP-PLUS -->
|
||
<button @click="handleClick">
|
||
{{ text }}
|
||
</button>
|
||
<!-- #endif -->
|
||
</view>
|
||
</template>
|
||
```
|
||
|
||
#### 4.3.2 业务组件封装
|
||
|
||
```vue
|
||
<!-- components/business/CourseCard.vue -->
|
||
<template>
|
||
<view class="course-card" @click="handleClick">
|
||
<image :src="course.coverImage" mode="aspectFill" />
|
||
<view class="course-info">
|
||
<text class="course-name">{{ course.name }}</text>
|
||
<text class="course-time">{{ course.startTime }}</text>
|
||
<text class="course-capacity">
|
||
{{ course.bookedCount }}/{{ course.capacity }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
interface Course {
|
||
id: number
|
||
name: string
|
||
coverImage: string
|
||
startTime: string
|
||
bookedCount: number
|
||
capacity: number
|
||
}
|
||
|
||
interface Props {
|
||
course: Course
|
||
}
|
||
|
||
const props = defineProps<Props>()
|
||
const emit = defineEmits<{
|
||
click: [course: Course]
|
||
}>()
|
||
|
||
const handleClick = () => {
|
||
emit('click', props.course)
|
||
}
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 五、路由设计
|
||
|
||
### 5.1 路由结构
|
||
|
||
#### 5.1.1 会员端路由
|
||
|
||
```typescript
|
||
// router/index.ts
|
||
const routes = [
|
||
{
|
||
path: '/pages/index/index',
|
||
name: 'Home',
|
||
meta: { title: '首页' }
|
||
},
|
||
{
|
||
path: '/pages/auth/login',
|
||
name: 'Login',
|
||
meta: { title: '登录' }
|
||
},
|
||
{
|
||
path: '/pages/member/profile',
|
||
name: 'Profile',
|
||
meta: { title: '个人中心', requiresAuth: true }
|
||
},
|
||
{
|
||
path: '/pages/booking/list',
|
||
name: 'BookingList',
|
||
meta: { title: '课程列表', requiresAuth: true }
|
||
},
|
||
{
|
||
path: '/pages/booking/detail',
|
||
name: 'BookingDetail',
|
||
meta: { title: '课程详情', requiresAuth: true }
|
||
},
|
||
{
|
||
path: '/pages/checkin/scan',
|
||
name: 'CheckInScan',
|
||
meta: { title: '扫码签到', requiresAuth: true }
|
||
}
|
||
]
|
||
```
|
||
|
||
#### 5.1.2 管理后台路由
|
||
|
||
```typescript
|
||
// router/index.ts
|
||
const routes = [
|
||
{
|
||
path: '/login',
|
||
name: 'Login',
|
||
component: () => import('@/views/auth/Login.vue'),
|
||
meta: { title: '登录' }
|
||
},
|
||
{
|
||
path: '/',
|
||
component: () => import('@/layouts/BasicLayout.vue'),
|
||
redirect: '/dashboard',
|
||
children: [
|
||
{
|
||
path: 'dashboard',
|
||
name: 'Dashboard',
|
||
component: () => import('@/views/dashboard/Index.vue'),
|
||
meta: { title: '仪表盘', requiresAuth: true }
|
||
},
|
||
{
|
||
path: 'member',
|
||
name: 'Member',
|
||
redirect: '/member/list',
|
||
meta: { title: '会员管理', requiresAuth: true, permission: 'member:view' },
|
||
children: [
|
||
{
|
||
path: 'list',
|
||
name: 'MemberList',
|
||
component: () => import('@/views/member/List.vue'),
|
||
meta: { title: '会员列表' }
|
||
},
|
||
{
|
||
path: 'detail/:id',
|
||
name: 'MemberDetail',
|
||
component: () => import('@/views/member/Detail.vue'),
|
||
meta: { title: '会员详情' }
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
]
|
||
```
|
||
|
||
### 5.2 路由守卫
|
||
|
||
#### 5.2.1 认证守卫
|
||
|
||
```typescript
|
||
// router/guards/auth.ts
|
||
import { useAuthStore } from '@/stores/auth'
|
||
|
||
router.beforeEach((to, from, next) => {
|
||
const authStore = useAuthStore()
|
||
|
||
if (to.meta.requiresAuth && !authStore.token) {
|
||
next('/login')
|
||
} else {
|
||
next()
|
||
}
|
||
})
|
||
```
|
||
|
||
#### 5.2.2 权限守卫
|
||
|
||
```typescript
|
||
// router/guards/permission.ts
|
||
import { usePermissionStore } from '@/stores/permission'
|
||
|
||
router.beforeEach((to, from, next) => {
|
||
const permissionStore = usePermissionStore()
|
||
|
||
if (to.meta.permission && !permissionStore.hasPermission(to.meta.permission as string)) {
|
||
next('/403')
|
||
} else {
|
||
next()
|
||
}
|
||
})
|
||
```
|
||
|
||
### 5.3 动态路由
|
||
|
||
```typescript
|
||
// router/dynamic.ts
|
||
import { usePermissionStore } from '@/stores/permission'
|
||
|
||
export function setupDynamicRoutes() {
|
||
const permissionStore = usePermissionStore()
|
||
const routes = permissionStore.generateRoutes()
|
||
|
||
routes.forEach(route => {
|
||
router.addRoute(route)
|
||
})
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 六、API设计
|
||
|
||
### 6.1 Axios封装
|
||
|
||
```typescript
|
||
// api/request.ts
|
||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||
import { useAuthStore } from '@/stores/auth'
|
||
|
||
class Request {
|
||
private instance: AxiosInstance
|
||
|
||
constructor(config: AxiosRequestConfig) {
|
||
this.instance = axios.create(config)
|
||
this.setupInterceptors()
|
||
}
|
||
|
||
private setupInterceptors() {
|
||
// 请求拦截器
|
||
this.instance.interceptors.request.use(
|
||
(config) => {
|
||
const authStore = useAuthStore()
|
||
if (authStore.token) {
|
||
config.headers.Authorization = `Bearer ${authStore.token}`
|
||
}
|
||
return config
|
||
},
|
||
(error) => {
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
|
||
// 响应拦截器
|
||
this.instance.interceptors.response.use(
|
||
(response: AxiosResponse) => {
|
||
const { code, data, message } = response.data
|
||
|
||
if (code === 200) {
|
||
return data
|
||
} else {
|
||
return Promise.reject(new Error(message))
|
||
}
|
||
},
|
||
(error) => {
|
||
if (error.response?.status === 401) {
|
||
const authStore = useAuthStore()
|
||
authStore.logout()
|
||
window.location.href = '/login'
|
||
}
|
||
return Promise.reject(error)
|
||
}
|
||
)
|
||
}
|
||
|
||
public get<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||
return this.instance.get(url, config)
|
||
}
|
||
|
||
public post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||
return this.instance.post(url, data, config)
|
||
}
|
||
|
||
public put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
|
||
return this.instance.put(url, data, config)
|
||
}
|
||
|
||
public delete<T = any>(url: string, config?: AxiosRequestConfig): Promise<T> {
|
||
return this.instance.delete(url, config)
|
||
}
|
||
}
|
||
|
||
export default new Request({
|
||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||
timeout: 10000
|
||
})
|
||
```
|
||
|
||
### 6.2 API模块化
|
||
|
||
```typescript
|
||
// api/modules/member.ts
|
||
import request from '../request'
|
||
|
||
export interface Member {
|
||
id: number
|
||
name: string
|
||
phone: string
|
||
avatar?: string
|
||
level: number
|
||
exp: number
|
||
}
|
||
|
||
export interface MemberListParams {
|
||
page: number
|
||
pageSize: number
|
||
keyword?: string
|
||
level?: number
|
||
}
|
||
|
||
export interface MemberListResponse {
|
||
list: Member[]
|
||
total: number
|
||
}
|
||
|
||
export const memberApi = {
|
||
// 获取会员列表
|
||
getList: (params: MemberListParams) => {
|
||
return request.get<MemberListResponse>('/member/list', { params })
|
||
},
|
||
|
||
// 获取会员详情
|
||
getDetail: (id: number) => {
|
||
return request.get<Member>(`/member/${id}`)
|
||
},
|
||
|
||
// 创建会员
|
||
create: (data: Partial<Member>) => {
|
||
return request.post<Member>('/member', data)
|
||
},
|
||
|
||
// 更新会员
|
||
update: (id: number, data: Partial<Member>) => {
|
||
return request.put<Member>(`/member/${id}`, data)
|
||
},
|
||
|
||
// 删除会员
|
||
delete: (id: number) => {
|
||
return request.delete(`/member/${id}`)
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 七、性能优化
|
||
|
||
### 7.1 加载性能优化
|
||
|
||
#### 7.1.1 代码分割
|
||
|
||
```typescript
|
||
// 路由懒加载
|
||
const routes = [
|
||
{
|
||
path: '/member/list',
|
||
component: () => import('@/views/member/List.vue')
|
||
}
|
||
]
|
||
```
|
||
|
||
#### 7.1.2 资源优化
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
export default defineConfig({
|
||
build: {
|
||
rollupOptions: {
|
||
output: {
|
||
manualChunks: {
|
||
'vue-vendor': ['vue', 'vue-router', 'pinia'],
|
||
'element-plus': ['element-plus'],
|
||
'utils': ['lodash-es', 'dayjs']
|
||
}
|
||
}
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
### 7.2 运行时性能优化
|
||
|
||
#### 7.2.1 虚拟滚动
|
||
|
||
```vue
|
||
<template>
|
||
<virtual-list
|
||
:data="memberList"
|
||
:item-size="80"
|
||
:buffer="10"
|
||
>
|
||
<template #default="{ item }">
|
||
<member-item :member="item" />
|
||
</template>
|
||
</virtual-list>
|
||
</template>
|
||
```
|
||
|
||
#### 7.2.2 防抖节流
|
||
|
||
```typescript
|
||
// utils/debounce.ts
|
||
export function debounce<T extends (...args: any[]) => any>(
|
||
fn: T,
|
||
delay: number
|
||
): (...args: Parameters<T>) => void {
|
||
let timer: ReturnType<typeof setTimeout>
|
||
|
||
return function(this: any, ...args: Parameters<T>) {
|
||
clearTimeout(timer)
|
||
timer = setTimeout(() => {
|
||
fn.apply(this, args)
|
||
}, delay)
|
||
}
|
||
}
|
||
|
||
// 使用
|
||
const handleSearch = debounce((keyword: string) => {
|
||
searchMembers(keyword)
|
||
}, 300)
|
||
```
|
||
|
||
### 7.3 渲染性能优化
|
||
|
||
#### 7.3.1 减少重渲染
|
||
|
||
```vue
|
||
<template>
|
||
<member-item
|
||
v-for="member in memberList"
|
||
:key="member.id"
|
||
:member="member"
|
||
@click="handleMemberClick"
|
||
/>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { shallowRef } from 'vue'
|
||
|
||
// 使用shallowRef减少响应式开销
|
||
const memberList = shallowRef<Member[]>([])
|
||
</script>
|
||
```
|
||
|
||
#### 7.3.2 计算属性缓存
|
||
|
||
```typescript
|
||
const filteredMembers = computed(() => {
|
||
return memberList.value.filter(member =>
|
||
member.name.includes(searchKeyword.value)
|
||
)
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 八、安全设计
|
||
|
||
### 8.1 XSS防护
|
||
|
||
```typescript
|
||
// utils/sanitize.ts
|
||
import DOMPurify from 'dompurify'
|
||
|
||
export function sanitizeHtml(html: string): string {
|
||
return DOMPurify.sanitize(html)
|
||
}
|
||
|
||
// 使用
|
||
const safeHtml = sanitizeHtml(userInput)
|
||
```
|
||
|
||
### 8.2 CSRF防护
|
||
|
||
```typescript
|
||
// api/request.ts
|
||
instance.interceptors.request.use((config) => {
|
||
const csrfToken = getCookie('csrf_token')
|
||
if (csrfToken) {
|
||
config.headers['X-CSRF-Token'] = csrfToken
|
||
}
|
||
return config
|
||
})
|
||
```
|
||
|
||
### 8.3 数据加密
|
||
|
||
```typescript
|
||
// utils/crypto.ts
|
||
import CryptoJS from 'crypto-js'
|
||
|
||
const SECRET_KEY = 'your-secret-key'
|
||
|
||
export function encrypt(text: string): string {
|
||
return CryptoJS.AES.encrypt(text, SECRET_KEY).toString()
|
||
}
|
||
|
||
export function decrypt(ciphertext: string): string {
|
||
const bytes = CryptoJS.AES.decrypt(ciphertext, SECRET_KEY)
|
||
return bytes.toString(CryptoJS.enc.Utf8)
|
||
}
|
||
```
|
||
|
||
### 8.4 CSP策略
|
||
|
||
```html
|
||
<!-- index.html -->
|
||
<meta http-equiv="Content-Security-Policy"
|
||
content="default-src 'self';
|
||
script-src 'self' 'unsafe-inline' 'unsafe-eval';
|
||
style-src 'self' 'unsafe-inline';
|
||
img-src 'self' data: https:;">
|
||
```
|
||
|
||
---
|
||
|
||
## 九、测试策略
|
||
|
||
### 9.1 单元测试
|
||
|
||
```typescript
|
||
// components/__tests__/Button.spec.ts
|
||
import { mount } from '@vue/test-utils'
|
||
import { describe, it, expect } from 'vitest'
|
||
import Button from '@/components/base/Button.vue'
|
||
|
||
describe('Button', () => {
|
||
it('renders text correctly', () => {
|
||
const wrapper = mount(Button, {
|
||
slots: {
|
||
default: 'Click me'
|
||
}
|
||
})
|
||
expect(wrapper.text()).toBe('Click me')
|
||
})
|
||
|
||
it('emits click event', async () => {
|
||
const wrapper = mount(Button)
|
||
await wrapper.trigger('click')
|
||
expect(wrapper.emitted('click')).toBeTruthy()
|
||
})
|
||
})
|
||
```
|
||
|
||
### 9.2 E2E测试
|
||
|
||
```typescript
|
||
// e2e/booking.spec.ts
|
||
import { test, expect } from '@playwright/test'
|
||
|
||
test('user can book a course', async ({ page }) => {
|
||
await page.goto('/booking/list')
|
||
await page.click('[data-testid="course-card"]:first-child')
|
||
await page.click('[data-testid="book-button"]')
|
||
await expect(page.locator('[data-testid="success-message"]')).toBeVisible()
|
||
})
|
||
```
|
||
|
||
---
|
||
|
||
## 十、监控与日志
|
||
|
||
### 10.1 错误监控
|
||
|
||
```typescript
|
||
// utils/sentry.ts
|
||
import * as Sentry from '@sentry/vue'
|
||
|
||
export function setupSentry(app: App) {
|
||
Sentry.init({
|
||
app,
|
||
dsn: import.meta.env.VITE_SENTRY_DSN,
|
||
environment: import.meta.env.MODE,
|
||
tracesSampleRate: 1.0,
|
||
beforeSend(event) {
|
||
// 过滤敏感信息
|
||
if (event.request) {
|
||
delete event.request.cookies
|
||
}
|
||
return event
|
||
}
|
||
})
|
||
}
|
||
```
|
||
|
||
### 10.2 性能监控
|
||
|
||
```typescript
|
||
// utils/analytics.ts
|
||
import { onCLS, onFID, onLCP } from 'web-vitals'
|
||
|
||
export function setupPerformanceMonitoring() {
|
||
onCLS((metric) => {
|
||
console.log('CLS:', metric.value)
|
||
})
|
||
|
||
onFID((metric) => {
|
||
console.log('FID:', metric.value)
|
||
})
|
||
|
||
onLCP((metric) => {
|
||
console.log('LCP:', metric.value)
|
||
})
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 十一、部署策略
|
||
|
||
### 11.1 构建配置
|
||
|
||
```typescript
|
||
// vite.config.ts
|
||
export default defineConfig({
|
||
base: '/',
|
||
build: {
|
||
outDir: 'dist',
|
||
assetsDir: 'assets',
|
||
sourcemap: false,
|
||
minify: 'terser',
|
||
terserOptions: {
|
||
compress: {
|
||
drop_console: true,
|
||
drop_debugger: true
|
||
}
|
||
}
|
||
}
|
||
})
|
||
```
|
||
|
||
### 11.2 环境配置
|
||
|
||
```typescript
|
||
// .env.development
|
||
VITE_API_BASE_URL=http://localhost:8080/api
|
||
VITE_APP_TITLE=健身房管理系统(开发环境)
|
||
|
||
// .env.production
|
||
VITE_API_BASE_URL=https://api.example.com/api
|
||
VITE_APP_TITLE=健身房管理系统
|
||
```
|
||
|
||
---
|
||
|
||
## 十二、总结
|
||
|
||
本文档详细描述了健身房管理系统的前端技术架构,包括:
|
||
|
||
1. **技术栈选型**:Vue3 + uniapp + TypeScript + Pinia + Vite
|
||
2. **架构设计**:分层架构、模块划分、目录结构
|
||
3. **数据流设计**:单向数据流、状态管理、实时数据处理
|
||
4. **组件设计**:组件分类、设计原则、复用策略
|
||
5. **路由设计**:路由结构、路由守卫、动态路由
|
||
6. **API设计**:Axios封装、API模块化
|
||
7. **性能优化**:加载性能、运行时性能、渲染性能
|
||
8. **安全设计**:XSS防护、CSRF防护、数据加密、CSP策略
|
||
9. **测试策略**:单元测试、E2E测试
|
||
10. **监控与日志**:错误监控、性能监控
|
||
11. **部署策略**:构建配置、环境配置
|
||
|
||
该架构设计充分考虑了跨平台需求、性能优化、安全性、可维护性和可扩展性,为健身房管理系统的前端开发提供了完整的技术指导。
|