完善团课前后端交互
This commit is contained in:
@@ -45,6 +45,40 @@ export function getTypeLabels(typeId, options = { cache: true, cacheTime: 5 * 60
|
|||||||
return request.get(`/groupCourse/types/${typeId}/labels`, {}, options)
|
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) {
|
export function bookGroupCourse(params) {
|
||||||
return request.post('/groupCourse/book', params)
|
return request.post('/groupCourse/book', params)
|
||||||
}
|
}
|
||||||
@@ -60,6 +94,7 @@ export function getMemberBookings(memberId, options = {}) {
|
|||||||
export default {
|
export default {
|
||||||
getGroupCourseList,
|
getGroupCourseList,
|
||||||
getGroupCoursePage,
|
getGroupCoursePage,
|
||||||
|
searchGroupCourse,
|
||||||
getGroupCourseById,
|
getGroupCourseById,
|
||||||
getGroupCourseDetail,
|
getGroupCourseDetail,
|
||||||
createGroupCourse,
|
createGroupCourse,
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="filter-section">
|
<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">
|
<view class="filter-item" @click="handleTimePick">
|
||||||
<uni-icons type="calendar" size="18" color="#5E6F8D" class="filter-icon" />
|
<uni-icons type="calendar" size="18" color="#5E6F8D" class="filter-icon" />
|
||||||
@@ -25,7 +40,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch, computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
timeRangeText: {
|
timeRangeText: {
|
||||||
@@ -35,46 +50,90 @@ const props = defineProps({
|
|||||||
sortOptions: {
|
sortOptions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [
|
default: () => [
|
||||||
{ label: '默认排序', value: 'default' },
|
{ label: '默认排序', value: 'default', priceSort: null, remainingMost: false },
|
||||||
{ label: '价格从低到高', value: 'priceAsc' },
|
{ label: '价格从低到高', value: 'priceAsc', priceSort: 'asc', remainingMost: false },
|
||||||
{ label: '价格从高到低', value: 'priceDesc' },
|
{ label: '价格从高到低', value: 'priceDesc', priceSort: 'desc', remainingMost: false },
|
||||||
{ label: '剩余名额最多', value: 'spotsDesc' },
|
{ label: '剩余名额最多', value: 'remainingMost', priceSort: null, remainingMost: true }
|
||||||
{ label: '仅次数卡', value: 'pointCardOnly' },
|
|
||||||
{ label: '仅储值卡', value: 'storedValueOnly' },
|
|
||||||
{ label: '两种支付', value: 'bothPayment' }
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
sortIndex: {
|
sortIndex: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
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 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) => {
|
watch(() => props.sortIndex, (val) => {
|
||||||
localSortIndex.value = val
|
localSortIndex.value = val
|
||||||
})
|
})
|
||||||
|
|
||||||
const onSortChange = (e) => {
|
const onSortChange = (e) => {
|
||||||
localSortIndex.value = e.detail.value
|
localSortIndex.value = e.detail.value
|
||||||
|
const sortOption = props.sortOptions[localSortIndex.value]
|
||||||
|
|
||||||
console.log('[FilterSection] 排序方式变更:', {
|
console.log('[FilterSection] 排序方式变更:', {
|
||||||
index: localSortIndex.value,
|
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)
|
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 = () => {
|
const handleTimePick = () => {
|
||||||
console.log('[FilterSection] 触发时间选择器')
|
console.log('[FilterSection] 触发时间选择器')
|
||||||
emit('timePick')
|
emit('timePick')
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFilterParams = () => {
|
const getFilterParams = () => {
|
||||||
|
const sortOption = props.sortOptions[localSortIndex.value]
|
||||||
|
const selectedType = courseTypeOptions.value[courseTypeIndex.value]
|
||||||
|
|
||||||
const params = {
|
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
|
timeRangeText: props.timeRangeText
|
||||||
}
|
}
|
||||||
console.log('[FilterSection] 获取筛选参数:', params)
|
console.log('[FilterSection] 获取筛选参数:', params)
|
||||||
@@ -173,18 +232,20 @@ defineExpose({
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.filter-section {
|
.filter-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
|
|
||||||
.filter-item {
|
.filter-item {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: calc(33.33% - 12rpx);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 12rpx;
|
gap: 8rpx;
|
||||||
padding: 20rpx 24rpx;
|
padding: 18rpx 16rpx;
|
||||||
background: #F5F7FA;
|
background: #F5F7FA;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
font-size: 26rpx;
|
font-size: 24rpx;
|
||||||
color: #5E6F8D;
|
color: #5E6F8D;
|
||||||
|
|
||||||
.filter-icon {
|
.filter-icon {
|
||||||
@@ -197,6 +258,13 @@ defineExpose({
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.filter-text {
|
||||||
|
max-width: 100rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { getGroupCoursePage, getTypeLabels } from '@/api/groupCourse.js'
|
import { getGroupCoursePage, getTypeLabels, searchGroupCourse } from '@/api/groupCourse.js'
|
||||||
|
|
||||||
export function useGroupCourseList() {
|
export function useGroupCourseList() {
|
||||||
const pageNum = ref(0)
|
const pageNum = ref(0)
|
||||||
@@ -14,11 +14,14 @@ export function useGroupCourseList() {
|
|||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
const hotKeywords = ref(['燃脂', '瑜伽', '单车', '普拉提', '高强度'])
|
const hotKeywords = ref(['燃脂', '瑜伽', '单车', '普拉提', '高强度'])
|
||||||
|
|
||||||
|
const courseType = ref(null)
|
||||||
|
const courseTypes = ref([])
|
||||||
|
|
||||||
const sortOptions = ref([
|
const sortOptions = ref([
|
||||||
{ label: '默认排序', value: 'id', order: 'asc' },
|
{ label: '默认排序', value: 'default', priceSort: null, remainingMost: false },
|
||||||
{ label: '价格从低到高', value: 'storedValueAmount', order: 'asc' },
|
{ label: '价格从低到高', value: 'priceAsc', priceSort: 'asc', remainingMost: false },
|
||||||
{ label: '价格从高到低', value: 'storedValueAmount', order: 'desc' },
|
{ label: '价格从高到低', value: 'priceDesc', priceSort: 'desc', remainingMost: false },
|
||||||
{ label: '剩余名额最多', value: 'currentMembers', order: 'asc' }
|
{ label: '剩余名额最多', value: 'remainingMost', priceSort: null, remainingMost: true }
|
||||||
])
|
])
|
||||||
const sortIndex = ref(0)
|
const sortIndex = ref(0)
|
||||||
|
|
||||||
@@ -73,6 +76,13 @@ export function useGroupCourseList() {
|
|||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听排序方式变化,重新获取数据
|
||||||
|
watch(sortIndex, () => {
|
||||||
|
console.log('[useGroupCourseList] 排序方式变化,触发重新查询')
|
||||||
|
pageNum.value = 0
|
||||||
|
fetchCourseList()
|
||||||
|
})
|
||||||
|
|
||||||
const getAllSearchParams = (searchBarRef, filterSectionRef, timePeriodRef, timeRangePickerRef) => {
|
const getAllSearchParams = (searchBarRef, filterSectionRef, timePeriodRef, timeRangePickerRef) => {
|
||||||
const searchParams = searchBarRef?.getSearchParams?.() || { keyword: searchKeyword.value }
|
const searchParams = searchBarRef?.getSearchParams?.() || { keyword: searchKeyword.value }
|
||||||
const filterParams = filterSectionRef?.getFilterParams?.() || { sortType: sortOptions.value[sortIndex.value].value }
|
const filterParams = filterSectionRef?.getFilterParams?.() || { sortType: sortOptions.value[sortIndex.value].value }
|
||||||
@@ -99,6 +109,8 @@ export function useGroupCourseList() {
|
|||||||
|
|
||||||
const onTimePeriodChange = (option) => {
|
const onTimePeriodChange = (option) => {
|
||||||
console.log('[useGroupCourseList] 时间段选择:', option)
|
console.log('[useGroupCourseList] 时间段选择:', option)
|
||||||
|
pageNum.value = 0
|
||||||
|
fetchCourseList()
|
||||||
}
|
}
|
||||||
|
|
||||||
const onTimeRangeConfirm = (params) => {
|
const onTimeRangeConfirm = (params) => {
|
||||||
@@ -108,6 +120,19 @@ export function useGroupCourseList() {
|
|||||||
timeRangeText.value = params.timeRangeText
|
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) => {
|
const handleBooking = (course) => {
|
||||||
console.log('[useGroupCourseList] 预约课程:', course)
|
console.log('[useGroupCourseList] 预约课程:', course)
|
||||||
uni.showToast({
|
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) => {
|
const fetchCourseList = async (isLoadMore = false) => {
|
||||||
if (loading.value) return
|
if (loading.value) return
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sortOption = sortOptions.value[sortIndex.value]
|
const currentPage = isLoadMore ? pageNum.value + 1 : pageNum.value
|
||||||
console.log('[useGroupCourseList] 请求参数:', {
|
pageNum.value = currentPage
|
||||||
page: isLoadMore ? pageNum.value + 1 : pageNum.value,
|
|
||||||
size: pageSize.value,
|
const searchParams = buildSearchParams()
|
||||||
sort: sortOption.value,
|
searchParams.page = currentPage
|
||||||
order: sortOption.order,
|
|
||||||
keyword: searchKeyword.value
|
console.log('[useGroupCourseList] 请求参数:', JSON.stringify(searchParams, null, 2))
|
||||||
})
|
|
||||||
|
const result = await searchGroupCourse(searchParams)
|
||||||
const result = await getGroupCoursePage({
|
|
||||||
page: isLoadMore ? pageNum.value + 1 : pageNum.value,
|
|
||||||
size: pageSize.value,
|
|
||||||
sort: sortOption.value,
|
|
||||||
order: sortOption.order,
|
|
||||||
keyword: searchKeyword.value
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('[useGroupCourseList] 响应结果:', JSON.stringify(result, null, 2))
|
console.log('[useGroupCourseList] 响应结果:', JSON.stringify(result, null, 2))
|
||||||
|
|
||||||
if (result && result.content) {
|
if (result && result.data && result.data.content) {
|
||||||
const { content: list, totalElements: totalCount, currentPage, totalPages: pages } = result
|
const { content: list, totalElements: totalCount, currentPage: respPage, totalPages: pages } = result.data
|
||||||
|
|
||||||
if (isLoadMore) {
|
if (isLoadMore) {
|
||||||
courseList.value = [...courseList.value, ...list]
|
courseList.value = [...courseList.value, ...list]
|
||||||
@@ -158,7 +225,6 @@ export function useGroupCourseList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
total.value = totalCount
|
total.value = totalCount
|
||||||
pageNum.value = currentPage
|
|
||||||
totalPages.value = pages
|
totalPages.value = pages
|
||||||
hasMore.value = currentPage < pages - 1
|
hasMore.value = currentPage < pages - 1
|
||||||
|
|
||||||
@@ -213,6 +279,8 @@ export function useGroupCourseList() {
|
|||||||
courseList,
|
courseList,
|
||||||
searchKeyword,
|
searchKeyword,
|
||||||
hotKeywords,
|
hotKeywords,
|
||||||
|
courseType,
|
||||||
|
courseTypes,
|
||||||
sortOptions,
|
sortOptions,
|
||||||
sortIndex,
|
sortIndex,
|
||||||
timePeriodOptions,
|
timePeriodOptions,
|
||||||
@@ -227,9 +295,13 @@ export function useGroupCourseList() {
|
|||||||
handleSearch,
|
handleSearch,
|
||||||
onTimePeriodChange,
|
onTimePeriodChange,
|
||||||
onTimeRangeConfirm,
|
onTimeRangeConfirm,
|
||||||
|
onCourseTypeChange,
|
||||||
|
clearCourseType,
|
||||||
handleBooking,
|
handleBooking,
|
||||||
goDetail,
|
goDetail,
|
||||||
fetchCourseList,
|
fetchCourseList,
|
||||||
|
buildSearchParams,
|
||||||
|
hasActiveFilters,
|
||||||
loadMore,
|
loadMore,
|
||||||
onScrollToLower
|
onScrollToLower
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
3. [团课管理接口](#团课管理接口)
|
3. [团课管理接口](#团课管理接口)
|
||||||
- [获取所有团课](#获取所有团课)
|
- [获取所有团课](#获取所有团课)
|
||||||
- [分页获取团课](#分页获取团课)
|
- [分页获取团课](#分页获取团课)
|
||||||
|
- [多条件查询团课](#多条件查询团课)
|
||||||
- [根据ID获取团课详情](#根据ID获取团课详情)
|
- [根据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获取团课详情
|
### 根据ID获取团课详情
|
||||||
|
|
||||||
| 属性 | 值 |
|
| 属性 | 值 |
|
||||||
|
|||||||
@@ -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": "查询成功"
|
||||||
|
}
|
||||||
@@ -107,7 +107,6 @@
|
|||||||
<view class="course-tags">
|
<view class="course-tags">
|
||||||
<view class="tags-label">
|
<view class="tags-label">
|
||||||
<uni-icons type="tag" size="20" color="#8A99B4" />
|
<uni-icons type="tag" size="20" color="#8A99B4" />
|
||||||
<text>课程标签</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="tags-list">
|
<view class="tags-list">
|
||||||
<view
|
<view
|
||||||
@@ -212,8 +211,8 @@
|
|||||||
|
|
||||||
<!-- 免费课程或单一支付方式 -->
|
<!-- 免费课程或单一支付方式 -->
|
||||||
<view v-else class="price-display">
|
<view v-else class="price-display">
|
||||||
<text v-if="course.storedValueAmount === 0 && course.pointCardAmount === 0" class="price free">免费</text>
|
<text v-if="Number(course.storedValueAmount) === 0 && Number(course.pointCardAmount) === 0" class="price free">免费</text>
|
||||||
<text v-else-if="course.storedValueAmount > 0" class="price">
|
<text v-else-if="Number(course.storedValueAmount) > 0" class="price">
|
||||||
<text class="currency">¥</text>{{ course.storedValueAmount }}
|
<text class="currency">¥</text>{{ course.storedValueAmount }}
|
||||||
</text>
|
</text>
|
||||||
<text v-else class="price">
|
<text v-else class="price">
|
||||||
@@ -233,7 +232,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue'
|
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'
|
import PageHeader from '@/components/index/PageHeader.vue'
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
@@ -305,7 +304,9 @@ const canBook = computed(() => {
|
|||||||
|
|
||||||
// 是否有多种支付方式
|
// 是否有多种支付方式
|
||||||
const hasMultiplePayment = 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 selectedPrice = computed(() => {
|
||||||
|
const storedValue = Number(course.value.storedValueAmount)
|
||||||
|
const pointCard = Number(course.value.pointCardAmount)
|
||||||
|
|
||||||
if (selectedPayment.value === 'storedValue') {
|
if (selectedPayment.value === 'storedValue') {
|
||||||
return { type: 'storedValue', amount: course.value.storedValueAmount }
|
return { type: 'storedValue', amount: storedValue }
|
||||||
} else if (selectedPayment.value === 'pointCard') {
|
} else if (selectedPayment.value === 'pointCard') {
|
||||||
return { type: 'pointCard', amount: course.value.pointCardAmount }
|
return { type: 'pointCard', amount: pointCard }
|
||||||
} else if (course.value.storedValueAmount > 0 && course.value.pointCardAmount > 0) {
|
} else if (storedValue > 0 && pointCard > 0) {
|
||||||
// 默认优先显示储值卡
|
// 默认优先显示储值卡
|
||||||
return { type: 'storedValue', amount: course.value.storedValueAmount }
|
return { type: 'storedValue', amount: storedValue }
|
||||||
} else if (course.value.storedValueAmount > 0) {
|
} else if (storedValue > 0) {
|
||||||
return { type: 'storedValue', amount: course.value.storedValueAmount }
|
return { type: 'storedValue', amount: storedValue }
|
||||||
} else if (course.value.pointCardAmount > 0) {
|
} else if (pointCard > 0) {
|
||||||
return { type: 'pointCard', amount: course.value.pointCardAmount }
|
return { type: 'pointCard', amount: pointCard }
|
||||||
}
|
}
|
||||||
return { type: 'free', amount: 0 }
|
return { type: 'free', amount: 0 }
|
||||||
})
|
})
|
||||||
@@ -378,20 +382,29 @@ const handleBooking = () => {
|
|||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
uni.showLoading({ title: '预约中...' })
|
uni.showLoading({ title: '预约中...' })
|
||||||
groupCourseService.book({
|
bookGroupCourse({
|
||||||
courseId: course.value.id,
|
courseId: course.value.id,
|
||||||
memberId: '1' // 模拟会员ID
|
memberId: '1', // 模拟会员ID
|
||||||
}).then(() => {
|
memberCardRecordId: '1' // 模拟会员卡记录ID
|
||||||
|
}).then((result) => {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
uni.showToast({
|
if (result.success) {
|
||||||
title: '预约成功',
|
uni.showToast({
|
||||||
icon: 'success'
|
title: '预约成功',
|
||||||
})
|
icon: 'success'
|
||||||
setTimeout(() => {
|
})
|
||||||
uni.navigateBack()
|
setTimeout(() => {
|
||||||
}, 1500)
|
uni.navigateBack()
|
||||||
}).catch(() => {
|
}, 1500)
|
||||||
|
} else {
|
||||||
|
uni.showToast({
|
||||||
|
title: result.message || '预约失败',
|
||||||
|
icon: 'none'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).catch((error) => {
|
||||||
uni.hideLoading()
|
uni.hideLoading()
|
||||||
|
console.error('[detail.vue] 预约失败:', error)
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: '预约失败',
|
title: '预约失败',
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
@@ -405,12 +418,16 @@ const handleBooking = () => {
|
|||||||
// 获取课程详情
|
// 获取课程详情
|
||||||
const fetchCourseDetail = async (id) => {
|
const fetchCourseDetail = async (id) => {
|
||||||
try {
|
try {
|
||||||
const result = await groupCourseService.getDetail(id)
|
console.log('[detail.vue] 开始获取课程详情, id:', id)
|
||||||
|
const result = await getGroupCourseDetail(id)
|
||||||
course.value = result
|
course.value = result
|
||||||
console.log('[detail.vue] 课程详情获取成功:', course.value)
|
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) {
|
} catch (error) {
|
||||||
console.error('[detail.vue] 获取课程详情失败:', error)
|
console.error('[detail.vue] 获取课程详情失败:', error)
|
||||||
uni.showToast({
|
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(() => {
|
onMounted(() => {
|
||||||
const pages = getCurrentPages()
|
const pages = getCurrentPages()
|
||||||
|
|||||||
@@ -18,7 +18,10 @@
|
|||||||
:time-range-text="timeRangeText"
|
:time-range-text="timeRangeText"
|
||||||
:sort-options="sortOptions"
|
:sort-options="sortOptions"
|
||||||
v-model:sort-index="sortIndex"
|
v-model:sort-index="sortIndex"
|
||||||
|
:course-types="courseTypes"
|
||||||
|
:current-course-type-id="courseType"
|
||||||
@time-pick="showTimePicker = true"
|
@time-pick="showTimePicker = true"
|
||||||
|
@course-type-change="onCourseTypeChange"
|
||||||
ref="filterSectionRef"
|
ref="filterSectionRef"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -79,7 +82,7 @@ import TimePeriodSelector from '@/components/groupCourse/TimePeriodSelector.vue'
|
|||||||
import TimeRangePicker from '@/components/groupCourse/TimeRangePicker.vue'
|
import TimeRangePicker from '@/components/groupCourse/TimeRangePicker.vue'
|
||||||
import PageHeader from '@/components/index/PageHeader.vue'
|
import PageHeader from '@/components/index/PageHeader.vue'
|
||||||
import { useGroupCourseList } from '@/composables/useGroupCourseList.js'
|
import { useGroupCourseList } from '@/composables/useGroupCourseList.js'
|
||||||
import { getTypeLabels } from '@/api/groupCourse.js'
|
import { getTypeLabels, getGroupCourseTypes } from '@/api/groupCourse.js'
|
||||||
|
|
||||||
// 组件引用
|
// 组件引用
|
||||||
const searchBarRef = ref(null)
|
const searchBarRef = ref(null)
|
||||||
@@ -97,6 +100,8 @@ const {
|
|||||||
hasMore,
|
hasMore,
|
||||||
searchKeyword,
|
searchKeyword,
|
||||||
hotKeywords,
|
hotKeywords,
|
||||||
|
courseType,
|
||||||
|
courseTypes,
|
||||||
sortOptions,
|
sortOptions,
|
||||||
sortIndex,
|
sortIndex,
|
||||||
timePeriodOptions,
|
timePeriodOptions,
|
||||||
@@ -112,6 +117,8 @@ const {
|
|||||||
handleSearch,
|
handleSearch,
|
||||||
onTimePeriodChange,
|
onTimePeriodChange,
|
||||||
onTimeRangeConfirm,
|
onTimeRangeConfirm,
|
||||||
|
onCourseTypeChange,
|
||||||
|
clearCourseType,
|
||||||
handleBooking,
|
handleBooking,
|
||||||
goDetail,
|
goDetail,
|
||||||
fetchCourseList,
|
fetchCourseList,
|
||||||
@@ -122,7 +129,11 @@ const {
|
|||||||
// 组件挂载时调用接口获取团课列表
|
// 组件挂载时调用接口获取团课列表
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
console.log('[list.vue] 页面组件已挂载,开始获取团课列表')
|
console.log('[list.vue] 页面组件已挂载,开始获取团课列表')
|
||||||
await fetchCourseList()
|
// 并行获取课程类型列表和课程列表
|
||||||
|
await Promise.all([
|
||||||
|
fetchCourseTypes(),
|
||||||
|
fetchCourseList()
|
||||||
|
])
|
||||||
// 获取所有团课的类型标签
|
// 获取所有团课的类型标签
|
||||||
await fetchAllCourseTypeLabels()
|
await fetchAllCourseTypeLabels()
|
||||||
console.log('[list.vue] 可用的搜索参数获取方法:')
|
console.log('[list.vue] 可用的搜索参数获取方法:')
|
||||||
@@ -133,9 +144,24 @@ onMounted(async () => {
|
|||||||
console.log(' - getAllSearchParams() 获取所有参数')
|
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 fetchAllCourseTypeLabels = async () => {
|
||||||
const courseTypes = [...new Set(filteredCourseList.value.map(c => c.courseType))]
|
const types = [...new Set(filteredCourseList.value.map(c => c.courseType))]
|
||||||
for (const type of courseTypes) {
|
for (const type of types) {
|
||||||
if (!courseTypeLabelsCache.value[type]) {
|
if (!courseTypeLabelsCache.value[type]) {
|
||||||
try {
|
try {
|
||||||
const result = await getTypeLabels(type)
|
const result = await getTypeLabels(type)
|
||||||
|
|||||||
Reference in New Issue
Block a user