|
|
@@ -0,0 +1,166 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import { onShow } from '@dcloudio/uni-app'
|
|
|
+import { getConfigByCode } from '@/api/common'
|
|
|
+import { openH5WhatsApp } from '@/utils/social'
|
|
|
+import { toast } from '@/utils/toast'
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ bottomOffset?: number
|
|
|
+}>()
|
|
|
+
|
|
|
+const STORAGE_KEY = 'customer_service_fab_position_v1'
|
|
|
+
|
|
|
+const x = ref<number>(0)
|
|
|
+const y = ref<number>(0)
|
|
|
+
|
|
|
+function rpxToPx(rpx: number) {
|
|
|
+ const sys = uni.getSystemInfoSync()
|
|
|
+ const windowWidth = sys.windowWidth || 375
|
|
|
+ return (rpx * windowWidth) / 750
|
|
|
+}
|
|
|
+
|
|
|
+function getBottomLimitPx() {
|
|
|
+ const sys = uni.getSystemInfoSync() as any
|
|
|
+ const bottomOffsetPx = rpxToPx(props.bottomOffset ?? 0)
|
|
|
+ const safeBottomPx = Number(sys?.safeAreaInsets?.bottom) || 0
|
|
|
+ return bottomOffsetPx + safeBottomPx
|
|
|
+}
|
|
|
+
|
|
|
+function initDefaultPosition() {
|
|
|
+ const sys = uni.getSystemInfoSync()
|
|
|
+ const windowWidth = sys.windowWidth || 375
|
|
|
+ const windowHeight = sys.windowHeight || 667
|
|
|
+ const sizePx = rpxToPx(80)
|
|
|
+ const bottomOffsetPx = getBottomLimitPx()
|
|
|
+ x.value = Math.max(0, windowWidth - sizePx)
|
|
|
+ y.value = Math.max(0, windowHeight - sizePx - bottomOffsetPx)
|
|
|
+}
|
|
|
+
|
|
|
+function clampPosition() {
|
|
|
+ const sys = uni.getSystemInfoSync()
|
|
|
+ const windowWidth = sys.windowWidth || 375
|
|
|
+ const windowHeight = sys.windowHeight || 667
|
|
|
+ const sizePx = rpxToPx(80)
|
|
|
+ const bottomOffsetPx = getBottomLimitPx()
|
|
|
+ const maxX = Math.max(0, windowWidth - sizePx)
|
|
|
+ const maxY = Math.max(0, windowHeight - sizePx - bottomOffsetPx)
|
|
|
+ x.value = Math.min(Math.max(0, x.value), maxX)
|
|
|
+ y.value = Math.min(Math.max(0, y.value), maxY)
|
|
|
+}
|
|
|
+
|
|
|
+function restorePosition() {
|
|
|
+ try {
|
|
|
+ const raw = uni.getStorageSync(STORAGE_KEY)
|
|
|
+ if (!raw) {
|
|
|
+ initDefaultPosition()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw
|
|
|
+ const nx = Number(parsed?.x)
|
|
|
+ const ny = Number(parsed?.y)
|
|
|
+ if (Number.isFinite(nx) && Number.isFinite(ny)) {
|
|
|
+ x.value = nx
|
|
|
+ y.value = ny
|
|
|
+ clampPosition()
|
|
|
+ return
|
|
|
+ }
|
|
|
+ initDefaultPosition()
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ initDefaultPosition()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function persistPosition() {
|
|
|
+ try {
|
|
|
+ uni.setStorageSync(STORAGE_KEY, JSON.stringify({ x: x.value, y: y.value }))
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function persistPositionClamped() {
|
|
|
+ clampPosition()
|
|
|
+ persistPosition()
|
|
|
+}
|
|
|
+
|
|
|
+onShow(() => {
|
|
|
+ restorePosition()
|
|
|
+})
|
|
|
+
|
|
|
+function handleMoveChange(e: any) {
|
|
|
+ const detail = e?.detail
|
|
|
+ const nx = Number(detail?.x)
|
|
|
+ const ny = Number(detail?.y)
|
|
|
+ if (Number.isFinite(nx))
|
|
|
+ x.value = nx
|
|
|
+ if (Number.isFinite(ny))
|
|
|
+ y.value = ny
|
|
|
+ clampPosition()
|
|
|
+}
|
|
|
+
|
|
|
+async function openCustomerService() {
|
|
|
+ try {
|
|
|
+ const res = await getConfigByCode({ code: 'live_chat' })
|
|
|
+ const value = res?.data?.valueInfo
|
|
|
+ if (!value) {
|
|
|
+ toast.info('客服暂不可用')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ openH5WhatsApp('live_chat', value)
|
|
|
+ }
|
|
|
+ catch {
|
|
|
+ toast.info('客服暂不可用')
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <movable-area class="customer-fab-area">
|
|
|
+ <movable-view
|
|
|
+ :x="x"
|
|
|
+ :y="y"
|
|
|
+ direction="all"
|
|
|
+ :inertia="false"
|
|
|
+ :animation="false"
|
|
|
+ :out-of-bounds="false"
|
|
|
+ class="customer-fab-movable"
|
|
|
+ @change="handleMoveChange"
|
|
|
+ @touchend="persistPositionClamped"
|
|
|
+ @touchcancel="persistPositionClamped"
|
|
|
+ >
|
|
|
+ <image
|
|
|
+ src="/static/icons/whatsapp.png"
|
|
|
+ class="customer-fab-trigger__icon"
|
|
|
+ mode="heightFix"
|
|
|
+ @click="openCustomerService"
|
|
|
+ />
|
|
|
+ </movable-view>
|
|
|
+ </movable-area>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.customer-fab-area {
|
|
|
+ position: fixed;
|
|
|
+ left: 0;
|
|
|
+ top: 0;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+ pointer-events: none;
|
|
|
+ z-index: 999;
|
|
|
+}
|
|
|
+
|
|
|
+.customer-fab-movable {
|
|
|
+ pointer-events: auto;
|
|
|
+ transition: none;
|
|
|
+ width: 80rpx;
|
|
|
+ height: 80rpx;
|
|
|
+}
|
|
|
+
|
|
|
+.customer-fab-trigger__icon {
|
|
|
+ box-shadow: 0 12rpx 30rpx rgba(0, 0, 0, 0.16);
|
|
|
+ height: 80rpx;
|
|
|
+ width: 80rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+}
|
|
|
+</style>
|