@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle :title="story.title" align="left" />
|
||||
<div class="story-layout">
|
||||
<div class="story-content">
|
||||
<template v-if="story.paragraphs">
|
||||
<p v-for="(para, i) in story.paragraphs" :key="i" class="story-para">{{ para }}</p>
|
||||
</template>
|
||||
<p v-else>{{ story.content }}</p>
|
||||
</div>
|
||||
<div class="story-photo">
|
||||
<div class="story-photo-card">
|
||||
<img
|
||||
:src="images.brandStory.photo"
|
||||
alt="一家三口穿蒙古族传统服饰在草原上欢笑"
|
||||
width="600"
|
||||
height="400"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<p class="story-photo-caption">带孩子回到母亲的故乡</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import images from '~/data/images.json'
|
||||
defineProps({ story: { type: Object, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.story-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-xl;
|
||||
align-items: center;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: @space-3xl;
|
||||
});
|
||||
}
|
||||
|
||||
.story-content {
|
||||
p {
|
||||
font-size: @font-size-md;
|
||||
line-height: @line-height-loose;
|
||||
color: @color-text-primary;
|
||||
}
|
||||
.story-para {
|
||||
text-indent: 2em;
|
||||
margin-bottom: @space-md;
|
||||
&:last-child { margin-bottom: 0; }
|
||||
}
|
||||
}
|
||||
|
||||
.story-moment {
|
||||
margin-top: @space-lg;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.story-photo-card {
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
box-shadow: @shadow-lg;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
aspect-ratio: 4 / 3;
|
||||
object-fit: cover;
|
||||
object-position: center 35%;
|
||||
}
|
||||
}
|
||||
|
||||
.story-photo-caption {
|
||||
text-align: center;
|
||||
margin-top: @space-md;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="知识产权" subtitle="商标全品类注册 + 4项国家版权登记" />
|
||||
<p class="copyright-intro">呼籁集团重视品牌知识产权保护。「呼籁」商标已完成全品类注册,核心产品名称、品牌标识和IP形象均已在中国版权保护中心完成作品登记。</p>
|
||||
|
||||
<!-- 商标 -->
|
||||
<div v-if="trademark" class="trademark-card">
|
||||
<div class="trademark-header">
|
||||
<span class="trademark-badge">® 商标</span>
|
||||
<h3 class="trademark-name">「{{ trademark.name }}」{{ trademark.scope }}</h3>
|
||||
</div>
|
||||
<p class="trademark-desc">{{ trademark.description }}</p>
|
||||
<p class="trademark-holder">持有人:{{ trademark.holder }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 版权 -->
|
||||
<div class="copyright-grid">
|
||||
<div v-for="c in copyrights" :key="c.regNo" class="copyright-card">
|
||||
<div class="copyright-header">
|
||||
<h3 class="copyright-name">{{ c.name }}</h3>
|
||||
<span class="copyright-category">{{ c.category }}</span>
|
||||
</div>
|
||||
<p class="copyright-desc">{{ c.description }}</p>
|
||||
<div class="copyright-meta">
|
||||
<div class="copyright-meta-item">
|
||||
<span class="meta-label">登记号</span>
|
||||
<span class="meta-value">{{ c.regNo }}</span>
|
||||
</div>
|
||||
<div class="copyright-meta-item">
|
||||
<span class="meta-label">登记日期</span>
|
||||
<span class="meta-value">{{ c.date }}</span>
|
||||
</div>
|
||||
<div class="copyright-meta-item">
|
||||
<span class="meta-label">著作权人</span>
|
||||
<span class="meta-value">{{ c.holder }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="copyright-note">以上作品均经中国版权保护中心审核,依据《作品自愿登记试行办法》予以登记。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
copyrights: { type: Array, required: true },
|
||||
trademark: { type: Object, default: null }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.copyright-intro {
|
||||
text-align: center;
|
||||
max-width: 600px;
|
||||
margin: 0 auto @space-xl;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
.trademark-card {
|
||||
max-width: 900px;
|
||||
margin: 0 auto @space-xl;
|
||||
padding: @space-xl;
|
||||
background: @color-primary-bg;
|
||||
border: 1px solid fade(@color-primary, 20%);
|
||||
border-radius: @border-radius-md;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.trademark-header {
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.trademark-badge {
|
||||
display: inline-block;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-bg-white;
|
||||
background: @color-primary;
|
||||
padding: 2px 10px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.trademark-name {
|
||||
font-size: @font-size-md;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin: @space-sm 0 0;
|
||||
}
|
||||
|
||||
.trademark-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-loose;
|
||||
max-width: 700px;
|
||||
margin: @space-sm auto @space-sm;
|
||||
}
|
||||
|
||||
.trademark-holder {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.copyright-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-md;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: 1fr 1fr;
|
||||
});
|
||||
}
|
||||
|
||||
.copyright-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-lg;
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.copyright-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.copyright-name {
|
||||
font-size: @font-size-md;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.copyright-category {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-primary;
|
||||
background: @color-primary-bg;
|
||||
padding: 2px 8px;
|
||||
border-radius: @border-radius-sm;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.copyright-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.copyright-meta {
|
||||
border-top: 1px solid @color-bg-gray;
|
||||
padding-top: @space-sm;
|
||||
}
|
||||
|
||||
.copyright-meta-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
padding: 3px 0;
|
||||
font-size: @font-size-xs;
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: @color-text-muted;
|
||||
flex-shrink: 0;
|
||||
margin-right: @space-sm;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: @color-text-secondary;
|
||||
text-align: right;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.copyright-note {
|
||||
text-align: center;
|
||||
margin-top: @space-xl;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="企业文化" />
|
||||
<p v-if="culture.core" class="culture-core">{{ culture.core }}</p>
|
||||
<div class="values-grid">
|
||||
<div v-for="v in culture.values" :key="v.name" class="value-item">
|
||||
<h3 class="value-name">{{ v.name }}</h3>
|
||||
<p class="value-expr">{{ v.expression }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ culture: { type: Object, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.culture-core {
|
||||
text-align: center; font-size: @font-size-xl; font-weight: 600; color: @color-primary;
|
||||
letter-spacing: 0.1em; margin-bottom: @space-2xl;
|
||||
}
|
||||
.values-grid {
|
||||
display: grid; grid-template-columns: 1fr; gap: @space-lg;
|
||||
.respond-md({ grid-template-columns: repeat(3, 1fr); });
|
||||
}
|
||||
.value-item { text-align: center; padding: @space-xl @space-lg; }
|
||||
.value-name { font-size: @font-size-xl; color: @color-primary; margin-bottom: @space-sm; }
|
||||
.value-expr { font-size: @font-size-base; color: @color-text-secondary; }
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<section class="section section--warm">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="品牌IP · 籁小咩" subtitle="LAI XIAO MIE" />
|
||||
|
||||
<div class="mascot-hero">
|
||||
<div class="mascot-image">
|
||||
<img
|
||||
:src="images.mascot.front"
|
||||
alt="籁小咩 - 呼籁旅行品牌IP吉祥物,一只可爱的小羊形象"
|
||||
width="280"
|
||||
height="380"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="mascot-info">
|
||||
<h3 class="mascot-name">籁小咩</h3>
|
||||
<p class="mascot-intro">呼籁旅行的品牌IP吉祥物,灵感来自呼伦贝尔草原上自由奔跑的小羊。圆润的羊毛卷发、温暖的大眼睛、招手的小手势——籁小咩代表着呼籁对每一个家庭的热情欢迎。</p>
|
||||
<div class="mascot-tags">
|
||||
<span class="mascot-tag">原创IP形象</span>
|
||||
<span class="mascot-tag">国家版权登记</span>
|
||||
<span class="mascot-tag">表情包</span>
|
||||
</div>
|
||||
<p class="mascot-copyright">国家版权局作品登记:国作登字-2026-F-00020268</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 草原场景 -->
|
||||
<div class="mascot-banner">
|
||||
<img
|
||||
:src="images.mascot.banner"
|
||||
alt="籁小咩在呼伦贝尔草原上向你招手"
|
||||
width="800"
|
||||
height="300"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 表情包二维码 -->
|
||||
<div class="mascot-sticker">
|
||||
<div class="mascot-sticker-qr">
|
||||
<img
|
||||
:src="images.mascot.stickerQr"
|
||||
alt="籁小咩微信表情包二维码"
|
||||
width="160"
|
||||
height="160"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="mascot-sticker-text">
|
||||
<p class="mascot-sticker-title">把籁小咩带进你的聊天</p>
|
||||
<p class="mascot-sticker-desc">微信扫一扫,免费领取「籁小咩」表情包。旅行前发给家人朋友,快乐先行一步。</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import images from '~/data/images.json'
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mascot-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: @space-xl;
|
||||
max-width: 800px;
|
||||
margin: 0 auto @space-2xl;
|
||||
|
||||
.respond-md({
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
gap: @space-2xl;
|
||||
});
|
||||
}
|
||||
|
||||
.mascot-image {
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 200px;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
}
|
||||
|
||||
.mascot-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mascot-name {
|
||||
font-size: 28px;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin: 0 0 @space-md;
|
||||
}
|
||||
|
||||
.mascot-intro {
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-loose;
|
||||
margin-bottom: @space-lg;
|
||||
}
|
||||
|
||||
.mascot-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-md;
|
||||
justify-content: center;
|
||||
|
||||
.respond-md({
|
||||
justify-content: flex-start;
|
||||
});
|
||||
}
|
||||
|
||||
.mascot-tag {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-primary;
|
||||
background: @color-primary-bg;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.mascot-copyright {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mascot-banner {
|
||||
max-width: 800px;
|
||||
margin: 0 auto @space-2xl;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.mascot-sticker {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: @space-lg;
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
padding: @space-xl;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-lg;
|
||||
border: 1px solid @color-border;
|
||||
|
||||
.respond-md({
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
});
|
||||
}
|
||||
|
||||
.mascot-sticker-qr {
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: @border-radius-md;
|
||||
}
|
||||
}
|
||||
|
||||
.mascot-sticker-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mascot-sticker-title {
|
||||
font-size: @font-size-md;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.mascot-sticker-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<!-- 资质认证 -->
|
||||
<CommonSectionTitle title="资质认证" />
|
||||
<div class="qual-list">
|
||||
<div v-for="c in certifications" :key="c.title" class="qual-item qual-item--cert">
|
||||
<div class="qual-icon">🏆</div>
|
||||
<div class="qual-body">
|
||||
<h3 class="qual-title">{{ c.title }}</h3>
|
||||
<p class="qual-detail">{{ c.detail }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 小红书WILL大会获奖照片 -->
|
||||
<div class="award-gallery">
|
||||
<div class="award-gallery-grid">
|
||||
<img src="/images/awards/will-award-person.jpg" alt="呼籁旅行创始人在2026小红书WILL商业大会领取年度成长型品牌奖" class="award-img award-img--main" loading="lazy" />
|
||||
<img src="/images/awards/will-award-trophy.jpg" alt="2026小红书WILL商业大会年度成长型品牌奖杯" class="award-img" loading="lazy" />
|
||||
<img src="/images/awards/will-award-stage.jpg" alt="2026小红书WILL商业大会年度成长型品牌颁奖典礼" class="award-img" loading="lazy" />
|
||||
</div>
|
||||
<p class="award-caption">2026小红书WILL商业大会 · 年度成长型品牌 · 文旅行业仅6家获此殊荣</p>
|
||||
</div>
|
||||
|
||||
<!-- 服务保障 -->
|
||||
<CommonSectionTitle title="服务保障" style="margin-top: 48px;" />
|
||||
<div class="qual-list qual-list--grid">
|
||||
<div v-for="g in guarantees" :key="g.title" class="qual-item qual-item--guarantee">
|
||||
<div class="qual-icon">✅</div>
|
||||
<div class="qual-body">
|
||||
<h3 class="qual-title">{{ g.title }}</h3>
|
||||
<p class="qual-detail">{{ g.detail }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
certifications: { type: Array, required: true },
|
||||
guarantees: { type: Array, required: true }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.qual-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.qual-list--grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: @space-md;
|
||||
.respond-md({
|
||||
grid-template-columns: 1fr;
|
||||
});
|
||||
}
|
||||
.qual-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: @space-sm;
|
||||
padding: @space-lg;
|
||||
border-radius: @border-radius-md;
|
||||
transition: transform 0.2s ease;
|
||||
&:hover { transform: translateY(-2px); }
|
||||
}
|
||||
.qual-item--cert {
|
||||
background: @color-primary-bg;
|
||||
border-left: 3px solid @color-primary;
|
||||
}
|
||||
.qual-item--guarantee {
|
||||
background: #f8faf5;
|
||||
border-left: 3px solid @color-primary;
|
||||
}
|
||||
.qual-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.qual-body { flex: 1; }
|
||||
.qual-title {
|
||||
font-size: @font-size-md;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-xs;
|
||||
font-weight: @font-weight-bold;
|
||||
}
|
||||
.qual-detail {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
.award-gallery {
|
||||
max-width: 800px;
|
||||
margin: @space-xl auto 0;
|
||||
}
|
||||
|
||||
.award-gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: @space-sm;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.award-img {
|
||||
width: 100%;
|
||||
border-radius: @border-radius-md;
|
||||
object-fit: cover;
|
||||
aspect-ratio: 3 / 4;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
}
|
||||
|
||||
.award-caption {
|
||||
text-align: center;
|
||||
margin-top: @space-md;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<section class="section section--gray">
|
||||
<div class="container">
|
||||
<CommonSectionTitle :title="`${subsidiaries.length}家集团企业`" subtitle="各司其职,共同为您的旅行保驾护航" />
|
||||
|
||||
<!-- 集团母公司 -->
|
||||
<div v-if="parent" class="sub-parent">
|
||||
<span class="sub-parent-tag">集团母公司</span>
|
||||
<h3 class="sub-parent-name">{{ parent.name }}</h3>
|
||||
<p class="sub-parent-role">{{ parent.role }}</p>
|
||||
<p v-if="parent.established" class="sub-parent-date">成立于{{ formatDate(parent.established) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 子公司网格 -->
|
||||
<div class="sub-grid">
|
||||
<div v-for="sub in children" :key="sub.name" class="sub-item">
|
||||
<h3 class="sub-name">{{ sub.name }}</h3>
|
||||
<p class="sub-role">{{ sub.role }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({ subsidiaries: { type: Array, required: true } })
|
||||
|
||||
const parent = props.subsidiaries.find(s => s.role.includes('集团母公司') || s.role.includes('母公司'))
|
||||
const children = props.subsidiaries.filter(s => s !== parent)
|
||||
|
||||
function formatDate(dateStr) {
|
||||
const [year, month] = dateStr.split('-')
|
||||
return `${year}年${parseInt(month)}月`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.sub-parent {
|
||||
max-width: 900px;
|
||||
margin: 0 auto @space-xl;
|
||||
padding: @space-xl @space-2xl;
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-lg;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.06);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sub-parent-tag {
|
||||
display: inline-block;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-bg-white;
|
||||
background: @color-primary;
|
||||
padding: 2px 12px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.sub-parent-name {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.sub-parent-role {
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.sub-parent-date {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sub-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-md;
|
||||
|
||||
.respond-md({ grid-template-columns: repeat(2, 1fr); });
|
||||
.respond-lg({ grid-template-columns: repeat(2, 1fr); max-width: 900px; margin-left: auto; margin-right: auto; });
|
||||
}
|
||||
|
||||
.sub-item {
|
||||
padding: @space-lg;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-md;
|
||||
border: 1px solid @color-border;
|
||||
}
|
||||
|
||||
.sub-name {
|
||||
font-size: @font-size-base;
|
||||
color: @color-primary;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.sub-role {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<section class="section section--warm">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="我们的团队" />
|
||||
<p class="team-summary">{{ team.summary }}</p>
|
||||
|
||||
<!-- 团队照片墙 -->
|
||||
<div class="team-gallery">
|
||||
<!-- 上排:两年踩线对比,体现年年坚持 -->
|
||||
<div class="team-gallery-row team-gallery-row--survey">
|
||||
<div class="team-photo-card">
|
||||
<div class="team-photo-wrap team-photo-wrap--survey">
|
||||
<img
|
||||
:src="images.team.guideGroup"
|
||||
alt="2024呼籁文旅车队踩线全体领队合影"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="team-photo-caption">
|
||||
<span class="caption-tag">2024踩线</span>
|
||||
<span class="caption-text">「额吉的故乡」全体车队师傅集体踩线</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-photo-card">
|
||||
<div class="team-photo-wrap team-photo-wrap--survey">
|
||||
<img
|
||||
:src="images.team.routeSurvey"
|
||||
alt="2025秋呼籁文旅游牧的森林车队踩线全体领队合影"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="team-photo-caption">
|
||||
<span class="caption-tag">2025踩线</span>
|
||||
<span class="caption-text">「游牧的森林」新品路线全员实地考察</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 下排:培训 + 牧场 -->
|
||||
<div class="team-gallery-row team-gallery-row--misc">
|
||||
<div class="team-photo-card">
|
||||
<div class="team-photo-wrap team-photo-wrap--misc">
|
||||
<img
|
||||
:src="images.team.guideTraining"
|
||||
alt="呼籁文旅2026年司导线下培训"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="team-photo-caption">
|
||||
<span class="caption-tag">专业培训</span>
|
||||
<span class="caption-text">每年冬季集中培训,统一服务标准</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="team-photo-card">
|
||||
<div class="team-photo-wrap team-photo-wrap--misc">
|
||||
<img
|
||||
:src="images.team.ranchStore"
|
||||
alt="呼籁牧场线下店开业团队合影"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="team-photo-caption">
|
||||
<span class="caption-tag">实体布局</span>
|
||||
<span class="caption-text">呼籁牧场线下体验店正式落成</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import images from '~/data/images.json'
|
||||
defineProps({ team: { type: Object, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.team-summary {
|
||||
max-width: 800px;
|
||||
margin: 0 auto @space-2xl;
|
||||
text-align: center;
|
||||
font-size: @font-size-md;
|
||||
line-height: @line-height-base;
|
||||
color: @color-text-secondary;
|
||||
}
|
||||
|
||||
.team-gallery {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-lg;
|
||||
|
||||
.respond-md({
|
||||
gap: @space-xl;
|
||||
});
|
||||
}
|
||||
|
||||
.team-gallery-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-lg;
|
||||
|
||||
.respond-md({
|
||||
gap: @space-xl;
|
||||
});
|
||||
}
|
||||
|
||||
.team-gallery-row--survey {
|
||||
.respond-md({
|
||||
grid-template-columns: 1fr 1fr;
|
||||
});
|
||||
}
|
||||
|
||||
.team-gallery-row--misc {
|
||||
.respond-md({
|
||||
grid-template-columns: 1fr 1fr;
|
||||
});
|
||||
}
|
||||
|
||||
.team-photo-card {
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
background: @color-bg-white;
|
||||
box-shadow: @shadow-md;
|
||||
transition: transform @transition-base, box-shadow @transition-base;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: @shadow-lg;
|
||||
}
|
||||
}
|
||||
|
||||
.team-photo-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.team-photo-wrap--survey {
|
||||
img {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
}
|
||||
|
||||
.team-photo-wrap--misc {
|
||||
img {
|
||||
aspect-ratio: 16 / 10;
|
||||
}
|
||||
}
|
||||
|
||||
.team-photo-caption {
|
||||
padding: @space-md @space-lg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-md;
|
||||
}
|
||||
|
||||
.caption-tag {
|
||||
flex-shrink: 0;
|
||||
display: inline-block;
|
||||
padding: 3px 10px;
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
border-radius: @border-radius-sm;
|
||||
}
|
||||
|
||||
.caption-text {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="小红书上的呼籁" subtitle="XIAOHONGSHU" />
|
||||
|
||||
<div class="xhs-card">
|
||||
<!-- 顶部:品牌主页截图作为横幅 -->
|
||||
<div class="xhs-banner">
|
||||
<img
|
||||
:src="images.xiaohongshu.storefront"
|
||||
alt="呼籁旅行小红书品牌主页 - Mr.小雷-呼籁旅行 50万家庭青睐的草原旅行品牌"
|
||||
width="800"
|
||||
height="300"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 底部:数据 + 信息 -->
|
||||
<div class="xhs-body">
|
||||
<!-- 账号信息 -->
|
||||
<div class="xhs-header">
|
||||
<div class="xhs-account">
|
||||
<span class="xhs-account-name">{{ xhs.account }}</span>
|
||||
<span class="xhs-badge">{{ xhs.verifiedType }}</span>
|
||||
</div>
|
||||
<p class="xhs-tagline">{{ xhs.tagline }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据指标 -->
|
||||
<div class="xhs-stats">
|
||||
<div class="xhs-stat">
|
||||
<span class="xhs-stat-value">{{ xhs.followers }}</span>
|
||||
<span class="xhs-stat-label">粉丝</span>
|
||||
</div>
|
||||
<div class="xhs-stat-divider" />
|
||||
<div class="xhs-stat">
|
||||
<span class="xhs-stat-value">{{ xhs.likes }}</span>
|
||||
<span class="xhs-stat-label">获赞与收藏</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 荣誉 + 描述 -->
|
||||
<div class="xhs-awards">
|
||||
<span v-for="award in xhs.awards" :key="award" class="xhs-award">{{ award }}</span>
|
||||
</div>
|
||||
|
||||
<p class="xhs-desc">{{ xhs.description }}</p>
|
||||
|
||||
<div class="xhs-tags">
|
||||
<span v-for="tag in xhs.tags" :key="tag" class="xhs-tag">{{ tag }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import images from '~/data/images.json'
|
||||
defineProps({ xhs: { type: Object, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.xhs-card {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
background: @color-bg-white;
|
||||
box-shadow: @shadow-md;
|
||||
}
|
||||
|
||||
.xhs-banner {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.xhs-body {
|
||||
padding: @space-xl;
|
||||
|
||||
.respond-md({
|
||||
padding: @space-xl @space-2xl;
|
||||
});
|
||||
}
|
||||
|
||||
.xhs-header {
|
||||
margin-bottom: @space-lg;
|
||||
}
|
||||
|
||||
.xhs-account {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-xs;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.xhs-account-name {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
}
|
||||
|
||||
.xhs-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 10px;
|
||||
background: #ff2442;
|
||||
color: #fff;
|
||||
font-size: @font-size-xs;
|
||||
border-radius: 20px;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.xhs-tagline {
|
||||
font-size: @font-size-base;
|
||||
color: @color-primary;
|
||||
font-weight: @font-weight-medium;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.xhs-stats {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-lg;
|
||||
padding: @space-lg 0;
|
||||
margin-bottom: @space-lg;
|
||||
border-top: 1px solid @color-border;
|
||||
border-bottom: 1px solid @color-border;
|
||||
}
|
||||
|
||||
.xhs-stat {
|
||||
text-align: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.xhs-stat-divider {
|
||||
width: 1px;
|
||||
height: 32px;
|
||||
background: @color-border;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.xhs-stat-value {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
line-height: 1.2;
|
||||
|
||||
.respond-md({
|
||||
font-size: 28px;
|
||||
});
|
||||
}
|
||||
|
||||
.xhs-stat-label {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xhs-awards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.xhs-award {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 5px 12px;
|
||||
background: linear-gradient(135deg, #FFF8E1, #FFF3CD);
|
||||
color: #B8860B;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
border-radius: @border-radius-sm;
|
||||
border: 1px solid #F0D58C;
|
||||
|
||||
&::before {
|
||||
content: "🏆";
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.xhs-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-loose;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.xhs-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
.xhs-tag {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-secondary;
|
||||
background: @color-bg-gray;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<article class="autumn-card" :class="{ 'autumn-card--popular': isPopular }">
|
||||
<!-- 头部 -->
|
||||
<div class="card-header">
|
||||
<div v-if="isPopular" class="popular-badge">推荐</div>
|
||||
<div class="card-duration">
|
||||
<span class="duration-num">{{ product.days }}</span>
|
||||
<span class="duration-unit">天</span>
|
||||
<span class="duration-num">{{ product.nights }}</span>
|
||||
<span class="duration-unit">晚</span>
|
||||
</div>
|
||||
<span class="card-tag">{{ product.tag }}</span>
|
||||
<span class="card-line">{{ product.line }} · {{ product.route }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 主体 -->
|
||||
<div class="card-body">
|
||||
<h3 class="card-name">{{ shortName }}</h3>
|
||||
<p class="card-audience">适合:{{ product.audience }}</p>
|
||||
<p class="card-desc">{{ product.description }}</p>
|
||||
<ul class="card-highlights">
|
||||
<li v-for="(h, i) in product.highlights" :key="i">
|
||||
<span class="check" aria-hidden="true">✓</span>
|
||||
{{ h }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 行程概览 -->
|
||||
<details v-if="product.itinerary && product.itinerary.length" class="card-itinerary">
|
||||
<summary class="itinerary-toggle">查看每日行程 ▾</summary>
|
||||
<ol class="itinerary-list">
|
||||
<li v-for="(day, i) in product.itinerary" :key="i">{{ day }}</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<!-- 底部 CTA -->
|
||||
<NuxtLink to="/contact" class="card-cta">
|
||||
咨询该行程 <span aria-hidden="true">→</span>
|
||||
</NuxtLink>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({ product: { type: Object, required: true } })
|
||||
|
||||
const isPopular = computed(() => props.product.id === 'autumn-south-7d6n')
|
||||
|
||||
const shortName = computed(() => {
|
||||
return props.product.name.replace(/^游牧的森林V3·/, '')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.autumn-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: box-shadow @transition-base, transform @transition-base;
|
||||
|
||||
&:hover {
|
||||
box-shadow: @shadow-lg;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
&--popular {
|
||||
border: 2px solid #D97706;
|
||||
box-shadow: @shadow-md;
|
||||
|
||||
.card-header {
|
||||
background: #D97706;
|
||||
}
|
||||
|
||||
.duration-num,
|
||||
.duration-unit,
|
||||
.card-tag,
|
||||
.card-line {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: @space-lg @space-lg @space-md;
|
||||
background: #FEF3C7;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popular-badge {
|
||||
position: absolute;
|
||||
top: @space-sm;
|
||||
right: @space-sm;
|
||||
padding: @space-xs @space-sm;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #fff;
|
||||
background: @color-accent;
|
||||
border-radius: @border-radius-sm;
|
||||
}
|
||||
|
||||
.card-duration {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.duration-num {
|
||||
font-size: @font-size-3xl;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #D97706;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.duration-unit {
|
||||
font-size: @font-size-sm;
|
||||
color: #D97706;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
display: inline-block;
|
||||
padding: @space-xs @space-md;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-medium;
|
||||
color: #D97706;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-full;
|
||||
}
|
||||
|
||||
.card-line {
|
||||
display: block;
|
||||
font-size: @font-size-xs;
|
||||
color: #64748B;
|
||||
margin-top: @space-xs;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: @space-lg;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.card-audience {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin-bottom: @space-md;
|
||||
.text-clamp(3);
|
||||
}
|
||||
|
||||
.card-highlights {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
margin-top: auto;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: @space-sm;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
}
|
||||
|
||||
.check {
|
||||
flex-shrink: 0;
|
||||
color: #D97706;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-bold;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// 行程概览
|
||||
.card-itinerary {
|
||||
border-top: 1px solid @color-border;
|
||||
}
|
||||
|
||||
.itinerary-toggle {
|
||||
display: block;
|
||||
padding: @space-md @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: background @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-bg-gray;
|
||||
color: @color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.itinerary-list {
|
||||
padding: 0 @space-lg @space-md;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-xs;
|
||||
|
||||
li {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
padding-left: @space-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部 CTA
|
||||
.card-cta {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: @space-md @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
color: #D97706;
|
||||
background: @color-bg-gray;
|
||||
text-decoration: none;
|
||||
transition: all @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: #D97706;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<nav v-if="items.length" class="breadcrumb" aria-label="面包屑导航">
|
||||
<ol class="breadcrumb-list">
|
||||
<li class="breadcrumb-item">
|
||||
<NuxtLink to="/" class="breadcrumb-link">首页</NuxtLink>
|
||||
</li>
|
||||
<li v-for="(item, index) in items" :key="index" class="breadcrumb-item">
|
||||
<span class="breadcrumb-sep" aria-hidden="true">/</span>
|
||||
<NuxtLink v-if="item.to" :to="item.to" class="breadcrumb-link">{{ item.text }}</NuxtLink>
|
||||
<span v-else class="breadcrumb-current" aria-current="page">{{ item.text }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
items: { type: Array, default: () => [] },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.breadcrumb-list {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-xs;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-xs;
|
||||
}
|
||||
|
||||
.breadcrumb-sep {
|
||||
color: @color-text-muted;
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
|
||||
.breadcrumb-link {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb-current {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="content-card" :class="{ 'content-card--hoverable': hoverable }">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
hoverable: { type: Boolean, default: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.content-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-lg;
|
||||
padding: @space-xl;
|
||||
transition: box-shadow 0.3s cubic-bezier(0.23, 1, 0.32, 1), transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
|
||||
&--hoverable:hover {
|
||||
box-shadow: @shadow-md;
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<NuxtLink :to="to" class="internal-link">
|
||||
<span>{{ text }}</span>
|
||||
<span v-if="arrow" class="internal-link-arrow" aria-hidden="true">→</span>
|
||||
</NuxtLink>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
to: { type: String, required: true },
|
||||
text: { type: String, required: true },
|
||||
arrow: { type: Boolean, default: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.internal-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
font-size: @font-size-base;
|
||||
color: @color-primary;
|
||||
text-decoration: none;
|
||||
font-weight: @font-weight-medium;
|
||||
transition: gap @transition-fast;
|
||||
|
||||
&:hover {
|
||||
gap: @space-md;
|
||||
color: @color-primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
.internal-link-arrow {
|
||||
font-size: @font-size-lg;
|
||||
transition: transform @transition-fast;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div class="qr-block" :class="[`qr-block--${size}`]">
|
||||
<img :src="image" :alt="`${label}二维码`" class="qr-image" loading="lazy" />
|
||||
<span class="qr-label">{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
image: { type: String, required: true },
|
||||
label: { type: String, required: true },
|
||||
size: { type: String, default: 'normal' },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.qr-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: @space-xs;
|
||||
}
|
||||
|
||||
.qr-image {
|
||||
border-radius: @border-radius-sm;
|
||||
background: @color-bg-gray;
|
||||
object-fit: contain;
|
||||
|
||||
.qr-block--small & {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
|
||||
.qr-block--normal & {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
.qr-block--large & {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
.qr-label {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
|
||||
.qr-block--normal &,
|
||||
.qr-block--large & {
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<section class="section-cta" :class="`section-cta--${bg}`">
|
||||
<div class="section-cta-bg" />
|
||||
<div class="container text-center section-cta-inner">
|
||||
<h2 class="section-cta-title">{{ title }}</h2>
|
||||
<p class="section-cta-desc">{{ description }}</p>
|
||||
<div class="section-cta-links">
|
||||
<NuxtLink
|
||||
v-for="(link, index) in links"
|
||||
:key="link.to"
|
||||
:to="link.to"
|
||||
class="cta-btn"
|
||||
:class="index === 0 ? 'cta-btn--primary' : 'cta-btn--outline'"
|
||||
>
|
||||
{{ link.text }}
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
description: { type: String, required: true },
|
||||
links: { type: Array, required: true },
|
||||
bg: { type: String, default: 'green' },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.section-cta {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.section-padding();
|
||||
}
|
||||
|
||||
.section-cta--green {
|
||||
background: linear-gradient(135deg, @color-primary-dark 0%, @color-primary 50%, @color-primary-light 100%);
|
||||
}
|
||||
|
||||
.section-cta--warm {
|
||||
background: linear-gradient(135deg, @color-bg-warm 0%, @color-accent-bg 100%);
|
||||
}
|
||||
|
||||
.section-cta-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse at 20% 50%, rgba(255, 255, 255, 0.08) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 80% 50%, rgba(255, 255, 255, 0.05) 0%, transparent 60%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.section-cta-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.section-cta-title {
|
||||
.section-cta--green & {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.section-cta-desc {
|
||||
margin-top: @space-md;
|
||||
margin-bottom: @space-xl;
|
||||
font-size: @font-size-md;
|
||||
line-height: @line-height-relaxed;
|
||||
max-width: 560px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
.section-cta--green & {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.section-cta--warm & {
|
||||
color: @color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.section-cta-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: @space-md;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.cta-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 14px @space-xl;
|
||||
border-radius: @border-radius-md;
|
||||
font-size: @font-size-base;
|
||||
font-weight: @font-weight-medium;
|
||||
text-decoration: none;
|
||||
transition: all @transition-base;
|
||||
|
||||
.respond-md({
|
||||
padding: 16px 36px;
|
||||
font-size: @font-size-md;
|
||||
});
|
||||
|
||||
&--primary {
|
||||
.section-cta--green & {
|
||||
background: #fff;
|
||||
color: @color-primary;
|
||||
border: 1px solid #fff;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: @color-primary-dark;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.section-cta--warm & {
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
border: 1px solid @color-primary;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary-light;
|
||||
border-color: @color-primary-light;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&--outline {
|
||||
.section-cta--green & {
|
||||
border: 1px 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;
|
||||
}
|
||||
}
|
||||
|
||||
.section-cta--warm & {
|
||||
background: transparent;
|
||||
color: @color-primary;
|
||||
border: 1px solid @color-primary;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="section-title" :class="[`section-title--${align}`]">
|
||||
<span v-if="label" class="section-title-label">{{ label }}</span>
|
||||
<component :is="tag" class="section-title-text">{{ title }}</component>
|
||||
<p v-if="subtitle" class="section-title-sub">{{ subtitle }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: { type: String, required: true },
|
||||
subtitle: { type: String, default: '' },
|
||||
label: { type: String, default: '' },
|
||||
tag: { type: String, default: 'h2' },
|
||||
align: { type: String, default: 'center' },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.section-title {
|
||||
margin-bottom: @space-xl;
|
||||
|
||||
.respond-md({
|
||||
margin-bottom: @space-2xl;
|
||||
});
|
||||
|
||||
&--center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&--left {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.section-title-label {
|
||||
display: inline-block;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.section-title-text {
|
||||
color: @color-text-primary;
|
||||
|
||||
// 标题下方品牌色渐变短线装饰
|
||||
&::after {
|
||||
content: '';
|
||||
display: block;
|
||||
width: 48px;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, @color-primary, @color-primary-light);
|
||||
border-radius: 2px;
|
||||
margin-top: @space-md;
|
||||
}
|
||||
|
||||
.section-title--center & {
|
||||
&::after {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.section-title-sub {
|
||||
margin-top: @space-md;
|
||||
font-size: @font-size-md;
|
||||
color: @color-text-muted;
|
||||
line-height: @line-height-relaxed;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="trust-badge" :class="{ 'trust-badge--dark': dark }">
|
||||
<div class="trust-badge-value">
|
||||
<span class="trust-number">{{ value }}</span>
|
||||
<span v-if="unit" class="trust-unit">{{ unit }}</span>
|
||||
</div>
|
||||
<div class="trust-badge-label">{{ label }}</div>
|
||||
<div v-if="attribution" class="trust-badge-note">{{ attribution }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
value: { type: String, required: true },
|
||||
unit: { type: String, default: '' },
|
||||
label: { type: String, required: true },
|
||||
attribution: { type: String, default: '' },
|
||||
dark: { type: Boolean, default: false },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.trust-badge {
|
||||
text-align: center;
|
||||
padding: @space-lg @space-md;
|
||||
}
|
||||
|
||||
.trust-badge-value {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: @space-xs;
|
||||
}
|
||||
|
||||
.trust-number {
|
||||
font-size: @font-size-3xl;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
letter-spacing: -1px;
|
||||
|
||||
.respond-md({
|
||||
font-size: @font-size-display;
|
||||
});
|
||||
|
||||
.trust-badge--dark & {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.trust-unit {
|
||||
font-size: @font-size-base;
|
||||
color: @color-primary-light;
|
||||
|
||||
.respond-md({
|
||||
font-size: @font-size-md;
|
||||
});
|
||||
|
||||
.trust-badge--dark & {
|
||||
color: @color-primary-lighter;
|
||||
}
|
||||
}
|
||||
|
||||
.trust-badge-label {
|
||||
margin-top: @space-sm;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
font-weight: @font-weight-medium;
|
||||
|
||||
.trust-badge--dark & {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.trust-badge-note {
|
||||
margin-top: @space-xs;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
|
||||
.trust-badge--dark & {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<CommonContentCard :class="{ 'channel-primary': channel.primary }">
|
||||
<div class="channel">
|
||||
<span v-if="channel.primary" class="channel-badge">推荐</span>
|
||||
<h3 class="channel-label">{{ channel.label }}</h3>
|
||||
<p class="channel-value">{{ channel.value }}</p>
|
||||
<p v-if="channel.description" class="channel-desc">{{ channel.description }}</p>
|
||||
<CommonQRCodeBlock
|
||||
v-if="channel.qrImage"
|
||||
:image="channel.qrImage"
|
||||
:label="channel.label"
|
||||
size="normal"
|
||||
/>
|
||||
</div>
|
||||
</CommonContentCard>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ channel: { type: Object, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.channel { text-align: center; }
|
||||
.channel-label { font-size: @font-size-md; color: @color-primary; margin-bottom: @space-sm; }
|
||||
.channel-value { font-size: @font-size-base; color: @color-text-primary; font-weight: @font-weight-medium; margin-bottom: @space-xs; }
|
||||
.channel-desc { font-size: @font-size-sm; color: @color-text-muted; margin-bottom: @space-md; }
|
||||
|
||||
.channel-primary {
|
||||
border: 2px solid @color-primary;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.channel-badge {
|
||||
display: inline-block;
|
||||
padding: @space-xs @space-md;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-medium;
|
||||
color: #fff;
|
||||
background: @color-primary;
|
||||
border-radius: @border-radius-full;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div class="contact-map">
|
||||
<p class="map-address">{{ address }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ address: { type: String, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.contact-map { padding: @space-lg; background: @color-bg-gray; border-radius: @border-radius-md; text-align: center; }
|
||||
.map-address { color: @color-text-secondary; }
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<section :id="category.id" class="faq-category">
|
||||
<h2 class="faq-category-title">{{ category.name }}</h2>
|
||||
<div class="faq-category-list">
|
||||
<FaqItem
|
||||
v-for="q in category.questions"
|
||||
:key="q.id"
|
||||
:question="q.question"
|
||||
:answer="q.answer"
|
||||
:related-links="q.relatedLinks"
|
||||
:initial-open="initialOpen"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
category: { type: Object, required: true },
|
||||
initialOpen: { type: Boolean, default: false },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.faq-category {
|
||||
margin-bottom: @space-2xl;
|
||||
scroll-margin-top: 80px;
|
||||
}
|
||||
|
||||
.faq-category-title {
|
||||
font-size: @font-size-xl;
|
||||
color: @color-primary;
|
||||
margin-bottom: @space-lg;
|
||||
padding-bottom: @space-sm;
|
||||
border-bottom: 2px solid @color-primary-lighter;
|
||||
}
|
||||
|
||||
.faq-category-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<nav class="faq-nav" aria-label="问题分类">
|
||||
<ul class="faq-nav-list">
|
||||
<li v-for="cat in categories" :key="cat.id">
|
||||
<a
|
||||
:href="`#${cat.id}`"
|
||||
class="faq-nav-item"
|
||||
:class="{ active: activeCat === cat.id }"
|
||||
@click.prevent="$emit('select', cat.id)"
|
||||
>
|
||||
{{ cat.name }}
|
||||
<span class="faq-nav-count">{{ cat.questions.length }}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
categories: { type: Array, required: true },
|
||||
activeCat: { type: String, default: '' },
|
||||
})
|
||||
|
||||
defineEmits(['select'])
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.faq-nav {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
// 移动端右侧渐变提示可横滑
|
||||
mask-image: linear-gradient(to right, #000 85%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to right, #000 85%, transparent 100%);
|
||||
|
||||
.respond-md({
|
||||
mask-image: none;
|
||||
-webkit-mask-image: none;
|
||||
overflow-x: visible;
|
||||
});
|
||||
}
|
||||
|
||||
.faq-nav-list {
|
||||
display: flex;
|
||||
gap: @space-sm;
|
||||
min-width: max-content;
|
||||
|
||||
.respond-md({
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
});
|
||||
}
|
||||
|
||||
.faq-nav-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: @space-xs;
|
||||
padding: @space-sm @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
background: @color-bg-gray;
|
||||
border-radius: @border-radius-full;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: all @transition-fast;
|
||||
min-height: 44px;
|
||||
touch-action: manipulation;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.faq-nav-count {
|
||||
font-size: @font-size-xs;
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<!--
|
||||
SEO关键设计:
|
||||
1. 用<details>/<summary>确保内容在HTML中始终存在(百度蜘蛛可抓取)
|
||||
2. 问题用<h3>标签,语义化
|
||||
3. 答案用<p>纯文本,AI直接引用
|
||||
-->
|
||||
<details class="faq-item" :open="initialOpen || undefined">
|
||||
<summary class="faq-item-question">
|
||||
<h3 class="faq-item-q-text">{{ question }}</h3>
|
||||
<span class="faq-item-icon" aria-hidden="true"></span>
|
||||
</summary>
|
||||
<div class="faq-item-answer">
|
||||
<p class="faq-item-a-text">{{ answer }}</p>
|
||||
<div v-if="relatedLinks && relatedLinks.length" class="faq-item-links">
|
||||
<NuxtLink
|
||||
v-for="link in relatedLinks"
|
||||
:key="link.url"
|
||||
:to="link.url"
|
||||
class="faq-item-link"
|
||||
>
|
||||
{{ link.text }} →
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
question: { type: String, required: true },
|
||||
answer: { type: String, required: true },
|
||||
relatedLinks: { type: Array, default: () => [] },
|
||||
initialOpen: { type: Boolean, default: false },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.faq-item {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
overflow: hidden;
|
||||
transition: border-color @transition-fast;
|
||||
|
||||
&[open] {
|
||||
border-color: @color-primary-lighter;
|
||||
|
||||
.faq-item-icon {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: @color-primary-lighter;
|
||||
}
|
||||
}
|
||||
|
||||
.faq-item-question {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: @space-lg;
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
touch-action: manipulation;
|
||||
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&::marker {
|
||||
display: none;
|
||||
content: '';
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: @color-bg-gray;
|
||||
}
|
||||
}
|
||||
|
||||
.faq-item-q-text {
|
||||
font-size: @font-size-md;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-text-primary;
|
||||
flex: 1;
|
||||
padding-right: @space-md;
|
||||
}
|
||||
|
||||
.faq-item-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
transition: transform @transition-base;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: @color-text-muted;
|
||||
transition: opacity @transition-fast;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 50%;
|
||||
left: 2px;
|
||||
right: 2px;
|
||||
height: 2px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 50%;
|
||||
top: 2px;
|
||||
bottom: 2px;
|
||||
width: 2px;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
details[open] & {
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 答案展开动画
|
||||
.faq-item-answer {
|
||||
padding: 0 @space-lg @space-lg;
|
||||
animation: fadeSlideIn @transition-base;
|
||||
}
|
||||
|
||||
@keyframes fadeSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.faq-item-a-text {
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
.faq-item-links {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-md;
|
||||
margin-top: @space-md;
|
||||
padding-top: @space-md;
|
||||
border-top: 1px solid @color-border;
|
||||
}
|
||||
|
||||
.faq-item-link {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-primary;
|
||||
text-decoration: none;
|
||||
font-weight: @font-weight-medium;
|
||||
|
||||
&:hover {
|
||||
color: @color-primary-light;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="activity-card">
|
||||
<h4 class="activity-name">
|
||||
<span class="activity-icon">{{ activity.icon }}</span>
|
||||
{{ activity.name }}
|
||||
</h4>
|
||||
<ul class="activity-points">
|
||||
<li v-for="(point, i) in activity.points" :key="i" class="activity-point">
|
||||
{{ point }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
activity: { type: Object, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.activity-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-lg;
|
||||
}
|
||||
|
||||
.activity-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
font-size: @font-size-md;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
margin: 0 0 @space-md;
|
||||
}
|
||||
|
||||
.activity-icon {
|
||||
font-size: @font-size-lg;
|
||||
}
|
||||
|
||||
.activity-points {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.activity-point {
|
||||
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: @font-weight-bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="guide-checklist">
|
||||
<div
|
||||
v-for="(item, i) in items"
|
||||
:key="i"
|
||||
class="checklist-item"
|
||||
:class="{ 'checklist-item--important': item.important }"
|
||||
>
|
||||
<span class="checklist-mark">{{ item.important ? '☑' : '☐' }}</span>
|
||||
<span class="checklist-text">{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
items: { type: Array, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.guide-checklist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
display: flex;
|
||||
gap: @space-sm;
|
||||
padding: @space-sm @space-md;
|
||||
border-left: 2px solid @color-border;
|
||||
line-height: @line-height-base;
|
||||
|
||||
&--important {
|
||||
border-left-color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.checklist-mark {
|
||||
flex-shrink: 0;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
.checklist-text {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
|
||||
.checklist-item--important & {
|
||||
color: @color-text-primary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="clothing-card" :class="{ 'clothing-card--highlight': month.highlight }">
|
||||
<div class="clothing-header">
|
||||
<h4 class="clothing-month">{{ month.name }}</h4>
|
||||
<span v-if="month.highlight" class="clothing-tag">大多数家庭选这个月</span>
|
||||
</div>
|
||||
<p class="clothing-desc">{{ month.desc }}</p>
|
||||
<div class="clothing-temps">
|
||||
<span class="temp-badge"><strong>白天</strong> {{ month.tempDay }}</span>
|
||||
<span class="temp-badge"><strong>夜间</strong> {{ month.tempNight }}</span>
|
||||
</div>
|
||||
<div class="clothing-wear">
|
||||
<p class="wear-line"><span class="wear-label">大人</span>{{ month.adultWear }}</p>
|
||||
<p class="wear-line"><span class="wear-label">小朋友</span>{{ month.kidsWear }}</p>
|
||||
</div>
|
||||
<p v-if="month.note" class="clothing-note">{{ month.note }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
month: { type: Object, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.clothing-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-lg;
|
||||
|
||||
&--highlight {
|
||||
border-color: @color-primary-lighter;
|
||||
box-shadow: 0 2px 12px rgba(45, 106, 79, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.clothing-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.clothing-month {
|
||||
font-size: @font-size-base;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.clothing-tag {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-accent;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.clothing-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.clothing-temps {
|
||||
display: flex;
|
||||
gap: @space-lg;
|
||||
margin-bottom: @space-md;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
|
||||
strong {
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.clothing-wear {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-xs;
|
||||
}
|
||||
|
||||
.wear-line {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wear-label {
|
||||
display: inline-block;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-primary;
|
||||
margin-right: @space-sm;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.clothing-note {
|
||||
margin-top: @space-md;
|
||||
padding: @space-sm @space-md;
|
||||
background: @color-bg-warm;
|
||||
border-radius: @border-radius-sm;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-accent;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,66 @@
|
||||
<template>
|
||||
<nav class="guide-nav">
|
||||
<button
|
||||
v-for="section in sections"
|
||||
:key="section.id"
|
||||
class="guide-nav-btn"
|
||||
:class="{ active: active === section.id }"
|
||||
@click="$emit('select', section.id)"
|
||||
>
|
||||
<span class="guide-nav-icon">{{ section.icon }}</span>
|
||||
<span class="guide-nav-text">{{ section.title }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
sections: { type: Array, required: true },
|
||||
active: { type: String, default: '' },
|
||||
})
|
||||
defineEmits(['select'])
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.guide-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-2xl;
|
||||
|
||||
.respond-md({
|
||||
justify-content: center;
|
||||
gap: @space-md;
|
||||
});
|
||||
}
|
||||
|
||||
.guide-nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: @space-sm @space-md;
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-full;
|
||||
cursor: pointer;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
transition: all @transition-fast;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
border-color: @color-primary-light;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: @color-primary;
|
||||
border-color: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.guide-nav-icon {
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div class="toddler-section">
|
||||
<!-- 推荐项目 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--green">{{ data.recommended.title }}</h3>
|
||||
<p class="group-desc">{{ data.recommended.desc }}</p>
|
||||
<div class="toddler-items">
|
||||
<div v-for="item in data.recommended.items" :key="item.name" class="toddler-item toddler-item--green">
|
||||
<span class="toddler-item-icon">{{ item.icon }}</span>
|
||||
<div>
|
||||
<strong class="toddler-item-name">{{ item.name }}</strong>
|
||||
<p class="toddler-item-desc">{{ item.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 视情况选择 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--amber">{{ data.conditional.title }}</h3>
|
||||
<p class="group-desc">{{ data.conditional.desc }}</p>
|
||||
<div class="toddler-items">
|
||||
<div v-for="item in data.conditional.items" :key="item.name" class="toddler-item toddler-item--amber">
|
||||
<span class="toddler-item-icon">{{ item.icon }}</span>
|
||||
<div>
|
||||
<strong class="toddler-item-name">{{ item.name }}</strong>
|
||||
<p class="toddler-item-desc">{{ item.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 不建议参与 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--muted">{{ data.notRecommended.title }}</h3>
|
||||
<p class="group-desc">{{ data.notRecommended.desc }}</p>
|
||||
<div class="toddler-not-items">
|
||||
<div v-for="item in data.notRecommended.items" :key="item.name" class="toddler-not-item">
|
||||
<strong>{{ item.name }}</strong> — {{ item.reason }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 装备 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--green">宝宝出行装备</h3>
|
||||
<div class="toddler-equip">
|
||||
<div v-for="eq in data.equipment" :key="eq.name" class="equip-item" :class="{ 'equip-item--provided': eq.provided }">
|
||||
<strong class="equip-name">{{ eq.name }}</strong>
|
||||
<span v-if="eq.provided" class="equip-badge">呼籁提供</span>
|
||||
<p class="equip-desc">{{ eq.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 用餐 -->
|
||||
<div class="toddler-group">
|
||||
<h3 class="group-title group-title--green">宝宝用餐小贴士</h3>
|
||||
<ul class="toddler-dining">
|
||||
<li v-for="(tip, i) in data.dining" :key="i">{{ tip }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 温馨提醒 -->
|
||||
<div class="toddler-notes">
|
||||
<div v-for="note in data.notes" :key="note.title" class="toddler-note">
|
||||
<strong>{{ note.title }}</strong>
|
||||
<p>{{ note.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
data: { type: Object, required: true },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.toddler-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-2xl;
|
||||
}
|
||||
|
||||
.toddler-group {
|
||||
// group container
|
||||
}
|
||||
|
||||
.group-title {
|
||||
font-size: @font-size-lg;
|
||||
margin: 0 0 @space-sm;
|
||||
|
||||
&--green { color: @color-primary; }
|
||||
&--amber { color: @color-accent; }
|
||||
&--muted { color: @color-text-muted; }
|
||||
}
|
||||
|
||||
.group-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
margin-bottom: @space-lg;
|
||||
}
|
||||
|
||||
.toddler-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
}
|
||||
|
||||
.toddler-item {
|
||||
display: flex;
|
||||
gap: @space-md;
|
||||
padding: @space-md @space-lg;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-md;
|
||||
border-left: 3px solid @color-border;
|
||||
|
||||
&--green { border-left-color: @color-primary; }
|
||||
&--amber { border-left-color: @color-accent; }
|
||||
}
|
||||
|
||||
.toddler-item-icon {
|
||||
font-size: @font-size-lg;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toddler-item-name {
|
||||
color: @color-text-primary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
.toddler-item-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
margin: @space-xs 0 0;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
.toddler-not-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
.toddler-not-item {
|
||||
padding: @space-sm @space-md;
|
||||
border-left: 2px solid @color-border;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
line-height: @line-height-base;
|
||||
|
||||
strong {
|
||||
color: @color-text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
.toddler-equip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
}
|
||||
|
||||
.equip-item {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-md @space-lg;
|
||||
|
||||
&--provided {
|
||||
border-top: 3px solid @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.equip-name {
|
||||
font-size: @font-size-base;
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
.equip-badge {
|
||||
display: inline-block;
|
||||
margin-left: @space-sm;
|
||||
padding: 2px @space-sm;
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
font-size: @font-size-xs;
|
||||
border-radius: @border-radius-sm;
|
||||
}
|
||||
|
||||
.equip-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
margin: @space-xs 0 0;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
.toddler-dining {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
|
||||
li {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
padding-left: @space-md;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '☑';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toddler-notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
}
|
||||
|
||||
.toddler-note {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-md;
|
||||
padding: @space-md @space-lg;
|
||||
|
||||
strong {
|
||||
color: @color-primary;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
margin: @space-xs 0 0;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,284 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle :title="data.title" :subtitle="data.subtitle" />
|
||||
<p class="compare-intro">{{ data.intro }}</p>
|
||||
|
||||
<!-- 对比表格 -->
|
||||
<div class="compare-table">
|
||||
<!-- 表头:三个目的地 -->
|
||||
<div class="compare-header">
|
||||
<div class="compare-label-col"></div>
|
||||
<div
|
||||
v-for="dest in data.destinations"
|
||||
:key="dest.id"
|
||||
class="compare-dest-col"
|
||||
:class="{ 'compare-dest-col--highlight': dest.highlight }"
|
||||
>
|
||||
<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="compare-row">
|
||||
<div class="compare-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="compare-value-col"
|
||||
:class="{ 'compare-value-col--highlight': data.destinations[i]?.highlight }"
|
||||
>
|
||||
<span class="dim-label-mobile">{{ dim.icon }} {{ dim.label }}</span>
|
||||
{{ val }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 坦诚说明 -->
|
||||
<div class="honest-block">
|
||||
<h3 class="honest-title">{{ data.honestNote.title }}</h3>
|
||||
<p class="honest-subtitle">{{ data.honestNote.subtitle }}</p>
|
||||
<ul class="honest-list">
|
||||
<li v-for="(item, i) in data.honestNote.items" :key="i">{{ item }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 结语 -->
|
||||
<div class="closing-block">
|
||||
<h3 class="closing-title">{{ data.closing.title }}</h3>
|
||||
<p class="closing-text">{{ data.closing.text }}</p>
|
||||
</div>
|
||||
|
||||
<!-- 数据来源 -->
|
||||
<p class="data-source">{{ data.dataSources }}</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import data from '~/data/destinations.json'
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.compare-intro {
|
||||
max-width: 800px;
|
||||
margin: 0 auto @space-xl;
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-loose;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
// 对比表格
|
||||
.compare-table {
|
||||
margin-bottom: @space-2xl;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.compare-header {
|
||||
display: none;
|
||||
|
||||
.respond-md({
|
||||
display: grid;
|
||||
grid-template-columns: 120px repeat(3, 1fr);
|
||||
background: @color-bg-gray;
|
||||
border-bottom: 1px solid @color-border;
|
||||
});
|
||||
}
|
||||
|
||||
.compare-label-col {
|
||||
padding: @space-md @space-lg;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-text-muted;
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
|
||||
.compare-dest-col {
|
||||
padding: @space-lg;
|
||||
text-align: center;
|
||||
|
||||
&--highlight {
|
||||
background: @color-primary-bg;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '推荐';
|
||||
position: absolute;
|
||||
top: @space-xs;
|
||||
right: @space-sm;
|
||||
padding: 2px @space-sm;
|
||||
font-size: 11px;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #fff;
|
||||
background: @color-primary;
|
||||
border-radius: @border-radius-sm;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dest-name {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.dest-tag {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
// 对比行
|
||||
.compare-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid @color-border;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.respond-md({
|
||||
display: grid;
|
||||
grid-template-columns: 120px repeat(3, 1fr);
|
||||
flex-direction: row;
|
||||
});
|
||||
}
|
||||
|
||||
.compare-value-col {
|
||||
padding: @space-md @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
border-bottom: 1px solid fadeout(@color-border, 50%);
|
||||
|
||||
.respond-md({
|
||||
border-bottom: none;
|
||||
border-left: 1px solid fadeout(@color-border, 50%);
|
||||
});
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--highlight {
|
||||
background: fadeout(@color-primary-bg, 30%);
|
||||
color: @color-text-primary;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端:每个值前显示维度标签
|
||||
.dim-label-mobile {
|
||||
display: block;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-muted;
|
||||
margin-bottom: @space-xs;
|
||||
|
||||
.respond-md({
|
||||
display: none;
|
||||
});
|
||||
}
|
||||
|
||||
.dim-icon {
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
.dim-label {
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
|
||||
// 移动端隐藏左侧标签列
|
||||
.compare-label-col {
|
||||
display: none;
|
||||
|
||||
.respond-md({
|
||||
display: flex;
|
||||
});
|
||||
}
|
||||
|
||||
// 坦诚说明
|
||||
.honest-block {
|
||||
max-width: 800px;
|
||||
margin: 0 auto @space-xl;
|
||||
padding: @space-xl;
|
||||
background: #FFF7ED;
|
||||
border-radius: @border-radius-lg;
|
||||
border-left: 4px solid #F59E0B;
|
||||
}
|
||||
|
||||
.honest-title {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #B45309;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.honest-subtitle {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.honest-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
|
||||
li {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
padding-left: @space-md;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '•';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: #F59E0B;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 结语
|
||||
.closing-block {
|
||||
max-width: 800px;
|
||||
margin: 0 auto @space-xl;
|
||||
padding: @space-xl;
|
||||
background: @color-primary-bg;
|
||||
border-radius: @border-radius-lg;
|
||||
border-left: 4px solid @color-primary;
|
||||
}
|
||||
|
||||
.closing-title {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.closing-text {
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-loose;
|
||||
}
|
||||
|
||||
// 数据来源
|
||||
.data-source {
|
||||
text-align: center;
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,129 @@
|
||||
<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 items"
|
||||
:key="item.title"
|
||||
class="diff-item"
|
||||
>
|
||||
<div
|
||||
v-if="item.image"
|
||||
class="diff-bg"
|
||||
:style="{ backgroundImage: `url(${item.image})` }"
|
||||
/>
|
||||
<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>
|
||||
defineProps({
|
||||
items: { type: Array, required: true },
|
||||
})
|
||||
|
||||
// SVG icons for each differentiator
|
||||
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;
|
||||
border: 1px solid @color-border;
|
||||
overflow: hidden;
|
||||
background: @color-bg-white;
|
||||
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);
|
||||
|
||||
.diff-icon {
|
||||
color: @color-primary;
|
||||
background: @color-primary-bg;
|
||||
}
|
||||
|
||||
.diff-bg {
|
||||
opacity: 0.45;
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.diff-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
opacity: 0.35;
|
||||
filter: saturate(0.5) brightness(1.1);
|
||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
||||
}
|
||||
|
||||
.diff-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
padding: @space-xl @space-lg;
|
||||
background: linear-gradient(150deg, rgba(255, 255, 255, 0.97) 30%, rgba(255, 255, 255, 0.85) 60%, rgba(255, 255, 255, 0.65) 100%);
|
||||
}
|
||||
|
||||
.diff-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: @border-radius-lg;
|
||||
background: @color-bg-gray;
|
||||
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 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>
|
||||
import brand from '~/data/brand.json'
|
||||
import images from '~/data/images.json'
|
||||
|
||||
defineProps({
|
||||
seo: { type: Object, required: true },
|
||||
})
|
||||
|
||||
// 仅在首页 preload Hero 图片,避免全站加载
|
||||
useHead({
|
||||
link: [
|
||||
{ rel: 'preload', href: images.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,218 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="旅行产品" label="PRODUCTS" subtitle="四季不同风景,总有一条线路适合你的家庭" />
|
||||
<div class="product-grid">
|
||||
<NuxtLink v-for="line in productLines" :key="line.id" :to="line.to" class="product-line-card">
|
||||
<div class="line-img-wrap">
|
||||
<img :src="line.image" :alt="line.name + ' ' + line.season + '旅行'" class="line-img" loading="lazy" width="400" height="240" />
|
||||
<span class="line-season-tag" :class="`line-season-tag--${line.theme}`">{{ 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" :class="`line-cta--${line.theme}`">
|
||||
了解详情
|
||||
<svg width="14" height="14" 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>
|
||||
</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-forest.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: 'camp',
|
||||
name: '小蒙马亲子营',
|
||||
season: '夏+冬',
|
||||
theme: 'camp',
|
||||
duration: '夏6天 / 冬7天',
|
||||
to: '/summer-camp',
|
||||
image: '/images/summer-camp/grassland-group.jpg',
|
||||
description: '亲子营模式,每期8组家庭结伴同行,孩子有伙伴大人能放松',
|
||||
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(4, 1fr);
|
||||
});
|
||||
}
|
||||
|
||||
.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 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(-6px);
|
||||
|
||||
.line-img {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line-img-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
aspect-ratio: 5 / 3;
|
||||
}
|
||||
|
||||
.line-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.5s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
}
|
||||
|
||||
.line-season-tag {
|
||||
position: absolute;
|
||||
top: @space-md;
|
||||
left: @space-md;
|
||||
padding: @space-xs @space-sm;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-bold;
|
||||
border-radius: @border-radius-sm;
|
||||
backdrop-filter: blur(4px);
|
||||
|
||||
&--summer { color: #fff; background: rgba(45, 106, 79, 0.85); }
|
||||
&--autumn { color: #fff; background: rgba(217, 119, 6, 0.85); }
|
||||
&--winter { color: #fff; background: rgba(59, 130, 246, 0.85); }
|
||||
&--camp { color: #fff; background: rgba(139, 92, 246, 0.85); }
|
||||
}
|
||||
|
||||
.line-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: @space-lg;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.line-meta {
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.line-duration {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
.line-name {
|
||||
font-size: @font-size-xl;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.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: 2px;
|
||||
top: 8px;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: @color-primary-lighter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.line-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: @space-xs;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
margin-top: auto;
|
||||
transition: gap @transition-fast;
|
||||
|
||||
.product-line-card:hover & {
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
&--summer { color: @color-primary; }
|
||||
&--autumn { color: #D97706; }
|
||||
&--winter { color: #3B82F6; }
|
||||
&--camp { color: #8B5CF6; }
|
||||
}
|
||||
</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>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<footer class="app-footer">
|
||||
<div class="footer-container">
|
||||
<div class="footer-grid">
|
||||
<!-- 品牌信息 -->
|
||||
<div class="footer-brand">
|
||||
<div class="footer-logo">
|
||||
<img :src="images.logo.main" alt="呼籁旅行" class="footer-logo-img" width="44" height="44" />
|
||||
<div class="footer-logo-text">
|
||||
<span class="logo-text">呼籁旅行</span>
|
||||
<span class="logo-sub">HULAI TRAVEL</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="footer-desc">{{ brand.slogan.functional }}</p>
|
||||
<div class="footer-qr-list">
|
||||
<CommonQRCodeBlock
|
||||
v-for="ch in qrChannels"
|
||||
:key="ch.type"
|
||||
:image="ch.qrImage"
|
||||
:label="ch.label"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航链接 -->
|
||||
<div v-for="group in footerNav" :key="group.title" class="footer-nav-group">
|
||||
<h4 class="footer-nav-title">{{ group.title }}</h4>
|
||||
<ul class="footer-nav-list">
|
||||
<li v-for="link in group.links" :key="link.to">
|
||||
<NuxtLink :to="link.to" class="footer-nav-link">{{ link.text }}</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="footer-bottom">
|
||||
<p class="footer-company">旅行服务:内蒙古呼籁国际旅行社有限公司</p>
|
||||
<p class="footer-legal">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank" rel="noopener" class="footer-icp-link">{{ brand.icp }}</a>
|
||||
<span class="footer-divider">|</span>
|
||||
{{ brand.icpEntity }}
|
||||
</p>
|
||||
<p class="footer-copyright">
|
||||
© {{ new Date().getFullYear() }} {{ brand.fullName }} 版权所有
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import brand from '~/data/brand.json'
|
||||
import images from '~/data/images.json'
|
||||
import navData from '~/data/navigation.json'
|
||||
import contact from '~/data/contact.json'
|
||||
|
||||
const footerNav = navData.footer
|
||||
const qrChannels = contact.channels.filter(c => c.qrImage && !c.hidden)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.app-footer {
|
||||
background: linear-gradient(180deg, @color-primary-dark 0%, #0F2419 100%);
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
padding: @space-2xl 0 @space-lg;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, @color-primary, @color-accent, @color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.footer-container {
|
||||
.container();
|
||||
}
|
||||
|
||||
.footer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-xl;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
|
||||
gap: @space-xl;
|
||||
});
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
margin-bottom: @space-lg;
|
||||
}
|
||||
|
||||
.footer-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.footer-logo-img {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.footer-logo-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.logo-text {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.logo-sub {
|
||||
font-size: @font-size-xs;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-desc {
|
||||
font-size: @font-size-sm;
|
||||
margin-bottom: @space-lg;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.footer-qr-list {
|
||||
display: flex;
|
||||
gap: @space-lg;
|
||||
}
|
||||
|
||||
.footer-nav-title {
|
||||
font-size: @font-size-base;
|
||||
font-weight: @font-weight-medium;
|
||||
color: #fff;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.footer-nav-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
.footer-nav-link {
|
||||
font-size: @font-size-sm;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-decoration: none;
|
||||
transition: color @transition-fast;
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-bottom {
|
||||
margin-top: @space-2xl;
|
||||
padding-top: @space-lg;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer-company {
|
||||
font-size: @font-size-sm;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.footer-legal {
|
||||
font-size: @font-size-xs;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.footer-divider {
|
||||
margin: 0 @space-sm;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.footer-copyright {
|
||||
font-size: @font-size-xs;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.footer-icp-link {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-decoration: none;
|
||||
transition: color @transition-fast;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<header class="app-header">
|
||||
<div class="header-container">
|
||||
<NuxtLink to="/" class="header-logo" aria-label="呼籁旅行首页">
|
||||
<img :src="images.logo.main" alt="呼籁旅行" class="logo-img" width="40" height="40" />
|
||||
<div class="logo-text-group">
|
||||
<span class="logo-text">呼籁旅行</span>
|
||||
<span class="logo-sub">HULAI TRAVEL</span>
|
||||
</div>
|
||||
</NuxtLink>
|
||||
|
||||
<nav class="header-nav" aria-label="主导航">
|
||||
<ul class="nav-list">
|
||||
<li
|
||||
v-for="item in navItems"
|
||||
:key="item.to"
|
||||
class="nav-item"
|
||||
:class="{ 'has-dropdown': item.children }"
|
||||
@mouseenter="item.children && (openDropdown = item.to)"
|
||||
@mouseleave="item.children && (openDropdown = null)"
|
||||
>
|
||||
<NuxtLink :to="item.to" class="nav-link" :class="{ active: isActive(item.to) }">
|
||||
{{ item.text }}
|
||||
<span v-if="item.children" class="nav-arrow">▾</span>
|
||||
</NuxtLink>
|
||||
<!-- 下拉菜单:真实 <a> 链接,SEO 可抓取 -->
|
||||
<ul v-if="item.children" class="nav-dropdown" :class="{ show: openDropdown === item.to }">
|
||||
<li v-for="child in item.children" :key="child.to" class="dropdown-item">
|
||||
<NuxtLink :to="child.to" class="dropdown-link" @click="openDropdown = null">
|
||||
<span class="dropdown-text">{{ child.text }}</span>
|
||||
<span v-if="child.season" class="dropdown-season">{{ child.season }}</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<button
|
||||
class="mobile-toggle"
|
||||
:aria-expanded="mobileOpen"
|
||||
aria-controls="mobile-nav"
|
||||
aria-label="打开导航菜单"
|
||||
@click="mobileOpen = !mobileOpen"
|
||||
>
|
||||
<span class="toggle-bar" :class="{ open: mobileOpen }"></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<LayoutMobileNav :is-open="mobileOpen" :items="navItems" @close="mobileOpen = false" />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import navData from '~/data/navigation.json'
|
||||
import images from '~/data/images.json'
|
||||
|
||||
const navItems = navData.header
|
||||
const mobileOpen = ref(false)
|
||||
const openDropdown = ref(null)
|
||||
const route = useRoute()
|
||||
|
||||
function isActive(path) {
|
||||
if (path === '/') return route.path === '/'
|
||||
return route.path.startsWith(path)
|
||||
}
|
||||
|
||||
watch(() => route.path, () => {
|
||||
mobileOpen.value = false
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.app-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
background: rgba(255, 255, 255, 0.85);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
transition: box-shadow @transition-base;
|
||||
|
||||
&:hover {
|
||||
box-shadow: @shadow-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.header-container {
|
||||
.container();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 64px;
|
||||
|
||||
.respond-lg({
|
||||
height: 72px;
|
||||
});
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-sm;
|
||||
text-decoration: none;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.respond-lg({
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
});
|
||||
}
|
||||
|
||||
.logo-text-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
|
||||
.respond-lg({
|
||||
font-size: @font-size-xl;
|
||||
});
|
||||
}
|
||||
|
||||
.logo-sub {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.header-nav {
|
||||
display: none;
|
||||
|
||||
.respond-lg({
|
||||
display: block;
|
||||
});
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: flex;
|
||||
gap: @space-xl;
|
||||
}
|
||||
|
||||
.nav-link {
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
text-decoration: none;
|
||||
padding: @space-sm 0;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: color @transition-fast, border-color @transition-fast;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
color: @color-primary;
|
||||
border-bottom-color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
// 下拉菜单
|
||||
.has-dropdown {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
opacity: 0.5;
|
||||
transition: transform @transition-fast;
|
||||
|
||||
.has-dropdown:hover & {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.nav-dropdown {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-width: 180px;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-md;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
|
||||
padding: @space-sm 0;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity @transition-fast, visibility @transition-fast;
|
||||
z-index: 10;
|
||||
|
||||
// 防止鼠标移到下拉菜单过程中菜单消失
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&.show {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: @space-sm @space-lg;
|
||||
text-decoration: none;
|
||||
color: @color-text-secondary;
|
||||
font-size: @font-size-sm;
|
||||
white-space: nowrap;
|
||||
transition: background @transition-fast, color @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dropdown-season {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
background: @color-bg-gray;
|
||||
padding: 2px 6px;
|
||||
border-radius: @border-radius-sm;
|
||||
margin-left: @space-sm;
|
||||
}
|
||||
|
||||
.mobile-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
.respond-lg({
|
||||
display: none;
|
||||
});
|
||||
}
|
||||
|
||||
.toggle-bar {
|
||||
position: relative;
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: @color-text-primary;
|
||||
transition: background @transition-fast;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 24px;
|
||||
height: 2px;
|
||||
background: @color-text-primary;
|
||||
transition: transform @transition-base;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: -7px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 7px;
|
||||
}
|
||||
|
||||
&.open {
|
||||
background: transparent;
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 0;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<Transition name="slide">
|
||||
<div v-if="isOpen" id="mobile-nav" class="mobile-nav" role="dialog" aria-label="导航菜单">
|
||||
<div class="mobile-nav-backdrop" @click="$emit('close')"></div>
|
||||
<nav class="mobile-nav-panel">
|
||||
<ul class="mobile-nav-list">
|
||||
<li v-for="item in items" :key="item.to" class="mobile-nav-item">
|
||||
<!-- 有子菜单的导航项 -->
|
||||
<template v-if="item.children">
|
||||
<button
|
||||
class="mobile-nav-link mobile-nav-toggle"
|
||||
:class="{ expanded: expandedItem === item.to }"
|
||||
@click="toggleExpand(item.to)"
|
||||
>
|
||||
{{ item.text }}
|
||||
<span class="mobile-nav-arrow">›</span>
|
||||
</button>
|
||||
<ul v-show="expandedItem === item.to" class="mobile-sub-list">
|
||||
<li v-for="child in item.children" :key="child.to" class="mobile-sub-item">
|
||||
<NuxtLink :to="child.to" class="mobile-sub-link" @click="$emit('close')">
|
||||
<span>{{ child.text }}</span>
|
||||
<span v-if="child.season" class="mobile-sub-season">{{ child.season }}</span>
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<!-- 普通导航项 -->
|
||||
<NuxtLink v-else :to="item.to" class="mobile-nav-link" @click="$emit('close')">
|
||||
{{ item.text }}
|
||||
</NuxtLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
isOpen: { type: Boolean, default: false },
|
||||
items: { type: Array, default: () => [] },
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
|
||||
const expandedItem = ref(null)
|
||||
|
||||
function toggleExpand(key) {
|
||||
expandedItem.value = expandedItem.value === key ? null : key
|
||||
}
|
||||
|
||||
// 关闭菜单时重置展开状态
|
||||
watch(() => props.isOpen, (open) => {
|
||||
if (import.meta.client) {
|
||||
document.body.style.overflow = open ? 'hidden' : ''
|
||||
}
|
||||
if (!open) {
|
||||
expandedItem.value = null
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (import.meta.client) {
|
||||
document.body.style.overflow = ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.mobile-nav {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.mobile-nav-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.mobile-nav-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: min(300px, 85vw);
|
||||
height: 100%;
|
||||
background: @color-bg-white;
|
||||
padding: @space-3xl @space-lg @space-lg;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mobile-nav-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-xs;
|
||||
}
|
||||
|
||||
.mobile-nav-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: @space-md @space-md;
|
||||
font-size: @font-size-md;
|
||||
color: @color-text-primary;
|
||||
text-decoration: none;
|
||||
border-radius: @border-radius-md;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: background @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-nav-toggle {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.mobile-nav-arrow {
|
||||
font-size: @font-size-lg;
|
||||
color: @color-text-muted;
|
||||
transition: transform @transition-fast;
|
||||
|
||||
.expanded & {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-sub-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-left: @space-md;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.mobile-sub-item {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.mobile-sub-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: @space-sm @space-md;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
text-decoration: none;
|
||||
border-radius: @border-radius-sm;
|
||||
transition: background @transition-fast, color @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary-bg;
|
||||
color: @color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.mobile-sub-season {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-muted;
|
||||
background: @color-bg-gray;
|
||||
padding: 2px 6px;
|
||||
border-radius: @border-radius-sm;
|
||||
}
|
||||
|
||||
.slide-enter-active,
|
||||
.slide-leave-active {
|
||||
transition: opacity @transition-base;
|
||||
|
||||
.mobile-nav-panel {
|
||||
transition: transform @transition-base;
|
||||
}
|
||||
}
|
||||
|
||||
.slide-enter-from,
|
||||
.slide-leave-to {
|
||||
opacity: 0;
|
||||
|
||||
.mobile-nav-panel {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<section class="page-hero" :class="{ 'page-hero--compact': compact }">
|
||||
<div class="container">
|
||||
<CommonBreadcrumbNav :items="breadcrumbs" />
|
||||
<h1 class="page-hero-title">{{ title }}</h1>
|
||||
<p v-if="subtitle" class="page-hero-subtitle">{{ subtitle }}</p>
|
||||
</div>
|
||||
<div class="page-hero-curve">
|
||||
<svg viewBox="0 0 1440 48" fill="none" preserveAspectRatio="none" aria-hidden="true">
|
||||
<path d="M0 48h1440V24C1200 0 240 0 0 24v24z" fill="currentColor" />
|
||||
</svg>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
title: { type: String, required: true },
|
||||
subtitle: { type: String, default: '' },
|
||||
compact: { type: Boolean, default: false },
|
||||
breadcrumbs: { type: Array, default: () => [] },
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.page-hero {
|
||||
background: linear-gradient(135deg, @color-primary-bg 0%, #E8F3ED 100%);
|
||||
padding: @space-2xl 0 @space-3xl;
|
||||
position: relative;
|
||||
|
||||
.respond-md({
|
||||
padding: @space-3xl 0 @space-4xl;
|
||||
});
|
||||
|
||||
&--compact {
|
||||
padding: @space-xl 0 @space-2xl;
|
||||
}
|
||||
}
|
||||
|
||||
.page-hero-title {
|
||||
margin-top: @space-md;
|
||||
color: @color-text-primary;
|
||||
}
|
||||
|
||||
.page-hero-subtitle {
|
||||
margin-top: @space-sm;
|
||||
font-size: @font-size-md;
|
||||
color: @color-text-secondary;
|
||||
max-width: 600px;
|
||||
line-height: @line-height-relaxed;
|
||||
}
|
||||
|
||||
.page-hero-curve {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
line-height: 0;
|
||||
color: @color-bg-white;
|
||||
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
|
||||
.respond-md({
|
||||
height: 48px;
|
||||
});
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<section class="section section--warm">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="产品进化" subtitle="从V1到V9,每一次更新都是客户反馈驱动" />
|
||||
<div class="evo-stats">
|
||||
<div class="evo-stat">
|
||||
<span class="evo-stat-num">{{ stats.iterations }}</span>
|
||||
<span class="evo-stat-label">次迭代</span>
|
||||
</div>
|
||||
<div class="evo-stat">
|
||||
<span class="evo-stat-num">{{ stats.years }}</span>
|
||||
<span class="evo-stat-label">年打磨</span>
|
||||
</div>
|
||||
<div class="evo-stat">
|
||||
<span class="evo-stat-num">{{ stats.guests }}</span>
|
||||
<span class="evo-stat-label">人次体验</span>
|
||||
</div>
|
||||
</div>
|
||||
<p class="evo-intro">2023年3月,呼籁做了一个"反常识"的决定——只做一条路线。不铺量、不贪多,把所有精力集中在打磨一个产品上。三年9次迭代,每一次版本更新背后,都有真实的客户反馈和一线体验数据。</p>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
stats: { type: Object, required: true }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.evo-stats {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: @space-2xl;
|
||||
margin-bottom: @space-xl;
|
||||
}
|
||||
|
||||
.evo-stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.evo-stat-num {
|
||||
display: inline-block;
|
||||
font-size: @font-size-3xl;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
line-height: @line-height-tight;
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 10%;
|
||||
right: 10%;
|
||||
height: 3px;
|
||||
background: @color-primary-lighter;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.evo-stat-label {
|
||||
display: block;
|
||||
margin-top: @space-sm;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.evo-intro {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-loose;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="关于价格" subtitle="透明、清晰、不套路" />
|
||||
<div class="pricing-content">
|
||||
<p>{{ content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ content: { type: String, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.pricing-content { max-width: 800px; margin: 0 auto; padding: @space-xl; background: @color-bg-warm; border-radius: @border-radius-lg;
|
||||
p { font-size: @font-size-md; line-height: @line-height-loose; color: @color-text-secondary; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<article
|
||||
class="product-card"
|
||||
:class="{ 'product-card--popular': isPopular }"
|
||||
itemscope
|
||||
itemtype="https://schema.org/TouristTrip"
|
||||
>
|
||||
<!-- 头部:天数 + 标签 -->
|
||||
<div class="product-header">
|
||||
<div v-if="isPopular" class="popular-badge">推荐</div>
|
||||
<div class="product-duration">
|
||||
<span class="duration-num">{{ product.days }}</span>
|
||||
<span class="duration-unit">天</span>
|
||||
<span class="duration-num">{{ product.nights }}</span>
|
||||
<span class="duration-unit">晚</span>
|
||||
</div>
|
||||
<span class="product-tag">{{ product.tag }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 主体 -->
|
||||
<div class="product-body">
|
||||
<h3 class="product-name" itemprop="name">
|
||||
<NuxtLink :to="`/products/${product.id}`" class="product-name-link">{{ shortName }}</NuxtLink>
|
||||
</h3>
|
||||
<p class="product-audience">
|
||||
适合:<span itemprop="touristType">{{ product.audience }}</span>
|
||||
</p>
|
||||
<p class="product-desc" itemprop="description">{{ product.description }}</p>
|
||||
<ul class="product-highlights">
|
||||
<li v-for="(h, i) in product.highlights" :key="i">
|
||||
<span class="highlight-check" aria-hidden="true">✓</span>
|
||||
{{ h }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 底部 CTA -->
|
||||
<NuxtLink to="/contact" class="product-cta">
|
||||
咨询该行程 <span aria-hidden="true">→</span>
|
||||
</NuxtLink>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({ product: { type: Object, required: true } })
|
||||
|
||||
const isPopular = computed(() => props.product.id === 'v9-6d5n-family')
|
||||
|
||||
// 去掉"额吉的故乡V9·"前缀,在标题区域已标明系列
|
||||
const shortName = computed(() => {
|
||||
return props.product.name.replace(/^额吉的故乡V9·/, '')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.product-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: box-shadow @transition-base, transform @transition-base;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
box-shadow: @shadow-lg;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
&--popular {
|
||||
border: 2px solid @color-primary;
|
||||
box-shadow: @shadow-md;
|
||||
|
||||
.product-header {
|
||||
background: @color-primary;
|
||||
}
|
||||
|
||||
.duration-num,
|
||||
.duration-unit,
|
||||
.product-tag {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.product-tag {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 头部
|
||||
.product-header {
|
||||
padding: @space-lg @space-lg @space-md;
|
||||
background: @color-primary-bg;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popular-badge {
|
||||
position: absolute;
|
||||
top: @space-sm;
|
||||
right: @space-sm;
|
||||
padding: @space-xs @space-sm;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #fff;
|
||||
background: @color-accent;
|
||||
border-radius: @border-radius-sm;
|
||||
}
|
||||
|
||||
.product-duration {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.duration-num {
|
||||
font-size: @font-size-3xl;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.duration-unit {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-primary;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.product-tag {
|
||||
display: inline-block;
|
||||
padding: @space-xs @space-md;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-primary;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-full;
|
||||
}
|
||||
|
||||
// 主体
|
||||
.product-body {
|
||||
padding: @space-lg;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
.product-name-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
&:hover { color: @color-primary; }
|
||||
}
|
||||
|
||||
.product-audience {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.product-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin-bottom: @space-md;
|
||||
.text-clamp(3);
|
||||
}
|
||||
|
||||
.product-highlights {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
margin-top: auto;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: @space-sm;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-check {
|
||||
flex-shrink: 0;
|
||||
color: @color-primary;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-bold;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// 底部 CTA
|
||||
.product-cta {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: @space-md @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-primary;
|
||||
background: @color-bg-gray;
|
||||
text-decoration: none;
|
||||
transition: all @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<section class="section section--warm">
|
||||
<div class="container">
|
||||
<div class="overview-content">
|
||||
<div class="overview-quote-mark" aria-hidden="true">"</div>
|
||||
<p class="overview-text">{{ narrative }}</p>
|
||||
<div class="overview-line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ narrative: { type: String, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.overview-content {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.overview-quote-mark {
|
||||
font-size: 80px;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary-lighter;
|
||||
line-height: 0.5;
|
||||
margin-bottom: @space-md;
|
||||
font-family: Georgia, serif;
|
||||
}
|
||||
|
||||
.overview-text {
|
||||
font-size: @font-size-md;
|
||||
line-height: @line-height-loose;
|
||||
color: @color-text-secondary;
|
||||
|
||||
.respond-md({
|
||||
font-size: @font-size-lg;
|
||||
});
|
||||
}
|
||||
|
||||
.overview-line {
|
||||
width: 60px;
|
||||
height: 3px;
|
||||
background: @color-primary;
|
||||
border-radius: 2px;
|
||||
margin: @space-xl auto 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<section id="guide" class="section section--gray">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="不知道选哪个?" subtitle="按您的情况推荐" />
|
||||
<div class="guide-groups">
|
||||
<div v-for="group in guideGroups" :key="group.title" class="guide-group">
|
||||
<h3 class="guide-group-title">{{ group.title }}</h3>
|
||||
<div class="guide-items">
|
||||
<div v-for="item in group.items" :key="item.condition" class="guide-item">
|
||||
<div class="guide-item-top">
|
||||
<span class="guide-condition">{{ item.condition }}</span>
|
||||
<span class="guide-arrow">→</span>
|
||||
<span class="guide-rec">{{ getProductName(item.recommendation) }}</span>
|
||||
</div>
|
||||
<p class="guide-reason">{{ item.reason }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
guide: { type: Object, required: true },
|
||||
versions: { type: Array, required: true },
|
||||
})
|
||||
|
||||
const guideGroups = computed(() => [
|
||||
{ title: '按假期长度', items: props.guide.byVacationLength },
|
||||
{ title: '按孩子年龄', items: props.guide.byChildAge },
|
||||
{ title: '按旅行偏好', items: props.guide.byPreference },
|
||||
])
|
||||
|
||||
function getProductName(id) {
|
||||
if (id === 'summer-camp') return '小蒙马夏令营'
|
||||
const v = props.versions.find(v => v.id === id)
|
||||
return v ? v.name : id
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.guide-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-2xl;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.guide-group-title {
|
||||
font-size: @font-size-lg;
|
||||
color: @color-primary;
|
||||
margin-bottom: @space-md;
|
||||
padding-left: @space-md;
|
||||
border-left: 3px solid @color-primary;
|
||||
}
|
||||
|
||||
.guide-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
}
|
||||
|
||||
.guide-item {
|
||||
padding: @space-lg;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-md;
|
||||
border: 1px solid @color-border;
|
||||
transition: border-color @transition-fast, box-shadow @transition-fast;
|
||||
|
||||
&:hover {
|
||||
border-color: @color-primary-lighter;
|
||||
box-shadow: @shadow-sm;
|
||||
}
|
||||
}
|
||||
|
||||
.guide-item-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.guide-condition {
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-text-primary;
|
||||
background: @color-bg-gray;
|
||||
padding: @space-xs @space-sm;
|
||||
border-radius: @border-radius-sm;
|
||||
font-size: @font-size-sm;
|
||||
}
|
||||
|
||||
.guide-arrow {
|
||||
color: @color-primary;
|
||||
font-weight: @font-weight-bold;
|
||||
}
|
||||
|
||||
.guide-rec {
|
||||
color: @color-primary;
|
||||
font-weight: @font-weight-bold;
|
||||
font-size: @font-size-base;
|
||||
}
|
||||
|
||||
.guide-reason {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
margin: 0;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<section id="summer-camp" class="section section--green">
|
||||
<div class="container">
|
||||
<CommonSectionTitle :title="camp.name" :subtitle="`适合${camp.ageRange}青少年`" />
|
||||
|
||||
<!-- 团宠+简介 -->
|
||||
<div class="camp-hero">
|
||||
<div class="camp-mascot">
|
||||
<img
|
||||
:src="images.mascot.xiaomengma"
|
||||
alt="小蒙马 - 小蒙马夏令营团宠,一只可爱的蒙古小马形象"
|
||||
width="200"
|
||||
height="200"
|
||||
loading="lazy"
|
||||
/>
|
||||
</div>
|
||||
<div class="camp-intro">
|
||||
<p class="camp-positioning">{{ camp.positioning }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="camp-content">
|
||||
<div v-if="camp.coreActivities.length" class="camp-activities">
|
||||
<h3>核心活动</h3>
|
||||
<ul>
|
||||
<li v-for="(a, i) in camp.coreActivities" :key="i">{{ a }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p class="camp-diff">{{ camp.differenceFromV9 }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import images from '~/data/images.json'
|
||||
defineProps({ camp: { type: Object, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.camp-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: @space-lg;
|
||||
max-width: 800px;
|
||||
margin: 0 auto @space-xl;
|
||||
|
||||
.respond-md({
|
||||
flex-direction: row;
|
||||
text-align: left;
|
||||
gap: @space-2xl;
|
||||
});
|
||||
}
|
||||
|
||||
.camp-mascot {
|
||||
flex-shrink: 0;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
width: 160px;
|
||||
height: auto;
|
||||
filter: drop-shadow(0 6px 20px rgba(0, 0, 0, 0.08));
|
||||
}
|
||||
}
|
||||
|
||||
.camp-intro {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.camp-content { max-width: 800px; margin: 0 auto; }
|
||||
.camp-positioning { font-size: @font-size-base; color: @color-text-secondary; line-height: @line-height-loose; }
|
||||
.camp-activities { margin-bottom: @space-lg;
|
||||
h3 { font-size: @font-size-md; margin-bottom: @space-md; }
|
||||
ul { display: flex; flex-direction: column; gap: @space-sm; }
|
||||
li { font-size: @font-size-base; color: @color-text-secondary; padding-left: @space-md; position: relative;
|
||||
&::before { content: '·'; position: absolute; left: 0; color: @color-primary; }
|
||||
}
|
||||
}
|
||||
.camp-diff { font-size: @font-size-base; color: @color-text-muted; padding: @space-md; background: @color-bg-white; border-radius: @border-radius-md; }
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="V8 → V9 升级对比" subtitle="7项关键升级一目了然" />
|
||||
<div class="compare-wrapper">
|
||||
<div class="compare-table">
|
||||
<div class="compare-head">
|
||||
<span class="compare-head-old">{{ compare.headers[0] }}</span>
|
||||
<span class="compare-head-new">{{ compare.headers[1] }}</span>
|
||||
</div>
|
||||
<div v-for="(row, i) in compare.rows" :key="i" class="compare-row">
|
||||
<span class="compare-old">{{ row[0] }}</span>
|
||||
<span class="compare-new">{{ row[1] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
compare: { type: Object, required: true }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.compare-wrapper {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.compare-table {
|
||||
border-radius: @border-radius-md;
|
||||
overflow: hidden;
|
||||
border: 1px solid @color-border;
|
||||
}
|
||||
|
||||
.compare-head {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.compare-head-old {
|
||||
font-weight: @font-weight-medium;
|
||||
text-align: center;
|
||||
background: @color-bg-gray;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
.compare-head-new {
|
||||
font-weight: @font-weight-medium;
|
||||
text-align: center;
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.compare-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
border-top: 1px solid @color-border;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
|
||||
.compare-old {
|
||||
color: @color-text-muted;
|
||||
background: @color-bg-white;
|
||||
}
|
||||
|
||||
.compare-new {
|
||||
color: @color-text-primary;
|
||||
background: @color-primary-bg;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
// mobile-first: 小屏紧凑,md 以上舒适
|
||||
.compare-row, .compare-head {
|
||||
font-size: @font-size-xs;
|
||||
|
||||
.respond-md({
|
||||
font-size: @font-size-sm;
|
||||
});
|
||||
}
|
||||
|
||||
.compare-old, .compare-new,
|
||||
.compare-head-old, .compare-head-new {
|
||||
padding: @space-sm @space-md;
|
||||
|
||||
.respond-md({
|
||||
padding: @space-md @space-lg;
|
||||
});
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<section class="section section--gray">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="更多V9亮点" subtitle="独家资源 · 自营服务 · 全程无忧" />
|
||||
<div class="highlights-list">
|
||||
<div v-for="item in highlights" :key="item.text" class="highlight-item">
|
||||
<span class="highlight-label">{{ item.label }}</span>
|
||||
<span class="highlight-text">{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
highlights: { type: Array, required: true }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.highlights-list {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.highlight-item {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: @space-md;
|
||||
padding: @space-md 0;
|
||||
border-bottom: 1px solid @color-border;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight-label {
|
||||
flex-shrink: 0;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
background: @color-primary-bg;
|
||||
padding: @space-xs @space-sm;
|
||||
border-radius: @border-radius-sm;
|
||||
min-width: 42px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.highlight-text {
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="9次迭代背后的产品哲学" subtitle="不是做加法,而是做取舍" />
|
||||
<div class="philosophy-wrapper">
|
||||
<div class="philosophy-list">
|
||||
<div v-for="item in philosophy" :key="item.label" class="philosophy-item">
|
||||
<h4 class="philosophy-label">{{ item.label }}</h4>
|
||||
<p class="philosophy-text">{{ item.text }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="quote-block">
|
||||
<p class="quote-text">"{{ quote.text }}"</p>
|
||||
<span class="quote-author">—— {{ quote.author }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
philosophy: { type: Array, required: true },
|
||||
quote: { type: Object, required: true }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.philosophy-wrapper {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.philosophy-list {
|
||||
margin-bottom: @space-2xl;
|
||||
}
|
||||
|
||||
.philosophy-item {
|
||||
padding: @space-lg 0;
|
||||
border-bottom: 1px solid @color-border;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.philosophy-label {
|
||||
font-size: @font-size-base;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin: 0 0 @space-xs;
|
||||
}
|
||||
|
||||
.philosophy-text {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quote-block {
|
||||
text-align: center;
|
||||
padding: @space-2xl;
|
||||
background: @color-primary;
|
||||
border-radius: @border-radius-lg;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.quote-text {
|
||||
font-size: @font-size-md;
|
||||
line-height: @line-height-loose;
|
||||
margin: 0 0 @space-lg;
|
||||
color: #fff;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.quote-author {
|
||||
font-size: @font-size-sm;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,175 @@
|
||||
<template>
|
||||
<section class="section section--gray">
|
||||
<div class="container">
|
||||
<CommonSectionTitle :title="title" :subtitle="subtitle" />
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline">
|
||||
<div v-for="v in timeline" :key="v.version" class="version-block">
|
||||
<div class="version-dot" :class="{ 'version-dot--first': v.version === timeline[0].version }"></div>
|
||||
<div class="version-header">
|
||||
<span class="version-tag">{{ v.version }}</span>
|
||||
<span class="version-date">{{ v.date }}</span>
|
||||
</div>
|
||||
<h4 class="version-title">{{ v.title }}</h4>
|
||||
<ul class="change-list">
|
||||
<li
|
||||
v-for="(c, i) in v.changes"
|
||||
:key="i"
|
||||
:class="'change-' + c.type"
|
||||
>{{ c.text }}</li>
|
||||
</ul>
|
||||
<div v-if="expandedVersion === v.version" class="version-reason">
|
||||
{{ v.reason }}
|
||||
</div>
|
||||
<button
|
||||
class="reason-toggle"
|
||||
@click="expandedVersion = expandedVersion === v.version ? '' : v.version"
|
||||
>
|
||||
{{ expandedVersion === v.version ? '收起' : '为什么这样改?' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
timeline: { type: Array, required: true },
|
||||
title: { type: String, default: '历史版本' },
|
||||
subtitle: { type: String, default: 'V1到V8的完整迭代记录' },
|
||||
})
|
||||
|
||||
const expandedVersion = ref('')
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.timeline-wrapper {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 24px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 1px;
|
||||
background: linear-gradient(180deg, @color-primary, @color-border);
|
||||
}
|
||||
}
|
||||
|
||||
.version-block {
|
||||
position: relative;
|
||||
padding-bottom: @space-xl;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.version-dot {
|
||||
position: absolute;
|
||||
left: -24px;
|
||||
top: 6px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: @color-border;
|
||||
}
|
||||
|
||||
.version-dot--first {
|
||||
background: @color-primary;
|
||||
}
|
||||
|
||||
.version-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: @space-sm;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
}
|
||||
|
||||
.version-date {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
.version-title {
|
||||
font-size: @font-size-base;
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-text-secondary;
|
||||
margin: 0 0 @space-md;
|
||||
}
|
||||
|
||||
.change-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0 0 @space-sm;
|
||||
}
|
||||
|
||||
.change-list li {
|
||||
font-size: @font-size-sm;
|
||||
line-height: @line-height-loose;
|
||||
color: @color-text-secondary;
|
||||
padding-left: 18px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
font-size: 12px;
|
||||
font-weight: @font-weight-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.change-add::before {
|
||||
content: '+';
|
||||
color: @color-primary;
|
||||
}
|
||||
|
||||
.change-remove::before {
|
||||
content: '−';
|
||||
color: @color-text-muted;
|
||||
}
|
||||
|
||||
.change-update::before {
|
||||
content: '△';
|
||||
color: @color-accent;
|
||||
}
|
||||
|
||||
.version-reason {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
line-height: @line-height-loose;
|
||||
padding: @space-md @space-lg;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-sm;
|
||||
border-left: 3px solid @color-primary;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.reason-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: @color-primary;
|
||||
font-size: @font-size-xs;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="2026年核心升级" subtitle="基于客户反馈的三大重磅升级" />
|
||||
<div class="upgrades-list">
|
||||
<div v-for="item in upgrades" :key="item.name" class="upgrade-item">
|
||||
<div class="upgrade-header">
|
||||
<span class="upgrade-tag">{{ item.tag }}</span>
|
||||
<h3 class="upgrade-name">{{ item.name }}</h3>
|
||||
</div>
|
||||
<p class="upgrade-desc">{{ item.description }}</p>
|
||||
<div class="upgrade-reason">{{ item.reason }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
upgrades: { type: Array, required: true }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.upgrades-list {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-xl;
|
||||
}
|
||||
|
||||
.upgrade-item {
|
||||
padding-bottom: @space-xl;
|
||||
border-bottom: 1px solid @color-border;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.upgrade-header {
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.upgrade-tag {
|
||||
display: inline-block;
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-medium;
|
||||
padding: @space-xs @space-md;
|
||||
border-radius: @border-radius-full;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.upgrade-name {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.upgrade-desc {
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-loose;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.upgrade-reason {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
line-height: @line-height-base;
|
||||
padding: @space-md @space-lg;
|
||||
background: @color-bg-gray;
|
||||
border-radius: @border-radius-sm;
|
||||
border-left: 3px solid @color-primary;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="review-filter">
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">按场景:</span>
|
||||
<button
|
||||
class="filter-btn"
|
||||
:class="{ active: !activeScene }"
|
||||
@click="$emit('filterScene', '')"
|
||||
>全部</button>
|
||||
<button
|
||||
v-for="s in scenes"
|
||||
:key="s"
|
||||
class="filter-btn"
|
||||
:class="{ active: activeScene === s }"
|
||||
@click="$emit('filterScene', s)"
|
||||
>{{ s }}</button>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">按关心点:</span>
|
||||
<button
|
||||
class="filter-btn"
|
||||
:class="{ active: !activeConcern }"
|
||||
@click="$emit('filterConcern', '')"
|
||||
>全部</button>
|
||||
<button
|
||||
v-for="c in concerns"
|
||||
:key="c"
|
||||
class="filter-btn"
|
||||
:class="{ active: activeConcern === c }"
|
||||
@click="$emit('filterConcern', c)"
|
||||
>{{ c }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
scenes: { type: Array, required: true },
|
||||
concerns: { type: Array, required: true },
|
||||
activeScene: { type: String, default: '' },
|
||||
activeConcern: { type: String, default: '' },
|
||||
})
|
||||
defineEmits(['filterScene', 'filterConcern'])
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.review-filter {
|
||||
margin-bottom: @space-xl;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-md;
|
||||
padding: @space-lg;
|
||||
background: @color-bg-gray;
|
||||
border-radius: @border-radius-md;
|
||||
}
|
||||
.filter-group { display: flex; align-items: flex-start; flex-wrap: wrap; gap: @space-xs; }
|
||||
.filter-label {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
white-space: nowrap;
|
||||
line-height: 44px;
|
||||
margin-right: @space-xs;
|
||||
}
|
||||
.filter-btn {
|
||||
padding: @space-sm @space-md;
|
||||
font-size: @font-size-sm;
|
||||
border: 1px solid @color-border;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-full;
|
||||
cursor: pointer;
|
||||
color: @color-text-secondary;
|
||||
transition: all @transition-fast;
|
||||
min-height: 44px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
&:hover { border-color: @color-primary; color: @color-primary; }
|
||||
&.active { background: @color-primary; color: #fff; border-color: @color-primary; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<!--
|
||||
双层设计:
|
||||
上层:截图(视觉证明真实性)
|
||||
下层:文字(SEO可抓取)
|
||||
-->
|
||||
<article class="review-item">
|
||||
<!-- 截图层 -->
|
||||
<div v-if="review.screenshot" class="review-screenshot" :class="{ 'is-expanded': expanded }">
|
||||
<img
|
||||
:src="review.screenshot"
|
||||
:alt="`客户${review.nickname}对呼籁旅行${review.productVersion}的评价`"
|
||||
loading="lazy"
|
||||
class="review-img"
|
||||
@click="expanded = !expanded"
|
||||
/>
|
||||
<button
|
||||
v-if="!expanded"
|
||||
class="screenshot-expand"
|
||||
@click="expanded = true"
|
||||
aria-label="查看完整截图"
|
||||
>
|
||||
<span class="expand-icon">▾</span> 查看完整截图
|
||||
</button>
|
||||
<button
|
||||
v-else
|
||||
class="screenshot-collapse"
|
||||
@click="expanded = false"
|
||||
aria-label="收起截图"
|
||||
>
|
||||
<span class="expand-icon expand-icon--up">▴</span> 收起截图
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 文字层(SEO核心) -->
|
||||
<div class="review-content">
|
||||
<div class="review-meta">
|
||||
<span class="review-name">{{ review.nickname }}</span>
|
||||
<span class="review-sep">·</span>
|
||||
<span class="review-date">{{ review.travelDate }}</span>
|
||||
</div>
|
||||
<p class="review-product">{{ review.productVersion }}</p>
|
||||
<blockquote class="review-text" :class="{ 'is-clamped': !textExpanded && isLongText }">
|
||||
{{ review.content }}
|
||||
</blockquote>
|
||||
<button
|
||||
v-if="isLongText"
|
||||
class="text-toggle"
|
||||
@click="textExpanded = !textExpanded"
|
||||
>
|
||||
{{ textExpanded ? '收起' : '展开全文' }}
|
||||
</button>
|
||||
<div class="review-tags">
|
||||
<span v-for="s in review.scenes" :key="s" class="tag tag--scene">{{ s }}</span>
|
||||
<span v-for="c in review.concerns" :key="c" class="tag tag--concern">{{ c }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({ review: { type: Object, required: true } })
|
||||
|
||||
const expanded = ref(false)
|
||||
const textExpanded = ref(false)
|
||||
const isLongText = computed(() => props.review.content && props.review.content.length > 150)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.review-item {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: box-shadow @transition-base, transform @transition-base;
|
||||
|
||||
&:hover {
|
||||
box-shadow: @shadow-md;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.review-screenshot {
|
||||
position: relative;
|
||||
background: @color-bg-gray;
|
||||
max-height: 320px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
|
||||
/* 底部渐变遮罩,提示可展开 */
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 60px;
|
||||
background: linear-gradient(transparent, rgba(248, 248, 250, 0.95));
|
||||
pointer-events: none;
|
||||
transition: opacity @transition-fast;
|
||||
}
|
||||
|
||||
&.is-expanded {
|
||||
max-height: none;
|
||||
|
||||
&::after {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.review-img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.screenshot-expand,
|
||||
.screenshot-collapse {
|
||||
position: absolute;
|
||||
bottom: @space-sm;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: @space-xs @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-primary;
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-primary;
|
||||
border-radius: @border-radius-full;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
white-space: nowrap;
|
||||
transition: all @transition-fast;
|
||||
font-weight: @font-weight-medium;
|
||||
box-shadow: @shadow-sm;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.expand-icon {
|
||||
font-size: @font-size-xs;
|
||||
margin-right: @space-xs;
|
||||
}
|
||||
|
||||
.screenshot-collapse {
|
||||
position: relative;
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
transform: none;
|
||||
display: block;
|
||||
margin: @space-sm auto;
|
||||
}
|
||||
|
||||
.review-content {
|
||||
padding: @space-md @space-lg @space-lg;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.review-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: @space-xs;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.review-name { font-weight: @font-weight-medium; color: @color-text-primary; }
|
||||
.review-sep { color: @color-text-muted; }
|
||||
.review-date { font-size: @font-size-sm; color: @color-text-muted; }
|
||||
.review-product { font-size: @font-size-sm; color: @color-primary; margin-bottom: @space-sm; }
|
||||
|
||||
.review-text {
|
||||
font-size: @font-size-sm;
|
||||
line-height: 1.7;
|
||||
color: @color-text-secondary;
|
||||
margin-bottom: @space-sm;
|
||||
|
||||
&.is-clamped {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.text-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: @color-primary;
|
||||
font-size: @font-size-xs;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-bottom: @space-sm;
|
||||
align-self: flex-start;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.review-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-xs;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.tag {
|
||||
padding: @space-xs @space-sm;
|
||||
font-size: @font-size-xs;
|
||||
border-radius: @border-radius-full;
|
||||
|
||||
&--scene { background: @color-primary-bg; color: @color-primary; }
|
||||
&--concern { background: @color-bg-warm; color: @color-accent; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,120 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- SEO层:全部评价文字内容在HTML中可见,供搜索引擎抓取 -->
|
||||
<div class="review-list">
|
||||
<ReviewsReviewItem v-for="r in visibleReviews" :key="r.id" :review="r" />
|
||||
<div v-if="!reviews.length" class="review-empty">
|
||||
<p>暂无符合条件的评价</p>
|
||||
<button class="review-reset" @click="$emit('reset')">清除筛选,查看全部评价</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="hasMore" class="review-loadmore">
|
||||
<button class="loadmore-btn" @click="loadMore">
|
||||
查看更多评价(还有{{ remaining }}条)
|
||||
</button>
|
||||
</div>
|
||||
<p v-else-if="reviews.length > pageSize" class="review-end">已展示全部{{ reviews.length }}条评价</p>
|
||||
|
||||
<!--
|
||||
SEO 隐藏层:将未展示的评价文字以语义化HTML输出到页面中
|
||||
视觉上隐藏但搜索引擎可抓取,确保全部82条评价内容被索引
|
||||
-->
|
||||
<div v-if="hiddenReviews.length" class="sr-only" aria-hidden="true">
|
||||
<div v-for="r in hiddenReviews" :key="'seo-' + r.id">
|
||||
<p>{{ r.nickname }} · {{ r.travelDate }} · {{ r.productVersion }}</p>
|
||||
<p>{{ r.content }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const props = defineProps({ reviews: { type: Array, required: true } })
|
||||
defineEmits(['reset'])
|
||||
|
||||
const pageSize = 12
|
||||
const showCount = ref(pageSize)
|
||||
|
||||
const visibleReviews = computed(() => props.reviews.slice(0, showCount.value))
|
||||
const hiddenReviews = computed(() => props.reviews.slice(showCount.value))
|
||||
const hasMore = computed(() => showCount.value < props.reviews.length)
|
||||
const remaining = computed(() => props.reviews.length - showCount.value)
|
||||
|
||||
function loadMore() {
|
||||
showCount.value += pageSize
|
||||
}
|
||||
|
||||
// 筛选条件变化时重置
|
||||
watch(() => props.reviews.length, () => {
|
||||
showCount.value = pageSize
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.review-list {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-lg;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
});
|
||||
}
|
||||
|
||||
.review-empty {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: @space-2xl;
|
||||
color: @color-text-muted;
|
||||
|
||||
p {
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
}
|
||||
|
||||
.review-reset {
|
||||
padding: @space-sm @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-primary;
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-primary;
|
||||
border-radius: @border-radius-full;
|
||||
cursor: pointer;
|
||||
transition: all @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.review-loadmore {
|
||||
text-align: center;
|
||||
margin-top: @space-xl;
|
||||
}
|
||||
|
||||
.loadmore-btn {
|
||||
padding: @space-sm @space-xl;
|
||||
font-size: @font-size-base;
|
||||
color: @color-primary;
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-primary;
|
||||
border-radius: @border-radius-full;
|
||||
cursor: pointer;
|
||||
transition: all @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.review-end {
|
||||
text-align: center;
|
||||
margin-top: @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<section class="section section--green">
|
||||
<div class="container">
|
||||
<div class="summary-grid">
|
||||
<div class="summary-stat">
|
||||
<span class="stat-value">{{ summary.totalCount }}<span class="stat-plus">+</span></span>
|
||||
<span class="stat-label">组家庭的选择</span>
|
||||
</div>
|
||||
<div class="summary-divider"></div>
|
||||
<div class="summary-stat">
|
||||
<span class="stat-value">{{ summary.approvalRate }}</span>
|
||||
<span class="stat-label">好评率</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-keywords">
|
||||
<span class="keywords-label">客户高频关键词</span>
|
||||
<div class="keywords-list">
|
||||
<span v-for="kw in summary.keywords" :key="kw" class="keyword-tag">{{ kw }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({ summary: { type: Object, required: true } })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.summary-grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: @space-2xl;
|
||||
margin-bottom: @space-xl;
|
||||
}
|
||||
|
||||
.summary-stat {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: @font-size-hero;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-primary;
|
||||
line-height: 1.1;
|
||||
position: relative;
|
||||
|
||||
.respond-md({
|
||||
font-size: 52px;
|
||||
});
|
||||
}
|
||||
|
||||
.stat-plus {
|
||||
font-size: @font-size-xl;
|
||||
color: @color-primary-light;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
margin-top: @space-sm;
|
||||
font-size: @font-size-base;
|
||||
color: @color-text-secondary;
|
||||
}
|
||||
|
||||
.summary-divider {
|
||||
width: 1px;
|
||||
height: 60px;
|
||||
background: @color-primary-lighter;
|
||||
}
|
||||
|
||||
.summary-keywords {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.keywords-label {
|
||||
display: block;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.keywords-list {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: @space-sm;
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
padding: @space-xs @space-md;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-full;
|
||||
color: @color-primary;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
transition: all @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<article class="winter-card" :class="{ 'winter-card--popular': isPopular }">
|
||||
<!-- 头部 -->
|
||||
<div class="card-header">
|
||||
<div v-if="isPopular" class="popular-badge">推荐</div>
|
||||
<div class="card-duration">
|
||||
<span class="duration-num">{{ product.days }}</span>
|
||||
<span class="duration-unit">天</span>
|
||||
<span class="duration-num">{{ product.nights }}</span>
|
||||
<span class="duration-unit">晚</span>
|
||||
</div>
|
||||
<span class="card-tag">{{ product.tag }}</span>
|
||||
<span class="card-line">{{ product.line }} · {{ product.route }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 主体 -->
|
||||
<div class="card-body">
|
||||
<h3 class="card-name">{{ shortName }}</h3>
|
||||
<p class="card-audience">适合:{{ product.audience }}</p>
|
||||
<p class="card-desc">{{ product.description }}</p>
|
||||
<ul class="card-highlights">
|
||||
<li v-for="(h, i) in product.highlights" :key="i">
|
||||
<span class="check" aria-hidden="true">✓</span>
|
||||
{{ h }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 行程概览 -->
|
||||
<details class="card-itinerary">
|
||||
<summary class="itinerary-toggle">查看每日行程 ▾</summary>
|
||||
<ol class="itinerary-list">
|
||||
<li v-for="(day, i) in product.itinerary" :key="i">{{ day }}</li>
|
||||
</ol>
|
||||
</details>
|
||||
|
||||
<!-- 底部 CTA -->
|
||||
<NuxtLink to="/contact" class="card-cta">
|
||||
咨询该行程 <span aria-hidden="true">→</span>
|
||||
</NuxtLink>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({ product: { type: Object, required: true } })
|
||||
|
||||
const isPopular = computed(() => props.product.id === 'winter-south-6d5n')
|
||||
|
||||
const shortName = computed(() => {
|
||||
return props.product.name.replace(/^嗨冰雪V3·/, '')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.winter-card {
|
||||
background: @color-bg-white;
|
||||
border: 1px solid @color-border;
|
||||
border-radius: @border-radius-lg;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
transition: box-shadow @transition-base, transform @transition-base;
|
||||
|
||||
&:hover {
|
||||
box-shadow: @shadow-lg;
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
&--popular {
|
||||
border: 2px solid #3B82F6;
|
||||
box-shadow: @shadow-md;
|
||||
|
||||
.card-header {
|
||||
background: #3B82F6;
|
||||
}
|
||||
|
||||
.duration-num,
|
||||
.duration-unit,
|
||||
.card-tag,
|
||||
.card-line {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: @space-lg @space-lg @space-md;
|
||||
background: #EFF6FF;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popular-badge {
|
||||
position: absolute;
|
||||
top: @space-sm;
|
||||
right: @space-sm;
|
||||
padding: @space-xs @space-sm;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #fff;
|
||||
background: @color-accent;
|
||||
border-radius: @border-radius-sm;
|
||||
}
|
||||
|
||||
.card-duration {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
margin-bottom: @space-sm;
|
||||
}
|
||||
|
||||
.duration-num {
|
||||
font-size: @font-size-3xl;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #3B82F6;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.duration-unit {
|
||||
font-size: @font-size-sm;
|
||||
color: #3B82F6;
|
||||
font-weight: @font-weight-medium;
|
||||
}
|
||||
|
||||
.card-tag {
|
||||
display: inline-block;
|
||||
padding: @space-xs @space-md;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-medium;
|
||||
color: #3B82F6;
|
||||
background: @color-bg-white;
|
||||
border-radius: @border-radius-full;
|
||||
}
|
||||
|
||||
.card-line {
|
||||
display: block;
|
||||
font-size: @font-size-xs;
|
||||
color: #64748B;
|
||||
margin-top: @space-xs;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: @space-lg;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: @font-size-lg;
|
||||
font-weight: @font-weight-bold;
|
||||
color: @color-text-primary;
|
||||
margin-bottom: @space-xs;
|
||||
}
|
||||
|
||||
.card-audience {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
margin-bottom: @space-md;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
margin-bottom: @space-md;
|
||||
.text-clamp(3);
|
||||
}
|
||||
|
||||
.card-highlights {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
margin-top: auto;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: @space-sm;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
}
|
||||
}
|
||||
|
||||
.check {
|
||||
flex-shrink: 0;
|
||||
color: #3B82F6;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-bold;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// 行程概览
|
||||
.card-itinerary {
|
||||
border-top: 1px solid @color-border;
|
||||
}
|
||||
|
||||
.itinerary-toggle {
|
||||
display: block;
|
||||
padding: @space-md @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
color: @color-text-muted;
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: background @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: @color-bg-gray;
|
||||
color: @color-text-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.itinerary-list {
|
||||
padding: 0 @space-lg @space-md;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-xs;
|
||||
|
||||
li {
|
||||
font-size: @font-size-xs;
|
||||
color: @color-text-secondary;
|
||||
line-height: @line-height-base;
|
||||
padding-left: @space-sm;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部 CTA
|
||||
.card-cta {
|
||||
display: block;
|
||||
text-align: center;
|
||||
padding: @space-md @space-lg;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
color: #3B82F6;
|
||||
background: @color-bg-gray;
|
||||
text-decoration: none;
|
||||
transition: all @transition-fast;
|
||||
|
||||
&:hover {
|
||||
background: #3B82F6;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
在新工单中引用
屏蔽一个用户