Files
gongxue-new/docs/.vitepress/theme/components/FloatingService.vue

179 lines
7.1 KiB
Vue

<script setup lang="ts">
import { ref, watch } from 'vue'
import { useRouter } from 'vitepress'
import { MessageCircle, Phone, BookOpen, Users, X } from 'lucide-vue-next'
const router = useRouter()
const isOpen = ref(false)
const showChatModal = ref(false)
const chatMessages = ref<{ text?: string; isQrcode?: boolean; isTyping?: boolean }[]>([])
const isTypingComplete = ref(false)
const menuItems = [
{ icon: MessageCircle, label: '在线咨询', sub: '实时答疑', href: '#consult' },
{ icon: Phone, label: '课程报名', sub: '18322616529', href: 'tel:18322616529' },
{ icon: BookOpen, label: '书籍购买', sub: '官方商城', href: '/books' },
{ icon: Users, label: '商务合作', sub: '企业/机构', href: '/contact' },
]
let timers: ReturnType<typeof setTimeout>[] = []
watch(showChatModal, (val) => {
if (!val) {
chatMessages.value = []
isTypingComplete.value = false
timers.forEach(clearTimeout)
timers = []
return
}
timers.push(setTimeout(() => { chatMessages.value = [{ isTyping: true }] }, 500))
timers.push(setTimeout(() => {
chatMessages.value = [{
text: '你好同学,正在接入人工客服,或者方便的话,您先留个手机号联系方式,或加我微信咱们再具体沟通?'
}]
}, 2500))
timers.push(setTimeout(() => {
chatMessages.value = [...chatMessages.value, { isTyping: true }]
}, 3000))
timers.push(setTimeout(() => {
chatMessages.value = [...chatMessages.value.filter(m => !m.isTyping), { isQrcode: true }]
isTypingComplete.value = true
}, 4000))
})
const handleMenuItemClick = (href: string) => {
if (href === '#consult') {
showChatModal.value = true
isOpen.value = false
} else if (href.startsWith('tel:')) {
window.location.href = href
} else {
router.go(href)
isOpen.value = false
}
}
</script>
<template>
<div class="fixed bottom-6 right-6 z-50 flex flex-col items-end">
<div
:class="[
'mb-4 flex flex-col gap-3 transition-all duration-300 origin-bottom-right',
isOpen
? 'opacity-100 scale-100 translate-y-0'
: 'opacity-0 scale-90 translate-y-10 pointer-events-none'
]"
>
<button
v-for="(item, index) in menuItems"
:key="index"
@click="handleMenuItemClick(item.href)"
class="flex items-center gap-3 bg-white/90 backdrop-blur-lg border border-white/20 p-3 pr-4 rounded-2xl shadow-lg cursor-pointer hover:bg-white hover:scale-105 transition-all group"
>
<div class="w-10 h-10 rounded-full bg-blue-50 text-apple-blue flex items-center justify-center group-hover:bg-apple-blue group-hover:text-white transition-colors flex-shrink-0">
<component :is="item.icon" :size="20" />
</div>
<div class="text-left min-w-[80px]">
<div class="text-sm font-bold text-gray-800">{{ item.label }}</div>
<div class="text-xs text-gray-400">{{ item.sub }}</div>
</div>
</button>
</div>
<button
@click="isOpen = !isOpen"
:class="[
'w-16 h-16 rounded-full shadow-2xl flex items-center justify-center transition-all duration-300 hover:scale-110 active:scale-95',
isOpen
? 'bg-gray-800 rotate-90'
: 'bg-apple-blue hover:bg-apple-blue-hover'
]"
:aria-label="isOpen ? '关闭菜单' : '打开客服菜单'"
>
<X v-if="isOpen" class="text-white" :size="28" />
<MessageCircle v-else class="text-white fill-current" :size="28" />
</button>
<div
v-if="showChatModal"
class="fixed inset-0 z-[9999] flex items-center justify-center p-4"
>
<div
class="absolute inset-0 bg-black/60 backdrop-blur-sm"
@click="showChatModal = false"
/>
<div class="relative bg-white rounded-3xl shadow-2xl w-full max-w-md max-h-[600px] flex flex-col animate-slide-up">
<div class="flex items-center justify-between p-4 border-b bg-gradient-to-r from-apple-blue to-blue-600 rounded-t-3xl">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-white/20 rounded-full flex items-center justify-center">
<MessageCircle :size="20" class="text-white" />
</div>
<div>
<h3 class="font-bold text-white">在线客服</h3>
<p class="text-xs text-white/80">在线</p>
</div>
</div>
<button
@click="showChatModal = false"
class="p-2 rounded-full hover:bg-white/20 transition-colors"
>
<X :size="20" class="text-white" />
</button>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
<div v-for="(msg, index) in chatMessages" :key="index">
<div v-if="msg.isTyping" class="flex items-start gap-2">
<div class="w-8 h-8 bg-apple-blue rounded-full flex items-center justify-center flex-shrink-0">
<MessageCircle :size="16" class="text-white" />
</div>
<div class="bg-white rounded-2xl rounded-tl-none px-4 py-3 shadow-sm">
<div class="flex gap-1">
<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 0ms;"></span>
<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 150ms;"></span>
<span class="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style="animation-delay: 300ms;"></span>
</div>
</div>
</div>
<div v-else-if="msg.isQrcode" class="flex items-start gap-2">
<div class="w-8 h-8 bg-apple-blue rounded-full flex items-center justify-center flex-shrink-0">
<MessageCircle :size="16" class="text-white" />
</div>
<div class="bg-white rounded-2xl rounded-tl-none p-4 shadow-sm">
<img
src="https://duiwaifengxiang-1304851894.cos.ap-beijing.myqcloud.com/gwgongxue/code.jpg"
alt="客服二维码"
class="w-48 h-48 rounded-lg"
/>
<p class="text-xs text-gray-500 text-center mt-2">扫码添加微信客服</p>
</div>
</div>
<div v-else class="flex items-start gap-2 animate-slide-up">
<div class="w-8 h-8 bg-apple-blue rounded-full flex items-center justify-center flex-shrink-0">
<MessageCircle :size="16" class="text-white" />
</div>
<div class="bg-white rounded-2xl rounded-tl-none px-4 py-3 shadow-sm max-w-[80%]">
<p class="text-sm text-gray-800 leading-relaxed">{{ msg.text }}</p>
</div>
</div>
</div>
</div>
<div class="p-4 border-t bg-white rounded-b-3xl">
<div class="bg-gray-50 rounded-xl p-3 text-center">
<p class="text-xs text-gray-500">
{{ isTypingComplete
? '工作时间:周一至周日 9:00-21:00'
: '客服正在输入中…' }}
</p>
</div>
</div>
</div>
</div>
</div>
</template>