增加Splitter
This commit is contained in:
426
Web/Vue/CubeLib/src/components/CubeSplitter.vue
Normal file
426
Web/Vue/CubeLib/src/components/CubeSplitter.vue
Normal file
@@ -0,0 +1,426 @@
|
||||
<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,即使宽度为 0,offsetWidth 也可能返回 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' || 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>
|
||||
@@ -1,8 +1,10 @@
|
||||
import { App, defineComponent } from 'vue'
|
||||
import CubeButton from './components/CubeButton.vue'
|
||||
import CubeSplitter from './components/CubeSplitter.vue'
|
||||
|
||||
const components = {
|
||||
CubeButton
|
||||
CubeButton,
|
||||
CubeSplitter
|
||||
}
|
||||
|
||||
const install = (app: App) => {
|
||||
@@ -17,4 +19,4 @@ const CubeLib = {
|
||||
}
|
||||
|
||||
export default CubeLib
|
||||
export { CubeButton }
|
||||
export { CubeButton, CubeSplitter }
|
||||
Reference in New Issue
Block a user