feat(app): implement home, booking, and profile pages
Home: brand banner, studio info swiper, smart quick entries based on membership status, upcoming bookings, card shop horizontal scroll Booking: 7-day date selector, time period filter, slot cards with status, booking confirm popup with membership picker Profile: user card with login, training stats, menu with admin entry 8 reusable components: BrandBanner, StudioInfo, QuickEntry, UpcomingBooking, CardShop, DateSelector, SlotCard, BookingConfirmPopup, TimePeriodFilter, UserCard, TrainingStats, ProfileMenu
This commit is contained in:
229
packages/app/src/components/UpcomingBooking.vue
Normal file
229
packages/app/src/components/UpcomingBooking.vue
Normal file
@@ -0,0 +1,229 @@
|
||||
<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"
|
||||
>
|
||||
<!-- 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.CONFIRMED]: '已确认',
|
||||
[BookingStatus.CANCELLED]: '已取消',
|
||||
[BookingStatus.COMPLETED]: '已完成',
|
||||
[BookingStatus.NO_SHOW]: '未出席',
|
||||
}
|
||||
return map[status] ?? status
|
||||
}
|
||||
|
||||
function statusDotClass(status: BookingStatus): string {
|
||||
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.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' })
|
||||
}
|
||||
</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: #c9a87c;
|
||||
}
|
||||
|
||||
.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: #c9a87c;
|
||||
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--confirmed { background: #27ae60; }
|
||||
.dot--completed { background: #3498db; }
|
||||
.dot--cancelled { background: #e74c3c; }
|
||||
.dot--default { background: #999; }
|
||||
|
||||
.status-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.text--confirmed { color: #27ae60; }
|
||||
.text--completed { color: #3498db; }
|
||||
.text--cancelled { color: #e74c3c; }
|
||||
|
||||
.booking-arrow {
|
||||
font-size: 36rpx;
|
||||
color: #ccc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user