diff --git a/miniprogram/scripts/verify-comprehensive-commerce.mjs b/miniprogram/scripts/verify-comprehensive-commerce.mjs index 9852f0e..d7b7c7b 100644 --- a/miniprogram/scripts/verify-comprehensive-commerce.mjs +++ b/miniprogram/scripts/verify-comprehensive-commerce.mjs @@ -10,15 +10,26 @@ 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 membershipApi = readFileSync(resolve(srcRoot, 'modules/comprehensive/api/membership.js'), 'utf8') const storedValueApi = readFileSync(resolve(srcRoot, 'modules/comprehensive/api/storedValue.js'), 'utf8') const pagesJson = readFileSync(resolve(srcRoot, 'pages.json'), 'utf8') const mallPage = readFileSync(resolve(srcRoot, 'pages/comprehensive/mall/index.vue'), 'utf8') +const availablePage = readFileSync(resolve(srcRoot, 'pages/comprehensive/membership/available-sessions.vue'), 'utf8') +const bookingDetail = readFileSync(resolve(srcRoot, 'pages/comprehensive/membership/booking-detail.vue'), 'utf8') expectIncludes(membershipApi, "url: '/api/membership/plans'", 'membership plans') expectIncludes(membershipApi, "url: '/api/membership/orders'", 'membership order') expectIncludes(membershipApi, 'data: { plan_id: planId }', 'membership payload') expectIncludes(membershipApi, 'toOrderBrief(data.order)', 'membership order mapping') +expectIncludes(membershipApi, "url: '/api/membership/cards'", 'member cards') +expectIncludes(membershipApi, 'cards/${cardId}/usage-records', 'card usage records') +expectIncludes(membershipApi, "url: '/api/membership/available-courses'", 'available sessions') +expectIncludes(membershipApi, "url: '/api/membership/bookings'", 'card bookings') +expectIncludes(membershipApi, 'data: { card_id: cardId, session_id: sessionId }', 'card booking payload') expectIncludes(storedValueApi, "url: '/api/stored-value/cards'", 'stored value cards') expectIncludes(storedValueApi, "url: '/api/stored-value/orders'", 'stored value order') expectIncludes(storedValueApi, 'data: { card_id: cardId }', 'stored value payload') @@ -26,6 +37,8 @@ expectIncludes(storedValueApi, 'toOrderBrief(data.order)', 'stored value order m expectIncludes(pagesJson, 'pages/comprehensive/mall/index', 'mall route') expectIncludes(mallPage, '储蓄卡', 'stored value UI copy') expectIncludes(mallPage, 'face_value', 'face value display') +expectIncludes(availablePage, 'available_card_ids', 'available card selection') +expectExcludes(bookingDetail, 'booking.order', 'card booking has no order') if (failures.length) { console.error(`Comprehensive commerce verification failed with ${failures.length} issue(s):`) diff --git a/miniprogram/src/modules/comprehensive/api/membership.js b/miniprogram/src/modules/comprehensive/api/membership.js index 45924ca..320e849 100644 --- a/miniprogram/src/modules/comprehensive/api/membership.js +++ b/miniprogram/src/modules/comprehensive/api/membership.js @@ -1,5 +1,5 @@ import { request } from './request' -import { toArray, toNumber } from './mappers' +import { toArray, toNumber, toPageQuery, toPagination } from './mappers' import { toOrderBrief } from './order' const toPlan = (item = {}) => ({ @@ -15,6 +15,79 @@ const toPlan = (item = {}) => ({ created_at: item.created_at || '' }) +const toCard = (backendCard = {}) => ({ + id: backendCard.id ?? null, + plan_id: backendCard.plan_id ?? null, + plan_name: backendCard.plan_name || '', + total_count: toNumber(backendCard.total_count), + remaining_count: toNumber(backendCard.remaining_count), + status: backendCard.status || '', + status_label: backendCard.status_label || backendCard.status || '', + order_id: backendCard.order_id ?? null, + created_at: backendCard.created_at || '' +}) + +const toUsageRecord = (backendRecord = {}) => ({ + id: backendRecord.id ?? null, + card_id: backendRecord.card_id ?? null, + count: toNumber(backendRecord.count), + kind: backendRecord.kind || '', + kind_label: backendRecord.kind_label || backendRecord.kind || '', + booking_id: backendRecord.booking_id ?? null, + order_id: backendRecord.order_id ?? null, + remark: backendRecord.remark || '', + created_at: backendRecord.created_at || '' +}) + +const toAvailableSession = (backendSession = {}) => ({ + id: backendSession.id ?? null, + course_id: backendSession.course_id ?? null, + title: backendSession.title || '', + 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', + available_card_ids: toArray(backendSession.available_card_ids).map(toNumber) +}) + +const toCardBookingSession = (backendSession = {}) => ({ + id: backendSession.id ?? null, + course_id: backendSession.course_id ?? null, + title: backendSession.title || '', + 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' +}) + +const toCardBookingCard = (backendCard = {}) => ({ + id: backendCard.id ?? null, + plan_name: backendCard.plan_name || '', + remaining_count: toNumber(backendCard.remaining_count) +}) + +const toCardBooking = (backendBooking = {}) => ({ + id: backendBooking.id ?? null, + card_id: backendBooking.card_id ?? null, + session_id: backendBooking.session_id ?? null, + status: backendBooking.status || '', + status_label: backendBooking.status_label || backendBooking.status || '', + checked_in_at: backendBooking.checked_in_at || null, + cancelled_at: backendBooking.cancelled_at || null, + cancel_reason: backendBooking.cancel_reason || '', + created_at: backendBooking.created_at || '', + card: toCardBookingCard(backendBooking.card), + session: toCardBookingSession(backendBooking.session) +}) + export const membershipApi = { getPlans() { return request({ @@ -27,5 +100,41 @@ export const membershipApi = { method: 'POST', data: { plan_id: planId } }).then((data) => toOrderBrief(data.order)) + }, + getCards(params = {}) { + return request({ + url: '/api/membership/cards', + data: toPageQuery(params) + }).then((data) => toPagination(data, toCard)) + }, + getUsageRecords(cardId, params = {}) { + return request({ + url: `/api/membership/cards/${cardId}/usage-records`, + data: toPageQuery(params) + }).then((data) => toPagination(data, toUsageRecord)) + }, + getAvailableSessions(params = {}) { + return request({ + url: '/api/membership/available-courses', + data: toPageQuery(params) + }).then((data) => toPagination(data, toAvailableSession)) + }, + createCardBooking({ cardId, sessionId }) { + return request({ + url: '/api/membership/bookings', + method: 'POST', + data: { card_id: cardId, session_id: sessionId } + }).then(toCardBooking) + }, + getCardBookings(params = {}) { + return request({ + url: '/api/membership/bookings', + data: toPageQuery(params) + }).then((data) => toPagination(data, toCardBooking)) + }, + getCardBookingDetail(bookingId) { + return request({ + url: `/api/membership/bookings/${bookingId}` + }).then(toCardBooking) } } diff --git a/miniprogram/src/pages.json b/miniprogram/src/pages.json index de9fc3a..ab88c88 100644 --- a/miniprogram/src/pages.json +++ b/miniprogram/src/pages.json @@ -144,6 +144,34 @@ "enablePullDownRefresh": true } }, + { + "path": "pages/comprehensive/membership/cards", + "style": { + "navigationBarTitleText": "我的次卡", + "enablePullDownRefresh": true + } + }, + { + "path": "pages/comprehensive/membership/card-detail", + "style": { + "navigationBarTitleText": "次卡详情", + "enablePullDownRefresh": true + } + }, + { + "path": "pages/comprehensive/membership/available-sessions", + "style": { + "navigationBarTitleText": "可约排课", + "enablePullDownRefresh": true + } + }, + { + "path": "pages/comprehensive/membership/booking-detail", + "style": { + "navigationBarTitleText": "次卡预约详情", + "enablePullDownRefresh": true + } + }, { "path": "pages/ice/home/index", "style": { diff --git a/miniprogram/src/pages/comprehensive/bookings/index.vue b/miniprogram/src/pages/comprehensive/bookings/index.vue index df50cc7..c32aa6d 100644 --- a/miniprogram/src/pages/comprehensive/bookings/index.vue +++ b/miniprogram/src/pages/comprehensive/bookings/index.vue @@ -12,18 +12,24 @@ {{ tab.label }} + + + {{ sub.label }} + + - + {{ getItemTitle(item) }} {{ getItemTime(item) }} {{ getItemDesc(item) }} - 订单金额 - ¥{{ item.order?.amount || '--' }} + {{ isMembershipCourse ? '次卡预约' : '订单金额' }} + 次卡预约 + ¥{{ item.order?.amount || '--' }} 暂无已预约记录 @@ -38,6 +44,7 @@ import { computed, onMounted, ref } from 'vue' import { bookingApi } from '@/modules/comprehensive/api/booking' import { ticketApi } from '@/modules/comprehensive/api/ticket' +import { membershipApi } from '@/modules/comprehensive/api/membership' import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session' import { formatDateTime, formatTimeRange } from '@/modules/comprehensive/utils/date' import VenueTabBar from '@/components/VenueTabBar.vue' @@ -45,6 +52,7 @@ import UnauthLoginPanel from '@/components/UnauthLoginPanel.vue' const session = useComprehensiveSessionStore() const businessType = ref('course') +const courseSubType = ref('regular') const items = ref([]) const isLoading = ref(false) @@ -54,6 +62,13 @@ const businessTabs = [ { label: '门票', value: 'ticket' } ] +const courseSubTabs = [ + { label: '普通预约', value: 'regular' }, + { label: '次卡预约', value: 'membership' } +] + +const isMembershipCourse = computed(() => businessType.value === 'course' && courseSubType.value === 'membership') + const bookedItems = computed(() => { return items.value.filter((item) => ['booked', 'available'].includes(item.status)) }) @@ -70,6 +85,12 @@ const login = async () => { const selectBusiness = (value) => { businessType.value = value + if (value !== 'course') courseSubType.value = 'regular' + loadItems() +} + +const selectCourseSub = (value) => { + courseSubType.value = value loadItems() } @@ -77,8 +98,13 @@ const loadItems = async () => { isLoading.value = true try { if (businessType.value === 'course') { - const data = await bookingApi.getCourseBookings({ page: 1, pageSize: 50 }) - items.value = data.items + if (courseSubType.value === 'membership') { + const data = await membershipApi.getCardBookings({ page: 1, pageSize: 50 }) + items.value = data.items + } else { + const data = await bookingApi.getCourseBookings({ page: 1, pageSize: 50 }) + items.value = data.items + } } else if (businessType.value === 'private') { const data = await bookingApi.getPrivateBookings({ page: 1, pageSize: 50 }) items.value = data.items @@ -111,14 +137,21 @@ const getItemTime = (item) => { } const getItemDesc = (item) => { - if (businessType.value === 'course') return `${item.session?.coach_name || ''} · ${item.session?.courts?.map((court) => court.name).join('、') || ''}` + if (businessType.value === 'course') { + if (courseSubType.value === 'membership') return `${item.session?.coach_name || ''}` + 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 } const goDetail = (item) => { if (businessType.value === 'course') { - uni.navigateTo({ url: `/pages/comprehensive/bookings/course-detail?id=${item.id}` }) + if (courseSubType.value === 'membership') { + uni.navigateTo({ url: `/pages/comprehensive/membership/booking-detail?id=${item.id}` }) + } else { + uni.navigateTo({ url: `/pages/comprehensive/bookings/course-detail?id=${item.id}` }) + } } else if (businessType.value === 'private') { uni.navigateTo({ url: `/pages/comprehensive/bookings/private-detail?id=${item.id}` }) } else if (item.order_id) { @@ -165,6 +198,30 @@ const goDetail = (item) => { color: #FFFFFF; } +.sub-tabs { + display: flex; + gap: $spacing-sm; + margin-top: $spacing-sm; +} + +.sub-tab { + flex: 1; + height: 56rpx; + border-radius: $border-radius-base; + background: $bg-color-hover; + color: $text-color-muted; + font-size: 24rpx; + font-weight: 800; + display: flex; + align-items: center; + justify-content: center; +} + +.sub-tab.active { + background: $color-primary; + color: #FFFFFF; +} + .list { padding: $spacing-md; } diff --git a/miniprogram/src/pages/comprehensive/membership/available-sessions.vue b/miniprogram/src/pages/comprehensive/membership/available-sessions.vue new file mode 100644 index 0000000..dfa6db2 --- /dev/null +++ b/miniprogram/src/pages/comprehensive/membership/available-sessions.vue @@ -0,0 +1,295 @@ + + + + + diff --git a/miniprogram/src/pages/comprehensive/membership/booking-detail.vue b/miniprogram/src/pages/comprehensive/membership/booking-detail.vue new file mode 100644 index 0000000..bb29024 --- /dev/null +++ b/miniprogram/src/pages/comprehensive/membership/booking-detail.vue @@ -0,0 +1,276 @@ + + + + + diff --git a/miniprogram/src/pages/comprehensive/membership/card-detail.vue b/miniprogram/src/pages/comprehensive/membership/card-detail.vue new file mode 100644 index 0000000..823fa3b --- /dev/null +++ b/miniprogram/src/pages/comprehensive/membership/card-detail.vue @@ -0,0 +1,371 @@ + + + + + diff --git a/miniprogram/src/pages/comprehensive/membership/cards.vue b/miniprogram/src/pages/comprehensive/membership/cards.vue new file mode 100644 index 0000000..94b9a44 --- /dev/null +++ b/miniprogram/src/pages/comprehensive/membership/cards.vue @@ -0,0 +1,189 @@ + + + + +