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">
|
||||
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
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