feat: migrate comprehensive courses to sessions

This commit is contained in:
gqt
2026-07-12 20:31:07 +08:00
parent d17b646ae0
commit f3f8aee9a0
10 changed files with 139 additions and 95 deletions
@@ -6,11 +6,11 @@
</view>
<view class="card course-card">
<image :src="booking.course.cover_image" mode="aspectFill" class="cover" />
<image :src="booking.session.cover_image" mode="aspectFill" class="cover" />
<view class="course-info">
<text class="course-title">{{ booking.course.title }}</text>
<text class="meta primary">{{ formatTimeRange(booking.course.start_at, booking.course.end_at) }}</text>
<text class="meta">{{ booking.course.coach_name }} · {{ booking.course.courts.map(item => item.name).join('、') }}</text>
<text class="course-title">{{ booking.session.title }}</text>
<text class="meta primary">{{ formatTimeRange(booking.session.start_at, booking.session.end_at) }}</text>
<text class="meta">{{ booking.session.coach_name }} · {{ booking.session.courts.map(item => item.name).join('、') }}</text>
</view>
</view>
@@ -99,19 +99,19 @@ const loadItems = async () => {
}
const getItemTitle = (item) => {
if (businessType.value === 'course') return item.course?.title || '课程预约'
if (businessType.value === 'course') return item.session?.title || '课程预约'
if (businessType.value === 'private') return item.courts?.map((court) => court.name).join('、') || '包场预约'
return `篮球门票 ${item.order_no}`
}
const getItemTime = (item) => {
if (businessType.value === 'course') return formatTimeRange(item.course?.start_at, item.course?.end_at)
if (businessType.value === 'course') return formatTimeRange(item.session?.start_at, item.session?.end_at)
if (businessType.value === 'private') return formatTimeRange(item.start_at, item.end_at)
return item.checked_in_at ? `核销时间 ${formatDateTime(item.checked_in_at)}` : `生成时间 ${formatDateTime(item.created_at)}`
}
const getItemDesc = (item) => {
if (businessType.value === 'course') return `${item.course?.coach_name || ''} · ${item.course?.courts?.map((court) => court.name).join('、') || ''}`
if (businessType.value === 'course') return `${item.session?.coach_name || ''} · ${item.session?.courts?.map((court) => court.name).join('、') || ''}`
if (businessType.value === 'private') return `${item.courts?.length || 0} 个场地`
return item.status === 'available' ? '可到馆核销使用' : item.status_label
}
@@ -1,29 +1,29 @@
<template>
<view class="confirm-page container page-bottom-safe" v-if="course">
<view class="confirm-page container page-bottom-safe" v-if="session">
<view class="summary-card card">
<text class="card-title">{{ course.title }}</text>
<text class="card-title">{{ session.title }}</text>
<view class="summary-row">
<text>上课时间</text>
<text>{{ formatTimeRange(course.start_at, course.end_at) }}</text>
<text>{{ formatTimeRange(session.start_at, session.end_at) }}</text>
</view>
<view class="summary-row">
<text>授课教练</text>
<text>{{ course.coach_name }}</text>
<text>{{ session.coach_name }}</text>
</view>
<view class="summary-row">
<text>上课场地</text>
<text>{{ course.courts.map(item => item.name).join('、') }}</text>
<text>{{ session.courts.map(item => item.name).join('、') }}</text>
</view>
<view class="summary-row">
<text>课程价格</text>
<text class="strong">¥{{ course.price }}</text>
<text class="strong">¥{{ session.price }}</text>
</view>
</view>
<view class="fixed-action-bar">
<view class="bar-price">
<text class="bar-label">应付金额</text>
<text class="price-text">¥{{ course.price }}</text>
<text class="price-text">¥{{ session.price }}</text>
</view>
<button class="primary-button submit-btn" :disabled="isSubmitting || !canSubmit" @tap="submit">
提交预约
@@ -38,19 +38,19 @@ import { onLoad } from '@dcloudio/uni-app'
import { courseApi } from '@/modules/comprehensive/api/course'
import { formatTimeRange } from '@/modules/comprehensive/utils/date'
const course = ref(null)
const session = ref(null)
const isSubmitting = ref(false)
const canSubmit = computed(() => course.value?.status === 'published' && course.value?.remaining_count > 0)
const canSubmit = computed(() => session.value?.status === 'published' && session.value?.remaining_count > 0)
onLoad(async (query) => {
course.value = await courseApi.getCourseDetail(query.id)
session.value = await courseApi.getSessionDetail(query.id)
})
const submit = async () => {
if (isSubmitting.value || !course.value || !canSubmit.value) return
if (isSubmitting.value || !session.value || !canSubmit.value) return
isSubmitting.value = true
try {
const data = await courseApi.createCourseBooking(course.value.id)
const data = await courseApi.createCourseBooking(session.value.id)
uni.navigateTo({ url: `/pages/comprehensive/pay/index?order_id=${data.order.id}` })
} finally {
isSubmitting.value = false
@@ -1,13 +1,13 @@
<template>
<view class="detail-page page-bottom-safe" v-if="course">
<image :src="course.cover_image" mode="aspectFill" class="hero-image" />
<view class="detail-page page-bottom-safe" v-if="session">
<image :src="session.cover_image" mode="aspectFill" class="hero-image" />
<view class="content">
<view class="title-card card">
<view class="title-row">
<text class="course-title">{{ course.title }}</text>
<text class="course-title">{{ session.title }}</text>
</view>
<view class="price-row">
<text class="price-text">¥{{ course.price }}</text>
<text class="price-text">¥{{ session.price }}</text>
</view>
</view>
@@ -22,7 +22,7 @@
<text class="section-title">课程详情</text>
</view>
<view class="rich-card card">
<rich-text :nodes="course.detail_html"></rich-text>
<rich-text :nodes="session.detail_html"></rich-text>
</view>
</view>
@@ -40,28 +40,28 @@ import { onLoad } from '@dcloudio/uni-app'
import { courseApi } from '@/modules/comprehensive/api/course'
import { formatTimeRange } from '@/modules/comprehensive/utils/date'
const course = ref(null)
const session = ref(null)
const canBook = computed(() => {
return course.value && course.value.remaining_count > 0 && course.value.status === 'published'
return session.value && session.value.remaining_count > 0 && session.value.status === 'published'
})
const infoItems = computed(() => {
if (!course.value) return []
if (!session.value) return []
return [
{ label: '上课时间', value: formatTimeRange(course.value.start_at, course.value.end_at) },
{ label: '授课教练', value: course.value.coach_name },
{ label: '上课场地', value: course.value.courts.map((item) => item.name).join('、') }
{ label: '上课时间', value: formatTimeRange(session.value.start_at, session.value.end_at) },
{ label: '授课教练', value: session.value.coach_name },
{ label: '上课场地', value: session.value.courts.map((item) => item.name).join('、') }
]
})
onLoad(async (query) => {
course.value = await courseApi.getCourseDetail(query.id)
session.value = await courseApi.getSessionDetail(query.id)
})
const goConfirm = () => {
if (!canBook.value) return
uni.navigateTo({ url: `/pages/comprehensive/courses/confirm?id=${course.value.id}` })
uni.navigateTo({ url: `/pages/comprehensive/courses/confirm?id=${session.value.id}` })
}
</script>
@@ -22,23 +22,23 @@
</view>
<view class="course-list">
<view v-for="course in courses" :key="course.id" class="course-card" @tap="goDetail(course.id)">
<image :src="course.cover_image" mode="aspectFill" class="course-cover" />
<view v-for="session in sessions" :key="session.id" class="course-card" @tap="goDetail(session.id)">
<image :src="session.cover_image" mode="aspectFill" class="course-cover" />
<view class="course-info">
<view class="course-top">
<text class="course-title">{{ course.title }}</text>
<text class="course-title">{{ session.title }}</text>
</view>
<text class="time-line">{{ formatTimeRange(course.start_at, course.end_at) }}</text>
<text class="meta">{{ course.coach_name }}</text>
<text class="time-line">{{ formatTimeRange(session.start_at, session.end_at) }}</text>
<text class="meta">{{ session.coach_name }}</text>
<view class="course-bottom">
<text class="meta">已约 {{ course.booked_count }}/{{ course.capacity }}</text>
<text class="course-price">¥{{ course.price }}</text>
<text class="meta">已约 {{ session.booked_count }}/{{ session.capacity }}</text>
<text class="course-price">¥{{ session.price }}</text>
</view>
</view>
</view>
<view v-if="isLoading" class="load-more-tip">加载中...</view>
<view v-else-if="courses.length === 0" class="empty-state">当前筛选条件下暂无课程</view>
<view v-else-if="sessions.length === 0" class="empty-state">当前筛选条件下暂无课程</view>
<view v-else-if="!hasMore" class="load-more-tip">已加载全部</view>
</view>
<VenueTabBar venue="comprehensive" active="courses" />
@@ -56,7 +56,7 @@ import VenueTabBar from '@/components/VenueTabBar.vue'
const selectedDate = ref(todayString())
const selectedCoachIndex = ref(0)
const selectedCourtTypeIndex = ref(0)
const courses = ref([])
const sessions = ref([])
const coaches = ref([])
const page = ref(1)
const pageSize = 20
@@ -75,25 +75,25 @@ const selectedCoachId = computed(() => coachOptions.value[selectedCoachIndex.val
const selectedCoachName = computed(() => coachOptions.value[selectedCoachIndex.value]?.name || '全部教练')
const selectedCourtType = computed(() => courtTypeOptions[selectedCourtTypeIndex.value]?.value || '')
const selectedCourtTypeLabel = computed(() => courtTypeOptions[selectedCourtTypeIndex.value]?.label || '全部')
const hasMore = computed(() => courses.value.length < total.value)
const hasMore = computed(() => sessions.value.length < total.value)
onMounted(async () => {
const coachData = await venueApi.getCoaches()
coaches.value = coachData.items
loadCourses(true)
loadSessions(true)
})
onPullDownRefresh(() => {
loadCourses(true).finally(() => uni.stopPullDownRefresh())
loadSessions(true).finally(() => uni.stopPullDownRefresh())
})
onReachBottom(() => {
if (!hasMore.value || isLoadingMore.value) return
page.value += 1
loadCourses(false)
loadSessions(false)
})
const loadCourses = async (reset) => {
const loadSessions = async (reset) => {
if (reset) {
page.value = 1
isLoading.value = true
@@ -101,14 +101,14 @@ const loadCourses = async (reset) => {
isLoadingMore.value = true
}
try {
const data = await courseApi.getCourses({
const data = await courseApi.getSessions({
page: page.value,
pageSize,
date: selectedDate.value,
coachId: selectedCoachId.value,
courtType: selectedCourtType.value
})
courses.value = reset ? data.items : [...courses.value, ...data.items]
sessions.value = reset ? data.items : [...sessions.value, ...data.items]
total.value = data.total
} finally {
isLoading.value = false
@@ -118,17 +118,17 @@ const loadCourses = async (reset) => {
const onDateChange = (e) => {
selectedDate.value = e.detail.value
loadCourses(true)
loadSessions(true)
}
const onCoachChange = (e) => {
selectedCoachIndex.value = Number(e.detail.value)
loadCourses(true)
loadSessions(true)
}
const onCourtTypeChange = (e) => {
selectedCourtTypeIndex.value = Number(e.detail.value)
loadCourses(true)
loadSessions(true)
}
const goDetail = (id) => {