<template>
<div
:class="['menu', { 'menu-collapsed': isCollapsed, 'menu-expanded': !isCollapsed }]"
:style="containerStyle"
>
<slot></slot>
<!-- 折叠展开按钮 -->
<div :class="isCollapsed ? 'btn-collapse' : 'btn-expand'">
<img
width="40px"
@click="toggleCollapse"
:src="isCollapsed ? expandImg : collapseImg"
alt=""
/>
</div>
<!-- 拖拽 -->
<div
v-if="!isCollapsed"
:class="['resize-handle', 'resize-handle-' + props.dragSide]"
@mousedown.prevent="onResizeMouseDown"
></div>
</div>
</template>
<script lang="ts" setup>
import expandImg from '@/assets/imgs/expand.png';
import collapseImg from '@/assets/imgs/collapse.png';
const props = defineProps({
/** 面板初始宽度*/
width: {
type: String,
default: '300px'
},
/**
* 拖拽的边'left' | 'right' | 'top' | 'bottom'
*/
dragSide: {
type: String,
default: 'right'
},
/** 最小尺寸(像素) */
minSize: {
type: Number,
default: 100
},
/** 最大尺寸(像素) */
maxSize: {
type: Number,
default: 800
}
});
const isCollapsed=ref(false);
const isDragging = ref(false);
// 当前尺寸
const size = ref<number>(parseInt((props.width), 10) || 200);
let startX = 0;
let startY = 0;
let startSize = size.value;
const isHorizontal = computed(() => {
return props.dragSide === 'left' || props.dragSide === 'right';
});
const containerStyle = computed(() => {
const style: Record<string, string> = {};
// 统一使用相对定位,子元素都是绝对定位
style.position = isCollapsed.value ? 'unset' : 'relative';
if (isCollapsed.value) {
if (isHorizontal.value) {
style.width = '0px';
} else {
style.height = '0px';
}
} else {
const px = `${size.value}px`;
if (isHorizontal.value) {
style.width = px;
} else {
style.height = px;
}
}
return style;
});
function toggleCollapse() {
isCollapsed.value = !isCollapsed.value;
}
function clampSize(val: number) {
const min = props.minSize;
const max = props.maxSize;
if (val < min) return min;
if (val > max) return max;
return val;
}
function onResizeMouseDown(e: MouseEvent) {
isDragging.value = true;
startX = e.clientX;
startY = e.clientY;
startSize = size.value;
window.addEventListener('mousemove', onResizeMouseMove);
window.addEventListener('mouseup', onResizeMouseUp);
}
function onResizeMouseMove(e: MouseEvent) {
if (!isDragging.value) return;
let delta = 0;
if (isHorizontal.value) {
delta = e.clientX - startX;
// 从左边拖拽时方向相反
if (props.dragSide === 'left') {
delta = -delta;
}
} else {
delta = e.clientY - startY;
// 从上边拖拽时方向相反
if (props.dragSide === 'top') {
delta = -delta;
}
}
size.value = clampSize(startSize + delta);
}
function onResizeMouseUp() {
if (!isDragging.value) return;
isDragging.value = false;
window.removeEventListener('mousemove', onResizeMouseMove);
window.removeEventListener('mouseup', onResizeMouseUp);
}
onBeforeUnmount(() => {
window.removeEventListener('mousemove', onResizeMouseMove);
window.removeEventListener('mouseup', onResizeMouseUp);
});
</script>
<style lang="scss" scoped>
.btn-expand {
position: absolute;
top: 50%;
right: -14px;
}
.btn-collapse {
position: absolute;
top: 50%;
left: -14px;
z-index: 10000000;
}
.menu {
transition: width 0.3s;
overflow: hidden;
}
.menu-collapsed {
width: 0px;
}
.menu-expanded {
width: calc(100% - 20px);
margin-right: 20px;
}
.resize-handle {
position: absolute;
z-index: 10000001;
background-color: transparent;
}
.resize-handle-left {
top: 0;
left: 0;
width: 4px;
height: 100%;
cursor: col-resize;
}
.resize-handle-right {
top: 0;
right: 0;
width: 4px;
height: 100%;
cursor: col-resize;
}
.resize-handle-top {
top: 0;
left: 0;
width: 100%;
height: 4px;
cursor: row-resize;
}
.resize-handle-bottom {
bottom: 0;
left: 0;
width: 100%;
height: 4px;
cursor: row-resize;
}
</style>