feat: 支持新版的首页 UI

This commit is contained in:
richarjiang
2026-10-25 21:15:15 +08:00
parent 4ca32c22f9
commit d93cc921db
30 changed files with 827 additions and 223 deletions

View File

@@ -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>