使用vue3写一个拖拽分割面板

需求

如VS code面板,文件展示和代码区域可进行宽度拖拽。由于使用了vue3,split-pane这个插件目前没有vue3版本。

原理

  • 鼠标悬停到拖拽线上按下,获取当前MouseEvent的clientX并记录为startX
  • 移动鼠标,获取当前MouseEvent的clientX并计算移动距离
  • 给拖拽线左右容器重设宽度
  • 竖向拖拽分割面板是计算clientY

vue3代码

<script lang="ts" setup name="SplitPanel">
import { onMounted, reactive, ref } from "vue";
import { useResizeObserver } from "@vueuse/core";

type mouseDataType = {
  id: number;
  width: number;
  startX: number;
  clientX: number;
};

type panelType = {
  id: number;
  width: number;
  type: string;
};

const MIN_WIDTH = 100;

const splitPanelRef = ref<HTMLElement | null>(null);
const isDown = ref(false);
// 整个容器的宽高
const container = reactive({
  width: 0,
});

// 子容器
const panelList = ref<panelType[]>([
  { id: 1, width: 100, type: "left" },
  { id: 2, width: 400, type: "center" },
  { id: 3, width: 10, type: "right" },
]);

const mouseData = ref<mouseDataType>({
  id: 0,
  width: 0,
  startX: 0,
  clientX: 0,
});

function setMouseData(options: { [key: string]: number }) {
  mouseData.value = {
    ...mouseData.value,
    ...options,
  };
}

useResizeObserver(splitPanelRef, (entries) => {
  const entry = entries[0];
  const { width } = entry.contentRect;
  container.width = width;
  initWidth();
});

onMounted(() => {
  document.onmouseup = onMouseUp;
});

function initWidth() {
  container.width = splitPanelRef.value?.clientWidth || 0;
  panelList.value[0].width = 100;
  panelList.value[1].width = container.width - 100 - 400;
  panelList.value[2].width = 400;
}

function resizePanel(options: mouseDataType) {
  if (options.id && options.clientX) {
    const index = drapIndex();

    const max_width = panelList.value.reduce((prev, curr, i) => {
      if (index === i || index + 1 === i) {
        prev += curr.width;
      }
      return prev;
    }, 0);

    panelList.value.forEach((el, i) => {
      if (el.id === options.id) {
        const offset = options.clientX - options.startX;
        el.width = options.width + offset;
        panelList.value[i + 1].width = max_width - (options.width + offset);
      }
    });
  }
}

function drapIndex() {
  return panelList.value.reduce((prev, curr, i) => {
    if (curr.id === mouseData.value.id) prev = i;
    return prev;
  }, -1);
}

function drapLine(item: panelType) {
  if (item.id === mouseData.value.id && isDown.value) {
    return {
      opacity: 0.5,
      right: -(mouseData.value.clientX - mouseData.value.startX) + "px",
      zIndex: 10,
    };
  } else {
    return { opacity: 0, right: 0, zIndex: 0 };
  }
}

function onMouseDown(e: MouseEvent, item: panelType) {
  //   console.log("down");
  isDown.value = true;
  setMouseData({ id: item.id, width: item.width, startX: e.clientX });
}

function onMouseMove(e: MouseEvent) {
  if (isDown.value) {
    const index = drapIndex();
    const min = panelList.value[index].width - MIN_WIDTH;
    const max = panelList.value[index + 1].width - MIN_WIDTH;
    const { startX } = mouseData.value;
    // 两侧容器的最小宽度为 MIN_WIDTH
    if (e.clientX - startX > max) {
      setMouseData({ clientX: startX + max });
    } else if (e.clientX - startX < -min) {
      setMouseData({ clientX: startX - min });
    } else {
      setMouseData({ clientX: e.clientX });
    }
  }
}

function onMouseUp() {
  console.log("up");
  if (isDown.value) {
    resizePanel(mouseData.value);
    isDown.value = false;
    setMouseData({ id: 0, startX: 0, clientX: 0 });
  }
}
</script>
<template>
  <div class="split-panel" @mousemove="onMouseMove" ref="splitPanelRef">
    <div
      v-for="(item, index) of panelList"
      :key="index + ''"
      class="panel-item"
      :style="{
        width: item.width + 'px',
      }"
    >
      <slot :name="item.type"></slot>
      <div
        class="handle"
        @mousedown="(e) => onMouseDown(e, item)"
        :style="drapLine(item)"
        v-if="index !== panelList.length - 1"
      ></div>
    </div>
  </div>
</template>
<style lang="scss" scoped>
.split-panel {
  width: 100%;
  // 定义容器高度
  height: calc(100vh - 33px - 30px);
  display: flex;
  .panel-item {
    position: relative;
    border-right: 1px solid #ccc;
    .handle {
      position: absolute;
      top: 0;
      right: 0px;
      width: 4px;
      height: 100%;
      border-right: 1px solid #ccc;
      cursor: e-resize;
      user-select: none;
    }
  }
}
</style>

使用

<SplitPane>
	<template #left>
		<div>left</div>
	</template>
	<template #center>
		<div>center</div>
	</template>
	<template #right>
		<div>right</div>
	</template>
</SplitPane>

完了

GitHub 加速计划 / vu / vue
207.52 K
33.66 K
下载
vuejs/vue: 是一个用于构建用户界面的 JavaScript 框架,具有简洁的语法和丰富的组件库,可以用于开发单页面应用程序和多页面应用程序。
最近提交(Master分支:1 个月前 )
73486cb5 * chore: fix link broken Signed-off-by: snoppy <michaleli@foxmail.com> * Update packages/template-compiler/README.md [skip ci] --------- Signed-off-by: snoppy <michaleli@foxmail.com> Co-authored-by: Eduardo San Martin Morote <posva@users.noreply.github.com> 3 个月前
e428d891 Updated Browser Compatibility reference. The previous currently returns HTTP 404. 3 个月前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