完善团课前后端交互

This commit is contained in:
2026-06-15 15:49:21 +08:00
parent 96b8fd2534
commit 4e69185c48
7 changed files with 880 additions and 83 deletions
+35
View File
@@ -45,6 +45,40 @@ export function getTypeLabels(typeId, options = { cache: true, cacheTime: 5 * 60
return request.get(`/groupCourse/types/${typeId}/labels`, {}, options)
}
export function searchGroupCourse(params = {}, options = {}) {
const {
courseName,
courseType,
startDate,
endDate,
timePeriod,
priceSort,
remainingMost,
page = 0,
size = 10
} = params
const requestBody = { page, size }
if (courseName) requestBody.courseName = courseName
if (courseType) requestBody.courseType = courseType
if (startDate) requestBody.startDate = formatDateTime(startDate)
if (endDate) requestBody.endDate = formatDateTime(endDate, true)
if (timePeriod) requestBody.timePeriod = timePeriod
if (priceSort) requestBody.priceSort = priceSort
if (remainingMost !== undefined) requestBody.remainingMost = remainingMost
return request.post('/groupCourse/search', requestBody, options)
}
function formatDateTime(dateStr, isEnd = false) {
if (!dateStr) return dateStr
if (dateStr.includes('T')) return dateStr
return isEnd
? `${dateStr}T23:59:59`
: `${dateStr}T00:00:00`
}
export function bookGroupCourse(params) {
return request.post('/groupCourse/book', params)
}
@@ -60,6 +94,7 @@ export function getMemberBookings(memberId, options = {}) {
export default {
getGroupCourseList,
getGroupCoursePage,
searchGroupCourse,
getGroupCourseById,
getGroupCourseDetail,
createGroupCourse,
@@ -1,5 +1,20 @@
<template>
<view class="filter-section">
<!-- 课程类型筛选 -->
<picker
mode="selector"
:range="courseTypeOptions"
range-key="label"
:value="courseTypeIndex"
@change="onCourseTypeChange"
>
<view class="filter-item">
<uni-icons type="apps" size="18" color="#5E6F8D" class="filter-icon" />
<text class="filter-text">{{ courseTypeOptions[courseTypeIndex]?.label || '全部类型' }}</text>
<uni-icons type="right" size="20" color="#A0AEC0" class="filter-arrow" />
</view>
</picker>
<!-- 时间区间筛选 -->
<view class="filter-item" @click="handleTimePick">
<uni-icons type="calendar" size="18" color="#5E6F8D" class="filter-icon" />
@@ -25,7 +40,7 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { ref, watch, computed } from 'vue'
const props = defineProps({
timeRangeText: {
@@ -35,46 +50,90 @@ const props = defineProps({
sortOptions: {
type: Array,
default: () => [
{ label: '默认排序', value: 'default' },
{ label: '价格从低到高', value: 'priceAsc' },
{ label: '价格从高到低', value: 'priceDesc' },
{ label: '剩余名额最多', value: 'spotsDesc' },
{ label: '仅次数卡', value: 'pointCardOnly' },
{ label: '仅储值卡', value: 'storedValueOnly' },
{ label: '两种支付', value: 'bothPayment' }
{ label: '默认排序', value: 'default', priceSort: null, remainingMost: false },
{ label: '价格从低到高', value: 'priceAsc', priceSort: 'asc', remainingMost: false },
{ label: '价格从高到低', value: 'priceDesc', priceSort: 'desc', remainingMost: false },
{ label: '剩余名额最多', value: 'remainingMost', priceSort: null, remainingMost: true }
]
},
sortIndex: {
type: Number,
default: 0
},
courseTypes: {
type: Array,
default: () => []
},
currentCourseTypeId: {
type: [Number, String, null],
default: null,
validator: (val) => val === null || val === '' || !isNaN(Number(val))
}
})
const emit = defineEmits(['update:sortIndex', 'timePick'])
const emit = defineEmits(['update:sortIndex', 'timePick', 'courseTypeChange'])
const localSortIndex = ref(props.sortIndex)
const courseTypeOptions = computed(() => {
return [{ id: null, label: '全部类型' }, ...props.courseTypes]
})
const courseTypeIndex = computed(() => {
if (props.currentCourseTypeId === null || props.currentCourseTypeId === '') return 0
const currentId = Number(props.currentCourseTypeId)
const index = courseTypeOptions.value.findIndex(item => Number(item.id) === currentId)
return index >= 0 ? index : 0
})
watch(() => props.sortIndex, (val) => {
localSortIndex.value = val
})
const onSortChange = (e) => {
localSortIndex.value = e.detail.value
const sortOption = props.sortOptions[localSortIndex.value]
console.log('[FilterSection] 排序方式变更:', {
index: localSortIndex.value,
value: props.sortOptions[localSortIndex.value]
value: sortOption
})
if (sortOption.priceSort && sortOption.remainingMost) {
console.warn('[FilterSection] 排序参数冲突警告: priceSort和remainingMost不能同时设置')
}
emit('update:sortIndex', localSortIndex.value)
}
const onCourseTypeChange = (e) => {
const index = e.detail.value
const selectedType = courseTypeOptions.value[index]
// 确保返回数字类型而非字符串
const typeId = selectedType.id !== null ? Number(selectedType.id) : null
console.log('[FilterSection] 课程类型变更:', {
index,
id: typeId,
label: selectedType.label
})
emit('courseTypeChange', typeId)
}
const handleTimePick = () => {
console.log('[FilterSection] 触发时间选择器')
emit('timePick')
}
const getFilterParams = () => {
const sortOption = props.sortOptions[localSortIndex.value]
const selectedType = courseTypeOptions.value[courseTypeIndex.value]
const params = {
sortType: props.sortOptions[localSortIndex.value].value,
sortType: sortOption.value,
priceSort: sortOption.priceSort,
remainingMost: sortOption.remainingMost,
courseTypeId: selectedType.id !== null ? Number(selectedType.id) : null,
courseTypeName: selectedType.label,
timeRangeText: props.timeRangeText
}
console.log('[FilterSection] 获取筛选参数:', params)
@@ -173,18 +232,20 @@ defineExpose({
<style lang="scss" scoped>
.filter-section {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.filter-item {
flex: 1;
min-width: calc(33.33% - 12rpx);
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 20rpx 24rpx;
gap: 8rpx;
padding: 18rpx 16rpx;
background: #F5F7FA;
border-radius: 16rpx;
font-size: 26rpx;
font-size: 24rpx;
color: #5E6F8D;
.filter-icon {
@@ -197,6 +258,13 @@ defineExpose({
display: flex;
align-items: center;
}
.filter-text {
max-width: 100rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
</style>
@@ -1,5 +1,5 @@
import { ref, computed } from 'vue'
import { getGroupCoursePage, getTypeLabels } from '@/api/groupCourse.js'
import { ref, computed, watch } from 'vue'
import { getGroupCoursePage, getTypeLabels, searchGroupCourse } from '@/api/groupCourse.js'
export function useGroupCourseList() {
const pageNum = ref(0)
@@ -14,11 +14,14 @@ export function useGroupCourseList() {
const searchKeyword = ref('')
const hotKeywords = ref(['燃脂', '瑜伽', '单车', '普拉提', '高强度'])
const courseType = ref(null)
const courseTypes = ref([])
const sortOptions = ref([
{ label: '默认排序', value: 'id', order: 'asc' },
{ label: '价格从低到高', value: 'storedValueAmount', order: 'asc' },
{ label: '价格从高到低', value: 'storedValueAmount', order: 'desc' },
{ label: '剩余名额最多', value: 'currentMembers', order: 'asc' }
{ label: '默认排序', value: 'default', priceSort: null, remainingMost: false },
{ label: '价格从低到高', value: 'priceAsc', priceSort: 'asc', remainingMost: false },
{ label: '价格从高到低', value: 'priceDesc', priceSort: 'desc', remainingMost: false },
{ label: '剩余名额最多', value: 'remainingMost', priceSort: null, remainingMost: true }
])
const sortIndex = ref(0)
@@ -73,6 +76,13 @@ export function useGroupCourseList() {
return result
})
// 监听排序方式变化,重新获取数据
watch(sortIndex, () => {
console.log('[useGroupCourseList] 排序方式变化,触发重新查询')
pageNum.value = 0
fetchCourseList()
})
const getAllSearchParams = (searchBarRef, filterSectionRef, timePeriodRef, timeRangePickerRef) => {
const searchParams = searchBarRef?.getSearchParams?.() || { keyword: searchKeyword.value }
const filterParams = filterSectionRef?.getFilterParams?.() || { sortType: sortOptions.value[sortIndex.value].value }
@@ -99,6 +109,8 @@ export function useGroupCourseList() {
const onTimePeriodChange = (option) => {
console.log('[useGroupCourseList] 时间段选择:', option)
pageNum.value = 0
fetchCourseList()
}
const onTimeRangeConfirm = (params) => {
@@ -108,6 +120,19 @@ export function useGroupCourseList() {
timeRangeText.value = params.timeRangeText
}
const onCourseTypeChange = (typeId) => {
console.log('[useGroupCourseList] 课程类型选择:', typeId)
courseType.value = typeId
pageNum.value = 0
fetchCourseList()
}
const clearCourseType = () => {
courseType.value = null
pageNum.value = 0
fetchCourseList()
}
const handleBooking = (course) => {
console.log('[useGroupCourseList] 预约课程:', course)
uni.showToast({
@@ -123,33 +148,75 @@ export function useGroupCourseList() {
})
}
const buildSearchParams = () => {
const sortOption = sortOptions.value[sortIndex.value]
const timePeriod = timePeriodOptions.value[timePeriodIndex.value]
const params = {
page: pageNum.value,
size: pageSize.value
}
if (searchKeyword.value) {
params.courseName = searchKeyword.value
}
if (courseType.value) {
params.courseType = courseType.value
}
if (startDate.value) {
params.startDate = startDate.value
}
if (endDate.value) {
params.endDate = endDate.value
}
if (timePeriod.value && timePeriod.value !== 'all') {
params.timePeriod = timePeriod.value
}
if (sortOption.priceSort) {
params.priceSort = sortOption.priceSort
}
if (sortOption.remainingMost) {
params.remainingMost = sortOption.remainingMost
}
return params
}
const hasActiveFilters = () => {
return searchKeyword.value ||
courseType.value ||
startDate.value ||
endDate.value ||
timePeriodOptions.value[timePeriodIndex.value].value !== 'all' ||
sortIndex.value !== 0
}
const fetchCourseList = async (isLoadMore = false) => {
if (loading.value) return
loading.value = true
try {
const sortOption = sortOptions.value[sortIndex.value]
console.log('[useGroupCourseList] 请求参数:', {
page: isLoadMore ? pageNum.value + 1 : pageNum.value,
size: pageSize.value,
sort: sortOption.value,
order: sortOption.order,
keyword: searchKeyword.value
})
const currentPage = isLoadMore ? pageNum.value + 1 : pageNum.value
pageNum.value = currentPage
const result = await getGroupCoursePage({
page: isLoadMore ? pageNum.value + 1 : pageNum.value,
size: pageSize.value,
sort: sortOption.value,
order: sortOption.order,
keyword: searchKeyword.value
})
const searchParams = buildSearchParams()
searchParams.page = currentPage
console.log('[useGroupCourseList] 请求参数:', JSON.stringify(searchParams, null, 2))
const result = await searchGroupCourse(searchParams)
console.log('[useGroupCourseList] 响应结果:', JSON.stringify(result, null, 2))
if (result && result.content) {
const { content: list, totalElements: totalCount, currentPage, totalPages: pages } = result
if (result && result.data && result.data.content) {
const { content: list, totalElements: totalCount, currentPage: respPage, totalPages: pages } = result.data
if (isLoadMore) {
courseList.value = [...courseList.value, ...list]
@@ -158,7 +225,6 @@ export function useGroupCourseList() {
}
total.value = totalCount
pageNum.value = currentPage
totalPages.value = pages
hasMore.value = currentPage < pages - 1
@@ -213,6 +279,8 @@ export function useGroupCourseList() {
courseList,
searchKeyword,
hotKeywords,
courseType,
courseTypes,
sortOptions,
sortIndex,
timePeriodOptions,
@@ -227,9 +295,13 @@ export function useGroupCourseList() {
handleSearch,
onTimePeriodChange,
onTimeRangeConfirm,
onCourseTypeChange,
clearCourseType,
handleBooking,
goDetail,
fetchCourseList,
buildSearchParams,
hasActiveFilters,
loadMore,
onScrollToLower
}
+144
View File
@@ -14,6 +14,7 @@
3. [团课管理接口](#团课管理接口)
- [获取所有团课](#获取所有团课)
- [分页获取团课](#分页获取团课)
- [多条件查询团课](#多条件查询团课)
- [根据ID获取团课详情](#根据ID获取团课详情)
- [创建团课](#创建团课)
- [更新团课](#更新团课)
@@ -154,6 +155,149 @@
---
### 多条件查询团课
| 属性 | 值 |
|------|-----|
| **HTTP方法** | POST |
| **接口路径** | `/api/groupCourse/search` |
| **所属文件** | `GroupCourseHandler.java` |
**功能说明**: 支持团课名称模糊查询、类型筛选、日期范围、时间段、价格排序、剩余名额排序等多条件组合查询,默认不查询不可预约的团课(已取消或已结束)。
**请求体**:
```json
{
"courseName": "瑜伽",
"courseType": 1,
"startDate": "2026-06-01T00:00:00",
"endDate": "2026-06-30T23:59:59",
"timePeriod": "morning",
"priceSort": "asc",
"remainingMost": true,
"page": 0,
"size": 10
}
```
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| courseName | String | 否 | - | 课程名称(模糊查询,不区分大小写) |
| courseType | Long | 否 | - | 课程类型ID |
| startDate | LocalDateTime | 否 | - | 查询开始日期 |
| endDate | LocalDateTime | 否 | - | 查询结束日期 |
| timePeriod | String | 否 | - | 时间段:`morning`(6:00-12:00)、`afternoon`(12:00-18:00)、`evening`(18:00-24:00) |
| priceSort | String | 否 | - | 价格排序:`asc`(从低到高)、`desc`(从高到低) |
| remainingMost | Boolean | 否 | false | 是否按剩余名额最多排序 |
| page | Integer | 否 | 0 | 页码,从0开始 |
| size | Integer | 否 | 10 | 每页数量,最大100 |
**查询条件优先级说明**:
| 优先级 | 条件类型 | 说明 |
|--------|----------|------|
| 1 | 默认过滤 | 自动过滤已删除和不可预约的团课(status != 0 |
| 2 | 基础筛选 | courseName、courseType、startDate、endDate、timePeriod |
| 3 | 排序规则 | remainingMost优先,其次priceSort,默认按startTime升序 |
**成功响应** (200 OK):
```json
{
"success": true,
"message": "查询成功",
"data": {
"content": [
{
"id": 1,
"courseName": "瑜伽入门",
"coachId": 1,
"courseType": 1,
"startTime": "2026-06-15T09:00:00",
"endTime": "2026-06-15T10:00:00",
"maxMembers": 20,
"currentMembers": 5,
"status": 0,
"location": "健身房A区",
"coverImage": "https://example.com/yoga.jpg",
"description": "适合初学者的瑜伽课程",
"pointCardAmount": 1,
"storedValueAmount": 50.00,
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00"
}
],
"totalPages": 3,
"totalElements": 25,
"currentPage": 0,
"pageSize": 10,
"first": true,
"last": false
}
}
```
**失败响应** (400 Bad Request):
```json
{
"success": false,
"message": "查询失败的原因"
}
```
**使用示例**:
1. **查询瑜伽课程**
```json
{
"courseName": "瑜伽"
}
```
2. **查询特定类型的早晨课程**
```json
{
"courseType": 1,
"timePeriod": "morning"
}
```
3. **查询下周的课程,按价格从低到高排序**
```json
{
"startDate": "2026-06-16T00:00:00",
"endDate": "2026-06-22T23:59:59",
"priceSort": "asc"
}
```
4. **查询剩余名额最多的晚间课程**
```json
{
"timePeriod": "evening",
"remainingMost": true
}
```
5. **多条件组合查询**
```json
{
"courseName": "瑜伽",
"courseType": 1,
"startDate": "2026-06-01T00:00:00",
"endDate": "2026-06-30T23:59:59",
"timePeriod": "morning",
"priceSort": "asc",
"remainingMost": true,
"page": 0,
"size": 10
}
```
---
### 根据ID获取团课详情
| 属性 | 值 |
+448
View File
@@ -0,0 +1,448 @@
[API] 响应数据: {
"data": {
"content": [
{
"id": "1",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-01T11:00:00",
"updatedAt": "2026-06-01T11:00:00",
"deletedAt": null,
"courseName": "极速燃脂单车",
"coachId": "104",
"courseType": "2",
"startTime": "2026-06-02T16:45:00",
"endTime": "2026-06-15T20:20:00",
"maxMembers": 25,
"currentMembers": 0,
"status": "0",
"location": "单车房",
"coverImage": "/images/spinning.jpg",
"description": "跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感,一节课消耗800大卡。",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "8",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00",
"deletedAt": null,
"courseName": "燃脂搏击_次数卡课程",
"coachId": "102",
"courseType": "2",
"startTime": "2026-06-10T19:30:00",
"endTime": "2026-06-10T20:30:00",
"maxMembers": 20,
"currentMembers": 0,
"status": "0",
"location": "综合训练区",
"coverImage": null,
"description": "高强度间歇训练,配合音乐快速燃脂,消耗1次",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "11",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-02T10:00:00",
"updatedAt": "2026-06-02T10:00:00",
"deletedAt": null,
"courseName": "时间冲突测试_A_13点-15点",
"coachId": "102",
"courseType": "2",
"startTime": "2026-06-15T13:00:00",
"endTime": "2026-06-15T15:00:00",
"maxMembers": 20,
"currentMembers": 0,
"status": "0",
"location": "综合训练区",
"coverImage": null,
"description": "测试用团课A,用于验证时间冲突检测",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "10",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-02T10:00:00",
"updatedAt": "2026-06-02T10:00:00",
"deletedAt": null,
"courseName": "晚间瑜伽_取消测试",
"coachId": "101",
"courseType": "1",
"startTime": "2026-06-15T19:00:00",
"endTime": "2026-06-15T20:00:00",
"maxMembers": 20,
"currentMembers": 3,
"status": "0",
"location": "瑜伽教室",
"coverImage": null,
"description": "适合所有级别的瑜伽课程,用于测试取消预约功能",
"pointCardAmount": 1,
"storedValueAmount": 30
},
{
"id": "12",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-02T10:00:00",
"updatedAt": "2026-06-02T10:00:00",
"deletedAt": null,
"courseName": "时间冲突测试_B_14点-16点",
"coachId": "103",
"courseType": "1",
"startTime": "2026-06-15T14:00:00",
"endTime": "2026-06-15T16:00:00",
"maxMembers": 15,
"currentMembers": 0,
"status": "0",
"location": "普拉提教室",
"coverImage": null,
"description": "测试用团课B,与团课A时间重叠(14:00-15:00",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "9",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00",
"deletedAt": null,
"courseName": "高端普拉提_储值卡课程",
"coachId": "103",
"courseType": "1",
"startTime": "2026-06-11T19:00:00",
"endTime": "2026-06-11T20:00:00",
"maxMembers": 15,
"currentMembers": 0,
"status": "0",
"location": "普拉提教室",
"coverImage": null,
"description": "精准训练核心肌群,消耗储值50元",
"pointCardAmount": 0,
"storedValueAmount": 20
},
{
"id": "13",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-02T10:00:00",
"updatedAt": "2026-06-02T10:00:00",
"deletedAt": null,
"courseName": "时间冲突测试_C_10点-12点",
"coachId": "101",
"courseType": "1",
"startTime": "2026-06-15T10:00:00",
"endTime": "2026-06-15T12:00:00",
"maxMembers": 15,
"currentMembers": 0,
"status": "0",
"location": "瑜伽教室",
"coverImage": null,
"description": "测试用团课C,与团课A/B不冲突",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "2",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00",
"deletedAt": null,
"courseName": "清晨流瑜伽",
"coachId": "101",
"courseType": "1",
"startTime": "2026-06-12T09:00:00",
"endTime": "2026-06-12T10:30:00",
"maxMembers": 15,
"currentMembers": 5,
"status": "0",
"location": "A座3楼瑜伽教室",
"coverImage": "/images/yoga_flow.jpg",
"description": "适合有一定基础的学员,通过流畅的体式连接呼吸,唤醒身体能量。",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "4",
"createBy": "coach_li",
"updateBy": null,
"createdAt": "2026-06-01T08:00:00",
"updatedAt": "2026-06-01T08:00:00",
"deletedAt": null,
"courseName": "哈他瑜伽",
"coachId": "101",
"courseType": "1",
"startTime": "2026-06-01T15:20:00",
"endTime": "2026-06-01T16:50:00",
"maxMembers": 12,
"currentMembers": 3,
"status": "0",
"location": "瑜伽教室B",
"coverImage": "/images/hatha_yoga.jpg",
"description": "基础哈他瑜伽,适合所有级别。距开始不足30分钟,已停止预约。",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "3001",
"createBy": null,
"updateBy": null,
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00",
"deletedAt": null,
"courseName": "瑜伽入门",
"coachId": "1",
"courseType": "1",
"startTime": "2026-06-09T08:00:00",
"endTime": "2026-06-09T09:00:00",
"maxMembers": 20,
"currentMembers": 15,
"status": "0",
"location": "健身房A区",
"coverImage": "https://example.com/yoga.jpg",
"description": "适合初学者的瑜伽课程",
"pointCardAmount": 1,
"storedValueAmount": 0
}
],
"totalPages": 2,
"totalElements": "13",
"currentPage": 0,
"pageSize": 10,
"first": true,
"last": false
},
"success": true,
"message": "查询成功"
} request.js:185:17
[useGroupCourseList] 响应结果: {
"data": {
"content": [
{
"id": "1",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-01T11:00:00",
"updatedAt": "2026-06-01T11:00:00",
"deletedAt": null,
"courseName": "极速燃脂单车",
"coachId": "104",
"courseType": "2",
"startTime": "2026-06-02T16:45:00",
"endTime": "2026-06-15T20:20:00",
"maxMembers": 25,
"currentMembers": 0,
"status": "0",
"location": "单车房",
"coverImage": "/images/spinning.jpg",
"description": "跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感,一节课消耗800大卡。",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "8",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00",
"deletedAt": null,
"courseName": "燃脂搏击_次数卡课程",
"coachId": "102",
"courseType": "2",
"startTime": "2026-06-10T19:30:00",
"endTime": "2026-06-10T20:30:00",
"maxMembers": 20,
"currentMembers": 0,
"status": "0",
"location": "综合训练区",
"coverImage": null,
"description": "高强度间歇训练,配合音乐快速燃脂,消耗1次",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "11",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-02T10:00:00",
"updatedAt": "2026-06-02T10:00:00",
"deletedAt": null,
"courseName": "时间冲突测试_A_13点-15点",
"coachId": "102",
"courseType": "2",
"startTime": "2026-06-15T13:00:00",
"endTime": "2026-06-15T15:00:00",
"maxMembers": 20,
"currentMembers": 0,
"status": "0",
"location": "综合训练区",
"coverImage": null,
"description": "测试用团课A,用于验证时间冲突检测",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "10",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-02T10:00:00",
"updatedAt": "2026-06-02T10:00:00",
"deletedAt": null,
"courseName": "晚间瑜伽_取消测试",
"coachId": "101",
"courseType": "1",
"startTime": "2026-06-15T19:00:00",
"endTime": "2026-06-15T20:00:00",
"maxMembers": 20,
"currentMembers": 3,
"status": "0",
"location": "瑜伽教室",
"coverImage": null,
"description": "适合所有级别的瑜伽课程,用于测试取消预约功能",
"pointCardAmount": 1,
"storedValueAmount": 30
},
{
"id": "12",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-02T10:00:00",
"updatedAt": "2026-06-02T10:00:00",
"deletedAt": null,
"courseName": "时间冲突测试_B_14点-16点",
"coachId": "103",
"courseType": "1",
"startTime": "2026-06-15T14:00:00",
"endTime": "2026-06-15T16:00:00",
"maxMembers": 15,
"currentMembers": 0,
"status": "0",
"location": "普拉提教室",
"coverImage": null,
"description": "测试用团课B,与团课A时间重叠(14:00-15:00",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "9",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00",
"deletedAt": null,
"courseName": "高端普拉提_储值卡课程",
"coachId": "103",
"courseType": "1",
"startTime": "2026-06-11T19:00:00",
"endTime": "2026-06-11T20:00:00",
"maxMembers": 15,
"currentMembers": 0,
"status": "0",
"location": "普拉提教室",
"coverImage": null,
"description": "精准训练核心肌群,消耗储值50元",
"pointCardAmount": 0,
"storedValueAmount": 20
},
{
"id": "13",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-02T10:00:00",
"updatedAt": "2026-06-02T10:00:00",
"deletedAt": null,
"courseName": "时间冲突测试_C_10点-12点",
"coachId": "101",
"courseType": "1",
"startTime": "2026-06-15T10:00:00",
"endTime": "2026-06-15T12:00:00",
"maxMembers": 15,
"currentMembers": 0,
"status": "0",
"location": "瑜伽教室",
"coverImage": null,
"description": "测试用团课C,与团课A/B不冲突",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "2",
"createBy": "admin",
"updateBy": null,
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00",
"deletedAt": null,
"courseName": "清晨流瑜伽",
"coachId": "101",
"courseType": "1",
"startTime": "2026-06-12T09:00:00",
"endTime": "2026-06-12T10:30:00",
"maxMembers": 15,
"currentMembers": 5,
"status": "0",
"location": "A座3楼瑜伽教室",
"coverImage": "/images/yoga_flow.jpg",
"description": "适合有一定基础的学员,通过流畅的体式连接呼吸,唤醒身体能量。",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "4",
"createBy": "coach_li",
"updateBy": null,
"createdAt": "2026-06-01T08:00:00",
"updatedAt": "2026-06-01T08:00:00",
"deletedAt": null,
"courseName": "哈他瑜伽",
"coachId": "101",
"courseType": "1",
"startTime": "2026-06-01T15:20:00",
"endTime": "2026-06-01T16:50:00",
"maxMembers": 12,
"currentMembers": 3,
"status": "0",
"location": "瑜伽教室B",
"coverImage": "/images/hatha_yoga.jpg",
"description": "基础哈他瑜伽,适合所有级别。距开始不足30分钟,已停止预约。",
"pointCardAmount": 1,
"storedValueAmount": 0
},
{
"id": "3001",
"createBy": null,
"updateBy": null,
"createdAt": "2026-06-01T10:00:00",
"updatedAt": "2026-06-01T10:00:00",
"deletedAt": null,
"courseName": "瑜伽入门",
"coachId": "1",
"courseType": "1",
"startTime": "2026-06-09T08:00:00",
"endTime": "2026-06-09T09:00:00",
"maxMembers": 20,
"currentMembers": 15,
"status": "0",
"location": "健身房A区",
"coverImage": "https://example.com/yoga.jpg",
"description": "适合初学者的瑜伽课程",
"pointCardAmount": 1,
"storedValueAmount": 0
}
],
"totalPages": 2,
"totalElements": "13",
"currentPage": 0,
"pageSize": 10,
"first": true,
"last": false
},
"success": true,
"message": "查询成功"
}
+37 -33
View File
@@ -107,7 +107,6 @@
<view class="course-tags">
<view class="tags-label">
<uni-icons type="tag" size="20" color="#8A99B4" />
<text>课程标签</text>
</view>
<view class="tags-list">
<view
@@ -212,8 +211,8 @@
<!-- 免费课程或单一支付方式 -->
<view v-else class="price-display">
<text v-if="course.storedValueAmount === 0 && course.pointCardAmount === 0" class="price free">免费</text>
<text v-else-if="course.storedValueAmount > 0" class="price">
<text v-if="Number(course.storedValueAmount) === 0 && Number(course.pointCardAmount) === 0" class="price free">免费</text>
<text v-else-if="Number(course.storedValueAmount) > 0" class="price">
<text class="currency">¥</text>{{ course.storedValueAmount }}
</text>
<text v-else class="price">
@@ -233,7 +232,7 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { groupCourseService } from '@/request_api/groupCourse.mock.js'
import { getGroupCourseDetail, bookGroupCourse } from '@/api/groupCourse.js'
import PageHeader from '@/components/index/PageHeader.vue'
// 加载状态
@@ -305,7 +304,9 @@ const canBook = computed(() => {
// 是否有多种支付方式
const hasMultiplePayment = computed(() => {
return course.value.storedValueAmount > 0 && course.value.pointCardAmount > 0
const storedValue = Number(course.value.storedValueAmount)
const pointCard = Number(course.value.pointCardAmount)
return storedValue > 0 && pointCard > 0
})
// 当前选中的支付方式
@@ -313,17 +314,20 @@ const selectedPayment = ref('storedValue')
// 当前选中的支付方式价格显示
const selectedPrice = computed(() => {
const storedValue = Number(course.value.storedValueAmount)
const pointCard = Number(course.value.pointCardAmount)
if (selectedPayment.value === 'storedValue') {
return { type: 'storedValue', amount: course.value.storedValueAmount }
return { type: 'storedValue', amount: storedValue }
} else if (selectedPayment.value === 'pointCard') {
return { type: 'pointCard', amount: course.value.pointCardAmount }
} else if (course.value.storedValueAmount > 0 && course.value.pointCardAmount > 0) {
return { type: 'pointCard', amount: pointCard }
} else if (storedValue > 0 && pointCard > 0) {
// 默认优先显示储值卡
return { type: 'storedValue', amount: course.value.storedValueAmount }
} else if (course.value.storedValueAmount > 0) {
return { type: 'storedValue', amount: course.value.storedValueAmount }
} else if (course.value.pointCardAmount > 0) {
return { type: 'pointCard', amount: course.value.pointCardAmount }
return { type: 'storedValue', amount: storedValue }
} else if (storedValue > 0) {
return { type: 'storedValue', amount: storedValue }
} else if (pointCard > 0) {
return { type: 'pointCard', amount: pointCard }
}
return { type: 'free', amount: 0 }
})
@@ -378,11 +382,13 @@ const handleBooking = () => {
success: (res) => {
if (res.confirm) {
uni.showLoading({ title: '预约中...' })
groupCourseService.book({
bookGroupCourse({
courseId: course.value.id,
memberId: '1' // 模拟会员ID
}).then(() => {
memberId: '1', // 模拟会员ID
memberCardRecordId: '1' // 模拟会员卡记录ID
}).then((result) => {
uni.hideLoading()
if (result.success) {
uni.showToast({
title: '预约成功',
icon: 'success'
@@ -390,8 +396,15 @@ const handleBooking = () => {
setTimeout(() => {
uni.navigateBack()
}, 1500)
}).catch(() => {
} else {
uni.showToast({
title: result.message || '预约失败',
icon: 'none'
})
}
}).catch((error) => {
uni.hideLoading()
console.error('[detail.vue] 预约失败:', error)
uni.showToast({
title: '预约失败',
icon: 'none'
@@ -405,12 +418,16 @@ const handleBooking = () => {
// 获取课程详情
const fetchCourseDetail = async (id) => {
try {
const result = await groupCourseService.getDetail(id)
console.log('[detail.vue] 开始获取课程详情, id:', id)
const result = await getGroupCourseDetail(id)
course.value = result
console.log('[detail.vue] 课程详情获取成功:', course.value)
// 获取课程类型标签
await fetchCourseLabels(course.value.courseType)
// 从完整信息中提取标签
if (result.labels && Array.isArray(result.labels)) {
courseLabels.value = result.labels
}
console.log('[detail.vue] 课程标签获取成功:', courseLabels.value)
} catch (error) {
console.error('[detail.vue] 获取课程详情失败:', error)
uni.showToast({
@@ -422,19 +439,6 @@ const fetchCourseDetail = async (id) => {
}
}
// 获取课程标签
const fetchCourseLabels = async (courseType) => {
try {
const result = await groupCourseService.getLabelsByCourseType(courseType)
if (result.code === 0) {
courseLabels.value = result.data
}
console.log('[detail.vue] 课程标签获取成功:', courseLabels.value)
} catch (error) {
console.error('[detail.vue] 获取课程标签失败:', error)
}
}
// 页面挂载时获取课程详情
onMounted(() => {
const pages = getCurrentPages()
+30 -4
View File
@@ -18,7 +18,10 @@
:time-range-text="timeRangeText"
:sort-options="sortOptions"
v-model:sort-index="sortIndex"
:course-types="courseTypes"
:current-course-type-id="courseType"
@time-pick="showTimePicker = true"
@course-type-change="onCourseTypeChange"
ref="filterSectionRef"
/>
@@ -79,7 +82,7 @@ import TimePeriodSelector from '@/components/groupCourse/TimePeriodSelector.vue'
import TimeRangePicker from '@/components/groupCourse/TimeRangePicker.vue'
import PageHeader from '@/components/index/PageHeader.vue'
import { useGroupCourseList } from '@/composables/useGroupCourseList.js'
import { getTypeLabels } from '@/api/groupCourse.js'
import { getTypeLabels, getGroupCourseTypes } from '@/api/groupCourse.js'
// 组件引用
const searchBarRef = ref(null)
@@ -97,6 +100,8 @@ const {
hasMore,
searchKeyword,
hotKeywords,
courseType,
courseTypes,
sortOptions,
sortIndex,
timePeriodOptions,
@@ -112,6 +117,8 @@ const {
handleSearch,
onTimePeriodChange,
onTimeRangeConfirm,
onCourseTypeChange,
clearCourseType,
handleBooking,
goDetail,
fetchCourseList,
@@ -122,7 +129,11 @@ const {
// 组件挂载时调用接口获取团课列表
onMounted(async () => {
console.log('[list.vue] 页面组件已挂载,开始获取团课列表')
await fetchCourseList()
// 并行获取课程类型列表和课程列表
await Promise.all([
fetchCourseTypes(),
fetchCourseList()
])
// 获取所有团课的类型标签
await fetchAllCourseTypeLabels()
console.log('[list.vue] 可用的搜索参数获取方法:')
@@ -133,9 +144,24 @@ onMounted(async () => {
console.log(' - getAllSearchParams() 获取所有参数')
})
const fetchCourseTypes = async () => {
try {
const result = await getGroupCourseTypes()
if (result && Array.isArray(result)) {
courseTypes.value = result.map(type => ({
id: type.id,
label: type.typeName
}))
console.log('[list.vue] 获取课程类型成功:', courseTypes.value)
}
} catch (error) {
console.error('[list.vue] 获取课程类型失败:', error)
}
}
const fetchAllCourseTypeLabels = async () => {
const courseTypes = [...new Set(filteredCourseList.value.map(c => c.courseType))]
for (const type of courseTypes) {
const types = [...new Set(filteredCourseList.value.map(c => c.courseType))]
for (const type of types) {
if (!courseTypeLabelsCache.value[type]) {
try {
const result = await getTypeLabels(type)