feat: add customer service chat, optimize case and homepage, fix api proxy
这一提交完成了多项功能升级与优化: 1. 新增全局浮动客服组件,集成腾讯云TRTC聊天能力并添加错误格式化工具 2. 新增图片资源并调整部分静态文件结构 3. 优化案例页面:重构筛选逻辑、新增空状态处理、添加加载更多功能 4. 优化首页布局:修复统计数字动画、调整移动端适配样式、优化轮播逻辑 5. 重构官方内容获取工具:添加缓存机制、优化媒体资源处理 6. 优化API代理配置,移除默认本地代理并新增环境变量配置 7. 新增页面数据恢复刷新逻辑,优化页面交互体验 8. 修复按钮组件冗余代码,调整头部导航移动端样式
This commit is contained in:
+164
-112
@@ -7,12 +7,17 @@ import SiteFooter from '~/components/site/SiteFooter.vue'
|
||||
const pageRoot = ref(null)
|
||||
const { openConsultation: openGlobalConsultation } = useConsultation()
|
||||
const { fetchHomepageSections } = useOfficialContent()
|
||||
const SIMPLE_STAT_VALUE = /^(\d+(?:\.\d+)?)([+%]*)$/
|
||||
let animationContext
|
||||
let animationMedia
|
||||
let counterContext
|
||||
|
||||
const { data: homepageSectionRows } = await useAsyncData('public-homepage-sections', () =>
|
||||
fetchHomepageSections()
|
||||
const { data: homepageSectionRows } = await useAsyncData(
|
||||
'public-homepage-sections',
|
||||
() => fetchHomepageSections(),
|
||||
{ server: false }
|
||||
)
|
||||
useRefreshNuxtDataOnResume('public-homepage-sections')
|
||||
|
||||
const homepageSections = computed(() => homepageSectionRows.value || [])
|
||||
const homepageSectionMap = computed(() =>
|
||||
@@ -23,29 +28,28 @@ function homepageSection(key) {
|
||||
return homepageSectionMap.value[key] || {}
|
||||
}
|
||||
|
||||
function backgroundStyle(url) {
|
||||
return url ? { backgroundImage: `url("${url}")` } : null
|
||||
function normalizeBackgroundUrl(value) {
|
||||
const url = String(value || '')
|
||||
.trim()
|
||||
.replace(/"|"|"/gi, '"')
|
||||
.replace(/&/gi, '&')
|
||||
const declarationMatch = url.match(/^background-image\s*:\s*(url\(.*\))\s*;?$/i)
|
||||
const source = declarationMatch ? declarationMatch[1] : url
|
||||
const urlMatch = source.match(/^url\((.*)\)$/i)
|
||||
return (urlMatch ? urlMatch[1] : source).trim().replace(/^['"]|['"]$/g, '')
|
||||
}
|
||||
|
||||
const fallbackLogos = [
|
||||
'Bond',
|
||||
'WaSun',
|
||||
'西西能源',
|
||||
'CIIC',
|
||||
'SUNAC',
|
||||
'B+ WORKS',
|
||||
'Bond',
|
||||
'WaSun',
|
||||
'西西能源',
|
||||
'CIIC',
|
||||
'SUNAC',
|
||||
'B+ WORKS'
|
||||
]
|
||||
function backgroundStyle(url) {
|
||||
const normalizedUrl = normalizeBackgroundUrl(url)
|
||||
return normalizedUrl ? { backgroundImage: `url(${normalizedUrl})` } : null
|
||||
}
|
||||
|
||||
const fallbackLogos = []
|
||||
const fallbackStats = [
|
||||
['300+', '累计服务客户', '/images/lanhu/slices/home-group100-image26@2x.png'],
|
||||
['12+', '覆盖行业', '/images/lanhu/slices/home-group101-image27@2x.png'],
|
||||
['126%', '客户AI搜索曝光平均提升', '/images/lanhu/slices/home-group102-image28@2x.png'],
|
||||
['65%', '客户转化链路效率平均提升', '/images/lanhu/slices/home-group103-image29@2x.png']
|
||||
['300+', '累计服务客户', '/images/lanhu/slices/home-group37-group70-image5@2x.png'],
|
||||
['12+', '覆盖行业', '/images/lanhu/slices/home-group36-group71-image5@2x.png'],
|
||||
['126%', '客户AI搜索曝光平均提升', '/images/lanhu/slices/home-group35-group72-image5@2x.png'],
|
||||
['65%', '客户转化链路效率平均提升', '/images/lanhu/slices/home-group34-group74-image5@2x.png']
|
||||
]
|
||||
const heroBackgroundImage = '/images/lanhu/slices/hero-geo-visual@2x.png'
|
||||
const fallbackCorePoints = [
|
||||
@@ -67,9 +71,21 @@ const fallbackRingModels = [
|
||||
}
|
||||
]
|
||||
const fallbackAdvantageCards = [
|
||||
['全域媒体资源优势', '5000+权威官媒+10万+优质自媒体矩阵'],
|
||||
['全链路闭环优势', '行业唯一打通「纯曝光+内容资产沉淀+转化闭环」'],
|
||||
['全客群定制优势', '覆盖全类型经营主体,一对一专属定制专属方案']
|
||||
{
|
||||
heading: '全域媒体资源优势',
|
||||
bodyOne: '5000+权威官媒+10万+优质自媒体矩阵',
|
||||
bodyTwo: '提升AI搜索权重 多渠道权威背书,全方位强化品牌 曝光公信力'
|
||||
},
|
||||
{
|
||||
heading: '全链路闭环优势',
|
||||
bodyOne: '行业唯一打通「纯曝光-内容资产沉淀·转化闭环',
|
||||
bodyTwo: '从曝光到获客全链路承接,避免流量断层实现 品效合'
|
||||
},
|
||||
{
|
||||
heading: '全客群定制优势',
|
||||
bodyOne: '覆盖全类型经营主体,一对一专属定制方案',
|
||||
bodyTwo: '拒绝标准化模板,根据企业阶段、行业、需求匹 記最优 GEO策略'
|
||||
}
|
||||
]
|
||||
const fallbackCaseImages = [
|
||||
'/images/lanhu/slices/home-group90-image15@2x.png',
|
||||
@@ -132,9 +148,9 @@ const processSection = computed(() => homepageSection('PROCESS'))
|
||||
const testimonialSection = computed(() => homepageSection('TESTIMONIAL'))
|
||||
const bottomCtaSection = computed(() => homepageSection('BOTTOM_CTA'))
|
||||
|
||||
const heroTitle = computed(
|
||||
() => heroSection.value.title || '大马棒 · 云途全域GEO\n让品牌在AI时代拥有持续增长力'
|
||||
)
|
||||
const heroTitle = computed(() => heroSection.value.title || '大马棒 · 云途全域GEO')
|
||||
const heroDescription = computed(() => heroSection.value.description || 'AI时代拥有持续增长力')
|
||||
console.log(heroSection.value.description)
|
||||
const heroTags = computed(() =>
|
||||
heroSection.value.tags?.length
|
||||
? heroSection.value.tags
|
||||
@@ -150,14 +166,57 @@ const stats = computed(() => {
|
||||
fallbackStats[index % fallbackStats.length][2]
|
||||
])
|
||||
})
|
||||
|
||||
watch(stats, async () => {
|
||||
if (typeof window === 'undefined') return
|
||||
|
||||
await nextTick()
|
||||
|
||||
if (window.innerWidth <= 760 || !pageRoot.value) return
|
||||
|
||||
counterContext?.revert()
|
||||
|
||||
counterContext = gsap.context(() => {
|
||||
document.querySelectorAll('.stat-item strong').forEach((element) => {
|
||||
const finalText = element.textContent.trim()
|
||||
const statMatch = finalText.match(SIMPLE_STAT_VALUE)
|
||||
if (!statMatch) return
|
||||
|
||||
const finalValue = Number.parseFloat(statMatch[1])
|
||||
const suffix = statMatch[2]
|
||||
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}`
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}, pageRoot.value)
|
||||
}, { immediate: true, flush: 'post' })
|
||||
|
||||
const corePoints = computed(() => {
|
||||
const cards = capabilitiesSection.value.cards
|
||||
if (!cards?.length) return fallbackCorePoints
|
||||
|
||||
return cards.slice(0, 2).map((card, index) => [
|
||||
card.title || fallbackCorePoints[index]?.[0] || '',
|
||||
card.details?.[0]?.description || card.details?.[0]?.heading || fallbackCorePoints[index]?.[1] || ''
|
||||
])
|
||||
return cards
|
||||
.slice(0, 2)
|
||||
.map((card, index) => [
|
||||
card.title || fallbackCorePoints[index]?.[0] || '',
|
||||
card.details?.[0]?.description ||
|
||||
card.details?.[0]?.heading ||
|
||||
fallbackCorePoints[index]?.[1] ||
|
||||
''
|
||||
])
|
||||
})
|
||||
const ringModels = computed(() => fallbackRingModels)
|
||||
const leadAdvantage = computed(() => {
|
||||
@@ -172,11 +231,10 @@ const leadAdvantage = computed(() => {
|
||||
})
|
||||
const advantageCards = computed(() => {
|
||||
const groups = advantagesSection.value.groups?.slice(1)
|
||||
console.log(groups);
|
||||
|
||||
if (!groups?.length) return fallbackAdvantageCards
|
||||
return groups.map((group) => [
|
||||
group.heading,
|
||||
[group.bodyOne, group.bodyTwo].filter(Boolean).join(';')
|
||||
])
|
||||
return groups
|
||||
})
|
||||
const caseCards = computed(() => {
|
||||
const contents = caseBlockSection.value.contents
|
||||
@@ -215,6 +273,12 @@ const deliverySteps = computed(() => {
|
||||
fallbackDeliverySteps[index % fallbackDeliverySteps.length][2]
|
||||
])
|
||||
})
|
||||
const testimonialCarouselEnabled = computed(() => testimonials.value.length > 3)
|
||||
const testimonialLoopItems = computed(() =>
|
||||
testimonialCarouselEnabled.value
|
||||
? [...testimonials.value, ...testimonials.value]
|
||||
: testimonials.value
|
||||
)
|
||||
const testimonials = computed(() => {
|
||||
const groups = testimonialSection.value.groups
|
||||
if (!groups?.length) {
|
||||
@@ -259,8 +323,7 @@ onMounted(async () => {
|
||||
|
||||
const heroTimeline = gsap.timeline({ defaults: { ease: 'power3.out' } })
|
||||
heroTimeline
|
||||
.from('.subsite-header', { autoAlpha: 0, y: -16, duration: 0.5, clearProps: 'all' })
|
||||
.from('.hero-copy h1', { autoAlpha: 0, y: 32, duration: 0.7, clearProps: 'all' }, '-=0.12')
|
||||
.from('.hero-copy h1', { autoAlpha: 0, y: 32, duration: 0.7, clearProps: 'all' })
|
||||
.from(
|
||||
'.hero-points span',
|
||||
{ autoAlpha: 0, y: 16, duration: 0.42, stagger: 0.09, clearProps: 'all' },
|
||||
@@ -294,7 +357,6 @@ onMounted(async () => {
|
||||
reveal('.delivery-banner', '.delivery-banner', { y: 22 })
|
||||
reveal('.delivery-grid article', '.delivery-grid', { y: 22, stagger: 0.1 })
|
||||
reveal('.testimonial-section .section-title', '.testimonial-section')
|
||||
reveal('.testimonial-track article', '.testimonial-track', { y: 22, stagger: 0.1 })
|
||||
reveal('.cta-content', '.cta-section', { y: 26 })
|
||||
|
||||
animationMedia = gsap.matchMedia()
|
||||
@@ -366,28 +428,6 @@ onMounted(async () => {
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -470,17 +510,6 @@ onMounted(async () => {
|
||||
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)
|
||||
@@ -489,6 +518,7 @@ onMounted(async () => {
|
||||
onBeforeUnmount(() => {
|
||||
animationMedia?.revert()
|
||||
animationContext?.revert()
|
||||
counterContext?.revert()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -497,19 +527,16 @@ onBeforeUnmount(() => {
|
||||
<SiteHeader />
|
||||
|
||||
<main>
|
||||
<section class="hero-section">
|
||||
<div
|
||||
class="hero-art"
|
||||
:style="backgroundStyle(heroBackgroundImage)"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<section class="hero-section">
|
||||
<div
|
||||
class="hero-art"
|
||||
:style="backgroundStyle(heroBackgroundImage)"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<div class="page-width hero-content">
|
||||
<div class="hero-copy">
|
||||
<h1>
|
||||
<template v-for="(line, index) in heroTitle.split('\n')" :key="`${line}-${index}`">
|
||||
<br v-if="index" />{{ line }}
|
||||
</template>
|
||||
</h1>
|
||||
<h1>{{ heroTitle }}</h1>
|
||||
<h1>{{ heroDescription }}</h1>
|
||||
<p class="hero-points">
|
||||
<span v-for="tag in heroTags" :key="tag">✓ {{ tag }}</span>
|
||||
</p>
|
||||
@@ -620,15 +647,15 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
<div class="advantage-grid">
|
||||
<article v-for="([title, text], index) in advantageCards" :key="title">
|
||||
<article v-for="(item, index) in advantageCards" :key="index">
|
||||
<img
|
||||
:src="`/images/lanhu/slices/home-group${37 - index}-group${70 + index}-image5@2x.png`"
|
||||
alt=""
|
||||
/>
|
||||
<div>
|
||||
<h3>{{ title }}</h3>
|
||||
<p><b>核心点:</b>{{ text }}</p>
|
||||
<p><b>价值:</b>提升AI搜索权重,构建持续增长的品牌信任力</p>
|
||||
<h3>{{ item.heading }}</h3>
|
||||
<p><b>核心点:</b>{{ item.bodyOne }}</p>
|
||||
<p><b>价值:</b>{{ item.bodyTwo }}</p>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
@@ -656,26 +683,28 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="industries" class="industries-section page-width">
|
||||
<SectionTitle
|
||||
>{{ industryBlockSection.title || '深耕12+行业,定制专属GEO增长方案'
|
||||
}}<template #subtitle
|
||||
>With In-depth Experience Across 12+ Industries, We Craft Exclusive Geo Growth
|
||||
Solutions<br />Tailored To Clients’ Needs</template
|
||||
></SectionTitle
|
||||
>
|
||||
<div class="industry-grid">
|
||||
<article v-for="card in industryCards" :key="card[0]">
|
||||
<h3>{{ card[0] }}</h3>
|
||||
<img :src="card[2]" alt="" />
|
||||
<div class="quote">“</div>
|
||||
<p>某企业服务客户:<br />{{ card[1] }}</p>
|
||||
<div class="tags">
|
||||
<span v-for="tag in card[3] || ['生活服务', '全域曝光']" :key="`${card[0]}-${tag}`"
|
||||
>#{{ tag }}</span
|
||||
>
|
||||
</div>
|
||||
</article>
|
||||
<section id="industries" class="industries-section">
|
||||
<div class="industries-content">
|
||||
<SectionTitle
|
||||
>{{ industryBlockSection.title || '深耕12+行业,定制专属GEO增长方案'
|
||||
}}<template #subtitle
|
||||
>With In-depth Experience Across 12+ Industries, We Craft Exclusive Geo Growth
|
||||
Solutions<br />Tailored To Clients’ Needs</template
|
||||
></SectionTitle
|
||||
>
|
||||
<div class="industry-grid">
|
||||
<article v-for="card in industryCards" :key="card[0]">
|
||||
<h3>{{ card[0] }}</h3>
|
||||
<img :src="card[2]" alt="" />
|
||||
<div class="quote">“</div>
|
||||
<p>某企业服务客户:<br />{{ card[1] }}</p>
|
||||
<div class="tags">
|
||||
<span v-for="tag in card[3] || ['生活服务', '全域曝光']" :key="`${card[0]}-${tag}`"
|
||||
>#{{ tag }}</span
|
||||
>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -709,11 +738,26 @@ onBeforeUnmount(() => {
|
||||
>Genuine Client Testimonials Attest To Our Service Quality</template
|
||||
></SectionTitle
|
||||
>
|
||||
<div class="testimonial-track">
|
||||
<article v-for="(item, index) in testimonials" :key="`${item.name}-${index}`">
|
||||
<div
|
||||
:class="[
|
||||
'testimonial-track',
|
||||
testimonialCarouselEnabled ? 'is-carousel' : 'is-centered'
|
||||
]"
|
||||
>
|
||||
<article
|
||||
v-for="(item, index) in testimonialLoopItems"
|
||||
:key="`${item.name}-${index}`"
|
||||
:aria-hidden="
|
||||
testimonialCarouselEnabled && index >= testimonials.length ? 'true' : null
|
||||
"
|
||||
>
|
||||
<header>
|
||||
<span class="avatar" :style="backgroundStyle(item.avatarUrl)"></span
|
||||
><b>{{ item.name }}</b
|
||||
<img
|
||||
v-if="normalizeBackgroundUrl(item.avatarUrl)"
|
||||
class="avatar"
|
||||
:src="normalizeBackgroundUrl(item.avatarUrl)"
|
||||
:alt="item.name ? `${item.name}头像` : ''"
|
||||
/><span v-else class="avatar"></span><b>{{ item.name }}</b
|
||||
><time>{{ item.date }}</time>
|
||||
</header>
|
||||
<p>{{ item.description }}</p>
|
||||
@@ -721,7 +765,15 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div>
|
||||
<span
|
||||
style="
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
background-image: url('https://cdn.xznawq.com/geo/___16_1784796340147.png');
|
||||
"
|
||||
></span>
|
||||
</div>
|
||||
<section class="cta-section">
|
||||
<div class="cta-art" :style="backgroundStyle(bottomCtaSection.assetUrl)"></div>
|
||||
<div class="cta-content">
|
||||
|
||||
Reference in New Issue
Block a user