From 8e1443f7e539d46b45c97ecb400725e01974c9ae Mon Sep 17 00:00:00 2001
From: gqt <3217233537@qq.com>
Date: Mon, 8 Jun 2026 20:54:25 +0800
Subject: [PATCH] feat: migrate comprehensive venue module
---
.../src/modules/comprehensive/api/auth.js | 44 ++
.../src/modules/comprehensive/api/booking.js | 90 ++++
.../src/modules/comprehensive/api/content.js | 52 ++
.../src/modules/comprehensive/api/course.js | 57 +++
.../src/modules/comprehensive/api/mappers.js | 27 +
.../src/modules/comprehensive/api/member.js | 80 +++
.../src/modules/comprehensive/api/order.js | 102 ++++
.../comprehensive/api/privateBooking.js | 80 +++
.../src/modules/comprehensive/api/request.js | 129 +++++
.../src/modules/comprehensive/api/ticket.js | 59 +++
.../src/modules/comprehensive/api/venue.js | 19 +
.../modules/comprehensive/stores/session.js | 58 +++
.../src/modules/comprehensive/utils/date.js | 39 ++
.../src/modules/comprehensive/utils/money.js | 10 +
.../src/modules/comprehensive/utils/status.js | 14 +
miniprogram/src/pages.json | 121 +++++
.../pages/comprehensive/activities/detail.vue | 57 +++
.../pages/comprehensive/activities/index.vue | 127 +++++
.../comprehensive/bookings/course-detail.vue | 245 +++++++++
.../pages/comprehensive/bookings/index.vue | 240 +++++++++
.../comprehensive/bookings/private-detail.vue | 201 ++++++++
.../pages/comprehensive/courses/confirm.vue | 113 +++++
.../pages/comprehensive/courses/detail.vue | 146 ++++++
.../src/pages/comprehensive/courses/index.vue | 235 +++++++++
.../src/pages/comprehensive/home/index.vue | 295 +++++++++++
.../src/pages/comprehensive/orders/detail.vue | 174 +++++++
.../src/pages/comprehensive/pay/index.vue | 299 +++++++++++
.../pages/comprehensive/private/confirm.vue | 127 +++++
.../src/pages/comprehensive/private/index.vue | 478 ++++++++++++++++++
.../pages/comprehensive/profile/balance.vue | 105 ++++
.../pages/comprehensive/profile/detail.vue | 233 +++++++++
.../src/pages/comprehensive/profile/index.vue | 453 +++++++++++++++++
.../pages/comprehensive/profile/orders.vue | 240 +++++++++
.../pages/comprehensive/profile/tickets.vue | 97 ++++
.../src/pages/comprehensive/tickets/index.vue | 171 +++++++
.../comprehensive/tab/booking-active.png | Bin 0 -> 433 bytes
.../src/static/comprehensive/tab/booking.png | Bin 0 -> 429 bytes
.../comprehensive/tab/course-active.png | Bin 0 -> 350 bytes
.../src/static/comprehensive/tab/course.png | Bin 0 -> 347 bytes
.../static/comprehensive/tab/home-active.png | Bin 0 -> 496 bytes
.../src/static/comprehensive/tab/home.png | Bin 0 -> 493 bytes
.../static/comprehensive/tab/mine-active.png | Bin 0 -> 469 bytes
.../src/static/comprehensive/tab/mine.png | Bin 0 -> 466 bytes
43 files changed, 5017 insertions(+)
create mode 100644 miniprogram/src/modules/comprehensive/api/auth.js
create mode 100644 miniprogram/src/modules/comprehensive/api/booking.js
create mode 100644 miniprogram/src/modules/comprehensive/api/content.js
create mode 100644 miniprogram/src/modules/comprehensive/api/course.js
create mode 100644 miniprogram/src/modules/comprehensive/api/mappers.js
create mode 100644 miniprogram/src/modules/comprehensive/api/member.js
create mode 100644 miniprogram/src/modules/comprehensive/api/order.js
create mode 100644 miniprogram/src/modules/comprehensive/api/privateBooking.js
create mode 100644 miniprogram/src/modules/comprehensive/api/request.js
create mode 100644 miniprogram/src/modules/comprehensive/api/ticket.js
create mode 100644 miniprogram/src/modules/comprehensive/api/venue.js
create mode 100644 miniprogram/src/modules/comprehensive/stores/session.js
create mode 100644 miniprogram/src/modules/comprehensive/utils/date.js
create mode 100644 miniprogram/src/modules/comprehensive/utils/money.js
create mode 100644 miniprogram/src/modules/comprehensive/utils/status.js
create mode 100644 miniprogram/src/pages/comprehensive/activities/detail.vue
create mode 100644 miniprogram/src/pages/comprehensive/activities/index.vue
create mode 100644 miniprogram/src/pages/comprehensive/bookings/course-detail.vue
create mode 100644 miniprogram/src/pages/comprehensive/bookings/index.vue
create mode 100644 miniprogram/src/pages/comprehensive/bookings/private-detail.vue
create mode 100644 miniprogram/src/pages/comprehensive/courses/confirm.vue
create mode 100644 miniprogram/src/pages/comprehensive/courses/detail.vue
create mode 100644 miniprogram/src/pages/comprehensive/courses/index.vue
create mode 100644 miniprogram/src/pages/comprehensive/home/index.vue
create mode 100644 miniprogram/src/pages/comprehensive/orders/detail.vue
create mode 100644 miniprogram/src/pages/comprehensive/pay/index.vue
create mode 100644 miniprogram/src/pages/comprehensive/private/confirm.vue
create mode 100644 miniprogram/src/pages/comprehensive/private/index.vue
create mode 100644 miniprogram/src/pages/comprehensive/profile/balance.vue
create mode 100644 miniprogram/src/pages/comprehensive/profile/detail.vue
create mode 100644 miniprogram/src/pages/comprehensive/profile/index.vue
create mode 100644 miniprogram/src/pages/comprehensive/profile/orders.vue
create mode 100644 miniprogram/src/pages/comprehensive/profile/tickets.vue
create mode 100644 miniprogram/src/pages/comprehensive/tickets/index.vue
create mode 100644 miniprogram/src/static/comprehensive/tab/booking-active.png
create mode 100644 miniprogram/src/static/comprehensive/tab/booking.png
create mode 100644 miniprogram/src/static/comprehensive/tab/course-active.png
create mode 100644 miniprogram/src/static/comprehensive/tab/course.png
create mode 100644 miniprogram/src/static/comprehensive/tab/home-active.png
create mode 100644 miniprogram/src/static/comprehensive/tab/home.png
create mode 100644 miniprogram/src/static/comprehensive/tab/mine-active.png
create mode 100644 miniprogram/src/static/comprehensive/tab/mine.png
diff --git a/miniprogram/src/modules/comprehensive/api/auth.js b/miniprogram/src/modules/comprehensive/api/auth.js
new file mode 100644
index 0000000..42b6012
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/auth.js
@@ -0,0 +1,44 @@
+import { authStorage, request } from './request'
+import { toNumber } from './mappers'
+
+const toMember = (backendMember = {}) => ({
+ id: backendMember.id ?? null,
+ openid: backendMember.openid || '',
+ unionid: backendMember.unionid || '',
+ nickname: backendMember.nickname || '',
+ phone: backendMember.phone || '',
+ avatar_url: backendMember.avatar_url || '',
+ balance: backendMember.balance || '0.00',
+ is_active: backendMember.is_active === true,
+ created_at: backendMember.created_at || ''
+})
+
+const toWechatLogin = (backendData = {}) => ({
+ access_token: backendData.access_token || '',
+ token_type: backendData.token_type || 'Bearer',
+ expires_in: toNumber(backendData.expires_in),
+ member: toMember(backendData.member)
+})
+
+export const authApi = {
+ async wechatLogin(payload = {}) {
+ const data = typeof payload === 'string' ? { code: payload } : payload
+ const loginData = await request({
+ url: '/api/auth/wechat-login',
+ method: 'POST',
+ data: {
+ code: data.code,
+ nickname: data.nickname || '',
+ avatar_url: data.avatar_url || ''
+ }
+ }).then(toWechatLogin)
+
+ authStorage.setToken(loginData.access_token)
+ return loginData
+ },
+ logout() {
+ authStorage.clear()
+ }
+}
+
+export { authStorage }
diff --git a/miniprogram/src/modules/comprehensive/api/booking.js b/miniprogram/src/modules/comprehensive/api/booking.js
new file mode 100644
index 0000000..b0b105e
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/booking.js
@@ -0,0 +1,90 @@
+import { request } from './request'
+import { toArray, toNumber, toPageQuery, toPagination } from './mappers'
+import { toOrderBrief } from './order'
+
+const toBookingCourse = (backendCourse = {}) => ({
+ id: backendCourse.id ?? null,
+ title: backendCourse.title || '',
+ cover_image: backendCourse.cover_image || '',
+ coach_name: backendCourse.coach_name || '',
+ start_at: backendCourse.start_at || '',
+ end_at: backendCourse.end_at || '',
+ capacity: toNumber(backendCourse.capacity),
+ booked_count: toNumber(backendCourse.booked_count),
+ remaining_count: toNumber(backendCourse.remaining_count),
+ price: backendCourse.price || '0.00',
+ courts: toArray(backendCourse.courts)
+})
+
+const toPrivateCourt = (backendCourt = {}) => ({
+ id: backendCourt.id ?? null,
+ name: backendCourt.name || '',
+ court_type: backendCourt.court_type || '',
+ court_type_label: backendCourt.court_type_label || ''
+})
+
+const toCourseBooking = (backendBooking = {}) => ({
+ id: backendBooking.id ?? null,
+ course_id: backendBooking.course_id ?? null,
+ order_id: backendBooking.order_id ?? null,
+ status: backendBooking.status || '',
+ status_label: backendBooking.status_label || backendBooking.status || '',
+ expire_at: backendBooking.expire_at || null,
+ checked_in_at: backendBooking.checked_in_at || null,
+ cancelled_at: backendBooking.cancelled_at || null,
+ cancel_by: backendBooking.cancel_by || '',
+ cancel_reason: backendBooking.cancel_reason || '',
+ can_cancel: backendBooking.can_cancel === true,
+ cancel_deadline_at: backendBooking.cancel_deadline_at || null,
+ created_at: backendBooking.created_at || '',
+ course: toBookingCourse(backendBooking.course),
+ order: toOrderBrief(backendBooking.order)
+})
+
+export const toPrivateBooking = (backendBooking = {}) => ({
+ id: backendBooking.id ?? null,
+ order_id: backendBooking.order_id ?? null,
+ status: backendBooking.status || '',
+ status_label: backendBooking.status_label || backendBooking.status || '',
+ expire_at: backendBooking.expire_at || null,
+ start_at: backendBooking.start_at || null,
+ end_at: backendBooking.end_at || null,
+ courts: toArray(backendBooking.courts).map(toPrivateCourt),
+ checked_in_at: backendBooking.checked_in_at || null,
+ cancelled_at: backendBooking.cancelled_at || null,
+ cancel_by: backendBooking.cancel_by || '',
+ cancel_reason: backendBooking.cancel_reason || '',
+ created_at: backendBooking.created_at || '',
+ order: toOrderBrief(backendBooking.order)
+})
+
+export const bookingApi = {
+ getCourseBookings(params = {}) {
+ return request({
+ url: '/api/bookings/courses',
+ data: toPageQuery(params)
+ }).then((data) => toPagination(data, toCourseBooking))
+ },
+ getCourseBookingDetail(id) {
+ return request({
+ url: `/api/bookings/courses/${id}`
+ }).then(toCourseBooking)
+ },
+ cancelCourseBooking(id) {
+ return request({
+ url: `/api/bookings/courses/${id}/cancel`,
+ method: 'POST'
+ }).then(toCourseBooking)
+ },
+ getPrivateBookings(params = {}) {
+ return request({
+ url: '/api/bookings/private',
+ data: toPageQuery(params)
+ }).then((data) => toPagination(data, toPrivateBooking))
+ },
+ getPrivateBookingDetail(id) {
+ return request({
+ url: `/api/bookings/private/${id}`
+ }).then(toPrivateBooking)
+ }
+}
diff --git a/miniprogram/src/modules/comprehensive/api/content.js b/miniprogram/src/modules/comprehensive/api/content.js
new file mode 100644
index 0000000..fa367c6
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/content.js
@@ -0,0 +1,52 @@
+import { request } from './request'
+import { compactParams, toArray, toNumber, toPageQuery, toPagination } from './mappers'
+import { toCourse } from './course'
+
+const toBanner = (backendBanner = {}) => ({
+ id: backendBanner.id ?? null,
+ title: backendBanner.title || '',
+ image: backendBanner.image || '',
+ sort_order: toNumber(backendBanner.sort_order)
+})
+
+const toArticle = (backendArticle = {}) => ({
+ id: backendArticle.id ?? null,
+ title: backendArticle.title || '',
+ cover_image: backendArticle.cover_image || '',
+ published_at: backendArticle.published_at || null,
+ created_at: backendArticle.created_at || '',
+ content_html: backendArticle.content_html || '',
+ updated_at: backendArticle.updated_at || ''
+})
+
+const toHomeContent = (backendData = {}) => ({
+ banners: toArray(backendData.banners).map(toBanner),
+ articles: toArray(backendData.articles).map(toArticle),
+ upcoming_courses: toArray(backendData.upcoming_courses).map(toCourse),
+ venue: {
+ name: backendData.venue?.name || ''
+ }
+})
+
+export const contentApi = {
+ getHomeContent(params = {}) {
+ return request({
+ url: '/api/contents/home',
+ data: compactParams({
+ banner_limit: params.banner_limit || params.bannerLimit,
+ article_limit: params.article_limit || params.articleLimit
+ })
+ }).then(toHomeContent)
+ },
+ getArticles(params = {}) {
+ return request({
+ url: '/api/contents/articles',
+ data: toPageQuery(params)
+ }).then((data) => toPagination(data, toArticle))
+ },
+ getArticleDetail(id) {
+ return request({
+ url: `/api/contents/articles/${id}`
+ }).then(toArticle)
+ }
+}
diff --git a/miniprogram/src/modules/comprehensive/api/course.js b/miniprogram/src/modules/comprehensive/api/course.js
new file mode 100644
index 0000000..738f073
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/course.js
@@ -0,0 +1,57 @@
+import { request } from './request'
+import { compactParams, toArray, toNumber, toPageQuery, toPagination } from './mappers'
+
+const toCourseCourt = (backendCourt = {}) => ({
+ id: backendCourt.id ?? null,
+ name: backendCourt.name || '',
+ court_type: backendCourt.court_type || '',
+ court_type_label: backendCourt.court_type_label || ''
+})
+
+export const toCourse = (backendCourse = {}) => ({
+ id: backendCourse.id ?? null,
+ title: backendCourse.title || '',
+ cover_image: backendCourse.cover_image || '',
+ coach_id: backendCourse.coach_id ?? null,
+ coach_name: backendCourse.coach_name || '',
+ start_at: backendCourse.start_at || '',
+ end_at: backendCourse.end_at || '',
+ capacity: toNumber(backendCourse.capacity),
+ booked_count: toNumber(backendCourse.booked_count),
+ remaining_count: toNumber(backendCourse.remaining_count),
+ price: backendCourse.price || '0.00',
+ status: backendCourse.status || '',
+ status_label: backendCourse.status_label || backendCourse.status || '',
+ courts: toArray(backendCourse.courts).map(toCourseCourt),
+ detail_html: backendCourse.detail_html || '',
+ created_at: backendCourse.created_at || '',
+ updated_at: backendCourse.updated_at || ''
+})
+
+const toCourseQuery = (params = {}) => compactParams({
+ ...toPageQuery(params),
+ date: params.date,
+ coach_id: params.coachId || params.coach_id,
+ court_type: params.courtType || params.court_type
+})
+
+export const courseApi = {
+ getCourses(params = {}) {
+ return request({
+ url: '/api/courses',
+ data: toCourseQuery(params)
+ }).then((data) => toPagination(data, toCourse))
+ },
+ getCourseDetail(id) {
+ return request({
+ url: `/api/courses/${id}`
+ }).then(toCourse)
+ },
+ createCourseBooking(courseId) {
+ return request({
+ url: '/api/bookings/course',
+ method: 'POST',
+ data: { course_id: courseId }
+ })
+ }
+}
diff --git a/miniprogram/src/modules/comprehensive/api/mappers.js b/miniprogram/src/modules/comprehensive/api/mappers.js
new file mode 100644
index 0000000..0e4eaef
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/mappers.js
@@ -0,0 +1,27 @@
+export const toNumber = (value, defaultValue = 0) => {
+ const numberValue = Number(value)
+ return Number.isFinite(numberValue) ? numberValue : defaultValue
+}
+
+export const toArray = (value) => (Array.isArray(value) ? value : [])
+
+export const compactParams = (params = {}) => {
+ return Object.entries(params).reduce((result, [key, value]) => {
+ if (value !== undefined && value !== null && value !== '') {
+ result[key] = value
+ }
+ return result
+ }, {})
+}
+
+export const toPageQuery = (params = {}) => compactParams({
+ page: params.page || 1,
+ page_size: params.pageSize || params.page_size || 20
+})
+
+export const toPagination = (backendData = {}, itemMapper = (item) => item) => ({
+ items: toArray(backendData.items).map(itemMapper),
+ total: toNumber(backendData.total),
+ page: toNumber(backendData.page, 1),
+ page_size: toNumber(backendData.page_size, 20)
+})
diff --git a/miniprogram/src/modules/comprehensive/api/member.js b/miniprogram/src/modules/comprehensive/api/member.js
new file mode 100644
index 0000000..20fd25b
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/member.js
@@ -0,0 +1,80 @@
+import { request, uploadFile } from './request'
+import { toNumber, toPageQuery, toPagination } from './mappers'
+
+const toMember = (backendMember = {}) => ({
+ id: backendMember.id ?? null,
+ openid: backendMember.openid || '',
+ unionid: backendMember.unionid || '',
+ nickname: backendMember.nickname || '',
+ phone: backendMember.phone || '',
+ avatar_url: backendMember.avatar_url || '',
+ balance: backendMember.balance || '0.00',
+ is_active: backendMember.is_active === true,
+ created_at: backendMember.created_at || ''
+})
+
+const toBalance = (backendData = {}) => ({
+ member_id: backendData.member_id ?? null,
+ balance: backendData.balance || '0.00'
+})
+
+const toBalanceRecord = (backendRecord = {}) => ({
+ id: backendRecord.id ?? null,
+ amount: backendRecord.amount || '0.00',
+ kind: backendRecord.kind || '',
+ kind_label: backendRecord.kind_label || backendRecord.kind || '',
+ order_id: backendRecord.order_id ?? null,
+ remark: backendRecord.remark || '',
+ created_at: backendRecord.created_at || ''
+})
+
+export const memberApi = {
+ getMemberMe() {
+ return request({
+ url: '/api/members/me'
+ }).then(toMember)
+ },
+ updateMemberProfile(payload = {}) {
+ return request({
+ url: '/api/members/profile',
+ method: 'POST',
+ data: {
+ nickname: payload.nickname || null,
+ avatar_url: payload.avatar_url || null
+ }
+ }).then(toMember)
+ },
+ uploadAvatar(filePath) {
+ return uploadFile({
+ url: '/api/members/avatar',
+ filePath
+ }).then((data) => ({
+ avatar_url: data.avatar_url || '',
+ member: toMember(data.member)
+ }))
+ },
+ bindMemberPhone(code) {
+ return request({
+ url: '/api/members/phone/bind',
+ method: 'POST',
+ data: { code }
+ }).then(toMember)
+ },
+ getBalance() {
+ return request({
+ url: '/api/members/balance'
+ }).then(toBalance)
+ },
+ getBalanceRecords(params = {}) {
+ return request({
+ url: '/api/members/balance-records',
+ data: toPageQuery(params)
+ }).then((data) => {
+ const pagination = toPagination(data, toBalanceRecord)
+ return {
+ ...pagination,
+ total: toNumber(pagination.total)
+ }
+ })
+ }
+}
diff --git a/miniprogram/src/modules/comprehensive/api/order.js b/miniprogram/src/modules/comprehensive/api/order.js
new file mode 100644
index 0000000..ebe050e
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/order.js
@@ -0,0 +1,102 @@
+import { request } from './request'
+import { compactParams, toArray, toNumber, toPageQuery, toPagination } from './mappers'
+
+export const toOrderBrief = (backendOrder = {}) => ({
+ id: backendOrder.id ?? null,
+ order_no: backendOrder.order_no || '',
+ business_type: backendOrder.business_type || '',
+ business_type_label: backendOrder.business_type_label || backendOrder.business_type || '',
+ amount: backendOrder.amount || '0.00',
+ status: backendOrder.status || '',
+ status_label: backendOrder.status_label || backendOrder.status || '',
+ expire_at: backendOrder.expire_at || null,
+ created_at: backendOrder.created_at || '',
+ updated_at: backendOrder.updated_at || ''
+})
+
+const toPayment = (backendPayment = {}) => ({
+ id: backendPayment.id ?? null,
+ order_id: backendPayment.order_id ?? null,
+ method: backendPayment.method || '',
+ method_label: backendPayment.method_label || backendPayment.method || '',
+ amount: backendPayment.amount || '0.00',
+ status: backendPayment.status || '',
+ status_label: backendPayment.status_label || backendPayment.status || '',
+ external_trade_no: backendPayment.external_trade_no || '',
+ paid_at: backendPayment.paid_at || null,
+ created_at: backendPayment.created_at || ''
+})
+
+const toRefund = (backendRefund = {}) => ({
+ id: backendRefund.id ?? null,
+ payment_id: backendRefund.payment_id ?? null,
+ refund_no: backendRefund.refund_no || '',
+ amount: backendRefund.amount || '0.00',
+ status: backendRefund.status || '',
+ status_label: backendRefund.status_label || backendRefund.status || '',
+ external_refund_no: backendRefund.external_refund_no || '',
+ reason: backendRefund.reason || '',
+ refunded_at: backendRefund.refunded_at || null,
+ created_at: backendRefund.created_at || ''
+})
+
+const toOrderDetail = (backendOrder = {}) => ({
+ ...toOrderBrief(backendOrder),
+ payments: toArray(backendOrder.payments).map(toPayment),
+ refunds: toArray(backendOrder.refunds).map(toRefund),
+ updated_at: backendOrder.updated_at || ''
+})
+
+const toWechatPayment = (backendData = {}) => ({
+ payment: toPayment(backendData.payment),
+ wechat: {
+ timeStamp: backendData.wechat?.timeStamp || '',
+ nonceStr: backendData.wechat?.nonceStr || '',
+ package: backendData.wechat?.package || '',
+ signType: backendData.wechat?.signType || 'RSA',
+ paySign: backendData.wechat?.paySign || ''
+ }
+})
+
+const toOrderQuery = (params = {}) => compactParams({
+ ...toPageQuery(params),
+ business_type: params.businessType || params.business_type,
+ status: params.status
+})
+
+export const orderApi = {
+ getOrders(params = {}) {
+ return request({
+ url: '/api/orders',
+ data: toOrderQuery(params)
+ }).then((data) => toPagination(data, toOrderBrief))
+ },
+ getOrderDetail(id) {
+ return request({
+ url: `/api/orders/${id}`
+ }).then(toOrderDetail)
+ },
+ closeOrder(id) {
+ return request({
+ url: `/api/orders/${id}/close`,
+ method: 'POST'
+ }).then(toOrderDetail)
+ },
+ payOrderByBalance(id) {
+ return request({
+ url: `/api/orders/${id}/pay/balance`,
+ method: 'POST'
+ }).then((payment) => ({
+ ...payment,
+ amount: payment?.amount || '0.00',
+ id: payment?.id ?? null,
+ order_id: payment?.order_id ?? toNumber(id)
+ }))
+ },
+ payOrderByWechat(id) {
+ return request({
+ url: `/api/orders/${id}/pay/wechat`,
+ method: 'POST'
+ }).then(toWechatPayment)
+ }
+}
diff --git a/miniprogram/src/modules/comprehensive/api/privateBooking.js b/miniprogram/src/modules/comprehensive/api/privateBooking.js
new file mode 100644
index 0000000..2593bfd
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/privateBooking.js
@@ -0,0 +1,80 @@
+import { request } from './request'
+import { compactParams, toArray, toNumber } from './mappers'
+import { toPrivateBooking } from './booking'
+import { toOrderBrief } from './order'
+
+const toQuoteCourt = (backendCourt = {}) => ({
+ id: backendCourt.id ?? null,
+ name: backendCourt.name || '',
+ court_type: backendCourt.court_type || '',
+ court_type_label: backendCourt.court_type_label || ''
+})
+
+const toPrivateQuote = (backendQuote = {}) => ({
+ amount: backendQuote.amount || '0.00',
+ unit_count: toNumber(backendQuote.unit_count),
+ start_at: backendQuote.start_at || '',
+ end_at: backendQuote.end_at || '',
+ courts: toArray(backendQuote.courts).map(toQuoteCourt)
+})
+
+const toPrivateCreateResult = (backendData = {}) => ({
+ booking: toPrivateBooking(backendData.booking),
+ order: toOrderBrief(backendData.order)
+})
+
+const toPrivateTimeOption = (backendOption = {}) => ({
+ start_at: backendOption.start_at || '',
+ end_at: backendOption.end_at || '',
+ available_court_count: toNumber(backendOption.available_court_count),
+ available_courts: toArray(backendOption.available_courts).map(toQuoteCourt)
+})
+
+const toPrivateBookingOptions = (backendData = {}) => ({
+ date: backendData.date || '',
+ court_type: backendData.court_type || '',
+ court_type_label: backendData.court_type_label || '',
+ open_time: backendData.open_time || '',
+ close_time: backendData.close_time || '',
+ unit_price: backendData.unit_price || '0.00',
+ duration_units: toNumber(backendData.duration_units, 2),
+ duration_minutes: toNumber(backendData.duration_minutes, 60),
+ amount_per_court: backendData.amount_per_court || '0.00',
+ total_court_count: toNumber(backendData.total_court_count),
+ time_options: toArray(backendData.time_options).map(toPrivateTimeOption)
+})
+
+export const privateBookingApi = {
+ getPrivateBookingOptions(params = {}) {
+ return request({
+ url: '/api/bookings/private/options',
+ data: compactParams({
+ date: params.date,
+ court_type: params.courtType || params.court_type,
+ duration_units: params.durationUnits || params.duration_units
+ })
+ }).then(toPrivateBookingOptions)
+ },
+ quotePrivateBooking(payload = {}) {
+ return request({
+ url: '/api/bookings/private/quote',
+ method: 'POST',
+ data: {
+ court_ids: toArray(payload.court_ids),
+ start_at: payload.start_at,
+ end_at: payload.end_at
+ }
+ }).then(toPrivateQuote)
+ },
+ createPrivateBooking(payload = {}) {
+ return request({
+ url: '/api/bookings/private',
+ method: 'POST',
+ data: {
+ court_ids: toArray(payload.court_ids),
+ start_at: payload.start_at,
+ end_at: payload.end_at
+ }
+ }).then(toPrivateCreateResult)
+ }
+}
diff --git a/miniprogram/src/modules/comprehensive/api/request.js b/miniprogram/src/modules/comprehensive/api/request.js
new file mode 100644
index 0000000..c84474c
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/request.js
@@ -0,0 +1,129 @@
+const KEY_TOKEN = 'COMPREHENSIVE_VENUE_TOKEN'
+
+const API_BASE_URL = import.meta.env.VITE_COMPREHENSIVE_API_BASE_URL || 'http://127.0.0.1:8001'
+
+const normalizeError = (res, fallbackMessage = '请求服务失败') => {
+ const body = res?.data || res || {}
+ return {
+ code: body.code || res?.statusCode || 50000,
+ message: body.message || fallbackMessage,
+ errors: body.errors || null,
+ statusCode: res?.statusCode
+ }
+}
+
+export const authStorage = {
+ setToken(token) {
+ uni.setStorageSync(KEY_TOKEN, token)
+ },
+ getToken() {
+ return uni.getStorageSync(KEY_TOKEN)
+ },
+ clear() {
+ uni.removeStorageSync(KEY_TOKEN)
+ },
+ isLoggedIn() {
+ return !!uni.getStorageSync(KEY_TOKEN)
+ }
+}
+
+export function request(options = {}) {
+ const token = authStorage.getToken()
+ const header = {
+ 'Content-Type': 'application/json',
+ ...(options.header || {})
+ }
+
+ if (token) {
+ header.Authorization = `Bearer ${token}`
+ }
+
+ return new Promise((resolve, reject) => {
+ uni.request({
+ url: options.url.startsWith('http') ? options.url : `${API_BASE_URL}${options.url}`,
+ method: options.method || 'GET',
+ data: options.data,
+ header,
+ success: (res) => {
+ const body = res.data
+
+ if (res.statusCode === 401 || body?.code === 40001) {
+ authStorage.clear()
+ }
+
+ if (res.statusCode >= 200 && res.statusCode < 300 && body?.code === 0) {
+ resolve(body.data)
+ return
+ }
+
+ const error = normalizeError(res, `请求错误 ${res.statusCode}`)
+ uni.showToast({
+ title: error.message,
+ icon: 'none'
+ })
+ reject(error)
+ },
+ fail: (err) => {
+ const error = normalizeError(err, '网络连接失败,请检查服务')
+ uni.showToast({
+ title: error.message,
+ icon: 'none'
+ })
+ reject(error)
+ }
+ })
+ })
+}
+
+export function uploadFile(options = {}) {
+ const token = authStorage.getToken()
+ const header = {
+ ...(options.header || {})
+ }
+
+ if (token) {
+ header.Authorization = `Bearer ${token}`
+ }
+
+ return new Promise((resolve, reject) => {
+ uni.uploadFile({
+ url: options.url.startsWith('http') ? options.url : `${API_BASE_URL}${options.url}`,
+ filePath: options.filePath,
+ name: options.name || 'file',
+ formData: options.formData || {},
+ header,
+ success: (res) => {
+ let body = {}
+ try {
+ body = JSON.parse(res.data || '{}')
+ } catch (error) {
+ body = {}
+ }
+
+ if (res.statusCode === 401 || body?.code === 40001) {
+ authStorage.clear()
+ }
+
+ if (res.statusCode >= 200 && res.statusCode < 300 && body?.code === 0) {
+ resolve(body.data)
+ return
+ }
+
+ const uploadError = normalizeError({ ...res, data: body }, `上传错误 ${res.statusCode}`)
+ uni.showToast({
+ title: uploadError.message,
+ icon: 'none'
+ })
+ reject(uploadError)
+ },
+ fail: (err) => {
+ const uploadError = normalizeError(err, '文件上传失败,请检查服务')
+ uni.showToast({
+ title: uploadError.message,
+ icon: 'none'
+ })
+ reject(uploadError)
+ }
+ })
+ })
+}
diff --git a/miniprogram/src/modules/comprehensive/api/ticket.js b/miniprogram/src/modules/comprehensive/api/ticket.js
new file mode 100644
index 0000000..1b8715b
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/ticket.js
@@ -0,0 +1,59 @@
+import { request } from './request'
+import { toArray, toNumber, toPageQuery } from './mappers'
+import { toOrderBrief } from './order'
+
+const toTicketProduct = (backendProduct = {}) => ({
+ id: backendProduct.id ?? null,
+ unit_price: backendProduct.unit_price || '0.00',
+ is_active: backendProduct.is_active === true,
+ created_at: backendProduct.created_at || '',
+ updated_at: backendProduct.updated_at || ''
+})
+
+const toTicketRecord = (backendRecord = {}) => ({
+ id: backendRecord.id ?? null,
+ order_id: backendRecord.order_id ?? null,
+ order_no: backendRecord.order_no || '',
+ order_status: backendRecord.order_status || '',
+ status: backendRecord.status || '',
+ status_label: backendRecord.status_label || backendRecord.status || '',
+ checked_in_at: backendRecord.checked_in_at || null,
+ cancelled_at: backendRecord.cancelled_at || null,
+ cancel_reason: backendRecord.cancel_reason || '',
+ created_at: backendRecord.created_at || ''
+})
+
+const toTicketAccount = (backendAccount = {}) => ({
+ remaining_count: toNumber(backendAccount.remaining_count),
+ records: toArray(backendAccount.records).map(toTicketRecord),
+ total: toNumber(backendAccount.total),
+ page: toNumber(backendAccount.page, 1),
+ page_size: toNumber(backendAccount.page_size, 20)
+})
+
+const toTicketOrder = (backendData = {}) => ({
+ order: toOrderBrief(backendData.order),
+ product: toTicketProduct(backendData.product),
+ quantity: toNumber(backendData.quantity)
+})
+
+export const ticketApi = {
+ getTicketProduct() {
+ return request({
+ url: '/api/tickets/product'
+ }).then(toTicketProduct)
+ },
+ getTicketAccount(params = {}) {
+ return request({
+ url: '/api/tickets/account',
+ data: toPageQuery(params)
+ }).then(toTicketAccount)
+ },
+ createTicketOrder(quantity) {
+ return request({
+ url: '/api/tickets/orders',
+ method: 'POST',
+ data: { quantity }
+ }).then(toTicketOrder)
+ }
+}
diff --git a/miniprogram/src/modules/comprehensive/api/venue.js b/miniprogram/src/modules/comprehensive/api/venue.js
new file mode 100644
index 0000000..6e2fb42
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/api/venue.js
@@ -0,0 +1,19 @@
+import { request } from './request'
+import { toArray } from './mappers'
+
+const toCoach = (backendCoach = {}) => ({
+ id: backendCoach.id ?? null,
+ name: backendCoach.name || '',
+ phone: backendCoach.phone || '',
+ is_active: backendCoach.is_active === true
+})
+
+export const venueApi = {
+ getCoaches() {
+ return request({
+ url: '/api/venues/coaches'
+ }).then((data = {}) => ({
+ items: toArray(data.items).map(toCoach)
+ }))
+ }
+}
diff --git a/miniprogram/src/modules/comprehensive/stores/session.js b/miniprogram/src/modules/comprehensive/stores/session.js
new file mode 100644
index 0000000..8298c8d
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/stores/session.js
@@ -0,0 +1,58 @@
+import { defineStore } from 'pinia'
+import { authApi, authStorage } from '@/modules/comprehensive/api/auth'
+import { memberApi } from '@/modules/comprehensive/api/member'
+
+export const useComprehensiveSessionStore = defineStore('comprehensiveVenueSession', {
+ state: () => ({
+ isReady: false,
+ isLoggedIn: false,
+ member: null
+ }),
+ actions: {
+ async init() {
+ this.isLoggedIn = authStorage.isLoggedIn()
+ if (this.isLoggedIn) {
+ try {
+ this.member = await memberApi.getMemberMe()
+ } catch (error) {
+ if ([40001, 40002, 401, 403].includes(error.code) || [401, 403].includes(error.statusCode)) {
+ authStorage.clear()
+ this.member = null
+ this.isLoggedIn = false
+ }
+ }
+ }
+ this.isReady = true
+ },
+ async login() {
+ const loginResult = await loginByWechat()
+ const data = await authApi.wechatLogin({ code: loginResult.code })
+ this.member = data.member
+ this.isLoggedIn = true
+ },
+ logout() {
+ authApi.logout()
+ this.member = null
+ this.isLoggedIn = false
+ },
+ async refreshMember() {
+ if (!authStorage.isLoggedIn()) return
+ this.member = await memberApi.getMemberMe()
+ this.isLoggedIn = true
+ }
+ }
+})
+
+const loginByWechat = () => new Promise((resolve, reject) => {
+ uni.login({
+ provider: 'weixin',
+ success: (res) => {
+ if (!res.code) {
+ reject(new Error('微信登录未返回 code'))
+ return
+ }
+ resolve(res)
+ },
+ fail: reject
+ })
+})
diff --git a/miniprogram/src/modules/comprehensive/utils/date.js b/miniprogram/src/modules/comprehensive/utils/date.js
new file mode 100644
index 0000000..ecd1ae2
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/utils/date.js
@@ -0,0 +1,39 @@
+import dayjs from 'dayjs'
+
+export const formatDate = (value, fallback = '-') => {
+ if (!value) return fallback
+ return dayjs(value).format('YYYY-MM-DD')
+}
+
+export const formatDateTime = (value, fallback = '-') => {
+ if (!value) return fallback
+ return dayjs(value).format('MM-DD HH:mm')
+}
+
+export const formatTime = (value, fallback = '-') => {
+ if (!value) return fallback
+ return dayjs(value).format('HH:mm')
+}
+
+export const formatTimeRange = (startAt, endAt) => {
+ if (!startAt || !endAt) return '-'
+ const start = dayjs(startAt)
+ const end = dayjs(endAt)
+ if (start.format('YYYY-MM-DD') === end.format('YYYY-MM-DD')) {
+ return `${start.format('MM-DD HH:mm')} - ${end.format('HH:mm')}`
+ }
+ return `${start.format('MM-DD HH:mm')} - ${end.format('MM-DD HH:mm')}`
+}
+
+export const formatDuration = (startAt, endAt) => {
+ if (!startAt || !endAt) return '-'
+ const minutes = dayjs(endAt).diff(dayjs(startAt), 'minute')
+ if (minutes <= 0) return '-'
+ const hours = Math.floor(minutes / 60)
+ const rest = minutes % 60
+ if (hours && rest) return `${hours}小时${rest}分钟`
+ if (hours) return `${hours}小时`
+ return `${rest}分钟`
+}
+
+export const todayString = () => dayjs().format('YYYY-MM-DD')
diff --git a/miniprogram/src/modules/comprehensive/utils/money.js b/miniprogram/src/modules/comprehensive/utils/money.js
new file mode 100644
index 0000000..5ba0958
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/utils/money.js
@@ -0,0 +1,10 @@
+export const toAmountNumber = (value) => {
+ const amount = Number(value)
+ return Number.isFinite(amount) ? amount : 0
+}
+
+export const formatAmount = (value) => toAmountNumber(value).toFixed(2)
+
+export const multiplyAmount = (unitPrice, quantity) => {
+ return formatAmount(toAmountNumber(unitPrice) * Number(quantity || 0))
+}
diff --git a/miniprogram/src/modules/comprehensive/utils/status.js b/miniprogram/src/modules/comprehensive/utils/status.js
new file mode 100644
index 0000000..9d55109
--- /dev/null
+++ b/miniprogram/src/modules/comprehensive/utils/status.js
@@ -0,0 +1,14 @@
+export const statusBadgeClass = (status) => {
+ if (['paid', 'booked', 'available', 'success', 'published'].includes(status)) return 'success'
+ if (['pending_pay', 'pending'].includes(status)) return 'warning'
+ if (['cancelled', 'closed', 'expired', 'refunded'].includes(status)) return 'danger'
+ return 'muted'
+}
+
+export const courtTypeLabel = (type) => {
+ const map = {
+ badminton: '羽毛球',
+ basketball: '篮球'
+ }
+ return map[type] || type
+}
diff --git a/miniprogram/src/pages.json b/miniprogram/src/pages.json
index 1abfe94..80a18e6 100644
--- a/miniprogram/src/pages.json
+++ b/miniprogram/src/pages.json
@@ -15,6 +15,127 @@
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTextStyle": "black"
}
+ },
+ {
+ "path": "pages/comprehensive/home/index",
+ "style": {
+ "navigationBarTitleText": "首页",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/comprehensive/courses/index",
+ "style": {
+ "navigationBarTitleText": "课程",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/comprehensive/bookings/index",
+ "style": {
+ "navigationBarTitleText": "我的预约",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/comprehensive/profile/index",
+ "style": {
+ "navigationBarTitleText": "会员中心"
+ }
+ },
+ {
+ "path": "pages/comprehensive/activities/index",
+ "style": {
+ "navigationBarTitleText": "活动公告",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/comprehensive/activities/detail",
+ "style": {
+ "navigationBarTitleText": "活动详情"
+ }
+ },
+ {
+ "path": "pages/comprehensive/courses/detail",
+ "style": {
+ "navigationBarTitleText": "课程详情"
+ }
+ },
+ {
+ "path": "pages/comprehensive/courses/confirm",
+ "style": {
+ "navigationBarTitleText": "确认课程预约"
+ }
+ },
+ {
+ "path": "pages/comprehensive/private/index",
+ "style": {
+ "navigationBarTitleText": "包场预约"
+ }
+ },
+ {
+ "path": "pages/comprehensive/private/confirm",
+ "style": {
+ "navigationBarTitleText": "确认包场"
+ }
+ },
+ {
+ "path": "pages/comprehensive/tickets/index",
+ "style": {
+ "navigationBarTitleText": "篮球门票"
+ }
+ },
+ {
+ "path": "pages/comprehensive/pay/index",
+ "style": {
+ "navigationBarTitleText": "订单支付"
+ }
+ },
+ {
+ "path": "pages/comprehensive/orders/detail",
+ "style": {
+ "navigationBarTitleText": "订单详情"
+ }
+ },
+ {
+ "path": "pages/comprehensive/bookings/course-detail",
+ "style": {
+ "navigationBarTitleText": "课程预约详情"
+ }
+ },
+ {
+ "path": "pages/comprehensive/bookings/private-detail",
+ "style": {
+ "navigationBarTitleText": "包场预约详情"
+ }
+ },
+ {
+ "path": "pages/comprehensive/profile/balance",
+ "style": {
+ "navigationBarTitleText": "余额流水",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/comprehensive/profile/tickets",
+ "style": {
+ "navigationBarTitleText": "我的门票",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/comprehensive/profile/orders",
+ "style": {
+ "navigationBarTitleText": "我的订单",
+ "enablePullDownRefresh": true
+ }
+ },
+ {
+ "path": "pages/comprehensive/profile/detail",
+ "style": {
+ "navigationBarTitleText": "个人信息"
+ }
}
],
"globalStyle": {
diff --git a/miniprogram/src/pages/comprehensive/activities/detail.vue b/miniprogram/src/pages/comprehensive/activities/detail.vue
new file mode 100644
index 0000000..ad71e21
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/activities/detail.vue
@@ -0,0 +1,57 @@
+
+
+ {{ article.title }}
+ {{ formatDate(article.published_at || article.created_at) }}
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/activities/index.vue b/miniprogram/src/pages/comprehensive/activities/index.vue
new file mode 100644
index 0000000..d086896
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/activities/index.vue
@@ -0,0 +1,127 @@
+
+
+
+ 活动公告
+ 场馆运营通知、课程活动与权益说明
+
+
+
+
+
+
+ {{ article.title }}
+ {{ formatDate(article.published_at || article.created_at) }}
+
+
+
+
+ 加载中...
+ 暂无活动公告
+ 已加载全部
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/bookings/course-detail.vue b/miniprogram/src/pages/comprehensive/bookings/course-detail.vue
new file mode 100644
index 0000000..b2ba88a
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/bookings/course-detail.vue
@@ -0,0 +1,245 @@
+
+
+
+ {{ booking.status_label }}
+ {{ statusDesc }}
+
+
+
+
+
+ {{ booking.course.title }}
+ {{ formatTimeRange(booking.course.start_at, booking.course.end_at) }}
+ {{ booking.course.coach_name }} · {{ booking.course.courts.map(item => item.name).join('、') }}
+
+
+
+
+ 订单信息
+
+ 订单号
+ {{ booking.order.order_no }}
+
+
+ 订单金额
+ ¥{{ booking.order.amount }}
+
+
+ 订单状态
+ {{ booking.order.status_label }}
+
+
+
+
+ 预约时间线
+
+
+
+ 创建预约
+ {{ formatDateTime(booking.created_at) }}
+
+
+
+
+
+ 支付截止
+ {{ formatDateTime(booking.expire_at) }}
+
+
+
+
+
+ 取消截止
+ {{ formatDateTime(booking.cancel_deadline_at) }}
+
+
+
+
+
+ 已取消
+ {{ formatDateTime(booking.cancelled_at) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/bookings/index.vue b/miniprogram/src/pages/comprehensive/bookings/index.vue
new file mode 100644
index 0000000..2975396
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/bookings/index.vue
@@ -0,0 +1,240 @@
+
+
+
+ 星河综合运动中心
+ 登录后查看课程预约、包场预约与篮球门票权益。
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+ {{ getItemTitle(item) }}
+
+ {{ getItemTime(item) }}
+ {{ getItemDesc(item) }}
+
+ 订单金额
+ ¥{{ item.order?.amount || '--' }}
+
+
+ 暂无已预约记录
+ 加载中...
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/bookings/private-detail.vue b/miniprogram/src/pages/comprehensive/bookings/private-detail.vue
new file mode 100644
index 0000000..ec9452d
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/bookings/private-detail.vue
@@ -0,0 +1,201 @@
+
+
+
+ {{ booking.status_label }}
+ {{ statusDesc }}
+ {{ booking.status_label }}
+
+
+
+ 包场信息
+
+ 预约场地
+ {{ booking.courts.map(item => item.name).join('、') }}
+
+
+ 预约时间
+ {{ formatTimeRange(booking.start_at, booking.end_at) }}
+
+
+ 预约时长
+ {{ formatDuration(booking.start_at, booking.end_at) }}
+
+
+
+
+ 订单信息
+
+ 订单号
+ {{ booking.order.order_no }}
+
+
+ 订单金额
+ ¥{{ booking.order.amount }}
+
+
+ 订单状态
+ {{ booking.order.status_label }}
+
+
+
+
+ 预约时间线
+
+
+
+ 创建包场预约
+ {{ formatDateTime(booking.created_at) }}
+
+
+
+
+
+ 支付截止
+ {{ formatDateTime(booking.expire_at) }}
+
+
+
+
+
+ 取消记录
+ {{ booking.cancel_reason || '已取消' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/courses/confirm.vue b/miniprogram/src/pages/comprehensive/courses/confirm.vue
new file mode 100644
index 0000000..3d692a4
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/courses/confirm.vue
@@ -0,0 +1,113 @@
+
+
+
+ {{ course.title }}
+
+ 上课时间
+ {{ formatTimeRange(course.start_at, course.end_at) }}
+
+
+ 授课教练
+ {{ course.coach_name }}
+
+
+ 上课场地
+ {{ course.courts.map(item => item.name).join('、') }}
+
+
+ 课程价格
+ ¥{{ course.price }}
+
+
+
+
+
+ 应付金额
+ ¥{{ course.price }}
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/courses/detail.vue b/miniprogram/src/pages/comprehensive/courses/detail.vue
new file mode 100644
index 0000000..4812ac8
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/courses/detail.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+ {{ course.title }}
+
+
+ ¥{{ course.price }}
+
+
+
+
+
+ {{ item.label }}
+ {{ item.value }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/courses/index.vue b/miniprogram/src/pages/comprehensive/courses/index.vue
new file mode 100644
index 0000000..f9e8311
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/courses/index.vue
@@ -0,0 +1,235 @@
+
+
+
+
+
+ 日期
+ {{ selectedDate || '全部日期' }}
+
+
+
+
+ 教练
+ {{ selectedCoachName }}
+
+
+
+
+ 类型
+ {{ selectedCourtTypeLabel }}
+
+
+
+
+
+
+
+
+
+ {{ course.title }}
+
+ {{ formatTimeRange(course.start_at, course.end_at) }}
+ {{ course.coach_name }}
+
+ 已约 {{ course.booked_count }}/{{ course.capacity }}
+ ¥{{ course.price }}
+
+
+
+
+ 加载中...
+ 当前筛选条件下暂无课程
+ 已加载全部
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/home/index.vue b/miniprogram/src/pages/comprehensive/home/index.vue
new file mode 100644
index 0000000..3ddfe91
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/home/index.vue
@@ -0,0 +1,295 @@
+
+
+
+
+
+
+
+
+ {{ banner.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ course.title }}
+
+ 时间:{{ formatTimeRange(course.start_at, course.end_at) }}
+
+ • 教练:{{ course.coach_name }}
+ • 人数:已约 {{ course.booked_count }}/{{ course.capacity }}人
+
+
+
+
+ 今日暂无课程排期
+
+
+
+
+
+
+
+
+
+ {{ article.title }}
+ {{ formatDate(article.published_at || article.created_at) }}
+
+
+
+ 暂无公告活动
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/orders/detail.vue b/miniprogram/src/pages/comprehensive/orders/detail.vue
new file mode 100644
index 0000000..a5b8924
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/orders/detail.vue
@@ -0,0 +1,174 @@
+
+
+
+ ¥{{ order.amount }}
+ {{ order.status_label }}
+
+
+
+ 订单信息
+
+ 订单号
+ {{ order.order_no }}
+
+
+ 创建时间
+ {{ formatDateTime(order.created_at) }}
+
+
+ 更新时间
+ {{ formatDateTime(order.updated_at) }}
+
+
+ 支付过期
+ {{ formatDateTime(order.expire_at) }}
+
+
+
+
+ 支付记录
+ 暂无支付记录
+
+
+ {{ payment.method_label || payment.method }}
+ {{ formatDateTime(payment.paid_at || payment.created_at) }}
+
+ ¥{{ payment.amount }}
+
+
+
+
+ 退款记录
+ 暂无退款记录
+
+
+ {{ refund.reason }}
+ {{ formatDateTime(refund.refunded_at) }}
+
+ ¥{{ refund.amount }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/pay/index.vue b/miniprogram/src/pages/comprehensive/pay/index.vue
new file mode 100644
index 0000000..c4bd567
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/pay/index.vue
@@ -0,0 +1,299 @@
+
+
+
+ {{ order.business_type_label }}
+ ¥{{ order.amount }}
+ {{ paymentCountdownText }}
+
+
+
+
+ 订单号
+ {{ order.order_no }}
+
+
+ 创建时间
+ {{ formatDateTime(order.created_at) }}
+
+
+ 订单状态
+ {{ order.status_label }}
+
+
+
+
+ 支付方式
+
+
+ 余额支付
+ 当前余额 ¥{{ balance.balance }}
+
+ {{ payMethod === 'balance' ? '已选' : '' }}
+
+
+
+ 微信支付
+ 使用微信支付完成订单
+
+ {{ payMethod === 'wechat' ? '已选' : '' }}
+
+
+
+
+
+
+
+ 待支付
+ ¥{{ order.amount }}
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/private/confirm.vue b/miniprogram/src/pages/comprehensive/private/confirm.vue
new file mode 100644
index 0000000..0f01bf5
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/private/confirm.vue
@@ -0,0 +1,127 @@
+
+
+
+ {{ courtTypeLabel(payload.court_type) }}包场
+
+ 预约场地
+ {{ quote.courts.map(item => item.name).join('、') }}
+
+
+ 预约时间
+ {{ formatTimeRange(quote.start_at, quote.end_at) }}
+
+
+ 预约时长
+ {{ formatDuration(quote.start_at, quote.end_at) }}
+
+
+
+
+ 费用明细
+
+ 半小时段
+ {{ quote.unit_count }} 段
+
+
+ 场地数量
+ {{ quote.courts.length }} 个
+
+
+ 合计金额
+ ¥{{ quote.amount }}
+
+
+
+
+
+ 应付金额
+ ¥{{ quote.amount }}
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/private/index.vue b/miniprogram/src/pages/comprehensive/private/index.vue
new file mode 100644
index 0000000..1ba8bcd
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/private/index.vue
@@ -0,0 +1,478 @@
+
+
+
+
+
+ {{ item.label }}
+
+
+
+ {{ businessHoursText }}
+ {{ options ? `¥${options.unit_price}/半小时` : '加载价格中' }}
+
+
+
+
+
+
+
+ {{ item.week }}
+ {{ item.day }}
+
+
+
+
+
+
+
+
+
+
+ {{ durationText }}
+ {{ options ? `单场 ¥${options.amount_per_court}` : '按选择时长试算' }}
+
+
+
+
+
+
+
+
+ 正在加载可预约时段
+ {{ loadError }}
+ 当前日期暂无可预约时段
+
+
+ {{ formatTime(item.start_at) }}
+ {{ item.available_court_count ? `余 ${item.available_court_count} 场` : '已满' }}
+
+
+
+
+
+
+
+ 请先选择开始时间
+ 该时段无可预约场地
+
+
+ {{ court.name }}
+
+
+
+
+
+
+ {{ selectionTitle }}
+ {{ selectionSubText }}
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/profile/balance.vue b/miniprogram/src/pages/comprehensive/profile/balance.vue
new file mode 100644
index 0000000..e3d6a50
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/profile/balance.vue
@@ -0,0 +1,105 @@
+
+
+
+ 当前余额
+ ¥{{ balance.balance }}
+
+
+
+
+
+ {{ record.kind_label }}
+ {{ record.amount }}
+
+
+ {{ formatDateTime(record.created_at) }}
+
+
+ 暂无余额流水
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/profile/detail.vue b/miniprogram/src/pages/comprehensive/profile/detail.vue
new file mode 100644
index 0000000..4d35938
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/profile/detail.vue
@@ -0,0 +1,233 @@
+
+
+
+
+
+
+
+ 昵称
+
+
+
+
+
+
+ 手机号
+ {{ form.phone || '未绑定' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/profile/index.vue b/miniprogram/src/pages/comprehensive/profile/index.vue
new file mode 100644
index 0000000..530b941
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/profile/index.vue
@@ -0,0 +1,453 @@
+
+
+
+ 星河综合运动中心
+ 登录后查看余额、订单、门票权益与个人资料。
+
+
+
+
+
+
+
+
+ {{ avatarInitial }}
+
+
+ {{ session.member?.nickname }}
+
+ {{ session.member.phone }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/profile/orders.vue b/miniprogram/src/pages/comprehensive/profile/orders.vue
new file mode 100644
index 0000000..ec9c071
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/profile/orders.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
+
+ {{ order.business_type_label }}
+ {{ order.status_label }}
+
+ {{ order.order_no }}
+
+ {{ formatDateTime(order.created_at) }}
+ ¥{{ order.amount }}
+
+
+
+
+
+
+
+ 加载中...
+ 暂无订单
+ 已加载全部
+
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/profile/tickets.vue b/miniprogram/src/pages/comprehensive/profile/tickets.vue
new file mode 100644
index 0000000..b853fbd
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/profile/tickets.vue
@@ -0,0 +1,97 @@
+
+
+
+ 当前可核销次数
+ {{ account.remaining_count }} 次
+
+
+
+
+
+
+ {{ record.order_no }}
+ {{ record.status_label }}
+
+ {{ record.checked_in_at ? `核销时间 ${formatDateTime(record.checked_in_at)}` : `生成时间 ${formatDateTime(record.created_at)}` }}
+ 订单状态:{{ record.order_status }}
+
+
+ 暂无门票权益
+
+
+
+
+
+
diff --git a/miniprogram/src/pages/comprehensive/tickets/index.vue b/miniprogram/src/pages/comprehensive/tickets/index.vue
new file mode 100644
index 0000000..f7b583f
--- /dev/null
+++ b/miniprogram/src/pages/comprehensive/tickets/index.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+ 篮球门票
+ 购买后进入会员权益账户,到馆由前台核销。
+
+
+
+ ¥{{ product.unit_price }}
+ / 次
+
+
+
+
+ 购买次数
+
+
+ {{ quantity }}
+
+
+
+ 合计金额
+ ¥{{ totalAmount }}
+
+
+
+
+
+ 合计
+ ¥{{ totalAmount }}
+
+
+
+
+
+
+
+
+
diff --git a/miniprogram/src/static/comprehensive/tab/booking-active.png b/miniprogram/src/static/comprehensive/tab/booking-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..eacef651076a2984dbffe9fbf38008adb3f47ea8
GIT binary patch
literal 433
zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V65o$NZuCFd`k^8cs1pbh(zZW2czyl*`PJ*Ep6|c?
z;`r^yx`hG%I23#s5}7!J9cC~bViCBu=lk7PN2@P>vUc3e+#B%2MZkC30>SS4ulB5F
zH^}-JJ-7ZUgPVR`_v;I+t)bir({}Iw_H~c`!FNe-3#@m2?M>$7>ORwO#d(5I)r(C>
zdBCQw`ypu%-TjNp7{wy7yFITbp54*Ck)@RrEV1Jg^GxNrC$#5YzWLbKmid~s_0sd(
z+SB{ZOBr4_FOt!{1r(M&uxqo#%J07N2SOp5I9{tYY}%~vzc>FE!#9z~85_^Z0a@F+
zKIH&e?}efTpFVl?CFqS}+JURc{`_N8drk5}@%y`%zeY42TE%8?T1P{4{o-Fd61FWt
vKmW|uaa`OMAT?)u{h3)W+880h7xC{mtK^gRmj<6Yyg(wJu6{1-oD!M<&rH0(
literal 0
HcmV?d00001
diff --git a/miniprogram/src/static/comprehensive/tab/booking.png b/miniprogram/src/static/comprehensive/tab/booking.png
new file mode 100644
index 0000000000000000000000000000000000000000..51593c630d29c43cf48207cd338ffc37ba524f62
GIT binary patch
literal 429
zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U@Y-;aSW-L^Y)J8q$UFa)_^&)
zUKcS>zo)=nHZ6Gb1V+`Lo)!74MHkq2=5B})18M_;88>w+KYaaHv9D^a&AaUv`?ntt
zf3)C_ki!gyLo5Pn4F-%YoC<3y-|xQK{e98X@}@L)sf8ap6=vx;DvRyES{crtaOKf<
ztN*JQd*c3xU0=w}wOZ_ePI>*?*FWQ$?jF7Mu&m^@^l>2(v6+l3+Z9y5TuAMf1e+T3
z(Kun7*jM2+6pM6rNnTf;T_~Q+$t4VyD0s?l*XNR!+%vCb2v>!=cQ(VRbNg(*?t966Be-YrQLE}bK$h02-5{2C
zj_Z`k-Iu*Lw$ES;SB7ZtVMt`+`26`xWV*xoz2*Mbf%dPKI-s+8LCBuISH=ln1iYSp
t&fmn+E9I0~aPOaC_9AIepaVg|&)4h>F6}EjoK%EBd{0+Dmvv4FO#oY0uFn7f
literal 0
HcmV?d00001
diff --git a/miniprogram/src/static/comprehensive/tab/course-active.png b/miniprogram/src/static/comprehensive/tab/course-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..1f57376e69b5ba2448ed34849c3a1fbeebe6771d
GIT binary patch
literal 350
zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U{v*VaSW-L^Y+fcz#|3%435qp
zi*s0qah<)<+igDRllXY4vLBhD}WTu
zVAz;@?84J*4c71g5D%<$-mmNzRtZ~5nc&*g9CBtGmdF|y*)<7&IHmr;?vK5Y`<#Z$
z>!*Ev@O5`nk}{*8Eo*kd-DLF#TR5^a_AU;;pecCmfz@C0F9*a~ApU*uYrg$;Dd!i{
S_?Lu(L_A&nT-G@yGywpeNQi9!
literal 0
HcmV?d00001
diff --git a/miniprogram/src/static/comprehensive/tab/course.png b/miniprogram/src/static/comprehensive/tab/course.png
new file mode 100644
index 0000000000000000000000000000000000000000..21f7bda4fa2dc9f651c791a03912e97fedc884d2
GIT binary patch
literal 347
zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>U{vySaSW-L^Y+fc+`|R}434TE
zyI&pfFF(*US?tqC-OTeQ3QgKV?6xJbKrJA!XZ8A52fsi5S)H3^EC2Yn;lBg2=KFX)
z*78~~ARbuhyL4>+$#oo8+d
O67h8Pb6Mw<&;$Ux=7^dA
literal 0
HcmV?d00001
diff --git a/miniprogram/src/static/comprehensive/tab/home-active.png b/miniprogram/src/static/comprehensive/tab/home-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..118a46045a3fbacffaffb7e7e01e08ce02e54450
GIT binary patch
literal 496
zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VBGHM;uumf=j~0$yh8>8t%>Td
z^3Gj5{6$4(S^DQ$tGc~JxgJQUzvumDzj8NMTcwZNH!^De
z{_^==d2Ld_oyf0}%N1i8J}x?rgyGDk}ZQgzpwM`y%euo-SNpI(P5SSH+2i8Fd`_i)~ALVk{1;FIZ|Z
z{n`C}|L@(pJeP6%(e{IfH`s1nYRMc`XHb~+FU;1~{>P14-oM(no^B{z^Z!G&!?kDo
z&${yT*G@SZ7CU3EtxtmlV}~Fr5gB(RZl_Ob^_PPz=u%*nC-Zvq?0Hw^U4&>pu!eDm
z?bbDms+I@`_&Nn#Q@r1_%7W$VZ)RAaGcYt1^~gUIer*xOw4e$k>gnp|vd$@?2>=}G
B)g}M{
literal 0
HcmV?d00001
diff --git a/miniprogram/src/static/comprehensive/tab/home.png b/miniprogram/src/static/comprehensive/tab/home.png
new file mode 100644
index 0000000000000000000000000000000000000000..711ddfd16bf6df2903e09c4485805ba8796871d5
GIT binary patch
literal 493
zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>VBF&A;uumf=k3kFyh8>8Es5%{
zO24z&6fw*1=xFy6<$55YZruIf|5Y^W>Avg77K#8hLcxJ)J7VhJ*Q;(^sv;`gyZ*ZO
zuJfCxwLChx>bwQ-OQsHU-*aM4vnKZT9ZS10Dagm6WZf&C%L(!hH8)(o3#?)8a6DVg
zcd)F#r)}Ba`;Wi>+s}QPv!gFTr9_v4?$Mnl
z|3Ciyer?`6o(dg19=W!+D|OFGUij498T@(stCv3y9)FkpBL1RY``YBc?f02Cnf=e!
zOR9Lg+-KXXLvP+BG9BS?=wU!1&U{k(b~07#_F{fDbSbb(pR(+d-lR-%qiSqlzo;%fkX4NKZ+=!?0)5s~uGhM?QjtJzf1=);T3K0RUCT
B+YA5z
literal 0
HcmV?d00001
diff --git a/miniprogram/src/static/comprehensive/tab/mine-active.png b/miniprogram/src/static/comprehensive/tab/mine-active.png
new file mode 100644
index 0000000000000000000000000000000000000000..5a19691c95474d1d04a0d5d32840a44fdb280783
GIT binary patch
literal 469
zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V4UOW;uumf=j|=Wyk-Ldw?v1#
zXKz-`n75?;<;^{LM;8T&cXD-o+++Cuwj1Z+Kh4PkrsX)=JtGh%*=EvVZZy=>>9
z&;X@|nUW4h%pPeBC%sq%gzfu|v0Pw#AE9Bb;={0a`OZUMrgJ^;30`ed-@b$A|EEk1
zhZu{4Q?~AZB4nbR!ti#H=2BmKvj**1t~vksB16vqXUTe8zp%SodXIREwier)hn|TE
zT%9em)tv-hva&o_XKLK>_TAa@>ymd@f8P8$QhRQW^~R7M3%8RD=ggLsW_TPkoX99}
z?=Fu6ujYToDZ=^e_4QZk4^5b|QvWpD0=*-LcTC&DuYRW^_h#_5NwYuP|F}M`D`zopr
E05n3*^8f$<
literal 0
HcmV?d00001
diff --git a/miniprogram/src/static/comprehensive/tab/mine.png b/miniprogram/src/static/comprehensive/tab/mine.png
new file mode 100644
index 0000000000000000000000000000000000000000..4f3c12568894de14c61eda79aa61f08dba0bd663
GIT binary patch
literal 466
zcmeAS@N?(olHy`uVBq!ia0vp^2_VeD1|%QND7Ro>V4Ugc;uumf=j|=Wyk-Ldw?v1%
z@9rp^zH@**@93f+@m{8`k9!Q?-*)3X{HHlt;IxwGBo!!{b#<4BNdNQitIyy19QbO^
zHOX0VmOZ&MxBQ%6ldtln#pO6#(0aBvq5^Z5Pp>jnVD9Li<;ue-d0_5?Yey|Cmi}M+
za(<0D_p%9#d2KH3iQ8}Nv{H42Y>w2@ox!XZ{LHp`am-*y`oPz^N%;^S9=ovmqX8}IJ@d9yU9Jio5YHhbyhT=i*Pg2y!^58OPqg^%~7g_;q=q203?
zS13NUZzy{7_QCu9U;Yx4jJ~GcWVq5=A#r`uw}&m#8+EHsMHLC(ll}i{sm52Xj0Vm#
z>mFxcIe9T}j~1^-c(uS41E#3yupotp?WAef>lg}W@BLnx%T@%8D+W(jKbLh*2~7a-
C|IAVV
literal 0
HcmV?d00001