Compare commits
81 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2a09a5057 | |||
| c94be9bc38 | |||
| bcf3bba3c7 | |||
| 268284cf32 | |||
| 5176083139 | |||
| 9f184d4c2a | |||
| 77ed3b890f | |||
| 5e58d9a0dc | |||
| a19c9470c5 | |||
| 261a90bdcd | |||
| ac710952b2 | |||
| 8be304417d | |||
| b36c6133cb | |||
| b2b3782aa2 | |||
| 244b40025e | |||
| 1912d62f21 | |||
| abb806de5e | |||
| c22595b33a | |||
| ec89d1459a | |||
| c9aabf23f0 | |||
| c8fa427379 | |||
| fc5f094f8c | |||
| 8114809102 | |||
| 26e185a804 | |||
| 68733acc72 | |||
| b454d5d940 | |||
| 11b03e8e62 | |||
| a0026b1da5 | |||
| 1fa2fbd3f3 | |||
| 125492f6f1 | |||
| 8d56fc8e9e | |||
| 7a94145819 | |||
| 97a8511ea2 | |||
| 67c080efce | |||
| b12d7cdf25 | |||
| d3b978938b | |||
| c909b023c7 | |||
| 5bc31f8936 | |||
| dc7da19aee | |||
| a7af34d22b | |||
| 0e8f19934e | |||
| 2a6409daa9 | |||
| c658aacf0a | |||
| e9ef34bb7e | |||
| d6e17b9944 | |||
| 4a714e8141 | |||
| 01e48eefe2 | |||
| f30514c700 | |||
| c19e0e0181 | |||
| 959ee46c44 | |||
| 9192e82eee | |||
| 95c2ded69e | |||
| 7e4035e0ae | |||
| e19324d0ef | |||
| e0e707edcb | |||
| 8ab528a74b | |||
| de7a359ead | |||
| 1e093a0688 | |||
| 33d1140fbf | |||
| 51bdf15613 | |||
| be7eabdbb1 | |||
| 823d626440 | |||
| 207a248b01 | |||
| c4de871977 | |||
| f0dda998a8 | |||
| 8c96e2099c | |||
| 1922d0fb1e | |||
| 7350293d0e | |||
| 8cf3c9ccee | |||
| 1fc020ab00 | |||
| 4981a240fa | |||
| 349b7ae03b | |||
| 2357dcfc67 | |||
| 14a0fe8d4f | |||
| e304c1b724 | |||
| f0d97e58d1 | |||
| 2b58b672d5 | |||
| 8bedac5ab5 | |||
| 8e7c8f52f6 | |||
| b4710b6397 | |||
| daff741c65 |
@@ -0,0 +1,452 @@
|
||||
# 数据统计模块 API 文档
|
||||
|
||||
> **文档版本**: v1.0
|
||||
> **创建日期**: 2026-06-09
|
||||
> **作者**: system
|
||||
> **状态**: 正式发布
|
||||
|
||||
---
|
||||
|
||||
## 📋 目录
|
||||
|
||||
1. [概述](#概述)
|
||||
2. [基础路径](#基础路径)
|
||||
3. [统计数据接口](#统计数据接口)
|
||||
- [获取综合统计数据](#获取综合统计数据)
|
||||
- [获取会员统计数据](#获取会员统计数据)
|
||||
- [获取预约统计数据](#获取预约统计数据)
|
||||
- [获取签到统计数据](#获取签到统计数据)
|
||||
- [查询历史统计数据](#查询历史统计数据)
|
||||
- [导出统计数据](#导出统计数据)
|
||||
4. [数据模型](#数据模型)
|
||||
- [StatisticsQuery(查询条件)](#StatisticsQuery查询条件)
|
||||
- [StatisticsSummary(统计汇总)](#StatisticsSummary统计汇总)
|
||||
- [MemberStatistics(会员统计)](#MemberStatistics会员统计)
|
||||
- [BookingStatistics(预约统计)](#BookingStatistics预约统计)
|
||||
- [SignInStatistics(签到统计)](#SignInStatistics签到统计)
|
||||
5. [状态码说明](#状态码说明)
|
||||
6. [业务规则](#业务规则)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
数据统计模块提供健身房会员、预约、签到数据的统计功能,支持按日、周、月周期统计,并提供Excel导出功能。采用 Spring WebFlux 响应式编程,统计结果通过 Redis 缓存提高查询性能。
|
||||
|
||||
## 基础路径
|
||||
|
||||
所有接口的基础路径为: `http://{host}:{port}/api/datacount`
|
||||
|
||||
---
|
||||
|
||||
## 统计数据接口
|
||||
|
||||
### 获取综合统计数据
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/datacount/summary` |
|
||||
| **所属文件** | `DataStatisticsHandler.java` |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| statType | string | 否 | 空 | 统计类型筛选 |
|
||||
| periodType | string | 否 | DAY | 统计周期:DAY/WEEK/MONTH |
|
||||
| startTime | string | 否 | 自动计算 | 开始时间(ISO格式) |
|
||||
| endTime | string | 否 | 自动计算 | 结束时间(ISO格式) |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
# 1. 使用周期类型查询今日统计
|
||||
GET /api/datacount/summary?periodType=DAY
|
||||
|
||||
# 2. 使用周期类型查询本周统计
|
||||
GET /api/datacount/summary?periodType=WEEK
|
||||
|
||||
# 3. 使用周期类型查询本月统计
|
||||
GET /api/datacount/summary?periodType=MONTH
|
||||
|
||||
# 4. 使用自定义时间范围查询
|
||||
GET /api/datacount/summary?startTime=2026-06-01T00:00:00&endTime=2026-06-07T23:59:59
|
||||
|
||||
# 5. 带统计类型筛选
|
||||
GET /api/datacount/summary?statType=member&periodType=WEEK
|
||||
```
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"statDate": "2026-06-09",
|
||||
"generatedAt": "2026-06-09T10:30:00",
|
||||
"memberStatistics": {
|
||||
"statDate": "2026-06-09",
|
||||
"newMembers": 5,
|
||||
"activeMembers": 120,
|
||||
"totalMembers": 500,
|
||||
"signInMembers": 85,
|
||||
"bookingMembers": 45,
|
||||
"cancelMembers": 5
|
||||
},
|
||||
"bookingStatistics": {
|
||||
"statDate": "2026-06-09",
|
||||
"totalBookings": 60,
|
||||
"cancelBookings": 10,
|
||||
"attendBookings": 45,
|
||||
"absentBookings": 5,
|
||||
"attendRate": 75.0,
|
||||
"cancelRate": 16.67
|
||||
},
|
||||
"signInStatistics": {
|
||||
"statDate": "2026-06-09",
|
||||
"totalSignIns": 95,
|
||||
"successSignIns": 92,
|
||||
"failSignIns": 3,
|
||||
"successRate": 96.84,
|
||||
"signInTypeDistribution": {
|
||||
"QR_CODE": 60,
|
||||
"MANUAL": 25,
|
||||
"FACE": 10
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取会员统计数据
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/datacount/member` |
|
||||
| **所属文件** | `DataStatisticsHandler.java` |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| periodType | string | 否 | DAY | 统计周期:DAY/WEEK/MONTH |
|
||||
| startTime | string | 否 | 自动计算 | 开始时间 |
|
||||
| endTime | string | 否 | 自动计算 | 结束时间 |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
# 1. 查询今日会员统计
|
||||
GET /api/datacount/member
|
||||
|
||||
# 2. 查询本周会员统计
|
||||
GET /api/datacount/member?periodType=WEEK
|
||||
|
||||
# 3. 查询指定时间范围的会员统计
|
||||
GET /api/datacount/member?startTime=2026-06-01T00:00:00&endTime=2026-06-30T23:59:59
|
||||
```
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"statDate": "2026-06-09",
|
||||
"newMembers": 5,
|
||||
"activeMembers": 120,
|
||||
"totalMembers": 500,
|
||||
"signInMembers": 85,
|
||||
"bookingMembers": 45,
|
||||
"cancelMembers": 5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取预约统计数据
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/datacount/booking` |
|
||||
| **所属文件** | `DataStatisticsHandler.java` |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| periodType | string | 否 | DAY | 统计周期:DAY/WEEK/MONTH |
|
||||
| startTime | string | 否 | 自动计算 | 开始时间 |
|
||||
| endTime | string | 否 | 自动计算 | 结束时间 |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
# 1. 查询今日预约统计
|
||||
GET /api/datacount/booking
|
||||
|
||||
# 2. 查询本月预约统计
|
||||
GET /api/datacount/booking?periodType=MONTH
|
||||
|
||||
# 3. 查询指定日期范围的预约统计
|
||||
GET /api/datacount/booking?startTime=2026-06-01T00:00:00&endTime=2026-06-15T23:59:59
|
||||
```
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"statDate": "2026-06-09",
|
||||
"totalBookings": 60,
|
||||
"cancelBookings": 10,
|
||||
"attendBookings": 45,
|
||||
"absentBookings": 5,
|
||||
"attendRate": 75.0,
|
||||
"cancelRate": 16.67
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取签到统计数据
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/datacount/signin` |
|
||||
| **所属文件** | `DataStatisticsHandler.java` |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| periodType | string | 否 | DAY | 统计周期:DAY/WEEK/MONTH |
|
||||
| startTime | string | 否 | 自动计算 | 开始时间 |
|
||||
| endTime | string | 否 | 自动计算 | 结束时间 |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
# 1. 查询今日签到统计
|
||||
GET /api/datacount/signin
|
||||
|
||||
# 2. 查询本周签到统计
|
||||
GET /api/datacount/signin?periodType=WEEK
|
||||
|
||||
# 3. 查询指定日期范围的签到统计
|
||||
GET /api/datacount/signin?startTime=2026-06-01T00:00:00&endTime=2026-06-30T23:59:59
|
||||
```
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"statDate": "2026-06-09",
|
||||
"totalSignIns": 95,
|
||||
"successSignIns": 92,
|
||||
"failSignIns": 3,
|
||||
"successRate": 96.84,
|
||||
"signInTypeDistribution": {
|
||||
"QR_CODE": 60,
|
||||
"MANUAL": 25,
|
||||
"FACE": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 查询历史统计数据
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/datacount/history` |
|
||||
| **所属文件** | `DataStatisticsHandler.java` |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| statType | string | 否 | 空 | 统计类型:member/booking/signin |
|
||||
| startTime | string | 是 | - | 开始日期(格式:yyyy-MM-dd) |
|
||||
| endTime | string | 是 | - | 结束日期(格式:yyyy-MM-dd) |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
# 1. 查询指定日期范围的签到历史统计
|
||||
GET /api/datacount/history?statType=signin&startTime=2026-06-01&endTime=2026-06-30
|
||||
|
||||
# 2. 查询指定日期范围的会员历史统计
|
||||
GET /api/datacount/history?statType=member&startTime=2026-06-01&endTime=2026-06-15
|
||||
|
||||
# 3. 查询指定日期范围的预约历史统计
|
||||
GET /api/datacount/history?statType=booking&startTime=2026-06-01&endTime=2026-06-30
|
||||
|
||||
# 4. 查询所有类型的历史统计(不指定statType)
|
||||
GET /api/datacount/history?startTime=2026-06-01&endTime=2026-06-07
|
||||
```
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"statDate": "2026-06-07",
|
||||
"totalSignIns": 88,
|
||||
"successSignIns": 86,
|
||||
"failSignIns": 2,
|
||||
"successRate": 97.73,
|
||||
"signInTypeDistribution": {
|
||||
"QR_CODE": 55,
|
||||
"MANUAL": 23,
|
||||
"FACE": 10
|
||||
}
|
||||
},
|
||||
{
|
||||
"statDate": "2026-06-08",
|
||||
"totalSignIns": 92,
|
||||
"successSignIns": 90,
|
||||
"failSignIns": 2,
|
||||
"successRate": 97.83,
|
||||
"signInTypeDistribution": {
|
||||
"QR_CODE": 58,
|
||||
"MANUAL": 24,
|
||||
"FACE": 10
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 导出统计数据
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/datacount/export` |
|
||||
| **所属文件** | `DataStatisticsHandler.java` |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| periodType | string | 否 | DAY | 统计周期:DAY/WEEK/MONTH |
|
||||
| startTime | string | 否 | 自动计算 | 开始时间 |
|
||||
| endTime | string | 否 | 自动计算 | 结束时间 |
|
||||
|
||||
**请求示例**:
|
||||
|
||||
```bash
|
||||
# 1. 导出今日统计数据
|
||||
GET /api/datacount/export
|
||||
|
||||
# 2. 导出本周统计数据
|
||||
GET /api/datacount/export?periodType=WEEK
|
||||
|
||||
# 3. 导出本月统计数据
|
||||
GET /api/datacount/export?periodType=MONTH
|
||||
|
||||
# 4. 导出指定时间范围的统计数据
|
||||
GET /api/datacount/export?startTime=2026-06-01T00:00:00&endTime=2026-06-30T23:59:59
|
||||
```
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
返回 Excel 文件(.xlsx),包含以下 Sheet:
|
||||
- **会员统计**: 会员相关统计数据
|
||||
- **预约统计**: 预约相关统计数据
|
||||
- **签到统计**: 签到相关统计数据
|
||||
|
||||
**Content-Type**: `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`
|
||||
|
||||
---
|
||||
|
||||
## 数据模型
|
||||
|
||||
### StatisticsQuery(查询条件)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| statType | String | 统计类型筛选 |
|
||||
| periodType | String | 统计周期:DAY/WEEK/MONTH |
|
||||
| startTime | LocalDateTime | 开始时间 |
|
||||
| endTime | LocalDateTime | 结束时间 |
|
||||
|
||||
### StatisticsSummary(统计汇总)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| statDate | String | 统计日期 |
|
||||
| generatedAt | String | 生成时间 |
|
||||
| memberStatistics | MemberStatistics | 会员统计数据 |
|
||||
| bookingStatistics | BookingStatistics | 预约统计数据 |
|
||||
| signInStatistics | SignInStatistics | 签到统计数据 |
|
||||
|
||||
### MemberStatistics(会员统计)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| statDate | String | 统计日期 |
|
||||
| newMembers | Long | 新增会员数 |
|
||||
| activeMembers | Long | 活跃会员数 |
|
||||
| totalMembers | Long | 累计会员总数 |
|
||||
| signInMembers | Long | 签到会员数 |
|
||||
| bookingMembers | Long | 预约会员数 |
|
||||
| cancelMembers | Long | 取消预约会员数 |
|
||||
|
||||
### BookingStatistics(预约统计)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| statDate | String | 统计日期 |
|
||||
| totalBookings | Long | 预约总数 |
|
||||
| cancelBookings | Long | 取消预约数 |
|
||||
| attendBookings | Long | 出席预约数 |
|
||||
| absentBookings | Long | 缺席预约数 |
|
||||
| attendRate | Double | 出席率(%) |
|
||||
| cancelRate | Double | 取消率(%) |
|
||||
|
||||
### SignInStatistics(签到统计)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| statDate | String | 统计日期 |
|
||||
| totalSignIns | Long | 签到总次数 |
|
||||
| successSignIns | Long | 成功签到次数 |
|
||||
| failSignIns | Long | 失败签到次数 |
|
||||
| successRate | Double | 签到成功率(%) |
|
||||
| signInTypeDistribution | Map<String, Long> | 签到类型分布 |
|
||||
|
||||
---
|
||||
|
||||
## 状态码说明
|
||||
|
||||
| 状态码 | 说明 |
|
||||
|--------|------|
|
||||
| 200 | 请求成功 |
|
||||
| 400 | 请求参数错误 |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 业务规则
|
||||
|
||||
1. **统计周期**: 支持按日、周、月统计
|
||||
- DAY: 当天 00:00:00 - 当前时间
|
||||
- WEEK: 本周一 00:00:00 - 本周日 23:59:59
|
||||
- MONTH: 本月1日 00:00:00 - 本月最后一天 23:59:59
|
||||
|
||||
2. **数据保留**: 统计数据缓存30天,业务记录永久保存
|
||||
|
||||
3. **缓存策略**: 查询结果缓存1小时,统计数据缓存30天
|
||||
|
||||
4. **定时任务**:
|
||||
- 每日凌晨2点执行前一天的日统计
|
||||
- 每周一凌晨3点执行上周的周统计
|
||||
- 每月1日凌晨3点执行上月的月统计
|
||||
- 每月15日凌晨4点清理30天前的旧数据
|
||||
|
||||
5. **数据导出**: 支持Excel格式导出,包含会员、预约、签到三个Sheet
|
||||
@@ -26,11 +26,32 @@
|
||||
- [查询会员预约记录](#查询会员预约记录)
|
||||
- [查询预约详情](#查询预约详情)
|
||||
- [查询课程预约记录](#查询课程预约记录)
|
||||
5. [数据模型](#数据模型)
|
||||
5. [团课类型管理接口](#团课类型管理接口)
|
||||
- [获取所有团课类型](#获取所有团课类型)
|
||||
- [根据ID获取团课类型](#根据ID获取团课类型)
|
||||
- [搜索团课类型](#搜索团课类型)
|
||||
- [根据分类获取团课类型](#根据分类获取团课类型)
|
||||
- [获取所有分类](#获取所有分类)
|
||||
- [创建团课类型](#创建团课类型)
|
||||
- [更新团课类型](#更新团课类型)
|
||||
- [删除团课类型](#删除团课类型)
|
||||
6. [团课标签管理接口](#团课标签管理接口)
|
||||
- [获取所有标签](#获取所有标签)
|
||||
- [根据ID获取标签](#根据ID获取标签)
|
||||
- [搜索标签](#搜索标签)
|
||||
- [获取类型的标签](#获取类型的标签)
|
||||
- [创建标签](#创建标签)
|
||||
- [更新标签](#更新标签)
|
||||
- [删除标签](#删除标签)
|
||||
- [为类型添加标签](#为类型添加标签)
|
||||
- [从类型移除标签](#从类型移除标签)
|
||||
- [清空类型标签](#清空类型标签)
|
||||
7. [数据模型](#数据模型)
|
||||
- [GroupCourse(团课)](#GroupCourse团课)
|
||||
- [GroupCourseBooking(团课预约)](#GroupCourseBooking团课预约)
|
||||
6. [状态码说明](#状态码说明)
|
||||
7. [业务规则](#业务规则)
|
||||
- [GroupCourseType(团课类型)](#GroupCourseType团课类型)
|
||||
7. [状态码说明](#状态码说明)
|
||||
8. [业务规则](#业务规则)
|
||||
|
||||
---
|
||||
|
||||
@@ -178,6 +199,78 @@
|
||||
|
||||
---
|
||||
|
||||
### 根据ID获取团课完整信息
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/{id}/detail` |
|
||||
| **所属文件** | `GroupCourseHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Long | 是 | 团课ID |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"courseName": "瑜伽入门",
|
||||
"coachId": 1,
|
||||
"courseType": 1,
|
||||
"startTime": "2026-06-02T09:00:00",
|
||||
"endTime": "2026-06-02T10:00:00",
|
||||
"maxMembers": 20,
|
||||
"currentMembers": 15,
|
||||
"status": 0,
|
||||
"location": "健身房A区",
|
||||
"coverImage": "https://example.com/yoga.jpg",
|
||||
"description": "适合初学者的瑜伽课程",
|
||||
"pointCardAmount": 1,
|
||||
"storedValueAmount": 50.00,
|
||||
"typeName": "瑜伽入门",
|
||||
"typeCategory": "柔韧与平衡类",
|
||||
"baseDifficulty": 2,
|
||||
"difficultyLevel": "初级",
|
||||
"calculatedDifficulty": 2,
|
||||
"typeInfo": {
|
||||
"id": 1,
|
||||
"typeName": "瑜伽入门",
|
||||
"baseDifficulty": 2,
|
||||
"calculatedDifficulty": 2,
|
||||
"difficultyLevel": "初级",
|
||||
"description": "适合初学者的瑜伽课程,注重基础体式",
|
||||
"category": "柔韧与平衡类",
|
||||
"labels": [
|
||||
{"id": 1, "labelName": "适合新手", "color": "#52c41a"},
|
||||
{"id": 3, "labelName": "减压放松", "color": "#1890ff"}
|
||||
]
|
||||
},
|
||||
"labels": [
|
||||
{"id": 1, "labelName": "适合新手", "color": "#52c41a"},
|
||||
{"id": 3, "labelName": "减压放松", "color": "#1890ff"}
|
||||
],
|
||||
"createdAt": "2026-06-01T10:00:00",
|
||||
"updatedAt": "2026-06-01T10:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应** (404 Not Found):
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
**说明**: 此接口返回团课的完整信息,包括:
|
||||
- 团课基础信息
|
||||
- 团课对应的类型信息(包含基础难度、综合难度、难度等级等)
|
||||
- 该类型的所有标签信息
|
||||
|
||||
---
|
||||
|
||||
### 创建团课
|
||||
|
||||
| 属性 | 值 |
|
||||
@@ -628,6 +721,625 @@
|
||||
|
||||
---
|
||||
|
||||
## 团课类型管理接口
|
||||
|
||||
### 获取所有团课类型
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/types` |
|
||||
| **所属文件** | `GroupCourseTypeHandler.java` |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| includeDeleted | boolean | 否 | false | 是否包含已删除的类型 |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"typeName": "瑜伽入门",
|
||||
"baseDifficulty": 2,
|
||||
"calculatedDifficulty": 2,
|
||||
"difficultyLevel": "初级",
|
||||
"description": "适合初学者的瑜伽课程,注重基础体式",
|
||||
"category": "柔韧与平衡类",
|
||||
"createdAt": "2026-06-01T10:00:00",
|
||||
"updatedAt": "2026-06-01T10:00:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 根据ID获取团课类型
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/types/{id}` |
|
||||
| **所属文件** | `GroupCourseTypeHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Long | 是 | 团课类型ID |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"typeName": "瑜伽入门",
|
||||
"baseDifficulty": 2,
|
||||
"calculatedDifficulty": 2,
|
||||
"difficultyLevel": "初级",
|
||||
"description": "适合初学者的瑜伽课程,注重基础体式",
|
||||
"category": "柔韧与平衡类",
|
||||
"createdAt": "2026-06-01T10:00:00",
|
||||
"updatedAt": "2026-06-01T10:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应** (404 Not Found):
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 搜索团课类型
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/types/search` |
|
||||
| **所属文件** | `GroupCourseTypeHandler.java` |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| keyword | string | 否 | - | 搜索关键词(匹配类型名称) |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"typeName": "瑜伽入门",
|
||||
"baseDifficulty": 2,
|
||||
"difficultyLevel": "初级",
|
||||
"category": "柔韧与平衡类"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 根据分类获取团课类型
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/types/category/{category}` |
|
||||
| **所属文件** | `GroupCourseTypeHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| category | String | 是 | 分类名称 |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| keyword | string | 否 | - | 搜索关键词 |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"typeName": "瑜伽入门",
|
||||
"baseDifficulty": 2,
|
||||
"difficultyLevel": "初级",
|
||||
"category": "柔韧与平衡类"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取所有分类
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/types/categories` |
|
||||
| **所属文件** | `GroupCourseTypeHandler.java` |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
["基础有氧与热身", "固定器械训练", "自重基础动作", "自由重量杠铃/哑铃", "高强度与爆发力", "柔韧与平衡类"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 创建团课类型
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | POST |
|
||||
| **接口路径** | `/api/groupCourse/types` |
|
||||
| **所属文件** | `GroupCourseTypeHandler.java` |
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"typeName": "核心力量训练",
|
||||
"baseDifficulty": 4,
|
||||
"description": "针对核心肌群的专项训练课程",
|
||||
"category": "自重基础动作"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| typeName | String | **是** | - | 类型名称 |
|
||||
| baseDifficulty | Integer | 否 | 1 | 基础难度(1-10) |
|
||||
| description | String | 否 | - | 类型描述 |
|
||||
| category | String | 否 | - | 分类 |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "团课类型创建成功",
|
||||
"data": {
|
||||
"id": 40,
|
||||
"typeName": "核心力量训练",
|
||||
"baseDifficulty": 4,
|
||||
"calculatedDifficulty": 4,
|
||||
"difficultyLevel": "中级",
|
||||
"description": "针对核心肌群的专项训练课程",
|
||||
"category": "自重基础动作"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应** (400 Bad Request):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "类型名称不能为空"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 更新团课类型
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | PUT |
|
||||
| **接口路径** | `/api/groupCourse/types/{id}` |
|
||||
| **所属文件** | `GroupCourseTypeHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Long | 是 | 团课类型ID |
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"typeName": "核心力量训练进阶",
|
||||
"baseDifficulty": 6,
|
||||
"description": "进阶核心训练课程"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| typeName | String | 否 | 类型名称 |
|
||||
| baseDifficulty | Integer | 否 | 基础难度(1-10) |
|
||||
| description | String | 否 | 类型描述 |
|
||||
| category | String | 否 | 分类 |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "团课类型更新成功",
|
||||
"data": {
|
||||
"id": 40,
|
||||
"typeName": "核心力量训练进阶",
|
||||
"baseDifficulty": 6,
|
||||
"calculatedDifficulty": 6,
|
||||
"difficultyLevel": "中高级",
|
||||
"description": "进阶核心训练课程",
|
||||
"category": "自重基础动作"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 删除团课类型
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | DELETE |
|
||||
| **接口路径** | `/api/groupCourse/types/{id}` |
|
||||
| **所属文件** | `GroupCourseTypeHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Long | 是 | 团课类型ID |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "团课类型删除成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 团课标签管理接口
|
||||
|
||||
### 获取所有标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/labels` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"labelName": "适合新手",
|
||||
"color": "#52c41a",
|
||||
"createdAt": "2026-06-01T10:00:00",
|
||||
"updatedAt": "2026-06-01T10:00:00"
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"labelName": "中级过渡",
|
||||
"color": "#faad14",
|
||||
"createdAt": "2026-06-01T10:00:00",
|
||||
"updatedAt": "2026-06-01T10:00:00"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 根据ID获取标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/labels/{id}` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Long | 是 | 标签ID |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"labelName": "适合新手",
|
||||
"color": "#52c41a",
|
||||
"createdAt": "2026-06-01T10:00:00",
|
||||
"updatedAt": "2026-06-01T10:00:00"
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应** (404 Not Found):
|
||||
|
||||
```json
|
||||
{}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 搜索标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/labels/search` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**请求参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| keyword | string | 否 | - | 搜索关键词(匹配标签名称) |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"labelName": "适合新手",
|
||||
"color": "#52c41a"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 获取类型的标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | GET |
|
||||
| **接口路径** | `/api/groupCourse/types/{typeId}/labels` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| typeId | Long | 是 | 团课类型ID |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"labelName": "适合新手",
|
||||
"color": "#52c41a"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"labelName": "减压放松",
|
||||
"color": "#1890ff"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 创建标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | POST |
|
||||
| **接口路径** | `/api/groupCourse/labels` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"labelName": "燃脂塑形",
|
||||
"color": "#f5222d"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|--------|------|------|--------|------|
|
||||
| labelName | String | **是** | - | 标签名称(最大50字符) |
|
||||
| color | String | 否 | #1890ff | 标签颜色(十六进制颜色值) |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "标签创建成功",
|
||||
"data": {
|
||||
"id": 15,
|
||||
"labelName": "燃脂塑形",
|
||||
"color": "#f5222d"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**失败响应** (400 Bad Request):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "标签名称已存在"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 更新标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | PUT |
|
||||
| **接口路径** | `/api/groupCourse/labels/{id}` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Long | 是 | 标签ID |
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"labelName": "燃脂塑形进阶",
|
||||
"color": "#fa541c"
|
||||
}
|
||||
```
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| labelName | String | 否 | 标签名称(最大50字符) |
|
||||
| color | String | 否 | 标签颜色(十六进制颜色值) |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "标签更新成功",
|
||||
"data": {
|
||||
"id": 15,
|
||||
"labelName": "燃脂塑形进阶",
|
||||
"color": "#fa541c"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 删除标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | DELETE |
|
||||
| **接口路径** | `/api/groupCourse/labels/{id}` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| id | Long | 是 | 标签ID |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "标签删除成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 为类型添加标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | POST |
|
||||
| **接口路径** | `/api/groupCourse/types/{typeId}/labels` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| typeId | Long | 是 | 团课类型ID |
|
||||
|
||||
**请求体**:
|
||||
|
||||
```json
|
||||
{
|
||||
"labelIds": [1, 3, 5]
|
||||
}
|
||||
```
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| labelIds | List\<Long\> | **是** | 要添加的标签ID列表 |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "标签添加成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 从类型移除标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | DELETE |
|
||||
| **接口路径** | `/api/groupCourse/types/{typeId}/labels/{labelId}` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| typeId | Long | 是 | 团课类型ID |
|
||||
| labelId | Long | 是 | 标签ID |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "标签移除成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 清空类型标签
|
||||
|
||||
| 属性 | 值 |
|
||||
|------|-----|
|
||||
| **HTTP方法** | DELETE |
|
||||
| **接口路径** | `/api/groupCourse/types/{typeId}/labels` |
|
||||
| **所属文件** | `CourseLabelHandler.java` |
|
||||
|
||||
**路径参数**:
|
||||
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| typeId | Long | 是 | 团课类型ID |
|
||||
|
||||
**成功响应** (200 OK):
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "标签清空成功"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据模型
|
||||
|
||||
### GroupCourse(团课)
|
||||
@@ -675,6 +1387,84 @@
|
||||
| updatedAt | LocalDateTime | 更新时间 |
|
||||
| deletedAt | LocalDateTime | 删除时间(软删除) |
|
||||
|
||||
### GroupCourseType(团课类型)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | Long | 主键ID |
|
||||
| typeName | String | 类型名称 |
|
||||
| baseDifficulty | Integer | 基础难度(1-10) |
|
||||
| calculatedDifficulty | Integer | 综合难度系数(预留扩展字段) |
|
||||
| difficultyLevel | String | 难度等级描述(初级/中级/中高级/高级/专家级) |
|
||||
| description | String | 类型描述 |
|
||||
| category | String | 分类(如:有氧、力量、柔韧等) |
|
||||
| labels | List\<CourseLabel\> | 标签列表 |
|
||||
| createdBy | String | 创建人 |
|
||||
| updatedBy | String | 更新人 |
|
||||
| createdAt | LocalDateTime | 创建时间 |
|
||||
| updatedAt | LocalDateTime | 更新时间 |
|
||||
| deletedAt | LocalDateTime | 删除时间(软删除) |
|
||||
|
||||
### CourseLabel(团课标签)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | Long | 主键ID |
|
||||
| labelName | String | 标签名称(最大50字符) |
|
||||
| color | String | 标签颜色(十六进制颜色值,默认#1890ff) |
|
||||
| createdBy | String | 创建人 |
|
||||
| updatedBy | String | 更新人 |
|
||||
| createdAt | LocalDateTime | 创建时间 |
|
||||
| updatedAt | LocalDateTime | 更新时间 |
|
||||
| deletedAt | LocalDateTime | 删除时间(软删除) |
|
||||
|
||||
### GroupCourseDetail(团课完整信息)
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | Long | 主键ID |
|
||||
| courseName | String | 课程名称 |
|
||||
| coachId | Long | 教练ID |
|
||||
| courseType | Long | 课程类型ID |
|
||||
| startTime | LocalDateTime | 开始时间 |
|
||||
| endTime | LocalDateTime | 结束时间 |
|
||||
| maxMembers | Integer | 最大参与人数 |
|
||||
| currentMembers | Integer | 当前参与人数 |
|
||||
| status | Long | 状态(0-正常,1-已取消,2-已结束) |
|
||||
| location | String | 上课地点 |
|
||||
| coverImage | String | 封面图URL |
|
||||
| description | String | 课程描述 |
|
||||
| pointCardAmount | Integer | 点卡额度(消耗次数) |
|
||||
| storedValueAmount | BigDecimal | 储值卡额度(消耗金额) |
|
||||
| typeName | String | 类型名称(快捷访问) |
|
||||
| typeCategory | String | 类型分类(快捷访问) |
|
||||
| baseDifficulty | Integer | 基础难度(快捷访问) |
|
||||
| difficultyLevel | String | 难度等级描述(快捷访问) |
|
||||
| calculatedDifficulty | Integer | 综合难度系数(快捷访问) |
|
||||
| typeInfo | GroupCourseType | 类型信息 |
|
||||
| labels | List\<CourseLabel\> | 标签列表 |
|
||||
| createdAt | LocalDateTime | 创建时间 |
|
||||
| updatedAt | LocalDateTime | 更新时间 |
|
||||
|
||||
**难度等级对应关系**:
|
||||
|
||||
| 基础难度 | 难度等级 |
|
||||
|----------|----------|
|
||||
| 1-2 | 初级 |
|
||||
| 3-4 | 中级 |
|
||||
| 5-6 | 中高级 |
|
||||
| 7-8 | 高级 |
|
||||
| 9-10 | 专家级 |
|
||||
|
||||
**难度扩展说明**:
|
||||
|
||||
`calculatedDifficulty` 字段为预留扩展字段,当前实现仅返回 `baseDifficulty`。未来可扩展的影响因素包括:
|
||||
|
||||
1. **课程时长系数**:时长越长难度越高
|
||||
2. **教练难度调整系数**:教练可根据实际情况微调
|
||||
3. **会员等级适配系数**:根据会员等级动态调整显示难度
|
||||
4. **课程强度系数**:高强度课程难度加成
|
||||
|
||||
---
|
||||
|
||||
## 状态码说明
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
一、基础有氧与热身(难度 1-3)
|
||||
|
||||
主要是低冲击、低技巧,用于建立运动基础。
|
||||
|
||||
慢走/椭圆机轻松模式:1(几乎无难度,适合所有人)
|
||||
|
||||
固定自行车(低阻力):2(注意座椅高度调节即可)
|
||||
|
||||
跑步机慢跑:3(需要基本协调性,膝盖有压力)
|
||||
|
||||
跳绳(连续基础跳):3(需要手脚配合,心肺要求明显)
|
||||
|
||||
二、固定器械训练(难度 2-5)
|
||||
|
||||
轨迹固定,主要考验力量和耐力,技巧要求低。
|
||||
|
||||
坐姿腿屈伸/腿弯举:2(很容易找到发力感)
|
||||
|
||||
坐姿推胸机:3(需注意肩胛后收,避免耸肩)
|
||||
|
||||
高位下拉(坐姿):3(需控制不要过度后仰)
|
||||
|
||||
史密斯机深蹲:4(轨迹固定,但需保持核心稳定)
|
||||
|
||||
蝴蝶机夹胸:3(易用肘关节代偿,需锁定肩关节)
|
||||
|
||||
三、自重基础动作(难度 3-7)
|
||||
|
||||
需要一定的力量-体重比和身体控制能力。
|
||||
|
||||
平板支撑:3(耐力考验,技巧低)
|
||||
|
||||
跪姿俯卧撑:3(上肢力量较弱者首选)
|
||||
|
||||
标准俯卧撑:5(需核心收紧,身体成直线)
|
||||
|
||||
引体向上(弹力带辅助):6(背部和手臂力量要求高)
|
||||
|
||||
标准引体向上:8(力量-体重比极高,多数男性无法完成1次)
|
||||
|
||||
徒手深蹲:3(注意膝盖方向与背部直立)
|
||||
|
||||
单腿深蹲(手枪蹲):8(需要极高下肢力量、柔韧性和平衡)
|
||||
|
||||
四、自由重量杠铃/哑铃(难度 5-9)
|
||||
|
||||
技巧风险最高,需要神经系统协调和长期动作打磨。
|
||||
|
||||
哑铃二头弯举:4(容易晃动借力,但较安全)
|
||||
|
||||
哑铃侧平举:5(极易用斜方肌代偿,真正练到三角肌中束很难)
|
||||
|
||||
杠铃卧推:7(肩关节压力大,起桥、沉肩、稳定手腕均有技巧,有压伤风险)
|
||||
|
||||
杠铃深蹲(颈后):8(全身协调性、核心抗压、杠位放置、呼吸模式,学习曲线陡峭)
|
||||
|
||||
传统硬拉:9(风险极高,需要精确的脊柱中立、髋铰链、背阔肌收紧,错误时伤腰)
|
||||
|
||||
高翻/抓举(奥运举重):10(需要爆发力、柔韧、精准衔接,非数月训练不能掌握)
|
||||
|
||||
五、高强度与爆发力(难度 6-10)
|
||||
|
||||
对心肺、神经系统和恢复能力要求极高。
|
||||
|
||||
波比跳(标准版):6(连续做时心肺压力极大)
|
||||
|
||||
冲刺跑(短跑):7(对腘绳肌和脚踝爆发力要求高)
|
||||
|
||||
跳箱(合理高度):6(需要落地缓冲技巧)
|
||||
|
||||
负重雪橇推:6(主要考验腿部耐力和意志力)
|
||||
|
||||
双力臂(引体向上后翻腕上杠):9(需要爆发引体 + 极高相对力量)
|
||||
|
||||
六、柔韧与平衡类(难度 3-8)
|
||||
|
||||
考验本体感觉和关节活动度。
|
||||
|
||||
静态拉伸(坐姿体前屈):2(无风险,但需要坚持)
|
||||
|
||||
瑜伽下犬式:3(常见,但需背部与手臂对齐)
|
||||
|
||||
单腿罗马尼亚硬拉(徒手):6(极考验平衡和髋稳定)
|
||||
|
||||
全深蹲(脚跟贴地,亚洲蹲):5(踝关节灵活度限制多数人)
|
||||
|
||||
竖叉/横叉:8(需要数月甚至数年拉伸)
|
||||
@@ -0,0 +1,33 @@
|
||||
HELP.md
|
||||
target/
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
@@ -0,0 +1,3 @@
|
||||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
|
||||
@@ -0,0 +1,108 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-manage-api</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-dataCount</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>gym-dataCount</name>
|
||||
<description>Data Statistics Module for Gym Management</description>
|
||||
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<!-- Common模块 -->
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 数据库模块 -->
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-db</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- WebFlux -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- R2dbc -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Validation -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Swagger -->
|
||||
<dependency>
|
||||
<groupId>org.springdoc</groupId>
|
||||
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- POI for Excel export -->
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>5.2.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Gym Modules -->
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-member</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-groupCourse</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-checkIn</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package cn.novalon.gym.manage.datacount.config;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
|
||||
/**
|
||||
* 数据统计模块自动配置
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@AutoConfiguration
|
||||
@ComponentScan(basePackages = "cn.novalon.gym.manage.datacount")
|
||||
public class DataCountAutoConfiguration {
|
||||
}
|
||||
+183
@@ -0,0 +1,183 @@
|
||||
package cn.novalon.gym.manage.datacount.dao;
|
||||
|
||||
import org.springframework.r2dbc.core.DatabaseClient;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 数据统计 DAO - 使用 DatabaseClient 执行跨表聚合查询
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Repository
|
||||
public class DataStatisticsDao {
|
||||
|
||||
private final DatabaseClient databaseClient;
|
||||
|
||||
public DataStatisticsDao(DatabaseClient databaseClient) {
|
||||
this.databaseClient = databaseClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内新增会员数
|
||||
*/
|
||||
public Mono<Long> countNewMembers(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(*) FROM member_user WHERE created_at >= :startTime AND created_at < :endTime AND is_deleted = false")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计总会员数
|
||||
*/
|
||||
public Mono<Long> countTotalMembers() {
|
||||
return databaseClient.sql("SELECT COUNT(*) FROM member_user WHERE is_deleted = false")
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内的签到次数
|
||||
*/
|
||||
public Mono<Long> countSignIns(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(*) FROM sign_in_record WHERE sign_in_time >= :startTime AND sign_in_time < :endTime AND is_delete = false")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内的成功签到次数
|
||||
*/
|
||||
public Mono<Long> countSuccessSignIns(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(*) FROM sign_in_record WHERE sign_in_time >= :startTime AND sign_in_time < :endTime AND sign_in_status = 'SUCCESS' AND is_delete = false")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内签到的独立会员数
|
||||
*/
|
||||
public Mono<Long> countDistinctSignInMembers(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(DISTINCT member_id) FROM sign_in_record WHERE sign_in_time >= :startTime AND sign_in_time < :endTime AND is_delete = false")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内的预约数
|
||||
*/
|
||||
public Mono<Long> countBookings(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(*) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND deleted_at IS NULL")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内的取消预约数
|
||||
*/
|
||||
public Mono<Long> countCancelBookings(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(*) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND status = '1' AND deleted_at IS NULL")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内的出席预约数
|
||||
*/
|
||||
public Mono<Long> countAttendBookings(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(*) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND status = '2' AND deleted_at IS NULL")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内的缺席预约数
|
||||
*/
|
||||
public Mono<Long> countAbsentBookings(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(*) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND status = '3' AND deleted_at IS NULL")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内有预约的独立会员数
|
||||
*/
|
||||
public Mono<Long> countDistinctBookingMembers(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(DISTINCT member_id) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND deleted_at IS NULL")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定时间范围内取消预约的独立会员数
|
||||
*/
|
||||
public Mono<Long> countDistinctCancelMembers(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT COUNT(DISTINCT member_id) FROM group_course_booking WHERE booking_time >= :startTime AND booking_time < :endTime AND status = '1' AND deleted_at IS NULL")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> row.get(0, Long.class))
|
||||
.one();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按签到类型统计次数
|
||||
*/
|
||||
public Flux<SignInTypeCount> countSignInsByType(LocalDateTime startTime, LocalDateTime endTime) {
|
||||
return databaseClient.sql("SELECT sign_in_type as type, COUNT(*) as count FROM sign_in_record WHERE sign_in_time >= :startTime AND sign_in_time < :endTime AND is_delete = false GROUP BY sign_in_type")
|
||||
.bind("startTime", startTime)
|
||||
.bind("endTime", endTime)
|
||||
.map(row -> {
|
||||
SignInTypeCount result = new SignInTypeCount();
|
||||
result.setType(row.get("type", String.class));
|
||||
result.setCount(row.get("count", Long.class));
|
||||
return result;
|
||||
})
|
||||
.all();
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到类型计数结果
|
||||
*/
|
||||
public static class SignInTypeCount {
|
||||
private String type;
|
||||
private Long count;
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public Long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(Long count) {
|
||||
this.count = count;
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package cn.novalon.gym.manage.datacount.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 预约数据统计结果
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BookingStatistics {
|
||||
|
||||
/**
|
||||
* 统计日期
|
||||
*/
|
||||
private String statDate;
|
||||
|
||||
/**
|
||||
* 新增预约数
|
||||
*/
|
||||
private Long newBookings;
|
||||
|
||||
/**
|
||||
* 取消预约数
|
||||
*/
|
||||
private Long cancelBookings;
|
||||
|
||||
/**
|
||||
* 出席预约数
|
||||
*/
|
||||
private Long attendBookings;
|
||||
|
||||
/**
|
||||
* 缺席预约数
|
||||
*/
|
||||
private Long absentBookings;
|
||||
|
||||
/**
|
||||
* 预约出席率
|
||||
*/
|
||||
private Double attendanceRate;
|
||||
|
||||
/**
|
||||
* 取消率
|
||||
*/
|
||||
private Double cancelRate;
|
||||
|
||||
/**
|
||||
* 预约人数(独立会员数)
|
||||
*/
|
||||
private Long bookingMembers;
|
||||
|
||||
/**
|
||||
* 取消人数(独立会员数)
|
||||
*/
|
||||
private Long cancelMembers;
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
package cn.novalon.gym.manage.datacount.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 数据统计结果域对象
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DataStatistics {
|
||||
|
||||
/**
|
||||
* 统计类型:MEMBER-会员统计,BOOKING-预约统计,SIGN_IN-签到统计
|
||||
*/
|
||||
private String statType;
|
||||
|
||||
/**
|
||||
* 统计周期:DAY-日统计,WEEK-周统计,MONTH-月统计
|
||||
*/
|
||||
private String periodType;
|
||||
|
||||
/**
|
||||
* 统计日期(DAY时为具体日期,WEEK时为周开始日期,MONTH时为月份第一天)
|
||||
*/
|
||||
private LocalDateTime statDate;
|
||||
|
||||
/**
|
||||
* 统计数据值
|
||||
*/
|
||||
private Long count;
|
||||
|
||||
/**
|
||||
* 关联ID(如会员ID等)
|
||||
*/
|
||||
private Long relatedId;
|
||||
|
||||
/**
|
||||
* 扩展数据(JSON格式存储额外信息)
|
||||
*/
|
||||
private String extraData;
|
||||
|
||||
/**
|
||||
* 统计周期常量
|
||||
*/
|
||||
public static final class PeriodType {
|
||||
/** 日统计 */
|
||||
public static final String DAY = "DAY";
|
||||
/** 周统计 */
|
||||
public static final String WEEK = "WEEK";
|
||||
/** 月统计 */
|
||||
public static final String MONTH = "MONTH";
|
||||
|
||||
private PeriodType() {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计类型常量
|
||||
*/
|
||||
public static final class StatType {
|
||||
/** 会员统计 */
|
||||
public static final String MEMBER = "MEMBER";
|
||||
/** 预约统计 */
|
||||
public static final String BOOKING = "BOOKING";
|
||||
/** 签到统计 */
|
||||
public static final String SIGN_IN = "SIGN_IN";
|
||||
|
||||
private StatType() {}
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package cn.novalon.gym.manage.datacount.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 会员数据统计结果
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class MemberStatistics {
|
||||
|
||||
/**
|
||||
* 统计日期
|
||||
*/
|
||||
private String statDate;
|
||||
|
||||
/**
|
||||
* 新增会员数
|
||||
*/
|
||||
private Long newMembers;
|
||||
|
||||
/**
|
||||
* 活跃会员数(当天有签到或预约的会员)
|
||||
*/
|
||||
private Long activeMembers;
|
||||
|
||||
/**
|
||||
* 累计会员总数
|
||||
*/
|
||||
private Long totalMembers;
|
||||
|
||||
/**
|
||||
* 今日签到会员数
|
||||
*/
|
||||
private Long signInMembers;
|
||||
|
||||
/**
|
||||
* 今日预约会员数
|
||||
*/
|
||||
private Long bookingMembers;
|
||||
|
||||
/**
|
||||
* 今日取消预约会员数
|
||||
*/
|
||||
private Long cancelBookingMembers;
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package cn.novalon.gym.manage.datacount.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 签到数据统计结果
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class SignInStatistics {
|
||||
|
||||
/**
|
||||
* 统计日期
|
||||
*/
|
||||
private String statDate;
|
||||
|
||||
/**
|
||||
* 签到总次数
|
||||
*/
|
||||
private Long totalSignIns;
|
||||
|
||||
/**
|
||||
* 成功签到次数
|
||||
*/
|
||||
private Long successSignIns;
|
||||
|
||||
/**
|
||||
* 失败签到次数
|
||||
*/
|
||||
private Long failedSignIns;
|
||||
|
||||
/**
|
||||
* 签到成功率
|
||||
*/
|
||||
private Double successRate;
|
||||
|
||||
/**
|
||||
* 签到人数(独立会员数)
|
||||
*/
|
||||
private Long signInMembers;
|
||||
|
||||
/**
|
||||
* 扫码签到次数
|
||||
*/
|
||||
private Long qrCodeSignIns;
|
||||
|
||||
/**
|
||||
* 手动签到次数
|
||||
*/
|
||||
private Long manualSignIns;
|
||||
|
||||
/**
|
||||
* 人脸识别签到次数
|
||||
*/
|
||||
private Long faceSignIns;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package cn.novalon.gym.manage.datacount.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 统计数据查询请求
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StatisticsQuery {
|
||||
|
||||
/**
|
||||
* 统计类型:MEMBER-会员统计,BOOKING-预约统计,SIGN_IN-签到统计
|
||||
* 空表示所有类型
|
||||
*/
|
||||
private String statType;
|
||||
|
||||
/**
|
||||
* 统计周期:DAY-日统计,WEEK-周统计,MONTH-月统计
|
||||
*/
|
||||
private String periodType;
|
||||
|
||||
/**
|
||||
* 开始时间
|
||||
*/
|
||||
private LocalDateTime startTime;
|
||||
|
||||
/**
|
||||
* 结束时间
|
||||
*/
|
||||
private LocalDateTime endTime;
|
||||
|
||||
/**
|
||||
* 分页页码
|
||||
*/
|
||||
private Integer page;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
private Integer size;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package cn.novalon.gym.manage.datacount.domain;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 统计数据汇总
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class StatisticsSummary {
|
||||
|
||||
/**
|
||||
* 统计日期
|
||||
*/
|
||||
private String statDate;
|
||||
|
||||
/**
|
||||
* 会员统计数据
|
||||
*/
|
||||
private MemberStatistics memberStatistics;
|
||||
|
||||
/**
|
||||
* 预约统计数据
|
||||
*/
|
||||
private BookingStatistics bookingStatistics;
|
||||
|
||||
/**
|
||||
* 签到统计数据
|
||||
*/
|
||||
private SignInStatistics signInStatistics;
|
||||
|
||||
/**
|
||||
* 统计数据生成时间
|
||||
*/
|
||||
private String generatedAt;
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
package cn.novalon.gym.manage.datacount.handler;
|
||||
|
||||
import cn.novalon.gym.manage.datacount.domain.*;
|
||||
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 数据统计 Handler
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Component
|
||||
@Tag(name = "数据统计", description = "数据统计相关操作")
|
||||
public class DataStatisticsHandler {
|
||||
|
||||
@Autowired
|
||||
private IDataStatisticsService dataStatisticsService;
|
||||
|
||||
@Operation(summary = "获取综合统计数据", description = "获取会员、预约、签到综合统计数据")
|
||||
public Mono<ServerResponse> getStatisticsSummary(ServerRequest request) {
|
||||
StatisticsQuery query = buildQueryFromRequest(request);
|
||||
|
||||
return dataStatisticsService.getStatisticsSummaryWithCache(query)
|
||||
.flatMap(summary -> ServerResponse.ok().bodyValue(summary))
|
||||
.onErrorResume(e -> {
|
||||
StatisticsSummary errorSummary = StatisticsSummary.builder()
|
||||
.statDate(LocalDateTime.now().toLocalDate().toString())
|
||||
.generatedAt(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
|
||||
.build();
|
||||
return ServerResponse.ok().bodyValue(errorSummary);
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "获取会员统计数据", description = "获取会员统计数据")
|
||||
public Mono<ServerResponse> getMemberStatistics(ServerRequest request) {
|
||||
StatisticsQuery query = buildQueryFromRequest(request);
|
||||
|
||||
return dataStatisticsService.getMemberStatistics(query)
|
||||
.flatMap(stats -> ServerResponse.ok().bodyValue(stats))
|
||||
.onErrorResume(e -> ServerResponse.ok().bodyValue(MemberStatistics.builder().statDate(query.getStartTime() != null ? query.getStartTime().toLocalDate().toString() : LocalDateTime.now().toLocalDate().toString()).build()));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取预约统计数据", description = "获取预约统计数据")
|
||||
public Mono<ServerResponse> getBookingStatistics(ServerRequest request) {
|
||||
StatisticsQuery query = buildQueryFromRequest(request);
|
||||
|
||||
return dataStatisticsService.getBookingStatistics(query)
|
||||
.flatMap(stats -> ServerResponse.ok().bodyValue(stats))
|
||||
.onErrorResume(e -> ServerResponse.ok().bodyValue(BookingStatistics.builder().statDate(query.getStartTime() != null ? query.getStartTime().toLocalDate().toString() : LocalDateTime.now().toLocalDate().toString()).build()));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取签到统计数据", description = "获取签到统计数据")
|
||||
public Mono<ServerResponse> getSignInStatistics(ServerRequest request) {
|
||||
StatisticsQuery query = buildQueryFromRequest(request);
|
||||
|
||||
return dataStatisticsService.getSignInStatistics(query)
|
||||
.flatMap(stats -> ServerResponse.ok().bodyValue(stats))
|
||||
.onErrorResume(e -> ServerResponse.ok().bodyValue(SignInStatistics.builder().statDate(query.getStartTime() != null ? query.getStartTime().toLocalDate().toString() : LocalDateTime.now().toLocalDate().toString()).build()));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询历史统计数据", description = "查询历史统计数据")
|
||||
public Mono<ServerResponse> queryHistoricalStatistics(ServerRequest request) {
|
||||
StatisticsQuery query = buildQueryFromRequest(request);
|
||||
|
||||
return dataStatisticsService.queryHistoricalStatistics(query)
|
||||
.collectList()
|
||||
.flatMap(stats -> ServerResponse.ok().bodyValue(stats));
|
||||
}
|
||||
|
||||
@Operation(summary = "导出统计数据", description = "导出统计数据为Excel文件")
|
||||
public Mono<ServerResponse> exportStatistics(ServerRequest request) {
|
||||
StatisticsQuery query = buildQueryFromRequest(request);
|
||||
|
||||
return dataStatisticsService.exportStatistics(query)
|
||||
.flatMap(bytes -> {
|
||||
String filename = "statistics_" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + ".xlsx";
|
||||
return ServerResponse.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||
.contentType(MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
|
||||
.bodyValue(bytes);
|
||||
})
|
||||
.onErrorResume(e -> ServerResponse.ok().bodyValue("导出失败: " + e.getMessage()));
|
||||
}
|
||||
|
||||
private StatisticsQuery buildQueryFromRequest(ServerRequest request) {
|
||||
StatisticsQuery.StatisticsQueryBuilder builder = StatisticsQuery.builder();
|
||||
|
||||
request.queryParam("statType").ifPresent(builder::statType);
|
||||
request.queryParam("periodType").ifPresent(builder::periodType);
|
||||
request.queryParam("startTime").ifPresent(startTimeStr -> {
|
||||
try {
|
||||
builder.startTime(LocalDateTime.parse(startTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
builder.startTime(LocalDateTime.parse(startTimeStr));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
request.queryParam("endTime").ifPresent(endTimeStr -> {
|
||||
try {
|
||||
builder.endTime(LocalDateTime.parse(endTimeStr, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
builder.endTime(LocalDateTime.parse(endTimeStr));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package cn.novalon.gym.manage.datacount.scheduler;
|
||||
|
||||
import cn.novalon.gym.manage.datacount.domain.DataStatistics;
|
||||
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
/**
|
||||
* 数据统计定时任务
|
||||
* 每日凌晨执行统计数据更新
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Component
|
||||
public class DataStatisticsScheduler {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DataStatisticsScheduler.class);
|
||||
|
||||
@Autowired
|
||||
private IDataStatisticsService dataStatisticsService;
|
||||
|
||||
/**
|
||||
* 每日凌晨2点执行前一天的日统计数据
|
||||
* 使用cron表达式: 0 0 2 * * ?
|
||||
*/
|
||||
@Scheduled(cron = "0 0 2 * * ?")
|
||||
public void executeDailyStatistics() {
|
||||
LocalDate yesterday = LocalDate.now().minusDays(1);
|
||||
log.info("Starting daily statistics task for date: {}", yesterday);
|
||||
|
||||
dataStatisticsService.executeDailyStatistics(yesterday)
|
||||
.subscribe(
|
||||
null,
|
||||
error -> log.error("Daily statistics task failed for date: {}", yesterday, error),
|
||||
() -> log.info("Daily statistics task completed for date: {}", yesterday)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每周一凌晨3点执行上周的周统计数据
|
||||
* 使用cron表达式: 0 0 3 ? * MON
|
||||
*/
|
||||
@Scheduled(cron = "0 0 3 ? * MON")
|
||||
public void executeWeeklyStatistics() {
|
||||
LocalDate lastWeek = LocalDate.now().minusWeeks(1);
|
||||
log.info("Starting weekly statistics task for week of: {}", lastWeek);
|
||||
|
||||
// 执行上周每天的统计数据汇总
|
||||
LocalDate weekStart = lastWeek.with(java.time.temporal.TemporalAdjusters.previousOrSame(java.time.DayOfWeek.MONDAY));
|
||||
LocalDate weekEnd = lastWeek.with(java.time.temporal.TemporalAdjusters.nextOrSame(java.time.DayOfWeek.SUNDAY));
|
||||
|
||||
for (LocalDate date = weekStart; !date.isAfter(weekEnd); date = date.plusDays(1)) {
|
||||
final LocalDate statDate = date;
|
||||
dataStatisticsService.executeDailyStatistics(statDate)
|
||||
.block();
|
||||
}
|
||||
|
||||
log.info("Weekly statistics task completed for week: {} - {}", weekStart, weekEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 每月1日凌晨3点执行上个月的月统计数据
|
||||
* 使用cron表达式: 0 0 3 1 * ?
|
||||
*/
|
||||
@Scheduled(cron = "0 0 3 1 * ?")
|
||||
public void executeMonthlyStatistics() {
|
||||
LocalDate lastMonth = LocalDate.now().minusMonths(1);
|
||||
log.info("Starting monthly statistics task for month: {}", lastMonth.getMonth());
|
||||
|
||||
// 执行上个月每天的统计数据补全
|
||||
LocalDate startOfMonth = lastMonth.withDayOfMonth(1);
|
||||
LocalDate endOfMonth = lastMonth.with(java.time.temporal.TemporalAdjusters.lastDayOfMonth());
|
||||
|
||||
for (LocalDate date = startOfMonth; !date.isAfter(endOfMonth); date = date.plusDays(1)) {
|
||||
final LocalDate statDate = date;
|
||||
dataStatisticsService.executeDailyStatistics(statDate)
|
||||
.block();
|
||||
}
|
||||
|
||||
log.info("Monthly statistics task completed for month: {}", lastMonth.getMonth());
|
||||
}
|
||||
|
||||
/**
|
||||
* 每月15日凌晨4点清理30天前的旧统计数据
|
||||
* 使用cron表达式: 0 0 4 15 * ?
|
||||
*/
|
||||
@Scheduled(cron = "0 0 4 15 * ?")
|
||||
public void cleanupOldStatistics() {
|
||||
LocalDate cutoffDate = LocalDate.now().minusDays(30);
|
||||
log.info("Starting cleanup old statistics task, removing data before: {}", cutoffDate);
|
||||
|
||||
// 清理Redis中的旧统计数据
|
||||
String pattern = "datacount:statistics:*:" + cutoffDate.toString();
|
||||
cn.novalon.gym.manage.common.util.RedisUtil redisUtil = null;
|
||||
try {
|
||||
// 这里可以通过注入的service来清理,但当前实现使用Redis缓存30天自动过期
|
||||
log.info("Old statistics cleanup completed, cutoff date: {}", cutoffDate);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to cleanup old statistics", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package cn.novalon.gym.manage.datacount.service;
|
||||
|
||||
import cn.novalon.gym.manage.datacount.domain.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* 数据统计服务接口
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
public interface IDataStatisticsService {
|
||||
|
||||
/**
|
||||
* 获取会员统计数据
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 会员统计数据
|
||||
*/
|
||||
Mono<MemberStatistics> getMemberStatistics(StatisticsQuery query);
|
||||
|
||||
/**
|
||||
* 获取预约统计数据
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 预约统计数据
|
||||
*/
|
||||
Mono<BookingStatistics> getBookingStatistics(StatisticsQuery query);
|
||||
|
||||
/**
|
||||
* 获取签到统计数据
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 签到统计数据
|
||||
*/
|
||||
Mono<SignInStatistics> getSignInStatistics(StatisticsQuery query);
|
||||
|
||||
/**
|
||||
* 获取综合统计数据(包含会员、预约、签到)
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 综合统计数据
|
||||
*/
|
||||
Mono<StatisticsSummary> getStatisticsSummary(StatisticsQuery query);
|
||||
|
||||
/**
|
||||
* 查询历史统计数据
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 历史统计数据列表
|
||||
*/
|
||||
Flux<DataStatistics> queryHistoricalStatistics(StatisticsQuery query);
|
||||
|
||||
/**
|
||||
* 执行每日统计数据更新(定时任务调用)
|
||||
*
|
||||
* @param statDate 统计日期
|
||||
* @return 更新结果
|
||||
*/
|
||||
Mono<Void> executeDailyStatistics(java.time.LocalDate statDate);
|
||||
|
||||
/**
|
||||
* 导出统计数据为Excel
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return Excel文件的字节数组
|
||||
*/
|
||||
Mono<byte[]> exportStatistics(StatisticsQuery query);
|
||||
|
||||
/**
|
||||
* 获取统计数据(带缓存)
|
||||
*
|
||||
* @param query 查询条件
|
||||
* @return 统计数据
|
||||
*/
|
||||
Mono<StatisticsSummary> getStatisticsSummaryWithCache(StatisticsQuery query);
|
||||
}
|
||||
+513
@@ -0,0 +1,513 @@
|
||||
package cn.novalon.gym.manage.datacount.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.datacount.dao.DataStatisticsDao;
|
||||
import cn.novalon.gym.manage.datacount.domain.*;
|
||||
import cn.novalon.gym.manage.datacount.service.IDataStatisticsService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 数据统计服务实现类
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-09
|
||||
*/
|
||||
@Service
|
||||
public class DataStatisticsServiceImpl implements IDataStatisticsService {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(DataStatisticsServiceImpl.class);
|
||||
|
||||
private static final String CACHE_KEY_PREFIX = "datacount:statistics:";
|
||||
private static final long CACHE_EXPIRE_SECONDS = 3600; // 1小时
|
||||
|
||||
@Autowired
|
||||
private DataStatisticsDao dataStatisticsDao;
|
||||
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Value("${datacount.cache.expire-seconds:3600}")
|
||||
private long cacheExpireSeconds = 3600;
|
||||
|
||||
@Override
|
||||
public Mono<MemberStatistics> getMemberStatistics(StatisticsQuery query) {
|
||||
LocalDateTime startTime = getStartTime(query);
|
||||
LocalDateTime endTime = getEndTime(query);
|
||||
|
||||
Mono<Long> newMembersMono = dataStatisticsDao.countNewMembers(startTime, endTime);
|
||||
Mono<Long> totalMembersMono = dataStatisticsDao.countTotalMembers();
|
||||
Mono<Long> signInMembersMono = dataStatisticsDao.countDistinctSignInMembers(startTime, endTime);
|
||||
Mono<Long> bookingMembersMono = dataStatisticsDao.countDistinctBookingMembers(startTime, endTime);
|
||||
Mono<Long> cancelMembersMono = dataStatisticsDao.countDistinctCancelMembers(startTime, endTime);
|
||||
|
||||
return Mono.zip(newMembersMono, totalMembersMono, signInMembersMono, bookingMembersMono, cancelMembersMono)
|
||||
.map(tuple -> {
|
||||
long newMembers = tuple.getT1() != null ? tuple.getT1() : 0L;
|
||||
long totalMembers = tuple.getT2() != null ? tuple.getT2() : 0L;
|
||||
long signInMembers = tuple.getT3() != null ? tuple.getT3() : 0L;
|
||||
long bookingMembers = tuple.getT4() != null ? tuple.getT4() : 0L;
|
||||
long cancelMembers = tuple.getT5() != null ? tuple.getT5() : 0L;
|
||||
|
||||
// 活跃会员数 = 有签到的 + 有预约的(去重后大概值)
|
||||
long activeMembers = signInMembers + bookingMembers;
|
||||
|
||||
return MemberStatistics.builder()
|
||||
.statDate(startTime.toLocalDate().toString())
|
||||
.newMembers(newMembers)
|
||||
.activeMembers(activeMembers)
|
||||
.totalMembers(totalMembers)
|
||||
.signInMembers(signInMembers)
|
||||
.bookingMembers(bookingMembers)
|
||||
.cancelBookingMembers(cancelMembers)
|
||||
.build();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<BookingStatistics> getBookingStatistics(StatisticsQuery query) {
|
||||
LocalDateTime startTime = getStartTime(query);
|
||||
LocalDateTime endTime = getEndTime(query);
|
||||
|
||||
Mono<Long> totalMono = dataStatisticsDao.countBookings(startTime, endTime);
|
||||
Mono<Long> cancelMono = dataStatisticsDao.countCancelBookings(startTime, endTime);
|
||||
Mono<Long> attendMono = dataStatisticsDao.countAttendBookings(startTime, endTime);
|
||||
Mono<Long> absentMono = dataStatisticsDao.countAbsentBookings(startTime, endTime);
|
||||
Mono<Long> bookingMembersMono = dataStatisticsDao.countDistinctBookingMembers(startTime, endTime);
|
||||
Mono<Long> cancelMembersMono = dataStatisticsDao.countDistinctCancelMembers(startTime, endTime);
|
||||
|
||||
return Mono.zip(totalMono, cancelMono, attendMono, absentMono, bookingMembersMono, cancelMembersMono)
|
||||
.map(tuple -> {
|
||||
long total = tuple.getT1() != null ? tuple.getT1() : 0L;
|
||||
long cancel = tuple.getT2() != null ? tuple.getT2() : 0L;
|
||||
long attend = tuple.getT3() != null ? tuple.getT3() : 0L;
|
||||
long absent = tuple.getT4() != null ? tuple.getT4() : 0L;
|
||||
long bookingMembers = tuple.getT5() != null ? tuple.getT5() : 0L;
|
||||
long cancelMembers = tuple.getT6() != null ? tuple.getT6() : 0L;
|
||||
|
||||
double attendanceRate = total > 0 ? (double) attend / total * 100 : 0;
|
||||
double cancelRate = total > 0 ? (double) cancel / total * 100 : 0;
|
||||
|
||||
return BookingStatistics.builder()
|
||||
.statDate(startTime.toLocalDate().toString())
|
||||
.newBookings(total)
|
||||
.cancelBookings(cancel)
|
||||
.attendBookings(attend)
|
||||
.absentBookings(absent)
|
||||
.attendanceRate(Math.round(attendanceRate * 100.0) / 100.0)
|
||||
.cancelRate(Math.round(cancelRate * 100.0) / 100.0)
|
||||
.bookingMembers(bookingMembers)
|
||||
.cancelMembers(cancelMembers)
|
||||
.build();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<SignInStatistics> getSignInStatistics(StatisticsQuery query) {
|
||||
LocalDateTime startTime = getStartTime(query);
|
||||
LocalDateTime endTime = getEndTime(query);
|
||||
|
||||
Mono<Long> totalMono = dataStatisticsDao.countSignIns(startTime, endTime);
|
||||
Mono<Long> successMono = dataStatisticsDao.countSuccessSignIns(startTime, endTime);
|
||||
Mono<Long> distinctMembersMono = dataStatisticsDao.countDistinctSignInMembers(startTime, endTime);
|
||||
|
||||
return Mono.zip(totalMono, successMono, distinctMembersMono)
|
||||
.flatMap(tuple -> {
|
||||
long total = tuple.getT1() != null ? tuple.getT1() : 0L;
|
||||
long success = tuple.getT2() != null ? tuple.getT2() : 0L;
|
||||
long distinctMembers = tuple.getT3() != null ? tuple.getT3() : 0L;
|
||||
|
||||
double successRate = total > 0 ? (double) success / total * 100 : 0;
|
||||
|
||||
return dataStatisticsDao.countSignInsByType(startTime, endTime)
|
||||
.collectMap(
|
||||
DataStatisticsDao.SignInTypeCount::getType,
|
||||
DataStatisticsDao.SignInTypeCount::getCount
|
||||
)
|
||||
.defaultIfEmpty(new HashMap<>())
|
||||
.map(typeCountMap -> {
|
||||
long qrCode = getCountByType(typeCountMap, SignInRecord.SignInType.QR_CODE);
|
||||
long manual = getCountByType(typeCountMap, SignInRecord.SignInType.MANUAL);
|
||||
long face = getCountByType(typeCountMap, SignInRecord.SignInType.FACE);
|
||||
|
||||
return SignInStatistics.builder()
|
||||
.statDate(startTime.toLocalDate().toString())
|
||||
.totalSignIns(total)
|
||||
.successSignIns(success)
|
||||
.failedSignIns(total - success)
|
||||
.successRate(Math.round(successRate * 100.0) / 100.0)
|
||||
.signInMembers(distinctMembers)
|
||||
.qrCodeSignIns(qrCode)
|
||||
.manualSignIns(manual)
|
||||
.faceSignIns(face)
|
||||
.build();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private long getCountByType(Map<String, Long> typeCountMap, String type) {
|
||||
Long count = typeCountMap.get(type);
|
||||
return count != null ? count : 0L;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<StatisticsSummary> getStatisticsSummary(StatisticsQuery query) {
|
||||
Mono<MemberStatistics> memberStatsMono = getMemberStatistics(query);
|
||||
Mono<BookingStatistics> bookingStatsMono = getBookingStatistics(query);
|
||||
Mono<SignInStatistics> signInStatsMono = getSignInStatistics(query);
|
||||
|
||||
return Mono.zip(memberStatsMono, bookingStatsMono, signInStatsMono)
|
||||
.map(tuple -> StatisticsSummary.builder()
|
||||
.statDate(LocalDateTime.now().toLocalDate().toString())
|
||||
.memberStatistics(tuple.getT1())
|
||||
.bookingStatistics(tuple.getT2())
|
||||
.signInStatistics(tuple.getT3())
|
||||
.generatedAt(LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public reactor.core.publisher.Flux<DataStatistics> queryHistoricalStatistics(StatisticsQuery query) {
|
||||
// 历史统计数据查询(从Redis缓存中获取)
|
||||
String cacheKey = buildCacheKey(query);
|
||||
return redisUtil.get(cacheKey, String.class)
|
||||
.flatMapMany(json -> {
|
||||
try {
|
||||
java.util.List<DataStatistics> stats = objectMapper.readValue(json,
|
||||
objectMapper.getTypeFactory().constructCollectionType(java.util.List.class, DataStatistics.class));
|
||||
return reactor.core.publisher.Flux.fromIterable(stats);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to parse historical statistics from cache", e);
|
||||
return reactor.core.publisher.Flux.empty();
|
||||
}
|
||||
})
|
||||
.switchIfEmpty(reactor.core.publisher.Flux.empty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> executeDailyStatistics(LocalDate statDate) {
|
||||
log.info("Executing daily statistics for date: {}", statDate);
|
||||
|
||||
LocalDateTime dayStart = statDate.atStartOfDay();
|
||||
LocalDateTime dayEnd = statDate.plusDays(1).atStartOfDay();
|
||||
|
||||
StatisticsQuery query = StatisticsQuery.builder()
|
||||
.startTime(dayStart)
|
||||
.endTime(dayEnd)
|
||||
.build();
|
||||
|
||||
Mono<MemberStatistics> memberStatsMono = getMemberStatistics(query);
|
||||
Mono<BookingStatistics> bookingStatsMono = getBookingStatistics(query);
|
||||
Mono<SignInStatistics> signInStatsMono = getSignInStatistics(query);
|
||||
|
||||
return Mono.zip(memberStatsMono, bookingStatsMono, signInStatsMono)
|
||||
.flatMap(tuple -> {
|
||||
MemberStatistics memberStats = tuple.getT1();
|
||||
BookingStatistics bookingStats = tuple.getT2();
|
||||
SignInStatistics signInStats = tuple.getT3();
|
||||
|
||||
String dateStr = statDate.toString();
|
||||
String memberKey = CACHE_KEY_PREFIX + "member:" + dateStr;
|
||||
String bookingKey = CACHE_KEY_PREFIX + "booking:" + dateStr;
|
||||
String signInKey = CACHE_KEY_PREFIX + "signin:" + dateStr;
|
||||
|
||||
try {
|
||||
String memberJson = objectMapper.writeValueAsString(memberStats);
|
||||
String bookingJson = objectMapper.writeValueAsString(bookingStats);
|
||||
String signInJson = objectMapper.writeValueAsString(signInStats);
|
||||
|
||||
return reactor.core.publisher.Flux.merge(
|
||||
redisUtil.setWithExpire(memberKey, memberJson, Duration.ofDays(30).getSeconds()),
|
||||
redisUtil.setWithExpire(bookingKey, bookingJson, Duration.ofDays(30).getSeconds()),
|
||||
redisUtil.setWithExpire(signInKey, signInJson, Duration.ofDays(30).getSeconds())
|
||||
).then();
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to serialize statistics data", e);
|
||||
return Mono.empty();
|
||||
}
|
||||
})
|
||||
.then()
|
||||
.doOnSuccess(v -> log.info("Daily statistics completed for date: {}", statDate))
|
||||
.doOnError(e -> log.error("Failed to execute daily statistics for date: {}", statDate, e));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<byte[]> exportStatistics(StatisticsQuery query) {
|
||||
return getStatisticsSummary(query)
|
||||
.flatMap(summary -> {
|
||||
try (Workbook workbook = new XSSFWorkbook()) {
|
||||
Sheet sheet = workbook.createSheet("数据统计报表");
|
||||
|
||||
CellStyle headerStyle = workbook.createCellStyle();
|
||||
Font headerFont = workbook.createFont();
|
||||
headerFont.setBold(true);
|
||||
headerStyle.setFont(headerFont);
|
||||
|
||||
createMainSheet(sheet, summary, headerStyle);
|
||||
|
||||
Sheet memberSheet = workbook.createSheet("会员统计");
|
||||
createMemberStatisticsSheet(memberSheet, summary.getMemberStatistics(), headerStyle);
|
||||
|
||||
Sheet bookingSheet = workbook.createSheet("预约统计");
|
||||
createBookingStatisticsSheet(bookingSheet, summary.getBookingStatistics(), headerStyle);
|
||||
|
||||
Sheet signInSheet = workbook.createSheet("签到统计");
|
||||
createSignInStatisticsSheet(signInSheet, summary.getSignInStatistics(), headerStyle);
|
||||
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
workbook.write(outputStream);
|
||||
return Mono.just(outputStream.toByteArray());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to export statistics", e);
|
||||
return Mono.error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createMainSheet(Sheet sheet, StatisticsSummary summary, CellStyle headerStyle) {
|
||||
int rowNum = 0;
|
||||
Row row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("统计项");
|
||||
row.createCell(1).setCellValue("数值");
|
||||
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("统计日期");
|
||||
row.createCell(1).setCellValue(summary.getStatDate());
|
||||
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("新增会员数");
|
||||
row.createCell(1).setCellValue(summary.getMemberStatistics().getNewMembers());
|
||||
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("活跃会员数");
|
||||
row.createCell(1).setCellValue(summary.getMemberStatistics().getActiveMembers());
|
||||
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("累计会员总数");
|
||||
row.createCell(1).setCellValue(summary.getMemberStatistics().getTotalMembers());
|
||||
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("新增预约数");
|
||||
row.createCell(1).setCellValue(summary.getBookingStatistics().getNewBookings());
|
||||
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("预约出席率");
|
||||
row.createCell(1).setCellValue(summary.getBookingStatistics().getAttendanceRate() + "%");
|
||||
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("签到总次数");
|
||||
row.createCell(1).setCellValue(summary.getSignInStatistics().getTotalSignIns());
|
||||
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("签到成功率");
|
||||
row.createCell(1).setCellValue(summary.getSignInStatistics().getSuccessRate() + "%");
|
||||
|
||||
sheet.autoSizeColumn(0);
|
||||
sheet.autoSizeColumn(1);
|
||||
}
|
||||
|
||||
private void createMemberStatisticsSheet(Sheet sheet, MemberStatistics stats, CellStyle headerStyle) {
|
||||
int rowNum = 0;
|
||||
Row row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("统计项");
|
||||
row.createCell(1).setCellValue("数值");
|
||||
|
||||
String[][] data = {
|
||||
{"统计日期", stats.getStatDate()},
|
||||
{"新增会员数", String.valueOf(stats.getNewMembers())},
|
||||
{"活跃会员数", String.valueOf(stats.getActiveMembers())},
|
||||
{"累计会员总数", String.valueOf(stats.getTotalMembers())},
|
||||
{"今日签到会员数", String.valueOf(stats.getSignInMembers())},
|
||||
{"今日预约会员数", String.valueOf(stats.getBookingMembers())},
|
||||
{"今日取消预约会员数", String.valueOf(stats.getCancelBookingMembers())}
|
||||
};
|
||||
|
||||
for (String[] rowData : data) {
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue(rowData[0]);
|
||||
row.createCell(1).setCellValue(rowData[1]);
|
||||
}
|
||||
|
||||
sheet.autoSizeColumn(0);
|
||||
sheet.autoSizeColumn(1);
|
||||
}
|
||||
|
||||
private void createBookingStatisticsSheet(Sheet sheet, BookingStatistics stats, CellStyle headerStyle) {
|
||||
int rowNum = 0;
|
||||
Row row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("统计项");
|
||||
row.createCell(1).setCellValue("数值");
|
||||
|
||||
String[][] data = {
|
||||
{"统计日期", stats.getStatDate()},
|
||||
{"新增预约数", String.valueOf(stats.getNewBookings())},
|
||||
{"取消预约数", String.valueOf(stats.getCancelBookings())},
|
||||
{"出席预约数", String.valueOf(stats.getAttendBookings())},
|
||||
{"缺席预约数", String.valueOf(stats.getAbsentBookings())},
|
||||
{"预约出席率", stats.getAttendanceRate() + "%"},
|
||||
{"取消率", stats.getCancelRate() + "%"},
|
||||
{"预约人数", String.valueOf(stats.getBookingMembers())},
|
||||
{"取消人数", String.valueOf(stats.getCancelMembers())}
|
||||
};
|
||||
|
||||
for (String[] rowData : data) {
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue(rowData[0]);
|
||||
row.createCell(1).setCellValue(rowData[1]);
|
||||
}
|
||||
|
||||
sheet.autoSizeColumn(0);
|
||||
sheet.autoSizeColumn(1);
|
||||
}
|
||||
|
||||
private void createSignInStatisticsSheet(Sheet sheet, SignInStatistics stats, CellStyle headerStyle) {
|
||||
int rowNum = 0;
|
||||
Row row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue("统计项");
|
||||
row.createCell(1).setCellValue("数值");
|
||||
|
||||
String[][] data = {
|
||||
{"统计日期", stats.getStatDate()},
|
||||
{"签到总次数", String.valueOf(stats.getTotalSignIns())},
|
||||
{"成功签到次数", String.valueOf(stats.getSuccessSignIns())},
|
||||
{"失败签到次数", String.valueOf(stats.getFailedSignIns())},
|
||||
{"签到成功率", stats.getSuccessRate() + "%"},
|
||||
{"签到人数", String.valueOf(stats.getSignInMembers())},
|
||||
{"扫码签到次数", String.valueOf(stats.getQrCodeSignIns())},
|
||||
{"手动签到次数", String.valueOf(stats.getManualSignIns())},
|
||||
{"人脸识别签到次数", String.valueOf(stats.getFaceSignIns())}
|
||||
};
|
||||
|
||||
for (String[] rowData : data) {
|
||||
row = sheet.createRow(rowNum++);
|
||||
row.createCell(0).setCellValue(rowData[0]);
|
||||
row.createCell(1).setCellValue(rowData[1]);
|
||||
}
|
||||
|
||||
sheet.autoSizeColumn(0);
|
||||
sheet.autoSizeColumn(1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<StatisticsSummary> getStatisticsSummaryWithCache(StatisticsQuery query) {
|
||||
String cacheKey = buildCacheKey(query);
|
||||
|
||||
return redisUtil.get(cacheKey, StatisticsSummary.class)
|
||||
.switchIfEmpty(
|
||||
getStatisticsSummary(query)
|
||||
.flatMap(summary -> {
|
||||
try {
|
||||
String json = objectMapper.writeValueAsString(summary);
|
||||
return redisUtil.setWithExpire(cacheKey, json, cacheExpireSeconds)
|
||||
.thenReturn(summary);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to serialize statistics summary", e);
|
||||
return Mono.just(summary);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private String buildCacheKey(StatisticsQuery query) {
|
||||
StringBuilder keyBuilder = new StringBuilder(CACHE_KEY_PREFIX);
|
||||
keyBuilder.append("summary:");
|
||||
|
||||
if (query.getStartTime() != null) {
|
||||
keyBuilder.append(query.getStartTime().toLocalDate().toString());
|
||||
}
|
||||
if (query.getEndTime() != null) {
|
||||
keyBuilder.append("_").append(query.getEndTime().toLocalDate().toString());
|
||||
}
|
||||
if (query.getStatType() != null) {
|
||||
keyBuilder.append("_").append(query.getStatType());
|
||||
}
|
||||
if (query.getPeriodType() != null) {
|
||||
keyBuilder.append("_").append(query.getPeriodType());
|
||||
}
|
||||
|
||||
return keyBuilder.toString();
|
||||
}
|
||||
|
||||
private LocalDateTime getStartTime(StatisticsQuery query) {
|
||||
if (query.getStartTime() != null) {
|
||||
return query.getStartTime();
|
||||
}
|
||||
|
||||
LocalDate today = LocalDate.now();
|
||||
String periodType = query.getPeriodType();
|
||||
|
||||
if (DataStatistics.PeriodType.WEEK.equals(periodType)) {
|
||||
// 周统计:本周一
|
||||
return today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay();
|
||||
} else if (DataStatistics.PeriodType.MONTH.equals(periodType)) {
|
||||
// 月统计:本月第一天
|
||||
return today.withDayOfMonth(1).atStartOfDay();
|
||||
} else {
|
||||
// 日统计:当天零点
|
||||
return today.atStartOfDay();
|
||||
}
|
||||
}
|
||||
|
||||
private LocalDateTime getEndTime(StatisticsQuery query) {
|
||||
if (query.getEndTime() != null) {
|
||||
return query.getEndTime();
|
||||
}
|
||||
|
||||
LocalDate today = LocalDate.now();
|
||||
String periodType = query.getPeriodType();
|
||||
|
||||
if (DataStatistics.PeriodType.WEEK.equals(periodType)) {
|
||||
// 周统计:本周日 23:59:59
|
||||
return today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(23, 59, 59);
|
||||
} else if (DataStatistics.PeriodType.MONTH.equals(periodType)) {
|
||||
// 月统计:本月最后一天 23:59:59
|
||||
return today.with(TemporalAdjusters.lastDayOfMonth()).atTime(23, 59, 59);
|
||||
} else {
|
||||
// 日统计:当前时间
|
||||
return LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据周期类型调整时间范围
|
||||
* 用于定时任务中的周期统计
|
||||
*/
|
||||
private LocalDateTime[] adjustTimeRangeByPeriod(LocalDate date, String periodType) {
|
||||
LocalDateTime startTime;
|
||||
LocalDateTime endTime;
|
||||
|
||||
if (DataStatistics.PeriodType.WEEK.equals(periodType)) {
|
||||
startTime = date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay();
|
||||
endTime = date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(23, 59, 59);
|
||||
} else if (DataStatistics.PeriodType.MONTH.equals(periodType)) {
|
||||
startTime = date.withDayOfMonth(1).atStartOfDay();
|
||||
endTime = date.with(TemporalAdjusters.lastDayOfMonth()).atTime(23, 59, 59);
|
||||
} else {
|
||||
startTime = date.atStartOfDay();
|
||||
endTime = date.plusDays(1).atStartOfDay();
|
||||
}
|
||||
|
||||
return new LocalDateTime[]{startTime, endTime};
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
cn.novalon.gym.manage.datacount.config.DataCountAutoConfiguration
|
||||
@@ -35,6 +35,11 @@
|
||||
<artifactId>manage-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-sys</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>manage-db</artifactId>
|
||||
|
||||
+31
@@ -4,8 +4,10 @@ package cn.novalon.gym.manage.groupcourse.converter;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
|
||||
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseBookingEntity;
|
||||
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
|
||||
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseTypeEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@@ -124,4 +126,33 @@ public class GroupCourseConverter {
|
||||
.map(this::toBookingEntity)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将团课类型实体转换为领域模型
|
||||
*/
|
||||
public GroupCourseType toGroupCourseType(GroupCourseTypeEntity entity){
|
||||
if(entity == null){
|
||||
return null;
|
||||
}
|
||||
GroupCourseType groupCourseType = new GroupCourseType();
|
||||
BeanUtil.copyProperties(entity, groupCourseType);
|
||||
log.debug("转换团课类型实体到领域模型:typeId={}", entity.getId());
|
||||
return groupCourseType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将团课类型领域模型转换为实体
|
||||
*/
|
||||
public GroupCourseTypeEntity toGroupCourseTypeEntity(GroupCourseType domain){
|
||||
if(domain == null){
|
||||
return null;
|
||||
}
|
||||
GroupCourseTypeEntity entity = new GroupCourseTypeEntity();
|
||||
BeanUtil.copyProperties(domain, entity);
|
||||
if (domain.getId() != null) {
|
||||
entity.markNotNew();
|
||||
}
|
||||
log.debug("转换团课类型领域模型到实体:typeId={}", domain.getId());
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
package cn.novalon.gym.manage.groupcourse.dao;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.entity.CourseLabelEntity;
|
||||
import org.springframework.data.r2dbc.repository.Modifying;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Repository
|
||||
public interface CourseLabelDao extends R2dbcRepository<CourseLabelEntity, Long> {
|
||||
|
||||
Mono<CourseLabelEntity> findByIdIsAndDeletedAtIsNull(Long id);
|
||||
|
||||
Flux<CourseLabelEntity> findAllByDeletedAtIsNull();
|
||||
|
||||
Flux<CourseLabelEntity> findByLabelNameContainingAndDeletedAtIsNull(String labelName);
|
||||
|
||||
Mono<CourseLabelEntity> findByLabelNameAndDeletedAtIsNull(String labelName);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE course_label SET deleted_at = :deletedAt WHERE id = :id")
|
||||
Mono<Integer> softDelete(Long id, LocalDateTime deletedAt);
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package cn.novalon.gym.manage.groupcourse.dao;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.entity.CourseTypeLabelEntity;
|
||||
import org.springframework.data.r2dbc.repository.Modifying;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface CourseTypeLabelDao extends R2dbcRepository<CourseTypeLabelEntity, Long> {
|
||||
|
||||
Flux<CourseTypeLabelEntity> findByTypeIdAndDeletedAtIsNull(Long typeId);
|
||||
|
||||
Flux<CourseTypeLabelEntity> findByLabelIdAndDeletedAtIsNull(Long labelId);
|
||||
|
||||
Mono<CourseTypeLabelEntity> findByTypeIdAndLabelIdAndDeletedAtIsNull(Long typeId, Long labelId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE course_type_label SET deleted_at = :deletedAt WHERE type_id = :typeId AND label_id = :labelId")
|
||||
Mono<Integer> deleteByTypeIdAndLabelId(Long typeId, Long labelId, LocalDateTime deletedAt);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE course_type_label SET deleted_at = :deletedAt WHERE type_id = :typeId")
|
||||
Mono<Integer> deleteByTypeId(Long typeId, LocalDateTime deletedAt);
|
||||
|
||||
@Modifying
|
||||
@Query("DELETE FROM course_type_label WHERE type_id = :typeId AND label_id = :labelId")
|
||||
Mono<Integer> physicalDeleteByTypeIdAndLabelId(Long typeId, Long labelId);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE course_type_label SET deleted_at = :deletedAt WHERE label_id = :labelId")
|
||||
Mono<Integer> deleteByLabelId(Long labelId, LocalDateTime deletedAt);
|
||||
}
|
||||
+2
@@ -36,4 +36,6 @@ public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long>
|
||||
@Modifying
|
||||
@Query("UPDATE group_course SET deleted_at = :deletedAt WHERE id = :id")
|
||||
Mono<Integer> softDelete(Long id, LocalDateTime deletedAt);
|
||||
|
||||
Flux<GroupCourseEntity> findByCourseTypeAndDeletedAtIsNull(Long courseType);
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package cn.novalon.gym.manage.groupcourse.dao;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseTypeEntity;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.r2dbc.repository.Modifying;
|
||||
import org.springframework.data.r2dbc.repository.Query;
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Repository
|
||||
public interface GroupCourseTypeDao extends R2dbcRepository<GroupCourseTypeEntity, Long> {
|
||||
|
||||
Mono<GroupCourseTypeEntity> findByIdIsAndDeletedAtIsNull(Long id);
|
||||
|
||||
Flux<GroupCourseTypeEntity> findAllByDeletedAtIsNull();
|
||||
|
||||
Flux<GroupCourseTypeEntity> findAllByDeletedAtIsNull(Sort sort);
|
||||
|
||||
Flux<GroupCourseTypeEntity> findByTypeNameContainingAndDeletedAtIsNull(String typeName);
|
||||
|
||||
Flux<GroupCourseTypeEntity> findByCategoryAndDeletedAtIsNull(String category);
|
||||
|
||||
Mono<GroupCourseTypeEntity> findByTypeNameAndDeletedAtIsNull(String typeName);
|
||||
|
||||
@Modifying
|
||||
@Query("UPDATE group_course_type SET deleted_at = :deletedAt WHERE id = :id")
|
||||
Mono<Integer> softDelete(Long id, LocalDateTime deletedAt);
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package cn.novalon.gym.manage.groupcourse.domain;
|
||||
|
||||
import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
public class CourseLabel extends BaseDomain {
|
||||
|
||||
//标签名称
|
||||
@Schema(description = "标签名称", example = "适合新手")
|
||||
private String labelName;
|
||||
|
||||
//标签颜色(十六进制)
|
||||
@Schema(description = "标签颜色(十六进制)", example = "#52c41a")
|
||||
private String color;
|
||||
|
||||
//标签描述
|
||||
@Schema(description = "标签描述", example = "适合健身初学者")
|
||||
private String description;
|
||||
|
||||
public String getLabelName() {
|
||||
return labelName;
|
||||
}
|
||||
|
||||
public void setLabelName(String labelName) {
|
||||
this.labelName = labelName;
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(String color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
+254
@@ -0,0 +1,254 @@
|
||||
package cn.novalon.gym.manage.groupcourse.domain;
|
||||
|
||||
import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 团课完整信息领域模型
|
||||
* 包含团课基础信息、关联的类型信息以及类型的标签信息
|
||||
*/
|
||||
public class GroupCourseDetail extends BaseDomain {
|
||||
|
||||
// ===== 团课基础信息 =====
|
||||
|
||||
@Schema(description = "课程名称", example = "瑜伽入门")
|
||||
private String courseName;
|
||||
|
||||
@Schema(description = "教练ID", example = "1")
|
||||
private Long coachId;
|
||||
|
||||
@Schema(description = "课程类型ID", example = "1")
|
||||
private Long courseType;
|
||||
|
||||
@Schema(description = "开始时间", example = "2026-06-02T09:00:00")
|
||||
private LocalDateTime startTime;
|
||||
|
||||
@Schema(description = "结束时间", example = "2026-06-02T10:00:00")
|
||||
private LocalDateTime endTime;
|
||||
|
||||
@Schema(description = "最大参与人数", example = "20")
|
||||
private Integer maxMembers;
|
||||
|
||||
@Schema(description = "当前参与人数", example = "15")
|
||||
private Integer currentMembers;
|
||||
|
||||
@Schema(description = "课程状态", example = "0")
|
||||
private Long status;
|
||||
|
||||
@Schema(description = "上课地点", example = "健身房A区")
|
||||
private String location;
|
||||
|
||||
@Schema(description = "封面图URL", example = "https://example.com/yoga.jpg")
|
||||
private String coverImage;
|
||||
|
||||
@Schema(description = "课程描述", example = "适合初学者的瑜伽课程")
|
||||
private String description;
|
||||
|
||||
@Schema(description = "点卡额度(消耗次数)", example = "1")
|
||||
private Integer pointCardAmount;
|
||||
|
||||
@Schema(description = "储值卡额度(消耗金额)", example = "50.00")
|
||||
private BigDecimal storedValueAmount;
|
||||
|
||||
// ===== 关联的类型信息 =====
|
||||
|
||||
@Schema(description = "类型信息")
|
||||
private GroupCourseType typeInfo;
|
||||
|
||||
// ===== 快捷访问属性(从类型信息派生)=====
|
||||
|
||||
@Schema(description = "类型名称", example = "瑜伽入门")
|
||||
private String typeName;
|
||||
|
||||
@Schema(description = "类型分类", example = "柔韧与平衡类")
|
||||
private String typeCategory;
|
||||
|
||||
@Schema(description = "基础难度", example = "2")
|
||||
private Integer baseDifficulty;
|
||||
|
||||
@Schema(description = "难度等级描述", example = "初级")
|
||||
private String difficultyLevel;
|
||||
|
||||
@Schema(description = "综合难度系数", example = "2")
|
||||
private Integer calculatedDifficulty;
|
||||
|
||||
// ===== 标签信息(从类型标签派生)=====
|
||||
|
||||
@Schema(description = "标签列表")
|
||||
private List<CourseLabel> labels;
|
||||
|
||||
// ===== Getters and Setters =====
|
||||
|
||||
public String getCourseName() {
|
||||
return courseName;
|
||||
}
|
||||
|
||||
public void setCourseName(String courseName) {
|
||||
this.courseName = courseName;
|
||||
}
|
||||
|
||||
public Long getCoachId() {
|
||||
return coachId;
|
||||
}
|
||||
|
||||
public void setCoachId(Long coachId) {
|
||||
this.coachId = coachId;
|
||||
}
|
||||
|
||||
public Long getCourseType() {
|
||||
return courseType;
|
||||
}
|
||||
|
||||
public void setCourseType(Long courseType) {
|
||||
this.courseType = courseType;
|
||||
}
|
||||
|
||||
public LocalDateTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalDateTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalDateTime getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(LocalDateTime endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Integer getMaxMembers() {
|
||||
return maxMembers;
|
||||
}
|
||||
|
||||
public void setMaxMembers(Integer maxMembers) {
|
||||
this.maxMembers = maxMembers;
|
||||
}
|
||||
|
||||
public Integer getCurrentMembers() {
|
||||
return currentMembers;
|
||||
}
|
||||
|
||||
public void setCurrentMembers(Integer currentMembers) {
|
||||
this.currentMembers = currentMembers;
|
||||
}
|
||||
|
||||
public Long getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Long status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public String getCoverImage() {
|
||||
return coverImage;
|
||||
}
|
||||
|
||||
public void setCoverImage(String coverImage) {
|
||||
this.coverImage = coverImage;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public Integer getPointCardAmount() {
|
||||
return pointCardAmount;
|
||||
}
|
||||
|
||||
public void setPointCardAmount(Integer pointCardAmount) {
|
||||
this.pointCardAmount = pointCardAmount;
|
||||
}
|
||||
|
||||
public BigDecimal getStoredValueAmount() {
|
||||
return storedValueAmount;
|
||||
}
|
||||
|
||||
public void setStoredValueAmount(BigDecimal storedValueAmount) {
|
||||
this.storedValueAmount = storedValueAmount;
|
||||
}
|
||||
|
||||
public GroupCourseType getTypeInfo() {
|
||||
return typeInfo;
|
||||
}
|
||||
|
||||
public void setTypeInfo(GroupCourseType typeInfo) {
|
||||
this.typeInfo = typeInfo;
|
||||
// 同步派生属性
|
||||
if (typeInfo != null) {
|
||||
this.typeName = typeInfo.getTypeName();
|
||||
this.typeCategory = typeInfo.getCategory();
|
||||
this.baseDifficulty = typeInfo.getBaseDifficulty();
|
||||
this.difficultyLevel = typeInfo.getDifficultyLevel();
|
||||
this.calculatedDifficulty = typeInfo.getCalculatedDifficulty();
|
||||
this.labels = typeInfo.getLabels();
|
||||
}
|
||||
}
|
||||
|
||||
public String getTypeName() {
|
||||
return typeName;
|
||||
}
|
||||
|
||||
public void setTypeName(String typeName) {
|
||||
this.typeName = typeName;
|
||||
}
|
||||
|
||||
public String getTypeCategory() {
|
||||
return typeCategory;
|
||||
}
|
||||
|
||||
public void setTypeCategory(String typeCategory) {
|
||||
this.typeCategory = typeCategory;
|
||||
}
|
||||
|
||||
public Integer getBaseDifficulty() {
|
||||
return baseDifficulty;
|
||||
}
|
||||
|
||||
public void setBaseDifficulty(Integer baseDifficulty) {
|
||||
this.baseDifficulty = baseDifficulty;
|
||||
}
|
||||
|
||||
public String getDifficultyLevel() {
|
||||
return difficultyLevel;
|
||||
}
|
||||
|
||||
public void setDifficultyLevel(String difficultyLevel) {
|
||||
this.difficultyLevel = difficultyLevel;
|
||||
}
|
||||
|
||||
public Integer getCalculatedDifficulty() {
|
||||
return calculatedDifficulty;
|
||||
}
|
||||
|
||||
public void setCalculatedDifficulty(Integer calculatedDifficulty) {
|
||||
this.calculatedDifficulty = calculatedDifficulty;
|
||||
}
|
||||
|
||||
public List<CourseLabel> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
public void setLabels(List<CourseLabel> labels) {
|
||||
this.labels = labels;
|
||||
}
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package cn.novalon.gym.manage.groupcourse.domain;
|
||||
|
||||
import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class GroupCourseType extends BaseDomain {
|
||||
|
||||
//类型名称
|
||||
@Schema(description = "类型名称", example = "瑜伽入门")
|
||||
private String typeName;
|
||||
|
||||
//基础难度(1-10)
|
||||
@Schema(description = "基础难度(1-10)", example = "2")
|
||||
private Integer baseDifficulty;
|
||||
|
||||
//类型描述
|
||||
@Schema(description = "类型描述", example = "适合初学者的瑜伽课程")
|
||||
private String description;
|
||||
|
||||
//分类(如:有氧、力量、柔韧等)
|
||||
@Schema(description = "分类", example = "柔韧与平衡类")
|
||||
private String category;
|
||||
|
||||
//标签列表
|
||||
@Schema(description = "标签列表")
|
||||
private List<CourseLabel> labels = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 计算综合难度系数
|
||||
*
|
||||
* 当前实现仅返回基础难度,为后续扩展预留空间。
|
||||
* 未来可扩展的影响因素包括:
|
||||
* 1. 课程时长系数(时长越长难度越高)
|
||||
* 2. 教练难度调整系数(教练可根据实际情况微调)
|
||||
* 3. 会员等级适配系数(根据会员等级动态调整显示难度)
|
||||
* 4. 课程强度系数(高强度课程难度加成)
|
||||
*
|
||||
* @return 综合难度系数(1-10)
|
||||
*/
|
||||
@Schema(description = "综合难度系数(预留扩展字段)", example = "2")
|
||||
public Integer getCalculatedDifficulty() {
|
||||
// TODO: 预留扩展点 - 未来可在此处添加更多难度计算逻辑
|
||||
// 例如:return calculateDynamicDifficulty(baseDifficulty, additionalFactors...);
|
||||
return this.baseDifficulty != null ? this.baseDifficulty : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取难度等级描述
|
||||
* 将数字难度转换为友好的文字描述
|
||||
*
|
||||
* @return 难度等级描述
|
||||
*/
|
||||
@Schema(description = "难度等级描述", example = "初级")
|
||||
public String getDifficultyLevel() {
|
||||
if (baseDifficulty == null) {
|
||||
return "未知";
|
||||
}
|
||||
if (baseDifficulty <= 2) {
|
||||
return "初级";
|
||||
} else if (baseDifficulty <= 4) {
|
||||
return "中级";
|
||||
} else if (baseDifficulty <= 6) {
|
||||
return "中高级";
|
||||
} else if (baseDifficulty <= 8) {
|
||||
return "高级";
|
||||
} else {
|
||||
return "专家级";
|
||||
}
|
||||
}
|
||||
|
||||
public String getTypeName() {
|
||||
return typeName;
|
||||
}
|
||||
|
||||
public void setTypeName(String typeName) {
|
||||
this.typeName = typeName;
|
||||
}
|
||||
|
||||
public Integer getBaseDifficulty() {
|
||||
return baseDifficulty;
|
||||
}
|
||||
|
||||
public void setBaseDifficulty(Integer baseDifficulty) {
|
||||
this.baseDifficulty = baseDifficulty;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public void setCategory(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
public List<CourseLabel> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
public void setLabels(List<CourseLabel> labels) {
|
||||
this.labels = labels;
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package cn.novalon.gym.manage.groupcourse.entity;
|
||||
|
||||
import cn.novalon.gym.manage.db.entity.BaseEntity;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
@Table("course_label")
|
||||
public class CourseLabelEntity extends BaseEntity {
|
||||
|
||||
//标签名称
|
||||
@Column("label_name")
|
||||
private String labelName;
|
||||
|
||||
//标签颜色(十六进制)
|
||||
@Column("color")
|
||||
private String color;
|
||||
|
||||
//标签描述
|
||||
@Column("description")
|
||||
private String description;
|
||||
|
||||
public String getLabelName() {
|
||||
return labelName;
|
||||
}
|
||||
|
||||
public void setLabelName(String labelName) {
|
||||
this.labelName = labelName;
|
||||
}
|
||||
|
||||
public String getColor() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setColor(String color) {
|
||||
this.color = color;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package cn.novalon.gym.manage.groupcourse.entity;
|
||||
|
||||
import cn.novalon.gym.manage.db.entity.BaseEntity;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
@Table("course_type_label")
|
||||
public class CourseTypeLabelEntity extends BaseEntity {
|
||||
|
||||
//团课类型ID
|
||||
@Column("type_id")
|
||||
private Long typeId;
|
||||
|
||||
//标签ID
|
||||
@Column("label_id")
|
||||
private Long labelId;
|
||||
|
||||
public Long getTypeId() {
|
||||
return typeId;
|
||||
}
|
||||
|
||||
public void setTypeId(Long typeId) {
|
||||
this.typeId = typeId;
|
||||
}
|
||||
|
||||
public Long getLabelId() {
|
||||
return labelId;
|
||||
}
|
||||
|
||||
public void setLabelId(Long labelId) {
|
||||
this.labelId = labelId;
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package cn.novalon.gym.manage.groupcourse.entity;
|
||||
|
||||
import cn.novalon.gym.manage.db.entity.BaseEntity;
|
||||
import org.springframework.data.relational.core.mapping.Column;
|
||||
import org.springframework.data.relational.core.mapping.Table;
|
||||
|
||||
@Table("group_course_type")
|
||||
public class GroupCourseTypeEntity extends BaseEntity {
|
||||
|
||||
//类型名称
|
||||
@Column("type_name")
|
||||
private String typeName;
|
||||
|
||||
//基础难度(1-10)
|
||||
@Column("base_difficulty")
|
||||
private Integer baseDifficulty;
|
||||
|
||||
//类型描述
|
||||
@Column("description")
|
||||
private String description;
|
||||
|
||||
//分类(如:有氧、力量、柔韧等)
|
||||
@Column("category")
|
||||
private String category;
|
||||
|
||||
public String getTypeName() {
|
||||
return typeName;
|
||||
}
|
||||
|
||||
public void setTypeName(String typeName) {
|
||||
this.typeName = typeName;
|
||||
}
|
||||
|
||||
public Integer getBaseDifficulty() {
|
||||
return baseDifficulty;
|
||||
}
|
||||
|
||||
public void setBaseDifficulty(Integer baseDifficulty) {
|
||||
this.baseDifficulty = baseDifficulty;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return category;
|
||||
}
|
||||
|
||||
public void setCategory(String category) {
|
||||
this.category = category;
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
package cn.novalon.gym.manage.groupcourse.handler;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
|
||||
import cn.novalon.gym.manage.groupcourse.service.ICourseLabelService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Tag(name = "团课标签管理", description = "团课标签相关操作")
|
||||
public class CourseLabelHandler {
|
||||
|
||||
private final ICourseLabelService courseLabelService;
|
||||
|
||||
public CourseLabelHandler(ICourseLabelService courseLabelService) {
|
||||
this.courseLabelService = courseLabelService;
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有标签", description = "获取系统中所有标签列表")
|
||||
public Mono<ServerResponse> getAllLabels(ServerRequest request) {
|
||||
return ServerResponse.ok()
|
||||
.body(courseLabelService.findAll(), CourseLabel.class);
|
||||
}
|
||||
|
||||
@Operation(summary = "根据ID获取标签", description = "根据ID获取标签详情")
|
||||
public Mono<ServerResponse> getLabelById(ServerRequest request) {
|
||||
Long id = Long.valueOf(request.pathVariable("id"));
|
||||
return courseLabelService.findById(id)
|
||||
.flatMap(label -> ServerResponse.ok().bodyValue(label))
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
|
||||
@Operation(summary = "搜索标签", description = "根据关键词搜索标签")
|
||||
public Mono<ServerResponse> searchLabels(ServerRequest request) {
|
||||
String keyword = request.queryParam("keyword").orElse("");
|
||||
return ServerResponse.ok()
|
||||
.body(courseLabelService.findByKeyword(keyword), CourseLabel.class);
|
||||
}
|
||||
|
||||
@Operation(summary = "创建标签", description = "创建新的标签")
|
||||
public Mono<ServerResponse> createLabel(ServerRequest request) {
|
||||
return request.bodyToMono(CourseLabel.class)
|
||||
.flatMap(courseLabel -> {
|
||||
if (courseLabel.getLabelName() == null || courseLabel.getLabelName().isEmpty()) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("success", false);
|
||||
error.put("message", "标签名称不能为空");
|
||||
return ServerResponse.badRequest().bodyValue(error);
|
||||
}
|
||||
|
||||
if (courseLabel.getLabelName().length() > 50) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("success", false);
|
||||
error.put("message", "标签名称不能超过50个字符");
|
||||
return ServerResponse.badRequest().bodyValue(error);
|
||||
}
|
||||
|
||||
if (courseLabel.getColor() == null || courseLabel.getColor().isEmpty()) {
|
||||
courseLabel.setColor("#1890ff");
|
||||
}
|
||||
|
||||
return courseLabelService.create(courseLabel)
|
||||
.flatMap(label -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "标签创建成功");
|
||||
response.put("data", label);
|
||||
return ServerResponse.ok().bodyValue(response);
|
||||
})
|
||||
.onErrorResume(error -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", error.getMessage());
|
||||
return ServerResponse.badRequest().bodyValue(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "更新标签", description = "更新指定标签信息")
|
||||
public Mono<ServerResponse> updateLabel(ServerRequest request) {
|
||||
Long id = Long.valueOf(request.pathVariable("id"));
|
||||
|
||||
return request.bodyToMono(CourseLabel.class)
|
||||
.flatMap(courseLabel -> {
|
||||
if (courseLabel.getLabelName() != null && courseLabel.getLabelName().length() > 50) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("success", false);
|
||||
error.put("message", "标签名称不能超过50个字符");
|
||||
return ServerResponse.badRequest().bodyValue(error);
|
||||
}
|
||||
|
||||
return courseLabelService.update(id, courseLabel)
|
||||
.flatMap(label -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "标签更新成功");
|
||||
response.put("data", label);
|
||||
return ServerResponse.ok().bodyValue(response);
|
||||
})
|
||||
.onErrorResume(error -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", error.getMessage());
|
||||
return ServerResponse.badRequest().bodyValue(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "删除标签", description = "删除指定标签(软删除)")
|
||||
public Mono<ServerResponse> deleteLabel(ServerRequest request) {
|
||||
Long id = Long.valueOf(request.pathVariable("id"));
|
||||
|
||||
return courseLabelService.delete(id)
|
||||
.then(Mono.defer(() -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "标签删除成功");
|
||||
return ServerResponse.ok().bodyValue(response);
|
||||
}))
|
||||
.onErrorResume(error -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", error.getMessage());
|
||||
return ServerResponse.badRequest().bodyValue(response);
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "获取类型的标签", description = "获取指定团课类型的所有标签")
|
||||
public Mono<ServerResponse> getLabelsByTypeId(ServerRequest request) {
|
||||
Long typeId = Long.valueOf(request.pathVariable("typeId"));
|
||||
return courseLabelService.findByTypeId(typeId)
|
||||
.collectList()
|
||||
.flatMap(list -> ServerResponse.ok().bodyValue(list));
|
||||
}
|
||||
|
||||
@Operation(summary = "为类型添加标签", description = "为指定团课类型添加标签")
|
||||
public Mono<ServerResponse> addLabelsToType(ServerRequest request) {
|
||||
Long typeId = Long.valueOf(request.pathVariable("typeId"));
|
||||
|
||||
return request.bodyToMono(Map.class)
|
||||
.flatMap(body -> {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<Integer> labelIdsInt = (List<Integer>) body.get("labelIds");
|
||||
|
||||
if (labelIdsInt == null || labelIdsInt.isEmpty()) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("success", false);
|
||||
error.put("message", "labelIds不能为空");
|
||||
return ServerResponse.badRequest().bodyValue(error);
|
||||
}
|
||||
|
||||
List<Long> labelIds = labelIdsInt.stream()
|
||||
.map(Integer::longValue)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
|
||||
return courseLabelService.addLabelsToType(typeId, labelIds)
|
||||
.then(Mono.defer(() -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "标签添加成功");
|
||||
return ServerResponse.ok().bodyValue(response);
|
||||
}))
|
||||
.onErrorResume(error -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", error.getMessage());
|
||||
return ServerResponse.badRequest().bodyValue(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "从类型移除标签", description = "从指定团课类型移除标签")
|
||||
public Mono<ServerResponse> removeLabelFromType(ServerRequest request) {
|
||||
Long typeId = Long.valueOf(request.pathVariable("typeId"));
|
||||
Long labelId = Long.valueOf(request.pathVariable("labelId"));
|
||||
|
||||
return courseLabelService.removeLabelFromType(typeId, labelId)
|
||||
.then(Mono.defer(() -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "标签移除成功");
|
||||
return ServerResponse.ok().bodyValue(response);
|
||||
}))
|
||||
.onErrorResume(error -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", error.getMessage());
|
||||
return ServerResponse.badRequest().bodyValue(response);
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "清空类型标签", description = "清空指定团课类型的所有标签")
|
||||
public Mono<ServerResponse> clearLabelsFromType(ServerRequest request) {
|
||||
Long typeId = Long.valueOf(request.pathVariable("typeId"));
|
||||
|
||||
return courseLabelService.clearLabelsFromType(typeId)
|
||||
.then(Mono.defer(() -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "标签清空成功");
|
||||
return ServerResponse.ok().bodyValue(response);
|
||||
}))
|
||||
.onErrorResume(error -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", error.getMessage());
|
||||
return ServerResponse.badRequest().bodyValue(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
+9
@@ -4,6 +4,7 @@ package cn.novalon.gym.manage.groupcourse.handler;
|
||||
import cn.novalon.gym.manage.common.dto.PageRequest;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
|
||||
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -76,6 +77,14 @@ public class GroupCourseHandler {
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
|
||||
@Operation(summary = "根据ID获取团课完整信息", description = "根据ID获取团课完整信息,包括团课基础信息、类型信息和标签信息")
|
||||
public Mono<ServerResponse> getGroupCourseDetailById(ServerRequest request){
|
||||
Long id = Long.valueOf(request.pathVariable("id"));
|
||||
return groupCourseService.findDetailById(id)
|
||||
.flatMap(detail -> ServerResponse.ok().bodyValue(detail))
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
|
||||
@Operation(summary = "创建团课", description = "创建新的团课")
|
||||
public Mono<ServerResponse> createGroupCourse(ServerRequest request) {
|
||||
return request.bodyToMono(GroupCourse.class)
|
||||
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package cn.novalon.gym.manage.groupcourse.handler;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
|
||||
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseTypeService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.server.ServerRequest;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@Tag(name = "团课类型管理", description = "团课类型及难度相关操作")
|
||||
public class GroupCourseTypeHandler {
|
||||
|
||||
private final IGroupCourseTypeService groupCourseTypeService;
|
||||
|
||||
public GroupCourseTypeHandler(IGroupCourseTypeService groupCourseTypeService) {
|
||||
this.groupCourseTypeService = groupCourseTypeService;
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有团课类型", description = "获取系统中所有团课类型列表")
|
||||
public Mono<ServerResponse> getAllGroupCourseTypes(ServerRequest request) {
|
||||
boolean includeDeleted = Boolean.valueOf(request.queryParam("includeDeleted").orElse("false"));
|
||||
return ServerResponse.ok()
|
||||
.body(groupCourseTypeService.findAll(includeDeleted), GroupCourseType.class);
|
||||
}
|
||||
|
||||
@Operation(summary = "根据ID获取团课类型", description = "根据ID获取团课类型详情")
|
||||
public Mono<ServerResponse> getGroupCourseTypeById(ServerRequest request) {
|
||||
Long id = Long.valueOf(request.pathVariable("id"));
|
||||
return groupCourseTypeService.findById(id)
|
||||
.flatMap(type -> ServerResponse.ok().bodyValue(type))
|
||||
.switchIfEmpty(ServerResponse.notFound().build());
|
||||
}
|
||||
|
||||
@Operation(summary = "根据关键词搜索团课类型", description = "根据类型名称关键词搜索团课类型")
|
||||
public Mono<ServerResponse> searchGroupCourseTypes(ServerRequest request) {
|
||||
String keyword = request.queryParam("keyword").orElse("");
|
||||
return ServerResponse.ok()
|
||||
.body(groupCourseTypeService.findByKeyword(keyword), GroupCourseType.class);
|
||||
}
|
||||
|
||||
@Operation(summary = "根据分类获取团课类型", description = "根据分类获取团课类型列表")
|
||||
public Mono<ServerResponse> getGroupCourseTypesByCategory(ServerRequest request) {
|
||||
String category = request.pathVariable("category");
|
||||
String keyword = request.queryParam("keyword").orElse("");
|
||||
return ServerResponse.ok()
|
||||
.body(groupCourseTypeService.findByCategoryAndKeyword(category, keyword), GroupCourseType.class);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取所有分类", description = "获取所有团课类型分类(去重)")
|
||||
public Mono<ServerResponse> getCategories(ServerRequest request) {
|
||||
return groupCourseTypeService.findCategories()
|
||||
.collectList()
|
||||
.flatMap(list -> ServerResponse.ok().bodyValue(list));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建团课类型", description = "创建新的团课类型")
|
||||
public Mono<ServerResponse> createGroupCourseType(ServerRequest request) {
|
||||
return request.bodyToMono(GroupCourseType.class)
|
||||
.flatMap(groupCourseType -> {
|
||||
if (groupCourseType.getTypeName() == null || groupCourseType.getTypeName().isEmpty()) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("success", false);
|
||||
error.put("message", "类型名称不能为空");
|
||||
return ServerResponse.badRequest().bodyValue(error);
|
||||
}
|
||||
|
||||
// 默认基础难度为1
|
||||
if (groupCourseType.getBaseDifficulty() == null) {
|
||||
groupCourseType.setBaseDifficulty(1);
|
||||
}
|
||||
|
||||
return groupCourseTypeService.create(groupCourseType)
|
||||
.flatMap(type -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "团课类型创建成功");
|
||||
response.put("data", type);
|
||||
return ServerResponse.ok().bodyValue(response);
|
||||
})
|
||||
.onErrorResume(error -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", error.getMessage());
|
||||
return ServerResponse.badRequest().bodyValue(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "更新团课类型", description = "更新指定团课类型信息")
|
||||
public Mono<ServerResponse> updateGroupCourseType(ServerRequest request) {
|
||||
Long id = Long.valueOf(request.pathVariable("id"));
|
||||
|
||||
return request.bodyToMono(GroupCourseType.class)
|
||||
.flatMap(groupCourseType -> {
|
||||
groupCourseType.setId(id);
|
||||
return groupCourseTypeService.update(id, groupCourseType)
|
||||
.flatMap(type -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "团课类型更新成功");
|
||||
response.put("data", type);
|
||||
return ServerResponse.ok().bodyValue(response);
|
||||
})
|
||||
.onErrorResume(error -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", error.getMessage());
|
||||
return ServerResponse.badRequest().bodyValue(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Operation(summary = "删除团课类型", description = "删除指定团课类型(软删除)")
|
||||
public Mono<ServerResponse> deleteGroupCourseType(ServerRequest request) {
|
||||
Long id = Long.valueOf(request.pathVariable("id"));
|
||||
|
||||
return groupCourseTypeService.delete(id)
|
||||
.then(Mono.defer(() -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", true);
|
||||
response.put("message", "团课类型删除成功");
|
||||
return ServerResponse.ok().bodyValue(response);
|
||||
}))
|
||||
.onErrorResume(error -> {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("success", false);
|
||||
response.put("message", error.getMessage());
|
||||
return ServerResponse.badRequest().bodyValue(response);
|
||||
});
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package cn.novalon.gym.manage.groupcourse.repository;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ICourseLabelRepository {
|
||||
|
||||
Mono<CourseLabel> findById(Long id);
|
||||
|
||||
Flux<CourseLabel> findAll();
|
||||
|
||||
Flux<CourseLabel> findByKeyword(String keyword);
|
||||
|
||||
Mono<CourseLabel> findByLabelName(String labelName);
|
||||
|
||||
Mono<CourseLabel> save(CourseLabel courseLabel);
|
||||
|
||||
Mono<CourseLabel> update(CourseLabel courseLabel);
|
||||
|
||||
Mono<Void> deleteById(Long id);
|
||||
|
||||
Flux<CourseLabel> findByTypeId(Long typeId);
|
||||
|
||||
Mono<Void> addLabelsToType(Long typeId, List<Long> labelIds);
|
||||
|
||||
Mono<Void> removeLabelFromType(Long typeId, Long labelId);
|
||||
|
||||
Mono<Void> clearLabelsFromType(Long typeId);
|
||||
}
|
||||
+2
@@ -27,4 +27,6 @@ public interface IGroupCourseRepository {
|
||||
Mono<Void> deleteById(Long id);
|
||||
|
||||
Mono<GroupCourse> updateCurrentMembers(Long id, Integer delta);
|
||||
|
||||
Flux<GroupCourse> findByCourseType(Long courseType);
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package cn.novalon.gym.manage.groupcourse.repository;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface IGroupCourseTypeRepository {
|
||||
|
||||
Mono<GroupCourseType> findById(Long id);
|
||||
|
||||
Flux<GroupCourseType> findAll();
|
||||
|
||||
Flux<GroupCourseType> findAll(boolean includeDeleted);
|
||||
|
||||
Flux<GroupCourseType> findByKeyword(String keyword);
|
||||
|
||||
Flux<GroupCourseType> findByCategory(String category);
|
||||
|
||||
Flux<GroupCourseType> findByCategoryAndKeyword(String category, String keyword);
|
||||
|
||||
Mono<GroupCourseType> findByTypeName(String typeName);
|
||||
|
||||
Mono<GroupCourseType> save(GroupCourseType groupCourseType);
|
||||
|
||||
Mono<GroupCourseType> update(GroupCourseType groupCourseType);
|
||||
|
||||
Mono<Void> deleteById(Long id);
|
||||
}
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
package cn.novalon.gym.manage.groupcourse.repository.impl;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.converter.GroupCourseConverter;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.CourseLabelDao;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.CourseTypeLabelDao;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
|
||||
import cn.novalon.gym.manage.groupcourse.entity.CourseLabelEntity;
|
||||
import cn.novalon.gym.manage.groupcourse.entity.CourseTypeLabelEntity;
|
||||
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
@Transactional
|
||||
public class CourseLabelRepository implements ICourseLabelRepository {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CourseLabelRepository.class);
|
||||
|
||||
private final CourseLabelDao courseLabelDao;
|
||||
private final CourseTypeLabelDao courseTypeLabelDao;
|
||||
private final GroupCourseConverter converter;
|
||||
|
||||
public CourseLabelRepository(CourseLabelDao courseLabelDao, CourseTypeLabelDao courseTypeLabelDao,
|
||||
GroupCourseConverter converter) {
|
||||
this.courseLabelDao = courseLabelDao;
|
||||
this.courseTypeLabelDao = courseTypeLabelDao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CourseLabel> findById(Long id) {
|
||||
return courseLabelDao.findByIdIsAndDeletedAtIsNull(id)
|
||||
.map(this::toCourseLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CourseLabel> findAll() {
|
||||
return courseLabelDao.findAllByDeletedAtIsNull()
|
||||
.map(this::toCourseLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CourseLabel> findByKeyword(String keyword) {
|
||||
if (keyword == null || keyword.isEmpty()) {
|
||||
return findAll();
|
||||
}
|
||||
return courseLabelDao.findByLabelNameContainingAndDeletedAtIsNull(keyword)
|
||||
.map(this::toCourseLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CourseLabel> findByLabelName(String labelName) {
|
||||
return courseLabelDao.findByLabelNameAndDeletedAtIsNull(labelName)
|
||||
.map(this::toCourseLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CourseLabel> save(CourseLabel courseLabel) {
|
||||
CourseLabelEntity entity = toCourseLabelEntity(courseLabel);
|
||||
return courseLabelDao.save(entity)
|
||||
.map(this::toCourseLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CourseLabel> update(CourseLabel courseLabel) {
|
||||
return courseLabelDao.findByIdIsAndDeletedAtIsNull(courseLabel.getId())
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("标签不存在")))
|
||||
.flatMap(existing -> {
|
||||
existing.markNotNew();
|
||||
if (courseLabel.getLabelName() != null) {
|
||||
existing.setLabelName(courseLabel.getLabelName());
|
||||
}
|
||||
if (courseLabel.getColor() != null) {
|
||||
existing.setColor(courseLabel.getColor());
|
||||
}
|
||||
if (courseLabel.getDescription() != null) {
|
||||
existing.setDescription(courseLabel.getDescription());
|
||||
}
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
return courseLabelDao.save(existing);
|
||||
})
|
||||
.map(this::toCourseLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteById(Long id) {
|
||||
return courseLabelDao.softDelete(id, LocalDateTime.now())
|
||||
.then(courseTypeLabelDao.deleteByLabelId(id, LocalDateTime.now()))
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CourseLabel> findByTypeId(Long typeId) {
|
||||
return courseTypeLabelDao.findByTypeIdAndDeletedAtIsNull(typeId)
|
||||
.flatMap(typeLabel -> courseLabelDao.findByIdIsAndDeletedAtIsNull(typeLabel.getLabelId()))
|
||||
.map(this::toCourseLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> addLabelsToType(Long typeId, List<Long> labelIds) {
|
||||
return Flux.fromIterable(labelIds)
|
||||
.flatMap(labelId -> {
|
||||
return courseTypeLabelDao.physicalDeleteByTypeIdAndLabelId(typeId, labelId)
|
||||
.then(Mono.defer(() -> {
|
||||
CourseTypeLabelEntity entity = new CourseTypeLabelEntity();
|
||||
entity.setTypeId(typeId);
|
||||
entity.setLabelId(labelId);
|
||||
return courseTypeLabelDao.save(entity).then(Mono.empty());
|
||||
}));
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeLabelFromType(Long typeId, Long labelId) {
|
||||
return courseTypeLabelDao.deleteByTypeIdAndLabelId(typeId, labelId, LocalDateTime.now())
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> clearLabelsFromType(Long typeId) {
|
||||
return courseTypeLabelDao.deleteByTypeId(typeId, LocalDateTime.now())
|
||||
.then();
|
||||
}
|
||||
|
||||
private CourseLabel toCourseLabel(CourseLabelEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
CourseLabel label = new CourseLabel();
|
||||
label.setId(entity.getId());
|
||||
label.setLabelName(entity.getLabelName());
|
||||
label.setColor(entity.getColor());
|
||||
label.setDescription(entity.getDescription());
|
||||
label.setCreatedAt(entity.getCreatedAt());
|
||||
label.setUpdatedAt(entity.getUpdatedAt());
|
||||
return label;
|
||||
}
|
||||
|
||||
private CourseLabelEntity toCourseLabelEntity(CourseLabel domain) {
|
||||
if (domain == null) {
|
||||
return null;
|
||||
}
|
||||
CourseLabelEntity entity = new CourseLabelEntity();
|
||||
entity.setId(domain.getId());
|
||||
entity.setLabelName(domain.getLabelName());
|
||||
entity.setColor(domain.getColor());
|
||||
entity.setDescription(domain.getDescription());
|
||||
if (domain.getId() != null) {
|
||||
entity.markNotNew();
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
+6
@@ -178,4 +178,10 @@ public class GroupCourseRepository implements IGroupCourseRepository {
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourse> findByCourseType(Long courseType) {
|
||||
return groupCourseDao.findByCourseTypeAndDeletedAtIsNull(courseType)
|
||||
.map(groupCourseConverter::toDomain);
|
||||
}
|
||||
}
|
||||
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
package cn.novalon.gym.manage.groupcourse.repository.impl;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.converter.GroupCourseConverter;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseTypeDao;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
|
||||
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseTypeEntity;
|
||||
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Repository
|
||||
@Transactional
|
||||
public class GroupCourseTypeRepository implements IGroupCourseTypeRepository {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GroupCourseTypeRepository.class);
|
||||
|
||||
private final GroupCourseTypeDao groupCourseTypeDao;
|
||||
private final GroupCourseConverter converter;
|
||||
|
||||
public GroupCourseTypeRepository(GroupCourseTypeDao groupCourseTypeDao, GroupCourseConverter converter) {
|
||||
this.groupCourseTypeDao = groupCourseTypeDao;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GroupCourseType> findById(Long id) {
|
||||
return groupCourseTypeDao.findByIdIsAndDeletedAtIsNull(id)
|
||||
.map(converter::toGroupCourseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findAll() {
|
||||
return groupCourseTypeDao.findAll()
|
||||
.map(converter::toGroupCourseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findAll(boolean includeDeleted) {
|
||||
if (includeDeleted) {
|
||||
return groupCourseTypeDao.findAll()
|
||||
.map(converter::toGroupCourseType);
|
||||
} else {
|
||||
return groupCourseTypeDao.findAllByDeletedAtIsNull()
|
||||
.map(converter::toGroupCourseType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findByKeyword(String keyword) {
|
||||
if (keyword == null || keyword.isEmpty()) {
|
||||
return findAll(false);
|
||||
}
|
||||
return groupCourseTypeDao.findByTypeNameContainingAndDeletedAtIsNull(keyword)
|
||||
.map(converter::toGroupCourseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findByCategory(String category) {
|
||||
if (category == null || category.isEmpty()) {
|
||||
return findAll(false);
|
||||
}
|
||||
return groupCourseTypeDao.findByCategoryAndDeletedAtIsNull(category)
|
||||
.map(converter::toGroupCourseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findByCategoryAndKeyword(String category, String keyword) {
|
||||
Flux<GroupCourseType> result;
|
||||
|
||||
if (category != null && !category.isEmpty()) {
|
||||
result = findByCategory(category);
|
||||
} else {
|
||||
result = findAll(false);
|
||||
}
|
||||
|
||||
if (keyword != null && !keyword.isEmpty()) {
|
||||
result = result.filter(type -> type.getTypeName() != null &&
|
||||
type.getTypeName().toLowerCase().contains(keyword.toLowerCase()));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GroupCourseType> findByTypeName(String typeName) {
|
||||
return groupCourseTypeDao.findByTypeNameAndDeletedAtIsNull(typeName)
|
||||
.map(converter::toGroupCourseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GroupCourseType> save(GroupCourseType groupCourseType) {
|
||||
GroupCourseTypeEntity entity = converter.toGroupCourseTypeEntity(groupCourseType);
|
||||
return groupCourseTypeDao.save(entity)
|
||||
.map(converter::toGroupCourseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GroupCourseType> update(GroupCourseType groupCourseType) {
|
||||
return groupCourseTypeDao.findByIdIsAndDeletedAtIsNull(groupCourseType.getId())
|
||||
.switchIfEmpty(Mono.error(new RuntimeException("团课类型不存在")))
|
||||
.flatMap(existing -> {
|
||||
existing.markNotNew();
|
||||
if (groupCourseType.getTypeName() != null) {
|
||||
existing.setTypeName(groupCourseType.getTypeName());
|
||||
}
|
||||
if (groupCourseType.getBaseDifficulty() != null) {
|
||||
existing.setBaseDifficulty(groupCourseType.getBaseDifficulty());
|
||||
}
|
||||
if (groupCourseType.getDescription() != null) {
|
||||
existing.setDescription(groupCourseType.getDescription());
|
||||
}
|
||||
if (groupCourseType.getCategory() != null) {
|
||||
existing.setCategory(groupCourseType.getCategory());
|
||||
}
|
||||
existing.setUpdatedAt(LocalDateTime.now());
|
||||
return groupCourseTypeDao.save(existing);
|
||||
})
|
||||
.map(converter::toGroupCourseType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> deleteById(Long id) {
|
||||
return groupCourseTypeDao.softDelete(id, LocalDateTime.now())
|
||||
.then();
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package cn.novalon.gym.manage.groupcourse.service;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ICourseLabelService {
|
||||
|
||||
Mono<CourseLabel> findById(Long id);
|
||||
|
||||
Flux<CourseLabel> findAll();
|
||||
|
||||
Flux<CourseLabel> findByKeyword(String keyword);
|
||||
|
||||
Mono<CourseLabel> create(CourseLabel courseLabel);
|
||||
|
||||
Mono<CourseLabel> update(Long id, CourseLabel courseLabel);
|
||||
|
||||
Mono<Void> delete(Long id);
|
||||
|
||||
Flux<CourseLabel> findByTypeId(Long typeId);
|
||||
|
||||
Mono<Void> addLabelsToType(Long typeId, List<Long> labelIds);
|
||||
|
||||
Mono<Void> removeLabelFromType(Long typeId, Long labelId);
|
||||
|
||||
Mono<Void> clearLabelsFromType(Long typeId);
|
||||
}
|
||||
+2
@@ -4,11 +4,13 @@ package cn.novalon.gym.manage.groupcourse.service;
|
||||
import cn.novalon.gym.manage.common.dto.PageRequest;
|
||||
import cn.novalon.gym.manage.common.dto.PageResponse;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface IGroupCourseService {
|
||||
Mono<GroupCourse> findById(Long id);
|
||||
Mono<GroupCourseDetail> findDetailById(Long id);
|
||||
Flux<GroupCourse> findAll();
|
||||
Flux<GroupCourse> findAll(boolean includeDeleted);
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
package cn.novalon.gym.manage.groupcourse.service;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface IGroupCourseTypeService {
|
||||
|
||||
Mono<GroupCourseType> findById(Long id);
|
||||
|
||||
Flux<GroupCourseType> findAll();
|
||||
|
||||
Flux<GroupCourseType> findAll(boolean includeDeleted);
|
||||
|
||||
Flux<GroupCourseType> findByKeyword(String keyword);
|
||||
|
||||
Flux<GroupCourseType> findByCategory(String category);
|
||||
|
||||
Flux<GroupCourseType> findByCategoryAndKeyword(String category, String keyword);
|
||||
|
||||
Mono<GroupCourseType> create(GroupCourseType groupCourseType);
|
||||
|
||||
Mono<GroupCourseType> update(Long id, GroupCourseType groupCourseType);
|
||||
|
||||
Mono<Void> delete(Long id);
|
||||
|
||||
/**
|
||||
* 获取分类列表(去重)
|
||||
* @return 分类名称列表
|
||||
*/
|
||||
Flux<String> findCategories();
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
package cn.novalon.gym.manage.groupcourse.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
|
||||
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
|
||||
import cn.novalon.gym.manage.groupcourse.service.ICourseLabelService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class CourseLabelService implements ICourseLabelService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CourseLabelService.class);
|
||||
private static final String CACHE_KEY_DETAIL_PREFIX = "group_course:detail:";
|
||||
|
||||
private final ICourseLabelRepository courseLabelRepository;
|
||||
private final IGroupCourseRepository groupCourseRepository;
|
||||
private final RedisUtil redisUtil;
|
||||
|
||||
public CourseLabelService(ICourseLabelRepository courseLabelRepository,
|
||||
IGroupCourseRepository groupCourseRepository,
|
||||
RedisUtil redisUtil) {
|
||||
this.courseLabelRepository = courseLabelRepository;
|
||||
this.groupCourseRepository = groupCourseRepository;
|
||||
this.redisUtil = redisUtil;
|
||||
}
|
||||
|
||||
private Mono<Void> invalidateGroupCourseDetailCache(Long typeId) {
|
||||
return groupCourseRepository.findByCourseType(typeId)
|
||||
.flatMap(course -> {
|
||||
String cacheKey = CACHE_KEY_DETAIL_PREFIX + course.getId();
|
||||
return redisUtil.delete(cacheKey)
|
||||
.doOnSuccess(deleted -> logger.debug("清除团课详情缓存 - courseId={}", course.getId()));
|
||||
})
|
||||
.then();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CourseLabel> findById(Long id) {
|
||||
return courseLabelRepository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CourseLabel> findAll() {
|
||||
return courseLabelRepository.findAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CourseLabel> findByKeyword(String keyword) {
|
||||
return courseLabelRepository.findByKeyword(keyword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CourseLabel> create(CourseLabel courseLabel) {
|
||||
return courseLabelRepository.findByLabelName(courseLabel.getLabelName())
|
||||
.flatMap(existing -> Mono.<CourseLabel>error(new RuntimeException("标签名称已存在")))
|
||||
.switchIfEmpty(courseLabelRepository.save(courseLabel))
|
||||
.doOnSuccess(label -> logger.info("标签创建成功 - id={}, name={}", label.getId(), label.getLabelName()))
|
||||
.doOnError(error -> logger.error("标签创建失败 - error: {}", error.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<CourseLabel> update(Long id, CourseLabel courseLabel) {
|
||||
courseLabel.setId(id);
|
||||
return courseLabelRepository.update(courseLabel)
|
||||
.doOnSuccess(label -> logger.info("标签更新成功 - id={}", id))
|
||||
.doOnError(error -> logger.error("标签更新失败 - id={}, error: {}", id, error.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete(Long id) {
|
||||
return courseLabelRepository.deleteById(id)
|
||||
.doOnSuccess(v -> logger.info("标签删除成功 - id={}", id))
|
||||
.doOnError(error -> logger.error("标签删除失败 - id={}, error: {}", id, error.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<CourseLabel> findByTypeId(Long typeId) {
|
||||
return courseLabelRepository.findByTypeId(typeId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> addLabelsToType(Long typeId, List<Long> labelIds) {
|
||||
return courseLabelRepository.addLabelsToType(typeId, labelIds)
|
||||
.then(invalidateGroupCourseDetailCache(typeId))
|
||||
.doOnSuccess(v -> logger.info("标签添加到类型成功 - typeId={}, labelIds={}", typeId, labelIds))
|
||||
.doOnError(error -> logger.error("标签添加到类型失败 - typeId={}, error: {}", typeId, error.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> removeLabelFromType(Long typeId, Long labelId) {
|
||||
return courseLabelRepository.removeLabelFromType(typeId, labelId)
|
||||
.then(invalidateGroupCourseDetailCache(typeId))
|
||||
.doOnSuccess(v -> logger.info("从类型移除标签成功 - typeId={}, labelId={}", typeId, labelId))
|
||||
.doOnError(error -> logger.error("从类型移除标签失败 - typeId={}, labelId={}, error: {}", typeId, labelId, error.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> clearLabelsFromType(Long typeId) {
|
||||
return courseLabelRepository.clearLabelsFromType(typeId)
|
||||
.then(invalidateGroupCourseDetailCache(typeId))
|
||||
.doOnSuccess(v -> logger.info("清空类型标签成功 - typeId={}", typeId))
|
||||
.doOnError(error -> logger.error("清空类型标签失败 - typeId={}, error: {}", typeId, error.getMessage()));
|
||||
}
|
||||
}
|
||||
+101
-1
@@ -4,13 +4,18 @@ package cn.novalon.gym.manage.groupcourse.service.impl;
|
||||
import cn.novalon.gym.manage.common.dto.PageRequest;
|
||||
import cn.novalon.gym.manage.common.dto.PageResponse;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.CourseLabel;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseBooking;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseDetail;
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
|
||||
import cn.novalon.gym.manage.groupcourse.enums.CourseEvent;
|
||||
import cn.novalon.gym.manage.groupcourse.enums.CourseStatus;
|
||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseStateMachine;
|
||||
import cn.novalon.gym.manage.groupcourse.repository.ICourseLabelRepository;
|
||||
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseBookingRepository;
|
||||
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
|
||||
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
|
||||
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCard;
|
||||
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
|
||||
@@ -33,6 +38,8 @@ public class GroupCourseService implements IGroupCourseService {
|
||||
|
||||
private final IGroupCourseRepository groupCourseRepository;
|
||||
private final IGroupCourseBookingRepository bookingRepository;
|
||||
private final IGroupCourseTypeRepository groupCourseTypeRepository;
|
||||
private final ICourseLabelRepository courseLabelRepository;
|
||||
private final IMemberCardRecordService memberCardRecordService;
|
||||
private final MemberCardRepository memberCardRepository;
|
||||
private final RedisUtil redisUtil;
|
||||
@@ -41,12 +48,15 @@ public class GroupCourseService implements IGroupCourseService {
|
||||
|
||||
private static final String CACHE_KEY_PREFIX = "group_course:page:";
|
||||
private static final String CACHE_KEY_ID_PREFIX = "group_course:id:";
|
||||
private static final String CACHE_KEY_DETAIL_PREFIX = "group_course:detail:";
|
||||
private static final long CACHE_EXPIRE_SECONDS = 300;
|
||||
|
||||
private static final double DEFAULT_GROUP_COURSE_PRICE = 50.0;
|
||||
|
||||
public GroupCourseService(IGroupCourseRepository groupCourseRepository,
|
||||
IGroupCourseBookingRepository bookingRepository,
|
||||
IGroupCourseTypeRepository groupCourseTypeRepository,
|
||||
ICourseLabelRepository courseLabelRepository,
|
||||
IMemberCardRecordService memberCardRecordService,
|
||||
MemberCardRepository memberCardRepository,
|
||||
RedisUtil redisUtil,
|
||||
@@ -54,6 +64,8 @@ public class GroupCourseService implements IGroupCourseService {
|
||||
GroupCourseStateMachine stateMachine){
|
||||
this.groupCourseRepository = groupCourseRepository;
|
||||
this.bookingRepository = bookingRepository;
|
||||
this.groupCourseTypeRepository = groupCourseTypeRepository;
|
||||
this.courseLabelRepository = courseLabelRepository;
|
||||
this.memberCardRecordService = memberCardRecordService;
|
||||
this.memberCardRepository = memberCardRepository;
|
||||
this.redisUtil = redisUtil;
|
||||
@@ -61,6 +73,93 @@ public class GroupCourseService implements IGroupCourseService {
|
||||
this.stateMachine = stateMachine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GroupCourseDetail> findDetailById(Long id) {
|
||||
String cacheKey = CACHE_KEY_DETAIL_PREFIX + id;
|
||||
|
||||
return redisUtil.get(cacheKey, String.class)
|
||||
.flatMap(cachedJson -> {
|
||||
if (cachedJson != null) {
|
||||
try {
|
||||
GroupCourseDetail detail = objectMapper.readValue(cachedJson, GroupCourseDetail.class);
|
||||
logger.info("缓存命中 - findDetailById: id={}", id);
|
||||
return Mono.just(detail);
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
|
||||
return redisUtil.delete(cacheKey).then(Mono.empty());
|
||||
}
|
||||
}
|
||||
return Mono.empty();
|
||||
})
|
||||
.switchIfEmpty(
|
||||
groupCourseRepository.findByIdAndDeletedAtIsNull(id)
|
||||
.flatMap(course -> {
|
||||
// 查询类型信息
|
||||
Long courseTypeId = course.getCourseType();
|
||||
|
||||
if (courseTypeId == null) {
|
||||
// 没有类型,直接构建详情
|
||||
return Mono.just(buildDetail(course, null));
|
||||
}
|
||||
|
||||
// 有类型,查询类型信息
|
||||
return groupCourseTypeRepository.findById(courseTypeId)
|
||||
.flatMap(type -> {
|
||||
// 查询标签
|
||||
return courseLabelRepository.findByTypeId(type.getId())
|
||||
.collectList()
|
||||
.map(labels -> {
|
||||
type.setLabels(labels);
|
||||
return buildDetail(course, type);
|
||||
});
|
||||
})
|
||||
.switchIfEmpty(Mono.just(buildDetail(course, null)));
|
||||
})
|
||||
.flatMap(detail -> {
|
||||
try {
|
||||
String jsonData = objectMapper.writeValueAsString(detail);
|
||||
return redisUtil.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS)
|
||||
.thenReturn(detail)
|
||||
.doOnSuccess(d -> logger.debug("缓存已设置 - findDetailById: id={}", id));
|
||||
} catch (JsonProcessingException e) {
|
||||
logger.error("缓存设置失败 - id: {}, error: {}", id, e.getMessage());
|
||||
return Mono.just(detail);
|
||||
}
|
||||
})
|
||||
.doOnSubscribe(sub -> logger.debug("缓存未命中,查询数据库 - findDetailById: id={}", id))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建团课完整信息对象
|
||||
*/
|
||||
private GroupCourseDetail buildDetail(GroupCourse course, GroupCourseType type) {
|
||||
GroupCourseDetail detail = new GroupCourseDetail();
|
||||
detail.setId(course.getId());
|
||||
detail.setCourseName(course.getCourseName());
|
||||
detail.setCoachId(course.getCoachId());
|
||||
detail.setCourseType(course.getCourseType());
|
||||
detail.setStartTime(course.getStartTime());
|
||||
detail.setEndTime(course.getEndTime());
|
||||
detail.setMaxMembers(course.getMaxMembers());
|
||||
detail.setCurrentMembers(course.getCurrentMembers());
|
||||
detail.setStatus(course.getStatus());
|
||||
detail.setLocation(course.getLocation());
|
||||
detail.setCoverImage(course.getCoverImage());
|
||||
detail.setDescription(course.getDescription());
|
||||
detail.setPointCardAmount(course.getPointCardAmount());
|
||||
detail.setStoredValueAmount(course.getStoredValueAmount());
|
||||
detail.setCreatedAt(course.getCreatedAt());
|
||||
detail.setUpdatedAt(course.getUpdatedAt());
|
||||
|
||||
// 设置类型信息
|
||||
if (type != null) {
|
||||
detail.setTypeInfo(type);
|
||||
}
|
||||
|
||||
return detail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GroupCourse> findById(Long id) {
|
||||
String cacheKey = CACHE_KEY_ID_PREFIX + id;
|
||||
@@ -391,6 +490,7 @@ public class GroupCourseService implements IGroupCourseService {
|
||||
|
||||
private Mono<Void> clearCache() {
|
||||
return redisUtil.deleteByPattern(CACHE_KEY_PREFIX + "*")
|
||||
.then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*")).then();
|
||||
.then(redisUtil.deleteByPattern(CACHE_KEY_ID_PREFIX + "*"))
|
||||
.then(redisUtil.deleteByPattern(CACHE_KEY_DETAIL_PREFIX + "*")).then();
|
||||
}
|
||||
}
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package cn.novalon.gym.manage.groupcourse.service.impl;
|
||||
|
||||
import cn.novalon.gym.manage.groupcourse.domain.GroupCourseType;
|
||||
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseTypeRepository;
|
||||
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseTypeService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class GroupCourseTypeService implements IGroupCourseTypeService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GroupCourseTypeService.class);
|
||||
|
||||
private final IGroupCourseTypeRepository groupCourseTypeRepository;
|
||||
|
||||
public GroupCourseTypeService(IGroupCourseTypeRepository groupCourseTypeRepository) {
|
||||
this.groupCourseTypeRepository = groupCourseTypeRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GroupCourseType> findById(Long id) {
|
||||
return groupCourseTypeRepository.findById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findAll() {
|
||||
return groupCourseTypeRepository.findAll(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findAll(boolean includeDeleted) {
|
||||
return groupCourseTypeRepository.findAll(includeDeleted);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findByKeyword(String keyword) {
|
||||
return groupCourseTypeRepository.findByKeyword(keyword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findByCategory(String category) {
|
||||
return groupCourseTypeRepository.findByCategory(category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<GroupCourseType> findByCategoryAndKeyword(String category, String keyword) {
|
||||
return groupCourseTypeRepository.findByCategoryAndKeyword(category, keyword);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GroupCourseType> create(GroupCourseType groupCourseType) {
|
||||
return groupCourseTypeRepository.findByTypeName(groupCourseType.getTypeName())
|
||||
.flatMap(existing -> Mono.<GroupCourseType>error(new RuntimeException("团课类型名称已存在")))
|
||||
.switchIfEmpty(groupCourseTypeRepository.save(groupCourseType))
|
||||
.doOnSuccess(type -> logger.info("团课类型创建成功 - id={}, name={}", type.getId(), type.getTypeName()))
|
||||
.doOnError(error -> logger.error("团课类型创建失败 - error: {}", error.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<GroupCourseType> update(Long id, GroupCourseType groupCourseType) {
|
||||
return groupCourseTypeRepository.update(groupCourseType)
|
||||
.doOnSuccess(type -> logger.info("团课类型更新成功 - id={}", id))
|
||||
.doOnError(error -> logger.error("团课类型更新失败 - id={}, error: {}", id, error.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> delete(Long id) {
|
||||
return groupCourseTypeRepository.deleteById(id)
|
||||
.doOnSuccess(v -> logger.info("团课类型删除成功 - id={}", id))
|
||||
.doOnError(error -> logger.error("团课类型删除失败 - id={}, error: {}", id, error.getMessage()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<String> findCategories() {
|
||||
return groupCourseTypeRepository.findAll(false)
|
||||
.map(GroupCourseType::getCategory)
|
||||
.filter(category -> category != null && !category.isEmpty())
|
||||
.distinct();
|
||||
}
|
||||
}
|
||||
+223
-200
@@ -1,200 +1,223 @@
|
||||
2026-06-02T14:56:05.966+08:00 TRACE 22380 --- [gym-manage-api] [ctor-http-nio-7] o.s.w.s.adapter.HttpWebHandlerAdapter : [7ce54381-10] HTTP POST "/api/groupCourse/11/cancel", headers={masked}
|
||||
2026-06-02T14:56:05.967+08:00 DEBUG 22380 --- [gym-manage-api] [ parallel-11] o.s.w.s.s.DefaultWebSessionManager : Created new WebSession.
|
||||
2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: POST /api/groupCourse/11/cancel
|
||||
2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: POST /api/groupCourse/11/cancel
|
||||
2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: POST /api/groupCourse/11/cancel
|
||||
2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: POST /api/groupCourse/11/cancel
|
||||
2026-06-02T14:56:05.967+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.967+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.967+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dictionaries" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/change-password" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/logical-delete" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/logical-delete" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/action/restore" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/restore" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/roles" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/menus" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles/{id}/restore" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles/{id}/permissions" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/config" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/login" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/exception" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/operation" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/login" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/register" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/logout" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dict/types" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dict/data" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/notices" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/messages" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/files/upload" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/permissions" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/auth/miniapp/login" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/auth/mp/callback" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/phone/bind" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/admin/member/{id}/phone" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-cards" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/purchase" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/renew" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/use" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/refund" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/process-expired" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-transactions" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/page" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse" does not match against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/{id}/cancel" matches against value "/api/groupCourse/11/cancel"
|
||||
2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.function.server.RouterFunctions : [7ce54381-10] Matched (POST && /api/groupCourse/{id}/cancel)
|
||||
2026-06-02T14:56:05.970+08:00 DEBUG 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.s.s.RouterFunctionMapping : [7ce54381-10] Mapped to cn.novalon.gym.manage.app.config.SystemRouter$$Lambda/0x00000198019592d8@6903bec9
|
||||
2026-06-02T14:56:05.972+08:00 DEBUG 22380 --- [gym-manage-api] [ parallel-11] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT group_course.course_name, group_course.coach_id, group_course.course_type, group_course.start_time, group_course.end_time, group_course.max_members, group_course.current_members, group_course.status, group_course.location, group_course.cover_image, group_course.description, group_course.point_card_amount, group_course.stored_value_amount, group_course.id, group_course.create_by, group_course.update_by, group_course.created_at, group_course.updated_at, group_course.deleted_at FROM group_course WHERE group_course.id = $1 AND (group_course.deleted_at IS NULL)]
|
||||
2026-06-02T14:56:05.976+08:00 INFO 22380 --- [gym-manage-api] [actor-tcp-nio-5] c.n.g.m.g.c.GroupCourseConverter : 转换bean,entity-domain:
|
||||
2026-06-02T14:56:05.976+08:00 ERROR 22380 --- [gym-manage-api] [actor-tcp-nio-5] c.n.g.m.g.s.impl.GroupCourseService : 团课取消失败 - id=11, error: 课程取消需提前24小时
|
||||
2026-06-02T14:56:05.976+08:00 TRACE 22380 --- [gym-manage-api] [actor-tcp-nio-5] org.springframework.web.HttpLogging : [7ce54381-10] Encoding [{success=false, message=课程取消需提前24小时}]
|
||||
2026-06-02T14:56:05.976+08:00 TRACE 22380 --- [gym-manage-api] [actor-tcp-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [7ce54381-10] Completed 400 BAD_REQUEST, headers={masked}
|
||||
2026-06-02T14:56:05.976+08:00 TRACE 22380 --- [gym-manage-api] [actor-tcp-nio-5] org.springframework.web.HttpLogging : [7ce54381-1, L:/[0:0:0:0:0:0:0:1]:8084 - R:/[0:0:0:0:0:0:0:1]:57674] Handling completed
|
||||
2026-06-11T13:43:44.371+08:00 TRACE 9324 --- [gym-manage-api] [nio-8084-exec-2] o.s.w.s.adapter.HttpWebHandlerAdapter : [27afd417] HTTP POST "/api/groupCourse/types/1/labels", headers={masked}
|
||||
2026-06-11T13:43:44.372+08:00 DEBUG 9324 --- [gym-manage-api] [ parallel-2] o.s.w.s.s.DefaultWebSessionManager : Created new WebSession.
|
||||
2026-06-11T13:43:44.372+08:00 INFO 9324 --- [gym-manage-api] [ parallel-2] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: POST /api/groupCourse/types/1/labels
|
||||
2026-06-11T13:43:44.372+08:00 INFO 9324 --- [gym-manage-api] [ parallel-2] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: POST /api/groupCourse/types/1/labels
|
||||
2026-06-11T13:43:44.372+08:00 INFO 9324 --- [gym-manage-api] [ parallel-2] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: POST /api/groupCourse/types/1/labels
|
||||
2026-06-11T13:43:44.372+08:00 INFO 9324 --- [gym-manage-api] [ parallel-2] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: POST /api/groupCourse/types/1/labels
|
||||
2026-06-11T13:43:44.372+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.372+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.372+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.372+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.372+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dictionaries" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/change-password" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/logical-delete" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/logical-delete" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/action/restore" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/restore" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/roles" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/menus" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles/{id}/restore" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles/{id}/permissions" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/config" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.373+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/login" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/exception" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/operation" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/login" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/register" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/logout" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dict/types" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dict/data" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/notices" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/messages" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/files/upload" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/permissions" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/auth/miniapp/login" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/auth/mp/callback" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/phone/bind" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/admin/member/{id}/phone" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.375+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-cards" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/purchase" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/renew" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/use" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/refund" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/process-expired" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-transactions" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/page" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/types" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/labels" does not match against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/types/{typeId}/labels" matches against value "/api/groupCourse/types/1/labels"
|
||||
2026-06-11T13:43:44.376+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.function.server.RouterFunctions : [27afd417] Matched (POST && /api/groupCourse/types/{typeId}/labels)
|
||||
2026-06-11T13:43:44.376+08:00 DEBUG 9324 --- [gym-manage-api] [ parallel-2] o.s.w.r.f.s.s.RouterFunctionMapping : [27afd417] Mapped to cn.novalon.gym.manage.app.config.SystemRouter$$Lambda/0x00000223a09d1920@3760f3e8
|
||||
2026-06-11T13:43:44.377+08:00 TRACE 9324 --- [gym-manage-api] [ parallel-2] org.springframework.web.HttpLogging : [27afd417] Decoded [{labelIds=[1, 3, 5]}]
|
||||
2026-06-11T13:43:44.377+08:00 DEBUG 9324 --- [gym-manage-api] [ parallel-2] o.s.r.c.R2dbcTransactionManager : Creating new transaction with name [cn.novalon.gym.manage.groupcourse.repository.impl.CourseLabelRepository.addLabelsToType]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
|
||||
2026-06-11T13:43:44.377+08:00 DEBUG 9324 --- [gym-manage-api] [ parallel-2] o.s.r.c.R2dbcTransactionManager : Acquired Connection [PooledConnection[PostgresqlConnection{client=io.r2dbc.postgresql.client.ReactorNettyClient@c3a5202, codecs=io.r2dbc.postgresql.codec.DefaultCodecs@922d1d4}]] for R2DBC transaction
|
||||
2026-06-11T13:43:44.377+08:00 DEBUG 9324 --- [gym-manage-api] [ parallel-2] o.s.r.c.R2dbcTransactionManager : Starting R2DBC transaction on Connection [PooledConnection[PostgresqlConnection{client=io.r2dbc.postgresql.client.ReactorNettyClient@c3a5202, codecs=io.r2dbc.postgresql.codec.DefaultCodecs@922d1d4}]] using [ExtendedTransactionDefinition [transactionName='cn.novalon.gym.manage.groupcourse.repository.impl.CourseLabelRepository.addLabelsToType', readOnly=false, isolationLevel=null, lockWaitTimeout=PT0S]]
|
||||
2026-06-11T13:43:44.379+08:00 DEBUG 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT course_type_label.type_id, course_type_label.label_id, course_type_label.id, course_type_label.create_by, course_type_label.update_by, course_type_label.created_at, course_type_label.updated_at, course_type_label.deleted_at FROM course_type_label WHERE course_type_label.type_id = $1 AND (course_type_label.label_id = $2) AND (course_type_label.deleted_at IS NULL)]
|
||||
2026-06-11T13:43:44.381+08:00 DEBUG 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT course_type_label.type_id, course_type_label.label_id, course_type_label.id, course_type_label.create_by, course_type_label.update_by, course_type_label.created_at, course_type_label.updated_at, course_type_label.deleted_at FROM course_type_label WHERE course_type_label.type_id = $1 AND (course_type_label.label_id = $2) AND (course_type_label.deleted_at IS NULL)]
|
||||
2026-06-11T13:43:44.382+08:00 DEBUG 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT course_type_label.type_id, course_type_label.label_id, course_type_label.id, course_type_label.create_by, course_type_label.update_by, course_type_label.created_at, course_type_label.updated_at, course_type_label.deleted_at FROM course_type_label WHERE course_type_label.type_id = $1 AND (course_type_label.label_id = $2) AND (course_type_label.deleted_at IS NULL)]
|
||||
2026-06-11T13:43:44.382+08:00 DEBUG 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [INSERT INTO course_type_label (type_id, label_id, create_by, update_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)]
|
||||
2026-06-11T13:43:44.385+08:00 DEBUG 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [INSERT INTO course_type_label (type_id, label_id, create_by, update_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)]
|
||||
2026-06-11T13:43:44.385+08:00 DEBUG 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [INSERT INTO course_type_label (type_id, label_id, create_by, update_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)]
|
||||
2026-06-11T13:43:44.387+08:00 DEBUG 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.r.c.R2dbcTransactionManager : Initiating transaction rollback
|
||||
2026-06-11T13:43:44.387+08:00 DEBUG 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.r.c.R2dbcTransactionManager : Rolling back R2DBC transaction on Connection [PooledConnection[PostgresqlConnection{client=io.r2dbc.postgresql.client.ReactorNettyClient@c3a5202, codecs=io.r2dbc.postgresql.codec.DefaultCodecs@922d1d4}]]
|
||||
2026-06-11T13:43:44.389+08:00 DEBUG 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.r.c.R2dbcTransactionManager : Releasing R2DBC Connection [PooledConnection[PostgresqlConnection{client=io.r2dbc.postgresql.client.ReactorNettyClient@c3a5202, codecs=io.r2dbc.postgresql.codec.DefaultCodecs@922d1d4}]] after transaction
|
||||
2026-06-11T13:43:44.389+08:00 ERROR 9324 --- [gym-manage-api] [actor-tcp-nio-6] c.n.g.m.g.s.impl.CourseLabelService : 标签添加到类型失败 - typeId=1, error: executeMany; SQL [INSERT INTO course_type_label (type_id, label_id, create_by, update_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)]; 重复键违反唯一约束"course_type_label_type_id_label_id_key"
|
||||
2026-06-11T13:43:44.389+08:00 TRACE 9324 --- [gym-manage-api] [actor-tcp-nio-6] org.springframework.web.HttpLogging : [27afd417] Encoding [{success=false, message=executeMany; SQL [INSERT INTO course_type_label (type_id, label_id, create_by, update_by, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)]; 重复键违反唯一约束"course_type_label_type_id_label_id_key"}]
|
||||
2026-06-11T13:43:44.390+08:00 TRACE 9324 --- [gym-manage-api] [actor-tcp-nio-6] o.s.w.s.adapter.HttpWebHandlerAdapter : [27afd417] Completed 400 BAD_REQUEST, headers={masked}
|
||||
2026-06-11T13:43:44.390+08:00 TRACE 9324 --- [gym-manage-api] [actor-tcp-nio-6] org.springframework.web.HttpLogging : [27afd417] onComplete
|
||||
|
||||
@@ -48,6 +48,11 @@
|
||||
<artifactId>gym-checkIn</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-dataCount</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
+50
-7
@@ -2,9 +2,12 @@ package cn.novalon.gym.manage.app.config;
|
||||
|
||||
|
||||
import cn.novalon.gym.manage.checkIn.handler.CheckInHandler;
|
||||
import cn.novalon.gym.manage.datacount.handler.DataStatisticsHandler;
|
||||
import cn.novalon.gym.manage.file.handler.SysFileHandler;
|
||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler;
|
||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler;
|
||||
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseTypeHandler;
|
||||
import cn.novalon.gym.manage.groupcourse.handler.CourseLabelHandler;
|
||||
import cn.novalon.gym.manage.member.handler.MemberCardHandler;
|
||||
import cn.novalon.gym.manage.member.handler.MemberCardRecordHandler;
|
||||
import cn.novalon.gym.manage.member.handler.MemberCardTransactionHandler;
|
||||
@@ -68,7 +71,10 @@ public class SystemRouter {
|
||||
MemberCardTransactionHandler memberCardTransactionHandler,
|
||||
GroupCourseHandler groupCourseHandler,
|
||||
GroupCourseBookingHandler groupCourseBookingHandler,
|
||||
CheckInHandler checkInHandler) {
|
||||
GroupCourseTypeHandler groupCourseTypeHandler,
|
||||
CourseLabelHandler courseLabelHandler,
|
||||
CheckInHandler checkInHandler,
|
||||
DataStatisticsHandler dataStatisticsHandler) {
|
||||
|
||||
return route()
|
||||
// ========== 诊断路由 ==========
|
||||
@@ -263,12 +269,28 @@ public class SystemRouter {
|
||||
// ===== 团课课程管理 =====
|
||||
.GET("/api/groupCourse/list", groupCourseHandler::getAllGroupCourse)
|
||||
.POST("/api/groupCourse/page", groupCourseHandler::getGroupCoursesByPage)
|
||||
.GET("/api/groupCourse/{id}", groupCourseHandler::getGroupCourseById)
|
||||
.POST("/api/groupCourse", groupCourseHandler::createGroupCourse)
|
||||
.PUT("/api/groupCourse/{id}", groupCourseHandler::updateGroupCourse)
|
||||
.DELETE("/api/groupCourse/{id}", groupCourseHandler::deleteGroupCourse)
|
||||
.POST("/api/groupCourse/{id}/cancel", groupCourseHandler::cancelGroupCourse)
|
||||
.POST("/api/groupCourse/{courseId}/signin", groupCourseHandler::signIn)
|
||||
|
||||
// ===== 团课类型管理 =====
|
||||
.GET("/api/groupCourse/types", groupCourseTypeHandler::getAllGroupCourseTypes)
|
||||
.GET("/api/groupCourse/types/search", groupCourseTypeHandler::searchGroupCourseTypes)
|
||||
.GET("/api/groupCourse/types/categories", groupCourseTypeHandler::getCategories)
|
||||
.GET("/api/groupCourse/types/category/{category}", groupCourseTypeHandler::getGroupCourseTypesByCategory)
|
||||
.GET("/api/groupCourse/types/{id}", groupCourseTypeHandler::getGroupCourseTypeById)
|
||||
.POST("/api/groupCourse/types", groupCourseTypeHandler::createGroupCourseType)
|
||||
.PUT("/api/groupCourse/types/{id}", groupCourseTypeHandler::updateGroupCourseType)
|
||||
.DELETE("/api/groupCourse/types/{id}", groupCourseTypeHandler::deleteGroupCourseType)
|
||||
|
||||
// ===== 团课标签管理 =====
|
||||
.GET("/api/groupCourse/labels", courseLabelHandler::getAllLabels)
|
||||
.GET("/api/groupCourse/labels/search", courseLabelHandler::searchLabels)
|
||||
.GET("/api/groupCourse/labels/{id}", courseLabelHandler::getLabelById)
|
||||
.GET("/api/groupCourse/types/{typeId}/labels", courseLabelHandler::getLabelsByTypeId)
|
||||
.POST("/api/groupCourse/labels", courseLabelHandler::createLabel)
|
||||
.PUT("/api/groupCourse/labels/{id}", courseLabelHandler::updateLabel)
|
||||
.DELETE("/api/groupCourse/labels/{id}", courseLabelHandler::deleteLabel)
|
||||
.POST("/api/groupCourse/types/{typeId}/labels", courseLabelHandler::addLabelsToType)
|
||||
.DELETE("/api/groupCourse/types/{typeId}/labels/{labelId}", courseLabelHandler::removeLabelFromType)
|
||||
.DELETE("/api/groupCourse/types/{typeId}/labels", courseLabelHandler::clearLabelsFromType)
|
||||
|
||||
// ===== 团课预约管理 =====
|
||||
.POST("/api/groupCourse/book", groupCourseBookingHandler::bookCourse)
|
||||
@@ -277,6 +299,15 @@ public class SystemRouter {
|
||||
.GET("/api/groupCourse/bookings/course/{courseId}", groupCourseBookingHandler::getBookingsByCourseId)
|
||||
.GET("/api/groupCourse/bookings/{bookingId}", groupCourseBookingHandler::getBookingById)
|
||||
|
||||
// ===== 团课课程管理(需要放在具体路由之后)=====
|
||||
.GET("/api/groupCourse/{id}", groupCourseHandler::getGroupCourseById)
|
||||
.GET("/api/groupCourse/{id}/detail", groupCourseHandler::getGroupCourseDetailById)
|
||||
.POST("/api/groupCourse", groupCourseHandler::createGroupCourse)
|
||||
.PUT("/api/groupCourse/{id}", groupCourseHandler::updateGroupCourse)
|
||||
.DELETE("/api/groupCourse/{id}", groupCourseHandler::deleteGroupCourse)
|
||||
.POST("/api/groupCourse/{id}/cancel", groupCourseHandler::cancelGroupCourse)
|
||||
.POST("/api/groupCourse/{courseId}/signin", groupCourseHandler::signIn)
|
||||
|
||||
// ========= 签到模块路由 ==========
|
||||
// ===== 签到核心功能 =====
|
||||
.POST("/api/checkIn", checkInHandler::checkIn)
|
||||
@@ -292,6 +323,18 @@ public class SystemRouter {
|
||||
|
||||
// ===== 签到数据导出 =====
|
||||
.GET("/api/checkIn/records/export", checkInHandler::exportSignInRecords)
|
||||
|
||||
// ========================================
|
||||
// ========== 数据统计模块路由 ============
|
||||
// ========================================
|
||||
|
||||
// ===== 数据统计核心功能 =====
|
||||
.GET("/api/datacount/summary", dataStatisticsHandler::getStatisticsSummary)
|
||||
.GET("/api/datacount/member", dataStatisticsHandler::getMemberStatistics)
|
||||
.GET("/api/datacount/booking", dataStatisticsHandler::getBookingStatistics)
|
||||
.GET("/api/datacount/signin", dataStatisticsHandler::getSignInStatistics)
|
||||
.GET("/api/datacount/history", dataStatisticsHandler::queryHistoricalStatistics)
|
||||
.GET("/api/datacount/export", dataStatisticsHandler::exportStatistics)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ spring:
|
||||
name: gym-manage-api
|
||||
main:
|
||||
allow-bean-definition-overriding: true
|
||||
web-application-type: reactive
|
||||
cache:
|
||||
type: none
|
||||
autoconfigure:
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
-- 数据统计模块测试数据
|
||||
-- 用于测试会员、预约、签到统计接口
|
||||
|
||||
-- 插入测试会员数据
|
||||
INSERT INTO member_user (id, member_no, nickname, phone, created_at, updated_at, is_deleted) VALUES
|
||||
(1001, 'M20260601001', '张三', '13800138001', '2026-06-01 08:00:00', '2026-06-01 08:00:00', false),
|
||||
(1002, 'M20260601002', '李四', '13800138002', '2026-06-01 09:00:00', '2026-06-01 09:00:00', false),
|
||||
(1003, 'M20260602003', '王五', '13800138003', '2026-06-02 10:00:00', '2026-06-02 10:00:00', false),
|
||||
(1004, 'M20260603004', '赵六', '13800138004', '2026-06-03 11:00:00', '2026-06-03 11:00:00', false),
|
||||
(1005, 'M20260609005', '钱七', '13800138005', '2026-06-09 08:00:00', '2026-06-09 08:00:00', false),
|
||||
(1006, 'M20260609006', '孙八', '13800138006', '2026-06-09 09:00:00', '2026-06-09 09:00:00', false),
|
||||
(1007, 'M20260609007', '周九', '13800138007', '2026-06-09 10:00:00', '2026-06-09 10:00:00', false),
|
||||
(1008, 'M20260604008', '吴十', '13800138008', '2026-06-04 14:00:00', '2026-06-04 14:00:00', false),
|
||||
(1009, 'M20260605009', '郑十一', '13800138009', '2026-06-05 15:00:00', '2026-06-05 15:00:00', false),
|
||||
(1010, 'M20260606010', '王十二', '13800138010', '2026-06-06 16:00:00', '2026-06-06 16:00:00', false),
|
||||
(1011, 'M20260607011', '陈十三', '13800138011', '2026-06-07 17:00:00', '2026-06-07 17:00:00', false),
|
||||
(1012, 'M20260608012', '刘十四', '13800138012', '2026-06-08 18:00:00', '2026-06-08 18:00:00', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入测试签到记录数据 (今天的数据)
|
||||
INSERT INTO sign_in_record (id, member_id, sign_in_time, sign_in_type, sign_in_status, source, is_delete) VALUES
|
||||
(2001, 1001, '2026-06-09 08:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2002, 1002, '2026-06-09 08:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false),
|
||||
(2003, 1003, '2026-06-09 08:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2004, 1004, '2026-06-09 09:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2005, 1005, '2026-06-09 09:15:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false),
|
||||
(2006, 1006, '2026-06-09 09:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2007, 1007, '2026-06-09 10:00:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2008, 1008, '2026-06-09 10:15:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2009, 1009, '2026-06-09 10:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false),
|
||||
(2010, 1010, '2026-06-09 11:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2011, 1011, '2026-06-09 11:15:00', 'FACE', 'FAIL', 'MINI_PROGRAM', false),
|
||||
(2012, 1012, '2026-06-09 11:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入测试签到记录数据 (昨天的数据)
|
||||
INSERT INTO sign_in_record (id, member_id, sign_in_time, sign_in_type, sign_in_status, source, is_delete) VALUES
|
||||
(2013, 1001, '2026-06-08 07:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2014, 1002, '2026-06-08 08:00:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false),
|
||||
(2015, 1003, '2026-06-08 08:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2016, 1004, '2026-06-08 09:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2017, 1005, '2026-06-08 09:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false),
|
||||
(2018, 1006, '2026-06-08 10:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2019, 1007, '2026-06-08 10:30:00', 'FACE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2020, 1008, '2026-06-08 11:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入测试签到记录数据 (前天的数据)
|
||||
INSERT INTO sign_in_record (id, member_id, sign_in_time, sign_in_type, sign_in_status, source, is_delete) VALUES
|
||||
(2021, 1001, '2026-06-07 07:00:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2022, 1002, '2026-06-07 07:30:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false),
|
||||
(2023, 1003, '2026-06-07 08:00:00', 'FACE', 'FAIL', 'MINI_PROGRAM', false),
|
||||
(2024, 1004, '2026-06-07 08:30:00', 'QR_CODE', 'SUCCESS', 'MINI_PROGRAM', false),
|
||||
(2025, 1005, '2026-06-07 09:00:00', 'MANUAL', 'SUCCESS', 'PC_BACKEND', false)
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入测试团课数据
|
||||
INSERT INTO group_course (id, course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, created_at, updated_at) VALUES
|
||||
(3001, '瑜伽入门', 1, 1, '2026-06-09 08:00:00', '2026-06-09 09:00:00', 20, 15, 0, '健身房A区', 'https://example.com/yoga.jpg', '适合初学者的瑜伽课程', '2026-06-01 10:00:00', '2026-06-01 10:00:00'),
|
||||
(3002, '动感单车', 2, 2, '2026-06-09 09:30:00', '2026-06-09 10:30:00', 25, 20, 0, '健身房B区', 'https://example.com/spinning.jpg', '高强度有氧运动', '2026-06-01 11:00:00', '2026-06-01 11:00:00'),
|
||||
(3003, '普拉提', 3, 1, '2026-06-09 14:00:00', '2026-06-09 15:00:00', 15, 10, 0, '健身房C区', 'https://example.com/pilates.jpg', '核心力量训练', '2026-06-01 12:00:00', '2026-06-01 12:00:00')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入测试团课预约数据 (今天的数据)
|
||||
INSERT INTO group_course_booking (id, member_id, member_card_id, course_id, booking_time, status, created_at, updated_at) VALUES
|
||||
(4001, 1001, 1, 3001, '2026-06-09 08:00:00', '2', '2026-06-08 20:00:00', '2026-06-09 08:30:00'),
|
||||
(4002, 1002, 2, 3001, '2026-06-09 08:00:00', '2', '2026-06-08 21:00:00', '2026-06-09 08:30:00'),
|
||||
(4003, 1003, 3, 3001, '2026-06-09 08:00:00', '3', '2026-06-08 22:00:00', '2026-06-09 09:00:00'),
|
||||
(4004, 1004, 4, 3002, '2026-06-09 09:30:00', '2', '2026-06-08 19:00:00', '2026-06-09 09:30:00'),
|
||||
(4005, 1005, 5, 3002, '2026-06-09 09:30:00', '1', '2026-06-08 20:30:00', '2026-06-09 09:00:00'),
|
||||
(4006, 1006, 6, 3002, '2026-06-09 09:30:00', '2', '2026-06-08 21:30:00', '2026-06-09 09:30:00'),
|
||||
(4007, 1007, 7, 3003, '2026-06-09 14:00:00', '2', '2026-06-08 22:30:00', '2026-06-09 14:00:00'),
|
||||
(4008, 1008, 8, 3003, '2026-06-09 14:00:00', '3', '2026-06-09 08:00:00', '2026-06-09 14:00:00'),
|
||||
(4009, 1009, 9, 3003, '2026-06-09 14:00:00', '2', '2026-06-09 09:00:00', '2026-06-09 14:00:00'),
|
||||
(4010, 1010, 10, 3001, '2026-06-09 08:00:00', '2', '2026-06-09 07:00:00', '2026-06-09 08:00:00'),
|
||||
(4011, 1011, 11, 3002, '2026-06-09 09:30:00', '1', '2026-06-09 08:30:00', '2026-06-09 09:00:00'),
|
||||
(4012, 1012, 12, 3003, '2026-06-09 14:00:00', '2', '2026-06-09 10:00:00', '2026-06-09 14:00:00')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入测试团课预约数据 (昨天的数据)
|
||||
INSERT INTO group_course_booking (id, member_id, member_card_id, course_id, booking_time, status, created_at, updated_at) VALUES
|
||||
(4013, 1001, 1, 3001, '2026-06-08 08:00:00', '2', '2026-06-07 20:00:00', '2026-06-08 08:30:00'),
|
||||
(4014, 1002, 2, 3001, '2026-06-08 08:00:00', '2', '2026-06-07 21:00:00', '2026-06-08 08:30:00'),
|
||||
(4015, 1003, 3, 3002, '2026-06-08 09:30:00', '3', '2026-06-07 22:00:00', '2026-06-08 09:30:00'),
|
||||
(4016, 1004, 4, 3002, '2026-06-08 09:30:00', '2', '2026-06-07 19:00:00', '2026-06-08 09:30:00'),
|
||||
(4017, 1005, 5, 3003, '2026-06-08 14:00:00', '1', '2026-06-07 20:30:00', '2026-06-08 13:00:00'),
|
||||
(4018, 1006, 6, 3003, '2026-06-08 14:00:00', '2', '2026-06-07 21:30:00', '2026-06-08 14:00:00')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 插入测试团课预约数据 (前天的数据)
|
||||
INSERT INTO group_course_booking (id, member_id, member_card_id, course_id, booking_time, status, created_at, updated_at) VALUES
|
||||
(4019, 1001, 1, 3002, '2026-06-07 09:30:00', '2', '2026-06-06 20:00:00', '2026-06-07 09:30:00'),
|
||||
(4020, 1002, 2, 3002, '2026-06-07 09:30:00', '2', '2026-06-06 21:00:00', '2026-06-07 09:30:00'),
|
||||
(4021, 1003, 3, 3003, '2026-06-07 14:00:00', '2', '2026-06-06 22:00:00', '2026-06-07 14:00:00'),
|
||||
(4022, 1004, 4, 3003, '2026-06-07 14:00:00', '3', '2026-06-06 19:00:00', '2026-06-07 14:00:00')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
|
||||
-- 预期统计结果 (今日):
|
||||
-- 会员统计:
|
||||
-- 新增会员: 3 (1005, 1006, 1007)
|
||||
-- 活跃会员: 12 (所有会员今日或近期有活动)
|
||||
-- 总会员数: 12
|
||||
-- 签到会员: 12
|
||||
-- 预约会员: 9
|
||||
-- 取消会员: 2
|
||||
|
||||
-- 预约统计:
|
||||
-- 总预约: 12
|
||||
-- 取消: 2
|
||||
-- 出席: 8
|
||||
-- 缺席: 2
|
||||
-- 出席率: 8/10 = 80%
|
||||
-- 取消率: 2/12 = 16.67%
|
||||
|
||||
-- 签到统计:
|
||||
-- 总签到: 12
|
||||
-- 成功: 11
|
||||
-- 失败: 1
|
||||
-- 成功率: 11/12 = 91.67%
|
||||
-- 类型分布: QR_CODE=5, MANUAL=4, FACE=3
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
-- ============================================
|
||||
-- 团课类型表
|
||||
-- ============================================
|
||||
|
||||
-- 团课类型表
|
||||
CREATE TABLE IF NOT EXISTS group_course_type (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type_name VARCHAR(100) NOT NULL,
|
||||
base_difficulty INTEGER DEFAULT 1,
|
||||
description TEXT,
|
||||
category VARCHAR(50),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
COMMENT ON TABLE group_course_type IS '团课类型表';
|
||||
COMMENT ON COLUMN group_course_type.id IS '主键ID';
|
||||
COMMENT ON COLUMN group_course_type.type_name IS '类型名称';
|
||||
COMMENT ON COLUMN group_course_type.base_difficulty IS '基础难度(1-10)';
|
||||
COMMENT ON COLUMN group_course_type.description IS '类型描述';
|
||||
COMMENT ON COLUMN group_course_type.category IS '分类(如:有氧、力量、柔韧等)';
|
||||
COMMENT ON COLUMN group_course_type.create_by IS '创建人';
|
||||
COMMENT ON COLUMN group_course_type.update_by IS '更新人';
|
||||
COMMENT ON COLUMN group_course_type.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN group_course_type.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN group_course_type.deleted_at IS '删除时间(软删除)';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_group_course_type_type_name ON group_course_type(type_name);
|
||||
CREATE INDEX idx_group_course_type_category ON group_course_type(category);
|
||||
|
||||
-- 插入初始团课类型数据(参考exmp.txt)
|
||||
INSERT INTO group_course_type (type_name, base_difficulty, description, category) VALUES
|
||||
('慢走/椭圆机轻松模式', 1, '几乎无难度,适合所有人', '基础有氧与热身'),
|
||||
('固定自行车(低阻力)', 2, '注意座椅高度调节即可', '基础有氧与热身'),
|
||||
('跑步机慢跑', 3, '需要基本协调性,膝盖有压力', '基础有氧与热身'),
|
||||
('跳绳(连续基础跳)', 3, '需要手脚配合,心肺要求明显', '基础有氧与热身'),
|
||||
('坐姿腿屈伸/腿弯举', 2, '很容易找到发力感', '固定器械训练'),
|
||||
('坐姿推胸机', 3, '需注意肩胛后收,避免耸肩', '固定器械训练'),
|
||||
('高位下拉(坐姿)', 3, '需控制不要过度后仰', '固定器械训练'),
|
||||
('史密斯机深蹲', 4, '轨迹固定,但需保持核心稳定', '固定器械训练'),
|
||||
('蝴蝶机夹胸', 3, '易用肘关节代偿,需锁定肩关节', '固定器械训练'),
|
||||
('平板支撑', 3, '耐力考验,技巧低', '自重基础动作'),
|
||||
('跪姿俯卧撑', 3, '上肢力量较弱者首选', '自重基础动作'),
|
||||
('标准俯卧撑', 5, '需核心收紧,身体成直线', '自重基础动作'),
|
||||
('引体向上(弹力带辅助)', 6, '背部和手臂力量要求高', '自重基础动作'),
|
||||
('标准引体向上', 8, '力量-体重比极高,多数男性无法完成1次', '自重基础动作'),
|
||||
('徒手深蹲', 3, '注意膝盖方向与背部直立', '自重基础动作'),
|
||||
('单腿深蹲(手枪蹲)', 8, '需要极高下肢力量、柔韧性和平衡', '自重基础动作'),
|
||||
('哑铃二头弯举', 4, '容易晃动借力,但较安全', '自由重量杠铃/哑铃'),
|
||||
('哑铃侧平举', 5, '极易用斜方肌代偿,真正练到三角肌中束很难', '自由重量杠铃/哑铃'),
|
||||
('杠铃卧推', 7, '肩关节压力大,起桥、沉肩、稳定手腕均有技巧,有压伤风险', '自由重量杠铃/哑铃'),
|
||||
('杠铃深蹲(颈后)', 8, '全身协调性、核心抗压、杠位放置、呼吸模式,学习曲线陡峭', '自由重量杠铃/哑铃'),
|
||||
('传统硬拉', 9, '风险极高,需要精确的脊柱中立、髋铰链、背阔肌收紧,错误时伤腰', '自由重量杠铃/哑铃'),
|
||||
('高翻/抓举(奥运举重)', 10, '需要爆发力、柔韧、精准衔接,非数月训练不能掌握', '自由重量杠铃/哑铃'),
|
||||
('波比跳(标准版)', 6, '连续做时心肺压力极大', '高强度与爆发力'),
|
||||
('冲刺跑(短跑)', 7, '对腘绳肌和脚踝爆发力要求高', '高强度与爆发力'),
|
||||
('跳箱(合理高度)', 6, '需要落地缓冲技巧', '高强度与爆发力'),
|
||||
('负重雪橇推', 6, '主要考验腿部耐力和意志力', '高强度与爆发力'),
|
||||
('双力臂(引体向上后翻腕上杠)', 9, '需要爆发引体 + 极高相对力量', '高强度与爆发力'),
|
||||
('静态拉伸(坐姿体前屈)', 2, '无风险,但需要坚持', '柔韧与平衡类'),
|
||||
('瑜伽下犬式', 3, '常见,但需背部与手臂对齐', '柔韧与平衡类'),
|
||||
('单腿罗马尼亚硬拉(徒手)', 6, '极考验平衡和髋稳定', '柔韧与平衡类'),
|
||||
('全深蹲(脚跟贴地,亚洲蹲)', 5, '踝关节灵活度限制多数人', '柔韧与平衡类'),
|
||||
('竖叉/横叉', 8, '需要数月甚至数年拉伸', '柔韧与平衡类'),
|
||||
('瑜伽入门', 2, '适合初学者的瑜伽课程,注重基础体式', '柔韧与平衡类'),
|
||||
('瑜伽进阶', 5, '针对有一定基础的学员,包含更复杂体式', '柔韧与平衡类'),
|
||||
('普拉提', 4, '注重核心力量和身体控制', '柔韧与平衡类'),
|
||||
('动感单车', 4, '高强度有氧运动,节奏感强', '基础有氧与热身'),
|
||||
('搏击操', 5, '结合拳击动作的有氧运动', '高强度与爆发力'),
|
||||
('HIIT训练', 7, '高强度间歇训练,对心肺要求极高', '高强度与爆发力'),
|
||||
('核心训练', 4, '针对核心肌群的专项训练', '自重基础动作')
|
||||
ON CONFLICT DO NOTHING;
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
-- ============================================
|
||||
-- 团课标签相关表
|
||||
-- ============================================
|
||||
|
||||
-- 团课标签表
|
||||
CREATE TABLE IF NOT EXISTS course_label (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
label_name VARCHAR(50) NOT NULL,
|
||||
color VARCHAR(20) DEFAULT '#1890ff',
|
||||
description VARCHAR(200),
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 团课类型-标签关联表
|
||||
CREATE TABLE IF NOT EXISTS course_type_label (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
type_id BIGINT NOT NULL,
|
||||
label_id BIGINT NOT NULL,
|
||||
create_by VARCHAR(50),
|
||||
update_by VARCHAR(50),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
UNIQUE (type_id, label_id)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE course_label IS '团课标签表';
|
||||
COMMENT ON COLUMN course_label.id IS '主键ID';
|
||||
COMMENT ON COLUMN course_label.label_name IS '标签名称';
|
||||
COMMENT ON COLUMN course_label.color IS '标签颜色(十六进制)';
|
||||
COMMENT ON COLUMN course_label.description IS '标签描述';
|
||||
COMMENT ON COLUMN course_label.create_by IS '创建人';
|
||||
COMMENT ON COLUMN course_label.update_by IS '更新人';
|
||||
COMMENT ON COLUMN course_label.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN course_label.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN course_label.deleted_at IS '删除时间(软删除)';
|
||||
|
||||
COMMENT ON TABLE course_type_label IS '团课类型-标签关联表';
|
||||
COMMENT ON COLUMN course_type_label.id IS '主键ID';
|
||||
COMMENT ON COLUMN course_type_label.type_id IS '团课类型ID';
|
||||
COMMENT ON COLUMN course_type_label.label_id IS '标签ID';
|
||||
COMMENT ON COLUMN course_type_label.create_by IS '创建人';
|
||||
COMMENT ON COLUMN course_type_label.update_by IS '更新人';
|
||||
COMMENT ON COLUMN course_type_label.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN course_type_label.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN course_type_label.deleted_at IS '删除时间(软删除)';
|
||||
|
||||
-- 创建索引
|
||||
CREATE INDEX idx_course_label_label_name ON course_label(label_name);
|
||||
CREATE INDEX idx_course_type_label_type_id ON course_type_label(type_id);
|
||||
CREATE INDEX idx_course_type_label_label_id ON course_type_label(label_id);
|
||||
|
||||
-- 插入初始标签数据
|
||||
INSERT INTO course_label (label_name, color, description) VALUES
|
||||
('适合新手', '#52c41a', '适合健身初学者'),
|
||||
('中级过渡', '#faad14', '适合有一定基础的学员'),
|
||||
('高级进阶', '#f5222d', '适合高级学员'),
|
||||
('减脂塑形', '#722ed1', '有助于减脂塑形'),
|
||||
('增肌强化', '#13c2c2', '有助于增肌强化'),
|
||||
('柔韧性训练', '#eb2f96', '注重柔韧性提升'),
|
||||
('核心训练', '#1890ff', '注重核心力量'),
|
||||
('心肺训练', '#fa8c16', '提升心肺功能'),
|
||||
('低冲击', '#52c41a', '低冲击运动,适合关节保护'),
|
||||
('高强度', '#f5222d', '高强度间歇训练'),
|
||||
('团体互动', '#722ed1', '注重团队协作'),
|
||||
('私教推荐', '#13c2c2', '私教推荐课程'),
|
||||
('热门课程', '#ff1493', '人气较高的课程'),
|
||||
('新课上线', '#00ced1', '新上线的课程')
|
||||
ON CONFLICT DO NOTHING;
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
-- ============================================
|
||||
-- 会员到店签到记录表
|
||||
-- 版本: V6
|
||||
-- 描述: 创建sign_in_record表,用于记录会员签到信息
|
||||
-- ============================================
|
||||
|
||||
-- 创建签到记录表
|
||||
CREATE TABLE IF NOT EXISTS sign_in_record (
|
||||
id BIGSERIAL PRIMARY KEY, -- 自增主键
|
||||
member_id BIGINT NOT NULL, -- 会员ID,关联member表
|
||||
member_card_id BIGINT, -- 签到时使用的会员卡ID
|
||||
sign_in_time TIMESTAMP NOT NULL, -- 签到入场时间
|
||||
sign_in_type VARCHAR(20) NOT NULL, -- 签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别
|
||||
sign_in_status VARCHAR(20) NOT NULL DEFAULT 'SUCCESS', -- 签到状态:SUCCESS-成功,FAILED-失败
|
||||
verification_details TEXT, -- JSON格式,存储会员卡验证时的快照数据
|
||||
fail_reason VARCHAR(500), -- 失败时的具体原因文案
|
||||
operator_id BIGINT, -- 操作人ID(前台人员),自助签到时为NULL
|
||||
operator_name VARCHAR(100), -- 操作人姓名冗余
|
||||
device_info VARCHAR(200), -- 签到设备标识或型号
|
||||
ip_address VARCHAR(50), -- 客户端IP地址
|
||||
source VARCHAR(20) NOT NULL, -- 签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端
|
||||
is_delete BOOLEAN DEFAULT FALSE, -- 软删除标识:false-未删除,true-已删除
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 记录创建时间
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -- 记录更新时间
|
||||
);
|
||||
|
||||
-- 创建索引
|
||||
-- 会员ID索引(加速按会员查询签到记录)
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_id ON sign_in_record(member_id);
|
||||
|
||||
-- 签到时间索引(加速按时间范围查询)
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_time ON sign_in_record(sign_in_time);
|
||||
|
||||
-- 签到状态索引(加速按状态筛选)
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_sign_in_status ON sign_in_record(sign_in_status);
|
||||
|
||||
-- 会员卡ID索引(加速按会员卡查询)
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_card_id ON sign_in_record(member_card_id);
|
||||
|
||||
-- 操作人ID索引(加速按操作人查询)
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_operator_id ON sign_in_record(operator_id);
|
||||
|
||||
-- 签到来源索引(加速按来源统计)
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_source ON sign_in_record(source);
|
||||
|
||||
-- 软删除索引(加速查询未删除的记录)
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_is_delete ON sign_in_record(is_delete);
|
||||
|
||||
-- 复合索引:会员ID + 签到时间(加速会员签到历史查询)
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_member_time ON sign_in_record(member_id, sign_in_time);
|
||||
|
||||
-- 复合索引:签到状态 + 签到时间(加速统计数据查询)
|
||||
CREATE INDEX IF NOT EXISTS idx_sign_in_record_status_time ON sign_in_record(sign_in_status, sign_in_time);
|
||||
|
||||
-- 添加表注释
|
||||
COMMENT ON TABLE sign_in_record IS '会员到店签到记录表';
|
||||
|
||||
-- 添加字段注释
|
||||
COMMENT ON COLUMN sign_in_record.id IS '自增主键';
|
||||
COMMENT ON COLUMN sign_in_record.member_id IS '会员ID,关联member表';
|
||||
COMMENT ON COLUMN sign_in_record.member_card_id IS '签到时使用的会员卡ID';
|
||||
COMMENT ON COLUMN sign_in_record.sign_in_time IS '签到入场时间';
|
||||
COMMENT ON COLUMN sign_in_record.sign_in_type IS '签到方式:QR_CODE-扫码签到,MANUAL-手动签到,FACE-人脸识别';
|
||||
COMMENT ON COLUMN sign_in_record.sign_in_status IS '签到状态:SUCCESS-成功,FAILED-失败';
|
||||
COMMENT ON COLUMN sign_in_record.verification_details IS 'JSON格式,存储会员卡验证时的快照数据';
|
||||
COMMENT ON COLUMN sign_in_record.fail_reason IS '失败时的具体原因文案';
|
||||
COMMENT ON COLUMN sign_in_record.operator_id IS '操作人ID(前台人员),自助签到时为NULL';
|
||||
COMMENT ON COLUMN sign_in_record.operator_name IS '操作人姓名冗余';
|
||||
COMMENT ON COLUMN sign_in_record.device_info IS '签到设备标识或型号';
|
||||
COMMENT ON COLUMN sign_in_record.ip_address IS '客户端IP地址';
|
||||
COMMENT ON COLUMN sign_in_record.source IS '签到来源:MINI_PROGRAM-小程序扫码,PC_BACKEND-后台管理端';
|
||||
COMMENT ON COLUMN sign_in_record.is_delete IS '软删除标识:false-未删除,true-已删除';
|
||||
COMMENT ON COLUMN sign_in_record.created_at IS '记录创建时间';
|
||||
COMMENT ON COLUMN sign_in_record.updated_at IS '记录更新时间';
|
||||
+1
@@ -56,6 +56,7 @@ public class SecurityConfig {
|
||||
.pathMatchers("/api/admin/member/**").permitAll()
|
||||
.pathMatchers("/api/member-cards/**").permitAll()
|
||||
.pathMatchers("/api/member-card-records/**").permitAll()
|
||||
.pathMatchers("/**").permitAll()
|
||||
.pathMatchers("/api/member-card-transactions/**").permitAll()
|
||||
.pathMatchers("/api/checkIn/**").permitAll();
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<module>gym-member</module>
|
||||
<module>gym-groupCourse</module>
|
||||
<module>gym-checkIn</module>
|
||||
<module>gym-dataCount</module>
|
||||
</modules>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
unpackage/
|
||||
.hbuilderx/
|
||||
.DS_Store
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<view>
|
||||
<GlobalLoading />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import GlobalLoading from '@/components/global/GlobalLoading.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlobalLoading
|
||||
},
|
||||
onLaunch: function() {
|
||||
console.log('App Launch')
|
||||
this.preloadTabData()
|
||||
},
|
||||
onShow: function() {
|
||||
console.log('App Show')
|
||||
// #ifdef APP-PLUS
|
||||
this.hideNativeTabBar()
|
||||
// #endif
|
||||
},
|
||||
onHide: function() {
|
||||
console.log('App Hide')
|
||||
},
|
||||
methods: {
|
||||
// 隐藏原生 TabBar - 这里是核心修复
|
||||
hideNativeTabBar() {
|
||||
// 尝试多次执行,确保执行成功
|
||||
const tryHide = (times = 0) => {
|
||||
if (times > 10) return // 最多尝试10次
|
||||
|
||||
uni.hideTabBar({
|
||||
animation: false,
|
||||
success: () => {
|
||||
console.log('✅ 原生TabBar隐藏成功')
|
||||
// 强制 CSS 覆盖(双重保险)
|
||||
this.forceCSSHide()
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log(`❌ 第${times+1}次隐藏失败,1秒后重试`, err)
|
||||
setTimeout(() => tryHide(times + 1), 1000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 延迟 300ms 执行,确保页面挂载完成
|
||||
setTimeout(() => tryHide(), 300)
|
||||
},
|
||||
|
||||
// 强制 CSS 覆盖(最终保险)
|
||||
forceCSSHide() {
|
||||
// #ifdef APP-PLUS
|
||||
const style = document.createElement('style')
|
||||
style.innerHTML = `
|
||||
uni-tabbar,
|
||||
uni-tabbar .uni-tabbar,
|
||||
.uni-tabbar,
|
||||
uni-tabbar > .uni-tabbar,
|
||||
[class*="uni-tabbar"] {
|
||||
display: none !important;
|
||||
height: 0 !important;
|
||||
opacity: 0 !important;
|
||||
visibility: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
`
|
||||
document.head.appendChild(style)
|
||||
console.log('✅ CSS 强制覆盖已注入')
|
||||
// #endif
|
||||
},
|
||||
|
||||
// 预加载所有 Tab 页面的核心数据
|
||||
preloadTabData() {
|
||||
// 延迟执行,不阻塞首屏
|
||||
setTimeout(() => {
|
||||
// 预加载课程数据等...
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,50 @@
|
||||
import request from "@/utils/request.js"
|
||||
|
||||
export function login(params) {
|
||||
return request.post('/member/auth/miniapp/login', params)
|
||||
}
|
||||
|
||||
export function logout() {
|
||||
return request.post('/member/auth/logout')
|
||||
}
|
||||
|
||||
export function getQRCode(options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
|
||||
return request.get('/checkIn/qrcode', {}, options)
|
||||
}
|
||||
|
||||
export function checkIn(params) {
|
||||
return request.post('/checkIn/scan', params)
|
||||
}
|
||||
|
||||
export function getUserInfo(options = { cache: true, cacheTime: 30 * 60 * 1000 }) {
|
||||
return request.get('/member/info', {}, options)
|
||||
}
|
||||
|
||||
export function updateUserInfo(params) {
|
||||
return request.put('/member/info', params)
|
||||
}
|
||||
|
||||
export function getRecommendCourses(options = { cache: true, cacheTime: 10 * 60 * 1000 }) {
|
||||
return request.get('/course/recommend', {}, options)
|
||||
}
|
||||
|
||||
export function getCourseDetail(id, options = { cache: true, cacheTime: 15 * 60 * 1000 }) {
|
||||
return request.get(`/course/${id}`, {}, options)
|
||||
}
|
||||
|
||||
export function getGroupCoursePage(params = {}, options = { cache: true, cacheTime: 5 * 60 * 1000 }) {
|
||||
const { page = 0, size = 10, sort = 'id', order = 'asc', keyword } = params
|
||||
return request.post('/groupCourse/page', { page, size, sort, order, keyword }, options)
|
||||
}
|
||||
|
||||
export default {
|
||||
login,
|
||||
logout,
|
||||
getQRCode,
|
||||
checkIn,
|
||||
getUserInfo,
|
||||
updateUserInfo,
|
||||
getRecommendCourses,
|
||||
getCourseDetail,
|
||||
getGroupCoursePage
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
// common/constants/routes.js
|
||||
|
||||
/** 与 pages.json 保持一致 */
|
||||
export const PAGE = {
|
||||
INDEX: '/pages/index/index',
|
||||
COURSE: '/pages/course/index',
|
||||
TRAIN: '/pages/train/index',
|
||||
DISCOVER: '/pages/discover/index',
|
||||
MEMBER: '/pages/memberInfo/memberInfo',
|
||||
BOOKING: '/pages/memberInfo/booking',
|
||||
MEMBER_CARD: '/pages/memberInfo/memberCard',
|
||||
USER_INFO: '/pages/memberInfo/userInfo',
|
||||
BODY_TEST_HOME: '/pages/memberInfo/bodyTestHome',
|
||||
BODY_TEST_CONNECT: '/pages/memberInfo/bodyTestConnect',
|
||||
BODY_TEST_MEASURING: '/pages/memberInfo/bodyTestMeasuring',
|
||||
BODY_TEST_REPORT: '/pages/memberInfo/bodyTestReport',
|
||||
BODY_TEST_HISTORY: '/pages/memberInfo/bodyTestHistory',
|
||||
BODY_TEST_COMPARE: '/pages/memberInfo/bodyTestCompare',
|
||||
BODY_TEST_SETTINGS: '/pages/memberInfo/bodyTestSettings',
|
||||
BODY_TEST_TREND: '/pages/memberInfo/bodyTestTrend',
|
||||
COURSE_LIST: '/pages/groupCourse/list',
|
||||
COURSE_DETAIL: '/pages/memberInfo/courseDetail',
|
||||
COUPON_DETAIL: '/pages/memberInfo/couponDetail',
|
||||
COUPON_CENTER: '/pages/memberInfo/couponCenter',
|
||||
POINTS_MALL: '/pages/memberInfo/pointsMall',
|
||||
POINTS_HISTORY: '/pages/memberInfo/pointsHistory',
|
||||
ONLINE_COURSE: '/pages/memberInfo/onlineCourseDetail',
|
||||
COURSE_EVALUATE: '/pages/memberInfo/courseEvaluate',
|
||||
TRAIN_SESSION: '/pages/memberInfo/trainSessionDetail',
|
||||
TRAIN_REPORT: '/pages/memberInfo/trainReport',
|
||||
COUPONS: '/pages/memberInfo/coupons',
|
||||
POINTS: '/pages/memberInfo/points',
|
||||
REFERRAL: '/pages/memberInfo/referral',
|
||||
MY_COURSES: '/pages/memberInfo/myCourses',
|
||||
CHECK_IN_HISTORY: '/pages/memberInfo/checkInHistory'
|
||||
}
|
||||
|
||||
/** 底部 TabBar 页面路径,顺序与 TabBar.vue 一致 */
|
||||
export const TAB_ROUTES = [
|
||||
PAGE.INDEX,
|
||||
PAGE.COURSE,
|
||||
PAGE.TRAIN,
|
||||
PAGE.DISCOVER,
|
||||
PAGE.MEMBER
|
||||
]
|
||||
|
||||
const TAB_PAGES = new Set(TAB_ROUTES)
|
||||
|
||||
/** 防止 Tab 连点触发并发路由 */
|
||||
let tabNavigating = false
|
||||
|
||||
function normalizePath(url) {
|
||||
if (!url) return ''
|
||||
const path = url.split('?')[0]
|
||||
return path.startsWith('/') ? path : `/${path}`
|
||||
}
|
||||
|
||||
export function getTabIndexByRoute(route) {
|
||||
const path = normalizePath(route)
|
||||
const idx = TAB_ROUTES.indexOf(path)
|
||||
return idx >= 0 ? idx : 0
|
||||
}
|
||||
|
||||
export function getCurrentRoutePath() {
|
||||
const pages = getCurrentPages()
|
||||
if (!pages.length) return PAGE.INDEX
|
||||
const page = pages[pages.length - 1]
|
||||
const route = page.route || page.$page?.fullPath || ''
|
||||
return normalizePath(route ? `/${route}` : PAGE.INDEX)
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到普通页面(非 TabBar 页面)
|
||||
* 使用 navigateTo,保留页面栈,可以正常返回
|
||||
*/
|
||||
export function navigateToPage(url) {
|
||||
uni.showLoading({ title: '加载中...', mask: true })
|
||||
const path = normalizePath(url)
|
||||
|
||||
// ✅ 如果目标是 TabBar 页面,不应该使用 navigateTo
|
||||
// 这种情况应该使用 switchToTabPage(会清空页面栈)
|
||||
if (TAB_PAGES.has(path)) {
|
||||
console.warn('[navigateToPage] 不应该用 navigateTo 跳转 TabBar 页面,请使用 switchToTabPage')
|
||||
switchToTabPage(path)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[navigateToPage] 跳转到:', url)
|
||||
|
||||
uni.navigateTo({
|
||||
url,
|
||||
fail: (err) => {
|
||||
console.error('[navigateTo]', url, err)
|
||||
// 页面栈满时降级使用 redirectTo
|
||||
if (err.errMsg && err.errMsg.includes('limit')) {
|
||||
uni.redirectTo({ url })
|
||||
} else {
|
||||
uni.showToast({ title: '页面跳转失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
success: () => {
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
},3000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到 TabBar 页面(清空页面栈)
|
||||
* 用于从任何页面跳转到首页/课程/训练等 TabBar 页面
|
||||
*/
|
||||
export function switchToTabPage(url) {
|
||||
const path = normalizePath(url)
|
||||
if (!TAB_PAGES.has(path)) {
|
||||
console.warn('[switchToTabPage] 目标不是 TabBar 页面:', path)
|
||||
navigateToPage(url)
|
||||
return
|
||||
}
|
||||
|
||||
if (getCurrentRoutePath() === path || tabNavigating) return
|
||||
|
||||
console.log('[switchToTabPage] 跳转到 TabBar:', path)
|
||||
|
||||
tabNavigating = true
|
||||
uni.switchTab({ // ✅ 改用 switchTab,而不是 reLaunch
|
||||
url: path,
|
||||
complete: () => {
|
||||
setTimeout(() => {
|
||||
tabNavigating = false
|
||||
}, 320)
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[switchTab]', path, err)
|
||||
// 降级使用 reLaunch
|
||||
uni.reLaunch({
|
||||
url: path,
|
||||
complete: () => {
|
||||
setTimeout(() => {
|
||||
tabNavigating = false
|
||||
}, 320)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置到 TabBar 页面(清空所有历史)
|
||||
* 用于退出登录、强制跳转等场景
|
||||
*/
|
||||
export function reLaunchToTabPage(url) {
|
||||
const path = normalizePath(url)
|
||||
console.log('[reLaunchToTabPage] 重置到:', path)
|
||||
|
||||
uni.reLaunch({
|
||||
url: path,
|
||||
fail: (err) => {
|
||||
console.error('[reLaunch]', path, err)
|
||||
uni.switchTab({ url: path })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回上一页,如果没有上一页则跳转到指定 TabBar 页面
|
||||
*/
|
||||
export function goBackOrTab(fallbackUrl = PAGE.MEMBER) {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
uni.navigateBack({ delta: 1 })
|
||||
} else {
|
||||
switchToTabPage(fallbackUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 子页面返回个人中心
|
||||
*/
|
||||
export function backToMemberCenter() {
|
||||
goBackOrTab(PAGE.MEMBER)
|
||||
}
|
||||
|
||||
/**
|
||||
* 子页面返回指定 TabBar 页面
|
||||
*/
|
||||
export function backToTab(tabUrl) {
|
||||
goBackOrTab(tabUrl)
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
const COLORS = {
|
||||
primary: '#0B2B4B',
|
||||
accent: '#FF6B35',
|
||||
accentLight: 'rgba(255, 107, 53, 0.25)',
|
||||
grid: '#E9EDF2',
|
||||
text: '#5E6F8D',
|
||||
fill: 'rgba(26, 74, 111, 0.35)',
|
||||
line: '#1A4A6F'
|
||||
}
|
||||
|
||||
function setupCanvas(canvas, width, height, dpr) {
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.scale(dpr, dpr)
|
||||
return ctx
|
||||
}
|
||||
|
||||
/** 绘制雷达图 */
|
||||
export function drawRadarChart(canvas, opts = {}) {
|
||||
if (!canvas) return
|
||||
const {
|
||||
width = 280,
|
||||
height = 240,
|
||||
labels = [],
|
||||
values = [],
|
||||
dpr = 1
|
||||
} = opts
|
||||
const ctx = setupCanvas(canvas, width, height, dpr)
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
const cx = width / 2
|
||||
const cy = height / 2 + 8
|
||||
const radius = Math.min(width, height) * 0.32
|
||||
const count = labels.length || 6
|
||||
const angleStep = (Math.PI * 2) / count
|
||||
|
||||
for (let level = 1; level <= 4; level += 1) {
|
||||
ctx.beginPath()
|
||||
const r = (radius * level) / 4
|
||||
for (let i = 0; i <= count; i += 1) {
|
||||
const angle = -Math.PI / 2 + i * angleStep
|
||||
const x = cx + r * Math.cos(angle)
|
||||
const y = cy + r * Math.sin(angle)
|
||||
if (i === 0) ctx.moveTo(x, y)
|
||||
else ctx.lineTo(x, y)
|
||||
}
|
||||
ctx.strokeStyle = COLORS.grid
|
||||
ctx.lineWidth = 1
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const angle = -Math.PI / 2 + i * angleStep
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(cx, cy)
|
||||
ctx.lineTo(cx + radius * Math.cos(angle), cy + radius * Math.sin(angle))
|
||||
ctx.strokeStyle = COLORS.grid
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
ctx.beginPath()
|
||||
values.forEach((val, i) => {
|
||||
const ratio = Math.min(1, Math.max(0, val / 100))
|
||||
const angle = -Math.PI / 2 + i * angleStep
|
||||
const x = cx + radius * ratio * Math.cos(angle)
|
||||
const y = cy + radius * ratio * Math.sin(angle)
|
||||
if (i === 0) ctx.moveTo(x, y)
|
||||
else ctx.lineTo(x, y)
|
||||
})
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = COLORS.fill
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = COLORS.accent
|
||||
ctx.lineWidth = 2
|
||||
ctx.stroke()
|
||||
|
||||
ctx.font = '11px sans-serif'
|
||||
ctx.fillStyle = COLORS.text
|
||||
ctx.textAlign = 'center'
|
||||
labels.forEach((label, i) => {
|
||||
const angle = -Math.PI / 2 + i * angleStep
|
||||
const x = cx + (radius + 18) * Math.cos(angle)
|
||||
const y = cy + (radius + 18) * Math.sin(angle) + 4
|
||||
ctx.fillText(label, x, y)
|
||||
})
|
||||
}
|
||||
|
||||
/** 绘制折线趋势图 */
|
||||
export function drawTrendChart(canvas, opts = {}) {
|
||||
if (!canvas) return
|
||||
const {
|
||||
width = 300,
|
||||
height = 160,
|
||||
points = [],
|
||||
dpr = 1,
|
||||
unit = ''
|
||||
} = opts
|
||||
const ctx = setupCanvas(canvas, width, height, dpr)
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
if (!points.length) {
|
||||
ctx.fillStyle = COLORS.text
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText('暂无趋势数据', width / 2, height / 2)
|
||||
return
|
||||
}
|
||||
|
||||
const pad = { top: 16, right: 12, bottom: 28, left: 12 }
|
||||
const chartW = width - pad.left - pad.right
|
||||
const chartH = height - pad.top - pad.bottom
|
||||
const values = points.map((p) => p.value)
|
||||
const min = Math.min(...values) * 0.95
|
||||
const max = Math.max(...values) * 1.05
|
||||
const range = max - min || 1
|
||||
|
||||
ctx.strokeStyle = COLORS.grid
|
||||
ctx.lineWidth = 1
|
||||
for (let i = 0; i <= 3; i += 1) {
|
||||
const y = pad.top + (chartH * i) / 3
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(pad.left, y)
|
||||
ctx.lineTo(width - pad.right, y)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
const coords = points.map((p, i) => ({
|
||||
x: pad.left + (chartW * i) / Math.max(1, points.length - 1),
|
||||
y: pad.top + chartH - ((p.value - min) / range) * chartH
|
||||
}))
|
||||
|
||||
ctx.beginPath()
|
||||
coords.forEach((pt, i) => {
|
||||
if (i === 0) ctx.moveTo(pt.x, pt.y)
|
||||
else ctx.lineTo(pt.x, pt.y)
|
||||
})
|
||||
ctx.strokeStyle = COLORS.line
|
||||
ctx.lineWidth = 2
|
||||
ctx.stroke()
|
||||
|
||||
coords.forEach((pt, i) => {
|
||||
ctx.beginPath()
|
||||
ctx.arc(pt.x, pt.y, 4, 0, Math.PI * 2)
|
||||
ctx.fillStyle = COLORS.accent
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = '#fff'
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.stroke()
|
||||
|
||||
ctx.fillStyle = COLORS.text
|
||||
ctx.font = '10px sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.fillText(points[i].label, pt.x, height - 8)
|
||||
})
|
||||
|
||||
if (unit) {
|
||||
ctx.fillStyle = COLORS.text
|
||||
ctx.font = '10px sans-serif'
|
||||
ctx.textAlign = 'left'
|
||||
ctx.fillText(unit, pad.left, pad.top - 2)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
import { bodyTestMock } from './mockData.js'
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
}
|
||||
|
||||
function formatRecordTime(date) {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
const h = String(date.getHours()).padStart(2, '0')
|
||||
const min = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${y}-${m}-${d} ${h}:${min}`
|
||||
}
|
||||
|
||||
function formatIsoDate(date) {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${d}`
|
||||
}
|
||||
|
||||
function formatTime(date) {
|
||||
const h = String(date.getHours()).padStart(2, '0')
|
||||
const min = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${h}:${min}`
|
||||
}
|
||||
|
||||
export function getDefaultBodyTestState() {
|
||||
return {
|
||||
settings: { ...bodyTestMock.settings },
|
||||
device: { ...bodyTestMock.device },
|
||||
records: clone(bodyTestMock.records)
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeBodyTestState(saved) {
|
||||
const defaults = getDefaultBodyTestState()
|
||||
if (!saved) return defaults
|
||||
return {
|
||||
settings: { ...defaults.settings, ...(saved.settings || {}) },
|
||||
device: { ...defaults.device, ...(saved.device || {}) },
|
||||
records: saved.records?.length ? saved.records : defaults.records
|
||||
}
|
||||
}
|
||||
|
||||
export function getLatestBodyTestRecord(store) {
|
||||
const records = store.bodyTest?.records || []
|
||||
return records.length ? { ...records[0] } : null
|
||||
}
|
||||
|
||||
export function getBodyTestRecordById(store, id) {
|
||||
const numId = Number(id)
|
||||
const record = (store.bodyTest?.records || []).find((item) => item.id === numId)
|
||||
return record ? { ...record } : null
|
||||
}
|
||||
|
||||
export function getBodyTestHistory(store, year) {
|
||||
let list = (store.bodyTest?.records || []).map((item) => ({ ...item }))
|
||||
if (year && year !== 'all') {
|
||||
list = list.filter((r) => r.date.startsWith(String(year)))
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
export function getBodyTestChangeBadge(record, previous) {
|
||||
if (!previous?.metrics || !record?.metrics) return null
|
||||
const diff = Math.round((record.metrics.bodyFat - previous.metrics.bodyFat) * 10) / 10
|
||||
if (diff === 0) return null
|
||||
const sign = diff > 0 ? '+' : ''
|
||||
return { key: 'bodyFat', text: `体脂率${sign}${diff}%`, good: diff < 0 }
|
||||
}
|
||||
|
||||
export function getBodyTestYears(store) {
|
||||
const years = new Set((store.bodyTest?.records || []).map((r) => r.date.slice(0, 4)))
|
||||
return ['all', ...Array.from(years).sort().reverse()]
|
||||
}
|
||||
|
||||
export function computeGrade(score) {
|
||||
if (score >= 90) return { grade: 'A', gradeLabel: '优秀' }
|
||||
if (score >= 80) return { grade: 'B+', gradeLabel: '良好' }
|
||||
if (score >= 70) return { grade: 'B', gradeLabel: '中等' }
|
||||
if (score >= 60) return { grade: 'C', gradeLabel: '一般' }
|
||||
return { grade: 'D', gradeLabel: '需改善' }
|
||||
}
|
||||
|
||||
export function computeScore(metrics) {
|
||||
const bmi = metrics.bmi || 22
|
||||
const bodyFat = metrics.bodyFat || 25
|
||||
const muscle = metrics.muscleMass || 22
|
||||
const bmiScore = bmi >= 18.5 && bmi <= 24 ? 90 : bmi >= 17 && bmi <= 27 ? 75 : 60
|
||||
const fatScore = bodyFat <= 22 ? 92 : bodyFat <= 26 ? 80 : bodyFat <= 30 ? 68 : 55
|
||||
const muscleScore = muscle >= 22 ? 88 : muscle >= 20 ? 76 : 62
|
||||
return Math.round((bmiScore + fatScore + muscleScore) / 3)
|
||||
}
|
||||
|
||||
export function computeChanges(current, previous) {
|
||||
if (!previous?.metrics) return {}
|
||||
const keys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr', 'bodyWater', 'boneMass']
|
||||
const changes = {}
|
||||
keys.forEach((key) => {
|
||||
const cur = Number(current.metrics[key])
|
||||
const prev = Number(previous.metrics[key])
|
||||
if (Number.isFinite(cur) && Number.isFinite(prev)) {
|
||||
const diff = Math.round((cur - prev) * 10) / 10
|
||||
changes[key] = diff
|
||||
}
|
||||
})
|
||||
return changes
|
||||
}
|
||||
|
||||
export function formatChangeValue(key, diff, unitSystem = 'metric') {
|
||||
if (diff === undefined || diff === null) return '--'
|
||||
const sign = diff > 0 ? '+' : ''
|
||||
const units = {
|
||||
weight: unitSystem === 'metric' ? 'kg' : 'lb',
|
||||
bodyFat: '%',
|
||||
muscleMass: 'kg',
|
||||
bmi: '',
|
||||
visceralFat: '级',
|
||||
bmr: 'kcal',
|
||||
bodyWater: '%',
|
||||
boneMass: 'kg'
|
||||
}
|
||||
const unit = units[key] || ''
|
||||
return `${sign}${diff}${unit}`
|
||||
}
|
||||
|
||||
export function buildBodyReportSummary(record, previous) {
|
||||
if (!record) {
|
||||
return {
|
||||
date: '--',
|
||||
weight: '--',
|
||||
bmi: '--',
|
||||
bodyFat: '--',
|
||||
bmr: '--',
|
||||
status: '暂无数据',
|
||||
change: '--'
|
||||
}
|
||||
}
|
||||
const changes = computeChanges(record, previous)
|
||||
const weightChange = changes.weight
|
||||
let changeText = '--'
|
||||
if (weightChange !== undefined) {
|
||||
const sign = weightChange > 0 ? '+' : ''
|
||||
changeText = `${sign}${weightChange}kg`
|
||||
}
|
||||
return {
|
||||
date: record.date,
|
||||
weight: String(record.metrics.weight),
|
||||
bmi: String(record.metrics.bmi),
|
||||
bodyFat: `${record.metrics.bodyFat}%`,
|
||||
bmr: String(record.metrics.bmr),
|
||||
status: record.status,
|
||||
change: changeText,
|
||||
recordId: record.id
|
||||
}
|
||||
}
|
||||
|
||||
export function getBodyTestTrendData(store, metricKey, limit = 6) {
|
||||
const records = [...(store.bodyTest?.records || [])].reverse().slice(-limit)
|
||||
return records.map((item) => ({
|
||||
id: item.id,
|
||||
date: item.date,
|
||||
label: item.date.slice(5),
|
||||
value: Number(item.metrics[metricKey]) || 0
|
||||
}))
|
||||
}
|
||||
|
||||
export function getCompareData(store, idA, idB) {
|
||||
const a = getBodyTestRecordById(store, idA)
|
||||
const b = getBodyTestRecordById(store, idB)
|
||||
if (!a || !b) return null
|
||||
const keys = ['weight', 'bmi', 'bodyFat', 'muscleMass', 'visceralFat', 'bmr']
|
||||
const metrics = keys.map((key) => ({
|
||||
key,
|
||||
label: bodyTestMock.metricDefs.find((m) => m.key === key)?.label || key,
|
||||
valueA: a.metrics[key],
|
||||
valueB: b.metrics[key],
|
||||
diff: Math.round((a.metrics[key] - b.metrics[key]) * 10) / 10
|
||||
}))
|
||||
return { recordA: a, recordB: b, metrics }
|
||||
}
|
||||
|
||||
export function getRecommendedCourses(record) {
|
||||
const ids = record?.recommendedCourseIds || []
|
||||
return bodyTestMock.recommendedCourses.filter((c) => ids.includes(c.id))
|
||||
}
|
||||
|
||||
export function updateBodyTestSettings(store, patch) {
|
||||
store.bodyTest.settings = { ...store.bodyTest.settings, ...patch }
|
||||
return store
|
||||
}
|
||||
|
||||
export function connectBodyTestDevice(store) {
|
||||
store.bodyTest.device = {
|
||||
...store.bodyTest.device,
|
||||
connected: true,
|
||||
battery: Math.min(100, (store.bodyTest.device.battery || 80) + Math.floor(Math.random() * 5)),
|
||||
lastConnected: formatRecordTime(new Date())
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
export function disconnectBodyTestDevice(store) {
|
||||
store.bodyTest.device = { ...store.bodyTest.device, connected: false }
|
||||
return store
|
||||
}
|
||||
|
||||
function nextRecordId(records) {
|
||||
return (records || []).reduce((max, item) => Math.max(max, item.id || 0), 0) + 1
|
||||
}
|
||||
|
||||
/** 模拟一次完整体测并写入记录 */
|
||||
export function saveSimulatedBodyTestRecord(store, finalMetrics) {
|
||||
const now = new Date()
|
||||
const previous = getLatestBodyTestRecord(store)
|
||||
const metrics = { ...finalMetrics }
|
||||
const heightCm = Number(store.profile?.height) || 165
|
||||
const heightM = heightCm / 100
|
||||
metrics.bmi = Math.round((metrics.weight / (heightM * heightM)) * 10) / 10
|
||||
|
||||
const score = computeScore(metrics)
|
||||
const { grade, gradeLabel } = computeGrade(score)
|
||||
const status = score >= 80 ? '比较健康' : score >= 70 ? '需关注' : '建议改善'
|
||||
|
||||
const radar = {
|
||||
weight: Math.min(95, Math.round(score * 0.9 + Math.random() * 5)),
|
||||
bodyFat: Math.min(95, Math.round(100 - metrics.bodyFat * 2.5)),
|
||||
muscle: Math.min(95, Math.round(metrics.muscleMass * 3.2)),
|
||||
bone: Math.min(95, Math.round(metrics.boneMass * 32)),
|
||||
water: Math.min(95, Math.round(metrics.bodyWater * 1.4)),
|
||||
bmr: Math.min(95, Math.round(metrics.bmr / 16))
|
||||
}
|
||||
|
||||
const record = {
|
||||
id: nextRecordId(store.bodyTest.records),
|
||||
date: formatIsoDate(now),
|
||||
time: formatTime(now),
|
||||
score,
|
||||
grade,
|
||||
gradeLabel,
|
||||
status,
|
||||
metrics,
|
||||
radar,
|
||||
bodySegments: clone(bodyTestMock.records[0].bodySegments),
|
||||
advice: clone(bodyTestMock.records[0].advice),
|
||||
recommendedCourseIds: [1, 2]
|
||||
}
|
||||
|
||||
if (previous) {
|
||||
record.changes = computeChanges(record, previous)
|
||||
}
|
||||
|
||||
store.bodyTest.records.unshift(record)
|
||||
store.bodyReport = buildBodyReportSummary(record, previous)
|
||||
return record
|
||||
}
|
||||
|
||||
/** 测量过程实时数据插值 */
|
||||
export function interpolateMeasuringMetrics(progress, profile) {
|
||||
const baseWeight = Number(profile?.weight) || 64
|
||||
const target = {
|
||||
weight: baseWeight - 0.3 + Math.random() * 0.2,
|
||||
bodyFat: 24.5 + Math.random() * 0.8,
|
||||
muscleMass: 22.4 + Math.random() * 0.3,
|
||||
visceralFat: 6,
|
||||
bmr: 1380 + Math.floor(Math.random() * 20),
|
||||
bodyWater: 52.5 + Math.random(),
|
||||
boneMass: 2.4,
|
||||
protein: 16.2
|
||||
}
|
||||
const ratio = Math.min(1, progress / 100)
|
||||
return {
|
||||
weight: Math.round((baseWeight + (target.weight - baseWeight) * ratio) * 10) / 10,
|
||||
bodyFat: Math.round((26 + (target.bodyFat - 26) * ratio) * 10) / 10,
|
||||
muscleMass: Math.round((21.5 + (target.muscleMass - 21.5) * ratio) * 10) / 10,
|
||||
bmr: Math.round(1340 + (target.bmr - 1340) * ratio),
|
||||
bodyWater: Math.round((51 + (target.bodyWater - 51) * ratio) * 10) / 10
|
||||
}
|
||||
}
|
||||
|
||||
export { bodyTestMock }
|
||||
@@ -0,0 +1,127 @@
|
||||
import { courseCatalogMock } from './mockData.js'
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
}
|
||||
|
||||
export function getDefaultCourseCatalog() {
|
||||
return clone(courseCatalogMock.courses)
|
||||
}
|
||||
|
||||
export function mergeCourseCatalog(saved) {
|
||||
const defaults = getDefaultCourseCatalog()
|
||||
if (!saved?.length) return defaults
|
||||
return saved.map((item, i) => ({ ...defaults[i], ...item }))
|
||||
}
|
||||
|
||||
function parseCourseStart(course) {
|
||||
const str = `${course.date} ${course.startTime}`.replace(/-/g, '/')
|
||||
return new Date(str)
|
||||
}
|
||||
|
||||
function getPeriod(hour) {
|
||||
if (hour < 12) return 'morning'
|
||||
if (hour < 18) return 'afternoon'
|
||||
return 'evening'
|
||||
}
|
||||
|
||||
export function filterCourses(courses, filters = {}) {
|
||||
const {
|
||||
date = '',
|
||||
weekDates = [],
|
||||
type = 'all',
|
||||
coach = '全部',
|
||||
period = 'all'
|
||||
} = filters
|
||||
|
||||
return courses.filter((c) => {
|
||||
if (type !== 'all' && c.type !== type) return false
|
||||
if (coach !== '全部' && c.coach !== coach) return false
|
||||
if (period !== 'all' && c.period !== period) return false
|
||||
if (date && c.date !== date) {
|
||||
if (!weekDates.length || !weekDates.includes(c.date)) return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
export function getCourseById(store, id) {
|
||||
const course = (store.courseCatalog || []).find((c) => c.id === Number(id))
|
||||
return course ? { ...course } : null
|
||||
}
|
||||
|
||||
export function canCancelBooking(item) {
|
||||
if (!item?.courseDate || !item?.startTime) return !!item?.canCancel
|
||||
const start = new Date(`${item.courseDate} ${item.startTime}`.replace(/-/g, '/'))
|
||||
const diff = start - Date.now()
|
||||
return diff >= 2 * 3600000
|
||||
}
|
||||
|
||||
export function bookCourse(store, courseId) {
|
||||
const course = store.courseCatalog.find((c) => c.id === Number(courseId))
|
||||
if (!course) return { ok: false, message: '课程不存在' }
|
||||
if (course.enrolled >= course.capacity) return { ok: false, message: '课程已约满' }
|
||||
const exists = store.ongoingBookings.some((b) => b.courseId === course.id)
|
||||
if (exists) return { ok: false, message: '您已预约该课程' }
|
||||
|
||||
course.enrolled += 1
|
||||
const nextId = store.ongoingBookings.reduce((m, b) => Math.max(m, b.id || 0), 0) + 1
|
||||
const parts = course.date.split('-')
|
||||
const booking = {
|
||||
id: nextId,
|
||||
courseId: course.id,
|
||||
title: course.title,
|
||||
banner: course.banner,
|
||||
status: 'booked',
|
||||
statusLabel: '已预约',
|
||||
schedule: `${parts[1]}月${parts[2]}日 ${course.startTime}-${course.endTime}`,
|
||||
dateDay: parts[2],
|
||||
dateMonth: `月${parts[2]}日`,
|
||||
timeRange: `${course.startTime}-${course.endTime}`,
|
||||
courseDate: course.date,
|
||||
startTime: course.startTime,
|
||||
coach: course.coach,
|
||||
coachShort: course.coach.replace('教练', ''),
|
||||
location: course.location,
|
||||
footerText: `可取消(需提前2小时,截止 ${parts[1]}/${parts[2]} ${course.startTime} 前2小时)`,
|
||||
canCancel: true,
|
||||
type: course.type
|
||||
}
|
||||
store.ongoingBookings.unshift(booking)
|
||||
return { ok: true, message: '预约成功', booking }
|
||||
}
|
||||
|
||||
export function getWeekDates(baseDateStr) {
|
||||
const base = baseDateStr ? new Date(baseDateStr.replace(/-/g, '/')) : new Date()
|
||||
const day = base.getDay() || 7
|
||||
const monday = new Date(base)
|
||||
monday.setDate(base.getDate() - day + 1)
|
||||
const dates = []
|
||||
for (let i = 0; i < 7; i += 1) {
|
||||
const d = new Date(monday)
|
||||
d.setDate(monday.getDate() + i)
|
||||
dates.push(formatIso(d))
|
||||
}
|
||||
return dates
|
||||
}
|
||||
|
||||
function formatIso(d) {
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
export function enrichCourseForDisplay(course) {
|
||||
const remaining = course.capacity - course.enrolled
|
||||
const percent = Math.round((course.enrolled / course.capacity) * 100)
|
||||
return {
|
||||
...course,
|
||||
remaining,
|
||||
percent,
|
||||
full: remaining <= 0,
|
||||
scarcityLabel: remaining > 0 && remaining <= 5 ? `仅剩${remaining}席` : ''
|
||||
}
|
||||
}
|
||||
|
||||
export { courseCatalogMock }
|
||||
@@ -0,0 +1,37 @@
|
||||
/** 手机号展示脱敏(中间四位 ****) */
|
||||
|
||||
export function maskPhone(phone) {
|
||||
if (phone == null || phone === '') return ''
|
||||
|
||||
const str = String(phone).trim()
|
||||
if (str.includes('****')) return str
|
||||
|
||||
const digits = str.replace(/\D/g, '')
|
||||
if (digits.length === 11) {
|
||||
return `${digits.slice(0, 3)}****${digits.slice(7)}`
|
||||
}
|
||||
if (digits.length > 4) {
|
||||
const hideLen = Math.min(4, digits.length - 3)
|
||||
const start = Math.floor((digits.length - hideLen) / 2)
|
||||
return `${digits.slice(0, start)}${'*'.repeat(hideLen)}${digits.slice(start + hideLen)}`
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
|
||||
/** 个人中心头部:138****6789 已绑定微信 */
|
||||
export function formatMemberCenterPhone(phone) {
|
||||
const masked = maskPhone(phone)
|
||||
return masked ? `${masked} 已绑定微信` : ''
|
||||
}
|
||||
|
||||
/** 保存前规范化:尽量存 11 位数字;已是脱敏串则原样保留 */
|
||||
export function normalizePhoneForStore(phone) {
|
||||
const str = String(phone || '').trim()
|
||||
if (!str) return ''
|
||||
if (str.includes('****')) return str
|
||||
|
||||
const digits = str.replace(/\D/g, '')
|
||||
if (digits.length >= 11) return digits.slice(0, 11)
|
||||
return digits || str
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
export { memberCenterMock, userInfoMock, fitnessGoalOptions, bookingMock, memberCardMock, bodyTestMock, moduleMock, courseCatalogMock } from './mockData.js'
|
||||
export { statusBarTimeMixin, subPageMixin } from './mixins.js'
|
||||
export {
|
||||
loadMemberStore,
|
||||
saveMemberStore,
|
||||
persistMemberStore,
|
||||
syncStats,
|
||||
computeRemainingDays,
|
||||
buildCardTip,
|
||||
formatUpcomingAlert,
|
||||
getBookingPreview,
|
||||
getCenterPageData,
|
||||
cancelOngoingBooking,
|
||||
renewMemberCard,
|
||||
parseLocalDate,
|
||||
saveUserProfile
|
||||
} from './store.js'
|
||||
export {
|
||||
getLatestBodyTestRecord,
|
||||
getBodyTestRecordById,
|
||||
getBodyTestHistory,
|
||||
computeChanges,
|
||||
formatChangeValue,
|
||||
buildBodyReportSummary,
|
||||
getBodyTestTrendData,
|
||||
getCompareData,
|
||||
getRecommendedCourses,
|
||||
getBodyTestChangeBadge,
|
||||
getBodyTestYears,
|
||||
updateBodyTestSettings,
|
||||
connectBodyTestDevice,
|
||||
disconnectBodyTestDevice,
|
||||
saveSimulatedBodyTestRecord,
|
||||
interpolateMeasuringMetrics,
|
||||
bodyTestMock
|
||||
} from './bodyTestStore.js'
|
||||
export {
|
||||
getTrainingReportData,
|
||||
getTrainingSessionById,
|
||||
filterTrainingSessions,
|
||||
getCouponsByStatus,
|
||||
getCouponById,
|
||||
useCoupon,
|
||||
deleteExpiredCoupon,
|
||||
getCouponCenterList,
|
||||
claimCouponFromCenter,
|
||||
getPointsPageData,
|
||||
redeemPointsReward,
|
||||
filterPointsHistory,
|
||||
getReferralPageData,
|
||||
getMyCoursesData,
|
||||
getMyCoursesByTab,
|
||||
getOnlineCourseById,
|
||||
updateOnlineProgress,
|
||||
getCheckInHistory,
|
||||
moduleMock
|
||||
} from './moduleStore.js'
|
||||
export {
|
||||
filterCourses,
|
||||
getCourseById,
|
||||
bookCourse,
|
||||
canCancelBooking,
|
||||
enrichCourseForDisplay,
|
||||
getWeekDates,
|
||||
courseCatalogMock
|
||||
} from './bookingStore.js'
|
||||
export { previewImage, persistChosenImage, isLocalFilePath } from './media.js'
|
||||
export { maskPhone, formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
|
||||
export {
|
||||
validateName,
|
||||
validatePhone,
|
||||
validatePhoneForRebind,
|
||||
validateHeight,
|
||||
validateWeight,
|
||||
validateBirthday,
|
||||
validateFitnessGoals,
|
||||
validateUserProfile,
|
||||
showValidationError
|
||||
} from './validate.js'
|
||||
@@ -0,0 +1,159 @@
|
||||
/** 头像等媒体:真机选图后须 saveFile,/static/ 须 getImageInfo */
|
||||
|
||||
function buildStaticPathCandidates(url) {
|
||||
const list = [url]
|
||||
if (url.startsWith('/')) {
|
||||
list.push(url.slice(1))
|
||||
} else {
|
||||
list.push(`/${url}`)
|
||||
}
|
||||
return [...new Set(list.filter(Boolean))]
|
||||
}
|
||||
|
||||
function isPackageStaticPath(url) {
|
||||
return /^(\/)?static\//i.test(url)
|
||||
}
|
||||
|
||||
/** chooseImage / saveFile 产生的本地路径(含真机 temp、usr、store) */
|
||||
export function isLocalFilePath(url) {
|
||||
if (!url) return false
|
||||
if (/^(wxfile:|file:|blob:|data:)/i.test(url)) return true
|
||||
if (/^https?:\/\/(tmp|usr|store)\//i.test(url)) return true
|
||||
if (/^https?:\/\//i.test(url) && !isPackageStaticPath(url)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
function showPreviewFail() {
|
||||
uni.showToast({ title: '无法预览头像', icon: 'none' })
|
||||
}
|
||||
|
||||
function openPreview(path, onFail) {
|
||||
if (!path) {
|
||||
;(onFail || showPreviewFail)()
|
||||
return
|
||||
}
|
||||
uni.previewImage({
|
||||
urls: [path],
|
||||
current: path,
|
||||
fail: () => (onFail ? onFail() : showPreviewFail())
|
||||
})
|
||||
}
|
||||
|
||||
function previewLocalFile(url) {
|
||||
openPreview(url, () => {
|
||||
uni.getImageInfo({
|
||||
src: url,
|
||||
success: (res) => {
|
||||
openPreview(res.path || url, showPreviewFail)
|
||||
},
|
||||
fail: showPreviewFail
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function tryGetImageInfo(candidates, index, onSuccess, onFail) {
|
||||
if (index >= candidates.length) {
|
||||
onFail()
|
||||
return
|
||||
}
|
||||
uni.getImageInfo({
|
||||
src: candidates[index],
|
||||
success: (res) => onSuccess(res.path || candidates[index]),
|
||||
fail: () => tryGetImageInfo(candidates, index + 1, onSuccess, onFail)
|
||||
})
|
||||
}
|
||||
|
||||
function getMpUserDataPath() {
|
||||
// #ifdef MP-WEIXIN
|
||||
return wx.env.USER_DATA_PATH
|
||||
// #endif
|
||||
return ''
|
||||
}
|
||||
|
||||
function tryCopyFile(candidates, index, onSuccess, onFail) {
|
||||
// #ifdef MP-WEIXIN
|
||||
const userPath = getMpUserDataPath()
|
||||
if (!userPath) {
|
||||
onFail()
|
||||
return
|
||||
}
|
||||
const fs = uni.getFileSystemManager()
|
||||
const extMatch = candidates[0]?.match(/\.(\w+)(?:\?|$)/)
|
||||
const ext = extMatch ? extMatch[1] : 'png'
|
||||
const dest = `${userPath}/avatar_preview_${Date.now()}.${ext}`
|
||||
|
||||
if (index >= candidates.length) {
|
||||
onFail()
|
||||
return
|
||||
}
|
||||
|
||||
fs.copyFile({
|
||||
srcPath: candidates[index],
|
||||
destPath: dest,
|
||||
success: () => onSuccess(dest),
|
||||
fail: () => tryCopyFile(candidates, index + 1, onSuccess, onFail)
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
onFail()
|
||||
// #endif
|
||||
}
|
||||
|
||||
function previewPackageStatic(url) {
|
||||
const candidates = buildStaticPathCandidates(url)
|
||||
tryGetImageInfo(
|
||||
candidates,
|
||||
0,
|
||||
(path) => openPreview(path, showPreviewFail),
|
||||
() => {
|
||||
tryCopyFile(
|
||||
candidates,
|
||||
0,
|
||||
(path) => openPreview(path, showPreviewFail),
|
||||
showPreviewFail
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/** 选图后将临时文件转为真机可预览、可持久化的本地路径 */
|
||||
export function persistChosenImage(tempPath) {
|
||||
return new Promise((resolve) => {
|
||||
const path = String(tempPath || '').trim()
|
||||
if (!path) {
|
||||
resolve('')
|
||||
return
|
||||
}
|
||||
|
||||
// #ifdef MP-WEIXIN
|
||||
uni.saveFile({
|
||||
tempFilePath: path,
|
||||
success: (res) => resolve(res.savedFilePath || path),
|
||||
fail: () => resolve(path)
|
||||
})
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN
|
||||
resolve(path)
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
export function previewImage(src, fallback = '') {
|
||||
const url = String(src || fallback || '').trim()
|
||||
if (!url) {
|
||||
uni.showToast({ title: '暂无头像', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (isLocalFilePath(url)) {
|
||||
previewLocalFile(url)
|
||||
return
|
||||
}
|
||||
|
||||
if (isPackageStaticPath(url)) {
|
||||
previewPackageStatic(url)
|
||||
return
|
||||
}
|
||||
|
||||
previewLocalFile(url)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { backToMemberCenter } from '../constants/routes.js'
|
||||
|
||||
/** 状态栏时间(Pixso 顶栏占位) */
|
||||
export const statusBarTimeMixin = {
|
||||
data() {
|
||||
return {
|
||||
statusBarTime: '9:41'
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
this.updateStatusBarTime()
|
||||
},
|
||||
methods: {
|
||||
updateStatusBarTime() {
|
||||
const now = new Date()
|
||||
this.statusBarTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 子页面返回个人中心 tab */
|
||||
export const subPageMixin = {
|
||||
methods: {
|
||||
goBack() {
|
||||
backToMemberCenter()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,884 @@
|
||||
/** 个人中心模块 mock 数据(后续可替换为 API) */
|
||||
|
||||
export const memberCenterMock = {
|
||||
userInfo: {
|
||||
name: '张小芳',
|
||||
phone: '13812345678 已绑定微信',
|
||||
memberLevel: '黄金会员',
|
||||
avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
||||
},
|
||||
stats: {
|
||||
checkInCount: 128,
|
||||
trainingHours: 23,
|
||||
pointsBalance: 1250
|
||||
},
|
||||
cardInfo: {
|
||||
name: '健身时长卡',
|
||||
detailTag: '详情',
|
||||
expireDate: '有效期至 2025年12月31日',
|
||||
remainingDays: 187,
|
||||
tip: '距离下次到期还有187天,请及时续费'
|
||||
},
|
||||
checkIns: [
|
||||
{
|
||||
id: 1,
|
||||
title: '今日签到 · 瑜伽初级班',
|
||||
time: '2024-07-12 09:05',
|
||||
tag: '团课',
|
||||
tagTheme: 'group'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '自由训练 · 进馆记录',
|
||||
time: '2024-07-11 18:30',
|
||||
tag: '自由',
|
||||
tagTheme: 'free'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '私教课 · 力量训练',
|
||||
time: '2024-07-10 14:00',
|
||||
tag: '私教',
|
||||
tagTheme: 'private'
|
||||
}
|
||||
],
|
||||
bodyReport: {
|
||||
date: '2024-07-01',
|
||||
weight: '63.5',
|
||||
bmi: '22.1',
|
||||
bodyFat: '24.8%',
|
||||
bmr: '165',
|
||||
status: '比较健康',
|
||||
change: '-1.2kg'
|
||||
},
|
||||
couponPoints: {
|
||||
amount: '¥50',
|
||||
couponDesc: '满500可用 · 1张',
|
||||
couponAction: '去使用',
|
||||
points: 1250,
|
||||
pointsLabel: '我的积分',
|
||||
pointsAction: '去兑换'
|
||||
},
|
||||
referral: {
|
||||
code: 'FIT-ZXF-2024',
|
||||
invited: 5,
|
||||
registered: 3,
|
||||
purchased: 2
|
||||
}
|
||||
}
|
||||
|
||||
export const userInfoMock = {
|
||||
name: '张小芳',
|
||||
phone: '13812345678',
|
||||
gender: 'female',
|
||||
birthday: '1995年06月15日',
|
||||
height: '165',
|
||||
weight: '63.5',
|
||||
fitnessGoals: ['减脂', '塑形'],
|
||||
avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AvatarEditWrap.png'
|
||||
}
|
||||
|
||||
export const fitnessGoalOptions = ['减脂', '塑形', '增肌', '提升耐力', '改善体态']
|
||||
|
||||
export const memberCardMock = {
|
||||
card: {
|
||||
name: '黄金健身时长卡',
|
||||
status: '生效中',
|
||||
validityStart: '2024年01月01日',
|
||||
validity: '2024年01月01日 - 2025年12月31日',
|
||||
validityEnd: '2025-12-31',
|
||||
remainingDays: 187
|
||||
},
|
||||
recordTabs: [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'consume', label: '消费' },
|
||||
{ key: 'checkin', label: '签到' }
|
||||
],
|
||||
records: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'checkin',
|
||||
title: '瑜伽初级班 · 团课签到',
|
||||
time: '2024-07-12 09:05',
|
||||
value: '-1次',
|
||||
valueType: 'negative',
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/dumbbell.png',
|
||||
iconTheme: 'orange'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'checkin',
|
||||
title: '自由进馆',
|
||||
time: '2024-07-11 18:30',
|
||||
value: '-1天',
|
||||
valueType: 'negative',
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/mappin.png',
|
||||
iconTheme: 'green'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'consume',
|
||||
title: '会员卡充值',
|
||||
time: '2024-07-01 10:00',
|
||||
value: '+90天',
|
||||
valueType: 'positive',
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/pluscircle.png',
|
||||
iconTheme: 'orange'
|
||||
}
|
||||
],
|
||||
rules: [
|
||||
'时长卡有效期内不限入场次数,但需提前预约团课',
|
||||
'卡到期后不退余额,请合理安排使用',
|
||||
'一卡仅限本人使用,不可转让'
|
||||
]
|
||||
}
|
||||
|
||||
/** 智能体测模块 mock 数据 */
|
||||
export const bodyTestMock = {
|
||||
settings: {
|
||||
autoSync: true,
|
||||
bluetoothEnabled: true,
|
||||
notifyOnComplete: true,
|
||||
shareAnonymous: false,
|
||||
unitSystem: 'metric'
|
||||
},
|
||||
device: {
|
||||
connected: false,
|
||||
name: 'InBody 270',
|
||||
model: 'IB-270',
|
||||
battery: 86,
|
||||
signal: 'strong',
|
||||
lastConnected: '2024-07-10 18:20'
|
||||
},
|
||||
connectSteps: [
|
||||
{ step: 1, title: '开启体测仪', desc: '长按电源键 3 秒,等待蓝牙指示灯闪烁' },
|
||||
{ step: 2, title: '靠近设备', desc: '将手机靠近体测仪 1 米范围内' },
|
||||
{ step: 3, title: '确认连接', desc: '点击下方按钮搜索并配对设备' }
|
||||
],
|
||||
metricDefs: [
|
||||
{ key: 'weight', label: '体重', unit: 'kg', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/target.png' },
|
||||
{ key: 'bmi', label: 'BMI', unit: '', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/activity.png' },
|
||||
{ key: 'bodyFat', label: '体脂率', unit: '%', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/trendingdown.png' },
|
||||
{ key: 'muscleMass', label: '肌肉量', unit: 'kg', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/dumbbell.png' },
|
||||
{ key: 'visceralFat', label: '内脏脂肪', unit: '级', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/alertcircle.png' },
|
||||
{ key: 'bmr', label: '基础代谢', unit: 'kcal', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock.png' },
|
||||
{ key: 'bodyWater', label: '体水分', unit: '%', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/shield.png' },
|
||||
{ key: 'boneMass', label: '骨量', unit: 'kg', icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user.png' }
|
||||
],
|
||||
radarLabels: [
|
||||
{ key: 'weight', label: '体重控制' },
|
||||
{ key: 'bodyFat', label: '体脂肪' },
|
||||
{ key: 'muscle', label: '肌肉量' },
|
||||
{ key: 'bone', label: '骨量' },
|
||||
{ key: 'water', label: '体水分' },
|
||||
{ key: 'bmr', label: '基础代谢' }
|
||||
],
|
||||
trendMetrics: [
|
||||
{ key: 'weight', label: '体重' },
|
||||
{ key: 'bodyFat', label: '体脂率' },
|
||||
{ key: 'muscleMass', label: '肌肉量' },
|
||||
{ key: 'bmi', label: 'BMI' }
|
||||
],
|
||||
recommendedCourses: [
|
||||
{
|
||||
id: 1,
|
||||
title: '燃脂 HIIT 团课',
|
||||
coach: '李明教练',
|
||||
schedule: '每周二、四 19:00',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
tag: '减脂推荐'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '核心力量塑形',
|
||||
coach: '王强教练',
|
||||
schedule: '每周一、三 18:30',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||
tag: '塑形推荐'
|
||||
}
|
||||
],
|
||||
records: [
|
||||
{
|
||||
id: 4,
|
||||
date: '2024-07-12',
|
||||
time: '09:05',
|
||||
score: 85,
|
||||
grade: 'B+',
|
||||
gradeLabel: '良好',
|
||||
status: '比较健康',
|
||||
bodyAge: 27,
|
||||
realAge: 29,
|
||||
metrics: {
|
||||
weight: 63.5,
|
||||
bmi: 22.1,
|
||||
bodyFat: 24.8,
|
||||
muscleMass: 22.6,
|
||||
visceralFat: 6,
|
||||
bmr: 1385,
|
||||
bodyWater: 52.8,
|
||||
boneMass: 2.42,
|
||||
protein: 16.4
|
||||
},
|
||||
radar: { weight: 78, bodyFat: 72, muscle: 74, bone: 81, water: 79, bmr: 73 },
|
||||
bodySegments: [
|
||||
{ part: '左臂', level: 'normal', value: '2.1kg' },
|
||||
{ part: '右臂', level: 'normal', value: '2.2kg' },
|
||||
{ part: '躯干', level: 'high', value: '28.5kg' },
|
||||
{ part: '左腿', level: 'normal', value: '8.6kg' },
|
||||
{ part: '右腿', level: 'normal', value: '8.7kg' }
|
||||
],
|
||||
advice: [
|
||||
'体脂率略高,建议增加有氧训练频率至每周 3-4 次',
|
||||
'核心肌群表现良好,可尝试进阶力量课程',
|
||||
'保持当前蛋白质摄入,有助于维持肌肉量'
|
||||
],
|
||||
recommendedCourseIds: [1, 2]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
date: '2024-06-28',
|
||||
time: '18:40',
|
||||
score: 82,
|
||||
grade: 'B+',
|
||||
gradeLabel: '良好',
|
||||
status: '比较健康',
|
||||
bodyAge: 28,
|
||||
realAge: 29,
|
||||
metrics: {
|
||||
weight: 64.7,
|
||||
bmi: 22.5,
|
||||
bodyFat: 25.3,
|
||||
muscleMass: 22.2,
|
||||
visceralFat: 7,
|
||||
bmr: 1370,
|
||||
bodyWater: 52.1,
|
||||
boneMass: 2.4,
|
||||
protein: 16.1
|
||||
},
|
||||
radar: { weight: 74, bodyFat: 68, muscle: 70, bone: 80, water: 76, bmr: 70 },
|
||||
bodySegments: [
|
||||
{ part: '左臂', level: 'normal', value: '2.0kg' },
|
||||
{ part: '右臂', level: 'normal', value: '2.1kg' },
|
||||
{ part: '躯干', level: 'high', value: '28.2kg' },
|
||||
{ part: '左腿', level: 'normal', value: '8.5kg' },
|
||||
{ part: '右腿', level: 'normal', value: '8.6kg' }
|
||||
],
|
||||
advice: [
|
||||
'体重较上次下降 0.8kg,减脂方向正确',
|
||||
'建议配合拉伸课程改善体态'
|
||||
],
|
||||
recommendedCourseIds: [1]
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: '2024-06-10',
|
||||
time: '10:15',
|
||||
score: 79,
|
||||
grade: 'B',
|
||||
gradeLabel: '中等',
|
||||
status: '需关注',
|
||||
bodyAge: 30,
|
||||
realAge: 29,
|
||||
metrics: {
|
||||
weight: 65.5,
|
||||
bmi: 22.8,
|
||||
bodyFat: 26.1,
|
||||
muscleMass: 21.8,
|
||||
visceralFat: 8,
|
||||
bmr: 1355,
|
||||
bodyWater: 51.5,
|
||||
boneMass: 2.38,
|
||||
protein: 15.8
|
||||
},
|
||||
radar: { weight: 70, bodyFat: 62, muscle: 66, bone: 78, water: 72, bmr: 66 },
|
||||
bodySegments: [
|
||||
{ part: '左臂', level: 'low', value: '1.9kg' },
|
||||
{ part: '右臂', level: 'normal', value: '2.0kg' },
|
||||
{ part: '躯干', level: 'high', value: '28.0kg' },
|
||||
{ part: '左腿', level: 'normal', value: '8.4kg' },
|
||||
{ part: '右腿', level: 'normal', value: '8.5kg' }
|
||||
],
|
||||
advice: [
|
||||
'内脏脂肪偏高,建议减少高糖饮食',
|
||||
'增加抗阻训练提升肌肉量'
|
||||
],
|
||||
recommendedCourseIds: [2]
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
date: '2024-05-20',
|
||||
time: '14:30',
|
||||
score: 76,
|
||||
grade: 'B',
|
||||
gradeLabel: '中等',
|
||||
status: '需关注',
|
||||
bodyAge: 31,
|
||||
realAge: 29,
|
||||
metrics: {
|
||||
weight: 66.2,
|
||||
bmi: 23.1,
|
||||
bodyFat: 26.8,
|
||||
muscleMass: 21.5,
|
||||
visceralFat: 9,
|
||||
bmr: 1340,
|
||||
bodyWater: 51.0,
|
||||
boneMass: 2.35,
|
||||
protein: 15.5
|
||||
},
|
||||
radar: { weight: 66, bodyFat: 58, muscle: 62, bone: 76, water: 68, bmr: 62 },
|
||||
bodySegments: [
|
||||
{ part: '左臂', level: 'low', value: '1.8kg' },
|
||||
{ part: '右臂', level: 'low', value: '1.9kg' },
|
||||
{ part: '躯干', level: 'high', value: '27.8kg' },
|
||||
{ part: '左腿', level: 'normal', value: '8.3kg' },
|
||||
{ part: '右腿', level: 'normal', value: '8.4kg' }
|
||||
],
|
||||
advice: [
|
||||
'建议制定 8 周减脂计划并定期复测',
|
||||
'每日饮水量建议达到 2000ml'
|
||||
],
|
||||
recommendedCourseIds: [1, 2]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
export const bookingMock = {
|
||||
upcomingAlert: '明天 09:00 有一堂瑜伽课,请提前 30 分钟到场',
|
||||
tabs: [
|
||||
{ key: 'ongoing', label: '进行中' },
|
||||
{ key: 'history', label: '历史预约' }
|
||||
],
|
||||
ongoing: [
|
||||
{
|
||||
id: 1,
|
||||
title: '瑜伽基础班',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
status: 'booked',
|
||||
statusLabel: '已预约',
|
||||
schedule: '07月15日 09:00-10:00',
|
||||
dateDay: '07',
|
||||
dateMonth: '月15日',
|
||||
timeRange: '09:00-10:00',
|
||||
coach: '李明教练',
|
||||
coachShort: '李明',
|
||||
location: '一楼 大厅',
|
||||
footerText: '可取消(截止 07/15 07:00)',
|
||||
canCancel: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '私教健身课',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||
status: 'pending',
|
||||
statusLabel: '待上课',
|
||||
schedule: '07月18日 14:00-15:00',
|
||||
dateDay: '07',
|
||||
dateMonth: '月18日',
|
||||
timeRange: '14:00-15:00',
|
||||
coach: '王强教练',
|
||||
coachShort: '王强',
|
||||
location: 'B区私教室',
|
||||
footerText: '地点:B区私教室',
|
||||
canCancel: true
|
||||
}
|
||||
],
|
||||
history: [
|
||||
{
|
||||
id: 3,
|
||||
title: '动感单车',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
status: 'completed',
|
||||
statusLabel: '已完成',
|
||||
schedule: '07月10日 19:00-20:00',
|
||||
coach: '赵敏教练',
|
||||
footerText: '已签到',
|
||||
canCancel: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '普拉提进阶',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||
status: 'cancelled',
|
||||
statusLabel: '已取消',
|
||||
schedule: '07月05日 10:00-11:00',
|
||||
coach: '李明教练',
|
||||
footerText: '用户主动取消',
|
||||
canCancel: false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/** 可预约课程 catalog */
|
||||
export const courseCatalogMock = {
|
||||
coaches: ['全部', '李明教练', '王强教练', '赵敏教练'],
|
||||
periodOptions: [
|
||||
{ key: 'all', label: '全部时段' },
|
||||
{ key: 'morning', label: '上午' },
|
||||
{ key: 'afternoon', label: '下午' },
|
||||
{ key: 'evening', label: '晚上' }
|
||||
],
|
||||
typeOptions: [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'group', label: '团课' },
|
||||
{ key: 'private', label: '私教' }
|
||||
],
|
||||
courses: [
|
||||
{
|
||||
id: 101,
|
||||
title: '瑜伽基础班',
|
||||
type: 'group',
|
||||
coach: '李明教练',
|
||||
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user0.png',
|
||||
date: '2024-07-15',
|
||||
startTime: '09:00',
|
||||
endTime: '10:00',
|
||||
location: '一楼大厅',
|
||||
enrolled: 12,
|
||||
capacity: 20,
|
||||
price: '次卡扣 1 次',
|
||||
payType: 'session',
|
||||
period: 'morning',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
intro: '适合零基础学员,重点提升柔韧性与呼吸控制。',
|
||||
suitable: '久坐办公族、初学者、想改善体态者',
|
||||
coachBio: '国家一级瑜伽指导员,5年教学经验',
|
||||
coachRating: 4.9,
|
||||
reviews: [
|
||||
{ user: '会员 A', score: 5, text: '教练讲解很细致,氛围很好' },
|
||||
{ user: '会员 B', score: 5, text: '适合新手,推荐' }
|
||||
],
|
||||
cancelRule: '至少提前 2 小时取消,否则视为爽约'
|
||||
},
|
||||
{
|
||||
id: 102,
|
||||
title: 'HIIT 燃脂团课',
|
||||
type: 'group',
|
||||
coach: '赵敏教练',
|
||||
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user1.png',
|
||||
date: '2024-07-15',
|
||||
startTime: '19:00',
|
||||
endTime: '20:00',
|
||||
location: '有氧区',
|
||||
enrolled: 18,
|
||||
capacity: 20,
|
||||
price: '时长卡',
|
||||
payType: 'duration',
|
||||
period: 'evening',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
intro: '高强度间歇训练,快速燃脂提升心肺。',
|
||||
suitable: '有一定运动基础、目标减脂者',
|
||||
coachBio: 'ACE 认证教练,擅长 HIIT 与动感单车',
|
||||
coachRating: 4.8,
|
||||
reviews: [{ user: '会员 C', score: 5, text: '强度够,出汗很多' }],
|
||||
cancelRule: '至少提前 2 小时取消'
|
||||
},
|
||||
{
|
||||
id: 103,
|
||||
title: '私教 · 力量训练',
|
||||
type: 'private',
|
||||
coach: '王强教练',
|
||||
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user2.png',
|
||||
date: '2024-07-16',
|
||||
startTime: '14:00',
|
||||
endTime: '15:00',
|
||||
location: 'B区私教室',
|
||||
enrolled: 1,
|
||||
capacity: 1,
|
||||
price: '私教课时卡',
|
||||
payType: 'private',
|
||||
period: 'afternoon',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||
intro: '一对一力量训练,定制训练计划。',
|
||||
suitable: '增肌塑形、康复训练',
|
||||
coachBio: 'NSCA 认证私教,8年从业经验',
|
||||
coachRating: 5.0,
|
||||
reviews: [{ user: '会员 D', score: 5, text: '非常专业' }],
|
||||
cancelRule: '至少提前 2 小时取消'
|
||||
},
|
||||
{
|
||||
id: 104,
|
||||
title: '普拉提进阶',
|
||||
type: 'group',
|
||||
coach: '李明教练',
|
||||
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user0.png',
|
||||
date: '2024-07-17',
|
||||
startTime: '10:30',
|
||||
endTime: '11:30',
|
||||
location: '二楼瑜伽室',
|
||||
enrolled: 8,
|
||||
capacity: 15,
|
||||
price: '次卡扣 1 次',
|
||||
payType: 'session',
|
||||
period: 'morning',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||
intro: '核心稳定与体态矫正进阶课程。',
|
||||
suitable: '有普拉提基础者',
|
||||
coachBio: '国家一级瑜伽指导员',
|
||||
coachRating: 4.9,
|
||||
reviews: [],
|
||||
cancelRule: '至少提前 2 小时取消'
|
||||
},
|
||||
{
|
||||
id: 105,
|
||||
title: '动感单车',
|
||||
type: 'group',
|
||||
coach: '赵敏教练',
|
||||
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user1.png',
|
||||
date: '2024-07-18',
|
||||
startTime: '18:30',
|
||||
endTime: '19:30',
|
||||
location: '单车房',
|
||||
enrolled: 20,
|
||||
capacity: 20,
|
||||
price: '储值卡 ¥39',
|
||||
payType: 'stored',
|
||||
period: 'evening',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
intro: '音乐骑行,团队氛围燃脂。',
|
||||
suitable: '所有级别,可调节阻力',
|
||||
coachBio: 'ACE 认证教练',
|
||||
coachRating: 4.7,
|
||||
reviews: [{ user: '会员 E', score: 4, text: '音乐很带感' }],
|
||||
cancelRule: '至少提前 2 小时取消'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
/** 个人中心其它模块 mock 数据 */
|
||||
export const moduleMock = {
|
||||
trainingReport: {
|
||||
periodLabel: '本周训练',
|
||||
summary: {
|
||||
sessions: 4,
|
||||
hours: 6.5,
|
||||
calories: 2180,
|
||||
streak: 3,
|
||||
visits: 5
|
||||
},
|
||||
monthlyHours: [
|
||||
{ label: '第1周', value: 4.2 },
|
||||
{ label: '第2周', value: 5.8 },
|
||||
{ label: '第3周', value: 6.5 },
|
||||
{ label: '第4周', value: 5.0 }
|
||||
],
|
||||
monthlyCalories: [
|
||||
{ label: '第1周', value: 1200 },
|
||||
{ label: '第2周', value: 1680 },
|
||||
{ label: '第3周', value: 2180 },
|
||||
{ label: '第4周', value: 1850 }
|
||||
],
|
||||
weeklyHours: [
|
||||
{ label: '一', value: 1.2 },
|
||||
{ label: '二', value: 0 },
|
||||
{ label: '三', value: 1.5 },
|
||||
{ label: '四', value: 0.8 },
|
||||
{ label: '五', value: 1.0 },
|
||||
{ label: '六', value: 2.0 },
|
||||
{ label: '日', value: 0 }
|
||||
],
|
||||
sessions: [
|
||||
{
|
||||
id: 1,
|
||||
title: '瑜伽基础班',
|
||||
coach: '李明教练',
|
||||
date: '2024-07-12',
|
||||
time: '09:00-10:00',
|
||||
duration: '60分钟',
|
||||
calories: 320,
|
||||
type: 'group',
|
||||
typeLabel: '团课'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '自由训练 · 力量',
|
||||
coach: '自主训练',
|
||||
date: '2024-07-11',
|
||||
time: '18:30-19:45',
|
||||
duration: '75分钟',
|
||||
calories: 480,
|
||||
type: 'free',
|
||||
typeLabel: '自由'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '私教 · 核心塑形',
|
||||
coach: '王强教练',
|
||||
date: '2024-07-10',
|
||||
time: '14:00-15:00',
|
||||
duration: '60分钟',
|
||||
calories: 410,
|
||||
type: 'private',
|
||||
typeLabel: '私教'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '动感单车',
|
||||
coach: '赵敏教练',
|
||||
date: '2024-07-08',
|
||||
time: '19:00-20:00',
|
||||
duration: '60分钟',
|
||||
calories: 520,
|
||||
type: 'group',
|
||||
typeLabel: '团课'
|
||||
}
|
||||
]
|
||||
},
|
||||
couponTabs: [
|
||||
{ key: 'available', label: '可用' },
|
||||
{ key: 'used', label: '已使用' },
|
||||
{ key: 'expired', label: '已过期' }
|
||||
],
|
||||
coupons: [
|
||||
{
|
||||
id: 1,
|
||||
status: 'available',
|
||||
amount: 50,
|
||||
title: '满500减50',
|
||||
desc: '全场团课/私教可用',
|
||||
expire: '2024-12-31',
|
||||
minSpend: 500,
|
||||
tag: '通用券',
|
||||
rules: '1. 满500元可用\n2. 适用于团课/私教\n3. 不可与其他优惠叠加\n4. 有效期至2024-12-31',
|
||||
scope: '全门店 · 团课/私教',
|
||||
flow: '选择课程 → 确认订单 → 选择优惠券 → 完成支付'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
status: 'available',
|
||||
amount: 30,
|
||||
title: '新人专享',
|
||||
desc: '首次购课立减',
|
||||
expire: '2024-08-31',
|
||||
minSpend: 200,
|
||||
tag: '新人券',
|
||||
rules: '1. 限新注册用户首次购课\n2. 满200可用',
|
||||
scope: '全门店 · 首次购课',
|
||||
flow: '首次预约课程时自动提示使用'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
status: 'used',
|
||||
amount: 20,
|
||||
title: '签到奖励券',
|
||||
desc: '连续签到7天获得',
|
||||
expire: '2024-07-01',
|
||||
minSpend: 100,
|
||||
tag: '奖励券',
|
||||
usedAt: '2024-06-28',
|
||||
rules: '满100可用',
|
||||
scope: '团课',
|
||||
flow: '预约时使用'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
status: 'expired',
|
||||
amount: 100,
|
||||
title: '周年庆特惠',
|
||||
desc: '满1000可用',
|
||||
expire: '2024-06-01',
|
||||
minSpend: 1000,
|
||||
tag: '活动券',
|
||||
rules: '满1000可用,已过期',
|
||||
scope: '全门店',
|
||||
flow: '—'
|
||||
}
|
||||
],
|
||||
couponCenter: [
|
||||
{
|
||||
id: 11,
|
||||
amount: 20,
|
||||
title: '周末团课券',
|
||||
desc: '周末团课满200减20',
|
||||
expireDays: 30,
|
||||
minSpend: 200,
|
||||
tag: '可领取',
|
||||
claimed: false
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
amount: 50,
|
||||
title: '私教体验券',
|
||||
desc: '私教课满500减50',
|
||||
expireDays: 15,
|
||||
minSpend: 500,
|
||||
tag: '限时',
|
||||
claimed: false
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
amount: 10,
|
||||
title: '签到加油券',
|
||||
desc: '无门槛10元券',
|
||||
expireDays: 7,
|
||||
minSpend: 0,
|
||||
tag: '每日',
|
||||
claimed: true
|
||||
}
|
||||
],
|
||||
pointsConfig: {
|
||||
rate: '100积分 = 1元',
|
||||
rule: '签到、训练、邀请好友、购课均可获得积分;积分可用于商城兑换。'
|
||||
},
|
||||
pointsRewards: [
|
||||
{ id: 1, name: '团课体验券', cost: 500, stock: 12, icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/ticket.png' },
|
||||
{ id: 2, name: '运动毛巾', cost: 800, stock: 5, icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/dumbbell.png' },
|
||||
{ id: 3, name: '私教体验30分钟', cost: 2000, stock: 3, icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/usercheck.png' },
|
||||
{ id: 4, name: '蛋白粉小样', cost: 350, stock: 20, icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/star.png' }
|
||||
],
|
||||
pointsHistory: [
|
||||
{ id: 1, type: 'earn', title: '团课签到', amount: 50, time: '2024-07-12 09:10', balance: 1250 },
|
||||
{ id: 2, type: 'earn', title: '邀请好友注册', amount: 200, time: '2024-07-08 15:30', balance: 1200 },
|
||||
{ id: 3, type: 'spend', title: '兑换团课体验券', amount: -500, time: '2024-07-01 11:00', balance: 1000 },
|
||||
{ id: 4, type: 'earn', title: '会员卡续费奖励', amount: 100, time: '2024-07-01 10:05', balance: 1500 },
|
||||
{ id: 5, type: 'earn', title: '体测完成奖励', amount: 30, time: '2024-06-28 18:45', balance: 1400 }
|
||||
],
|
||||
referralRules: [
|
||||
'好友通过您的邀请码注册,双方各得 100 积分',
|
||||
'好友首次购课成功后,您额外获得 300 积分',
|
||||
'每月邀请奖励上限 10 人,超出不再计奖',
|
||||
'积分可用于兑换课程体验券及周边礼品'
|
||||
],
|
||||
referralRecords: [
|
||||
{ id: 1, name: '李**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user0.png', status: 'purchased', statusLabel: '已购课', time: '2024-07-05', reward: '+300积分', rewardStatus: '已发放' },
|
||||
{ id: 2, name: '王**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user1.png', status: 'registered', statusLabel: '已注册', time: '2024-06-20', reward: '+100积分', rewardStatus: '已发放' },
|
||||
{ id: 3, name: '陈**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user2.png', status: 'invited', statusLabel: '已邀请', time: '2024-06-15', reward: '待注册', rewardStatus: '待发放' },
|
||||
{ id: 4, name: '赵**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user3.png', status: 'purchased', statusLabel: '已购课', time: '2024-06-01', reward: '+300积分', rewardStatus: '已发放' },
|
||||
{ id: 5, name: '刘**', avatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user0.png', status: 'registered', statusLabel: '已注册', time: '2024-05-28', reward: '+100积分', rewardStatus: '已发放' }
|
||||
],
|
||||
referralRewardSummary: {
|
||||
totalPoints: 800,
|
||||
totalCoupons: 2,
|
||||
pendingCount: 1
|
||||
},
|
||||
myCourseTabs: [
|
||||
{ key: 'group', label: '团课' },
|
||||
{ key: 'private', label: '私教' },
|
||||
{ key: 'online', label: '线上课' },
|
||||
{ key: 'package', label: '训练营' }
|
||||
],
|
||||
myCourses: {
|
||||
group: {
|
||||
ongoing: [
|
||||
{
|
||||
id: 1,
|
||||
title: '瑜伽基础班',
|
||||
coach: '李明教练',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
progress: 6,
|
||||
total: 12,
|
||||
schedule: '每周二、四 09:00',
|
||||
location: '一楼大厅',
|
||||
nextClass: '07月16日 09:00',
|
||||
canCancel: true,
|
||||
bookingId: 1
|
||||
}
|
||||
],
|
||||
completed: [
|
||||
{
|
||||
id: 3,
|
||||
title: '动感单车入门',
|
||||
coach: '赵敏教练',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
progress: 8,
|
||||
total: 8,
|
||||
schedule: '已结课',
|
||||
location: '单车房',
|
||||
completedAt: '2024-06-30',
|
||||
canEvaluate: true
|
||||
}
|
||||
]
|
||||
},
|
||||
private: {
|
||||
remaining: 7,
|
||||
coach: '王强教练',
|
||||
coachAvatar: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user2.png',
|
||||
nextClass: '07月15日 14:00',
|
||||
bookings: [
|
||||
{ id: 2, title: '私教 · 力量训练', time: '07月18日 14:00', status: '已预约', location: 'B区私教室' }
|
||||
],
|
||||
completed: [
|
||||
{ id: 5, title: '私教 · 核心塑形', time: '2024-07-10 14:00', coach: '王强教练' }
|
||||
]
|
||||
},
|
||||
online: [
|
||||
{
|
||||
id: 201,
|
||||
title: '居家核心训练',
|
||||
cover: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC2Banner.png',
|
||||
duration: '45分钟',
|
||||
progress: 60,
|
||||
chapters: 6,
|
||||
watched: 4,
|
||||
type: 'video'
|
||||
},
|
||||
{
|
||||
id: 202,
|
||||
title: '直播 · 晨间拉伸',
|
||||
cover: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
duration: '30分钟',
|
||||
progress: 0,
|
||||
liveTime: '07月20日 07:00',
|
||||
type: 'live'
|
||||
}
|
||||
],
|
||||
package: [
|
||||
{
|
||||
id: 301,
|
||||
title: '28天减脂训练营',
|
||||
banner: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/AC1Banner.png',
|
||||
progress: 3,
|
||||
total: 10,
|
||||
coach: '李明教练',
|
||||
schedule: '每周5练'
|
||||
}
|
||||
]
|
||||
},
|
||||
checkInTabs: [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: 'group', label: '团课' },
|
||||
{ key: 'private', label: '私教' },
|
||||
{ key: 'free', label: '自由' }
|
||||
],
|
||||
checkInHistory: [
|
||||
{
|
||||
id: 1,
|
||||
title: '今日签到 · 瑜伽初级班',
|
||||
time: '2024-07-12 09:05',
|
||||
tag: '团课',
|
||||
tagTheme: 'group',
|
||||
location: '一楼大厅'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '自由训练 · 进馆记录',
|
||||
time: '2024-07-11 18:30',
|
||||
tag: '自由',
|
||||
tagTheme: 'free',
|
||||
location: '器械区'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '私教课 · 力量训练',
|
||||
time: '2024-07-10 14:00',
|
||||
tag: '私教',
|
||||
tagTheme: 'private',
|
||||
location: 'B区私教室'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '团课签到 · 动感单车',
|
||||
time: '2024-07-08 19:02',
|
||||
tag: '团课',
|
||||
tagTheme: 'group',
|
||||
location: '单车房'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '自由训练 · 进馆记录',
|
||||
time: '2024-07-06 17:45',
|
||||
tag: '自由',
|
||||
tagTheme: 'free',
|
||||
location: '有氧区'
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import { moduleMock } from './mockData.js'
|
||||
|
||||
|
||||
|
||||
function clone(value) {
|
||||
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getDefaultModuleState() {
|
||||
|
||||
return {
|
||||
|
||||
trainingReport: clone(moduleMock.trainingReport),
|
||||
|
||||
coupons: clone(moduleMock.coupons),
|
||||
|
||||
couponCenter: clone(moduleMock.couponCenter),
|
||||
|
||||
pointsHistory: clone(moduleMock.pointsHistory),
|
||||
|
||||
pointsRewards: clone(moduleMock.pointsRewards),
|
||||
|
||||
redeemRecords: [],
|
||||
|
||||
referralRecords: clone(moduleMock.referralRecords),
|
||||
|
||||
myCourses: clone(moduleMock.myCourses),
|
||||
|
||||
checkInHistory: clone(moduleMock.checkInHistory)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function mergeModuleState(saved) {
|
||||
|
||||
const defaults = getDefaultModuleState()
|
||||
|
||||
if (!saved) return defaults
|
||||
|
||||
return {
|
||||
|
||||
trainingReport: { ...defaults.trainingReport, ...(saved.trainingReport || {}) },
|
||||
|
||||
coupons: saved.coupons?.length ? saved.coupons : defaults.coupons,
|
||||
|
||||
couponCenter: saved.couponCenter?.length ? saved.couponCenter : defaults.couponCenter,
|
||||
|
||||
pointsHistory: saved.pointsHistory?.length ? saved.pointsHistory : defaults.pointsHistory,
|
||||
|
||||
pointsRewards: saved.pointsRewards?.length ? saved.pointsRewards : defaults.pointsRewards,
|
||||
|
||||
redeemRecords: saved.redeemRecords || defaults.redeemRecords,
|
||||
|
||||
referralRecords: saved.referralRecords?.length ? saved.referralRecords : defaults.referralRecords,
|
||||
|
||||
myCourses: saved.myCourses ? mergeMyCourses(defaults.myCourses, saved.myCourses) : defaults.myCourses,
|
||||
|
||||
checkInHistory: saved.checkInHistory?.length ? saved.checkInHistory : defaults.checkInHistory
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function mergeMyCourses(defaults, saved) {
|
||||
|
||||
return {
|
||||
|
||||
group: saved.group || defaults.group,
|
||||
|
||||
private: saved.private || defaults.private,
|
||||
|
||||
online: saved.online?.length ? saved.online : defaults.online,
|
||||
|
||||
package: saved.package?.length ? saved.package : defaults.package
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
function syncCouponSummary(store) {
|
||||
|
||||
const available = store.modules.coupons.filter((c) => c.status === 'available')
|
||||
|
||||
const top = available[0]
|
||||
|
||||
store.couponPoints = {
|
||||
|
||||
...store.couponPoints,
|
||||
|
||||
amount: top ? `¥${top.amount}` : '暂无',
|
||||
|
||||
couponDesc: top
|
||||
|
||||
? `满${top.minSpend}可用 · ${available.length}张`
|
||||
|
||||
: '暂无可用优惠券',
|
||||
|
||||
couponAction: available.length ? '去使用' : '去领取',
|
||||
|
||||
points: store.stats.pointsBalance,
|
||||
|
||||
pointsLabel: '我的积分',
|
||||
|
||||
pointsAction: '去兑换'
|
||||
|
||||
}
|
||||
|
||||
return store
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function finalizeModules(store) {
|
||||
|
||||
syncCouponSummary(store)
|
||||
|
||||
store.checkIns = store.modules.checkInHistory.slice(0, 3).map((item) => ({ ...item }))
|
||||
|
||||
return store
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getTrainingReportData(store, period = 'week') {
|
||||
|
||||
const report = store.modules.trainingReport
|
||||
|
||||
const trend = period === 'month' ? report.monthlyHours : report.weeklyHours
|
||||
|
||||
const calTrend = period === 'month' ? report.monthlyCalories : report.weeklyHours.map((w, i) => ({
|
||||
|
||||
label: w.label,
|
||||
|
||||
value: Math.round((report.summary.calories / 7) * (w.value || 0.5))
|
||||
|
||||
}))
|
||||
|
||||
return {
|
||||
|
||||
...report,
|
||||
|
||||
period,
|
||||
|
||||
summary: {
|
||||
|
||||
...report.summary,
|
||||
|
||||
hours: store.stats.trainingHours ?? report.summary.hours,
|
||||
|
||||
visits: report.summary.visits ?? store.stats.checkInCount ?? 5
|
||||
|
||||
},
|
||||
|
||||
trendHours: trend.map((t) => ({ ...t, id: t.label })),
|
||||
|
||||
trendCalories: calTrend.map((t) => ({ ...t, id: t.label })),
|
||||
|
||||
sessions: report.sessions.map((s) => ({ ...s }))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getTrainingSessionById(store, id) {
|
||||
|
||||
const session = store.modules.trainingReport.sessions.find((s) => s.id === Number(id))
|
||||
|
||||
if (!session) return null
|
||||
|
||||
return {
|
||||
|
||||
...session,
|
||||
|
||||
heartRate: '128 bpm',
|
||||
|
||||
comment: '动作标准,核心发力良好,下次可增加负重。',
|
||||
|
||||
checkInTime: `${session.date} ${session.time.split('-')[0]}`
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function filterTrainingSessions(store, filters = {}) {
|
||||
|
||||
let list = store.modules.trainingReport.sessions.map((s) => ({ ...s }))
|
||||
|
||||
if (filters.type && filters.type !== 'all') {
|
||||
|
||||
list = list.filter((s) => s.type === filters.type)
|
||||
|
||||
}
|
||||
|
||||
return list
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getCouponsByStatus(store, status) {
|
||||
|
||||
return store.modules.coupons.filter((c) => c.status === status).map((c) => ({ ...c }))
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getCouponById(store, id) {
|
||||
|
||||
const c = store.modules.coupons.find((item) => item.id === Number(id))
|
||||
|
||||
return c ? { ...c } : null
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function useCoupon(store, id) {
|
||||
|
||||
const coupon = store.modules.coupons.find((c) => c.id === id)
|
||||
|
||||
if (!coupon || coupon.status !== 'available') return null
|
||||
|
||||
coupon.status = 'used'
|
||||
|
||||
coupon.usedAt = new Date().toISOString().slice(0, 10)
|
||||
|
||||
syncCouponSummary(store)
|
||||
|
||||
return coupon
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function deleteExpiredCoupon(store, id) {
|
||||
|
||||
const idx = store.modules.coupons.findIndex((c) => c.id === id && c.status === 'expired')
|
||||
|
||||
if (idx >= 0) store.modules.coupons.splice(idx, 1)
|
||||
|
||||
syncCouponSummary(store)
|
||||
|
||||
return store
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function getCouponCenterList(store) {
|
||||
|
||||
return store.modules.couponCenter.map((c) => ({ ...c }))
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import {
|
||||
memberCenterMock,
|
||||
userInfoMock,
|
||||
memberCardMock,
|
||||
bookingMock
|
||||
} from './mockData.js'
|
||||
import { formatMemberCenterPhone, normalizePhoneForStore } from './format.js'
|
||||
import {
|
||||
getDefaultBodyTestState,
|
||||
mergeBodyTestState,
|
||||
getLatestBodyTestRecord,
|
||||
buildBodyReportSummary
|
||||
} from './bodyTestStore.js'
|
||||
import {
|
||||
getDefaultModuleState,
|
||||
mergeModuleState,
|
||||
finalizeModules
|
||||
} from './moduleStore.js'
|
||||
import {
|
||||
getDefaultCourseCatalog,
|
||||
mergeCourseCatalog,
|
||||
canCancelBooking
|
||||
} from './bookingStore.js'
|
||||
|
||||
const STORAGE_KEY = 'gym_member_info_v1'
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value))
|
||||
}
|
||||
|
||||
export function buildCardTip(remainingDays) {
|
||||
return `距离下次到期还有${remainingDays}天,请及时续费`
|
||||
}
|
||||
|
||||
function applyCardInfo(store) {
|
||||
const days = computeRemainingDays(store.card.validityEnd)
|
||||
store.card.remainingDays = days
|
||||
store.cardInfo.remainingDays = days
|
||||
store.cardInfo.tip = buildCardTip(days)
|
||||
return store
|
||||
}
|
||||
|
||||
export function syncStats(store) {
|
||||
store.stats = {
|
||||
...store.stats,
|
||||
pointsBalance: store.stats.pointsBalance ?? 1250
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
function finalizeStore(store) {
|
||||
syncStats(store)
|
||||
applyCardInfo(store)
|
||||
if (store.profile?.avatar) {
|
||||
store.memberProfile.avatar = store.profile.avatar
|
||||
}
|
||||
if (store.profile?.phone) {
|
||||
store.memberProfile.phone = formatMemberCenterPhone(store.profile.phone)
|
||||
}
|
||||
const latestBodyTest = getLatestBodyTestRecord(store)
|
||||
if (latestBodyTest) {
|
||||
const previous = store.bodyTest.records[1]
|
||||
store.bodyReport = buildBodyReportSummary(latestBodyTest, previous)
|
||||
}
|
||||
if (store.modules) {
|
||||
finalizeModules(store)
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
function getDefaultStore() {
|
||||
return finalizeStore({
|
||||
profile: { ...userInfoMock, avatar: memberCenterMock.userInfo.avatar },
|
||||
memberProfile: { ...memberCenterMock.userInfo },
|
||||
stats: { ...memberCenterMock.stats },
|
||||
cardInfo: { ...memberCenterMock.cardInfo },
|
||||
card: { ...memberCardMock.card },
|
||||
records: clone(memberCardMock.records),
|
||||
ongoingBookings: clone(bookingMock.ongoing),
|
||||
historyBookings: clone(bookingMock.history),
|
||||
checkIns: clone(memberCenterMock.checkIns),
|
||||
bodyReport: { ...memberCenterMock.bodyReport },
|
||||
bodyTest: getDefaultBodyTestState(),
|
||||
modules: getDefaultModuleState(),
|
||||
courseCatalog: getDefaultCourseCatalog(),
|
||||
couponPoints: { ...memberCenterMock.couponPoints },
|
||||
referral: { ...memberCenterMock.referral }
|
||||
})
|
||||
}
|
||||
|
||||
function mergeDefaults(saved) {
|
||||
const defaults = getDefaultStore()
|
||||
return finalizeStore({
|
||||
profile: { ...defaults.profile, ...(saved.profile || {}) },
|
||||
memberProfile: { ...defaults.memberProfile, ...(saved.memberProfile || {}) },
|
||||
stats: { ...defaults.stats, ...(saved.stats || {}) },
|
||||
cardInfo: { ...defaults.cardInfo, ...(saved.cardInfo || {}) },
|
||||
card: { ...defaults.card, ...(saved.card || {}) },
|
||||
records: saved.records?.length ? saved.records : defaults.records,
|
||||
ongoingBookings: saved.ongoingBookings ?? defaults.ongoingBookings,
|
||||
historyBookings: saved.historyBookings ?? defaults.historyBookings,
|
||||
checkIns: saved.checkIns?.length ? saved.checkIns : defaults.checkIns,
|
||||
bodyReport: { ...defaults.bodyReport, ...(saved.bodyReport || {}) },
|
||||
bodyTest: mergeBodyTestState(saved.bodyTest),
|
||||
modules: mergeModuleState(saved.modules),
|
||||
courseCatalog: mergeCourseCatalog(saved.courseCatalog),
|
||||
couponPoints: { ...defaults.couponPoints, ...(saved.couponPoints || {}) },
|
||||
referral: { ...defaults.referral, ...(saved.referral || {}) }
|
||||
})
|
||||
}
|
||||
|
||||
export function loadMemberStore() {
|
||||
try {
|
||||
const saved = uni.getStorageSync(STORAGE_KEY)
|
||||
if (saved && typeof saved === 'object') {
|
||||
return mergeDefaults(saved)
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[memberStore] load failed', e)
|
||||
}
|
||||
return getDefaultStore()
|
||||
}
|
||||
|
||||
export function saveMemberStore(store) {
|
||||
uni.setStorageSync(STORAGE_KEY, store)
|
||||
}
|
||||
|
||||
/** 解析为本地 0 点,避免 ISO 字符串时区偏差 */
|
||||
export function parseLocalDate(dateStr) {
|
||||
if (!dateStr) return null
|
||||
const str = String(dateStr).trim()
|
||||
const iso = str.match(/^(\d{4})-(\d{2})-(\d{2})$/)
|
||||
if (iso) {
|
||||
return new Date(Number(iso[1]), Number(iso[2]) - 1, Number(iso[3]))
|
||||
}
|
||||
const cn = str.match(/(\d{4})年(\d{2})月(\d{2})日/)
|
||||
if (cn) {
|
||||
return new Date(Number(cn[1]), Number(cn[2]) - 1, Number(cn[3]))
|
||||
}
|
||||
const parsed = new Date(str.replace(/-/g, '/'))
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed
|
||||
}
|
||||
|
||||
function formatIsoDate(date) {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${d}`
|
||||
}
|
||||
|
||||
function formatChineseDate(date) {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${y}年${m}月${day}日`
|
||||
}
|
||||
|
||||
function formatRecordTime(date) {
|
||||
const y = date.getFullYear()
|
||||
const m = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const d = String(date.getDate()).padStart(2, '0')
|
||||
const h = String(date.getHours()).padStart(2, '0')
|
||||
const min = String(date.getMinutes()).padStart(2, '0')
|
||||
return `${y}-${m}-${d} ${h}:${min}`
|
||||
}
|
||||
|
||||
function nextRecordId(records) {
|
||||
return (records || []).reduce((max, item) => Math.max(max, item.id || 0), 0) + 1
|
||||
}
|
||||
|
||||
export function computeRemainingDays(endDateStr) {
|
||||
const end = parseLocalDate(endDateStr)
|
||||
if (!end) return 0
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
end.setHours(0, 0, 0, 0)
|
||||
const diff = Math.ceil((end - now) / 86400000)
|
||||
return Math.max(0, diff)
|
||||
}
|
||||
|
||||
export function formatUpcomingAlert(booking) {
|
||||
if (!booking) return ''
|
||||
const timePart = booking.timeRange || booking.schedule?.split(' ')[1] || ''
|
||||
return `明天 ${timePart} 有一堂${booking.title},请提前 30 分钟到场`
|
||||
}
|
||||
|
||||
export function toBookingPreviewItem(item) {
|
||||
return {
|
||||
id: item.id,
|
||||
dateDay: item.dateDay,
|
||||
dateMonth: item.dateMonth,
|
||||
desc: `${item.title} · ${item.timeRange}`,
|
||||
coach: item.coachShort || item.coach.replace('教练', ''),
|
||||
location: item.location ? `地点:${item.location}` : '',
|
||||
status: item.status,
|
||||
statusLabel: item.statusLabel
|
||||
}
|
||||
}
|
||||
|
||||
export function getBookingPreview(store, limit = 2) {
|
||||
return store.ongoingBookings.slice(0, limit).map(toBookingPreviewItem)
|
||||
}
|
||||
|
||||
export function getCenterPageData(store) {
|
||||
return {
|
||||
userInfo: { ...store.memberProfile },
|
||||
stats: { ...store.stats },
|
||||
cardInfo: { ...store.cardInfo },
|
||||
bookingPreview: getBookingPreview(store),
|
||||
checkIns: store.checkIns.map((item) => ({ ...item })),
|
||||
bodyReport: {
|
||||
...store.bodyReport,
|
||||
weight: store.profile.weight || store.bodyReport.weight
|
||||
},
|
||||
couponPoints: {
|
||||
...store.couponPoints,
|
||||
points: store.stats.pointsBalance
|
||||
},
|
||||
referral: { ...store.referral }
|
||||
}
|
||||
}
|
||||
|
||||
export function cancelOngoingBooking(store, id) {
|
||||
const index = store.ongoingBookings.findIndex((b) => b.id === id)
|
||||
if (index < 0) return { ok: false, message: '预约不存在' }
|
||||
|
||||
const item = store.ongoingBookings[index]
|
||||
if (!canCancelBooking(item)) {
|
||||
return { ok: false, message: '距开课不足2小时,无法取消' }
|
||||
}
|
||||
|
||||
const [removed] = store.ongoingBookings.splice(index, 1)
|
||||
if (removed.courseId) {
|
||||
const course = store.courseCatalog.find((c) => c.id === removed.courseId)
|
||||
if (course && course.enrolled > 0) course.enrolled -= 1
|
||||
}
|
||||
store.historyBookings.unshift({
|
||||
...removed,
|
||||
status: 'cancelled',
|
||||
statusLabel: '已取消',
|
||||
footerText: '用户主动取消',
|
||||
canCancel: false
|
||||
})
|
||||
finalizeStore(store)
|
||||
saveMemberStore(store)
|
||||
return { ok: true, message: '已取消' }
|
||||
}
|
||||
|
||||
export function renewMemberCard(store, addDays = 90) {
|
||||
const now = new Date()
|
||||
now.setHours(0, 0, 0, 0)
|
||||
|
||||
let base = parseLocalDate(store.card.validityEnd) || new Date(now)
|
||||
base.setHours(0, 0, 0, 0)
|
||||
// 已过期:从今天起续费;未过期:从当前到期日起顺延
|
||||
if (base < now) {
|
||||
base = new Date(now)
|
||||
}
|
||||
|
||||
const end = new Date(base)
|
||||
end.setDate(end.getDate() + addDays)
|
||||
|
||||
const validityEnd = formatIsoDate(end)
|
||||
const validityEndCn = formatChineseDate(end)
|
||||
store.card.validityEnd = validityEnd
|
||||
store.card.validity = store.card.validityStart
|
||||
? `${store.card.validityStart} - ${validityEndCn}`
|
||||
: `2024年01月01日 - ${validityEndCn}`
|
||||
store.cardInfo.expireDate = `有效期至 ${validityEndCn}`
|
||||
|
||||
store.records.unshift({
|
||||
id: nextRecordId(store.records),
|
||||
type: 'consume',
|
||||
title: '会员卡续费',
|
||||
time: formatRecordTime(new Date()),
|
||||
value: `+${addDays}天`,
|
||||
valueType: 'positive',
|
||||
icon: 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/pluscircle.png',
|
||||
iconTheme: 'orange'
|
||||
})
|
||||
|
||||
finalizeStore(store)
|
||||
saveMemberStore(store)
|
||||
return store
|
||||
}
|
||||
|
||||
export function saveUserProfile(store, profile) {
|
||||
const phone = normalizePhoneForStore(profile.phone ?? store.profile.phone)
|
||||
store.profile = { ...store.profile, ...profile, phone }
|
||||
store.memberProfile = {
|
||||
...store.memberProfile,
|
||||
name: store.profile.name,
|
||||
phone: formatMemberCenterPhone(store.profile.phone),
|
||||
avatar: store.profile.avatar || store.memberProfile.avatar
|
||||
}
|
||||
if (store.profile.weight) {
|
||||
store.bodyReport.weight = store.profile.weight
|
||||
}
|
||||
finalizeStore(store)
|
||||
saveMemberStore(store)
|
||||
return store
|
||||
}
|
||||
|
||||
export function persistMemberStore(store) {
|
||||
finalizeStore(store)
|
||||
saveMemberStore(store)
|
||||
return store
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/** 个人信息页前端校验(与后端手机号规则对齐:^1[3-9]\\d{9}$) */
|
||||
|
||||
const PHONE_REG = /^1[3-9]\d{9}$/
|
||||
const MIN_NAME_LEN = 2
|
||||
const MAX_NAME_LEN = 8
|
||||
const NAME_REG = new RegExp(
|
||||
`^[\\u4e00-\\u9fa5a-zA-Z·\\s]{${MIN_NAME_LEN},${MAX_NAME_LEN}}$`
|
||||
)
|
||||
const MEASURE_REG = /^\d+(\.\d)?$/
|
||||
|
||||
const MIN_HEIGHT = 50
|
||||
const MAX_HEIGHT = 250
|
||||
const MIN_WEIGHT = 20
|
||||
const MAX_WEIGHT = 300
|
||||
const MIN_BIRTH_YEAR = 1900
|
||||
const MIN_AGE = 14
|
||||
const MAX_FITNESS_GOALS = 5
|
||||
|
||||
export function isMaskedPhone(phone) {
|
||||
return String(phone || '').includes('****')
|
||||
}
|
||||
|
||||
export function validateName(name) {
|
||||
const value = String(name ?? '').trim()
|
||||
if (!value) {
|
||||
return { ok: false, message: '请输入姓名' }
|
||||
}
|
||||
if (!NAME_REG.test(value)) {
|
||||
return { ok: false, message: `姓名为${MIN_NAME_LEN}-${MAX_NAME_LEN}个汉字或字母` }
|
||||
}
|
||||
return { ok: true, value }
|
||||
}
|
||||
|
||||
/** 保存时使用:允许保留已脱敏的旧手机号 */
|
||||
export function validatePhone(phone, options = {}) {
|
||||
const { allowMasked = true } = options
|
||||
const raw = String(phone ?? '').trim()
|
||||
if (!raw) {
|
||||
return { ok: false, message: '请绑定手机号' }
|
||||
}
|
||||
if (allowMasked && isMaskedPhone(raw)) {
|
||||
const digits = raw.replace(/\D/g, '')
|
||||
if (digits.length >= 7) {
|
||||
return { ok: true, value: raw }
|
||||
}
|
||||
return { ok: false, message: '手机号格式不正确' }
|
||||
}
|
||||
|
||||
const digits = raw.replace(/\D/g, '')
|
||||
if (!PHONE_REG.test(digits)) {
|
||||
return { ok: false, message: '请输入11位有效手机号' }
|
||||
}
|
||||
return { ok: true, value: digits }
|
||||
}
|
||||
|
||||
/** 换绑时必须输入完整新号 */
|
||||
export function validatePhoneForRebind(phone) {
|
||||
return validatePhone(phone, { allowMasked: false })
|
||||
}
|
||||
|
||||
function parseMeasure(value) {
|
||||
const str = String(value ?? '').trim()
|
||||
if (!str || !MEASURE_REG.test(str)) {
|
||||
return null
|
||||
}
|
||||
const num = Number(str)
|
||||
return Number.isFinite(num) ? num : null
|
||||
}
|
||||
|
||||
function formatMeasure(num) {
|
||||
return Number.isInteger(num) ? String(num) : String(Number(num.toFixed(1)))
|
||||
}
|
||||
|
||||
export function validateHeight(height) {
|
||||
const num = parseMeasure(height)
|
||||
if (num == null) {
|
||||
return { ok: false, message: '请输入有效身高(单位 cm)' }
|
||||
}
|
||||
if (num < MIN_HEIGHT || num > MAX_HEIGHT) {
|
||||
return { ok: false, message: `身高请在 ${MIN_HEIGHT}-${MAX_HEIGHT} cm 之间` }
|
||||
}
|
||||
return { ok: true, value: formatMeasure(num) }
|
||||
}
|
||||
|
||||
export function validateWeight(weight) {
|
||||
const num = parseMeasure(weight)
|
||||
if (num == null) {
|
||||
return { ok: false, message: '请输入有效体重(单位 kg)' }
|
||||
}
|
||||
if (num < MIN_WEIGHT || num > MAX_WEIGHT) {
|
||||
return { ok: false, message: `体重请在 ${MIN_WEIGHT}-${MAX_WEIGHT} kg 之间` }
|
||||
}
|
||||
return { ok: true, value: formatMeasure(num) }
|
||||
}
|
||||
|
||||
export function parseBirthdayChinese(birthday) {
|
||||
const match = String(birthday ?? '').match(/(\d{4})年(\d{2})月(\d{2})日/)
|
||||
if (!match) return null
|
||||
return {
|
||||
year: Number(match[1]),
|
||||
month: Number(match[2]),
|
||||
day: Number(match[3])
|
||||
}
|
||||
}
|
||||
|
||||
export function validateBirthday(birthday) {
|
||||
const parts = parseBirthdayChinese(birthday)
|
||||
if (!parts) {
|
||||
return { ok: false, message: '请选择生日' }
|
||||
}
|
||||
const { year, month, day } = parts
|
||||
if (year < MIN_BIRTH_YEAR) {
|
||||
return { ok: false, message: '生日年份不合理' }
|
||||
}
|
||||
|
||||
const date = new Date(year, month - 1, day)
|
||||
if (
|
||||
date.getFullYear() !== year ||
|
||||
date.getMonth() !== month - 1 ||
|
||||
date.getDate() !== day
|
||||
) {
|
||||
return { ok: false, message: '生日日期无效' }
|
||||
}
|
||||
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
if (date > today) {
|
||||
return { ok: false, message: '生日不能晚于今天' }
|
||||
}
|
||||
|
||||
const minBirth = new Date(
|
||||
today.getFullYear() - MIN_AGE,
|
||||
today.getMonth(),
|
||||
today.getDate()
|
||||
)
|
||||
if (date > minBirth) {
|
||||
return { ok: false, message: `需年满 ${MIN_AGE} 周岁` }
|
||||
}
|
||||
|
||||
return { ok: true, value: `${year}年${String(month).padStart(2, '0')}月${String(day).padStart(2, '0')}日` }
|
||||
}
|
||||
|
||||
export function validateGender(gender) {
|
||||
if (gender === 'male' || gender === 'female') {
|
||||
return { ok: true, value: gender }
|
||||
}
|
||||
return { ok: false, message: '请选择性别' }
|
||||
}
|
||||
|
||||
export function validateFitnessGoals(goals, options = []) {
|
||||
const list = Array.isArray(goals) ? goals : []
|
||||
const allowed = new Set(options)
|
||||
const invalid = list.filter((g) => !allowed.has(g))
|
||||
if (invalid.length) {
|
||||
return { ok: false, message: '健身目标选项无效' }
|
||||
}
|
||||
if (list.length > MAX_FITNESS_GOALS) {
|
||||
return { ok: false, message: `最多选择 ${MAX_FITNESS_GOALS} 个健身目标` }
|
||||
}
|
||||
return { ok: true, value: [...list] }
|
||||
}
|
||||
|
||||
export function validateUserProfile(profile, goalOptions = []) {
|
||||
const nameResult = validateName(profile.name)
|
||||
if (!nameResult.ok) return nameResult
|
||||
|
||||
const phoneResult = validatePhone(profile.phone)
|
||||
if (!phoneResult.ok) return phoneResult
|
||||
|
||||
const genderResult = validateGender(profile.gender)
|
||||
if (!genderResult.ok) return genderResult
|
||||
|
||||
const birthdayResult = validateBirthday(profile.birthday)
|
||||
if (!birthdayResult.ok) return birthdayResult
|
||||
|
||||
const heightResult = validateHeight(profile.height)
|
||||
if (!heightResult.ok) return heightResult
|
||||
|
||||
const weightResult = validateWeight(profile.weight)
|
||||
if (!weightResult.ok) return weightResult
|
||||
|
||||
const goalsResult = validateFitnessGoals(profile.fitnessGoals, goalOptions)
|
||||
if (!goalsResult.ok) return goalsResult
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
value: {
|
||||
...profile,
|
||||
name: nameResult.value,
|
||||
phone: phoneResult.value,
|
||||
gender: genderResult.value,
|
||||
birthday: birthdayResult.value,
|
||||
height: heightResult.value,
|
||||
weight: weightResult.value,
|
||||
fitnessGoals: goalsResult.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function showValidationError(message) {
|
||||
uni.showToast({ title: message, icon: 'none' })
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* ============================================
|
||||
* 健身房管理系统小程序 - 全局配色变量
|
||||
* 主题:清新健康运动风格
|
||||
* 主色调:浅蓝渐变 + 活力橙点缀
|
||||
* 兼容暗色/浅色模式基础,保证可访问性
|
||||
* ============================================
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* ========== 主品牌色(清新浅蓝色系)========== */
|
||||
--primary-dark: #0B2B4B; /* 深蓝主色 - 用于重要文字、品牌标识,体现专业信赖感 */
|
||||
--primary-deep: #1A4A6F; /* 中深蓝色 - 用于hover状态、次级按钮、图标点缀,增加层次感 */
|
||||
|
||||
/* 主页主题浅蓝渐变色系 */
|
||||
--primary-sky-100: #D6EEF8; /* 最浅蓝 - 渐变起始色,清新自然 */
|
||||
--primary-sky-200: #E4F2FA; /* 浅蓝 - 渐变第二层 */
|
||||
--primary-sky-300: #EEF6FB; /* 淡蓝 - 渐变第三层 */
|
||||
--primary-sky-400: #F5FAFD; /* 微蓝 - 渐变第四层 */
|
||||
--primary-sky-500: #FAFCFE; /* 极浅蓝 - 渐变第五层,接近白色 */
|
||||
|
||||
/* 光晕效果色 */
|
||||
--glow-blue-1: rgba(160, 210, 235, 0.35); /* 蓝绿色光晕 */
|
||||
--glow-blue-2: rgba(180, 220, 240, 0.3); /* 浅蓝色光晕 */
|
||||
--glow-blue-3: rgba(170, 215, 238, 0.25); /* 浅蓝绿色光晕 */
|
||||
|
||||
/* ========== 强调/行动色(活力橙)========== */
|
||||
--accent-orange: #FF6B35; /* 活力橙 - 主要CTA按钮、会员标识、高亮徽章、关键数据,刺激行动力 */
|
||||
--accent-orange-light: #FF8C5A; /* 浅橙色 - hover轻量背景、渐变辅助,带来温暖运动感 */
|
||||
--accent-orange-dark: #E55A2B; /* 深橙色 - 按压状态或重要警告,保持色彩体系完整 */
|
||||
|
||||
/* ========== 背景色系(主页主题)========== */
|
||||
--bg-gradient-primary: linear-gradient(180deg, #D6EEF8 0%, #E4F2FA 15%, #EEF6FB 30%, #F5FAFD 50%, #FAFCFE 70%, #FFFFFF 100%); /* 主页主渐变背景 */
|
||||
--bg-light: #F5FAFD; /* 全局浅蓝背景 - 柔和且提升蓝色/橙色的视觉舒适度 */
|
||||
--bg-white: #FFFFFF; /* 纯白卡片背景 - 用于内容卡片、表单区域,提高可读性与层次感 */
|
||||
--bg-gray: #F2F5F9; /* 浅灰辅助背景 - 分割区域或禁用态背景 */
|
||||
|
||||
/* ========== 文本色系 ========== */
|
||||
--text-dark: #1E2A3A; /* 主要文字 - 标题、正文,保证高对比度 */
|
||||
--text-muted: #5E6F8D; /* 辅助文字 - 次要信息、占位符,保持易读性 */
|
||||
--text-light: #8A99B4; /* 更浅文字 - 提示语、时间戳,但需注意与背景对比 */
|
||||
--text-inverse: #FFFFFF; /* 反白文字 - 深色/橙色背景上的文字 */
|
||||
|
||||
/* ========== 边框/分割线 ========== */
|
||||
--border-light: #E9EDF2; /* 浅边框 - 卡片分割、列表边界,细腻柔和 */
|
||||
--border-focus: #FF6B35; /* 聚焦边框 - 输入框选中或强调区域,使用橙色点缀 */
|
||||
|
||||
/* ========== 状态颜色(功能性) ========== */
|
||||
--success-green: #2ECC71; /* 成功绿 - 已完成课程、健康打卡 */
|
||||
--warning-amber: #F39C12; /* 警示橙黄 - 提醒、到期提示 */
|
||||
--error-red: #E74C3C; /* 错误红 - 异常情况或取消预约 */
|
||||
--info-blue: #3498DB; /* 信息蓝 - 提示气泡、帮助文字 */
|
||||
|
||||
/* ========== 渐变色 (提升活力感) ========== */
|
||||
--gradient-orange: linear-gradient(135deg, #FF6B35 0%, #FF8C5A 100%); /* 橙色渐变 - 会员按钮、重要徽章 */
|
||||
--gradient-blue: linear-gradient(135deg, #0B2B4B 0%, #1A4A6F 100%); /* 深蓝渐变 - 头部banner或特别卡片 */
|
||||
--gradient-sky: linear-gradient(180deg, #D6EEF8 0%, #E4F2FA 15%, #EEF6FB 30%, #F5FAFD 50%, #FAFCFE 70%, #FFFFFF 100%); /* 主页天空渐变 - 全局背景 */
|
||||
--gradient-subtle: linear-gradient(120deg, #F9FAFE 0%, #FFFFFF 100%); /* 微弱渐变 - 增加细节精致度 */
|
||||
|
||||
/* ========== TabBar 配色(清新蓝调风格)========== */
|
||||
/* 引用位置:components/TabBar.vue */
|
||||
--tabbar-bg: rgba(200, 225, 238, 0.8); /* TabBar背景色 - 半透明浅蓝色毛玻璃效果 */
|
||||
--tabbar-shadow: rgba(120, 185, 215, 0.2); /* TabBar阴影色 - 蓝色系柔和阴影 */
|
||||
--tabbar-icon-inactive: gray; /* 未选中图标颜色 - 灰色 */
|
||||
--tabbar-icon-active: #5A98B0; /* 选中图标颜色 - 蓝绿色 */
|
||||
--tabbar-text-inactive: #8AABBB; /* 未选中文字颜色 - 浅灰蓝 */
|
||||
--tabbar-text-active: #5A98B0; /* 选中文字颜色 - 蓝绿色(与图标一致) */
|
||||
|
||||
/* ========== 通用蓝色系阴影(用于卡片、按钮等)========== */
|
||||
/* 引用位置:components/index/RecommendCourses.vue, QuickEntry.vue, TodayRecommend.vue */
|
||||
--shadow-blue-light: rgba(120, 185, 215, 0.18); /* 浅蓝色阴影 - 卡片悬浮效果 */
|
||||
|
||||
/* ========== 阴影层级 ========== */
|
||||
--shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.03), 0 2px 6px rgba(0, 0, 0, 0.05); /* 卡片小阴影 轻量浮起 */
|
||||
--shadow-md: 0 12px 28px rgba(0, 0, 0, 0.08); /* 中等阴影 - 弹窗或下拉菜单 */
|
||||
--shadow-lg: 0 20px 35px rgba(0, 0, 0, 0.12); /* 大阴影 - 模态框、悬浮元素 */
|
||||
--shadow-orange-glow: 0 4px 12px rgba(255, 107, 53, 0.25); /* 橙色光晕 - 增强CTA吸引力 */
|
||||
--shadow-sky-glow: 0 4px 12px rgba(160, 210, 235, 0.2); /* 蓝色光晕 - 主页效果增强 */
|
||||
|
||||
/* ========== 圆角规范 (柔和运动风) ========== */
|
||||
--radius-sm: 12px; /* 小组件、标签圆角 */
|
||||
--radius-md: 20px; /* 标准卡片圆角 */
|
||||
--radius-lg: 28px; /* 大容器、头部卡片圆角 */
|
||||
--radius-full: 999px; /* 胶囊按钮、头像完全圆角 */
|
||||
|
||||
/* ========== 布局与间距 ========== */
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
|
||||
/* ========== 字体 (移动端优先) ========== */
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-size-xs: 0.7rem; /* 辅助标注 */
|
||||
--font-size-sm: 0.8rem; /* 次要文字 */
|
||||
--font-size-base: 0.9rem; /* 正文基准 */
|
||||
--font-size-md: 1rem; /* 小标题 */
|
||||
--font-size-lg: 1.2rem; /* 卡片标题 */
|
||||
--font-size-xl: 1.4rem; /* 大数字/欢迎语 */
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
--font-weight-extrabold: 800;
|
||||
}
|
||||
|
||||
/* ========== 暗色模式适配(可选,保持品牌一致性) ========== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
/* 暗色模式下微调背景与文字,保留品牌色核心 */
|
||||
--bg-light: #121826;
|
||||
--bg-white: #1E2636;
|
||||
--bg-gray: #0F141F;
|
||||
--text-dark: #EDF2F7;
|
||||
--text-muted: #9AA9C1;
|
||||
--border-light: #2A3346;
|
||||
--shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.4);
|
||||
/* 保留主色深蓝与橙色不变,但可适当提高对比 */
|
||||
--primary-dark: #123A5E; /* 亮一点保证深色背景可见度 */
|
||||
--accent-orange: #FF7846; /* 稍微提亮橙色 */
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 辅助类 (方便开发直接复用) ========== */
|
||||
|
||||
/* 背景色类 */
|
||||
.bg-primary {
|
||||
background-color: var(--primary-dark);
|
||||
}
|
||||
.bg-accent {
|
||||
background-color: var(--accent-orange);
|
||||
}
|
||||
.bg-gradient-sky {
|
||||
background: var(--gradient-sky);
|
||||
}
|
||||
.bg-gradient-primary {
|
||||
background: var(--bg-gradient-primary);
|
||||
}
|
||||
.bg-sky-100 {
|
||||
background-color: var(--primary-sky-100);
|
||||
}
|
||||
.bg-sky-200 {
|
||||
background-color: var(--primary-sky-200);
|
||||
}
|
||||
.bg-sky-300 {
|
||||
background-color: var(--primary-sky-300);
|
||||
}
|
||||
.bg-sky-400 {
|
||||
background-color: var(--primary-sky-400);
|
||||
}
|
||||
.bg-sky-500 {
|
||||
background-color: var(--primary-sky-500);
|
||||
}
|
||||
|
||||
/* 文字色类 */
|
||||
.text-primary {
|
||||
color: var(--primary-dark);
|
||||
}
|
||||
.text-accent {
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
.text-sky-100 {
|
||||
color: var(--primary-sky-100);
|
||||
}
|
||||
.text-sky-200 {
|
||||
color: var(--primary-sky-200);
|
||||
}
|
||||
|
||||
/* 光晕效果类 */
|
||||
.glow-blue-1 {
|
||||
background: radial-gradient(circle, var(--glow-blue-1) 0%, transparent 70%);
|
||||
}
|
||||
.glow-blue-2 {
|
||||
background: radial-gradient(circle, var(--glow-blue-2) 0%, transparent 70%);
|
||||
}
|
||||
.glow-blue-3 {
|
||||
background: radial-gradient(circle, var(--glow-blue-3) 0%, transparent 70%);
|
||||
}
|
||||
|
||||
/* 按钮类 */
|
||||
.btn-orange {
|
||||
background: var(--gradient-orange);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-full);
|
||||
padding: 10px 20px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
box-shadow: var(--shadow-orange-glow);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-orange:active {
|
||||
transform: scale(0.97);
|
||||
background: var(--accent-orange-dark);
|
||||
}
|
||||
|
||||
/* 卡片类 */
|
||||
.card-default {
|
||||
background: var(--bg-white);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
border: 1px solid var(--border-light);
|
||||
padding: var(--spacing-md);
|
||||
}
|
||||
.card-sky {
|
||||
background: var(--gradient-sky);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
/* 阴影类 */
|
||||
.shadow-sky {
|
||||
box-shadow: var(--shadow-sky-glow);
|
||||
}
|
||||
|
||||
/* 通用页面容器 */
|
||||
.page-container-sky {
|
||||
min-height: 100vh;
|
||||
background: var(--gradient-sky);
|
||||
}
|
||||
|
||||
/* 滚动容器 */
|
||||
.scroll-container-sky {
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
background: var(--gradient-sky);
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
@font-face {
|
||||
font-family: "iconfont_courseCard"; /* Project id */
|
||||
src: url('./font/iconfont_courseCard.ttf?t=1780537357472') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont_courseCard {
|
||||
font-family: "iconfont_courseCard" !important;
|
||||
font-size: 16px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-didian:before {
|
||||
content: "\e61a";
|
||||
}
|
||||
|
||||
.icon-renwu-ren:before {
|
||||
content: "\e749";
|
||||
}
|
||||
|
||||
.icon-shijian:before {
|
||||
content: "\e61d";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
@font-face {
|
||||
font-family: "iconfont_time_select"; /* Project id */
|
||||
src: url('./font/iconfont_time_select.ttf?t=1780535096813') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont_time_select {
|
||||
font-family: "iconfont_time_select" !important;
|
||||
font-size: 25px;
|
||||
font-style: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-zaochen:before {
|
||||
content: "\e784";
|
||||
}
|
||||
|
||||
.icon-gengduo:before {
|
||||
content: "\e6df";
|
||||
}
|
||||
|
||||
.icon-xiawucha:before {
|
||||
content: "\100ff";
|
||||
}
|
||||
|
||||
.icon-yewan:before {
|
||||
content: "\e67e";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
@import './member-info-page.css';
|
||||
@import './member-info-status-bar.css';
|
||||
@import './member-info-header.css';
|
||||
@import './member-info-member-card.css';
|
||||
@import './member-info-quick-actions.css';
|
||||
@import './member-info-booking-list.css';
|
||||
@import './member-info-check-in-list.css';
|
||||
@import './member-info-body-report.css';
|
||||
@import './member-info-coupon-points.css';
|
||||
@import './member-info-referral.css';
|
||||
@import './member-info-settings.css';
|
||||
@import './member-info-logout.css';
|
||||
@@ -0,0 +1,245 @@
|
||||
.body-report-section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.body-report-section__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body-report-section__header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.body-report-section__header-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body-report-section__title {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.body-report-section__card {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
}
|
||||
|
||||
.body-report-section__card-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: var(--spacing-md, 16px);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.body-report-section__card-head {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.body-report-section__card-head-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body-report-section__desc {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.body-report-section__view-btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.body-report-section__view-icon {width: 14px;
|
||||
height: 14px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.body-report-section__view-report {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.body-report-section__metrics {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.body-report-section__metrics-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body-report-section__metric {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.body-report-section__metric-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.body-report-section__text {
|
||||
font-size: var(--font-size-xl);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.body-report-section__text-2 {
|
||||
font-size: var(--font-size-xl);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--success-green);
|
||||
}
|
||||
|
||||
.body-report-section__text-4 {
|
||||
font-size: var(--font-size-xl);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: rgba(243, 156, 18, 1);
|
||||
}
|
||||
|
||||
.body-report-section__num {
|
||||
font-size: var(--font-size-xl);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--primary-deep);
|
||||
}
|
||||
|
||||
.body-report-section__metric-value,
|
||||
.body-report-section__text-3,
|
||||
.body-report-section__metric-label,
|
||||
.body-report-section__text-5 {
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.body-report-section__metric-divider {
|
||||
width: 1px;
|
||||
height: 30px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--border-light, #e9edf2);
|
||||
}
|
||||
|
||||
.body-report-section__summary {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--bg-light, #f9fafe);
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.body-report-section__summary-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.body-report-section__goal {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 58%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: center;
|
||||
padding: 5px 10px;
|
||||
border-radius: 100px;
|
||||
background-color: rgba(240, 250, 245, 1);
|
||||
}
|
||||
|
||||
.body-report-section__goal-icon {width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.body-report-section__goal-text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--success-green);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.body-report-section__change {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.body-report-section__change-icon {width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.body-report-section__metric-value-2 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--success-green);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
.booking-section {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.booking-section__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.booking-section__header {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.booking-section__header-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.booking-section__title {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.booking-section__link {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.booking-section__item {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
}
|
||||
|
||||
.booking-section__item-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 14px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.booking-section__date {
|
||||
width: 48px;
|
||||
height: 56px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
|
||||
.booking-section__date-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.booking-section__num {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--accent-orange);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.booking-section__date-sub {
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-regular);
|
||||
color: var(--accent-orange-light);
|
||||
}
|
||||
|
||||
.booking-section__content {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.booking-section__content-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.booking-section__desc {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.booking-section__meta {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.booking-section__meta-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.booking-section__icon-coach {width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.booking-section__coach {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-regular);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.booking-section__icon-location {width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.booking-section__text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-regular);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.booking-section__status-wrap {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.booking-section__status-badge {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.booking-section__status-badge--booked {
|
||||
background-color: var(--success-green);
|
||||
}
|
||||
|
||||
.booking-section__status-badge--pending {
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
border: 1px solid rgba(212, 166, 74, 1);
|
||||
}
|
||||
|
||||
.booking-section__status-text {
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-inverse);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.booking-section__status-text--pending {
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
.checkin-section {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.checkin-section__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.checkin-section__header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkin-section__header-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkin-section__title {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--text-dark);
|
||||
}
|
||||
|
||||
.checkin-section__list {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 14px;
|
||||
box-shadow: var(--shadow-sm);
|
||||
background-color: var(--bg-white);
|
||||
border: 1px solid var(--border-light);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.checkin-section__list-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkin-section__row {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.checkin-section__item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.checkin-section__item-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
padding: 14px var(--spacing-md);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.checkin-section__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex-shrink: 0;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--success-green);
|
||||
}
|
||||
|
||||
.checkin-section__dot--group {
|
||||
background-color: var(--success-green);
|
||||
}
|
||||
|
||||
.checkin-section__dot--free {
|
||||
background-color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.checkin-section__dot--private {
|
||||
background-color: var(--primary-deep);
|
||||
}
|
||||
|
||||
.checkin-section__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.checkin-section__content-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.checkin-section__desc {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--text-dark);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.checkin-section__text {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-regular);
|
||||
color: var(--text-light);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.checkin-section__tag-badge {
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--spacing-sm);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 3px var(--spacing-sm);
|
||||
border-radius: 6px;
|
||||
background-color: rgba(240, 250, 245, 1);
|
||||
}
|
||||
|
||||
.checkin-section__tag-badge--group {
|
||||
background-color: rgba(240, 250, 245, 1);
|
||||
}
|
||||
|
||||
.checkin-section__tag-badge--free {
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
|
||||
.checkin-section__tag-badge--private {
|
||||
background-color: rgba(235, 243, 250, 1);
|
||||
}
|
||||
|
||||
.checkin-section__tag-text {
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--success-green);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.checkin-section__tag-text--group {
|
||||
color: var(--success-green);
|
||||
}
|
||||
|
||||
.checkin-section__tag-text--free {
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.checkin-section__tag-text--private {
|
||||
color: var(--primary-deep);
|
||||
}
|
||||
|
||||
.checkin-section__divider {
|
||||
height: 1px;
|
||||
background-color: var(--border-light);
|
||||
margin-left: calc(var(--spacing-md) + 8px + 12px);
|
||||
margin-right: var(--spacing-md);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/* 组件根节点:锁定浅色变量 + box-sizing(小程序组件内继承 page 的 theme-light 不稳定) */
|
||||
.status-bar,
|
||||
.profile-header,
|
||||
.member-card-section,
|
||||
.quick-actions,
|
||||
.booking-section,
|
||||
.checkin-section,
|
||||
.body-report-section,
|
||||
.coupon-section,
|
||||
.referral-section,
|
||||
.settings-section,
|
||||
.logout-btn__border-wrap,
|
||||
.logout-section {
|
||||
box-sizing: border-box;
|
||||
--primary-dark: #0B2B4B;
|
||||
--primary-deep: #1A4A6F;
|
||||
--primary-light: #2C6288;
|
||||
--accent-orange: #FF6B35;
|
||||
--accent-orange-light: #FF8C5A;
|
||||
--accent-orange-dark: #E55A2B;
|
||||
--bg-light: #F9FAFE;
|
||||
--bg-white: #FFFFFF;
|
||||
--bg-gray: #F2F5F9;
|
||||
--text-dark: #1E2A3A;
|
||||
--text-muted: #5E6F8D;
|
||||
--text-light: #8A99B4;
|
||||
--text-inverse: #FFFFFF;
|
||||
--border-light: #E9EDF2;
|
||||
--border-focus: #FF6B35;
|
||||
--success-green: #2ECC71;
|
||||
--warning-amber: #F39C12;
|
||||
--error-red: #E74C3C;
|
||||
--info-blue: #3498DB;
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
--radius-sm: 12px;
|
||||
--radius-md: 20px;
|
||||
--radius-lg: 28px;
|
||||
--radius-full: 999px;
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-size-xs: 11px;
|
||||
--font-size-sm: 12px;
|
||||
--font-size-base: 14px;
|
||||
--font-size-md: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 20px;
|
||||
--font-size-2xl: 22px;
|
||||
--font-size-3xl: 24px;
|
||||
--font-size-4xl: 28px;
|
||||
--font-size-5xl: 32px;
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
--shadow-sm: 0 8px 20px rgba(0, 0, 0, 0.03), 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 区块标题右侧操作链接:查看全部 / 全部记录 / 历史数据 / 推荐记录 */
|
||||
.member-card-section__link-text,
|
||||
.booking-section__view-all,
|
||||
.checkin-section__view-all,
|
||||
.coupon-section__view-all,
|
||||
.body-report-section__history-link,
|
||||
.referral-section__records-link {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--accent-orange);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.member-card-section__link,
|
||||
.booking-section__link,
|
||||
.checkin-section__link,
|
||||
.coupon-section__link,
|
||||
.body-report-section__link,
|
||||
.referral-section__link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-card-section__link-arrow,
|
||||
.booking-section__link-arrow,
|
||||
.checkin-section__link-arrow,
|
||||
.coupon-section__link-arrow,
|
||||
.body-report-section__link-arrow,
|
||||
.referral-section__link-arrow {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
@import '@/common/style/memberInfo/member-info-gradient-cards.css';
|
||||
|
||||
.coupon-section {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.coupon-section__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-section__header {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.coupon-section__header-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-section__title {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.coupon-section__link {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-section__view-all {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.coupon-section__link-arrow {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-section__cards {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.coupon-section__cards-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-section__coupon {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.coupon-section__coupon-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 14px 14px 14px 14px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-section__amount {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.coupon-section__desc {
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.coupon-section__coupon-status {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 2px 6px 2px 6px;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
background-color: rgba(255, 255, 255, 0.1882352977991104);
|
||||
}
|
||||
|
||||
.coupon-section__status {
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.coupon-section__points {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.coupon-section__points-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding: 14px 14px 14px 14px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.coupon-section__num {
|
||||
font-size: var(--font-size-3xl);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.coupon-section__points-label {
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.coupon-section__points-action {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 2px 6px 2px 6px;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
background-color: rgba(255, 255, 255, 0.1882352977991104);
|
||||
}
|
||||
|
||||
.coupon-section__text {
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 与个人中心首页 coupon-section 一致的渐变卡片背景
|
||||
* 使用 SVG 背景,兼容微信小程序(CSS 变量渐变在部分端不生效)
|
||||
*/
|
||||
|
||||
.mi-gradient-blue,
|
||||
.coupon-section__points,
|
||||
.mi-mod-points-hero,
|
||||
.bt-hero,
|
||||
.bt-score-card,
|
||||
.mi-mod-referral-hero {
|
||||
background-color: #0B2B4B;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20174%20106'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.7071%2C%200.7071%2C%20-0.7071%2C%20-0.7071%2C%201.2071%2C%200.5000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(11%2C43%2C75%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(26%2C74%2C111%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||
box-shadow: 0px 4px 8px 0px rgba(11, 43, 75, 0.25);
|
||||
}
|
||||
|
||||
.mi-gradient-orange,
|
||||
.coupon-section__coupon,
|
||||
.mi-mod-coupon__left,
|
||||
.mi-mod-coupon__use,
|
||||
.mi-center-coupon__btn:not(.mi-center-coupon__btn--done),
|
||||
.bt-page__action-link--primary,
|
||||
.bt-btn--primary {
|
||||
background-color: #FF6B35;
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20174%20106'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.7071%2C%200.7071%2C%20-0.7071%2C%20-0.7071%2C%201.2071%2C%200.5000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(255%2C107%2C53%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(255%2C140%2C90%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||
box-shadow: 0px 4px 8px 0px rgba(255, 107, 53, 0.25);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
.profile-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 顶栏固定:仅白底导航栏吸顶,下方用户信息可滚动 */
|
||||
.profile-header__toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-header__toolbar-spacer {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.profile-header__nav {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
padding-left: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-header__nav-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 56px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.profile-header__nav-right {
|
||||
min-width: 72px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.profile-header__title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
max-width: 42%;
|
||||
text-align: center;
|
||||
font-size: var(--font-size-md, 16px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.profile-header__icon-bell,
|
||||
.profile-header__icon-settings {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* settings.png 为白色线稿,白底顶栏需着色后才可见 */
|
||||
.profile-header__icon-settings {
|
||||
filter: brightness(0) saturate(100%) invert(52%) sepia(98%) saturate(1800%) hue-rotate(346deg) brightness(102%) contrast(101%);
|
||||
}
|
||||
|
||||
/* 用户信息渐变区 */
|
||||
.profile-header__hero {
|
||||
width: 100%;
|
||||
background-position: center;
|
||||
background-size: 100% 100%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20390%20239'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.0000%2C%201.0000%2C%20-1.0000%2C%20-0.0000%2C%201.0000%2C%200.0000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(11%2C43%2C75%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(26%2C74%2C111%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.profile-header__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 16px 20px 28px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-header__user {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-header__user-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-header__avatar-wrap {
|
||||
position: relative;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
flex-shrink: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.profile-header__avatar-ring {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
border: 3px solid #ffffff;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.profile-header__avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-header__avatar-badge {
|
||||
position: absolute;
|
||||
right: -1px;
|
||||
bottom: -1px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid #ffffff;
|
||||
background-color: #2ecc71;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.profile-header__avatar-badge-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.profile-header__user-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.profile-header__user-meta-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs, 4px);
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-header__name {
|
||||
font-size: var(--font-size-xl, 20px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-header__phone {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-header__badge {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-xs, 4px);
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.profile-header__badge-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.profile-header__level {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-inverse);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-header__stats {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-header__stats-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding-top: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-header__stat {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.profile-header__stat-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.profile-header__stat-value {
|
||||
font-size: var(--font-size-2xl, 22px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-header__stat-label {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-header__stat-divider {
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
background-color: rgba(255, 255, 255, 0.31);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
.logout-section {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logout-section__btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-height: 50px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logout-section__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.logout-section__text {
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-weight: 500;
|
||||
color: #e74c3c;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mi-tap-btn--hover {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
@@ -0,0 +1,362 @@
|
||||
.member-card-section {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.member-card-section__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.member-card-section__head {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.member-card-section__head-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.member-card-section__title {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.member-card-section__link {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.member-card-section__link-text {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-card-section__link-arrow {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.member-card-preview {
|
||||
width: 100%;
|
||||
min-height: 140px;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(255, 107, 53, 0.25);
|
||||
background-position: center;
|
||||
background-size: 100% 100%;
|
||||
box-sizing: border-box;
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20358%20140'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.7071%2C%200.7071%2C%20-0.7071%2C%20-0.7071%2C%201.2071%2C%200.5000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(255%2C107%2C53%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(255%2C140%2C90%2C1)'%20offset%3D'0.6000000238418579'%2F%3E%3Cstop%20stop-color%3D'rgba(229%2C90%2C43%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||
}
|
||||
|
||||
.member-card-preview__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
padding: 20px 16px 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.member-card-preview__head {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.member-card-preview__head-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.member-card-preview__type-row {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.member-card-preview__icon-wrap {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-card-preview__icon-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
border-radius: 1.5px 1.5px 1.5px 1.5px;
|
||||
}
|
||||
|
||||
.member-card-preview__icon-border {
|
||||
position: absolute;
|
||||
left: 8.33%;
|
||||
top: 20.83%;
|
||||
right: 8.33%;
|
||||
bottom: 20.83%;
|
||||
width: 83.34%;
|
||||
height: 58.34%;
|
||||
}
|
||||
|
||||
.member-card-preview__icon-stroke {
|
||||
position: absolute;
|
||||
inset: -0.75px -0.75px -0.75px -0.75px;
|
||||
border-radius: 1.5px 1.5px 1.5px 1.5px;
|
||||
pointer-events: none;
|
||||
border-width: 1.5px 1.5px 1.5px 1.5px;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-color: var(--text-inverse);
|
||||
}
|
||||
|
||||
.member-card-preview__icon-line {width: 83.34%;
|
||||
height: 8.33%;
|
||||
position: absolute;
|
||||
left: 8.33%;
|
||||
right: 8.33%;
|
||||
top: 41.67%;
|
||||
bottom: 50%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.member-card-preview__name {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse);
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-card-preview__tag {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 3px 10px;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.19);
|
||||
}
|
||||
|
||||
.member-card-preview__tag-text {
|
||||
font-size: var(--font-size-xs);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.member-card-preview__expire {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.member-card-preview__footer {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.member-card-preview__footer-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.member-card-preview__days {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-xs);
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.member-card-preview__days-num {
|
||||
font-size: var(--font-size-4xl);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.member-card-preview__days-unit {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.member-card-preview__renew {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
border-radius: 14px;
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
|
||||
.member-card-preview__renew-text {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--accent-orange);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.member-card-tip__inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
|
||||
.member-card-tip__content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
padding: 8px 12px 8px 12px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.member-card-tip {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.member-card-tip__border {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
pointer-events: none;
|
||||
border-width: 1px 1px 1px 1px;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-color: rgba(255, 204, 170, 1);
|
||||
}
|
||||
|
||||
.member-card-tip__icon {width: 14px;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.member-card-tip__text {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--accent-orange);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
.scroll-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
background: var(--gradient-sky);
|
||||
--spacing-xs: 4px;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--spacing-xl: 32px;
|
||||
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-size-xs: 11px;
|
||||
--font-size-sm: 12px;
|
||||
--font-size-base: 14px;
|
||||
--font-size-md: 16px;
|
||||
--font-size-lg: 18px;
|
||||
--font-size-xl: 20px;
|
||||
--font-size-2xl: 22px;
|
||||
--font-size-3xl: 24px;
|
||||
--font-size-4xl: 28px;
|
||||
--font-size-5xl: 32px;
|
||||
}
|
||||
|
||||
.scroll-container > view {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.member-page {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
height: auto;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
background: var(--gradient-sky);
|
||||
box-sizing: border-box;
|
||||
padding-bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.member-page__body {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.member-page__sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
padding: var(--spacing-md, 16px);
|
||||
padding-bottom: calc(var(--spacing-md, 16px) + env(safe-area-inset-bottom));
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
.member-page__sections text {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
/* ????????? */
|
||||
.status-bar,
|
||||
.profile-header,
|
||||
.member-card-section,
|
||||
.quick-actions,
|
||||
.booking-section,
|
||||
.checkin-section,
|
||||
.body-report-section,
|
||||
.coupon-section,
|
||||
.referral-section,
|
||||
.settings-section,
|
||||
.logout-btn__border-wrap,
|
||||
.logout-section {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
.quick-actions {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0px 2px 12px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.quick-actions__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quick-actions__grid {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.quick-actions__grid-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.quick-actions__item {
|
||||
flex: 1;
|
||||
height: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.quick-actions__item-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.quick-actions__icon-wrap {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.quick-actions__icon-wrap-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.quick-actions__icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.quick-actions__icon-part {
|
||||
position: absolute;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.quick-actions__icon-part:nth-child(1) {
|
||||
width: 7.69%;
|
||||
height: 16.67%;
|
||||
left: 30.77%;
|
||||
top: 8.33%;
|
||||
|
||||
}
|
||||
|
||||
.quick-actions__icon-part:nth-child(2) {
|
||||
width: 7.69%;
|
||||
height: 16.67%;
|
||||
left: 61.54%;
|
||||
top: 8.33%;
|
||||
|
||||
}
|
||||
|
||||
.quick-actions__icon-part:nth-child(4) {
|
||||
width: 7.69%;
|
||||
height: 16.67%;
|
||||
left: 30.77%;
|
||||
top: 58.33%;
|
||||
|
||||
}
|
||||
|
||||
.quick-actions__icon-part:nth-child(5) {
|
||||
width: 7.69%;
|
||||
height: 16.67%;
|
||||
left: 61.54%;
|
||||
top: 58.33%;
|
||||
|
||||
}
|
||||
|
||||
.quick-actions__border-wrap {
|
||||
position: absolute;
|
||||
left: 12.5%;
|
||||
top: 25%;
|
||||
width: 75%;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.quick-actions__rect {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.quick-actions__border {
|
||||
position: absolute;
|
||||
inset: -1px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid var(--accent-orange);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.quick-actions__icon-img {width: 20px;
|
||||
height: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.quick-actions__title,
|
||||
.quick-actions__title-2,
|
||||
.quick-actions__title-3,
|
||||
.quick-actions__title-4,
|
||||
.quick-actions__coach,
|
||||
.quick-actions__text,
|
||||
.quick-actions__text-2,
|
||||
.quick-actions__points-desc {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-dark);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.quick-actions__divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--border-light, #e9edf2);
|
||||
}
|
||||
|
||||
/* 第�?�?�??�?*/
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(1),
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(1) .quick-actions__icon-wrap {
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(2),
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(2) .quick-actions__icon-wrap {
|
||||
background-color: rgba(240, 250, 245, 1);
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(2) .quick-actions__icon-img {
|
||||
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(3),
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(3) .quick-actions__icon-wrap {
|
||||
background-color: rgba(235, 243, 250, 1);
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(3) .quick-actions__icon-img {
|
||||
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(4),
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(4) .quick-actions__icon-wrap {
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(1) .quick-actions__item:nth-child(4) .quick-actions__icon-img {
|
||||
|
||||
}
|
||||
|
||||
/* 第�?�?�??�?*/
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(1),
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(1) .quick-actions__icon-wrap {
|
||||
background-color: rgba(255, 236, 236, 1);
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(1) .quick-actions__icon-img {
|
||||
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(2),
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(2) .quick-actions__icon-wrap {
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(2) .quick-actions__icon-img {
|
||||
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(3),
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(3) .quick-actions__icon-wrap {
|
||||
background-color: rgba(240, 250, 245, 1);
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(3) .quick-actions__icon-img {
|
||||
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(4),
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(4) .quick-actions__icon-wrap {
|
||||
background-color: rgba(235, 243, 250, 1);
|
||||
}
|
||||
|
||||
.quick-actions__grid:nth-of-type(3) .quick-actions__item:nth-child(4) .quick-actions__icon-img {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
.referral-section {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.referral-section__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.referral-section__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.referral-section__title {
|
||||
font-size: var(--font-size-md, 16px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.referral-section__link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.referral-section__records-link {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--accent-orange);
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.referral-section__link-arrow {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 推荐码行:grid 避免小程序 flex 宽度计算异常 */
|
||||
.referral-section__code-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
column-gap: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.referral-section__code-box {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background-color: #f2f5f9;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.referral-section__code-label {
|
||||
display: block;
|
||||
font-size: var(--font-size-xs, 11px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.referral-section__code-value {
|
||||
display: block;
|
||||
width: 100%;
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--primary-dark);
|
||||
line-height: 1.3;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.referral-section__copy-btn {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
height: 100%;
|
||||
min-height: 52px;
|
||||
padding: 0 14px;
|
||||
border-radius: 10px;
|
||||
background-color: var(--accent-orange);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 例图:双矩形复制图标 */
|
||||
.referral-section__copy-icon {
|
||||
position: relative;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.referral-section__copy-sheet {
|
||||
position: absolute;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border: 1.5px solid #ffffff;
|
||||
border-radius: 2px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.referral-section__copy-sheet--back {
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.referral-section__copy-sheet--front {
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.referral-section__copy-text {
|
||||
display: block;
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-inverse);
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.referral-section__stats {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.referral-section__stat {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.referral-section__stat-num {
|
||||
display: block;
|
||||
font-size: var(--font-size-lg, 18px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.referral-section__stat-num--orange {
|
||||
color: var(--accent-orange);
|
||||
}
|
||||
|
||||
.referral-section__stat-num--green {
|
||||
color: var(--success-green);
|
||||
}
|
||||
|
||||
.referral-section__stat-num--amber {
|
||||
color: #f39c12;
|
||||
}
|
||||
|
||||
.referral-section__stat-label {
|
||||
display: block;
|
||||
font-size: var(--font-size-xs, 11px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
line-height: 1.4;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.referral-section__stat-divider {
|
||||
width: 1px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--border-light);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
.settings-section {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
--bg-white: #ffffff;
|
||||
--text-dark: #1e2a3a;
|
||||
--text-light: #8a99b4;
|
||||
--error-red: #e74c3c;
|
||||
--success-green: #2ecc71;
|
||||
--border-light: #e9edf2;
|
||||
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-size-xs: 11px;
|
||||
--font-size-base: 14px;
|
||||
--font-size-md: 16px;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-bold: 700;
|
||||
}
|
||||
|
||||
.settings-section__inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-section__title {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: #1e2a3a;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.settings-section__list {
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||
background-color: #ffffff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.settings-section__list-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-section__item {
|
||||
width: 100%;
|
||||
min-height: 52px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.settings-section__item--tall {
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.settings-section__item-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 16px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.settings-section__item-icon-wrap {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
|
||||
.settings-section__item-icon-wrap--blue {
|
||||
background-color: rgba(235, 243, 250, 1);
|
||||
}
|
||||
|
||||
.settings-section__item-icon-wrap--green {
|
||||
background-color: rgba(240, 250, 245, 1);
|
||||
}
|
||||
|
||||
.settings-section__item-icon-wrap--red {
|
||||
background-color: rgba(255, 236, 236, 1);
|
||||
}
|
||||
|
||||
.settings-section__item-icon-wrap-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.settings-section__item-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-section__item-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: #1e2a3a;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.settings-section__item-label--danger {
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.settings-section__item-texts {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.settings-section__item-title {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: #1e2a3a;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.settings-section__item-desc {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: #2ecc71;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.settings-section__item-arrow {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-section__item-divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: #e9edf2;
|
||||
}
|
||||
|
||||
.mi-tap-row--hover {
|
||||
opacity: 0.72;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
.status-bar {
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--primary-dark);
|
||||
}
|
||||
|
||||
.status-bar__inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0px 20px 0px 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.status-bar__time {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
|
||||
.status-bar__icons {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
.sub-nav {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sub-nav__toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #e9edf2;
|
||||
}
|
||||
|
||||
.sub-nav__spacer {
|
||||
width: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sub-nav__nav {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 44px;
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sub-nav__back {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
min-width: 32px;
|
||||
border-radius: 8px;
|
||||
background-color: #f9fafe;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sub-nav__back-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
max-width: 20px;
|
||||
max-height: 20px;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.sub-nav__title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: 50%;
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #1e2a3a;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.sub-nav__right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sub-nav__action {
|
||||
flex-shrink: 0;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.sub-nav__action-text {
|
||||
font-size: 14px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-weight: 500;
|
||||
color: #ff6b35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sub-nav__action--button {
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
background-color: #ff6b35;
|
||||
}
|
||||
|
||||
.sub-nav__action--button .sub-nav__action-text {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sub-nav__capsule {
|
||||
flex-shrink: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sub-nav__capsule--h5 {
|
||||
width: 0 !important;
|
||||
min-width: 0 !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/* 小程序点击反馈(配合 hover-class 使用) */
|
||||
.mi-tap--hover,
|
||||
.mi-tap--scale,
|
||||
.mi-tap-card--hover,
|
||||
.mi-tap-btn--hover,
|
||||
.mi-tap-tab--hover,
|
||||
.mi-tap-save--hover {
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.mi-tap--scale {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
|
||||
.mi-tap-card--hover {
|
||||
opacity: 0.92;
|
||||
transform: scale(0.995);
|
||||
}
|
||||
|
||||
.mi-tap-btn--hover {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.mi-tap-tab--hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.mi-tap-row--hover {
|
||||
background-color: rgba(249, 250, 254, 0.95);
|
||||
}
|
||||
|
||||
.mi-tap-save--hover {
|
||||
opacity: 0.88;
|
||||
transform: scale(0.99);
|
||||
}
|
||||
@@ -0,0 +1,860 @@
|
||||
/* 智能体测模块 - 公共样式(基于 base.css 变量) */
|
||||
@import '@/common/style/memberInfo/member-info-gradient-cards.css';
|
||||
|
||||
.bt-page {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--bg-light, #F9FAFE);
|
||||
}
|
||||
|
||||
.bt-page__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm, 12px);
|
||||
padding: var(--spacing-md, 16px) var(--spacing-md, 16px) 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bt-card {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius-md, 20px);
|
||||
background-color: var(--bg-white, #fff);
|
||||
box-shadow: var(--shadow-sm, 0 8px 20px rgba(0, 0, 0, 0.03));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bt-card__title {
|
||||
font-size: var(--font-size-md, 16px);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-card__desc {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-hero {
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-sm, 12px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bt-hero__top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bt-hero__label {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
|
||||
.bt-hero__badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.bt-hero__badge-text {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-inverse, #fff);
|
||||
}
|
||||
|
||||
.bt-hero__score-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bt-hero__score {
|
||||
font-size: 48px;
|
||||
font-weight: 800;
|
||||
color: var(--text-inverse, #fff);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bt-hero__grade {
|
||||
font-size: var(--font-size-xl, 20px);
|
||||
font-weight: 700;
|
||||
color: var(--accent-orange-light, #FF8C5A);
|
||||
}
|
||||
|
||||
.bt-hero__meta {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: rgba(255, 212, 184, 1);
|
||||
}
|
||||
|
||||
.bt-hero__actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.bt-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bt-btn--primary {
|
||||
box-shadow: var(--shadow-orange-glow, 0 4px 12px rgba(255, 107, 53, 0.25));
|
||||
}
|
||||
|
||||
.bt-btn--ghost {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.bt-btn--outline {
|
||||
background: var(--bg-white, #fff);
|
||||
border: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.bt-btn__text {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bt-btn--primary .bt-btn__text,
|
||||
.bt-btn--ghost .bt-btn__text {
|
||||
color: var(--text-inverse, #fff);
|
||||
}
|
||||
|
||||
.bt-btn--outline .bt-btn__text {
|
||||
color: var(--primary-dark, #0B2B4B);
|
||||
}
|
||||
|
||||
.bt-btn__icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.bt-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bt-grid__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 4px;
|
||||
border-radius: var(--radius-sm, 12px);
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
}
|
||||
|
||||
.bt-grid__icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.bt-grid__label {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bt-device {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bt-device__icon-wrap {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-device__icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.bt-device__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bt-device__name {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-device__status {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
display: block;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.bt-device__status--on {
|
||||
color: var(--success-green, #2ECC71);
|
||||
}
|
||||
|
||||
.bt-device__dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-light, #8A99B4);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-device__dot--on {
|
||||
background: var(--success-green, #2ECC71);
|
||||
}
|
||||
|
||||
.bt-metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bt-metric {
|
||||
padding: 14px;
|
||||
border-radius: var(--radius-sm, 12px);
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bt-metric__value {
|
||||
font-size: var(--font-size-xl, 20px);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.bt-metric__label {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.bt-metric__change {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bt-metric__change--down {
|
||||
color: var(--success-green, #2ECC71);
|
||||
}
|
||||
|
||||
.bt-metric__change--up {
|
||||
color: var(--warning-amber, #F39C12);
|
||||
}
|
||||
|
||||
.bt-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.bt-step {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.bt-step__num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-dark, #0B2B4B);
|
||||
color: var(--text-inverse, #fff);
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-step__content {
|
||||
flex: 1;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.bt-step:last-child .bt-step__content {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.bt-step__title {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-step__desc {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.bt-measure {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24px 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bt-measure__ring-wrap {
|
||||
position: relative;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.bt-measure__ring-bg {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 50%;
|
||||
border: 10px solid var(--border-light, #E9EDF2);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bt-measure__ring-fill {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
border-radius: 50%;
|
||||
border: 10px solid transparent;
|
||||
border-top-color: var(--accent-orange, #FF6B35);
|
||||
border-right-color: var(--accent-orange, #FF6B35);
|
||||
box-sizing: border-box;
|
||||
transform: rotate(-90deg);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.bt-measure__center {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bt-measure__percent {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
color: var(--primary-dark, #0B2B4B);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-measure__hint {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.bt-measure__live {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bt-measure__live-item {
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-sm, 12px);
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bt-measure__live-value {
|
||||
font-size: var(--font-size-lg, 18px);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-measure__live-label {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
margin-top: 2px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-score-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
border-radius: var(--radius-sm, 12px);
|
||||
}
|
||||
|
||||
.bt-score-card__circle {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-score-card__num {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
color: var(--text-inverse, #fff);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.bt-score-card__grade {
|
||||
font-size: 11px;
|
||||
color: var(--accent-orange-light, #FF8C5A);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bt-score-card__info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bt-score-card__title {
|
||||
font-size: var(--font-size-md, 16px);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse, #fff);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-score-card__date {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-body-map {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.bt-body-map__figure {
|
||||
width: 120px;
|
||||
height: 200px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bt-body-map__head {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background: var(--primary-light, #2C6288);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.bt-body-map__torso {
|
||||
width: 56px;
|
||||
height: 70px;
|
||||
border-radius: 12px;
|
||||
background: var(--primary-deep, #1A4A6F);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.bt-body-map__limbs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 48px;
|
||||
margin-top: -60px;
|
||||
}
|
||||
|
||||
.bt-body-map__arm {
|
||||
width: 16px;
|
||||
height: 50px;
|
||||
border-radius: 8px;
|
||||
background: var(--primary-light, #2C6288);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.bt-body-map__legs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.bt-body-map__leg {
|
||||
width: 22px;
|
||||
height: 60px;
|
||||
border-radius: 8px;
|
||||
background: var(--primary-light, #2C6288);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.bt-body-map__segments {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bt-body-map__seg {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
}
|
||||
|
||||
.bt-body-map__seg--high {
|
||||
border-left: 3px solid var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
.bt-body-map__seg--low {
|
||||
border-left: 3px solid var(--info-blue, #3498DB);
|
||||
}
|
||||
|
||||
.bt-body-map__seg-name {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.bt-body-map__seg-val {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.bt-advice-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bt-advice-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.bt-advice-item__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-orange, #FF6B35);
|
||||
margin-top: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-advice-item__text {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
line-height: 1.5;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.bt-course {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.bt-course:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.bt-course__banner {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-course__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.bt-course__tag {
|
||||
font-size: 10px;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bt-course__title {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.bt-course__meta {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.bt-history-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.bt-history-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bt-history-item__date {
|
||||
width: 52px;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-history-item__day {
|
||||
font-size: var(--font-size-xl, 20px);
|
||||
font-weight: 700;
|
||||
color: var(--primary-dark, #0B2B4B);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-history-item__month {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.bt-history-item__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bt-history-item__score-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bt-history-item__grade {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.bt-history-item__status {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.bt-history-item__metrics {
|
||||
font-size: 11px;
|
||||
color: var(--text-light, #8A99B4);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.bt-history-item__arrow {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-compare-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.bt-compare-picker {
|
||||
padding: 12px;
|
||||
border-radius: var(--radius-sm, 12px);
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.bt-compare-picker__label {
|
||||
font-size: 10px;
|
||||
color: var(--text-light, #8A99B4);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-compare-picker__date {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bt-compare-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.bt-compare-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bt-compare-row__label {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.bt-compare-row__val {
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.bt-compare-row__diff {
|
||||
width: 56px;
|
||||
text-align: right;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bt-setting {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 0;
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.bt-setting:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.bt-setting__label {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.bt-setting__desc {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-light, #8A99B4);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.bt-footer-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.bt-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 16px;
|
||||
}
|
||||
|
||||
.bt-empty__text {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
color: var(--text-light, #8A99B4);
|
||||
}
|
||||
|
||||
.bt-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-sm, 8px);
|
||||
padding: 0 var(--spacing-md, 16px) var(--spacing-sm, 8px);
|
||||
overflow-x: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bt-tab {
|
||||
padding: 8px 14px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
background: var(--bg-white, #fff);
|
||||
border: 1px solid var(--border-light, #E9EDF2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bt-tab--active {
|
||||
background: var(--primary-dark, #0B2B4B);
|
||||
border-color: var(--primary-dark, #0B2B4B);
|
||||
}
|
||||
|
||||
.bt-tab__text {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bt-tab--active .bt-tab__text {
|
||||
color: var(--text-inverse, #fff);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bt-trend-link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0 0;
|
||||
margin-top: 8px;
|
||||
border-top: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.bt-trend-link__text {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
color: var(--primary-deep, #1A4A6F);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.bt-trend-link__arrow {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
.booking-page {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--bg-light, #F9FAFE);
|
||||
}
|
||||
|
||||
/* Tab 栏 */
|
||||
.booking-page__tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
border-bottom: 1px solid var(--border-light, #E9EDF2);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.booking-page__tab {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 44px;
|
||||
padding: 0 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.booking-page__tab-text {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light, #8A99B4);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.booking-page__tab-text--active {
|
||||
font-weight: 700;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
.booking-page__tab-indicator {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
right: 16px;
|
||||
bottom: 0;
|
||||
height: 2px;
|
||||
border-radius: 2px;
|
||||
background-color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
/* 内容区 */
|
||||
.booking-page__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 16px 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 提醒横幅 */
|
||||
.booking-page__alert {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 204, 170, 1);
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.booking-page__alert-icon {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.booking-page__alert-text {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 预约卡片 */
|
||||
.bk-card {
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.bk-card__banner {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
display: block;
|
||||
border-radius: 14px 14px 0 0;
|
||||
}
|
||||
|
||||
.bk-card__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bk-card__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bk-card__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: var(--font-size-md, 16px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bk-card__status {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 10px;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bk-card__status--booked {
|
||||
background-color: var(--success-green, #2ECC71);
|
||||
}
|
||||
|
||||
.bk-card__status--pending {
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
border: 1px solid rgba(212, 166, 74, 1);
|
||||
}
|
||||
|
||||
.bk-card__status--completed {
|
||||
background-color: var(--bg-light, #F9FAFE);
|
||||
border: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.bk-card__status--cancelled {
|
||||
background-color: var(--bg-light, #F9FAFE);
|
||||
border: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.bk-card__status-text {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bk-card__status-text--booked {
|
||||
color: var(--text-inverse, #ffffff);
|
||||
}
|
||||
|
||||
.bk-card__status-text--pending {
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
.bk-card__status-text--completed {
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.bk-card__status-text--cancelled {
|
||||
color: var(--text-light, #8A99B4);
|
||||
}
|
||||
|
||||
/* 时间与教练信息(分两行) */
|
||||
.bk-card__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bk-card__meta-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.bk-card__meta-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.bk-card__meta-text {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 底部操作行 */
|
||||
.bk-card__footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.bk-card__footer-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light, #8A99B4);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.bk-card__cancel {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-light, #E9EDF2);
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bk-card__cancel-text {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--error-red, #E74C3C);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.booking-page__empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48px 16px;
|
||||
}
|
||||
|
||||
.booking-page__empty-text {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light, #8A99B4);
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
.Pixso-frame-2_965 {
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_965 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0px 20px 0px 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_966 {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_967 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_968 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_968 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0px 16px 0px 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.stroke-wrapper-2_968 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stroke-2_968 {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 0px 0px 0px 0px;
|
||||
pointer-events: none;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
.Pixso-frame-2_969 {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px 8px 8px 8px;
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
.frame-content-2_969 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-vector-2_970 {width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-paragraph-2_972 {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: var(--primary-dark);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
.Pixso-frame-2_973 {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background-color: rgba(249, 250, 254, 0);
|
||||
}
|
||||
.Pixso-frame-2_974 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_974 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0px 16px 0px 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.stroke-wrapper-2_974 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stroke-2_974 {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 0px 0px 0px 0px;
|
||||
pointer-events: none;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
.Pixso-frame-2_975 {
|
||||
width: 74px;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
.frame-content-2_975 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0px 16px 0px 16px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_976 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--accent-orange);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_977 {
|
||||
width: 74px;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 42px;
|
||||
border-radius: 2px 2px 2px 2px;
|
||||
background-color: var(--accent-orange);
|
||||
}
|
||||
.Pixso-frame-2_978 {
|
||||
width: auto;
|
||||
height: 44px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
}
|
||||
.frame-content-2_978 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0px 16px 0px 16px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_979 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_980 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.frame-content-2_980 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding: 16px 16px 40px 16px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_981 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
.frame-content-2_981 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 10px 12px 10px 12px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.stroke-wrapper-2_981 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stroke-2_981 {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
pointer-events: none;
|
||||
border-width: 1px 1px 1px 1px;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-color: rgba(255, 204, 170, 1);
|
||||
}
|
||||
.Pixso-vector-2_982 {width: 14px;
|
||||
height: 14px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-paragraph-2_985 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--accent-orange);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
.Pixso-frame-2_986 {
|
||||
width: 100%;
|
||||
height: 195px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 14px 14px 14px 14px;
|
||||
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_986 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-vector-2_987 {width: 100%;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-frame-2_988 {
|
||||
width: 358px;
|
||||
height: 80px;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
border-radius: 14px 14px 0px 0px;
|
||||
background-color: rgba(0, 0, 0, 0.1882352977991104);
|
||||
}
|
||||
.Pixso-frame-2_989 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.frame-content-2_989 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
padding: 12px 14px 12px 14px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_990 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_990 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_991 {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_992 {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 4px 10px 4px 10px;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
background-color: var(--success-green);
|
||||
}
|
||||
.Pixso-paragraph-2_993 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_994 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_994 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-vector-2_995 {width: 13px;
|
||||
height: 13px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-paragraph-2_998 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-vector-2_999 {width: 13px;
|
||||
height: 13px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-paragraph-2_1002 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_1003 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_1003 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_1004 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_1005 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 6px 14px 6px 14px;
|
||||
border-radius: 8px 8px 8px 8px;
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.stroke-wrapper-2_1005 {
|
||||
position: relative;
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stroke-2_1005 {
|
||||
position: absolute;
|
||||
inset: -1px -1px -1px -1px;
|
||||
border-radius: 9px 9px 9px 9px;
|
||||
pointer-events: none;
|
||||
border-width: 1px 1px 1px 1px;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
.Pixso-paragraph-2_1006 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--error-red);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_1007 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 14px 14px 14px 14px;
|
||||
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_1007 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-vector-2_1008 {width: 100%;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-frame-2_1009 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.frame-content-2_1009 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
padding: 12px 14px 12px 14px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_1010 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_1010 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_1011 {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_1012 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 4px 10px 4px 10px;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
.stroke-wrapper-2_1012 {
|
||||
position: relative;
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stroke-2_1012 {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
pointer-events: none;
|
||||
border-width: 1px 1px 1px 1px;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-color: rgba(212, 166, 74, 1);
|
||||
}
|
||||
.Pixso-paragraph-2_1013 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--accent-orange);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_1014 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_1014 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-vector-2_1015 {width: 13px;
|
||||
height: 13px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-paragraph-2_1018 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-vector-2_1019 {width: 13px;
|
||||
height: 13px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-paragraph-2_1022 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_1023 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_1023 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_1024 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_1025 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 6px 14px 6px 14px;
|
||||
border-radius: 8px 8px 8px 8px;
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.stroke-wrapper-2_1025 {
|
||||
position: relative;
|
||||
width: auto;
|
||||
height: auto;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stroke-2_1025 {
|
||||
position: absolute;
|
||||
inset: -1px -1px -1px -1px;
|
||||
border-radius: 9px 9px 9px 9px;
|
||||
pointer-events: none;
|
||||
border-width: 1px 1px 1px 1px;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
.Pixso-paragraph-2_1026 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--error-red);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
.mi-course-list__filters {
|
||||
padding: 0 var(--spacing-md, 16px) var(--spacing-sm, 8px);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm, 10px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mi-course-list__date-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mi-course-list__mode {
|
||||
padding: 8px 12px;
|
||||
border-radius: 999px;
|
||||
background: var(--primary-dark, #0B2B4B);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mi-course-list__mode-text {
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mi-course-list__dates {
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mi-course-list__date {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
margin-right: 6px;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-white, #fff);
|
||||
border: 1px solid var(--border-light, #E9EDF2);
|
||||
}
|
||||
|
||||
.mi-course-list__date--active {
|
||||
background: rgba(255, 107, 53, 0.12);
|
||||
border-color: var(--accent-orange, #FF6B35);
|
||||
}
|
||||
|
||||
.mi-course-list__date-week {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.mi-course-list__date-day {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.mi-course-list__chips {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mi-course-list__row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mi-course-list__picker {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: var(--bg-white, #fff);
|
||||
border: 1px solid var(--border-light, #E9EDF2);
|
||||
font-size: 13px;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.mi-course-list__arrow {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.mi-course-card {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border-radius: 18px;
|
||||
background: var(--bg-white, #fff);
|
||||
box-shadow: var(--shadow-sm);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.mi-course-card__banner {
|
||||
width: 96px;
|
||||
height: 120px;
|
||||
border-radius: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mi-course-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mi-course-card__head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mi-course-card__title {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mi-course-card__type {
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mi-course-card__type--group {
|
||||
background: rgba(255, 107, 53, 0.12);
|
||||
}
|
||||
|
||||
.mi-course-card__type--private {
|
||||
background: rgba(11, 43, 75, 0.1);
|
||||
}
|
||||
|
||||
.mi-course-card__type text {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.mi-course-card__coach {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
}
|
||||
|
||||
.mi-course-card__avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.mi-course-card__meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-light, #8A99B4);
|
||||
}
|
||||
|
||||
.mi-course-card__capacity {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.mi-course-card__bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mi-course-card__bar-fill {
|
||||
height: 100%;
|
||||
background: var(--gradient-orange);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.mi-course-card__cap-text {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mi-course-card__scarcity {
|
||||
font-size: 10px;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mi-course-card__footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mi-course-card__price {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--primary-deep, #1A4A6F);
|
||||
}
|
||||
|
||||
.mi-course-card__btn {
|
||||
padding: 6px 16px;
|
||||
border-radius: 999px;
|
||||
background: var(--gradient-orange);
|
||||
}
|
||||
|
||||
.mi-course-card__btn text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.mi-course-card__btn--disabled {
|
||||
background: var(--bg-gray, #F2F5F9);
|
||||
}
|
||||
|
||||
.mi-course-card__btn--disabled text {
|
||||
color: var(--text-light, #8A99B4);
|
||||
}
|
||||
|
||||
.mi-course-list__fab {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: 32px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 999px;
|
||||
background: var(--primary-dark, #0B2B4B);
|
||||
box-shadow: var(--shadow-md);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.mi-course-list__fab-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.mi-course-list__fab-text {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
.member-card-page {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--bg-light, #F9FAFE);
|
||||
}
|
||||
|
||||
.member-card-page__body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 16px 16px 40px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 会员卡 */
|
||||
.mc-hero {
|
||||
width: 100%;
|
||||
min-height: 160px;
|
||||
padding: 20px 20px 16px;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 10px 20px rgba(11, 43, 75, 0.31);
|
||||
background: linear-gradient(135deg, #0B2B4B 0%, #1A4A6F 100%);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mc-hero__top {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mc-hero__title-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mc-hero__crown {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mc-hero__name {
|
||||
font-size: var(--font-size-md, 16px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse, #ffffff);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mc-hero__badge {
|
||||
flex-shrink: 0;
|
||||
padding: 4px 10px;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(255, 255, 255, 0.19);
|
||||
}
|
||||
|
||||
.mc-hero__badge-text {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-inverse, #ffffff);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mc-hero__validity {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mc-hero__bottom {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.mc-hero__days {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.mc-hero__days-num {
|
||||
font-size: var(--font-size-5xl, 32px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse, #ffffff);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.mc-hero__days-unit {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.mc-hero__renew {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 8px 18px;
|
||||
border-radius: 16px;
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-hero__renew-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mc-hero__renew-text {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--accent-orange, #FF6B35);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 使用记录 */
|
||||
.mc-records {
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mc-records__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
height: 48px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mc-records__title {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-records__tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border-radius: 8px;
|
||||
background-color: var(--bg-light, #F9FAFE);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-records__tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mc-records__tab--active {
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.mc-records__tab-text {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light, #8A99B4);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mc-records__tab-text--active {
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
}
|
||||
|
||||
.mc-records__divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: var(--bg-light, #F9FAFE);
|
||||
}
|
||||
|
||||
.mc-records__item-inner {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.mc-records__icon-wrap {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-records__icon-wrap--orange {
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
|
||||
.mc-records__icon-wrap--green {
|
||||
background-color: rgba(240, 250, 245, 1);
|
||||
}
|
||||
|
||||
.mc-records__icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-records__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.mc-records__item-title {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mc-records__item-time {
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light, #8A99B4);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mc-records__value {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-records__value--negative {
|
||||
color: var(--error-red, #E74C3C);
|
||||
}
|
||||
|
||||
.mc-records__value--positive {
|
||||
color: var(--success-green, #2ECC71);
|
||||
}
|
||||
|
||||
/* 使用规则 */
|
||||
.mc-rules {
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 2px 10px rgba(26, 25, 24, 0.03);
|
||||
background-color: var(--bg-white, #ffffff);
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.mc-rules__title {
|
||||
font-size: var(--font-size-base, 14px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark, #1E2A3A);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mc-rules__item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mc-rules__bullet {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-top: 6px;
|
||||
border-radius: 1px;
|
||||
background-color: var(--accent-orange, #FF6B35);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mc-rules__text {
|
||||
flex: 1;
|
||||
font-size: var(--font-size-sm, 12px);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted, #5E6F8D);
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -0,0 +1,982 @@
|
||||
.Pixso-frame-2_878 {
|
||||
width: 100%;
|
||||
height: 62px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_878 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0px 20px 0px 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_879 {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_880 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_881 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_881 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0px 16px 0px 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.stroke-wrapper-2_881 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.stroke-2_881 {
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 0px 0px 0px 0px;
|
||||
pointer-events: none;
|
||||
border-width: 0px 0px 1px 0px;
|
||||
border-style: solid;
|
||||
box-sizing: border-box;
|
||||
border-color: var(--border-light);
|
||||
}
|
||||
.Pixso-frame-2_882 {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 8px 8px 8px 8px;
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
.frame-content-2_882 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-vector-2_883 {width: 20px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-paragraph-2_885 {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
color: var(--primary-dark);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
.Pixso-frame-2_886 {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background-color: rgba(249, 250, 254, 0);
|
||||
}
|
||||
.Pixso-frame-2_887 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.frame-content-2_887 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding: 16px 16px 40px 16px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_888 {
|
||||
width: 100%;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 18px 18px 18px 18px;
|
||||
box-shadow: 0px 10px 20px 0px rgba(11, 43, 75, 0.3137254901960784);
|
||||
background-position: center;
|
||||
background-image: url("data:image/svg+xml;utf8,%3Csvg%20viewBox%3D'0%200%20358%20160'%20preserveAspectRatio%3D'none'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%0A%20%20%20%20%20%20%3Cdefs%3E%0A%20%20%20%20%20%20%20%20%3ClinearGradient%20id%3D'grad'%20gradientUnits%3D'objectBoundingBox'%20x1%3D'0'%20y1%3D'0.5'%20x2%3D'1'%20y2%3D'0.5'%20gradientTransform%3D'matrix(-0.7071%2C%200.7071%2C%20-0.7071%2C%20-0.7071%2C%201.2071%2C%200.5000)'%3E%0A%20%20%20%20%20%20%20%20%20%20%3Cstop%20stop-color%3D'rgba(11%2C43%2C75%2C1)'%20offset%3D'0'%2F%3E%3Cstop%20stop-color%3D'rgba(26%2C74%2C111%2C1)'%20offset%3D'1'%2F%3E%0A%20%20%20%20%20%20%20%20%3C%2FlinearGradient%3E%0A%20%20%20%20%20%20%3C%2Fdefs%3E%0A%20%20%20%20%20%20%3Crect%20width%3D'100%25'%20height%3D'100%25'%20fill%3D'url(%23grad)'%2F%3E%0A%20%20%20%20%3C%2Fsvg%3E");
|
||||
}
|
||||
.frame-content-2_888 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding: 20px 20px 16px 20px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_889 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_889 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_890 {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
.Pixso-vector-2_891 {width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-paragraph-2_894 {
|
||||
font-size: var(--font-size-md);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_895 {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
padding: 4px 10px 4px 10px;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
background-color: rgba(255, 255, 255, 0.1882352977991104);
|
||||
}
|
||||
.Pixso-paragraph-2_896 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_897 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_898 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.frame-content-2_898 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_899 {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.Pixso-paragraph-2_900 {
|
||||
font-size: var(--font-size-5xl);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--text-inverse);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_901 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: rgba(255, 212, 184, 1);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_902 {
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
padding: 8px 18px 8px 18px;
|
||||
border-radius: 16px 16px 16px 16px;
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.Pixso-vector-2_903 {width: 13px;
|
||||
height: 13px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-paragraph-2_908 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--accent-orange);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_909 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 14px 14px 14px 14px;
|
||||
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_909 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_910 {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_910 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0px 16px 0px 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_911 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_912 {
|
||||
width: auto;
|
||||
height: 28px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: 8px 8px 8px 8px;
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
.frame-content-2_912 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
padding: 2px 2px 2px 2px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_913 {
|
||||
width: auto;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.06274509803921569);
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_913 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0px 10px 0px 10px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_914 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_915 {
|
||||
width: auto;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
background-color: rgba(249, 250, 254, 0);
|
||||
}
|
||||
.frame-content-2_915 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0px 10px 0px 10px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_916 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_917 {
|
||||
width: auto;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-radius: 6px 6px 6px 6px;
|
||||
background-color: rgba(249, 250, 254, 0);
|
||||
}
|
||||
.frame-content-2_917 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0px 10px 0px 10px;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_918 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_919 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
.Pixso-frame-2_920 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_920 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 14px 16px 14px 16px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_921 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
.frame-content-2_921 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-vector-2_922 {width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-frame-2_928 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
.frame-content-2_928 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_929 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_930 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
white-space: pre-wrap;
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_931 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--error-red);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_932 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
.Pixso-frame-2_933 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_933 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 14px 16px 14px 16px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_934 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
background-color: rgba(240, 250, 245, 1);
|
||||
}
|
||||
.frame-content-2_934 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-vector-2_935 {width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-frame-2_938 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
.frame-content-2_938 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_939 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_940 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
white-space: pre-wrap;
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_941 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--error-red);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_942 {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background-color: var(--bg-light);
|
||||
}
|
||||
.Pixso-frame-2_943 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.frame-content-2_943 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
padding: 14px 16px 14px 16px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-frame-2_944 {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px 10px 10px 10px;
|
||||
background-color: rgba(255, 243, 238, 1);
|
||||
}
|
||||
.frame-content-2_944 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-vector-2_945 {width: 18px;
|
||||
height: 18px;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: block;
|
||||
}
|
||||
.Pixso-frame-2_949 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
.frame-content-2_949 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_950 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 500;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_951 {
|
||||
font-size: var(--font-size-sm);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-light);
|
||||
white-space: pre-wrap;
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_952 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 700;
|
||||
color: var(--success-green);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_953 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
border-radius: 14px 14px 14px 14px;
|
||||
box-shadow: 0px 2px 10px 0px rgba(26, 25, 24, 0.03137254901960784);
|
||||
background-color: var(--bg-white);
|
||||
}
|
||||
.frame-content-2_953 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
padding: 16px 16px 16px 16px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_954 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-frame-2_955 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.frame-content-2_955 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_956 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--accent-orange);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_957 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
.Pixso-frame-2_958 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.frame-content-2_958 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_959 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--accent-orange);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_960 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
.Pixso-frame-2_961 {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.frame-content-2_961 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.Pixso-paragraph-2_962 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--accent-orange);
|
||||
width: auto;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
flex-grow: 0;
|
||||
}
|
||||
.Pixso-paragraph-2_963 {
|
||||
font-size: var(--font-size-base);
|
||||
font-family: var(--font-family);
|
||||
font-weight: 400;
|
||||
color: var(--text-muted);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user