hulai-website/pages/winter-camp.vue
Mimingguang ee078b61bc feat: 后台管理系统完整重构 + 前后台数据联通
- 后台全面引入 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>
2026-03-24 17:48:02 +08:00

648 行
16 KiB
Vue

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

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

<template>
<div class="page-winter-camp">
<LayoutPageHero
:title="seo.h1"
subtitle="呼伦贝尔雪原亲子冬令营·7天6晚·每期8组家庭"
:breadcrumbs="[{ text: '小蒙马冬季营' }]"
/>
<!-- TL;DR -->
<section class="page-tldr" aria-label="冬令营摘要">
<div class="container">
<div class="page-tldr-inner">
<p>小蒙马冬令营7天6晚呼伦贝尔雪原亲子冬令营每期仅限8组家庭冰雪越野雪地骑马冬捕体验蒙古包住宿零下30度的童话世界</p>
</div>
</div>
</section>
<!-- 风景大图 -->
<div class="scenic-banner">
<img src="/images/seasons/winter-yurt.jpg" alt="冬季呼伦贝尔雪原蒙古包" loading="eager" />
</div>
<!-- 核心定位 -->
<section v-if="camp" class="section">
<div class="container">
<div class="camp-hero">
<div class="camp-mascot">
<img
:src="images.mascot.xiaomengma"
alt="小蒙马 - 冬季亲子营吉祥物"
width="200"
height="200"
loading="lazy"
/>
</div>
<div class="camp-intro">
<h2 class="camp-slogan">一片干净的雪原,一段只属于你们的记忆</h2>
<p class="camp-positioning">{{ camp.positioning }}</p>
<div class="camp-meta">
<span class="camp-meta-item">{{ camp.days }}天{{ camp.nights }}晚</span>
<span class="camp-meta-item">每期{{ camp.maxFamilies }}组家庭</span>
<span class="camp-meta-item">今年仅{{ camp.totalSessions }}期</span>
<span class="camp-meta-item">{{ camp.ageRange }}亲子</span>
</div>
</div>
</div>
</div>
</section>
<!-- 为什么选呼伦贝尔 -->
<section v-if="camp" class="section section--ice reveal">
<div class="container">
<CommonSectionTitle title="为什么是呼伦贝尔?" subtitle="不是更冷,而是更纯粹" />
<div class="why-block">
<p>{{ camp.whyHulunbuir }}</p>
</div>
</div>
</section>
<!-- 亲子营优势 -->
<section v-if="camp" class="section reveal">
<div class="container">
<CommonSectionTitle title="亲子营的优势" subtitle="和独立小包团相比" />
<div class="advantages-grid">
<div v-for="(adv, i) in camp.campAdvantages" :key="i" class="advantage-card">
<span class="advantage-check">✓</span>
<p>{{ adv }}</p>
</div>
</div>
</div>
</section>
<!-- 咨询 CTA -->
<CommonSectionCTA
title="想了解营期安排和费用?"
description="添加微信客服,获取最新营期时间和报名方式"
:links="[{ to: '/contact', text: '咨询小蒙马冬季营' }]"
/>
<!-- 住宿保障 -->
<section v-if="camp" class="section section--warm reveal">
<div class="container">
<CommonSectionTitle title="住宿安排" subtitle="住得舒服,玩得才能惬意" />
<div class="hotels-grid">
<div v-for="(hotel, i) in camp.hotels" :key="i" class="hotel-card">
<div class="hotel-star">{{ hotel.star }}</div>
<h3 class="hotel-name">{{ hotel.name }}</h3>
<span class="hotel-nights">{{ hotel.nights }}</span>
<p class="hotel-desc">{{ hotel.description }}</p>
</div>
</div>
</div>
</section>
<!-- 7天行程 -->
<section v-if="camp" class="section reveal">
<div class="container">
<CommonSectionTitle title="7天行程概览" subtitle="不赶路、不折腾,每一天都有记忆点" />
<div class="itinerary-list">
<details v-for="day in camp.itinerary" :key="day.day" class="itinerary-item" :open="day.day <= 2">
<summary class="itinerary-header">
<span class="itinerary-day">D{{ day.day }}</span>
<span class="itinerary-title">{{ day.title }}</span>
<span v-if="day.hotel" class="itinerary-hotel">🏨 {{ day.hotel }}</span>
</summary>
<div class="itinerary-body">
<p class="itinerary-summary">{{ day.summary }}</p>
<ul class="itinerary-highlights">
<li v-for="(h, j) in day.highlights" :key="j">{{ h }}</li>
</ul>
</div>
</details>
</div>
</div>
</section>
<!-- 冬季风景 -->
<div class="scenic-duo reveal">
<img src="/images/seasons/winter-yurt.jpg" alt="冬季雪原蒙古包日落" loading="lazy" />
<img src="/images/seasons/winter-frost.jpg" alt="大兴安岭雾凇森林" loading="lazy" />
</div>
<!-- 摄影师 -->
<section v-if="camp" class="section section--ice reveal">
<div class="container">
<CommonSectionTitle title="随行摄影师" subtitle="不错过每一个自然的瞬间" />
<div class="photographer-block">
<h3>摄影师{{ camp.photographer.name }}</h3>
<p>{{ camp.photographer.description }}</p>
<p class="photographer-special">📍 {{ camp.photographer.specialDay2 }}</p>
</div>
</div>
</section>
<!-- 穿衣指南 -->
<section v-if="camp" class="section reveal">
<div class="container">
<CommonSectionTitle title="冬季穿衣指南" subtitle="穿对了,冷也是一种享受" />
<div class="clothing-grid">
<div class="clothing-card">
<h4>👕 上身</h4>
<p>{{ camp.winterClothing.upper }}</p>
</div>
<div class="clothing-card">
<h4>👖 下身</h4>
<p>{{ camp.winterClothing.lower }}</p>
</div>
<div class="clothing-card">
<h4>👟 鞋子</h4>
<p>{{ camp.winterClothing.shoes }}</p>
</div>
<div class="clothing-card">
<h4>🧣 配件</h4>
<p>{{ camp.winterClothing.accessories }}</p>
</div>
</div>
</div>
</section>
<!-- 随行服务 -->
<section v-if="camp" class="section section--warm reveal">
<div class="container">
<CommonSectionTitle title="随行服务配置" subtitle="比夏季更谨慎、更细致" />
<div class="service-grid">
<div v-for="(s, i) in camp.serviceConfig" :key="i" class="service-item">
<span class="service-icon">👤</span>
<span>{{ s }}</span>
</div>
</div>
</div>
</section>
<!-- 家长FAQ -->
<section v-if="camp?.faq" class="section section--gray">
<div class="container">
<CommonSectionTitle title="家长常见问题" />
<div class="camp-faq">
<FaqItem
v-for="(item, i) in camp.faq"
:key="item.question"
:question="item.question"
:answer="item.answer"
:initial-open="i === 0"
/>
</div>
</div>
</section>
<!-- 结语 -->
<section v-if="camp" class="section">
<div class="container">
<div class="closing-note">
<p>{{ camp.closingNote }}</p>
</div>
</div>
</section>
<!-- 底部 CTA -->
<CommonSectionCTA
title="给孩子一次真正的冬天"
description="小蒙马冬季亲子营,今年仅开3期,全力以赴"
:links="[
{ to: '/contact', text: '咨询冬季营' },
{ to: '/summer-camp', text: '了解夏季小蒙马' },
]"
/>
</div>
</template>
<script setup>
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const { data: camp } = await usePublicApi('winter-camp')
const { data: images } = await usePublicApi('images')
const seo = useSEO('winter-camp', {
title: '小蒙马冬季亲子营 - 呼伦贝尔雪原冬令营·7天6晚·每期8组家庭 - 呼籁旅行',
description: '呼籁旅行小蒙马冬季亲子营,7天6晚呼伦贝尔+阿尔山雪原冬令营。莫日格勒大雪原、冬季那达慕、狼园探秘、雪地骑马、阿尔山滑雪、不冻河漂流、私汤温泉。每期8组家庭,5-12岁亲子同行。',
h1: '小蒙马冬季亲子营',
})
useScrollReveal()
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '小蒙马冬季营' },
])
</script>
<style lang="less" scoped>
// Hero 区
.camp-hero {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: @space-xl;
max-width: 800px;
margin: 0 auto;
.respond-md({
flex-direction: row;
text-align: left;
gap: @space-2xl;
});
}
.camp-mascot {
flex-shrink: 0;
img {
width: 160px;
height: auto;
filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.08));
}
}
.camp-intro {
flex: 1;
}
.camp-slogan {
font-size: @font-size-2xl;
color: #3B82F6;
font-weight: @font-weight-bold;
margin-bottom: @space-md;
}
.camp-positioning {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-lg;
}
.camp-meta {
display: flex;
flex-wrap: wrap;
gap: @space-sm;
}
.camp-meta-item {
font-size: @font-size-sm;
color: #3B82F6;
background: #EFF6FF;
padding: @space-xs @space-md;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
}
// 为什么呼伦贝尔
.section--ice {
background: #F0F9FF;
}
.why-block {
max-width: 800px;
margin: 0 auto;
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
padding: @space-xl;
background: @color-bg-white;
border-radius: @border-radius-lg;
border-left: 4px solid #3B82F6;
}
// 亲子营优势
.advantages-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
max-width: 800px;
margin: 0 auto;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
}
.advantage-card {
display: flex;
gap: @space-md;
align-items: flex-start;
padding: @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
transition: box-shadow @transition-fast;
&:hover {
box-shadow: @shadow-md;
}
p {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
}
.advantage-check {
flex-shrink: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background: #3B82F6;
color: #fff;
border-radius: 50%;
font-size: @font-size-xs;
font-weight: @font-weight-bold;
}
// 酒店
.hotels-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-md({
grid-template-columns: repeat(3, 1fr);
});
}
.hotel-card {
padding: @space-xl;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
text-align: center;
transition: box-shadow @transition-base, transform @transition-base;
&:hover {
box-shadow: @shadow-md;
transform: translateY(-2px);
}
}
.hotel-star {
display: inline-block;
padding: @space-xs @space-md;
font-size: @font-size-xs;
font-weight: @font-weight-bold;
color: #B45309;
background: #FEF3C7;
border-radius: @border-radius-full;
margin-bottom: @space-sm;
}
.hotel-name {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-xs;
}
.hotel-nights {
display: block;
font-size: @font-size-sm;
color: #3B82F6;
font-weight: @font-weight-medium;
margin-bottom: @space-md;
}
.hotel-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
text-align: left;
}
// 行程
.itinerary-list {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: @space-sm;
}
.itinerary-item {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
overflow: hidden;
transition: box-shadow @transition-fast;
&[open] {
box-shadow: @shadow-md;
border-color: #93C5FD;
}
}
.itinerary-header {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: @space-sm;
padding: @space-md @space-lg;
cursor: pointer;
transition: background @transition-fast;
&:hover {
background: #F0F9FF;
}
}
.itinerary-day {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: #3B82F6;
min-width: 36px;
}
.itinerary-title {
flex: 1;
font-size: @font-size-base;
font-weight: @font-weight-medium;
color: @color-text-primary;
}
.itinerary-hotel {
font-size: @font-size-xs;
color: @color-text-muted;
}
.itinerary-body {
padding: 0 @space-lg @space-lg;
padding-left: calc(@space-lg + 36px + @space-sm);
}
.itinerary-summary {
font-size: @font-size-sm;
color: #3B82F6;
font-weight: @font-weight-medium;
margin-bottom: @space-md;
}
.itinerary-highlights {
display: flex;
flex-direction: column;
gap: @space-sm;
li {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
padding-left: @space-md;
position: relative;
&::before {
content: '❄';
position: absolute;
left: 0;
color: #3B82F6;
font-size: 10px;
}
}
}
// 摄影师
.photographer-block {
max-width: 700px;
margin: 0 auto;
text-align: center;
h3 {
font-size: @font-size-xl;
color: @color-text-primary;
margin-bottom: @space-md;
}
p {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-md;
}
}
.photographer-special {
padding: @space-md @space-lg;
background: @color-bg-white;
border-radius: @border-radius-md;
border-left: 3px solid #3B82F6;
text-align: left;
}
// 穿衣指南
.clothing-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(4, 1fr);
});
}
.clothing-card {
padding: @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
h4 {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-sm;
}
p {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
}
}
// 服务配置
.service-grid {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: @space-md;
max-width: 800px;
margin: 0 auto;
}
.service-item {
display: flex;
align-items: center;
gap: @space-sm;
padding: @space-sm @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-full;
font-size: @font-size-sm;
color: @color-text-secondary;
}
.service-icon {
font-size: @font-size-base;
}
// 结语
.closing-note {
max-width: 700px;
margin: 0 auto;
text-align: center;
font-size: @font-size-lg;
color: @color-text-secondary;
line-height: @line-height-loose;
font-style: italic;
padding: @space-xl;
border-top: 1px solid @color-border;
border-bottom: 1px solid @color-border;
}
// FAQ
.camp-faq {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: @space-md;
}
// 风景图
.scenic-banner {
width: 100%;
overflow: hidden;
max-height: 480px;
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.scenic-duo {
display: grid;
grid-template-columns: 1fr;
gap: 4px;
max-height: 360px;
overflow: hidden;
.respond-md({
grid-template-columns: 1fr 1fr;
});
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
max-height: 360px;
}
}
</style>