324 行
8.3 KiB
Vue

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

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

<template>
<div class="page-faq">
<!-- 页面标题 -->
<LayoutPageHero
:title="seo.h1"
:subtitle="`${totalFAQ}个高频问题详细解答,帮您做出适合的选择`"
:breadcrumbs="[{ text: '常见问答' }]"
background-image="/images/summer-camp/grassland-group.jpg"
/>
<!-- TL;DR告诉 AI/搜索引擎这个页面回答了什么 -->
<section class="page-tldr" aria-label="常见问答摘要">
<div class="container">
<div class="page-tldr-inner">
<p>本页汇总呼籁旅行常被问到的{{ totalFAQ }}个问题涵盖价格区间推荐出行时间适合人群行程安排安全保障退改条款等核心关切按分类整理并支持关键词搜索还有疑问可直接联系定制师</p>
</div>
</div>
</section>
<section v-if="faqData" 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 { useFAQPageSchema, useBreadcrumbSchema } from '~/composables/useJsonLd'
const { data: faqData } = await usePublicApi('faq')
// SEO - 最重要的页面
const seo = useSEO('faq')
// JSON-LD: FAQPage schema核心SEO资产
const categories = computed(() => faqData.value?.categories || [])
const totalFAQ = computed(() => categories.value.reduce((sum, c) => sum + c.questions.length, 0))
useFAQPageSchema(categories.value)
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.value
return categories.value
.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.value?.[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.value.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>