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

367 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<view class="scroll-container theme-light">
<view class="bt-page">
<PageHeader title="我的课程" subtitle="" :show-back="true">
<template #right>
<view class="header-scan-btn" @click="handleScan">
<text class="header-scan-icon">📷</text>
</view>
</template>
</PageHeader>
<view class="bt-page__body">
<!-- 子标签进行中 / 已完成 -->
<view class="mi-mod-subtabs">
<text
class="mi-mod-subtab"
:class="{ 'mi-mod-subtab--on': activeTab === 'ongoing' }"
@tap="activeTab = 'ongoing'"
>进行中</text>
<text
class="mi-mod-subtab"
:class="{ 'mi-mod-subtab--on': activeTab === 'completed' }"
@tap="activeTab = 'completed'"
>已完成</text>
</view>
<!-- 骨架屏 -->
<ListSkeleton v-if="loading" :count="4" layout="card" />
<!-- 预约列表 -->
<view
v-for="item in displayedBookings"
:key="item.id"
class="mi-mod-course-card"
hover-class="mi-tap-card--hover"
>
<image
class="mi-mod-course-card__banner"
:src="item.coverImage"
mode="aspectFill"
/>
<view class="mi-mod-course-card__content">
<view class="mi-mod-course-card__header">
<text class="mi-mod-course-card__title">{{ item.title }}</text>
<text class="mi-mod-course-card__status" :class="'mi-mod-course-card__status--' + item.statusClass">{{ item.statusLabel }}</text>
</view>
<text class="mi-mod-course-card__meta">{{ item.schedule }} · {{ item.location }}</text>
<!-- 仅已预约状态显示取消按钮 -->
<view v-if="activeTab === 'ongoing' && item.statusClass === 'booked'" class="mi-mod-course-card__cancel" @tap.stop="handleCancel(item)">
<text>取消预约</text>
</view>
</view>
</view>
<!-- 无数据 -->
<view v-if="!displayedBookings.length" class="bt-empty">
<text class="bt-empty__text">
{{ activeTab === 'ongoing' ? '暂无进行中的预约' : '暂无历史预约' }}
</text>
</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, qrSignInGroupCourse } from '@/api/groupCourse.js'
import { cancelBooking as cancelBookingApi } from '@/api/groupCourse.js'
const activeTab = ref('ongoing')
const ongoingBookings = ref([])
const completedBookings = ref([])
const loading = ref(false)
const displayedBookings = computed(() => {
return activeTab.value === 'ongoing'
? ongoingBookings.value
: completedBookings.value
})
async function fetchBookings() {
if (loading.value) return
loading.value = true
console.log('[myCourses] 开始获取预约记录')
const memberId = getMemberId()
if (!memberId) {
console.warn('[myCourses] 未登录')
loading.value = false
return
}
try {
const res = await getMemberBookings(memberId)
const bookings = Array.isArray(res) ? res : (res?.data || res?.content || [])
console.log('[myCourses] 预约记录:', bookings.length, '条', JSON.stringify(bookings))
if (Array.isArray(bookings)) {
const mapped = bookings.map(mapBooking)
ongoingBookings.value = mapped.filter(b => b.status === 'ongoing')
completedBookings.value = mapped.filter(b => b.status === 'completed')
} else {
ongoingBookings.value = []
completedBookings.value = []
}
} catch (e) {
console.error('[myCourses] 获取预约记录失败:', e)
ongoingBookings.value = []
completedBookings.value = []
} finally {
loading.value = false
}
}
function mapBooking(b) {
const courseStartTime = b.courseStartTime || b.startTime || ''
const bookStatus = String(b.status || '0')
// status: 0-已预约,1-已取消,2-已出席,3-缺席
let status = 'ongoing'
let statusLabel = '已预约'
let statusClass = 'booked'
if (bookStatus === '1') {
status = 'completed'
statusLabel = '已取消'
statusClass = 'cancelled'
} else if (bookStatus === '2') {
// 已签到保留在进行中,方便用户查看签到结果
status = 'ongoing'
statusLabel = '已签到'
statusClass = 'attended'
} else if (bookStatus === '3') {
status = 'ongoing'
statusLabel = '缺席'
statusClass = 'absent'
} else if (bookStatus === '0') {
status = 'ongoing'
statusLabel = '已预约'
statusClass = 'booked'
}
return {
id: b.id,
courseId: b.courseId,
title: b.courseName || '预约课程',
status: status,
originalStatus: bookStatus,
statusLabel: statusLabel,
statusClass: statusClass,
schedule: formatBookingTime(courseStartTime),
location: b.location || '',
coverImage: getCourseCoverUrl(b.coverImage) || 'https://gymfuture.oss-cn-chengdu.aliyuncs.com/static/images/default-course.png'
}
}
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 handleCancel(item) {
uni.showModal({
title: '取消预约',
content: `确定要取消「${item.title}」吗?`,
success: async (res) => {
if (!res.confirm) return
uni.showLoading({ title: '取消中...' })
try {
const result = await cancelBookingApi(Number(item.id), {
memberId: getMemberId()
})
uni.hideLoading()
if (result.success) {
uni.showToast({ title: '已取消', icon: 'success' })
fetchBookings()
} else {
uni.showToast({ title: result.message || '取消失败', icon: 'none', duration: 3000 })
}
} catch (e) {
uni.hideLoading()
console.error('[myCourses] 取消预约失败:', e)
const errMsg = e?.message || '取消失败'
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
}
}
})
}
// ===== 扫码签到 =====
function handleScan() {
uni.scanCode({
success: async (res) => {
const qrContent = res.result
console.log('[myCourses] 扫码结果:', qrContent)
// 解析二维码内容获取 courseId
let courseId = null
try {
const parsed = JSON.parse(qrContent)
courseId = Number(parsed.id || parsed.courseId)
} catch {
const num = Number(qrContent)
if (!isNaN(num)) {
courseId = num
}
}
if (!courseId) {
uni.showToast({ title: '无效的签到二维码', icon: 'none' })
return
}
const memberId = getMemberId()
if (!memberId) {
uni.showToast({ title: '请先登录', icon: 'none' })
return
}
uni.showLoading({ title: '签到中...' })
try {
const result = await qrSignInGroupCourse(courseId, memberId)
uni.hideLoading()
if (result.success) {
uni.showToast({ title: '签到成功', icon: 'success' })
// 签到成功后刷新列表
await fetchBookings()
} else {
uni.showToast({ title: result.message || '签到失败', icon: 'none', duration: 3000 })
}
} catch (e) {
uni.hideLoading()
console.error('[myCourses] 扫码签到失败:', e)
const errMsg = e?.message || e?.data?.message || '签到失败'
uni.showToast({ title: errMsg, icon: 'none', duration: 3000 })
}
},
fail: (err) => {
console.log('[myCourses] 扫码取消或失败:', err)
}
})
}
onMounted(() => {
fetchBookings()
})
</script>
<style>
@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/body-test-common.css';
@import '@/common/style/memberInfo/pages/module-pages-common.css';
.mi-mod-subtabs {
display: flex;
flex-direction: row;
gap: 16px;
margin-bottom: 12px;
}
.mi-mod-subtab {
font-size: 14px;
color: var(--text-muted, #5E6F8D);
}
.mi-mod-subtab--on {
color: var(--primary-dark, #0B2B4B);
font-weight: 700;
}
.mi-mod-course-card__cancel {
margin-top: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8rpx 24rpx;
border-radius: 16rpx;
background: rgba(255, 107, 53, 0.1);
border: 2rpx solid var(--accent-orange, #FF6B35);
}
.mi-mod-course-card__cancel text {
font-size: 24rpx;
font-weight: 700;
color: var(--accent-orange, #FF6B35);
}
/* 状态标签 */
.mi-mod-course-card__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16rpx;
}
.mi-mod-course-card__status {
font-size: 22rpx;
font-weight: 600;
padding: 4rpx 16rpx;
border-radius: 12rpx;
flex-shrink: 0;
white-space: nowrap;
}
.mi-mod-course-card__status--booked {
background: rgba(24, 144, 255, 0.1);
color: #1890ff;
border: 2rpx solid rgba(24, 144, 255, 0.2);
}
.mi-mod-course-card__status--attended {
background: rgba(82, 196, 26, 0.1);
color: #389e0d;
border: 2rpx solid rgba(82, 196, 26, 0.2);
}
.mi-mod-course-card__status--cancelled {
background: rgba(153, 153, 153, 0.1);
color: #999;
border: 2rpx solid rgba(153, 153, 153, 0.2);
}
.mi-mod-course-card__status--absent {
background: rgba(250, 173, 20, 0.1);
color: #d48806;
border: 2rpx solid rgba(250, 173, 20, 0.2);
}
/* 头部扫码按钮 */
.header-scan-btn {
width: 64rpx;
height: 64rpx;
background: rgba(24, 144, 255, 0.1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.header-scan-btn:active {
background: rgba(24, 144, 255, 0.2);
}
.header-scan-icon {
font-size: 32rpx;
}
</style>