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:
203
packages/app/src/components/StudioInfo.vue
Normal file
203
packages/app/src/components/StudioInfo.vue
Normal file
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<view class="studio-info card">
|
||||
<!-- Photo Swiper -->
|
||||
<swiper
|
||||
v-if="studioInfo?.photos?.length"
|
||||
class="photo-swiper"
|
||||
:indicator-dots="studioInfo.photos.length > 1"
|
||||
:autoplay="true"
|
||||
:interval="4000"
|
||||
:duration="500"
|
||||
indicator-color="rgba(255,255,255,0.5)"
|
||||
indicator-active-color="#c9a87c"
|
||||
circular
|
||||
>
|
||||
<swiper-item
|
||||
v-for="(photo, idx) in studioInfo.photos"
|
||||
:key="idx"
|
||||
>
|
||||
<image
|
||||
class="photo"
|
||||
:src="photo"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- Placeholder when no photos -->
|
||||
<view v-else class="photo-placeholder">
|
||||
<text class="placeholder-icon">🏃</text>
|
||||
<text class="placeholder-text">专业普拉提工作室</text>
|
||||
</view>
|
||||
|
||||
<!-- Info rows -->
|
||||
<view class="info-rows">
|
||||
<!-- Address -->
|
||||
<view class="info-row" @tap="handleAddressTap">
|
||||
<view class="icon-wrap">
|
||||
<text class="iconfont">📍</text>
|
||||
</view>
|
||||
<text class="info-text address-text">
|
||||
{{ studioInfo?.address || '地址加载中…' }}
|
||||
</text>
|
||||
<text class="info-action">
|
||||
{{ studioInfo?.latitude ? '导航' : '复制' }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- Phone -->
|
||||
<view class="info-row" @tap="handlePhoneTap">
|
||||
<view class="icon-wrap">
|
||||
<text class="iconfont">📞</text>
|
||||
</view>
|
||||
<text class="info-text">{{ studioInfo?.phone || '—' }}</text>
|
||||
<text class="info-action">拨打</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { StudioConfig } from '@mp-pilates/shared'
|
||||
|
||||
const props = defineProps<{
|
||||
studioInfo: StudioConfig | null
|
||||
}>()
|
||||
|
||||
function handleAddressTap() {
|
||||
if (!props.studioInfo) return
|
||||
|
||||
const { latitude, longitude, address, name } = props.studioInfo
|
||||
|
||||
if (latitude && longitude) {
|
||||
uni.openLocation({
|
||||
latitude,
|
||||
longitude,
|
||||
name: name || '普拉提工作室',
|
||||
address: address,
|
||||
fail() {
|
||||
copyAddress()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
copyAddress()
|
||||
}
|
||||
}
|
||||
|
||||
function copyAddress() {
|
||||
const address = props.studioInfo?.address
|
||||
if (!address) return
|
||||
uni.setClipboardData({
|
||||
data: address,
|
||||
success() {
|
||||
uni.showToast({ title: '地址已复制', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handlePhoneTap() {
|
||||
const phone = props.studioInfo?.phone
|
||||
if (!phone) return
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: phone,
|
||||
fail() {
|
||||
uni.showToast({ title: '拨号失败', icon: 'none' })
|
||||
},
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.studio-info {
|
||||
margin: 24rpx 24rpx 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.photo-swiper {
|
||||
width: 100%;
|
||||
height: 360rpx;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.photo-placeholder {
|
||||
width: 100%;
|
||||
height: 280rpx;
|
||||
background: linear-gradient(135deg, #f0f0f0, #e8e8e8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
border-radius: 16rpx 16rpx 0 0;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
|
||||
.placeholder-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.info-rows {
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
gap: 16rpx;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-wrap {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.address-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.info-action {
|
||||
font-size: 24rpx;
|
||||
color: #c9a87c;
|
||||
padding: 6rpx 16rpx;
|
||||
border: 1rpx solid #c9a87c;
|
||||
border-radius: 24rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user