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>
|
||||
|
||||
@@ -1,52 +1,22 @@
|
||||
<template>
|
||||
<view class="home-page">
|
||||
<!-- ① Brand Banner — fixed background layer -->
|
||||
<view class="home-page" :style="{ height: pageHeight }">
|
||||
<view class="banner-fixed">
|
||||
<BrandBanner :studio-info="studioStore.studioInfo" />
|
||||
</view>
|
||||
|
||||
<!-- Pull-to-refresh wrapper — scrollable foreground -->
|
||||
<scroll-view
|
||||
class="page-scroll"
|
||||
scroll-y
|
||||
:scroll-top="scrollTop"
|
||||
:scroll-with-animation="true"
|
||||
:refresher-enabled="true"
|
||||
:refresher-triggered="refreshing"
|
||||
@refresherrefresh="handleRefresh"
|
||||
@refresherrestore="refreshing = false"
|
||||
>
|
||||
<!-- Transparent spacer to reveal Banner behind -->
|
||||
<scroll-view class="page-scroll" scroll-y :scroll-into-view="scrollTarget"
|
||||
scroll-with-animation refresher-enabled :refresher-triggered="refreshing"
|
||||
@refresherrefresh="handleRefresh" @refresherrestore="refreshing = false">
|
||||
<view class="banner-spacer" />
|
||||
|
||||
<!-- Floating card with rounded top corners -->
|
||||
<view class="floating-card">
|
||||
<!-- Drag indicator -->
|
||||
<view class="card-handle">
|
||||
<view class="card-handle-bar" />
|
||||
</view>
|
||||
|
||||
<!-- ② Studio Info (photo strip + address/phone) -->
|
||||
<StudioInfo :studio-info="studioStore.studioInfo" />
|
||||
|
||||
<!-- ③ Quick Entry (login / trial / book / renew) -->
|
||||
<view class="card-handle"><view class="card-handle-bar" /></view>
|
||||
<QuickEntry @scroll-to-card-shop="scrollToCardShop" />
|
||||
|
||||
<!-- ④ Upcoming Bookings -->
|
||||
<UpcomingBooking />
|
||||
|
||||
<!-- ④.5 Flash Sale Section -->
|
||||
<StudioInfo :studio-info="studioStore.studioInfo" />
|
||||
<FlashSaleSection ref="flashSaleRef" />
|
||||
|
||||
<!-- ⑤ Card Shop (vertical list) -->
|
||||
<view :id="cardShopAnchorId">
|
||||
<CardShop ref="cardShopRef" />
|
||||
</view>
|
||||
|
||||
<!-- ⑥ About (teacher + studio gallery) -->
|
||||
<AboutSection />
|
||||
|
||||
<!-- Bottom padding for tab bar -->
|
||||
<view class="bottom-padding" />
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -55,7 +25,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, nextTick, onUnmounted } from 'vue'
|
||||
import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
import { onShow, onResize, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
|
||||
|
||||
import BrandBanner from '../../components/BrandBanner.vue'
|
||||
import StudioInfo from '../../components/StudioInfo.vue'
|
||||
@@ -94,7 +64,9 @@ const refreshing = ref(false)
|
||||
const cardShopRef = ref<InstanceType<typeof CardShop> | null>(null)
|
||||
const flashSaleRef = ref<InstanceType<typeof FlashSaleSection> | null>(null)
|
||||
const cardShopAnchorId = 'card-shop-anchor'
|
||||
const scrollTop = ref(0)
|
||||
const scrollTarget = ref('')
|
||||
const pageHeight = ref(`${uni.getWindowInfo().windowHeight}px`)
|
||||
onResize(() => { pageHeight.value = `${uni.getWindowInfo().windowHeight}px` })
|
||||
const pendingScrollToCardShop = ref(false)
|
||||
|
||||
// Listen for cross-page scroll request (e.g. from booking page "去购买")
|
||||
@@ -132,8 +104,10 @@ async function refreshData() {
|
||||
await Promise.allSettled(tasks)
|
||||
|
||||
// Also refresh card shop and flash sales
|
||||
cardShopRef.value?.fetchCardTypes()
|
||||
flashSaleRef.value?.fetchFlashSales()
|
||||
await Promise.allSettled([
|
||||
cardShopRef.value?.fetchCardTypes(),
|
||||
flashSaleRef.value?.fetchFlashSales(),
|
||||
])
|
||||
}
|
||||
|
||||
async function handleRefresh() {
|
||||
@@ -145,78 +119,30 @@ async function handleRefresh() {
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToCardShop() {
|
||||
// Reset first so setting the same value still triggers scroll
|
||||
scrollTop.value = 0
|
||||
nextTick(() => {
|
||||
uni.createSelectorQuery()
|
||||
.select(`#${cardShopAnchorId}`)
|
||||
.boundingClientRect()
|
||||
.selectViewport()
|
||||
.scrollOffset((res) => {
|
||||
if (res) {
|
||||
scrollTop.value = (res as UniApp.NodeInfo).scrollTop ?? 0
|
||||
}
|
||||
})
|
||||
.exec()
|
||||
})
|
||||
async function scrollToCardShop() {
|
||||
scrollTarget.value = ''
|
||||
await nextTick()
|
||||
scrollTarget.value = cardShopAnchorId
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-page {
|
||||
position: relative;
|
||||
height: 100vh;
|
||||
background: #FAF8F5;
|
||||
}
|
||||
|
||||
/* Banner fixed behind everything */
|
||||
.banner-fixed {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* Scroll layer sits above banner */
|
||||
.page-scroll {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
flex: 1;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Transparent spacer lets banner peek through */
|
||||
.banner-spacer {
|
||||
height: 420rpx;
|
||||
}
|
||||
|
||||
/* Floating card that overlaps the banner */
|
||||
.home-page { position: relative; height: 100vh; background: #fbf9f6; }
|
||||
.banner-fixed { position: fixed; top: 0; left: 0; width: 100%; z-index: 0; }
|
||||
.page-scroll { position: relative; z-index: 1; height: 100%; }
|
||||
.banner-spacer { height: 420rpx; }
|
||||
.floating-card {
|
||||
position: relative;
|
||||
background: #FAF8F5;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
min-height: 100vh;
|
||||
padding-top: 12rpx;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* Small drag indicator at top of card */
|
||||
.card-handle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16rpx 0 8rpx;
|
||||
}
|
||||
|
||||
.card-handle-bar {
|
||||
width: 64rpx;
|
||||
height: 8rpx;
|
||||
border-radius: 4rpx;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.bottom-padding {
|
||||
height: 120rpx;
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.7);
|
||||
background: linear-gradient(180deg, rgba(251, 249, 246, 0.86), #fbf9f6 260rpx);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
backdrop-filter: blur(16px);
|
||||
box-shadow: 0 -4rpx 20rpx rgba(66, 54, 45, 0.06);
|
||||
}
|
||||
.card-handle { display: flex; justify-content: center; padding: 16rpx 0 8rpx; }
|
||||
.card-handle-bar { width: 64rpx; height: 8rpx; border-radius: 4rpx; background: rgba(112, 94, 79, 0.18); }
|
||||
.bottom-padding { height: 48rpx; }
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<template>
|
||||
<view class="profile-page">
|
||||
<!-- Custom nav bar (transparent, blends with UserCard gradient) -->
|
||||
<CustomNavBar title="我的" transparent />
|
||||
<view class="profile-page" :style="{ paddingTop: navBarHeight + 'px' }">
|
||||
<view class="profile-page__nav" :style="{ paddingTop: statusBarHeight + 'px' }">
|
||||
<view class="profile-page__nav-title">我的</view>
|
||||
</view>
|
||||
|
||||
<!-- User card -->
|
||||
<UserCard :logged-in="loggedIn" :has-profile="hasProfile" :user="user" :stats="stats" :memberships="memberships"
|
||||
:loading="loginLoading" :nav-bar-height="navBarHeight" @login="handleLogin" />
|
||||
|
||||
<PracticeActivityCard v-if="loggedIn" :key="userStore.token" :refresh-key="activityRefreshKey" />
|
||||
:loading="loginLoading" @login="handleLogin" @edit="goToInfo" />
|
||||
|
||||
<!-- Menu section: always visible -->
|
||||
<ProfileMenu
|
||||
@@ -18,7 +17,9 @@
|
||||
:invite-share-eligible="!!user?.inviteShareEligible"
|
||||
@clear-cache="handleClearCache"
|
||||
@require-login="handleLogin"
|
||||
/>
|
||||
>
|
||||
<PracticeActivityCard v-if="loggedIn" :key="userStore.token" :refresh-key="activityRefreshKey" />
|
||||
</ProfileMenu>
|
||||
|
||||
<!-- Logout button: only when logged in -->
|
||||
<view v-if="loggedIn" class="profile-page__logout-wrap">
|
||||
@@ -38,7 +39,6 @@ import { getErrorMessage } from '../../utils/auth'
|
||||
import PracticeActivityCard from '../../components/PracticeActivityCard.vue'
|
||||
import UserCard from '../../components/UserCard.vue'
|
||||
import ProfileMenu from '../../components/ProfileMenu.vue'
|
||||
import CustomNavBar from '../../components/CustomNavBar.vue'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const bookingStore = useBookingStore()
|
||||
@@ -47,7 +47,8 @@ const { upcomingBookings } = storeToRefs(bookingStore)
|
||||
|
||||
const activityRefreshKey = ref(0)
|
||||
const loginLoading = ref(false)
|
||||
const navBarHeight = ref(64)
|
||||
const navBarHeight = ref(getSystemLayout().navBarHeight)
|
||||
const statusBarHeight = getSystemLayout().statusBarHeight
|
||||
|
||||
const activeMembershipCount = computed(
|
||||
() => user.value?.activeMembershipCount ?? userStore.activeMemberships.length,
|
||||
@@ -107,12 +108,16 @@ async function handleLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
function goToInfo() {
|
||||
uni.navigateTo({ url: '/pages/profile/info' })
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
confirmText: '退出',
|
||||
confirmColor: '#ff4d4f',
|
||||
confirmColor: '#9c7a6e',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
userStore.logout()
|
||||
@@ -138,27 +143,10 @@ function handleClearCache() {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
|
||||
&__logout-wrap {
|
||||
margin: $spacing-xl $spacing-lg $spacing-xl;
|
||||
}
|
||||
|
||||
&__logout-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: $bg-card;
|
||||
color: $error-color;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: $radius-lg;
|
||||
text-align: center;
|
||||
|
||||
&::after {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
min-height: 100vh; box-sizing: border-box; background: #fbf9f6; padding-bottom: 36rpx;
|
||||
&__nav { position: fixed; top: 0; left: 0; right: 0; z-index: 101; background: #fbf9f6; }
|
||||
&__nav-title { height: 88rpx; display: flex; align-items: center; justify-content: center; font-size: 34rpx; font-weight: 500; color: #514943; }
|
||||
&__logout-wrap { margin: 28rpx 32rpx 0; }
|
||||
&__logout-btn { width: 100%; height: 80rpx; line-height: 80rpx; padding: 0; margin: 0; border: none; border-radius: 24rpx; background: #f2ede7; color: #91766a; font-size: 25rpx; font-weight: 400; &::after { border: none; } }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user