|
|
@@ -0,0 +1,231 @@
|
|
|
+<script setup lang="ts">
|
|
|
+const props = withDefaults(
|
|
|
+ defineProps<{
|
|
|
+ mode?: "h" | "v" | "horizontal" | "vertical" // 拖拽方向
|
|
|
+ size?: number | string // 初始尺寸,支持像素或百分比(如 '50%')
|
|
|
+ minSize?: number | string // 最小尺寸,支持像素或百分比(如 '10%')
|
|
|
+ maxSize?: number | string // 最大尺寸,支持像素或百分比(如 '80%')
|
|
|
+ containerSize?: string | number // 容器尺寸
|
|
|
+ }>(),
|
|
|
+ {
|
|
|
+ mode: "h",
|
|
|
+ size: 200,
|
|
|
+ minSize: 100,
|
|
|
+ maxSize: 2000,
|
|
|
+ containerSize: "100%"
|
|
|
+ }
|
|
|
+)
|
|
|
+const emits = defineEmits<{
|
|
|
+ (e: "dragComplete", n1: number, n2: number): void
|
|
|
+}>()
|
|
|
+
|
|
|
+const panelRef = ref(null)
|
|
|
+const isDragging = ref(false)
|
|
|
+const currentSize = ref<number>(
|
|
|
+ typeof props.size === "number" ? props.size : parseFloat(props.size as string) || 200
|
|
|
+)
|
|
|
+const parentSize = ref(0)
|
|
|
+
|
|
|
+const validatePositive = (value, name) => {
|
|
|
+ const num = Number(value)
|
|
|
+ if (!isNaN(num) && num < 0) {
|
|
|
+ throw new Error(`VbSplitPanel:${name} 不能小于 0,当前值:${value}`)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const realMode = computed(() => {
|
|
|
+ const m = props.mode
|
|
|
+ return m === "h" || m === "horizontal" ? "horizontal" : "vertical"
|
|
|
+})
|
|
|
+
|
|
|
+// 解析尺寸值,将百分比转换为像素值
|
|
|
+const parseSizeValue = (value: number | string, parentSize: number): number => {
|
|
|
+ if (typeof value === "string" && value.endsWith("%")) {
|
|
|
+ const percentage = parseFloat(value)
|
|
|
+
|
|
|
+ if (!isNaN(percentage)) {
|
|
|
+ if (percentage > 100) {
|
|
|
+ console.warn(`VbSplitPanel:${value} 超出范围,已设置为 90%`)
|
|
|
+ return (parentSize * 90) / 100
|
|
|
+ } else if (percentage < 0) {
|
|
|
+ console.warn(`VbSplitPanel:${value} 不能小于 0,已设置为 10%`)
|
|
|
+ return (parentSize * 10) / 100
|
|
|
+ } else {
|
|
|
+ return (parentSize * percentage) / 100
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return typeof value === "number" ? value : parseFloat(value) || 0
|
|
|
+}
|
|
|
+
|
|
|
+// 获取父元素拖拽方向尺寸
|
|
|
+const getParentSize = () => {
|
|
|
+ if (!panelRef.value?.parentElement) return
|
|
|
+ const parent = panelRef.value.parentElement
|
|
|
+ parent.style.userSelect = "none"
|
|
|
+ const size = realMode.value === "horizontal" ? parent.offsetWidth : parent.offsetHeight
|
|
|
+
|
|
|
+ validatePositive(size, `拖拽父元素${realMode.value === "horizontal" ? "水平" : "垂直"}方向尺寸`)
|
|
|
+ parentSize.value = size
|
|
|
+}
|
|
|
+
|
|
|
+// 容器样式
|
|
|
+const containerStyle = computed(() => {
|
|
|
+ const isH = realMode.value === "horizontal"
|
|
|
+ const sizeProp = isH ? "width" : "height"
|
|
|
+ const crossProp = isH ? "height" : "width"
|
|
|
+ const val = props.containerSize
|
|
|
+
|
|
|
+ validatePositive(val, "size")
|
|
|
+
|
|
|
+ let finalSize
|
|
|
+ if (val === "auto") {
|
|
|
+ if (parentSize.value < 0) {
|
|
|
+ throw new Error(`SplitPanel:size=auto,但父元素${sizeProp}为 0,无法渲染`)
|
|
|
+ }
|
|
|
+ finalSize = `${parentSize.value}px`
|
|
|
+ } else if (!isNaN(Number(val))) {
|
|
|
+ finalSize = `${val}px`
|
|
|
+ } else {
|
|
|
+ finalSize = val
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ [sizeProp]: finalSize,
|
|
|
+ [crossProp]: "100%",
|
|
|
+ display: "flex",
|
|
|
+ overflow: "hidden"
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const modeClass = computed(() => realMode.value)
|
|
|
+const handlerClass = computed(() => (realMode.value === "horizontal" ? "h-handler" : "v-handler"))
|
|
|
+
|
|
|
+const firstPanelStyle = computed(() => {
|
|
|
+ const pixelSize = parseSizeValue(currentSize.value, parentSize.value)
|
|
|
+ return {
|
|
|
+ ...(realMode.value === "horizontal"
|
|
|
+ ? { width: `${pixelSize}px` }
|
|
|
+ : { height: `${pixelSize}px` }),
|
|
|
+ flex: "none",
|
|
|
+ overflow: "auto"
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 拖拽逻辑
|
|
|
+const startDrag = (e) => {
|
|
|
+ e.preventDefault()
|
|
|
+ isDragging.value = true
|
|
|
+ const startPos = realMode.value === "horizontal" ? e.clientX : e.clientY
|
|
|
+ // 确保 start 是数值类型
|
|
|
+ const start =
|
|
|
+ typeof currentSize.value === "number"
|
|
|
+ ? currentSize.value
|
|
|
+ : parseFloat(currentSize.value as string) || 200
|
|
|
+ const onMove = (e) => {
|
|
|
+ if (!isDragging.value) return
|
|
|
+ const pos = realMode.value === "horizontal" ? e.clientX : e.clientY
|
|
|
+ let size = start + (pos - startPos)
|
|
|
+
|
|
|
+ // 解析 minSize 和 maxSize 为像素值
|
|
|
+ const minPixelSize = parseSizeValue(props.minSize, parentSize.value)
|
|
|
+ const maxPixelSize = parseSizeValue(props.maxSize, parentSize.value)
|
|
|
+
|
|
|
+ size = Math.max(minPixelSize, Math.min(maxPixelSize, size))
|
|
|
+ size = Math.min(size, parentSize.value - minPixelSize)
|
|
|
+ currentSize.value = size
|
|
|
+ }
|
|
|
+
|
|
|
+ const stop = () => {
|
|
|
+ isDragging.value = false
|
|
|
+ document.removeEventListener("mousemove", onMove)
|
|
|
+ document.removeEventListener("mouseup", stop)
|
|
|
+ // 确保发送的数值是正确的数字类型
|
|
|
+ const finalSize =
|
|
|
+ typeof currentSize.value === "number"
|
|
|
+ ? currentSize.value
|
|
|
+ : parseFloat(currentSize.value as string) || 200
|
|
|
+ const secondPanelSize = parentSize.value - finalSize - 6
|
|
|
+ nextTick(() => {
|
|
|
+ emits("dragComplete", finalSize, secondPanelSize)
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ document.addEventListener("mousemove", onMove)
|
|
|
+ document.addEventListener("mouseup", stop)
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ getParentSize()
|
|
|
+ // 在父元素尺寸获取后,重新计算初始尺寸
|
|
|
+ if (parentSize.value > 0) {
|
|
|
+ currentSize.value = parseSizeValue(props.size, parentSize.value)
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+watch(() => [props.mode, props.containerSize], getParentSize)
|
|
|
+
|
|
|
+// 监听 size 属性变化
|
|
|
+watch(
|
|
|
+ () => props.size,
|
|
|
+ (newSize) => {
|
|
|
+ if (parentSize.value > 0) {
|
|
|
+ currentSize.value = parseSizeValue(newSize, parentSize.value)
|
|
|
+ }
|
|
|
+ }
|
|
|
+)
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="split-panel" ref="panelRef" :class="modeClass" :style="containerStyle">
|
|
|
+ <div class="panel-item first-panel" :style="firstPanelStyle">
|
|
|
+ <slot name="first"></slot>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="split-handler" :class="handlerClass" @mousedown="startDrag"></div>
|
|
|
+
|
|
|
+ <div class="panel-item second-panel">
|
|
|
+ <slot name="second"></slot>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.split-panel {
|
|
|
+ position: relative;
|
|
|
+}
|
|
|
+.horizontal {
|
|
|
+ flex-direction: row;
|
|
|
+}
|
|
|
+.vertical {
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.panel-item {
|
|
|
+ background: #f9fafb;
|
|
|
+}
|
|
|
+.second-panel {
|
|
|
+ flex: 1;
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.split-handler {
|
|
|
+ background: #e5e7eb;
|
|
|
+ transition: background 0.2s;
|
|
|
+ z-index: 10;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.h-handler {
|
|
|
+ width: 6px;
|
|
|
+ height: 100%;
|
|
|
+ cursor: col-resize;
|
|
|
+}
|
|
|
+.v-handler {
|
|
|
+ height: 6px;
|
|
|
+ width: 100%;
|
|
|
+ cursor: row-resize;
|
|
|
+}
|
|
|
+.split-handler:hover {
|
|
|
+ background: #409eff;
|
|
|
+}
|
|
|
+</style>
|