vue3+ts实现拖拽缩放,全屏

DragResizeZoom.vue

背景:在cesium大屏项目需要拖拽,缩放模块,还需要全屏。

功能如下:

  1. 拖拽组件

  2. 拉升组件长宽

  3. 对角线拉升会比例缩放

  4. 双击组件则全屏

  5. 复原长宽和位置

使用案例:

ts 复制代码
<script setup lang="ts">
import { ref } from "vue";
import DragResizeZoom from "./components/DragResizeZoom.vue";
const dragResizeZoom = ref<InstanceType<typeof DragResizeZoom> | null>(null);

function handleReset() {
  dragResizeZoom.value?.resetPosition();
}
</script>

<template>
  <div id="app">
    <button @click="handleReset">reset</button>
    <DragResizeZoom
      ref="dragResizeZoom"
      :initial-x="100"
      :initial-y="100"
      :initial-width="400"
      :initial-height="300"
      xdirection="right"
    >
      <div class="c1">
        <div><h1>这是一个标题</h1></div>
        <div>这是一个段落fdsafdsafdsafdsfdsa</div>
      </div>
    </DragResizeZoom>
  </div>
</template>

<style scoped style="less">
#app {
  width: 100vw;
  height: 100vh;
  background-color: #b4b4b4;
}
.c1 {
  /* width: 300px;
  height: 300px; */
  background-color: #fff;
  color: pink;
}
</style>
ts 复制代码
<template>
  <div
    ref="containerRef"
    class="drag-resize-zoom"
    :style="containerStyle"
    @mousedown="handleMouseDown"
    @mouseenter="showHandles = true"
    @mouseleave="showHandles = false"
    @dblclick="toggleFullscreen"
  >
    <slot></slot>
    <!-- Resize handles -->
    <!-- <div class="resize-handle resize-nw" @mousedown="(e: any) => handleResizeMouseDown(e, 'nw')"></div> -->
    <!-- <div
      class="resize-handle resize-ne"
      @mousedown="(e: any) => handleResizeMouseDown(e, 'ne')"
    ></div> -->
    <!-- <div
      class="resize-handle resize-sw"
      @mousedown="(e: any) => handleResizeMouseDown(e, 'sw')"
    ></div> -->
    <div
      class="resize-handle resize-se"
      :class="{ 'handle-visible': showHandles }"
      @mousedown="(e: any) => handleResizeMouseDown(e, 'se')"
    ></div>
    <!-- <div class="resize-handle resize-n" @mousedown="(e: any) => handleResizeMouseDown(e, 'n')"></div> -->
    <div
      class="resize-handle resize-s"
      :class="{ 'handle-visible': showHandles }"
      @mousedown="(e: any) => handleResizeMouseDown(e, 's')"
    ></div>
    <!-- <div
      class="resize-handle resize-w"
      @mousedown="(e: any) => handleResizeMouseDown(e, 'w')"
    ></div> -->
    <div
      class="resize-handle resize-e"
      :class="{ 'handle-visible': showHandles }"
      @mousedown="(e: any) => handleResizeMouseDown(e, 'e')"
    ></div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from "vue";

// 添加props定义
const props = defineProps({
  initialX: {
    type: Number,
    default: 0,
  },
  initialY: {
    type: Number,
    default: 0,
  },
  initialWidth: {
    type: Number,
    default: 300,
  },
  initialHeight: {
    type: Number,
    default: 300,
  },
  // 添加xdirection属性,支持'left'和'right'值
  xdirection: {
    type: String,
    default: "left",
    validator: (value: string) => ["left", "right"].includes(value),
  },
});

const containerRef = ref<HTMLElement | null>(null);

// 添加控制句柄显示的状态
const showHandles = ref(false);

// 添加全屏状态
const isFullscreen = ref(false);

// 组件状态
const state = ref({
  x: props.initialX, // 使用props.initialX初始化
  y: props.initialY, // 使用props.initialY初始化
  width: props.initialWidth, // 使用props.initialWidth初始化
  height: props.initialHeight, // 使用props.initialHeight初始化
  zoom: 1,
  isDragging: false,
  isResizing: false,
  dragStartX: 0,
  dragStartY: 0,
  startX: 0,
  startY: 0,
  startWidth: 0,
  startHeight: 0,
  resizeDirection: "",
  // 添加right和bottom属性
  right: undefined as number | undefined,
  bottom: undefined as number | undefined,
});

