style:完善主页动画样式
This commit is contained in:
+255
-52
@@ -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="" />业务核心价值点
|
||||
|
||||
Reference in New Issue
Block a user