Files
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

619 lines
15 KiB
Vue
Raw Permalink 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>
import SiteFooter from '~/components/site/SiteFooter.vue'
import SiteHeader from '~/components/site/SiteHeader.vue'
import UEditor from '~/components/UEditor.vue'
const route = useRoute()
const { openConsultation } = useConsultation()
const { bySortAndTime, fetchPublicContent, normalizeCase } = useOfficialContent()
const slug = String(route.params.slug || '')
const { data: pageData } = await useAsyncData(`public-case-${slug}`, async () => {
const rows = await fetchPublicContent('CASE')
const cases = rows.sort(bySortAndTime).map((item) => normalizeCase(item, new Map()))
return {
cases,
current: cases.find((item) => item.slug === slug) || null
}
})
if (!pageData.value?.current) {
throw createError({ statusCode: 404, statusMessage: '案例不存在' })
}
const caseStudy = computed(() => pageData.value.current)
const articleTitle = computed(() => caseStudy.value.seoTitle || caseStudy.value.title)
const articleTag = computed(() => caseStudy.value.industry || '客户案例')
const articleHtml = computed(() => caseStudy.value.contentHtml || '<p>暂无正文内容。</p>')
const caseTags = computed(() =>
[caseStudy.value.industry, caseStudy.value.isFullCase ? '完整案例' : '结果快照'].filter(Boolean)
)
const relatedCases = computed(() =>
(pageData.value?.cases || [])
.filter((item) => item.slug !== slug && item.isRecommended === true)
.slice(0, 4)
)
const recommendedCases = computed(() =>
(pageData.value?.cases || [])
.filter(
(item) => item.slug !== slug && item.isRecommended === true && item.isFullCase && item.slug
)
.slice(0, 5)
)
function caseListLink(item) {
return item.isFullCase && item.slug ? `/cases/${item.slug}` : '/cases'
}
useHead(() => ({
title: `${articleTitle.value}|客户案例`,
meta: [
{
name: 'description',
content:
caseStudy.value.seoDescription ||
caseStudy.value.description ||
caseStudy.value.summary ||
articleTitle.value
},
{ property: 'og:title', content: articleTitle.value },
{ property: 'og:description', content: caseStudy.value.description || caseStudy.value.summary },
{ property: 'og:image', content: caseStudy.value.cover }
]
}))
function requestPlanConsultation() {
openConsultation({
title: '方案咨询',
industry: caseStudy.value.industry,
need: articleTitle.value,
source: '客户案例详情页 CTA'
})
}
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;')
}
function renderParagraphs(paragraphs = []) {
return paragraphs.map((paragraph) => `<p>${escapeHtml(paragraph)}</p>`).join('')
}
function renderList(items = []) {
if (!items.length) return ''
return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join('')}</ul>`
}
function renderKeyValueList(items = []) {
if (!items.length) return ''
return `<ul>${items
.map((item) => `<li><strong>${escapeHtml(item[0])}</strong>${escapeHtml(item[1])}</li>`)
.join('')}</ul>`
}
function renderQuestionFramework(items = []) {
if (!items.length) return ''
return items
.map(
(item) => `
<h3>${escapeHtml(item.title)}</h3>
<p>${escapeHtml(item.content)}</p>
<blockquote>${escapeHtml(item.goal)}</blockquote>
`
)
.join('')
}
function renderPhases(items = []) {
if (!items.length) return ''
return items
.map(
(item) => `
<h3>${escapeHtml(item.number)}${escapeHtml(item.title)}</h3>
<p>${escapeHtml(item.text)}</p>
`
)
.join('')
}
function renderDistribution(items = []) {
if (!items.length) return ''
return `<ul>${items
.map(
(item) => `
<li>
<strong>${escapeHtml(item[0])}</strong>${escapeHtml(item[1])}
<p>${escapeHtml(item[2])}</p>
</li>
`
)
.join('')}</ul>`
}
function renderResultTable(rows = []) {
if (!rows.length) return ''
return `
<table>
<thead>
<tr>
<th>核心指标</th>
<th>项目前</th>
<th>项目后 / 峰值</th>
<th>变化说明</th>
</tr>
</thead>
<tbody>
${rows
.map(
(row) => `
<tr>
<td>${escapeHtml(row[0])}</td>
<td>${escapeHtml(row[1])}</td>
<td><strong>${escapeHtml(row[2])}</strong></td>
<td>${escapeHtml(row[3])}</td>
</tr>
`
)
.join('')}
</tbody>
</table>
`
}
function buildCaseArticleHtml(caseData) {
return `
<h2>项目背景</h2>
${renderParagraphs(caseData.background)}
<h2>客户希望解决什么</h2>
${renderList(caseData.objectives)}
<h2>前期诊断结果</h2>
<p>项目启动前,我们先围绕真实用户提问、品牌信息完整度、平台推荐表现与转化承接路径做了一轮基线评估。</p>
${renderKeyValueList(caseData.baseline)}
<h2>解决方案</h2>
<p>方案不只是补充内容,而是围绕 AI 能理解、能引用、能推荐的结构,重新组织品牌事实、用户问题和可信信源。</p>
${renderQuestionFramework(caseData.questionFramework)}
<h2>内容建设节奏</h2>
${renderPhases(caseData.phases)}
<h2>可信信源分发</h2>
${renderDistribution(caseData.distribution)}
<h2>项目成果</h2>
<p>以下指标用于展示项目推进前后的变化,不代表对其他项目作出相同效果承诺。</p>
${renderResultTable(caseData.resultTable)}
`
}
</script>
<template>
<div class="case-news-detail news-detail-page">
<a class="skip-link" href="#main-content">跳到主要内容</a>
<SiteHeader />
<main id="main-content" class="news-detail-main">
<div class="news-detail-shell">
<nav class="news-breadcrumb" aria-label="面包屑">
<NuxtLink to="/">首页</NuxtLink>
<span>/</span>
<NuxtLink to="/cases">客户案例</NuxtLink>
<span>/</span>
<b>{{ articleTag }}</b>
</nav>
<div class="news-detail-layout">
<article class="news-article">
<header class="news-article-header">
<h1>{{ articleTitle }}</h1>
<p class="news-excerpt">{{ caseStudy.description || caseStudy.summary }}</p>
<div class="news-meta">
<span>{{ caseStudy.client }}</span>
<span>{{ articleTag }}</span>
</div>
<div class="news-tag-row">
<span v-for="tag in caseTags" :key="tag">{{ tag }}</span>
</div>
</header>
<section class="news-consult-banner" aria-label="方案咨询">
<p>
<span>GEO重构流量入口让Al诊断您的品牌</span>
<span>事件影响力量化评估品牌价值</span>
</p>
<button type="button" @click="requestPlanConsultation">方案咨询</button>
</section>
<figure v-if="caseStudy.cover" class="news-cover">
<img :src="caseStudy.cover" :alt="articleTitle" />
</figure>
<UEditor :content="articleHtml" />
</article>
<aside class="news-sidebar" aria-label="相关推荐">
<section v-if="relatedCases.length" class="sidebar-panel">
<h2>相关案例</h2>
<ol class="hot-list">
<li v-for="(item, index) in relatedCases" :key="item.id || item.slug">
<NuxtLink :to="caseListLink(item)">
<span>{{ index + 1 }}</span>
<strong>{{ item.title }}</strong>
<em>{{ item.metric }}</em>
</NuxtLink>
</li>
</ol>
</section>
<section v-if="recommendedCases.length" class="sidebar-panel">
<h2>推荐案例</h2>
<div class="recommend-list">
<NuxtLink
v-for="item in recommendedCases"
:key="item.id || item.slug"
:to="`/cases/${item.slug}`"
>
<img :src="item.cover" :alt="item.title" />
<span>
<strong>{{ item.title }}</strong>
<small>{{ item.secondary }}</small>
</span>
</NuxtLink>
</div>
</section>
</aside>
</div>
</div>
</main>
<SiteFooter />
</div>
</template>
<style scoped>
.news-detail-page {
min-height: 100vh;
color: #132033;
background:
radial-gradient(circle at 90% 0, rgba(73, 144, 255, 0.12), transparent 320px),
linear-gradient(180deg, #f7fbff 0, #fff 330px);
}
.skip-link {
position: fixed;
z-index: 100;
top: 12px;
left: 12px;
padding: 10px 14px;
border-radius: 8px;
color: #fff;
background: #1677ff;
transform: translateY(-150%);
transition: transform 0.2s ease;
}
.skip-link:focus {
transform: translateY(0);
}
.news-detail-main {
padding: 42px 0 96px;
}
.news-detail-shell {
width: min(1296px, calc(100% - 48px));
margin: 0 auto;
}
.news-breadcrumb {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
color: #718197;
font-size: 14px;
}
.news-breadcrumb a {
color: #61738b;
text-decoration: none;
}
.news-breadcrumb a:hover,
.news-breadcrumb b {
color: #1677ff;
}
.news-detail-layout {
display: grid;
grid-template-columns: minmax(0, 880px) 320px;
align-items: start;
gap: 72px;
margin-top: 34px;
}
.news-article {
min-width: 0;
}
.news-article-header {
padding-bottom: 34px;
border-bottom: 1px solid #e6edf6;
}
.news-kicker {
margin: 0;
color: #1677ff;
font-size: 14px;
font-weight: 700;
letter-spacing: 0.08em;
}
.news-article h1 {
margin: 18px 0 0;
color: #111d2f;
font-size: clamp(30px, 3vw, 44px);
font-weight: 600;
letter-spacing: -0.035em;
line-height: 1.38;
text-wrap: pretty;
}
.news-excerpt {
max-width: 820px;
margin: 24px 0 0;
color: #6c7b90;
font-size: 16px;
line-height: 1.9;
}
.news-meta {
display: flex;
flex-wrap: wrap;
gap: 18px 28px;
margin-top: 26px;
color: #7b8da3;
font-size: 14px;
font-variant-numeric: tabular-nums;
}
.news-tag-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 18px;
}
.news-tag-row span {
padding: 5px 10px;
color: #6b7f96;
background: #eef5fd;
font-size: 13px;
}
.news-consult-banner {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 28px;
margin: 34px 0 38px;
padding: 28px 34px;
overflow: hidden;
background:
radial-gradient(circle at 8% 20%, rgba(255, 255, 255, 0.28), transparent 24%),
linear-gradient(105deg, #0f63ff 0%, #08b9d8 100%);
color: #fff;
}
.news-consult-banner p {
margin: 0;
font-size: clamp(18px, 2vw, 24px);
font-weight: 700;
letter-spacing: -0.025em;
line-height: 1.48;
}
.news-consult-banner span {
display: block;
}
.news-consult-banner button {
min-width: 136px;
height: 48px;
padding: 0 28px;
border: 0;
background: #fff;
color: #1677ff;
cursor: pointer;
font-size: 16px;
font-weight: 700;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
}
.news-consult-banner button:hover {
transform: translateY(-2px);
box-shadow: 0 14px 26px rgba(4, 67, 152, 0.24);
}
.news-cover {
margin: 42px 0 44px;
overflow: hidden;
border-radius: 10px;
background: #eef5fd;
}
.news-cover img {
width: 100%;
max-height: 420px;
display: block;
object-fit: cover;
}
.news-article :deep(.ueditor-content) {
color: #27364a;
font-size: 16px;
line-height: 2;
}
.news-article :deep(.ueditor-content h2) {
margin-top: 58px;
color: #111d2f;
font-size: 25px;
line-height: 1.45;
}
.news-article :deep(.ueditor-content h3) {
margin-top: 34px;
font-size: 20px;
}
.news-article :deep(.ueditor-content blockquote) {
margin: 36px 0;
border-left: 4px solid #d9e8fb;
border-radius: 0;
color: #53657c;
background: transparent;
}
.news-article :deep(.ueditor-content table) {
font-size: 14px;
}
.news-sidebar {
position: sticky;
top: 88px;
display: grid;
gap: 34px;
}
.sidebar-panel h2 {
position: relative;
margin: 0 0 18px;
color: #111d2f;
font-size: 16px;
font-weight: 700;
}
.sidebar-panel h2::after {
color: #ff7a1a;
content: '▼';
font-size: 10px;
margin-left: 2px;
}
.hot-list {
display: grid;
gap: 1px;
margin: 0;
padding: 0;
list-style: none;
background: #f5f9fd;
}
.hot-list a {
display: grid;
grid-template-columns: 28px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 13px 14px;
color: #172236;
text-decoration: none;
background: #f8fbff;
transition:
color 0.2s ease,
background 0.2s ease,
transform 0.2s ease;
}
.hot-list a:hover {
color: #1677ff;
background: #eef6ff;
transform: translateX(2px);
}
.hot-list span {
color: #ff8a2a;
font-weight: 700;
}
.hot-list strong {
overflow: hidden;
font-size: 14px;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
}
.hot-list em {
color: #1677ff;
font-size: 13px;
font-style: normal;
font-weight: 700;
}
.tag-cloud {
display: flex;
flex-wrap: wrap;
gap: 12px;
}
.tag-cloud span {
padding: 9px 15px;
border: 1px solid #e2eaf4;
color: #27364a;
background: #fff;
font-size: 14px;
}
.recommend-list {
display: grid;
gap: 15px;
}
.recommend-list a {
display: grid;
grid-template-columns: 82px minmax(0, 1fr);
gap: 12px;
color: #172236;
text-decoration: none;
}
.recommend-list img {
width: 82px;
height: 56px;
object-fit: cover;
background: #eef5fd;
}
.recommend-list strong {
display: -webkit-box;
overflow: hidden;
font-size: 14px;
font-weight: 500;
line-height: 1.5;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.recommend-list small {
display: block;
margin-top: 6px;
color: #7d8da1;
font-size: 13px;
line-height: 1.5;
}
@media (max-width: 980px) {
.news-detail-layout {
grid-template-columns: 1fr;
gap: 48px;
}
.news-sidebar {
position: static;
}
}
@media (max-width: 640px) {
.news-detail-main {
padding: 28px 0 68px;
}
.news-detail-shell {
width: min(100% - 24px, 560px);
}
.news-detail-layout {
margin-top: 24px;
}
.news-article h1 {
font-size: 28px;
}
.news-consult-banner {
grid-template-columns: 1fr;
gap: 18px;
margin: 28px 0 32px;
padding: 24px;
}
.news-consult-banner button {
width: 100%;
}
.news-cover img {
max-height: 260px;
}
.hot-list a {
grid-template-columns: 24px minmax(0, 1fr);
}
.hot-list em {
display: none;
}
}
</style>