sync: 同步最新呼籁官网代码到仓库

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
刘涛
2026-03-22 21:57:35 +08:00
共同撰写人 Claude Sonnet 4.6
当前提交 a25f3a77ce
共修改 319 个文件,包含 46379 行新增和 0 行删除
+139
查看文件
@@ -0,0 +1,139 @@
<template>
<div class="page-about">
<LayoutPageHero
:title="seo.h1"
subtitle="一个只做呼伦贝尔的旅行品牌"
:breadcrumbs="[{ text: '关于我们' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="关于我们摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行创立于2015年,是内蒙古呼伦贝尔本地旅行品牌,11年专注呼伦贝尔一个目的地。旗下持有三重旅行社经营许可证,购买1000万旅游责任险,已服务超10000个家庭。集团旗下涵盖旅行社、牧场商贸、研学教育等多家主体。</p>
</div>
</div>
</section>
<!-- 品牌故事 -->
<AboutBrandStory :story="about.story" class="reveal" />
<!-- 集团实力 -->
<AboutSubsidiaryGrid :subsidiaries="about.subsidiaries" class="reveal" />
<!-- 资质认证 & 服务保障 -->
<AboutQualificationList :certifications="about.certifications" :guarantees="about.guarantees" class="reveal" />
<!-- 小红书 -->
<AboutXiaohongshuShowcase :xhs="about.xiaohongshu" class="reveal" />
<!-- 知识产权 -->
<AboutCopyrightList :copyrights="about.copyrights" :trademark="about.trademark" class="reveal" />
<!-- 品牌IP -->
<AboutMascotShowcase class="reveal" />
<!-- 团队介绍 -->
<AboutTeamIntro :team="about.team" class="reveal" />
<!-- 8个温度时刻 -->
<section class="section section--gray reveal" aria-label="服务旅程">
<div class="container">
<CommonSectionTitle :title="about.serviceJourney.title" :subtitle="about.serviceJourney.subtitle" />
<ol class="journey-list">
<li v-for="m in about.serviceJourney.moments" :key="m.step" class="journey-item">
<span class="journey-step">{{ m.step }}</span>
<div class="journey-body">
<strong class="journey-name">{{ m.name }}</strong>
<p class="journey-detail">{{ m.detail }}</p>
</div>
</li>
</ol>
</div>
</section>
<!-- 企业文化 -->
<AboutCultureValues :culture="about.culture" class="reveal" />
<!-- 内链 -->
<CommonSectionCTA
title="想了解我们的产品?"
description="查看额吉的故乡V9系列和小蒙马夏令营"
:links="[{ to: '/products', text: '查看呼伦贝尔定制游产品' }]"
/>
</div>
</template>
<script setup>
import about from '~/data/about.json'
import brand from '~/data/brand.json'
import contact from '~/data/contact.json'
import { useOrganizationSchema, useBreadcrumbSchema, usePersonSchema } from '~/composables/useJsonLd'
const seo = useSEO('about')
useScrollReveal()
useOrganizationSchema(brand, contact)
usePersonSchema(about.founder)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '关于我们' },
])
</script>
<style lang="less" scoped>
.journey-list {
list-style: none;
padding: 0;
margin: @space-xl 0 0;
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(4, 1fr);
});
}
.journey-item {
display: flex;
gap: @space-md;
align-items: flex-start;
}
.journey-step {
flex-shrink: 0;
width: 32px;
height: 32px;
background: @color-primary;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: @font-size-sm;
font-weight: @font-weight-bold;
}
.journey-body {
flex: 1;
}
.journey-name {
display: block;
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-xs;
}
.journey-detail {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
</style>
+591
查看文件
@@ -0,0 +1,591 @@
<template>
<div v-if="article" class="page-blog-detail">
<LayoutPageHero
:title="article.title"
:subtitle="article.subtitle"
:breadcrumbs="[{ text: '攻略', to: '/blog' }, { text: article.category }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="文章摘要">
<div class="container">
<div class="page-tldr-inner">
<p>{{ article.summary }}</p>
</div>
</div>
</section>
<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"
width="800"
height="450"
loading="eager"
/>
</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 blogData from '~/data/blog.json'
import productsData from '~/data/products.json'
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
import useScrollReveal from '~/composables/useScrollReveal'
const route = useRoute()
const allArticles = blogData.articles
const article = allArticles.find(a => a.id === route.params.id)
if (!article) {
throw createError({ statusCode: 404, statusMessage: '文章不存在' })
}
// SEO
useHead({
title: article.seo?.title || article.title,
meta: [
{ name: 'description', content: article.seo?.description || article.summary },
{ name: 'keywords', content: article.seo?.keywords || (article.tags || []).join(',') },
{ name: 'author', content: article.author },
{ name: 'article:published_time', content: article.date + 'T00:00:00+08:00' },
{ name: 'article:modified_time', content: article.date + 'T00:00:00+08:00' },
{ name: 'geo.region', content: 'CN-NM' },
{ name: 'geo.placename', content: '呼伦贝尔' },
{ property: 'og:title', content: article.seo?.title || article.title },
{ property: 'og:description', content: article.seo?.description || article.summary },
{ property: 'og:type', content: 'article' },
{ property: 'og:url', content: `https://1814.love/blog/${article.id}` },
{ property: 'og:site_name', content: '呼籁旅行' },
{ property: 'og:locale', content: 'zh_CN' },
],
link: [
{ rel: 'canonical', href: `https://1814.love/blog/${article.id}` },
],
})
useScrollReveal()
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '旅行攻略', to: '/blog' },
{ text: article.category, to: `/blog?category=${article.category}` },
{ text: article.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.id}#article`,
headline: article.title,
description: article.summary,
image: article.coverImage ? `https://1814.love${article.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.date,
dateModified: article.date,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://1814.love/blog/${article.id}`,
},
inLanguage: 'zh-CN',
keywords: article.tags.join(','),
}),
}],
})
// 相关产品
const relatedProducts = computed(() => {
if (!article.relatedProducts?.length || !productsData?.versions) return []
return productsData.versions
.filter(v => article.relatedProducts.includes(v.id))
.map(v => ({ id: v.id, name: v.name, duration: `${v.days}天${v.nights}晚 · ${v.tag}` }))
})
// 上下篇
const currentIndex = allArticles.findIndex(a => a.id === article.id)
const prevArticle = currentIndex > 0 ? allArticles[currentIndex - 1] : null
const nextArticle = currentIndex < allArticles.length - 1 ? allArticles[currentIndex + 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>
+363
查看文件
@@ -0,0 +1,363 @@
<template>
<div class="page-blog">
<LayoutPageHero
title="旅行攻略"
subtitle="呼伦贝尔旅行的干货指南,帮你把每一趟出行都过得值"
:breadcrumbs="[{ text: '攻略' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="旅行攻略摘要">
<div class="container">
<div class="page-tldr-inner">
<p>本专栏持续发布呼伦贝尔旅行实用干货,内容涵盖:亲子游完全攻略、最佳出行时间与月份选择、旅行花费明细、真实避坑经验总结、草原旅拍穿搭指南等核心主题。所有内容由呼籁旅行11年带队团队基于真实经验撰写,持续更新。</p>
</div>
</div>
</section>
<section 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 blogData from '~/data/blog.json'
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
import useScrollReveal from '~/composables/useScrollReveal'
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 = blogData.articles
const filteredArticles = computed(() => {
if (activeCategory.value === 'all') return allArticles
return allArticles.filter(a => a.category === activeCategory.value)
})
function getCategoryCount(key) {
if (key === 'all') return allArticles.length
return allArticles.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>
+519
查看文件
@@ -0,0 +1,519 @@
<template>
<div class="page-calendar">
<LayoutPageHero
:title="seo.h1"
subtitle="呼伦贝尔什么时候去最好?基于11年带队数据,为您提供每月详细出行参考"
:breadcrumbs="[{ text: '行程日历' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="行程日历摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼伦贝尔全年均可出行,最佳旅游时间为6-9月。其中6-8月草原翠绿、那达慕节庆热闹,是亲子出行的旺季;9月白桦林金黄,是摄影爱好者公认的圣地;5月和10月适合错峰出行,游客少价格更优;12月至次年2月适合追求冰雪极寒体验的家庭。本页含逐月温度、景色特征、推荐指数和适合产品。</p>
</div>
</div>
</section>
<section class="section">
<div class="container">
<!-- Quick recommendation summary -->
<div class="cal-summary">
<h2 class="cal-summary-title">呼伦贝尔最佳出行时间一览</h2>
<div class="summary-grid">
<div
v-for="rec in calData.meta.quickRecommendations"
:key="rec.label"
class="summary-card"
>
<div class="summary-label">{{ rec.label }}</div>
<div class="summary-months">{{ rec.months }}</div>
<div class="summary-reason">{{ rec.reason }}</div>
</div>
</div>
</div>
<!-- Month grid -->
<h2 class="months-heading">逐月出行指南</h2>
<div class="months-grid">
<div
v-for="m in calData.months"
:key="m.month"
:class="['month-card', `month-card--${m.season}`, { 'month-card--peak': m.isPeakSeason }]"
>
<!-- Header -->
<div class="month-header">
<div class="month-name-area">
<span class="month-number">{{ m.month }}</span>
<span class="month-unit">月</span>
<span class="month-name">{{ m.name }}</span>
</div>
<span v-if="m.isPeakSeason" class="peak-badge">旺季</span>
</div>
<!-- Star rating -->
<div class="month-stars" :aria-label="`推荐指数 ${m.ratingStars} 星`">
<span
v-for="i in 5"
:key="i"
:class="['star', { 'star--active': i <= m.ratingStars }]"
aria-hidden="true"
>★</span>
<span class="stars-label">推荐指数</span>
</div>
<!-- Temperature -->
<div class="month-temp">{{ m.tempRange }}</div>
<!-- Weather -->
<p class="month-weather">{{ m.weatherFeature }}</p>
<!-- Highlights -->
<ul class="month-highlights">
<li v-for="(h, i) in m.highlights.slice(0, 3)" :key="i">{{ h }}</li>
</ul>
<!-- Clothing -->
<div class="month-clothing">
<span class="clothing-label">穿搭</span>
<span class="clothing-tip">{{ m.clothingTips }}</span>
</div>
<!-- Recommended products -->
<div v-if="m.recommendedProducts.length" class="month-products">
<span
v-for="prod in m.recommendedProducts"
:key="prod"
class="product-tag"
>{{ prod }}</span>
</div>
<!-- Tagline -->
<p class="month-tagline">{{ m.tagline }}</p>
</div>
</div>
<!-- SEO text answer section -->
<div class="cal-answer">
<h2>呼伦贝尔几月份去最好?详细解答</h2>
<p>
呼伦贝尔全年皆可出行,但不同季节体验差异显著。<strong>6月至9月</strong>是最受欢迎的黄金时段:6-8月草原翠绿,那达慕节庆热闹,骑马、露营、亲子出行体验最佳;9月白桦林金黄,草原秋色无双,是摄影爱好者公认的圣地。<strong>5月和10月</strong>是错峰出行的好时机,游客较少、价格相对优惠,同样有独特景观。<strong>冬季(12月至次年2月)</strong>适合追求极寒体验的家庭,冰雪那达慕、冰钓、驯鹿部落是别处难寻的冬日记忆。
</p>
<div class="cal-links">
<CommonInternalLink to="/guides" text="出行指南(穿搭 · 行李清单)" />
<CommonInternalLink to="/products" text="夏季产品:额吉的故乡V9" />
<CommonInternalLink to="/products/autumn" text="秋季产品:游牧的森林" />
<CommonInternalLink to="/products/winter" text="冬季产品:嗨冰雪" />
<CommonInternalLink to="/contact" text="咨询最佳出行方案" />
</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
import calData from '~/data/calendar.json'
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const seo = useSEO('calendar')
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '行程日历' },
])
// TouristDestination JSON-LD
useHead({
script: [{
key: 'tourist-destination-hulunbuir',
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'TouristDestination',
name: '呼伦贝尔大草原',
description: '呼伦贝尔是中国最美草原之一,位于内蒙古自治区东北部,拥有莫日格勒河、额尔古纳湿地、白桦林、呼伦湖等核心景观。最佳旅游时节为6-9月,夏季草原翠绿,秋季白桦金黄。',
url: 'https://1814.love/calendar',
image: 'https://1814.love/images/team/hero-grassland.jpg',
geo: {
'@type': 'GeoCoordinates',
latitude: 49.2143,
longitude: 119.7674,
},
address: {
'@type': 'PostalAddress',
addressLocality: '呼伦贝尔市',
addressRegion: '内蒙古自治区',
addressCountry: 'CN',
},
touristType: ['家庭游客', '亲子家庭', '摄影爱好者', '自然爱好者'],
includesAttraction: [
{ '@type': 'TouristAttraction', name: '莫日格勒河(额吉的故乡)', description: '呼伦贝尔最美河湾,夏季翠绿如画,是呼籁旅行夏季核心景点' },
{ '@type': 'TouristAttraction', name: '额尔古纳湿地', description: '中国最大湿地之一,鸟类天堂,秋季倒影绝美' },
{ '@type': 'TouristAttraction', name: '白桦林', description: '九月金黄,摄影圣地,游牧的森林秋季产品核心景点' },
{ '@type': 'TouristAttraction', name: '呼伦湖', description: '中国北方第一大湖,冬季冰钓胜地,夏季湖畔骑马' },
{ '@type': 'TouristAttraction', name: '中俄边境公路', description: '室韦至黑山头沿线,风景壮阔,国境线骑行体验独特' },
],
}),
}],
})
// Seasonal Event JSON-LD
const seasonalEvents = [
{
key: 'event-summer',
name: '呼伦贝尔夏季旅游最佳期(6-8月)',
description: '呼伦贝尔草原夏季旅游高峰,绿草如茵,那达慕传统节庆,骑马、露营、亲子出行最佳时段。呼籁旅行提供额吉的故乡V9系列定制游及小蒙马亲子夏令营。',
startDate: '2026-06-01',
endDate: '2026-08-31',
},
{
key: 'event-autumn',
name: '呼伦贝尔秋季白桦林观赏期(9月)',
description: '呼伦贝尔草原秋季白桦林金黄,草原转秋色,是摄影爱好者公认的好时节。呼籁旅行提供游牧的森林秋季定制游产品。',
startDate: '2026-09-01',
endDate: '2026-09-30',
},
{
key: 'event-winter',
name: '呼伦贝尔冬季冰雪体验期(12-2月)',
description: '呼伦贝尔冬季冰雪奇观,冰雪那达慕、冰钓、驯鹿部落探访,零下30度的极寒体验。呼籁旅行提供嗨冰雪及小蒙马冬季营产品。',
startDate: '2026-12-01',
endDate: '2027-02-28',
},
]
seasonalEvents.forEach((event) => {
useHead({
script: [{
key: event.key,
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Event',
name: event.name,
description: event.description,
startDate: event.startDate,
endDate: event.endDate,
location: {
'@type': 'Place',
name: '呼伦贝尔大草原',
address: {
'@type': 'PostalAddress',
addressLocality: '呼伦贝尔市',
addressRegion: '内蒙古自治区',
addressCountry: 'CN',
},
geo: { '@type': 'GeoCoordinates', latitude: 49.2143, longitude: 119.7674 },
},
organizer: { '@id': 'https://1814.love/#organization' },
eventStatus: 'https://schema.org/EventScheduled',
eventAttendanceMode: 'https://schema.org/OfflineEventAttendanceMode',
url: 'https://1814.love/calendar',
offers: {
'@type': 'Offer',
priceCurrency: 'CNY',
availability: 'https://schema.org/InStock',
seller: { '@id': 'https://1814.love/#organization' },
},
}),
}],
})
})
</script>
<style lang="less" scoped>
// Summary section
.cal-summary {
margin-bottom: @space-3xl;
}
.cal-summary-title {
font-size: @font-size-xl;
text-align: center;
color: @color-primary;
margin-bottom: @space-xl;
}
.summary-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-md;
.respond-md({
grid-template-columns: repeat(4, 1fr);
});
}
.summary-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-top: 3px solid @color-primary;
border-radius: @border-radius-md;
padding: @space-lg;
text-align: center;
}
.summary-label {
font-size: @font-size-xs;
color: @color-text-muted;
font-weight: @font-weight-medium;
margin-bottom: @space-xs;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.summary-months {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-xs;
}
.summary-reason {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
}
// Month heading
.months-heading {
font-size: @font-size-xl;
color: @color-text-primary;
margin-bottom: @space-xl;
text-align: center;
}
// Month grid
.months-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
margin-bottom: @space-3xl;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
// Month card
.month-card {
border-radius: @border-radius-lg;
padding: @space-lg;
border: 1px solid @color-border;
&--summer {
background: @color-primary-bg;
border-color: rgba(45, 106, 79, 0.2);
}
&--autumn {
background: #FFF8F0;
border-color: rgba(212, 136, 58, 0.2);
}
&--winter {
background: #EFF4F8;
border-color: rgba(91, 141, 184, 0.2);
}
&--spring {
background: #F4FAF4;
border-color: rgba(82, 163, 106, 0.2);
}
&--peak {
box-shadow: @shadow-md;
}
}
.month-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: @space-sm;
}
.month-name-area {
display: flex;
align-items: baseline;
gap: 2px;
}
.month-number {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
line-height: 1;
}
.month-unit {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
}
.month-name {
font-size: @font-size-sm;
color: @color-text-muted;
margin-left: @space-xs;
}
.peak-badge {
display: inline-block;
padding: 2px @space-sm;
background: @color-accent;
color: white;
border-radius: @border-radius-full;
font-size: @font-size-xs;
font-weight: @font-weight-bold;
}
.month-stars {
display: flex;
align-items: center;
gap: 2px;
margin-bottom: @space-sm;
}
.star {
font-size: 14px;
color: @color-border;
&--active {
color: @color-accent;
}
}
.stars-label {
font-size: @font-size-xs;
color: @color-text-muted;
margin-left: @space-xs;
}
.month-temp {
display: inline-block;
padding: 2px @space-sm;
background: rgba(255, 255, 255, 0.8);
border-radius: @border-radius-sm;
font-size: @font-size-sm;
font-weight: @font-weight-medium;
color: @color-text-primary;
margin-bottom: @space-sm;
}
.month-weather {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin-bottom: @space-md;
}
.month-highlights {
list-style: none;
padding: 0;
margin: 0 0 @space-md;
li {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
padding-left: @space-md;
position: relative;
margin-bottom: 2px;
&::before {
content: '·';
position: absolute;
left: 0;
color: @color-primary;
font-weight: @font-weight-bold;
}
}
}
.month-clothing {
display: flex;
gap: @space-sm;
align-items: flex-start;
margin-bottom: @space-md;
padding: @space-sm @space-md;
background: rgba(255, 255, 255, 0.6);
border-radius: @border-radius-sm;
}
.clothing-label {
font-size: @font-size-xs;
font-weight: @font-weight-bold;
color: @color-primary;
flex-shrink: 0;
padding-top: 1px;
}
.clothing-tip {
font-size: @font-size-xs;
color: @color-text-secondary;
line-height: @line-height-base;
}
.month-products {
display: flex;
flex-wrap: wrap;
gap: @space-xs;
margin-bottom: @space-sm;
}
.product-tag {
display: inline-block;
padding: 2px 6px;
background: @color-primary;
color: white;
border-radius: @border-radius-sm;
font-size: 11px;
}
.month-tagline {
font-size: @font-size-xs;
color: @color-text-muted;
font-style: italic;
margin: 0;
border-top: 1px solid rgba(0, 0, 0, 0.06);
padding-top: @space-sm;
}
// SEO answer section
.cal-answer {
background: @color-bg-gray;
border-radius: @border-radius-lg;
padding: @space-2xl;
h2 {
font-size: @font-size-xl;
color: @color-primary;
margin-bottom: @space-md;
}
p {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-base;
margin-bottom: @space-xl;
strong {
color: @color-text-primary;
}
}
}
.cal-links {
display: flex;
flex-wrap: wrap;
gap: @space-lg;
}
</style>
+204
查看文件
@@ -0,0 +1,204 @@
<template>
<div class="page-contact">
<LayoutPageHero
:title="seo.h1"
subtitle="选择最方便的方式联系我们"
:breadcrumbs="[{ text: '联系我们' }]"
/>
<!-- TL;DR -->
<section class="page-tldr" aria-label="联系我们摘要">
<div class="container">
<div class="page-tldr-inner">
<p>联系呼籁旅行:微信扫码加客服,或拨打电话咨询。定制师工作日2小时内回复,节假日24小时内回复。预订旺季行程(7月)建议提前2-3个月联系。</p>
</div>
</div>
</section>
<!-- 主要联系方式:微信 + 电话 -->
<section class="section">
<div class="container">
<div class="contact-primary">
<!-- 微信客服 -->
<div class="contact-wechat">
<span class="contact-badge">推荐</span>
<h2 class="contact-title">微信客服</h2>
<p class="contact-desc">添加微信,定制师一对一沟通行程需求</p>
<CommonQRCodeBlock
v-if="wechatChannel"
:image="wechatChannel.qrImage"
label="扫码添加微信"
size="large"
/>
</div>
<!-- 电话 -->
<div class="contact-phone" v-if="phoneChannel">
<h2 class="contact-title">电话咨询</h2>
<p class="contact-phone-number">{{ phoneChannel.value }}</p>
<p class="contact-desc">{{ phoneChannel.description }}</p>
</div>
</div>
<!-- 其他联系方式 -->
<div class="contact-secondary">
<div v-for="ch in secondaryChannels" :key="ch.type" class="contact-info-item">
<h3 class="contact-info-label">{{ ch.label }}</h3>
<p class="contact-info-value">{{ ch.value }}</p>
<p v-if="ch.description" class="contact-info-desc">{{ ch.description }}</p>
</div>
</div>
<!-- 安全提示 -->
<div class="contact-security">
<p>{{ contact.securityNotice }}</p>
</div>
</div>
</section>
<!-- 长辈定制提示 -->
<section class="section section--gray">
<div class="container" style="max-width: 640px; text-align: center;">
<p style="font-size: 15px; color: #5a6b5e; line-height: 1.8;">想给爸妈安排一趟草原旅行?联系定制师时告知「牵手草原」长辈行程,我们会根据长辈的身体状况和偏好,定制节奏更缓、住宿更舒适的专属方案。也支持爸妈和朋友一起组团出发。</p>
</div>
</section>
<!-- 内链闭环 -->
<CommonSectionCTA
title="还有疑问?"
description="查看常见问题的详细解答"
:links="[{ to: '/faq', text: '查看呼伦贝尔亲子游常见问答' }]"
bg="gray"
/>
</div>
</template>
<script setup>
import contact from '~/data/contact.json'
import brand from '~/data/brand.json'
import { useOrganizationSchema, useBreadcrumbSchema, useContactPageSchema } from '~/composables/useJsonLd'
const visibleChannels = contact.channels.filter(ch => !ch.hidden)
const wechatChannel = visibleChannels.find(ch => ch.type === 'wechat')
const phoneChannel = visibleChannels.find(ch => ch.type === 'phone')
const secondaryChannels = visibleChannels.filter(ch => ch.type !== 'wechat' && ch.type !== 'phone')
const seo = useSEO('contact')
useOrganizationSchema(brand, contact)
useContactPageSchema(brand, contact)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '联系我们' },
])
</script>
<style lang="less" scoped>
.contact-primary {
display: flex;
gap: @space-2xl;
align-items: center;
justify-content: center;
max-width: 700px;
margin: 0 auto @space-2xl;
@media (max-width: 640px) {
flex-direction: column;
align-items: center;
gap: @space-xl;
}
}
.contact-wechat {
text-align: center;
flex: 1;
}
.contact-phone {
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.contact-badge {
display: inline-block;
padding: @space-xs @space-md;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
color: #fff;
background: @color-primary;
border-radius: @border-radius-full;
margin-bottom: @space-sm;
}
.contact-title {
font-size: @font-size-lg;
color: @color-primary;
margin-bottom: @space-xs;
}
.contact-desc {
font-size: @font-size-sm;
color: @color-text-muted;
margin-bottom: @space-md;
}
.contact-phone-number {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-xs;
letter-spacing: 1px;
}
.contact-secondary {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-md;
max-width: 700px;
margin: 0 auto @space-xl;
@media (max-width: 480px) {
grid-template-columns: 1fr;
}
}
.contact-info-item {
padding: @space-md @space-lg;
background: @color-bg-gray;
border-radius: @border-radius-md;
}
.contact-info-label {
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-xs;
}
.contact-info-value {
font-size: @font-size-sm;
color: @color-primary;
word-break: break-all;
margin-bottom: 2px;
}
.contact-info-desc {
font-size: @font-size-xs;
color: @color-text-muted;
}
.contact-security {
max-width: 700px;
margin: 0 auto;
padding: @space-md @space-lg;
background: @color-bg-warm;
border-radius: @border-radius-md;
text-align: center;
p {
font-size: @font-size-sm;
color: @color-text-secondary;
}
}
</style>
+775
查看文件
@@ -0,0 +1,775 @@
<template>
<div class="page-courses">
<LayoutPageHero
:title="seo.h1"
subtitle="小蒙马亲子研学·6大课程模块·3-12岁分龄设计"
:breadcrumbs="[{ text: '小蒙马夏令营', to: '/summer-camp' }, { text: '研学课程体系' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="研学课程摘要">
<div class="container">
<div class="page-tldr-inner">
<p>小蒙马研学是呼籁旅行旗下亲子自然教育品牌,以呼伦贝尔草原为天然课堂。课程体系包含6大模块:草原生态认知、马术体验、游牧文化传承、自然艺术创作、团队协作成长、LNT无痕环保实践;按3-6岁(探索组)、7-9岁(探险组)、10-12岁(领袖组)三个年龄段分龄设计不同目标和深度。</p>
</div>
</div>
</section>
<!-- 教育理念 -->
<section class="section section--warm reveal">
<div class="container">
<div class="philosophy-banner">
<div class="philosophy-text">
<h2 class="philosophy-headline">{{ courses.philosophy.headline }}</h2>
<p class="philosophy-tagline">{{ courses.philosophy.tagline }}</p>
<p class="philosophy-desc">{{ courses.philosophy.description }}</p>
</div>
<div class="philosophy-values">
<div
v-for="v in courses.philosophy.values"
:key="v.label"
class="philosophy-value-item"
>
<span class="philosophy-value-label">{{ v.label }}</span>
<p class="philosophy-value-desc">{{ v.desc }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- 年龄分层选择器 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle
title="按年龄找适合的课程"
subtitle="不同年龄段,有不同的学习目标和课程深度"
/>
<div class="age-tabs">
<button
v-for="group in courses.ageGroups"
:key="group.id"
class="age-tab"
:class="{ 'age-tab--active': activeGroup === group.id }"
@click="activeGroup = group.id"
>
<span class="age-tab-emoji">{{ group.emoji }}</span>
<span class="age-tab-label">{{ group.label }}</span>
</button>
</div>
<div v-if="activeGroupData" class="age-content">
<div class="age-content-header">
<h3 class="age-content-headline">{{ activeGroupData.headline }}</h3>
<span class="age-focus-badge">{{ activeGroupData.focus }}</span>
</div>
<p class="age-content-desc">{{ activeGroupData.description }}</p>
<div class="age-content-body">
<div class="age-highlights">
<h4 class="age-highlights-title">这个年龄段能做什么</h4>
<ul class="age-highlights-list">
<li v-for="h in activeGroupData.highlights" :key="h" class="age-highlight-item">
{{ h }}
</li>
</ul>
</div>
<div class="age-modules">
<h4 class="age-modules-title">推荐课程模块</h4>
<div class="age-module-tags">
<span
v-for="mod in activeGroupModules"
:key="mod.id"
class="age-module-tag"
>
{{ mod.icon }} {{ mod.name }}
</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 中间 CTA -->
<CommonSectionCTA
title="想了解小蒙马夏令营详情?"
description="7天6晚亲子研学,每期限8组家庭,现在咨询了解营期安排"
:links="[
{ to: '/summer-camp', text: '查看小蒙马夏令营' },
{ to: '/contact', text: '咨询报名' },
]"
/>
<!-- 六大课程模块 -->
<section class="section section--gray reveal">
<div class="container">
<CommonSectionTitle
title="6大课程模块"
subtitle="每个模块有明确的学习目标、活动设计和教育理念"
/>
<div class="modules-grid">
<div
v-for="mod in courses.modules"
:key="mod.id"
class="module-card"
:class="{ 'module-card--expanded': expandedModules[mod.id] }"
>
<button class="module-card-header" @click="toggleModule(mod.id)">
<div class="module-card-title-row">
<span class="module-icon">{{ mod.icon }}</span>
<div class="module-title-group">
<h3 class="module-name">{{ mod.name }}</h3>
<div class="module-meta">
<span class="module-meta-item">{{ mod.ageRange }}</span>
<span class="module-meta-sep">·</span>
<span class="module-meta-item">{{ mod.duration }}</span>
</div>
</div>
</div>
<p class="module-short-desc">{{ mod.shortDesc }}</p>
<span class="module-toggle">{{ expandedModules[mod.id] ? '收起' : '展开详情' }}</span>
</button>
<div v-show="expandedModules[mod.id]" class="module-card-body">
<div class="module-section">
<h4 class="module-section-title">学习目标</h4>
<ul class="module-list">
<li v-for="obj in mod.objectives" :key="obj">{{ obj }}</li>
</ul>
</div>
<div class="module-section">
<h4 class="module-section-title">活动内容</h4>
<ul class="module-list">
<li v-for="act in mod.activities" :key="act">{{ act }}</li>
</ul>
</div>
<blockquote class="module-philosophy">
{{ mod.philosophy }}
</blockquote>
</div>
</div>
</div>
</div>
</section>
<!-- 师资介绍(占位,待补充真实导师信息后启用) -->
<section v-if="false" class="section reveal">
<div class="container">
<CommonSectionTitle
title="课程导师团队"
subtitle="自然教育导师 + 草原领队,全程陪同每一位孩子"
/>
<div class="instructor-placeholder-box">
<span class="instructor-placeholder-icon">⚠️</span>
<div class="instructor-placeholder-text">
<p class="instructor-placeholder-title">此处需补充真实导师信息</p>
<p class="instructor-placeholder-desc">
请提供导师姓名、资质认证(自然教育导师证/急救证等)、从业经历和个人介绍,由运营团队填充。
</p>
</div>
</div>
<div class="instructor-cards">
<div v-for="inst in courses.instructors.placeholder" :key="inst.title" class="instructor-card">
<div class="instructor-avatar-placeholder">
<span>⚠️</span>
</div>
<div class="instructor-info">
<p class="instructor-name">{{ inst.name }}</p>
<p class="instructor-title">{{ inst.title }}</p>
<p class="instructor-cred">{{ inst.credentials }}</p>
<p class="instructor-bio">{{ inst.bio }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- 家长常见问题 -->
<section class="section section--warm">
<div class="container">
<CommonSectionTitle title="家长常见问题" />
<div class="courses-faq">
<FaqItem
v-for="(item, i) in courses.faq"
:key="item.q"
:question="item.q"
:answer="item.a"
:initial-open="i === 0"
/>
</div>
</div>
</section>
<!-- 底部 CTA -->
<CommonSectionCTA
title="准备好让孩子在草原上上课了吗?"
description="报名小蒙马夏令营,或联系我们了解定制研学方案"
:links="[
{ to: '/summer-camp', text: '小蒙马夏令营详情' },
{ to: '/customize', text: '定制研学方案' },
]"
/>
</div>
</template>
<script setup>
import courses from '~/data/courses.json'
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const seo = useSEO('courses')
useScrollReveal()
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '小蒙马夏令营', to: '/summer-camp' },
{ text: '研学课程体系' },
])
// JSON-LD: EducationalOrganization + Course schemas
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'EducationalOrganization',
name: '小蒙马研学·呼籁旅行',
url: 'https://1814.love/courses',
description: '小蒙马研学是呼籁旅行旗下亲子研学品牌,以呼伦贝尔草原为天然课堂,提供3-12岁分龄自然教育课程体系,含草原生态、马术体验、游牧文化、自然艺术、团队成长、LNT环保六大模块。',
address: {
'@type': 'PostalAddress',
addressLocality: '呼伦贝尔市',
addressRegion: '内蒙古自治区',
addressCountry: 'CN',
},
parentOrganization: {
'@type': 'Organization',
name: '呼籁旅行',
url: 'https://1814.love',
},
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: '小蒙马研学课程体系',
itemListElement: courses.modules.map((mod, i) => ({
'@type': 'Course',
position: i + 1,
name: mod.name,
description: mod.shortDesc,
provider: {
'@type': 'EducationalOrganization',
name: '小蒙马研学·呼籁旅行',
},
hasCourseInstance: {
'@type': 'CourseInstance',
courseMode: 'onsite',
location: {
'@type': 'Place',
name: '呼伦贝尔草原',
address: {
'@type': 'PostalAddress',
addressLocality: '呼伦贝尔市',
addressRegion: '内蒙古自治区',
addressCountry: 'CN',
},
},
},
})),
},
}),
},
],
})
// 年龄分层选择
const activeGroup = ref('child')
const activeGroupData = computed(() => courses.ageGroups.find((g) => g.id === activeGroup.value))
const activeGroupModules = computed(() =>
activeGroupData.value
? courses.modules.filter((m) => activeGroupData.value.recommendedModules.includes(m.id))
: [],
)
// 模块展开/收起
const expandedModules = ref({})
function toggleModule(id) {
expandedModules.value = { ...expandedModules.value, [id]: !expandedModules.value[id] }
}
</script>
<style lang="less" scoped>
// ===== 教育理念 =====
.philosophy-banner {
display: flex;
flex-direction: column;
gap: @space-2xl;
.respond-lg({
flex-direction: row;
align-items: flex-start;
gap: @space-3xl;
});
}
.philosophy-text {
flex: 1;
}
.philosophy-headline {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-sm;
}
.philosophy-tagline {
font-size: @font-size-lg;
color: @color-text-secondary;
font-weight: @font-weight-medium;
margin-bottom: @space-md;
}
.philosophy-desc {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin: 0;
}
.philosophy-values {
flex: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-md;
}
.philosophy-value-item {
padding: @space-lg;
background: @color-bg-white;
border-radius: @border-radius-lg;
border: 1px solid @color-border;
transition: box-shadow @transition-fast;
&:hover {
box-shadow: @shadow-md;
}
}
.philosophy-value-label {
display: inline-block;
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-primary;
background: @color-primary-bg;
padding: @space-xs @space-sm;
border-radius: @border-radius-full;
margin-bottom: @space-sm;
}
.philosophy-value-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
// ===== 年龄分层 =====
.age-tabs {
display: flex;
flex-direction: column;
gap: @space-sm;
margin-bottom: @space-xl;
.respond-md({
flex-direction: row;
gap: @space-md;
});
}
.age-tab {
flex: 1;
display: flex;
align-items: center;
gap: @space-sm;
padding: @space-md @space-lg;
border: 2px solid @color-border;
border-radius: @border-radius-lg;
background: @color-bg-white;
cursor: pointer;
text-align: left;
transition: border-color @transition-fast, background @transition-fast, box-shadow @transition-fast;
&:hover {
border-color: @color-primary-lighter;
box-shadow: @shadow-sm;
}
&--active {
border-color: @color-primary;
background: @color-primary-bg;
box-shadow: @shadow-md;
}
}
.age-tab-emoji {
font-size: 24px;
line-height: 1;
flex-shrink: 0;
}
.age-tab-label {
font-size: @font-size-sm;
font-weight: @font-weight-medium;
color: @color-text-primary;
.age-tab--active & {
color: @color-primary;
}
}
.age-content {
padding: @space-xl;
background: @color-bg-white;
border-radius: @border-radius-lg;
border: 1px solid @color-border;
}
.age-content-header {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: @space-sm;
margin-bottom: @space-md;
}
.age-content-headline {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-primary;
margin: 0;
flex: 1;
min-width: 200px;
}
.age-focus-badge {
font-size: @font-size-xs;
color: @color-accent;
background: fade(@color-accent, 12%);
padding: @space-xs @space-md;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
white-space: nowrap;
}
.age-content-desc {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-xl;
}
.age-content-body {
display: grid;
grid-template-columns: 1fr;
gap: @space-xl;
.respond-md({
grid-template-columns: 3fr 2fr;
});
}
.age-highlights-title,
.age-modules-title {
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-md;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.age-highlights-list {
display: flex;
flex-direction: column;
gap: @space-sm;
}
.age-highlight-item {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
padding-left: @space-lg;
position: relative;
&::before {
content: '✓';
position: absolute;
left: 0;
color: @color-primary;
font-weight: @font-weight-bold;
}
}
.age-module-tags {
display: flex;
flex-wrap: wrap;
gap: @space-sm;
}
.age-module-tag {
font-size: @font-size-sm;
color: @color-primary;
background: @color-primary-bg;
border: 1px solid fadeout(@color-primary, 70%);
padding: @space-xs @space-md;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
}
// ===== 课程模块 =====
.modules-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.module-card {
background: @color-bg-white;
border-radius: @border-radius-lg;
border: 1px solid @color-border;
overflow: hidden;
transition: border-color @transition-fast, box-shadow @transition-fast;
&:hover,
&--expanded {
border-color: @color-primary-lighter;
box-shadow: @shadow-md;
}
}
.module-card-header {
width: 100%;
padding: @space-xl;
background: none;
border: none;
cursor: pointer;
text-align: left;
display: flex;
flex-direction: column;
gap: @space-sm;
}
.module-card-title-row {
display: flex;
align-items: flex-start;
gap: @space-md;
}
.module-icon {
font-size: 28px;
line-height: 1;
flex-shrink: 0;
margin-top: 2px;
}
.module-title-group {
flex: 1;
}
.module-name {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-xs;
line-height: @line-height-tight;
}
.module-meta {
display: flex;
align-items: center;
gap: @space-xs;
flex-wrap: wrap;
}
.module-meta-item {
font-size: @font-size-xs;
color: @color-text-muted;
}
.module-meta-sep {
color: @color-text-muted;
font-size: @font-size-xs;
}
.module-short-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
.module-toggle {
font-size: @font-size-xs;
color: @color-primary;
font-weight: @font-weight-medium;
align-self: flex-start;
}
.module-card-body {
padding: 0 @space-xl @space-xl;
border-top: 1px solid @color-border;
padding-top: @space-lg;
}
.module-section {
margin-bottom: @space-lg;
&:last-of-type {
margin-bottom: @space-md;
}
}
.module-section-title {
font-size: @font-size-xs;
font-weight: @font-weight-bold;
color: @color-text-muted;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: @space-sm;
}
.module-list {
display: flex;
flex-direction: column;
gap: @space-xs;
li {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
padding-left: @space-md;
position: relative;
&::before {
content: '·';
position: absolute;
left: 0;
color: @color-primary;
font-weight: bold;
}
}
}
.module-philosophy {
margin: 0;
padding: @space-md @space-lg;
background: @color-primary-bg;
border-left: 3px solid @color-primary;
border-radius: 0 @border-radius-md @border-radius-md 0;
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-loose;
font-style: italic;
}
// ===== 师资占位 =====
.instructor-placeholder-box {
display: flex;
align-items: flex-start;
gap: @space-md;
padding: @space-lg @space-xl;
background: #fff8e6;
border: 1px solid #f0c060;
border-radius: @border-radius-lg;
margin-bottom: @space-xl;
}
.instructor-placeholder-icon {
font-size: 24px;
flex-shrink: 0;
line-height: 1;
}
.instructor-placeholder-title {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: #a06000;
margin: 0 0 @space-xs;
}
.instructor-placeholder-desc {
font-size: @font-size-sm;
color: #7a5000;
margin: 0;
line-height: @line-height-base;
}
.instructor-cards {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
}
.instructor-card {
display: flex;
gap: @space-lg;
padding: @space-xl;
background: @color-bg-gray;
border-radius: @border-radius-lg;
border: 1px dashed @color-border;
}
.instructor-avatar-placeholder {
width: 64px;
height: 64px;
border-radius: 50%;
background: @color-border;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.instructor-name {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-secondary;
margin: 0 0 @space-xs;
}
.instructor-title {
font-size: @font-size-sm;
color: @color-primary;
font-weight: @font-weight-medium;
margin: 0 0 @space-xs;
}
.instructor-cred,
.instructor-bio {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0 0 @space-xs;
line-height: @line-height-base;
}
// ===== FAQ =====
.courses-faq {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: @space-md;
}
</style>
+1009
查看文件
文件差异内容过多而无法显示 加载差异
+397
查看文件
@@ -0,0 +1,397 @@
<template>
<main class="destination-detail" v-if="dest">
<section class="dest-hero">
<div class="container">
<nav class="breadcrumb" aria-label="面包屑导航">
<NuxtLink to="/">首页</NuxtLink> / <NuxtLink to="/destinations">景区攻略</NuxtLink> / <span>{{ dest.name }}</span>
</nav>
<div class="hero-content">
<span class="dest-tag">{{ dest.tag }}</span>
<h1>{{ dest.name }}</h1>
<p class="hero-subtitle">{{ dest.subtitle }}</p>
</div>
</div>
</section>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="目的地摘要">
<div class="container">
<div class="page-tldr-inner">
<p>{{ dest.description }}</p>
</div>
</div>
</section>
<section class="dest-description">
<div class="container">
<p>{{ dest.description }}</p>
</div>
</section>
<section class="dest-highlights" v-if="dest.highlights?.length">
<div class="container">
<h2>景点亮点</h2>
<ul>
<li v-for="(h, i) in dest.highlights" :key="i">{{ h }}</li>
</ul>
</div>
</section>
<section class="dest-season">
<div class="container">
<h2>最佳游览季节</h2>
<div class="season-card">
<div class="season-primary">
<span class="season-label">最佳时节</span>
<span class="season-value">{{ dest.bestSeason.primary }}</span>
</div>
<div class="season-secondary" v-if="dest.bestSeason.secondary">
<span class="season-label">次佳时节</span>
<span class="season-value">{{ dest.bestSeason.secondary }}</span>
</div>
<p class="season-desc">{{ dest.bestSeason.description }}</p>
</div>
</div>
</section>
<section class="dest-tips" v-if="dest.tips?.length">
<div class="container">
<h2>实用旅行贴士</h2>
<ul>
<li v-for="(tip, i) in dest.tips" :key="i">{{ tip }}</li>
</ul>
</div>
</section>
<section class="dest-products" v-if="relatedProducts.length">
<div class="container">
<h2>包含此景点的行程</h2>
<p class="products-intro">以下呼籁定制游产品涵盖{{ dest.name }},由专属领队带队深度游览。</p>
<div class="product-links">
<NuxtLink
v-for="p in relatedProducts"
:key="p.id"
:to="`/products/${p.id}`"
class="product-link"
>
<span class="product-name">{{ p.name }}</span>
<span class="product-meta">{{ p.days }}天{{ p.nights }}晚 · {{ p.tag }}</span>
</NuxtLink>
</div>
</div>
</section>
<section class="dest-cta">
<div class="container">
<h2>想去{{ dest.name }}?让定制师为你规划</h2>
<p>扫码添加微信,定制师一对一为你安排深度游览方案</p>
<NuxtLink to="/contact" class="cta-button">联系定制师</NuxtLink>
</div>
</section>
</main>
</template>
<script setup>
import destinationsData from '~/data/destinations-detail.json'
import productsData from '~/data/products.json'
import winterProductsData from '~/data/winter-products.json'
import autumnProductsData from '~/data/autumn-products.json'
import useSEO from '~/composables/useSEO'
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const route = useRoute()
const destId = route.params.id
const dest = destinationsData.destinations.find(d => d.id === destId)
if (!dest) {
throw createError({ statusCode: 404, statusMessage: '目的地不存在' })
}
// SEO
const seoOverrides = {
title: dest.seo?.title,
description: dest.seo?.description,
}
useSEO('destinations', seoOverrides)
useHead({
meta: [
{ name: 'keywords', content: dest.seo?.keywords },
],
})
// JSON-LD: TouristAttraction
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'TouristAttraction',
name: dest.name,
description: dest.description,
url: `https://1814.love/destinations/${dest.id}`,
touristType: '家庭游客,亲子游,摄影爱好者',
isAccessibleForFree: false,
photo: dest.heroImage ? { '@type': 'ImageObject', url: `https://1814.love${dest.heroImage}` } : undefined,
containedInPlace: {
'@type': 'AdministrativeArea',
name: '呼伦贝尔',
address: {
'@type': 'PostalAddress',
addressLocality: '呼伦贝尔市',
addressRegion: '内蒙古自治区',
addressCountry: 'CN',
},
},
publicAccess: true,
availableLanguage: 'Chinese',
}),
},
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Article',
headline: dest.seo?.title || dest.name,
description: dest.seo?.description || dest.description,
url: `https://1814.love/destinations/${dest.id}`,
author: { '@id': 'https://1814.love/#organization' },
publisher: { '@id': 'https://1814.love/#organization' },
dateModified: new Date().toISOString().split('T')[0],
about: {
'@type': 'TouristAttraction',
name: dest.name,
},
keywords: dest.seo?.keywords,
}),
},
],
})
// 面包屑
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '景区攻略', to: '/destinations' },
{ text: dest.name },
])
// 关联产品详情——从三个产品线数据中查找
const allVersions = [
...(productsData.versions || []),
...(winterProductsData.versions || []),
...(autumnProductsData.versions || []),
]
const relatedProducts = (dest.relatedProducts || [])
.map(pid => allVersions.find(v => v.id === pid))
.filter(Boolean)
</script>
<style scoped>
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
}
.dest-hero {
background: linear-gradient(135deg, #f0f5f1, #e8ede9);
padding: 40px 0 48px;
}
.breadcrumb {
font-size: 14px;
color: #7a857c;
margin-bottom: 24px;
}
.breadcrumb a {
color: #3a7d44;
text-decoration: none;
}
.dest-tag {
display: inline-block;
background: #3a7d44;
color: #fff;
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
margin-bottom: 12px;
}
h1 {
font-size: 32px;
font-weight: 800;
color: #1a1f1c;
line-height: 1.3;
margin-bottom: 10px;
}
.hero-subtitle {
font-size: 17px;
color: #4a5e4d;
}
.dest-description {
padding: 40px 0;
}
.dest-description p {
font-size: 17px;
line-height: 1.9;
color: #3d4a40;
}
.dest-highlights {
padding: 40px 0;
background: #fafbfa;
}
.dest-highlights h2,
.dest-season h2,
.dest-tips h2,
.dest-products h2 {
font-size: 24px;
font-weight: 700;
margin-bottom: 20px;
color: #1a1f1c;
}
.dest-highlights ul {
list-style: none;
padding: 0;
}
.dest-highlights li {
padding: 14px 0;
border-bottom: 1px solid #eef0ef;
font-size: 16px;
line-height: 1.6;
color: #3d4a40;
padding-left: 24px;
position: relative;
}
.dest-highlights li::before {
content: '✓';
position: absolute;
left: 0;
color: #3a7d44;
font-weight: 700;
}
.dest-season {
padding: 40px 0;
}
.season-card {
background: #f0f5f1;
border-radius: 10px;
padding: 24px;
display: flex;
flex-wrap: wrap;
gap: 20px;
align-items: flex-start;
}
.season-primary,
.season-secondary {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 120px;
}
.season-label {
font-size: 12px;
color: #7a857c;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.season-value {
font-size: 18px;
font-weight: 700;
color: #3a7d44;
}
.season-desc {
flex: 1 1 100%;
font-size: 15px;
line-height: 1.7;
color: #4a5e4d;
margin: 0;
}
.dest-tips {
padding: 40px 0;
background: #fafbfa;
}
.dest-tips ul {
list-style: none;
padding: 0;
}
.dest-tips li {
padding: 14px 0;
border-bottom: 1px solid #eef0ef;
font-size: 16px;
line-height: 1.6;
color: #3d4a40;
padding-left: 28px;
position: relative;
}
.dest-tips li::before {
content: '💡';
position: absolute;
left: 0;
font-size: 14px;
}
.dest-products {
padding: 40px 0;
}
.products-intro {
font-size: 15px;
color: #4a5e4d;
margin-bottom: 20px;
}
.product-links {
display: flex;
flex-direction: column;
gap: 12px;
}
.product-link {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border: 1px solid #d8e4da;
border-radius: 8px;
text-decoration: none;
transition: border-color 0.2s, background 0.2s;
}
.product-link:hover {
border-color: #3a7d44;
background: #f0f5f1;
}
.product-name {
font-size: 16px;
font-weight: 600;
color: #1a1f1c;
}
.product-meta {
font-size: 14px;
color: #7a857c;
}
.dest-cta {
padding: 48px 0;
text-align: center;
background: linear-gradient(135deg, #f0f5f1, #e8ede9);
}
.dest-cta h2 {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
color: #1a1f1c;
}
.dest-cta p {
color: #4a5e4d;
margin-bottom: 20px;
}
.cta-button {
display: inline-block;
background: #3a7d44;
color: #fff;
padding: 12px 32px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
font-size: 16px;
}
.cta-button:hover {
background: #2e6435;
}
</style>
+454
查看文件
@@ -0,0 +1,454 @@
<template>
<div class="page-destinations">
<LayoutPageHero
title="呼伦贝尔核心景区攻略"
subtitle="深耕呼伦贝尔11年,带你发现最真实的草原秘境"
:breadcrumbs="[{ text: '景区攻略' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="景区攻略摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼伦贝尔核心景区覆盖草原、湿地、森林、边境、火山地质等多种景观类型。本页梳理了呼籁旅行三大产品线(额吉的故乡·夏季、游牧的森林·秋季、嗨冰雪·冬季)覆盖的所有核心景区,包括莫日格勒河、额尔古纳湿地、白桦林、恩和室韦、黑山头、满洲里、阿尔山国家森林公园、大兴安岭莫尔道嘎、敖鲁古雅驯鹿部落、根河等,提供每处景区的实用攻略、最佳时间和旅行贴士。</p>
</div>
</div>
</section>
<section 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 destinationsData from '~/data/destinations-detail.json'
import useSEO from '~/composables/useSEO'
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
import useScrollReveal from '~/composables/useScrollReveal'
const destinations = destinationsData.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.length
return destinations.filter(d => d.seasons?.includes(key)).length
}
const filteredDestinations = computed(() => {
return destinations.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.length,
itemListElement: destinations.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>
+321
查看文件
@@ -0,0 +1,321 @@
<template>
<div class="page-faq">
<!-- 页面标题 -->
<LayoutPageHero
:title="seo.h1"
:subtitle="`${totalFAQ}个高频问题详细解答,帮您做出最好的选择`"
:breadcrumbs="[{ text: '常见问答' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="常见问答摘要">
<div class="container">
<div class="page-tldr-inner">
<p>本页汇总呼籁旅行被问到最多的{{ totalFAQ }}个问题,涵盖价格区间、最佳出行时间、适合人群、行程安排、安全保障、退改条款等核心关切,按分类整理并支持关键词搜索。还有疑问可直接联系定制师。</p>
</div>
</div>
</section>
<section class="section">
<div class="container">
<!-- 搜索框 -->
<div class="faq-search-wrap">
<div class="faq-search-box">
<span class="faq-search-icon" aria-hidden="true">🔍</span>
<input
v-model="searchQuery"
type="search"
class="faq-search-input"
placeholder="搜索问题,如:多少钱、几月份去、穿什么…"
aria-label="搜索常见问题"
@input="onSearchInput"
/>
<button v-if="searchQuery" class="faq-search-clear" aria-label="清除搜索" @click="clearSearch">✕</button>
</div>
<p v-if="searchQuery" class="faq-search-summary">
<template v-if="searchResultCount > 0">
找到 <strong>{{ searchResultCount }}</strong> 条相关问题
</template>
<template v-else>
未找到相关问题,试试其他关键词
</template>
</p>
</div>
<!-- 搜索结果视图 -->
<div v-if="searchQuery" class="faq-content">
<template v-if="searchResultCount > 0">
<div v-for="cat in filteredCategories" :key="cat.id" class="faq-search-group">
<h2 class="faq-category-label">{{ cat.name }}</h2>
<div class="faq-category-list">
<FaqItem
v-for="q in cat.questions"
:key="q.id"
:question="q.question"
:answer="q.answer"
:related-links="q.relatedLinks"
:initial-open="true"
/>
</div>
</div>
</template>
<div v-else class="faq-no-result">
<p>没有找到包含"{{ searchQuery }}"的问题</p>
<button class="faq-no-result-btn" @click="clearSearch">查看全部问题</button>
</div>
</div>
<!-- 默认分类视图 -->
<template v-else>
<div class="faq-nav-sticky">
<FaqCategoryNav
:categories="categories"
:active-cat="activeCat"
@select="scrollToCategory"
/>
</div>
<div class="faq-content">
<FaqCategory
v-for="(cat, index) in categories"
:key="cat.id"
:category="cat"
:initial-open="index === 0"
/>
</div>
</template>
<!-- 内链闭环:FAQ→产品 -->
<CommonSectionCTA
title="还有疑问?"
description="查看我们的产品详情,或直接联系定制师获取专属方案"
:links="[
{ to: '/products', text: '查看4-7天定制游产品' },
{ to: '/contact', text: '联系定制师获取方案' },
]"
/>
</div>
</section>
</div>
</template>
<script setup>
import faqData from '~/data/faq.json'
import { useFAQPageSchema, useBreadcrumbSchema } from '~/composables/useJsonLd'
// SEO - 最重要的页面
const seo = useSEO('faq')
// JSON-LD: FAQPage schema(核心SEO资产)
const categories = faqData.categories
const totalFAQ = categories.reduce((sum, c) => sum + c.questions.length, 0)
useFAQPageSchema(categories)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '常见问答' },
])
// 搜索功能 — 支持 URL ?q= 参数初始化(对应 WebSite SearchAction)
const route = useRoute()
const router = useRouter()
const searchQuery = ref(route.query.q ? String(route.query.q) : '')
const filteredCategories = computed(() => {
const q = searchQuery.value.trim().toLowerCase()
if (!q) return categories
return categories
.map(cat => ({
...cat,
questions: cat.questions.filter(
item =>
item.question.toLowerCase().includes(q) ||
item.answer.toLowerCase().includes(q)
),
}))
.filter(cat => cat.questions.length > 0)
})
const searchResultCount = computed(() =>
filteredCategories.value.reduce((sum, c) => sum + c.questions.length, 0)
)
function onSearchInput() {
const q = searchQuery.value.trim()
router.replace({ query: q ? { q } : {} })
}
function clearSearch() {
searchQuery.value = ''
router.replace({ query: {} })
}
// 分类锚点导航 + 滚动联动
const activeCat = ref(categories[0]?.id || '')
function scrollToCategory(id) {
activeCat.value = id
if (import.meta.client) {
const el = document.getElementById(id)
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
}
// 滚动时自动高亮当前可见分类
onMounted(() => {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting) {
activeCat.value = entry.target.id
}
}
},
{ rootMargin: '-100px 0px -60% 0px', threshold: 0 }
)
categories.forEach((cat) => {
const el = document.getElementById(cat.id)
if (el) observer.observe(el)
})
onUnmounted(() => observer.disconnect())
})
</script>
<style lang="less" scoped>
// 搜索框
.faq-search-wrap {
max-width: 800px;
margin: 0 auto @space-xl;
}
.faq-search-box {
position: relative;
display: flex;
align-items: center;
background: @color-bg-white;
border: 2px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-sm @space-lg;
transition: border-color @transition-fast, box-shadow @transition-fast;
&:focus-within {
border-color: @color-primary;
box-shadow: 0 0 0 3px rgba(58, 125, 68, 0.12);
}
}
.faq-search-icon {
font-size: @font-size-base;
margin-right: @space-sm;
flex-shrink: 0;
}
.faq-search-input {
flex: 1;
border: none;
outline: none;
font-size: @font-size-base;
color: @color-text-primary;
background: transparent;
padding: 0;
&::placeholder {
color: @color-text-muted;
}
}
.faq-search-clear {
border: none;
background: none;
cursor: pointer;
color: @color-text-muted;
font-size: @font-size-sm;
padding: 0 0 0 @space-sm;
flex-shrink: 0;
&:hover {
color: @color-text-primary;
}
}
.faq-search-summary {
margin-top: @space-sm;
font-size: @font-size-sm;
color: @color-text-secondary;
strong {
color: @color-primary;
}
}
// 搜索结果
.faq-search-group {
margin-bottom: @space-2xl;
}
.faq-category-label {
font-size: @font-size-xl;
color: @color-primary;
margin-bottom: @space-lg;
padding-bottom: @space-sm;
border-bottom: 2px solid @color-primary-lighter;
}
.faq-category-list {
display: flex;
flex-direction: column;
gap: @space-md;
}
.faq-no-result {
max-width: 800px;
margin: 0 auto;
text-align: center;
padding: @space-2xl;
color: @color-text-secondary;
p {
margin-bottom: @space-lg;
font-size: @font-size-base;
}
}
.faq-no-result-btn {
display: inline-block;
padding: @space-sm @space-xl;
background: @color-primary;
color: #fff;
border: none;
border-radius: @border-radius-md;
cursor: pointer;
font-size: @font-size-sm;
transition: background @transition-fast;
&:hover {
background: @color-primary-light;
}
}
// 默认视图
.faq-nav-sticky {
position: sticky;
top: 64px;
z-index: 10;
background: @color-bg-white;
padding-top: @space-sm;
padding-bottom: @space-sm;
margin-bottom: @space-lg;
@media (min-width: 1024px) {
top: 72px;
}
}
.faq-content {
max-width: 800px;
margin: 0 auto;
}
</style>
+614
查看文件
@@ -0,0 +1,614 @@
<template>
<div class="page-gallery">
<!-- 全屏 Hero:用第一张照片做背景 -->
<section class="gallery-hero">
<div class="gallery-hero__bg">
<img
:src="gallery.works[7].image"
alt="呼籁草原旅拍"
class="gallery-hero__img"
/>
<div class="gallery-hero__overlay" />
</div>
<div class="gallery-hero__content">
<nav class="gallery-hero__breadcrumb" aria-label="面包屑导航">
<NuxtLink to="/">首页</NuxtLink>
<span>/</span>
<span>旅拍作品</span>
</nav>
<h1 class="gallery-hero__title">呼籁旅拍</h1>
<p class="gallery-hero__subtitle">在呼籁营地,用蒙古袍定格草原最美的你</p>
<div class="gallery-hero__stats">
<div v-for="stat in gallery.intro.stats" :key="stat.label" class="gallery-hero__stat">
<span class="gallery-hero__stat-value">{{ stat.value }}</span>
<span class="gallery-hero__stat-label">{{ stat.label }}</span>
</div>
</div>
</div>
<div class="gallery-hero__scroll-hint" aria-hidden="true">
<span>向下滑动浏览作品</span>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10 4v12M4 10l6 6 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</section>
<!-- TL;DR -->
<section class="page-tldr" aria-label="旅拍作品摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行在自营营地配备专属旅拍摄影团队,拥有200+件蒙古族服饰(传统袍服、节日盛装、亲子套装)。旅拍在呼籁营地进行,无需单独预约,每位出行客人均可体验,获得专业摄影师拍摄的草原珍贵瞬间。</p>
</div>
</div>
</section>
<!-- 作品瀑布流 -->
<section class="gallery-section">
<div class="container">
<div class="gallery-section__header">
<h2 class="gallery-section__title">旅拍作品集</h2>
<p class="gallery-section__desc">每一张都是真实客户在呼籁营地的草原记忆</p>
</div>
<!-- 瀑布流 -->
<div class="gallery-masonry">
<div
v-for="(work, index) in gallery.works"
:key="work.id"
class="gallery-item reveal"
:class="`gallery-item--${work.orientation}`"
@click="openLightbox(index)"
>
<div class="gallery-card">
<img
:src="work.image"
:alt="`${work.title} - 呼籁营地旅拍`"
class="gallery-card__img"
loading="lazy"
/>
<div class="gallery-card__hover">
<div class="gallery-card__hover-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Lightbox 灯箱 -->
<Teleport to="body">
<Transition name="lightbox">
<div
v-if="lightboxOpen"
class="lightbox"
@click.self="closeLightbox"
@keydown.esc="closeLightbox"
@keydown.left="prevPhoto"
@keydown.right="nextPhoto"
tabindex="0"
ref="lightboxRef"
>
<button class="lightbox__close" @click="closeLightbox" aria-label="关闭">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<button class="lightbox__nav lightbox__nav--prev" @click="prevPhoto" aria-label="上一张">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="lightbox__img-wrap">
<img
:src="gallery.works[lightboxIndex].image"
:alt="gallery.works[lightboxIndex].title"
class="lightbox__img"
/>
</div>
<button class="lightbox__nav lightbox__nav--next" @click="nextPhoto" aria-label="下一张">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none">
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
<div class="lightbox__counter">
{{ lightboxIndex + 1 }} / {{ gallery.works.length }}
</div>
</div>
</Transition>
</Teleport>
<!-- 旅拍说明 -->
<section class="gallery-about section">
<div class="container">
<CommonSectionTitle
title="关于呼籁旅拍"
subtitle="记录真实的草原瞬间,而不是摆拍"
align="center"
/>
<div class="gallery-about__content">
<p class="gallery-about__lead">我们追求的是在游玩和互动过程中,捕捉最自然、最真实的瞬间——不做浓妆、不做刻意摆拍的重民族风。草原本身就是最好的背景,真实的笑容比任何姿势都好看。</p>
<div class="gallery-about__details">
<div class="gallery-about__item">
<h3>提前沟通,拍出你想要的</h3>
<p>如果你对旅拍有自己的想法和偏好,可以提前和我们的摄影师沟通。我们会根据你的需求,在拍摄时更有针对性地抓拍。</p>
</div>
<div class="gallery-about__item">
<h3>妆容自由,丰俭由人</h3>
<p>旅拍默认不含化妆——因为每个人的肤质不同,适合的化妆品也不一样。建议出发前自己画个淡妆,配上草原的蓝天绿草就非常好看。营地有专业化妆师,需要的话可以现场预约,明码标价、费用透明。</p>
</div>
<div class="gallery-about__item">
<h3>底片全送,没有隐形消费</h3>
<p>所有底片全部赠送,不存在选片环节的加价。收到照片后如果有任何想法,第一时间联系我们——每个人对「好看」的理解不同,我们愿意倾听你的反馈,一起磨合出更满意的作品。</p>
</div>
</div>
</div>
</div>
</section>
<!-- 预约旅拍 CTA -->
<CommonSectionCTA
title="预约您的草原旅拍"
description="旅拍在呼籁营地进行,随行程一并安排,无需单独预约。有任何拍摄想法,可以提前和摄影师沟通。"
:links="[
{ to: '/customize', text: '立即预约,开始定制行程' },
{ to: '/contact', text: '咨询旅拍详情' },
]"
/>
</div>
</template>
<script setup>
import galleryData from '~/data/gallery.json'
import { useBreadcrumbSchema, useGallerySchema } from '~/composables/useJsonLd'
import useScrollReveal from '~/composables/useScrollReveal'
const seo = useSEO('gallery')
useScrollReveal()
const gallery = galleryData
// Lightbox
const lightboxOpen = ref(false)
const lightboxIndex = ref(0)
const lightboxRef = ref(null)
function openLightbox(index) {
lightboxIndex.value = index
lightboxOpen.value = true
document.body.style.overflow = 'hidden'
nextTick(() => {
lightboxRef.value?.focus()
})
}
function closeLightbox() {
lightboxOpen.value = false
document.body.style.overflow = ''
}
function nextPhoto() {
lightboxIndex.value = (lightboxIndex.value + 1) % gallery.works.length
}
function prevPhoto() {
lightboxIndex.value = (lightboxIndex.value - 1 + gallery.works.length) % gallery.works.length
}
// 结构化数据
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '旅拍作品' },
])
useGallerySchema(gallery)
</script>
<style lang="less" scoped>
// ========== Hero ==========
.gallery-hero {
position: relative;
height: 100vh;
min-height: 600px;
max-height: 1000px;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
&__bg {
position: absolute;
inset: 0;
z-index: 0;
}
&__img {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 60%;
}
&__overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.5);
}
&__content {
position: relative;
z-index: 1;
text-align: center;
color: #fff !important;
padding: 0 @space-lg;
* {
color: #fff !important;
}
}
&__breadcrumb {
font-size: @font-size-sm;
margin-bottom: @space-lg;
opacity: 0.85;
a {
color: #fff !important;
text-decoration: none;
&:hover { text-decoration: underline; }
}
span { margin: 0 @space-xs; }
}
&__title {
font-size: 48px;
font-weight: @font-weight-bold;
letter-spacing: 8px;
margin: 0 0 @space-md;
color: #fff !important;
text-shadow: 0 2px 40px rgba(0, 0, 0, 0.6), 0 1px 8px rgba(0, 0, 0, 0.4);
.respond-md({
font-size: 64px;
letter-spacing: 12px;
});
}
&__subtitle {
font-size: @font-size-lg;
color: rgba(255, 255, 255, 0.95) !important;
margin: 0 0 @space-2xl;
font-weight: 300;
letter-spacing: 2px;
text-shadow: 0 1px 10px rgba(0, 0, 0, 0.5);
.respond-md({
font-size: @font-size-xl;
});
}
&__stats {
display: flex;
justify-content: center;
gap: @space-xl;
flex-wrap: wrap;
.respond-md({
gap: @space-3xl;
});
}
&__stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
&__stat-value {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
line-height: 1.2;
.respond-md({
font-size: 32px;
});
}
&__stat-label {
font-size: @font-size-xs;
opacity: 0.8;
letter-spacing: 1px;
}
&__scroll-hint {
position: absolute;
bottom: @space-xl;
left: 50%;
transform: translateX(-50%);
z-index: 1;
color: #fff;
opacity: 0.6;
display: flex;
flex-direction: column;
align-items: center;
gap: @space-xs;
font-size: @font-size-xs;
letter-spacing: 1px;
animation: bounce 2s ease-in-out infinite;
svg {
animation: bounce-arrow 2s ease-in-out infinite;
}
}
}
@keyframes bounce {
0%, 100% { transform: translateX(-50%) translateY(0); }
50% { transform: translateX(-50%) translateY(-4px); }
}
@keyframes bounce-arrow {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(4px); }
}
// ========== Gallery Section ==========
.gallery-section {
padding: @space-3xl 0;
.respond-md({
padding: 80px 0;
});
&__header {
text-align: center;
margin-bottom: @space-2xl;
.respond-md({
margin-bottom: @space-3xl;
});
}
&__title {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
letter-spacing: 2px;
.respond-md({
font-size: 32px;
});
}
&__desc {
font-size: @font-size-base;
color: @color-text-secondary;
margin: 0;
}
}
// ========== Masonry ==========
.gallery-masonry {
columns: 2;
column-gap: @space-sm;
.respond-md({
columns: 2;
column-gap: @space-md;
});
.respond-lg({
columns: 3;
column-gap: @space-lg;
});
}
.gallery-item {
break-inside: avoid;
margin-bottom: @space-sm;
cursor: pointer;
.respond-md({
margin-bottom: @space-md;
});
.respond-lg({
margin-bottom: @space-lg;
});
}
// ========== Card ==========
.gallery-card {
position: relative;
border-radius: @border-radius-md;
overflow: hidden;
background: @color-bg-gray;
.respond-md({
border-radius: @border-radius-lg;
});
&__img {
display: block;
width: 100%;
height: auto;
transition: transform 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
&__hover {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0);
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
}
&__hover-icon {
color: #fff;
opacity: 0;
transform: scale(0.8);
transition: all 0.3s ease;
}
&:hover {
.gallery-card__img {
transform: scale(1.05);
}
.gallery-card__hover {
background: rgba(0, 0, 0, 0.25);
}
.gallery-card__hover-icon {
opacity: 1;
transform: scale(1);
}
}
}
// ========== About Section ==========
.gallery-about {
&__content {
max-width: 800px;
margin: 0 auto;
}
&__lead {
text-align: center;
font-size: @font-size-md;
color: @color-text-secondary;
line-height: @line-height-relaxed;
margin: @space-lg auto @space-2xl;
max-width: 680px;
}
&__details {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-md({
grid-template-columns: repeat(3, 1fr);
});
}
&__item {
padding: @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
h3 {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
}
p {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-relaxed;
margin: 0;
}
}
}
</style>
<!-- Lightbox 样式必须 unscoped,因为 Teleport 把 DOM 移到 body -->
<style lang="less">
.lightbox {
position: fixed;
inset: 0;
z-index: 10000;
background: rgba(0, 0, 0, 0.96);
display: flex;
align-items: center;
justify-content: center;
outline: none;
&__close {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
background: none;
border: none;
color: #fff;
cursor: pointer;
padding: 8px;
opacity: 0.7;
transition: opacity 0.2s;
&:hover { opacity: 1; }
}
&__nav {
position: absolute;
top: 50%;
transform: translateY(-50%);
z-index: 10;
background: rgba(255, 255, 255, 0.1);
border: none;
color: #fff;
cursor: pointer;
padding: 16px;
border-radius: 50%;
opacity: 0.6;
transition: all 0.2s;
&:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.2);
}
&--prev { left: 20px; }
&--next { right: 20px; }
}
&__img-wrap {
max-width: 90vw;
max-height: 85vh;
display: flex;
align-items: center;
justify-content: center;
}
&__img {
max-width: 100%;
max-height: 85vh;
object-fit: contain;
border-radius: 4px;
}
&__counter {
position: absolute;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
color: rgba(255, 255, 255, 0.6);
font-size: 14px;
letter-spacing: 2px;
}
}
.lightbox-enter-active,
.lightbox-leave-active {
transition: opacity 0.3s ease;
}
.lightbox-enter-from,
.lightbox-leave-to {
opacity: 0;
}
</style>
+258
查看文件
@@ -0,0 +1,258 @@
<template>
<div class="page-guide-custom">
<LayoutPageHero
title="呼伦贝尔定制游怎么选"
subtitle="定制游 vs 跟团游 vs 自由行,一篇讲透"
:breadcrumbs="[{ text: '出行指南', to: '/guides' }, { text: '定制游怎么选' }]"
/>
<section class="page-tldr" aria-label="定制游摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼伦贝尔定制游是一家一单一车的私家出行方式,区别于传统跟团游的拼团大巴和走马观花。本文解释定制游的优势、适合人群、价格构成,帮你判断是否适合选择定制游。</p>
</div>
</div>
</section>
<article class="section">
<div class="container article-body">
<h2>什么是定制游</h2>
<p>定制游的核心是「一家一单一车」——你的家庭独享一辆车和一位领队,行程根据你的需求量身安排,不与陌生人拼团。出发时间、游玩节奏、停留时长都可以灵活调整,孩子累了可以多休息,遇到好风景可以多停一会儿。</p>
<p>这不是高价版的跟团游,而是完全不同的旅行方式。</p>
<h2>定制游 vs 跟团游 vs 自由行</h2>
<div class="compare-table-wrap">
<table class="compare-table">
<thead>
<tr>
<th></th>
<th>定制游</th>
<th>跟团游</th>
<th>自由行</th>
</tr>
</thead>
<tbody>
<tr>
<td class="row-label">用车</td>
<td class="highlight">一家一车,专属司机</td>
<td>拼团大巴,30-50人</td>
<td>自驾或包车</td>
</tr>
<tr>
<td class="row-label">行程</td>
<td class="highlight">按家庭需求定制</td>
<td>固定路线,统一安排</td>
<td>完全自己规划</td>
</tr>
<tr>
<td class="row-label">节奏</td>
<td class="highlight">随走随停,灵活调整</td>
<td>按时间表走,赶景点</td>
<td>自由但费心</td>
</tr>
<tr>
<td class="row-label">领队</td>
<td class="highlight">专属领队全程陪同</td>
<td>导游带大团</td>
<td>无</td>
</tr>
<tr>
<td class="row-label">购物</td>
<td class="highlight">全程零购物</td>
<td>可能有购物店</td>
<td>无</td>
</tr>
<tr>
<td class="row-label">适合</td>
<td class="highlight">亲子家庭、品质出行</td>
<td>预算有限、不想操心</td>
<td>经验丰富的老手</td>
</tr>
</tbody>
</table>
</div>
<h2>为什么亲子家庭更适合定制游</h2>
<p>带孩子旅行和大人出行完全不同。孩子需要按自己的节奏来——可能早上起不来,可能中午要午睡,可能在某个地方玩嗨了不想走。跟团游的固定时间表很难适配孩子的节奏,自由行又需要父母花大量精力规划路线。</p>
<p>定制游解决的正是这个问题:专业的定制师提前根据孩子年龄安排行程节奏,专属领队全程照顾,家长只需要带着孩子享受旅行。</p>
<h2>定制游的价格构成</h2>
<p>定制游的价格包含车辆、住宿、领队服务、活动体验和保险,正餐自理。看起来比跟团游贵,但每一分钱都花在了体验上:</p>
<ul>
<li>没有购物店——不会被拉去消费</li>
<li>没有隐形消费——费用明细清晰透明</li>
<li>没有赶场式行程——每个地方都停够、看够</li>
<li>所有资源专属调配——你的领队只服务你一家</li>
</ul>
<p>以呼籁旅行为例,5天4晚行程舒适套餐2580元/人起,高档套餐3380元/人起,价格随出行日期浮动。定金500元/人,出发前7天可无条件退改。</p>
<h2>怎么判断自己适不适合定制游</h2>
<p>如果你符合以下任一条件,定制游大概率是更好的选择:</p>
<ul>
<li>带3-12岁孩子出行,需要灵活节奏</li>
<li>三代同行,需要照顾老人和孩子</li>
<li>对住宿品质有要求,不想住快捷酒店</li>
<li>不想和陌生人拼团,注重私密性</li>
<li>第一次去呼伦贝尔,不了解路线</li>
<li>想深度体验而不是走马观花</li>
</ul>
<h2>呼籁旅行的定制游有什么不同</h2>
<p>呼籁旅行深耕呼伦贝尔11年,是本地自营的亲子游品牌,不是中间商转手。我们有自己的车队、自己的营地、自己的旅拍团队,从产品设计到服务交付全链路自己把控。</p>
<p>目前提供额吉的故乡V9系列共6个版本(5天4晚至7天6晚),每个版本都是针对不同家庭需求专门设计的,不是简单增减天数。每个家庭配备5人服务团队:定制师+房务管家+车务管家+客服管家+领队。</p>
<div class="article-cta">
<h3>想了解具体行程?</h3>
<div class="cta-links">
<NuxtLink to="/products" class="cta-btn cta-btn--primary">查看6个版本行程</NuxtLink>
<NuxtLink to="/selector" class="cta-btn cta-btn--outline">1分钟匹配最适合的版本</NuxtLink>
<NuxtLink to="/contact" class="cta-btn cta-btn--outline">联系定制师免费咨询</NuxtLink>
</div>
</div>
</div>
</article>
</div>
</template>
<script setup>
import { useBreadcrumbSchema, useFAQPageSchema } from '~/composables/useJsonLd'
useHead({
title: '呼伦贝尔定制游怎么选 - 定制游vs跟团游vs自由行 - 呼籁旅行',
meta: [
{ name: 'description', content: '呼伦贝尔定制游是一家一单一车的私家出行方式。本文对比定制游、跟团游、自由行的区别,解释定制游的价格构成和适合人群,帮你判断是否适合选择呼伦贝尔定制游。' },
{ name: 'keywords', content: '呼伦贝尔定制游,定制游和跟团游的区别,呼伦贝尔旅游怎么选,呼伦贝尔一家一车,呼伦贝尔私家团,呼伦贝尔亲子游' },
],
})
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '出行指南', to: '/guides' },
{ text: '定制游怎么选' },
])
</script>
<style lang="less" scoped>
.article-body {
max-width: 760px;
margin: 0 auto;
padding-bottom: @space-2xl;
h2 {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: @space-2xl 0 @space-md;
&:first-child {
margin-top: 0;
}
}
p {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-md;
}
ul {
padding-left: @space-lg;
margin-bottom: @space-md;
li {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-xs;
}
}
}
.compare-table-wrap {
overflow-x: auto;
margin: @space-lg 0 @space-xl;
}
.compare-table {
width: 100%;
border-collapse: collapse;
font-size: @font-size-sm;
th, td {
padding: @space-sm @space-md;
text-align: left;
border-bottom: 1px solid @color-border;
}
th {
background: @color-bg-gray;
font-weight: @font-weight-bold;
color: @color-text-primary;
&:nth-child(2) {
color: @color-primary;
}
}
.row-label {
font-weight: @font-weight-medium;
color: @color-text-primary;
white-space: nowrap;
}
.highlight {
color: @color-primary;
font-weight: @font-weight-medium;
}
td {
color: @color-text-secondary;
}
}
.article-cta {
margin-top: @space-2xl;
padding: @space-xl;
background: @color-primary-bg;
border-radius: @border-radius-lg;
text-align: center;
h3 {
font-size: @font-size-lg;
color: @color-text-primary;
margin-bottom: @space-lg;
}
}
.cta-links {
display: flex;
gap: @space-md;
justify-content: center;
flex-wrap: wrap;
}
.cta-btn {
display: inline-block;
padding: @space-sm @space-lg;
border-radius: @border-radius-md;
text-decoration: none;
font-size: @font-size-sm;
font-weight: @font-weight-medium;
transition: all @transition-fast;
&--primary {
background: @color-primary;
color: #fff;
&:hover { background: @color-primary-light; }
}
&--outline {
background: @color-bg-white;
color: @color-primary;
border: 1px solid @color-primary;
&:hover { background: @color-primary; color: #fff; }
}
}
</style>
+428
查看文件
@@ -0,0 +1,428 @@
<template>
<div class="page-guides">
<LayoutPageHero
:title="seo.h1"
subtitle="证件·穿搭·行李清单·活动安全·幼童专属指南,帮您轻松准备呼伦贝尔之行"
:breadcrumbs="[{ text: '出行指南' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="出行指南摘要">
<div class="container">
<div class="page-tldr-inner">
<p>去呼伦贝尔出行前需准备:中国居民身份证(14岁以下携带户口本),草原早晚温差大需分层穿搭,必备防晒防蚊产品。本指南涵盖六个章节:证件清单、按月穿搭参考(6-9月及冬季)、行李打包建议、草原活动安全须知、实用信息与推荐App、幼童专属注意事项,全部来自11年带队实践经验。</p>
</div>
</div>
</section>
<section class="section">
<div class="container">
<p class="guide-intro">{{ guidesData.pageIntro }}</p>
<!-- 锚点导航 -->
<GuidesGuideNav
:sections="guidesData.sections"
:active="activeSection"
@select="scrollToSection"
/>
<!-- 01 证件 -->
<div :id="sDocuments.id" class="guide-section">
<CommonSectionTitle
:title="sDocuments.title"
:subtitle="sDocuments.subtitle"
align="left"
/>
<p class="guide-text">{{ sDocuments.content }}</p>
<CommonContentCard :hoverable="false">
<GuidesGuideChecklist :items="sDocuments.items" />
</CommonContentCard>
<div v-if="sDocuments.tips" class="guide-tips">
<p v-for="(tip, i) in sDocuments.tips" :key="i" class="guide-tip">{{ tip }}</p>
</div>
</div>
<!-- 02 穿搭 -->
<div :id="sClothing.id" class="guide-section">
<CommonSectionTitle
:title="sClothing.title"
:subtitle="sClothing.subtitle"
align="left"
/>
<p class="guide-text">{{ sClothing.content }}</p>
<div class="clothing-grid">
<GuidesGuideClothingCard
v-for="month in sClothing.months"
:key="month.name"
:month="month"
/>
</div>
<div class="guide-tips">
<p v-for="(tip, i) in sClothing.extraTips" :key="i" class="guide-tip">{{ tip }}</p>
</div>
</div>
<!-- 03 行李清单 -->
<div :id="sPacking.id" class="guide-section">
<CommonSectionTitle
:title="sPacking.title"
:subtitle="sPacking.subtitle"
align="left"
/>
<div class="packing-categories">
<div v-for="cat in sPacking.categories" :key="cat.name" class="packing-category">
<h3 class="packing-cat-name">{{ cat.name }}</h3>
<p v-if="cat.note" class="packing-cat-note">{{ cat.note }}</p>
<CommonContentCard :hoverable="false">
<GuidesGuideChecklist :items="cat.items" />
</CommonContentCard>
</div>
</div>
<!-- 已帮您准备好的 -->
<div class="provided-block">
<h3 class="provided-title">{{ sPacking.provided.title }}</h3>
<p class="provided-desc">{{ sPacking.provided.description }}</p>
<p class="provided-items">{{ sPacking.provided.items }}</p>
</div>
<!-- 不用带 -->
<div class="dont-bring">
<h4 class="dont-bring-title">这些不用带:</h4>
<p v-for="(item, i) in sPacking.dontBring" :key="i" class="dont-bring-item">✗ {{ item }}</p>
</div>
<div v-if="sPacking.tips" class="guide-tips">
<p v-for="(tip, i) in sPacking.tips" :key="i" class="guide-tip">{{ tip }}</p>
</div>
</div>
<!-- 04 活动安全 -->
<div :id="sActivities.id" class="guide-section">
<CommonSectionTitle
:title="sActivities.title"
:subtitle="sActivities.subtitle"
align="left"
/>
<p class="guide-text">{{ sActivities.content }}</p>
<div class="activities-grid">
<GuidesGuideActivityCard
v-for="activity in sActivities.activities"
:key="activity.name"
:activity="activity"
/>
</div>
</div>
<!-- 05 实用信息 -->
<div :id="sPractical.id" class="guide-section">
<CommonSectionTitle
:title="sPractical.title"
:subtitle="sPractical.subtitle"
align="left"
/>
<div class="topics-grid">
<div v-for="topic in sPractical.topics" :key="topic.name" class="topic-card">
<h4 class="topic-name">{{ topic.name }}</h4>
<p class="topic-content">{{ topic.content }}</p>
</div>
</div>
<!-- 推荐App -->
<div class="apps-block">
<h3 class="apps-title">出发前手机里装什么</h3>
<div class="apps-list">
<div v-for="app in sPractical.apps" :key="app.category" class="app-item">
<span class="app-category">{{ app.category }}</span>
<span class="app-names">{{ app.names }}</span>
</div>
</div>
<p class="apps-note">{{ sPractical.appNote }}</p>
</div>
</div>
<!-- 06 幼童指南 -->
<div :id="sToddler.id" class="guide-section">
<CommonSectionTitle
:title="sToddler.title"
:subtitle="sToddler.subtitle"
align="left"
/>
<p class="guide-text">{{ sToddler.content }}</p>
<GuidesGuideToddlerSection :data="sToddler" />
</div>
<!-- 内链闭环 -->
<div class="guide-cta">
<h2>准备好了吗?</h2>
<p>草原的风,已经在等您了。</p>
<div class="guide-cta-links">
<CommonInternalLink to="/products" text="查看4-7天定制游产品" />
<CommonInternalLink to="/faq" text="查看常见问答" />
<CommonInternalLink to="/contact" text="联系定制师获取方案" />
</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
import guidesData from '~/data/guides.json'
import { useBreadcrumbSchema, useHowToSchema } from '~/composables/useJsonLd'
const seo = useSEO('guides')
useHowToSchema(guidesData.sections)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '出行指南' },
])
// 按 id 索引 sections,避免硬编码数组下标
const sections = guidesData.sections
function getSection(id) {
return sections.find(s => s.id === id)
}
const sDocuments = getSection('documents')
const sClothing = getSection('clothing')
const sPacking = getSection('packing')
const sActivities = getSection('activities')
const sPractical = getSection('practical')
const sToddler = getSection('toddler')
const activeSection = ref('')
function scrollToSection(id) {
activeSection.value = id
if (import.meta.client) {
const el = document.getElementById(id)
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
}
}
}
</script>
<style lang="less" scoped>
.guide-intro {
font-size: @font-size-md;
color: @color-text-secondary;
max-width: 700px;
margin: 0 auto @space-xl;
text-align: center;
line-height: @line-height-base;
}
.guide-section {
margin-bottom: @space-3xl;
padding-top: @space-lg;
scroll-margin-top: 80px;
}
.guide-text {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-base;
margin-bottom: @space-lg;
}
.guide-tips {
margin-top: @space-lg;
}
.guide-tip {
padding: @space-sm @space-md;
background: @color-primary-bg;
border-radius: @border-radius-sm;
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin-bottom: @space-sm;
}
// Clothing
.clothing-grid {
display: flex;
flex-direction: column;
gap: @space-md;
}
// Packing
.packing-categories {
display: flex;
flex-direction: column;
gap: @space-xl;
}
.packing-cat-name {
font-size: @font-size-md;
font-weight: @font-weight-bold;
color: @color-primary;
margin: 0 0 @space-xs;
}
.packing-cat-note {
font-size: @font-size-sm;
color: @color-accent;
margin-bottom: @space-md;
}
.provided-block {
margin-top: @space-xl;
padding: @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-top: 3px solid @color-primary;
border-radius: @border-radius-md;
}
.provided-title {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-primary;
margin: 0 0 @space-sm;
}
.provided-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
margin-bottom: @space-sm;
}
.provided-items {
font-size: @font-size-sm;
color: @color-text-muted;
line-height: @line-height-loose;
}
.dont-bring {
margin-top: @space-lg;
}
.dont-bring-title {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0 0 @space-sm;
}
.dont-bring-item {
font-size: @font-size-sm;
color: @color-text-muted;
line-height: @line-height-loose;
margin: 0;
}
// Activities
.activities-grid {
display: flex;
flex-direction: column;
gap: @space-md;
}
// Practical
.topics-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
.respond-md({
grid-template-columns: 1fr 1fr;
});
}
.topic-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-md;
padding: @space-lg;
}
.topic-name {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-primary;
margin: 0 0 @space-sm;
}
.topic-content {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
// Apps
.apps-block {
margin-top: @space-xl;
}
.apps-title {
font-size: @font-size-md;
font-weight: @font-weight-bold;
color: @color-primary;
margin: 0 0 @space-md;
}
.apps-list {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-md;
overflow: hidden;
}
.app-item {
display: flex;
gap: @space-md;
padding: @space-md @space-lg;
border-bottom: 1px solid @color-bg-gray;
font-size: @font-size-sm;
&:last-child {
border-bottom: none;
}
}
.app-category {
font-weight: @font-weight-medium;
color: @color-primary;
min-width: 80px;
flex-shrink: 0;
}
.app-names {
color: @color-text-secondary;
}
.apps-note {
margin-top: @space-sm;
font-size: @font-size-sm;
color: @color-text-muted;
}
// CTA
.guide-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;
}
}
.guide-cta-links {
display: flex;
justify-content: center;
gap: @space-xl;
flex-wrap: wrap;
}
</style>
+437
查看文件
@@ -0,0 +1,437 @@
<template>
<div class="page-home">
<!-- Hero Banner -->
<HomeHeroBanner :seo="seo" />
<!-- 身份定义 + TL;DR(GEO/SEO 核心锚点:告诉 AI 这个站点是谁) -->
<section class="page-tldr" aria-label="品牌身份摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行是深耕呼伦贝尔11年的专属旅行品牌,创立于2015年,已服务超10000个家庭。不做拼团、不走马观花;提供额吉的故乡V9系列定制游(4-7天)、小蒙马亲子夏令营(7天6晚)及秋冬季专属产品,全程一单一车、独立成团。</p>
</div>
</div>
</section>
<!-- 信任数据 -->
<HomeTrustBar :stats="brand.trustStats" class="reveal" />
<!-- 产品预览 -->
<HomeProductPreview class="reveal" />
<!-- 产品语义树(帮助 AI 按维度理解产品分类)-->
<section class="section section--gray reveal" aria-label="产品分类导航">
<div class="container">
<CommonSectionTitle title="怎么找到适合你的行程" label="FIND YOUR TRIP" subtitle="按天数 · 按人群 · 按季节 三个维度快速定位" />
<div class="product-tree-grid">
<div class="product-tree-card product-tree-card--days">
<div class="product-tree-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</div>
<h3 class="product-tree-card__title">按出行天数选</h3>
<ul class="product-tree-list">
<li><NuxtLink to="/products">4天3晚</NuxtLink>:快闪草原,适合假期有限的家庭</li>
<li><NuxtLink to="/products">5天4晚</NuxtLink>:经典版本,覆盖核心景点</li>
<li><NuxtLink to="/products">6天5晚</NuxtLink>:深度探访,含边境秘境</li>
<li><NuxtLink to="/products">7天6晚</NuxtLink>:终极版,全线贯穿</li>
</ul>
</div>
<div class="product-tree-card product-tree-card--people">
<div class="product-tree-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>
</div>
<h3 class="product-tree-card__title">按出行人群选</h3>
<ul class="product-tree-list">
<li><NuxtLink to="/products">亲子家庭</NuxtLink>:儿童友好、安全有趣</li>
<li><NuxtLink to="/summer-camp">研学营家庭</NuxtLink>:小蒙马7天自然教育夏令营</li>
<li><NuxtLink to="/products">摄影爱好者</NuxtLink>:专属秘境取景、旅拍服务</li>
<li><NuxtLink to="/products">两代三代出行</NuxtLink>:老人孩子均适合的舒适线路</li>
</ul>
</div>
<div class="product-tree-card product-tree-card--season">
<div class="product-tree-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
</div>
<h3 class="product-tree-card__title">按出行季节选</h3>
<ul class="product-tree-list">
<li><NuxtLink to="/products">夏季(6-8月)</NuxtLink>:草原绿、那达慕、骑马露营</li>
<li><NuxtLink to="/products/autumn">秋季(9月)</NuxtLink>:白桦林金黄,摄影圣地</li>
<li><NuxtLink to="/products/winter">冬季(12-2月)</NuxtLink>:冰雪那达慕、驯鹿、极寒</li>
<li><NuxtLink to="/calendar">查看全年出行日历 →</NuxtLink></li>
</ul>
</div>
</div>
<div class="product-tree-cta">
<CommonInternalLink to="/selector" text="不确定选哪个?用选版工具1分钟匹配 →" />
</div>
</div>
</section>
<!-- 核心差异化 -->
<HomeDifferentiatorList :items="brand.differentiators" class="reveal" />
<!-- 目的地对比 -->
<HomeDestinationCompare class="reveal" />
<!-- 知识库入口(告诉 AI 这是持续产出内容的权威站点)-->
<section class="section reveal" aria-label="旅行知识库">
<div class="container">
<CommonSectionTitle title="11年积累的呼伦贝尔知识库" label="KNOWLEDGE" subtitle="从规划到出发,每个问题都有答案" />
<div class="knowledge-hub-grid">
<NuxtLink to="/calendar" class="knowledge-hub-card">
<div class="knowledge-hub-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
</div>
<h3 class="knowledge-hub-card__title">出行时间日历</h3>
<p class="knowledge-hub-card__desc">每月天气、景色、推荐指数,找到你的最佳出行窗口</p>
</NuxtLink>
<NuxtLink to="/guides" class="knowledge-hub-card">
<div class="knowledge-hub-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"/><circle cx="12" cy="10" r="3"/></svg>
</div>
<h3 class="knowledge-hub-card__title">出行准备指南</h3>
<p class="knowledge-hub-card__desc">证件清单、分月穿搭、行李打包、活动安全、幼童须知</p>
</NuxtLink>
<NuxtLink to="/blog" class="knowledge-hub-card">
<div class="knowledge-hub-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>
</div>
<h3 class="knowledge-hub-card__title">旅行攻略专栏</h3>
<p class="knowledge-hub-card__desc">亲子游完全指南、避坑经验、旅拍穿搭,持续更新</p>
</NuxtLink>
<NuxtLink to="/destinations" class="knowledge-hub-card">
<div class="knowledge-hub-card__icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/><line x1="8" y1="2" x2="8" y2="18"/><line x1="16" y1="6" x2="16" y2="22"/></svg>
</div>
<h3 class="knowledge-hub-card__title">核心景区攻略</h3>
<p class="knowledge-hub-card__desc">莫日格勒河、额尔古纳湿地、阿尔山等必玩景区详解</p>
</NuxtLink>
</div>
</div>
</section>
<!-- FAQ预览 -->
<section class="section section--gray reveal">
<div class="container">
<CommonSectionTitle title="常见问答" label="FAQ" subtitle="您可能想了解的问题" />
<div class="faq-preview">
<details v-for="q in featuredFAQ" :key="q.id" class="faq-preview-item">
<summary class="faq-preview-q">
<span class="faq-q-text">{{ q.question }}</span>
<span class="faq-toggle-icon" />
</summary>
<p class="faq-preview-a">{{ truncate(q.answer, 200) }}</p>
</details>
</div>
<div class="faq-preview-more">
<CommonInternalLink to="/faq" :text="`查看全部${totalFAQ}个常见问题`" />
</div>
</div>
</section>
<!-- 评价精选 -->
<HomeReviewHighlights :reviews="featuredReviews" class="reveal" />
<!-- 内链闭环:首页→产品→选版→定制 -->
<CommonSectionCTA
title="准备好出发了吗?"
description="还不确定选哪个版本?先用选版工具匹配一下,再看价格明细,最后找定制师拿专属方案。"
:links="[
{ to: '/selector', text: '看看哪个版本适合你' },
{ to: '/pricing', text: '了解费用明细' },
{ to: '/customize', text: '免费获取专属方案' }
]"
/>
</div>
</template>
<script setup>
import brand from '~/data/brand.json'
import faq from '~/data/faq.json'
import reviews from '~/data/reviews.json'
import contact from '~/data/contact.json'
import navData from '~/data/navigation.json'
import { useOrganizationSchema, useWebSiteSchema, useSiteNavigationSchema } from '~/composables/useJsonLd'
// SEO
const seo = useSEO('home')
// 滚动入场动画
useScrollReveal()
// JSON-LD
useOrganizationSchema(brand, contact)
useWebSiteSchema()
useSiteNavigationSchema(navData.header)
// 精选数据
const featuredReviews = reviews.items.slice(0, 3)
const allQuestions = faq.categories.flatMap(c => c.questions)
const totalFAQ = allQuestions.length
const featuredFAQ = allQuestions.slice(0, 5)
function truncate(text, maxLen = 120) {
if (text.length <= maxLen) return text
// 在最大长度内找最后一个句子结尾(。,!?;)
const slice = text.slice(0, maxLen)
const lastBreak = Math.max(
slice.lastIndexOf('。'),
slice.lastIndexOf(','),
slice.lastIndexOf('!'),
slice.lastIndexOf('?'),
slice.lastIndexOf(';'),
)
if (lastBreak > maxLen * 0.4) {
return text.slice(0, lastBreak + 1) + '……'
}
return slice + '……'
}
</script>
<style lang="less" scoped>
// ===== 产品语义树 =====
.product-tree-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
margin-top: @space-xl;
.respond-md({
grid-template-columns: repeat(3, 1fr);
});
}
.product-tree-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
transition: box-shadow 0.35s cubic-bezier(0.23, 1, 0.32, 1), transform 0.35s cubic-bezier(0.23, 1, 0.32, 1);
&:hover {
box-shadow: @shadow-md;
transform: translateY(-3px);
}
&--days { border-top: 3px solid @color-primary; }
&--people { border-top: 3px solid @color-accent; }
&--season { border-top: 3px solid #3B82F6; }
&__title {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-md;
}
}
.product-tree-icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: @border-radius-md;
background: @color-bg-gray;
color: @color-primary;
margin-bottom: @space-md;
.product-tree-card--people & { color: @color-accent; }
.product-tree-card--season & { color: #3B82F6; }
}
.product-tree-list {
display: flex;
flex-direction: column;
gap: @space-sm;
li {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
a {
color: @color-primary;
font-weight: @font-weight-medium;
text-decoration: none;
transition: color @transition-fast;
&:hover {
color: @color-primary-light;
text-decoration: underline;
}
}
}
}
.product-tree-cta {
text-align: center;
margin-top: @space-2xl;
}
// ===== 知识库入口 =====
.knowledge-hub-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-lg;
margin-top: @space-xl;
.respond-lg({
grid-template-columns: repeat(4, 1fr);
});
}
.knowledge-hub-card {
display: flex;
flex-direction: column;
gap: @space-md;
padding: @space-xl;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
text-decoration: none;
transition: border-color 0.3s ease, box-shadow 0.3s ease, transform 0.3s ease;
&:hover {
border-color: @color-primary;
box-shadow: @shadow-md;
transform: translateY(-3px);
.knowledge-hub-card__icon {
background: @color-primary;
color: #fff;
}
}
&__icon {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
border-radius: @border-radius-md;
background: @color-primary-bg;
color: @color-primary;
transition: background @transition-base, color @transition-base;
}
&__title {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0;
}
&__desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
}
// ===== FAQ预览 =====
.faq-preview {
display: flex;
flex-direction: column;
gap: @space-md;
max-width: 800px;
margin: 0 auto;
}
.faq-preview-item {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
overflow: hidden;
transition: box-shadow @transition-base;
&[open] {
box-shadow: @shadow-card;
.faq-toggle-icon {
&::after {
transform: rotate(0deg);
}
}
}
}
.faq-preview-q {
display: flex;
align-items: center;
justify-content: space-between;
gap: @space-md;
padding: @space-lg @space-xl;
cursor: pointer;
list-style: none;
user-select: none;
&::-webkit-details-marker {
display: none;
}
&::marker {
display: none;
content: '';
}
&:hover {
background: @color-bg-gray;
}
}
.faq-q-text {
font-size: @font-size-md;
color: @color-text-primary;
font-weight: @font-weight-medium;
flex: 1;
}
.faq-toggle-icon {
width: 20px;
height: 20px;
position: relative;
flex-shrink: 0;
&::before,
&::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
background: @color-text-muted;
border-radius: 1px;
transition: transform @transition-base;
}
&::before {
width: 14px;
height: 2px;
transform: translate(-50%, -50%);
}
&::after {
width: 2px;
height: 14px;
transform: translate(-50%, -50%) rotate(90deg);
}
}
.faq-preview-a {
padding: 0 @space-xl @space-xl;
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-relaxed;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
.faq-preview-more {
text-align: center;
margin-top: @space-2xl;
}
</style>
+267
查看文件
@@ -0,0 +1,267 @@
<template>
<div class="page-news">
<LayoutPageHero
:title="seo.h1"
subtitle="品牌动态、产品发布、活动公告,第一时间了解呼籁旅行最新资讯"
:breadcrumbs="[{ text: '最新动态' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="最新动态摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行最新动态:产品上新、旺季出发公告、营地活动、品牌资讯。记录一个专注呼伦贝尔11年的旅行品牌的日常与成长。</p>
</div>
</div>
</section>
<section class="section">
<div class="container">
<!-- Category filter -->
<div class="news-filter">
<button
v-for="cat in categories"
:key="cat"
:class="['filter-btn', { 'filter-btn--active': activeCategory === cat }]"
@click="activeCategory = cat"
>
{{ cat }}
</button>
</div>
<!-- News list -->
<div class="news-list">
<article
v-for="article in filteredArticles"
:key="article.id"
class="news-card"
>
<div class="news-meta">
<span :class="['news-cat', `news-cat--${article.category}`]">{{ article.category }}</span>
<time class="news-date" :datetime="article.date">{{ formatDate(article.date) }}</time>
</div>
<h2 class="news-title">{{ article.title }}</h2>
<p class="news-summary">{{ article.summary }}</p>
</article>
</div>
<!-- CTA -->
<div class="news-cta">
<h3>想了解更多?</h3>
<p>草原的风,已经在等您了。</p>
<div class="news-cta-links">
<CommonInternalLink to="/products" text="查看2026旅行产品" />
<CommonInternalLink to="/summer-camp" text="小蒙马夏令营报名" />
<CommonInternalLink to="/contact" text="联系定制师" />
</div>
</div>
</div>
</section>
</div>
</template>
<script setup>
import newsData from '~/data/news.json'
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const seo = useSEO('news')
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '最新动态' },
])
// NewsArticle JSON-LD for each article
newsData.articles.forEach((article) => {
useHead({
script: [{
key: `news-article-${article.id}`,
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'NewsArticle',
headline: article.title,
description: article.summary,
articleBody: article.content,
datePublished: article.date + 'T00:00:00+08:00',
dateModified: article.date + 'T00:00:00+08:00',
articleSection: article.category,
inLanguage: 'zh-CN',
url: 'https://1814.love/news',
author: { '@type': 'Organization', name: '呼籁旅行', url: 'https://1814.love' },
publisher: { '@id': 'https://1814.love/#organization' },
about: { '@type': 'TravelAgency', name: '呼籁旅行', url: 'https://1814.love' },
keywords: '呼籁旅行,呼伦贝尔旅游,额吉的故乡,小蒙马夏令营',
}),
}],
})
})
const categories = ['全部', '产品', '活动', '公告']
const activeCategory = ref('全部')
const filteredArticles = computed(() => {
if (activeCategory.value === '全部') return newsData.articles
return newsData.articles.filter(a => a.category === activeCategory.value)
})
function formatDate(dateStr) {
const d = new Date(dateStr)
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日`
}
</script>
<style lang="less" scoped>
.news-filter {
display: flex;
gap: @space-sm;
margin-bottom: @space-2xl;
flex-wrap: wrap;
}
.filter-btn {
padding: @space-sm @space-lg;
border: 1px solid @color-border;
border-radius: @border-radius-full;
background: @color-bg-white;
color: @color-text-secondary;
font-size: @font-size-sm;
cursor: pointer;
transition: all @transition-base;
&:hover {
border-color: @color-primary;
color: @color-primary;
}
&--active {
background: @color-primary;
border-color: @color-primary;
color: @color-bg-white;
font-weight: @font-weight-medium;
}
}
.news-list {
display: flex;
flex-direction: column;
gap: @space-lg;
margin-bottom: @space-3xl;
}
.news-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-md;
padding: @space-xl;
transition: box-shadow @transition-base;
&:hover {
box-shadow: @shadow-md;
}
}
.news-meta {
display: flex;
align-items: center;
gap: @space-md;
margin-bottom: @space-md;
}
.news-cat {
display: inline-block;
padding: 2px @space-sm;
border-radius: @border-radius-sm;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
&--产品 {
background: @color-primary-bg;
color: @color-primary;
}
&--活动 {
background: #FFF3E6;
color: @color-accent;
}
&--公告 {
background: @color-bg-gray;
color: @color-text-secondary;
}
}
.news-date {
font-size: @font-size-sm;
color: @color-text-muted;
}
.news-title {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
line-height: @line-height-tight;
}
.news-summary {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
.news-cta {
text-align: center;
padding: @space-2xl;
background: @color-primary-bg;
border-radius: @border-radius-lg;
h3 {
font-size: @font-size-xl;
margin-bottom: @space-sm;
color: @color-primary;
}
p {
color: @color-text-secondary;
margin-bottom: @space-xl;
}
}
.news-cta-links {
display: flex;
justify-content: center;
gap: @space-xl;
flex-wrap: wrap;
}
@media (max-width: 768px) {
.news-card {
padding: @space-lg;
}
.news-title {
font-size: @font-size-base;
}
.news-cta-links {
flex-direction: column;
align-items: center;
gap: @space-md;
}
}
@media (max-width: 480px) {
.news-card {
padding: @space-md;
}
.news-meta {
flex-wrap: wrap;
gap: @space-sm;
}
}
</style>
+510
查看文件
@@ -0,0 +1,510 @@
<template>
<div class="page-partners">
<LayoutPageHero
:title="seo.h1"
subtitle="行业认证 · 平台背书 · 景区合作 · 高校联合"
:breadcrumbs="[{ text: '关于我们', to: '/about' }, { text: '合作伙伴' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="合作伙伴摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行合作伙伴与平台认证:携程/飞猪/同程官方认证旅行社、与多家景区及高校达成合作、获内蒙古自治区文化和旅游厅颁发旅游经营许可证。三家旅行社主体均持证合规经营。</p>
</div>
</div>
</section>
<!-- 平台认证(已验证,排在最前) -->
<section class="section section-platforms">
<div class="container">
<CommonSectionTitle
title="平台认证与荣誉"
subtitle="官方认证账号,平台公开可查"
/>
<div class="platforms-grid reveal">
<div
v-for="platform in verifiedPlatforms"
:key="platform.platform"
class="platform-card platform-card--verified"
>
<div class="platform-card__status">✓ {{ platform.status }}</div>
<h3 class="platform-card__name">{{ platform.platform }}</h3>
<p class="platform-card__detail">{{ platform.detail }}</p>
<p v-if="platform.followers" class="platform-card__followers">粉丝 {{ platform.followers }}</p>
</div>
</div>
</div>
</section>
<!-- 政府与行业认证 -->
<section class="section section-gov section-alt">
<div class="container">
<CommonSectionTitle
title="政府资质与行业背书"
subtitle="来自官方机构的认可与合作"
/>
<div class="partners-list reveal">
<div class="partners-list__group">
<h3 class="partners-list__group-title">官方资质认定</h3>
<div class="partners-items">
<div v-for="item in partners.associations.slice(0, 1)" :key="item.name" class="partner-item partner-item--official">
<div class="partner-item__icon">🏛️</div>
<div class="partner-item__content">
<h4 class="partner-item__name">{{ item.name }}</h4>
<p class="partner-item__relation">{{ item.relationship }}</p>
</div>
</div>
</div>
</div>
<div v-if="verifiedAssociations.length > 0" class="partners-list__group">
<h3 class="partners-list__group-title">行业协会</h3>
<div class="partners-items">
<div
v-for="item in verifiedAssociations"
:key="item.name"
class="partner-item"
>
<div class="partner-item__icon">🤝</div>
<div class="partner-item__content">
<h4 class="partner-item__name">{{ item.name }}</h4>
<p class="partner-item__relation">{{ item.memberType }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 高校合作 -->
<section class="section section-academic">
<div class="container">
<CommonSectionTitle
title="校企合作"
subtitle="联合高校,培养本地旅游人才"
/>
<div class="academic-grid reveal">
<div
v-for="coop in partners.academicCoops"
:key="coop.name"
class="academic-card"
>
<div class="academic-card__icon">🎓</div>
<div class="academic-card__content">
<h3 class="academic-card__name">{{ coop.name }}</h3>
<span class="academic-card__type">{{ coop.relationship }}</span>
<p class="academic-card__desc">{{ coop.description }}</p>
<span v-if="coop.since" class="academic-card__since">合作始于 {{ coop.since }} 年</span>
</div>
</div>
</div>
</div>
</section>
<!-- 景区合作 -->
<section class="section section-scenic section-alt">
<div class="container">
<CommonSectionTitle
title="景区合作"
subtitle="与呼伦贝尔核心景区深度合作,确保专属体验"
/>
<div class="scenic-grid reveal">
<div
v-for="scenic in verifiedScenicPartners"
:key="scenic.name"
class="scenic-card"
>
<h3 class="scenic-card__name">{{ scenic.name }}</h3>
<p class="scenic-card__desc">{{ scenic.description }}</p>
</div>
</div>
</div>
</section>
<!-- 媒体报道:仅在有真实报道时展示 -->
<section v-if="verifiedMediaReports.length > 0" class="section section-media">
<div class="container">
<CommonSectionTitle
title="媒体报道"
subtitle="第三方媒体对呼籁旅行的报道与采访"
/>
<div class="media-grid reveal">
<div
v-for="(report, index) in verifiedMediaReports"
:key="index"
class="media-card"
>
<div class="media-card__outlet">{{ report.outlet }}</div>
<h3 class="media-card__title">{{ report.title }}</h3>
<p class="media-card__summary">{{ report.summary }}</p>
<div class="media-card__footer">
<span class="media-card__date">{{ report.date }}</span>
<a v-if="report.url && !report.url.includes('⚠️')" :href="report.url" target="_blank" class="media-card__link">查看报道</a>
</div>
</div>
</div>
</div>
</section>
<!-- 内链 -->
<CommonSectionCTA
title="选择有资质的旅行品牌"
description="查看呼籁的完整资质证明,或直接联系定制师"
:links="[
{ to: '/qualifications', text: '查看完整资质证书' },
{ to: '/reviews', text: '查看真实客户评价' },
]"
/>
</div>
</template>
<script setup>
import { computed } from 'vue'
import partners from '~/data/partners.json'
import brand from '~/data/brand.json'
import contact from '~/data/contact.json'
import { useOrganizationSchema, useBreadcrumbSchema } from '~/composables/useJsonLd'
const seo = useSEO('partners')
useScrollReveal()
useOrganizationSchema(brand, contact)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '关于我们', to: '/about' },
{ text: '合作伙伴与媒体' },
])
const verifiedPlatforms = computed(() =>
partners.platformEndorsements.filter(p => p.verified)
)
const verifiedScenicPartners = computed(() =>
partners.scenicPartners.filter(p => p.verified)
)
// 行业协会:只展示已确认的(name 不含 ⚠️)
const verifiedAssociations = computed(() =>
partners.associations.slice(1).filter(p => p.verified && !p.name.includes('⚠️'))
)
// 媒体报道:只展示非占位符的真实报道
const verifiedMediaReports = computed(() =>
partners.mediaReports.filter(r => !r.placeholder && !r.outlet.includes('⚠️'))
)
</script>
<style lang="less" scoped>
// 平台认证
.platforms-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.platform-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
&--verified {
border-color: @color-primary;
background: @color-primary-bg;
}
&__status {
font-size: @font-size-xs;
color: @color-primary;
font-weight: @font-weight-bold;
margin-bottom: @space-sm;
}
&__name {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
}
&__detail {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0 0 @space-sm;
line-height: @line-height-base;
}
&__followers {
font-size: @font-size-sm;
color: @color-primary;
font-weight: @font-weight-medium;
margin: 0;
}
}
// 政府合作
.section-gov {
background: @color-bg-gray;
}
.partners-list {
display: flex;
flex-direction: column;
gap: @space-xl;
&__group {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
}
&__group-title {
font-size: @font-size-md;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-lg;
display: flex;
align-items: center;
gap: @space-md;
}
}
.group-title-note {
font-size: @font-size-xs;
font-weight: normal;
color: #e67e22;
background: #fef9e7;
padding: 2px @space-sm;
border-radius: @border-radius-sm;
}
.partners-items {
display: flex;
flex-direction: column;
gap: @space-md;
}
.partner-item {
display: flex;
gap: @space-md;
align-items: flex-start;
padding: @space-md;
border-radius: @border-radius-md;
background: @color-bg-gray;
&--official {
background: @color-primary-bg;
}
&--unverified {
opacity: 0.7;
}
&__icon {
font-size: 24px;
flex-shrink: 0;
}
&__content {
flex: 1;
}
&__name {
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-xs;
}
&__relation {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0;
}
}
// 高校合作
.academic-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-lg({
grid-template-columns: repeat(2, 1fr);
});
}
.academic-card {
display: flex;
gap: @space-lg;
align-items: flex-start;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
&__icon {
font-size: 36px;
flex-shrink: 0;
}
&__content {
flex: 1;
}
&__name {
font-size: @font-size-md;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-xs;
}
&__type {
display: inline-block;
font-size: @font-size-xs;
padding: 2px @space-sm;
background: @color-primary;
color: #fff;
border-radius: @border-radius-full;
margin-bottom: @space-sm;
}
&__desc {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0 0 @space-sm;
line-height: @line-height-base;
}
&__since {
font-size: @font-size-xs;
color: @color-text-muted;
}
}
// 景区合作
.section-scenic {
background: @color-bg-gray;
}
.scenic-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-md;
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.scenic-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-md;
padding: @space-lg;
&__name {
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
}
&__desc {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0;
line-height: @line-height-base;
}
}
.section-placeholder-note {
margin-top: @space-lg;
text-align: center;
font-size: @font-size-sm;
color: #e67e22;
}
// 媒体报道
.section-header-with-note {
margin-bottom: @space-xl;
}
.placeholder-banner {
background: #fef9e7;
border: 1px solid #f39c12;
border-radius: @border-radius-md;
padding: @space-md @space-lg;
font-size: @font-size-sm;
color: #7d6608;
margin-top: @space-lg;
}
.media-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.media-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
&--placeholder {
border-style: dashed;
opacity: 0.7;
}
&__outlet {
font-size: @font-size-xs;
color: @color-primary;
font-weight: @font-weight-bold;
margin-bottom: @space-sm;
}
&__title {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
line-height: @line-height-tight;
}
&__summary {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0 0 @space-lg;
line-height: @line-height-base;
flex: 1;
}
&__footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: @space-sm;
border-top: 1px solid @color-border;
}
&__date {
font-size: @font-size-xs;
color: @color-text-muted;
}
&__link-placeholder {
font-size: @font-size-xs;
color: #e67e22;
}
}
</style>
+418
查看文件
@@ -0,0 +1,418 @@
<template>
<div class="page-pricing">
<LayoutPageHero
:title="seo.h1"
subtitle="不藏价格,不绕弯子"
:breadcrumbs="[{ text: '价格参考' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="价格参考摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行实行价格透明原则,无隐形消费。定制游产品按家庭整体报价(非人头计费),覆盖4-7天6条路线及亲子夏令营。费用包含全程住宿、餐饮、交通、导游,不含大交通(飞机/火车)和个人消费。签正规电子合同,购买1000万旅游责任险,报价后2小时内回复。</p>
</div>
</div>
</section>
<!-- 简介 -->
<section class="section section-intro reveal">
<div class="container">
<div class="intro-box">
<p class="intro-text">{{ pricing.intro }}</p>
<div class="trust-badges">
<span class="badge">✓ 全程无隐形消费</span>
<span class="badge">✓ 正规电子合同</span>
<span class="badge">✓ 1000万旅游责任险</span>
<span class="badge">✓ 报价后2小时回复</span>
</div>
</div>
</div>
</section>
<!-- 产品价格卡片 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle
title="产品线价格参考"
subtitle="6条定制游线路 + 1个亲子夏令营 + 秋季产品"
/>
<div class="pricing-grid">
<div
v-for="product in pricing.products"
:key="product.id"
class="pricing-card"
>
<div class="card-header">
<span class="card-tag">{{ product.tag }}</span>
<h3 class="card-name">{{ product.name }}</h3>
<p class="card-audience">适合:{{ product.audience }}</p>
</div>
<div class="card-body">
<div class="card-duration" v-if="product.days > 0">
<span class="duration-text">{{ product.days }}天{{ product.nights }}晚</span>
</div>
<ul class="card-highlights">
<li v-for="h in product.highlights" :key="h">{{ h }}</li>
</ul>
</div>
<div class="card-footer">
<div class="price-label">{{ product.priceLabel }}</div>
<p class="price-note">{{ product.priceNote }}</p>
<NuxtLink :to="product.url" class="card-link">查看产品详情 →</NuxtLink>
</div>
</div>
</div>
</div>
</section>
<!-- 费用包含/不含 -->
<section class="section section-inclusions reveal">
<div class="container">
<CommonSectionTitle
title="费用说明"
subtitle="所有产品遵循同一套费用标准"
/>
<div class="inclusions-grid">
<div class="inclusion-col included">
<h3 class="inclusion-title">
<span class="inclusion-icon">✓</span> 费用包含
</h3>
<ul class="inclusion-list">
<li v-for="item in pricing.included" :key="item">{{ item }}</li>
</ul>
</div>
<div class="inclusion-col excluded">
<h3 class="inclusion-title">
<span class="inclusion-icon">✗</span> 费用不含
</h3>
<ul class="inclusion-list">
<li v-for="item in pricing.excluded" :key="item">{{ item }}</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 价格透明原则 -->
<section class="section section-philosophy reveal">
<div class="container">
<div class="philosophy-box">
<h2 class="philosophy-title">{{ pricing.philosophy.title }}</h2>
<p class="philosophy-honest">{{ pricing.philosophy.honestNote }}</p>
<p class="philosophy-content">{{ pricing.philosophy.content }}</p>
<p class="philosophy-promise">{{ pricing.philosophy.promise }}</p>
</div>
</div>
</section>
<!-- CTA -->
<CommonSectionCTA
:title="pricing.cta.title"
:description="pricing.cta.description"
:links="[{ to: pricing.cta.to, text: pricing.cta.button }]"
/>
</div>
</template>
<script setup>
import pricing from '~/data/pricing.json'
import brand from '~/data/brand.json'
import contact from '~/data/contact.json'
import { useOrganizationSchema, useBreadcrumbSchema, usePricingSchema } from '~/composables/useJsonLd'
const seo = useSEO('pricing')
useScrollReveal()
useOrganizationSchema(brand, contact)
usePricingSchema(pricing.products)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '价格参考' },
])
</script>
<style lang="less" scoped>
.intro-box {
max-width: 800px;
margin: 0 auto;
text-align: center;
}
.intro-text {
font-size: @font-size-md;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-xl;
}
.trust-badges {
display: flex;
flex-wrap: wrap;
gap: @space-md;
justify-content: center;
}
.badge {
display: inline-block;
padding: @space-sm @space-lg;
background: @color-primary-bg;
color: @color-primary;
border-radius: @border-radius-full;
font-size: @font-size-sm;
font-weight: @font-weight-medium;
border: 1px solid @color-primary-lighter;
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: @space-xl;
margin-top: @space-2xl;
@media (max-width: @breakpoint-lg) {
grid-template-columns: repeat(2, 1fr);
}
@media (max-width: @breakpoint-md) {
grid-template-columns: 1fr;
}
}
.pricing-card {
display: flex;
flex-direction: column;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
overflow: hidden;
background: @color-bg-white;
transition: box-shadow @transition-base, transform @transition-base;
&:hover {
box-shadow: @shadow-md;
transform: translateY(-2px);
}
}
.card-header {
padding: @space-xl @space-xl @space-lg;
background: @color-bg-gray;
border-bottom: 1px solid @color-border;
}
.card-tag {
display: inline-block;
padding: 2px @space-sm;
background: @color-primary;
color: @color-bg-white;
border-radius: @border-radius-sm;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
margin-bottom: @space-sm;
}
.card-name {
font-size: @font-size-md;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-sm;
line-height: @line-height-tight;
}
.card-audience {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0;
}
.card-body {
flex: 1;
padding: @space-lg @space-xl;
}
.card-duration {
margin-bottom: @space-md;
}
.duration-text {
font-size: @font-size-sm;
color: @color-accent;
font-weight: @font-weight-medium;
background: #FFF8F0;
padding: 2px @space-sm;
border-radius: @border-radius-sm;
}
.card-highlights {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: @space-sm;
li {
font-size: @font-size-sm;
color: @color-text-secondary;
padding-left: @space-md;
position: relative;
line-height: @line-height-base;
&::before {
content: '·';
position: absolute;
left: 0;
color: @color-primary;
font-weight: @font-weight-bold;
}
}
}
.card-footer {
padding: @space-lg @space-xl @space-xl;
border-top: 1px solid @color-border;
}
.price-label {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-sm;
}
.price-note {
font-size: @font-size-xs;
color: @color-text-muted;
line-height: @line-height-base;
margin-bottom: @space-md;
}
.card-link {
font-size: @font-size-sm;
color: @color-primary;
text-decoration: none;
font-weight: @font-weight-medium;
&:hover {
text-decoration: underline;
}
}
.section-inclusions {
background: @color-bg-gray;
}
.inclusions-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: @space-2xl;
margin-top: @space-2xl;
@media (max-width: @breakpoint-md) {
grid-template-columns: 1fr;
gap: @space-xl;
}
}
.inclusion-col {
padding: @space-2xl;
border-radius: @border-radius-lg;
&.included {
background: @color-primary-bg;
border: 1px solid @color-primary-lighter;
}
&.excluded {
background: @color-bg-white;
border: 1px solid @color-border;
}
}
.inclusion-title {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-lg;
display: flex;
align-items: center;
gap: @space-sm;
}
.inclusion-icon {
.included & {
color: @color-primary;
}
.excluded & {
color: @color-text-muted;
}
}
.inclusion-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: @space-md;
li {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-base;
padding-left: @space-md;
position: relative;
&::before {
content: '—';
position: absolute;
left: 0;
color: @color-text-muted;
}
}
}
.section-philosophy {
background: @color-bg-warm;
}
.philosophy-box {
max-width: 760px;
margin: 0 auto;
}
.philosophy-title {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-lg;
}
.philosophy-honest {
font-size: @font-size-md;
color: @color-text-primary;
line-height: @line-height-loose;
margin-bottom: @space-lg;
font-weight: @font-weight-medium;
border-left: 3px solid @color-primary;
padding-left: @space-lg;
}
.philosophy-content {
font-size: @font-size-md;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-lg;
}
.philosophy-promise {
font-size: @font-size-base;
color: @color-primary;
font-weight: @font-weight-medium;
line-height: @line-height-base;
padding: @space-lg;
background: @color-primary-bg;
border-radius: @border-radius-md;
border-left: 3px solid @color-primary;
margin: 0;
}
</style>
+260
查看文件
@@ -0,0 +1,260 @@
<template>
<main class="product-detail" v-if="product">
<section class="product-hero">
<div class="container">
<nav class="breadcrumb" aria-label="面包屑导航">
<NuxtLink to="/">首页</NuxtLink> / <NuxtLink to="/products">产品线</NuxtLink> / <span>{{ product.name }}</span>
</nav>
<div class="hero-content">
<span class="product-tag">{{ product.tag }}</span>
<h1>{{ seo.h1 || product.name }}</h1>
<p class="hero-audience">适合:{{ product.audience }}</p>
<p class="hero-duration">{{ product.days }}天{{ product.nights }}晚 · 一家一单一车</p>
</div>
</div>
</section>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="产品摘要">
<div class="container">
<div class="page-tldr-inner">
<p>{{ product.name }}:{{ product.description }}适合{{ product.audience }},{{ product.days }}天{{ product.nights }}晚,一家一单一车私家定制,旺季需提前预订。</p>
</div>
</div>
</section>
<section class="product-description">
<div class="container">
<p>{{ product.description }}</p>
</div>
</section>
<section class="product-highlights" v-if="product.highlights?.length">
<div class="container">
<h2>行程亮点</h2>
<ul>
<li v-for="(h, i) in product.highlights" :key="i">{{ h }}</li>
</ul>
</div>
</section>
<section class="product-faq" v-if="relatedFaq.length">
<div class="container">
<h2>常见问题</h2>
<div class="faq-list">
<details v-for="q in relatedFaq" :key="q.id" class="faq-item">
<summary>{{ q.question }}</summary>
<p>{{ q.answer }}</p>
</details>
</div>
</div>
</section>
<section class="product-cta">
<div class="container">
<h2>开始定制您的行程</h2>
<p>扫码添加微信,定制师一对一为您规划</p>
<NuxtLink to="/contact" class="cta-button">联系定制师</NuxtLink>
</div>
</section>
</main>
</template>
<script setup>
import productsData from '~/data/products.json'
import winterProductsData from '~/data/winter-products.json'
import autumnProductsData from '~/data/autumn-products.json'
import faqData from '~/data/faq.json'
import useSEO from '~/composables/useSEO'
import { useTouristTripSchema, useFAQPageSchema, useBreadcrumbSchema } from '~/composables/useJsonLd'
const route = useRoute()
const productId = route.params.id
const allVersions = [
...(productsData.versions || []),
...(winterProductsData.versions || []),
...(autumnProductsData.versions || []),
]
const product = allVersions.find(v => v.id === productId)
if (!product) {
throw createError({ statusCode: 404, statusMessage: '产品不存在' })
}
// SEO - 每个产品独立的 meta
const seoOverrides = {
title: `${product.name} - 呼伦贝尔${product.days}天${product.nights}晚定制游 - 呼籁旅行`,
description: product.description?.substring(0, 160),
h1: product.name,
}
const seo = useSEO('products', seoOverrides)
// 产品页专属 keywords
useHead({
meta: [
{ name: 'keywords', content: `${product.name},呼伦贝尔${product.days}天${product.nights}晚,呼伦贝尔定制游,呼伦贝尔亲子游,${product.tag}` },
],
})
// JSON-LD: 单个产品的 TouristTrip
useTouristTripSchema([product])
// 面包屑
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '产品线', to: '/products' },
{ text: product.name },
])
// 相关 FAQ - 从 FAQ 数据中匹配与天数/产品相关的问题
const allQuestions = faqData.categories.flatMap(cat => cat.questions)
const relatedFaq = allQuestions.filter(q =>
q.question.includes(String(product.days)) ||
q.question.includes('几天') ||
q.question.includes('亲子') ||
q.question.includes('区别') ||
q.question.includes('多少钱')
).slice(0, 5)
// 产品页也注入 FAQ schema(SEO精选摘要加分项)
if (relatedFaq.length) {
useFAQPageSchema([{ questions: relatedFaq }])
}
</script>
<style scoped>
.container {
max-width: 800px;
margin: 0 auto;
padding: 0 20px;
}
.product-hero {
background: linear-gradient(135deg, #f0f5f1, #e8ede9);
padding: 40px 0 48px;
}
.breadcrumb {
font-size: 14px;
color: #7a857c;
margin-bottom: 24px;
}
.breadcrumb a {
color: #3a7d44;
text-decoration: none;
}
.product-tag {
display: inline-block;
background: #3a7d44;
color: #fff;
padding: 4px 12px;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
margin-bottom: 12px;
}
h1 {
font-size: 32px;
font-weight: 800;
color: #1a1f1c;
line-height: 1.3;
margin-bottom: 12px;
}
.hero-audience, .hero-duration {
font-size: 16px;
color: #4a5e4d;
margin-bottom: 4px;
}
.product-description {
padding: 40px 0;
}
.product-description p {
font-size: 17px;
line-height: 1.8;
color: #3d4a40;
}
.product-highlights {
padding: 40px 0;
background: #fafbfa;
}
.product-highlights h2 {
font-size: 24px;
font-weight: 700;
margin-bottom: 20px;
color: #1a1f1c;
}
.product-highlights ul {
list-style: none;
padding: 0;
}
.product-highlights li {
padding: 14px 0;
border-bottom: 1px solid #eef0ef;
font-size: 16px;
line-height: 1.6;
color: #3d4a40;
padding-left: 24px;
position: relative;
}
.product-highlights li::before {
content: '✓';
position: absolute;
left: 0;
color: #3a7d44;
font-weight: 700;
}
.product-faq {
padding: 40px 0;
}
.product-faq h2 {
font-size: 24px;
font-weight: 700;
margin-bottom: 20px;
color: #1a1f1c;
}
.faq-item {
border-bottom: 1px solid #eef0ef;
padding: 16px 0;
}
.faq-item summary {
font-size: 16px;
font-weight: 600;
color: #1a1f1c;
cursor: pointer;
list-style: none;
}
.faq-item summary::marker { display: none; }
.faq-item summary::-webkit-details-marker { display: none; }
.faq-item p {
margin-top: 12px;
font-size: 15px;
line-height: 1.7;
color: #4a5e4d;
}
.product-cta {
padding: 48px 0;
text-align: center;
background: linear-gradient(135deg, #f0f5f1, #e8ede9);
}
.product-cta h2 {
font-size: 24px;
font-weight: 700;
margin-bottom: 8px;
}
.product-cta p {
color: #4a5e4d;
margin-bottom: 20px;
}
.cta-button {
display: inline-block;
background: #3a7d44;
color: #fff;
padding: 12px 32px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
font-size: 16px;
}
.cta-button:hover {
background: #2e6435;
}
</style>
+224
查看文件
@@ -0,0 +1,224 @@
<template>
<div class="page-autumn">
<LayoutPageHero
:title="seo.h1"
subtitle="独立成团不拼团 · 一单一车 · 随走随停 · 不错过每处风景"
:breadcrumbs="[{ text: '产品线', to: '/products' }, { text: '游牧的森林' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="秋季产品摘要">
<div class="container">
<div class="page-tldr-inner">
<p>游牧的森林——呼籁旅行秋季产品线:9月下旬至10月,大兴安岭金色白桦林+草原秋景,4-7天多版本,独立成团不拼团,一家一单一车。适合亲子家庭和摄影爱好者,秋色旺季档期有限,建议8月底前预订。</p>
</div>
</div>
</section>
<!-- 风景大图 -->
<div class="scenic-banner">
<img src="/images/seasons/autumn-hero.jpg" alt="秋季大兴安岭金色林海公路航拍" loading="eager" />
</div>
<!-- 品牌理念 -->
<ProductsProductOverview :narrative="autumnData.narrative" class="reveal" />
<!-- 产品卡片 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="游牧的森林V3系列" subtitle="4-7天南北两线,专属私家团" />
<div class="products-grid">
<AutumnProductCard v-for="p in autumnData.versions" :key="p.id" :product="p" />
</div>
</div>
</section>
<!-- 选行程指南 -->
<CommonSectionCTA
title="不确定选南线还是北线?"
description="告诉定制师您的假期和偏好,一分钟帮您选对行程"
:links="[{ to: '/contact', text: '联系定制师选行程' }]"
/>
<!-- 共同亮点 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="秋季行程亮点" subtitle="每一条线路都会体验到" />
<div class="highlights-grid">
<div v-for="(h, i) in autumnData.highlights" :key="i" class="highlight-card">
<h3 class="highlight-title">{{ h.title }}</h3>
<p class="highlight-desc">{{ h.description }}</p>
</div>
</div>
</div>
</section>
<!-- 秋季风景 -->
<div class="scenic-duo reveal">
<img src="/images/seasons/autumn-tianchi.jpg" alt="阿尔山心形天池航拍" loading="lazy" />
<img src="/images/seasons/autumn-forest.jpg" alt="金秋白桦林层林尽染" loading="lazy" />
</div>
<!-- 南线特色 -->
<section class="section section--warm reveal">
<div class="container">
<CommonSectionTitle title="南线特色 · 呼伦贝尔+阿尔山" subtitle="金秋草原与火山天池的碰撞" />
<div class="highlights-grid">
<div v-for="(h, i) in autumnData.southHighlights" :key="i" class="highlight-card">
<h3 class="highlight-title">{{ h.title }}</h3>
<p class="highlight-desc">{{ h.description }}</p>
</div>
</div>
</div>
</section>
<!-- 北线风景 -->
<div class="scenic-banner reveal">
<img src="/images/seasons/autumn-train.jpg" alt="秋季大兴安岭列车穿越金色林海" loading="lazy" />
</div>
<!-- 北线特色 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="北线特色 · 呼伦贝尔+大兴安岭" subtitle="绝美公路与原始村落的秘境" />
<div class="highlights-grid">
<div v-for="(h, i) in autumnData.northHighlights" :key="i" class="highlight-card">
<h3 class="highlight-title">{{ h.title }}</h3>
<p class="highlight-desc">{{ h.description }}</p>
</div>
</div>
</div>
</section>
<!-- 版本迭代历史 -->
<ProductsVersionTimeline :timeline="autumnData.timeline" title="游牧的森林版本迭代" subtitle="V1到V3 + V4预告" />
<!-- 内链 -->
<CommonSectionCTA
title="听听秋季旅行的家庭怎么说"
description="看看真实客户的秋季旅行评价"
:links="[{ to: '/reviews', text: '查看客户评价' }, { to: '/contact', text: '咨询秋季行程' }]"
/>
</div>
</template>
<script setup>
import autumnData from '~/data/autumn-products.json'
import { useBreadcrumbSchema, useAutumnProductsSchema } from '~/composables/useJsonLd'
const seo = useSEO('products-autumn', {
title: '游牧的森林 - 呼伦贝尔秋季定制游 · 南北两线4-7天 - 呼籁旅行',
description: '呼籁旅行秋季定制游产品「游牧的森林V3」,呼伦贝尔+阿尔山/大兴安岭,4-7天南北两线。大兴安岭金秋、扎罗木得断崖、阿尔山国家森林公园、火山天池、秋野素拍。一单一车专属私家团。',
h1: '游牧的森林 · 呼伦贝尔秋季定制游',
})
useScrollReveal()
useAutumnProductsSchema(autumnData)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '产品线', to: '/products' },
{ text: '游牧的森林' },
])
</script>
<style lang="less" scoped>
.products-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-xl;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
> * {
display: flex;
}
}
.highlights-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.highlight-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
transition: box-shadow @transition-base, transform @transition-base;
&:hover {
box-shadow: @shadow-md;
transform: translateY(-2px);
}
}
.highlight-title {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-sm;
&::before {
content: '🍂 ';
color: #D97706;
}
}
.highlight-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
}
.section--warm {
background: @color-bg-warm;
}
// 风景大图
.scenic-banner {
width: 100%;
overflow: hidden;
max-height: 480px;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.scenic-duo {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
max-height: 360px;
overflow: hidden;
.respond-md({
grid-template-columns: 1fr 1fr;
});
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
max-height: 360px;
}
}
</style>
+177
查看文件
@@ -0,0 +1,177 @@
<template>
<div class="page-products">
<LayoutPageHero
:title="seo.h1"
subtitle="独立成团不拼团 · 一单一车 · 随走随停 · 不错过每处风景"
:breadcrumbs="[{ text: '产品线', to: '/products' }, { text: '额吉的故乡' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="产品页摘要">
<div class="container">
<div class="page-tldr-inner">
<p>额吉的故乡V9系列提供5-7天6个版本的呼伦贝尔亲子游,涵盖森林版、草原版、亲子版、探索版、旷野版等不同需求。所有版本均为独立成团、一家一单一车,不与陌生人拼团。不确定选哪个版本,可使用<NuxtLink to="/selector">选版工具</NuxtLink>进行1分钟匹配,或参考<NuxtLink to="/pricing">价格参考页</NuxtLink>了解费用区间。</p>
</div>
</div>
</section>
<!-- 风景大图 -->
<div class="scenic-banner">
<img src="/images/seasons/summer-hero.jpg" alt="夏季呼伦贝尔莫日格勒河草原全景" loading="eager" />
</div>
<!-- 产品设计理念 -->
<ProductsProductOverview :narrative="products.narrative" class="reveal" />
<!-- 产品卡片 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="额吉的故乡V9系列" subtitle="5-7天多版本行程" />
<div class="products-grid">
<ProductsProductCard v-for="p in products.versions" :key="p.id" :product="p" />
</div>
</div>
</section>
<!-- 夏季风景 -->
<div class="scenic-duo reveal">
<img src="/images/seasons/summer-river.jpg" alt="夏季草原蒙古包莫日格勒河全景" loading="lazy" />
<img src="/images/seasons/summer-car.jpg" alt="呼伦贝尔一单一车草原越野" loading="lazy" />
</div>
<!-- 中间转化入口 -->
<CommonSectionCTA
title="不确定选哪个版本?"
description="5个问题,1分钟,找到最适合你家庭的行程版本"
:links="[{ to: '/selector', text: '使用选版工具' }, { to: '/contact', text: '联系定制师' }]"
/>
<!-- 选择指南 -->
<ProductsSelectionGuide :guide="products.selectionGuide" :versions="products.versions" class="reveal" />
<!-- 产品进化:统计数据 -->
<ProductsEvolutionStats :stats="versions.stats" class="reveal" />
<!-- 2026核心升级 -->
<ProductsVersionUpgrades :upgrades="versions.upgrades2026" class="reveal" />
<!-- V9亮点 -->
<ProductsVersionHighlights :highlights="versions.highlights" class="reveal" />
<!-- V8→V9对比 -->
<ProductsVersionCompare :compare="versions.compareV8V9" class="reveal" />
<!-- 历史版本时间线 -->
<ProductsVersionTimeline :timeline="versions.timeline" />
<!-- 产品哲学 -->
<ProductsVersionPhilosophy :philosophy="versions.philosophy" :quote="versions.quote" />
<!-- 价格透明 -->
<ProductsPricingPhilosophy :content="products.pricingPhilosophy" />
<!-- 爸妈也想来草原 -->
<section class="section section--gray reveal">
<div class="container" style="max-width: 720px; text-align: center;">
<h2 class="section-title" style="font-size: 24px; margin-bottom: 8px;">爸妈也想来草原?</h2>
<p style="color: #5a6b5e; line-height: 1.8; margin-bottom: 24px;">很多家庭带孩子来过呼伦贝尔之后,又给爸妈报了名。自己上班没时间陪,但想让父母也体验一次靠谱的草原旅行——我们的「牵手草原」长辈定制行程,专为父母和长辈设计,节奏更缓、住宿更舒适、服务更细致。</p>
<NuxtLink to="/contact" class="senior-cta">联系定制师了解「牵手草原」</NuxtLink>
</div>
</section>
<!-- 内链 -->
<CommonSectionCTA
title="听听其他家庭怎么说"
description="看看真实客户的旅行评价"
:links="[{ to: '/reviews', text: '查看10000+家庭的真实评价' }]"
/>
</div>
</template>
<script setup>
import products from '~/data/products.json'
import versions from '~/data/versions.json'
import { useProductsListSchema, useBreadcrumbSchema } from '~/composables/useJsonLd'
const seo = useSEO('products')
useScrollReveal()
useProductsListSchema(products.versions)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '产品线', to: '/products' },
{ text: '额吉的故乡' },
])
</script>
<style lang="less" scoped>
.products-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-xl;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
// 让卡片等高对齐
> * {
display: flex;
}
}
.scenic-banner {
width: 100%;
overflow: hidden;
max-height: 480px;
img {
width: 100%;
height: 480px;
object-fit: cover;
object-position: center 70%;
display: block;
}
}
.scenic-duo {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
max-height: 360px;
overflow: hidden;
.respond-md({
grid-template-columns: 1fr 1fr;
});
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
max-height: 360px;
}
}
.senior-cta {
display: inline-block;
padding: 12px 28px;
background: @color-bg-white;
color: @color-primary;
border: 1px solid @color-primary;
border-radius: @border-radius-md;
text-decoration: none;
font-size: @font-size-sm;
font-weight: @font-weight-medium;
transition: all @transition-fast;
&:hover {
background: @color-primary;
color: #fff;
}
}
</style>
+223
查看文件
@@ -0,0 +1,223 @@
<template>
<div class="page-winter">
<LayoutPageHero
:title="seo.h1"
subtitle="独立成团不拼团 · 一单一车 · 随走随停 · 不错过每处风景"
:breadcrumbs="[{ text: '产品线', to: '/products' }, { text: '嗨冰雪' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="冬季产品摘要">
<div class="container">
<div class="page-tldr-inner">
<p>嗨冰雪——呼籁旅行冬季产品线:12月至次年2月,呼伦贝尔雪原体验,5-7天多版本,独立成团不拼团,一家一单一车。雪地摩托、冰钓、冬季那达慕、驯鹿文化、边境雪原。元旦春节黄金档期需提前1-2个月预订。</p>
</div>
</div>
</section>
<!-- 风景大图 -->
<div class="scenic-banner">
<img src="/images/seasons/winter-hero.jpg" alt="冬季呼伦贝尔大雪原航拍" loading="eager" />
</div>
<!-- 品牌理念 -->
<ProductsProductOverview :narrative="winterData.narrative" class="reveal" />
<!-- 产品卡片 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="嗨冰雪V3系列" subtitle="5-7天南北两线,专属私家团" />
<div class="products-grid">
<WinterProductCard v-for="p in winterData.versions" :key="p.id" :product="p" />
</div>
</div>
</section>
<!-- 选行程指南 -->
<CommonSectionCTA
title="不确定选南线还是北线?"
description="告诉定制师您的假期和偏好,一分钟帮您选对行程"
:links="[{ to: '/contact', text: '联系定制师选行程' }]"
/>
<!-- 共同亮点 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="冬季行程亮点" subtitle="每一条线路都会体验到" />
<div class="highlights-grid">
<div v-for="(h, i) in winterData.highlights" :key="i" class="highlight-card">
<h3 class="highlight-title">{{ h.title }}</h3>
<p class="highlight-desc">{{ h.description }}</p>
</div>
</div>
</div>
</section>
<!-- 冬季风景 -->
<div class="scenic-duo reveal">
<img src="/images/seasons/winter-yurt.jpg" alt="冬季雪原蒙古包日落" loading="lazy" />
<img src="/images/seasons/winter-frost.jpg" alt="大兴安岭雾凇森林" loading="lazy" />
</div>
<!-- 南线特色 -->
<section class="section section--warm reveal">
<div class="container">
<CommonSectionTitle title="南线特色 · 呼伦贝尔+阿尔山" subtitle="大雪原与童话小城的碰撞" />
<div class="highlights-grid">
<div v-for="(h, i) in winterData.southHighlights" :key="i" class="highlight-card">
<h3 class="highlight-title">{{ h.title }}</h3>
<p class="highlight-desc">{{ h.description }}</p>
</div>
</div>
</div>
</section>
<!-- 北线风景 -->
<div class="scenic-banner reveal">
<img src="/images/seasons/winter-tree.jpg" alt="冬季雪原孤树" loading="lazy" />
</div>
<!-- 北线特色 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="北线特色 · 呼伦贝尔+大兴安岭" subtitle="边境风情与驯鹿部落的秘境" />
<div class="highlights-grid">
<div v-for="(h, i) in winterData.northHighlights" :key="i" class="highlight-card">
<h3 class="highlight-title">{{ h.title }}</h3>
<p class="highlight-desc">{{ h.description }}</p>
</div>
</div>
</div>
</section>
<!-- 版本迭代历史 -->
<ProductsVersionTimeline :timeline="winterData.timeline" title="嗨冰雪版本迭代" subtitle="V1到V3的完整更新记录" />
<!-- 内链 -->
<CommonSectionCTA
title="听听冬季旅行的家庭怎么说"
description="看看真实客户的冬季旅行评价"
:links="[{ to: '/reviews', text: '查看客户评价' }, { to: '/contact', text: '咨询冬季行程' }]"
/>
</div>
</template>
<script setup>
import winterData from '~/data/winter-products.json'
import { useBreadcrumbSchema, useWinterProductsSchema } from '~/composables/useJsonLd'
const seo = useSEO('products-winter', {
title: '嗨冰雪 - 呼伦贝尔冬季定制游 · 南北两线5-7天 - 呼籁旅行',
description: '呼籁旅行冬季定制游产品「嗨冰雪V3」,南线呼伦贝尔+阿尔山5-6天,北线呼伦贝尔+大兴安岭7天。冰雪那达慕、雪地狼群、白狼峰日出、呼伦湖蓝冰、驯鹿部落。一单一车专属私家团。',
h1: '嗨冰雪 · 呼伦贝尔冬季定制游',
})
useScrollReveal()
useWinterProductsSchema(winterData)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '产品线', to: '/products' },
{ text: '嗨冰雪' },
])
</script>
<style lang="less" scoped>
.products-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-xl;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
> * {
display: flex;
}
}
.highlights-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.highlight-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
transition: box-shadow @transition-base, transform @transition-base;
&:hover {
box-shadow: @shadow-md;
transform: translateY(-2px);
}
}
.highlight-title {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-sm;
&::before {
content: '❄ ';
color: #3B82F6;
}
}
.highlight-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
}
.section--warm {
background: @color-bg-warm;
}
.scenic-banner {
width: 100%;
overflow: hidden;
max-height: 480px;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.scenic-duo {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
max-height: 360px;
overflow: hidden;
.respond-md({
grid-template-columns: 1fr 1fr;
});
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
max-height: 360px;
}
}
</style>
+693
查看文件
@@ -0,0 +1,693 @@
<template>
<div class="page-qualifications">
<LayoutPageHero
:title="seo.h1"
subtitle="三重旅行社资质 · 1000万责任险 · 深耕呼伦贝尔11年"
:breadcrumbs="[{ text: '关于我们', to: '/about' }, { text: '资质与荣誉' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="资质与荣誉摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁集团旗下三家主体(呼伦贝尔呼籁旅行社、呼籁旅游、嘎查旅行社)均持有旅游经营许可证,购买1000万元旅游责任险,持有「呼籁」注册商标和多项著作权。2015年创立至今已深耕呼伦贝尔11年,本页展示完整的资质证书、荣誉认证、企业发展历程和知识产权信息。</p>
</div>
</div>
</section>
<!-- 核心信任声明 -->
<section class="section section-trust-bar">
<div class="container">
<div class="trust-bar reveal">
<div v-for="item in qualifications.safetyGuarantees.slice(0, 4)" :key="item.title" class="trust-bar__item">
<strong class="trust-bar__title">{{ item.title }}</strong>
<p class="trust-bar__detail">{{ item.detail }}</p>
</div>
</div>
</div>
</section>
<!-- 三重旅行社资质 -->
<section class="section section-licenses">
<div class="container">
<CommonSectionTitle
title="三重旅行社资质保障"
subtitle="旗下三家主体均持有旅游经营许可证,选呼籁不是在选黑旅行社"
/>
<div class="licenses-grid reveal">
<div
v-for="(license, index) in qualifications.licenses"
:key="index"
class="license-card"
:class="{ 'license-card--highlight': index === 2 }"
>
<div class="license-card__badge">旅游经营许可证</div>
<h3 class="license-card__holder">{{ license.holder }}</h3>
<div class="license-card__info">
<div class="license-info-row">
<span class="license-info-label">颁发机构</span>
<span class="license-info-value">{{ license.issuer }}</span>
</div>
<div v-if="license.licenseNo && !license.licenseNo.includes('占位符')" class="license-info-row">
<span class="license-info-label">许可证号</span>
<span class="license-info-value license-info-value--no">{{ license.licenseNo }}</span>
</div>
<div class="license-info-row">
<span class="license-info-label">有效期</span>
<span class="license-info-value">{{ license.validPeriod }}</span>
</div>
</div>
<p v-if="license.note" class="license-card__note">{{ license.note }}</p>
</div>
</div>
</div>
</section>
<!-- 旅游责任险 -->
<section class="section section-insurance section-alt">
<div class="container">
<div class="insurance-block reveal">
<div class="insurance-block__icon">🛡️</div>
<div class="insurance-block__content">
<h2 class="insurance-block__title">{{ qualifications.insurance.title }}</h2>
<p class="insurance-block__desc">{{ qualifications.insurance.description }}</p>
<ul class="insurance-block__details">
<li v-if="qualifications.insurance.insurer && !qualifications.insurance.insurer.includes('占位符')">承保方:{{ qualifications.insurance.insurer }}</li>
<li>覆盖范围:{{ qualifications.insurance.policyScope }}</li>
<li>理赔支持:{{ qualifications.insurance.claimContact }}</li>
</ul>
</div>
</div>
</div>
</section>
<!-- 荣誉与认证 -->
<section class="section section-awards">
<div class="container">
<CommonSectionTitle
title="荣誉与认证"
subtitle="行业机构和平台对呼籁服务质量的认可"
/>
<div class="awards-grid reveal">
<div v-for="award in qualifications.awards" :key="award.title" class="award-card">
<div class="award-card__year">{{ award.year }}</div>
<h3 class="award-card__title">{{ award.title }}</h3>
<p class="award-card__detail">{{ award.detail }}</p>
<span class="award-card__issuer">颁发方:{{ award.issuer }}</span>
</div>
</div>
</div>
</section>
<!-- 集团企业矩阵 -->
<section class="section section-matrix section-alt">
<div class="container">
<CommonSectionTitle
:title="`集团${qualifications.enterpriseMatrix.totalCount}家企业矩阵`"
:subtitle="qualifications.enterpriseMatrix.description"
/>
<div class="matrix-grid reveal">
<div
v-for="company in qualifications.enterpriseMatrix.companies"
:key="company.name"
class="matrix-card"
>
<div class="matrix-card__header">
<span class="matrix-card__tag">{{ company.tag }}</span>
<span class="matrix-card__year">{{ company.established }}</span>
</div>
<h3 class="matrix-card__name">{{ company.name }}</h3>
<p class="matrix-card__role">{{ company.role }}</p>
</div>
</div>
</div>
</section>
<!-- 发展历程 -->
<section class="section section-heritage">
<div class="container">
<CommonSectionTitle
:title="`${qualifications.heritage.brandYears}年深耕呼伦贝尔`"
subtitle="从2015年呼籁品牌创立到今天,每一步都走在同一片草原上"
/>
<div class="timeline reveal">
<div
v-for="milestone in qualifications.heritage.milestones"
:key="milestone.year"
class="timeline-item"
>
<div class="timeline-item__year">{{ milestone.year }}</div>
<div class="timeline-item__dot"></div>
<div class="timeline-item__event">{{ milestone.event }}</div>
</div>
</div>
</div>
</section>
<!-- 知识产权 -->
<section class="section section-ip section-alt">
<div class="container">
<CommonSectionTitle
title="知识产权保护"
subtitle="呼籁品牌的原创内容和商标均受法律保护"
/>
<div class="ip-block reveal">
<div class="ip-trademark">
<div class="ip-trademark__badge">商标注册</div>
<h3 class="ip-trademark__name">「{{ qualifications.intellectualProperty.trademark.name }}」</h3>
<p class="ip-trademark__scope">{{ qualifications.intellectualProperty.trademark.scope }}</p>
<p class="ip-trademark__holder">持有方:{{ qualifications.intellectualProperty.trademark.holder }}</p>
<p class="ip-trademark__desc">{{ qualifications.intellectualProperty.trademark.description }}</p>
</div>
<div class="ip-copyrights">
<h3 class="ip-copyrights__title">著作权登记</h3>
<div class="copyright-list">
<div
v-for="cr in qualifications.intellectualProperty.copyrights"
:key="cr.regNo"
class="copyright-item"
>
<span class="copyright-item__name">{{ cr.name }}</span>
<span class="copyright-item__category">{{ cr.category }}</span>
<span class="copyright-item__no">{{ cr.regNo }}</span>
<span class="copyright-item__date">{{ cr.date }}</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 内链 -->
<CommonSectionCTA
title="了解更多,或直接咨询"
description="查看呼籁旅行产品,或联系定制师了解行程详情"
:links="[
{ to: '/products', text: '查看旅行产品' },
{ to: '/contact', text: '联系定制师' },
]"
/>
</div>
</template>
<script setup>
import qualifications from '~/data/qualifications.json'
import brand from '~/data/brand.json'
import contact from '~/data/contact.json'
import { useOrganizationSchema, useBreadcrumbSchema, useQualificationsPageSchema } from '~/composables/useJsonLd'
const seo = useSEO('qualifications')
useScrollReveal()
useOrganizationSchema(brand, contact)
useQualificationsPageSchema(brand, qualifications)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '关于我们', to: '/about' },
{ text: '资质与荣誉' },
])
</script>
<style lang="less" scoped>
// 信任横条
.section-trust-bar {
background: @color-primary;
padding-top: @space-xl;
padding-bottom: @space-xl;
}
.trust-bar {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-lg;
.respond-lg({
grid-template-columns: repeat(4, 1fr);
});
&__item {
text-align: center;
color: #fff;
}
&__title {
display: block;
font-size: @font-size-base;
font-weight: @font-weight-bold;
margin-bottom: @space-xs;
}
&__detail {
font-size: @font-size-sm;
opacity: 0.85;
margin: 0;
line-height: @line-height-base;
}
}
// 旅行社资质
.licenses-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.license-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
position: relative;
&--highlight {
border-color: @color-primary;
background: @color-primary-bg;
&::after {
content: '集团最早实体';
position: absolute;
top: -1px;
right: @space-lg;
background: @color-primary;
color: #fff;
font-size: @font-size-xs;
padding: 3px @space-sm;
border-radius: 0 0 @border-radius-sm @border-radius-sm;
}
}
&__badge {
display: inline-block;
padding: 3px @space-sm;
background: @color-primary;
color: #fff;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
border-radius: @border-radius-sm;
margin-bottom: @space-md;
}
&__holder {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-md;
line-height: @line-height-tight;
}
&__info {
display: flex;
flex-direction: column;
gap: @space-sm;
margin-bottom: @space-md;
}
&__note {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0;
padding-top: @space-sm;
border-top: 1px solid @color-border;
font-style: italic;
}
}
.license-info-row {
display: flex;
gap: @space-sm;
font-size: @font-size-sm;
}
.license-info-label {
color: @color-text-muted;
min-width: 60px;
flex-shrink: 0;
}
.license-info-value {
color: @color-text-secondary;
&--no {
font-family: monospace;
font-size: @font-size-xs;
color: @color-text-muted;
}
}
// 责任险
.section-insurance {
background: @color-bg-gray;
}
.insurance-block {
display: flex;
gap: @space-2xl;
align-items: flex-start;
background: @color-bg-white;
border: 2px solid @color-primary;
border-radius: @border-radius-lg;
padding: @space-2xl;
&__icon {
font-size: 48px;
flex-shrink: 0;
line-height: 1;
}
&__content {
flex: 1;
}
&__title {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
}
&__desc {
font-size: @font-size-base;
color: @color-text-secondary;
margin: 0 0 @space-md;
}
&__details {
margin: 0;
padding-left: @space-lg;
display: flex;
flex-direction: column;
gap: @space-xs;
li {
font-size: @font-size-sm;
color: @color-text-secondary;
}
}
}
// 荣誉
.awards-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.award-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
text-align: center;
&__year {
display: inline-block;
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-primary;
line-height: 1;
margin-bottom: @space-md;
}
&__title {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
line-height: @line-height-tight;
}
&__detail {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0 0 @space-md;
}
&__issuer {
font-size: @font-size-xs;
color: @color-text-muted;
}
}
// 企业矩阵
.section-matrix {
background: @color-bg-gray;
}
.matrix-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.matrix-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-md;
padding: @space-lg;
transition: box-shadow @transition-fast;
&:hover {
box-shadow: @shadow-sm;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: @space-sm;
}
&__tag {
font-size: @font-size-xs;
padding: 2px @space-sm;
background: @color-primary-bg;
color: @color-primary;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
}
&__year {
font-size: @font-size-xs;
color: @color-text-muted;
}
&__name {
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
line-height: @line-height-tight;
}
&__role {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0;
line-height: @line-height-base;
}
}
// 发展历程
.timeline {
position: relative;
max-width: 720px;
margin: 0 auto;
&::before {
content: '';
position: absolute;
left: 80px;
top: 0;
bottom: 0;
width: 2px;
background: @color-primary-bg;
.respond-lg({
left: 90px;
});
}
}
.timeline-item {
display: grid;
grid-template-columns: 80px 20px 1fr;
gap: 0 @space-md;
align-items: flex-start;
margin-bottom: @space-xl;
padding-bottom: @space-xl;
border-bottom: 1px solid @color-border;
.respond-lg({
grid-template-columns: 90px 20px 1fr;
});
&:last-child {
margin-bottom: 0;
padding-bottom: 0;
border-bottom: none;
}
&__year {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-primary;
text-align: right;
padding-top: 2px;
line-height: 1;
}
&__dot {
width: 12px;
height: 12px;
background: @color-primary;
border-radius: 50%;
margin-top: 4px;
position: relative;
z-index: 1;
flex-shrink: 0;
justify-self: center;
}
&__event {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-base;
}
}
// 知识产权
.section-ip {
background: @color-bg-gray;
}
.ip-block {
display: grid;
grid-template-columns: 1fr;
gap: @space-xl;
.respond-lg({
grid-template-columns: 1fr 1.5fr;
align-items: start;
});
}
.ip-trademark {
background: @color-bg-white;
border: 2px solid @color-primary-bg;
border-radius: @border-radius-lg;
padding: @space-xl;
text-align: center;
&__badge {
display: inline-block;
padding: 3px @space-md;
background: @color-primary;
color: #fff;
font-size: @font-size-xs;
border-radius: @border-radius-full;
margin-bottom: @space-md;
}
&__name {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-primary;
margin: 0 0 @space-xs;
}
&__scope {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0 0 @space-sm;
font-weight: @font-weight-medium;
}
&__holder {
font-size: @font-size-xs;
color: @color-text-muted;
margin: 0 0 @space-md;
}
&__desc {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0;
line-height: @line-height-base;
text-align: left;
}
}
.ip-copyrights {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-xl;
&__title {
font-size: @font-size-md;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-lg;
}
}
.copyright-list {
display: flex;
flex-direction: column;
gap: @space-sm;
}
.copyright-item {
display: grid;
grid-template-columns: auto auto 1fr auto;
gap: @space-sm;
align-items: center;
padding: @space-md;
background: @color-bg-gray;
border-radius: @border-radius-md;
font-size: @font-size-sm;
@media (max-width: 768px) {
grid-template-columns: 1fr 1fr;
grid-template-rows: auto auto;
}
&__name {
font-weight: @font-weight-bold;
color: @color-text-primary;
}
&__category {
color: @color-text-muted;
font-size: @font-size-xs;
padding: 2px @space-sm;
background: @color-primary-bg;
color: @color-primary;
border-radius: @border-radius-full;
}
&__no {
font-family: monospace;
font-size: @font-size-xs;
color: @color-text-muted;
.text-clamp(1);
}
&__date {
font-size: @font-size-xs;
color: @color-text-muted;
white-space: nowrap;
}
}
</style>
+232
查看文件
@@ -0,0 +1,232 @@
<template>
<div class="page-reviews">
<LayoutPageHero
:title="seo.h1"
subtitle="来自真实家庭的旅行评价"
:breadcrumbs="[{ text: '客户评价' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="客户评价摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行已服务超10000个家庭,本页收录来自真实客户的旅行评价,均为未经修改的原始反馈。可按出行场景(亲子、摄影、蜜月等)和关注维度(安全性、性价比、服务质量等)筛选,找到与自己情况最相似的参考评价。</p>
</div>
</div>
</section>
<!-- 数据汇总 -->
<ReviewsReviewSummary :summary="reviews.summary" class="reveal" />
<!-- 筛选+列表 -->
<section class="section">
<div class="container">
<!-- sticky 筛选栏:滚动时收起为紧凑模式 -->
<div class="filter-sticky" :class="{ 'is-compact': isCompact && !filterExpanded }">
<!-- 紧凑模式:一行摘要 -->
<div v-if="isCompact && !filterExpanded" class="filter-compact" @click="filterExpanded = true">
<div class="filter-compact-info">
<span class="filter-compact-label">筛选</span>
<span v-if="activeScene" class="filter-compact-tag">{{ activeScene }}</span>
<span v-if="activeConcern" class="filter-compact-tag">{{ activeConcern }}</span>
<span v-if="!activeScene && !activeConcern" class="filter-compact-hint">全部评价</span>
<span class="filter-compact-count">{{ filteredReviews.length }} 条</span>
</div>
<span class="filter-compact-toggle">展开筛选 &#9662;</span>
</div>
<!-- 完整模式 -->
<div v-else>
<ReviewsReviewFilter
:scenes="allScenes"
:concerns="allConcerns"
:active-scene="activeScene"
:active-concern="activeConcern"
@filter-scene="activeScene = $event"
@filter-concern="activeConcern = $event"
/>
<div class="filter-bottom">
<span class="filter-count">
<template v-if="activeScene || activeConcern">
筛选结果:<strong>{{ filteredReviews.length }}</strong> 条评价
</template>
<template v-else>
共 <strong>{{ reviews.items.length }}</strong> 条评价
</template>
</span>
<button v-if="isCompact" class="filter-collapse-btn" @click="filterExpanded = false">
收起 &#9652;
</button>
</div>
</div>
</div>
<ReviewsReviewList :reviews="filteredReviews" @reset="activeScene = ''; activeConcern = ''" />
</div>
</section>
<!-- 内链 -->
<CommonSectionCTA
title="准备好了吗?"
description="联系定制师,开始规划您的呼伦贝尔之旅"
:links="[{ to: '/contact', text: '联系定制师,获取专属行程方案' }]"
/>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import reviews from '~/data/reviews.json'
import { useBreadcrumbSchema, useAggregateRatingSchema } from '~/composables/useJsonLd'
const seo = useSEO('reviews')
useScrollReveal()
useAggregateRatingSchema(reviews.summary, reviews.items)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '客户评价' },
])
const activeScene = ref('')
const activeConcern = ref('')
const filterExpanded = ref(false)
const isCompact = ref(false)
const allScenes = [...new Set(reviews.items.flatMap(r => r.scenes))]
const allConcerns = [...new Set(reviews.items.flatMap(r => r.concerns))]
const filteredReviews = computed(() => {
return reviews.items.filter(r => {
if (activeScene.value && !r.scenes.includes(activeScene.value)) return false
if (activeConcern.value && !r.concerns.includes(activeConcern.value)) return false
return true
})
})
// 监听滚动:向下滚过 200px 后进入紧凑模式
onMounted(() => {
const onScroll = () => {
isCompact.value = window.scrollY > 200
// 切回紧凑时自动收起
if (!isCompact.value) {
filterExpanded.value = false
}
}
window.addEventListener('scroll', onScroll, { passive: true })
onUnmounted(() => window.removeEventListener('scroll', onScroll))
})
</script>
<style lang="less" scoped>
.filter-sticky {
position: sticky;
top: 64px;
z-index: 10;
background: @color-bg-white;
padding-top: @space-sm;
padding-bottom: @space-sm;
margin-bottom: @space-lg;
transition: box-shadow @transition-fast;
.respond-lg({
top: 72px;
});
&.is-compact {
box-shadow: @shadow-sm;
border-bottom: 1px solid @color-border;
}
}
// 紧凑模式:单行
.filter-compact {
display: flex;
align-items: center;
justify-content: space-between;
padding: @space-sm @space-md;
background: @color-bg-gray;
border-radius: @border-radius-md;
cursor: pointer;
min-height: 44px;
transition: background @transition-fast;
&:hover {
background: @color-primary-bg;
}
}
.filter-compact-info {
display: flex;
align-items: center;
gap: @space-sm;
flex-wrap: wrap;
flex: 1;
}
.filter-compact-label {
font-size: @font-size-sm;
font-weight: @font-weight-medium;
color: @color-text-primary;
}
.filter-compact-tag {
padding: @space-xs @space-sm;
font-size: @font-size-xs;
color: #fff;
background: @color-primary;
border-radius: @border-radius-full;
}
.filter-compact-hint {
font-size: @font-size-sm;
color: @color-text-muted;
}
.filter-compact-count {
font-size: @font-size-sm;
color: @color-primary;
font-weight: @font-weight-medium;
}
.filter-compact-toggle {
font-size: @font-size-xs;
color: @color-primary;
white-space: nowrap;
margin-left: @space-md;
}
// 完整模式底部
.filter-bottom {
display: flex;
align-items: center;
justify-content: center;
gap: @space-lg;
padding-top: @space-sm;
}
.filter-count {
font-size: @font-size-sm;
color: @color-text-muted;
strong {
color: @color-primary;
font-weight: @font-weight-bold;
}
}
.filter-collapse-btn {
padding: @space-xs @space-md;
font-size: @font-size-xs;
color: @color-primary;
background: none;
border: 1px solid @color-primary;
border-radius: @border-radius-full;
cursor: pointer;
transition: all @transition-fast;
&:hover {
background: @color-primary;
color: #fff;
}
}
</style>
+816
查看文件
@@ -0,0 +1,816 @@
<template>
<div class="page-selector">
<LayoutPageHero
:title="seo.h1"
subtitle="5个问题 · 1分钟 · 找到最适合你的版本"
:breadcrumbs="[{ text: '产品线', to: '/products' }, { text: '选版工具' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="选版工具摘要">
<div class="container">
<div class="page-tldr-inner">
<p>不知道额吉的故乡哪个版本适合你?回答5个问题(出行天数偏好、孩子年龄、主要诉求、预算范围、季节),系统根据评分算法自动匹配最适合你家庭的版本,并给出具体的匹配理由。工具免费使用,结果仅供参考,亦可联系定制师进行1对1选版。</p>
</div>
</div>
</section>
<section class="selector-section">
<div class="container">
<div class="selector-wrap">
<!-- 问答流程 -->
<template v-if="!showResult">
<!-- 进度条 -->
<div class="progress-bar-wrap">
<div class="progress-bar-track">
<div class="progress-bar-fill" :style="{ width: progressPercent + '%' }" />
</div>
<span class="progress-label">{{ currentStepIndex + 1 }} / {{ visibleQuestions.length }}</span>
</div>
<!-- 问题卡片 -->
<Transition name="step" mode="out-in">
<div :key="currentQuestion.id" class="question-card">
<p class="question-hint">{{ currentQuestion.hint }}</p>
<h2 class="question-text">{{ currentQuestion.text }}</h2>
<div class="options-grid" :class="{ 'options-grid--3': currentQuestion.options.length === 3 }">
<button
v-for="opt in currentQuestion.options"
:key="opt.value"
class="option-btn"
:class="{ 'option-btn--selected': answers[currentQuestion.id] === opt.value }"
@click="selectAnswer(currentQuestion.id, opt.value)"
>
<span class="option-label">{{ opt.label }}</span>
<span class="option-desc">{{ opt.desc }}</span>
</button>
</div>
<!-- 导航按钮(非第一步才显示返回) -->
<div class="step-nav">
<button v-if="currentStepIndex > 0" class="btn-back" @click="goBack">
← 上一步
</button>
<span v-else />
<button
v-if="answers[currentQuestion.id]"
class="btn-next"
@click="goNext"
>
{{ isLastQuestion ? '查看推荐' : '下一步 →' }}
</button>
</div>
</div>
</Transition>
</template>
<!-- 推荐结果 -->
<Transition name="step" mode="out-in">
<div v-if="showResult" key="result" class="result-wrap">
<div class="result-header">
<p class="result-eyebrow">根据你的回答,我们推荐</p>
<div class="result-tag">{{ recommended.tag }}</div>
<h2 class="result-name">{{ recommended.name }}</h2>
<p class="result-audience">{{ recommended.audience }}</p>
</div>
<p class="result-desc">{{ recommended.description }}</p>
<!-- 推荐理由 -->
<div class="match-reasons">
<h3 class="match-reasons-title">为什么推荐这个版本</h3>
<ul class="match-reasons-list">
<li v-for="(reason, i) in matchReasons" :key="i" class="match-reason-item">
<span class="match-reason-icon">✓</span>
<span>{{ reason }}</span>
</li>
</ul>
</div>
<!-- 产品亮点 -->
<div class="result-highlights">
<h3 class="result-highlights-title">行程亮点</h3>
<ul class="highlights-list">
<li v-for="(h, i) in recommended.highlights" :key="i" class="highlight-item">
<span class="highlight-dot" />
<span>{{ h }}</span>
</li>
</ul>
</div>
<!-- 操作区 -->
<div class="result-actions">
<NuxtLink :to="`/products/${recommended.id}`" class="btn-primary">
查看详细行程
</NuxtLink>
<NuxtLink to="/customize" class="btn-cta">
在线定制这条行程
</NuxtLink>
</div>
<!-- 换一个 -->
<div class="alternate-wrap">
<p class="alternate-label">不太满意?</p>
<button class="btn-alternate" @click="showAlternate">
换一个版本看看 →
</button>
<button class="btn-restart" @click="restart">
重新回答
</button>
</div>
<!-- 备选结果展示 -->
<Transition name="fade">
<div v-if="alternateIndex > 0 && rankedProducts[alternateIndex]" class="alternate-result">
<p class="alternate-eyebrow">备选推荐</p>
<div class="alternate-tag">{{ rankedProducts[alternateIndex].tag }}</div>
<h3 class="alternate-name">{{ rankedProducts[alternateIndex].name }}</h3>
<p class="alternate-desc">{{ rankedProducts[alternateIndex].description }}</p>
<div class="alternate-actions">
<NuxtLink :to="`/products/${rankedProducts[alternateIndex].id}`" class="btn-outline">
查看这个版本
</NuxtLink>
</div>
</div>
</Transition>
<!-- 全系列入口 -->
<div class="all-versions">
<p>想自己对比所有版本?</p>
<NuxtLink to="/products" class="btn-link">查看全系列6个版本 →</NuxtLink>
</div>
</div>
</Transition>
</div>
</div>
</section>
<!-- 底部CTA -->
<CommonSectionCTA
title="还有疑问?直接问定制师"
description="告诉我们你的家庭情况,定制师为你1对1选版本、排行程"
:links="[{ to: '/customize', text: '在线定制' }, { to: '/contact', text: '联系定制师' }]"
/>
</div>
</template>
<script setup>
import selectorData from '~/data/selector.json'
import productsData from '~/data/products.json'
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const seo = useSEO('selector')
useScrollReveal()
// JSON-LD: Quiz + ItemList schema
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'Quiz',
name: '呼伦贝尔行程版本选择工具',
description: '5个问题帮你找到最适合的呼伦贝尔行程版本。根据出行天数、人群、孩子年龄、预算和偏好,从额吉的故乡V9系列6个版本中精准推荐。',
url: 'https://1814.love/selector',
about: {
'@type': 'TouristTrip',
name: '呼伦贝尔家庭定制游',
touristType: '家庭亲子游客',
},
numberOfQuestions: 5,
provider: {
'@type': 'TravelAgency',
name: '呼籁旅行',
url: 'https://1814.love',
},
}),
},
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'ItemList',
name: '额吉的故乡V9系列行程版本',
description: '呼籁旅行呼伦贝尔亲子游6个版本,5天4晚至7天6晚,覆盖森林版、草原版、亲子版、探索版、旷野版',
url: 'https://1814.love/selector',
numberOfItems: productsData.versions.length,
itemListElement: productsData.versions.map((v, i) => ({
'@type': 'ListItem',
position: i + 1,
name: v.name,
description: v.audience,
url: `https://1814.love/products/${v.id}`,
})),
}),
},
],
})
useBreadcrumbSchema([
{ name: '首页', url: 'https://1814.love/' },
{ name: '产品线', url: 'https://1814.love/products' },
{ name: '帮我选行程', url: 'https://1814.love/selector' },
])
const { questions, scoring, matchReasons: reasonsMap } = selectorData
const allProducts = productsData.versions
// 状态
const answers = ref({})
const currentStepIndex = ref(0)
const showResult = ref(false)
const alternateIndex = ref(0)
// 过滤出当前应显示的问题(根据条件)
const visibleQuestions = computed(() => {
return questions.filter((q) => {
if (!q.condition) return true
return answers.value[q.condition.key] === q.condition.value
})
})
const currentQuestion = computed(() => visibleQuestions.value[currentStepIndex.value])
const isLastQuestion = computed(() => currentStepIndex.value === visibleQuestions.value.length - 1)
const progressPercent = computed(() =>
Math.round(((currentStepIndex.value) / visibleQuestions.value.length) * 100)
)
// 计分逻辑
const scores = computed(() => {
const totals = {}
for (const [questionId, questionScores] of Object.entries(scoring)) {
const answer = answers.value[questionId]
if (!answer) continue
const productScores = questionScores[answer] || {}
for (const [productId, score] of Object.entries(productScores)) {
totals[productId] = (totals[productId] || 0) + score
}
}
return totals
})
// 按得分排序
const rankedProducts = computed(() => {
return [...allProducts].sort(
(a, b) => (scores.value[b.id] || 0) - (scores.value[a.id] || 0)
)
})
const recommended = computed(() => rankedProducts.value[0])
// 生成匹配原因
const matchReasons = computed(() => {
if (!recommended.value) return []
const pid = recommended.value.id
const reasonMap = reasonsMap[pid] || {}
const result = []
// 找匹配答案的原因
for (const [qid, answer] of Object.entries(answers.value)) {
const key = `${qid}:${answer}`
if (reasonMap[key] && result.length < 3) {
result.push(reasonMap[key])
}
}
// 补充默认原因
if (result.length === 0 && reasonMap.default) {
result.push(reasonMap.default)
}
return result.slice(0, 3)
})
// 操作方法
function selectAnswer(questionId, value) {
answers.value = { ...answers.value, [questionId]: value }
// 移动端自动前进
if (window.innerWidth < 768) {
setTimeout(() => goNext(), 200)
}
}
function goNext() {
if (isLastQuestion.value) {
showResult.value = true
} else {
currentStepIndex.value++
}
}
function goBack() {
if (currentStepIndex.value > 0) {
currentStepIndex.value--
}
}
function showAlternate() {
if (alternateIndex.value < rankedProducts.value.length - 1) {
alternateIndex.value++
}
}
function restart() {
answers.value = {}
currentStepIndex.value = 0
showResult.value = false
alternateIndex.value = 0
}
</script>
<style lang="less" scoped>
.selector-section {
padding: @space-3xl 0;
background: @color-bg-warm;
}
.selector-wrap {
max-width: 680px;
margin: 0 auto;
}
// 进度条
.progress-bar-wrap {
display: flex;
align-items: center;
gap: @space-md;
margin-bottom: @space-2xl;
}
.progress-bar-track {
flex: 1;
height: 4px;
background: @color-border;
border-radius: 2px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: @color-primary;
border-radius: 2px;
transition: width 0.4s ease;
}
.progress-label {
font-size: @font-size-sm;
color: @color-text-muted;
white-space: nowrap;
}
// 问题卡片
.question-card {
background: @color-bg-white;
border-radius: 16px;
padding: @space-2xl;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.06);
.respond-md({
padding: @space-3xl;
});
}
.question-hint {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0 0 @space-sm;
}
.question-text {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-xl;
line-height: @line-height-tight;
.respond-md({
font-size: @font-size-2xl;
});
}
// 选项网格
.options-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-md;
margin-bottom: @space-xl;
&--3 {
grid-template-columns: repeat(3, 1fr);
}
}
.option-btn {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: @space-xs;
padding: @space-md @space-lg;
background: @color-bg-gray;
border: 2px solid @color-border;
border-radius: 12px;
cursor: pointer;
transition: all @transition-base;
text-align: left;
width: 100%;
&:hover {
border-color: @color-primary-light;
background: @color-primary-lighter;
}
&--selected {
border-color: @color-primary;
background: @color-primary-lighter;
.option-label {
color: @color-primary;
}
}
}
.option-label {
font-size: @font-size-base;
font-weight: @font-weight-medium;
color: @color-text-primary;
transition: color @transition-base;
}
.option-desc {
font-size: @font-size-xs;
color: @color-text-muted;
line-height: @line-height-base;
}
// 步骤导航
.step-nav {
display: flex;
justify-content: space-between;
align-items: center;
}
.btn-back {
font-size: @font-size-sm;
color: @color-text-muted;
background: none;
border: none;
cursor: pointer;
padding: @space-sm 0;
transition: color @transition-base;
&:hover {
color: @color-text-primary;
}
}
.btn-next {
background: @color-primary;
color: white;
border: none;
border-radius: 8px;
padding: @space-sm @space-xl;
font-size: @font-size-base;
font-weight: @font-weight-medium;
cursor: pointer;
transition: background @transition-base;
&:hover {
background: @color-primary-light;
}
}
// 结果区域
.result-wrap {
background: @color-bg-white;
border-radius: 16px;
padding: @space-2xl;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.06);
.respond-md({
padding: @space-3xl;
});
}
.result-header {
margin-bottom: @space-xl;
}
.result-eyebrow {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0 0 @space-sm;
}
.result-tag {
display: inline-block;
background: @color-primary;
color: white;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
padding: 3px 10px;
border-radius: 20px;
margin-bottom: @space-sm;
}
.result-name {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-xs;
line-height: @line-height-tight;
.respond-md({
font-size: @font-size-3xl;
});
}
.result-audience {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0;
}
.result-desc {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin: 0 0 @space-xl;
padding-bottom: @space-xl;
border-bottom: 1px solid @color-border;
}
// 匹配原因
.match-reasons {
margin-bottom: @space-xl;
}
.match-reasons-title,
.result-highlights-title {
font-size: @font-size-base;
font-weight: @font-weight-medium;
color: @color-text-primary;
margin: 0 0 @space-md;
}
.match-reasons-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: @space-sm;
}
.match-reason-item {
display: flex;
gap: @space-sm;
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
}
.match-reason-icon {
color: @color-primary;
font-weight: @font-weight-bold;
flex-shrink: 0;
}
// 亮点
.result-highlights {
margin-bottom: @space-xl;
padding-bottom: @space-xl;
border-bottom: 1px solid @color-border;
}
.highlights-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: @space-sm;
}
.highlight-item {
display: flex;
gap: @space-sm;
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
}
.highlight-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: @color-accent;
flex-shrink: 0;
margin-top: 6px;
}
// 操作按钮
.result-actions {
display: flex;
flex-direction: column;
gap: @space-md;
margin-bottom: @space-xl;
.respond-md({
flex-direction: row;
});
}
.btn-primary {
display: inline-block;
background: @color-bg-white;
color: @color-primary;
border: 2px solid @color-primary;
border-radius: 8px;
padding: @space-md @space-xl;
font-size: @font-size-base;
font-weight: @font-weight-medium;
text-decoration: none;
text-align: center;
transition: all @transition-base;
&:hover {
background: @color-primary-lighter;
}
}
.btn-cta {
display: inline-block;
background: @color-primary;
color: white;
border-radius: 8px;
padding: @space-md @space-xl;
font-size: @font-size-base;
font-weight: @font-weight-medium;
text-decoration: none;
text-align: center;
transition: background @transition-base;
&:hover {
background: @color-primary-light;
}
}
// 换一个
.alternate-wrap {
display: flex;
align-items: center;
gap: @space-md;
flex-wrap: wrap;
margin-bottom: @space-xl;
}
.alternate-label {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0;
}
.btn-alternate {
font-size: @font-size-sm;
color: @color-primary;
background: none;
border: none;
cursor: pointer;
padding: 0;
text-decoration: underline;
transition: color @transition-base;
&:hover {
color: @color-primary-light;
}
}
.btn-restart {
font-size: @font-size-sm;
color: @color-text-muted;
background: none;
border: none;
cursor: pointer;
padding: 0;
transition: color @transition-base;
&:hover {
color: @color-text-secondary;
}
}
// 备选结果
.alternate-result {
background: @color-bg-gray;
border-radius: 12px;
padding: @space-lg;
margin-bottom: @space-xl;
}
.alternate-eyebrow {
font-size: @font-size-xs;
color: @color-text-muted;
margin: 0 0 @space-xs;
}
.alternate-tag {
display: inline-block;
background: @color-accent;
color: white;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
padding: 2px 8px;
border-radius: 20px;
margin-bottom: @space-xs;
}
.alternate-name {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-sm;
}
.alternate-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0 0 @space-md;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.alternate-actions {
display: flex;
}
.btn-outline {
display: inline-block;
background: transparent;
color: @color-primary;
border: 1px solid @color-primary;
border-radius: 6px;
padding: @space-sm @space-lg;
font-size: @font-size-sm;
text-decoration: none;
transition: all @transition-base;
&:hover {
background: @color-primary-lighter;
}
}
// 全系列入口
.all-versions {
text-align: center;
padding-top: @space-lg;
border-top: 1px solid @color-border;
p {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0 0 @space-xs;
}
}
.btn-link {
font-size: @font-size-sm;
color: @color-primary;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
// 过渡动画
.step-enter-active,
.step-leave-active {
transition: opacity 0.25s ease, transform 0.25s ease;
}
.step-enter-from {
opacity: 0;
transform: translateY(16px);
}
.step-leave-to {
opacity: 0;
transform: translateY(-16px);
}
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
+624
查看文件
@@ -0,0 +1,624 @@
<template>
<div v-if="story" class="page-story-detail">
<LayoutPageHero
:title="story.title"
:subtitle="story.subtitle"
:breadcrumbs="[{ text: '客户故事', to: '/stories' }, { text: story.familyType }]"
/>
<section class="section">
<div class="container">
<div class="story-layout">
<!-- 主内容 -->
<main class="story-main">
<!-- 故事元信息 -->
<div class="story-meta">
<span class="story-meta__family">{{ story.familyType }}</span>
<span v-if="story.childrenAge && !story.childrenAge.includes('⚠️')" class="story-meta__children">孩子 {{ story.childrenAge }}</span>
<span v-if="story.fromCity && story.fromCity !== '⚠️待填'" class="story-meta__city">来自 {{ story.fromCity }}</span>
<span class="story-meta__date">{{ story.travelDate }}</span>
<span v-if="story.tripProduct" class="story-meta__product">{{ story.tripProduct }}</span>
</div>
<!-- 小红书来源信息 -->
<div v-if="story.author" class="story-source">
<span class="story-source__author">{{ story.author }}</span>
<span v-if="story.authorPlatform" class="story-source__platform">{{ story.authorPlatform }}</span>
<span v-if="story.likes" class="story-source__likes">{{ story.likes }} 赞</span>
</div>
<!-- 配图建议 -->
<div v-if="story.imageHints" class="story-image-placeholder">
<div class="story-image-placeholder__inner">
<span class="story-image-placeholder__icon">📷</span>
<p class="story-image-placeholder__hint">{{ story.imageHints }}</p>
</div>
</div>
<!-- 故事摘要 -->
<div class="story-summary">
<p>{{ story.summary }}</p>
</div>
<!-- 小红书搜索提示(直链需要token,引导用户搜索) -->
<div v-if="story.authorPlatform === '小红书' && story.title" class="story-source-link">
<span class="story-source-link__tip">📱 在小红书搜索「{{ story.title }}」可查看原帖</span>
</div>
<!-- 故事章节 -->
<div v-if="story.sections && story.sections.length" class="story-sections">
<div
v-for="(section, index) in story.sections"
:key="index"
class="story-section"
>
<h2 class="story-section__heading">{{ section.heading }}</h2>
<p class="story-section__content">{{ section.content }}</p>
</div>
</div>
<!-- 印象深刻的瞬间 -->
<div v-if="story.keyMoments && story.keyMoments.length" class="story-moments">
<h2 class="story-moments__title">印象深刻的瞬间</h2>
<div class="story-moments__list">
<div
v-for="(moment, index) in story.keyMoments"
:key="index"
class="moment-item"
>
<span class="moment-item__icon">✨</span>
<p class="moment-item__text">{{ moment }}</p>
</div>
</div>
</div>
<!-- 来源声明 -->
<div v-if="story.disclaimer" class="story-disclaimer">
<p>{{ story.disclaimer }}</p>
</div>
<!-- 标签 -->
<div class="story-tags">
<span class="story-tags__label">标签:</span>
<span v-for="tag in story.tags" :key="tag" class="story-tag">{{ tag }}</span>
</div>
<!-- 前后导航 -->
<div class="story-nav">
<NuxtLink
v-if="prevStory"
:to="`/stories/${prevStory.id}`"
class="story-nav__item story-nav__item--prev"
>
<span class="story-nav__label">上一个故事</span>
<span class="story-nav__title">{{ prevStory.title }}</span>
</NuxtLink>
<NuxtLink
v-if="nextStory"
:to="`/stories/${nextStory.id}`"
class="story-nav__item story-nav__item--next"
>
<span class="story-nav__label">下一个故事</span>
<span class="story-nav__title">{{ nextStory.title }}</span>
</NuxtLink>
</div>
</main>
<!-- 侧边栏 -->
<aside class="story-sidebar">
<!-- 咨询 CTA -->
<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-stories">
<li
v-for="s in detailStories"
:key="s.id"
class="sidebar-story"
:class="{ 'sidebar-story--active': s.id === story.id }"
>
<NuxtLink :to="`/stories/${s.id}`" class="sidebar-story__link">
<span class="sidebar-story__family">{{ s.familyType }}</span>
<span class="sidebar-story__title">{{ s.title }}</span>
</NuxtLink>
</li>
</ul>
</div>
<!-- 快速链接 -->
<div class="sidebar-card">
<h3 class="sidebar-card__title">相关内容</h3>
<div class="sidebar-links">
<CommonInternalLink to="/reviews" text="查看更多客户评价" />
<CommonInternalLink to="/products" text="旅行产品详情" />
<CommonInternalLink to="/faq" text="常见问答" />
<CommonInternalLink to="/about" text="关于呼籁" />
</div>
</div>
</aside>
</div>
</div>
</section>
</div>
<!-- 404 -->
<div v-else class="page-not-found">
<div class="container">
<h1>故事不存在</h1>
<NuxtLink to="/stories">返回客户故事列表</NuxtLink>
</div>
</div>
</template>
<script setup>
import storiesData from '~/data/stories.json'
import { useBreadcrumbSchema, useCustomerStorySchema } from '~/composables/useJsonLd'
const route = useRoute()
const allStories = storiesData.stories
// 只有原创故事(有 content 字段)才有详情页
const detailStories = allStories.filter(s => s.content)
const story = allStories.find(s => s.id === route.params.id)
if (!story) {
throw createError({ statusCode: 404, statusMessage: '故事不存在' })
}
// SEO
useHead({
title: story.seo?.title || story.title,
meta: [
{ name: 'description', content: story.seo?.description || story.summary },
{ name: 'keywords', content: story.seo?.keywords || (story.tags || []).join(',') },
{ name: 'author', content: story.authorName || '呼籁旅行' },
{ name: 'article:published_time', content: story.travelDate + '-01T00:00:00+08:00' },
{ name: 'geo.region', content: 'CN-NM' },
{ name: 'geo.placename', content: '呼伦贝尔' },
{ property: 'og:title', content: story.seo?.title || story.title },
{ property: 'og:description', content: story.seo?.description || story.summary },
{ property: 'og:type', content: 'article' },
{ property: 'og:url', content: `https://1814.love/stories/${story.id}` },
{ property: 'og:site_name', content: '呼籁旅行' },
{ property: 'og:locale', content: 'zh_CN' },
],
link: [
{ rel: 'canonical', href: `https://1814.love/stories/${story.id}` },
],
})
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '客户故事', to: '/stories' },
{ text: story.familyType },
{ text: story.title },
])
useCustomerStorySchema(story)
// 上下篇(仅在有详情页的故事之间切换)
const currentIndex = detailStories.findIndex(s => s.id === story.id)
const prevStory = currentIndex > 0 ? detailStories[currentIndex - 1] : null
const nextStory = currentIndex < detailStories.length - 1 ? detailStories[currentIndex + 1] : null
</script>
<style lang="less" scoped>
.story-layout {
display: grid;
grid-template-columns: 1fr;
gap: @space-2xl;
.respond-lg({
grid-template-columns: 1fr 300px;
align-items: start;
});
}
.story-main {
min-width: 0;
}
// 元信息
.story-meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: @space-sm;
margin-bottom: @space-lg;
font-size: @font-size-sm;
}
.story-meta__family {
padding: 3px @space-sm;
background: @color-primary;
color: #fff;
border-radius: @border-radius-sm;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
}
.story-meta__children,
.story-meta__city,
.story-meta__date {
color: @color-text-muted;
}
.story-meta__product {
color: @color-primary;
font-weight: @font-weight-medium;
font-size: @font-size-xs;
padding: 2px @space-sm;
background: @color-primary-bg;
border-radius: @border-radius-full;
}
// 占位提示
.story-placeholder-notice {
background: #fef9e7;
border: 1px solid #f39c12;
border-radius: @border-radius-md;
padding: @space-md @space-lg;
font-size: @font-size-sm;
color: #7d6608;
margin-bottom: @space-xl;
}
// 配图占位
.story-image-placeholder {
margin-bottom: @space-xl;
border-radius: @border-radius-lg;
overflow: hidden;
aspect-ratio: 16 / 7;
background: @color-primary-bg;
border: 2px dashed @color-primary-lighter;
&__inner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
padding: @space-xl;
gap: @space-md;
}
&__icon {
font-size: 36px;
}
&__hint {
font-size: @font-size-sm;
color: @color-text-muted;
text-align: center;
line-height: @line-height-base;
margin: 0;
}
}
// 摘要
.story-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;
}
}
// 故事章节
.story-sections {
display: flex;
flex-direction: column;
gap: @space-xl;
margin-bottom: @space-2xl;
}
.story-section {
&__heading {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-md;
padding-bottom: @space-sm;
border-bottom: 2px solid @color-primary-bg;
}
&__content {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
}
// 印象瞬间
.story-moments {
background: @color-bg-gray;
border-radius: @border-radius-lg;
padding: @space-xl;
margin-bottom: @space-2xl;
&__title {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-lg;
}
&__list {
display: flex;
flex-direction: column;
gap: @space-md;
}
}
.moment-item {
display: flex;
gap: @space-md;
align-items: flex-start;
&__icon {
font-size: 18px;
flex-shrink: 0;
padding-top: 2px;
}
&__text {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
}
// 标签
.story-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;
margin-bottom: @space-xl;
}
.story-tags__label {
font-size: @font-size-sm;
color: @color-text-muted;
}
.story-tag {
padding: 3px @space-sm;
background: @color-primary-bg;
color: @color-primary;
border-radius: @border-radius-sm;
font-size: @font-size-xs;
}
// 前后导航
.story-nav {
display: grid;
grid-template-columns: 1fr 1fr;
gap: @space-md;
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);
}
}
// 侧边栏
.story-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-stories {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 2px;
}
.sidebar-story {
&--active .sidebar-story__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;
}
}
&__family {
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;
}
// 小红书来源信息
.story-source {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: @space-sm;
margin-bottom: @space-lg;
&__author {
font-size: @font-size-sm;
font-weight: @font-weight-medium;
color: @color-text-secondary;
}
&__platform {
font-size: @font-size-xs;
padding: 2px @space-sm;
background: #ff2442;
color: #fff;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
}
&__likes {
font-size: @font-size-xs;
color: @color-text-muted;
&::before {
content: '❤️ ';
}
}
}
// 查看原文链接
.story-source-link {
margin-bottom: @space-xl;
&__tip {
font-size: @font-size-sm;
color: @color-text-secondary;
background: @color-bg-gray;
padding: @space-xs @space-sm;
border-radius: @border-radius-sm;
display: inline-block;
}
&__a {
font-size: @font-size-sm;
color: #ff2442;
text-decoration: none;
font-weight: @font-weight-medium;
transition: opacity @transition-fast;
&:hover {
opacity: 0.8;
text-decoration: underline;
}
}
}
// 来源声明
.story-disclaimer {
margin-top: @space-xl;
padding: @space-md @space-lg;
background: @color-bg-gray;
border-radius: @border-radius-md;
border-left: 3px solid @color-border;
p {
margin: 0;
font-size: @font-size-xs;
color: @color-text-muted;
line-height: @line-height-base;
}
}
// 404
.page-not-found {
text-align: center;
padding: @space-3xl 0;
h1 {
margin-bottom: @space-lg;
}
a {
color: @color-primary;
}
}
</style>
+265
查看文件
@@ -0,0 +1,265 @@
<template>
<div class="page-stories">
<LayoutPageHero
:title="seo.h1"
subtitle="来自小红书的真实旅行分享"
:breadcrumbs="[{ text: '客户故事' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="客户故事摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行客户在小红书上的真实旅行分享,记录草原亲子游、家庭旅拍、露营体验等真实出行感受,点击可查看原文。</p>
</div>
</div>
</section>
<!-- 瀑布流卡片墙 -->
<section class="section section-stories">
<div class="container">
<div v-if="readyStories.length > 0" class="stories-wall">
<a
v-for="story in readyStories"
:key="story.id"
:href="story.sourceUrl"
target="_blank"
rel="noopener nofollow"
class="story-card reveal"
>
<div class="story-card__visual">
<img
v-if="story.coverImage"
:src="story.coverImage"
:alt="story.title"
class="story-card__img"
loading="lazy"
/>
<div v-else class="story-card__img-placeholder" />
<!-- 小红书角标 -->
<span class="story-card__badge">
<svg class="story-card__badge-icon" viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
<path d="M7.5 2h9a1.5 1.5 0 011.5 1.5v17a1.5 1.5 0 01-2.3 1.27L12 19l-3.7 2.77A1.5 1.5 0 016 20.5v-17A1.5 1.5 0 017.5 2z"/>
</svg>
小红书
</span>
<!-- 家庭类型 -->
<span class="story-card__family">{{ story.familyType }}</span>
</div>
<div class="story-card__body">
<h2 class="story-card__title">{{ story.title }}</h2>
<p class="story-card__desc">{{ story.subtitle || story.summary }}</p>
<div class="story-card__meta">
<span v-if="story.author" class="story-card__author">
{{ story.author }}
</span>
<span class="story-card__date">{{ story.travelDate }}</span>
</div>
<div class="story-card__tags">
<span v-for="tag in (story.tags || []).slice(0, 3)" :key="tag" class="story-tag">#{{ tag }}</span>
</div>
</div>
</a>
</div>
<div v-else class="stories-coming-soon reveal">
<p class="stories-coming-soon__text">更多真实旅行故事整理中,敬请期待。</p>
</div>
</div>
</section>
<!-- 内链 -->
<CommonSectionCTA
title="这些故事,可能也会是你的故事"
description="联系定制师,开始规划您的呼伦贝尔家庭旅行"
:links="[
{ to: '/contact', text: '联系定制师规划行程' },
{ to: '/reviews', text: '查看更多客户评价' },
]"
/>
</div>
</template>
<script setup>
import storiesData from '~/data/stories.json'
import brand from '~/data/brand.json'
import contact from '~/data/contact.json'
import { useOrganizationSchema, useBreadcrumbSchema, useStoriesListSchema } from '~/composables/useJsonLd'
const seo = useSEO('stories')
useScrollReveal()
useOrganizationSchema(brand, contact)
useStoriesListSchema(storiesData.stories)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '客户故事' },
])
const stories = storiesData.stories
const readyStories = computed(() =>
stories.filter(s => s.summary && !s.summary.includes('⚠️'))
)
</script>
<style lang="less" scoped>
.section-stories {
background: @color-bg-gray;
padding-bottom: @space-3xl;
}
// 瀑布流网格
.stories-wall {
column-count: 2;
column-gap: @space-md;
.respond-md({
column-count: 3;
});
.respond-xl({
column-count: 4;
column-gap: @space-lg;
});
}
// 卡片
.story-card {
display: inline-block;
width: 100%;
break-inside: avoid;
margin-bottom: @space-md;
background: @color-bg-white;
border-radius: @border-radius-lg;
overflow: hidden;
text-decoration: none;
color: inherit;
transition: all @transition-base;
border: 1px solid transparent;
.respond-xl({
margin-bottom: @space-lg;
});
&:hover {
transform: translateY(-3px);
box-shadow: @shadow-md;
border-color: @color-primary-light;
}
&__visual {
position: relative;
overflow: hidden;
}
&__img {
width: 100%;
display: block;
object-fit: cover;
// 不固定高度,让图片保持原始比例实现瀑布流效果
}
&__img-placeholder {
width: 100%;
height: 200px;
background: @color-primary-bg;
}
// 角标(小红书/原创)
&__badge {
position: absolute;
top: @space-sm;
right: @space-sm;
background: rgba(255, 46, 46, 0.9);
color: #fff;
font-size: 11px;
padding: 2px @space-sm;
border-radius: @border-radius-full;
display: flex;
align-items: center;
gap: 3px;
font-weight: @font-weight-medium;
backdrop-filter: blur(4px);
}
&__badge-icon {
flex-shrink: 0;
}
// 家庭类型
&__family {
position: absolute;
bottom: @space-sm;
left: @space-sm;
background: rgba(0, 0, 0, 0.6);
color: #fff;
font-size: @font-size-xs;
padding: 2px @space-sm;
border-radius: @border-radius-full;
backdrop-filter: blur(4px);
}
&__body {
padding: @space-md;
display: flex;
flex-direction: column;
gap: @space-xs;
}
&__title {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0;
line-height: @line-height-tight;
.text-clamp(2);
}
&__desc {
font-size: @font-size-sm;
color: @color-text-secondary;
margin: 0;
line-height: @line-height-base;
.text-clamp(2);
}
&__meta {
display: flex;
align-items: center;
gap: @space-sm;
font-size: @font-size-xs;
color: @color-text-muted;
}
&__author {
font-weight: @font-weight-medium;
color: @color-text-secondary;
}
&__tags {
display: flex;
flex-wrap: wrap;
gap: @space-xs;
margin-top: @space-xs;
}
}
.story-tag {
font-size: 11px;
color: @color-primary;
background: @color-primary-bg;
padding: 1px @space-sm;
border-radius: @border-radius-full;
}
.stories-coming-soon {
text-align: center;
padding: @space-3xl 0;
&__text {
font-size: @font-size-lg;
color: @color-text-secondary;
}
}
</style>
+559
查看文件
@@ -0,0 +1,559 @@
<template>
<div class="page-summer-camp">
<LayoutPageHero
:title="seo.h1"
subtitle="呼伦贝尔草原亲子夏令营·6天5晚·每期8组家庭"
:breadcrumbs="[{ text: '小蒙马夏令营' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="夏令营摘要">
<div class="container">
<div class="page-tldr-inner">
<p>小蒙马夏令营:呼伦贝尔草原亲子夏令营,6天5晚,每期限8组家庭,6-12岁儿童适合。亲子同行,非托管模式,草原骑马、篝火晚会、营地课程、牧民拜访等体验。7月旺季需提前2-3个月报名。</p>
</div>
</div>
</section>
<!-- 核心定位 -->
<section class="section">
<div class="container">
<div class="camp-hero">
<div class="camp-mascot">
<img
:src="images.mascot.xiaomengma"
alt="小蒙马 - 小蒙马夏令营吉祥物,一只可爱的蒙古小马形象"
width="200"
height="200"
loading="lazy"
/>
</div>
<div class="camp-intro">
<h2 class="camp-slogan">孩子在探索世界,你也在找回自己</h2>
<p class="camp-positioning">{{ camp.positioning }}</p>
<div class="camp-meta">
<span class="camp-meta-item">{{ camp.days }}天{{ camp.nights }}晚</span>
<span class="camp-meta-item">每期8组家庭</span>
<span class="camp-meta-item">亲子同行</span>
</div>
</div>
</div>
</div>
</section>
<!-- 实拍照片 + 期数数据 -->
<section class="section section--warm reveal">
<div class="container">
<div class="camp-gallery">
<div class="camp-photo">
<img
:src="images.summerCamp.river"
alt="小蒙马亲子夏令营在莫日格勒河旁举旗合影,背景是蜿蜒的草原河流"
width="1600"
height="1067"
loading="lazy"
/>
<span class="camp-photo-tag">莫日格勒河</span>
</div>
<div class="camp-photo">
<img
:src="images.summerCamp.grassland"
alt="小蒙马夏令营亲子家庭在呼伦贝尔草原上合影,手持小蒙马标志牌"
width="1600"
height="1067"
loading="lazy"
/>
<span class="camp-photo-tag">草原合影</span>
</div>
<div class="camp-photo">
<img
:src="images.summerCamp.birch"
alt="小蒙马夏令营亲子家庭在白桦林合影,家长和孩子们一起开心比耶"
width="1600"
height="1067"
loading="lazy"
/>
<span class="camp-photo-tag">白桦林研学</span>
</div>
<div class="camp-photo">
<img
:src="images.summerCamp.graduation"
alt="小蒙马夏令营结营仪式,家长和孩子们手持结营证书在营地合影"
width="1600"
height="1067"
loading="lazy"
/>
<span class="camp-photo-tag">结营仪式</span>
</div>
</div>
<div class="camp-stats">
<div class="camp-stat-item">
<span class="camp-stat-value">{{ camp.sessions['2025'] }}</span>
<span class="camp-stat-label">2025年成功举办期数</span>
</div>
<div class="camp-stat-item">
<span class="camp-stat-value">{{ camp.sessions['2026plan'] }}</span>
<span class="camp-stat-label">2026年计划开营期数</span>
</div>
<div class="camp-stat-item">
<span class="camp-stat-value">8</span>
<span class="camp-stat-label">每期限定家庭数</span>
</div>
</div>
</div>
</section>
<!-- 中间 CTA:看完照片后的转化入口 -->
<CommonSectionCTA
title="想了解营期安排和费用?"
description="添加微信客服,获取最新营期时间和报名方式"
:links="[{ to: '/contact', text: '咨询小蒙马夏令营' }]"
/>
<!-- 四大原则 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="行程设计四大原则" subtitle="每一处安排都经过精心考量" />
<div class="principles-grid">
<div v-for="(p, i) in camp.principles" :key="i" class="principle-card">
<div class="principle-index">{{ String(i + 1).padStart(2, '0') }}</div>
<h3 class="principle-name">{{ p }}</h3>
</div>
</div>
</div>
</section>
<!-- 行程概览 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="6天行程概览" subtitle="不赶路、不折腾,每一天都有记忆点" />
<div class="itinerary-list">
<div v-for="(day, i) in camp.itinerary" :key="i" class="itinerary-item">
<div class="itinerary-day">D{{ i + 1 }}</div>
<p class="itinerary-text">{{ day.replace(/^Day\d+\s*/, '') }}</p>
</div>
</div>
</div>
</section>
<!-- 核心体验 -->
<section class="section section--warm reveal">
<div class="container">
<CommonSectionTitle title="行程核心体验" subtitle="草原写生·非遗研学·民俗体验,每一天都有收获" />
<div class="activities-grid">
<div v-for="(activity, i) in camp.coreActivities" :key="i" class="activity-card">
<div class="activity-index">{{ String(i + 1).padStart(2, '0') }}</div>
<p class="activity-text">{{ activity }}</p>
</div>
</div>
</div>
</section>
<!-- 与V9的区别 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="小蒙马 vs 额吉的故乡" subtitle="两种产品,两种精彩" />
<div class="compare-cards">
<div class="compare-card compare-card--camp">
<h3>小蒙马夏令营</h3>
<ul>
<li>亲子同行,家长和孩子一起出发</li>
<li>每期8组家庭结伴同行,孩子有玩伴</li>
<li>草原写生+非遗研学+民俗体验</li>
<li>设有「爸妈托管」时段,家长也能放松</li>
<li>孩子有伙伴一起学、一起玩</li>
<li>6天5晚,节奏舒缓不赶路</li>
</ul>
</div>
<div class="compare-card compare-card--v9">
<h3>额吉的故乡V9</h3>
<ul>
<li>全家一起出行,行程灵活自由</li>
<li>一家一单一车,专属私家定制</li>
<li>专属领队一对一服务</li>
<li>行程可根据需求随时调整</li>
<li>享受亲密家庭时光</li>
<li>4-7天多版本可选</li>
</ul>
<NuxtLink to="/products" class="compare-link">查看V9系列 →</NuxtLink>
</div>
</div>
<p class="compare-detail">{{ camp.differenceFromV9 }}</p>
</div>
</section>
<!-- 家长常见问题(可折叠) -->
<section class="section section--gray">
<div class="container">
<CommonSectionTitle title="家长常见问题" />
<div class="camp-faq">
<FaqItem
v-for="(item, i) in campFAQ"
:key="item.question || item.q"
:question="item.question || item.q"
:answer="item.answer || item.a"
:initial-open="i === 0"
/>
</div>
</div>
</section>
<!-- 底部 CTA:双入口 -->
<CommonSectionCTA
title="想了解家庭一起出行的产品?"
description="额吉的故乡V9系列,一家一单一车,全家共享草原时光"
:links="[
{ to: '/contact', text: '咨询小蒙马夏令营' },
{ to: '/products', text: '查看额吉的故乡V9系列' },
]"
/>
</div>
</template>
<script setup>
import products from '~/data/products.json'
import images from '~/data/images.json'
import { useBreadcrumbSchema, useSummerCampSchema } from '~/composables/useJsonLd'
const seo = useSEO('summer-camp')
useScrollReveal()
const camp = products.summerCamp
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '小蒙马夏令营' },
])
useSummerCampSchema(camp)
// 家长关心的问题(从数据文件读取)
const campFAQ = camp.faq || []
</script>
<style lang="less" scoped>
.camp-hero {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: @space-xl;
max-width: 800px;
margin: 0 auto;
.respond-md({
flex-direction: row;
text-align: left;
gap: @space-2xl;
});
}
.camp-mascot {
flex-shrink: 0;
img {
width: 160px;
height: auto;
filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.08));
}
}
.camp-intro {
flex: 1;
}
.camp-slogan {
font-size: @font-size-2xl;
color: @color-primary;
font-weight: @font-weight-bold;
margin-bottom: @space-md;
}
.camp-positioning {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-lg;
}
.camp-meta {
display: flex;
flex-wrap: wrap;
gap: @space-sm;
}
.camp-meta-item {
font-size: @font-size-sm;
color: @color-primary;
background: @color-primary-bg;
padding: @space-xs @space-md;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
}
// 实拍照片
.camp-gallery {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
margin-bottom: @space-xl;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
}
.camp-photo {
position: relative;
border-radius: @border-radius-lg;
overflow: hidden;
img {
width: 100%;
height: auto;
display: block;
transition: transform @transition-slow;
}
&:hover img {
transform: scale(1.03);
}
}
.camp-photo-tag {
position: absolute;
bottom: @space-sm;
left: @space-sm;
font-size: @font-size-xs;
color: #fff;
background: rgba(0, 0, 0, 0.5);
padding: @space-xs @space-sm;
border-radius: @border-radius-full;
}
.camp-stats {
display: flex;
justify-content: center;
gap: @space-xl;
flex-wrap: wrap;
}
.camp-stat-item {
text-align: center;
}
.camp-stat-value {
display: block;
font-size: @font-size-3xl;
font-weight: @font-weight-bold;
color: @color-primary;
line-height: 1.2;
}
.camp-stat-label {
display: block;
font-size: @font-size-sm;
color: @color-text-secondary;
margin-top: @space-xs;
}
// 四大原则
.principles-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-md;
.respond-md({
grid-template-columns: repeat(4, 1fr);
});
}
.principle-card {
padding: @space-lg;
background: @color-bg-white;
border-radius: @border-radius-lg;
border: 1px solid @color-border;
text-align: center;
transition: border-color @transition-fast, box-shadow @transition-fast;
&:hover {
border-color: @color-primary-lighter;
box-shadow: @shadow-md;
}
}
.principle-index {
font-size: 28px;
font-weight: @font-weight-bold;
color: @color-primary-lighter;
line-height: 1;
margin-bottom: @space-sm;
}
.principle-name {
font-size: @font-size-md;
color: @color-text-primary;
font-weight: @font-weight-medium;
}
// 行程概览
.itinerary-list {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: @space-md;
}
.itinerary-item {
display: flex;
gap: @space-md;
align-items: flex-start;
padding: @space-md @space-lg;
background: @color-bg-gray;
border-radius: @border-radius-md;
border-left: 3px solid @color-primary;
transition: background @transition-fast, border-color @transition-fast;
&:hover {
background: @color-primary-bg;
}
}
.itinerary-day {
flex-shrink: 0;
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-primary;
min-width: 32px;
}
.itinerary-text {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
// 核心活动
.activities-grid {
.grid-responsive();
}
.activity-card {
padding: @space-xl;
background: @color-bg-white;
border-radius: @border-radius-lg;
border: 1px solid @color-border;
transition: border-color @transition-fast, box-shadow @transition-fast;
&:hover {
border-color: @color-primary-lighter;
box-shadow: @shadow-md;
}
}
.activity-index {
font-size: @font-size-3xl;
font-weight: @font-weight-bold;
margin-bottom: @space-sm;
line-height: 1;
color: @color-primary-lighter;
}
.activity-text {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin: 0;
}
// 对比卡片
.compare-cards {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
max-width: 800px;
margin: 0 auto @space-xl;
.respond-md({
grid-template-columns: 1fr 1fr;
});
}
.compare-card {
padding: @space-xl;
border-radius: @border-radius-lg;
border: 1px solid @color-border;
h3 {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
margin-bottom: @space-md;
}
ul {
display: flex;
flex-direction: column;
gap: @space-sm;
}
li {
font-size: @font-size-sm;
color: @color-text-secondary;
padding-left: @space-md;
position: relative;
&::before {
content: '·';
position: absolute;
left: 0;
color: @color-primary;
font-weight: bold;
}
}
}
.compare-card--camp {
background: @color-primary-bg;
border-color: fadeout(@color-primary, 80%);
h3 {
color: @color-primary;
}
}
.compare-card--v9 {
background: @color-bg-white;
}
.compare-link {
display: inline-block;
margin-top: @space-md;
font-size: @font-size-sm;
color: @color-primary;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
.compare-detail {
max-width: 800px;
margin: 0 auto;
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
padding: @space-lg;
background: @color-bg-gray;
border-radius: @border-radius-md;
}
// 家长FAQ
.camp-faq {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: @space-md;
}
</style>
+647
查看文件
@@ -0,0 +1,647 @@
<template>
<div class="page-winter-camp">
<LayoutPageHero
:title="seo.h1"
subtitle="呼伦贝尔雪原亲子冬令营·7天6晚·每期8组家庭"
:breadcrumbs="[{ text: '小蒙马冬季营' }]"
/>
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="冬令营摘要">
<div class="container">
<div class="page-tldr-inner">
<p>小蒙马冬季营:呼伦贝尔雪原亲子冬令营,7天6晚,每期限8组家庭,5-12岁儿童适合。亲子同行,雪地摩托、冰钓、冬季那达慕、驯鹿文化体验等。元旦春节档期为主,需提前1-2个月报名。</p>
</div>
</div>
</section>
<!-- 风景大图 -->
<div class="scenic-banner">
<img src="/images/seasons/winter-hero.jpg" alt="冬季呼伦贝尔大雪原" loading="eager" />
</div>
<!-- 核心定位 -->
<section class="section">
<div class="container">
<div class="camp-hero">
<div class="camp-mascot">
<img
:src="images.mascot.xiaomengma"
alt="小蒙马 - 冬季亲子营吉祥物"
width="200"
height="200"
loading="lazy"
/>
</div>
<div class="camp-intro">
<h2 class="camp-slogan">一片干净的雪原,一段只属于你们的记忆</h2>
<p class="camp-positioning">{{ camp.positioning }}</p>
<div class="camp-meta">
<span class="camp-meta-item">{{ camp.days }}天{{ camp.nights }}晚</span>
<span class="camp-meta-item">每期{{ camp.maxFamilies }}组家庭</span>
<span class="camp-meta-item">今年仅{{ camp.totalSessions }}期</span>
<span class="camp-meta-item">{{ camp.ageRange }}亲子</span>
</div>
</div>
</div>
</div>
</section>
<!-- 为什么选呼伦贝尔 -->
<section class="section section--ice reveal">
<div class="container">
<CommonSectionTitle title="为什么是呼伦贝尔?" subtitle="不是更冷,而是更纯粹" />
<div class="why-block">
<p>{{ camp.whyHulunbuir }}</p>
</div>
</div>
</section>
<!-- 亲子营优势 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="亲子营的优势" subtitle="和独立小包团相比" />
<div class="advantages-grid">
<div v-for="(adv, i) in camp.campAdvantages" :key="i" class="advantage-card">
<span class="advantage-check">✓</span>
<p>{{ adv }}</p>
</div>
</div>
</div>
</section>
<!-- 咨询 CTA -->
<CommonSectionCTA
title="想了解营期安排和费用?"
description="添加微信客服,获取最新营期时间和报名方式"
:links="[{ to: '/contact', text: '咨询小蒙马冬季营' }]"
/>
<!-- 住宿保障 -->
<section class="section section--warm reveal">
<div class="container">
<CommonSectionTitle title="住宿安排" subtitle="住得舒服,玩得才能惬意" />
<div class="hotels-grid">
<div v-for="(hotel, i) in camp.hotels" :key="i" class="hotel-card">
<div class="hotel-star">{{ hotel.star }}</div>
<h3 class="hotel-name">{{ hotel.name }}</h3>
<span class="hotel-nights">{{ hotel.nights }}</span>
<p class="hotel-desc">{{ hotel.description }}</p>
</div>
</div>
</div>
</section>
<!-- 7天行程 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="7天行程概览" subtitle="不赶路、不折腾,每一天都有记忆点" />
<div class="itinerary-list">
<details v-for="day in camp.itinerary" :key="day.day" class="itinerary-item" :open="day.day <= 2">
<summary class="itinerary-header">
<span class="itinerary-day">D{{ day.day }}</span>
<span class="itinerary-title">{{ day.title }}</span>
<span v-if="day.hotel" class="itinerary-hotel">🏨 {{ day.hotel }}</span>
</summary>
<div class="itinerary-body">
<p class="itinerary-summary">{{ day.summary }}</p>
<ul class="itinerary-highlights">
<li v-for="(h, j) in day.highlights" :key="j">{{ h }}</li>
</ul>
</div>
</details>
</div>
</div>
</section>
<!-- 冬季风景 -->
<div class="scenic-duo reveal">
<img src="/images/seasons/winter-yurt.jpg" alt="冬季雪原蒙古包日落" loading="lazy" />
<img src="/images/seasons/winter-frost.jpg" alt="大兴安岭雾凇森林" loading="lazy" />
</div>
<!-- 摄影师 -->
<section class="section section--ice reveal">
<div class="container">
<CommonSectionTitle title="随行摄影师" subtitle="不错过每一个自然的瞬间" />
<div class="photographer-block">
<h3>摄影师:{{ camp.photographer.name }}</h3>
<p>{{ camp.photographer.description }}</p>
<p class="photographer-special">📍 {{ camp.photographer.specialDay2 }}</p>
</div>
</div>
</section>
<!-- 穿衣指南 -->
<section class="section reveal">
<div class="container">
<CommonSectionTitle title="冬季穿衣指南" subtitle="穿对了,冷也是一种享受" />
<div class="clothing-grid">
<div class="clothing-card">
<h4>👕 上身</h4>
<p>{{ camp.winterClothing.upper }}</p>
</div>
<div class="clothing-card">
<h4>👖 下身</h4>
<p>{{ camp.winterClothing.lower }}</p>
</div>
<div class="clothing-card">
<h4>👟 鞋子</h4>
<p>{{ camp.winterClothing.shoes }}</p>
</div>
<div class="clothing-card">
<h4>🧣 配件</h4>
<p>{{ camp.winterClothing.accessories }}</p>
</div>
</div>
</div>
</section>
<!-- 随行服务 -->
<section class="section section--warm reveal">
<div class="container">
<CommonSectionTitle title="随行服务配置" subtitle="比夏季更谨慎、更细致" />
<div class="service-grid">
<div v-for="(s, i) in camp.serviceConfig" :key="i" class="service-item">
<span class="service-icon">👤</span>
<span>{{ s }}</span>
</div>
</div>
</div>
</section>
<!-- 家长FAQ -->
<section class="section section--gray">
<div class="container">
<CommonSectionTitle title="家长常见问题" />
<div class="camp-faq">
<FaqItem
v-for="(item, i) in camp.faq"
:key="item.q"
:question="item.q"
:answer="item.a"
:initial-open="i === 0"
/>
</div>
</div>
</section>
<!-- 结语 -->
<section class="section">
<div class="container">
<div class="closing-note">
<p>{{ camp.closingNote }}</p>
</div>
</div>
</section>
<!-- 底部 CTA -->
<CommonSectionCTA
title="给孩子一次真正的冬天"
description="小蒙马冬季亲子营,今年仅开3期,全力以赴"
:links="[
{ to: '/contact', text: '咨询冬季营' },
{ to: '/summer-camp', text: '了解夏季小蒙马' },
]"
/>
</div>
</template>
<script setup>
import camp from '~/data/winter-camp.json'
import images from '~/data/images.json'
import { useBreadcrumbSchema, useWinterCampSchema } from '~/composables/useJsonLd'
const seo = useSEO('winter-camp', {
title: '小蒙马冬季亲子营 - 呼伦贝尔雪原冬令营·7天6晚·每期8组家庭 - 呼籁旅行',
description: '呼籁旅行小蒙马冬季亲子营,7天6晚呼伦贝尔+阿尔山雪原冬令营。莫日格勒大雪原、冬季那达慕、狼园探秘、雪地骑马、阿尔山滑雪、不冻河漂流、私汤温泉。每期8组家庭,5-12岁亲子同行。',
h1: '小蒙马冬季亲子营',
})
useScrollReveal()
useWinterCampSchema(camp)
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '小蒙马冬季营' },
])
</script>
<style lang="less" scoped>
// Hero 区
.camp-hero {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: @space-xl;
max-width: 800px;
margin: 0 auto;
.respond-md({
flex-direction: row;
text-align: left;
gap: @space-2xl;
});
}
.camp-mascot {
flex-shrink: 0;
img {
width: 160px;
height: auto;
filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.08));
}
}
.camp-intro {
flex: 1;
}
.camp-slogan {
font-size: @font-size-2xl;
color: #3B82F6;
font-weight: @font-weight-bold;
margin-bottom: @space-md;
}
.camp-positioning {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-lg;
}
.camp-meta {
display: flex;
flex-wrap: wrap;
gap: @space-sm;
}
.camp-meta-item {
font-size: @font-size-sm;
color: #3B82F6;
background: #EFF6FF;
padding: @space-xs @space-md;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
}
// 为什么呼伦贝尔
.section--ice {
background: #F0F9FF;
}
.why-block {
max-width: 800px;
margin: 0 auto;
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
padding: @space-xl;
background: @color-bg-white;
border-radius: @border-radius-lg;
border-left: 4px solid #3B82F6;
}
// 亲子营优势
.advantages-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
max-width: 800px;
margin: 0 auto;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
}
.advantage-card {
display: flex;
gap: @space-md;
align-items: flex-start;
padding: @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
transition: box-shadow @transition-fast;
&:hover {
box-shadow: @shadow-md;
}
p {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
}
.advantage-check {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: #3B82F6;
color: #fff;
border-radius: 50%;
font-size: @font-size-xs;
font-weight: @font-weight-bold;
}
// 酒店
.hotels-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-md({
grid-template-columns: repeat(3, 1fr);
});
}
.hotel-card {
padding: @space-xl;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
text-align: center;
transition: box-shadow @transition-base, transform @transition-base;
&:hover {
box-shadow: @shadow-md;
transform: translateY(-2px);
}
}
.hotel-star {
display: inline-block;
padding: @space-xs @space-md;
font-size: @font-size-xs;
font-weight: @font-weight-bold;
color: #B45309;
background: #FEF3C7;
border-radius: @border-radius-full;
margin-bottom: @space-sm;
}
.hotel-name {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-xs;
}
.hotel-nights {
display: block;
font-size: @font-size-sm;
color: #3B82F6;
font-weight: @font-weight-medium;
margin-bottom: @space-md;
}
.hotel-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
text-align: left;
}
// 行程
.itinerary-list {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: @space-sm;
}
.itinerary-item {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
overflow: hidden;
transition: box-shadow @transition-fast;
&[open] {
box-shadow: @shadow-md;
border-color: #93C5FD;
}
}
.itinerary-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: @space-sm;
padding: @space-md @space-lg;
cursor: pointer;
transition: background @transition-fast;
&:hover {
background: #F0F9FF;
}
}
.itinerary-day {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: #3B82F6;
min-width: 36px;
}
.itinerary-title {
flex: 1;
font-size: @font-size-base;
font-weight: @font-weight-medium;
color: @color-text-primary;
}
.itinerary-hotel {
font-size: @font-size-xs;
color: @color-text-muted;
}
.itinerary-body {
padding: 0 @space-lg @space-lg;
padding-left: calc(@space-lg + 36px + @space-sm);
}
.itinerary-summary {
font-size: @font-size-sm;
color: #3B82F6;
font-weight: @font-weight-medium;
margin-bottom: @space-md;
}
.itinerary-highlights {
display: flex;
flex-direction: column;
gap: @space-sm;
li {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
padding-left: @space-md;
position: relative;
&::before {
content: '❄';
position: absolute;
left: 0;
color: #3B82F6;
font-size: 10px;
}
}
}
// 摄影师
.photographer-block {
max-width: 700px;
margin: 0 auto;
text-align: center;
h3 {
font-size: @font-size-xl;
color: @color-text-primary;
margin-bottom: @space-md;
}
p {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-md;
}
}
.photographer-special {
padding: @space-md @space-lg;
background: @color-bg-white;
border-radius: @border-radius-md;
border-left: 3px solid #3B82F6;
text-align: left;
}
// 穿衣指南
.clothing-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(4, 1fr);
});
}
.clothing-card {
padding: @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
h4 {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-sm;
}
p {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
}
}
// 服务配置
.service-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: @space-md;
max-width: 800px;
margin: 0 auto;
}
.service-item {
display: flex;
align-items: center;
gap: @space-sm;
padding: @space-sm @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-full;
font-size: @font-size-sm;
color: @color-text-secondary;
}
.service-icon {
font-size: @font-size-base;
}
// 结语
.closing-note {
max-width: 700px;
margin: 0 auto;
text-align: center;
font-size: @font-size-lg;
color: @color-text-secondary;
line-height: @line-height-loose;
font-style: italic;
padding: @space-xl;
border-top: 1px solid @color-border;
border-bottom: 1px solid @color-border;
}
// FAQ
.camp-faq {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: @space-md;
}
// 风景图
.scenic-banner {
width: 100%;
overflow: hidden;
max-height: 480px;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.scenic-duo {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
max-height: 360px;
overflow: hidden;
.respond-md({
grid-template-columns: 1fr 1fr;
});
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
max-height: 360px;
}
}
</style>