246 lines
5.9 KiB
Vue
246 lines
5.9 KiB
Vue
<template>
|
|
<view class="booking-detail container page-bottom-safe" v-if="booking">
|
|
<view class="status-card card">
|
|
<text class="status-title">{{ booking.status_label }}</text>
|
|
<text class="status-desc">{{ statusDesc }}</text>
|
|
</view>
|
|
|
|
<view class="card course-card">
|
|
<image :src="booking.course.cover_image" mode="aspectFill" class="cover" />
|
|
<view class="course-info">
|
|
<text class="course-title">{{ booking.course.title }}</text>
|
|
<text class="meta primary">{{ formatTimeRange(booking.course.start_at, booking.course.end_at) }}</text>
|
|
<text class="meta">{{ booking.course.coach_name }} · {{ booking.course.courts.map(item => item.name).join('、') }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="card">
|
|
<text class="card-title">订单信息</text>
|
|
<view class="info-row">
|
|
<text>订单号</text>
|
|
<text>{{ booking.order.order_no }}</text>
|
|
</view>
|
|
<view class="info-row">
|
|
<text>订单金额</text>
|
|
<text>¥{{ booking.order.amount }}</text>
|
|
</view>
|
|
<view class="info-row">
|
|
<text>订单状态</text>
|
|
<text>{{ booking.order.status_label }}</text>
|
|
</view>
|
|
</view>
|
|
|
|
<view class="card">
|
|
<text class="card-title">预约时间线</text>
|
|
<view class="timeline-item">
|
|
<text class="dot"></text>
|
|
<view>
|
|
<text class="timeline-title">创建预约</text>
|
|
<text class="timeline-time">{{ formatDateTime(booking.created_at) }}</text>
|
|
</view>
|
|
</view>
|
|
<view v-if="booking.expire_at" class="timeline-item">
|
|
<text class="dot warning"></text>
|
|
<view>
|
|
<text class="timeline-title">支付截止</text>
|
|
<text class="timeline-time">{{ formatDateTime(booking.expire_at) }}</text>
|
|
</view>
|
|
</view>
|
|
<view v-if="booking.cancel_deadline_at" class="timeline-item">
|
|
<text class="dot"></text>
|
|
<view>
|
|
<text class="timeline-title">取消截止</text>
|
|
<text class="timeline-time">{{ formatDateTime(booking.cancel_deadline_at) }}</text>
|
|
</view>
|
|
</view>
|
|
<view v-if="booking.cancelled_at" class="timeline-item">
|
|
<text class="dot danger"></text>
|
|
<view>
|
|
<text class="timeline-title">已取消</text>
|
|
<text class="timeline-time">{{ formatDateTime(booking.cancelled_at) }}</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<view v-if="booking.status === 'pending_pay' || booking.can_cancel" class="fixed-action-bar">
|
|
<button v-if="booking.can_cancel" class="ghost-button action-btn" @tap="cancelBooking">取消预约</button>
|
|
<button v-if="booking.status === 'pending_pay'" class="primary-button action-btn" @tap="goPay">去支付</button>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, ref } from 'vue'
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
import { bookingApi } from '@/modules/comprehensive/api/booking'
|
|
import { formatDateTime, formatTimeRange } from '@/modules/comprehensive/utils/date'
|
|
|
|
const bookingId = ref('')
|
|
const booking = ref(null)
|
|
|
|
const statusDesc = computed(() => {
|
|
if (!booking.value) return ''
|
|
if (booking.value.status === 'pending_pay') return '订单待支付,完成支付后预约生效。'
|
|
if (booking.value.status === 'booked') return '预约已生效,到馆后按场馆流程核销。'
|
|
if (booking.value.status === 'cancelled') return booking.value.cancel_reason || '预约已取消。'
|
|
return '预约记录已结束。'
|
|
})
|
|
|
|
onLoad(async (query) => {
|
|
bookingId.value = query.id
|
|
await loadBooking()
|
|
})
|
|
|
|
const loadBooking = async () => {
|
|
booking.value = await bookingApi.getCourseBookingDetail(bookingId.value)
|
|
}
|
|
|
|
const goPay = () => {
|
|
uni.navigateTo({ url: `/pages/comprehensive/pay/index?order_id=${booking.value.order_id}` })
|
|
}
|
|
|
|
const cancelBooking = async () => {
|
|
await bookingApi.cancelCourseBooking(booking.value.id)
|
|
uni.showToast({ title: '预约已取消', icon: 'none' })
|
|
await loadBooking()
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.status-card,
|
|
.course-card,
|
|
.card {
|
|
margin-bottom: $spacing-md;
|
|
}
|
|
|
|
.status-card {
|
|
text-align: center;
|
|
}
|
|
|
|
.status-title,
|
|
.status-desc,
|
|
.course-title,
|
|
.meta,
|
|
.card-title,
|
|
.timeline-title,
|
|
.timeline-time {
|
|
display: block;
|
|
}
|
|
|
|
.status-title {
|
|
color: $text-color-main;
|
|
font-size: 42rpx;
|
|
font-weight: 900;
|
|
}
|
|
|
|
.status-desc {
|
|
margin: $spacing-sm 0 $spacing-md;
|
|
color: $text-color-muted;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.course-card {
|
|
display: flex;
|
|
gap: $spacing-md;
|
|
}
|
|
|
|
.cover {
|
|
width: 180rpx;
|
|
height: 150rpx;
|
|
border-radius: $border-radius-base;
|
|
background: $bg-color-hover;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.course-info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.course-title,
|
|
.card-title {
|
|
color: $text-color-main;
|
|
font-size: 30rpx;
|
|
font-weight: 900;
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.meta {
|
|
margin-top: $spacing-xs;
|
|
color: $text-color-muted;
|
|
font-size: 24rpx;
|
|
}
|
|
|
|
.primary {
|
|
margin-top: $spacing-sm;
|
|
color: $color-primary;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.info-row {
|
|
min-height: 70rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: $spacing-md;
|
|
color: $text-color-muted;
|
|
font-size: 25rpx;
|
|
border-bottom: 1rpx solid $bg-color-hover;
|
|
}
|
|
|
|
.info-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.info-row text:last-child {
|
|
flex: 1;
|
|
color: $text-color-main;
|
|
font-weight: 800;
|
|
text-align: right;
|
|
}
|
|
|
|
.timeline-item {
|
|
display: flex;
|
|
gap: $spacing-md;
|
|
padding: $spacing-md 0;
|
|
border-bottom: 1rpx solid $bg-color-hover;
|
|
}
|
|
|
|
.timeline-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.dot {
|
|
width: 18rpx;
|
|
height: 18rpx;
|
|
margin-top: 10rpx;
|
|
border-radius: 50%;
|
|
background: $color-success;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.dot.warning {
|
|
background: $color-warning;
|
|
}
|
|
|
|
.dot.danger {
|
|
background: $color-danger;
|
|
}
|
|
|
|
.timeline-title {
|
|
color: $text-color-main;
|
|
font-size: 26rpx;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.timeline-time {
|
|
margin-top: 4rpx;
|
|
color: $text-color-muted;
|
|
font-size: 22rpx;
|
|
}
|
|
|
|
.action-btn {
|
|
flex: 1;
|
|
}
|
|
</style>
|