test
This commit is contained in:
parent
3b690b03a7
commit
dfd3d962c7
@ -1,43 +1,94 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* SplitLayout 组件
|
||||
*
|
||||
* 一个可调整大小的分割面板布局组件,支持水平和垂直方向的分割。
|
||||
* 通过拖拽分隔条可以调整各个面板的大小。
|
||||
* 使用provide/inject机制与子组件SplitPanel通信。
|
||||
*/
|
||||
import { ref, provide, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 分割方向
|
||||
* - horizontal: 水平分割,面板左右排列
|
||||
* - vertical: 垂直分割,面板上下排列
|
||||
*/
|
||||
direction: {
|
||||
type: String,
|
||||
default: 'horizontal', // 'horizontal' or 'vertical'
|
||||
validator: (value: string) => ['horizontal', 'vertical'].includes(value),
|
||||
},
|
||||
/**
|
||||
* 分隔条的大小(像素)
|
||||
* 控制分隔条的宽度(水平方向)或高度(垂直方向)
|
||||
*/
|
||||
gutterSize: {
|
||||
type: Number,
|
||||
default: 4,
|
||||
},
|
||||
/**
|
||||
* 面板的默认最小尺寸(像素)
|
||||
* 当SplitPanel未指定minSize时使用此值
|
||||
*/
|
||||
minSize: {
|
||||
type: Number,
|
||||
default: 50, // 最小面板尺寸(像素)
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['resize', 'resizeEnd'])
|
||||
// 定义组件事件
|
||||
const emit = defineEmits([
|
||||
/** 调整大小过程中触发 */
|
||||
'resize',
|
||||
/** 调整大小结束时触发 */
|
||||
'resizeEnd',
|
||||
])
|
||||
|
||||
// 分割器容器的DOM引用
|
||||
const splitterRef = ref<HTMLElement | null>(null)
|
||||
// 使用vueuse的useElementSize获取容器的宽高
|
||||
const { width, height } = useElementSize(splitterRef)
|
||||
|
||||
// 计算属性:是否为水平方向
|
||||
const isHorizontal = computed(() => props.direction === 'horizontal')
|
||||
// 存储所有面板的信息
|
||||
const panes = ref<Array<{ id: string; size: number; minSize: number; maxSize: number }>>([])
|
||||
const dragging = ref(false)
|
||||
const activePaneIndex = ref(-1)
|
||||
const startPosition = ref(0)
|
||||
const startSizes = ref<number[]>([])
|
||||
// 拖拽状态相关变量
|
||||
const dragging = ref(false) // 是否正在拖拽
|
||||
const activePaneIndex = ref(-1) // 当前活动的面板索引
|
||||
const startPosition = ref(0) // 拖拽开始时的鼠标位置
|
||||
const startSizes = ref<number[]>([]) // 拖拽开始时各面板的尺寸
|
||||
|
||||
// 提供给Pane组件的上下文
|
||||
/**
|
||||
* 提供给SplitPanel子组件的上下文
|
||||
* 通过provide机制向子组件提供方法和状态,实现父子组件通信
|
||||
*/
|
||||
provide('splitter-context', {
|
||||
// 提供分割方向的计算属性,子组件可以根据方向调整样式
|
||||
direction: computed(() => props.direction),
|
||||
// 提供panes数组的引用,使子组件可以访问最新的尺寸数据
|
||||
panes, // 提供panes数组的引用,使Pane组件可以访问最新的尺寸
|
||||
/**
|
||||
* 注册面板方法
|
||||
* 子组件挂载时调用此方法注册自身到父组件
|
||||
* @param id 面板唯一标识
|
||||
* @param initialSize 初始尺寸百分比
|
||||
* @param minSize 最小尺寸(像素)
|
||||
* @param maxSize 最大尺寸(像素)
|
||||
* @returns 面板在数组中的索引
|
||||
*/
|
||||
registerPane: (id: string, initialSize: number, minSize: number, maxSize: number) => {
|
||||
panes.value.push({ id, size: initialSize, minSize, maxSize })
|
||||
return panes.value.length - 1 // 返回索引
|
||||
},
|
||||
/**
|
||||
* 注销面板方法
|
||||
* 子组件卸载时调用此方法从父组件移除自身
|
||||
* @param index 面板在数组中的索引
|
||||
*/
|
||||
unregisterPane: (index: number) => {
|
||||
if (index >= 0 && index < panes.value.length) {
|
||||
panes.value.splice(index, 1)
|
||||
@ -45,11 +96,14 @@ provide('splitter-context', {
|
||||
},
|
||||
})
|
||||
|
||||
// 计算每个面板的尺寸百分比
|
||||
/**
|
||||
* 计算每个面板的尺寸百分比
|
||||
* 处理自动分配尺寸和确保总和为100%
|
||||
*/
|
||||
const calculateSizes = () => {
|
||||
if (panes.value.length === 0) return
|
||||
|
||||
// 如果没有设置尺寸,平均分配
|
||||
// 找出所有未设置尺寸的面板(size <= 0)
|
||||
const unsetPanes = panes.value.filter((pane) => pane.size <= 0)
|
||||
if (unsetPanes.length > 0) {
|
||||
const totalSetSize = panes.value.reduce((sum, pane) => sum + (pane.size > 0 ? pane.size : 0), 0)
|
||||
@ -63,7 +117,7 @@ const calculateSizes = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 确保总和为100%
|
||||
// 计算所有面板尺寸总和,确保总和为100%
|
||||
const totalSize = panes.value.reduce((sum, pane) => sum + pane.size, 0)
|
||||
if (totalSize !== 100) {
|
||||
const ratio = 100 / totalSize
|
||||
@ -73,7 +127,12 @@ const calculateSizes = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 处理拖动开始
|
||||
/**
|
||||
* 处理拖动开始事件
|
||||
* 当用户点击分隔条开始拖拽时触发
|
||||
* @param index 分隔条对应的面板索引
|
||||
* @param event 鼠标事件对象
|
||||
*/
|
||||
const handleDragStart = (index: number, event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
dragging.value = true
|
||||
@ -91,7 +150,11 @@ const handleDragStart = (index: number, event: MouseEvent) => {
|
||||
document.addEventListener('mouseup', handleDragEnd)
|
||||
}
|
||||
|
||||
// 处理拖动移动
|
||||
/**
|
||||
* 处理拖动移动事件
|
||||
* 当用户拖动分隔条时持续触发,计算并更新面板尺寸
|
||||
* @param event 鼠标事件对象
|
||||
*/
|
||||
const handleDragMove = (event: MouseEvent) => {
|
||||
if (!dragging.value || activePaneIndex.value < 0) return
|
||||
|
||||
@ -103,17 +166,18 @@ const handleDragMove = (event: MouseEvent) => {
|
||||
const containerSize = isHorizontal.value ? width.value : height.value
|
||||
const deltaPercent = (delta / containerSize) * 100
|
||||
|
||||
// 当前面板和下一个面板
|
||||
// 获取当前正在调整的面板和下一个面板
|
||||
const currentPane = panes.value[activePaneIndex.value]
|
||||
const nextPane = panes.value[activePaneIndex.value + 1]
|
||||
|
||||
if (!currentPane || !nextPane) return
|
||||
|
||||
// 计算新尺寸
|
||||
// 根据鼠标移动距离计算新的面板尺寸百分比
|
||||
// 当前面板尺寸增加,下一个面板尺寸减少,保持总尺寸不变
|
||||
let newCurrentSize = startSizes.value[activePaneIndex.value] + deltaPercent
|
||||
let newNextSize = startSizes.value[activePaneIndex.value + 1] - deltaPercent
|
||||
|
||||
// 检查最小尺寸限制
|
||||
// 将像素单位的最小尺寸转换为百分比,用于限制面板大小
|
||||
const currentMinPercent = (currentPane.minSize / containerSize) * 100
|
||||
const nextMinPercent = (nextPane.minSize / containerSize) * 100
|
||||
|
||||
@ -131,7 +195,8 @@ const handleDragMove = (event: MouseEvent) => {
|
||||
nextMinPercent
|
||||
}
|
||||
|
||||
// 检查最大尺寸限制
|
||||
// 检查最大尺寸限制(如果设置了maxSize > 0)
|
||||
// 确保面板尺寸不超过最大限制
|
||||
if (currentPane.maxSize > 0) {
|
||||
const currentMaxPercent = (currentPane.maxSize / containerSize) * 100
|
||||
if (newCurrentSize > currentMaxPercent) {
|
||||
@ -154,35 +219,49 @@ const handleDragMove = (event: MouseEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新尺寸
|
||||
// 应用计算后的新尺寸到面板数组
|
||||
panes.value[activePaneIndex.value].size = newCurrentSize
|
||||
panes.value[activePaneIndex.value + 1].size = newNextSize
|
||||
|
||||
// 触发resize事件,将所有面板的最新尺寸信息传递给父组件
|
||||
emit(
|
||||
'resize',
|
||||
panes.value.map((pane) => ({ id: pane.id, size: pane.size })),
|
||||
)
|
||||
}
|
||||
|
||||
// 处理拖动结束
|
||||
/**
|
||||
* 处理拖动结束事件
|
||||
* 当用户释放鼠标按钮结束拖拽时触发
|
||||
*/
|
||||
const handleDragEnd = () => {
|
||||
dragging.value = false
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
|
||||
// 如果有活动面板,触发resizeEnd事件并重置活动面板索引
|
||||
if (activePaneIndex.value >= 0) {
|
||||
// 触发resizeEnd事件,将所有面板的最终尺寸信息传递给父组件
|
||||
emit(
|
||||
'resizeEnd',
|
||||
panes.value.map((pane) => ({ id: pane.id, size: pane.size })),
|
||||
)
|
||||
// 重置活动面板索引
|
||||
activePaneIndex.value = -1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件挂载完成后,计算初始面板尺寸
|
||||
*/
|
||||
onMounted(() => {
|
||||
calculateSizes()
|
||||
})
|
||||
|
||||
/**
|
||||
* 组件卸载前,移除全局事件监听器
|
||||
* 防止内存泄漏和事件冲突
|
||||
*/
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('mousemove', handleDragMove)
|
||||
document.removeEventListener('mouseup', handleDragEnd)
|
||||
@ -190,6 +269,7 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 分割器容器,根据方向应用不同的样式类 -->
|
||||
<div
|
||||
ref="splitterRef"
|
||||
class="splitter"
|
||||
@ -198,10 +278,12 @@ onBeforeUnmount(() => {
|
||||
'splitter--vertical': direction === 'vertical',
|
||||
}"
|
||||
>
|
||||
<!-- 插槽用于放置SplitPanel子组件 -->
|
||||
<slot></slot>
|
||||
|
||||
<!-- 分隔条 -->
|
||||
<!-- 分隔条:在每个面板之间渲染一个可拖拽的分隔条 -->
|
||||
<template v-for="(_, index) in panes" :key="index">
|
||||
<!-- 除了最后一个面板外,每个面板后面都有一个分隔条 -->
|
||||
<div
|
||||
v-if="index < panes.length - 1"
|
||||
class="splitter__gutter"
|
||||
@ -210,12 +292,15 @@ onBeforeUnmount(() => {
|
||||
'splitter__gutter--vertical': direction === 'vertical',
|
||||
}"
|
||||
:style="{
|
||||
/* 根据方向设置分隔条的宽度或高度 */
|
||||
[direction === 'horizontal' ? 'width' : 'height']: `${gutterSize}px`,
|
||||
/* 计算分隔条的位置:根据前面所有面板的尺寸百分比总和确定位置 */
|
||||
[direction === 'horizontal' ? 'left' : 'top']:
|
||||
`calc(${panes.slice(0, index + 1).reduce((sum, pane) => sum + pane.size, 0)}% - ${gutterSize / 2}px)`,
|
||||
}"
|
||||
@mousedown="handleDragStart(index, $event)"
|
||||
>
|
||||
<!-- 分隔条内部的把手,用于视觉提示和交互 -->
|
||||
<div class="splitter__gutter-handle"></div>
|
||||
</div>
|
||||
</template>
|
||||
@ -223,64 +308,75 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 分割器容器的基本样式 */
|
||||
.splitter {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex; /* 使用Flex布局排列子元素 */
|
||||
width: 100%; /* 容器宽度占满父元素 */
|
||||
height: 100%; /* 容器高度占满父元素 */
|
||||
position: relative; /* 相对定位,作为分隔条的定位参考 */
|
||||
overflow: hidden; /* 隐藏溢出内容 */
|
||||
}
|
||||
|
||||
/* 水平分割模式:子元素水平排列 */
|
||||
.splitter--horizontal {
|
||||
flex-direction: row;
|
||||
flex-direction: row; /* 子元素从左到右排列 */
|
||||
}
|
||||
|
||||
/* 垂直分割模式:子元素垂直排列 */
|
||||
.splitter--vertical {
|
||||
flex-direction: column;
|
||||
flex-direction: column; /* 子元素从上到下排列 */
|
||||
}
|
||||
|
||||
/* 分隔条的基本样式 */
|
||||
.splitter__gutter {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
transition: background-color 0.2s;
|
||||
position: absolute; /* 绝对定位,可以精确控制位置 */
|
||||
z-index: 1; /* 确保分隔条显示在面板上方 */
|
||||
display: flex; /* 使用Flex布局居中把手 */
|
||||
justify-content: center; /* 水平居中把手 */
|
||||
align-items: center; /* 垂直居中把手 */
|
||||
background-color: transparent; /* 默认透明背景 */
|
||||
transition: background-color 0.2s; /* 背景色变化时的平滑过渡效果 */
|
||||
}
|
||||
|
||||
/* 分隔条悬停效果 */
|
||||
.splitter__gutter:hover,
|
||||
.splitter__gutter:active {
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
background-color: rgba(0, 0, 0, 0.1); /* 悬停时显示半透明背景 */
|
||||
}
|
||||
|
||||
/* 水平分隔条样式 */
|
||||
.splitter__gutter--horizontal {
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
height: 100%; /* 高度占满容器 */
|
||||
cursor: col-resize; /* 显示列调整光标,提示用户可以水平拖动 */
|
||||
}
|
||||
|
||||
/* 垂直分隔条样式 */
|
||||
.splitter__gutter--vertical {
|
||||
width: 100%;
|
||||
cursor: row-resize;
|
||||
width: 100%; /* 宽度占满容器 */
|
||||
cursor: row-resize; /* 显示行调整光标,提示用户可以垂直拖动 */
|
||||
}
|
||||
|
||||
/* 分隔条把手的基本样式 */
|
||||
.splitter__gutter-handle {
|
||||
background-color: #e8e8e8;
|
||||
transition: background-color 0.2s;
|
||||
background-color: #e8e8e8; /* 把手的默认背景色 */
|
||||
transition: background-color 0.2s; /* 背景色变化时的平滑过渡效果 */
|
||||
}
|
||||
|
||||
/* 分隔条悬停时把手的样式 */
|
||||
.splitter__gutter:hover .splitter__gutter-handle,
|
||||
.splitter__gutter:active .splitter__gutter-handle {
|
||||
background-color: #1890ff;
|
||||
background-color: #1890ff; /* 悬停时把手变为蓝色 */
|
||||
}
|
||||
|
||||
/* 水平分隔条把手的尺寸 */
|
||||
.splitter__gutter--horizontal .splitter__gutter-handle {
|
||||
width: 2px;
|
||||
height: 30px;
|
||||
width: 2px; /* 把手宽度 */
|
||||
height: 30px; /* 把手高度 */
|
||||
}
|
||||
|
||||
/* 垂直分隔条把手的尺寸 */
|
||||
.splitter__gutter--vertical .splitter__gutter-handle {
|
||||
width: 30px;
|
||||
height: 2px;
|
||||
width: 30px; /* 把手宽度 */
|
||||
height: 2px; /* 把手高度 */
|
||||
}
|
||||
</style>
|
||||
|
@ -1,26 +1,51 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* SplitPanel 组件
|
||||
*
|
||||
* 该组件作为SplitLayout的子组件,表示一个可调整大小的面板。
|
||||
* 通过provide/inject机制与SplitLayout父组件通信,实现面板的注册、尺寸计算和更新。
|
||||
*/
|
||||
import { ref, inject, computed, onMounted, onBeforeUnmount, useId } from 'vue'
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
/**
|
||||
* 面板的初始尺寸(百分比)
|
||||
* 值为0时表示自动分配剩余空间
|
||||
*/
|
||||
size: {
|
||||
type: Number,
|
||||
default: 0, // 0表示自动分配
|
||||
},
|
||||
/**
|
||||
* 面板的最小尺寸(像素)
|
||||
* 拖拽调整大小时不会小于此值
|
||||
*/
|
||||
minSize: {
|
||||
type: Number,
|
||||
default: 50, // 最小尺寸(像素)
|
||||
},
|
||||
/**
|
||||
* 面板的最大尺寸(像素)
|
||||
* 值为0时表示无限制
|
||||
*/
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 0, // 0表示无限制
|
||||
},
|
||||
})
|
||||
|
||||
// 生成唯一ID,用于在父组件中标识此面板
|
||||
const id = useId()
|
||||
// 面板DOM元素的引用
|
||||
const paneRef = ref<HTMLElement | null>(null)
|
||||
// 当前面板在父组件panes数组中的索引,初始为-1表示未注册
|
||||
const paneIndex = ref(-1)
|
||||
|
||||
// 注入Splitter提供的上下文
|
||||
/**
|
||||
* 注入SplitLayout提供的上下文
|
||||
* 通过inject获取父组件提供的方法和状态,实现组件间通信
|
||||
*/
|
||||
const splitterContext = inject('splitter-context') as {
|
||||
direction: { value: string }
|
||||
panes: { value: Array<{ id: string; size: number; minSize: number; maxSize: number }> }
|
||||
@ -28,13 +53,17 @@ const splitterContext = inject('splitter-context') as {
|
||||
unregisterPane: (index: number) => void
|
||||
}
|
||||
|
||||
// 计算样式
|
||||
/**
|
||||
* 计算面板样式
|
||||
* 根据方向和尺寸动态计算面板的CSS样式
|
||||
*/
|
||||
const paneStyle = computed(() => {
|
||||
const direction = splitterContext.direction.value
|
||||
const isHorizontal = direction === 'horizontal'
|
||||
|
||||
// 如果已注册并且能在panes数组中找到对应的面板,则使用panes中的size值
|
||||
// 否则使用props中的size值(初始渲染时)
|
||||
// 确定面板尺寸:
|
||||
// 1. 如果面板已注册到父组件且能在panes数组中找到,使用父组件计算后的size值
|
||||
// 2. 否则使用props中传入的初始size值(初始渲染时)
|
||||
let currentSize = props.size
|
||||
if (paneIndex.value >= 0 && paneIndex.value < splitterContext.panes.value.length) {
|
||||
currentSize = splitterContext.panes.value[paneIndex.value].size
|
||||
@ -50,13 +79,22 @@ const paneStyle = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 组件挂载时,向父组件注册此面板
|
||||
* 将面板的id、尺寸、最小尺寸和最大尺寸传递给父组件
|
||||
* 并获取返回的索引值,用于后续更新和注销
|
||||
*/
|
||||
onMounted(() => {
|
||||
// 注册面板
|
||||
// 注册面板,并保存返回的索引值
|
||||
paneIndex.value = splitterContext.registerPane(id, props.size, props.minSize, props.maxSize)
|
||||
})
|
||||
|
||||
/**
|
||||
* 组件卸载前,从父组件注销此面板
|
||||
* 确保面板被正确移除,避免内存泄漏和计算错误
|
||||
*/
|
||||
onBeforeUnmount(() => {
|
||||
// 注销面板
|
||||
// 注销面板,传入索引值
|
||||
splitterContext.unregisterPane(paneIndex.value)
|
||||
})
|
||||
</script>
|
||||
|
Loading…
x
Reference in New Issue
Block a user