diff --git a/docs/changelog/2026-06-11-global-floating-browser.md b/docs/changelog/2026-06-11-global-floating-browser.md index cde49ff8..1b9d62cd 100644 --- a/docs/changelog/2026-06-11-global-floating-browser.md +++ b/docs/changelog/2026-06-11-global-floating-browser.md @@ -41,6 +41,7 @@ cd frontend && npm run type-check - 移除「关闭浏览器」按钮;仅 **最小化** 或 **重启**(重启保留浏览历史) - 最小化后为全局 **可拖动浮动条**(双击展开),位置持久化 `minimized_rect` - 仅剩一个标签时不可关闭该标签 +- 修复最小化条无法拖动:`useFloatingPanel` 最小尺寸改为可配置(迷你条 200×44),专用拖动手柄 ## 说明 diff --git a/frontend/src/components/GlobalBrowserPanel.vue b/frontend/src/components/GlobalBrowserPanel.vue index 43c32d35..385922aa 100644 --- a/frontend/src/components/GlobalBrowserPanel.vue +++ b/frontend/src/components/GlobalBrowserPanel.vue @@ -12,9 +12,14 @@ :class="{ 'global-browser-toolbar--draggable': !maximized }" @mousedown="onToolbarMouseDown" > - - mdi-drag - +
+ mdi-drag +
- mdi-drag +
+ mdi-drag +
- + {{ activeTab ? tabLabel(activeTab) : '浏览器' }} {{ tabs.length }} @@ -303,8 +317,15 @@ const { rect: miniRect, panelStyle: miniPanelStyle, startDrag: startMiniDrag, - clampToViewport: clampMiniViewport, -} = useFloatingPanel('nexus_global_browser_mini_rect_v1', defaultMiniRect()) +} = useFloatingPanel('nexus_global_browser_mini_rect_v1', defaultMiniRect(), { + minW: 200, + minH: 44, + maxW: 520, + maxH: 56, +}) + +let miniDragMoved = false +let suppressMiniTitleClick = false function seedRectFromServer() { if (panelRect.value) { @@ -332,15 +353,50 @@ watch(miniRect, (value) => { saveMinimizedRect({ ...value }) }, { deep: true }) +function isDragBlockedTarget(target: EventTarget | null): boolean { + if (!(target instanceof HTMLElement)) return true + return Boolean(target.closest('button, .v-tab, .v-btn, input, a, .v-field, .v-chip')) +} + function onToolbarMouseDown(e: MouseEvent) { if (maximized.value) return + if (isDragBlockedTarget(e.target)) return + e.preventDefault() startDrag(e) } -function onMiniToolbarMouseDown(e: MouseEvent) { +function onMiniDragStart(e: MouseEvent) { if (e.button !== 0) return + e.preventDefault() + miniDragMoved = false + const originX = e.clientX + const originY = e.clientY + const onMove = (ev: MouseEvent) => { + if (Math.abs(ev.clientX - originX) + Math.abs(ev.clientY - originY) > 4) { + miniDragMoved = true + } + } + const onUp = () => { + window.removeEventListener('mousemove', onMove) + window.removeEventListener('mouseup', onUp) + suppressMiniTitleClick = miniDragMoved + if (miniDragMoved) { + window.setTimeout(() => { + suppressMiniTitleClick = false + }, 0) + } + } + window.addEventListener('mousemove', onMove) + window.addEventListener('mouseup', onUp) startMiniDrag(e) - clampMiniViewport() +} + +function onMiniFloatMouseDown(e: MouseEvent) { + if (e.button !== 0) return + if (isDragBlockedTarget(e.target)) return + if ((e.target as HTMLElement).closest('.global-browser-drag-strip')) return + e.preventDefault() + onMiniDragStart(e) } function onResizeStart(e: MouseEvent) { @@ -363,6 +419,11 @@ function onLauncherClick() { newEmptyTab() } +function onMiniTitleClick() { + if (suppressMiniTitleClick) return + openPanel() +} + function onRestart() { showRestartConfirm.value = true } @@ -381,6 +442,7 @@ function confirmRestart() { display: flex; flex-direction: column; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35); + touch-action: none; } .global-browser-float--maximized { inset: 0 !important; @@ -408,9 +470,28 @@ function confirmRestart() { border-bottom: none; } .global-browser-toolbar--draggable { - cursor: move; + cursor: default; user-select: none; } +.global-browser-drag-strip { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 40px; + margin-left: 4px; + cursor: move; + touch-action: none; +} +.global-browser-drag-strip--mini { + width: 24px; + height: 100%; + margin-left: 0; + margin-right: 4px; +} +.global-browser-mini-title { + cursor: pointer; +} .global-browser-nav { background: rgb(var(--v-theme-surface)); } diff --git a/frontend/src/composables/useFloatingPanel.ts b/frontend/src/composables/useFloatingPanel.ts index fd6a9b44..ac72cf4c 100644 --- a/frontend/src/composables/useFloatingPanel.ts +++ b/frontend/src/composables/useFloatingPanel.ts @@ -34,8 +34,28 @@ function saveRect(key: string, rect: FloatRect) { } } +export interface FloatingPanelOptions { + /** Minimum width when clamping (default 480) */ + minW?: number + /** Minimum height when clamping (default 320) */ + minH?: number + /** Maximum width cap (default 1400) */ + maxW?: number + /** Maximum height cap (default 900) */ + maxH?: number +} + /** 可拖动浮动面板位置与尺寸(持久化到 localStorage) */ -export function useFloatingPanel(storageKey: string, fallback: FloatRect) { +export function useFloatingPanel( + storageKey: string, + fallback: FloatRect, + options: FloatingPanelOptions = {}, +) { + const minW = options.minW ?? 480 + const minH = options.minH ?? 320 + const maxWCap = options.maxW ?? 1400 + const maxHCap = options.maxH ?? 900 + const rect = ref(loadRect(storageKey, fallback)) let dragOrigin = { px: 0, py: 0, rx: 0, ry: 0 } @@ -51,18 +71,18 @@ export function useFloatingPanel(storageKey: string, fallback: FloatRect) { function clampToViewport() { const margin = 8 - const maxW = Math.min(window.innerWidth - margin * 2, 1400) - const maxH = Math.min(window.innerHeight - margin * 2, 900) - rect.value.w = Math.max(480, Math.min(rect.value.w, maxW)) - rect.value.h = Math.max(320, Math.min(rect.value.h, maxH)) + const maxW = Math.min(window.innerWidth - margin * 2, maxWCap) + const maxH = Math.min(window.innerHeight - margin * 2, maxHCap) + rect.value.w = Math.max(minW, Math.min(rect.value.w, maxW)) + rect.value.h = Math.max(minH, Math.min(rect.value.h, maxH)) rect.value.x = Math.max(margin, Math.min(rect.value.x, window.innerWidth - rect.value.w - margin)) rect.value.y = Math.max(margin, Math.min(rect.value.y, window.innerHeight - rect.value.h - margin)) } function onDragMove(e: MouseEvent) { if (resizing) { - rect.value.w = Math.max(480, resizeOrigin.rw + e.clientX - resizeOrigin.px) - rect.value.h = Math.max(320, resizeOrigin.rh + e.clientY - resizeOrigin.py) + rect.value.w = Math.max(minW, resizeOrigin.rw + e.clientX - resizeOrigin.px) + rect.value.h = Math.max(minH, resizeOrigin.rh + e.clientY - resizeOrigin.py) clampToViewport() return } @@ -80,6 +100,7 @@ export function useFloatingPanel(storageKey: string, fallback: FloatRect) { function startDrag(e: MouseEvent) { if (e.button !== 0) return + e.preventDefault() dragOrigin = { px: e.clientX, py: e.clientY, rx: rect.value.x, ry: rect.value.y } window.addEventListener('mousemove', onDragMove) window.addEventListener('mouseup', onDragEnd)