perf: 优化我的课程 UI
This commit is contained in:
288
packages/app/src/components/SessionRow.vue
Normal file
288
packages/app/src/components/SessionRow.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user