Compare commits
18 Commits
d00304ec9c
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e78101f690 | |||
| 5bd4d7dcea | |||
| 0b694ff6db | |||
| e4a05379ce | |||
| 7765d3dae1 | |||
| 3832a686d8 | |||
| 7dc0314565 | |||
| 30c2b5ffea | |||
| 2ca2ead6da | |||
| 00f1bcd0fa | |||
| 1a3137bd12 | |||
| 4872f180dc | |||
| 605d463a00 | |||
| d88ef1756f | |||
| 7139865bff | |||
| d07356e9f1 | |||
| 30cad4e765 | |||
| 37eb77aede |
@@ -58,7 +58,7 @@ defineProps({
|
|||||||
},
|
},
|
||||||
primaryText: {
|
primaryText: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '微信一键登录'
|
default: '点击登录'
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export const toPrivateBooking = (backendBooking = {}) => ({
|
|||||||
cancelled_at: backendBooking.cancelled_at || null,
|
cancelled_at: backendBooking.cancelled_at || null,
|
||||||
cancel_by: backendBooking.cancel_by || '',
|
cancel_by: backendBooking.cancel_by || '',
|
||||||
cancel_reason: backendBooking.cancel_reason || '',
|
cancel_reason: backendBooking.cancel_reason || '',
|
||||||
|
can_cancel: backendBooking.can_cancel === true,
|
||||||
created_at: backendBooking.created_at || '',
|
created_at: backendBooking.created_at || '',
|
||||||
order: toOrderBrief(backendBooking.order)
|
order: toOrderBrief(backendBooking.order)
|
||||||
})
|
})
|
||||||
@@ -94,5 +95,16 @@ export const bookingApi = {
|
|||||||
return request({
|
return request({
|
||||||
url: `/api/bookings/private/${id}`
|
url: `/api/bookings/private/${id}`
|
||||||
}).then(toPrivateBooking)
|
}).then(toPrivateBooking)
|
||||||
|
},
|
||||||
|
getPrivateBookingRefundQuote(id) {
|
||||||
|
return request({
|
||||||
|
url: `/api/bookings/private/${id}/refund-quote`
|
||||||
|
})
|
||||||
|
},
|
||||||
|
cancelPrivateBooking(id) {
|
||||||
|
return request({
|
||||||
|
url: `/api/bookings/private/${id}/cancel`,
|
||||||
|
method: 'POST'
|
||||||
|
}).then(toPrivateBooking)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { request } from './request'
|
||||||
|
import { toPageQuery, toPagination } from './mappers'
|
||||||
|
|
||||||
|
const toCourseBrief = (backendCourse = {}) => ({
|
||||||
|
id: backendCourse.id ?? null,
|
||||||
|
title: backendCourse.title || '',
|
||||||
|
cover_image: backendCourse.cover_image || '',
|
||||||
|
updated_at: backendCourse.updated_at || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const toCourseDetail = (backendCourse = {}) => ({
|
||||||
|
...toCourseBrief(backendCourse),
|
||||||
|
detail: backendCourse.detail || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
export const courseInfoApi = {
|
||||||
|
getCourses(params = {}) {
|
||||||
|
return request({
|
||||||
|
url: '/api/courses',
|
||||||
|
data: toPageQuery(params)
|
||||||
|
}).then((data) => toPagination(data, toCourseBrief))
|
||||||
|
},
|
||||||
|
getCourseDetail(id) {
|
||||||
|
return request({
|
||||||
|
url: `/api/courses/${id}`
|
||||||
|
}).then(toCourseDetail)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,10 +46,8 @@ const toPrivateBookingOptions = (backendData = {}) => ({
|
|||||||
court_type_label: backendData.court_type_label || '',
|
court_type_label: backendData.court_type_label || '',
|
||||||
open_time: backendData.open_time || '',
|
open_time: backendData.open_time || '',
|
||||||
close_time: backendData.close_time || '',
|
close_time: backendData.close_time || '',
|
||||||
unit_price: backendData.unit_price || '0.00',
|
duration_units: toNumber(backendData.duration_units, 1),
|
||||||
duration_units: toNumber(backendData.duration_units, 2),
|
|
||||||
duration_minutes: toNumber(backendData.duration_minutes, 60),
|
duration_minutes: toNumber(backendData.duration_minutes, 60),
|
||||||
amount_per_court: backendData.amount_per_court || '0.00',
|
|
||||||
total_court_count: toNumber(backendData.total_court_count),
|
total_court_count: toNumber(backendData.total_court_count),
|
||||||
time_options: toArray(backendData.time_options).map(toPrivateTimeOption)
|
time_options: toArray(backendData.time_options).map(toPrivateTimeOption)
|
||||||
})
|
})
|
||||||
@@ -67,7 +65,8 @@ const toGridSlot = (backendSlot = {}) => ({
|
|||||||
const toGridOccupation = (backendOcc = {}) => ({
|
const toGridOccupation = (backendOcc = {}) => ({
|
||||||
court_id: backendOcc.court_id ?? null,
|
court_id: backendOcc.court_id ?? null,
|
||||||
start_at: backendOcc.start_at || '',
|
start_at: backendOcc.start_at || '',
|
||||||
end_at: backendOcc.end_at || ''
|
end_at: backendOcc.end_at || '',
|
||||||
|
source_type: backendOcc.source_type || ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const toPrivateBookingGrid = (backendData = {}) => ({
|
const toPrivateBookingGrid = (backendData = {}) => ({
|
||||||
@@ -76,13 +75,24 @@ const toPrivateBookingGrid = (backendData = {}) => ({
|
|||||||
court_type_label: backendData.court_type_label || '',
|
court_type_label: backendData.court_type_label || '',
|
||||||
open_time: backendData.open_time || '',
|
open_time: backendData.open_time || '',
|
||||||
close_time: backendData.close_time || '',
|
close_time: backendData.close_time || '',
|
||||||
unit_price: backendData.unit_price || '0.00',
|
|
||||||
courts: toArray(backendData.courts).map(toGridCourt),
|
courts: toArray(backendData.courts).map(toGridCourt),
|
||||||
time_slots: toArray(backendData.time_slots).map(toGridSlot),
|
time_slots: toArray(backendData.time_slots).map(toGridSlot),
|
||||||
occupations: toArray(backendData.occupations).map(toGridOccupation)
|
occupations: toArray(backendData.occupations).map(toGridOccupation),
|
||||||
|
slot_prices: backendData.slot_prices || {}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toPrivateBookingConfig = (backendData = {}) => ({
|
||||||
|
member_advance_days: toNumber(backendData.member_advance_days, 7),
|
||||||
|
non_member_advance_days: toNumber(backendData.non_member_advance_days, 3)
|
||||||
})
|
})
|
||||||
|
|
||||||
export const privateBookingApi = {
|
export const privateBookingApi = {
|
||||||
|
getPrivateBookingConfig() {
|
||||||
|
return request({
|
||||||
|
url: '/api/bookings/private/config'
|
||||||
|
}).then(toPrivateBookingConfig)
|
||||||
|
},
|
||||||
|
|
||||||
getPrivateBookingOptions(params = {}) {
|
getPrivateBookingOptions(params = {}) {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/bookings/private/options',
|
url: '/api/bookings/private/options',
|
||||||
|
|||||||
@@ -86,10 +86,12 @@ export function request(options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const error = normalizeError(res, `请求错误 ${res.statusCode}`)
|
const error = normalizeError(res, `请求错误 ${res.statusCode}`)
|
||||||
|
if (!options.silent) {
|
||||||
uni.showToast({
|
uni.showToast({
|
||||||
title: error.message,
|
title: error.message,
|
||||||
icon: 'none'
|
icon: 'none'
|
||||||
})
|
})
|
||||||
|
}
|
||||||
reject(error)
|
reject(error)
|
||||||
},
|
},
|
||||||
fail: (err) => {
|
fail: (err) => {
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ const toTicketOrder = (backendData = {}) => ({
|
|||||||
export const ticketApi = {
|
export const ticketApi = {
|
||||||
getTicketProduct() {
|
getTicketProduct() {
|
||||||
return request({
|
return request({
|
||||||
url: '/api/tickets/product'
|
url: '/api/tickets/product',
|
||||||
|
silent: true
|
||||||
}).then(toTicketProduct)
|
}).then(toTicketProduct)
|
||||||
},
|
},
|
||||||
getTicketAccount(params = {}) {
|
getTicketAccount(params = {}) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const statusBadgeClass = (status) => {
|
export const statusBadgeClass = (status) => {
|
||||||
if (['paid', 'booked', 'available', 'success', 'published'].includes(status)) return 'success'
|
if (['paid', 'booked', 'available', 'success', 'published'].includes(status)) return 'success'
|
||||||
if (['pending_pay', 'pending'].includes(status)) return 'warning'
|
if (['pending_pay', 'pending', 'part_refunded'].includes(status)) return 'warning'
|
||||||
if (['cancelled', 'closed', 'expired', 'refunded'].includes(status)) return 'danger'
|
if (['cancelled', 'closed', 'expired', 'refunded'].includes(status)) return 'danger'
|
||||||
return 'muted'
|
return 'muted'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,11 @@ export const contentApi = {
|
|||||||
return request({
|
return request({
|
||||||
url: `/api/contents/activities/${id}`
|
url: `/api/contents/activities/${id}`
|
||||||
}).then(toActivityDetail)
|
}).then(toActivityDetail)
|
||||||
|
},
|
||||||
|
|
||||||
|
getPageImages() {
|
||||||
|
return request({
|
||||||
|
url: '/api/contents/page-images'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,8 @@
|
|||||||
{
|
{
|
||||||
"path": "pages/profile/index",
|
"path": "pages/profile/index",
|
||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "我的"
|
"navigationBarTitleText": "我的",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -94,6 +95,19 @@
|
|||||||
"navigationBarTitleText": "课程详情"
|
"navigationBarTitleText": "课程详情"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/comprehensive/courses/list",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "全部课程",
|
||||||
|
"enablePullDownRefresh": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/comprehensive/courses/info-detail",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "课程详情"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/comprehensive/courses/membership-detail",
|
"path": "pages/comprehensive/courses/membership-detail",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -311,10 +325,10 @@
|
|||||||
"selectedIconPath": "static/comprehensive/tab/home-active.png"
|
"selectedIconPath": "static/comprehensive/tab/home-active.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/private/index",
|
"pagePath": "pages/courses/index",
|
||||||
"text": "订场",
|
"text": "约课",
|
||||||
"iconPath": "static/comprehensive/tab/private.png",
|
"iconPath": "static/comprehensive/tab/course.png",
|
||||||
"selectedIconPath": "static/comprehensive/tab/private-active.png"
|
"selectedIconPath": "static/comprehensive/tab/course-active.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/mall/index",
|
"pagePath": "pages/mall/index",
|
||||||
@@ -323,10 +337,10 @@
|
|||||||
"selectedIconPath": "static/comprehensive/tab/mall-active.png"
|
"selectedIconPath": "static/comprehensive/tab/mall-active.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/courses/index",
|
"pagePath": "pages/private/index",
|
||||||
"text": "约课",
|
"text": "订场",
|
||||||
"iconPath": "static/comprehensive/tab/course.png",
|
"iconPath": "static/comprehensive/tab/private.png",
|
||||||
"selectedIconPath": "static/comprehensive/tab/course-active.png"
|
"selectedIconPath": "static/comprehensive/tab/private-active.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/profile/index",
|
"pagePath": "pages/profile/index",
|
||||||
|
|||||||
@@ -20,10 +20,6 @@
|
|||||||
<text>订单号</text>
|
<text>订单号</text>
|
||||||
<text>{{ booking.order.order_no }}</text>
|
<text>{{ booking.order.order_no }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="info-row">
|
|
||||||
<text>订单金额</text>
|
|
||||||
<text>¥{{ booking.order.amount }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="info-row">
|
<view class="info-row">
|
||||||
<text>订单状态</text>
|
<text>订单状态</text>
|
||||||
<text>{{ booking.order.status_label }}</text>
|
<text>{{ booking.order.status_label }}</text>
|
||||||
|
|||||||
@@ -63,8 +63,40 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="booking.status === 'pending_pay'" class="fixed-action-bar">
|
<view v-if="booking.status === 'pending_pay' || (booking.status === 'booked' && booking.can_cancel)" class="fixed-action-bar">
|
||||||
<button class="primary-button action-btn" @tap="goPay">去支付</button>
|
<button v-if="booking.status === 'booked' && booking.can_cancel" class="ghost-button action-btn" @tap="onCancelBooking">取消预订</button>
|
||||||
|
<button v-if="booking.status === 'pending_pay'" class="primary-button action-btn" @tap="goPay">去支付</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="refundPopupVisible" class="refund-popup-mask" @tap="closeRefundPopup">
|
||||||
|
<view class="refund-popup" @tap.stop>
|
||||||
|
<view class="refund-popup-header">
|
||||||
|
<text class="refund-popup-title">取消预订</text>
|
||||||
|
<text class="refund-popup-close" @tap="closeRefundPopup">×</text>
|
||||||
|
</view>
|
||||||
|
<view class="refund-popup-body" v-if="refundQuote">
|
||||||
|
<view class="refund-rules">
|
||||||
|
<view class="refund-rules-header">
|
||||||
|
<text>距开始时间</text>
|
||||||
|
<text>退款比例</text>
|
||||||
|
<text>说明</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="(rule, idx) in refundQuote.rules" :key="idx" class="refund-rule-row" :class="{ active: rule.description === refundQuote.applicable_tier_description }">
|
||||||
|
<text>{{ formatHoursBefore(rule.hours_before_start, idx) }}</text>
|
||||||
|
<text>{{ rule.refund_percentage }}%</text>
|
||||||
|
<text>{{ rule.description }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="refund-amount-box">
|
||||||
|
<text class="refund-amount-label">本次退款金额</text>
|
||||||
|
<text class="refund-amount-value">¥{{ refundQuote.refund_amount }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="refund-popup-footer">
|
||||||
|
<button class="ghost-button refund-btn" @tap="closeRefundPopup">再想想</button>
|
||||||
|
<button class="primary-button refund-btn" @tap="confirmCancelBooking" :disabled="refundLoading">确认取消</button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -94,6 +126,49 @@ onLoad(async (query) => {
|
|||||||
const goPay = () => {
|
const goPay = () => {
|
||||||
uni.navigateTo({ url: `/pages/comprehensive/pay/index?order_id=${booking.value.order_id}` })
|
uni.navigateTo({ url: `/pages/comprehensive/pay/index?order_id=${booking.value.order_id}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refundPopupVisible = ref(false)
|
||||||
|
const refundQuote = ref(null)
|
||||||
|
const refundLoading = ref(false)
|
||||||
|
|
||||||
|
const onCancelBooking = async () => {
|
||||||
|
try {
|
||||||
|
const data = await bookingApi.getPrivateBookingRefundQuote(bookingId.value)
|
||||||
|
refundQuote.value = data
|
||||||
|
refundPopupVisible.value = true
|
||||||
|
} catch {
|
||||||
|
uni.showToast({ title: '获取退款信息失败', icon: 'none' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeRefundPopup = () => {
|
||||||
|
refundPopupVisible.value = false
|
||||||
|
refundQuote.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmCancelBooking = async () => {
|
||||||
|
if (refundLoading.value) return
|
||||||
|
refundLoading.value = true
|
||||||
|
try {
|
||||||
|
await bookingApi.cancelPrivateBooking(bookingId.value)
|
||||||
|
refundPopupVisible.value = false
|
||||||
|
const amount = refundQuote.value?.refund_amount || '0'
|
||||||
|
uni.showToast({ title: `取消成功,退款¥${amount}`, icon: 'none' })
|
||||||
|
booking.value = await bookingApi.getPrivateBookingDetail(bookingId.value)
|
||||||
|
} catch {
|
||||||
|
uni.showToast({ title: '取消失败,请稍后重试', icon: 'none' })
|
||||||
|
} finally {
|
||||||
|
refundLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatHoursBefore = (hours, idx) => {
|
||||||
|
const h = Number(hours)
|
||||||
|
const rules = refundQuote.value?.rules || []
|
||||||
|
if (idx === 0 || h >= 24) return `>${h}小时`
|
||||||
|
const prev = Number(rules[idx - 1]?.hours_before_start ?? h)
|
||||||
|
return `${h}-${prev}小时`
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@@ -198,4 +273,131 @@ const goPay = () => {
|
|||||||
.action-btn {
|
.action-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.refund-popup-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 999;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-popup {
|
||||||
|
width: 100%;
|
||||||
|
background: $bg-color-soft;
|
||||||
|
border-radius: $border-radius-lg $border-radius-lg 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-popup-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: $spacing-md $spacing-lg;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1rpx solid $bg-color-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-popup-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $text-color-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-popup-close {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: $text-color-muted;
|
||||||
|
padding: 0 $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-popup-body {
|
||||||
|
padding: $spacing-md $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-rules {
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-rules-header {
|
||||||
|
display: flex;
|
||||||
|
padding: $spacing-sm $spacing-md;
|
||||||
|
background: $bg-color-hover;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $text-color-muted;
|
||||||
|
|
||||||
|
text {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:first-child { flex: 0.8; }
|
||||||
|
&:last-child { flex: 1.4; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-rule-row {
|
||||||
|
display: flex;
|
||||||
|
padding: $spacing-sm $spacing-md;
|
||||||
|
border-bottom: 1rpx solid $bg-color-hover;
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: $text-color-muted;
|
||||||
|
|
||||||
|
text {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
&:first-child { flex: 0.8; }
|
||||||
|
&:last-child { flex: 1.4; text-align: left; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: rgba(64, 158, 255, 0.08);
|
||||||
|
color: $color-primary;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-rule-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-amount-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: $spacing-md;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-amount-label {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: $text-color-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-amount-value {
|
||||||
|
font-size: 36rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-popup-footer {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-md;
|
||||||
|
padding: $spacing-md $spacing-lg $spacing-lg;
|
||||||
|
padding-bottom: calc(#{$spacing-lg} + env(safe-area-inset-bottom));
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refund-btn {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
<view class="courses-page">
|
<view class="courses-page">
|
||||||
<block>
|
<block>
|
||||||
<view class="tab-bar">
|
<view class="tab-bar">
|
||||||
<view class="tab-item" :class="{ active: activeTab === 'membership' }" @tap="switchTab('membership')">会员卡</view>
|
<view class="tab-item" :class="{ active: activeTab === 'membership' }" @tap="switchTab('membership')">课程</view>
|
||||||
<view class="tab-item" :class="{ active: activeTab === 'group' }" @tap="switchTab('group')">团课</view>
|
<view class="tab-item" :class="{ active: activeTab === 'tickets' }" @tap="switchTab('tickets')">篮球门票</view>
|
||||||
<view class="tab-item" :class="{ active: activeTab === 'tickets' }" @tap="switchTab('tickets')">门票</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<template v-if="activeTab === 'tickets'">
|
<template v-if="activeTab === 'tickets'">
|
||||||
@@ -13,7 +12,6 @@
|
|||||||
<view class="product-head">
|
<view class="product-head">
|
||||||
<view>
|
<view>
|
||||||
<text class="product-title">篮球门票</text>
|
<text class="product-title">篮球门票</text>
|
||||||
<text class="product-subtitle">购买后进入会员权益账户,到馆由前台核销。</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="price-block">
|
<view class="price-block">
|
||||||
@@ -54,6 +52,25 @@
|
|||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<view v-else-if="ticketPaused" class="ticket-section">
|
||||||
|
<view class="card ticket-paused-card">
|
||||||
|
<text class="ticket-paused-title">门票暂停购买</text>
|
||||||
|
<text class="ticket-paused-desc">门票暂未开放购买,请稍后再来</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="ticketRecords.length" class="ticket-records-section">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">我的门票</text>
|
||||||
|
</view>
|
||||||
|
<view v-for="record in ticketRecords" :key="record.id" class="card ticket-record-card">
|
||||||
|
<view class="ticket-record-top">
|
||||||
|
<text class="ticket-record-no">订单号 {{ record.order_no }}</text>
|
||||||
|
<text class="ticket-status-badge" :class="record.status">{{ record.status_label }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="ticket-record-date">{{ formatDate(record.created_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
<view v-else class="load-more-tip">加载中...</view>
|
<view v-else class="load-more-tip">加载中...</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -85,7 +102,6 @@
|
|||||||
<text class="meta">{{ item.coach_name }}</text>
|
<text class="meta">{{ item.coach_name }}</text>
|
||||||
<view class="course-bottom">
|
<view class="course-bottom">
|
||||||
<text class="meta">剩余 {{ item.remaining_count }}/{{ item.capacity }}</text>
|
<text class="meta">剩余 {{ item.remaining_count }}/{{ item.capacity }}</text>
|
||||||
<text class="course-price">¥{{ item.price }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -121,6 +137,7 @@ const isLoadingMore = ref(false)
|
|||||||
let loadRequestId = 0
|
let loadRequestId = 0
|
||||||
|
|
||||||
const ticketProduct = ref(null)
|
const ticketProduct = ref(null)
|
||||||
|
const ticketPaused = ref(false)
|
||||||
const ticketQty = ref(1)
|
const ticketQty = ref(1)
|
||||||
const ticketSubmitting = ref(false)
|
const ticketSubmitting = ref(false)
|
||||||
const ticketRecords = ref([])
|
const ticketRecords = ref([])
|
||||||
@@ -202,8 +219,11 @@ const loadTicketProduct = async () => {
|
|||||||
if (ticketProduct.value) return
|
if (ticketProduct.value) return
|
||||||
try {
|
try {
|
||||||
ticketProduct.value = await ticketApi.getTicketProduct()
|
ticketProduct.value = await ticketApi.getTicketProduct()
|
||||||
} catch {
|
ticketPaused.value = false
|
||||||
// ignore
|
} catch (error) {
|
||||||
|
if (error?.code === 40400 || error?.statusCode === 404) {
|
||||||
|
ticketPaused.value = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +285,7 @@ const goDetail = (item) => {
|
|||||||
const refresh = async () => {
|
const refresh = async () => {
|
||||||
if (activeTab.value === 'tickets') {
|
if (activeTab.value === 'tickets') {
|
||||||
ticketProduct.value = null
|
ticketProduct.value = null
|
||||||
|
ticketPaused.value = false
|
||||||
ticketRecords.value = []
|
ticketRecords.value = []
|
||||||
await Promise.all([loadTicketProduct(), loadTicketRecords()])
|
await Promise.all([loadTicketProduct(), loadTicketRecords()])
|
||||||
} else {
|
} else {
|
||||||
@@ -469,12 +490,6 @@ defineExpose({ refresh, loadMore })
|
|||||||
margin-top: 6rpx;
|
margin-top: 6rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.course-price {
|
|
||||||
color: $text-color-main;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 篮球门票 */
|
/* 篮球门票 */
|
||||||
.ticket-section {
|
.ticket-section {
|
||||||
padding: $spacing-md;
|
padding: $spacing-md;
|
||||||
@@ -486,6 +501,25 @@ defineExpose({ refresh, loadMore })
|
|||||||
margin-bottom: $spacing-md;
|
margin-bottom: $spacing-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ticket-paused-card {
|
||||||
|
text-align: center;
|
||||||
|
padding: $spacing-xl $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-paused-title {
|
||||||
|
display: block;
|
||||||
|
color: $text-color-main;
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticket-paused-desc {
|
||||||
|
display: block;
|
||||||
|
margin-top: $spacing-xs;
|
||||||
|
color: $text-color-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.product-head,
|
.product-head,
|
||||||
.amount-row,
|
.amount-row,
|
||||||
.stepper-row {
|
.stepper-row {
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<template>
|
||||||
|
<view class="article-detail container" v-if="course">
|
||||||
|
<text class="title">{{ course.title }}</text>
|
||||||
|
<text class="date">{{ formatDate(course.updated_at) }}</text>
|
||||||
|
<image v-if="course.cover_image" :src="course.cover_image" mode="aspectFill" class="cover" />
|
||||||
|
<view class="content-card card">
|
||||||
|
<rich-text :nodes="course.detail"></rich-text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="loading-state container">
|
||||||
|
<text>加载中...</text>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
|
||||||
|
import { formatDate } from '@/modules/comprehensive/utils/date'
|
||||||
|
|
||||||
|
const course = ref(null)
|
||||||
|
|
||||||
|
onLoad(async (query) => {
|
||||||
|
course.value = await courseInfoApi.getCourseDetail(query.id)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.title,
|
||||||
|
.date {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $text-color-main;
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
margin-top: $spacing-sm;
|
||||||
|
color: $text-color-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 360rpx;
|
||||||
|
margin: $spacing-lg 0;
|
||||||
|
border-radius: $border-radius-lg;
|
||||||
|
background: $bg-color-hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
color: $text-color-regular;
|
||||||
|
font-size: 28rpx;
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-state {
|
||||||
|
padding: 200rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-color-light;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<template>
|
||||||
|
<view class="course-list-page container">
|
||||||
|
<view class="page-title-block">
|
||||||
|
<text class="page-title">全部课程</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="article-list">
|
||||||
|
<view v-for="course in courses" :key="course.id" class="article-card card" @tap="goDetail(course.id)">
|
||||||
|
<image :src="course.cover_image" mode="aspectFill" class="cover" />
|
||||||
|
<view class="article-info">
|
||||||
|
<text class="title">{{ course.title }}</text>
|
||||||
|
<text class="date">{{ formatDate(course.updated_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="isLoading" class="load-more-tip">加载中...</view>
|
||||||
|
<view v-else-if="courses.length === 0" class="empty-state">暂无课程</view>
|
||||||
|
<view v-else-if="!hasMore" class="load-more-tip">已加载全部</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||||
|
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
|
||||||
|
import { formatDate } from '@/modules/comprehensive/utils/date'
|
||||||
|
|
||||||
|
const courses = ref([])
|
||||||
|
const page = ref(1)
|
||||||
|
const pageSize = 20
|
||||||
|
const total = ref(0)
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const isLoadingMore = ref(false)
|
||||||
|
const hasMore = computed(() => courses.value.length < total.value)
|
||||||
|
|
||||||
|
onMounted(() => loadCourses(true))
|
||||||
|
|
||||||
|
onPullDownRefresh(() => {
|
||||||
|
loadCourses(true).finally(() => uni.stopPullDownRefresh())
|
||||||
|
})
|
||||||
|
|
||||||
|
onReachBottom(() => {
|
||||||
|
if (!hasMore.value || isLoadingMore.value) return
|
||||||
|
page.value += 1
|
||||||
|
loadCourses(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadCourses = async (reset) => {
|
||||||
|
if (reset) {
|
||||||
|
page.value = 1
|
||||||
|
isLoading.value = true
|
||||||
|
} else {
|
||||||
|
isLoadingMore.value = true
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await courseInfoApi.getCourses({ page: page.value, pageSize })
|
||||||
|
courses.value = reset ? data.items : [...courses.value, ...data.items]
|
||||||
|
total.value = data.total
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false
|
||||||
|
isLoadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const goDetail = (id) => {
|
||||||
|
uni.navigateTo({ url: `/pages/comprehensive/courses/info-detail?id=${id}` })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.page-title-block {
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title,
|
||||||
|
.title,
|
||||||
|
.date {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
color: $text-color-main;
|
||||||
|
font-size: 42rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-card {
|
||||||
|
display: flex;
|
||||||
|
gap: $spacing-md;
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cover {
|
||||||
|
width: 190rpx;
|
||||||
|
height: 136rpx;
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
background: $bg-color-hover;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
color: $text-color-main;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
margin-top: $spacing-md;
|
||||||
|
color: $text-color-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 160rpx 0;
|
||||||
|
text-align: center;
|
||||||
|
color: $text-color-light;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -6,9 +6,6 @@
|
|||||||
<view class="title-row">
|
<view class="title-row">
|
||||||
<text class="course-title">{{ sessionData.title }}</text>
|
<text class="course-title">{{ sessionData.title }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="price-row">
|
|
||||||
<text class="price-text">¥{{ sessionData.price }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="info-card card">
|
<view class="info-card card">
|
||||||
@@ -172,7 +169,6 @@ const confirmBook = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.title-row,
|
.title-row,
|
||||||
.price-row,
|
|
||||||
.info-row {
|
.info-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -188,10 +184,6 @@ const confirmBook = async () => {
|
|||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.price-row {
|
|
||||||
margin-top: $spacing-md;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-card,
|
.info-card,
|
||||||
.rich-card {
|
.rich-card {
|
||||||
margin-top: $spacing-md;
|
margin-top: $spacing-md;
|
||||||
|
|||||||
@@ -38,6 +38,24 @@
|
|||||||
<view v-else class="empty-placeholder">暂无公告活动</view>
|
<view v-else class="empty-placeholder">暂无公告活动</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="section-container">
|
||||||
|
<view class="section-header">
|
||||||
|
<text class="section-title">课程</text>
|
||||||
|
<text v-if="hasMoreCourses" class="section-more" @tap="go('/pages/comprehensive/courses/list')">查看全部</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view v-if="courses.length" class="activity-grid">
|
||||||
|
<view v-for="course in courses" :key="course.id" class="activity-card" @tap="go(`/pages/comprehensive/courses/info-detail?id=${course.id}`)">
|
||||||
|
<image :src="course.cover_image" mode="aspectFill" class="activity-cover" />
|
||||||
|
<view class="activity-info">
|
||||||
|
<text class="activity-title">{{ course.title }}</text>
|
||||||
|
<text class="activity-date">{{ formatDate(course.updated_at) }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view v-else class="empty-placeholder">暂无课程</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<VenueSwitchHeader venue-name="综合场馆" />
|
<VenueSwitchHeader venue-name="综合场馆" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -46,20 +64,28 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { contentApi } from '@/modules/comprehensive/api/content'
|
import { contentApi } from '@/modules/comprehensive/api/content'
|
||||||
|
import { courseInfoApi } from '@/modules/comprehensive/api/courseInfo'
|
||||||
import { formatDate } from '@/modules/comprehensive/utils/date'
|
import { formatDate } from '@/modules/comprehensive/utils/date'
|
||||||
import VenueSwitchHeader from '@/components/VenueSwitchHeader.vue'
|
import VenueSwitchHeader from '@/components/VenueSwitchHeader.vue'
|
||||||
|
|
||||||
const banners = ref([])
|
const banners = ref([])
|
||||||
const articles = ref([])
|
const articles = ref([])
|
||||||
|
const courses = ref([])
|
||||||
|
const hasMoreCourses = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadPage()
|
loadPage()
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadPage = async () => {
|
const loadPage = async () => {
|
||||||
const home = await contentApi.getHomeContent({ banner_limit: 5, article_limit: 3 })
|
const [home, courseData] = await Promise.all([
|
||||||
|
contentApi.getHomeContent({ banner_limit: 5, article_limit: 3 }),
|
||||||
|
courseInfoApi.getCourses({ page: 1, pageSize: 5 })
|
||||||
|
])
|
||||||
banners.value = home.banners || []
|
banners.value = home.banners || []
|
||||||
articles.value = home.articles || []
|
articles.value = home.articles || []
|
||||||
|
courses.value = courseData.items || []
|
||||||
|
hasMoreCourses.value = courseData.total > courses.value.length
|
||||||
}
|
}
|
||||||
|
|
||||||
const go = (url) => {
|
const go = (url) => {
|
||||||
@@ -131,6 +157,11 @@ defineExpose({ refresh: loadPage })
|
|||||||
background-color: $text-color-main;
|
background-color: $text-color-main;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-more {
|
||||||
|
color: $text-color-muted;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.activity-grid {
|
.activity-grid {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
<view
|
<view
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:class="{ active: activeTab === 'stored_value' }"
|
:class="{ active: activeTab === 'stored_value' }"
|
||||||
@tap="activeTab = 'stored_value'"
|
@tap="switchTab('stored_value')"
|
||||||
>
|
>
|
||||||
储值卡
|
会员卡
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:class="{ active: activeTab === 'membership' }"
|
:class="{ active: activeTab === 'membership' }"
|
||||||
@tap="activeTab = 'membership'"
|
@tap="switchTab('membership')"
|
||||||
>
|
>
|
||||||
会员卡
|
课程套餐
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -80,7 +80,18 @@ const goDetail = (item) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ refresh: loadPage })
|
const switchTab = (tab) => {
|
||||||
|
if (activeTab.value === tab) return
|
||||||
|
activeTab.value = tab
|
||||||
|
loadPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
await session.init()
|
||||||
|
await loadPage()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ refresh })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
<view class="card method-card">
|
<view class="card method-card">
|
||||||
<text class="card-title">支付方式</text>
|
<text class="card-title">支付方式</text>
|
||||||
<view
|
<view
|
||||||
v-if="!isStoredValueOrder"
|
v-if="!isWechatOnlyOrder"
|
||||||
class="method-item"
|
class="method-item"
|
||||||
:class="{ active: payMethod === 'balance', disabled: !canUseBalance }"
|
:class="{ active: payMethod === 'balance', disabled: !canUseBalance }"
|
||||||
@tap="selectMethod('balance')"
|
@tap="selectMethod('balance')"
|
||||||
@@ -74,9 +74,8 @@ const isSubmitting = ref(false)
|
|||||||
const nowTimestamp = ref(Date.now())
|
const nowTimestamp = ref(Date.now())
|
||||||
let countdownTimer = null
|
let countdownTimer = null
|
||||||
|
|
||||||
const isStoredValueOrder = computed(() => order.value?.business_type === 'stored_value')
|
const isWechatOnlyOrder = computed(() => order.value?.business_type !== 'private_booking')
|
||||||
const canUseBalance = computed(() => {
|
const canUseBalance = computed(() => {
|
||||||
if (isStoredValueOrder.value) return false
|
|
||||||
return toAmountNumber(balance.value.balance) >= toAmountNumber(order.value?.amount)
|
return toAmountNumber(balance.value.balance) >= toAmountNumber(order.value?.amount)
|
||||||
})
|
})
|
||||||
const canPay = computed(() => order.value?.status === 'pending_pay' && (payMethod.value !== 'balance' || canUseBalance.value))
|
const canPay = computed(() => order.value?.status === 'pending_pay' && (payMethod.value !== 'balance' || canUseBalance.value))
|
||||||
@@ -107,16 +106,12 @@ onUnmounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loadPage = async () => {
|
const loadPage = async () => {
|
||||||
const [orderData, balanceData] = await Promise.all([
|
order.value = await orderApi.getOrderDetail(orderId.value)
|
||||||
orderApi.getOrderDetail(orderId.value),
|
if (isWechatOnlyOrder.value) {
|
||||||
memberApi.getBalance()
|
|
||||||
])
|
|
||||||
order.value = orderData
|
|
||||||
balance.value = balanceData
|
|
||||||
if (isStoredValueOrder.value) {
|
|
||||||
payMethod.value = 'wechat'
|
|
||||||
} else if (!canUseBalance.value) {
|
|
||||||
payMethod.value = 'wechat'
|
payMethod.value = 'wechat'
|
||||||
|
} else {
|
||||||
|
balance.value = await memberApi.getBalance()
|
||||||
|
payMethod.value = canUseBalance.value ? 'balance' : 'wechat'
|
||||||
}
|
}
|
||||||
updateCountdownTimer()
|
updateCountdownTimer()
|
||||||
}
|
}
|
||||||
@@ -140,22 +135,15 @@ const stopCountdown = () => {
|
|||||||
const padTime = (value) => String(value).padStart(2, '0')
|
const padTime = (value) => String(value).padStart(2, '0')
|
||||||
|
|
||||||
const selectMethod = (method) => {
|
const selectMethod = (method) => {
|
||||||
if (method === 'balance') {
|
if (method === 'balance' && !canUseBalance.value) {
|
||||||
if (isStoredValueOrder.value) {
|
|
||||||
uni.showToast({ title: '储值卡订单仅支持微信支付', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!canUseBalance.value) {
|
|
||||||
uni.showToast({ title: '余额不足', icon: 'none' })
|
uni.showToast({ title: '余额不足', icon: 'none' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
payMethod.value = method
|
payMethod.value = method
|
||||||
}
|
}
|
||||||
|
|
||||||
const pay = async () => {
|
const pay = async () => {
|
||||||
if (!canPay.value || isSubmitting.value) return
|
if (!canPay.value || isSubmitting.value) return
|
||||||
if (isStoredValueOrder.value && payMethod.value === 'balance') return
|
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
if (payMethod.value === 'balance') {
|
if (payMethod.value === 'balance') {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
<view class="card fee-card">
|
<view class="card fee-card">
|
||||||
<text class="card-title">费用明细</text>
|
<text class="card-title">费用明细</text>
|
||||||
<view class="summary-row">
|
<view class="summary-row">
|
||||||
<text>半小时段</text>
|
<text>时段</text>
|
||||||
<text>{{ quote.unit_count }} 段</text>
|
<text>{{ quote.unit_count }} 段</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="summary-row">
|
<view class="summary-row">
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
<view class="private-page">
|
<view class="private-page">
|
||||||
<view class="tab-bar">
|
<view class="tab-bar">
|
||||||
<view
|
<view
|
||||||
v-for="item in typeOptions"
|
v-for="tab in mainTabs"
|
||||||
:key="item.value"
|
:key="tab.value"
|
||||||
class="tab-item"
|
class="tab-item"
|
||||||
:class="{ active: courtType === item.value }"
|
:class="{ active: activeTab === tab.value }"
|
||||||
@tap="selectType(item.value)"
|
@tap="switchTab(tab.value)"
|
||||||
>{{ item.label }}</view>
|
>{{ tab.label }}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<template v-if="activeTab === 'booking'">
|
||||||
<scroll-view scroll-x class="date-bar" :show-scrollbar="false">
|
<scroll-view scroll-x class="date-bar" :show-scrollbar="false">
|
||||||
<view class="date-list">
|
<view class="date-list">
|
||||||
<view
|
<view
|
||||||
@@ -61,7 +62,9 @@
|
|||||||
class="grid-cell"
|
class="grid-cell"
|
||||||
:class="getCellClass(court.id, si)"
|
:class="getCellClass(court.id, si)"
|
||||||
@tap="toggleCell(court.id, si)"
|
@tap="toggleCell(court.id, si)"
|
||||||
></view>
|
>
|
||||||
|
<text v-if="getCellPrice(court.id, si)" class="cell-price">¥{{ getCellPrice(court.id, si) }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view
|
<view
|
||||||
v-if="currentTimeLineVisible && currentTimeTop >= 0"
|
v-if="currentTimeLineVisible && currentTimeTop >= 0"
|
||||||
@@ -84,6 +87,22 @@
|
|||||||
<button class="primary-button submit-btn" :disabled="!canConfirm" @tap="goConfirm">预约</button>
|
<button class="primary-button submit-btn" :disabled="!canConfirm" @tap="goConfirm">预约</button>
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="activeTab === 'history'">
|
||||||
|
<scroll-view scroll-y class="history-scroll" @scrolltolower="loadHistory()">
|
||||||
|
<view v-for="item in historyList" :key="item.id" class="history-card" @tap="goBookingDetail(item)">
|
||||||
|
<view class="history-card-top">
|
||||||
|
<text class="history-court">{{ historyCourtNames(item) }}</text>
|
||||||
|
<text class="history-status">{{ item.status_label }}</text>
|
||||||
|
</view>
|
||||||
|
<text class="history-time">{{ historyTimeText(item) }}</text>
|
||||||
|
<text class="history-amount">¥{{ item.order?.amount || '--' }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="historyLoading" class="history-tip">加载中...</view>
|
||||||
|
<view v-else-if="historyList.length === 0" class="history-empty">暂无预定记录</view>
|
||||||
|
</scroll-view>
|
||||||
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -91,17 +110,23 @@
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { privateBookingApi } from '@/modules/comprehensive/api/privateBooking'
|
import { privateBookingApi } from '@/modules/comprehensive/api/privateBooking'
|
||||||
|
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
|
||||||
|
import { bookingApi } from '@/modules/comprehensive/api/booking'
|
||||||
|
import { formatTimeRange } from '@/modules/comprehensive/utils/date'
|
||||||
|
import { toAmountNumber } from '@/modules/comprehensive/utils/money'
|
||||||
|
|
||||||
const HEADER_HEIGHT = 75
|
const HEADER_HEIGHT = 75
|
||||||
const CELL_HEIGHT = 107
|
const CELL_HEIGHT = 107
|
||||||
const TIME_COL_WIDTH = 128
|
const TIME_COL_WIDTH = 128
|
||||||
const COURT_COL_WIDTH = 149
|
const COURT_COL_WIDTH = 149
|
||||||
|
|
||||||
const typeOptions = [
|
const mainTabs = [
|
||||||
{ label: '羽毛球', value: 'badminton' },
|
{ label: '订场', value: 'booking' },
|
||||||
{ label: '篮球', value: 'basketball' }
|
{ label: '我的预定', value: 'history' }
|
||||||
]
|
]
|
||||||
|
const activeTab = ref('booking')
|
||||||
|
|
||||||
|
const session = useComprehensiveSessionStore()
|
||||||
const courtType = ref('badminton')
|
const courtType = ref('badminton')
|
||||||
const selectedDate = ref(dayjs().format('YYYY-MM-DD'))
|
const selectedDate = ref(dayjs().format('YYYY-MM-DD'))
|
||||||
const grid = ref(null)
|
const grid = ref(null)
|
||||||
@@ -111,10 +136,22 @@ const selectedCells = ref({})
|
|||||||
const now = ref(dayjs())
|
const now = ref(dayjs())
|
||||||
let loadRequestId = 0
|
let loadRequestId = 0
|
||||||
|
|
||||||
|
const historyList = ref([])
|
||||||
|
const historyLoading = ref(false)
|
||||||
|
const historyPage = ref(1)
|
||||||
|
const historyPageSize = 20
|
||||||
|
const historyHasMore = ref(true)
|
||||||
|
|
||||||
|
const bookingConfig = ref({ member_advance_days: 7, non_member_advance_days: 3 })
|
||||||
|
|
||||||
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
const weekDays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||||
|
const hasPositiveBalance = computed(() => toAmountNumber(session.member?.balance) > 0)
|
||||||
const dateOptions = computed(() => {
|
const dateOptions = computed(() => {
|
||||||
const today = dayjs()
|
const today = dayjs()
|
||||||
return Array.from({ length: 7 }).map((_, i) => {
|
const dateCount = hasPositiveBalance.value
|
||||||
|
? bookingConfig.value.member_advance_days
|
||||||
|
: bookingConfig.value.non_member_advance_days
|
||||||
|
return Array.from({ length: dateCount }).map((_, i) => {
|
||||||
const d = today.add(i, 'day')
|
const d = today.add(i, 'day')
|
||||||
return {
|
return {
|
||||||
label: i === 0 ? '今天' : i === 1 ? '明天' : weekDays[d.day()],
|
label: i === 0 ? '今天' : i === 1 ? '明天' : weekDays[d.day()],
|
||||||
@@ -124,9 +161,6 @@ const dateOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const openTime = computed(() => normalizeClock(grid.value?.open_time))
|
|
||||||
const closeTime = computed(() => normalizeClock(grid.value?.close_time))
|
|
||||||
|
|
||||||
const occupationMap = computed(() => {
|
const occupationMap = computed(() => {
|
||||||
const map = {}
|
const map = {}
|
||||||
if (!grid.value) return map
|
if (!grid.value) return map
|
||||||
@@ -146,6 +180,7 @@ const cellStatus = computed(() => {
|
|||||||
for (const court of grid.value.courts) {
|
for (const court of grid.value.courts) {
|
||||||
status[court.id] = {}
|
status[court.id] = {}
|
||||||
const occs = occupationMap.value[court.id] || []
|
const occs = occupationMap.value[court.id] || []
|
||||||
|
const courtPrices = grid.value?.slot_prices?.[court.id] || []
|
||||||
|
|
||||||
for (let si = 0; si < grid.value.time_slots.length; si++) {
|
for (let si = 0; si < grid.value.time_slots.length; si++) {
|
||||||
const slot = grid.value.time_slots[si]
|
const slot = grid.value.time_slots[si]
|
||||||
@@ -157,13 +192,23 @@ const cellStatus = computed(() => {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const isOccupied = occs.some((occ) => {
|
const overlappingOcc = occs.find((occ) => {
|
||||||
const occStart = dayjs(occ.start_at)
|
const occStart = dayjs(occ.start_at)
|
||||||
const occEnd = dayjs(occ.end_at)
|
const occEnd = dayjs(occ.end_at)
|
||||||
return occStart.isBefore(slotEnd) && occEnd.isAfter(slotStart)
|
return occStart.isBefore(slotEnd) && occEnd.isAfter(slotStart)
|
||||||
})
|
})
|
||||||
|
|
||||||
status[court.id][si] = isOccupied ? 'occupied' : 'available'
|
if (overlappingOcc) {
|
||||||
|
status[court.id][si] = overlappingOcc.source_type === 'lock' ? 'locked' : 'occupied'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!courtPrices[si]) {
|
||||||
|
status[court.id][si] = 'no_price'
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
status[court.id][si] = 'available'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return status
|
return status
|
||||||
@@ -180,7 +225,7 @@ const currentTimeTop = computed(() => {
|
|||||||
const closeMin = parseTimeToMinutes(grid.value.close_time)
|
const closeMin = parseTimeToMinutes(grid.value.close_time)
|
||||||
const currentMin = now.value.hour() * 60 + now.value.minute()
|
const currentMin = now.value.hour() * 60 + now.value.minute()
|
||||||
if (currentMin < openMin || currentMin > closeMin) return -1
|
if (currentMin < openMin || currentMin > closeMin) return -1
|
||||||
return HEADER_HEIGHT + ((currentMin - openMin) / 30) * CELL_HEIGHT
|
return HEADER_HEIGHT + ((currentMin - openMin) / 60) * CELL_HEIGHT
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedCourtCount = computed(() => Object.keys(selectedCells.value).length)
|
const selectedCourtCount = computed(() => Object.keys(selectedCells.value).length)
|
||||||
@@ -203,11 +248,18 @@ const selectionTimeText = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const selectionAmount = computed(() => {
|
const selectionAmount = computed(() => {
|
||||||
const priceVal = parseFloat(grid.value?.unit_price || '0')
|
let total = 0
|
||||||
return (priceVal * selectedTotalSlots.value).toFixed(2)
|
for (const [courtId, slots] of Object.entries(selectedCells.value)) {
|
||||||
|
const prices = grid.value?.slot_prices?.[courtId] || []
|
||||||
|
for (const slotIndex of slots) {
|
||||||
|
const p = parseFloat(prices[slotIndex] || '0')
|
||||||
|
total += p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total.toFixed(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(loadGrid)
|
onMounted(refresh)
|
||||||
|
|
||||||
async function loadGrid() {
|
async function loadGrid() {
|
||||||
const requestId = ++loadRequestId
|
const requestId = ++loadRequestId
|
||||||
@@ -229,10 +281,59 @@ async function loadGrid() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectType(type) {
|
function switchTab(tab) {
|
||||||
if (courtType.value === type) return
|
if (activeTab.value === tab) return
|
||||||
courtType.value = type
|
activeTab.value = tab
|
||||||
loadGrid()
|
if (tab === 'history') {
|
||||||
|
loadHistory(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadHistory(reset = false) {
|
||||||
|
if (historyLoading.value) return
|
||||||
|
if (reset) {
|
||||||
|
historyPage.value = 1
|
||||||
|
historyHasMore.value = true
|
||||||
|
}
|
||||||
|
if (!historyHasMore.value) return
|
||||||
|
historyLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await bookingApi.getPrivateBookings({
|
||||||
|
page: historyPage.value,
|
||||||
|
pageSize: historyPageSize
|
||||||
|
})
|
||||||
|
const tenDaysAgo = dayjs().subtract(10, 'day')
|
||||||
|
const booked = data.items.filter(
|
||||||
|
(item) => item.status === 'booked' && item.start_at && dayjs(item.start_at) >= tenDaysAgo
|
||||||
|
)
|
||||||
|
if (reset) {
|
||||||
|
historyList.value = booked
|
||||||
|
} else {
|
||||||
|
historyList.value = [...historyList.value, ...booked]
|
||||||
|
}
|
||||||
|
historyHasMore.value = data.page * data.page_size < data.total
|
||||||
|
if (historyHasMore.value) historyPage.value++
|
||||||
|
} catch {
|
||||||
|
if (reset) historyList.value = []
|
||||||
|
} finally {
|
||||||
|
historyLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function historyCourtNames(item) {
|
||||||
|
return item.courts?.map((c) => c.name).join('、') || '包场预约'
|
||||||
|
}
|
||||||
|
|
||||||
|
function historyTimeText(item) {
|
||||||
|
if (!item.start_at || !item.end_at) return '-'
|
||||||
|
const start = dayjs(item.start_at)
|
||||||
|
return `${start.month() + 1}/${start.date()} ${formatTimeRange(item.start_at, item.end_at)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBookingDetail(item) {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: `/pages/comprehensive/bookings/private-detail?id=${item.id}`
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectDate(date) {
|
function selectDate(date) {
|
||||||
@@ -249,6 +350,12 @@ function getCellClass(courtId, slotIndex) {
|
|||||||
return { [`cell-${status}`]: true }
|
return { [`cell-${status}`]: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getCellPrice(courtId, slotIndex) {
|
||||||
|
if (cellStatus.value[courtId]?.[slotIndex] !== 'available') return ''
|
||||||
|
const prices = grid.value?.slot_prices?.[courtId] || []
|
||||||
|
return prices[slotIndex] || ''
|
||||||
|
}
|
||||||
|
|
||||||
function toggleCell(courtId, slotIndex) {
|
function toggleCell(courtId, slotIndex) {
|
||||||
const status = cellStatus.value[courtId]?.[slotIndex]
|
const status = cellStatus.value[courtId]?.[slotIndex]
|
||||||
if (status !== 'available') return
|
if (status !== 'available') return
|
||||||
@@ -328,11 +435,28 @@ function normalizeClock(value) {
|
|||||||
return value.slice(0, 5)
|
return value.slice(0, 5)
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
async function refresh() {
|
||||||
refresh() {
|
await session.init()
|
||||||
now.value = dayjs()
|
try {
|
||||||
loadGrid()
|
bookingConfig.value = await privateBookingApi.getPrivateBookingConfig()
|
||||||
|
} catch {
|
||||||
|
// 获取配置失败时使用默认值 7/3,不阻塞页面
|
||||||
}
|
}
|
||||||
|
if (!hasPositiveBalance.value) {
|
||||||
|
const todayStr = dayjs().format('YYYY-MM-DD')
|
||||||
|
const maxOffset = bookingConfig.value.non_member_advance_days - 1
|
||||||
|
const maxDateStr = dayjs().add(maxOffset, 'day').format('YYYY-MM-DD')
|
||||||
|
if (selectedDate.value < todayStr || selectedDate.value > maxDateStr) {
|
||||||
|
selectedDate.value = todayStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
now.value = dayjs()
|
||||||
|
await loadGrid()
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refresh,
|
||||||
|
loadGrid
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -530,18 +654,45 @@ defineExpose({
|
|||||||
height: 107rpx;
|
height: 107rpx;
|
||||||
border-right: 1rpx solid $bg-color-hover;
|
border-right: 1rpx solid $bg-color-hover;
|
||||||
border-bottom: 1rpx solid $bg-color-hover;
|
border-bottom: 1rpx solid $bg-color-hover;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-price {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-color-muted;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-selected .cell-price {
|
||||||
|
color: #FFFFFF;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-available {
|
.cell-available {
|
||||||
background: $bg-color-main;
|
background: $bg-color-main;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-occupied {
|
.cell-occupied,
|
||||||
background: $color-danger-light;
|
.cell-past,
|
||||||
|
.cell-locked,
|
||||||
|
.cell-no_price {
|
||||||
|
background: $bg-color-hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-past {
|
.cell-locked::after {
|
||||||
background: $bg-color-hover;
|
content: '锁';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 20rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $text-color-light;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cell-locked {
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-selected {
|
.cell-selected {
|
||||||
@@ -615,4 +766,65 @@ defineExpose({
|
|||||||
color: $text-color-light;
|
color: $text-color-light;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-empty {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: $text-color-light;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-scroll {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-card {
|
||||||
|
background: $bg-color-main;
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
padding: $spacing-md;
|
||||||
|
margin: $spacing-sm $spacing-md;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $spacing-xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-card-top {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-court {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: $text-color-main;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-status {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-time {
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: $text-color-regular;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-amount {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
color: $color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-tip {
|
||||||
|
text-align: center;
|
||||||
|
padding: $spacing-md;
|
||||||
|
color: $text-color-light;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -122,6 +122,9 @@ const bindPhone = async (event) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const goHome = () => {
|
const goHome = () => {
|
||||||
|
if (authStorage.getPendingToken()) {
|
||||||
|
session.completeLogin()
|
||||||
|
}
|
||||||
uni.switchTab({ url: '/pages/home/index' })
|
uni.switchTab({ url: '/pages/home/index' })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
|
import { useComprehensiveSessionStore } from '@/modules/comprehensive/stores/session'
|
||||||
import { memberApi } from '@/modules/comprehensive/api/member'
|
import { memberApi } from '@/modules/comprehensive/api/member'
|
||||||
import { formatAmount } from '@/modules/comprehensive/utils/money'
|
import { formatAmount } from '@/modules/comprehensive/utils/money'
|
||||||
@@ -88,21 +88,11 @@ const balance = ref('0.00')
|
|||||||
const balanceText = computed(() => formatAmount(balance.value))
|
const balanceText = computed(() => formatAmount(balance.value))
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{ title: '我的会员卡', url: '/pages/comprehensive/membership/cards', theme: 'profile-theme', icon: 'profile' },
|
{ title: '我的课程套餐', url: '/pages/comprehensive/membership/cards', theme: 'profile-theme', icon: 'profile' },
|
||||||
{ title: '我的订单', url: '/pages/comprehensive/profile/orders', theme: 'order-theme', icon: 'order' },
|
{ title: '我的订单', url: '/pages/comprehensive/profile/orders', theme: 'order-theme', icon: 'order' },
|
||||||
{ title: '余额流水', url: '/pages/comprehensive/profile/balance', theme: 'order-theme', icon: 'order' }
|
{ title: '余额流水', url: '/pages/comprehensive/profile/balance', theme: 'order-theme', icon: 'order' }
|
||||||
]
|
]
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await session.init()
|
|
||||||
try {
|
|
||||||
const data = await memberApi.getBalance()
|
|
||||||
balance.value = data.balance
|
|
||||||
} catch {
|
|
||||||
balance.value = '0.00'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const login = () => {
|
const login = () => {
|
||||||
uni.navigateTo({ url: '/pages/comprehensive/profile/login' })
|
uni.navigateTo({ url: '/pages/comprehensive/profile/login' })
|
||||||
}
|
}
|
||||||
@@ -116,7 +106,17 @@ const go = (url) => {
|
|||||||
uni.navigateTo({ url })
|
uni.navigateTo({ url })
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ onPageShow: () => {} })
|
const refresh = async () => {
|
||||||
|
await session.init()
|
||||||
|
try {
|
||||||
|
const data = await memberApi.getBalance()
|
||||||
|
balance.value = data.balance
|
||||||
|
} catch {
|
||||||
|
balance.value = '0.00'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ onPageShow: refresh, refresh })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ const statusOptions = [
|
|||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
{ label: '待支付', value: 'pending_pay' },
|
{ label: '待支付', value: 'pending_pay' },
|
||||||
{ label: '已支付', value: 'paid' },
|
{ label: '已支付', value: 'paid' },
|
||||||
|
{ label: '部分退款', value: 'part_refunded' },
|
||||||
{ label: '已关闭', value: 'closed' },
|
{ label: '已关闭', value: 'closed' },
|
||||||
{ label: '已退款', value: 'refunded' }
|
{ label: '已退款', value: 'refunded' }
|
||||||
]
|
]
|
||||||
@@ -192,6 +193,11 @@ loadOrders(true)
|
|||||||
color: $color-warning;
|
color: $color-warning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-tag.part_refunded {
|
||||||
|
background: $color-warning-light;
|
||||||
|
color: $color-warning;
|
||||||
|
}
|
||||||
|
|
||||||
.status-tag.closed {
|
.status-tag.closed {
|
||||||
background: $bg-color-hover;
|
background: $bg-color-hover;
|
||||||
color: $text-color-muted;
|
color: $text-color-muted;
|
||||||
|
|||||||
@@ -1,44 +1,50 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="dev-page">
|
<view class="image-page">
|
||||||
<view class="dev-content">
|
<image
|
||||||
<text class="dev-icon">🚧</text>
|
v-if="imageUrl"
|
||||||
<text class="dev-title">正在开发中</text>
|
class="full-image"
|
||||||
<text class="dev-desc">此功能即将上线,敬请期待</text>
|
:src="imageUrl"
|
||||||
</view>
|
mode="widthFix"
|
||||||
|
:show-menu-by-longpress="true"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { contentApi } from '@/modules/ice/api/content'
|
||||||
|
|
||||||
|
const imageUrl = ref('')
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
const data = await contentApi.getPageImages()
|
||||||
|
imageUrl.value = data.image_1 || ''
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
refresh() {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
loadData()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dev-page {
|
.image-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: $bg-color-soft;
|
background: $bg-color-soft;
|
||||||
}
|
|
||||||
|
|
||||||
.dev-content {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $spacing-md;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dev-icon {
|
.full-image {
|
||||||
font-size: 80rpx;
|
width: 100%;
|
||||||
}
|
display: block;
|
||||||
|
|
||||||
.dev-title {
|
|
||||||
color: $text-color-main;
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dev-desc {
|
|
||||||
color: $text-color-muted;
|
|
||||||
font-size: 26rpx;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,47 +1,51 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="dev-page">
|
<view class="image-page">
|
||||||
<view class="dev-content">
|
<image
|
||||||
<text class="dev-icon">🚧</text>
|
v-if="imageUrl"
|
||||||
<text class="dev-title">正在开发中</text>
|
class="full-image"
|
||||||
<text class="dev-desc">此功能即将上线,敬请期待</text>
|
:src="imageUrl"
|
||||||
</view>
|
mode="widthFix"
|
||||||
|
:show-menu-by-longpress="true"
|
||||||
|
/>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { contentApi } from '@/modules/ice/api/content'
|
||||||
|
|
||||||
|
const imageUrl = ref('')
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
try {
|
||||||
|
const data = await contentApi.getPageImages()
|
||||||
|
imageUrl.value = data.image_2 || ''
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
refresh() {}
|
refresh() {
|
||||||
|
loadData()
|
||||||
|
},
|
||||||
|
loadGrid() {}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
loadData()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.dev-page {
|
.image-page {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: $bg-color-soft;
|
background: $bg-color-soft;
|
||||||
}
|
|
||||||
|
|
||||||
.dev-content {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $spacing-md;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dev-icon {
|
.full-image {
|
||||||
font-size: 80rpx;
|
width: 100%;
|
||||||
}
|
display: block;
|
||||||
|
|
||||||
.dev-title {
|
|
||||||
color: $text-color-main;
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dev-desc {
|
|
||||||
color: $text-color-muted;
|
|
||||||
font-size: 26rpx;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -165,7 +165,7 @@
|
|||||||
<view class="svg-card-chip" />
|
<view class="svg-card-chip" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="item-title">我的会员卡</text>
|
<text class="item-title">我的课程套餐</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="item-right">
|
<view class="item-right">
|
||||||
<view class="count-badge" v-if="member.cardCount > 0">
|
<view class="count-badge" v-if="member.cardCount > 0">
|
||||||
@@ -497,7 +497,7 @@ const formatDate = (dateStr) => {
|
|||||||
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({ onPageShow: refreshSession })
|
defineExpose({ onPageShow: refreshSession, refresh: refreshSession })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
@@ -7,13 +7,22 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onPullDownRefresh } from '@dcloudio/uni-app'
|
import { onPullDownRefresh, onShow } from '@dcloudio/uni-app'
|
||||||
import { getStoredVenue } from '@/utils/venue'
|
import { getStoredVenue } from '@/utils/venue'
|
||||||
import ComprehensiveMall from '@/pages/comprehensive/mall/index.vue'
|
import ComprehensiveMall from '@/pages/comprehensive/mall/index.vue'
|
||||||
import IceMall from '@/pages/ice/mall/index.vue'
|
import IceMall from '@/pages/ice/mall/index.vue'
|
||||||
|
|
||||||
const isComprehensive = getStoredVenue() === 'comprehensive'
|
const isComprehensive = getStoredVenue() === 'comprehensive'
|
||||||
const contentRef = ref()
|
const contentRef = ref()
|
||||||
|
let isFirstShow = true
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
if (isFirstShow) {
|
||||||
|
isFirstShow = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
contentRef.value?.refresh?.()
|
||||||
|
})
|
||||||
|
|
||||||
onPullDownRefresh(async () => {
|
onPullDownRefresh(async () => {
|
||||||
await contentRef.value?.refresh?.()
|
await contentRef.value?.refresh?.()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow, onHide } from '@dcloudio/uni-app'
|
||||||
import { getStoredVenue } from '@/utils/venue'
|
import { getStoredVenue } from '@/utils/venue'
|
||||||
import ComprehensivePrivate from '@/pages/comprehensive/private/index.vue'
|
import ComprehensivePrivate from '@/pages/comprehensive/private/index.vue'
|
||||||
import IcePrivate from '@/pages/ice/private/index.vue'
|
import IcePrivate from '@/pages/ice/private/index.vue'
|
||||||
@@ -15,13 +15,33 @@ import IcePrivate from '@/pages/ice/private/index.vue'
|
|||||||
const isComprehensive = getStoredVenue() === 'comprehensive'
|
const isComprehensive = getStoredVenue() === 'comprehensive'
|
||||||
const contentRef = ref()
|
const contentRef = ref()
|
||||||
let isFirstShow = true
|
let isFirstShow = true
|
||||||
|
let autoRefreshTimer = null
|
||||||
|
|
||||||
|
const startAutoRefresh = () => {
|
||||||
|
stopAutoRefresh()
|
||||||
|
autoRefreshTimer = setInterval(() => {
|
||||||
|
contentRef.value?.loadGrid?.()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopAutoRefresh = () => {
|
||||||
|
if (autoRefreshTimer) {
|
||||||
|
clearInterval(autoRefreshTimer)
|
||||||
|
autoRefreshTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
if (isFirstShow) {
|
if (isFirstShow) {
|
||||||
isFirstShow = false
|
isFirstShow = false
|
||||||
return
|
} else {
|
||||||
}
|
|
||||||
contentRef.value?.refresh?.()
|
contentRef.value?.refresh?.()
|
||||||
|
}
|
||||||
|
startAutoRefresh()
|
||||||
|
})
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
stopAutoRefresh()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { onShow } from '@dcloudio/uni-app'
|
import { onShow, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||||
import { getStoredVenue } from '@/utils/venue'
|
import { getStoredVenue } from '@/utils/venue'
|
||||||
import ComprehensiveProfile from '@/pages/comprehensive/profile/index.vue'
|
import ComprehensiveProfile from '@/pages/comprehensive/profile/index.vue'
|
||||||
import IceProfile from '@/pages/ice/profile/index.vue'
|
import IceProfile from '@/pages/ice/profile/index.vue'
|
||||||
@@ -18,6 +18,11 @@ const contentRef = ref()
|
|||||||
onShow(() => {
|
onShow(() => {
|
||||||
contentRef.value?.onPageShow?.()
|
contentRef.value?.onPageShow?.()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onPullDownRefresh(async () => {
|
||||||
|
await contentRef.value?.refresh?.()
|
||||||
|
uni.stopPullDownRefresh()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|||||||
Reference in New Issue
Block a user