✨ feat(im): 优化 ResizableAside.vue 组件
parent
a973406b2a
commit
802a10cf85
|
|
@ -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>
|
||||
Loading…
Reference in New Issue