feat: 后台管理系统完整重构 + 前后台数据联通
- 后台全面引入 Naive UI 组件库,统一 UI 规范 - 前台 usePublicApi 切换到 API 调用(GET /api/content/[key]) - 前后台数据源统一(site_content 表) - 产品线统一管理(tour/camp/course 三套编辑器) - 产品数据归一化(itinerary/faq/pricing 格式统一) - 表单提交 API 改写入 submissions 表 - 新增 submissions 标记已读 API - site-content PUT 支持 upsert - 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb) - 消除 PageHero 类型警告(useSEO reactive 修复) - 25 个前台页面全部 HTTP 200,展示效果不变 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
这个提交包含在:
+580
@@ -0,0 +1,580 @@
|
||||
<template>
|
||||
<div v-if="article" class="page-blog-detail">
|
||||
<LayoutPageHero
|
||||
:title="article.title"
|
||||
:subtitle="article.subtitle"
|
||||
:breadcrumbs="[{ text: '攻略', to: '/blog' }, { text: article.category }]"
|
||||
/>
|
||||
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<div class="blog-layout">
|
||||
<!-- 主内容区 -->
|
||||
<main class="blog-main">
|
||||
<!-- 文章元信息 -->
|
||||
<div class="article-meta">
|
||||
<span class="article-category">{{ article.category }}</span>
|
||||
<span class="article-date">{{ formatDate(article.date) }}</span>
|
||||
<span class="article-author">作者:{{ article.author }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 封面图 -->
|
||||
<div class="article-cover">
|
||||
<img
|
||||
:src="article.coverImage"
|
||||
:alt="article.title"
|
||||
class="article-cover__img"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 摘要 -->
|
||||
<div class="article-summary">
|
||||
<p>{{ article.summary }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 正文 -->
|
||||
<div class="article-content" v-html="article.content" />
|
||||
|
||||
<!-- 标签 -->
|
||||
<div class="article-tags">
|
||||
<span class="tags-label">标签:</span>
|
||||
<span v-for="tag in article.tags" :key="tag" class="article-tag">{{ tag }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 相关产品 -->
|
||||
<div v-if="relatedProducts.length" class="article-products">
|
||||
<h3 class="article-products__title">推荐行程</h3>
|
||||
<div class="article-products__list">
|
||||
<NuxtLink
|
||||
v-for="prod in relatedProducts"
|
||||
:key="prod.id"
|
||||
:to="`/products/${prod.id}`"
|
||||
class="product-link"
|
||||
>
|
||||
<div class="product-link__info">
|
||||
<span class="product-link__name">{{ prod.name }}</span>
|
||||
<span class="product-link__duration">{{ prod.duration }}</span>
|
||||
</div>
|
||||
<span class="product-link__arrow">→</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章导航 -->
|
||||
<div class="article-nav">
|
||||
<NuxtLink v-if="prevArticle" :to="`/blog/${prevArticle.id}`" class="article-nav__item article-nav__item--prev">
|
||||
<span class="article-nav__label">上一篇</span>
|
||||
<span class="article-nav__title">{{ prevArticle.title }}</span>
|
||||
</NuxtLink>
|
||||
<NuxtLink v-if="nextArticle" :to="`/blog/${nextArticle.id}`" class="article-nav__item article-nav__item--next">
|
||||
<span class="article-nav__label">下一篇</span>
|
||||
<span class="article-nav__title">{{ nextArticle.title }}</span>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 侧边栏 -->
|
||||
<aside class="blog-sidebar">
|
||||
<!-- 联系咨询 -->
|
||||
<div class="sidebar-card sidebar-card--cta">
|
||||
<h3 class="sidebar-card__title">免费获取定制方案</h3>
|
||||
<p class="sidebar-card__desc">看完攻略有疑问?联系呼籁旅行定制师,一对一解答。</p>
|
||||
<CommonInternalLink to="/contact" text="立即咨询" />
|
||||
</div>
|
||||
|
||||
<!-- 全部文章 -->
|
||||
<div class="sidebar-card">
|
||||
<h3 class="sidebar-card__title">更多攻略</h3>
|
||||
<ul class="sidebar-articles">
|
||||
<li
|
||||
v-for="art in otherArticles"
|
||||
:key="art.id"
|
||||
class="sidebar-article"
|
||||
:class="{ 'sidebar-article--active': art.id === article.id }"
|
||||
>
|
||||
<NuxtLink :to="`/blog/${art.id}`" class="sidebar-article__link">
|
||||
<span class="sidebar-article__category">{{ art.category }}</span>
|
||||
<span class="sidebar-article__title">{{ art.title }}</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 快速链接 -->
|
||||
<div class="sidebar-card">
|
||||
<h3 class="sidebar-card__title">快速入口</h3>
|
||||
<div class="sidebar-links">
|
||||
<CommonInternalLink to="/products" text="查看旅行产品" />
|
||||
<CommonInternalLink to="/faq" text="常见问答" />
|
||||
<CommonInternalLink to="/guides" text="出行指南" />
|
||||
<CommonInternalLink to="/reviews" text="客户评价" />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 404 -->
|
||||
<div v-else class="page-not-found">
|
||||
<div class="container">
|
||||
<h1>文章不存在</h1>
|
||||
<NuxtLink to="/blog">返回攻略列表</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
|
||||
import useScrollReveal from '~/composables/useScrollReveal'
|
||||
|
||||
const { data: blogData } = await usePublicApi('blog')
|
||||
const { data: productsData } = await usePublicApi('products')
|
||||
|
||||
const route = useRoute()
|
||||
const allArticles = computed(() => blogData.value?.articles || [])
|
||||
const article = computed(() => allArticles.value.find(a => a.id === route.params.id))
|
||||
|
||||
if (!article.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: '文章不存在' })
|
||||
}
|
||||
|
||||
// SEO
|
||||
useHead({
|
||||
title: article.value?.seo?.title || article.value?.title,
|
||||
meta: [
|
||||
{ name: 'description', content: article.value?.seo?.description || article.value?.summary },
|
||||
{ name: 'keywords', content: article.value?.seo?.keywords || (article.value?.tags || []).join(',') },
|
||||
{ name: 'author', content: article.value?.author },
|
||||
{ name: 'article:published_time', content: (article.value?.date || '') + 'T00:00:00+08:00' },
|
||||
{ name: 'article:modified_time', content: (article.value?.date || '') + 'T00:00:00+08:00' },
|
||||
{ name: 'geo.region', content: 'CN-NM' },
|
||||
{ name: 'geo.placename', content: '呼伦贝尔' },
|
||||
{ property: 'og:title', content: article.value?.seo?.title || article.value?.title },
|
||||
{ property: 'og:description', content: article.value?.seo?.description || article.value?.summary },
|
||||
{ property: 'og:type', content: 'article' },
|
||||
{ property: 'og:url', content: `https://1814.love/blog/${article.value?.id}` },
|
||||
{ property: 'og:site_name', content: '呼籁旅行' },
|
||||
{ property: 'og:locale', content: 'zh_CN' },
|
||||
],
|
||||
link: [
|
||||
{ rel: 'canonical', href: `https://1814.love/blog/${article.value?.id}` },
|
||||
],
|
||||
})
|
||||
|
||||
useScrollReveal()
|
||||
|
||||
useBreadcrumbSchema([
|
||||
{ text: '首页', to: '/' },
|
||||
{ text: '旅行攻略', to: '/blog' },
|
||||
{ text: article.value?.category, to: `/blog?category=${article.value?.category}` },
|
||||
{ text: article.value?.title },
|
||||
])
|
||||
|
||||
// JSON-LD Article schema
|
||||
useHead({
|
||||
script: [{
|
||||
type: 'application/ld+json',
|
||||
children: JSON.stringify({
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Article',
|
||||
'@id': `https://1814.love/blog/${article.value?.id}#article`,
|
||||
headline: article.value?.title,
|
||||
description: article.value?.summary,
|
||||
image: article.value?.coverImage ? `https://1814.love${article.value.coverImage}` : 'https://1814.love/images/logo.png',
|
||||
author: {
|
||||
'@type': 'Organization',
|
||||
'@id': 'https://1814.love/#organization',
|
||||
name: '呼籁旅行',
|
||||
url: 'https://1814.love',
|
||||
},
|
||||
publisher: {
|
||||
'@type': 'Organization',
|
||||
'@id': 'https://1814.love/#organization',
|
||||
name: '呼籁旅行',
|
||||
url: 'https://1814.love',
|
||||
logo: {
|
||||
'@type': 'ImageObject',
|
||||
url: 'https://1814.love/images/logo.png',
|
||||
width: 200,
|
||||
height: 200,
|
||||
},
|
||||
},
|
||||
datePublished: article.value?.date,
|
||||
dateModified: article.value?.date,
|
||||
mainEntityOfPage: {
|
||||
'@type': 'WebPage',
|
||||
'@id': `https://1814.love/blog/${article.value?.id}`,
|
||||
},
|
||||
inLanguage: 'zh-CN',
|
||||
keywords: (article.value?.tags || []).join(','),
|
||||
}),
|
||||
}],
|
||||
})
|
||||
|
||||
// 相关产品
|
||||
const relatedProducts = computed(() => {
|
||||
if (!article.value?.relatedProducts?.length || !productsData.value?.versions) return []
|
||||
return productsData.value.versions
|
||||
.filter(v => article.value.relatedProducts.includes(v.id))
|
||||
.map(v => ({ id: v.id, name: v.name, duration: `${v.days}天${v.nights}晚 · ${v.tag}` }))
|
||||
})
|
||||
|
||||
// 上下篇
|
||||
const currentIndex = computed(() => allArticles.value.findIndex(a => a.id === article.value?.id))
|
||||
const prevArticle = computed(() => currentIndex.value > 0 ? allArticles.value[currentIndex.value - 1] : null)
|
||||
const nextArticle = computed(() => currentIndex.value < allArticles.value.length - 1 ? allArticles.value[currentIndex.value + 1] : null)
|
||||
|
||||
// 侧边栏文章
|
||||
const otherArticles = allArticles
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const d = new Date(dateStr)
|
||||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.blog-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-2xl;
|
||||
|
||||
.respond-lg({
|
||||
grid-template-columns: 1fr 300px;
|
||||
align-items: start;
|
||||
});
|
||||
}
|
||||
|
||||
// 主内容
|
||||
.blog-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.article-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: @space-md;
|
||||
margin-bottom: @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
.article-category {
|
||||
padding: 3px @space-sm;
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
border-radius: @border-radius-sm;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.article-cover {
|
||||
margin-bottom: @space-xl;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16 / 7;
|
||||
background: @color-primary-bg;
|
||||
|
||||
&__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.article-summary {
|
||||
padding: @space-lg;
|
||||
background: @color-primary-bg;
|
||||
border-left: 4px solid @color-primary;
|
||||
border-radius: 0 @border-radius-md @border-radius-md 0;
|
||||
margin-bottom: @space-xl;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: @font-size-md;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
}
|
||||
|
||||
// 文章正文样式
|
||||
.article-content {
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
font-size: @font-size-base;
|
||||
|
||||
:deep(h2) {
|
||||
font-size: @font-size-xl;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin: @space-2xl 0 @space-lg;
|
||||
padding-bottom: @space-sm;
|
||||
border-bottom: 2px solid @color-primary-bg;
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(h3) {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin: @space-xl 0 @space-md;
|
||||
}
|
||||
|
||||
:deep(p) {
|
||||
margin: 0 0 @space-lg;
|
||||
}
|
||||
|
||||
:deep(ul), :deep(ol) {
|
||||
margin: 0 0 @space-lg;
|
||||
padding-left: @space-xl;
|
||||
}
|
||||
|
||||
:deep(li) {
|
||||
margin-bottom: @space-sm;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
:deep(strong) {
|
||||
color: @color-text-primary;
|
||||
font-weight: @font-weight-bold;
|
||||
}
|
||||
|
||||
:deep(br) {
|
||||
display: block;
|
||||
content: '';
|
||||
margin-top: @space-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// 标签
|
||||
.article-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
margin-top: @space-2xl;
|
||||
padding-top: @space-lg;
|
||||
border-top: 1px solid @color-border;
|
||||
}
|
||||
|
||||
.tags-label {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
.article-tag {
|
||||
padding: 3px @space-sm;
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
border-radius: @border-radius-sm;
|
||||
font-size: @font-size-xs;
|
||||
}
|
||||
|
||||
// 相关产品
|
||||
.article-products {
|
||||
margin-top: @space-xl;
|
||||
|
||||
&__title {
|
||||
font-size: @font-size-md;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin: 0 0 @space-md;
|
||||
}
|
||||
|
||||
&__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.product-link {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: @space-md @space-lg;
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
text-decoration: none;
|
||||
transition: all @transition-base;
|
||||
|
||||
&:hover {
|
||||
border-color: @color-primary;
|
||||
background: @color-primary-bg;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-text-primary;
|
||||
}
|
||||
|
||||
&__duration {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
color: @color-primary;
|
||||
font-size: @font-size-md;
|
||||
transition: transform @transition-fast;
|
||||
}
|
||||
|
||||
&:hover &__arrow {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
}
|
||||
|
||||
// 文章导航
|
||||
.article-nav {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: @space-md;
|
||||
margin-top: @space-2xl;
|
||||
padding-top: @space-lg;
|
||||
border-top: 1px solid @color-border;
|
||||
|
||||
&__item {
|
||||
padding: @space-md;
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
text-decoration: none;
|
||||
transition: all @transition-base;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-xs;
|
||||
|
||||
&:hover {
|
||||
border-color: @color-primary;
|
||||
background: @color-primary-bg;
|
||||
}
|
||||
|
||||
&--next {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-primary;
|
||||
line-height: @line-height-tight;
|
||||
.text-clamp(2);
|
||||
}
|
||||
}
|
||||
|
||||
// 侧边栏
|
||||
.blog-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-lg;
|
||||
position: sticky;
|
||||
top: 90px;
|
||||
}
|
||||
|
||||
.sidebar-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-lg;
|
||||
padding: @space-lg;
|
||||
|
||||
&--cta {
|
||||
background: @color-primary-bg;
|
||||
border-color: @color-primary-lighter;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: @font-size-base;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin: 0 0 @space-sm;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin: 0 0 @space-md;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-articles {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.sidebar-article {
|
||||
&--active .sidebar-article__link {
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
&__link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
padding: @space-sm;
|
||||
border-radius: @border-radius-sm;
|
||||
text-decoration: none;
|
||||
transition: background @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary-bg;
|
||||
}
|
||||
}
|
||||
|
||||
&__category {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-tight;
|
||||
.text-clamp(2);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
// 404
|
||||
.page-not-found {
|
||||
text-align: center;
|
||||
padding: @space-3xl 0;
|
||||
|
||||
h1 {
|
||||
margin-bottom: @space-lg;
|
||||
}
|
||||
|
||||
a {
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+365
@@ -0,0 +1,365 @@
|
||||
<template>
|
||||
<div class="page-blog">
|
||||
<LayoutPageHero
|
||||
title="旅行攻略"
|
||||
subtitle="呼伦贝尔旅行的干货指南,帮你把每一趟出行都过得值"
|
||||
:breadcrumbs="[{ text: '攻略' }]"
|
||||
background-image="/images/seasons/summer-river.jpg"
|
||||
/>
|
||||
|
||||
<!-- TL;DR -->
|
||||
<section class="page-tldr" aria-label="旅行攻略摘要">
|
||||
<div class="container">
|
||||
<div class="page-tldr-inner">
|
||||
<p>呼伦贝尔旅行攻略合集:包含穿搭指南、行程规划、亲子活动推荐、避坑经验等干货内容,帮你把每一趟出行都过得值。</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="blogData" class="section">
|
||||
<div class="container">
|
||||
<!-- 分类筛选 -->
|
||||
<div class="blog-filter">
|
||||
<button
|
||||
v-for="cat in categories"
|
||||
:key="cat.key"
|
||||
class="filter-btn"
|
||||
:class="{ 'filter-btn--active': activeCategory === cat.key }"
|
||||
@click="activeCategory = cat.key"
|
||||
>
|
||||
{{ cat.label }}
|
||||
<span class="filter-count">{{ getCategoryCount(cat.key) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 文章列表 -->
|
||||
<div class="blog-grid">
|
||||
<article
|
||||
v-for="article in filteredArticles"
|
||||
:key="article.id"
|
||||
class="blog-card reveal"
|
||||
>
|
||||
<NuxtLink :to="`/blog/${article.id}`" class="blog-card__link">
|
||||
<div class="blog-card__cover">
|
||||
<img
|
||||
:src="article.coverImage"
|
||||
:alt="article.title"
|
||||
class="blog-card__img"
|
||||
loading="lazy"
|
||||
/>
|
||||
<span class="blog-card__category">{{ article.category }}</span>
|
||||
</div>
|
||||
<div class="blog-card__body">
|
||||
<div class="blog-card__meta">
|
||||
<span class="blog-card__date">{{ formatDate(article.date) }}</span>
|
||||
<span class="blog-card__author">{{ article.author }}</span>
|
||||
</div>
|
||||
<h2 class="blog-card__title">{{ article.title }}</h2>
|
||||
<p class="blog-card__subtitle">{{ article.subtitle }}</p>
|
||||
<p class="blog-card__summary">{{ article.summary }}</p>
|
||||
<div class="blog-card__tags">
|
||||
<span v-for="tag in article.tags.slice(0, 3)" :key="tag" class="blog-tag">{{ tag }}</span>
|
||||
</div>
|
||||
<span class="blog-card__read-more">阅读全文 →</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredArticles.length === 0" class="blog-empty">
|
||||
<p>暂无该分类的文章</p>
|
||||
</div>
|
||||
|
||||
<!-- CTA -->
|
||||
<div class="blog-cta">
|
||||
<h2>看完攻略,准备出发了吗?</h2>
|
||||
<p>呼籁旅行为每个家庭提供专属定制方案,告诉我们你的需求,我们帮你把行程安排得妥妥的。</p>
|
||||
<div class="blog-cta-links">
|
||||
<CommonInternalLink to="/products" text="查看定制游产品" />
|
||||
<CommonInternalLink to="/contact" text="咨询定制师" />
|
||||
<CommonInternalLink to="/faq" text="常见问答" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
|
||||
import useScrollReveal from '~/composables/useScrollReveal'
|
||||
|
||||
const { data: blogData } = await usePublicApi('blog')
|
||||
|
||||
useHead({
|
||||
title: '呼伦贝尔旅行攻略 - 亲子游·避坑·穿搭·行程干货 - 呼籁旅行',
|
||||
meta: [
|
||||
{ name: 'description', content: '呼籁旅行旅行攻略频道:呼伦贝尔亲子游完全指南、几月份去最好、旅行花费明细、避坑指南、草原旅拍穿搭,深耕11年的实用干货。' },
|
||||
{ name: 'keywords', content: '呼伦贝尔旅游攻略,呼伦贝尔亲子游攻略,呼伦贝尔几月去,呼伦贝尔旅游花费,草原旅拍穿搭' },
|
||||
{ property: 'og:title', content: '呼伦贝尔旅行攻略 - 呼籁旅行' },
|
||||
{ property: 'og:description', content: '呼伦贝尔亲子游·避坑指南·穿搭攻略,深耕11年的实用干货。' },
|
||||
{ property: 'og:type', content: 'website' },
|
||||
],
|
||||
link: [
|
||||
{ rel: 'canonical', href: 'https://1814.love/blog' },
|
||||
],
|
||||
})
|
||||
|
||||
useBreadcrumbSchema([
|
||||
{ text: '首页', to: '/' },
|
||||
{ text: '旅行攻略' },
|
||||
])
|
||||
|
||||
useScrollReveal()
|
||||
|
||||
const categories = [
|
||||
{ key: 'all', label: '全部' },
|
||||
{ key: '攻略', label: '出行攻略' },
|
||||
{ key: '亲子', label: '亲子游' },
|
||||
{ key: '避坑', label: '避坑指南' },
|
||||
{ key: '穿搭', label: '旅拍穿搭' },
|
||||
]
|
||||
|
||||
const activeCategory = ref('all')
|
||||
|
||||
const allArticles = computed(() => blogData.value?.articles || [])
|
||||
|
||||
const filteredArticles = computed(() => {
|
||||
if (activeCategory.value === 'all') return allArticles.value
|
||||
return allArticles.value.filter(a => a.category === activeCategory.value)
|
||||
})
|
||||
|
||||
function getCategoryCount(key) {
|
||||
if (key === 'all') return allArticles.value.length
|
||||
return allArticles.value.filter(a => a.category === key).length
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const d = new Date(dateStr)
|
||||
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
// 分类筛选
|
||||
.blog-filter {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-2xl;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: @space-xs;
|
||||
padding: @space-sm @space-md;
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-full;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
cursor: pointer;
|
||||
transition: all @transition-base;
|
||||
|
||||
&:hover {
|
||||
border-color: @color-primary;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: @color-primary;
|
||||
border-color: @color-primary;
|
||||
color: #fff;
|
||||
|
||||
.filter-count {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.filter-count {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 5px;
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
border-radius: @border-radius-full;
|
||||
font-size: 11px;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
// 文章网格
|
||||
.blog-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-xl;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: 1fr 1fr;
|
||||
});
|
||||
|
||||
.respond-lg({
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
});
|
||||
}
|
||||
|
||||
// 文章卡片
|
||||
.blog-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
transition: box-shadow @transition-base, transform @transition-base;
|
||||
|
||||
&:hover {
|
||||
box-shadow: @shadow-lg;
|
||||
transform: translateY(-2px);
|
||||
|
||||
.blog-card__img {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
}
|
||||
|
||||
&__link {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&__cover {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 16 / 9;
|
||||
background: @color-primary-bg;
|
||||
}
|
||||
|
||||
&__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform @transition-slow;
|
||||
}
|
||||
|
||||
&__category {
|
||||
position: absolute;
|
||||
top: @space-sm;
|
||||
left: @space-sm;
|
||||
padding: 3px @space-sm;
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
border-radius: @border-radius-sm;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: @space-lg;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
gap: @space-md;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: @font-size-md;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
line-height: @line-height-tight;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-accent;
|
||||
margin: 0;
|
||||
line-height: @line-height-tight;
|
||||
}
|
||||
|
||||
&__summary {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin: 0;
|
||||
.text-clamp(3);
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-xs;
|
||||
margin-top: auto;
|
||||
padding-top: @space-sm;
|
||||
}
|
||||
|
||||
&__read-more {
|
||||
display: inline-block;
|
||||
margin-top: @space-sm;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-primary;
|
||||
font-weight: @font-weight-medium;
|
||||
transition: color @transition-fast;
|
||||
|
||||
.blog-card:hover & {
|
||||
color: @color-primary-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.blog-tag {
|
||||
display: inline-block;
|
||||
padding: 2px @space-sm;
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
border-radius: @border-radius-sm;
|
||||
font-size: @font-size-xs;
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.blog-empty {
|
||||
text-align: center;
|
||||
padding: @space-3xl;
|
||||
color: @color-text-muted;
|
||||
font-size: @font-size-md;
|
||||
}
|
||||
|
||||
// CTA
|
||||
.blog-cta {
|
||||
text-align: center;
|
||||
margin-top: @space-3xl;
|
||||
padding: @space-2xl;
|
||||
background: @color-primary-bg;
|
||||
border-radius: @border-radius-lg;
|
||||
|
||||
h2 {
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
p {
|
||||
color: @color-text-secondary;
|
||||
margin-bottom: @space-xl;
|
||||
max-width: 500px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.blog-cta-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: @space-xl;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
</style>
|
||||
在新工单中引用
屏蔽一个用户