feat: migrate comprehensive venue module

This commit is contained in:
gqt
2026-06-08 20:54:25 +08:00
parent c9650b0db1
commit 8e1443f7e5
43 changed files with 5017 additions and 0 deletions
@@ -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 }
@@ -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)
}
}
@@ -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)
}
}
@@ -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 }
})
}
}
@@ -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)
})
@@ -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)
}
})
}
}
@@ -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)
}
}
@@ -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)
}
}
@@ -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)
}
})
})
}
@@ -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)
}
}
@@ -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)
}))
}
}