| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <script lang="ts" setup> |
| | | import { onBeforeUnmount, onMounted, ref } from 'vue' |
| | | |
| | | defineOptions({ name: 'ImResizableAside' }) |
| | | |
| | | const props = withDefaults( |
| | | defineProps<{ |
| | | defaultWidth?: number // é»è®¤å®½åº¦ |
| | | maxWidth?: number // æå¤§å®½åº¦ |
| | | minWidth?: number // æå°å®½åº¦ |
| | | storageKey: string // localStorage åå¨ keyï¼å¿
å¡«ï¼è°ç¨æ¹ä¼ StorageKeys.localStorage.asideWidth |
| | | }>(), |
| | | { |
| | | 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 = Number.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> |
| | | |
| | | <template> |
| | | <!-- |
| | | 坿æ½å®½åº¦ç左侧 Aside |
| | | - ä½¿ç¨ localStorage è®°ä½ç¨æ·ä¸æ¬¡è°æ´ç宽度ï¼storageKey å¿
å¡«ï¼ |
| | | - ææ½åºå¨å³è¾¹ç¼ï¼é¼ æ å col-resize |
| | | --> |
| | | <aside |
| | | class="relative flex flex-col shrink-0 bg-[var(--ant-color-fill-secondary)] border-r border-r-solid border-[var(--im-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> |
| | | |
| | | <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: var(--im-resize-line-color); |
| | | } |
| | | </style> |