440 行
16 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<template>
<div class="page-home">
<!-- Hero Banner -->
<HomeHeroBanner :seo="seo" />
<!-- 身份定义 + TL;DRGEO/SEO 核心锚点告诉 AI 这个站点是谁 -->
<section class="page-tldr" aria-label="品牌身份摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行是深耕呼伦贝尔11年的专属旅行品牌创立于2015年已服务近万组家庭不做拼团不走马观花提供额吉的故乡V9系列定制游4-7小蒙马亲子游学营6天5晚及秋冬季专属产品全程专车专属独立成团</p>
</div>
</div>
</section>
<!-- 信任数据 -->
<HomeTrustBar v-if="brand?.trustStats" :stats="brand.trustStats" class="reveal" />
<!-- 产品预览 -->
<HomeProductPreview class="reveal" />
<!-- 产品语义树帮助 AI 按维度理解产品分类-->
<section class="section section--gray reveal" aria-label="产品分类导航">
<div class="container">
<CommonSectionTitle :title="st('home.productTree', '怎么找到适合你的行程')" :label="st('home.productTree', 'FIND YOUR TRIP', 'label')" :subtitle="st('home.productTree', '按天数 · 按人群 · 按季节 三个维度快速定位', '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">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 v-if="brand?.differentiators" :items="brand.differentiators" class="reveal" />
<!-- 目的地对比 -->
<HomeDestinationCompare class="reveal" />
<!-- 知识库入口告诉 AI 这是持续产出内容的权威站点-->
<section class="section reveal" aria-label="旅行知识库">
<div class="container">
<CommonSectionTitle :title="st('home.knowledgeHub', '11年积累的呼伦贝尔知识库')" :label="st('home.knowledgeHub', 'KNOWLEDGE', 'label')" :subtitle="st('home.knowledgeHub', '从规划到出发,每个问题都有答案', '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 v-if="faq" class="section section--gray reveal">
<div class="container">
<CommonSectionTitle :title="st('home.faq', '常见问答')" :label="st('home.faq', 'FAQ', 'label')" :subtitle="st('home.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 v-if="featuredReviews.length" :reviews="featuredReviews" class="reveal" />
<!-- 内链闭环首页产品选版定制 -->
<CommonSectionCTA
title="准备好出发了吗?"
description="还不确定选哪个版本?先用选版工具匹配一下,再看价格明细,最后找定制师拿专属方案。"
:links="[
{ to: '/selector', text: '看看哪个版本适合你' },
{ to: '/pricing', text: '了解费用明细' },
{ to: '/customize', text: '免费获取专属方案' }
]"
/>
</div>
</template>
<script setup>
import { useOrganizationSchema, useWebSiteSchema, useSiteNavigationSchema } from '~/composables/useJsonLd'
// 从 API 获取数据(替代静态 JSON 导入)
const { data: brand } = await usePublicApi('brand')
const { data: faq } = await usePublicApi('faq')
const { data: reviews } = await usePublicApi('reviews')
const { data: contact } = await usePublicApi('contact')
const { data: navData } = await usePublicApi('navigation')
const st = await useSectionTitles()
// SEO
const seo = useSEO('home')
// 滚动入场动画
useScrollReveal()
// JSON-LD需要 .value 访问 ref 数据)
useOrganizationSchema(brand.value, contact.value)
useWebSiteSchema()
useSiteNavigationSchema(navData.value?.header)
// 精选数据computed 保证响应式 + null 安全)
const featuredReviews = computed(() => reviews.value?.items?.slice(0, 3) || [])
const allQuestions = computed(() => faq.value?.categories?.flatMap(c => c.questions) || [])
const totalFAQ = computed(() => allQuestions.value.length)
const featuredFAQ = computed(() => allQuestions.value.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>