- 后台全面引入 Naive UI 组件库,统一 UI 规范 - 前台 usePublicApi 切换到 API 调用(GET /api/content/[key]) - 前后台数据源统一(site_content 表) - 产品线统一管理(tour/camp/course 三套编辑器) - 产品数据归一化(itinerary/faq/pricing 格式统一) - 表单提交 API 改写入 submissions 表 - 新增 submissions 标记已读 API - site-content PUT 支持 upsert - 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb) - 消除 PageHero 类型警告(useSEO reactive 修复) - 25 个前台页面全部 HTTP 200,展示效果不变 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
447 行
15 KiB
Vue
447 行
15 KiB
Vue
<template>
|
||
<div v-if="contact" class="page-contact">
|
||
<LayoutPageHero
|
||
:title="seo.h1"
|
||
subtitle="选择最方便的方式联系我们"
|
||
:breadcrumbs="[{ text: '联系我们' }]"
|
||
background-image="/images/summer-camp/grassland-group.jpg"
|
||
/>
|
||
|
||
<!-- TL;DR -->
|
||
<section class="page-tldr" aria-label="联系我们摘要">
|
||
<div class="container">
|
||
<div class="page-tldr-inner">
|
||
<p>联系呼籁旅行:微信扫码加客服,或拨打电话咨询。定制师工作日2小时内回复,节假日24小时内回复。预订旺季行程(7月)建议提前2-3个月联系。</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 主要联系方式:微信 + 电话 -->
|
||
<section class="section reveal" aria-label="主要联系方式">
|
||
<div class="container">
|
||
<CommonSectionTitle title="立即联系" subtitle="微信或电话,开启你的呼伦贝尔之旅" />
|
||
<div class="contact-primary">
|
||
<!-- 微信客服 -->
|
||
<div class="contact-card contact-card--wechat">
|
||
<div class="contact-card__badge">推荐方式</div>
|
||
<div class="contact-card__icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="32" height="32">
|
||
<path d="M8.5 10.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM14.5 10.5a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z" fill="currentColor" stroke="none"/>
|
||
<path d="M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 0 1-4.255-.96L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8Z"/>
|
||
</svg>
|
||
</div>
|
||
<h2 class="contact-card__title">微信客服</h2>
|
||
<p class="contact-card__desc">添加微信,定制师一对一沟通行程需求</p>
|
||
<CommonQRCodeBlock
|
||
v-if="wechatChannel"
|
||
:image="wechatChannel.qrImage"
|
||
label="扫码添加微信"
|
||
size="large"
|
||
/>
|
||
</div>
|
||
|
||
<!-- 电话咨询 -->
|
||
<div class="contact-card contact-card--phone" v-if="phoneChannel">
|
||
<div class="contact-card__icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="32" height="32">
|
||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92Z"/>
|
||
</svg>
|
||
</div>
|
||
<h2 class="contact-card__title">电话咨询</h2>
|
||
<p class="contact-card__phone-number">{{ phoneChannel.value }}</p>
|
||
<p class="contact-card__desc">{{ phoneChannel.description }}</p>
|
||
<a :href="`tel:${phoneChannel.value}`" class="contact-card__action">
|
||
拨打电话
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 其他联系方式 -->
|
||
<section class="section section--gray reveal" aria-label="更多联系方式">
|
||
<div class="container">
|
||
<CommonSectionTitle title="更多联系方式" subtitle="商务合作、投诉建议、加入团队" />
|
||
<div class="contact-grid">
|
||
<div v-for="ch in secondaryChannels" :key="ch.type" class="contact-grid-item">
|
||
<div class="contact-grid-item__icon">
|
||
<svg v-if="ch.type === 'address'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="24" height="24"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0Z"/><circle cx="12" cy="10" r="3"/></svg>
|
||
<svg v-else-if="ch.type === 'email'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="24" height="24"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
|
||
<svg v-else-if="ch.type === 'complaint'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="24" height="24"><path d="M7.9 20A9 9 0 1 0 4 16.1L2 22Z"/><path d="M12 8v4M12 16h.01"/></svg>
|
||
<svg v-else-if="ch.type === 'hr'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="24" height="24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg>
|
||
<svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="24" height="24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4M12 8h.01"/></svg>
|
||
</div>
|
||
<div class="contact-grid-item__content">
|
||
<h3 class="contact-grid-item__label">{{ ch.label }}</h3>
|
||
<p class="contact-grid-item__value">{{ ch.value }}</p>
|
||
<p v-if="ch.description" class="contact-grid-item__desc">{{ ch.description }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 安全提示 -->
|
||
<section class="section reveal" aria-label="安全提示">
|
||
<div class="container">
|
||
<div class="contact-security">
|
||
<div class="contact-security__icon">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" width="28" height="28"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10Z"/><path d="m9 12 2 2 4-4"/></svg>
|
||
</div>
|
||
<div class="contact-security__content">
|
||
<h3 class="contact-security__title">官方渠道保障</h3>
|
||
<p class="contact-security__text">{{ contact.securityNotice }}</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 长辈定制提示 -->
|
||
<section class="section section--warm reveal" aria-label="长辈定制">
|
||
<div class="container">
|
||
<div class="contact-elder">
|
||
<div class="contact-elder__badge">牵手草原</div>
|
||
<h3 class="contact-elder__title">给爸妈安排一趟草原之旅</h3>
|
||
<p class="contact-elder__desc">联系定制师时告知「牵手草原」长辈行程,我们会根据长辈的身体状况和偏好,定制节奏更缓、住宿更舒适的专属方案。也支持爸妈和朋友一起组团出发。</p>
|
||
<NuxtLink to="/contact" class="contact-elder__action" @click.prevent="scrollToTop">
|
||
立即咨询长辈行程
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="16" height="16"><path d="M5 12h14M12 5l7 7-7 7"/></svg>
|
||
</NuxtLink>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 内链闭环 -->
|
||
<CommonSectionCTA
|
||
title="还有疑问?"
|
||
description="查看常见问题的详细解答"
|
||
:links="[{ to: '/faq', text: '查看呼伦贝尔亲子游常见问答' }]"
|
||
bg="gray"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { useOrganizationSchema, useBreadcrumbSchema, useContactPageSchema } from '~/composables/useJsonLd'
|
||
|
||
const { data: contact } = await usePublicApi('contact')
|
||
const { data: brand } = await usePublicApi('brand')
|
||
|
||
const visibleChannels = computed(() => (contact.value?.channels || []).filter(ch => !ch.hidden))
|
||
const wechatChannel = computed(() => visibleChannels.value.find(ch => ch.type === 'wechat'))
|
||
const phoneChannel = computed(() => visibleChannels.value.find(ch => ch.type === 'phone'))
|
||
const secondaryChannels = computed(() => visibleChannels.value.filter(ch => ch.type !== 'wechat' && ch.type !== 'phone'))
|
||
|
||
const seo = useSEO('contact')
|
||
useScrollReveal()
|
||
useOrganizationSchema(brand.value, contact.value)
|
||
useContactPageSchema(brand.value, contact.value)
|
||
useBreadcrumbSchema([
|
||
{ text: '首页', to: '/' },
|
||
{ text: '联系我们' },
|
||
])
|
||
|
||
function scrollToTop() {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||
}
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
// ===== 主要联系方式 =====
|
||
.contact-primary {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: @space-xl;
|
||
max-width: 800px;
|
||
margin: @space-xl auto 0;
|
||
|
||
.respond-md({
|
||
grid-template-columns: repeat(2, 1fr);
|
||
});
|
||
}
|
||
|
||
.contact-card {
|
||
position: relative;
|
||
text-align: center;
|
||
padding: @space-2xl @space-xl;
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-xl;
|
||
box-shadow: @shadow-xs;
|
||
transition: box-shadow @transition-base, transform @transition-base;
|
||
|
||
&:hover {
|
||
box-shadow: @shadow-lg;
|
||
transform: translateY(-4px);
|
||
border-color: @color-primary-lighter;
|
||
}
|
||
|
||
&__badge {
|
||
position: absolute;
|
||
top: -12px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
padding: @space-xs @space-lg;
|
||
font-size: @font-size-xs;
|
||
font-weight: @font-weight-bold;
|
||
color: #fff;
|
||
background: linear-gradient(135deg, @color-primary-dark, @color-primary);
|
||
border-radius: @border-radius-full;
|
||
letter-spacing: 2px;
|
||
white-space: nowrap;
|
||
box-shadow: 0 2px 8px rgba(45, 106, 79, 0.3);
|
||
}
|
||
|
||
&__icon {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 64px;
|
||
height: 64px;
|
||
border-radius: 50%;
|
||
background: fade(@color-primary, 8%);
|
||
color: @color-primary;
|
||
margin-bottom: @space-lg;
|
||
transition: background @transition-base;
|
||
}
|
||
|
||
&:hover &__icon {
|
||
background: fade(@color-primary, 15%);
|
||
}
|
||
|
||
&__title {
|
||
font-size: @font-size-xl;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin-bottom: @space-sm;
|
||
}
|
||
|
||
&__desc {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-muted;
|
||
margin-bottom: @space-lg;
|
||
line-height: @line-height-base;
|
||
}
|
||
|
||
&__phone-number {
|
||
font-size: @font-size-3xl;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-primary;
|
||
letter-spacing: 2px;
|
||
margin-bottom: @space-sm;
|
||
|
||
.respond-md({
|
||
font-size: @font-size-2xl;
|
||
});
|
||
}
|
||
|
||
&__action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: @space-xs;
|
||
padding: @space-sm @space-xl;
|
||
font-size: @font-size-sm;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-primary;
|
||
border: 2px solid @color-primary;
|
||
border-radius: @border-radius-full;
|
||
text-decoration: none;
|
||
transition: all @transition-base;
|
||
margin-top: @space-md;
|
||
|
||
&:hover {
|
||
background: @color-primary;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
&--phone {
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
// ===== 联系方式网格 =====
|
||
.contact-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr;
|
||
gap: @space-lg;
|
||
max-width: 900px;
|
||
margin: @space-xl auto 0;
|
||
|
||
.respond-md({
|
||
grid-template-columns: repeat(2, 1fr);
|
||
});
|
||
}
|
||
|
||
.contact-grid-item {
|
||
display: flex;
|
||
gap: @space-lg;
|
||
align-items: flex-start;
|
||
padding: @space-xl;
|
||
background: @color-bg-white;
|
||
border: 1px solid @color-border;
|
||
border-radius: @border-radius-lg;
|
||
transition: box-shadow @transition-base, transform @transition-base;
|
||
|
||
&:hover {
|
||
box-shadow: @shadow-md;
|
||
transform: translateY(-2px);
|
||
border-color: @color-primary-lighter;
|
||
|
||
.contact-grid-item__icon {
|
||
background: @color-primary;
|
||
color: #fff;
|
||
}
|
||
}
|
||
|
||
&__icon {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: @border-radius-md;
|
||
background: fade(@color-primary, 8%);
|
||
color: @color-primary;
|
||
transition: all @transition-base;
|
||
}
|
||
|
||
&__content {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
&__label {
|
||
font-size: @font-size-base;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin-bottom: @space-xs;
|
||
}
|
||
|
||
&__value {
|
||
font-size: @font-size-sm;
|
||
color: @color-primary;
|
||
word-break: break-all;
|
||
margin-bottom: 4px;
|
||
font-weight: @font-weight-medium;
|
||
}
|
||
|
||
&__desc {
|
||
font-size: @font-size-xs;
|
||
color: @color-text-muted;
|
||
line-height: @line-height-base;
|
||
}
|
||
}
|
||
|
||
// ===== 安全提示 =====
|
||
.contact-security {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: @space-lg;
|
||
max-width: 700px;
|
||
margin: 0 auto;
|
||
padding: @space-xl @space-2xl;
|
||
background: linear-gradient(135deg, fade(@color-primary, 5%), fade(@color-primary, 10%));
|
||
border: 1px solid fade(@color-primary, 15%);
|
||
border-radius: @border-radius-xl;
|
||
|
||
@media (max-width: 480px) {
|
||
flex-direction: column;
|
||
align-items: center;
|
||
text-align: center;
|
||
padding: @space-xl;
|
||
}
|
||
|
||
&__icon {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 56px;
|
||
height: 56px;
|
||
border-radius: 50%;
|
||
background: @color-primary;
|
||
color: #fff;
|
||
}
|
||
|
||
&__content {
|
||
flex: 1;
|
||
}
|
||
|
||
&__title {
|
||
font-size: @font-size-base;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-primary-dark;
|
||
margin-bottom: @space-xs;
|
||
}
|
||
|
||
&__text {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-relaxed;
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
// ===== 长辈定制 =====
|
||
.contact-elder {
|
||
max-width: 640px;
|
||
margin: 0 auto;
|
||
text-align: center;
|
||
|
||
&__badge {
|
||
display: inline-block;
|
||
padding: @space-xs @space-lg;
|
||
font-size: @font-size-xs;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-accent;
|
||
background: fade(@color-accent, 12%);
|
||
border-radius: @border-radius-full;
|
||
letter-spacing: 3px;
|
||
margin-bottom: @space-lg;
|
||
}
|
||
|
||
&__title {
|
||
font-size: @font-size-xl;
|
||
font-weight: @font-weight-bold;
|
||
color: @color-text-primary;
|
||
margin-bottom: @space-md;
|
||
|
||
.respond-md({
|
||
font-size: @font-size-2xl;
|
||
});
|
||
}
|
||
|
||
&__desc {
|
||
font-size: @font-size-sm;
|
||
color: @color-text-secondary;
|
||
line-height: @line-height-relaxed;
|
||
margin-bottom: @space-xl;
|
||
}
|
||
|
||
&__action {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: @space-xs;
|
||
padding: @space-md @space-2xl;
|
||
font-size: @font-size-sm;
|
||
font-weight: @font-weight-bold;
|
||
color: #fff;
|
||
background: linear-gradient(135deg, @color-accent, darken(@color-accent, 8%));
|
||
border-radius: @border-radius-full;
|
||
text-decoration: none;
|
||
transition: all @transition-base;
|
||
box-shadow: 0 4px 14px rgba(212, 136, 58, 0.3);
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(212, 136, 58, 0.4);
|
||
}
|
||
}
|
||
}
|
||
</style>
|