feat: 官网代码全量更新 (2026-04-18)
这个提交包含在:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="知识产权" subtitle="商标全品类注册 + 4项国家版权登记" />
|
||||
<CommonSectionTitle :title="st('aboutPage.copyright', '知识产权')" :subtitle="st('aboutPage.copyright', '商标全品类注册 + 4项国家版权登记', 'subtitle')" />
|
||||
<p class="copyright-intro">呼籁集团重视品牌知识产权保护。「呼籁」商标已完成全品类注册,核心产品名称、品牌标识和IP形象均已在中国版权保护中心完成作品登记。</p>
|
||||
|
||||
<!-- 商标 -->
|
||||
@@ -48,6 +48,7 @@ defineProps({
|
||||
copyrights: { type: Array, required: true },
|
||||
trademark: { type: Object, default: null }
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="企业文化" />
|
||||
<CommonSectionTitle :title="st('aboutPage.cultureValues', '企业文化')" />
|
||||
<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">
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
<script setup>
|
||||
defineProps({ culture: { type: Object, required: true } })
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section section--warm">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="品牌IP · 籁小咩" subtitle="LAI XIAO MIE" />
|
||||
<CommonSectionTitle :title="st('aboutPage.mascot', '品牌IP · 籁小咩')" :subtitle="st('aboutPage.mascot', 'LAI XIAO MIE', 'subtitle')" />
|
||||
|
||||
<div class="mascot-hero">
|
||||
<div class="mascot-image">
|
||||
@@ -61,6 +61,7 @@
|
||||
defineProps({
|
||||
images: { type: Object, default: () => ({}) },
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<!-- 资质认证 -->
|
||||
<CommonSectionTitle title="资质认证" />
|
||||
<CommonSectionTitle :title="st('aboutPage.qualCert', '资质认证')" />
|
||||
<div class="qual-list">
|
||||
<div v-for="c in certifications" :key="c.title" class="qual-item qual-item--cert">
|
||||
<div class="qual-icon">🏆</div>
|
||||
@@ -16,15 +16,15 @@
|
||||
<!-- 小红书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" />
|
||||
<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;" />
|
||||
<CommonSectionTitle :title="st('aboutPage.qualService', '服务保障')" 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>
|
||||
@@ -43,6 +43,7 @@ defineProps({
|
||||
certifications: { type: Array, required: true },
|
||||
guarantees: { type: Array, required: true }
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section section--warm">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="我们的团队" />
|
||||
<CommonSectionTitle :title="st('aboutPage.team', '我们的团队')" />
|
||||
<p class="team-summary">{{ team.summary }}</p>
|
||||
|
||||
<!-- 团队照片墙 -->
|
||||
@@ -75,6 +75,7 @@ defineProps({
|
||||
team: { type: Object, required: true },
|
||||
images: { type: Object, default: () => ({}) },
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="小红书上的呼籁" subtitle="XIAOHONGSHU" />
|
||||
<CommonSectionTitle :title="st('aboutPage.xiaohongshu', '小红书上的呼籁')" :subtitle="st('aboutPage.xiaohongshu', 'XIAOHONGSHU', 'subtitle')" />
|
||||
|
||||
<div class="xhs-card">
|
||||
<!-- 顶部:品牌主页截图作为横幅 -->
|
||||
@@ -60,6 +60,7 @@ defineProps({
|
||||
xhs: { type: Object, required: true },
|
||||
images: { type: Object, default: () => ({}) },
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
<template>
|
||||
<div class="rich-editor">
|
||||
<div v-if="editor" class="rich-editor-toolbar">
|
||||
<button type="button" :class="btn(editor.isActive('heading', { level: 2 }))" @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" title="大标题">H2</button>
|
||||
<button type="button" :class="btn(editor.isActive('heading', { level: 3 }))" @click="editor.chain().focus().toggleHeading({ level: 3 }).run()" title="小标题">H3</button>
|
||||
<button type="button" :class="btn(editor.isActive('paragraph'))" @click="editor.chain().focus().setParagraph().run()" title="正文">¶</button>
|
||||
<span class="divider" />
|
||||
<button type="button" :class="btn(editor.isActive('bold'))" @click="editor.chain().focus().toggleBold().run()" title="加粗"><b>B</b></button>
|
||||
<button type="button" :class="btn(editor.isActive('italic'))" @click="editor.chain().focus().toggleItalic().run()" title="斜体"><i>I</i></button>
|
||||
<button type="button" :class="btn(editor.isActive('strike'))" @click="editor.chain().focus().toggleStrike().run()" title="删除线"><s>S</s></button>
|
||||
<span class="divider" />
|
||||
<button type="button" :class="btn(editor.isActive('bulletList'))" @click="editor.chain().focus().toggleBulletList().run()" title="无序列表">• 列表</button>
|
||||
<button type="button" :class="btn(editor.isActive('orderedList'))" @click="editor.chain().focus().toggleOrderedList().run()" title="有序列表">1. 列表</button>
|
||||
<button type="button" :class="btn(editor.isActive('blockquote'))" @click="editor.chain().focus().toggleBlockquote().run()" title="引用">" 引用</button>
|
||||
<span class="divider" />
|
||||
<button type="button" :class="btn(editor.isActive('link'))" @click="setLink" title="链接">🔗 链接</button>
|
||||
<button type="button" class="toolbar-btn" @click="addImage" title="插入图片">🖼 图片</button>
|
||||
<span class="divider" />
|
||||
<button type="button" class="toolbar-btn" @click="editor.chain().focus().undo().run()" title="撤销">↶</button>
|
||||
<button type="button" class="toolbar-btn" @click="editor.chain().focus().redo().run()" title="重做">↷</button>
|
||||
<span class="divider" />
|
||||
<button type="button" class="toolbar-btn" :class="{ active: showHtml }" @click="showHtml = !showHtml" title="源代码视图"></> HTML</button>
|
||||
</div>
|
||||
|
||||
<EditorContent v-show="!showHtml" :editor="editor" class="rich-editor-body" />
|
||||
<textarea
|
||||
v-show="showHtml"
|
||||
:value="modelValue"
|
||||
class="rich-editor-html"
|
||||
rows="20"
|
||||
@input="$emit('update:modelValue', $event.target.value); editor?.commands.setContent($event.target.value, false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { EditorContent, useEditor } from '@tiptap/vue-3'
|
||||
import StarterKit from '@tiptap/starter-kit'
|
||||
import Link from '@tiptap/extension-link'
|
||||
import Image from '@tiptap/extension-image'
|
||||
import { watch, ref, onBeforeUnmount } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '开始写作...' },
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const showHtml = ref(false)
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.modelValue,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Link.configure({ openOnClick: false, HTMLAttributes: { rel: 'noopener', target: '_blank' } }),
|
||||
Image,
|
||||
],
|
||||
onUpdate: ({ editor }) => {
|
||||
emit('update:modelValue', editor.getHTML())
|
||||
},
|
||||
})
|
||||
|
||||
watch(() => props.modelValue, (val) => {
|
||||
if (!editor.value) return
|
||||
const current = editor.value.getHTML()
|
||||
if (val !== current) editor.value.commands.setContent(val || '', false)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => editor.value?.destroy())
|
||||
|
||||
function btn(active) { return { 'toolbar-btn': true, active } }
|
||||
|
||||
function setLink() {
|
||||
const prev = editor.value.getAttributes('link').href
|
||||
const url = window.prompt('链接 URL', prev || 'https://')
|
||||
if (url === null) return
|
||||
if (url === '') { editor.value.chain().focus().extendMarkRange('link').unsetLink().run(); return }
|
||||
editor.value.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
||||
}
|
||||
|
||||
function addImage() {
|
||||
const url = window.prompt('图片 URL(可填 /uploads/ 或 /images/ 路径)')
|
||||
if (url) editor.value.chain().focus().setImage({ src: url }).run()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.rich-editor {
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.rich-editor-toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #fafafa;
|
||||
border-radius: 4px 4px 0 0;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 4px 10px;
|
||||
font-size: 13px;
|
||||
border: 1px solid #e5e7eb;
|
||||
background: #fff;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
color: #374151;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
.toolbar-btn:hover { background: #f3f4f6; border-color: #3a7d44; }
|
||||
.toolbar-btn.active { background: #3a7d44; color: #fff; border-color: #3a7d44; }
|
||||
|
||||
.divider {
|
||||
width: 1px;
|
||||
background: #e5e7eb;
|
||||
margin: 2px 4px;
|
||||
}
|
||||
|
||||
.rich-editor-body {
|
||||
padding: 12px 14px;
|
||||
min-height: 400px;
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: #111827;
|
||||
}
|
||||
.rich-editor-body :deep(.ProseMirror) { outline: none; min-height: 380px; }
|
||||
.rich-editor-body :deep(.ProseMirror:focus) { outline: none; }
|
||||
.rich-editor-body :deep(h2) { font-size: 20px; font-weight: 600; margin: 18px 0 10px; color: #111827; }
|
||||
.rich-editor-body :deep(h3) { font-size: 16px; font-weight: 600; margin: 14px 0 8px; color: #111827; }
|
||||
.rich-editor-body :deep(p) { margin: 8px 0; }
|
||||
.rich-editor-body :deep(ul), .rich-editor-body :deep(ol) { padding-left: 24px; margin: 8px 0; }
|
||||
.rich-editor-body :deep(li) { margin: 4px 0; }
|
||||
.rich-editor-body :deep(blockquote) {
|
||||
border-left: 3px solid #3a7d44;
|
||||
padding-left: 12px;
|
||||
color: #6b7280;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.rich-editor-body :deep(a) { color: #3a7d44; text-decoration: underline; }
|
||||
.rich-editor-body :deep(img) { max-width: 100%; height: auto; border-radius: 4px; }
|
||||
.rich-editor-body :deep(strong) { font-weight: 600; }
|
||||
|
||||
.rich-editor-html {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: none;
|
||||
outline: none;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
resize: vertical;
|
||||
min-height: 400px;
|
||||
background: #1f2937;
|
||||
color: #d1fae5;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -21,17 +21,54 @@
|
||||
<ul class="card-highlights">
|
||||
<li v-for="(h, i) in product.highlights" :key="i">
|
||||
<span class="check" aria-hidden="true">✓</span>
|
||||
{{ h }}
|
||||
{{ itineraryText(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>
|
||||
<!-- 价格区间 -->
|
||||
<div v-if="priceRange" class="card-price">
|
||||
<div class="price-main">
|
||||
<span class="price-label">成人价</span>
|
||||
<span class="price-value">¥{{ priceRange.min }}</span>
|
||||
<span v-if="priceRange.max > priceRange.min" class="price-range">
|
||||
起 · 旺季 ¥{{ priceRange.max }}
|
||||
</span>
|
||||
<span v-else class="price-range"> 起</span>
|
||||
</div>
|
||||
<div v-if="priceRange.childMin" class="price-sub">
|
||||
儿童 ¥{{ priceRange.childMin }} 起
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 行程 + 报价展开 -->
|
||||
<details v-if="hasItinerary || hasPricing" class="card-itinerary">
|
||||
<summary class="itinerary-toggle">
|
||||
<span v-if="hasItinerary && hasPricing">查看每日行程 + 分日期报价 ▾</span>
|
||||
<span v-else-if="hasItinerary">查看每日行程 ▾</span>
|
||||
<span v-else>查看分日期报价 ▾</span>
|
||||
</summary>
|
||||
<div v-if="hasItinerary" class="itinerary-section">
|
||||
<div class="section-head">每日行程</div>
|
||||
<ol class="itinerary-list">
|
||||
<li v-for="(day, i) in product.itinerary" :key="i">{{ itineraryText(day) }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div v-if="hasPricing" class="pricing-section">
|
||||
<div class="section-head">分日期报价</div>
|
||||
<div class="pricing-table">
|
||||
<div class="pricing-row pricing-head">
|
||||
<span class="col-date">出行日期</span>
|
||||
<span class="col-price">成人价</span>
|
||||
<span class="col-price">儿童价</span>
|
||||
</div>
|
||||
<div v-for="(row, i) in product.pricing" :key="i" class="pricing-row">
|
||||
<span class="col-date">{{ row.date }}</span>
|
||||
<span class="col-price">¥{{ row.adult }}</span>
|
||||
<span class="col-price">¥{{ row.child }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- 底部 CTA -->
|
||||
@@ -49,6 +86,27 @@ const isPopular = computed(() => props.product.id === 'autumn-south-5d4n')
|
||||
const shortName = computed(() => {
|
||||
return props.product.name.replace(/^游牧的森林V3·/, '')
|
||||
})
|
||||
|
||||
const hasItinerary = computed(() => Array.isArray(props.product.itinerary) && props.product.itinerary.length > 0)
|
||||
const hasPricing = computed(() => Array.isArray(props.product.pricing) && props.product.pricing.length > 0)
|
||||
|
||||
const priceRange = computed(() => {
|
||||
if (!hasPricing.value) return null
|
||||
const adults = props.product.pricing.map(p => Number(p.adult)).filter(n => n > 0)
|
||||
const children = props.product.pricing.map(p => Number(p.child)).filter(n => n > 0)
|
||||
if (!adults.length) return null
|
||||
return {
|
||||
min: Math.min(...adults),
|
||||
max: Math.max(...adults),
|
||||
childMin: children.length ? Math.min(...children) : null,
|
||||
}
|
||||
})
|
||||
|
||||
function itineraryText(item) {
|
||||
if (typeof item === 'string') return item
|
||||
if (item && typeof item === 'object') return item.title || ''
|
||||
return ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -196,7 +254,47 @@ const shortName = computed(() => {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
// 行程概览
|
||||
// 价格区间
|
||||
.card-price {
|
||||
padding: @space-md @space-lg;
|
||||
background: linear-gradient(135deg, #FFF7ED 0%, #FEF3C7 100%);
|
||||
border-top: 1px solid rgba(217, 119, 6, 0.15);
|
||||
border-bottom: 1px solid rgba(217, 119, 6, 0.15);
|
||||
}
|
||||
|
||||
.price-main {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.price-label {
|
||||
font-size: @font-size-xs;
|
||||
color: #92400E;
|
||||
margin-right: @space-xs;
|
||||
}
|
||||
|
||||
.price-value {
|
||||
font-size: @font-size-2xl;
|
||||
font-weight: @font-weight-bold;
|
||||
color: #B45309;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.price-range {
|
||||
font-size: @font-size-xs;
|
||||
color: #92400E;
|
||||
}
|
||||
|
||||
.price-sub {
|
||||
margin-top: 4px;
|
||||
font-size: @font-size-xs;
|
||||
color: #92400E;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
// 行程概览 + 分日期报价
|
||||
.card-itinerary {
|
||||
border-top: 1px solid @color-border;
|
||||
}
|
||||
@@ -209,6 +307,9 @@ const shortName = computed(() => {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
transition: background @transition-fast;
|
||||
list-style: none;
|
||||
|
||||
&::-webkit-details-marker { display: none; }
|
||||
|
||||
&:hover {
|
||||
background: @color-bg-gray;
|
||||
@@ -216,6 +317,20 @@ const shortName = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.section-head {
|
||||
padding: 0 @space-lg @space-xs;
|
||||
font-size: @font-size-xs;
|
||||
font-weight: @font-weight-medium;
|
||||
color: #D97706;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.itinerary-section + .pricing-section {
|
||||
margin-top: @space-md;
|
||||
padding-top: @space-md;
|
||||
border-top: 1px dashed @color-border;
|
||||
}
|
||||
|
||||
.itinerary-list {
|
||||
padding: 0 @space-lg @space-md;
|
||||
display: flex;
|
||||
@@ -230,6 +345,35 @@ const shortName = computed(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.pricing-table {
|
||||
padding: 0 @space-lg @space-md;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: @font-size-xs;
|
||||
}
|
||||
|
||||
.pricing-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1.6fr 1fr 1fr;
|
||||
gap: @space-sm;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.06);
|
||||
color: @color-text-secondary;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
|
||||
.col-date { color: @color-text-primary; }
|
||||
.col-price { text-align: right; color: #B45309; font-weight: @font-weight-medium; font-variant-numeric: tabular-nums; }
|
||||
}
|
||||
|
||||
.pricing-head {
|
||||
font-weight: @font-weight-medium;
|
||||
color: @color-text-muted;
|
||||
border-bottom: 1px solid @color-border;
|
||||
|
||||
.col-date, .col-price { color: @color-text-muted; font-weight: @font-weight-medium; }
|
||||
}
|
||||
|
||||
// 底部 CTA
|
||||
.card-cta {
|
||||
display: block;
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div v-if="visible" class="floating-contact-wrap">
|
||||
<!-- 折叠态:小圆点,点击展开 -->
|
||||
<button
|
||||
v-if="collapsed"
|
||||
type="button"
|
||||
class="floating-contact floating-contact--mini"
|
||||
aria-label="打开咨询客服"
|
||||
@click="expand"
|
||||
>
|
||||
<span class="fc-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<!-- 展开态:胶囊按钮 + 关闭小钮 -->
|
||||
<NuxtLink
|
||||
v-else
|
||||
:to="target"
|
||||
class="floating-contact"
|
||||
:aria-label="ariaLabel"
|
||||
@click="onClick"
|
||||
>
|
||||
<span class="fc-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span class="fc-label">{{ label }}</span>
|
||||
<span class="fc-pulse" aria-hidden="true"></span>
|
||||
</NuxtLink>
|
||||
<button
|
||||
v-if="!collapsed"
|
||||
type="button"
|
||||
class="floating-contact-close"
|
||||
aria-label="收起咨询客服"
|
||||
@click.stop="collapse"
|
||||
>
|
||||
<svg viewBox="0 0 16 16" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
||||
<path d="M4 4l8 8M12 4l-8 8" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const route = useRoute()
|
||||
const { data: brand } = await usePublicApi('brand')
|
||||
|
||||
const STORAGE_KEY = 'hulai:floatingContact:collapsed'
|
||||
const collapsed = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
try {
|
||||
collapsed.value = sessionStorage.getItem(STORAGE_KEY) === '1'
|
||||
} catch {}
|
||||
})
|
||||
|
||||
function collapse() {
|
||||
collapsed.value = true
|
||||
try { sessionStorage.setItem(STORAGE_KEY, '1') } catch {}
|
||||
track('collapse')
|
||||
}
|
||||
function expand() {
|
||||
collapsed.value = false
|
||||
try { sessionStorage.removeItem(STORAGE_KEY) } catch {}
|
||||
track('expand')
|
||||
}
|
||||
|
||||
function track(action) {
|
||||
try {
|
||||
if (typeof window !== 'undefined' && typeof window.gtag === 'function') {
|
||||
window.gtag('event', 'floating_contact_' + action, { page_path: route.path })
|
||||
}
|
||||
if (typeof window !== 'undefined' && window._hmt && Array.isArray(window._hmt)) {
|
||||
window._hmt.push(['_trackEvent', 'FloatingContact', action, route.path])
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('floating-contact', { detail: { action, path: route.path } }))
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function onClick() {
|
||||
track('click')
|
||||
}
|
||||
|
||||
// 后台可配:brand.floatingCTA = { enabled, text, to, hideOnPaths }
|
||||
const config = computed(() => brand.value?.floatingCTA || {})
|
||||
const label = computed(() => config.value.text || '咨询客服')
|
||||
const target = computed(() => config.value.to || '/contact')
|
||||
const ariaLabel = computed(() => label.value)
|
||||
const hideOnPaths = computed(() => {
|
||||
const p = config.value.hideOnPaths
|
||||
if (Array.isArray(p) && p.length) return p
|
||||
return ['/contact']
|
||||
})
|
||||
|
||||
const visible = computed(() => {
|
||||
if (config.value.enabled === false) return false
|
||||
const cur = route.path
|
||||
return !hideOnPaths.value.some(p => cur === p || (p.endsWith('/') ? cur.startsWith(p) : cur.startsWith(p + '/')) || cur === p)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.floating-contact-wrap {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 24px;
|
||||
z-index: 90;
|
||||
|
||||
@media (max-width: 767px) {
|
||||
right: 16px;
|
||||
bottom: calc(20px + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
.respond-md({
|
||||
bottom: calc(28px + env(safe-area-inset-bottom, 0px));
|
||||
right: 28px;
|
||||
});
|
||||
}
|
||||
|
||||
.floating-contact {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 0 18px 0 14px;
|
||||
height: 52px;
|
||||
border-radius: 999px;
|
||||
|
||||
background: linear-gradient(135deg, @color-primary 0%, @color-primary-light 100%);
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
letter-spacing: 0.5px;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
box-shadow:
|
||||
0 10px 24px -6px rgba(45, 106, 79, 0.45),
|
||||
0 4px 10px -2px rgba(0, 0, 0, 0.15);
|
||||
|
||||
transition: transform 0.25s ease, box-shadow 0.25s ease, background 0.25s ease;
|
||||
will-change: transform;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow:
|
||||
0 14px 30px -6px rgba(45, 106, 79, 0.55),
|
||||
0 6px 14px -2px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&--mini {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
opacity: 0.85;
|
||||
|
||||
&:hover { opacity: 1; }
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
padding: 0;
|
||||
justify-content: center;
|
||||
|
||||
.fc-label { display: none; }
|
||||
|
||||
&--mini {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.floating-contact-close {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
color: #374151;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
|
||||
transition: background 0.2s ease, transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #f3f4f6;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
svg { display: block; }
|
||||
}
|
||||
|
||||
.fc-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.fc-label {
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.fc-pulse {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 0 0 0 rgba(64, 145, 108, 0.55);
|
||||
animation: fc-pulse 2.4s ease-out infinite;
|
||||
}
|
||||
|
||||
@keyframes fc-pulse {
|
||||
0% { box-shadow: 0 0 0 0 rgba(64, 145, 108, 0.5); }
|
||||
70% { box-shadow: 0 0 0 14px rgba(64, 145, 108, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(64, 145, 108, 0); }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.fc-pulse { animation: none; }
|
||||
.floating-contact { transition: none; }
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="wechat-cta-qr">
|
||||
<img src="/images/contact/qrcode-wechat.jpg" alt="定制师微信二维码" class="wechat-cta-qrimg" />
|
||||
<img :src="qrSrc" alt="定制师微信二维码" class="wechat-cta-qrimg" />
|
||||
<span class="wechat-cta-qrlabel">扫码添加定制师微信</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -32,6 +32,7 @@ defineProps({
|
||||
links: { type: Array, default: () => [] },
|
||||
bg: { type: String, default: 'light' },
|
||||
})
|
||||
const qrSrc = '/images/contact/qrcode-wechat.jpg'
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section section--warm">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="为什么选择呼籁" label="WHY HULAI" subtitle="不只是包车,是一套成熟的旅行服务体系" />
|
||||
<CommonSectionTitle :title="st('home.differentiator', '为什么选择呼籁')" :label="st('home.differentiator', 'WHY HULAI', 'label')" :subtitle="st('home.differentiator', '不只是包车,是一套成熟的旅行服务体系', 'subtitle')" />
|
||||
<div class="diff-grid">
|
||||
<div v-for="(item, index) in itemsWithBg" :key="item.title" class="diff-item">
|
||||
<div class="diff-bg">
|
||||
@@ -23,6 +23,8 @@ const props = defineProps({
|
||||
items: { type: Array, required: true },
|
||||
})
|
||||
|
||||
const st = await useSectionTitles()
|
||||
|
||||
const bgImages = [
|
||||
'/images/seasons/summer-hero.jpg',
|
||||
'/images/seasons/summer-car.jpg',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="旅行产品" subtitle="四季不同风景,总有一条线路适合你的家庭" />
|
||||
<CommonSectionTitle :title="st('home.productPreview', '旅行产品')" :subtitle="st('home.productPreview', '四季不同风景,总有一条线路适合你的家庭', 'subtitle')" />
|
||||
<div class="product-grid">
|
||||
<NuxtLink
|
||||
v-for="line in productLines"
|
||||
@@ -36,6 +36,8 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const st = await useSectionTitles()
|
||||
|
||||
const productLines = [
|
||||
{
|
||||
id: 'summer',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section section--gray">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="客户怎么说" label="REVIEWS" subtitle="来自真实家庭的旅行评价" />
|
||||
<CommonSectionTitle :title="st('home.reviewHighlights', '客户怎么说')" :label="st('home.reviewHighlights', 'REVIEWS', 'label')" :subtitle="st('home.reviewHighlights', '来自真实家庭的旅行评价', 'subtitle')" />
|
||||
<div class="review-grid">
|
||||
<div v-for="r in reviews" :key="r.id" class="review-card">
|
||||
<div class="review-quote-mark">"</div>
|
||||
@@ -27,6 +27,7 @@
|
||||
defineProps({
|
||||
reviews: { type: Array, required: true },
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
+109
-33
@@ -4,22 +4,15 @@
|
||||
<div v-if="brand && images" class="footer-grid">
|
||||
<!-- 品牌信息 -->
|
||||
<div class="footer-brand">
|
||||
<div class="footer-logo">
|
||||
<img v-if="images?.logo?.main" :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 class="footer-brand-main">
|
||||
<div class="footer-logo">
|
||||
<img v-if="images?.logo?.main" :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>
|
||||
</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"
|
||||
/>
|
||||
<p class="footer-desc">{{ brand?.slogan?.functional }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -32,11 +25,22 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 二维码区(桌面端独立列,手机端嵌入品牌区下方) -->
|
||||
<div v-if="qrChannels.length" class="footer-qr-list">
|
||||
<CommonQRCodeBlock
|
||||
v-for="ch in qrChannels"
|
||||
:key="ch.type"
|
||||
:image="ch.qrImage"
|
||||
:label="ch.label"
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<div class="footer-bottom">
|
||||
<p class="footer-company">旅行服务:内蒙古呼籁国际旅行社有限公司</p>
|
||||
<p v-if="brand?.serviceEntity" class="footer-company">旅行服务:{{ brand.serviceEntity }}</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>
|
||||
@@ -84,19 +88,72 @@ const qrChannels = computed(() => (contact.value?.channels || []).filter(c => c.
|
||||
|
||||
.footer-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: @space-xl;
|
||||
// 手机端:品牌区占满一行,三个导航组 3 列并排,二维码单独一行
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-template-areas:
|
||||
'brand brand brand'
|
||||
'nav1 nav2 nav3'
|
||||
'qr qr qr';
|
||||
gap: @space-lg @space-md;
|
||||
|
||||
.respond-md({
|
||||
grid-template-columns: 2fr 1fr 1fr 1fr 1fr;
|
||||
gap: @space-xl;
|
||||
// 桌面端:品牌 | 3 列导航 | 二维码,布局紧凑
|
||||
grid-template-columns: 1.3fr 1fr 1fr 1fr auto;
|
||||
grid-template-areas: 'brand nav1 nav2 nav3 qr';
|
||||
gap: @space-2xl;
|
||||
align-items: start;
|
||||
});
|
||||
}
|
||||
|
||||
.footer-qr-list {
|
||||
grid-area: qr;
|
||||
display: flex;
|
||||
gap: @space-md;
|
||||
flex-shrink: 0;
|
||||
|
||||
// 手机端二维码缩到 64px
|
||||
@media (max-width: 767px) {
|
||||
:deep(.qr-image) {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
}
|
||||
|
||||
.respond-md({
|
||||
gap: @space-lg;
|
||||
});
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
margin-bottom: @space-lg;
|
||||
grid-area: brand;
|
||||
margin-bottom: 0;
|
||||
// 手机端:logo + 文字 + 二维码横向排列,节省高度
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: @space-md;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.respond-md({
|
||||
// 桌面端:品牌信息紧凑堆叠(logo + slogan),二维码移到右侧独立列
|
||||
display: block;
|
||||
margin-bottom: 0;
|
||||
});
|
||||
}
|
||||
|
||||
.footer-brand-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.respond-md({
|
||||
flex: none;
|
||||
width: 100%;
|
||||
});
|
||||
}
|
||||
|
||||
.footer-nav-group:nth-child(2) { grid-area: nav1; }
|
||||
.footer-nav-group:nth-child(3) { grid-area: nav2; }
|
||||
.footer-nav-group:nth-child(4) { grid-area: nav3; }
|
||||
|
||||
.footer-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -105,10 +162,15 @@ const qrChannels = computed(() => (contact.value?.channels || []).filter(c => c.
|
||||
}
|
||||
|
||||
.footer-logo-img {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.respond-md({
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
});
|
||||
}
|
||||
|
||||
.footer-logo-text {
|
||||
@@ -129,35 +191,49 @@ const qrChannels = computed(() => (contact.value?.channels || []).filter(c => c.
|
||||
}
|
||||
|
||||
.footer-desc {
|
||||
font-size: @font-size-sm;
|
||||
margin-bottom: @space-lg;
|
||||
font-size: 12px;
|
||||
margin-bottom: 0;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
line-height: 1.6;
|
||||
|
||||
.footer-qr-list {
|
||||
display: flex;
|
||||
gap: @space-lg;
|
||||
.respond-md({
|
||||
font-size: @font-size-sm;
|
||||
margin-bottom: 0;
|
||||
});
|
||||
}
|
||||
|
||||
.footer-nav-title {
|
||||
font-size: @font-size-base;
|
||||
font-size: @font-size-sm;
|
||||
font-weight: @font-weight-medium;
|
||||
color: #fff;
|
||||
margin-bottom: @space-md;
|
||||
margin-bottom: @space-sm;
|
||||
|
||||
.respond-md({
|
||||
font-size: @font-size-base;
|
||||
margin-bottom: @space-md;
|
||||
});
|
||||
}
|
||||
|
||||
.footer-nav-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: @space-sm;
|
||||
gap: 6px;
|
||||
|
||||
.respond-md({
|
||||
gap: @space-sm;
|
||||
});
|
||||
}
|
||||
|
||||
.footer-nav-link {
|
||||
font-size: @font-size-sm;
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-decoration: none;
|
||||
transition: color @transition-fast;
|
||||
|
||||
.respond-md({
|
||||
font-size: @font-size-sm;
|
||||
});
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
+16
-12
@@ -175,9 +175,10 @@ watch(() => route.path, () => {
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
opacity: 0.5;
|
||||
font-size: 13px;
|
||||
margin-left: 4px;
|
||||
opacity: 0.85;
|
||||
color: @color-primary;
|
||||
transition: transform @transition-fast;
|
||||
|
||||
.has-dropdown:hover & {
|
||||
@@ -256,9 +257,10 @@ watch(() => route.path, () => {
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
opacity: 0.5;
|
||||
font-size: 13px;
|
||||
margin-left: 4px;
|
||||
opacity: 0.85;
|
||||
color: @color-primary;
|
||||
transition: transform @transition-fast;
|
||||
|
||||
.has-dropdown:hover & {
|
||||
@@ -337,9 +339,10 @@ watch(() => route.path, () => {
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
opacity: 0.5;
|
||||
font-size: 13px;
|
||||
margin-left: 4px;
|
||||
opacity: 0.85;
|
||||
color: @color-primary;
|
||||
transition: transform @transition-fast;
|
||||
|
||||
.has-dropdown:hover & {
|
||||
@@ -418,9 +421,10 @@ watch(() => route.path, () => {
|
||||
}
|
||||
|
||||
.nav-arrow {
|
||||
font-size: 10px;
|
||||
margin-left: 2px;
|
||||
opacity: 0.5;
|
||||
font-size: 13px;
|
||||
margin-left: 4px;
|
||||
opacity: 0.85;
|
||||
color: @color-primary;
|
||||
transition: transform @transition-fast;
|
||||
|
||||
.has-dropdown:hover & {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section section--warm">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="产品进化" subtitle="从V1到V9,每一次更新都是客户反馈驱动" />
|
||||
<CommonSectionTitle :title="st('products.evolutionStats', '产品进化')" :subtitle="st('products.evolutionStats', '从V1到V9,每一次更新都是客户反馈驱动', 'subtitle')" />
|
||||
<div class="evo-stats">
|
||||
<div class="evo-stat">
|
||||
<span class="evo-stat-num">{{ stats.iterations }}</span>
|
||||
@@ -25,6 +25,7 @@
|
||||
defineProps({
|
||||
stats: { type: Object, required: true }
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="关于价格" subtitle="透明、清晰、不套路" />
|
||||
<CommonSectionTitle :title="st('products.pricingPhilosophy', '关于价格')" :subtitle="st('products.pricingPhilosophy', '透明、清晰、不套路', 'subtitle')" />
|
||||
<div class="pricing-content">
|
||||
<p>{{ content }}</p>
|
||||
</div>
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
<script setup>
|
||||
defineProps({ content: { type: String, required: true } })
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section id="guide" class="section section--gray">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="不知道选哪个?" subtitle="按您的情况推荐" />
|
||||
<CommonSectionTitle :title="st('products.selectionGuide', '不知道选哪个?')" :subtitle="st('products.selectionGuide', '按您的情况推荐', '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>
|
||||
@@ -26,6 +26,7 @@ const props = defineProps({
|
||||
guide: { type: Object, required: true },
|
||||
versions: { type: Array, required: true },
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
|
||||
const guideGroups = computed(() => [
|
||||
{ title: '按假期长度', items: props.guide.byVacationLength },
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="camp-hero">
|
||||
<div class="camp-mascot">
|
||||
<img
|
||||
:src="images.mascot.xiaomengma"
|
||||
:src="mascotSrc"
|
||||
alt="小蒙马 - 小蒙马亲子游学营团宠,一只可爱的蒙古小马形象"
|
||||
width="200"
|
||||
height="200"
|
||||
@@ -33,8 +33,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import images from '~/data/images.json'
|
||||
defineProps({ camp: { type: Object, required: true } })
|
||||
|
||||
// 从 API 动态读取图片配置(admin 编辑后即时同步)
|
||||
const { data: images } = await usePublicApi('images')
|
||||
const mascotSrc = computed(() => images.value?.mascot?.xiaomengma || '/images/mascot/xiaomengma.png')
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="V8 → V9 升级对比" subtitle="7项关键升级一目了然" />
|
||||
<CommonSectionTitle :title="st('products.compareV8V9', 'V8 → V9 升级对比')" :subtitle="st('products.compareV8V9', '7项关键升级一目了然', 'subtitle')" />
|
||||
<div class="compare-wrapper">
|
||||
<div class="compare-table">
|
||||
<div class="compare-head">
|
||||
@@ -22,6 +22,7 @@
|
||||
defineProps({
|
||||
compare: { type: Object, required: true }
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section section--gray">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="更多V9亮点" subtitle="独家资源 · 自营服务 · 全程无忧" />
|
||||
<CommonSectionTitle :title="st('products.highlights', '更多V9亮点')" :subtitle="st('products.highlights', '专属资源 · 自营服务 · 全程无忧', 'subtitle')" />
|
||||
<div class="highlights-list">
|
||||
<div v-for="item in highlights" :key="item.text" class="highlight-item">
|
||||
<span class="highlight-label">{{ item.label }}</span>
|
||||
@@ -16,6 +16,7 @@
|
||||
defineProps({
|
||||
highlights: { type: Array, required: true }
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="9次迭代背后的产品哲学" subtitle="不是做加法,而是做取舍" />
|
||||
<CommonSectionTitle :title="st('products.philosophy', '9次迭代背后的产品哲学')" :subtitle="st('products.philosophy', '不是做加法,而是做取舍', 'subtitle')" />
|
||||
<div class="philosophy-wrapper">
|
||||
<div class="philosophy-list">
|
||||
<div v-for="item in philosophy" :key="item.label" class="philosophy-item">
|
||||
@@ -24,6 +24,7 @@ defineProps({
|
||||
philosophy: { type: Array, required: true },
|
||||
quote: { type: Object, required: true }
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<section class="section">
|
||||
<div class="container">
|
||||
<CommonSectionTitle title="2026年核心升级" subtitle="基于客户反馈的三大重磅升级" />
|
||||
<CommonSectionTitle :title="st('products.upgrades', '2026年核心升级')" :subtitle="st('products.upgrades', '基于客户反馈的三大重磅升级', 'subtitle')" />
|
||||
<div class="upgrades-list">
|
||||
<div v-for="item in upgrades" :key="item.name" class="upgrade-item">
|
||||
<div class="upgrade-header">
|
||||
@@ -20,6 +20,7 @@
|
||||
defineProps({
|
||||
upgrades: { type: Array, required: true }
|
||||
})
|
||||
const st = await useSectionTitles()
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
在新工单中引用
屏蔽一个用户