112 lines
3.4 KiB
Vue
112 lines
3.4 KiB
Vue
<template>
|
||
<!--
|
||
可拖拽宽度的左侧 Aside
|
||
- 使用 localStorage 记住用户上次调整的宽度(storageKey 必填)
|
||
- 拖拽区在右边缘,鼠标变 col-resize
|
||
-->
|
||
<aside
|
||
class="relative flex flex-col shrink-0 bg-[var(--el-fill-color-light)] border-r border-r-solid border-[var(--el-border-color-lighter)] shadow-[2px_0_8px_rgba(0,0,0,0.05)]"
|
||
:style="{ width: asideWidth + 'px' }"
|
||
>
|
||
<slot></slot>
|
||
<div
|
||
class="im-resizable-aside__handle absolute top-0 right--0.75 z-10 flex items-center justify-center w-1.5 h-full cursor-col-resize transition-colors"
|
||
:class="{ 'is-resizing': isResizing }"
|
||
title="拖拽调整宽度"
|
||
@mousedown="startResize"
|
||
>
|
||
<div class="im-resizable-aside__line w-0.5 h-full rounded-0.5 bg-transparent transition-all"></div>
|
||
</div>
|
||
</aside>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import { onMounted, onBeforeUnmount, ref } from 'vue'
|
||
|
||
defineOptions({ name: 'ImResizableAside' })
|
||
|
||
const props = withDefaults(
|
||
defineProps<{
|
||
defaultWidth?: number // 默认宽度
|
||
minWidth?: number // 最小宽度
|
||
maxWidth?: number // 最大宽度
|
||
storageKey: string // localStorage 存储 key,必填;调用方传 StorageKeys.asideWidth(三 Tab 共用一份)
|
||
}>(),
|
||
{
|
||
defaultWidth: 260,
|
||
minWidth: 200,
|
||
maxWidth: 500
|
||
}
|
||
)
|
||
|
||
const asideWidth = ref<number>(props.defaultWidth)
|
||
const isResizing = ref(false)
|
||
let startX = 0
|
||
let startWidth = 0
|
||
|
||
onMounted(() => {
|
||
const saved = localStorage.getItem(props.storageKey)
|
||
if (saved) {
|
||
const w = parseInt(saved, 10)
|
||
if (!Number.isNaN(w)) {
|
||
asideWidth.value = clamp(w)
|
||
}
|
||
}
|
||
document.addEventListener('mousemove', handleResize)
|
||
document.addEventListener('mouseup', stopResize)
|
||
})
|
||
|
||
onBeforeUnmount(() => {
|
||
// 拖拽中卸载:复用 stopResize 复位 body cursor/userSelect 并写回当前宽度,避免全局状态泄漏
|
||
if (isResizing.value) {
|
||
stopResize()
|
||
}
|
||
document.removeEventListener('mousemove', handleResize)
|
||
document.removeEventListener('mouseup', stopResize)
|
||
})
|
||
|
||
/** 把宽度夹到 [minWidth, maxWidth] 区间,恢复 / 拖拽路径都走它兜底 */
|
||
function clamp(w: number) {
|
||
return Math.max(props.minWidth, Math.min(props.maxWidth, w))
|
||
}
|
||
|
||
/** 按下拖拽手柄:记录起始位置 + 锁定 body cursor/userSelect,避免拖拽中误选文本 */
|
||
function startResize(e: MouseEvent) {
|
||
isResizing.value = true
|
||
startX = e.clientX
|
||
startWidth = asideWidth.value
|
||
document.body.style.cursor = 'col-resize'
|
||
document.body.style.userSelect = 'none'
|
||
e.preventDefault()
|
||
}
|
||
|
||
/** 拖拽中:按鼠标位移计算新宽度并 clamp 到允许区间 */
|
||
function handleResize(e: MouseEvent) {
|
||
if (!isResizing.value) {
|
||
return
|
||
}
|
||
const deltaX = e.clientX - startX
|
||
asideWidth.value = clamp(startWidth + deltaX)
|
||
}
|
||
|
||
/** 松开鼠标:解锁 body 全局态并把当前宽度写入 localStorage 持久化 */
|
||
function stopResize() {
|
||
if (!isResizing.value) {
|
||
return
|
||
}
|
||
isResizing.value = false
|
||
document.body.style.cursor = ''
|
||
document.body.style.userSelect = ''
|
||
localStorage.setItem(props.storageKey, String(asideWidth.value))
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* hover / 拖拽中 把内部 line 加粗变深,提示手柄可拖;状态在父 handle 上 → 通过后代选择器联动子 line */
|
||
.im-resizable-aside__handle:hover .im-resizable-aside__line,
|
||
.im-resizable-aside__handle.is-resizing .im-resizable-aside__line {
|
||
width: 3px;
|
||
background-color: #d0d4db;
|
||
}
|
||
</style>
|