- 后台全面引入 Naive UI 组件库,统一 UI 规范 - 前台 usePublicApi 切换到 API 调用(GET /api/content/[key]) - 前后台数据源统一(site_content 表) - 产品线统一管理(tour/camp/course 三套编辑器) - 产品数据归一化(itinerary/faq/pricing 格式统一) - 表单提交 API 改写入 submissions 表 - 新增 submissions 标记已读 API - site-content PUT 支持 upsert - 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb) - 消除 PageHero 类型警告(useSEO reactive 修复) - 25 个前台页面全部 HTTP 200,展示效果不变 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
431 行
11 KiB
Vue
431 行
11 KiB
Vue
<template>
|
||
<div class="page-guides">
|
||
<LayoutPageHero
|
||
:title="seo.h1"
|
||
subtitle="证件·穿搭·行李清单·活动安全·幼童专属指南,帮您轻松准备呼伦贝尔之行"
|
||
:breadcrumbs="[{ text: '出行指南' }]"
|
||
background-image="/images/seasons/summer-car.jpg"
|
||
/>
|
||
|
||
<!-- TL;DR:告诉 AI/搜索引擎这个页面回答了什么 -->
|
||
<section class="page-tldr" aria-label="出行指南摘要">
|
||
<div class="container">
|
||
<div class="page-tldr-inner">
|
||
<p>去呼伦贝尔出行前需准备:中国居民身份证(14岁以下携带户口本),草原早晚温差大需分层穿搭,必备防晒防蚊产品。本指南涵盖六个章节:证件清单、按月穿搭参考(6-9月及冬季)、行李打包建议、草原活动安全须知、实用信息与推荐App、幼童专属注意事项,全部来自11年带队实践经验。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<section v-if="guidesData" class="section">
|
||
<div class="container">
|
||
<p class="guide-intro">{{ guidesData.pageIntro }}</p>
|
||
|
||
<!-- 锚点导航 -->
|
||
<GuidesGuideNav
|
||
:sections="guidesData.sections"
|
||
:active="activeSection"
|
||
@select="scrollToSection"
|
||
/>
|
||
|
||
<!-- 01 证件 -->
|
||
<div :id="sDocuments.id" class="guide-section">
|
||
<CommonSectionTitle
|
||
:title="sDocuments.title"
|
||
:subtitle="sDocuments.subtitle"
|
||
align="left"
|
||
/>
|
||
<p class="guide-text">{{ sDocuments.content }}</p>
|
||
<CommonContentCard :hoverable="false">
|
||
<GuidesGuideChecklist :items="sDocuments.items" />
|
||
</CommonContentCard>
|
||
<div v-if="sDocuments.tips" class="guide-tips">
|
||
<p v-for="(tip, i) in sDocuments.tips" :key="i" class="guide-tip">{{ tip }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 02 穿搭 -->
|
||
<div :id="sClothing.id" class="guide-section">
|
||
<CommonSectionTitle
|
||
:title="sClothing.title"
|
||
:subtitle="sClothing.subtitle"
|
||
align="left"
|
||
/>
|
||
<p class="guide-text">{{ sClothing.content }}</p>
|
||
<div class="clothing-grid">
|
||
<GuidesGuideClothingCard
|
||
v-for="month in sClothing.months"
|
||
:key="month.name"
|
||
:month="month"
|
||
/>
|
||
</div>
|
||
<div class="guide-tips">
|
||
<p v-for="(tip, i) in sClothing.extraTips" :key="i" class="guide-tip">{{ tip }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 03 行李清单 -->
|
||
<div :id="sPacking.id" class="guide-section">
|
||
<CommonSectionTitle
|
||
:title="sPacking.title"
|
||
:subtitle="sPacking.subtitle"
|
||
align="left"
|
||
/>
|
||
|
||
<div class="packing-categories">
|
||
<div v-for="cat in sPacking.categories" :key="cat.name" class="packing-category">
|
||
<h3 class="packing-cat-name">{{ cat.name }}</h3>
|
||
<p v-if="cat.note" class="packing-cat-note">{{ cat.note }}</p>
|
||
<CommonContentCard :hoverable="false">
|
||
<GuidesGuideChecklist :items="cat.items" />
|
||
</CommonContentCard>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 已帮您准备好的 -->
|
||
<div class="provided-block">
|
||
<h3 class="provided-title">{{ sPacking.provided.title }}</h3>
|
||
<p class="provided-desc">{{ sPacking.provided.description }}</p>
|
||
<p class="provided-items">{{ sPacking.provided.items }}</p>
|
||
</div>
|
||
|
||
<!-- 不用带 -->
|
||
<div class="dont-bring">
|
||
<h4 class="dont-bring-title">这些不用带:</h4>
|
||
<p v-for="(item, i) in sPacking.dontBring" :key="i" class="dont-bring-item">✗ {{ item }}</p>
|
||
</div>
|
||
|
||
<div v-if="sPacking.tips" class="guide-tips">
|
||
<p v-for="(tip, i) in sPacking.tips" :key="i" class="guide-tip">{{ tip }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 04 活动安全 -->
|
||
<div :id="sActivities.id" class="guide-section">
|
||
<CommonSectionTitle
|
||
:title="sActivities.title"
|
||
:subtitle="sActivities.subtitle"
|
||
align="left"
|
||
/>
|
||
<p class="guide-text">{{ sActivities.content }}</p>
|
||
<div class="activities-grid">
|
||
<GuidesGuideActivityCard
|
||
v-for="activity in sActivities.activities"
|
||
:key="activity.name"
|
||
:activity="activity"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 05 实用信息 -->
|
||
<div :id="sPractical.id" class="guide-section">
|
||
<CommonSectionTitle
|
||
:title="sPractical.title"
|
||
:subtitle="sPractical.subtitle"
|
||
align="left"
|
||
/>
|
||
<div class="topics-grid">
|
||
<div v-for="topic in sPractical.topics" :key="topic.name" class="topic-card">
|
||
<h4 class="topic-name">{{ topic.name }}</h4>
|
||
<p class="topic-content">{{ topic.content }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 推荐App -->
|
||
<div class="apps-block">
|
||
<h3 class="apps-title">出发前手机里装什么</h3>
|
||
<div class="apps-list">
|
||
<div v-for="app in sPractical.apps" :key="app.category" class="app-item">
|
||
<span class="app-category">{{ app.category }}</span>
|
||
<span class="app-names">{{ app.names }}</span>
|
||
</div>
|
||
</div>
|
||
<p class="apps-note">{{ sPractical.appNote }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 06 幼童指南 -->
|
||
<div :id="sToddler.id" class="guide-section">
|
||
<CommonSectionTitle
|
||
:title="sToddler.title"
|
||
:subtitle="sToddler.subtitle"
|
||
align="left"
|
||
/>
|
||
<p class="guide-text">{{ sToddler.content }}</p>
|
||
<GuidesGuideToddlerSection :data="sToddler" />
|
||
</div>
|
||
|
||
<!-- 内链闭环 -->
|
||
<div class="guide-cta">
|
||
<h2>准备好了吗?</h2>
|
||
<p>草原的风,已经在等您了。</p>
|
||
<div class="guide-cta-links">
|
||
<CommonInternalLink to="/products" text="查看4-7天亲子游产品" />
|
||
<CommonInternalLink to="/faq" text="查看常见问答" />
|
||
<CommonInternalLink to="/contact" text="联系定制师获取方案" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { useBreadcrumbSchema, useHowToSchema } from '~/composables/useJsonLd'
|
||
|
||
const { data: guidesData } = await usePublicApi('guides')
|
||
|
||
const seo = useSEO('guides')
|
||
|
||
useHowToSchema(guidesData.value?.sections)
|
||
useBreadcrumbSchema([
|
||
{ text: '首页', to: '/' },
|
||
{ text: '出行指南' },
|
||
])
|
||
|
||
// 按 id 索引 sections,避免硬编码数组下标
|
||
const sections = computed(() => guidesData.value?.sections || [])
|
||
function getSection(id) {
|
||
return sections.value.find(s => s.id === id)
|
||
}
|
||
const sDocuments = computed(() => getSection('documents'))
|
||
const sClothing = computed(() => getSection('clothing'))
|
||
const sPacking = computed(() => getSection('packing'))
|
||
const sActivities = computed(() => getSection('activities'))
|
||
const sPractical = computed(() => getSection('practical'))
|
||
const sToddler = computed(() => getSection('toddler'))
|
||
|
||
const activeSection = ref('')
|
||
|
||
function scrollToSection(id) {
|
||
activeSection.value = id
|
||
if (import.meta.client) {
|
||
const el = document.getElementById(id)
|
||
if (el) {
|
||
el.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.guide-intro {
|
||
font-size: @font-size-md;
|
||
color: @color-text-secondary;
|
||
max-width: 700px;
|
||
margin: 0 auto @space-xl;
|
||
text-align: center;
|
||
line-height: @line-height-base;
|
||
}
|
||
|
||
.guide-section {
|
||
margin-bottom: @space-3xl;
|
||
padding-top: @space-lg;
|
||
scroll-margin-top: 80px;
|
||
}
|
||
|
||
.guide-text {
|
||
font-size: @font-size-base;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-base;
|
||
margin-bottom: @space-lg;
|
||
}
|
||
|
||
.guide-tips {
|
||
margin-top: @space-lg;
|
||
}
|
||
|
||
.guide-tip {
|
||
padding: @space-sm @space-md;
|
||
background: @color-primary-bg;
|
||
border-radius: @border-radius-sm;
|
||
font-size: @font-size-sm;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-base;
|
||
margin-bottom: @space-sm;
|
||
}
|
||
|
||
// Clothing
|
||
.clothing-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: @space-md;
|
||
}
|
||
|
||
// Packing
|
||
.packing-categories {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: @space-xl;
|
||
}
|
||
|
||
.packing-cat-name {
|
||
font-size: @font-size-md;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-primary;
|
||
margin: 0 0 @space-xs;
|
||
}
|
||
|
||
.packing-cat-note {
|
||
font-size: @font-size-sm;
|
||
color: @color-accent;
|
||
margin-bottom: @space-md;
|
||
}
|
||
|
||
.provided-block {
|
||
margin-top: @space-xl;
|
||
padding: @space-lg;
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-top: 3px solid @color-primary;
|
||
border-radius: @border-radius-md;
|
||
}
|
||
|
||
.provided-title {
|
||
font-size: @font-size-base;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-primary;
|
||
margin: 0 0 @space-sm;
|
||
}
|
||
|
||
.provided-desc {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-secondary;
|
||
margin-bottom: @space-sm;
|
||
}
|
||
|
||
.provided-items {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-muted;
|
||
line-height: @line-height-loose;
|
||
}
|
||
|
||
.dont-bring {
|
||
margin-top: @space-lg;
|
||
}
|
||
|
||
.dont-bring-title {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-muted;
|
||
margin: 0 0 @space-sm;
|
||
}
|
||
|
||
.dont-bring-item {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-muted;
|
||
line-height: @line-height-loose;
|
||
margin: 0;
|
||
}
|
||
|
||
// Activities
|
||
.activities-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: @space-md;
|
||
}
|
||
|
||
// Practical
|
||
.topics-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: @space-md;
|
||
|
||
.respond-md({
|
||
grid-template-columns: 1fr 1fr;
|
||
});
|
||
}
|
||
|
||
.topic-card {
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-md;
|
||
padding: @space-lg;
|
||
}
|
||
|
||
.topic-name {
|
||
font-size: @font-size-base;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-primary;
|
||
margin: 0 0 @space-sm;
|
||
}
|
||
|
||
.topic-content {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-base;
|
||
margin: 0;
|
||
}
|
||
|
||
// Apps
|
||
.apps-block {
|
||
margin-top: @space-xl;
|
||
}
|
||
|
||
.apps-title {
|
||
font-size: @font-size-md;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-primary;
|
||
margin: 0 0 @space-md;
|
||
}
|
||
|
||
.apps-list {
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-md;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.app-item {
|
||
display: flex;
|
||
gap: @space-md;
|
||
padding: @space-md @space-lg;
|
||
border-bottom: 1px solid @color-bg-gray;
|
||
font-size: @font-size-sm;
|
||
|
||
&:last-child {
|
||
border-bottom: none;
|
||
}
|
||
}
|
||
|
||
.app-category {
|
||
font-weight: @font-weight-medium;
|
||
color: @color-primary;
|
||
min-width: 80px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.app-names {
|
||
color: @color-text-secondary;
|
||
}
|
||
|
||
.apps-note {
|
||
margin-top: @space-sm;
|
||
font-size: @font-size-sm;
|
||
color: @color-text-muted;
|
||
}
|
||
|
||
// CTA
|
||
.guide-cta {
|
||
text-align: center;
|
||
margin-top: @space-3xl;
|
||
padding: @space-2xl;
|
||
background: @color-primary-bg;
|
||
border-radius: @border-radius-lg;
|
||
|
||
h2 {
|
||
margin-bottom: @space-sm;
|
||
}
|
||
|
||
p {
|
||
color: @color-text-secondary;
|
||
margin-bottom: @space-xl;
|
||
}
|
||
}
|
||
|
||
.guide-cta-links {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: @space-xl;
|
||
flex-wrap: wrap;
|
||
}
|
||
</style>
|