This commit is contained in:
skycurtain 2025-03-19 16:58:19 +08:00
parent 516fcf6b82
commit 34833f6ae6
6 changed files with 476 additions and 15 deletions

View File

@ -1,10 +1,117 @@
<script setup lang="ts">
import { Splitter } from './components/Splitter'
import { Splitter, Pane } from './components'
</script>
<template>
<div>Splitter</div>
<Splitter />
<div class="container">
<h1>Splitter 组件示例</h1>
<h2>水平分割</h2>
<div class="demo-box">
<Splitter direction="horizontal">
<Pane :size="30" :min-size="100">
<div class="pane-content">
<h3>左侧面板</h3>
<p>这是左侧面板的内容初始宽度为30%最小宽度为100px</p>
</div>
</Pane>
<Pane>
<div class="pane-content">
<h3>中间面板</h3>
<p>这是中间面板的内容宽度自动分配</p>
</div>
</Pane>
<Pane :size="30">
<div class="pane-content">
<h3>右侧面板</h3>
<p>这是右侧面板的内容初始宽度为30%</p>
</div>
</Pane>
</Splitter>
</div>
<h2>垂直分割</h2>
<div class="demo-box">
<Splitter direction="vertical">
<Pane :size="40">
<div class="pane-content">
<h3>上方面板</h3>
<p>这是上方面板的内容初始高度为40%</p>
</div>
</Pane>
<Pane>
<div class="pane-content">
<h3>下方面板</h3>
<p>这是下方面板的内容高度自动分配</p>
</div>
</Pane>
</Splitter>
</div>
<h2>嵌套分割</h2>
<div class="demo-box">
<Splitter direction="horizontal">
<Pane :size="30">
<div class="pane-content">
<h3>左侧面板</h3>
<p>这是左侧面板的内容</p>
</div>
</Pane>
<Pane :size="70">
<Splitter direction="vertical">
<Pane :size="50">
<div class="pane-content">
<h3>右上面板</h3>
<p>这是右上面板的内容</p>
</div>
</Pane>
<Pane :size="50">
<div class="pane-content">
<h3>右下面板</h3>
<p>这是右下面板的内容</p>
</div>
</Pane>
</Splitter>
</Pane>
</Splitter>
</div>
</div>
</template>
<style scoped></style>
<style scoped>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
text-align: center;
margin-bottom: 30px;
}
h2 {
margin-top: 30px;
margin-bottom: 15px;
}
.demo-box {
border: 1px solid #e8e8e8;
border-radius: 4px;
height: 300px;
margin-bottom: 30px;
}
.pane-content {
padding: 16px;
height: 100%;
box-sizing: border-box;
background-color: #f5f5f5;
}
.pane-content h3 {
margin-top: 0;
margin-bottom: 10px;
color: #1890ff;
}
</style>

74
src/components/Pane.vue Normal file
View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import { ref, inject, computed, onMounted, onBeforeUnmount, useId } from 'vue'
const props = defineProps({
size: {
type: Number,
default: 0, // 0
},
minSize: {
type: Number,
default: 50, //
},
maxSize: {
type: Number,
default: 0, // 0
},
})
const id = useId()
const paneRef = ref<HTMLElement | null>(null)
const paneIndex = ref(-1)
// Splitter
const splitterContext = inject('splitter-context') as {
direction: { value: string }
panes: { value: Array<{ id: string; size: number; minSize: number; maxSize: number }> }
registerPane: (id: string, size: number, minSize: number, maxSize: number) => number
unregisterPane: (index: number) => void
}
//
const paneStyle = computed(() => {
const direction = splitterContext.direction.value
const isHorizontal = direction === 'horizontal'
// panes使panessize
// 使propssize
let currentSize = props.size
if (paneIndex.value >= 0 && paneIndex.value < splitterContext.panes.value.length) {
currentSize = splitterContext.panes.value[paneIndex.value].size
}
return {
[isHorizontal ? 'width' : 'height']: `${currentSize}%`,
[isHorizontal ? 'height' : 'width']: '100%',
flexShrink: 0,
flexGrow: 0,
overflow: 'auto',
position: 'relative' as const,
}
})
onMounted(() => {
//
paneIndex.value = splitterContext.registerPane(id, props.size, props.minSize, props.maxSize)
})
onBeforeUnmount(() => {
//
splitterContext.unregisterPane(paneIndex.value)
})
</script>
<template>
<div ref="paneRef" class="pane" :style="paneStyle">
<slot></slot>
</div>
</template>
<style scoped>
.pane {
box-sizing: border-box;
}
</style>

View File

@ -1,11 +0,0 @@
import { defineComponent, h } from 'vue'
export const Splitter = defineComponent({
props: {},
emits: {},
setup() {
return () => {
return h('div', () => '123')
}
},
})

286
src/components/Splitter.vue Normal file
View File

