feat(im): 优化 ResizableAside.vue 组件

im
YunaiV 2026-04-26 21:35:27 +08:00
parent a973406b2a
commit 802a10cf85
1 changed files with 112 additions and 0 deletions

View File

@ -0,0 +1,112 @@
<template>
<!--
可拖拽宽度的左侧 Aside
- 使用 localStorage 记住用户上次调整的宽度storageKey 必填
- 拖拽区在右边缘鼠标变 col-resize
-->
<aside
class="relative flex flex-col shrink-0 bg-[var(--el-bg-color)] border-r 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(page)
}>(),
{
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 / 拖拽中变色UnoCSS 同时控制"handle 状态 → 子 line 样式"的选择器链比较绕
scoped CSS 直接描述更清晰 */
.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>