| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275 |
- <script setup lang="ts">
- const props = withDefaults(
- defineProps<{
- mode?: "h" | "v" | "horizontal" | "vertical" // 布局方向
- isDrag?: boolean // 是否可以拖拽
- size?: number | string // 初始尺寸,支持像素或百分比(如 '50%')
- minSize?: number | string // 最小尺寸,支持像素或百分比(如 '10%')
- maxSize?: number | string // 最大尺寸,支持像素或百分比(如 '80%')
- containerSize?: string | number // 容器尺寸
- panelStyle?: object // 面板样式,支持内联样式
- panelItemStyle?: object // 面板项样式,支持内联样式
- firstPanelItemStyle?: object // 第1个面板项样式,支持内联样式
- secondPanelItemStyle?: object // 第2个面板项样式,支持内联样式
- handlerBg?: string // 拖拽条背景色
- handlerHoverBg?: string // 拖拽条悬停背景色
- handlerWidth?: number // 拖拽条宽度
- handlerMargin?: number // 拖拽条边距
- }>(),
- {
- mode: "h",
- isDrag: true,
- size: 200,
- minSize: 100,
- maxSize: 2000,
- containerSize: "100%",
- handlerBg: "#e5e7eb",
- handlerHoverBg: "#409eff",
- handlerWidth: 6,
- handlerMargin: 2
- }
- )
- const emits = defineEmits<{
- (e: "dragComplete", n1: number, n2: number): void
- }>()
- const panelRef = ref(null)
- const isDragging = ref(false)
- const parentSize = ref(0)
- const firstPanelSize = ref<number>(
- typeof props.size === "number" ? props.size : parseFloat(props.size as string) || 200
- )
- const secondPanelSize = computed(() =>
- Math.floor(
- parentSize.value - firstPanelSize.value - (props.handlerWidth || 6) - (props.handlerMargin || 2)
- )
- )
- 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 Math.floor((parentSize * 90) / 100)
- } else if (percentage < 0) {
- console.warn(`VbSplitPanel:${value} 不能小于 0,已设置为 10%`)
- return Math.floor((parentSize * 10) / 100)
- } else {
- return Math.floor((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 = Math.floor(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 {
- ...(props.panelStyle || {}),
- "--split-handler-bg": props.handlerBg || "#e5e7eb",
- "--split-handler-hover-bg": props.isDrag
- ? props.handlerHoverBg || "#409eff"
- : "var(--split-handler-bg)",
- "--split-handler-width": (props.handlerWidth || 6) + "px",
- "--split-handler-margin": (props.handlerMargin || 2) + "px",
- "--split-handler-cursor": props.isDrag
- ? realMode.value === "horizontal"
- ? "col-resize"
- : "row-resize"
- : "default",
- [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(firstPanelSize.value, parentSize.value)
- return {
- ...(props.panelItemStyle || {}),
- ...(props.firstPanelItemStyle || {}),
- ...(realMode.value === "horizontal"
- ? { width: `${pixelSize}px` }
- : { height: `${pixelSize}px` }),
- flex: "none"
- }
- })
- const secondPanelStyle = computed(() => {
- return {
- ...(props.panelItemStyle || {}),
- ...(props.secondPanelItemStyle || {}),
- ...(realMode.value === "horizontal"
- ? { width: `${secondPanelSize.value}px` }
- : { height: `${secondPanelSize.value}px` }),
- flex: 1
- }
- })
- // 拖拽逻辑
- const startDrag = (e) => {
- if (!props.isDrag) return
- e.preventDefault()
- isDragging.value = true
- const startPos = realMode.value === "horizontal" ? e.clientX : e.clientY
- // 确保 start 是数值类型
- const start =
- typeof firstPanelSize.value === "number"
- ? firstPanelSize.value
- : parseFloat(firstPanelSize.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.floor(Math.min(size, parentSize.value - minPixelSize))
- firstPanelSize.value = size
- }
- const stop = () => {
- isDragging.value = false
- document.removeEventListener("mousemove", onMove)
- document.removeEventListener("mouseup", stop)
- nextTick(() => {
- emits("dragComplete", firstPanelSize.value, secondPanelSize.value)
- })
- }
- document.addEventListener("mousemove", onMove)
- document.addEventListener("mouseup", stop)
- }
- onMounted(() => {
- getParentSize()
- // 在父元素尺寸获取后,重新计算初始尺寸
- if (parentSize.value > 0) {
- firstPanelSize.value = parseSizeValue(props.size, parentSize.value)
- }
- })
- watch(() => [props.mode, props.containerSize], getParentSize)
- // 监听 size 属性变化
- watch(
- () => props.size,
- (newSize) => {
- if (parentSize.value > 0) {
- firstPanelSize.value = parseSizeValue(newSize, parentSize.value)
- }
- }
- )
- </script>
- <template>
- <div class="split-panel" ref="panelRef" :class="modeClass" :style="containerStyle">
- <div class="panel-item" :style="firstPanelStyle">
- <slot name="first"></slot>
- </div>
- <div class="split-handler" :class="handlerClass" @mousedown="startDrag"></div>
- <div class="panel-item" :style="secondPanelStyle">
- <slot name="second"></slot>
- </div>
- </div>
- </template>
- <style scoped lang="scss">
- .split-panel {
- position: relative;
- background: transparent;
- --split-handler-bg: #e5e7eb;
- --split-handler-hover-bg: #409eff;
- --split-handler-width: 6px;
- --split-handler-margin: 2px;
- --split-handler-cursor: row-resize;
- &.horizontal {
- flex-direction: row;
- }
- &.vertical {
- flex-direction: column;
- }
- .panel-item {
- background: transparent;
- overflow: auto;
- }
- .split-handler {
- background: var(--split-handler-bg);
- transition: background 0.2s;
- z-index: 10;
- flex-shrink: 0;
- &.h-handler {
- width: var(--split-handler-width);
- height: 100%;
- cursor: var(--split-handler-cursor);
- margin: 0 var(--split-handler-margin);
- }
- &.v-handler {
- height: var(--split-handler-width);
- width: 100%;
- cursor: var(--split-handler-cursor);
- margin: var(--split-handler-margin) 0;
- }
- &:hover {
- background: var(--split-handler-hover-bg);
- }
- }
- }
- </style>
|