Files
JoyD/Web/Vue/CubeLib/src/components/CubeSplitter.vue
2026-01-30 10:24:50 +08:00

432 lines
13 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
// 修复props和emit的使用方式
const props = defineProps({
direction: {
type: String,
default: 'vertical', // 'vertical' 或 'horizontal'
validator: (value) => ['vertical', 'horizontal'].includes(value)
},
position: {
type: String,
default: 'left', // 'left', 'right', 'top' 或 'bottom'
validator: (value) => ['left', 'right', 'top', 'bottom'].includes(value)
},
initialSize: {
type: Number,
default: null
},
minSize: {
type: Number,
default: 200
},
maxSize: {
type: Number,
default: 1000
}
})
const emit = defineEmits(['resize', 'resizeEnd', 'handleClick', 'collapse', 'expand']) // 添加collapse和expand事件
const dividerRef = ref(null)
const isResizing = ref(false)
let startX = 0
let startY = 0
let startWidth = 0
let startHeight = 0
// 使用 ref 管理 currentSize 状态
// 注意:具体初始化值将在 onMounted 中设置,确保能正确访问 props
const currentSize = ref(0)
// 使用 ref 管理 isCollapsed 状态,与 isResizing 保持一致
const isCollapsed = ref(false) // 用于跟踪面板是否处于折叠状态
// 添加统一的边界检查函数
const clampSize = (size, min, max) => {
return Math.max(min, Math.min(max, size))
}
// 计算是否处于有效折叠状态(考虑右侧面板的边框影响)
const isPanelEffectivelyCollapsed = (elementSize, minSize) => {
// 对于右侧分隔条,需要特殊处理最小尺寸的判断
// 因为右侧面板有 border-left: 1px即使宽度为 0offsetWidth 也可能返回 1
return elementSize <= minSize + 1
}
// 计算鼠标移动的 delta 值
const calculateDelta = (e) => {
const deltaX = e.clientX - startX
const deltaY = e.clientY - startY
return { deltaX, deltaY }
}
// 获取目标元素
const getTargetElement = () => {
// 边界检查确保dividerRef.value存在
if (!dividerRef.value) {
console.warn('可调整分隔条dividerRef 不可用')
return null
}
// 根据position属性获取正确的相邻元素
let targetElement = null
if (props.position === 'left' || props.position === 'top') {
targetElement = dividerRef.value.previousElementSibling
} else if (props.position === 'right' || props.position === 'bottom') {
targetElement = dividerRef.value.nextElementSibling
}
return targetElement
}
// 计算新的尺寸
const calculateNewSize = (startSize, delta, minSize, maxSize, position) => {
let newSize = startSize
if (position === 'left' || position === 'top') {
// 左侧或顶部分隔条:向左/上拖减小尺寸,向右/下拖增大尺寸
// 当向左拖动时delta 为负数startSize + delta 会减小
// 当向右拖动时delta 为正数startSize + delta 会增大
newSize = startSize + delta
} else if (position === 'right' || position === 'bottom') {
// 右侧或底部分隔条:向左/上拖减小尺寸,向右/下拖增大尺寸
// 当向左拖动时delta 为负数startSize - delta 会增大
// 当向右拖动时delta 为正数startSize - delta 会减小
// 注意:这里的逻辑与左侧相反,因为右侧分隔条控制的是右侧面板
newSize = startSize - delta
}
return Math.max(minSize, Math.min(maxSize, newSize))
}
const onMouseDown = (e) => {
// 边界检查确保dividerRef.value存在
if (!dividerRef.value) {
console.warn('可调整分隔条dividerRef 不可用')
return
}
isResizing.value = true
startX = e.clientX
startY = e.clientY
// 获取目标元素
const targetElement = getTargetElement()
// 边界检查确保targetElement存在
if (!targetElement) {
console.warn('可调整分隔条:未找到目标元素')
isResizing.value = false
return
}
// 获取相邻元素的初始尺寸
startWidth = targetElement.offsetWidth
startHeight = targetElement.offsetHeight
// 注意:不再重新初始化 currentSize避免覆盖拖拽时的更新值
// 阻止默认行为,防止选中文本
e.preventDefault()
// 添加事件监听器到document
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
document.body.style.cursor = props.direction === 'vertical' ? 'col-resize' : 'row-resize'
// 添加拖拽样式
dividerRef.value.classList.add('resizing')
}
const onHandleClick = (e) => {
try {
// 阻止事件冒泡
e.stopPropagation()
// 边界检查确保dividerRef.value存在
if (!dividerRef.value) {
console.warn('可调整分隔条dividerRef 不可用')
return
}
// 添加简单的视觉反馈
dividerRef.value.classList.add('collapsing')
setTimeout(() => {
dividerRef.value?.classList.remove('collapsing')
}, 300)
// 获取目标元素
const targetElement = getTargetElement()
// 边界检查确保targetElement存在
if (!targetElement) {
console.warn('可调整分隔条:未找到目标元素')
return
}
// 获取目标元素的当前尺寸
const currentElementSize = props.direction === 'vertical' ? targetElement.offsetWidth : targetElement.offsetHeight
// 使用统一的尺寸判断逻辑
const effectivelyCollapsed = isPanelEffectivelyCollapsed(currentElementSize, props.minSize)
// 计算切换后的尺寸
let newSize = 0
if (!effectivelyCollapsed) {
// 当前尺寸大于最小尺寸,切换到最小尺寸(折叠)
currentSize.value = clampSize(currentElementSize, props.minSize, props.maxSize) // 保存当前尺寸,以便稍后恢复
newSize = props.minSize
isCollapsed.value = true
// 只触发折叠事件,让父组件根据需要处理尺寸更新
emit('collapse', newSize)
} else {
// 当前尺寸等于最小尺寸,切换到之前保存的尺寸(展开)
// 确保 restoreSize 有合理的值
const restoreSize = clampSize(
currentSize.value > props.minSize ? currentSize.value :
(props.initialSize && props.initialSize > props.minSize ? props.initialSize : 200),
props.minSize, props.maxSize
)
newSize = restoreSize
isCollapsed.value = false
// 只触发展开事件,让父组件根据需要处理尺寸更新
emit('expand', newSize)
}
} catch (error) {
console.error('可调整分隔条:处理单击事件时出错:', error)
}
}
const onMouseMove = (e) => {
if (!isResizing.value) return
// 计算鼠标移动的 delta 值
const { deltaX, deltaY } = calculateDelta(e)
let newSize = 0
if (props.direction === 'vertical') {
// 垂直分隔条:计算新的宽度
newSize = calculateNewSize(startWidth, deltaX, props.minSize, props.maxSize, props.position)
// 获取目标元素的实际尺寸
const targetElement = getTargetElement()
const currentElementSize = targetElement ? targetElement.offsetWidth : newSize
// 使用统一的尺寸判断逻辑
const effectivelyCollapsed = isPanelEffectivelyCollapsed(currentElementSize, props.minSize)
// 如果不是折叠状态,更新 currentSize
if (!effectivelyCollapsed) {
currentSize.value = clampSize(newSize, props.minSize, props.maxSize)
isCollapsed.value = false
} else {
isCollapsed.value = true
}
emit('resize', newSize)
} else {
// 水平分隔条:计算新的高度
newSize = calculateNewSize(startHeight, deltaY, props.minSize, props.maxSize, props.position)
// 获取目标元素的实际尺寸
const targetElement = getTargetElement()
const currentElementSize = targetElement ? targetElement.offsetHeight : newSize
// 使用统一的尺寸判断逻辑
const effectivelyCollapsed = isPanelEffectivelyCollapsed(currentElementSize, props.minSize)
// 如果不是折叠状态,更新 currentSize
if (!effectivelyCollapsed) {
currentSize.value = clampSize(newSize, props.minSize, props.maxSize)
isCollapsed.value = false
} else {
isCollapsed.value = true
}
emit('resize', newSize)
}
}
const onMouseUp = (e) => {
isResizing.value = false
// 计算鼠标移动的 delta 值
const deltaX = e.clientX - startX
const deltaY = e.clientY - startY
// 计算最终尺寸
const finalSize = props.direction === 'vertical' ?
calculateNewSize(startWidth, deltaX, props.minSize, props.maxSize, props.position) :
calculateNewSize(startHeight, deltaY, props.minSize, props.maxSize, props.position)
// 获取目标元素的实际尺寸
const targetElement = getTargetElement()
const currentElementSize = targetElement ?
(props.direction === 'vertical' ? targetElement.offsetWidth : targetElement.offsetHeight) :
finalSize
// 使用统一的尺寸判断逻辑
const effectivelyCollapsed = isPanelEffectivelyCollapsed(currentElementSize, props.minSize)
// 如果不是折叠状态,更新 currentSize
if (!effectivelyCollapsed) {
currentSize.value = clampSize(finalSize, props.minSize, props.maxSize)
}
// 触发拖拽结束事件
emit('resizeEnd', finalSize)
// 移除事件监听器
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.body.style.cursor = ''
// 移除拖拽样式
if (dividerRef.value) {
dividerRef.value.classList.remove('resizing')
}
}
onMounted(() => {
// 初始化 currentSize 为合理的默认值
if (props.initialSize && props.initialSize > props.minSize) {
currentSize.value = clampSize(props.initialSize, props.minSize, props.maxSize)
isCollapsed.value = false
} else {
currentSize.value = 200 // 默认值
isCollapsed.value = false
}
// 初始化时设置初始尺寸
if (props.initialSize) {
emit('resize', props.initialSize)
}
})
onUnmounted(() => {
// 清理全局事件监听器,避免内存泄漏
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
// 清理DOM相关资源
if (dividerRef.value) {
// 移除拖拽和折叠样式
dividerRef.value.classList.remove('resizing', 'collapsing')
}
// 清理全局样式
document.body.style.cursor = ''
// 重置组件状态
isResizing.value = false
isCollapsed.value = false
// Vue的响应式数据会自动清理无需额外处理
})
</script>
<template>
<div
ref="dividerRef"
class="cube-splitter"
:class="direction"
@mousedown="onMouseDown"
>
<div class="splitter-handle" @click.stop.prevent="onHandleClick"></div>
</div>
</template>
<style scoped>
/* CSS 变量定义 */
:root {
--cube-splitter-color: #e0e0e0;
--cube-splitter-hover-color: #667eea;
--cube-splitter-active-color: #5568d3;
--cube-splitter-handle-color: #fff;
--cube-splitter-handle-shadow: 0 0 4px rgba(0, 0, 0, 0.2);
--cube-splitter-collapsing-color: #667eea;
--cube-splitter-collapsing-shadow: 0 0 10px rgba(102, 126, 234, 0.5);
--cube-splitter-transition-speed: 0.3s;
}
.cube-splitter {
position: relative;
background-color: var(--cube-splitter-color);
display: flex;
align-items: center;
justify-content: center;
/* 确保可以接收鼠标事件 */
cursor: pointer;
user-select: none;
flex-shrink: 0;
z-index: 100;
}
.cube-splitter:hover {
background-color: var(--cube-splitter-hover-color);
}
.cube-splitter.vertical {
width: 6px;
height: 100%;
cursor: col-resize;
}
.cube-splitter.horizontal {
width: 100%;
height: 6px;
cursor: row-resize;
}
.splitter-handle {
position: absolute;
opacity: 0;
}
.cube-splitter.vertical .splitter-handle {
width: 4px;
height: 40px;
background-color: var(--cube-splitter-handle-color);
box-shadow: var(--cube-splitter-handle-shadow);
cursor: pointer;
}
.cube-splitter.horizontal .splitter-handle {
width: 40px;
height: 4px;
background-color: var(--cube-splitter-handle-color);
box-shadow: var(--cube-splitter-handle-shadow);
cursor: pointer;
}
.cube-splitter:hover .splitter-handle {
opacity: 1;
}
/* 拖拽时的样式 */
.cube-splitter:active,
.cube-splitter.resizing {
background-color: var(--cube-splitter-active-color);
transition: none;
}
.cube-splitter:active .splitter-handle,
.cube-splitter.resizing .splitter-handle {
opacity: 1;
transition: none;
}
/* 折叠/展开时的视觉反馈 */
.cube-splitter.collapsing {
background-color: var(--cube-splitter-collapsing-color);
transition: background-color var(--cube-splitter-transition-speed) ease;
box-shadow: var(--cube-splitter-collapsing-shadow);
}
.cube-splitter.collapsing .splitter-handle {
opacity: 1;
transform: scale(1.1);
transition: transform var(--cube-splitter-transition-speed) ease;
box-shadow: 0 0 8px rgba(102, 126, 234, 0.8);
}
</style>