diff --git a/gym-manage-uniapp/api/groupCourse.js b/gym-manage-uniapp/api/groupCourse.js index 89c5787..801d2d2 100644 --- a/gym-manage-uniapp/api/groupCourse.js +++ b/gym-manage-uniapp/api/groupCourse.js @@ -54,6 +54,7 @@ export function searchGroupCourse(params = {}, options = {}) { timePeriod, priceSort, remainingMost, + isRecurring, page = 0, size = 10 } = params @@ -67,6 +68,7 @@ export function searchGroupCourse(params = {}, options = {}) { if (timePeriod) requestBody.timePeriod = timePeriod if (priceSort) requestBody.priceSort = priceSort if (remainingMost !== undefined) requestBody.remainingMost = remainingMost + if (isRecurring !== undefined) requestBody.isRecurring = isRecurring return request.post('/groupCourse/search', requestBody, options) } @@ -91,14 +93,24 @@ export function getMemberBookings(memberId, options = {}) { return request.get(`/groupCourse/bookings/member/${memberId}`, {}, options) } -export function getActiveRecommendCourses(options = { cache: true, cacheTime: 5 * 60 * 1000 }) { +export function getActiveRecommendCourses(options = { cache: false }) { return request.get('/groupCourse/recommend/active', {}, options) } -export function getGroupCourseRecommendList(params = {}, options = { cache: true, cacheTime: 5 * 60 * 1000 }) { +export function getGroupCourseRecommendList(params = {}, options = { cache: false }) { return request.get('/groupCourse/recommend/list', params, options) } +/** + * 扫码签到 + * 扫描团课二维码后签到,将预约状态更新为已出席 + * @param {number} courseId - 团课ID + * @param {number} memberId - 会员ID + */ +export function qrSignInGroupCourse(courseId, memberId) { + return request.post(`/groupCourse/${courseId}/qrsignin`, { memberId }) +} + export default { getGroupCourseList, getGroupCoursePage, diff --git a/gym-manage-uniapp/api/main.js b/gym-manage-uniapp/api/main.js index 916aa2a..6656265 100644 --- a/gym-manage-uniapp/api/main.js +++ b/gym-manage-uniapp/api/main.js @@ -328,6 +328,17 @@ export function getStoredCardRechargeRecords(memberId, page = 1, size = 20, opti return request.get(`/stored-card/${memberId}/recharges?page=${page}&size=${size}`, {}, options) } +// ========== 系统配置相关API ========== + +/** + * 根据配置键获取配置值 + * @param {string} configKey - 配置键 + * @param {object} options - 请求选项 + */ +export function getConfigByKey(configKey, options = {}) { + return request.get(`/config/key/${configKey}`, {}, options) +} + // ========== 支付相关API ========== /** @@ -482,6 +493,16 @@ export function getCourseBookings(courseId, options = { cache: false }) { return request.get(`/groupCourse/bookings/course/${courseId}`, {}, options) } +// ========== 轮播图相关API ========== + +/** + * 获取启用的轮播图列表 + * @param {object} options - 请求选项 { cache: boolean, cacheTime: number } + */ +export function getActiveBanners(options = { cache: true, cacheTime: 10 * 60 * 1000 }) { + return request.get('/banner/active', {}, options) +} + export default { login, sendCode, @@ -520,5 +541,6 @@ export default { cancelBooking, signinGroupCourse, getMemberBookings, - getCourseBookings + getCourseBookings, + getActiveBanners } diff --git a/gym-manage-uniapp/common/constants/routes.js b/gym-manage-uniapp/common/constants/routes.js index 003f0be..574be66 100644 --- a/gym-manage-uniapp/common/constants/routes.js +++ b/gym-manage-uniapp/common/constants/routes.js @@ -80,6 +80,7 @@ export function navigateToPage(url) { // 这种情况应该使用 switchToTabPage(会清空页面栈) if (TAB_PAGES.has(path)) { console.warn('[navigateToPage] 不应该用 navigateTo 跳转 TabBar 页面,请使用 switchToTabPage') + uni.hideLoading() switchToTabPage(path) return } @@ -90,6 +91,7 @@ export function navigateToPage(url) { url, fail: (err) => { console.error('[navigateTo]', url, err) + uni.hideLoading() // 页面栈满时降级使用 redirectTo if (err.errMsg && err.errMsg.includes('limit')) { uni.redirectTo({ url }) @@ -97,11 +99,11 @@ export function navigateToPage(url) { uni.showToast({ title: '页面跳转失败', icon: 'none' }) } }, - success: () => { - setTimeout(() => { - uni.hideLoading() - },3000) - } + complete: () => { + // 页面已发起跳转,隐藏 loading + // 目标页面的 onLoad/onReady 也会调用 hideLoading 做兜底 + uni.hideLoading() + } }) } @@ -122,19 +124,21 @@ export function switchToTabPage(url) { console.log('[switchToTabPage] 跳转到 TabBar:', path) tabNavigating = true - uni.switchTab({ // ✅ 改用 switchTab,而不是 reLaunch + uni.switchTab({ url: path, complete: () => { + uni.hideLoading() setTimeout(() => { tabNavigating = false }, 320) }, fail: (err) => { console.error('[switchTab]', path, err) - // 降级使用 reLaunch + uni.hideLoading() uni.reLaunch({ url: path, complete: () => { + uni.hideLoading() setTimeout(() => { tabNavigating = false }, 320) diff --git a/gym-manage-uniapp/common/memberInfo/store.js b/gym-manage-uniapp/common/memberInfo/store.js index ec234c8..6042a12 100644 --- a/gym-manage-uniapp/common/memberInfo/store.js +++ b/gym-manage-uniapp/common/memberInfo/store.js @@ -54,7 +54,9 @@ function applyCardInfo(store) { export function syncStats(store) { store.stats = { ...store.stats, - pointsBalance: store.stats.pointsBalance ?? 1250 + checkInCount: store.stats.checkInCount ?? 0, + courseCount: store.stats.courseCount ?? 0, + pointsBalance: store.stats.pointsBalance ?? 0 } return store } @@ -230,7 +232,7 @@ export function getCenterPageData(store) { return { stats: { checkInCount: store.stats?.checkInCount ?? 0, - trainingHours: store.stats?.trainingHours ?? 0, + courseCount: store.stats?.courseCount ?? 0, pointsBalance: store.stats?.pointsBalance ?? 0 }, cardInfo: { ...store.cardInfo }, diff --git a/gym-manage-uniapp/common/style/memberInfo/member-info-member-card.css b/gym-manage-uniapp/common/style/memberInfo/member-info-member-card.css index 7374cb6..83802d3 100644 --- a/gym-manage-uniapp/common/style/memberInfo/member-info-member-card.css +++ b/gym-manage-uniapp/common/style/memberInfo/member-info-member-card.css @@ -362,27 +362,29 @@ } .member-card-section__count { - display: flex; + display: inline-flex; flex-direction: row; align-items: center; - gap: 4px; - padding: 4px 12px; - border-radius: 12px; - background-color: rgba(255, 255, 255, 0.2); + gap: 4rpx; + padding: 4rpx 14rpx; + border-radius: 20rpx; + background: rgba(255, 255, 255, 0.25); + backdrop-filter: blur(4px); + flex-shrink: 0; } .member-card-section__count-num { - font-size: var(--font-size-md); - font-family: var(--font-family); + font-size: 26rpx; font-weight: 700; - color: var(--text-inverse); + color: #333; + line-height: 1; } .member-card-section__count-text { - font-size: var(--font-size-xs); - font-family: var(--font-family); + font-size: 22rpx; font-weight: 400; - color: rgba(255, 212, 184, 1); + color: #333; + line-height: 1; } .member-card-preview__tag--expiring { diff --git a/gym-manage-uniapp/components/TabBar.vue b/gym-manage-uniapp/components/TabBar.vue index 33933f4..f768edc 100644 --- a/gym-manage-uniapp/components/TabBar.vue +++ b/gym-manage-uniapp/components/TabBar.vue @@ -183,9 +183,7 @@ function onTabTap(index) { emit('tab-change', index) // 设置导航标志,阻止轮询覆盖状态 isNavigating = true - let timer = setTimeout(() => { - uni.showLoading({ title: '加载中...', mask: true }) - }, 50) + uni.showLoading({ title: '加载中...', mask: true }) isSwitching = true uni.switchTab({ url: targetPath, @@ -195,7 +193,6 @@ function onTabTap(index) { uni.reLaunch({ url: targetPath }) }, complete: () => { - clearTimeout(timer) uni.hideLoading() setTimeout(() => { isSwitching = false diff --git a/gym-manage-uniapp/components/groupCourse/CourseCard.vue b/gym-manage-uniapp/components/groupCourse/CourseCard.vue index 4a2588a..fbb8a0d 100644 --- a/gym-manage-uniapp/components/groupCourse/CourseCard.vue +++ b/gym-manage-uniapp/components/groupCourse/CourseCard.vue @@ -103,7 +103,7 @@ - {{ canBook ? '立即预约' : (course.currentMembers >= course.maxMembers ? '已满员' : '已结束') }} + {{ buttonText }} @@ -123,6 +123,10 @@ const props = defineProps({ courseTypeLabels: { type: Array, default: () => [] + }, + booked: { + type: Boolean, + default: false } }) @@ -147,7 +151,14 @@ const statusClass = computed(() => { const canBook = computed(() => { const status = props.course.status const isFull = props.course.currentMembers >= props.course.maxMembers - return status === '0' && !isFull + return status === '0' && !isFull && !props.booked +}) + +const buttonText = computed(() => { + if (props.booked) return '已预约' + if (!canBook.value && props.course.currentMembers >= props.course.maxMembers) return '已满员' + if (!canBook.value && Number(props.course.status) !== 0) return '已结束' + return '立即预约' }) const formatTime = (dateStr) => { @@ -191,7 +202,7 @@ const formatDuration = (startStr, endStr) => { } const goDetail = () => { - emit('detail', props.course.id) + emit('detail', Number(props.course.id)) } const handleBooking = () => { diff --git a/gym-manage-uniapp/components/index/BannerSwiper.vue b/gym-manage-uniapp/components/index/BannerSwiper.vue index 9a6e529..c27fd7d 100644 --- a/gym-manage-uniapp/components/index/BannerSwiper.vue +++ b/gym-manage-uniapp/components/index/BannerSwiper.vue @@ -45,9 +45,11 @@ \ No newline at end of file diff --git a/gym-manage-uniapp/components/index/QuickEntry.vue b/gym-manage-uniapp/components/index/QuickEntry.vue index b477306..a5d857f 100644 --- a/gym-manage-uniapp/components/index/QuickEntry.vue +++ b/gym-manage-uniapp/components/index/QuickEntry.vue @@ -12,26 +12,12 @@ {{ item.title }} {{ item.desc }} - - - - - - 到店签到 - - - - 团课签到 - - - diff --git a/gym-manage-uniapp/components/memberInfo/MemberInfoCheckInList.vue b/gym-manage-uniapp/components/memberInfo/MemberInfoCheckInList.vue index 97d1a95..d12ef87 100644 --- a/gym-manage-uniapp/components/memberInfo/MemberInfoCheckInList.vue +++ b/gym-manage-uniapp/components/memberInfo/MemberInfoCheckInList.vue @@ -20,7 +20,11 @@ - + + + 暂无签到记录 + + diff --git a/gym-manage-uniapp/components/memberInfo/MemberInfoHeader.vue b/gym-manage-uniapp/components/memberInfo/MemberInfoHeader.vue index e3a3e03..5196a3c 100644 --- a/gym-manage-uniapp/components/memberInfo/MemberInfoHeader.vue +++ b/gym-manage-uniapp/components/memberInfo/MemberInfoHeader.vue @@ -1,89 +1,50 @@ @@ -94,10 +55,9 @@ import { computed } from 'vue' const props = defineProps({ userInfo: { type: Object, required: true }, stats: { type: Object, required: true }, - isLogin: { type: Boolean } + isLogin: { type: Boolean } }) - const emit = defineEmits(['user-info', 'guest-login']) const displayAvatar = computed(() => { @@ -115,8 +75,112 @@ function handleGuestLogin() { } - diff --git a/gym-manage-uniapp/components/memberInfo/MemberInfoMemberCard.vue b/gym-manage-uniapp/components/memberInfo/MemberInfoMemberCard.vue index 9ad1c04..08854c8 100644 --- a/gym-manage-uniapp/components/memberInfo/MemberInfoMemberCard.vue +++ b/gym-manage-uniapp/components/memberInfo/MemberInfoMemberCard.vue @@ -1,98 +1,82 @@ - diff --git a/gym-manage-uniapp/composables/useGroupCourseList.js b/gym-manage-uniapp/composables/useGroupCourseList.js index 0897dc4..d2e62ce 100644 --- a/gym-manage-uniapp/composables/useGroupCourseList.js +++ b/gym-manage-uniapp/composables/useGroupCourseList.js @@ -33,6 +33,8 @@ export function useGroupCourseList() { ]) const timePeriodIndex = ref(0) + const isRecurring = ref(false) // 常态化团课筛选 + const showTimePicker = ref(false) const startDate = ref('') const endDate = ref('') @@ -138,9 +140,8 @@ export function useGroupCourseList() { const handleBooking = (course) => { console.log('[useGroupCourseList] 预约课程:', course) - uni.showToast({ - title: `预约课程:${course.courseName}`, - icon: 'success' + uni.navigateTo({ + url: `/pages/groupCourse/detail?id=${course.id}` }) } @@ -180,6 +181,10 @@ export function useGroupCourseList() { params.timePeriod = timePeriod.value } + if (isRecurring.value) { + params.isRecurring = true + } + if (sortOption.priceSort) { params.priceSort = sortOption.priceSort } @@ -291,6 +296,7 @@ export function useGroupCourseList() { sortIndex, timePeriodOptions, timePeriodIndex, + isRecurring, showTimePicker, startDate, endDate, diff --git a/gym-manage-uniapp/groupcourse-recommend-api.md b/gym-manage-uniapp/groupcourse-recommend-api.md new file mode 100644 index 0000000..ffcb613 --- /dev/null +++ b/gym-manage-uniapp/groupcourse-recommend-api.md @@ -0,0 +1,530 @@ +# 团课推荐模块 API 文档 + +> **文档版本**: v1.0 +> **创建日期**: 2026-06-15 +> **作者**: 张翔 +> **状态**: 正式发布 + +--- + +## 📋 目录 + +1. [概述](#概述) +2. [基础路径](#基础路径) +3. [团课推荐管理接口](#团课推荐管理接口) + - [获取所有团课推荐](#获取所有团课推荐) + - [获取所有启用的团课推荐](#获取所有启用的团课推荐) + - [根据ID获取团课推荐](#根据ID获取团课推荐) + - [根据团课ID获取推荐](#根据团课ID获取推荐) + - [创建团课推荐](#创建团课推荐) + - [更新团课推荐](#更新团课推荐) + - [删除团课推荐](#删除团课推荐) + - [启用团课推荐](#启用团课推荐) + - [禁用团课推荐](#禁用团课推荐) +4. [数据模型](#数据模型) + - [GroupCourseRecommend(团课推荐)](#GroupCourseRecommend团课推荐) +5. [状态码说明](#状态码说明) +6. [业务规则](#业务规则) + +--- + +## 概述 + +团课推荐模块提供团课推荐信息的创建、编辑、查询、删除和状态管理功能。推荐信息包含团课ID、推荐标题、推荐内容、推荐理由、优先级等必要信息,支持按优先级排序展示。 + +## 基础路径 + +所有接口的基础路径为: `http://{host}:{port}/api/groupCourse/recommend` + +--- + +## 团课推荐管理接口 + +### 获取所有团课推荐 + +| 属性 | 值 | +|------|-----| +| **HTTP方法** | GET | +| **接口路径** | `/api/groupCourse/recommend/list` | +| **所属文件** | `GroupCourseRecommendHandler.java` | + +**请求参数**: + +| 参数名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| sortBy | string | 否 | priority | 排序字段(支持:priority、createdAt、updatedAt) | +| sortOrder | string | 否 | desc | 排序方式(asc-升序,desc-降序) | + +**成功响应** (200 OK): + +```json +[ + { + "id": 1, + "courseId": 1, + "recommendTitle": "本周热门课程", + "recommendContent": "这是一门非常棒的课程,快来参加吧!", + "recommendReason": "教练专业,课程内容丰富", + "priority": 10, + "isActive": true, + "groupCourse": { + "id": 1, + "courseName": "瑜伽入门", + "coachId": 1, + "courseType": 1, + "startTime": "2026-06-15T09:00:00", + "endTime": "2026-06-15T10:00:00", + "maxMembers": 20, + "currentMembers": 15, + "status": 0, + "location": "健身房A区", + "coverImage": "https://example.com/yoga.jpg", + "description": "适合初学者的瑜伽课程" + }, + "createdAt": "2026-06-15T10:00:00", + "updatedAt": "2026-06-15T10:00:00" + } +] +``` + +--- + +### 获取所有启用的团课推荐 + +| 属性 | 值 | +|------|-----| +| **HTTP方法** | GET | +| **接口路径** | `/api/groupCourse/recommend/active` | +| **所属文件** | `GroupCourseRecommendHandler.java` | + +**功能说明**: 获取系统中所有已启用的团课推荐列表,按优先级从高到低排序。 + +**成功响应** (200 OK): + +```json +[ + { + "id": 2, + "courseId": 3, + "recommendTitle": "新学员推荐", + "recommendContent": "专为新学员设计的入门课程,轻松上手", + "recommendReason": "零基础友好,教练耐心指导", + "priority": 20, + "isActive": true, + "groupCourse": { + "id": 3, + "courseName": "基础有氧", + "coachId": 2, + "courseType": 2, + "startTime": "2026-06-16T18:00:00", + "endTime": "2026-06-16T19:00:00", + "maxMembers": 30, + "currentMembers": 8, + "status": 0, + "location": "健身房B区", + "coverImage": "https://example.com/aerobic.jpg", + "description": "适合所有健身水平的有氧课程" + }, + "createdAt": "2026-06-15T11:00:00", + "updatedAt": "2026-06-15T11:00:00" + } +] +``` + +--- + +### 根据ID获取团课推荐 + +| 属性 | 值 | +|------|-----| +| **HTTP方法** | GET | +| **接口路径** | `/api/groupCourse/recommend/{id}` | +| **所属文件** | `GroupCourseRecommendHandler.java` | + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 团课推荐ID | + +**成功响应** (200 OK): + +```json +{ + "id": 1, + "courseId": 1, + "recommendTitle": "本周热门课程", + "recommendContent": "这是一门非常棒的课程,快来参加吧!", + "recommendReason": "教练专业,课程内容丰富", + "priority": 10, + "isActive": true, + "groupCourse": { + "id": 1, + "courseName": "瑜伽入门", + "coachId": 1, + "courseType": 1, + "startTime": "2026-06-15T09:00:00", + "endTime": "2026-06-15T10:00:00", + "maxMembers": 20, + "currentMembers": 15, + "status": 0, + "location": "健身房A区", + "coverImage": "https://example.com/yoga.jpg", + "description": "适合初学者的瑜伽课程" + }, + "createdAt": "2026-06-15T10:00:00", + "updatedAt": "2026-06-15T10:00:00" +} +``` + +**失败响应** (404 Not Found): + +```json +{} +``` + +--- + +### 根据团课ID获取推荐 + +| 属性 | 值 | +|------|-----| +| **HTTP方法** | GET | +| **接口路径** | `/api/groupCourse/recommend/course/{courseId}` | +| **所属文件** | `GroupCourseRecommendHandler.java` | + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| courseId | Long | 是 | 团课ID | + +**成功响应** (200 OK): + +```json +[ + { + "id": 1, + "courseId": 1, + "recommendTitle": "本周热门课程", + "recommendContent": "这是一门非常棒的课程,快来参加吧!", + "recommendReason": "教练专业,课程内容丰富", + "priority": 10, + "isActive": true, + "groupCourse": { + "id": 1, + "courseName": "瑜伽入门", + "coachId": 1, + "courseType": 1, + "startTime": "2026-06-15T09:00:00", + "endTime": "2026-06-15T10:00:00", + "maxMembers": 20, + "currentMembers": 15, + "status": 0, + "location": "健身房A区" + }, + "createdAt": "2026-06-15T10:00:00", + "updatedAt": "2026-06-15T10:00:00" + } +] +``` + +--- + +### 创建团课推荐 + +| 属性 | 值 | +|------|-----| +| **HTTP方法** | POST | +| **接口路径** | `/api/groupCourse/recommend` | +| **所属文件** | `GroupCourseRecommendHandler.java` | + +**请求体**: + +```json +{ + "courseId": 1, + "recommendTitle": "本周热门课程", + "recommendContent": "这是一门非常棒的课程,快来参加吧!", + "recommendReason": "教练专业,课程内容丰富", + "priority": 10, + "isActive": true +} +``` + +| 参数名 | 类型 | 必填 | 默认值 | 说明 | +|--------|------|------|--------|------| +| courseId | Long | **是** | - | 团课ID(必须是有效的团课) | +| recommendTitle | String | 否 | - | 推荐标题 | +| recommendContent | String | 否 | - | 推荐内容 | +| recommendReason | String | 否 | - | 推荐理由 | +| priority | Integer | 否 | 0 | 优先级(数字越大优先级越高) | +| isActive | Boolean | 否 | true | 是否启用 | + +**成功响应** (200 OK): + +```json +{ + "success": true, + "message": "团课推荐创建成功", + "data": { + "id": 1, + "courseId": 1, + "recommendTitle": "本周热门课程", + "recommendContent": "这是一门非常棒的课程,快来参加吧!", + "recommendReason": "教练专业,课程内容丰富", + "priority": 10, + "isActive": true, + "createdAt": "2026-06-15T10:00:00", + "updatedAt": "2026-06-15T10:00:00" + } +} +``` + +**失败响应** (400 Bad Request): + +```json +{ + "success": false, + "message": "团课ID不能为空" +} +``` + +--- + +### 更新团课推荐 + +| 属性 | 值 | +|------|-----| +| **HTTP方法** | PUT | +| **接口路径** | `/api/groupCourse/recommend/{id}` | +| **所属文件** | `GroupCourseRecommendHandler.java` | + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 团课推荐ID | + +**请求体**: + +```json +{ + "recommendTitle": "本周热门课程(更新)", + "recommendContent": "更新后的推荐内容", + "recommendReason": "更新后的推荐理由", + "priority": 15, + "isActive": true +} +``` + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| courseId | Long | 否 | 团课ID | +| recommendTitle | String | 否 | 推荐标题 | +| recommendContent | String | 否 | 推荐内容 | +| recommendReason | String | 否 | 推荐理由 | +| priority | Integer | 否 | 优先级 | +| isActive | Boolean | 否 | 是否启用 | + +**成功响应** (200 OK): + +```json +{ + "success": true, + "message": "团课推荐更新成功", + "data": { + "id": 1, + "courseId": 1, + "recommendTitle": "本周热门课程(更新)", + "recommendContent": "更新后的推荐内容", + "recommendReason": "更新后的推荐理由", + "priority": 15, + "isActive": true, + "updatedAt": "2026-06-15T12:00:00" + } +} +``` + +**失败响应** (400 Bad Request): + +```json +{ + "success": false, + "message": "团课推荐不存在" +} +``` + +--- + +### 删除团课推荐 + +| 属性 | 值 | +|------|-----| +| **HTTP方法** | DELETE | +| **接口路径** | `/api/groupCourse/recommend/{id}` | +| **所属文件** | `GroupCourseRecommendHandler.java` | + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 团课推荐ID | + +**成功响应** (200 OK): + +```json +{ + "success": true, + "message": "团课推荐删除成功" +} +``` + +**失败响应** (400 Bad Request): + +```json +{ + "success": false, + "message": "团课推荐不存在" +} +``` + +--- + +### 启用团课推荐 + +| 属性 | 值 | +|------|-----| +| **HTTP方法** | POST | +| **接口路径** | `/api/groupCourse/recommend/{id}/enable` | +| **所属文件** | `GroupCourseRecommendHandler.java` | + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 团课推荐ID | + +**成功响应** (200 OK): + +```json +{ + "success": true, + "message": "团课推荐启用成功", + "data": { + "id": 1, + "courseId": 1, + "recommendTitle": "本周热门课程", + "isActive": true + } +} +``` + +**失败响应** (400 Bad Request): + +```json +{ + "success": false, + "message": "团课推荐不存在" +} +``` + +--- + +### 禁用团课推荐 + +| 属性 | 值 | +|------|-----| +| **HTTP方法** | POST | +| **接口路径** | `/api/groupCourse/recommend/{id}/disable` | +| **所属文件** | `GroupCourseRecommendHandler.java` | + +**路径参数**: + +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 团课推荐ID | + +**成功响应** (200 OK): + +```json +{ + "success": true, + "message": "团课推荐禁用成功", + "data": { + "id": 1, + "courseId": 1, + "recommendTitle": "本周热门课程", + "isActive": false + } +} +``` + +**失败响应** (400 Bad Request): + +```json +{ + "success": false, + "message": "团课推荐不存在" +} +``` + +--- + +## 数据模型 + +### GroupCourseRecommend(团课推荐) + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| id | Long | 主键ID | +| courseId | Long | 团课ID(关联group_course.id) | +| recommendTitle | String | 推荐标题 | +| recommendContent | String | 推荐内容 | +| recommendReason | String | 推荐理由 | +| priority | Integer | 优先级(数字越大优先级越高),默认0 | +| isActive | Boolean | 是否启用,默认true | +| groupCourse | GroupCourse | 关联的团课信息(查询时自动填充) | +| createdBy | String | 创建人 | +| updatedBy | String | 更新人 | +| createdAt | LocalDateTime | 创建时间 | +| updatedAt | LocalDateTime | 更新时间 | +| deletedAt | LocalDateTime | 删除时间(软删除) | + +--- + +## 状态码说明 + +### 推荐状态 + +| 状态值 | 含义 | +|--------|------| +| true | 启用 | +| false | 禁用 | + +--- + +## 业务规则 + +### 团课推荐管理 +1. **创建推荐**:团课ID为必填项,且必须是有效的团课 +2. **优先级排序**:获取启用的推荐列表时,按优先级从高到低排序 +3. **删除推荐**:采用软删除机制,数据保留可恢复 +4. **状态管理**:支持启用/禁用推荐状态,禁用的推荐不会在推荐列表中显示 + +--- + +## 附录:错误响应格式 + +所有接口的错误响应统一格式: + +```json +{ + "success": false, + "message": "错误描述信息" +} +``` + +--- + +*文档结束* \ No newline at end of file diff --git a/gym-manage-uniapp/pages.json b/gym-manage-uniapp/pages.json index e591e70..14be932 100644 --- a/gym-manage-uniapp/pages.json +++ b/gym-manage-uniapp/pages.json @@ -39,6 +39,7 @@ { "path": "pages/memberInfo/memberInfo", "style": { + "navigationStyle": "custom", "navigationBarTitleText": "我的", "app-plus": { "animationType": "fade-in", diff --git a/gym-manage-uniapp/pages/checkIn/checkIn.vue b/gym-manage-uniapp/pages/checkIn/checkIn.vue index 97a1240..d41727d 100644 --- a/gym-manage-uniapp/pages/checkIn/checkIn.vue +++ b/gym-manage-uniapp/pages/checkIn/checkIn.vue @@ -110,7 +110,7 @@ import { getMemberId } from '@/utils/request.js' const STQRC = ref(false)//是否扫码 const isCheckIn = ref(false) const isHaveCard = ref(true) - const webSoketURL = "ws://localhost:8084/webSocket/checkIn" + const webSoketURL = "ws://192.168.5.15:8084/webSocket/checkIn" const qrcode = ref("") diff --git a/gym-manage-uniapp/pages/course/index.vue b/gym-manage-uniapp/pages/course/index.vue index dcbbaba..466406c 100644 --- a/gym-manage-uniapp/pages/course/index.vue +++ b/gym-manage-uniapp/pages/course/index.vue @@ -130,6 +130,7 @@ function handleTabActive(index) { } onLoad(() => { + uni.hideLoading() // 优先显示缓存 const hasCache = loadFromCache() if (!hasCache) { @@ -143,6 +144,7 @@ onLoad(() => { }) onShow(() => { + uni.hideLoading() // 每次显示时尝试刷新数据(后台静默更新) if (!loading.value) { loadFromNetwork() diff --git a/gym-manage-uniapp/pages/groupCourse/detail.vue b/gym-manage-uniapp/pages/groupCourse/detail.vue index c6d71cf..05b4414 100644 --- a/gym-manage-uniapp/pages/groupCourse/detail.vue +++ b/gym-manage-uniapp/pages/groupCourse/detail.vue @@ -228,16 +228,36 @@ + + + @@ -963,7 +1022,10 @@ onMounted(() => { box-shadow: none; } -/* 骨架屏样式 */ +.booking-btn.disabled text { + color: #c8c8c8; +} + .skeleton { padding-bottom: calc(140rpx + constant(safe-area-inset-bottom)); padding-bottom: calc(140rpx + env(safe-area-inset-bottom)); diff --git a/gym-manage-uniapp/pages/groupCourse/list.vue b/gym-manage-uniapp/pages/groupCourse/list.vue index c39b163..afde16d 100644 --- a/gym-manage-uniapp/pages/groupCourse/list.vue +++ b/gym-manage-uniapp/pages/groupCourse/list.vue @@ -32,6 +32,18 @@ @change="onTimePeriodChange" ref="timePeriodRef" /> + + + + + {{ isRecurring ? '✓' : '' }} + 常态化团课 + + @@ -46,6 +58,7 @@ :key="course.id" :course="course" :courseTypeLabels="getCourseLabels(course)" + :booked="isCourseBooked(course.id)" @booking="handleBooking" @detail="goDetail" > @@ -74,6 +87,7 @@ @@ -167,18 +87,6 @@ onMounted(() => { padding-bottom: 160rpx; } -/* 固定白色块 */ -.hand { - position: fixed; - top: 0; - left: 0; - z-index: 100; - background-color: white; - width: 100%; - text-align: center; - font-size: 34rpx; -} - /* 固定 TabBar */ .tabbar-fixed { position: fixed; diff --git a/gym-manage-uniapp/pages/memberInfo/booking.vue b/gym-manage-uniapp/pages/memberInfo/booking.vue index bd79181..8eef01e 100644 --- a/gym-manage-uniapp/pages/memberInfo/booking.vue +++ b/gym-manage-uniapp/pages/memberInfo/booking.vue @@ -1,7 +1,7 @@ diff --git a/gym-manage-uniapp/pages/memberInfo/checkInHistory.vue b/gym-manage-uniapp/pages/memberInfo/checkInHistory.vue index c9fb638..1ab47c9 100644 --- a/gym-manage-uniapp/pages/memberInfo/checkInHistory.vue +++ b/gym-manage-uniapp/pages/memberInfo/checkInHistory.vue @@ -1,7 +1,7 @@ diff --git a/gym-manage-uniapp/pages/memberInfo/memberCenter.vue b/gym-manage-uniapp/pages/memberInfo/memberCenter.vue index 9ec0537..e350cb1 100644 --- a/gym-manage-uniapp/pages/memberInfo/memberCenter.vue +++ b/gym-manage-uniapp/pages/memberInfo/memberCenter.vue @@ -3,85 +3,43 @@ - - - - - - - 个人中心 - - + + + - - - - - - - - - - - - - - - - {{ userInfo.name }} - {{ userInfo.phone }} - - - {{ userInfo.memberLevel }} - - - - + + + + - - - - - {{ stats.checkInCount }} - 累计签到 - - - - - - {{ stats.trainingHours }} - 训练时长 - - - - - - {{ stats.pointsBalance }} - 累计积分 - + + + {{ userInfo.name }} + + + {{ userInfo.memberLevel }} + {{ userInfo.phone }} + + + + + {{ stats.checkInCount ?? 0 }} + 累计签到 + + + {{ stats.courseCount ?? 0 }} + 报课次数 + + + {{ stats.pointsBalance ?? 0 }} + 累计积分 @@ -233,88 +191,6 @@ - - - - - - 我的预约 - - 预约记录 - - - - - - - - - - {{ item.desc }} - - - - - - {{ item.statusLabel }} - - - - - - 暂无进行中的预约 - - - - - - - - - - 签到记录 - - 查看全部 - - - - - - - - - - - - - - {{ item.title }} - {{ item.time }} - - - - - - - - - - - @@ -514,27 +390,22 @@ import { ref, computed, onMounted } from 'vue' import { onShow } from '@dcloudio/uni-app' import { PAGE, navigateToPage } from '@/common/constants/routes.js' -import { getCenterPageData, renewMemberCard, computeRemainingDays, saveMemberStore } from '@/common/memberInfo/store.js' +import { getCenterPageData, renewMemberCard, computeRemainingDays, saveMemberStore, loadMemberStore } from '@/common/memberInfo/store.js' import { getMemberId } from '@/utils/request.js' -import { getMyMemberCards } from '@/api/main.js' +import { getMyMemberCards, getCheckInStats, getMemberDetail } from '@/api/main.js' +import { getMemberBookings } from '@/api/groupCourse.js' import TabBar from '@/components/TabBar.vue' +import PageHeader from '@/components/index/PageHeader.vue' // 页面数据 const userInfo = ref({}) const stats = ref({}) const cardInfo = ref({}) const expiringCards = ref([]) -const bookingPreview = ref([]) -const checkIns = ref([]) const bodyReport = ref({}) const couponPoints = ref({}) const referral = ref({}) -// 导航栏样式 -const toolbarStyle = ref({}) -const toolbarSpacerStyle = ref({}) -const navRightStyle = ref({}) - // 快捷操作配置 const quickActionsRow1 = ref([ { key: 'booking', label: '预约课程', textClass: 'quick-actions__title', icon: '' }, @@ -592,33 +463,6 @@ const displayAvatar = computed(() => { }) // 方法 -function syncNavSafeArea() { - try { - const sys = uni.getSystemInfoSync() - const statusBarHeight = sys.statusBarHeight || 0 - const navHeight = 44 - const menu = uni.getMenuButtonBoundingClientRect?.() - - toolbarStyle.value = { - paddingTop: `${statusBarHeight}px` - } - - toolbarSpacerStyle.value = { - height: `${statusBarHeight + navHeight}px` - } - - if (menu && menu.width) { - const capsuleGap = sys.windowWidth - menu.left + 8 - navRightStyle.value = { - width: `${capsuleGap}px`, - minWidth: `${capsuleGap}px` - } - } - } catch (e) { - toolbarSpacerStyle.value = { height: '44px' } - } -} - function refreshFromStore() { const store = loadMemberStore() const pageData = getCenterPageData(store) @@ -626,13 +470,94 @@ function refreshFromStore() { stats.value = pageData.stats cardInfo.value = pageData.cardInfo expiringCards.value = pageData.expiringCards || [] - bookingPreview.value = pageData.bookingPreview - checkIns.value = pageData.checkIns bodyReport.value = pageData.bodyReport couponPoints.value = pageData.couponPoints referral.value = pageData.referral } +async function refreshStatsFromServer() { + const store = loadMemberStore() + let changed = false + + // 获取签到统计(累计签到 = 团课签到次数) + try { + const statsRes = await getCheckInStats() + console.log('[MemberCenter] getCheckInStats 原始响应:', JSON.stringify(statsRes)) + // 兼容多种后端响应格式 + const statsData = statsRes?.data || statsRes || {} + const totalCount = Number(statsData.totalCount ?? statsData.total ?? 0) + console.log('[MemberCenter] 解析后 totalCount:', totalCount) + if (store.stats.checkInCount !== totalCount) { + store.stats.checkInCount = totalCount + changed = true + } + } catch (e) { + console.error('[MemberCenter] 获取签到统计失败:', e) + } + + // 获取报课次数(会员预约数) + try { + const memberId = getMemberId() + console.log('[MemberCenter] getMemberId 结果:', memberId) + if (memberId) { + const bookingsRes = await getMemberBookings(memberId) + console.log('[MemberCenter] getMemberBookings 原始响应:', JSON.stringify(bookingsRes)) + // 兼容多种后端响应格式: + // 1. [{...}, {...}] → 直接数组 + // 2. { data: [{...}] } → data 是数组 + // 3. { data: { content: [...] } } → Spring 分页双层包装 + // 4. { content: [...] } → Spring Page 扁平原样 + let bookings = [] + const raw = bookingsRes || {} + if (Array.isArray(raw)) { + bookings = raw + } else if (Array.isArray(raw.data)) { + bookings = raw.data + } else if (raw.data && Array.isArray(raw.data.content)) { + bookings = raw.data.content + } else if (Array.isArray(raw.content)) { + bookings = raw.content + } else if (raw.data && typeof raw.data === 'object' && raw.data.data && Array.isArray(raw.data.data)) { + bookings = raw.data.data + } else if (raw.data && typeof raw.data === 'object' && raw.data.data && raw.data.data.content && Array.isArray(raw.data.data.content)) { + bookings = raw.data.data.content + } + const bookingCount = bookings.length + console.log('[MemberCenter] 解析后 bookingCount:', bookingCount, 'bookings:', JSON.stringify(bookings?.slice?.(0, 2))) + if (store.stats.courseCount !== bookingCount) { + store.stats.courseCount = bookingCount + changed = true + } + } else { + console.warn('[MemberCenter] getMemberId 返回 null/undefined,无法获取报课次数') + } + } catch (e) { + console.error('[MemberCenter] 获取报课次数失败:', e) + } + + // 获取积分 + try { + const detailRes = await getMemberDetail() + console.log('[MemberCenter] getMemberDetail 原始响应:', JSON.stringify(detailRes)) + const detailData = detailRes?.data || detailRes || {} + const points = Number(detailData.pointsBalance ?? detailData.points ?? 0) + console.log('[MemberCenter] 解析后 points:', points) + if (store.stats.pointsBalance !== points) { + store.stats.pointsBalance = points + changed = true + } + } catch (e) { + console.error('[MemberCenter] 获取积分失败:', e) + } + + console.log('[MemberCenter] 最终 stats:', JSON.stringify(store.stats)) + + if (changed) { + saveMemberStore(store) + refreshFromStore() + } +} + async function refreshCardsFromServer() { const memberId = getMemberId() if (!memberId) return @@ -699,14 +624,6 @@ function goMemberCard() { navigateToPage(PAGE.MEMBER_CARD) } -function goBooking() { - navigateToPage(PAGE.BOOKING) -} - -function goCheckInHistory() { - navigateToPage(PAGE.CHECK_IN_HISTORY) -} - function goBodyTestHistory() { navigateToPage(PAGE.BODY_TEST_HISTORY) } @@ -759,14 +676,6 @@ function onQuickAction(type) { } } -function onCheckInTap(item) { - uni.showModal({ - title: item.title, - content: item.time, - showCancel: false - }) -} - function copyReferralCode() { uni.setClipboardData({ data: referral.value.code, @@ -819,11 +728,11 @@ function handleLogout() { } onMounted(() => { - syncNavSafeArea() refreshFromStore() }) -onShow(() => { +onShow(async () => { + await refreshStatsFromServer() refreshCardsFromServer() }) @@ -833,17 +742,121 @@ onShow(() => { @import '@/common/style/memberInfo/member-info-component-reset.css'; @import '@/common/style/memberInfo/member-info-tap.css'; @import '@/common/style/memberInfo/member-info-page.css'; -@import '@/common/style/memberInfo/member-info-header.css'; @import '@/common/style/memberInfo/member-info-member-card.css'; @import '@/common/style/memberInfo/member-info-quick-actions.css'; -@import '@/common/style/memberInfo/member-info-booking-list.css'; -@import '@/common/style/memberInfo/member-info-check-in-list.css'; @import '@/common/style/memberInfo/member-info-body-report.css'; @import '@/common/style/memberInfo/member-info-coupon-points.css'; @import '@/common/style/memberInfo/member-info-referral.css'; @import '@/common/style/memberInfo/member-info-settings.css'; @import '@/common/style/memberInfo/member-info-logout.css'; +/* 个人中心页面头部背景 */ +.member-center-page-header { + background: #F5F7FA; +} + +/* 简约个人信息卡片 */ +.profile-card { + background: #FFFFFF; + border-radius: 20rpx; + padding: 28rpx 28rpx 24rpx; + box-shadow: 0 4rpx 20rpx rgba(45, 74, 90, 0.06); + display: flex; + flex-direction: column; + gap: 24rpx; + margin: 0 24rpx; +} + +.profile-card__main { + display: flex; + align-items: center; + gap: 20rpx; +} + +.profile-avatar { + width: 88rpx; + height: 88rpx; + border-radius: 50%; + overflow: hidden; + flex-shrink: 0; + border: 3rpx solid #E8F4F8; +} + +.profile-avatar__img { + width: 100%; + height: 100%; + border-radius: 50%; +} + +.profile-info { + flex: 1; + min-width: 0; +} + +.profile-info__row { + display: flex; + align-items: center; + gap: 12rpx; + margin-bottom: 6rpx; +} + +.profile-info__name { + font-size: 32rpx; + font-weight: 700; + color: #1A202C; + line-height: 1.2; +} + +.profile-info__badge { + display: flex; + align-items: center; + gap: 4rpx; + background: linear-gradient(135deg, #FFF7ED, #FFF1E6); + border-radius: 20rpx; + padding: 4rpx 14rpx; +} + +.profile-info__badge-icon { + width: 24rpx; + height: 24rpx; +} + +.profile-info__level { + font-size: 20rpx; + font-weight: 600; + color: #ED8936; +} + +.profile-info__phone { + font-size: 24rpx; + color: #A0AEC0; +} + +.profile-card__stats { + display: flex; + justify-content: space-around; + padding-top: 20rpx; + border-top: 1rpx solid #F0F4F8; +} + +.profile-stat { + display: flex; + flex-direction: column; + align-items: center; + gap: 4rpx; +} + +.profile-stat__value { + font-size: 36rpx; + font-weight: 700; + color: #2D4A5A; +} + +.profile-stat__label { + font-size: 22rpx; + color: #A0AEC0; +} + .tabbar-fixed { position: fixed; bottom: 0; @@ -853,18 +866,6 @@ onShow(() => { background-color: transparent; } -.booking-section__empty { - display: flex; - align-items: center; - justify-content: center; - padding: 24px 16px; -} - -.booking-section__empty-text { - font-size: 14px; - color: #8A99B4; -} - .expiring-cards-section { margin: 0 16px 12px; background: linear-gradient(135deg, #FFF7E6 0%, #FFE4CC 100%); diff --git a/gym-manage-uniapp/pages/memberInfo/memberInfo.vue b/gym-manage-uniapp/pages/memberInfo/memberInfo.vue index 6fdfbb5..c758bf8 100644 --- a/gym-manage-uniapp/pages/memberInfo/memberInfo.vue +++ b/gym-manage-uniapp/pages/memberInfo/memberInfo.vue @@ -1,5 +1,10 @@