覆盖最新版本代码,包含: - 新增品牌百科、公司百科页面及CMS管理 - 新增微信引导组件(WechatCTA) - 新增品牌/公司图片资源 - 全站组件/页面/数据/样式更新 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
295 行
7.7 KiB
Vue
295 行
7.7 KiB
Vue
<template>
|
||
<div class="page-stories">
|
||
<LayoutPageHero
|
||
:title="seo.h1"
|
||
subtitle="来自小红书的真实旅行分享"
|
||
:breadcrumbs="[{ text: '客户故事' }]"
|
||
background-image="/images/summer-camp/birch-group.jpg"
|
||
/>
|
||
|
||
<!-- 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">
|
||
<component
|
||
:is="getCardComponent(story)"
|
||
v-for="story in readyStories"
|
||
:key="story.id"
|
||
v-bind="getCardProps(story)"
|
||
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 v-if="story.sourceUrl" 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 v-else-if="story.content" class="story-card__badge story-card__badge--original">
|
||
原创故事
|
||
</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>
|
||
</component>
|
||
</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 { resolveComponent } from 'vue'
|
||
import { useOrganizationSchema, useBreadcrumbSchema, useStoriesListSchema } from '~/composables/useJsonLd'
|
||
|
||
const { data: storiesData } = await usePublicApi('stories')
|
||
const { data: brand } = await usePublicApi('brand')
|
||
const { data: contact } = await usePublicApi('contact')
|
||
|
||
const seo = useSEO('stories')
|
||
useScrollReveal()
|
||
useOrganizationSchema(brand.value, contact.value)
|
||
useStoriesListSchema(storiesData.value?.stories)
|
||
useBreadcrumbSchema([
|
||
{ text: '首页', to: '/' },
|
||
{ text: '客户故事' },
|
||
])
|
||
|
||
const stories = computed(() => storiesData.value?.stories || [])
|
||
|
||
const readyStories = computed(() =>
|
||
stories.value.filter(s => s.summary && !s.summary.includes('⚠️'))
|
||
)
|
||
|
||
// 占位内容未替换时,阻止搜索引擎索引空页面
|
||
if (readyStories.value.length === 0) {
|
||
useHead({ meta: [{ key: 'robots', name: 'robots', content: 'noindex, nofollow' }] })
|
||
}
|
||
|
||
// 原创故事用 NuxtLink,xhs 故事用 <a> 外链
|
||
function getCardComponent(story) {
|
||
return story.sourceUrl ? 'a' : resolveComponent('NuxtLink')
|
||
}
|
||
|
||
function getCardProps(story) {
|
||
if (story.sourceUrl) {
|
||
return {
|
||
href: story.sourceUrl,
|
||
target: '_blank',
|
||
rel: 'noopener nofollow',
|
||
}
|
||
}
|
||
return { to: `/stories/${story.slug}` }
|
||
}
|
||
</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);
|
||
|
||
&--original {
|
||
background: rgba(45, 106, 79, 0.9);
|
||
}
|
||
}
|
||
|
||
&__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>
|