feat: migrate comprehensive courses to sessions
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"verify:startup": "node scripts/verify-venue-startup.mjs",
|
"verify:startup": "node scripts/verify-venue-startup.mjs",
|
||||||
"verify:home-switch": "node scripts/verify-home-venue-switch.mjs",
|
"verify:home-switch": "node scripts/verify-home-venue-switch.mjs",
|
||||||
"verify:comprehensive-auth": "node scripts/verify-comprehensive-auth-state.mjs",
|
"verify:comprehensive-auth": "node scripts/verify-comprehensive-auth-state.mjs",
|
||||||
|
"verify:comprehensive-session": "node scripts/verify-comprehensive-session-contract.mjs",
|
||||||
"verify:auth-agreements": "node scripts/verify-auth-agreements.mjs"
|
"verify:auth-agreements": "node scripts/verify-auth-agreements.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { dirname, resolve } from 'node:path'
|
||||||
|
|
||||||
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||||
|
const srcRoot = resolve(root, 'src')
|
||||||
|
const failures = []
|
||||||
|
|
||||||
|
const expectIncludes = (content, expected, label) => {
|
||||||
|
if (!content.includes(expected)) failures.push(`${label} is missing: ${expected}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const expectExcludes = (content, unexpected, label) => {
|
||||||
|
if (content.includes(unexpected)) failures.push(`${label} should not contain: ${unexpected}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const courseApi = readFileSync(resolve(srcRoot, 'modules/comprehensive/api/course.js'), 'utf8')
|
||||||
|
const bookingApi = readFileSync(resolve(srcRoot, 'modules/comprehensive/api/booking.js'), 'utf8')
|
||||||
|
|
||||||
|
expectIncludes(courseApi, "url: '/api/sessions'", 'session list endpoint')
|
||||||
|
expectIncludes(courseApi, 'url: `/api/sessions/${id}`', 'session detail endpoint')
|
||||||
|
expectIncludes(courseApi, 'data: { session_id: sessionId }', 'booking payload')
|
||||||
|
expectExcludes(courseApi, "url: '/api/courses'", 'legacy course endpoint')
|
||||||
|
expectIncludes(bookingApi, 'session_id: backendBooking.session_id', 'booking session id')
|
||||||
|
expectIncludes(bookingApi, 'session: toBookingSession(backendBooking.session)', 'booking session mapper')
|
||||||
|
expectExcludes(bookingApi, 'course: toBookingCourse(backendBooking.course)', 'legacy booking course')
|
||||||
|
|
||||||
|
if (failures.length) {
|
||||||
|
console.error(`Comprehensive session contract verification failed with ${failures.length} issue(s):`)
|
||||||
|
for (const failure of failures) console.error(`- ${failure}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Comprehensive session contract verification passed.')
|
||||||
@@ -2,18 +2,26 @@ import { request } from './request'
|
|||||||
import { toArray, toNumber, toPageQuery, toPagination } from './mappers'
|
import { toArray, toNumber, toPageQuery, toPagination } from './mappers'
|
||||||
import { toOrderBrief } from './order'
|
import { toOrderBrief } from './order'
|
||||||
|
|
||||||
const toBookingCourse = (backendCourse = {}) => ({
|
const toBookingCourt = (backendCourt = {}) => ({
|
||||||
id: backendCourse.id ?? null,
|
id: backendCourt.id ?? null,
|
||||||
title: backendCourse.title || '',
|
name: backendCourt.name || '',
|
||||||
cover_image: backendCourse.cover_image || '',
|
court_type: backendCourt.court_type || '',
|
||||||
coach_name: backendCourse.coach_name || '',
|
court_type_label: backendCourt.court_type_label || ''
|
||||||
start_at: backendCourse.start_at || '',
|
})
|
||||||
end_at: backendCourse.end_at || '',
|
|
||||||
capacity: toNumber(backendCourse.capacity),
|
const toBookingSession = (backendSession = {}) => ({
|
||||||
booked_count: toNumber(backendCourse.booked_count),
|
id: backendSession.id ?? null,
|
||||||
remaining_count: toNumber(backendCourse.remaining_count),
|
course_id: backendSession.course_id ?? null,
|
||||||
price: backendCourse.price || '0.00',
|
title: backendSession.title || '',
|
||||||
courts: toArray(backendCourse.courts)
|
cover_image: backendSession.cover_image || '',
|
||||||
|
coach_name: backendSession.coach_name || '',
|
||||||
|
start_at: backendSession.start_at || '',
|
||||||
|
end_at: backendSession.end_at || '',
|
||||||
|
capacity: toNumber(backendSession.capacity),
|
||||||
|
booked_count: toNumber(backendSession.booked_count),
|
||||||
|
remaining_count: toNumber(backendSession.remaining_count),
|
||||||
|
price: backendSession.price || '0.00',
|
||||||
|
courts: toArray(backendSession.courts).map(toBookingCourt)
|
||||||
})
|
})
|
||||||
|
|
||||||
const toPrivateCourt = (backendCourt = {}) => ({
|
const toPrivateCourt = (backendCourt = {}) => ({
|
||||||
@@ -25,7 +33,7 @@ const toPrivateCourt = (backendCourt = {}) => ({
|
|||||||
|
|
||||||
const toCourseBooking = (backendBooking = {}) => ({
|
const toCourseBooking = (backendBooking = {}) => ({
|
||||||
id: backendBooking.id ?? null,
|
id: backendBooking.id ?? null,
|
||||||
course_id: backendBooking.course_id ?? null,
|
session_id: backendBooking.session_id ?? null,
|
||||||
order_id: backendBooking.order_id ?? null,
|
order_id: backendBooking.order_id ?? null,
|
||||||
status: backendBooking.status || '',
|
status: backendBooking.status || '',
|
||||||
status_label: backendBooking.status_label || backendBooking.status || '',
|
status_label: backendBooking.status_label || backendBooking.status || '',
|
||||||
@@ -37,7 +45,7 @@ const toCourseBooking = (backendBooking = {}) => ({
|
|||||||
can_cancel: backendBooking.can_cancel === true,
|
can_cancel: backendBooking.can_cancel === true,
|
||||||
cancel_deadline_at: backendBooking.cancel_deadline_at || null,
|
cancel_deadline_at: backendBooking.cancel_deadline_at || null,
|
||||||
created_at: backendBooking.created_at || '',
|
created_at: backendBooking.created_at || '',
|
||||||
course: toBookingCourse(backendBooking.course),
|
session: toBookingSession(backendBooking.session),
|
||||||
order: toOrderBrief(backendBooking.order)
|
order: toOrderBrief(backendBooking.order)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { request } from './request'
|
import { request } from './request'
|
||||||
import { compactParams, toArray, toNumber, toPageQuery, toPagination } from './mappers'
|
import { compactParams, toArray, toNumber, toPageQuery, toPagination } from './mappers'
|
||||||
import { toCourse } from './course'
|
import { toSession } from './course'
|
||||||
|
|
||||||
const toBanner = (backendBanner = {}) => ({
|
const toBanner = (backendBanner = {}) => ({
|
||||||
id: backendBanner.id ?? null,
|
id: backendBanner.id ?? null,
|
||||||
@@ -22,7 +22,7 @@ const toArticle = (backendArticle = {}) => ({
|
|||||||
const toHomeContent = (backendData = {}) => ({
|
const toHomeContent = (backendData = {}) => ({
|
||||||
banners: toArray(backendData.banners).map(toBanner),
|
banners: toArray(backendData.banners).map(toBanner),
|
||||||
articles: toArray(backendData.articles).map(toArticle),
|
articles: toArray(backendData.articles).map(toArticle),
|
||||||
upcoming_courses: toArray(backendData.upcoming_courses).map(toCourse),
|
upcoming_courses: toArray(backendData.upcoming_courses).map(toSession),
|
||||||
venue: {
|
venue: {
|
||||||
name: backendData.venue?.name || ''
|
name: backendData.venue?.name || ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,27 +8,28 @@ const toCourseCourt = (backendCourt = {}) => ({
|
|||||||
court_type_label: backendCourt.court_type_label || ''
|
court_type_label: backendCourt.court_type_label || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
export const toCourse = (backendCourse = {}) => ({
|
export const toSession = (backendSession = {}) => ({
|
||||||
id: backendCourse.id ?? null,
|
id: backendSession.id ?? null,
|
||||||
title: backendCourse.title || '',
|
course_id: backendSession.course_id ?? null,
|
||||||
cover_image: backendCourse.cover_image || '',
|
title: backendSession.title || '',
|
||||||
coach_id: backendCourse.coach_id ?? null,
|
cover_image: backendSession.cover_image || '',
|
||||||
coach_name: backendCourse.coach_name || '',
|
coach_id: backendSession.coach_id ?? null,
|
||||||
start_at: backendCourse.start_at || '',
|
coach_name: backendSession.coach_name || '',
|
||||||
end_at: backendCourse.end_at || '',
|
start_at: backendSession.start_at || '',
|
||||||
capacity: toNumber(backendCourse.capacity),
|
end_at: backendSession.end_at || '',
|
||||||
booked_count: toNumber(backendCourse.booked_count),
|
capacity: toNumber(backendSession.capacity),
|
||||||
remaining_count: toNumber(backendCourse.remaining_count),
|
booked_count: toNumber(backendSession.booked_count),
|
||||||
price: backendCourse.price || '0.00',
|
remaining_count: toNumber(backendSession.remaining_count),
|
||||||
status: backendCourse.status || '',
|
price: backendSession.price || '0.00',
|
||||||
status_label: backendCourse.status_label || backendCourse.status || '',
|
status: backendSession.status || '',
|
||||||
courts: toArray(backendCourse.courts).map(toCourseCourt),
|
status_label: backendSession.status_label || backendSession.status || '',
|
||||||
detail_html: backendCourse.detail_html || '',
|
courts: toArray(backendSession.courts).map(toCourseCourt),
|
||||||
created_at: backendCourse.created_at || '',
|
detail_html: backendSession.detail_html || '',
|
||||||
updated_at: backendCourse.updated_at || ''
|
created_at: backendSession.created_at || '',
|
||||||
|
updated_at: backendSession.updated_at || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const toCourseQuery = (params = {}) => compactParams({
|
const toSessionQuery = (params = {}) => compactParams({
|
||||||
...toPageQuery(params),
|
...toPageQuery(params),
|
||||||
date: params.date,
|
date: params.date,
|
||||||
coach_id: params.coachId || params.coach_id,
|
coach_id: params.coachId || params.coach_id,
|
||||||
@@ -36,22 +37,22 @@ const toCourseQuery = (params = {}) => compactParams({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export const courseApi = {
|
export const courseApi = {
|
||||||
getCourses(params = {}) {
|
getSessions(params = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/courses',
|
url: '/api/sessions',
|
||||||
data: toCourseQuery(params)
|
data: toSessionQuery(params)
|
||||||
}).then((data) => toPagination(data, toCourse))
|
}).then((data) => toPagination(data, toSession))
|
||||||
},
|
},
|
||||||
getCourseDetail(id) {
|
getSessionDetail(id) {
|
||||||
return request({
|
return request({
|
||||||
url: `/api/courses/${id}`
|
url: `/api/sessions/${id}`
|
||||||
}).then(toCourse)
|
}).then(toSession)
|
||||||
},
|
},
|
||||||
createCourseBooking(courseId) {
|
createCourseBooking(sessionId) {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/bookings/course',
|
url: '/api/bookings/course',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { course_id: courseId }
|
data: { session_id: sessionId }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="card course-card">
|
<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">
|
<view class="course-info">
|
||||||
<text class="course-title">{{ booking.course.title }}</text>
|
<text class="course-title">{{ booking.session.title }}</text>
|
||||||
<text class="meta primary">{{ formatTimeRange(booking.course.start_at, booking.course.end_at) }}</text>
|
<text class="meta primary">{{ formatTimeRange(booking.session.start_at, booking.session.end_at) }}</text>
|
||||||
<text class="meta">{{ booking.course.coach_name }} · {{ booking.course.courts.map(item => item.name).join('、') }}</text>
|
<text class="meta">{{ booking.session.coach_name }} · {{ booking.session.courts.map(item => item.name).join('、') }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|||||||
@@ -99,19 +99,19 @@ const loadItems = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getItemTitle = (item) => {
|
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('、') || '包场预约'
|
if (businessType.value === 'private') return item.courts?.map((court) => court.name).join('、') || '包场预约'
|
||||||
return `篮球门票 ${item.order_no}`
|
return `篮球门票 ${item.order_no}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getItemTime = (item) => {
|
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)
|
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)}`
|
return item.checked_in_at ? `核销时间 ${formatDateTime(item.checked_in_at)}` : `生成时间 ${formatDateTime(item.created_at)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const getItemDesc = (item) => {
|
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} 个场地`
|
if (businessType.value === 'private') return `共 ${item.courts?.length || 0} 个场地`
|
||||||
return item.status === 'available' ? '可到馆核销使用' : item.status_label
|
return item.status === 'available' ? '可到馆核销使用' : item.status_label
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
<template>
|
<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">
|
<view class="summary-card card">
|
||||||
<text class="card-title">{{ course.title }}</text>
|
<text class="card-title">{{ session.title }}</text>
|
||||||
<view class="summary-row">
|
<view class="summary-row">
|
||||||
<text>上课时间</text>
|
<text>上课时间</text>
|
||||||
<text>{{ formatTimeRange(course.start_at, course.end_at) }}</text>
|
<text>{{ formatTimeRange(session.start_at, session.end_at) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="summary-row">
|
<view class="summary-row">
|
||||||
<text>授课教练</text>
|
<text>授课教练</text>
|
||||||
<text>{{ course.coach_name }}</text>
|
<text>{{ session.coach_name }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="summary-row">
|
<view class="summary-row">
|
||||||
<text>上课场地</text>
|
<text>上课场地</text>
|
||||||
<text>{{ course.courts.map(item => item.name).join('、') }}</text>
|
<text>{{ session.courts.map(item => item.name).join('、') }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="summary-row">
|
<view class="summary-row">
|
||||||
<text>课程价格</text>
|
<text>课程价格</text>
|
||||||
<text class="strong">¥{{ course.price }}</text>
|
<text class="strong">¥{{ session.price }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="fixed-action-bar">
|
<view class="fixed-action-bar">
|
||||||
<view class="bar-price">
|
<view class="bar-price">
|
||||||
<text class="bar-label">应付金额</text>
|
<text class="bar-label">应付金额</text>
|
||||||
<text class="price-text">¥{{ course.price }}</text>
|
<text class="price-text">¥{{ session.price }}</text>
|
||||||
</view>
|
</view>
|
||||||
<button class="primary-button submit-btn" :disabled="isSubmitting || !canSubmit" @tap="submit">
|
<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 { courseApi } from '@/modules/comprehensive/api/course'
|
||||||
import { formatTimeRange } from '@/modules/comprehensive/utils/date'
|
import { formatTimeRange } from '@/modules/comprehensive/utils/date'
|
||||||
|
|
||||||
const course = ref(null)
|
const session = ref(null)
|
||||||
const isSubmitting = ref(false)
|
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) => {
|
onLoad(async (query) => {
|
||||||
course.value = await courseApi.getCourseDetail(query.id)
|
session.value = await courseApi.getSessionDetail(query.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
if (isSubmitting.value || !course.value || !canSubmit.value) return
|
if (isSubmitting.value || !session.value || !canSubmit.value) return
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
try {
|
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}` })
|
uni.navigateTo({ url: `/pages/comprehensive/pay/index?order_id=${data.order.id}` })
|
||||||
} finally {
|
} finally {
|
||||||
isSubmitting.value = false
|
isSubmitting.value = false
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="detail-page page-bottom-safe" v-if="course">
|
<view class="detail-page page-bottom-safe" v-if="session">
|
||||||
<image :src="course.cover_image" mode="aspectFill" class="hero-image" />
|
<image :src="session.cover_image" mode="aspectFill" class="hero-image" />
|
||||||
<view class="content">
|
<view class="content">
|
||||||
<view class="title-card card">
|
<view class="title-card card">
|
||||||
<view class="title-row">
|
<view class="title-row">
|
||||||
<text class="course-title">{{ course.title }}</text>
|
<text class="course-title">{{ session.title }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="price-row">
|
<view class="price-row">
|
||||||
<text class="price-text">¥{{ course.price }}</text>
|
<text class="price-text">¥{{ session.price }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<text class="section-title">课程详情</text>
|
<text class="section-title">课程详情</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="rich-card card">
|
<view class="rich-card card">
|
||||||
<rich-text :nodes="course.detail_html"></rich-text>
|
<rich-text :nodes="session.detail_html"></rich-text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -40,28 +40,28 @@ import { onLoad } from '@dcloudio/uni-app'
|
|||||||
import { courseApi } from '@/modules/comprehensive/api/course'
|
import { courseApi } from '@/modules/comprehensive/api/course'
|
||||||
import { formatTimeRange } from '@/modules/comprehensive/utils/date'
|
import { formatTimeRange } from '@/modules/comprehensive/utils/date'
|
||||||
|
|
||||||
const course = ref(null)
|
const session = ref(null)
|
||||||
|
|
||||||
const canBook = computed(() => {
|
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(() => {
|
const infoItems = computed(() => {
|
||||||
if (!course.value) return []
|
if (!session.value) return []
|
||||||
return [
|
return [
|
||||||
{ label: '上课时间', value: formatTimeRange(course.value.start_at, course.value.end_at) },
|
{ label: '上课时间', value: formatTimeRange(session.value.start_at, session.value.end_at) },
|
||||||
{ label: '授课教练', value: course.value.coach_name },
|
{ label: '授课教练', value: session.value.coach_name },
|
||||||
{ label: '上课场地', value: course.value.courts.map((item) => item.name).join('、') }
|
{ label: '上课场地', value: session.value.courts.map((item) => item.name).join('、') }
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
onLoad(async (query) => {
|
onLoad(async (query) => {
|
||||||
course.value = await courseApi.getCourseDetail(query.id)
|
session.value = await courseApi.getSessionDetail(query.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const goConfirm = () => {
|
const goConfirm = () => {
|
||||||
if (!canBook.value) return
|
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>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -22,23 +22,23 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="course-list">
|
<view class="course-list">
|
||||||
<view v-for="course in courses" :key="course.id" class="course-card" @tap="goDetail(course.id)">
|
<view v-for="session in sessions" :key="session.id" class="course-card" @tap="goDetail(session.id)">
|
||||||
<image :src="course.cover_image" mode="aspectFill" class="course-cover" />
|
<image :src="session.cover_image" mode="aspectFill" class="course-cover" />
|
||||||
<view class="course-info">
|
<view class="course-info">
|
||||||
<view class="course-top">
|
<view class="course-top">
|
||||||
<text class="course-title">{{ course.title }}</text>
|
<text class="course-title">{{ session.title }}</text>
|
||||||
</view>
|
</view>
|
||||||
<text class="time-line">{{ formatTimeRange(course.start_at, course.end_at) }}</text>
|
<text class="time-line">{{ formatTimeRange(session.start_at, session.end_at) }}</text>
|
||||||
<text class="meta">{{ course.coach_name }}</text>
|
<text class="meta">{{ session.coach_name }}</text>
|
||||||
<view class="course-bottom">
|
<view class="course-bottom">
|
||||||
<text class="meta">已约 {{ course.booked_count }}/{{ course.capacity }}</text>
|
<text class="meta">已约 {{ session.booked_count }}/{{ session.capacity }}</text>
|
||||||
<text class="course-price">¥{{ course.price }}</text>
|
<text class="course-price">¥{{ session.price }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="isLoading" class="load-more-tip">加载中...</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 v-else-if="!hasMore" class="load-more-tip">已加载全部</view>
|
||||||
</view>
|
</view>
|
||||||
<VenueTabBar venue="comprehensive" active="courses" />
|
<VenueTabBar venue="comprehensive" active="courses" />
|
||||||
@@ -56,7 +56,7 @@ import VenueTabBar from '@/components/VenueTabBar.vue'
|
|||||||
const selectedDate = ref(todayString())
|
const selectedDate = ref(todayString())
|
||||||
const selectedCoachIndex = ref(0)
|
const selectedCoachIndex = ref(0)
|
||||||
const selectedCourtTypeIndex = ref(0)
|
const selectedCourtTypeIndex = ref(0)
|
||||||
const courses = ref([])
|
const sessions = ref([])
|
||||||
const coaches = ref([])
|
const coaches = ref([])
|
||||||
const page = ref(1)
|
const page = ref(1)
|
||||||
const pageSize = 20
|
const pageSize = 20
|
||||||
@@ -75,25 +75,25 @@ const selectedCoachId = computed(() => coachOptions.value[selectedCoachIndex.val
|
|||||||
const selectedCoachName = computed(() => coachOptions.value[selectedCoachIndex.value]?.name || '全部教练')
|
const selectedCoachName = computed(() => coachOptions.value[selectedCoachIndex.value]?.name || '全部教练')
|
||||||
const selectedCourtType = computed(() => courtTypeOptions[selectedCourtTypeIndex.value]?.value || '')
|
const selectedCourtType = computed(() => courtTypeOptions[selectedCourtTypeIndex.value]?.value || '')
|
||||||
const selectedCourtTypeLabel = computed(() => courtTypeOptions[selectedCourtTypeIndex.value]?.label || '全部')
|
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 () => {
|
onMounted(async () => {
|
||||||
const coachData = await venueApi.getCoaches()
|
const coachData = await venueApi.getCoaches()
|
||||||
coaches.value = coachData.items
|
coaches.value = coachData.items
|
||||||
loadCourses(true)
|
loadSessions(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
onPullDownRefresh(() => {
|
onPullDownRefresh(() => {
|
||||||
loadCourses(true).finally(() => uni.stopPullDownRefresh())
|
loadSessions(true).finally(() => uni.stopPullDownRefresh())
|
||||||
})
|
})
|
||||||
|
|
||||||
onReachBottom(() => {
|
onReachBottom(() => {
|
||||||
if (!hasMore.value || isLoadingMore.value) return
|
if (!hasMore.value || isLoadingMore.value) return
|
||||||
page.value += 1
|
page.value += 1
|
||||||
loadCourses(false)
|
loadSessions(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadCourses = async (reset) => {
|
const loadSessions = async (reset) => {
|
||||||
if (reset) {
|
if (reset) {
|
||||||
page.value = 1
|
page.value = 1
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
@@ -101,14 +101,14 @@ const loadCourses = async (reset) => {
|
|||||||
isLoadingMore.value = true
|
isLoadingMore.value = true
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const data = await courseApi.getCourses({
|
const data = await courseApi.getSessions({
|
||||||
page: page.value,
|
page: page.value,
|
||||||
pageSize,
|
pageSize,
|
||||||
date: selectedDate.value,
|
date: selectedDate.value,
|
||||||
coachId: selectedCoachId.value,
|
coachId: selectedCoachId.value,
|
||||||
courtType: selectedCourtType.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
|
total.value = data.total
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
@@ -118,17 +118,17 @@ const loadCourses = async (reset) => {
|
|||||||
|
|
||||||
const onDateChange = (e) => {
|
const onDateChange = (e) => {
|
||||||
selectedDate.value = e.detail.value
|
selectedDate.value = e.detail.value
|
||||||
loadCourses(true)
|
loadSessions(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCoachChange = (e) => {
|
const onCoachChange = (e) => {
|
||||||
selectedCoachIndex.value = Number(e.detail.value)
|
selectedCoachIndex.value = Number(e.detail.value)
|
||||||
loadCourses(true)
|
loadSessions(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCourtTypeChange = (e) => {
|
const onCourtTypeChange = (e) => {
|
||||||
selectedCourtTypeIndex.value = Number(e.detail.value)
|
selectedCourtTypeIndex.value = Number(e.detail.value)
|
||||||
loadCourses(true)
|
loadSessions(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const goDetail = (id) => {
|
const goDetail = (id) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user