Files
BigStickNewOfficialWebsite-V2/components/site/SiteFooter.vue
T
Zhanglj 2ff1064de7 feat: add customer service chat, optimize case and homepage, fix api proxy
这一提交完成了多项功能升级与优化:
1.  新增全局浮动客服组件,集成腾讯云TRTC聊天能力并添加错误格式化工具
2.  新增图片资源并调整部分静态文件结构
3.  优化案例页面:重构筛选逻辑、新增空状态处理、添加加载更多功能
4.  优化首页布局:修复统计数字动画、调整移动端适配样式、优化轮播逻辑
5.  重构官方内容获取工具:添加缓存机制、优化媒体资源处理
6.  优化API代理配置,移除默认本地代理并新增环境变量配置
7.  新增页面数据恢复刷新逻辑,优化页面交互体验
8.  修复按钮组件冗余代码,调整头部导航移动端样式
2026-07-24 18:55:51 +08:00

326 lines
8.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
const route = useRoute()
const { openConsultation } = useConsultation()
const { fetchPublicMediaAsset, fetchPublicSiteConfig } = useOfficialContent()
const { data: siteConfigRows } = await useAsyncData('public-site-config-footer', () =>
fetchPublicSiteConfig()
)
function parseConfigValue(value, fallback) {
if (value == null || value === '') return fallback
if (typeof value !== 'string') return value
try {
return JSON.parse(value)
} catch {
return fallback
}
}
const siteConfig = computed(() => siteConfigRows.value || {})
const brandConfig = computed(() => parseConfigValue(siteConfig.value.BRAND_PROFILE, {}))
const contactConfig = computed(() => parseConfigValue(siteConfig.value.CONTACT, {}))
const footerConfig = computed(() => parseConfigValue(siteConfig.value.FOOTER, {}))
const companyName = computed(
() => brandConfig.value.companyName || brandConfig.value.name || '成都大马棒传媒有限公司'
)
const companyIntro = computed(
() =>
brandConfig.value.description ||
brandConfig.value.slogan ||
'聚焦 AI 搜索增长,用可信内容、全域信源与转化承接帮助品牌建立长期数字资产。'
)
const contactAddress = computed(
() => contactConfig.value.address || '成都市武侯区芯通科技大厦B座9楼'
)
const contactPhone = computed(() => contactConfig.value.phone || '待补充')
const contactEmail = computed(() => contactConfig.value.email || '待补充')
const copyrightText = computed(
() => footerConfig.value.copyright || `© 2026 ${companyName.value}`
)
const legalText = computed(() => {
const items = [footerConfig.value.icp || '备案信息待补充', footerConfig.value.privacyText || '隐私说明']
return items.filter(Boolean).join(' · ')
})
const footerQrAssetId = computed(() => footerConfig.value.qrAssetId || '')
const isDirectAssetUrl = (value) =>
/^(https?:)?\/\//.test(value) || value.startsWith('/') || value.startsWith('data:image/')
const directQrSrc = computed(() => {
const value = String(footerQrAssetId.value || '').trim()
return value && isDirectAssetUrl(value) ? value : ''
})
const { data: footerQrAsset } = await useAsyncData(
'public-site-config-footer-qr',
() => {
const id = String(footerQrAssetId.value || '').trim()
if (!id || isDirectAssetUrl(id)) return null
return fetchPublicMediaAsset(id).catch(() => null)
},
{ watch: [footerQrAssetId] }
)
const footerQrSrc = computed(
() =>
directQrSrc.value ||
footerQrAsset.value?.url ||
footerQrAsset.value?.fileUrl ||
footerQrAsset.value?.filePath ||
''
)
const footerQrAlt = computed(() => footerQrAsset.value?.alt || footerQrAsset.value?.name || '社媒二维码')
const sourceLabel = computed(() => {
const labels = {
'/services': '营销增长服务',
'/solutions': '行业解决方案',
'/cases': '客户案例',
'/advantages': '核心优势',
'/insights': '增长洞察',
'/about': '关于我们'
}
const base = Object.keys(labels).find(
(path) => route.path === path || route.path.startsWith(`${path}/`)
)
return `${labels[base] || '全站'}页页脚`
})
</script>
<template>
<footer class="subsite-footer" :class="{ 'is-interior': route.path !== '/' }">
<div class="page-width footer-content">
<div class="footer-company">
<img src="/images/lanhu/slices/home-group114-image33@2x.png" :alt="companyName" />
<p>{{ companyIntro }}</p>
<button type="button" @click="openConsultation({ source: sourceLabel })">
<span>预约免费诊断</span>
<AppIcon name="arrow-right" />
</button>
</div>
<nav class="footer-navigation" aria-label="页脚导航">
<div>
<strong>快速导航</strong>
<NuxtLink to="/">首页</NuxtLink>
<NuxtLink to="/services">营销增长服务</NuxtLink>
<NuxtLink to="/solutions">行业解决方案</NuxtLink>
<NuxtLink to="/cases">客户案例</NuxtLink>
<NuxtLink to="/advantages">核心优势</NuxtLink>
<NuxtLink to="/insights">增长洞察</NuxtLink>
<NuxtLink to="/about">关于我们</NuxtLink>
</div>
<div>
<strong>业务方向</strong>
<NuxtLink to="/services#brand-exposure">品牌曝光</NuxtLink>
<NuxtLink to="/services#lead-conversion">精准获客转化</NuxtLink>
<NuxtLink to="/services#omnichannel">全域营销</NuxtLink>
</div>
<div>
<strong>联系我们</strong>
<span class="contact-line"><AppIcon name="location" />{{ contactAddress }}</span>
<span class="contact-line"><AppIcon name="phone" />联系电话{{ contactPhone }}</span>
<span class="contact-line"><AppIcon name="message" />商务邮箱{{ contactEmail }}</span>
</div>
</nav>
<div class="footer-qr" aria-label="社媒二维码">
<img v-if="footerQrSrc" class="qr-image" :src="footerQrSrc" :alt="footerQrAlt" loading="lazy" />
<span v-else class="qr-pattern" aria-hidden="true"></span>
<strong>扫码添加专属顾问</strong>
<small>获取 1 1 GEO 增长建议</small>
</div>
</div>
<div class="footer-legal">
<div class="page-width">
<span>{{ copyrightText }}</span><span>{{ legalText }}</span>
</div>
</div>
</footer>
</template>
<style scoped>
.subsite-footer {
color: #cbd6e1;
background: #12202a;
}
.subsite-footer.is-interior {
background:
linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px) 0 0 / 38px 38px,
linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px) 0 0 / 38px 38px,
#12202a;
}
.footer-content {
min-height: 285px;
display: grid;
grid-template-columns: 1.2fr 1.7fr 0.65fr;
align-items: center;
gap: 52px;
padding-top: 42px;
padding-bottom: 42px;
}
.footer-company img {
display: block;
width: 255px;
padding: 8px;
border-radius: 6px;
background: #fff;
}
.is-interior .footer-company img {
border-radius: 5px;
}
.footer-company p {
max-width: 390px;
margin: 20px 0;
color: #b6c2cc;
font-size: 14px;
line-height: 1.8;
}
.footer-company button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0;
color: #76b7ff;
background: transparent;
font-size: 14px;
font-weight: 600;
}
.footer-company button:hover {
color: #fff;
}
.is-interior .footer-company button {
transition:
color 0.22s ease,
transform 0.22s ease;
}
.is-interior .footer-company button:hover {
transform: translateX(3px);
}
.footer-navigation {
display: grid;
grid-template-columns: 0.8fr 0.9fr 1.45fr;
gap: 32px;
}
.footer-navigation div {
display: grid;
align-content: start;
gap: 12px;
}
.footer-navigation strong {
margin-bottom: 5px;
color: #eff6fb;
font-size: 14px;
}
.footer-navigation a,
.footer-navigation span {
color: #b1bdc7;
font-size: 13px;
line-height: 1.65;
text-decoration: none;
}
.footer-navigation a:hover {
color: #70b5ff;
}
.is-interior .footer-navigation a {
transition:
color 0.2s ease,
transform 0.2s ease;
}
.is-interior .footer-navigation a:hover {
transform: translateX(2px);
}
.footer-navigation .contact-line {
display: grid;
grid-template-columns: 16px 1fr;
align-items: start;
gap: 8px;
}
.footer-navigation .contact-line :deep(.app-icon) {
margin-top: 3px;
color: #70b5ff;
}
.footer-qr {
display: grid;
justify-items: center;
text-align: center;
}
.qr-image {
width: 86px;
height: 86px;
display: block;
border: 7px solid #fff;
background: #fff;
object-fit: contain;
}
.qr-pattern {
width: 86px;
height: 86px;
display: block;
border: 7px solid #fff;
background:
linear-gradient(90deg, #13212b 25%, transparent 25% 50%, #13212b 50% 75%, transparent 75%) 0 0 /
16px 16px,
linear-gradient(#13212b 25%, transparent 25% 50%, #13212b 50% 75%, transparent 75%) 8px 8px /
16px 16px,
#fff;
}
.footer-qr strong {
margin-top: 13px;
color: #e8f0f6;
font-size: 13px;
}
.footer-qr small {
margin-top: 6px;
color: #aebac4;
font-size: 12px;
}
.footer-legal {
min-height: 54px;
display: grid;
place-items: center;
color: #aeb9c2;
background: #263139;
font-size: 12px;
}
.is-interior .footer-legal {
border-top: 1px solid rgba(255, 255, 255, 0.055);
background: rgba(38, 49, 57, 0.88);
}
.footer-legal .page-width {
display: flex;
justify-content: space-between;
gap: 24px;
}
@media (max-width: 960px) {
.footer-content {
grid-template-columns: 1fr 1.5fr;
}
.footer-qr {
display: none;
}
}
@media (max-width: 680px) {
.footer-content {
grid-template-columns: 1fr;
gap: 32px;
padding-top: 40px;
padding-bottom: 36px;
}
.footer-company img {
width: 230px;
}
.footer-navigation {
grid-template-columns: 1fr 1fr;
gap: 28px 18px;
}
.footer-navigation div:last-child {
grid-column: 1 / -1;
}
.footer-legal .page-width {
display: grid;
justify-content: center;
padding: 14px 0;
text-align: center;
}
}
</style>