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:
+138
-80
@@ -1,29 +1,47 @@
|
||||
<script setup>
|
||||
import SiteHeader from '~/components/site/SiteHeader.vue'
|
||||
import SiteFooter from '~/components/site/SiteFooter.vue'
|
||||
const { ALL_LABEL, bySortAndTime, categoryList, fetchPublicContent, normalizeCase } =
|
||||
const { ALL_LABEL, bySortAndTime, fetchPublicCategories, fetchPublicContent, normalizeCase } =
|
||||
useOfficialContent()
|
||||
|
||||
const activeCategory = ref(ALL_LABEL)
|
||||
const snapshotVisibleCount = ref(3)
|
||||
const { openConsultation } = useConsultation()
|
||||
|
||||
const { data: caseRows } = await useAsyncData('public-cases', async () => {
|
||||
const rows = await fetchPublicContent('CASE')
|
||||
return rows.sort(bySortAndTime).map((item) => normalizeCase(item, new Map()))
|
||||
})
|
||||
const { data: pageData } = await useAsyncData('public-cases', async () => {
|
||||
const [rows, categories] = await Promise.all([
|
||||
fetchPublicContent('CASE'),
|
||||
fetchPublicCategories().catch(() => ({}))
|
||||
])
|
||||
|
||||
const cases = computed(() => caseRows.value || [])
|
||||
const caseCategories = computed(() => categoryList(cases.value, 'industry'))
|
||||
const featuredCase = computed(
|
||||
() =>
|
||||
cases.value.find((item) => item.isFeatured) ||
|
||||
cases.value.find((item) => item.isFullCase) ||
|
||||
cases.value[0] ||
|
||||
null
|
||||
)
|
||||
return {
|
||||
cases: rows.sort(bySortAndTime).map((item) => normalizeCase(item, new Map())),
|
||||
categories: categories || {}
|
||||
}
|
||||
})
|
||||
useRefreshNuxtDataOnResume('public-cases')
|
||||
|
||||
const cases = computed(() => pageData.value?.cases || [])
|
||||
const caseCategories = computed(() => [
|
||||
ALL_LABEL,
|
||||
...Array.from(
|
||||
new Set(
|
||||
(pageData.value?.categories?.caseIndustries || []).map((item) => item.name).filter(Boolean)
|
||||
)
|
||||
)
|
||||
])
|
||||
const filteredCases = computed(() => {
|
||||
if (activeCategory.value === ALL_LABEL) return cases.value
|
||||
return cases.value.filter((item) => item.industry === activeCategory.value)
|
||||
})
|
||||
const primaryCases = computed(() => {
|
||||
if (activeCategory.value === ALL_LABEL) return cases.value.filter((item) => item.isFeatured)
|
||||
return filteredCases.value.filter((item) => item.isFullCase)
|
||||
})
|
||||
const resultSnapshots = computed(() =>
|
||||
cases.value.filter((item) => item.id !== featuredCase.value?.id)
|
||||
filteredCases.value.filter((item) => item.isRecommended === false)
|
||||
)
|
||||
const heroPromotion = computed(() => filteredCases.value.filter((item) => item.isRecommended ).slice(0, 3))
|
||||
|
||||
useHead({
|
||||
title: '客户案例|大马棒·云途全域 GEO',
|
||||
@@ -36,15 +54,26 @@ useHead({
|
||||
]
|
||||
})
|
||||
|
||||
const showFeatured = computed(() => {
|
||||
if (!featuredCase.value) return false
|
||||
return activeCategory.value === ALL_LABEL || activeCategory.value === featuredCase.value.industry
|
||||
const visibleSnapshots = computed(() => resultSnapshots.value.slice(0, snapshotVisibleCount.value))
|
||||
const canLoadMoreSnapshots = computed(
|
||||
() => snapshotVisibleCount.value < resultSnapshots.value.length
|
||||
)
|
||||
const hasPrimaryCases = computed(() => primaryCases.value.length > 0)
|
||||
const primaryEmptyKicker = computed(() =>
|
||||
activeCategory.value === ALL_LABEL ? '精选案例正在整理' : '完整案例正在整理'
|
||||
)
|
||||
const primaryEmptyTitle = computed(() =>
|
||||
activeCategory.value === ALL_LABEL
|
||||
? '精选案例尚未上线'
|
||||
: `${activeCategory.value}行业的完整案例尚未上线`
|
||||
)
|
||||
const primaryEmptySource = computed(() =>
|
||||
activeCategory.value === ALL_LABEL ? '案例全部精选空状态' : '行业完整案例空状态'
|
||||
)
|
||||
|
||||
watch(activeCategory, () => {
|
||||
snapshotVisibleCount.value = 3
|
||||
})
|
||||
const visibleSnapshots = computed(() => {
|
||||
if (activeCategory.value === ALL_LABEL) return resultSnapshots.value
|
||||
return resultSnapshots.value.filter((item) => item.industry === activeCategory.value)
|
||||
})
|
||||
const hasResults = computed(() => showFeatured.value || visibleSnapshots.value.length > 0)
|
||||
|
||||
function requestPlan(industry, source) {
|
||||
openConsultation({ title: `获取${industry}行业增长方案`, industry, source })
|
||||
@@ -86,7 +115,7 @@ function requestCustomPlan(source) {
|
||||
|
||||
<div class="case-collage" aria-label="不同行业案例结果预览">
|
||||
<article
|
||||
v-for="(snapshot, index) in resultSnapshots"
|
||||
v-for="(snapshot, index) in heroPromotion"
|
||||
:key="snapshot.industry"
|
||||
:class="`collage-card collage-card-${index + 1}`"
|
||||
>
|
||||
@@ -98,7 +127,7 @@ function requestCustomPlan(source) {
|
||||
</div>
|
||||
</article>
|
||||
<div class="collage-center">
|
||||
<small>CASE LIBRARY</small><strong>真实增长路径</strong><span>持续更新</span>
|
||||
<img src="/images/cases/real-growth-path.png" alt="真实增长路径" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,7 +140,6 @@ function requestCustomPlan(source) {
|
||||
<p class="section-kicker">按行业查看</p>
|
||||
<h2>结果相似,背后的路径并不相同</h2>
|
||||
</div>
|
||||
<p>筛选行业,查看已经确认的完整案例和结果快照。未经客户授权,案例默认使用匿名名称。</p>
|
||||
</header>
|
||||
|
||||
<div class="case-filters" role="group" aria-label="案例行业筛选">
|
||||
@@ -128,27 +156,57 @@ function requestCustomPlan(source) {
|
||||
|
||||
<Transition name="case-change" mode="out-in">
|
||||
<div :key="activeCategory" class="case-results">
|
||||
<article v-if="showFeatured" class="featured-case">
|
||||
<article
|
||||
v-for="caseItem in primaryCases"
|
||||
:key="caseItem.id || caseItem.slug"
|
||||
class="featured-case"
|
||||
>
|
||||
<div class="featured-case-image">
|
||||
<img :src="featuredCase.cover" alt="教育培训行业 GEO 案例封面" />
|
||||
<span>{{ featuredCase.industry }} · 完整案例</span>
|
||||
<img :src="caseItem.cover" :alt="`${caseItem.industry}行业 GEO 案例封面`" />
|
||||
<span
|
||||
>{{ caseItem.industry }} ·
|
||||
{{ caseItem.isFullCase ? '完整案例' : '精选案例' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="featured-case-copy">
|
||||
<p>{{ featuredCase.client }}</p>
|
||||
<h2>{{ featuredCase.title }}</h2>
|
||||
<span class="featured-summary">{{ featuredCase.summary }}</span>
|
||||
<p>{{ caseItem.client }}</p>
|
||||
<h2>{{ caseItem.title }}</h2>
|
||||
<span class="featured-summary">{{ caseItem.summary }}</span>
|
||||
<div class="featured-metrics">
|
||||
<div v-for="metric in featuredCase.metrics" :key="metric[1]">
|
||||
<div v-for="metric in caseItem.metrics" :key="metric[1]">
|
||||
<strong>{{ metric[0] }}</strong
|
||||
><span>{{ metric[1] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<NuxtLink :to="`/cases/${featuredCase.slug}`"
|
||||
<NuxtLink
|
||||
v-if="caseItem.isFullCase && caseItem.slug"
|
||||
:to="`/cases/${caseItem.slug}`"
|
||||
>查看完整复盘 <AppIcon name="arrow-right"
|
||||
/></NuxtLink>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
@click="requestPlan(caseItem.industry, `${caseItem.industry}精选案例`)"
|
||||
>
|
||||
获取同类方案 <AppIcon name="arrow-right" />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div v-if="!hasPrimaryCases" class="case-empty primary-empty">
|
||||
<div class="empty-visual" aria-hidden="true"><span></span><span></span><i></i></div>
|
||||
<p class="section-kicker">{{ primaryEmptyKicker }}</p>
|
||||
<h3>{{ primaryEmptyTitle }}</h3>
|
||||
<p>我们可以先结合您的业务情况,分享对应行业的策略思路与可参考指标。</p>
|
||||
<button
|
||||
class="primary-button small"
|
||||
type="button"
|
||||
@click="requestPlan(activeCategory, primaryEmptySource)"
|
||||
>
|
||||
获取{{ activeCategory }}行业建议 <AppIcon name="arrow-right" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="visibleSnapshots.length" class="snapshot-heading">
|
||||
<div>
|
||||
<p class="section-kicker">结果快照</p>
|
||||
@@ -185,20 +243,14 @@ function requestCustomPlan(source) {
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div v-if="!hasResults" class="case-empty">
|
||||
<div class="empty-visual" aria-hidden="true"><span></span><span></span><i></i></div>
|
||||
<p class="section-kicker">案例正在整理</p>
|
||||
<h3>该行业的公开案例尚未上线</h3>
|
||||
<p>我们可以先结合您的业务情况,分享对应行业的策略思路与可参考指标。</p>
|
||||
<button
|
||||
class="primary-button small"
|
||||
type="button"
|
||||
@click="requestPlan(activeCategory, '案例筛选空状态')"
|
||||
>
|
||||
获取{{ activeCategory }}行业建议 <AppIcon name="arrow-right" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
v-if="canLoadMoreSnapshots"
|
||||
class="case-load-more"
|
||||
type="button"
|
||||
@click="snapshotVisibleCount += 2"
|
||||
>
|
||||
加载更多 <AppIcon name="plus" />
|
||||
</button>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
@@ -442,35 +494,20 @@ function requestCustomPlan(source) {
|
||||
}
|
||||
.collage-center {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
z-index: 04;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 170px;
|
||||
height: 170px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 12px solid rgba(255, 255, 255, 0.66);
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
background: linear-gradient(145deg, #2c82ef, #4bb8e9);
|
||||
box-shadow: 0 24px 48px rgba(40, 123, 224, 0.28);
|
||||
width: min(230px, 68%);
|
||||
aspect-ratio: 1838 / 1300;
|
||||
overflow: hidden;
|
||||
border-radius: 18px;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.collage-center small {
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.14em;
|
||||
opacity: 0.76;
|
||||
}
|
||||
.collage-center strong {
|
||||
margin-top: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.collage-center span {
|
||||
margin-top: 4px;
|
||||
font-size: 10px;
|
||||
opacity: 0.76;
|
||||
.collage-center img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
.case-library-section {
|
||||
padding: 112px 0 130px;
|
||||
@@ -533,6 +570,9 @@ function requestCustomPlan(source) {
|
||||
background: #eaf4ff;
|
||||
box-shadow: 0 28px 60px rgba(60, 123, 201, 0.13);
|
||||
}
|
||||
.featured-case + .featured-case {
|
||||
margin-top: 28px;
|
||||
}
|
||||
.featured-case-image {
|
||||
position: relative;
|
||||
min-height: 480px;
|
||||
@@ -612,12 +652,15 @@ function requestCustomPlan(source) {
|
||||
color: #8392a4;
|
||||
font-size: 9px;
|
||||
}
|
||||
.featured-case-copy > a {
|
||||
.featured-case-copy > a,
|
||||
.featured-case-copy > button {
|
||||
align-self: flex-start;
|
||||
margin-top: 29px;
|
||||
padding-bottom: 5px;
|
||||
border-radius: 0;
|
||||
border-bottom: 1px solid #82b3ea;
|
||||
color: #247be8;
|
||||
background: transparent;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
@@ -739,6 +782,26 @@ function requestCustomPlan(source) {
|
||||
background: transparent;
|
||||
font-size: 11px;
|
||||
}
|
||||
.case-load-more {
|
||||
min-width: 150px;
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12px;
|
||||
margin: 42px auto 0;
|
||||
border: 1px solid #bad2ea;
|
||||
border-radius: 5px;
|
||||
color: #527497;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.case-load-more:hover {
|
||||
color: #247be8;
|
||||
border-color: #73aae4;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.case-empty {
|
||||
min-height: 440px;
|
||||
display: flex;
|
||||
@@ -975,12 +1038,7 @@ function requestCustomPlan(source) {
|
||||
right: 0;
|
||||
}
|
||||
.collage-center {
|
||||
width: 132px;
|
||||
height: 132px;
|
||||
border-width: 9px;
|
||||
}
|
||||
.collage-center strong {
|
||||
font-size: 15px;
|
||||
width: 76%;
|
||||
}
|
||||
.case-library-section {
|
||||
padding-top: 76px;
|
||||
|
||||
Reference in New Issue
Block a user