feat: 初始化慧遇书院官网一期
This commit is contained in:
204
app/pages/index.vue
Normal file
204
app/pages/index.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user