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>
相关推荐
WeiXiao_Hyy4 分钟前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡21 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone27 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09011 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农1 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳2 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js