- 后台全面引入 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>
457 行
12 KiB
Vue
457 行
12 KiB
Vue
<template>
|
||
<div class="page-destinations">
|
||
<LayoutPageHero
|
||
title="呼伦贝尔核心景区攻略"
|
||
subtitle="深耕呼伦贝尔11年,带你发现最真实的草原秘境"
|
||
:breadcrumbs="[{ text: '景区攻略' }]"
|
||
background-image="/images/seasons/summer-river.jpg"
|
||
/>
|
||
|
||
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
|
||
<section class="page-tldr" aria-label="景区攻略摘要">
|
||
<div class="container">
|
||
<div class="page-tldr-inner">
|
||
<p>呼伦贝尔核心景区覆盖草原、湿地、森林、边境、火山地质等多种景观类型。本页梳理了呼籁旅行三大产品线(额吉的故乡·夏季、游牧的森林·秋季、嗨冰雪·冬季)覆盖的所有核心景区,包括莫日格勒河、额尔古纳湿地、白桦林、恩和室韦、黑山头、满洲里、阿尔山国家森林公园、大兴安岭莫尔道嘎、敖鲁古雅驯鹿部落、根河等,提供每处景区的实用攻略、最佳时间和旅行贴士。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="destinationsData" class="section">
|
||
<div class="container">
|
||
<!-- 季节筛选 -->
|
||
<div class="dest-filter">
|
||
<button
|
||
v-for="s in seasonFilters"
|
||
:key="s.key"
|
||
class="filter-btn"
|
||
:class="{ 'filter-btn--active': activeSeason === s.key }"
|
||
@click="activeSeason = s.key"
|
||
>
|
||
{{ s.label }}
|
||
<span class="filter-count">{{ getSeasonCount(s.key) }}</span>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 搜索 -->
|
||
<div class="dest-search">
|
||
<svg class="dest-search__icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
|
||
<input
|
||
v-model="searchQuery"
|
||
type="text"
|
||
placeholder="搜索景区名称或关键词…"
|
||
class="dest-search__input"
|
||
/>
|
||
<button v-if="searchQuery" class="dest-search__clear" @click="searchQuery = ''">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 景区列表 -->
|
||
<div v-if="filteredDestinations.length === 0" class="dest-empty">
|
||
<p>没有找到匹配的景区,试试其他关键词</p>
|
||
</div>
|
||
|
||
<div class="dest-grid">
|
||
<NuxtLink
|
||
v-for="dest in filteredDestinations"
|
||
:key="dest.id"
|
||
:to="`/destinations/${dest.id}`"
|
||
class="dest-card reveal"
|
||
>
|
||
<div class="dest-card__header">
|
||
<span class="dest-card__tag">{{ dest.tag }}</span>
|
||
<span class="dest-card__season">{{ dest.bestSeason.primary }}</span>
|
||
</div>
|
||
<h2 class="dest-card__name">{{ dest.name }}</h2>
|
||
<p class="dest-card__subtitle">{{ dest.subtitle }}</p>
|
||
<p class="dest-card__desc">{{ dest.description.substring(0, 100) }}…</p>
|
||
<div class="dest-card__highlights">
|
||
<span v-for="(h, i) in dest.highlights.slice(0, 2)" :key="i" class="dest-card__highlight">{{ h.split('——')[0] }}</span>
|
||
</div>
|
||
<div class="dest-card__seasons">
|
||
<span v-if="dest.seasons.includes('summer')" class="season-dot season-dot--summer" title="额吉的故乡·夏季">夏</span>
|
||
<span v-if="dest.seasons.includes('autumn')" class="season-dot season-dot--autumn" title="游牧的森林·秋季">秋</span>
|
||
<span v-if="dest.seasons.includes('winter')" class="season-dot season-dot--winter" title="嗨冰雪·冬季">冬</span>
|
||
</div>
|
||
<span class="dest-card__link">查看攻略 →</span>
|
||
</NuxtLink>
|
||
</div>
|
||
|
||
<!-- CTA -->
|
||
<div class="dest-cta">
|
||
<h2>不确定去哪些景区?让定制师帮你组合</h2>
|
||
<p>呼籁旅行定制师深耕呼伦贝尔11年,根据你的时间、季节、孩子年龄,帮你搭配最合适的景区组合和行程节奏。</p>
|
||
<div class="dest-cta__links">
|
||
<CommonInternalLink to="/contact" text="免费咨询定制师" />
|
||
<CommonInternalLink to="/products" text="查看定制游产品" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import useSEO from '~/composables/useSEO'
|
||
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
|
||
import useScrollReveal from '~/composables/useScrollReveal'
|
||
|
||
const { data: destinationsData } = await usePublicApi('destinations-detail')
|
||
|
||
const destinations = computed(() => destinationsData.value?.destinations || [])
|
||
|
||
const searchQuery = ref('')
|
||
const activeSeason = ref('all')
|
||
|
||
const seasonFilters = [
|
||
{ key: 'all', label: '全部景区' },
|
||
{ key: 'summer', label: '夏季·额吉的故乡' },
|
||
{ key: 'autumn', label: '秋季·游牧的森林' },
|
||
{ key: 'winter', label: '冬季·嗨冰雪' },
|
||
]
|
||
|
||
function getSeasonCount(key) {
|
||
if (key === 'all') return destinations.value.length
|
||
return destinations.value.filter(d => d.seasons?.includes(key)).length
|
||
}
|
||
|
||
const filteredDestinations = computed(() => {
|
||
return destinations.value.filter(d => {
|
||
const matchSeason = activeSeason.value === 'all' || d.seasons?.includes(activeSeason.value)
|
||
const q = searchQuery.value.trim().toLowerCase()
|
||
const matchSearch = !q
|
||
|| d.name.toLowerCase().includes(q)
|
||
|| d.subtitle?.toLowerCase().includes(q)
|
||
|| d.tag.toLowerCase().includes(q)
|
||
|| (d.highlights || []).some(h => h.toLowerCase().includes(q))
|
||
return matchSeason && matchSearch
|
||
})
|
||
})
|
||
|
||
useSEO('destinations', {
|
||
title: '呼伦贝尔核心景区攻略 - 莫日格勒河 额尔古纳 阿尔山等景点详解 | 呼籁旅行',
|
||
description: '呼籁旅行深耕呼伦贝尔11年,为你梳理三大产品线覆盖的核心景区攻略:莫日格勒河、额尔古纳湿地、白桦林、恩和室韦、黑山头、满洲里、阿尔山、大兴安岭、敖鲁古雅驯鹿部落、根河,含实用贴士和最佳时间。',
|
||
})
|
||
|
||
useHead({
|
||
meta: [
|
||
{ name: 'keywords', content: '呼伦贝尔景点,呼伦贝尔旅游攻略,莫日格勒河,额尔古纳湿地,白桦林,黑山头,满洲里,阿尔山,大兴安岭,敖鲁古雅,根河,呼伦贝尔景区' },
|
||
],
|
||
script: [
|
||
{
|
||
type: 'application/ld+json',
|
||
innerHTML: JSON.stringify({
|
||
'@context': 'https://schema.org',
|
||
'@type': 'ItemList',
|
||
name: '呼伦贝尔核心旅游景区',
|
||
description: '呼籁旅行三大产品线覆盖的呼伦贝尔核心景区,含草原、湿地、森林、边境、火山地质等多种景观',
|
||
url: 'https://1814.love/destinations',
|
||
numberOfItems: destinations.value.length,
|
||
itemListElement: destinations.value.map((d, i) => ({
|
||
'@type': 'ListItem',
|
||
position: i + 1,
|
||
name: d.name,
|
||
description: d.description.substring(0, 120),
|
||
url: `https://1814.love/destinations/${d.id}`,
|
||
item: {
|
||
'@type': 'TouristAttraction',
|
||
name: d.name,
|
||
description: d.subtitle,
|
||
},
|
||
})),
|
||
}),
|
||
},
|
||
],
|
||
})
|
||
|
||
useBreadcrumbSchema([
|
||
{ text: '首页', to: '/' },
|
||
{ text: '景区攻略' },
|
||
])
|
||
|
||
useScrollReveal()
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
// 筛选
|
||
.dest-filter {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: @space-sm;
|
||
margin-bottom: @space-lg;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
// 搜索
|
||
.dest-search {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
max-width: 480px;
|
||
margin-bottom: @space-2xl;
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-full;
|
||
padding: 0 @space-md;
|
||
transition: border-color @transition-fast;
|
||
|
||
&:focus-within {
|
||
border-color: @color-primary;
|
||
}
|
||
|
||
&__icon {
|
||
flex-shrink: 0;
|
||
color: @color-text-muted;
|
||
margin-right: @space-sm;
|
||
}
|
||
|
||
&__input {
|
||
flex: 1;
|
||
border: none;
|
||
outline: none;
|
||
font-size: @font-size-sm;
|
||
color: @color-text-primary;
|
||
background: transparent;
|
||
padding: 10px 0;
|
||
|
||
&::placeholder {
|
||
color: @color-text-muted;
|
||
}
|
||
}
|
||
|
||
&__clear {
|
||
flex-shrink: 0;
|
||
background: none;
|
||
border: none;
|
||
cursor: pointer;
|
||
color: @color-text-muted;
|
||
padding: @space-xs;
|
||
display: flex;
|
||
align-items: center;
|
||
transition: color @transition-fast;
|
||
|
||
&:hover {
|
||
color: @color-primary;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 空状态
|
||
.dest-empty {
|
||
text-align: center;
|
||
padding: @space-3xl;
|
||
color: @color-text-muted;
|
||
font-size: @font-size-md;
|
||
}
|
||
|
||
// 景区网格
|
||
.dest-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: @space-lg;
|
||
|
||
.respond-md({
|
||
grid-template-columns: 1fr 1fr;
|
||
});
|
||
|
||
.respond-lg({
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
});
|
||
}
|
||
|
||
// 景区卡片
|
||
.dest-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: @space-sm;
|
||
padding: @space-lg;
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-lg;
|
||
text-decoration: none;
|
||
color: inherit;
|
||
transition: box-shadow @transition-base, transform @transition-base;
|
||
|
||
&:hover {
|
||
box-shadow: @shadow-lg;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
&__header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
&__tag {
|
||
display: inline-block;
|
||
background: @color-primary;
|
||
color: #fff;
|
||
padding: 3px @space-sm;
|
||
border-radius: @border-radius-sm;
|
||
font-size: @font-size-xs;
|
||
font-weight: @font-weight-medium;
|
||
}
|
||
|
||
&__season {
|
||
font-size: @font-size-xs;
|
||
color: @color-text-muted;
|
||
}
|
||
|
||
&__name {
|
||
font-size: @font-size-lg;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
line-height: @line-height-tight;
|
||
margin: 0;
|
||
}
|
||
|
||
&__subtitle {
|
||
font-size: @font-size-xs;
|
||
color: @color-text-muted;
|
||
margin: 0;
|
||
line-height: @line-height-tight;
|
||
}
|
||
|
||
&__desc {
|
||
font-size: @font-size-sm;
|
||
line-height: @line-height-base;
|
||
color: @color-text-secondary;
|
||
margin: 0;
|
||
.text-clamp(3);
|
||
}
|
||
|
||
&__highlights {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: @space-xs;
|
||
}
|
||
|
||
&__highlight {
|
||
display: inline-block;
|
||
padding: 2px @space-sm;
|
||
background: @color-primary-bg;
|
||
color: @color-primary;
|
||
border-radius: @border-radius-sm;
|
||
font-size: @font-size-xs;
|
||
.text-clamp(1);
|
||
}
|
||
|
||
&__seasons {
|
||
display: flex;
|
||
gap: @space-xs;
|
||
margin-top: auto;
|
||
padding-top: @space-xs;
|
||
}
|
||
|
||
&__link {
|
||
font-size: @font-size-sm;
|
||
color: @color-primary;
|
||
font-weight: @font-weight-medium;
|
||
transition: color @transition-fast;
|
||
}
|
||
|
||
&:hover &__link {
|
||
color: @color-primary-light;
|
||
}
|
||
}
|
||
|
||
// 季节标签
|
||
.season-dot {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 22px;
|
||
height: 22px;
|
||
border-radius: @border-radius-full;
|
||
font-size: 11px;
|
||
font-weight: @font-weight-medium;
|
||
|
||
&--summer {
|
||
background: #E8F5E9;
|
||
color: #2E7D32;
|
||
}
|
||
|
||
&--autumn {
|
||
background: #FFF3E0;
|
||
color: #E65100;
|
||
}
|
||
|
||
&--winter {
|
||
background: #E3F2FD;
|
||
color: #1565C0;
|
||
}
|
||
}
|
||
|
||
// CTA
|
||
.dest-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: 560px;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
}
|
||
|
||
.dest-cta__links {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: @space-xl;
|
||
flex-wrap: wrap;
|
||
}
|
||
</style>
|