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>
这个提交包含在:
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<section v-if="data" class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle :title="data.title" :subtitle="data.subtitle" />
|
||||
<p class="compare-intro">{{ data.intro }}</p>
|
||||
|
||||
<!-- 桌面端:紧凑对比表格 -->
|
||||
<div class="compare-table-desktop">
|
||||
<div class="compare-grid">
|
||||
<!-- 表头 -->
|
||||
<div class="grid-header">
|
||||
<div class="grid-label-col"></div>
|
||||
<div
|
||||
v-for="dest in data.destinations"
|
||||
:key="dest.id"
|
||||
class="grid-dest-col"
|
||||
:class="{ 'grid-dest-col--highlight': dest.highlight }"
|
||||
>
|
||||
<span v-if="dest.highlight" class="recommend-badge">推荐</span>
|
||||
<h3 class="dest-name">{{ dest.name }}</h3>
|
||||
<span class="dest-tag">{{ dest.tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 数据行 -->
|
||||
<div v-for="dim in data.dimensions" :key="dim.label" class="grid-row">
|
||||
<div class="grid-label-col">
|
||||
<span class="dim-icon">{{ dim.icon }}</span>
|
||||
<span class="dim-label">{{ dim.label }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="(val, i) in dim.values"
|
||||
:key="i"
|
||||
class="grid-value-col"
|
||||
:class="{ 'grid-value-col--highlight': data.destinations[i]?.highlight }"
|
||||
>
|
||||
{{ val }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 坦诚说明 - 桌面端紧贴表格底部 -->
|
||||
<div class="honest-strip">
|
||||
<div class="honest-strip-header">
|
||||
<span class="honest-icon">💬</span>
|
||||
<span class="honest-label">{{ data.honestNote.title }}</span>
|
||||
<span class="honest-dash">—</span>
|
||||
<span class="honest-sub">{{ data.honestNote.subtitle }}</span>
|
||||
</div>
|
||||
<div class="honest-strip-items">
|
||||
<span v-for="(item, i) in data.honestNote.items" :key="i" class="honest-item">
|
||||
{{ item }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端:Tab切换卡片 -->
|
||||
<div class="compare-mobile">
|
||||
<div class="tab-bar">
|
||||
<button
|
||||
v-for="(dest, i) in data.destinations"
|
||||
:key="dest.id"
|
||||
class="tab-btn"
|
||||
:class="{ 'tab-btn--active': activeTab === i, 'tab-btn--highlight': dest.highlight }"
|
||||
@click="activeTab = i"
|
||||
>
|
||||
<span v-if="dest.highlight && activeTab !== i" class="tab-dot"></span>
|
||||
{{ dest.name }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="dest-card">
|
||||
<p class="dest-card-tag">{{ data.destinations[activeTab].tag }}</p>
|
||||
<div class="dest-card-grid">
|
||||
<div v-for="dim in data.dimensions" :key="dim.label" class="dest-card-item">
|
||||
<div class="dest-card-dim">
|
||||
<span class="dest-card-icon">{{ dim.icon }}</span>
|
||||
<span class="dest-card-label">{{ dim.label }}</span>
|
||||
</div>
|
||||
<p class="dest-card-value">{{ dim.values[activeTab] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 坦诚说明 - 移动端 -->
|
||||
<div v-if="data.destinations[activeTab]?.highlight" class="honest-mobile">
|
||||
<p class="honest-mobile-title">
|
||||
<span class="honest-icon-sm">💬</span>
|
||||
{{ data.honestNote.title }}
|
||||
</p>
|
||||
<ul class="honest-mobile-list">
|
||||
<li v-for="(item, i) in data.honestNote.items" :key="i">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据来源 -->
|
||||
<p class="data-source">{{ data.dataSources }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const { data } = await usePublicApi('destinations')
|
||||
|
||||
const activeTab = ref(0)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.compare-intro {
|
||||
max-width: 720px;
|
||||
margin: 0 auto @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// ========== 桌面端表格 ==========
|
||||
.compare-table-desktop {
|
||||
display: none;
|
||||
|
||||
.respond-md({
|
||||
display: block;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
box-shadow: @shadow-card;
|
||||
margin-bottom: @space-xl;
|
||||
});
|
||||
}
|
||||
|
||||
.compare-grid {
|
||||
background: @color-bg-white;
|
||||
}
|
||||
|
||||
.grid-header {
|
||||
display: grid;
|
||||
grid-template-columns: 100px repeat(3, 1fr);
|
||||
background: @color-primary-bg;
|
||||
}
|
||||
|
||||
.grid-label-col {
|
||||
padding: @space-md @space-md;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.grid-dest-col {
|
||||
padding: @space-lg @space-md;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
&--highlight {
|
||||
background: fadeout(@color-primary, 92%);
|
||||
}
|
||||
}
|
||||
|
||||
.recommend-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
font-size: 11px;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #fff;
|
||||
background: @color-primary;
|
||||
border-radius: @border-radius-full;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.dest-name {
|
||||
font-size: @font-size-md;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.dest-tag {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
// 数据行
|
||||
.grid-row {
|
||||
display: grid;
|
||||
grid-template-columns: 100px repeat(3, 1fr);
|
||||
border-top: 1px solid @color-divider;
|
||||
|
||||
&:nth-child(even) {
|
||||
background: @color-bg-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.dim-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dim-label {
|
||||
font-size: @font-size-xs;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.grid-value-col {
|
||||
padding: 12px @space-md;
|
||||
font-size: 13px;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-normal;
|
||||
border-left: 1px solid @color-divider;
|
||||
|
||||
&--highlight {
|
||||
color: @color-text-primary;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 坦诚说明条 - 桌面 ==========
|
||||
.honest-strip {
|
||||
background: @color-bg-warm;
|
||||
padding: @space-md @space-lg;
|
||||
border-top: 1px solid fadeout(@color-accent, 80%);
|
||||
}
|
||||
|
||||
.honest-strip-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: @space-sm;
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
|
||||
.honest-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.honest-label {
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-accent;
|
||||
}
|
||||
|
||||
.honest-dash {
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
.honest-sub {
|
||||
color: @color-text-muted;
|
||||
font-size: @font-size-xs;
|
||||
}
|
||||
|
||||
.honest-strip-items {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px @space-md;
|
||||
}
|
||||
|
||||
.honest-item {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-normal;
|
||||
position: relative;
|
||||
padding-left: 12px;
|
||||
|
||||
&::before {
|
||||
content: '·';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: @color-accent;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
top: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 移动端 Tab + 卡片 ==========
|
||||
.compare-mobile {
|
||||
display: block;
|
||||
|
||||
.respond-md({
|
||||
display: none;
|
||||
});
|
||||
}
|
||||
|
||||
.tab-bar {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
background: @color-bg-gray;
|
||||
border-radius: @border-radius-md;
|
||||
padding: 3px;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
flex: 1;
|
||||
padding: 10px 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-text-muted;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all @transition-fast;
|
||||
position: relative;
|
||||
|
||||
&--active {
|
||||
background: @color-bg-white;
|
||||
color: @color-text-primary;
|
||||
font-weight: @font-weight-bold;
|
||||
box-shadow: @shadow-xs;
|
||||
}
|
||||
|
||||
&--highlight:not(.tab-btn--active) {
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: @color-primary;
|
||||
border-radius: 50%;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.dest-card {
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-lg;
|
||||
box-shadow: @shadow-card;
|
||||
padding: @space-md;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.dest-card-tag {
|
||||
text-align: center;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
margin-bottom: @space-md;
|
||||
padding-bottom: @space-sm;
|
||||
border-bottom: 1px solid @color-divider;
|
||||
}
|
||||
|
||||
.dest-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1px;
|
||||
background: @color-divider;
|
||||
border-radius: @border-radius-md;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dest-card-item {
|
||||
padding: 12px;
|
||||
background: @color-bg-white;
|
||||
|
||||
// 让最后一个奇数项占满整行
|
||||
&:last-child:nth-child(odd) {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.dest-card-dim {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.dest-card-icon {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.dest-card-label {
|
||||
font-size: 11px;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-muted;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.dest-card-value {
|
||||
font-size: 13px;
|
||||
color: @color-text-primary;
|
||||
line-height: @line-height-normal;
|
||||
}
|
||||
|
||||
// ========== 坦诚说明 - 移动端 ==========
|
||||
.honest-mobile {
|
||||
background: @color-bg-warm;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-md;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.honest-mobile-title {
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-accent;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.honest-icon-sm {
|
||||
font-size: 14px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.honest-mobile-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
li {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-normal;
|
||||
padding-left: 14px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '·';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: @color-accent;
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 数据来源 ==========
|
||||
.data-source {
|
||||
text-align: center;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-placeholder;
|
||||
margin-top: @space-sm;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<section class="section section--warm">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="为什么选择呼籁" label="WHY HULAI" subtitle="不只是包车,是一套成熟的旅行服务体系" />
|
||||
<div class="diff-grid">
|
||||
<div v-for="(item, index) in itemsWithBg" :key="item.title" class="diff-item">
|
||||
<div class="diff-bg">
|
||||
<img :src="item.bgImage" :alt="item.title" loading="lazy" />
|
||||
</div>
|
||||
<div class="diff-content">
|
||||
<div class="diff-icon" v-html="icons[index % icons.length]" />
|
||||
<h3 class="diff-title">{{ item.title }}</h3>
|
||||
<p class="diff-desc">{{ item.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
items: { type: Array, required: true },
|
||||
})
|
||||
|
||||
const bgImages = [
|
||||
'/images/seasons/summer-hero.jpg',
|
||||
'/images/seasons/summer-car.jpg',
|
||||
'/images/seasons/summer-river.jpg',
|
||||
'/images/seasons/autumn-forest.jpg',
|
||||
'/images/seasons/autumn-lake.jpg',
|
||||
'/images/seasons/winter-hero.jpg',
|
||||
]
|
||||
|
||||
const itemsWithBg = computed(() =>
|
||||
props.items.map((item, i) => ({
|
||||
...item,
|
||||
bgImage: bgImages[i % bgImages.length],
|
||||
}))
|
||||
)
|
||||
|
||||
const icons = [
|
||||
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21l9-18 9 18"/><path d="M12 3v18"/><path d="M3 21h18"/></svg>',
|
||||
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="1" y="3" width="15" height="13" rx="2"/><polygon points="16 8 20 8 23 11 23 16 16 16 16 8"/><circle cx="5.5" cy="18.5" r="2.5"/><circle cx="18.5" cy="18.5" r="2.5"/></svg>',
|
||||
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>',
|
||||
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z"/><circle cx="12" cy="13" r="4"/></svg>',
|
||||
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 00-4-4H5a4 4 0 00-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 00-3-3.87"/><path d="M16 3.13a4 4 0 010 7.75"/></svg>',
|
||||
'<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/></svg>',
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.diff-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-lg;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
});
|
||||
|
||||
.respond-lg({
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
});
|
||||
}
|
||||
|
||||
.diff-item {
|
||||
position: relative;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
border: 1px solid @color-border;
|
||||
transition: box-shadow 0.35s cubic-bezier(0.23, 1, 0.32, 1), transform 0.35s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
&:hover {
|
||||
box-shadow: @shadow-lg;
|
||||
transform: translateY(-3px);
|
||||
|
||||
.diff-bg img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.diff-icon {
|
||||
color: @color-primary;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 浅色背景图
|
||||
.diff-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
opacity: 0.15;
|
||||
filter: saturate(0.6);
|
||||
transition: transform 0.6s cubic-bezier(0.23, 1, 0.32, 1), opacity 0.4s ease;
|
||||
}
|
||||
|
||||
// 叠加一层白色渐变让文字区域更清晰
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(255,255,255,0.7) 0%, rgba(255,255,255,0.4) 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.diff-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: @space-xl @space-lg;
|
||||
}
|
||||
|
||||
.diff-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: @border-radius-lg;
|
||||
background: rgba(240, 247, 244, 0.9);
|
||||
color: @color-primary-light;
|
||||
margin-bottom: @space-lg;
|
||||
transition: color @transition-base, background @transition-base;
|
||||
}
|
||||
|
||||
.diff-title {
|
||||
font-size: @font-size-lg;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.diff-desc {
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-relaxed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<section v-if="brand && images" class="hero">
|
||||
<div class="hero-bg">
|
||||
<img
|
||||
:src="images.hero.background"
|
||||
alt="呼伦贝尔草原风光 莫日格勒河蜿蜒穿过辽阔草原"
|
||||
class="hero-bg-img"
|
||||
width="1600"
|
||||
height="900"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
<div class="hero-overlay" />
|
||||
</div>
|
||||
<div class="container hero-inner">
|
||||
<h1 class="hero-title">{{ seo.h1 }}</h1>
|
||||
<p class="hero-emotional">{{ brand.slogan.emotional }}</p>
|
||||
<p class="hero-functional">{{ brand.slogan.functional }}</p>
|
||||
<div class="hero-actions">
|
||||
<NuxtLink to="/products" class="hero-btn hero-btn--primary">
|
||||
<span>查看产品</span>
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M6 3l5 5-5 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</NuxtLink>
|
||||
<NuxtLink to="/faq" class="hero-btn hero-btn--outline">常见问答</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部弧形裁切 -->
|
||||
<div class="hero-curve">
|
||||
<svg viewBox="0 0 1440 64" fill="none" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path d="M0 64h1440V32C1200 0 240 0 0 32v32z" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
seo: { type: Object, required: true },
|
||||
})
|
||||
|
||||
const { data: brand } = await usePublicApi('brand')
|
||||
const { data: images } = await usePublicApi('images')
|
||||
|
||||
// 仅在首页 preload Hero 图片,避免全站加载
|
||||
useHead({
|
||||
link: [
|
||||
{ rel: 'preload', href: images.value?.hero?.background, as: 'image' },
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.hero {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 480px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.respond-md({
|
||||
min-height: 580px;
|
||||
});
|
||||
|
||||
.respond-lg({
|
||||
min-height: 640px;
|
||||
});
|
||||
}
|
||||
|
||||
.hero-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hero-bg-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center 40%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
// 更丰富的渐变遮罩
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(26, 26, 46, 0.35) 0%, rgba(26, 26, 46, 0.15) 40%, rgba(26, 26, 46, 0.5) 100%),
|
||||
linear-gradient(135deg, rgba(45, 106, 79, 0.3) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
.hero-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding-top: @space-4xl;
|
||||
padding-bottom: 100px;
|
||||
text-align: center;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
|
||||
.respond-md({
|
||||
padding-top: @space-4xl;
|
||||
padding-bottom: 120px;
|
||||
});
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
color: #FFFFFF;
|
||||
margin-bottom: @space-lg;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.2);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.hero-emotional {
|
||||
font-size: @font-size-xl;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-weight: @font-weight-medium;
|
||||
margin-bottom: @space-sm;
|
||||
text-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
|
||||
|
||||
.respond-md({
|
||||
font-size: @font-size-2xl;
|
||||
});
|
||||
}
|
||||
|
||||
.hero-functional {
|
||||
font-size: @font-size-md;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
margin-bottom: @space-xl;
|
||||
|
||||
.respond-md({
|
||||
font-size: @font-size-lg;
|
||||
});
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: @space-md;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.hero-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
padding: 14px 32px;
|
||||
border-radius: @border-radius-full;
|
||||
font-size: @font-size-base;
|
||||
font-weight: @font-weight-medium;
|
||||
text-decoration: none;
|
||||
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
.respond-md({
|
||||
padding: 16px 40px;
|
||||
font-size: @font-size-md;
|
||||
});
|
||||
|
||||
&--primary {
|
||||
background: #fff;
|
||||
color: @color-primary;
|
||||
border: 2px solid #fff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-color: rgba(255, 255, 255, 0.8);
|
||||
color: #fff;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 底部弧形
|
||||
.hero-curve {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
line-height: 0;
|
||||
color: @color-bg-gray;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
|
||||
.respond-md({
|
||||
height: 64px;
|
||||
});
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="旅行产品" subtitle="四季不同风景,总有一条线路适合你的家庭" />
|
||||
<div class="product-grid">
|
||||
<NuxtLink
|
||||
v-for="line in productLines"
|
||||
:key="line.id"
|
||||
:to="line.to"
|
||||
class="product-line-card"
|
||||
:class="`product-line-card--${line.theme}`"
|
||||
>
|
||||
<!-- 封面图 -->
|
||||
<div class="line-cover">
|
||||
<img :src="line.image" :alt="line.name" loading="lazy" />
|
||||
<div class="line-cover-overlay" />
|
||||
<span class="line-season-badge">{{ line.season }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 内容区 -->
|
||||
<div class="line-body">
|
||||
<div class="line-meta">
|
||||
<span class="line-duration">{{ line.duration }}</span>
|
||||
</div>
|
||||
<h3 class="line-name">{{ line.name }}</h3>
|
||||
<p class="line-desc">{{ line.description }}</p>
|
||||
<ul class="line-highlights">
|
||||
<li v-for="(h, i) in line.highlights" :key="i">{{ h }}</li>
|
||||
</ul>
|
||||
<span class="line-cta">了解详情 →</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const productLines = [
|
||||
{
|
||||
id: 'summer',
|
||||
name: '额吉的故乡',
|
||||
season: '夏季',
|
||||
theme: 'summer',
|
||||
duration: '4-7天 · V9系列',
|
||||
to: '/products',
|
||||
image: '/images/seasons/summer-hero.jpg',
|
||||
description: '呼伦贝尔草原家庭定制游,一家一单一车,6个版本适配不同假期',
|
||||
highlights: ['莫日格勒河草原穿越', '专业骑马体验', '呼籁自有营地住宿'],
|
||||
},
|
||||
{
|
||||
id: 'autumn',
|
||||
name: '游牧的森林',
|
||||
season: '秋季',
|
||||
theme: 'autumn',
|
||||
duration: '4-7天 · V3系列',
|
||||
to: '/products/autumn',
|
||||
image: '/images/seasons/autumn-hero.jpg',
|
||||
description: '金秋草原+阿尔山/大兴安岭,南北两线专属私家团',
|
||||
highlights: ['大兴安岭金秋林海', '阿尔山火山天池', '扎罗木得断崖秘境'],
|
||||
},
|
||||
{
|
||||
id: 'winter',
|
||||
name: '嗨冰雪',
|
||||
season: '冬季',
|
||||
theme: 'winter',
|
||||
duration: '5-7天 · V3系列',
|
||||
to: '/products/winter',
|
||||
image: '/images/seasons/winter-hero.jpg',
|
||||
description: '呼伦贝尔冬季定制游,南北两线,冰雪那达慕+驯鹿部落',
|
||||
highlights: ['冰雪那达慕体验', '雪地狼群投喂', '大雪原日出日落'],
|
||||
},
|
||||
{
|
||||
id: 'summer-camp',
|
||||
name: '小蒙马夏令营',
|
||||
season: '夏季',
|
||||
theme: 'camp',
|
||||
duration: '7天6晚',
|
||||
to: '/summer-camp',
|
||||
image: '/images/seasons/summer-river.jpg',
|
||||
description: '亲子营模式,每期8组家庭结伴同行,孩子有伙伴大人能放松',
|
||||
highlights: ['专业研学领队带队', '随团摄影师全程跟拍', '草原写生+非遗研学'],
|
||||
},
|
||||
{
|
||||
id: 'winter-camp',
|
||||
name: '小蒙马冬令营',
|
||||
season: '冬季',
|
||||
theme: 'winter-camp',
|
||||
duration: '7天6晚',
|
||||
to: '/winter-camp',
|
||||
image: '/images/seasons/winter-frost.jpg',
|
||||
description: '冰雪研学营,雪地徒步+冰上运动+驯鹿部落,零下30°的成长体验',
|
||||
highlights: ['冰雪那达慕体验', '驯鹿部落探访', '冬季星空观测'],
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-lg;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
});
|
||||
|
||||
.respond-lg({
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: @space-xl;
|
||||
});
|
||||
}
|
||||
|
||||
.product-line-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
box-shadow: @shadow-card;
|
||||
transition: box-shadow @transition-base, transform @transition-base;
|
||||
|
||||
&:hover {
|
||||
box-shadow: @shadow-card-hover;
|
||||
transform: translateY(-6px);
|
||||
|
||||
.line-cover img {
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
.line-cta {
|
||||
color: @color-primary-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 封面图
|
||||
.line-cover {
|
||||
position: relative;
|
||||
aspect-ratio: 16 / 10;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.6s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.line-cover-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(to bottom, transparent 40%, rgba(0, 0, 0, 0.3) 100%);
|
||||
}
|
||||
|
||||
.line-season-badge {
|
||||
position: absolute;
|
||||
top: @space-md;
|
||||
left: @space-md;
|
||||
padding: @space-xs @space-md;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-bold;
|
||||
border-radius: @border-radius-full;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
// 季节主题色
|
||||
.product-line-card--summer .line-season-badge {
|
||||
background: rgba(45, 106, 79, 0.85);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.product-line-card--autumn .line-season-badge {
|
||||
background: rgba(217, 119, 6, 0.85);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.product-line-card--winter .line-season-badge {
|
||||
background: rgba(59, 130, 246, 0.85);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.product-line-card--camp .line-season-badge {
|
||||
background: rgba(139, 92, 246, 0.85);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.product-line-card--winter-camp .line-season-badge {
|
||||
background: rgba(99, 102, 241, 0.85);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
// 内容区
|
||||
.line-body {
|
||||
padding: @space-lg @space-xl @space-xl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.line-meta {
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.line-duration {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.line-name {
|
||||
font-size: @font-size-xl;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-sm;
|
||||
line-height: @line-height-tight;
|
||||
}
|
||||
|
||||
.line-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.line-highlights {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-xs;
|
||||
margin-bottom: @space-lg;
|
||||
flex: 1;
|
||||
|
||||
li {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
padding-left: @space-md;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
font-size: @font-size-xs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line-cta {
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-primary;
|
||||
margin-top: auto;
|
||||
transition: color @transition-fast;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<section class="section section--gray">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="客户怎么说" label="REVIEWS" subtitle="来自真实家庭的旅行评价" />
|
||||
<div class="review-grid">
|
||||
<div v-for="r in reviews" :key="r.id" class="review-card">
|
||||
<div class="review-quote-mark">"</div>
|
||||
<blockquote class="review-text">{{ r.content }}</blockquote>
|
||||
<div class="review-footer">
|
||||
<div class="review-avatar">{{ r.nickname.charAt(0) }}</div>
|
||||
<div class="review-info">
|
||||
<span class="review-name">{{ r.nickname }}</span>
|
||||
<span class="review-date">{{ r.travelDate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="review-product">{{ r.productVersion }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="review-more">
|
||||
<CommonInternalLink to="/reviews" text="查看更多客户评价" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
reviews: { type: Array, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.review-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-lg;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
});
|
||||
|
||||
.respond-lg({
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
});
|
||||
}
|
||||
|
||||
.review-card {
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-lg;
|
||||
padding: @space-xl;
|
||||
border: 1px solid @color-border;
|
||||
border-left: 3px solid @color-primary-lighter;
|
||||
position: relative;
|
||||
transition: box-shadow 0.35s cubic-bezier(0.23, 1, 0.32, 1), transform 0.35s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
&:hover {
|
||||
box-shadow: @shadow-md;
|
||||
transform: translateY(-3px);
|
||||
border-left-color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.review-quote-mark {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 64px;
|
||||
line-height: 1;
|
||||
color: @color-primary-lighter;
|
||||
margin-bottom: -@space-md;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.review-text {
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-relaxed;
|
||||
margin-bottom: @space-lg;
|
||||
.text-clamp(4);
|
||||
}
|
||||
|
||||
.review-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-md;
|
||||
}
|
||||
|
||||
.review-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, @color-primary-bg, @color-primary-lighter);
|
||||
color: @color-primary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-bold;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.review-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.review-name {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-primary;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.review-date {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
.review-product {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
margin-top: @space-sm;
|
||||
padding-top: @space-sm;
|
||||
border-top: 1px solid @color-divider;
|
||||
}
|
||||
|
||||
.review-more {
|
||||
text-align: center;
|
||||
margin-top: @space-2xl;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<section class="trust-section">
|
||||
<div class="container">
|
||||
<div class="trust-grid">
|
||||
<CommonTrustBadge
|
||||
v-for="stat in stats"
|
||||
:key="stat.label"
|
||||
:value="stat.value"
|
||||
:unit="stat.unit"
|
||||
:label="stat.label"
|
||||
:attribution="stat.attribution"
|
||||
dark
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
stats: { type: Array, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.trust-section {
|
||||
background: linear-gradient(135deg, @color-primary-dark 0%, @color-primary 100%);
|
||||
padding: @space-xl 0;
|
||||
|
||||
.respond-md({
|
||||
padding: @space-2xl 0;
|
||||
});
|
||||
}
|
||||
|
||||
.trust-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: @space-md;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: @space-xl;
|
||||
});
|
||||
}
|
||||
</style>
|
||||
在新工单中引用
屏蔽一个用户