feat: 拖拽侧边栏宽度

This commit is contained in:
2026-06-18 12:37:57 +08:00
parent a2057da8fa
commit da12f4f5ef
3 changed files with 101 additions and 4 deletions

View File

@@ -0,0 +1,71 @@
import { type Ref, onUnmounted } from 'vue'
/**
* 拖拽调整宽度 Hook 的参数
*/
interface UseResizeHandleOptions {
/** 当前宽度响应式引用 */
width: Ref<number>
/** 宽度变更回调,传入新宽度 */
onWidthChange: (width: number) => void
}
/**
* 将侧边栏拖拽调整宽度的逻辑封装为可复用的 composable
*
* 在 mousedown 时绑定 document 级 mousemove/mouseup 监听,
* 拖拽过程中通过 onWidthChange 回调通知外部更新宽度(由外部负责约束范围),
* 拖拽结束后自动清理事件监听和全局样式。
*
* @param options - 配置项,包含宽度 ref 和变更回调
* @returns 返回 mousedown 事件处理器,绑定到拖拽手柄元素
*/
export function useResizeHandle(options: UseResizeHandleOptions): {
onMouseDown: (event: MouseEvent) => void
} {
const { onWidthChange } = options
/** 是否正在拖拽 */
let isResizing = false
/**
* 处理 mousemove计算新宽度并通知外部
*/
const onMouseMove = (event: MouseEvent): void => {
if (!isResizing) return
onWidthChange(event.clientX)
}
/**
* 处理 mouseup结束拖拽并清理
*/
const onMouseUp = (): void => {
isResizing = false
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.body.style.userSelect = ''
document.body.style.cursor = ''
}
/**
* 处理 mousedown开始拖拽
*/
const onMouseDown = (event: MouseEvent): void => {
event.preventDefault()
isResizing = true
document.addEventListener('mousemove', onMouseMove)
document.addEventListener('mouseup', onMouseUp)
document.body.style.userSelect = 'none'
document.body.style.cursor = 'ew-resize'
}
/** 组件卸载时兜底清理 */
onUnmounted(() => {
document.removeEventListener('mousemove', onMouseMove)
document.removeEventListener('mouseup', onMouseUp)
document.body.style.userSelect = ''
document.body.style.cursor = ''
})
return { onMouseDown }
}

View File

@@ -11,7 +11,7 @@
<!-- 主体区域 --> <!-- 主体区域 -->
<div class="base-layout__main"> <div class="base-layout__main">
<!-- 侧边栏 --> <!-- 侧边栏 -->
<div class="base-layout__sidebar"> <div class="base-layout__sidebar" :style="{ width: sidebarWidth + 'px' }">
<!-- 侧边栏内容 --> <!-- 侧边栏内容 -->
<div class="base-layout__sidebar-content"> <div class="base-layout__sidebar-content">
<GameList /> <GameList />
@@ -23,7 +23,7 @@
</div> </div>
</div> </div>
<!-- 拖拽调整宽度的手柄 --> <!-- 拖拽调整宽度的手柄 -->
<div class="base-layout__resize-handle"></div> <div class="base-layout__resize-handle" @mousedown="onResizeMouseDown"></div>
</div> </div>
<!-- 主内容区域 --> <!-- 主内容区域 -->
@@ -40,9 +40,16 @@ import GameList from '@/components/baseLayouts/GameList.vue'
import Lobby from '@/components/baseLayouts/Lobby.vue' import Lobby from '@/components/baseLayouts/Lobby.vue'
import { useBaseLayoutStore } from '@/stores' import { useBaseLayoutStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import { useResizeHandle } from '@/common/hooks/useResizeHandle'
const baseLayoutStore = useBaseLayoutStore() const baseLayoutStore = useBaseLayoutStore()
const { activeGameIcon, activeGameName } = storeToRefs(baseLayoutStore) const { activeGameIcon, activeGameName, sidebarWidth } = storeToRefs(baseLayoutStore)
/** 拖拽手柄事件处理器 */
const { onMouseDown: onResizeMouseDown } = useResizeHandle({
width: sidebarWidth,
onWidthChange: (width) => baseLayoutStore.setSidebarWidth(width)
})
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View File

@@ -1,6 +1,13 @@
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
/** 侧边栏最小宽度 */
export const MIN_SIDEBAR_WIDTH = 200
/** 侧边栏最大宽度 */
export const MAX_SIDEBAR_WIDTH = 650
/** 侧边栏默认宽度 */
export const DEFAULT_SIDEBAR_WIDTH = 400
export const useBaseLayoutStore = defineStore( export const useBaseLayoutStore = defineStore(
'baseLayout', 'baseLayout',
() => { () => {
@@ -12,6 +19,8 @@ export const useBaseLayoutStore = defineStore(
const activeGameIcon = ref<string>('🐍') const activeGameIcon = ref<string>('🐍')
/** 当前选中的游戏名称 */ /** 当前选中的游戏名称 */
const activeGameName = ref<string>('贪吃蛇') const activeGameName = ref<string>('贪吃蛇')
/** 侧边栏当前宽度 */
const sidebarWidth = ref<number>(DEFAULT_SIDEBAR_WIDTH)
/** /**
* 设置顶部导航栏标题 * 设置顶部导航栏标题
@@ -33,13 +42,23 @@ export const useBaseLayoutStore = defineStore(
activeGameName.value = name activeGameName.value = name
} }
/**
* 设置侧边栏宽度,自动约束在最小和最大值之间
* @param width - 新宽度px
*/
const setSidebarWidth = (width: number): void => {
sidebarWidth.value = Math.min(Math.max(width, MIN_SIDEBAR_WIDTH), MAX_SIDEBAR_WIDTH)
}
return { return {
topbarTitle, topbarTitle,
activeGameId, activeGameId,
activeGameIcon, activeGameIcon,
activeGameName, activeGameName,
sidebarWidth,
setTopbarTitle, setTopbarTitle,
setActiveGame setActiveGame,
setSidebarWidth
} }
}, },
{ {