// 容器样式
const containerStyle = computed(() => {
  // 根据xdirection决定使用left还是right定位
  if (props.xdirection === "right") {
    return {
      position: "absolute" as const,
      right: state.value.x + "px",
      top: state.value.y + "px",
      // 添加bottom,当它有值时使用
      ...(state.value.bottom !== undefined && {
        bottom: state.value.bottom + "px",
      }),
      width: state.value.width + "px",
      height: state.value.height + "px",
      transform: `scale(${state.value.zoom})`,
      transformOrigin: "top left",
    };
  } else {
    return {
      position: "absolute" as const,
      left: state.value.x + "px",
      top: state.value.y + "px",
      // 添加right和bottom,当它们有值时使用
      ...(state.value.right !== undefined && {
        right: state.value.right + "px",
      }),
      ...(state.value.bottom !== undefined && {
        bottom: state.value.bottom + "px",
      }),
      width: state.value.width + "px",
      height: state.value.height + "px",
      transform: `scale(${state.value.zoom})`,
      transformOrigin: "top left",
    };
  }
});

// 处理鼠标按下事件(拖拽)
const handleMouseDown = (e: MouseEvent) => {
  if ((e.target as HTMLElement).classList.contains("resize-handle")) {
    return;
  }

  state.value.isDragging = true;
  state.value.dragStartX = e.clientX;
  state.value.dragStartY = e.clientY;
  state.value.startX = state.value.x;
  state.value.startY = state.value.y;

  document.addEventListener("mousemove", handleMouseMove);
  document.addEventListener("mouseup", handleMouseUp);
};

// 处理鼠标移动事件(拖拽)
const handleMouseMove = (e: MouseEvent) => {
  if (state.value.isDragging) {
    // 根据xdirection决定如何更新x坐标
    if (props.xdirection === "right") {
      state.value.x = state.value.startX - (e.clientX - state.value.dragStartX);
    } else {
      state.value.x = state.value.startX + (e.clientX - state.value.dragStartX);
    }
    state.value.y = state.value.startY + (e.clientY - state.value.dragStartY);
  }

  if (state.value.isResizing) {
    resizeComponent(e);
  }
};

// 处理鼠标释放事件
const handleMouseUp = () => {
  state.value.isDragging = false;
  state.value.isResizing = false;
  document.removeEventListener("mousemove", handleMouseMove);
  document.removeEventListener("mouseup", handleMouseUp);
};

// 处理调整大小鼠标按下事件
const handleResizeMouseDown = (e: MouseEvent, direction: string) => {
  e.stopPropagation();
  state.value.isResizing = true;
  state.value.resizeDirection = direction;
  state.value.startX = e.clientX;
  state.value.startY = e.clientY;
  state.value.startWidth = state.value.width;
  state.value.startHeight = state.value.height;

  document.addEventListener("mousemove", handleMouseMove);
  document.addEventListener("mouseup", handleMouseUp);
};

