- CardShop: 采用 stale-while-revalidate 模式,仅首次加载显示骨架屏, 切换 tab 回来时保留旧数据静默刷新,消除列表闪烁 - UpcomingBooking: 补充 PENDING_CONFIRMATION 状态的中文映射和样式 - UpcomingBooking: 卡片点击跳转到预约详情页
240 lines
5.5 KiB
Vue
240 lines
5.5 KiB
Vue
<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>
|