236 行
6.8 KiB
Vue
236 行
6.8 KiB
Vue
<template>
|
||
<div class="page-reviews">
|
||
<LayoutPageHero
|
||
:title="seo.h1"
|
||
:subtitle="heroes?.reviews || '来自真实家庭的旅行评价'"
|
||
:breadcrumbs="[{ text: '客户评价' }]"
|
||
background-image="/images/summer-camp/camp-graduation.jpg"
|
||
/>
|
||
|
||
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
|
||
<section class="page-tldr" aria-label="客户评价摘要">
|
||
<div class="container">
|
||
<div class="page-tldr-inner">
|
||
<p>呼籁旅行已服务近万组家庭,本页收录来自真实客户的旅行评价,均为未经修改的原始反馈。可按出行场景(亲子、摄影、蜜月等)和关注维度(安全性、性价比、服务质量等)筛选,找到与自己情况最相似的参考评价。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 数据汇总 -->
|
||
<ReviewsReviewSummary v-if="reviews" :summary="reviews.summary" class="reveal" />
|
||
|
||
<!-- 筛选+列表 -->
|
||
<section v-if="reviews" 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">展开筛选 ▾</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">
|
||
收起 ▴
|
||
</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 { useBreadcrumbSchema, useAggregateRatingSchema } from '~/composables/useJsonLd'
|
||
|
||
const { data: reviews } = await usePublicApi('reviews')
|
||
const { data: heroes } = await usePublicApi('heroes')
|
||
|
||
const seo = useSEO('reviews')
|
||
useScrollReveal()
|
||
useAggregateRatingSchema(reviews.value?.summary, reviews.value?.items)
|
||
useBreadcrumbSchema([
|
||
{ text: '首页', to: '/' },
|
||
{ text: '客户评价' },
|
||
])
|
||
|
||
const activeScene = ref('')
|
||
const activeConcern = ref('')
|
||
const filterExpanded = ref(false)
|
||
const isCompact = ref(false)
|
||
|
||
const allScenes = computed(() => [...new Set((reviews.value?.items || []).flatMap(r => r.scenes))])
|
||
const allConcerns = computed(() => [...new Set((reviews.value?.items || []).flatMap(r => r.concerns))])
|
||
|
||
const filteredReviews = computed(() => {
|
||
return (reviews.value?.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>
|