fix(browser): 修复浮动浏览器最小化条与面板无法拖动
迷你窗口使用独立最小尺寸与拖动手柄;展开态增加左侧拖拽条。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -41,6 +41,7 @@ cd frontend && npm run type-check
|
||||
- 移除「关闭浏览器」按钮;仅 **最小化** 或 **重启**(重启保留浏览历史)
|
||||
- 最小化后为全局 **可拖动浮动条**(双击展开),位置持久化 `minimized_rect`
|
||||
- 仅剩一个标签时不可关闭该标签
|
||||
- 修复最小化条无法拖动:`useFloatingPanel` 最小尺寸改为可配置(迷你条 200×44),专用拖动手柄
|
||||
|
||||
## 说明
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user