270 lines
10 KiB
Vue
270 lines
10 KiB
Vue
<template>
|
|
<view class="scroll-container theme-light">
|
|
<view class="booking-page">
|
|
<MemberInfoSubNav title="我的预约" @back="goBack" />
|
|
|
|
<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="setActiveTab(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="bt-page__action-bar bt-page__action-bar--end">
|
|
<text
|
|
class="bt-page__action-link bt-page__action-link--primary"
|
|
hover-class="mi-tap--hover"
|
|
:hover-stay-time="150"
|
|
@tap="goCourseList"
|
|
>
|
|
预约课程
|
|
</text>
|
|
</view>
|
|
|
|
<view class="booking-page__body">
|
|
<view
|
|
v-if="activeTab === 'ongoing' && upcomingAlert"
|
|
class="booking-page__alert"
|
|
>
|
|
<image
|
|
class="booking-page__alert-icon"
|
|
src="https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/clock1.png"
|
|
mode="aspectFit"
|
|
/>
|
|
<text class="booking-page__alert-text">{{ upcomingAlert }}</text>
|
|
</view>
|
|
|
|
<view
|
|
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">
|
|
<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 v-if="!displayedBookings.length" class="booking-page__empty">
|
|
<text class="booking-page__empty-text">
|
|
{{ activeTab === 'ongoing' ? '暂无进行中的预约' : '暂无历史预约' }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
import MemberInfoSubNav from '@/components/memberInfo/MemberInfoSubNav.vue'
|
|
import { PAGE, navigateToPage, backToMemberCenter } from '@/common/constants/routes.js'
|
|
import {
|
|
cancelOngoingBooking,
|
|
formatUpcomingAlert
|
|
} from '@/common/memberInfo/store.js'
|
|
import { getMemberId } from '@/utils/request.js'
|
|
import { canCancelBooking, canSigninCourse } from '@/common/memberInfo/bookingStore.js'
|
|
import { signinGroupCourse, cancelBooking as cancelBookingApi } from '@/api/main.js'
|
|
|
|
const tabs = ref([])
|
|
const ongoingBookings = ref([])
|
|
const historyBookings = ref([])
|
|
const activeTab = ref('ongoing')
|
|
|
|
const displayedBookings = computed(() => {
|
|
return activeTab.value === 'ongoing'
|
|
? ongoingBookings.value
|
|
: historyBookings.value
|
|
})
|
|
|
|
const upcomingAlert = computed(() => {
|
|
return formatUpcomingAlert(ongoingBookings.value[0])
|
|
})
|
|
|
|
function refreshFromStore() {
|
|
const store = loadMemberStore()
|
|
ongoingBookings.value = store.ongoingBookings.map((item) => ({
|
|
...item,
|
|
canCancel: canCancelBooking(item),
|
|
canSignin: canSigninCourse(item)
|
|
}))
|
|
historyBookings.value = store.historyBookings.map((item) => ({ ...item }))
|
|
}
|
|
|
|
function goBack() {
|
|
backToMemberCenter()
|
|
}
|
|
|
|
function goCourseList() {
|
|
navigateToPage(PAGE.COURSE_LIST)
|
|
}
|
|
|
|
function setActiveTab(tab) {
|
|
activeTab.value = tab
|
|
}
|
|
|
|
// 签到团课
|
|
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' })
|
|
// 更新本地状态
|
|
refreshFromStore()
|
|
} 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) {
|
|
if (!canCancelBooking(item)) {
|
|
uni.showToast({ title: '距开课不足2小时,无法取消', icon: 'none' })
|
|
return
|
|
}
|
|
uni.showModal({
|
|
title: '取消预约',
|
|
content: `确定要取消「${item.title}」吗?`,
|
|
success: async (res) => {
|
|
if (!res.confirm) return
|
|
|
|
uni.showLoading({ title: '取消中...', mask: true })
|
|
try {
|
|
// 调用后端API取消预约
|
|
const result = await cancelBookingApi(Number(item.id))
|
|
uni.hideLoading()
|
|
|
|
if (result.code === 200 || result.success) {
|
|
// 更新本地状态
|
|
const store = loadMemberStore()
|
|
const localResult = cancelOngoingBooking(store, item.id)
|
|
if (localResult.ok) {
|
|
refreshFromStore()
|
|
}
|
|
uni.showToast({ title: '已取消', icon: 'success' })
|
|
} else {
|
|
uni.showToast({ title: result.message || '取消失败', icon: 'none' })
|
|
}
|
|
} catch (e) {
|
|
uni.hideLoading()
|
|
console.error('[booking] 取消预约失败:', e)
|
|
uni.showToast({ title: e.message || '取消失败', icon: 'none' })
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
refreshFromStore()
|
|
</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-sub-nav.css';
|
|
@import '@/common/style/memberInfo/member-info-tap.css';
|
|
@import '@/common/style/memberInfo/pages/booking-page.css';
|
|
</style>
|