332 lines
8.3 KiB
Vue
332 lines
8.3 KiB
Vue
<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> |