247 行
6.3 KiB
Vue
247 行
6.3 KiB
Vue
<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>
|