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(() => {
/>
-