vue左侧收起,四向拖拽伸缩组件

4 阅读1分钟
<template> 
  <div
    :class="['menu', { 'menu-collapsed': isCollapsed, 'menu-expanded': !isCollapsed }]"
    :style="containerStyle"
  >
    <slot></slot>
    <!-- 折叠展开按钮 -->
    <div :class="isCollapsed ? 'btn-collapse' : 'btn-expand'">
      <img
        width="40px"
        @click="toggleCollapse"
        :src="isCollapsed ? expandImg : collapseImg"
        alt=""
      />
    </div>
    <!-- 拖拽 -->
    <div
      v-if="!isCollapsed"
      :class="['resize-handle', 'resize-handle-' + props.dragSide]"
      @mousedown.prevent="onResizeMouseDown"
    ></div>
  </div>
</template>
<script lang="ts" setup>
import expandImg from '@/assets/imgs/expand.png';
import collapseImg from '@/assets/imgs/collapse.png';
const props = defineProps({
  /** 面板初始宽度*/
  width: {
    type: String,
    default: '300px'
  },
  /**
   * 拖拽的边'left' | 'right' | 'top' | 'bottom'
   */
  dragSide: {
    type: String,
    default: 'right'
  },
  /** 最小尺寸(像素) */
  minSize: {
    type: Number,
    default: 100
  },
  /** 最大尺寸(像素) */
  maxSize: {
    type: Number,
    default: 800
  }
});
const isCollapsed=ref(false);
const isDragging = ref(false);

// 当前尺寸
const size = ref<number>(parseInt((props.width), 10) || 200);

let startX = 0;
let startY = 0;
let startSize = size.value;

const isHorizontal = computed(() => {
  return props.dragSide === 'left' || props.dragSide === 'right';
});

const containerStyle = computed(() => {
  const style: Record<string, string> = {};
  // 统一使用相对定位,子元素都是绝对定位
  style.position = isCollapsed.value ? 'unset' : 'relative';
  if (isCollapsed.value) {
    if (isHorizontal.value) {
      style.width = '0px';
    } else {
      style.height = '0px';
    }
  } else {
    const px = `${size.value}px`;
    if (isHorizontal.value) {
      style.width = px;
    } else {
      style.height = px;
    }
  }
  return style;
});

function toggleCollapse() {
  isCollapsed.value = !isCollapsed.value;
}

function clampSize(val: number) {
  const min = props.minSize;
  const max = props.maxSize;
  if (val < min) return min;
  if (val > max) return max;
  return val;
}

function onResizeMouseDown(e: MouseEvent) {
  isDragging.value = true;
  startX = e.clientX;
  startY = e.clientY;
  startSize = size.value;
  window.addEventListener('mousemove', onResizeMouseMove);
  window.addEventListener('mouseup', onResizeMouseUp);
}

function onResizeMouseMove(e: MouseEvent) {
  if (!isDragging.value) return;
  let delta = 0;
  if (isHorizontal.value) {
    delta = e.clientX - startX;
    // 从左边拖拽时方向相反
    if (props.dragSide === 'left') {
      delta = -delta;
    }
  } else {
    delta = e.clientY - startY;
    // 从上边拖拽时方向相反
    if (props.dragSide === 'top') {
      delta = -delta;
    }
  }
  size.value = clampSize(startSize + delta);
}

function onResizeMouseUp() {
  if (!isDragging.value) return;
  isDragging.value = false;
  window.removeEventListener('mousemove', onResizeMouseMove);
  window.removeEventListener('mouseup', onResizeMouseUp);
}

onBeforeUnmount(() => {
  window.removeEventListener('mousemove', onResizeMouseMove);
  window.removeEventListener('mouseup', onResizeMouseUp);
});
</script>
<style lang="scss" scoped>
.btn-expand { 
  position: absolute;
  top: 50%;
  right: -14px;
}
.btn-collapse {
  position: absolute;
  top: 50%;
  left: -14px;
  z-index: 10000000;
}
.menu {
  transition: width 0.3s; 
  overflow: hidden;
}
.menu-collapsed {
  width: 0px; 
}
.menu-expanded {
  width: calc(100% - 20px);
  margin-right: 20px;
}

.resize-handle {
  position: absolute;
  z-index: 10000001;
  background-color: transparent; 
}

.resize-handle-left {
  top: 0;
  left: 0;
  width: 4px;
  height: 100%;
  cursor: col-resize;
}

.resize-handle-right {
  top: 0;
  right: 0;
  width: 4px;
  height: 100%;
  cursor: col-resize;
}

.resize-handle-top {
  top: 0;
  left: 0;
  width: 100%;
  height: 4px;
  cursor: row-resize;
}

.resize-handle-bottom {
  bottom: 0;
  left: 0;
  width: 100%;
  height: 4px;
  cursor: row-resize;
}
</style>