289 lines
6.5 KiB
Vue
289 lines
6.5 KiB
Vue
<template>
|
||
<view
|
||
class="session"
|
||
:class="[
|
||
`session--${tone}`,
|
||
{ 'session--muted': muted, 'session--history': history },
|
||
]"
|
||
@tap="handleTap"
|
||
>
|
||
<!-- 左侧日期块 -->
|
||
<view class="session__date">
|
||
<text class="session__day">{{ dayNumber }}</text>
|
||
<text class="session__weekday">{{ weekdayLabel }}</text>
|
||
</view>
|
||
|
||
<!-- 中央分隔:虚线 -->
|
||
<view class="session__rail">
|
||
<view class="session__dot" />
|
||
<view class="session__line" />
|
||
</view>
|
||
|
||
<!-- 主体信息 -->
|
||
<view class="session__body">
|
||
<view class="session__top">
|
||
<text class="session__time">{{ startTime }}</text>
|
||
<text class="session__time-end">— {{ endTime }}</text>
|
||
</view>
|
||
|
||
<text class="session__membership">{{ cardName }}</text>
|
||
|
||
<view class="session__bottom">
|
||
<view class="session__status">
|
||
<view class="session__status-dot" />
|
||
<text class="session__status-text">{{ statusLabel }}</text>
|
||
</view>
|
||
<text v-if="!history" class="session__cancel" @tap.stop="handleCancel">取消预约</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed } from 'vue'
|
||
import type { BookingWithDetails } from '@mp-pilates/shared'
|
||
import { BookingStatus } from '@mp-pilates/shared'
|
||
import {
|
||
bookingStatusLabel,
|
||
bookingStatusStripeClass,
|
||
} from '../utils/booking-helpers'
|
||
|
||
const props = defineProps<{
|
||
booking: BookingWithDetails
|
||
/** 历史记录模式(更紧凑,隐藏取消按钮) */
|
||
history?: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
tap: [booking: BookingWithDetails]
|
||
cancel: [booking: BookingWithDetails]
|
||
}>()
|
||
|
||
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||
|
||
function parseDate(dateStr: string): Date {
|
||
const normalized = dateStr.slice(0, 10)
|
||
const [y, m, d] = normalized.split('-').map(Number)
|
||
return new Date(y, m - 1, d)
|
||
}
|
||
|
||
const date = computed(() => parseDate(props.booking.timeSlot.date))
|
||
|
||
const dayNumber = computed(() => {
|
||
const d = date.value.getDate()
|
||
return d < 10 ? `0${d}` : String(d)
|
||
})
|
||
|
||
const weekdayLabel = computed(() => weekdays[date.value.getDay()])
|
||
const startTime = computed(() => props.booking.timeSlot.startTime.slice(0, 5))
|
||
const endTime = computed(() => props.booking.timeSlot.endTime.slice(0, 5))
|
||
const cardName = computed(() => props.booking.membership?.cardType?.name || '会员卡')
|
||
const statusLabel = computed(() => bookingStatusLabel(props.booking.status))
|
||
const tone = computed(() => bookingStatusStripeClass(props.booking.status))
|
||
|
||
const muted = computed(() => {
|
||
return props.booking.status === BookingStatus.CANCELLED || props.booking.status === BookingStatus.NO_SHOW
|
||
})
|
||
|
||
function handleTap() {
|
||
emit('tap', props.booking)
|
||
}
|
||
|
||
function handleCancel() {
|
||
emit('cancel', props.booking)
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.session {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: stretch;
|
||
gap: 20rpx;
|
||
padding: 28rpx 24rpx;
|
||
margin: 0 32rpx 16rpx;
|
||
border-radius: 28rpx;
|
||
background: #fdfbf7;
|
||
border: 1rpx solid #ede5d8;
|
||
box-shadow:
|
||
0 1rpx 0 rgba(122, 99, 84, 0.02),
|
||
0 4rpx 16rpx rgba(122, 99, 84, 0.03);
|
||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||
|
||
&__date {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2rpx;
|
||
width: 80rpx;
|
||
flex-shrink: 0;
|
||
padding: 4rpx 0;
|
||
}
|
||
|
||
&__day {
|
||
font-family: "Songti SC", "STSong", "Noto Serif SC", "Source Han Serif SC", serif;
|
||
font-size: 44rpx;
|
||
font-weight: 500;
|
||
line-height: 1;
|
||
color: #3a322b;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
&__weekday {
|
||
font-size: 19rpx;
|
||
color: #a89d92;
|
||
letter-spacing: 1rpx;
|
||
margin-top: 4rpx;
|
||
}
|
||
|
||
&__rail {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0;
|
||
width: 14rpx;
|
||
flex-shrink: 0;
|
||
padding: 6rpx 0;
|
||
}
|
||
|
||
&__dot {
|
||
width: 10rpx;
|
||
height: 10rpx;
|
||
border-radius: 50%;
|
||
background: #6e8b7d;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
&__line {
|
||
width: 1rpx;
|
||
flex: 1;
|
||
background: linear-gradient(
|
||
to bottom,
|
||
rgba(155, 138, 117, 0.3) 0%,
|
||
rgba(155, 138, 117, 0) 100%
|
||
);
|
||
}
|
||
|
||
// 状态颜色
|
||
&--stripe--pending &__dot { background: #b8967c; }
|
||
&--stripe--confirmed &__dot { background: #6e8b7d; }
|
||
&--stripe--completed &__dot { background: #6e8b7d; }
|
||
&--stripe--cancelled &__dot { background: #c4a09a; }
|
||
&--stripe--noshow &__dot { background: #b89a93; }
|
||
|
||
&__body {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 6rpx;
|
||
padding: 4rpx 0;
|
||
}
|
||
|
||
&__top {
|
||
display: flex;
|
||
align-items: baseline;
|
||
gap: 6rpx;
|
||
}
|
||
|
||
&__time {
|
||
font-family: "Songti SC", "STSong", "Noto Serif SC", "Source Han Serif SC", serif;
|
||
font-size: 34rpx;
|
||
font-weight: 500;
|
||
color: #3a322b;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
&__time-end {
|
||
font-size: 22rpx;
|
||
color: #a89d92;
|
||
font-variant-numeric: tabular-nums;
|
||
}
|
||
|
||
&__membership {
|
||
font-size: 22rpx;
|
||
color: #786b61;
|
||
line-height: 1.5;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
&__bottom {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 12rpx;
|
||
margin-top: 6rpx;
|
||
}
|
||
|
||
&__status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8rpx;
|
||
}
|
||
|
||
&__status-dot {
|
||
width: 8rpx;
|
||
height: 8rpx;
|
||
border-radius: 50%;
|
||
background: #6e8b7d;
|
||
}
|
||
|
||
&__status-text {
|
||
font-size: 21rpx;
|
||
color: #786b61;
|
||
letter-spacing: 1rpx;
|
||
}
|
||
|
||
&--stripe--pending &__status-text { color: #957c65; }
|
||
&--stripe--pending &__status-dot { background: #b8967c; }
|
||
&--stripe--cancelled &__status-text { color: #a89d92; }
|
||
&--stripe--cancelled &__status-dot { background: #c4a09a; }
|
||
&--stripe--noshow &__status-text { color: #9c7a6e; }
|
||
&--stripe--noshow &__status-dot { background: #b89a93; }
|
||
|
||
&__cancel {
|
||
font-size: 22rpx;
|
||
color: #a89d92;
|
||
padding: 6rpx 0 6rpx 16rpx;
|
||
letter-spacing: 1rpx;
|
||
transition: color 0.2s ease;
|
||
}
|
||
|
||
&__cancel:active {
|
||
color: #9c7a6e;
|
||
}
|
||
|
||
// 已取消 / 未出席:弱化
|
||
&--muted {
|
||
background: #f5f0e8;
|
||
border-color: #e8e0d4;
|
||
|
||
.session__day {
|
||
color: #a89d92;
|
||
}
|
||
|
||
.session__time {
|
||
color: #a89d92;
|
||
}
|
||
|
||
.session__membership {
|
||
color: #a89d92;
|
||
}
|
||
|
||
.session__dot {
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
}
|
||
|
||
.session:active {
|
||
transform: scale(0.99);
|
||
box-shadow:
|
||
0 1rpx 0 rgba(122, 99, 84, 0.02),
|
||
0 2rpx 8rpx rgba(122, 99, 84, 0.04);
|
||
}
|
||
</style>
|