Files
2026-07-21 11:09:17 +08:00

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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;')
}
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>