feat: 官网代码全量更新

覆盖最新版本代码,包含:
- 新增品牌百科、公司百科页面及CMS管理
- 新增微信引导组件(WechatCTA)
- 新增品牌/公司图片资源
- 全站组件/页面/数据/样式更新

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
这个提交包含在:
wx
2026-04-16 13:21:11 +08:00
共同撰写人 Claude Opus 4.6
父节点 bd0518eda6
当前提交 ac90205395
共修改 113 个文件,包含 4463 行新增和 405 行删除
+6 -5
查看文件
@@ -17,7 +17,7 @@
</section>
<!-- 1. 品牌故事:为什么只做呼伦贝尔 -->
<AboutBrandStory v-if="about" :story="about.story" class="reveal" />
<AboutBrandStory v-if="about" :story="about.story" :images="images" class="reveal" />
<!-- 2. 服务旅程:跟呼籁出行是什么体验 -->
<section v-if="about" class="section section--gray reveal" aria-label="服务旅程">
@@ -36,10 +36,10 @@
</section>
<!-- 3. 团队介绍:谁在照顾你 -->
<AboutTeamIntro v-if="about" :team="about.team" class="reveal" />
<AboutTeamIntro v-if="about" :team="about.team" :images="images" class="reveal" />
<!-- 4. 小红书 -->
<AboutXiaohongshuShowcase v-if="about" :xhs="about.xiaohongshu" class="reveal" />
<AboutXiaohongshuShowcase v-if="about" :xhs="about.xiaohongshu" :images="images" class="reveal" />
<!-- 6. 品牌发展历程 -->
<section v-if="about" class="section reveal" aria-label="品牌发展历程">
@@ -64,7 +64,7 @@
<!-- 8. 品牌资产:知识产权 + 吉祥物 -->
<AboutCopyrightList v-if="about" :copyrights="about.copyrights" :trademark="about.trademark" class="reveal" />
<AboutMascotShowcase class="reveal" />
<AboutMascotShowcase :images="images" class="reveal" />
<!-- 9. 企业文化 -->
<section v-if="about" class="section reveal" aria-label="企业文化">
@@ -103,7 +103,7 @@
<!-- CTA -->
<CommonSectionCTA
title="想了解我们的产品?"
description="查看额吉的故乡V9系列和小蒙马夏令营"
description="查看额吉的故乡V9系列和小蒙马亲子游学营"
:links="[
{ to: '/products', text: '查看旅行产品' },
{ to: '/contact', text: '联系定制师' },
@@ -118,6 +118,7 @@ import { useOrganizationSchema, useBreadcrumbSchema, usePersonSchema } from '~/c
const { data: about } = await usePublicApi('about')
const { data: brand } = await usePublicApi('brand')
const { data: contact } = await usePublicApi('contact')
const { data: images } = await usePublicApi('images')
const seo = useSEO('about')
useScrollReveal()
+257
查看文件
@@ -0,0 +1,257 @@
<template>
<div style="max-width: 1100px">
<n-space justify="space-between" align="center" style="margin-bottom: 20px">
<div>
<h1 style="margin: 0 0 4px; font-size: 20px; font-weight: 600; color: #111827">呼籁旅行百科</h1>
<p style="margin: 0; font-size: 13px; color: #9ca3af">管理"呼籁旅行"品牌百科页面内容</p>
</div>
<n-space>
<n-button tag="a" href="/brand" target="_blank" secondary>查看前台 ↗</n-button>
<n-button type="primary" :loading="saving" @click="save">保存</n-button>
</n-space>
</n-space>
<n-spin :show="!loaded">
<n-collapse :default-expanded-names="['basic']">
<!-- 基本信息 -->
<n-collapse-item title="基本信息" name="basic">
<n-card>
<n-form label-placement="top">
<n-grid :cols="2" :x-gap="12">
<n-gi><n-form-item label="标题"><n-input v-model:value="data.title" /></n-form-item></n-gi>
<n-gi><n-form-item label="Logo 路径"><n-input v-model:value="data.logo" /></n-form-item></n-gi>
</n-grid>
<n-form-item label="副标题">
<n-input v-model:value="data.subtitle" />
</n-form-item>
<n-form-item label="摘要">
<n-input v-model:value="data.summary" type="textarea" :autosize="{ minRows: 3, maxRows: 8 }" />
</n-form-item>
</n-form>
</n-card>
</n-collapse-item>
<!-- 信息框 -->
<n-collapse-item title="信息框" name="infoBox">
<n-card>
<div style="width: 100%">
<div v-for="(_, key) in data.infoBox" :key="key" style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px">
<n-input :value="key" placeholder="字段名" style="width: 160px" @update:value="v => renameKey(data.infoBox, key, v)" />
<n-input v-model:value="data.infoBox[key]" placeholder="值" style="flex: 1" />
<n-button size="tiny" type="error" quaternary @click="delete data.infoBox[key]">删</n-button>
</div>
<n-button size="small" type="primary" dashed @click="addInfoBoxItem">添加字段</n-button>
</div>
</n-card>
</n-collapse-item>
<!-- 内容章节 -->
<n-collapse-item title="内容章节" name="sections">
<n-card>
<div style="width: 100%">
<n-card v-for="(sec, i) in data.sections" :key="i" size="small" style="margin-bottom: 16px">
<template #header>
<n-space align="center">
<span style="font-size: 14px; font-weight: 500">{{ sec.title || '新章节' }}</span>
<n-tag :bordered="false" size="small" type="info">{{ getSectionType(sec) }}</n-tag>
<n-button size="tiny" type="error" quaternary @click="data.sections.splice(i, 1)">删除</n-button>
</n-space>
</template>
<n-form label-placement="top">
<n-form-item label="章节标题">
<n-input v-model:value="sec.title" />
</n-form-item>
<!-- 纯文本型 -->
<template v-if="getSectionType(sec) === '文本'">
<n-form-item label="内容">
<n-input v-model:value="sec.content" type="textarea" :autosize="{ minRows: 3, maxRows: 10 }" />
</n-form-item>
<n-form-item v-if="sec.image !== undefined" label="图片路径">
<n-input v-model:value="sec.image" />
</n-form-item>
</template>
<!-- 子章节型(品牌历史) -->
<template v-if="getSectionType(sec) === '子章节'">
<n-form-item label="子章节">
<div style="width: 100%">
<n-card v-for="(sub, j) in sec.subsections" :key="j" size="small" style="margin-bottom: 8px">
<template #header>
<n-space align="center">
<span style="font-size: 13px; color: #6b7280">{{ j + 1 }}.</span>
<n-button size="tiny" type="error" quaternary @click="sec.subsections.splice(j, 1)">删</n-button>
</n-space>
</template>
<n-form-item label="标题"><n-input v-model:value="sub.title" /></n-form-item>
<n-form-item label="内容"><n-input v-model:value="sub.content" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" /></n-form-item>
</n-card>
<n-button size="small" type="primary" dashed @click="sec.subsections.push({ title: '', content: '' })">添加子章节</n-button>
</div>
</n-form-item>
</template>
<!-- 产品型 -->
<template v-if="getSectionType(sec) === '产品'">
<n-form-item label="简介">
<n-input v-model:value="sec.intro" type="textarea" :autosize="{ minRows: 2, maxRows: 5 }" />
</n-form-item>
<n-form-item label="产品列表">
<div style="width: 100%">
<n-card v-for="(prod, j) in sec.products" :key="j" size="small" style="margin-bottom: 8px">
<template #header>
<n-space align="center">
<span style="font-size: 13px; color: #6b7280">{{ prod.name || '新产品' }}</span>
<n-button size="tiny" type="error" quaternary @click="sec.products.splice(j, 1)">删</n-button>
</n-space>
</template>
<n-form-item label="名称"><n-input v-model:value="prod.name" /></n-form-item>
<n-form-item label="描述"><n-input v-model:value="prod.description" type="textarea" :autosize="{ minRows: 2, maxRows: 6 }" /></n-form-item>
</n-card>
<n-button size="small" type="primary" dashed @click="sec.products.push({ name: '', description: '' })">添加产品</n-button>
</div>
</n-form-item>
</template>
<!-- 步骤型(服务流程) -->
<template v-if="getSectionType(sec) === '步骤'">
<n-form-item label="步骤">
<div style="width: 100%">
<div v-for="(step, j) in sec.steps" :key="j" style="display: flex; gap: 8px; align-items: flex-start; margin-bottom: 8px">
<n-input-number v-model:value="step.step" placeholder="序号" style="width: 80px" :min="1" />
<n-input v-model:value="step.name" placeholder="名称" style="width: 140px" />
<n-input v-model:value="step.detail" placeholder="详情" style="flex: 1" />
<n-button size="tiny" type="error" quaternary @click="sec.steps.splice(j, 1)">删</n-button>
</div>
<n-button size="small" type="primary" dashed @click="sec.steps.push({ step: sec.steps.length + 1, name: '', detail: '' })">添加步骤</n-button>
</div>
</n-form-item>
</template>
<!-- 键值对型(文化理念) -->
<template v-if="getSectionType(sec) === '键值对'">
<n-form-item label="条目">
<div style="width: 100%">
<div v-for="(item, j) in sec.items" :key="j" style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px">
<n-input v-model:value="item.label" placeholder="标签" style="width: 160px" />
<n-input v-model:value="item.value" placeholder="内容" style="flex: 1" />
<n-button size="tiny" type="error" quaternary @click="sec.items.splice(j, 1)">删</n-button>
</div>
<n-button size="small" type="primary" dashed @click="sec.items.push({ label: '', value: '' })">添加条目</n-button>
</div>
</n-form-item>
</template>
</n-form>
</n-card>
<n-space style="margin-top: 12px">
<n-button size="small" type="primary" dashed @click="addSection('text')">+ 文本章节</n-button>
<n-button size="small" type="primary" dashed @click="addSection('subsections')">+ 子章节</n-button>
<n-button size="small" type="primary" dashed @click="addSection('products')">+ 产品章节</n-button>
<n-button size="small" type="primary" dashed @click="addSection('steps')">+ 步骤章节</n-button>
<n-button size="small" type="primary" dashed @click="addSection('items')">+ 键值对章节</n-button>
</n-space>
</div>
</n-card>
</n-collapse-item>
<!-- 图库 -->
<n-collapse-item title="图库" name="gallery">
<n-card>
<n-dynamic-input v-model:value="data.gallery" :min="0" placeholder="图片路径,如 /images/brand/xxx.jpg" />
</n-card>
</n-collapse-item>
<!-- SEO -->
<n-collapse-item title="SEO" name="seo">
<n-card>
<n-form label-placement="top">
<n-form-item label="页面标题">
<n-input v-model:value="data.seo.title" />
</n-form-item>
<n-form-item label="描述">
<n-input v-model:value="data.seo.description" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" />
</n-form-item>
<n-form-item label="关键词">
<n-input v-model:value="data.seo.keywords" />
</n-form-item>
</n-form>
</n-card>
</n-collapse-item>
</n-collapse>
</n-spin>
</div>
</template>
<script setup>
import { NButton, NCard, NSpace, NInput, NForm, NFormItem, NSpin, NGrid, NGi, NCollapse, NCollapseItem, NDynamicInput, NInputNumber, NTag } from 'naive-ui'
import { useMessage } from 'naive-ui'
definePageMeta({ layout: 'admin', middleware: 'admin' })
const { adminFetch } = useAdmin()
const message = useMessage()
const data = ref({
title: '', subtitle: '', summary: '', logo: '',
infoBox: {},
sections: [],
gallery: [],
seo: { title: '', description: '', keywords: '' }
})
const loaded = ref(false)
const saving = ref(false)
function getSectionType(sec) {
if (sec.subsections) return '子章节'
if (sec.products) return '产品'
if (sec.steps) return '步骤'
if (sec.items) return '键值对'
return '文本'
}
function addSection(type) {
const templates = {
text: { title: '', content: '' },
subsections: { title: '', subsections: [{ title: '', content: '' }] },
products: { title: '', intro: '', products: [{ name: '', description: '' }] },
steps: { title: '', steps: [{ step: 1, name: '', detail: '' }] },
items: { title: '', items: [{ label: '', value: '' }] },
}
data.value.sections.push(templates[type])
}
function addInfoBoxItem() {
const key = `新字段${Object.keys(data.value.infoBox).length + 1}`
data.value.infoBox[key] = ''
}
function renameKey(obj, oldKey, newKey) {
if (newKey && newKey !== oldKey && !(newKey in obj)) {
const val = obj[oldKey]
delete obj[oldKey]
obj[newKey] = val
}
}
async function load() {
const res = await adminFetch('/api/admin/site-content?key=brand-wiki')
data.value = res.data
if (!data.value.seo) data.value.seo = { title: '', description: '', keywords: '' }
if (!data.value.gallery) data.value.gallery = []
loaded.value = true
}
async function save() {
saving.value = true
try {
await adminFetch('/api/admin/site-content', { method: 'PUT', body: { key: 'brand-wiki', data: data.value } })
message.success('已保存')
} catch (e) {
message.error('保存失败')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
+261
查看文件
@@ -0,0 +1,261 @@
<template>
<div style="max-width: 1100px">
<n-space justify="space-between" align="center" style="margin-bottom: 20px">
<div>
<h1 style="margin: 0 0 4px; font-size: 20px; font-weight: 600; color: #111827">呼籁文旅集团百科</h1>
<p style="margin: 0; font-size: 13px; color: #9ca3af">管理"呼籁文旅集团"公司百科页面内容</p>
</div>
<n-space>
<n-button tag="a" href="/company" target="_blank" secondary>查看前台 ↗</n-button>
<n-button type="primary" :loading="saving" @click="save">保存</n-button>
</n-space>
</n-space>
<n-spin :show="!loaded">
<n-collapse :default-expanded-names="['basic']">
<!-- 基本信息 -->
<n-collapse-item title="基本信息" name="basic">
<n-card>
<n-form label-placement="top">
<n-grid :cols="2" :x-gap="12">
<n-gi><n-form-item label="公司名称"><n-input v-model:value="data.title" /></n-form-item></n-gi>
<n-gi><n-form-item label="Logo 路径"><n-input v-model:value="data.logo" /></n-form-item></n-gi>
</n-grid>
<n-form-item label="副标题">
<n-input v-model:value="data.subtitle" />
</n-form-item>
<n-form-item label="摘要">
<n-input v-model:value="data.summary" type="textarea" :autosize="{ minRows: 3, maxRows: 8 }" />
</n-form-item>
</n-form>
</n-card>
</n-collapse-item>
<!-- 信息框 -->
<n-collapse-item title="信息框" name="infoBox">
<n-card>
<div style="width: 100%">
<div v-for="(_, key) in data.infoBox" :key="key" style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px">
<n-input :value="key" placeholder="字段名" style="width: 160px" @update:value="v => renameKey(data.infoBox, key, v)" />
<n-input v-model:value="data.infoBox[key]" placeholder="值" style="flex: 1" />
<n-button size="tiny" type="error" quaternary @click="delete data.infoBox[key]">删</n-button>
</div>
<n-button size="small" type="primary" dashed @click="addInfoBoxItem">添加字段</n-button>
</div>
</n-card>
</n-collapse-item>
<!-- 内容章节 -->
<n-collapse-item title="内容章节" name="sections">
<n-card>
<div style="width: 100%">
<n-card v-for="(sec, i) in data.sections" :key="i" size="small" style="margin-bottom: 16px">
<template #header>
<n-space align="center">
<span style="font-size: 14px; font-weight: 500">{{ sec.title || '新章节' }}</span>
<n-tag :bordered="false" size="small" type="info">{{ getSectionType(sec) }}</n-tag>
<n-button size="tiny" type="error" quaternary @click="data.sections.splice(i, 1)">删除</n-button>
</n-space>
</template>
<n-form label-placement="top">
<n-form-item label="章节标题">
<n-input v-model:value="sec.title" />
</n-form-item>
<!-- 纯文本型 -->
<template v-if="getSectionType(sec) === '文本'">
<n-form-item label="内容">
<n-input v-model:value="sec.content" type="textarea" :autosize="{ minRows: 3, maxRows: 10 }" />
</n-form-item>
</template>
<!-- 时间线型(发展历程) -->
<template v-if="getSectionType(sec) === '时间线'">
<n-form-item label="时间线">
<div style="width: 100%">
<div v-for="(item, j) in sec.timeline" :key="j" style="display: flex; gap: 8px; align-items: flex-start; margin-bottom: 8px">
<n-input v-model:value="item.year" placeholder="年份" style="width: 120px" />
<n-input v-model:value="item.content" placeholder="事件" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" style="flex: 1" />
<n-button size="tiny" type="error" quaternary @click="sec.timeline.splice(j, 1)">删</n-button>
</div>
<n-button size="small" type="primary" dashed @click="sec.timeline.push({ year: '', content: '' })">添加事件</n-button>
</div>
</n-form-item>
</template>
<!-- 子公司型 -->
<template v-if="getSectionType(sec) === '子公司'">
<n-form-item label="简介">
<n-input v-model:value="sec.intro" type="textarea" :autosize="{ minRows: 2, maxRows: 5 }" />
</n-form-item>
<n-form-item label="子公司列表">
<div style="width: 100%">
<div v-for="(sub, j) in sec.subsidiaries" :key="j" style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px">
<n-input :value="sub" placeholder="子公司描述" style="flex: 1" @update:value="v => sec.subsidiaries[j] = v" />
<n-button size="tiny" type="error" quaternary @click="sec.subsidiaries.splice(j, 1)">删</n-button>
</div>
<n-button size="small" type="primary" dashed @click="sec.subsidiaries.push('')">添加子公司</n-button>
</div>
</n-form-item>
</template>
<!-- 业务线型 -->
<template v-if="getSectionType(sec) === '业务线'">
<n-form-item label="业务板块">
<div style="width: 100%">
<n-card v-for="(biz, j) in sec.businesses" :key="j" size="small" style="margin-bottom: 8px">
<template #header>
<n-space align="center">
<span style="font-size: 13px; color: #6b7280">{{ biz.name || '新业务' }}</span>
<n-button size="tiny" type="error" quaternary @click="sec.businesses.splice(j, 1)">删</n-button>
</n-space>
</template>
<n-form-item label="名称"><n-input v-model:value="biz.name" /></n-form-item>
<n-form-item label="描述"><n-input v-model:value="biz.description" type="textarea" :autosize="{ minRows: 1, maxRows: 4 }" /></n-form-item>
<n-form-item v-if="biz.extra !== undefined" label="备注"><n-input v-model:value="biz.extra" /></n-form-item>
</n-card>
<n-button size="small" type="primary" dashed @click="sec.businesses.push({ name: '', description: '' })">添加业务</n-button>
</div>
</n-form-item>
</template>
<!-- 键值对型(文化理念) -->
<template v-if="getSectionType(sec) === '键值对'">
<n-form-item label="条目">
<div style="width: 100%">
<div v-for="(item, j) in sec.items" :key="j" style="display: flex; gap: 8px; align-items: center; margin-bottom: 8px">
<n-input v-model:value="item.label" placeholder="标签" style="width: 160px" />
<n-input v-model:value="item.value" placeholder="内容" style="flex: 1" />
<n-button size="tiny" type="error" quaternary @click="sec.items.splice(j, 1)">删</n-button>
</div>
<n-button size="small" type="primary" dashed @click="sec.items.push({ label: '', value: '' })">添加条目</n-button>
</div>
</n-form-item>
</template>
<!-- 带图片的文本型(团队) -->
<template v-if="getSectionType(sec) === '图文'">
<n-form-item label="内容">
<n-input v-model:value="sec.content" type="textarea" :autosize="{ minRows: 3, maxRows: 10 }" />
</n-form-item>
<n-form-item label="图片">
<n-dynamic-input v-model:value="sec.images" :min="0" placeholder="图片路径" />
</n-form-item>
</template>
</n-form>
</n-card>
<n-space style="margin-top: 12px">
<n-button size="small" type="primary" dashed @click="addSection('text')">+ 文本章节</n-button>
<n-button size="small" type="primary" dashed @click="addSection('timeline')">+ 时间线</n-button>
<n-button size="small" type="primary" dashed @click="addSection('subsidiaries')">+ 子公司</n-button>
<n-button size="small" type="primary" dashed @click="addSection('businesses')">+ 业务线</n-button>
<n-button size="small" type="primary" dashed @click="addSection('items')">+ 键值对</n-button>
<n-button size="small" type="primary" dashed @click="addSection('images')">+ 图文</n-button>
</n-space>
</div>
</n-card>
</n-collapse-item>
<!-- 图库 -->
<n-collapse-item title="图库" name="gallery">
<n-card>
<n-dynamic-input v-model:value="data.gallery" :min="0" placeholder="图片路径,如 /images/company/xxx.jpg" />
</n-card>
</n-collapse-item>
<!-- SEO -->
<n-collapse-item title="SEO" name="seo">
<n-card>
<n-form label-placement="top">
<n-form-item label="页面标题">
<n-input v-model:value="data.seo.title" />
</n-form-item>
<n-form-item label="描述">
<n-input v-model:value="data.seo.description" type="textarea" :autosize="{ minRows: 2, maxRows: 4 }" />
</n-form-item>
<n-form-item label="关键词">
<n-input v-model:value="data.seo.keywords" />
</n-form-item>
</n-form>
</n-card>
</n-collapse-item>
</n-collapse>
</n-spin>
</div>
</template>
<script setup>
import { NButton, NCard, NSpace, NInput, NForm, NFormItem, NSpin, NGrid, NGi, NCollapse, NCollapseItem, NDynamicInput, NTag } from 'naive-ui'
import { useMessage } from 'naive-ui'
definePageMeta({ layout: 'admin', middleware: 'admin' })
const { adminFetch } = useAdmin()
const message = useMessage()
const data = ref({
title: '', subtitle: '', summary: '', logo: '',
infoBox: {},
sections: [],
gallery: [],
seo: { title: '', description: '', keywords: '' }
})
const loaded = ref(false)
const saving = ref(false)
function getSectionType(sec) {
if (sec.timeline) return '时间线'
if (sec.subsidiaries) return '子公司'
if (sec.businesses) return '业务线'
if (sec.items) return '键值对'
if (sec.images) return '图文'
return '文本'
}
function addSection(type) {
const templates = {
text: { title: '', content: '' },
timeline: { title: '', timeline: [{ year: '', content: '' }] },
subsidiaries: { title: '', intro: '', subsidiaries: [''] },
businesses: { title: '', businesses: [{ name: '', description: '' }] },
items: { title: '', items: [{ label: '', value: '' }] },
images: { title: '', content: '', images: [] },
}
data.value.sections.push(templates[type])
}
function addInfoBoxItem() {
const key = `新字段${Object.keys(data.value.infoBox).length + 1}`
data.value.infoBox[key] = ''
}
function renameKey(obj, oldKey, newKey) {
if (newKey && newKey !== oldKey && !(newKey in obj)) {
const val = obj[oldKey]
delete obj[oldKey]
obj[newKey] = val
}
}
async function load() {
const res = await adminFetch('/api/admin/site-content?key=company-wiki')
data.value = res.data
if (!data.value.seo) data.value.seo = { title: '', description: '', keywords: '' }
if (!data.value.gallery) data.value.gallery = []
loaded.value = true
}
async function save() {
saving.value = true
try {
await adminFetch('/api/admin/site-content', { method: 'PUT', body: { key: 'company-wiki', data: data.value } })
message.success('已保存')
} catch (e) {
message.error('保存失败')
} finally {
saving.value = false
}
}
onMounted(load)
</script>
+1 -1
查看文件
@@ -2,7 +2,7 @@
<div>
<n-space justify="space-between" align="center" style="margin-bottom: 20px;">
<div>
<h1 style="margin: 0 0 4px; font-size: 22px; color: #1a1a2e;">小蒙马夏令营</h1>
<h1 style="margin: 0 0 4px; font-size: 22px; color: #1a1a2e;">小蒙马亲子游学营</h1>
<p style="margin: 0; font-size: 13px; color: #888;">管理研学课程体系,对应官网「夏令营」页面</p>
</div>
<n-space>
+35 -10
查看文件
@@ -78,8 +78,10 @@
<!-- 联系咨询 -->
<div class="sidebar-card sidebar-card--cta">
<h3 class="sidebar-card__title">免费获取定制方案</h3>
<p class="sidebar-card__desc">看完攻略有疑问?联系呼籁旅行定制师,一对一解答。</p>
<CommonInternalLink to="/contact" text="立即咨询" />
<p class="sidebar-card__desc">看完攻略有疑问?扫码添加定制师微信,一对一解答。</p>
<img src="/images/contact/qrcode-wechat.jpg" alt="定制师微信二维码" class="sidebar-qrcode" />
<span class="sidebar-qrcode-label">扫码添加定制师微信</span>
<CommonInternalLink to="/contact" text="其他联系方式" />
</div>
<!-- 全部文章 -->
@@ -142,17 +144,17 @@ if (!article.value) {
// SEO
useHead({
title: article.value?.seo?.title || article.value?.title,
title: article.value?.seoTitle || article.value?.title,
meta: [
{ name: 'description', content: article.value?.seo?.description || article.value?.summary },
{ name: 'keywords', content: article.value?.seo?.keywords || (article.value?.tags || []).join(',') },
{ name: 'description', content: article.value?.seoDescription || article.value?.summary },
{ name: 'keywords', content: article.value?.seoKeywords || (article.value?.tags || []).join(',') },
{ name: 'author', content: article.value?.author },
{ name: 'article:published_time', content: (article.value?.date || '') + 'T00:00:00+08:00' },
{ name: 'article:modified_time', content: (article.value?.date || '') + 'T00:00:00+08:00' },
{ name: 'geo.region', content: 'CN-NM' },
{ name: 'geo.placename', content: '呼伦贝尔' },
{ property: 'og:title', content: article.value?.seo?.title || article.value?.title },
{ property: 'og:description', content: article.value?.seo?.description || article.value?.summary },
{ property: 'og:title', content: article.value?.seoTitle || article.value?.title },
{ property: 'og:description', content: article.value?.seoDescription || article.value?.summary },
{ property: 'og:type', content: 'article' },
{ property: 'og:url', content: `https://1814.love/blog/${article.value?.id}` },
{ property: 'og:site_name', content: '呼籁旅行' },
@@ -277,13 +279,12 @@ function formatDate(dateStr) {
margin-bottom: @space-xl;
border-radius: @border-radius-lg;
overflow: hidden;
aspect-ratio: 16 / 7;
background: @color-primary-bg;
&__img {
width: 100%;
height: 100%;
object-fit: cover;
height: auto;
display: block;
}
}
@@ -347,6 +348,14 @@ function formatDate(dateStr) {
font-weight: @font-weight-bold;
}
:deep(img) {
max-width: 100%;
height: auto;
border-radius: @border-radius-lg;
display: block;
margin: 0 auto;
}
:deep(br) {
display: block;
content: '';
@@ -518,6 +527,22 @@ function formatDate(dateStr) {
}
}
.sidebar-qrcode {
display: block;
width: 120px;
height: 120px;
border-radius: @border-radius-sm;
object-fit: contain;
margin-bottom: @space-xs;
}
.sidebar-qrcode-label {
display: block;
font-size: @font-size-xs;
color: @color-text-muted;
margin-bottom: @space-md;
}
.sidebar-articles {
list-style: none;
margin: 0;
+6 -10
查看文件
@@ -37,7 +37,7 @@
<article
v-for="article in filteredArticles"
:key="article.id"
class="blog-card reveal"
class="blog-card"
>
<NuxtLink :to="`/blog/${article.id}`" class="blog-card__link">
<div class="blog-card__cover">
@@ -72,15 +72,11 @@
</div>
<!-- CTA -->
<div class="blog-cta">
<h2>看完攻略,准备出发了吗?</h2>
<p>呼籁旅行为每个家庭提供专属定制方案,告诉我们你的需求,我们帮你把行程安排得妥妥的。</p>
<div class="blog-cta-links">
<CommonInternalLink to="/products" text="查看定制游产品" />
<CommonInternalLink to="/contact" text="咨询定制师" />
<CommonInternalLink to="/faq" text="常见问答" />
</div>
</div>
<CommonWechatCTA
title="看完攻略,准备出发了吗?"
description="扫码添加定制师微信,根据你的家庭情况和假期安排,推荐合适的行程方案。"
:links="[{ to: '/products', text: '查看定制游产品' }, { to: '/faq', text: '常见问答' }]"
/>
</div>
</section>
</div>
+422
查看文件
@@ -0,0 +1,422 @@
<template>
<div class="page-wiki">
<LayoutPageHero
:title="data?.title || '呼籁旅行'"
:subtitle="data?.subtitle"
:breadcrumbs="[{ text: '关于我们', to: '/about' }, { text: '呼籁旅行' }]"
background-image="/images/brand/photo-01.jpeg"
/>
<!-- 摘要 + 信息卡 -->
<section class="section" aria-label="品牌概述">
<div class="container">
<div class="wiki-header">
<div class="wiki-summary">
<div class="wiki-summary-inner">
<h2 class="wiki-summary-heading">品牌简介</h2>
<p class="wiki-summary-text">{{ data?.summary }}</p>
</div>
</div>
<div v-if="data?.infoBox" class="wiki-infobox">
<div class="wiki-infobox-logo">
<img :src="data.logo" :alt="data.title" />
</div>
<div class="wiki-infobox-body">
<div v-for="(value, key) in data.infoBox" :key="key" class="wiki-infobox-row">
<span class="wiki-infobox-label">{{ key }}</span>
<span class="wiki-infobox-value">{{ value }}</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 各段落 -->
<template v-for="(section, idx) in data?.sections" :key="idx">
<!-- 品牌历史(子段落) -->
<section v-if="section.subsections" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<div v-for="(sub, si) in section.subsections" :key="si" class="wiki-subsection">
<h3 class="wiki-subsection-title">{{ sub.title }}</h3>
<p class="wiki-text">{{ sub.content }}</p>
</div>
</div>
</section>
<!-- 产品及服务 -->
<section v-if="section.products" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<p v-if="section.intro" class="wiki-text">{{ section.intro }}</p>
<div class="wiki-products">
<div v-for="(prod, pi) in section.products" :key="pi" class="wiki-product-card">
<h3 class="wiki-product-name">{{ prod.name }}</h3>
<p class="wiki-product-desc">{{ prod.description }}</p>
</div>
</div>
</div>
</section>
<!-- 服务流程 -->
<section v-if="section.steps" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<div class="wiki-steps">
<div v-for="(s, si) in section.steps" :key="si" class="wiki-step">
<div class="wiki-step-number">{{ s.step }}</div>
<div class="wiki-step-body">
<h4 class="wiki-step-name">{{ s.name }}</h4>
<p class="wiki-step-detail">{{ s.detail }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- 品牌文化(items列表) -->
<section v-if="section.items" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<div class="wiki-culture-list">
<div v-for="(item, ii) in section.items" :key="ii" class="wiki-culture-item">
<span class="wiki-culture-label">{{ item.label }}</span>
<p class="wiki-culture-value">{{ item.value }}</p>
</div>
</div>
</div>
</section>
<!-- 普通文本段落(含可选图片) -->
<section v-if="section.content && !section.subsections && !section.products && !section.steps && !section.items" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<p class="wiki-text">{{ section.content }}</p>
<div v-if="section.image" class="wiki-logo-img">
<img :src="section.image" :alt="section.title" />
</div>
</div>
</section>
</template>
<!-- 图库 -->
<section v-if="data?.gallery?.length" class="section section--gray" aria-label="品牌图库">
<div class="container">
<h2 class="wiki-section-title">品牌图库</h2>
<div class="wiki-gallery">
<div v-for="(img, gi) in data.gallery" :key="gi" class="wiki-gallery-item">
<img :src="img" :alt="`${data.title} 图片 ${gi + 1}`" loading="lazy" />
</div>
</div>
</div>
</section>
<!-- CTA -->
<CommonSectionCTA
title="想了解更多?"
description="查看我们的旅行产品或直接联系定制师"
:links="[
{ to: '/products', text: '查看旅行产品' },
{ to: '/contact', text: '联系定制师' },
]"
/>
</div>
</template>
<script setup>
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const { data } = await usePublicApi('brand-wiki')
useSEO('brand-wiki', {
title: data.value?.seo?.title || '呼籁旅行 - 专注呼伦贝尔的家庭旅行品牌',
description: data.value?.seo?.description || '',
})
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '关于我们', to: '/about' },
{ text: '呼籁旅行' },
])
useScrollReveal()
</script>
<style lang="less" scoped>
.wiki-header {
display: flex;
gap: @space-2xl;
align-items: flex-start;
@media (max-width: 768px) {
flex-direction: column;
gap: @space-xl;
}
}
.wiki-summary {
flex: 1;
min-width: 0;
}
.wiki-summary-inner {
padding: @space-xl @space-2xl;
background: @color-bg-warm;
border-radius: @border-radius-lg;
border-left: 4px solid @color-primary;
}
.wiki-summary-heading {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-md;
}
.wiki-summary-text {
font-size: @font-size-base;
line-height: 2;
color: @color-text-primary;
margin: 0;
}
.wiki-infobox {
flex-shrink: 0;
width: 360px;
background: @color-bg-white;
border-radius: @border-radius-lg;
overflow: hidden;
box-shadow: @shadow-card;
@media (max-width: 768px) {
width: 100%;
}
}
.wiki-infobox-logo {
display: flex;
justify-content: center;
align-items: center;
padding: @space-xl @space-lg;
background: @color-primary-bg;
border-bottom: 1px solid @color-divider;
img {
max-width: 160px;
height: auto;
}
}
.wiki-infobox-body {
padding: @space-sm 0;
}
.wiki-infobox-row {
display: flex;
padding: @space-sm @space-lg;
border-bottom: 1px solid @color-divider;
gap: @space-md;
&:last-child {
border-bottom: none;
}
}
.wiki-infobox-label {
flex-shrink: 0;
width: 80px;
font-size: @font-size-sm;
font-weight: @font-weight-medium;
color: @color-text-muted;
padding-top: 1px;
}
.wiki-infobox-value {
flex: 1;
font-size: @font-size-sm;
line-height: 1.6;
color: @color-text-primary;
}
.wiki-section-title {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-lg;
padding-bottom: @space-sm;
border-bottom: 2px solid @color-primary;
display: inline-block;
}
.wiki-text {
font-size: @font-size-base;
line-height: 1.8;
color: @color-text-primary;
max-width: 800px;
}
.wiki-logo-img {
margin-top: @space-lg;
text-align: center;
img {
max-width: 160px;
height: auto;
}
}
.wiki-subsection {
margin-bottom: @space-lg;
&:last-child {
margin-bottom: 0;
}
}
.wiki-subsection-title {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-sm;
}
.wiki-products {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
margin-top: @space-lg;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
}
.wiki-product-card {
padding: @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
}
.wiki-product-name {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-sm;
}
.wiki-product-desc {
font-size: @font-size-sm;
line-height: 1.7;
color: @color-text-secondary;
margin: 0;
}
.wiki-steps {
display: flex;
flex-direction: column;
gap: @space-md;
margin-top: @space-lg;
}
.wiki-step {
display: flex;
gap: @space-md;
align-items: flex-start;
}
.wiki-step-number {
flex-shrink: 0;
width: 36px;
height: 36px;
background: @color-primary;
color: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: @font-weight-bold;
font-size: @font-size-sm;
}
.wiki-step-body {
flex: 1;
padding-top: 4px;
}
.wiki-step-name {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-xs;
}
.wiki-step-detail {
font-size: @font-size-sm;
line-height: 1.7;
color: @color-text-secondary;
margin: 0;
}
.wiki-culture-list {
display: flex;
flex-direction: column;
gap: @space-lg;
}
.wiki-culture-item {
padding: @space-lg;
background: @color-bg-white;
border-left: 4px solid @color-primary;
border-radius: 0 @border-radius-lg @border-radius-lg 0;
box-shadow: @shadow-sm;
}
.wiki-culture-label {
display: block;
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-xs;
}
.wiki-culture-value {
font-size: @font-size-base;
line-height: 1.8;
color: @color-text-primary;
margin: 0;
}
.wiki-gallery {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-md;
margin-top: @space-lg;
.respond-md({
grid-template-columns: repeat(3, 1fr);
});
.respond-lg({
grid-template-columns: repeat(4, 1fr);
});
}
.wiki-gallery-item {
border-radius: @border-radius-lg;
overflow: hidden;
aspect-ratio: 16 / 10;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
&:hover img {
transform: scale(1.05);
}
}
</style>
+7 -1
查看文件
@@ -131,6 +131,12 @@
</div>
</section>
<CommonWechatCTA
title="确定了出行时间?扫码找定制师"
description="告诉定制师你的出行日期和家庭情况,帮你匹配最合适的行程方案。"
:links="[{ to: '/products', text: '查看产品线' }]"
/>
</div>
</template>
@@ -186,7 +192,7 @@ const seasonalEvents = [
{
key: 'event-summer',
name: '呼伦贝尔夏季旅游最佳期(6-8月)',
description: '呼伦贝尔草原夏季旅游高峰,绿草如茵,那达慕传统节庆,骑马、露营、亲子出行最佳时段。呼籁旅行提供额吉的故乡V9系列定制游及小蒙马亲子夏令营。',
description: '呼伦贝尔草原夏季旅游高峰,绿草如茵,那达慕传统节庆,骑马、露营、亲子出行最佳时段。呼籁旅行提供额吉的故乡V9系列定制游及小蒙马亲子游学营。',
startDate: '2026-06-01',
endDate: '2026-08-31',
},
+484
查看文件
@@ -0,0 +1,484 @@
<template>
<div class="page-wiki">
<LayoutPageHero
:title="data?.title || '呼籁文旅集团'"
:subtitle="data?.subtitle"
:breadcrumbs="[{ text: '关于我们', to: '/about' }, { text: '呼籁文旅集团' }]"
background-image="/images/company/photo-03.jpeg"
/>
<!-- 摘要 + 信息卡 -->
<section class="section" aria-label="集团概述">
<div class="container">
<div class="wiki-header">
<div class="wiki-summary">
<div class="wiki-summary-inner">
<h2 class="wiki-summary-heading">集团简介</h2>
<p class="wiki-summary-text">{{ data?.summary }}</p>
</div>
</div>
<div v-if="data?.infoBox" class="wiki-infobox">
<div class="wiki-infobox-logo">
<img :src="data.logo" :alt="data.title" />
</div>
<div class="wiki-infobox-body">
<div v-for="(value, key) in data.infoBox" :key="key" class="wiki-infobox-row">
<span class="wiki-infobox-label">{{ key }}</span>
<span class="wiki-infobox-value">{{ value }}</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- 各段落 -->
<template v-for="(section, idx) in data?.sections" :key="idx">
<!-- 发展历程(时间线) -->
<section v-if="section.timeline" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<div class="wiki-timeline">
<div v-for="(item, ti) in section.timeline" :key="ti" class="wiki-timeline-item">
<div class="wiki-timeline-marker">
<div class="wiki-timeline-dot" />
</div>
<div class="wiki-timeline-card">
<h4 class="wiki-timeline-year">{{ item.year }}</h4>
<p class="wiki-timeline-text">{{ item.content }}</p>
</div>
</div>
</div>
</div>
</section>
<!-- 普通文本段落 -->
<section v-if="section.content && !section.timeline && !section.subsidiaries && !section.businesses && !section.items && !section.images" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<p class="wiki-text">{{ section.content }}</p>
<div v-if="section.image" class="wiki-logo-img">
<img :src="section.image" :alt="section.title" />
</div>
</div>
</section>
<!-- 组织架构(子公司列表) -->
<section v-if="section.subsidiaries" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<p v-if="section.intro" class="wiki-text">{{ section.intro }}</p>
<ul class="wiki-subsidiary-list">
<li v-for="(sub, si) in section.subsidiaries" :key="si" class="wiki-subsidiary-item">
{{ sub }}
</li>
</ul>
</div>
</section>
<!-- 管理团队(带图片) -->
<section v-if="section.content && section.images" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<p class="wiki-text">{{ section.content }}</p>
<div v-if="section.images?.length" class="wiki-team-images">
<img v-for="(img, ii) in section.images" :key="ii" :src="img" :alt="`${section.title} ${ii + 1}`" loading="lazy" />
</div>
</div>
</section>
<!-- 主要业务 -->
<section v-if="section.businesses" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<div class="wiki-businesses">
<div v-for="(biz, bi) in section.businesses" :key="bi" class="wiki-business-card">
<h3 class="wiki-business-name">{{ biz.name }}</h3>
<p class="wiki-business-desc">{{ biz.description }}</p>
<p v-if="biz.extra" class="wiki-business-extra">{{ biz.extra }}</p>
</div>
</div>
</div>
</section>
<!-- 企业文化(items列表) -->
<section v-if="section.items" class="section" :class="{ 'section--gray': idx % 2 === 1 }">
<div class="container">
<h2 class="wiki-section-title">{{ section.title }}</h2>
<div class="wiki-culture-list">
<div v-for="(item, ii) in section.items" :key="ii" class="wiki-culture-item">
<span class="wiki-culture-label">{{ item.label }}</span>
<p class="wiki-culture-value">{{ item.value }}</p>
</div>
</div>
</div>
</section>
</template>
<!-- 图库 -->
<section v-if="data?.gallery?.length" class="section section--gray" aria-label="集团图库">
<div class="container">
<h2 class="wiki-section-title">集团图库</h2>
<div class="wiki-gallery">
<div v-for="(img, gi) in data.gallery" :key="gi" class="wiki-gallery-item">
<img :src="img" :alt="`${data.title} 图片 ${gi + 1}`" loading="lazy" />
</div>
</div>
</div>
</section>
<!-- CTA -->
<CommonSectionCTA
title="想了解更多?"
description="了解呼籁旅行品牌或直接联系我们"
:links="[
{ to: '/brand', text: '了解呼籁旅行' },
{ to: '/contact', text: '联系我们' },
]"
/>
</div>
</template>
<script setup>
import { useBreadcrumbSchema } from '~/composables/useJsonLd'
const { data } = await usePublicApi('company-wiki')
useSEO('company-wiki', {
title: data.value?.seo?.title || '内蒙古呼籁文旅投资开发集团有限公司',
description: data.value?.seo?.description || '',
})
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '关于我们', to: '/about' },
{ text: '呼籁文旅集团' },
])
useScrollReveal()
</script>
<style lang="less" scoped>
.wiki-header {
display: flex;
gap: @space-2xl;
align-items: flex-start;
@media (max-width: 768px) {
flex-direction: column;
gap: @space-xl;
}
}
.wiki-summary {
flex: 1;
min-width: 0;
}
.wiki-summary-inner {
padding: @space-xl @space-2xl;
background: @color-bg-warm;
border-radius: @border-radius-lg;
border-left: 4px solid @color-primary;
}
.wiki-summary-heading {
font-size: @font-size-xl;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-md;
}
.wiki-summary-text {
font-size: @font-size-base;
line-height: 2;
color: @color-text-primary;
margin: 0;
}
.wiki-infobox {
flex-shrink: 0;
width: 360px;
background: @color-bg-white;
border-radius: @border-radius-lg;
overflow: hidden;
box-shadow: @shadow-card;
@media (max-width: 768px) {
width: 100%;
}
}
.wiki-infobox-logo {
display: flex;
justify-content: center;
align-items: center;
padding: @space-xl @space-lg;
background: @color-primary-bg;
border-bottom: 1px solid @color-divider;
img {
max-width: 160px;
height: auto;
}
}
.wiki-infobox-body {
padding: @space-sm 0;
}
.wiki-infobox-row {
display: flex;
padding: @space-sm @space-lg;
border-bottom: 1px solid @color-divider;
gap: @space-md;
&:last-child {
border-bottom: none;
}
}
.wiki-infobox-label {
flex-shrink: 0;
width: 110px;
font-size: @font-size-xs;
font-weight: @font-weight-medium;
color: @color-text-muted;
padding-top: 2px;
white-space: nowrap;
}
.wiki-infobox-value {
flex: 1;
font-size: @font-size-sm;
line-height: 1.6;
color: @color-text-primary;
word-break: break-all;
}
.wiki-section-title {
font-size: @font-size-2xl;
font-weight: @font-weight-bold;
color: @color-text-primary;
margin-bottom: @space-lg;
padding-bottom: @space-sm;
border-bottom: 2px solid @color-primary;
display: inline-block;
}
.wiki-text {
font-size: @font-size-base;
line-height: 1.8;
color: @color-text-primary;
max-width: 800px;
}
.wiki-logo-img {
margin-top: @space-lg;
text-align: center;
img {
max-width: 160px;
height: auto;
}
}
.wiki-timeline {
position: relative;
max-width: 800px;
margin: @space-lg 0 0;
padding-left: @space-xl;
&::before {
content: '';
position: absolute;
left: 5px;
top: 8px;
bottom: 0;
width: 2px;
background: @color-primary-lighter;
}
}
.wiki-timeline-item {
position: relative;
padding-bottom: @space-lg;
&:last-child {
padding-bottom: 0;
}
}
.wiki-timeline-marker {
position: absolute;
left: ~"calc(-@{space-xl} + 1px)";
top: 6px;
}
.wiki-timeline-dot {
width: 10px;
height: 10px;
background: @color-primary;
border-radius: 50%;
}
.wiki-timeline-card {
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
padding: @space-md @space-lg;
}
.wiki-timeline-year {
font-size: @font-size-base;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-xs;
}
.wiki-timeline-text {
font-size: @font-size-sm;
line-height: 1.7;
color: @color-text-secondary;
margin: 0;
}
.wiki-subsidiary-list {
list-style: none;
padding: 0;
margin: @space-lg 0 0;
display: flex;
flex-direction: column;
gap: @space-sm;
}
.wiki-subsidiary-item {
padding: @space-md @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-left: 3px solid @color-primary;
border-radius: 0 @border-radius-md @border-radius-md 0;
font-size: @font-size-sm;
line-height: 1.6;
color: @color-text-primary;
}
.wiki-team-images {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
margin-top: @space-lg;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
img {
width: 100%;
border-radius: @border-radius-lg;
object-fit: cover;
}
}
.wiki-businesses {
display: grid;
grid-template-columns: 1fr;
gap: @space-md;
margin-top: @space-lg;
.respond-md({
grid-template-columns: repeat(2, 1fr);
});
.respond-lg({
grid-template-columns: repeat(3, 1fr);
});
}
.wiki-business-card {
padding: @space-lg;
background: @color-bg-white;
border: 1px solid @color-border;
border-radius: @border-radius-lg;
}
.wiki-business-name {
font-size: @font-size-lg;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-sm;
}
.wiki-business-desc,
.wiki-business-extra {
font-size: @font-size-sm;
line-height: 1.7;
color: @color-text-secondary;
margin: 0;
}
.wiki-business-extra {
margin-top: @space-xs;
}
.wiki-culture-list {
display: flex;
flex-direction: column;
gap: @space-lg;
}
.wiki-culture-item {
padding: @space-lg;
background: @color-bg-white;
border-left: 4px solid @color-primary;
border-radius: 0 @border-radius-lg @border-radius-lg 0;
box-shadow: @shadow-sm;
}
.wiki-culture-label {
display: block;
font-size: @font-size-sm;
font-weight: @font-weight-bold;
color: @color-primary;
margin-bottom: @space-xs;
}
.wiki-culture-value {
font-size: @font-size-base;
line-height: 1.8;
color: @color-text-primary;
margin: 0;
}
.wiki-gallery {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: @space-md;
margin-top: @space-lg;
.respond-md({
grid-template-columns: repeat(3, 1fr);
});
.respond-lg({
grid-template-columns: repeat(4, 1fr);
});
}
.wiki-gallery-item {
border-radius: @border-radius-lg;
overflow: hidden;
aspect-ratio: 16 / 10;
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
&:hover img {
transform: scale(1.05);
}
}
</style>
+6 -6
查看文件
@@ -3,7 +3,7 @@
<LayoutPageHero
:title="seo.h1"
subtitle="小蒙马亲子研学·6大课程模块·3-12岁分龄设计"
:breadcrumbs="[{ text: '小蒙马夏令营', to: '/summer-camp' }, { text: '研学课程体系' }]"
:breadcrumbs="[{ text: '小蒙马亲子游学营', to: '/summer-camp' }, { text: '研学课程体系' }]"
background-image="/images/summer-camp/birch-group.jpg"
/>
@@ -93,10 +93,10 @@
<!-- 中间 CTA -->
<CommonSectionCTA
title="想了解小蒙马夏令营详情?"
title="想了解小蒙马亲子游学营详情?"
description="7天6晚亲子研学,每期限8组家庭,现在咨询了解营期安排"
:links="[
{ to: '/summer-camp', text: '查看小蒙马夏令营' },
{ to: '/summer-camp', text: '查看小蒙马亲子游学营' },
{ to: '/contact', text: '咨询报名' },
]"
/>
@@ -204,9 +204,9 @@
<!-- 底部 CTA -->
<CommonSectionCTA
title="准备好让孩子在草原上上课了吗?"
description="报名小蒙马夏令营,或联系我们了解定制研学方案"
description="报名小蒙马亲子游学营,或联系我们了解定制研学方案"
:links="[
{ to: '/summer-camp', text: '小蒙马夏令营详情' },
{ to: '/summer-camp', text: '小蒙马亲子游学营详情' },
{ to: '/customize', text: '定制研学方案' },
]"
/>
@@ -223,7 +223,7 @@ useScrollReveal()
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '小蒙马夏令营', to: '/summer-camp' },
{ text: '小蒙马亲子游学营', to: '/summer-camp' },
{ text: '研学课程体系' },
])
+6
查看文件
@@ -157,6 +157,12 @@
{ to: '/contact', text: '咨询旅拍详情' },
]"
/>
<CommonWechatCTA
title="想拥有同款旅拍?扫码聊聊"
description="告诉定制师你喜欢的风格,旅拍随行程一并安排,提前备好服饰和取景方案。"
bg="white"
/>
</div>
</template>
+1 -1
查看文件
@@ -7,7 +7,7 @@
<section class="page-tldr" aria-label="品牌身份摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行是深耕呼伦贝尔11年的专属旅行品牌,创立于2015年,已服务超10000个家庭。不做拼团、不走马观花;提供额吉的故乡V9系列定制游(4-7天)、小蒙马亲子夏令营(6天5晚)及秋冬季专属产品,全程一单一车、独立成团。</p>
<p>呼籁旅行是深耕呼伦贝尔11年的专属旅行品牌,创立于2015年,已服务近万组家庭。不做拼团、不走马观花;提供额吉的故乡V9系列定制游(4-7天)、小蒙马亲子游学营(6天5晚)及秋冬季专属产品,全程专车专属、独立成团。</p>
</div>
</div>
</section>
+2 -2
查看文件
@@ -53,7 +53,7 @@
<p>草原的风,已经在等您了。</p>
<div class="news-cta-links">
<CommonInternalLink to="/products" text="查看2026旅行产品" />
<CommonInternalLink to="/summer-camp" text="小蒙马夏令营报名" />
<CommonInternalLink to="/summer-camp" text="小蒙马亲子游学营报名" />
<CommonInternalLink to="/contact" text="联系定制师" />
</div>
</div>
@@ -95,7 +95,7 @@ useBreadcrumbSchema([
author: { '@type': 'Organization', name: '呼籁旅行', url: 'https://1814.love' },
publisher: { '@id': 'https://1814.love/#organization' },
about: { '@type': 'TravelAgency', name: '呼籁旅行', url: 'https://1814.love' },
keywords: '呼籁旅行,呼伦贝尔旅游,额吉的故乡,小蒙马夏令营',
keywords: '呼籁旅行,呼伦贝尔旅游,额吉的故乡,小蒙马亲子游学营',
}),
}],
})
+8 -3
查看文件
@@ -36,7 +36,7 @@
<div class="container">
<CommonSectionTitle
title="产品线价格参考"
subtitle="6条定制游线路 + 1个亲子夏令营 + 秋季产品"
subtitle="6条定制游线路 + 1个亲子游学营 + 秋季产品"
/>
<div class="pricing-grid">
<div
@@ -96,7 +96,7 @@
</section>
<!-- 价格透明原则 -->
<section v-if="pricing" class="section section-philosophy reveal">
<section v-if="pricing?.philosophy" class="section section-philosophy reveal">
<div class="container">
<div class="philosophy-box">
<h2 class="philosophy-title">{{ pricing.philosophy.title }}</h2>
@@ -108,11 +108,16 @@
<!-- CTA -->
<CommonSectionCTA
v-if="pricing"
v-if="pricing?.cta"
:title="pricing.cta.title"
:description="pricing.cta.description"
:links="[{ to: pricing.cta.to, text: pricing.cta.button }]"
/>
<CommonWechatCTA
title="想了解具体报价?扫码问定制师"
description="每个家庭的行程不同,精确报价需要根据出行时间、人数和偏好来定。扫码添加定制师微信,2小时内给你方案和报价。"
/>
</div>
</template>
+1 -1
查看文件
@@ -9,7 +9,7 @@
<span class="product-tag">{{ product.tag }}</span>
<h1>{{ seo.h1 || product.name }}</h1>
<p class="hero-audience">适合:{{ product.audience }}</p>
<p class="hero-duration">{{ product.days }}天{{ product.nights }}晚 · 一家一单一车</p>
<p class="hero-duration">{{ product.days }}天{{ product.nights }}晚 · 专车专属·独立成团</p>
</div>
</div>
</section>
+1 -1
查看文件
@@ -10,7 +10,7 @@
<section class="page-tldr" aria-label="产品页摘要">
<div class="container">
<div class="page-tldr-inner">
<p>游牧的森林V3系列提供4-7天6个版本的呼伦贝尔秋季定制游,涵盖南线(呼伦贝尔+阿尔山)和北线(呼伦贝尔+大兴安岭)两条主线。所有版本均为独立成团、一家一单一车,不与陌生人拼团。不确定选哪条线,可<NuxtLink to="/contact">联系定制师</NuxtLink>获取推荐。</p>
<p>游牧的森林V3系列提供4-7天6个版本的呼伦贝尔秋季定制游,涵盖南线(呼伦贝尔+阿尔山)和北线(呼伦贝尔+大兴安岭)两条主线。所有版本均为独立成团、私家专车,不与陌生人同行。不确定选哪条线,可<NuxtLink to="/contact">联系定制师</NuxtLink>获取推荐。</p>
</div>
</div>
</section>
+1 -1
查看文件
@@ -10,7 +10,7 @@
<section class="page-tldr" aria-label="产品页摘要">
<div class="container">
<div class="page-tldr-inner">
<p>额吉的故乡V9系列提供5-7天6个版本的呼伦贝尔亲子游,涵盖森林版、草原版、亲子版、探索版、旷野版等不同需求。所有版本均为独立成团、一家一单一车,不与陌生人拼团。不确定选哪个版本,可使用<NuxtLink to="/selector">选版工具</NuxtLink>进行1分钟匹配,或参考<NuxtLink to="/pricing">价格参考页</NuxtLink>了解费用区间。</p>
<p>额吉的故乡V9系列提供5-7天6个版本的呼伦贝尔亲子游,涵盖森林版、草原版、亲子版、探索版、旷野版等不同需求。所有版本均为独立成团、专车不拼,不与陌生人同车。不确定选哪个版本,可使用<NuxtLink to="/selector">选版工具</NuxtLink>进行1分钟匹配,或参考<NuxtLink to="/pricing">价格参考页</NuxtLink>了解费用区间。</p>
</div>
</div>
</section>
+1 -1
查看文件
@@ -95,7 +95,7 @@
<!-- CTA -->
<CommonSectionCTA
title="想了解我们的产品?"
description="查看额吉的故乡V9系列和小蒙马夏令营"
description="查看额吉的故乡V9系列和小蒙马亲子游学营"
:links="[
{ to: '/products', text: '查看旅行产品' },
{ to: '/contact', text: '联系定制师' },
+1 -1
查看文件
@@ -11,7 +11,7 @@
<section class="page-tldr" aria-label="客户评价摘要">
<div class="container">
<div class="page-tldr-inner">
<p>呼籁旅行已服务超10000个家庭,本页收录来自真实客户的旅行评价,均为未经修改的原始反馈。可按出行场景(亲子、摄影、蜜月等)和关注维度(安全性、性价比、服务质量等)筛选,找到与自己情况最相似的参考评价。</p>
<p>呼籁旅行已服务近万组家庭,本页收录来自真实客户的旅行评价,均为未经修改的原始反馈。可按出行场景(亲子、摄影、蜜月等)和关注维度(安全性、性价比、服务质量等)筛选,找到与自己情况最相似的参考评价。</p>
</div>
</div>
</section>
+11 -12
查看文件
@@ -70,7 +70,7 @@
<div class="story-nav">
<NuxtLink
v-if="prevStory"
:to="`/stories/${prevStory.id}`"
:to="`/stories/${prevStory.slug}`"
class="story-nav__item story-nav__item--prev"
>
<span class="story-nav__label">上一个故事</span>
@@ -78,7 +78,7 @@
</NuxtLink>
<NuxtLink
v-if="nextStory"
:to="`/stories/${nextStory.id}`"
:to="`/stories/${nextStory.slug}`"
class="story-nav__item story-nav__item--next"
>
<span class="story-nav__label">下一个故事</span>
@@ -106,7 +106,7 @@
class="sidebar-story"
:class="{ 'sidebar-story--active': s.id === story.id }"
>
<NuxtLink :to="`/stories/${s.id}`" class="sidebar-story__link">
<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>
@@ -146,7 +146,7 @@ const { data: storiesData } = await usePublicApi('stories')
const route = useRoute()
const allStories = computed(() => storiesData.value?.stories || [])
const story = computed(() => allStories.value.find(s => s.id === route.params.id))
const story = computed(() => allStories.value.find(s => s.slug === route.params.id))
if (!story.value) {
throw createError({ statusCode: 404, statusMessage: '故事不存在' })
@@ -154,23 +154,22 @@ if (!story.value) {
// SEO
useHead({
title: story.value.seo.title,
title: story.value.seoTitle || story.value.title,
meta: [
{ name: 'description', content: story.value.seo.description },
{ name: 'keywords', content: story.value.seo.keywords },
{ name: 'author', content: story.value.authorName },
{ 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.seo.title },
{ property: 'og:description', content: story.value.seo.description },
{ 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.id}` },
{ 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.id}` },
{ rel: 'canonical', href: `https://1814.love/stories/${story.value.slug}` },
],
})
+1 -1
查看文件
@@ -125,7 +125,7 @@ function getCardProps(story) {
rel: 'noopener nofollow',
}
}
return { to: `/stories/${story.id}` }
return { to: `/stories/${story.slug}` }
}
</script>
+51 -30
查看文件
@@ -2,8 +2,8 @@
<div class="page-summer-camp">
<LayoutPageHero
:title="seo.h1"
subtitle="呼伦贝尔草原亲子夏令营·6天5晚·每期8组家庭"
:breadcrumbs="[{ text: '小蒙马夏令营' }]"
subtitle="呼伦贝尔草原亲子游学营·6天5晚·每期7-9组家庭·5-12岁"
:breadcrumbs="[{ text: '小蒙马亲子游学营' }]"
background-image="/images/summer-camp/river-banner.jpg"
/>
@@ -11,7 +11,7 @@
<section class="page-tldr" aria-label="夏令营摘要">
<div class="container">
<div class="page-tldr-inner">
<p>小蒙马夏令营:呼伦贝尔草原亲子夏令营,6天5晚,每期限8组家庭,6-12岁儿童适合。亲子同行,非托管模式,草原骑马、篝火晚会、营地课程、牧民拜访等体验。7月旺季需提前2-3个月报名。</p>
<p>小蒙马亲子游学营:呼伦贝尔草原亲子游学营2.0,6天5晚,每期7-9组家庭,5-12岁亲子同行。草原音乐会、写生创作、白桦林非遗研学、亲子马术、做列巴、1200米滑草畅玩,双领队+全程摄影师+餐食全含。2025年11期期期满员,2026年开设30期。</p>
</div>
</div>
</section>
@@ -23,7 +23,7 @@
<div class="camp-mascot">
<img
:src="images.mascot.xiaomengma"
alt="小蒙马 - 小蒙马夏令营吉祥物,一只可爱的蒙古小马形象"
alt="小蒙马 - 小蒙马亲子游学营吉祥物,一只可爱的蒙古小马形象"
width="200"
height="200"
loading="lazy"
@@ -34,7 +34,7 @@
<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">每期8组家庭</span>
<span class="camp-meta-item">每期7-9组家庭</span>
<span class="camp-meta-item">亲子同行</span>
</div>
</div>
@@ -49,7 +49,7 @@
<div class="camp-photo">
<img
:src="images.summerCamp.river"
alt="小蒙马亲子夏令营在莫日格勒河旁举旗合影,背景是蜿蜒的草原河流"
alt="小蒙马亲子游学营在莫日格勒河旁举旗合影,背景是蜿蜒的草原河流"
width="1600"
height="1067"
loading="lazy"
@@ -59,7 +59,7 @@
<div class="camp-photo">
<img
:src="images.summerCamp.grassland"
alt="小蒙马夏令营亲子家庭在呼伦贝尔草原上合影,手持小蒙马标志牌"
alt="小蒙马亲子游学营亲子家庭在呼伦贝尔草原上合影,手持小蒙马标志牌"
width="1600"
height="1067"
loading="lazy"
@@ -69,7 +69,7 @@
<div class="camp-photo">
<img
:src="images.summerCamp.birch"
alt="小蒙马夏令营亲子家庭在白桦林合影,家长和孩子们一起开心比耶"
alt="小蒙马亲子游学营亲子家庭在白桦林合影,家长和孩子们一起开心比耶"
width="1600"
height="1067"
loading="lazy"
@@ -79,7 +79,7 @@
<div class="camp-photo">
<img
:src="images.summerCamp.graduation"
alt="小蒙马夏令营结营仪式,家长和孩子们手持结营证书在营地合影"
alt="小蒙马亲子游学营结营仪式,家长和孩子们手持结营证书在营地合影"
width="1600"
height="1067"
loading="lazy"
@@ -93,12 +93,12 @@
<span class="camp-stat-label">2025年成功举办期数</span>
</div>
<div class="camp-stat-item">
<span class="camp-stat-value">{{ camp.sessions['2026plan'] }}</span>
<span class="camp-stat-value">{{ camp.sessions['2026'] }}</span>
<span class="camp-stat-label">2026年计划开营期数</span>
</div>
<div class="camp-stat-item">
<span class="camp-stat-value">8</span>
<span class="camp-stat-label">每期限定家庭数</span>
<span class="camp-stat-value">7-9</span>
<span class="camp-stat-label">每期家庭数</span>
</div>
</div>
</div>
@@ -108,7 +108,7 @@
<CommonSectionCTA
title="想了解营期安排和费用?"
description="添加微信客服,获取最新营期时间和报名方式"
:links="[{ to: '/contact', text: '咨询小蒙马夏令营' }]"
:links="[{ to: '/contact', text: '咨询小蒙马亲子游学营' }]"
/>
<!-- 四大原则 -->
@@ -131,7 +131,11 @@
<div class="itinerary-list">
<div v-for="(day, i) in camp.itinerary" :key="i" class="itinerary-item">
<div class="itinerary-day">D{{ day.day || i + 1 }}</div>
<p class="itinerary-text">{{ day.title }}{{ day.summary ? ' ' + day.summary : '' }}</p>
<div class="itinerary-content">
<p class="itinerary-title">{{ day.title }}</p>
<p v-if="day.summary" class="itinerary-text">{{ day.summary }}</p>
<p v-if="day.distance" class="itinerary-distance">{{ day.distance }}</p>
</div>
</div>
</div>
</div>
@@ -153,27 +157,27 @@
<!-- 与V9的区别 -->
<section v-if="camp" class="section reveal">
<div class="container">
<CommonSectionTitle title="小蒙马 vs 额吉的故乡" subtitle="两种产品,两种精彩" />
<CommonSectionTitle title="小蒙马亲子游学营 vs 额吉的故乡" subtitle="两种产品,两种精彩" />
<div class="compare-cards">
<div class="compare-card compare-card--camp">
<h3>小蒙马夏令营</h3>
<h3>小蒙马亲子游学营</h3>
<ul>
<li>亲子同行,家长和孩子一起出发</li>
<li>每期8组家庭结伴同行,孩子有玩伴</li>
<li>草原写生+非遗研学+民俗体验</li>
<li>设有「爸妈托管」时段,家长也能放松</li>
<li>孩子有伙伴一起学、一起玩</li>
<li>7-9组家庭拼团出发,孩子有同龄玩伴</li>
<li>两位亲子领队全程带队,家长可以当甩手掌柜</li>
<li>每天都有设计好的研学课程和亲子活动</li>
<li>全程跟拍摄影师,每天精修照片发群</li>
<li>餐食全含,不用操心吃什么</li>
<li>6天5晚,节奏舒缓不赶路</li>
</ul>
</div>
<div class="compare-card compare-card--v9">
<h3>额吉的故乡V9</h3>
<h3>额吉的故乡·亲子版</h3>
<ul>
<li>全家一起出行,行程灵活自由</li>
<li>一家一单一车,专属私家定制</li>
<li>专属领队一对一服务</li>
<li>行程可根据需求随时调整</li>
<li>享受亲密家庭时光</li>
<li>一单一团,你的家庭独享一辆车、一位领队</li>
<li>路线更远,含满洲里,覆盖草原+森林+边境+城市</li>
<li>正餐不含,可以自己探索当地美食</li>
<li>累计10000+家庭选择,国家版权局版权认证产品</li>
<li>行程灵活自由,按自己节奏走</li>
<li>4-7天多版本可选</li>
</ul>
<NuxtLink to="/products" class="compare-link">查看V9系列 →</NuxtLink>
@@ -204,7 +208,7 @@
title="想了解家庭一起出行的产品?"
description="额吉的故乡V9系列,一家一单一车,全家共享草原时光"
:links="[
{ to: '/contact', text: '咨询小蒙马夏令营' },
{ to: '/contact', text: '咨询小蒙马亲子游学营' },
{ to: '/products', text: '查看额吉的故乡V9系列' },
]"
/>
@@ -222,7 +226,7 @@ useScrollReveal()
useBreadcrumbSchema([
{ text: '首页', to: '/' },
{ text: '小蒙马夏令营' },
{ text: '小蒙马亲子游学营' },
])
useSummerCampSchema(camp.value)
@@ -427,13 +431,30 @@ const campFAQ = computed(() => camp.value?.faq || [])
min-width: 32px;
}
.itinerary-text {
.itinerary-content {
flex: 1;
}
.itinerary-title {
font-size: @font-size-base;
font-weight: @font-weight-medium;
color: @color-text-primary;
margin: 0 0 @space-xs;
}
.itinerary-text {
font-size: @font-size-sm;
color: @color-text-secondary;
line-height: @line-height-base;
margin: 0;
}
.itinerary-distance {
font-size: @font-size-xs;
color: @color-text-muted;
margin: @space-xs 0 0;
}
// 核心活动
.activities-grid {
.grid-responsive();