// 调整组件大小
const resizeComponent = (e: MouseEvent) => {
  const deltaX = e.clientX - state.value.startX;
  const deltaY = e.clientY - state.value.startY;

  const direction = state.value.resizeDirection;

  // 记录调整前的尺寸用于计算缩放
  const oldWidth = state.value.width;
  const oldHeight = state.value.height;
  const oldX = state.value.x;
  const oldY = state.value.y;

  // 重置right和bottom值
  state.value.right = undefined;
  state.value.bottom = undefined;

  switch (direction) {
    case "nw": // 西北角
      state.value.width = Math.max(50, state.value.startWidth - deltaX);
      state.value.height = Math.max(50, state.value.startHeight - deltaY);
      // 修改:根据需求调整left和top定位
      if (state.value.width !== oldWidth || state.value.height !== oldHeight) {
        if (props.xdirection === "right") {
          state.value.x = oldX - (state.value.startWidth - state.value.width);
        } else {
          state.value.x = e.clientX;
        }
        state.value.y = oldY + (state.value.startHeight - state.value.height);
      }
      break;
    case "ne": // 东北角
      state.value.width = Math.max(50, state.value.startWidth + deltaX);
      state.value.height = Math.max(50, state.value.startHeight - deltaY);
      break;
    case "sw": // 西南角
      state.value.width = Math.max(50, state.value.startWidth - deltaX);
      state.value.height = Math.max(50, state.value.startHeight + deltaY);
      // 使用right定位
      if (state.value.width !== oldWidth) {
        if (props.xdirection === "right") {
          state.value.x = oldX - (state.value.startWidth - state.value.width);
        } else {
          state.value.right = window.innerWidth - oldX - state.value.startWidth;
          state.value.x = oldX + (state.value.startWidth - state.value.width);
        }
      }
      break;
    case "se": // 东南角
      state.value.width = Math.max(50, state.value.startWidth + deltaX);
      state.value.height = Math.max(50, state.value.startHeight + deltaY);
      break;
    case "n": // 上边
      state.value.height = Math.max(50, state.value.startHeight - deltaY);
      if (state.value.height !== oldHeight) {
        state.value.y = oldY + (state.value.startHeight - state.value.height);
      }
      break;
    case "s": // 下边
      state.value.height = Math.max(50, state.value.startHeight + deltaY);
      break;
    case "w": // 左边
      state.value.width = Math.max(50, state.value.startWidth - deltaX);
      if (state.value.width !== oldWidth) {
        if (props.xdirection === "right") {
          state.value.x = oldX - (state.value.startWidth - state.value.width);
        } else {
          state.value.x = oldX + (state.value.startWidth - state.value.width);
        }
      }
      break;
    case "e": // 右边
      state.value.width = Math.max(50, state.value.startWidth + deltaX);
      break;
  }

  // 计算缩放比例(仅对角调整时应用缩放)
  if (
    direction === "se" ||
    direction === "ne" ||
    direction === "sw" ||
    direction === "nw"
  ) {
    const widthRatio = state.value.width / state.value.startWidth;
    const heightRatio = state.value.height / state.value.startHeight;
    state.value.zoom = Math.min(widthRatio, heightRatio);
  }
};

// 添加全屏切换功能
const toggleFullscreen = () => {
  if (!containerRef.value) return;

  const element = containerRef.value;

  if (!isFullscreen.value) {
    // 进入全屏
    if (element.requestFullscreen) {
      element.requestFullscreen();
    } else if ((element as any).mozRequestFullScreen) {
      // Firefox
      (element as any).mozRequestFullScreen();
    } else if ((element as any).webkitRequestFullscreen) {
      // Chrome, Safari and Opera
      (element as any).webkitRequestFullscreen();
    } else if ((element as any).msRequestFullscreen) {
      // IE/Edge
      (element as any).msRequestFullscreen();
    }
  } else {
    // 退出全屏
    if (document.exitFullscreen) {
      document.exitFullscreen();
    } else if ((document as any).mozCancelFullScreen) {
      // Firefox
      (document as any).mozCancelFullScreen();
    } else if ((document as any).webkitExitFullscreen) {
      // Chrome, Safari and Opera
      (document as any).webkitExitFullscreen();
    } else if ((document as any).msExitFullscreen) {
      // IE/Edge
      (document as any).msExitFullscreen();
    }
  }

  isFullscreen.value = !isFullscreen.value;
};

// 监听全屏变化事件
const handleFullscreenChange = () => {
  isFullscreen.value = !!(
    document.fullscreenElement ||
    (document as any).mozFullScreenElement ||
    (document as any).webkitFullscreenElement ||
    (document as any).msFullscreenElement
  );
};

// 添加恢复初始位置的函数
const resetPosition = () => {
  state.value.x = props.initialX;
  state.value.y = props.initialY;
  state.value.width = props.initialWidth;
  state.value.height = props.initialHeight;
  state.value.zoom = 1;
  state.value.right = undefined;
  state.value.bottom = undefined;
};

