diff --git a/docs/specs/sidebar-resizer.md b/docs/specs/sidebar-resizer.md new file mode 100644 index 0000000..e666500 --- /dev/null +++ b/docs/specs/sidebar-resizer.md @@ -0,0 +1,48 @@ +# Sidebar Resizer 规格 + +## 概述 + +在地图区域和事件记录面板(sidebar)之间添加一条可拖曳的分隔线,允许用户调整 sidebar 的宽度。 + +## 需求 + +### 功能 + +- 在 `.map-container` 和 `.sidebar` 之间添加一个垂直的 resizer 手柄。 +- 用户可以通过拖曳该手柄来调整 sidebar 的宽度。 + +### 宽度限制 + +- **最小宽度**: 300px +- **最大宽度**: 50% 屏幕宽度 +- **默认宽度**: 400px + +### 持久化 + +- 不需要持久化,每次打开页面都恢复为默认 400px。 + +## 实现细节 + +### 修改文件 + +- `web/src/App.vue` - resizer 逻辑 +- `web/src/components/game/GameCanvas.vue` - canvas 尺寸调整 + +### 技术方案 + +1. 在 `.main-content` 中,在 `.map-container` 和 `.sidebar` 之间插入一个 `.resizer` 元素。 +2. 使用 `mousedown` / `mousemove` / `mouseup` 事件实现拖曳逻辑。 +3. 拖曳时动态计算并设置 sidebar 宽度。 +4. Canvas 使用窗口大小而非容器大小,确保拖曳 sidebar 时地图不会被缩放,只改变可视区域。 + +### UI 细节 + +- Resizer 宽度: 4px +- 默认颜色: 透明或与背景融合 +- Hover/拖曳时颜色: 高亮(如 `#555` 或主题色) +- 鼠标样式: `col-resize` + +### 边界处理 + +- 拖曳超出最小/最大范围时,宽度固定在边界值。 +- 窗口 resize 时,如果当前宽度超过 50% 屏幕宽,自动调整为 50%。 diff --git a/web/src/App.vue b/web/src/App.vue index 5f7f3eb..41cc203 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -27,6 +27,35 @@ const uiStore = useUiStore() const settingStore = useSettingStore() const showSplash = ref(true) + +// Sidebar resizer 状态。 +const sidebarWidth = ref(400) +const isResizing = ref(false) +const MIN_SIDEBAR_WIDTH = 300 + +function getMaxSidebarWidth() { + return Math.floor(window.innerWidth * 0.5) +} + +function onResizerMouseDown(e: MouseEvent) { + e.preventDefault() + isResizing.value = true + document.addEventListener('mousemove', onResizerMouseMove) + document.addEventListener('mouseup', onResizerMouseUp) +} + +function onResizerMouseMove(e: MouseEvent) { + if (!isResizing.value) return + const newWidth = window.innerWidth - e.clientX + const maxWidth = getMaxSidebarWidth() + sidebarWidth.value = Math.max(MIN_SIDEBAR_WIDTH, Math.min(newWidth, maxWidth)) +} + +function onResizerMouseUp() { + isResizing.value = false + document.removeEventListener('mousemove', onResizerMouseMove) + document.removeEventListener('mouseup', onResizerMouseUp) +} const openedFromSplash = ref(false) // 1. 游戏初始化逻辑 @@ -146,14 +175,26 @@ async function handleReturnToMain() { } } +// 窗口 resize 时,确保 sidebar 宽度不超过最大值。 +function onWindowResize() { + const maxWidth = getMaxSidebarWidth() + if (sidebarWidth.value > maxWidth) { + sidebarWidth.value = maxWidth + } +} + onMounted(() => { window.addEventListener('keydown', onKeydown) + window.addEventListener('resize', onWindowResize) // Ensure backend language setting matches frontend preference settingStore.syncBackend() }) onUnmounted(() => { window.removeEventListener('keydown', onKeydown) + window.removeEventListener('resize', onWindowResize) + document.removeEventListener('mousemove', onResizerMouseMove) + document.removeEventListener('mouseup', onResizerMouseUp) }) @@ -210,7 +251,12 @@ onUnmounted(() => { /> -