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 @@
+
+
+
+
+
+
+
+
+
+
+ {{ session.title }}
+
+ {{ formatTimeRange(session.start_at, session.end_at) }}
+ {{ session.coach_name }}
+
+ 剩余 {{ session.remaining_count }}/{{ session.capacity }}
+ ¥{{ session.price }}
+
+
+
+
+ 加载中...
+ 暂无可约课程
+ 已加载全部
+ 加载中...
+
+
+
+
+ 选择次卡
+
+ {{ getCardLabel(cardId) }}
+
+ 取消
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+ 预约不存在
+ 该次卡预约可能已被删除或链接失效
+
+
+
+ {{ booking.status_label }}
+ {{ statusDesc }}
+
+
+
+
+
+ {{ booking.session.title }}
+ {{ formatTimeRange(booking.session.start_at, booking.session.end_at) }}
+ {{ booking.session.coach_name }}
+
+
+
+
+ 次卡信息
+
+ 次卡名称
+ {{ booking.card.plan_name }}
+
+
+ 剩余次数
+ {{ booking.card.remaining_count }}
+
+
+
+
+ 预约时间线
+
+
+
+ 创建预约
+ {{ formatDateTime(booking.created_at) }}
+
+
+
+
+
+ 已核销
+ {{ formatDateTime(booking.checked_in_at) }}
+
+
+
+
+
+ 已取消
+ {{ formatDateTime(booking.cancelled_at) }}
+ 原因:{{ booking.cancel_reason }}
+
+
+
+
+ 加载中...
+
+
+
+
+
+
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 @@
+
+
+
+
+ 次卡不存在
+ 该次卡可能已被删除或链接失效
+
+ 加载中...
+
+
+
+ {{ card.plan_name }}
+ {{ card.status_label }}
+
+
+
+ {{ card.remaining_count }}
+ 剩余次数
+
+
+
+ {{ card.total_count }}
+ 总次数
+
+
+
+ 购买时间
+ {{ formatDate(card.created_at) }}
+
+
+
+
+ 适用课程
+
+ {{ course.title }}
+
+
+
+
+ 使用记录
+ 暂无使用记录
+
+
+ {{ record.kind_label }}
+
+ {{ record.count > 0 ? '+' : '' }}{{ record.count }}
+
+
+
+ {{ formatDateTime(record.created_at) }}
+
+ 加载中...
+ 已加载全部
+ 加载中...
+
+
+
+
+
+
+
+
+
+
+
+
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 @@
+
+
+
+
+
+
+
+
+ {{ card.plan_name }}
+ {{ card.status_label }}
+
+
+
+ {{ card.remaining_count }}
+ 剩余次数
+
+
+
+ {{ card.total_count }}
+ 总次数
+
+
+
+
+ 加载中...
+ 暂无次卡
+ 已加载全部
+ 加载中...
+
+
+
+
+
+
+
+