// 暴露函数给父组件使用
defineExpose({
  resetPosition,
});

// 清理事件监听器
onBeforeUnmount(() => {
  document.removeEventListener("mousemove", handleMouseMove);
  document.removeEventListener("mouseup", handleMouseUp);
  document.removeEventListener("fullscreenchange", handleFullscreenChange);
  document.removeEventListener(
    "webkitfullscreenchange",
    handleFullscreenChange
  );
  document.removeEventListener("mozfullscreenchange", handleFullscreenChange);
  document.removeEventListener("MSFullscreenChange", handleFullscreenChange);
});

// 添加全屏事件监听
onMounted(() => {
  document.addEventListener("fullscreenchange", handleFullscreenChange);
  document.addEventListener("webkitfullscreenchange", handleFullscreenChange);
  document.addEventListener("mozfullscreenchange", handleFullscreenChange);
  document.addEventListener("MSFullscreenChange", handleFullscreenChange);
});
</script>

<style scoped>
.drag-resize-zoom {
  /* margin: 3px; */
  position: relative;
  cursor: move;
  z-index: 9999;
  overflow: hidden;
}

.resize-handle {
  position: absolute;
  /* background: linear-gradient(135deg, #4a90e2, #1a3a6a); */
  background-color: #5a9fff;
  z-index: 9999;
  opacity: 0; /* 默认隐藏 */
  transition: opacity 0.2s ease;
}

.handle-visible {
  opacity: 1;
}

.resize-handle::before {
  content: "";
  position: absolute;
  top: -3px;
  left: -3px;
  right: -3px;
  bottom: -3px;
  opacity: 0;
}

.resize-handle:hover::before {
  opacity: 1;
}

.resize-se {
  width: 12px;
  height: 12px;
  background: transparent;
  border: none;
  box-shadow: none;
  top: auto;
  right: 0px;
  bottom: 0px;
  cursor: se-resize; /* 添加斜对角鼠标样式 */
}

.resize-se::before {
  display: none;
}

.resize-se::after {
  content: "";
  position: absolute;
  width: 0;
  height: 0;
  border-style: solid;
  border-width: 0 0 12px 12px;
  border-color: transparent transparent #4a90e2 transparent;
  box-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
  /* transition: all 0.2s ease; */
}

.resize-se:hover::after {
  border-color: transparent transparent #5aa0ff transparent;
  box-shadow: 2px 2px 4px rgba(74, 144, 226, 0.8);
  transform: scale(1.1);
}

.resize-s {
  /* 下边句柄改为瘦高 */
  width: 100%;
  height: 3px;
  /* border-radius: 1px; */
  bottom: 0px;
  left: 0%;
  margin-left: -8px;
  cursor: s-resize;
}

.resize-e {
  /* 右边句柄改为瘦高 */
  width: 3px;
  height: 100%;
  /* border-radius: 2px; */
  top: 0%;
  right: 0px;
  margin-top: -8px;
  cursor: e-resize;
}
</style>
相关推荐
aesthetician2 小时前
@tanstack/react-query:React 服务器状态管理与数据同步解决方案
服务器·前端·react.js
Nan_Shu_6142 小时前
学习:uniapp全栈微信小程序vue3后台(28)
前端·学习·微信小程序·小程序·uni-app
珍宝商店2 小时前
原生 JavaScript 方法实战指南
开发语言·前端·javascript
蓝莓味的口香糖3 小时前
【企业微信】VUE项目在企微中自定义转发内容
前端·vue.js·企业微信
IT_陈寒3 小时前
告别低效!用这5个Python技巧让你的数据处理速度提升300% 🚀
前端·人工智能·后端
—Qeyser3 小时前
Laravel + UniApp AES加密/解密
前端·uni-app·laravel
C++chaofan3 小时前
游标查询在对话历史场景下的独特优势
java·前端·javascript·数据库·spring boot
cg.family3 小时前
Vue3 v-slot 详解与示例
前端·javascript·vue.js
FreeBuf_3 小时前
新型域名前置攻击利用Google Meet、YouTube、Chrome及GCP构建流量隧道
前端·chrome