523 lines
12 KiB
Vue
523 lines
12 KiB
Vue
<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 { bySortAndTime, fetchPublicContent, normalizeArticle } = useOfficialContent()
|
|
const slug = String(route.params.slug || '')
|
|
|
|
const { data: pageData } = await useAsyncData(`public-insight-${slug}`, async () => {
|
|
const rows = await fetchPublicContent('ARTICLE')
|
|
const articles = rows.sort(bySortAndTime).map((item) => normalizeArticle(item, new Map()))
|
|
return {
|
|
articles,
|
|
current: articles.find((item) => item.slug === slug) || null
|
|
}
|
|
})
|
|
|
|
if (!pageData.value?.current) {
|
|
throw createError({ statusCode: 404, statusMessage: '洞察文章不存在' })
|
|
}
|
|
|
|
const article = computed(() => pageData.value.current)
|
|
const relatedArticles = computed(() =>
|
|
(pageData.value?.articles || []).filter((item) => item.slug !== article.value.slug).slice(0, 6)
|
|
)
|
|
const hotArticles = computed(() =>
|
|
(pageData.value?.articles || []).filter((item) => item.slug !== article.value.slug).slice(0, 8)
|
|
)
|
|
const articleHtml = computed(() => article.value.contentHtml || '<p>暂无正文内容。</p>')
|
|
const articleTagList = computed(() =>
|
|
article.value.tags?.length ? article.value.tags : [article.value.category, 'GEO', '品牌增长'].filter(Boolean)
|
|
)
|
|
|
|
|
|
useHead(() => ({
|
|
title: `${article.value.title}|云途全域增长洞察`,
|
|
meta: [
|
|
{ name: 'description', content: article.value.excerpt },
|
|
{ property: 'og:title', content: article.value.title },
|
|
{ property: 'og:description', content: article.value.excerpt },
|
|
{ property: 'og:image', content: article.value.cover }
|
|
]
|
|
}))
|
|
|
|
|
|
|
|
function escapeHtml(value) {
|
|
return String(value ?? '')
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''')
|
|
}
|
|
|
|
function renderParagraphs(paragraphs = []) {
|
|
return paragraphs.map((paragraph) => `<p>${escapeHtml(paragraph)}</p>`).join('')
|
|
}
|
|
|
|
function renderComparisonTable(rows = []) {
|
|
if (!rows.length) return ''
|
|
|
|
return `
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>比较维度</th>
|
|
<th>传统 SEO</th>
|
|
<th>GEO</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
${rows
|
|
.map(
|
|
(row) => `
|
|
<tr>
|
|
<td><strong>${escapeHtml(row[0])}</strong></td>
|
|
<td>${escapeHtml(row[1])}</td>
|
|
<td>${escapeHtml(row[2])}</td>
|
|
</tr>
|
|
`
|
|
)
|
|
.join('')}
|
|
</tbody>
|
|
</table>
|
|
`
|
|
}
|
|
|
|
function renderSections(sections = []) {
|
|
if (!sections.length) {
|
|
return `
|
|
<h2>核心观察</h2>
|
|
<p>${escapeHtml(article.value.excerpt)}</p>
|
|
<blockquote>完整内容正在编辑和复核中,当前先保留摘要信息,避免发布未经确认的细节。</blockquote>
|
|
`
|
|
}
|
|
|
|
return sections
|
|
.map(
|
|
(section) => `
|
|
<h2>${escapeHtml(section.title)}</h2>
|
|
${renderParagraphs(section.paragraphs)}
|
|
${renderComparisonTable(section.comparison)}
|
|
`
|
|
)
|
|
.join('')
|
|
}
|
|
|
|
function buildInsightArticleHtml(articleData) {
|
|
return `
|
|
${articleData.lead ? `<blockquote>${escapeHtml(articleData.lead)}</blockquote>` : ''}
|
|
${renderSections(articleData.sections)}
|
|
<blockquote>GEO 的目标不是让品牌在每一个问题里出现,而是在真正影响用户决策的问题中,被准确理解并获得可信推荐。</blockquote>
|
|
`
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="news-detail-page insight-news-detail">
|
|
<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="/insights">增长洞察</NuxtLink>
|
|
<span>/</span>
|
|
<b>{{ article.category }}</b>
|
|
</nav>
|
|
|
|
<div class="news-detail-layout">
|
|
<article class="news-article">
|
|
<header class="news-article-header">
|
|
<h1>{{ article.title }}</h1>
|
|
<p class="news-excerpt">{{ article.excerpt }}</p>
|
|
<div class="news-meta">
|
|
<time :datetime="article.date">{{ article.date }}</time>
|
|
<span>{{ article.readingTime }}</span>
|
|
<span>{{ article.views }} 阅读</span>
|
|
</div>
|
|
<div class="news-tag-row">
|
|
<span v-for="tag in articleTagList" :key="tag">{{ tag }}</span>
|
|
</div>
|
|
</header>
|
|
|
|
<figure v-if="article.cover" class="news-cover">
|
|
<img :src="article.cover" :alt="article.title" />
|
|
</figure>
|
|
<UEditor :content="articleHtml" />
|
|
</article>
|
|
|
|
<aside class="news-sidebar" aria-label="相关推荐">
|
|
<section class="sidebar-panel">
|
|
<h2>推荐文章</h2>
|
|
<ol class="hot-list">
|
|
<li v-for="(item, index) in hotArticles" :key="item.slug">
|
|
<NuxtLink :to="`/insights/${item.slug}`">
|
|
<span>{{ index + 1 }}</span>
|
|
<strong>{{ item.title }}</strong>
|
|
<time :datetime="item.date">{{ item.date.replaceAll('-', '/') }}</time>
|
|
</NuxtLink>
|
|
</li>
|
|
</ol>
|
|
</section>
|
|
|
|
<section class="sidebar-panel">
|
|
<h2>相关文章</h2>
|
|
<div class="recommend-list">
|
|
<NuxtLink
|
|
v-for="item in relatedArticles.slice(0, 4)"
|
|
:key="item.slug"
|
|
:to="`/insights/${item.slug}`"
|
|
>
|
|
<img :src="item.cover" :alt="item.title" />
|
|
<span>
|
|
<strong>{{ item.title }}</strong>
|
|
<small>{{ item.date }}</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 time {
|
|
color: #7b8b9e;
|
|
font-size: 13px;
|
|
}
|
|
.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;
|
|
}
|
|
@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 time {
|
|
display: none;
|
|
}
|
|
}
|
|
</style>
|