Files
guanwang/app/pages/index.vue

205 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import {
PhArrowRight as ArrowRight,
PhBuildings as Buildings,
PhCalendarBlank as CalendarBlank,
PhCaretDown as CaretDown,
PhCpu as Cpu,
PhEnvelopeSimple as EnvelopeSimple,
PhLeaf as Leaf,
PhMapPin as MapPin,
PhPhone as Phone,
PhSealCheck as SealCheck,
PhSparkle as Sparkle,
PhUsersThree as UsersThree,
} from '@phosphor-icons/vue'
const { site, isPreview } = await useSiteDocument()
const config = useRuntimeConfig()
const absoluteUrl = useAbsoluteUrl()
const publishedProducts = computed(() => site.value.products.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published'))
const news = computed(() => site.value.articles.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published').slice(0, 4))
const team = computed(() => site.value.team.filter(item => isPreview.value ? item.status !== 'offline' : item.status === 'published').slice(0, 4))
const hero = ref<HTMLElement>()
let heroFrame = 0
const absoluteHero = computed(() => absoluteUrl(site.value.hero.backgroundImage))
const contactPhoneHref = computed(() => /[待演示]/.test(site.value.contact.phone) ? '' : `tel:${site.value.contact.phone.replace(/\s/g, '')}`)
const contactEmailHref = computed(() => site.value.contact.email.includes('@') && !site.value.contact.email.includes('演示') ? `mailto:${site.value.contact.email}` : '')
const usesDefaultHero = computed(() => site.value.hero.backgroundImage === '/images/hero-bg-v3.webp')
const previewTo = (path: string) => isPreview.value ? { path, query: { preview: 'draft' } } : path
useSeoMeta({
title: () => site.value?.seo.title,
description: () => site.value?.seo.description,
ogTitle: () => site.value?.seo.title,
ogDescription: () => site.value?.seo.description,
ogImage: () => absoluteHero.value,
})
useHead(() => ({
link: usesDefaultHero.value ? [{ rel: 'preload', as: 'image', href: '/images/hero-bg-v3.avif', type: 'image/avif' }] : [],
script: [{ type: 'application/ld+json', textContent: JSON.stringify({ '@context': 'https://schema.org', '@type': 'Organization', name: site.value.brand.companyName, url: config.public.siteUrl, logo: absoluteUrl(site.value.brand.logo), description: site.value.seo.description, email: site.value.contact.email, telephone: site.value.contact.phone }) }],
}))
const philosophies = [
{ title: '觉察', subtitle: '看见自己,是改变的开始', icon: Sparkle },
{ title: '关系', subtitle: '关系是心灵的镜子', icon: UsersThree },
{ title: '成长', subtitle: '在觉察与实践中持续成长', icon: Leaf },
]
const updateHeroScroll = () => {
heroFrame = 0
hero.value?.style.setProperty('--hero-scroll', String(Math.min(1, window.scrollY / Math.max(window.innerHeight, 1))))
}
const onHeroScroll = () => { if (!heroFrame) heroFrame = requestAnimationFrame(updateHeroScroll) }
const onHeroPointer = (event: PointerEvent) => {
if (!hero.value) return
const bounds = hero.value.getBoundingClientRect()
hero.value.style.setProperty('--pointer-x', String((event.clientX - bounds.left) / bounds.width - 0.5))
hero.value.style.setProperty('--pointer-y', String((event.clientY - bounds.top) / bounds.height - 0.5))
}
onMounted(() => {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
updateHeroScroll()
window.addEventListener('scroll', onHeroScroll, { passive: true })
hero.value?.addEventListener('pointermove', onHeroPointer, { passive: true })
})
onBeforeUnmount(() => {
if (import.meta.client) window.removeEventListener('scroll', onHeroScroll)
hero.value?.removeEventListener('pointermove', onHeroPointer)
if (heroFrame) cancelAnimationFrame(heroFrame)
})
</script>
<template>
<main id="main-content" class="site-shell">
<section id="home" ref="hero" class="hero">
<picture class="hero-media" aria-hidden="true">
<source v-if="usesDefaultHero" srcset="/images/hero-bg-v3.avif" type="image/avif">
<source v-if="usesDefaultHero" srcset="/images/hero-bg-v3.webp" type="image/webp">
<img :src="site.hero.backgroundImage" alt="" width="1672" height="941" fetchpriority="high" decoding="async">
</picture>
<SiteHeader :logo="site.brand.logo" :brand-name="site.brand.name" :english-name="site.brand.englishName" />
<div class="hero-copy">
<h1><span>{{ site.hero.titleLine1 }}</span><span>{{ site.hero.titleLine2 }}</span></h1>
<p class="hero-subtitle">{{ site.hero.subtitle }}</p>
<a href="#about" class="primary-button">{{ site.hero.cta }} <ArrowRight :size="20" /></a>
</div>
<a class="scroll-cue" href="#about" aria-label="向下滚动了解更多">
<span>向下探索</span>
<CaretDown :size="22" />
</a>
<div class="hero-wave" aria-hidden="true" />
</section>
<RevealSection id="about" class="about section-pad">
<div class="section-heading centered">
<p class="eyebrow">ABOUT HUIYU</p>
<h2>{{ site.about.title }}</h2>
<p>{{ site.about.summary }}</p>
</div>
<div class="about-layout">
<div class="about-visual">
<img src="/images/hero-bg-v3.webp" alt="晨光中的湖面与远山" width="3344" height="1882" loading="lazy" decoding="async">
</div>
<div class="metric-grid">
<article v-for="(metric, index) in site.about.metrics" :key="metric.label" class="metric-card" :style="{ '--delay': `${index * 90}ms` }">
<component :is="index === 0 ? Leaf : index === 1 ? UsersThree : CalendarBlank" :size="28" weight="light" />
<AnimatedMetric :value="metric.value" />
<span>{{ metric.label }}</span>
<small>{{ metric.description }}</small>
</article>
</div>
</div>
</RevealSection>
<RevealSection id="teacher" class="teacher-section section-pad">
<div class="teacher-card">
<div class="teacher-portrait">
<img :src="site.teacher.portrait" :alt="site.teacher.name" width="536" height="804" loading="lazy" decoding="async">
</div>
<div class="teacher-copy">
<p class="eyebrow">CORE MENTOR</p>
<h2>{{ site.teacher.name }}</h2>
<p class="teacher-title">{{ site.teacher.title }}</p>
<p class="teacher-summary">{{ site.teacher.summary }}</p>
<div class="tag-list">
<span v-for="tag in site.teacher.tags" :key="tag">{{ tag }}</span>
</div>
<NuxtLink :to="previewTo('/luhui')" class="text-link">了解更多 <ArrowRight :size="18" /></NuxtLink>
</div>
</div>
</RevealSection>
<RevealSection class="philosophy section-pad">
<div class="section-heading centered">
<p class="eyebrow">PHILOSOPHY</p>
<h2>理念与方法</h2>
</div>
<div class="philosophy-grid">
<article v-for="(item, index) in philosophies" :key="item.title" class="philosophy-card">
<span class="philosophy-index">0{{ index + 1 }}</span>
<component :is="item.icon" :size="30" weight="light" />
<h3>{{ item.title }}</h3>
<p>{{ item.subtitle }}</p>
</article>
</div>
</RevealSection>
<RevealSection id="products" class="products section-pad">
<div class="section-heading split-heading">
<div><p class="eyebrow">COURSES & PRODUCTS</p><h2>课程与产品</h2></div>
<p>围绕家庭关系个人成长与日常实践提供清晰可持续的陪伴路径</p>
</div>
<div class="product-grid">
<NuxtLink v-for="product in publishedProducts" :key="product.slug" :to="previewTo(`/products/${product.slug}`)" class="product-card">
<img :src="product.image" :alt="product.title" width="1536" height="1024" loading="lazy" decoding="async">
<div><h3>{{ product.title }}</h3><p>{{ product.subtitle }}</p></div>
<ArrowRight :size="20" />
</NuxtLink>
</div>
</RevealSection>
<RevealSection id="official" class="official section-pad">
<div class="section-heading centered"><p class="eyebrow">OFFICIAL INFORMATION</p><h2>官方信息</h2></div>
<div class="official-grid">
<article><Buildings :size="32" weight="light" /><h3>公司主体</h3><p>统一维护并对外公示</p></article>
<article><UsersThree :size="32" weight="light" /><h3>官方老师</h3><p>认证身份与官方资料</p></article>
<article><SealCheck :size="32" weight="light" /><h3>官方账号</h3><p>官方平台与联系方式</p></article>
<article><Cpu :size="32" weight="light" /><h3>科技产品</h3><p>智能工具与服务入口</p></article>
</div>
<div v-if="site.channels.length" class="authority-links centered-links"><h2>官方渠道</h2><template v-for="channel in site.channels" :key="channel.name"><a v-if="channel.url" :href="channel.url" target="_blank" rel="noopener noreferrer">{{ channel.name }} · {{ channel.account }}</a><span v-else class="disabled-channel" aria-disabled="true">{{ channel.name }} · {{ channel.account }}</span></template></div>
</RevealSection>
<RevealSection v-if="team.length" class="team-section section-pad">
<div class="section-heading centered"><p class="eyebrow">COMPANION TEAM</p><h2>陪伴团队</h2></div>
<div class="team-grid compact-team"><article v-for="member in team" :key="member.slug"><img :src="member.avatar" :alt="member.name" width="1536" height="1024" loading="lazy" decoding="async"><div><h3>{{ member.name }}</h3><p class="member-role">{{ member.role }}</p><p>{{ member.summary }}</p></div></article></div>
<div class="center-action"><NuxtLink :to="previewTo('/team')" class="text-link">查看团队介绍 <ArrowRight :size="18" /></NuxtLink></div>
</RevealSection>
<RevealSection id="news" class="news-section section-pad">
<div class="news-panel">
<div class="news-feature">
<p class="eyebrow">BRAND NEWS</p>
<h2>品牌动态</h2>
<p>发布官方声明品牌进展与课程信息建立长期可信的官方信息源</p>
</div>
<div class="news-list">
<NuxtLink v-for="item in news" :key="item.slug" :to="previewTo(item.type === 'statement' ? `/statements/${item.slug}` : `/news/${item.slug}`)"><span>{{ item.title }}</span><time>{{ item.publishedAt }}</time><ArrowRight :size="18" /></NuxtLink>
</div>
</div>
</RevealSection>
<footer id="contact" class="site-footer">
<div class="footer-brand"><BrandLogo :src="site.brand.logo" :name="site.brand.name" :english-name="site.brand.englishName" /><p>{{ site.brand.subSlogan }}</p></div>
<div class="footer-contact">
<p><Phone :size="18" /><a v-if="contactPhoneHref" :href="contactPhoneHref">{{ site.contact.phone }}</a><span v-else>{{ site.contact.phone }}</span></p>
<p><EnvelopeSimple :size="18" /><a v-if="contactEmailHref" :href="contactEmailHref">{{ site.contact.email }}</a><span v-else>{{ site.contact.email }}</span></p>
<p><MapPin :size="18" /> {{ site.contact.address }}</p>
</div>
<div class="footer-meta"><span>{{ site.brand.copyright }}</span><span><NuxtLink to="/privacy">隐私政策</NuxtLink> · <NuxtLink to="/terms">服务条款</NuxtLink> · <FilingLinks :icp="site.brand.icp" :police-record="site.brand.policeRecord" /></span></div>
</footer>
</main>
</template>