feat: 支持新版的首页 UI
This commit is contained in:
@@ -1,126 +1,114 @@
|
||||
<template>
|
||||
<view class="brand-banner">
|
||||
<!-- Background image layer with blur -->
|
||||
<image
|
||||
class="banner-bg"
|
||||
:src="bannerImage"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<!-- Dark overlay for readability -->
|
||||
<view class="banner-overlay" />
|
||||
|
||||
<!-- Centered content -->
|
||||
<view class="banner-content">
|
||||
<!-- Circular logo -->
|
||||
<view class="logo-circle">
|
||||
<image
|
||||
v-if="logoImage"
|
||||
class="logo-img"
|
||||
:src="logoImage"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view v-else class="logo-placeholder">
|
||||
<text>{{ studioName.slice(0, 1) || 'F' }}</text>
|
||||
<view class="brand-banner" :class="{ 'is-entering': animate }" :style="layoutStyle">
|
||||
<view class="brand-nav">
|
||||
<view class="brand-identity">
|
||||
<view class="logo-circle">
|
||||
<image v-if="logoImage && !logoFailed" class="logo-img" :src="logoImage" mode="aspectFill" @error="logoFailed = true" />
|
||||
<text v-else class="logo-letter">{{ studioName.slice(0, 1) }}</text>
|
||||
</view>
|
||||
<view class="brand-copy">
|
||||
<text class="studio-name">{{ studioName }}</text>
|
||||
<text class="studio-category">普拉提私教</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- Studio name -->
|
||||
<text class="studio-name">{{ studioName }}</text>
|
||||
</view>
|
||||
<view class="welcome-copy">
|
||||
<text class="date-label">{{ moment.dateLabel }}</text>
|
||||
<text class="greeting" :class="{ 'greeting-long': greeting.length > 12 }">{{ greeting }}</text>
|
||||
<view class="welcome-note">
|
||||
<text v-if="visitorState === 'guest'">从了解身体开始,<text class="note-continuation">找到自己的练习节奏。</text></text>
|
||||
<text v-else-if="visitorState === 'member'">很高兴,今天又见到你。</text>
|
||||
<text v-else>今天,也留一点时间给自己。</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="studio-photo">
|
||||
<image v-if="!photoFailed" class="hero-image" :class="{ 'is-loaded': photoLoaded }" :src="heroImage"
|
||||
mode="aspectFill" :fade-show="false" :draggable="false" aria-label="晨光中的普拉提核心床"
|
||||
@load="photoLoaded = true" @error="photoFailed = true" />
|
||||
<view v-else class="photo-fallback"><text>给自己,一段专注的练习时光。</text></view>
|
||||
</view>
|
||||
<view class="daily-reminder">
|
||||
<view class="sun-icon" aria-hidden="true">
|
||||
<view class="sun-core" />
|
||||
<view v-for="ray in 4" :key="ray" class="sun-ray" :class="'ray-' + ray" />
|
||||
</view>
|
||||
<view class="reminder-copy">
|
||||
<text class="reminder-label">今日小提醒</text>
|
||||
<text class="reminder-text">{{ moment.reminder }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import type { StudioConfig } from '@mp-pilates/shared'
|
||||
import { getHomeGreeting, getHomeMoment, type HomeVisitorState } from '../utils/home'
|
||||
|
||||
const props = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
studioInfo: StudioConfig | null
|
||||
}>()
|
||||
now: number
|
||||
visitorState: HomeVisitorState
|
||||
nickname?: string | null
|
||||
statusBarHeight: number
|
||||
navigationHeight: number
|
||||
menuLeft: number
|
||||
animate?: boolean
|
||||
}>(), { animate: true, nickname: null })
|
||||
|
||||
const fallbackBannerImage = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/bannerBg.jpg'
|
||||
const fallbackLogoImage = 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/mp/images/logo.jpg'
|
||||
|
||||
const bannerImage = computed(() => props.studioInfo?.bannerUrl || fallbackBannerImage)
|
||||
const logoImage = computed(() => props.studioInfo?.logo || fallbackLogoImage)
|
||||
const studioName = computed(() => props.studioInfo?.name || 'Focus Core')
|
||||
// Bundled cover reproduces the approved design and is available without network.
|
||||
const heroImage = '/static/home/studio-morning.jpg'
|
||||
const logoFailed = ref(false)
|
||||
const photoLoaded = ref(false)
|
||||
const photoFailed = ref(false)
|
||||
const studioName = computed(() => props.studioInfo?.name?.trim() || 'Focus Core')
|
||||
const logoImage = computed(() => props.studioInfo?.logo || '')
|
||||
const moment = computed(() => getHomeMoment(new Date(props.now)))
|
||||
const greeting = computed(() => getHomeGreeting(moment.value.period, props.visitorState, props.nickname))
|
||||
const layoutStyle = computed(() => ({
|
||||
'--home-status-height': `${props.statusBarHeight}px`,
|
||||
'--home-navigation-height': `${props.navigationHeight}px`,
|
||||
'--home-brand-max-width': `${Math.max(100, props.menuLeft - 36)}px`,
|
||||
}))
|
||||
watch(logoImage, () => { logoFailed.value = false })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.brand-banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 580rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(160deg, #E1F4FA 0%, $primary-color 50%, $primary-dark 100%);
|
||||
}
|
||||
|
||||
.banner-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: blur(2px);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.banner-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba($primary-dark, 0.25);
|
||||
}
|
||||
|
||||
.banner-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding-top: 120rpx;
|
||||
}
|
||||
|
||||
.logo-circle {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.logo-placeholder {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 50%;
|
||||
font-size: 64rpx;
|
||||
font-weight: 800;
|
||||
color: #333;
|
||||
letter-spacing: 4rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.studio-name {
|
||||
margin-top: 28rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
letter-spacing: 6rpx;
|
||||
text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.3);
|
||||
.brand-banner { margin: 0 32rpx; padding-top: calc(var(--home-status-height) + var(--home-navigation-height) + 46rpx); border-bottom: 1rpx solid #e3ddd5; color: #403c36; }
|
||||
.brand-nav { position: fixed; top: 0; left: 0; right: 0; z-index: 20; box-sizing: border-box; padding: var(--home-status-height) 40rpx 0; height: calc(var(--home-status-height) + var(--home-navigation-height)); display: flex; align-items: center; background: #fbf9f6; }
|
||||
.brand-identity { display: flex; align-items: center; gap: 16rpx; max-width: var(--home-brand-max-width); min-width: 0; }
|
||||
.logo-circle { display: flex; flex-shrink: 0; align-items: center; justify-content: center; width: 76rpx; height: 76rpx; border-radius: 50%; overflow: hidden; background: #292a27; }
|
||||
.logo-img { width: 100%; height: 100%; }
|
||||
.logo-letter { color: #fffdf8; font-family: 'Baskerville', 'Times New Roman', serif; font-size: 49rpx; line-height: 1; }
|
||||
.brand-copy { min-width: 0; }
|
||||
.studio-name { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: 'Baskerville', 'Songti SC', serif; font-size: 34rpx; line-height: 1.2; color: #292a27; }
|
||||
.studio-category { display: block; margin-top: 4rpx; font-size: 20rpx; line-height: 1.4; color: #8b817b; letter-spacing: 1rpx; }
|
||||
.welcome-copy { padding: 0 8rpx; }
|
||||
.date-label { display: block; font-size: 24rpx; line-height: 1.6; color: #8b817b; }
|
||||
.greeting { display: block; margin-top: 18rpx; font-size: 64rpx; font-weight: 600; letter-spacing: -2rpx; line-height: 1.35; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
|
||||
.greeting-long { font-size: 54rpx; letter-spacing: -1rpx; }
|
||||
.welcome-note { margin-top: 18rpx; min-height: 92rpx; font-size: 32rpx; line-height: 1.55; color: #756a60; }
|
||||
.note-continuation { display: block; }
|
||||
.studio-photo { position: relative; width: 100%; height: 452rpx; margin-top: 20rpx; border-radius: 36rpx; overflow: hidden; background: #e9e1d5; }
|
||||
.hero-image { display: block; width: 100%; height: 100%; opacity: 0; transition: opacity .35s ease; }
|
||||
.hero-image.is-loaded { opacity: 1; }
|
||||
.photo-fallback { display: flex; align-items: center; justify-content: center; height: 100%; padding: 0 36rpx; box-sizing: border-box; color: #756a60; font-size: 28rpx; line-height: 1.8; }
|
||||
.daily-reminder { display: flex; align-items: center; gap: 24rpx; padding: 32rpx 10rpx 42rpx; }
|
||||
.reminder-copy { flex: 1; min-width: 0; }
|
||||
.reminder-label { display: block; color: #8b817b; font-size: 23rpx; line-height: 1.5; }
|
||||
.reminder-text { display: block; margin-top: 6rpx; color: #756a60; font-size: 25rpx; line-height: 1.65; overflow-wrap: anywhere; }
|
||||
.sun-icon { position: relative; flex-shrink: 0; width: 60rpx; height: 60rpx; color: #8c826f; }
|
||||
.sun-core { position: absolute; top: 18rpx; left: 18rpx; width: 24rpx; height: 24rpx; box-sizing: border-box; border: 2rpx solid currentColor; border-radius: 50%; }
|
||||
.sun-ray { position: absolute; top: 0; left: 29rpx; height: 60rpx; width: 2rpx; }
|
||||
.sun-ray::before, .sun-ray::after { position: absolute; left: 0; width: 2rpx; height: 10rpx; border-radius: 2rpx; background: currentColor; content: ''; }
|
||||
.sun-ray::before { top: 0; }.sun-ray::after { bottom: 0; }
|
||||
.ray-2 { transform: rotate(45deg); }.ray-3 { transform: rotate(90deg); }.ray-4 { transform: rotate(135deg); }
|
||||
.is-entering .welcome-copy { animation: welcome-in .52s cubic-bezier(.22, 1, .36, 1) both; }
|
||||
.is-entering .studio-photo { animation: welcome-in .6s .08s cubic-bezier(.22, 1, .36, 1) both; }
|
||||
.is-entering .daily-reminder { animation: welcome-in .5s .16s cubic-bezier(.22, 1, .36, 1) both; }
|
||||
@keyframes welcome-in { from { opacity: 0; transform: translateY(16rpx); } to { opacity: 1; transform: translateY(0); } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.is-entering .welcome-copy, .is-entering .studio-photo, .is-entering .daily-reminder { animation: none; }
|
||||
.hero-image { transition: none; }
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user