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:
2026-07-24 18:55:51 +08:00
parent 314cd6ccd7
commit 2ff1064de7
25 changed files with 4596 additions and 327 deletions
+76 -6
View File
@@ -5,11 +5,11 @@ import SiteFooter from '~/components/site/SiteFooter.vue'
const pageRoot = ref(null)
const { openConsultation } = useConsultation()
const { fetchPublicSiteConfig } = useOfficialContent()
const { fetchPublicMediaAsset, fetchPublicSiteConfig } = useOfficialContent()
let animationContext
const { data: siteConfigRows } = await useAsyncData('public-site-config-about', () =>
fetchPublicSiteConfig()
fetchPublicSiteConfig({ fresh: true })
)
function parseConfigValue(value, fallback) {
@@ -31,6 +31,35 @@ const contactConfig = computed(() =>
address: '待补充'
})
)
const footerConfig = computed(() => parseConfigValue(siteConfig.value.FOOTER, {}))
const contactQrAssetId = computed(() => footerConfig.value.qrAssetId || '')
const isDirectAssetUrl = (value) =>
/^(https?:)?\/\//.test(value) || value.startsWith('/') || value.startsWith('data:image/')
const directContactQrSrc = computed(() => {
const value = String(contactQrAssetId.value || '').trim()
return value && isDirectAssetUrl(value) ? value : ''
})
const { data: contactQrAsset } = await useAsyncData(
'public-site-config-about-contact-qr',
() => {
const id = String(contactQrAssetId.value || '').trim()
if (!id || isDirectAssetUrl(id)) return null
return fetchPublicMediaAsset(id).catch(() => null)
},
{ watch: [contactQrAssetId] }
)
useRefreshNuxtDataOnResume(['public-site-config-about', 'public-site-config-about-contact-qr'])
const contactQrSrc = computed(
() =>
directContactQrSrc.value ||
contactQrAsset.value?.url ||
contactQrAsset.value?.fileUrl ||
contactQrAsset.value?.filePath ||
''
)
const contactQrAlt = computed(
() => contactQrAsset.value?.alt || contactQrAsset.value?.name || '社媒二维码'
)
useHead(() => ({
title: '关于我们|大马棒·云途全域 GEO',
@@ -44,14 +73,28 @@ useHead(() => ({
]
}))
const stats = [
const fallbackStats = [
['1000+', '累计服务客户'],
['12+', '覆盖主流行业'],
['5 万', '真人内容生产网络'],
['5000+', '权威媒体资源']
]
const serviceFlow = [
const stats = computed(() => {
const rows = parseConfigValue(siteConfig.value.ABOUT_STATS, [])
if (!Array.isArray(rows) || rows.length === 0) return fallbackStats
return rows
.filter((item) => item.status !== 'OFFLINE')
.sort((left, right) => (Number(left.sortOrder) || 0) - (Number(right.sortOrder) || 0))
.map((item) => [
item.value || item.metricValue || '',
item.label || item.metricLabel || item.title || ''
])
.filter((item) => item[0] || item[1])
})
const fallbackServiceFlow = [
['01', '可见性诊断', '测试品牌在主流 AI 平台中的认知、提及与推荐表现。'],
['02', '策略规划', '围绕业务目标、用户问题与行业特征确定 GEO 路径。'],
['03', '内容建设', '把真实业务信息转化为用户愿意读、AI 能识别的内容。'],
@@ -59,6 +102,21 @@ const serviceFlow = [
['05', '效果复盘', '持续监测覆盖率、推荐率、流量与咨询线索质量。']
]
const serviceFlow = computed(() => {
const rows = parseConfigValue(siteConfig.value.ABOUT_SERVICE_FLOW, [])
if (!Array.isArray(rows) || rows.length === 0) return fallbackServiceFlow
return rows
.filter((item) => item.status !== 'OFFLINE')
.sort((left, right) => (Number(left.sortOrder) || 0) - (Number(right.sortOrder) || 0))
.map((item, index) => [
item.step || item.number || String(index + 1).padStart(2, '0'),
item.title || item.heading || '',
item.description || item.body || ''
])
.filter((item) => item[1] || item[2])
})
const fallbackTimeline = [
{
year: '2023',
@@ -101,13 +159,24 @@ const timeline = computed(() => {
}))
})
const teamCapabilities = [
const fallbackTeamCapabilities = [
['搜索理解', '经历 PC 搜索、移动搜索到 AI 搜索的持续变化。'],
['内容运营', '理解平台规则,也理解真实用户愿意阅读和相信什么。'],
['媒体资源', '长期积累权威媒体、垂直信源与真实创作者网络。'],
['业务转化', '不止关注曝光,更关注咨询、线索质量与长期资产。']
]
const teamCapabilities = computed(() => {
const rows = parseConfigValue(siteConfig.value.ABOUT_TEAM_CAPABILITIES, [])
if (!Array.isArray(rows) || rows.length === 0) return fallbackTeamCapabilities
return rows
.filter((item) => item.status !== 'OFFLINE')
.sort((left, right) => (Number(left.sortOrder) || 0) - (Number(right.sortOrder) || 0))
.map((item) => [item.title || item.heading || '', item.description || item.body || ''])
.filter((item) => item[0] || item[1])
})
function requestDiagnosis(source) {
openConsultation({ title: '预约免费 GEO 增长诊断', source })
}
@@ -344,7 +413,8 @@ onBeforeUnmount(() => animationContext?.revert())
</div>
</dl>
<div class="contact-qr">
<span aria-hidden="true"></span><strong>扫码添加专属顾问</strong
<img v-if="contactQrSrc" :src="contactQrSrc" :alt="contactQrAlt" loading="lazy" />
<span v-else aria-hidden="true"></span><strong>扫码添加专属顾问</strong
><small>获取 1 1 GEO 增长诊断</small>
</div>
</div>