Parcourir la source

Add 添加分割拖拽面板组件

Yue il y a 1 semaine
Parent
commit
1a9872abc4
1 fichiers modifiés avec 231 ajouts et 0 suppressions
  1. 231 0
      UI/VAP_V3.VUE/src/components/split-panel/VbSplitPanel.vue

+ 231 - 0
UI/VAP_V3.VUE/src/components/split-panel/VbSplitPanel.vue

@@ -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>