feat: 优化首页、个人中心页面 UI
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<view class="booking-page">
|
||||
<view class="booking-page" :style="{ height: pageHeight }">
|
||||
<!-- ──────────── Status bar spacing ──────────── -->
|
||||
<view class="status-bar" :style="{ height: statusBarHeight }" />
|
||||
|
||||
@@ -10,15 +10,18 @@
|
||||
|
||||
<!-- ──────────── Date & period filters ──────────── -->
|
||||
<view class="filter-header">
|
||||
<DateSelector v-model="selectedDate" variant="booking" @select="onDateSelect" />
|
||||
<TimePeriodFilter v-model="selectedPeriod" variant="booking" @change="onPeriodChange" />
|
||||
<view class="calendar-heading">
|
||||
<text class="calendar-month">{{ selectedMonthLabel }}</text>
|
||||
<text class="calendar-hint">选择上课日期</text>
|
||||
</view>
|
||||
<DateSelector v-model="selectedDate" variant="soft" @select="onDateSelect" />
|
||||
<TimePeriodFilter v-model="selectedPeriod" variant="soft" @change="onPeriodChange" />
|
||||
</view>
|
||||
|
||||
<!-- ──────────── Slot list ──────────── -->
|
||||
<scroll-view
|
||||
class="slot-scroll"
|
||||
scroll-y
|
||||
:style="{ height: scrollHeight }"
|
||||
refresher-enabled
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh"
|
||||
@@ -37,12 +40,7 @@
|
||||
|
||||
<!-- Empty state -->
|
||||
<view v-else-if="filteredSlots.length === 0" class="empty-wrap">
|
||||
<view class="empty-illustration">
|
||||
<view class="empty-circle outer" />
|
||||
<view class="empty-circle inner" />
|
||||
<view class="empty-dot" />
|
||||
</view>
|
||||
<text class="empty-text">当日暂无可约时段</text>
|
||||
<text class="empty-text">这个时段还没有课程</text>
|
||||
<text class="empty-sub">请选择其他日期或时段查看</text>
|
||||
</view>
|
||||
|
||||
@@ -50,9 +48,8 @@
|
||||
<view v-else class="slot-list">
|
||||
<!-- Date summary -->
|
||||
<view class="date-summary">
|
||||
<text class="date-summary-text">
|
||||
共 {{ filteredSlots.length }} 个可选时段
|
||||
</text>
|
||||
<text class="date-summary-title">{{ selectedDayLabel }}</text>
|
||||
<text class="date-summary-text">{{ filteredSlots.length }} 个时段</text>
|
||||
</view>
|
||||
|
||||
<SlotCard
|
||||
@@ -82,7 +79,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
import { onResize, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
import type { TimeSlotWithBookingStatus, MembershipWithCardType } from '@mp-pilates/shared'
|
||||
import { BookingStatus, TIME_PERIODS } from '@mp-pilates/shared'
|
||||
import { useBookingStore } from '../../stores/booking'
|
||||
@@ -126,28 +123,26 @@ onShareTimeline(() => {
|
||||
|
||||
// ─── Layout ───────────────────────────────────────────────
|
||||
const statusBarHeight = ref('20px')
|
||||
const scrollHeight = ref('500px')
|
||||
// Heights of static elements above scroll-view (in rpx, converted to px)
|
||||
const PAGE_HEADER_RPX = 88 // title bar height
|
||||
const FILTER_HEADER_RPX = 240 // DateSelector + TimePeriodFilter
|
||||
const TABBAR_RPX = 100
|
||||
const pageHeight = ref('100vh')
|
||||
|
||||
function updateLayout() {
|
||||
const { statusBarHeight: statusBarPx, windowWidth } = getSystemLayout()
|
||||
const ratio = windowWidth / 750
|
||||
statusBarHeight.value = `${statusBarPx}px`
|
||||
|
||||
const headerPx = Math.round(PAGE_HEADER_RPX * ratio)
|
||||
const filterPx = Math.round(FILTER_HEADER_RPX * ratio)
|
||||
const tabbarPx = Math.round(TABBAR_RPX * ratio)
|
||||
|
||||
// scroll-view fills remaining space: window - statusBar - pageHeader - filters - tabbar
|
||||
const { windowHeight } = uni.getWindowInfo()
|
||||
const remaining = windowHeight - statusBarPx - headerPx - filterPx - tabbarPx
|
||||
scrollHeight.value = `${remaining}px`
|
||||
statusBarHeight.value = `${getSystemLayout().statusBarHeight}px`
|
||||
// The mini-program window already excludes its native tab bar. Flex layout
|
||||
// gives the remaining height to the list as the filters change size.
|
||||
pageHeight.value = `${uni.getWindowInfo().windowHeight}px`
|
||||
}
|
||||
|
||||
updateLayout()
|
||||
onResize(updateLayout)
|
||||
|
||||
const selectedMonthLabel = computed(() => {
|
||||
const [year, month] = selectedDate.value.split('-')
|
||||
return `${year} 年 ${Number(month)} 月`
|
||||
})
|
||||
const selectedDayLabel = computed(() => {
|
||||
const [, month, day] = selectedDate.value.split('-')
|
||||
return `${Number(month)} 月 ${Number(day)} 日的课程`
|
||||
})
|
||||
|
||||
// ─── Filtered slots ───────────────────────────────────────
|
||||
const filteredSlots = computed<TimeSlotWithBookingStatus[]>(() => {
|
||||
@@ -310,216 +305,33 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.booking-page {
|
||||
height: 100vh;
|
||||
background:
|
||||
radial-gradient(circle at top, rgba(255, 232, 218, 0.36), transparent 34%),
|
||||
linear-gradient(180deg, #fbf7f3 0%, #f6efea 100%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Status bar ───────────────────────────────────── */
|
||||
.status-bar {
|
||||
flex-shrink: 0;
|
||||
background: #fcfaf8;
|
||||
}
|
||||
|
||||
/* ── Page header ──────────────────────────────────── */
|
||||
.page-header {
|
||||
flex-shrink: 0;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fcfaf8;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #3a2e2a;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
/* ── Filter header ────────────────────────────────── */
|
||||
.filter-header {
|
||||
flex-shrink: 0;
|
||||
background: rgba(252, 250, 248, 0.96);
|
||||
box-shadow: 0 12rpx 30rpx rgba(120, 91, 79, 0.06);
|
||||
}
|
||||
|
||||
/* ── Scroll container ──────────────────────────────── */
|
||||
.slot-scroll {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ── Slot list ─────────────────────────────────────── */
|
||||
.slot-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 28rpx 0 0;
|
||||
}
|
||||
|
||||
/* ── Date summary ──────────────────────────────────── */
|
||||
.date-summary {
|
||||
padding: 0 24rpx 16rpx;
|
||||
}
|
||||
|
||||
.date-summary-text {
|
||||
font-size: 24rpx;
|
||||
color: #9d8b83;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Loading skeleton ──────────────────────────────── */
|
||||
.loading-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-card {
|
||||
height: 220rpx;
|
||||
border-radius: 26rpx;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 28rpx 48rpx;
|
||||
gap: 20rpx;
|
||||
margin: 0 24rpx;
|
||||
box-shadow: 0 16rpx 36rpx rgba(120, 91, 79, 0.08);
|
||||
}
|
||||
|
||||
.skeleton-time {
|
||||
width: 90rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 12rpx;
|
||||
background: linear-gradient(90deg, rgba(236, 225, 217, 0.9) 25%, rgba(248, 240, 233, 0.98) 50%, rgba(236, 225, 217, 0.9) 75%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.4s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.skeleton-body {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
.skeleton-title {
|
||||
width: 60%;
|
||||
height: 28rpx;
|
||||
border-radius: 8rpx;
|
||||
background: linear-gradient(90deg, rgba(236, 225, 217, 0.9) 25%, rgba(248, 240, 233, 0.98) 50%, rgba(236, 225, 217, 0.9) 75%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.4s infinite;
|
||||
}
|
||||
|
||||
.skeleton-sub {
|
||||
width: 40%;
|
||||
height: 20rpx;
|
||||
border-radius: 6rpx;
|
||||
background: linear-gradient(90deg, rgba(236, 225, 217, 0.9) 25%, rgba(248, 240, 233, 0.98) 50%, rgba(236, 225, 217, 0.9) 75%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.4s infinite;
|
||||
}
|
||||
|
||||
.skeleton-btn {
|
||||
width: 100rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(90deg, rgba(236, 225, 217, 0.9) 25%, rgba(248, 240, 233, 0.98) 50%, rgba(236, 225, 217, 0.9) 75%);
|
||||
background-size: 400% 100%;
|
||||
animation: shimmer 1.4s infinite;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Empty state ───────────────────────────────────── */
|
||||
.empty-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 40rpx 80rpx;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
/* Zen-inspired geometric illustration */
|
||||
.empty-illustration {
|
||||
position: relative;
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
margin-bottom: 56rpx;
|
||||
}
|
||||
|
||||
.empty-circle {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
&.outer {
|
||||
width: 180rpx;
|
||||
height: 180rpx;
|
||||
border: 2rpx solid rgba(192, 154, 137, 0.18);
|
||||
animation: breathe 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&.inner {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: linear-gradient(135deg, #f6e9e1 0%, #ddc1b4 50%, #b98f7d 100%);
|
||||
opacity: 0.6;
|
||||
animation: breathe 3s ease-in-out infinite 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-dot {
|
||||
position: absolute;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #a87d6c;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 32rpx;
|
||||
color: #6f605b;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-sub {
|
||||
font-size: 26rpx;
|
||||
color: #a18a82;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
@keyframes breathe {
|
||||
0%, 100% { transform: translate(-50%, -50%) scale(1); opacity: 0.6; }
|
||||
50% { transform: translate(-50%, -50%) scale(1.05); opacity: 0.4; }
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
50% { opacity: 0.5; transform: translate(-50%, -50%) scale(0.8); }
|
||||
}
|
||||
|
||||
/* ── Bottom spacer ─────────────────────────────────── */
|
||||
.scroll-bottom-spacer {
|
||||
height: 48rpx;
|
||||
.booking-page { height: 100vh; background: #fbf9f6; display: flex; flex-direction: column; overflow: hidden; }
|
||||
.status-bar { flex-shrink: 0; }
|
||||
.page-header { flex-shrink: 0; height: 88rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.page-title { font-size: 34rpx; font-weight: 500; color: #514943; }
|
||||
.filter-header { flex-shrink: 0; border-bottom: 1rpx solid #eee8e3; }
|
||||
.calendar-heading { display: flex; align-items: baseline; justify-content: space-between; padding: 24rpx 32rpx 16rpx; gap: 16rpx; }
|
||||
.calendar-month { font-size: 28rpx; font-weight: 500; color: #514943; }
|
||||
.calendar-hint { font-size: 22rpx; color: #8b817b; }
|
||||
.slot-scroll { flex: 1; height: 0; min-height: 0; width: 100%; box-sizing: border-box; }
|
||||
.slot-list { display: flex; flex-direction: column; padding-top: 28rpx; }
|
||||
.date-summary { display: flex; justify-content: space-between; align-items: baseline; gap: 16rpx; padding: 0 32rpx 20rpx; }
|
||||
.date-summary-title { font-size: 25rpx; color: #6f655e; }
|
||||
.date-summary-text { font-size: 22rpx; color: #8b817b; }
|
||||
.loading-wrap { display: flex; flex-direction: column; gap: 20rpx; padding: 28rpx 32rpx; }
|
||||
.skeleton-card { height: 200rpx; box-sizing: border-box; border-radius: 28rpx; background: #fff; display: flex; align-items: center; padding: 28rpx; gap: 20rpx; }
|
||||
.skeleton-time, .skeleton-title, .skeleton-sub, .skeleton-btn {
|
||||
border-radius: 10rpx;
|
||||
background: linear-gradient(90deg, #f0eae5 25%, #faf7f3 50%, #f0eae5 75%);
|
||||
background-size: 400% 100%; animation: shimmer 1.4s infinite;
|
||||
}
|
||||
.skeleton-time { width: 90rpx; height: 70rpx; flex-shrink: 0; }
|
||||
.skeleton-body { flex: 1; display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.skeleton-title { width: 80%; height: 28rpx; }
|
||||
.skeleton-sub { width: 60%; height: 20rpx; }
|
||||
.skeleton-btn { width: 110rpx; height: 60rpx; border-radius: 999rpx; flex-shrink: 0; }
|
||||
.empty-wrap { margin: 32rpx; padding: 64rpx 28rpx; border-radius: 28rpx; background: #f3efea; display: flex; flex-direction: column; align-items: center; gap: 14rpx; }
|
||||
.empty-text { font-size: 28rpx; color: #6f655e; font-weight: 400; }
|
||||
.empty-sub { font-size: 23rpx; color: #8b817b; text-align: center; line-height: 1.6; }
|
||||
.scroll-bottom-spacer { height: 28rpx; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user