hulai-website/pages/courses.vue

766 行
19 KiB
Vue

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

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

<template>
<div class="page-courses">
<LayoutPageHero
:title="seo.h1"
:subtitle="heroes?.courses || '小蒙马亲子研学·6大课程模块·3-12岁分龄设计'"
:breadcrumbs="[{ text: '小蒙马亲子游学营', to: '/summer-camp' }, { text: '研学课程体系' }]"
background-image="/images/summer-camp/birch-group.jpg"
/>
<!-- TL;DR -->
<section class="page-tldr" aria-label="研学课程摘要">
<div class="container">
<div class="page-tldr-inner">
<p>小蒙马亲子研学课程体系6大课程模块3-12岁分龄设计融合草原生态蒙古文化艺术创作户外探索等主题寓教于乐</p>
</div>
</div>
</section>
<!-- 教育理念 -->
<section v-if="courses" class="section section--warm reveal">
<div class="container">
<div class="philosophy-banner">
<div class="philosophy-text">
<h2 class="philosophy-headline">{{ courses.philosophy.headline }}</h2>
<p class="philosophy-tagline">{{ courses.philosophy.tagline }}</p>
<p class="philosophy-desc">{{ courses.philosophy.description }}</p>
</div>
<div class="philosophy-values">
<div
v-for="v in courses.philosophy.values"
:key="v.label"
class="philosophy-value-item"
>
<span class="philosophy-value-label">{{ v.label }}</span>
<p class="philosophy-value-desc">{{ v.desc }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- 年龄分层选择器 -->
<section v-if="courses" class="section reveal">
<div class="container">
<CommonSectionTitle
:title="st('courses.byAge', '按年龄找适合的课程')"
:subtitle="st('courses.byAge', '不同年龄段,有不同的学习目标和课程深度', 'subtitle')"
/>
<div class="age-tabs">
<button
v-for="group in courses.ageGroups"
:key="group.id"
class="age-tab"
:class="{ 'age-tab--active': activeGroup === group.id }"
@click="activeGroup = group.id"
>
<span class="age-tab-emoji">{{ group.emoji }}</span>
<span class="age-tab-label">{{ group.label }}</span>
</button>
</div>
<div v-if="activeGroupData" class="age-content">
<div class="age-content-header">
<h3 class="age-content-headline">{{ activeGroupData.headline }}</h3>
<span class="age-focus-badge">{{ activeGroupData.focus }}</span>
</div>
<p class="age-content-desc">{{ activeGroupData.description }}</p>
<div class="age-content-body">
<div class="age-highlights">
<h4 class="age-highlights-title">这个年龄段能做什么</h4>
<ul class="age-highlights-list">
<li v-for="h in activeGroupData.highlights" :key="h" class="age-highlight-item">
{{ h }}
</li>
</ul>
</div>
<div class="age-modules">
<h4 class="age-modules-title">推荐课程模块</h4>
<div class="age-module-tags">
<span
v-for="mod in activeGroupModules"
:key="mod.id"
class="age-module-tag"
>
{{ mod.icon }} {{ mod.name }}
</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 中间 CTA -->
<CommonSectionCTA
title="想了解小蒙马亲子游学营详情?"
description="7天6晚亲子研学,每期限8组家庭,现在咨询了解营期安排"
:links="[
{ to: '/summer-camp', text: '查看小蒙马亲子游学营' },
{ to: '/contact', text: '咨询报名' },
]"
/>
<!-- 六大课程模块 -->
<section v-if="courses" class="section section--gray reveal">
<div class="container">
<CommonSectionTitle
:title="st('courses.modules', '6大课程模块')"
:subtitle="st('courses.modules', '每个模块有明确的学习目标、活动设计和教育理念', 'subtitle')"
/>
<div class="modules-grid">
<div
v-for="mod in courses.modules"
:key="mod.id"
class="module-card"
:class="{ 'module-card--expanded': expandedModules[mod.id] }"
>
<button class="module-card-header" @click="toggleModule(mod.id)">
<div class="module-card-title-row">
<span class="module-icon">{{ mod.icon }}</span>
<div class="module-title-group">
<h3 class="module-name">{{ mod.name }}</h3>
<div class="module-meta">
<span class="module-meta-item">{{ mod.ageRange }}</span>
<span class="module-meta-sep">·</span>
<span class="module-meta-item">{{ mod.duration }}</span>
</div>
</div>
</div>
<p class="module-short-desc">{{ mod.shortDesc }}</p>
<span class="module-toggle">{{ expandedModules[mod.id] ? '收起' : '展开详情' }}</span>
</button>
<div v-show="expandedModules[mod.id]" class="module-card-body">
<div class="module-section">
<h4 class="module-section-title">学习目标</h4>
<ul class="module-list">
<li v-for="obj in mod.objectives" :key="obj">{{ obj }}</li>
</ul>
</div>
<div class="module-section">
<h4 class="module-section-title">活动内容</h4>
<ul class="module-list">
<li v-for="act in mod.activities" :key="act">{{ act }}</li>
</ul>
</div>
<blockquote class="module-philosophy">
{{ mod.philosophy }}
</blockquote>
</div>
</div>
</div>
</div>
</section>
<!-- 师资介绍仅在有真实导师数据时显示 -->
<section v-if="courses && realInstructors.length" class="section reveal">
<div class="container">
<CommonSectionTitle
:title="st('courses.mentors', '课程导师团队')"
:subtitle="st('courses.mentors', '自然教育导师 + 草原领队,全程陪同每一位孩子', 'subtitle')"
/>
<div class="instructor-cards">
<div v-for="inst in realInstructors" :key="inst.name" class="instructor-card">
<div v-if="inst.avatar" class="instructor-avatar">
<img :src="inst.avatar" :alt="inst.name" loading="lazy" />
</div>
<div class="instructor-info">
<p class="instructor-name">{{ inst.name }}</p>
<p class="instructor-title">{{ inst.title }}</p>
<p class="instructor-cred">{{ inst.credentials }}</p>
<p class="instructor-bio">{{ inst.bio }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- 家长常见问题 -->
<section v-if="courses" class="section section--warm">
<div class="container">
<CommonSectionTitle :title="st('courses.faq', '家长常见问题')" />
<div class="courses-faq">
<FaqItem
v-for="(item, i) in courses.faq"
:key="item.question"
:question="item.question"
:answer="item.answer"
:initial-open="i === 0"
/>
</div>
</div>
</section>
<!-- 底部 CTA -->
<CommonSectionCTA
title="准备好让孩子在草原上上课了吗?"
description="报名小蒙马亲子游学营,或联系我们了解定制研学方案"
:links="[
{ to: '/summer-camp', text: '小蒙马亲子游学营详情' },
{ to: '/customize', text: '定制研学方案' },
]"
/>
</div>
</template>
<script setup>
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const { data: courses } = await usePublicApi('courses')
const { data: heroes } = await usePublicApi('heroes')
const st = await useSectionTitles()
const seo = useSEO('courses')
useScrollReveal()
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '小蒙马亲子游学营', to: '/summer-camp' },
{ text: '研学课程体系' },
])
// JSON-LD: EducationalOrganization + Course schemas
useHead({
script: [
{
type: 'application/ld+json',
innerHTML: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'EducationalOrganization',
name: '小蒙马研学·呼籁旅行',
url: 'https://1814.love/courses',
description: '小蒙马研学是呼籁旅行旗下亲子研学品牌,以呼伦贝尔草原为天然课堂,提供3-12岁分龄自然教育课程体系,含草原生态、马术体验、游牧文化、自然艺术、团队成长、LNT环保六大模块。',
address: {
'@type': 'PostalAddress',
addressLocality: '呼伦贝尔市',
addressRegion: '内蒙古自治区',
addressCountry: 'CN',
},
parentOrganization: {
'@type': 'Organization',
name: '呼籁旅行',
url: 'https://1814.love',
},
hasOfferCatalog: {
'@type': 'OfferCatalog',
name: '小蒙马研学课程体系',
itemListElement: (courses.value?.modules || []).map((mod, i) => ({
'@type': 'Course',
position: i + 1,
name: mod.name,
description: mod.shortDesc,
provider: {
},
hasCourseInstance: {
'@type': 'CourseInstance',
courseMode: 'onsite',
location: {
'@type': 'Place',
name: '呼伦贝尔草原',
},
},
})),
},
}),
},
],
})
// 年龄分层选择
const realInstructors = computed(() =>
(courses.value?.instructors?.list || []).filter(i => i?.name && !String(JSON.stringify(i)).includes('⚠️'))
)
const activeGroup = ref('child')
const activeGroupData = computed(() => (courses.value?.ageGroups || []).find((g) => g.id === activeGroup.value))
const activeGroupModules = computed(() =>
activeGroupData.value
? (courses.value?.modules || []).filter((m) => activeGroupData.value.recommendedModules.includes(m.id))
: [],
)
// 模块展开/收起
const expandedModules = ref({})
function toggleModule(id) {
expandedModules.value = { ...expandedModules.value, [id]: !expandedModules.value[id] }
}
</script>
<style lang="less" scoped>
// ===== 教育理念 =====
.philosophy-banner {
display: flex;
flex-direction: column;
gap: @space-2xl;
.respond-lg({
flex-direction: row;
align-items: flex-start;
gap: @space-3xl;
});
}
.philosophy-text {
flex: 1;
}
.philosophy-headline {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-sm;
}
.philosophy-tagline {
font-size: @font-size-lg;
color: @color-text-secondary;
font-weight: @font-weight-medium;
margin-bottom: @space-md;
}
.philosophy-desc {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin: 0;
}
.philosophy-values {
flex: 1;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-md;
}
.philosophy-value-item {
padding: @space-lg;
background: @color-bg-white;
border-radius: @border-radius-lg;
border: 1px solid @color-border;
transition: box-shadow @transition-fast;
&:hover {
box-shadow: @shadow-md;
}
}
.philosophy-value-label {
display: inline-block;
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-primary;
background: @color-primary-bg;
padding: @space-xs @space-sm;
border-radius: @border-radius-full;
margin-bottom: @space-sm;
}
.philosophy-value-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
// ===== 年龄分层 =====
.age-tabs {
display: flex;
flex-direction: column;
gap: @space-sm;
margin-bottom: @space-xl;
.respond-md({
flex-direction: row;
gap: @space-md;
});
}
.age-tab {
flex: 1;
display: flex;
align-items: center;
gap: @space-sm;
padding: @space-md @space-lg;
border: 2px solid @color-border;
border-radius: @border-radius-lg;
background: @color-bg-white;
cursor: pointer;
text-align: left;
transition: border-color @transition-fast, background @transition-fast, box-shadow @transition-fast;
&:hover {
border-color: @color-primary-lighter;
box-shadow: @shadow-sm;
}
&--active {
border-color: @color-primary;
background: @color-primary-bg;
box-shadow: @shadow-md;
}
}
.age-tab-emoji {
font-size: 24px;
line-height: 1;
flex-shrink: 0;
}
.age-tab-label {
font-size: @font-size-sm;
font-weight: @font-weight-medium;
color: @color-text-primary;
.age-tab--active & {
color: @color-primary;
}
}
.age-content {
padding: @space-xl;
background: @color-bg-white;
border-radius: @border-radius-lg;
border: 1px solid @color-border;
}
.age-content-header {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: @space-sm;
margin-bottom: @space-md;
}
.age-content-headline {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-primary;
margin: 0;
flex: 1;
min-width: 200px;
}
.age-focus-badge {
font-size: @font-size-xs;
color: @color-accent;
background: fade(@color-accent, 12%);
padding: @space-xs @space-md;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
white-space: nowrap;
}
.age-content-desc {
font-size: @font-size-base;
color: @color-text-secondary;
line-height: @line-height-loose;
margin-bottom: @space-xl;
}
.age-content-body {
display: grid;
grid-template-columns: 1fr;
gap: @space-xl;
.respond-md({
grid-template-columns: 3fr 2fr;
});
}
.age-highlights-title,
.age-modules-title {
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-md;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.age-highlights-list {
display: flex;
flex-direction: column;
gap: @space-sm;
}
.age-highlight-item {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
padding-left: @space-lg;
position: relative;
&::before {
content: '✓';
position: absolute;
left: 0;
color: @color-primary;
font-weight: @font-weight-bold;
}
}
.age-module-tags {
display: flex;
flex-wrap: wrap;
gap: @space-sm;
}
.age-module-tag {
font-size: @font-size-sm;
color: @color-primary;
background: @color-primary-bg;
border: 1px solid fadeout(@color-primary, 70%);
padding: @space-xs @space-md;
border-radius: @border-radius-full;
font-weight: @font-weight-medium;
}
// ===== 课程模块 =====
.modules-grid {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.module-card {
background: @color-bg-white;
border-radius: @border-radius-lg;
border: 1px solid @color-border;
overflow: hidden;
transition: border-color @transition-fast, box-shadow @transition-fast;
&:hover,
&--expanded {
border-color: @color-primary-lighter;
box-shadow: @shadow-md;
}
}
.module-card-header {
width: 100%;
padding: @space-xl;
background: none;
border: none;
cursor: pointer;
text-align: left;
display: flex;
flex-direction: column;
gap: @space-sm;
}
.module-card-title-row {
display: flex;
align-items: flex-start;
gap: @space-md;
}
.module-icon {
font-size: 28px;
line-height: 1;
flex-shrink: 0;
margin-top: 2px;
}
.module-title-group {
flex: 1;
}
.module-name {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin: 0 0 @space-xs;
line-height: @line-height-tight;
}
.module-meta {
display: flex;
align-items: center;
gap: @space-xs;
flex-wrap: wrap;
}
.module-meta-item {
font-size: @font-size-xs;
color: @color-text-muted;
}
.module-meta-sep {
color: @color-text-muted;
font-size: @font-size-xs;
}
.module-short-desc {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
.module-toggle {
font-size: @font-size-xs;
color: @color-primary;
font-weight: @font-weight-medium;
align-self: flex-start;
}
.module-card-body {
padding: 0 @space-xl @space-xl;
border-top: 1px solid @color-border;
padding-top: @space-lg;
}
.module-section {
margin-bottom: @space-lg;
&:last-of-type {
margin-bottom: @space-md;
}
}
.module-section-title {
font-size: @font-size-xs;
font-weight: @font-weight-bold;
color: @color-text-muted;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: @space-sm;
}
.module-list {
display: flex;
flex-direction: column;
gap: @space-xs;
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: @color-primary;
font-weight: bold;
}
}
}
.module-philosophy {
margin: 0;
padding: @space-md @space-lg;
background: @color-primary-bg;
border-left: 3px solid @color-primary;
border-radius: 0 @border-radius-md @border-radius-md 0;
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-loose;
font-style: italic;
}
// ===== 师资占位 =====
.instructor-placeholder-box {
display: flex;
align-items: flex-start;
gap: @space-md;
padding: @space-lg @space-xl;
background: #fff8e6;
border: 1px solid #f0c060;
border-radius: @border-radius-lg;
margin-bottom: @space-xl;
}
.instructor-placeholder-icon {
font-size: 24px;
flex-shrink: 0;
line-height: 1;
}
.instructor-placeholder-title {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: #a06000;
margin: 0 0 @space-xs;
}
.instructor-placeholder-desc {
font-size: @font-size-sm;
color: #7a5000;
margin: 0;
line-height: @line-height-base;
}
.instructor-cards {
display: grid;
grid-template-columns: 1fr;
gap: @space-lg;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
}
.instructor-card {
display: flex;
gap: @space-lg;
padding: @space-xl;
background: @color-bg-gray;
border-radius: @border-radius-lg;
border: 1px dashed @color-border;
}
.instructor-avatar-placeholder {
width: 64px;
height: 64px;
border-radius: 50%;
background: @color-border;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
}
.instructor-name {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-secondary;
margin: 0 0 @space-xs;
}
.instructor-title {
font-size: @font-size-sm;
color: @color-primary;
font-weight: @font-weight-medium;
margin: 0 0 @space-xs;
}
.instructor-cred,
.instructor-bio {
font-size: @font-size-sm;
color: @color-text-muted;
margin: 0 0 @space-xs;
line-height: @line-height-base;
}
// ===== FAQ =====
.courses-faq {
max-width: 800px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: @space-md;
}
</style>