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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 @@ + + + + + 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 0000000..eacef65 Binary files /dev/null and b/miniprogram/src/static/comprehensive/tab/booking-active.png differ diff --git a/miniprogram/src/static/comprehensive/tab/booking.png b/miniprogram/src/static/comprehensive/tab/booking.png new file mode 100644 index 0000000..51593c6 Binary files /dev/null and b/miniprogram/src/static/comprehensive/tab/booking.png differ 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 0000000..1f57376 Binary files /dev/null and b/miniprogram/src/static/comprehensive/tab/course-active.png differ diff --git a/miniprogram/src/static/comprehensive/tab/course.png b/miniprogram/src/static/comprehensive/tab/course.png new file mode 100644 index 0000000..21f7bda Binary files /dev/null and b/miniprogram/src/static/comprehensive/tab/course.png differ 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 0000000..118a460 Binary files /dev/null and b/miniprogram/src/static/comprehensive/tab/home-active.png differ diff --git a/miniprogram/src/static/comprehensive/tab/home.png b/miniprogram/src/static/comprehensive/tab/home.png new file mode 100644 index 0000000..711ddfd Binary files /dev/null and b/miniprogram/src/static/comprehensive/tab/home.png differ 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 0000000..5a19691 Binary files /dev/null and b/miniprogram/src/static/comprehensive/tab/mine-active.png differ diff --git a/miniprogram/src/static/comprehensive/tab/mine.png b/miniprogram/src/static/comprehensive/tab/mine.png new file mode 100644 index 0000000..4f3c125 Binary files /dev/null and b/miniprogram/src/static/comprehensive/tab/mine.png differ