@ -0,0 +1,286 @@
<script setup lang="ts">
import { ref, provide, computed, onMounted, onBeforeUnmount } from 'vue'
import { useElementSize } from '@vueuse/core'
const props = defineProps({
direction: {
type: String,
default: 'horizontal', // 'horizontal' or 'vertical'
validator: (value: string) => ['horizontal', 'vertical'].includes(value),
},
gutterSize: {
type: Number,
default: 4,
},
minSize: {
type: Number,
default: 50, //
},
})
const emit = defineEmits(['resize', 'resizeEnd'])
const splitterRef = ref<HTMLElement | null>(null)
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[]>([])
// Pane
provide('splitter-context', {
direction: computed(() => props.direction),
panes, // panes使Pane访
registerPane: (id: string, initialSize: number, minSize: number, maxSize: number) => {
panes.value.push({ id, size: initialSize, minSize, maxSize })
return panes.value.length - 1 //
},
unregisterPane: (index: number) => {
if (index >= 0 && index < panes.value.length) {
panes.value.splice(index, 1)
}
},
})
//
const calculateSizes = () => {
if (panes.value.length === 0) return
//
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)
const remainingSize = 100 - totalSetSize
const sizePerPane = remainingSize / unsetPanes.length
panes.value.forEach((pane) => {
if (pane.size <= 0) {
pane.size = sizePerPane
}
})
}
// 100%
const totalSize = panes.value.reduce((sum, pane) => sum + pane.size, 0)
if (totalSize !== 100) {
const ratio = 100 / totalSize
panes.value.forEach((pane) => {
pane.size = pane.size * ratio
})
}
}
//
const handleDragStart = (index: number, event: MouseEvent) => {
event.preventDefault()
dragging.value = true
activePaneIndex.value = index
if (isHorizontal.value) {
startPosition.value = event.clientX
} else {
startPosition.value = event.clientY
}
startSizes.value = panes.value.map((pane) => pane.size)
document.addEventListener('mousemove', handleDragMove)
document.addEventListener('mouseup', handleDragEnd)
}
//
const handleDragMove = (event: MouseEvent) => {
if (!dragging.value || activePaneIndex.value < 0) return
const currentPosition = isHorizontal.value ? event.clientX : event.clientY
const delta = currentPosition - startPosition.value
if (delta === 0) return
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
if (newCurrentSize < currentMinPercent) {
newCurrentSize = currentMinPercent
newNextSize =
startSizes.value[activePaneIndex.value] +
startSizes.value[activePaneIndex.value + 1] -
currentMinPercent
} else if (newNextSize < nextMinPercent) {
newNextSize = nextMinPercent
newCurrentSize =
startSizes.value[activePaneIndex.value] +
startSizes.value[activePaneIndex.value + 1] -
nextMinPercent
}
//
if (currentPane.maxSize > 0) {
const currentMaxPercent = (currentPane.maxSize / containerSize) * 100
if (newCurrentSize > currentMaxPercent) {
newCurrentSize = currentMaxPercent
newNextSize =
startSizes.value[activePaneIndex.value] +
startSizes.value[activePaneIndex.value + 1] -
currentMaxPercent
}
}
if (nextPane.maxSize > 0) {
const nextMaxPercent = (nextPane.maxSize / containerSize) * 100
if (newNextSize > nextMaxPercent) {
newNextSize = nextMaxPercent
newCurrentSize =
startSizes.value[activePaneIndex.value] +
startSizes.value[activePaneIndex.value + 1] -
nextMaxPercent
}
}
//
panes.value[activePaneIndex.value].size = newCurrentSize
panes.value[activePaneIndex.value + 1].size = newNextSize
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)
if (activePaneIndex.value >= 0) {
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)
})
</script>
<template>
<div
ref="splitterRef"
class="splitter"
:class="{
'splitter--horizontal': direction === 'horizontal',
'splitter--vertical': direction === 'vertical',
}"
>
<slot></slot>
<!-- 分隔条 -->
<template v-for="(_, index) in panes" :key="index">
<div
v-if="index < panes.length - 1"
class="splitter__gutter"
:class="{
'splitter__gutter--horizontal': direction === 'horizontal',
'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>
</div>
</template>
<style scoped>
.splitter {
display: flex;
width: 100%;
height: 100%;
position: relative;
overflow: hidden;
}
.splitter--horizontal {
flex-direction: row;
}
.splitter--vertical {
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;
}
.splitter__gutter:hover,
.splitter__gutter:active {
background-color: rgba(0, 0, 0, 0.1);
}
.splitter__gutter--horizontal {
height: 100%;
cursor: col-resize;
}
.splitter__gutter--vertical {
width: 100%;
cursor: row-resize;
}
.splitter__gutter-handle {
background-color: #e8e8e8;
transition: background-color 0.2s;
}
.splitter__gutter:hover .splitter__gutter-handle,
.splitter__gutter:active .splitter__gutter-handle {
background-color: #1890ff;
}
.splitter__gutter--horizontal .splitter__gutter-handle {
width: 2px;
height: 30px;
}
.splitter__gutter--vertical .splitter__gutter-handle {
width: 30px;
height: 2px;
}
</style>

5
src/components/index.ts Normal file
View File

@ -0,0 +1,5 @@
import Splitter from './Splitter.vue'
import Pane from './Pane.vue'
export { Splitter, Pane }
export default { Splitter, Pane }