Files
gym-manage/gym-manage-uniapp/pages/memberInfo/booking.vue
T

308 lines
12 KiB
Vue

<template>
<view class="scroll-container theme-light">
<view class="booking-page">
<PageHeader title="我的预约" subtitle="" :show-back="true" />
<view class="booking-page__tabs">
<view
v-for="tab in tabs"
:key="tab.key"
class="booking-page__tab"
:class="{ 'booking-page__tab--active': activeTab === tab.key }"
hover-class="mi-tap-tab--hover"
:hover-stay-time="150"
@tap="activeTab = tab.key"
>
<text
class="booking-page__tab-text"
:class="{ 'booking-page__tab-text--active': activeTab === tab.key }"
>
{{ tab.label }}
</text>
<view
v-if="activeTab === tab.key"
class="booking-page__tab-indicator"
></view>
</view>
</view>
<view class="booking-page__body">
<!-- 骨架屏 -->
<ListSkeleton v-if="loading" :count="4" layout="card" />
<!-- 无数据 -->
<view v-if="!displayedBookings.length && !loading" class="booking-page__empty">
<text class="booking-page__empty-text">
{{ activeTab === 'ongoing' ? '暂无进行中的预约' : '暂无历史预约' }}
</text>
</view>
<view v-else
v-for="item in displayedBookings"
:key="item.id"
class="bk-card"
hover-class="mi-tap-card--hover"
:hover-stay-time="150"
>
<image
class="bk-card__banner"
:src="item.banner"
mode="aspectFill"
/>
<view class="bk-card__content">
<view class="bk-card__header">
<text class="bk-card__title">{{ item.title }}</text>
<view
class="bk-card__status"
:class="'bk-card__status--' + item.status"
>
<text
class="bk-card__status-text"
:class="'bk-card__status-text--' + item.status"
>
{{ item.statusLabel }}
</text>
</view>
</view>
<view class="bk-card__meta">
<view class="bk-card__meta-row">
<image
class="bk-card__meta-icon"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock0.png"
mode="aspectFit"
/>
<text class="bk-card__meta-text">{{ item.schedule }}</text>
</view>
<view class="bk-card__meta-row">
<image
class="bk-card__meta-icon"
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/user0.png"
mode="aspectFit"
/>
<text class="bk-card__meta-text">{{ item.coach }}</text>
</view>
</view>
<view class="bk-card__footer" v-if="item.footerText">
<text class="bk-card__footer-info">{{ item.footerText }}</text>
<view class="bk-card__actions">
<view
v-if="item.canSignin"
class="bk-card__signin"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap.stop="signinCourse(item)"
>
<text class="bk-card__signin-text">签到</text>
</view>
<view
v-if="item.canCancel"
class="bk-card__cancel"
hover-class="mi-tap-btn--hover"
:hover-stay-time="150"
@tap.stop="cancelBooking(item)"
>
<text class="bk-card__cancel-text">取消预约</text>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import PageHeader from '@/components/index/PageHeader.vue'
import ListSkeleton from '@/components/Skeleton/ListSkeleton.vue'
import { getMemberId } from '@/utils/request.js'
import { getCourseCoverUrl } from '@/utils/request.js'
import { getMemberBookings, signinGroupCourse, cancelBooking as cancelBookingApi } from '@/api/main.js'
const tabs = ref([
{ key: 'ongoing', label: '进行中' },
{ key: 'history', label: '历史记录' }
])
const activeTab = ref('ongoing')
const ongoingBookings = ref([])
const historyBookings = ref([])
const loading = ref(false)
const displayedBookings = computed(() => {
return activeTab.value === 'ongoing'
? ongoingBookings.value
: historyBookings.value
})
async function fetchBookings() {
if (loading.value) return
loading.value = true
console.log('[booking] 开始获取预约记录')
const memberId = getMemberId()
if (!memberId) {
console.warn('[booking] 未登录,无法获取预约记录')
loading.value = false
return
}
try {
const res = await getMemberBookings(memberId)
console.log('[booking] getMemberBookings 返回:', JSON.stringify(res, null, 2))
const bookings = res?.data || res?.content || []
if (Array.isArray(bookings) && bookings.length > 0) {
const mapped = bookings.map(mapBooking)
ongoingBookings.value = mapped.filter((b) => b.status === 'ongoing')
historyBookings.value = mapped.filter((b) => b.status !== 'ongoing')
} else {
ongoingBookings.value = []
historyBookings.value = []
}
} catch (e) {
console.error('[booking] 获取预约记录失败:', e)
ongoingBookings.value = []
historyBookings.value = []
} finally {
loading.value = false
}
}
function mapBooking(b) {
const status = getBookingStatus(b)
return {
id: b.id,
courseId: b.courseId,
title: b.courseName || b.courseTitle || '预约课程',
status: status,
statusLabel: getBookingStatusLabel(b),
schedule: formatBookingTime(b.courseStartTime || b.startTime || b.bookingTime),
banner: getCourseCoverUrl(b.coverImage) || 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/default-course.png',
coach: b.coachName || b.coach || '教练',
footerText: status === 'ongoing' ? '请准时参加课程' : '',
canSignin: status === 'ongoing',
canCancel: status === 'ongoing'
}
}
function getBookingStatus(b) {
if (b.status) {
const s = String(b.status).toLowerCase()
if (s === 'cancelled' || s === 'canceled') return 'cancelled'
if (s === 'completed' || s === 'finished') return 'completed'
}
const startTime = b.courseStartTime || b.startTime
if (startTime) {
const now = new Date()
const start = new Date(startTime)
if (now > start) return 'completed'
}
return 'ongoing'
}
function getBookingStatusLabel(b) {
if (b.status) {
const s = String(b.status).toLowerCase()
if (s === 'cancelled' || s === 'canceled') return '已取消'
if (s === 'completed' || s === 'finished') return '已完成'
if (s === 'ongoing' || s === 'confirmed') return '进行中'
}
const status = getBookingStatus(b)
if (status === 'cancelled') return '已取消'
if (status === 'completed') return '已完成'
return '进行中'
}
function formatBookingTime(timeStr) {
if (!timeStr) return ''
try {
const d = new Date(timeStr)
const month = d.getMonth() + 1
const day = d.getDate()
const hour = d.getHours().toString().padStart(2, '0')
const minute = d.getMinutes().toString().padStart(2, '0')
return `${month}${day}${hour}:${minute}`
} catch {
return timeStr
}
}
// 签到团课
async function signinCourse(item) {
const memberId = getMemberId()
if (!memberId) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
uni.showModal({
title: '确认签到',
content: `确认签到「${item.title}」吗?`,
success: async (res) => {
if (!res.confirm) return
uni.showLoading({ title: '签到中...', mask: true })
try {
const result = await signinGroupCourse(Number(memberId), Number(item.courseId || item.id))
uni.hideLoading()
if (result.code === 200 || result.success) {
uni.showToast({ title: '签到成功', icon: 'success' })
fetchBookings()
} else {
uni.showToast({ title: result.message || '签到失败', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
console.error('[booking] 签到失败:', e)
uni.showToast({ title: e.message || '签到失败', icon: 'none' })
}
}
})
}
// 取消预约
async function cancelBooking(item) {
uni.showModal({
title: '取消预约',
content: `确定要取消「${item.title}」吗?`,
success: async (res) => {
if (!res.confirm) return
uni.showLoading({ title: '取消中...', mask: true })
try {
const result = await cancelBookingApi(Number(item.id))
uni.hideLoading()
if (result.code === 200 || result.success) {
uni.showToast({ title: '已取消', icon: 'success' })
fetchBookings()
} else {
uni.showToast({ title: result.message || '取消失败', icon: 'none' })
}
} catch (e) {
uni.hideLoading()
console.error('[booking] 取消预约失败:', e)
uni.showToast({ title: e.message || '取消失败', icon: 'none' })
}
}
})
}
onMounted(() => {
fetchBookings()
})
</script>
<style lang="scss">
@import '@/common/style/base.css';
@import '@/common/style/memberInfo/pages/page-reset.css';
@import '@/common/style/memberInfo/pages/sub-page-base.css';
@import '@/common/style/memberInfo/member-info-component-reset.css';
@import '@/common/style/memberInfo/member-info-tap.css';
@import '@/common/style/memberInfo/pages/booking-page.css';
</style>