style:完善主页动画样式

This commit is contained in:
2026-07-15 18:32:19 +08:00
parent 4e1bf77cb2
commit cb9928936b
3 changed files with 464 additions and 80 deletions
+255 -52
View File
@@ -4,9 +4,12 @@ import { ScrollTrigger } from 'gsap/ScrollTrigger'
const consultationOpen = ref(false)
const mobileMenuOpen = ref(false)
const activeSection = ref('home')
const headerScrolled = ref(false)
const notice = ref('')
const pageRoot = ref(null)
let animationContext
let animationMedia
const navItems = [
['首页', 'home'],
@@ -39,8 +42,22 @@ const stats = [
['65%', '客户转化链路效率平均提升', '/images/lanhu/slices/home-group103-image29@2x.png']
]
const corePoints = [
['曝光', '打造AI搜索语境下的品牌信任声量,一次布局 长期复用'],
['获客', '流量,让每一分投入都有回报 让曝光直接落地为可跟进的精准线索,告别无效']
['曝光', '打造AI搜索语境下的品牌信任声量,一次布局长期复用'],
['获客', '让曝光直接沉淀为可跟进的精准线索,告别无效流量']
]
const ringModels = [
{
title: '声量资产环',
image: '/images/lanhu/slices/home-group80-group83-image11@2x.png',
icon: 'asset',
labels: ['真人内容生产', '全域媒体分发', 'AI信任构建', '品牌资产沉淀']
},
{
title: '获客转化环',
image: '/images/lanhu/slices/home-group86-group87-image12@2x.png',
icon: 'conversion',
labels: ['AI搜索曝光', '精准流量承接', '全链路转化', '数据复盘优化']
}
]
const advantageCards = [
['全域媒体资源优势', '5000+权威官媒+10万+优质自媒体矩阵'],
@@ -177,60 +194,228 @@ onMounted(async () => {
reveal('.testimonial-track article', '.testimonial-track', { y: 22, stagger: 0.1 })
reveal('.cta-content', '.cta-section', { y: 26 })
ScrollTrigger.matchMedia({
'(max-width: 760px)': () => {
gsap.to('.logo-track', {
xPercent: -50,
duration: 24,
ease: 'none',
repeat: -1
})
animationMedia = gsap.matchMedia()
gsap.to('.ring:first-child', {
x: -12,
rotation: -2,
ease: 'none',
scrollTrigger: {
trigger: '.model-layout',
start: 'top bottom',
end: 'bottom top',
scrub: 0.8
animationMedia.add('(max-width: 760px)', () => {
gsap.to('.logo-track', {
xPercent: -50,
duration: 24,
ease: 'none',
repeat: -1
})
gsap.to('.ring:first-child', {
x: -12,
rotation: -2,
ease: 'none',
scrollTrigger: {
trigger: '.model-layout',
start: 'top bottom',
end: 'bottom top',
scrub: 0.8
}
})
gsap.to('.ring:nth-child(2)', {
x: 12,
rotation: 2,
ease: 'none',
scrollTrigger: {
trigger: '.model-layout',
start: 'top bottom',
end: 'bottom top',
scrub: 0.8
}
})
gsap.to('.cta-art', {
yPercent: -6,
ease: 'none',
scrollTrigger: {
trigger: '.cta-section',
start: 'top bottom',
end: 'bottom top',
scrub: 0.8
}
})
})
animationMedia.add('(min-width: 761px)', () => {
const cleanup = []
const addListener = (element, event, handler) => {
element.addEventListener(event, handler)
cleanup.push(() => element.removeEventListener(event, handler))
}
ScrollTrigger.create({
start: 80,
end: 'max',
onToggle: ({ isActive }) => {
headerScrolled.value = isActive
}
})
navItems.forEach(([, target]) => {
const trigger = target === 'home' ? '.hero-section' : `#${target}`
if (!document.querySelector(trigger)) return
ScrollTrigger.create({
trigger,
start: 'top 48%',
end: 'bottom 48%',
onEnter: () => {
activeSection.value = target
},
onEnterBack: () => {
activeSection.value = target
}
})
})
gsap.to('.ring:nth-child(2)', {
x: 12,
rotation: 2,
ease: 'none',
scrollTrigger: {
trigger: '.model-layout',
start: 'top bottom',
end: 'bottom top',
scrub: 0.8
}
const hero = document.querySelector('.hero-section')
const heroCopy = document.querySelector('.hero-copy')
if (hero && heroCopy) {
const moveX = gsap.quickTo(heroCopy, 'x', { duration: 0.7, ease: 'power3.out' })
const moveY = gsap.quickTo(heroCopy, 'y', { duration: 0.7, ease: 'power3.out' })
addListener(hero, 'pointermove', (event) => {
const bounds = hero.getBoundingClientRect()
moveX(((event.clientX - bounds.left) / bounds.width - 0.5) * 16)
moveY(((event.clientY - bounds.top) / bounds.height - 0.5) * 10)
})
gsap.to('.cta-art', {
yPercent: -6,
ease: 'none',
scrollTrigger: {
trigger: '.cta-section',
start: 'top bottom',
end: 'bottom top',
scrub: 0.8
}
addListener(hero, 'pointerleave', () => {
moveX(0)
moveY(0)
})
}
document.querySelectorAll('.stat-item strong').forEach((element) => {
const finalText = element.textContent.trim()
const finalValue = Number.parseFloat(finalText)
const suffix = finalText.replace(/[\d.]/g, '')
const counter = { value: 0 }
ScrollTrigger.create({
trigger: element,
start: 'top 88%',
once: true,
onEnter: () => {
gsap.to(counter, {
value: finalValue,
duration: 1.45,
ease: 'power2.out',
onUpdate: () => {
element.textContent = `${Math.round(counter.value)}${suffix}`
}
})
}
})
})
document.querySelectorAll('.section-title').forEach((title) => {
ScrollTrigger.create({
trigger: title,
start: 'top 84%',
once: true,
onEnter: () => title.classList.add('is-visible')
})
})
const interactiveCards = document.querySelectorAll(
'.stat-item, .advantage-grid article, .case-panel article, .industry-grid article, .delivery-grid article'
)
interactiveCards.forEach((card) => {
card.classList.add('motion-card')
const image = card.querySelector('img')
const imageX = image
? gsap.quickTo(image, 'x', { duration: 0.45, ease: 'power3.out' })
: null
const imageY = image
? gsap.quickTo(image, 'y', { duration: 0.45, ease: 'power3.out' })
: null
addListener(card, 'pointermove', (event) => {
const bounds = card.getBoundingClientRect()
const x = event.clientX - bounds.left
const y = event.clientY - bounds.top
card.style.setProperty('--spot-x', `${x}px`)
card.style.setProperty('--spot-y', `${y}px`)
gsap.to(card, { '--spot-opacity': 1, duration: 0.2 })
imageX?.((x / bounds.width - 0.5) * 9)
imageY?.((y / bounds.height - 0.5) * 9)
})
addListener(card, 'pointerleave', () => {
gsap.to(card, { '--spot-opacity': 0, duration: 0.35 })
imageX?.(0)
imageY?.(0)
})
cleanup.push(() => card.classList.remove('motion-card'))
})
gsap.to('.model-layout .ring:first-child', {
x: -34,
rotation: -4,
ease: 'none',
scrollTrigger: {
trigger: '.model-layout',
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
})
gsap.to('.model-layout .ring:nth-child(2)', {
x: 34,
rotation: 4,
ease: 'none',
scrollTrigger: {
trigger: '.model-layout',
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
})
gsap.to('.case-background', {
xPercent: -4,
scale: 1.08,
ease: 'none',
scrollTrigger: {
trigger: '.cases-section',
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
})
gsap.to('.delivery-banner', {
backgroundPosition: '50% 62%',
ease: 'none',
scrollTrigger: {
trigger: '.delivery-section',
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
})
gsap.to('.testimonial-track', {
x: -720,
ease: 'none',
scrollTrigger: {
trigger: '.testimonial-section',
start: 'top bottom',
end: 'bottom top',
scrub: 1
}
})
return () => cleanup.forEach((dispose) => dispose())
})
}, pageRoot.value)
})
onBeforeUnmount(() => animationContext?.revert())
onBeforeUnmount(() => {
animationMedia?.revert()
animationContext?.revert()
})
</script>
<template>
<main id="home" ref="pageRoot" class="site-shell">
<header class="site-header">
<header class="site-header" :class="{ scrolled: headerScrolled }">
<div class="page-width header-inner">
<a href="#home" class="brand" aria-label="大马棒首页"
><img
@@ -241,7 +426,7 @@ onBeforeUnmount(() => animationContext?.revert())
<button
v-for="([label, target], index) in navItems"
:key="target"
:class="{ active: index === 0 }"
:class="{ active: target === activeSection }"
@click="scrollToSection(target)"
>
{{ label }}<span v-if="index > 0 && index !== 3" class="nav-chevron"></span>
@@ -340,16 +525,34 @@ onBeforeUnmount(() => animationContext?.revert())
></SectionTitle
>
<div class="model-layout">
<img
class="ring"
src="/images/lanhu/slices/home-group80-group83-image11@2x.png"
alt="声量资产环"
/>
<img
class="ring"
src="/images/lanhu/slices/home-group86-group87-image12@2x.png"
alt="获客转化环"
/>
<div
v-for="model in ringModels"
:key="model.title"
class="ring ring-diagram"
role="img"
:aria-label="model.title"
>
<img class="ring-base" :src="model.image" alt="" />
<span
v-for="(label, labelIndex) in model.labels"
:key="label"
:class="`ring-label ring-label-${labelIndex + 1}`"
>{{ label }}</span
>
<div class="ring-center-label">
<svg v-if="model.icon === 'asset'" viewBox="0 0 48 48" aria-hidden="true">
<path d="M10 38V16l14-7 14 7v22H10Z" />
<path d="M17 21h4m6 0h4m-14 7h4m6 0h4M22 38v-6h4v6" />
</svg>
<svg v-else viewBox="0 0 48 48" aria-hidden="true">
<circle cx="15" cy="17" r="6" />
<circle cx="34" cy="14" r="5" />
<circle cx="31" cy="34" r="6" />
<path d="m20 18 9-3m2 4v9m-5 4-7-10" />
</svg>
<strong>{{ model.title }}</strong>
</div>
</div>
<div class="core-value">
<h3>
<img src="/images/lanhu/slices/home-group88-image13@2x.png" alt="" />业务核心价值点