<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>

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