Files
mp-pilates/packages/app/src/components/UpcomingBooking.vue
richarjiang 54e30da003 fix(app): 优化首页会员卡闪烁和即将上课卡片交互
- CardShop: 采用 stale-while-revalidate 模式,仅首次加载显示骨架屏,
  切换 tab 回来时保留旧数据静默刷新,消除列表闪烁
- UpcomingBooking: 补充 PENDING_CONFIRMATION 状态的中文映射和样式
- UpcomingBooking: 卡片点击跳转到预约详情页
2026-04-10 11:29:09 +08:00

240 lines
5.5 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 v-if="bookingStore.upcomingBookings.length" class="upcoming-section">
<view class="section-header">
<text class="section-title">即将上课</text>
<text class="section-more" @tap="goToBookings">全部预约 </text>
</view>
<view
v-for="booking in displayedBookings"
:key="booking.id"
class="booking-card"
@tap="goToBookingDetail(booking.id)"
>
<!-- Date column -->
<view class="date-col">
<text class="date-day">{{ getDayNumber(booking.timeSlot.date) }}</text>
<text class="date-month">{{ getMonthLabel(booking.timeSlot.date) }}</text>
<text class="date-weekday">{{ getWeekdayLabel(booking.timeSlot.date) }}</text>
</view>
<!-- Divider -->
<view class="booking-divider" />
<!-- Details column -->
<view class="details-col">
<text class="card-name">{{ booking.membership.cardType.name }}</text>
<view class="time-row">
<text class="time-icon">🕐</text>
<text class="time-text">
{{ formatTime(booking.timeSlot.startTime) }} {{ formatTime(booking.timeSlot.endTime) }}
</text>
</view>
<view class="status-row">
<view class="status-dot" :class="statusDotClass(booking.status)" />
<text class="status-text" :class="statusTextClass(booking.status)">
{{ statusLabel(booking.status) }}
</text>
</view>
</view>
<!-- Right arrow -->
<text class="booking-arrow"></text>
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useBookingStore } from '../stores/booking'
import { getWeekdayLabel } from '../utils/format'
import { BookingStatus } from '@mp-pilates/shared'
const bookingStore = useBookingStore()
// Show at most 2 upcoming bookings
const displayedBookings = computed(() =>
bookingStore.upcomingBookings.slice(0, 2),
)
function getDayNumber(dateStr: string): string {
return new Date(dateStr).getDate().toString()
}
function getMonthLabel(dateStr: string): string {
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
return months[new Date(dateStr).getMonth()]
}
function formatTime(timeStr: string): string {
// timeStr might be "HH:mm:ss" or "HH:mm"
return timeStr.slice(0, 5)
}
function statusLabel(status: BookingStatus): string {
const map: Record<BookingStatus, string> = {
[BookingStatus.PENDING_CONFIRMATION]: '待确认',
[BookingStatus.CONFIRMED]: '已确认',
[BookingStatus.CANCELLED]: '已取消',
[BookingStatus.COMPLETED]: '已完成',
[BookingStatus.NO_SHOW]: '未出席',
}
return map[status] ?? status
}
function statusDotClass(status: BookingStatus): string {
if (status === BookingStatus.PENDING_CONFIRMATION) return 'dot--pending'
if (status === BookingStatus.CONFIRMED) return 'dot--confirmed'
if (status === BookingStatus.COMPLETED) return 'dot--completed'
if (status === BookingStatus.CANCELLED) return 'dot--cancelled'
return 'dot--default'
}
function statusTextClass(status: BookingStatus): string {
if (status === BookingStatus.PENDING_CONFIRMATION) return 'text--pending'
if (status === BookingStatus.CONFIRMED) return 'text--confirmed'
if (status === BookingStatus.COMPLETED) return 'text--completed'
if (status === BookingStatus.CANCELLED) return 'text--cancelled'
return ''
}
function goToBookings() {
uni.navigateTo({ url: '/pages/profile/bookings' })
}
function goToBookingDetail(id: string) {
uni.navigateTo({ url: `/pages/booking/detail?id=${id}` })
}
</script>
<style lang="scss" scoped>
.upcoming-section {
margin: 24rpx 24rpx 0;
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: 700;
color: #1a1a2e;
}
.section-more {
font-size: 26rpx;
color: $primary-dark;
}
.booking-card {
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
padding: 28rpx 28rpx;
display: flex;
align-items: center;
gap: 24rpx;
margin-bottom: 16rpx;
}
.date-col {
display: flex;
flex-direction: column;
align-items: center;
flex-shrink: 0;
width: 88rpx;
}
.date-day {
font-size: 52rpx;
font-weight: 800;
color: #1a1a2e;
line-height: 1;
}
.date-month {
font-size: 22rpx;
color: $primary-dark;
margin-top: 4rpx;
}
.date-weekday {
font-size: 22rpx;
color: #999;
margin-top: 4rpx;
}
.booking-divider {
width: 2rpx;
height: 80rpx;
background: #f0f0f0;
flex-shrink: 0;
}
.details-col {
flex: 1;
}
.card-name {
display: block;
font-size: 30rpx;
font-weight: 600;
color: #1a1a2e;
margin-bottom: 10rpx;
}
.time-row {
display: flex;
align-items: center;
gap: 8rpx;
margin-bottom: 10rpx;
}
.time-icon {
font-size: 24rpx;
}
.time-text {
font-size: 26rpx;
color: #555;
}
.status-row {
display: flex;
align-items: center;
gap: 8rpx;
}
.status-dot {
width: 12rpx;
height: 12rpx;
border-radius: 50%;
}
.dot--pending { background: #f39c12; }
.dot--confirmed { background: #27ae60; }
.dot--completed { background: #3498db; }
.dot--cancelled { background: #e74c3c; }
.dot--default { background: #999; }
.status-text {
font-size: 24rpx;
color: #999;
}
.text--pending { color: #f39c12; }
.text--confirmed { color: #27ae60; }
.text--completed { color: #3498db; }
.text--cancelled { color: #e74c3c; }
.booking-arrow {
font-size: 36rpx;
color: #ccc;
flex-shrink: 0;
}
</style>