覆盖最新版本代码,包含: - 新增品牌百科、公司百科页面及CMS管理 - 新增微信引导组件(WechatCTA) - 新增品牌/公司图片资源 - 全站组件/页面/数据/样式更新 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
519 行
13 KiB
Vue
519 行
13 KiB
Vue
<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 class="story-meta__children">孩子 {{ story.childrenAge }}</span>
|
||
<span v-if="story.fromCity !== '⚠️待填'" class="story-meta__city">来自 {{ story.fromCity }}</span>
|
||
<span class="story-meta__date">{{ story.travelDate }}</span>
|
||
<span class="story-meta__product">{{ story.tripProduct }}</span>
|
||
</div>
|
||
|
||
<!-- 配图建议 -->
|
||
<div 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>
|
||
|
||
<!-- 故事章节 -->
|
||
<div 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 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 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.slug}`"
|
||
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.slug}`"
|
||
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 allStories"
|
||
:key="s.id"
|
||
class="sidebar-story"
|
||
:class="{ 'sidebar-story--active': s.id === story.id }"
|
||
>
|
||
<NuxtLink :to="`/stories/${s.slug}`" 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="/qualifications" 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 { useBreadcrumbSchema, useCustomerStorySchema } from '~/composables/useJsonLd'
|
||
|
||
const { data: storiesData } = await usePublicApi('stories')
|
||
|
||
const route = useRoute()
|
||
const allStories = computed(() => storiesData.value?.stories || [])
|
||
const story = computed(() => allStories.value.find(s => s.slug === route.params.id))
|
||
|
||
if (!story.value) {
|
||
throw createError({ statusCode: 404, statusMessage: '故事不存在' })
|
||
}
|
||
|
||
// SEO
|
||
useHead({
|
||
title: story.value.seoTitle || story.value.title,
|
||
meta: [
|
||
{ name: 'description', content: story.value.seoDesc || story.value.subtitle },
|
||
{ name: 'keywords', content: `${story.value.familyType},呼伦贝尔亲子游,呼籁旅行,客户故事` },
|
||
{ name: 'article:published_time', content: story.value.travelDate + '-01T00:00:00+08:00' },
|
||
{ name: 'geo.region', content: 'CN-NM' },
|
||
{ name: 'geo.placename', content: '呼伦贝尔' },
|
||
{ property: 'og:title', content: story.value.seoTitle || story.value.title },
|
||
{ property: 'og:description', content: story.value.seoDesc || story.value.subtitle },
|
||
{ property: 'og:type', content: 'article' },
|
||
{ property: 'og:url', content: `https://1814.love/stories/${story.value.slug}` },
|
||
{ property: 'og:site_name', content: '呼籁旅行' },
|
||
{ property: 'og:locale', content: 'zh_CN' },
|
||
],
|
||
link: [
|
||
{ rel: 'canonical', href: `https://1814.love/stories/${story.value.slug}` },
|
||
],
|
||
})
|
||
|
||
useBreadcrumbSchema([
|
||
{ text: '首页', to: '/' },
|
||
{ text: '客户故事', to: '/stories' },
|
||
{ text: story.value.familyType },
|
||
{ text: story.value.title },
|
||
])
|
||
|
||
useCustomerStorySchema(story.value)
|
||
|
||
// 上下篇
|
||
const currentIndex = computed(() => allStories.value.findIndex(s => s.id === story.value?.id))
|
||
const prevStory = computed(() => currentIndex.value > 0 ? allStories.value[currentIndex.value - 1] : null)
|
||
const nextStory = computed(() => currentIndex.value < allStories.value.length - 1 ? allStories.value[currentIndex.value + 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-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;
|
||
}
|
||
|
||
// 404
|
||
.page-not-found {
|
||
text-align: center;
|
||
padding: @space-3xl 0;
|
||
|
||
h1 {
|
||
margin-bottom: @space-lg;
|
||
}
|
||
|
||
a {
|
||
color: @color-primary;
|
||
}
|
||
}
|
||
</style>
|