hulai-website/pages/contact.vue
Mimingguang ee078b61bc feat: 后台管理系统完整重构 + 前后台数据联通
- 后台全面引入 Naive UI 组件库,统一 UI 规范
- 前台 usePublicApi 切换到 API 调用(GET /api/content/[key])
- 前后台数据源统一(site_content 表)
- 产品线统一管理(tour/camp/course 三套编辑器)
- 产品数据归一化(itinerary/faq/pricing 格式统一)
- 表单提交 API 改写入 submissions 表
- 新增 submissions 标记已读 API
- site-content PUT 支持 upsert
- 修复前台 bug(stories 路由冲突、占位符警告、selector breadcrumb)
- 消除 PageHero 类型警告(useSEO reactive 修复)
- 25 个前台页面全部 HTTP 200,展示效果不变

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:48:02 +08:00

447 行
15 KiB
Vue

此文件含有模棱两可的 Unicode 字符

此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。

<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>