test
This commit is contained in:
parent
516fcf6b82
commit
34833f6ae6
115
src/App.vue
115
src/App.vue
@ -1,10 +1,117 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Splitter } from './components/Splitter'
|
import { Splitter, Pane } from './components'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>Splitter</div>
|
<div class="container">
|
||||||
<Splitter />
|
<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>
|
</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
74
src/components/Pane.vue
Normal 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数组中找到对应的面板,则使用panes中的size值
|
||||||
|
// 否则使用props中的size值(初始渲染时)
|
||||||
|
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>
|
@ -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
286
src/components/Splitter.vue
Normal 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
5
src/components/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import Splitter from './Splitter.vue'
|
||||||
|
import Pane from './Pane.vue'
|
||||||
|
|
||||||
|
export { Splitter, Pane }
|
||||||
|
export default { Splitter, Pane }
|
Loading…
x
Reference in New Issue
Block a user