feat: 官网代码全量更新 (2026-04-18)
这个提交包含在:
@@ -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>
|
||||
|
||||
在新工单中引用
屏蔽一个用户