vue3 +uni-app模仿豆包固定导航入口,可拖拽,兼容PC端和移动端
·
<template>
<!-- PC端使用div,移动端使用view -->
<div
v-if="deviceType === 'pc'"
class="customer-service"
:class="{ 'is-dragging': isDragging, 'is-pc': true }"
:style="componentStyle"
@mousedown="handleMouseDown"
@click="handleClick"
>
<img
class="customer-icon"
src="/static/user/Ivy.png"
alt="AI助手"
:draggable="false"
/>
<!-- 拖拽时的视觉反馈 -->
<div v-if="isDragging" class="drag-indicator"></div>
</div>
<!-- 移动端使用view -->
<view
v-else
class="customer-service"
:class="{ 'is-dragging': isDragging }"
:style="componentStyle"
@touchstart="handleTouchStart"
@touchmove.prevent="handleTouchMove"
@touchend="handleTouchEnd"
@click="handleClick"
>
<image
class="customer-icon"
src="/static/user/Ivy.png"
alt="AI助手"
:draggable="false"
/>
<!-- 拖拽时的视觉反馈 -->
<view v-if="isDragging" class="drag-indicator"></view>
</view>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, reactive, watch } from "vue";
import { eventBus } from "@/utils/eventBus.js";
const props = defineProps({
bottom: {
type: String,
default: "200px"
},
});
// 设备类型检测
const deviceType = ref('mobile');
const windowSize = reactive({
width: 0,
height: 0
});
// 位置状态
const position = reactive({
right: 20,
bottom: parseInt(props.bottom) || 200
});
// 拖拽状态
const isDragging = ref(false);
const dragStart = reactive({ x: 0, y: 0 });
const dragDistance = ref(0);
const lastPosition = reactive({ right: 0, bottom: 0 });
// 动画和过渡
const transitionDuration = ref('0.3s');
// 存储键名
const POSITION_STORAGE_KEY = 'customer-service-position';
const POSITION_CHANGE_EVENT = 'customer-service-position-change';
// 计算组件样式
const componentStyle = computed(() => {
const baseStyle = {
position: 'fixed',
right: `${position.right}px`,
bottom: `${position.bottom}px`,
width: '54px',
height: '54px',
cursor: isDragging.value ? 'grabbing' : 'grab',
transition: isDragging.value ? 'none' : `all ${transitionDuration.value} cubic-bezier(0.4, 0, 0.2, 1)`,
transform: isDragging.value ? 'scale(1.1)' : 'scale(1)',
zIndex: isDragging.value ? 9999 : 999
};
return baseStyle;
});
/**
* 检测设备类型
*/
const detectDeviceType = () => {
try {
const systemInfo = uni.getSystemInfoSync();
windowSize.width = systemInfo.windowWidth;
windowSize.height = systemInfo.windowHeight;
// 更精确的设备检测
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const isSmallScreen = windowSize.width < 768;
// 在H5环境下的判断
// #ifdef H5
if (!isMobile && !isSmallScreen) {
deviceType.value = 'pc';
} else {
deviceType.value = 'mobile';
}
// #endif
// 在APP环境下的判断
// #ifdef APP-PLUS
if (systemInfo.platform === 'windows' || systemInfo.platform === 'mac' || systemInfo.platform === 'other') {
deviceType.value = 'pc';
} else {
deviceType.value = 'mobile';
}
// #endif
console.log('设备类型检测结果:', deviceType.value, '窗口宽度:', windowSize.width);
} catch (e) {
// 降级方案
const width = window.innerWidth || document.documentElement.clientWidth || 375;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
windowSize.width = width;
windowSize.height = window.innerHeight || document.documentElement.clientHeight || 667;
deviceType.value = (isMobile || width < 768) ? 'mobile' : 'pc';
console.error('设备检测失败,使用降级方案:', e);
}
};
/**
* 处理窗口大小变化
*/
const handleResize = () => {
detectDeviceType();
// 确保组件不会超出新的视口边界
ensureInViewport();
};
/**
* 确保组件在视口内
*/
const ensureInViewport = () => {
const componentSize = 54;
const maxRight = windowSize.width - componentSize;
const maxBottom = windowSize.height - componentSize;
let needUpdate = false;
let newRight = position.right;
let newBottom = position.bottom;
if (position.right > maxRight) {
newRight = maxRight;
needUpdate = true;
}
if (position.right < 0) {
newRight = 0;
needUpdate = true;
}
if (position.bottom > maxBottom) {
newBottom = maxBottom;
needUpdate = true;
}
if (position.bottom < 0) {
newBottom = 0;
needUpdate = true;
}
if (needUpdate) {
updatePosition(newRight, newBottom);
}
};
/**
* 更新位置
*/
const updatePosition = (right, bottom) => {
position.right = right;
position.bottom = bottom;
savePosition();
eventBus.emit(POSITION_CHANGE_EVENT, { right, bottom });
};
/**
* 处理点击事件
*/
const handleClick = (event) => {
console.log('点击事件触发,拖拽距离:', dragDistance.value);
// 如果刚完成拖拽操作,不触发点击跳转
if (dragDistance.value > 5) {
console.log('检测到拖拽操作,取消点击跳转');
event.stopPropagation();
dragDistance.value = 0; // 重置拖拽距离
return;
}
console.log('执行页面跳转到AI聊天页面');
// 跳转到AI聊天页面
uni.navigateTo({
url: "/pages/ai-chat",
success: () => {
console.log('成功跳转到AI聊天页面');
},
fail: (err) => {
console.error('跳转到聊天页面失败:', err);
// 尝试使用switchTab作为备选方案
uni.switchTab({
url: "/pages/ai-chat",
success: () => {
console.log('通过switchTab成功跳转到AI聊天页面');
},
});
}
});
};
/**
* PC端鼠标事件处理
*/
const handleMouseDown = (event) => {
if (event.button !== 0) return; // 只处理左键
console.log('鼠标按下事件触发');
event.preventDefault();
event.stopPropagation();
isDragging.value = true;
dragDistance.value = 0;
dragStart.x = event.clientX;
dragStart.y = event.clientY;
lastPosition.right = position.right;
lastPosition.bottom = position.bottom;
// 添加全局事件监听
document.addEventListener('mousemove', handleMouseMove, { passive: false });
document.addEventListener('mouseup', handleMouseUp);
document.body.style.userSelect = 'none'; // 防止选中文本
};
const handleMouseMove = (event) => {
if (!isDragging.value) return;
event.preventDefault();
const deltaX = event.clientX - dragStart.x;
const deltaY = event.clientY - dragStart.y;
dragDistance.value = Math.abs(deltaX) + Math.abs(deltaY);
// 计算新位置 - 修正PC端的方向
let newRight = lastPosition.right - deltaX; // 向右移动时deltaX为正,right应该减小
let newBottom = lastPosition.bottom - deltaY; // 向下移动时deltaY为正,bottom应该减小
// 限制边界
const componentSize = 54;
newRight = Math.max(0, Math.min(newRight, windowSize.width - componentSize));
newBottom = Math.max(0, Math.min(newBottom, windowSize.height - componentSize));
position.right = newRight;
position.bottom = newBottom;
console.log('鼠标移动,新位置:', { right: newRight, bottom: newBottom });
};
const handleMouseUp = (event) => {
console.log('鼠标释放事件触发');
isDragging.value = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.userSelect = ''; // 恢复文本选择
// 保存位置
if (dragDistance.value > 5) {
savePosition();
eventBus.emit(POSITION_CHANGE_EVENT, {
right: position.right,
bottom: position.bottom
});
}
};
/**
* 移动端触摸事件处理
*/
const handleTouchStart = (event) => {
if (event.touches.length !== 1) return; // 只处理单指触摸
console.log('触摸开始事件触发');
// 不要立即阻止默认行为,先记录触摸开始位置
isDragging.value = false; // 初始不认为是拖拽
dragDistance.value = 0;
const touch = event.touches[0];
dragStart.x = touch.clientX;
dragStart.y = touch.clientY;
lastPosition.right = position.right;
lastPosition.bottom = position.bottom;
};
const handleTouchMove = (event) => {
if (event.touches.length !== 1) return;
const touch = event.touches[0];
const deltaX = touch.clientX - dragStart.x;
const deltaY = touch.clientY - dragStart.y;
dragDistance.value = Math.abs(deltaX) + Math.abs(deltaY);
// 如果移动距离超过阈值,认为是拖拽操作
if (dragDistance.value > 5 && !isDragging.value) {
isDragging.value = true;
// 开始拖拽时才阻止默认行为
event.preventDefault();
event.stopPropagation();
}
// 只有在拖拽状态下才更新位置
if (isDragging.value) {
event.preventDefault(); // 阻止页面滚动
// 计算新位置 - 修正方向
let newRight = lastPosition.right - deltaX; // 向右移动时deltaX为正,right应该减小
let newBottom = lastPosition.bottom - deltaY; // 向下移动时deltaY为正,bottom应该减小
// 限制边界
const componentSize = 54;
newRight = Math.max(0, Math.min(newRight, windowSize.width - componentSize));
newBottom = Math.max(0, Math.min(newBottom, windowSize.height - componentSize));
position.right = newRight;
position.bottom = newBottom;
console.log('触摸移动,新位置:', { right: newRight, bottom: newBottom });
}
};
const handleTouchEnd = (event) => {
console.log('触摸结束事件触发,拖拽状态:', isDragging.value, '拖拽距离:', dragDistance.value);
// 如果是拖拽操作,保存位置并阻止点击
if (isDragging.value && dragDistance.value > 5) {
savePosition();
eventBus.emit(POSITION_CHANGE_EVENT, {
right: position.right,
bottom: position.bottom
});
// 阻止后续的click事件
event.preventDefault();
event.stopPropagation();
}
// 重置拖拽状态
isDragging.value = false;
// 如果不是拖拽操作(移动距离小于阈值),则允许click事件正常触发
// 不需要在这里手动调用handleClick,让原生的click事件处理
};
/**
* 保存位置到本地存储
*/
const savePosition = () => {
try {
const positionData = {
right: position.right,
bottom: position.bottom,
deviceType: deviceType.value,
timestamp: Date.now()
};
uni.setStorageSync(POSITION_STORAGE_KEY, JSON.stringify(positionData));
console.log('位置已保存:', positionData);
} catch (e) {
console.error('保存位置失败:', e);
}
};
/**
* 从本地存储加载位置
*/
const loadPosition = () => {
try {
const savedData = uni.getStorageSync(POSITION_STORAGE_KEY);
if (savedData) {
const data = JSON.parse(savedData);
position.right = data.right || 20;
position.bottom = data.bottom || parseInt(props.bottom) || 200;
console.log('加载保存的位置:', data);
}
} catch (e) {
console.error('加载位置失败:', e);
}
};
/**
* 处理其他页面发来的位置更新事件
*/
const handlePositionChange = (newPosition) => {
if (newPosition && !isDragging.value) {
position.right = newPosition.right;
position.bottom = newPosition.bottom;
console.log('收到位置更新事件:', newPosition);
}
};
// 监听设备类型变化
watch(deviceType, (newType, oldType) => {
if (oldType && newType !== oldType) {
// console.log(`设备类型切换: ${oldType} -> ${newType}`);
// 重置拖拽状态
isDragging.value = false;
dragDistance.value = 0;
}
});
// 生命周期
onMounted(() => {
detectDeviceType();
loadPosition();
// 监听窗口大小变化
window.addEventListener('resize', handleResize);
// 监听方向变化(移动设备)
window.addEventListener('orientationchange', handleResize);
// 监听位置变化事件
eventBus.on(POSITION_CHANGE_EVENT, handlePositionChange);
// 确保组件在视口内
ensureInViewport();
});
onUnmounted(() => {
console.log('CustomerService组件已卸载');
window.removeEventListener('resize', handleResize);
window.removeEventListener('orientationchange', handleResize);
eventBus.off(POSITION_CHANGE_EVENT, handlePositionChange);
// 清理可能残留的事件监听
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
});
</script>
<style lang="scss" scoped>
.customer-service {
position: fixed;
z-index: 999;
border-radius: 50%;
overflow: hidden;
background: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
user-select: none;
-webkit-user-select: none;
-webkit-tap-highlight-color: transparent;
touch-action: none; // 防止触摸时的默认行为
&.is-dragging {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
.customer-icon {
opacity: 0.9;
}
}
&.is-pc {
&:hover {
transform: scale(1.05);
box-shadow: 0 3px 16px rgba(0, 0, 0, 0.2);
}
}
.customer-icon {
width: 100%;
height: 100%;
display: block;
border-radius: 50%;
transition: opacity 0.3s ease;
pointer-events: none; // 防止图片干扰拖拽
}
.drag-indicator {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 30px;
height: 30px;
border: 2px solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
animation: pulse 1s infinite;
pointer-events: none;
}
@keyframes pulse {
0% {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
100% {
transform: translate(-50%, -50%) scale(1.5);
opacity: 0;
}
}
}
// 移动端优化
@media (max-width: 768px) {
.customer-service {
&:active {
transform: scale(0.95);
}
}
}
</style>
更多推荐



所有评论(0)