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:
richarjiang
2026-04-02 14:35:17 +08:00
parent 554fc30954
commit 3a29aca0db
26 changed files with 7766 additions and 74 deletions

View File

@@ -0,0 +1,118 @@
<template>
<view class="brand-banner" :style="bannerStyle">
<!-- Status bar spacer -->
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }" />
<!-- Nav area -->
<view class="nav-bar">
<view class="studio-name-row">
<image
v-if="studioInfo?.logo"
class="logo"
:src="studioInfo.logo"
mode="aspectFit"
/>
<text class="studio-name">{{ studioInfo?.name || '普拉提工作室' }}</text>
</view>
<text class="studio-slogan">专业 · 精致 · 健康</text>
</view>
<!-- Decorative circles -->
<view class="deco-circle deco-circle--1" />
<view class="deco-circle deco-circle--2" />
</view>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import type { StudioConfig } from '@mp-pilates/shared'
const props = defineProps<{
studioInfo: StudioConfig | null
}>()
const statusBarHeight = ref(0)
onMounted(() => {
const sysInfo = uni.getSystemInfoSync()
statusBarHeight.value = sysInfo.statusBarHeight ?? 20
})
const bannerStyle = computed(() => {
if (props.studioInfo?.bannerUrl) {
return {
backgroundImage: `url(${props.studioInfo.bannerUrl})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}
}
return {}
})
</script>
<style lang="scss" scoped>
.brand-banner {
position: relative;
width: 100%;
min-height: 300rpx;
background: linear-gradient(135deg, #1a1a2e 0%, #2d2d5e 50%, #1a1a2e 100%);
overflow: hidden;
padding-bottom: 40rpx;
}
.nav-bar {
position: relative;
z-index: 2;
padding: 16rpx 40rpx 0;
}
.studio-name-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.logo {
width: 64rpx;
height: 64rpx;
border-radius: 12rpx;
}
.studio-name {
font-size: 44rpx;
font-weight: 700;
color: #ffffff;
letter-spacing: 2rpx;
}
.studio-slogan {
display: block;
margin-top: 10rpx;
font-size: 24rpx;
color: #c9a87c;
letter-spacing: 6rpx;
}
/* Decorative blurred circles */
.deco-circle {
position: absolute;
border-radius: 50%;
opacity: 0.12;
background: #c9a87c;
}
.deco-circle--1 {
width: 300rpx;
height: 300rpx;
top: -80rpx;
right: -60rpx;
}
.deco-circle--2 {
width: 180rpx;
height: 180rpx;
bottom: -40rpx;
right: 120rpx;
opacity: 0.08;
}
</style>