Files
gym-manage/gym-manage-uniapp/pages/groupCourse/list.vue
T

332 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="group-course-page">
<!-- 页面头部 -->
<PageHeader title="团课" subtitle="精品团课 · 专业教练 · 小班教学" :show-back="true" />
<!-- 搜索区域 -->
<view class="search-section">
<!-- 搜索框组件 -->
<SearchBar
v-model="searchKeyword"
:hot-keywords="hotKeywords"
@search="handleSearch"
ref="searchBarRef"
/>
<!-- 筛选条件组件 -->
<FilterSection
: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"
/>
<!-- 时间段选择组件 -->
<TimePeriodSelector
:time-period-options="timePeriodOptions"
v-model="timePeriodIndex"
@change="onTimePeriodChange"
ref="timePeriodRef"
/>
<!-- 常态化团课筛选按钮 -->
<view class="recurring-filter">
<view
class="recurring-btn"
:class="{ 'recurring-btn--active': isRecurring }"
@tap="toggleRecurring"
>
<text class="recurring-icon">{{ isRecurring ? '✓' : '' }}</text>
<text class="recurring-text">常态化团课</text>
</view>
</view>
</view>
<scroll-view
v-if="!pageReady"
class="course-list-skel"
>
<ListSkeleton :count="4" layout="card" :show-tabs="false" />
</scroll-view>
<!-- 团课列表 -->
<scroll-view
v-else
scroll-y
class="course-list"
@scrolltolower="onScrollToLower"
scroll-with-animation
>
<GroupCourseCard
v-for="course in filteredCourseList"
:key="course.id"
:course="course"
:courseTypeLabels="getCourseLabels(course)"
:booked="isCourseBooked(course.id)"
@booking="handleBooking"
@detail="goDetail"
></GroupCourseCard>
<!-- 加载更多提示 -->
<view class="load-more">
<text v-if="loading">加载中...</text>
<text v-else-if="!hasMore">没有更多数据了</text>
<text v-else class="load-more-text" @tap="loadMore">点击加载更多</text>
</view>
</scroll-view>
<!-- 底部导航 -->
<TabBar :active-tab="1" />
<!-- 时间选择器组件 -->
<TimeRangePicker
v-model:visible="showTimePicker"
v-model:start-date="startDate"
v-model:end-date="endDate"
@confirm="onTimeRangeConfirm"
ref="timeRangePickerRef"
/>
</view>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import TabBar from '@/components/TabBar.vue'
import GroupCourseCard from '@/components/groupCourse/CourseCard.vue'
import SearchBar from '@/components/groupCourse/SearchBar.vue'
import FilterSection from '@/components/groupCourse/FilterSection.vue'
import TimePeriodSelector from '@/components/groupCourse/TimePeriodSelector.vue'
import TimeRangePicker from '@/components/groupCourse/TimeRangePicker.vue'
import PageHeader from '@/components/index/PageHeader.vue'
import ListSkeleton from '@/components/Skeleton/ListSkeleton.vue'
import { useGroupCourseList } from '@/composables/useGroupCourseList.js'
import { getTypeLabels, getGroupCourseTypes, getMemberBookings } from '@/api/groupCourse.js'
import { getMemberId } from '@/utils/request.js'
// 组件引用
const searchBarRef = ref(null)
const filterSectionRef = ref(null)
const timePeriodRef = ref(null)
const timeRangePickerRef = ref(null)
// 课程类型标签缓存
const courseTypeLabelsCache = ref({})
// 已预约课程ID集合
const bookedCourseIds = ref(new Set())
// 页面加载状态
const pageReady = ref(false)
// 使用组合式函数
const {
// 状态
loading,
hasMore,
searchKeyword,
hotKeywords,
courseType,
courseTypes,
sortOptions,
sortIndex,
timePeriodOptions,
timePeriodIndex,
isRecurring,
showTimePicker,
startDate,
endDate,
timeRangeText,
filteredCourseList,
// 方法
getAllSearchParams,
handleSearch,
onTimePeriodChange,
onTimeRangeConfirm,
onCourseTypeChange,
clearCourseType,
handleBooking,
goDetail,
fetchCourseList,
loadMore,
onScrollToLower
} = useGroupCourseList()
// 切换常态化团课筛选
const toggleRecurring = () => {
isRecurring.value = !isRecurring.value
fetchCourseList()
}
// 组件挂载时调用接口获取团课列表
onMounted(async () => {
console.log('[list.vue] 页面组件已挂载,开始获取团课列表')
// 并行获取课程类型列表、课程列表和已预约课程
await Promise.all([
fetchCourseTypes(),
fetchCourseList(),
fetchBookedCourses()
])
// 获取所有团课的类型标签
await fetchAllCourseTypeLabels()
pageReady.value = true
console.log('[list.vue] 可用的搜索参数获取方法:')
console.log(' - searchBarRef.getSearchParams()')
console.log(' - filterSectionRef.getFilterParams()')
console.log(' - timePeriodRef.getTimePeriodParams()')
console.log(' - timeRangePickerRef.getTimeRangeParams()')
console.log(' - getAllSearchParams() 获取所有参数')
})
// 页面显示时刷新已预约状态(从其他页面返回时)
onShow(async () => {
console.log('[list.vue] onShow 刷新已预约状态')
await fetchBookedCourses()
})
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 fetchBookedCourses = async () => {
const memberId = getMemberId()
if (!memberId) {
console.log('[list.vue] 未登录,跳过获取已预约课程')
return
}
try {
const res = await getMemberBookings(memberId)
const bookings = Array.isArray(res) ? res : (res?.data || res?.content || [])
if (Array.isArray(bookings)) {
const ids = new Set()
bookings.forEach(b => {
// 只统计状态为正常的预约(status='0'),排除已取消(status='1'
if (b.courseId && String(b.status) === '0') ids.add(Number(b.courseId))
})
bookedCourseIds.value = ids
console.log('[list.vue] 已预约课程ID:', [...ids])
}
} catch (error) {
console.error('[list.vue] 获取已预约课程失败:', error)
}
}
const isCourseBooked = (courseId) => {
return bookedCourseIds.value.has(Number(courseId))
}
const fetchAllCourseTypeLabels = async () => {
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)
if (result) {
courseTypeLabelsCache.value[type] = result
}
} catch (error) {
console.error(`[list.vue] 获取类型标签失败,type=${type}`, error)
}
}
}
}
// 根据课程获取对应的标签
const getCourseLabels = (course) => {
return courseTypeLabelsCache.value[course.courseType] || []
}
// 暴露方法供外部调用
defineExpose({
getAllSearchParams: () => getAllSearchParams(searchBarRef.value, filterSectionRef.value, timePeriodRef.value, timeRangePickerRef.value),
searchBarRef,
filterSectionRef,
timePeriodRef,
timeRangePickerRef
})
</script>
<style lang="scss" scoped>
.group-course-page {
min-height: 100vh;
background: #F5F7FA;
}
/* 搜索区域 */
.search-section {
background: #ffffff;
padding: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
gap: 24rpx;
}
/* 课程列表 */
.course-list {
height: calc(100vh - 480rpx);
padding: 24rpx 24rpx;
padding-bottom: calc(160rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(160rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
/* 加载更多提示 */
.load-more {
text-align: center;
padding: 30rpx 0;
color: #999999;
font-size: 26rpx;
}
.load-more-text {
color: #1890ff;
}
/* 常态化团课筛选按钮 */
.recurring-filter {
padding-top: 8rpx;
}
.recurring-btn {
display: inline-flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 28rpx;
border-radius: 32rpx;
border: 2rpx solid #e0e0e0;
background: #fff;
font-size: 26rpx;
color: #666;
transition: all 0.2s;
}
.recurring-btn--active {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border-color: #66bb6a;
color: #2e7d32;
font-weight: 600;
}
.recurring-icon {
font-size: 22rpx;
font-weight: 700;
}
</style>