fix(browser): 修复浮动浏览器最小化条与面板无法拖动

迷你窗口使用独立最小尺寸与拖动手柄;展开态增加左侧拖拽条。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nexus Agent
2026-06-11 20:19:49 +08:00
parent 2e8daad802
commit 31846c4212
3 changed files with 122 additions and 19 deletions
@@ -41,6 +41,7 @@ cd frontend && npm run type-check
- 移除「关闭浏览器」按钮;仅 **最小化****重启**(重启保留浏览历史)
- 最小化后为全局 **可拖动浮动条**(双击展开),位置持久化 `minimized_rect`
- 仅剩一个标签时不可关闭该标签
- 修复最小化条无法拖动:`useFloatingPanel` 最小尺寸改为可配置(迷你条 200×44),专用拖动手柄
## 说明
+93 -12
View File
@@ -12,9 +12,14 @@
:class="{ 'global-browser-toolbar--draggable': !maximized }"
@mousedown="onToolbarMouseDown"
>
<v-icon v-if="!maximized" size="16" class="ml-2 mr-1 text-disabled" @mousedown.stop>
mdi-drag
</v-icon>
<div
v-if="!maximized"
class="global-browser-drag-strip flex-shrink-0"
title="拖动"
@mousedown="onToolbarMouseDown"
>
<v-icon size="16" class="text-disabled">mdi-drag</v-icon>
</div>
<v-tabs
:model-value="activeTabId ?? undefined"
@@ -182,16 +187,25 @@
v-if="minimized && tabs.length"
class="global-browser-mini-float"
:style="miniPanelStyle"
@mousedown="onMiniFloatMouseDown"
>
<v-card border rounded="lg" class="global-browser-mini-panel h-100 d-flex flex-column">
<div
class="global-browser-mini-toolbar d-flex align-center px-2 flex-grow-1 global-browser-toolbar--draggable"
@mousedown="onMiniToolbarMouseDown"
@dblclick="openPanel"
@dblclick.stop="openPanel"
>
<v-icon size="16" class="mr-1 text-disabled flex-shrink-0" @mousedown.stop>mdi-drag</v-icon>
<div
class="global-browser-drag-strip global-browser-drag-strip--mini flex-shrink-0"
title="拖动"
@mousedown.stop="onMiniDragStart"
>
<v-icon size="16" class="text-disabled">mdi-drag</v-icon>
</div>
<v-icon icon="mdi-web" size="18" class="mr-2 flex-shrink-0" />
<span class="text-caption text-truncate flex-grow-1 min-w-0" @click="openPanel">
<span
class="text-caption text-truncate flex-grow-1 min-w-0 global-browser-mini-title"
@click="onMiniTitleClick"
>
{{ activeTab ? tabLabel(activeTab) : '浏览器' }}
</span>
<v-chip v-if="tabs.length > 1" size="x-small" class="mx-1 flex-shrink-0">{{ tabs.length }}</v-chip>
@@ -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));
}
+28 -7
View File
@@ -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<FloatRect>(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)