mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-09-08 09:16:58 +08:00
fix(agent): stabilize assistant entry dragging (#505)
This commit is contained in:
@@ -62,6 +62,8 @@ const FAB_BUBBLE_EDGE_ARROW_OFFSET = 38
|
|||||||
const FAB_BUBBLE_UNDOCK_POSITION_SYNC_DELAY = 260
|
const FAB_BUBBLE_UNDOCK_POSITION_SYNC_DELAY = 260
|
||||||
const FAB_RANDOM_ACTION_MIN_DELAY = 8000
|
const FAB_RANDOM_ACTION_MIN_DELAY = 8000
|
||||||
const FAB_RANDOM_ACTION_MAX_DELAY = 18000
|
const FAB_RANDOM_ACTION_MAX_DELAY = 18000
|
||||||
|
const FAB_RIGHT_EDGE_RESIZE_FOLLOW_DISTANCE = 128
|
||||||
|
const FAB_DRAG_SUPPRESS_CLICK_DELAY = 450
|
||||||
|
|
||||||
const FAB_RANDOM_ACTIONS = ['wave', 'sit', 'eye-roll', 'faint', 'disassemble', 'happy-jump'] as const
|
const FAB_RANDOM_ACTIONS = ['wave', 'sit', 'eye-roll', 'faint', 'disassemble', 'happy-jump'] as const
|
||||||
|
|
||||||
@@ -83,6 +85,27 @@ interface FabPosition {
|
|||||||
y: number
|
y: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FabPositionRangeOptions {
|
||||||
|
useOpenBounds?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FabPositionRatio {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type FabPositionAnchor =
|
||||||
|
| {
|
||||||
|
mode: 'right'
|
||||||
|
rightOffset: number
|
||||||
|
yRatio: number
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
mode: 'free'
|
||||||
|
xRatio: number
|
||||||
|
yRatio: number
|
||||||
|
}
|
||||||
|
|
||||||
interface FabInteractiveBounds {
|
interface FabInteractiveBounds {
|
||||||
height: number
|
height: number
|
||||||
offsetX: number
|
offsetX: number
|
||||||
@@ -170,6 +193,7 @@ const fabRandomAction = ref<FabRandomAction | null>(null)
|
|||||||
let fabIdleTimer: number | null = null
|
let fabIdleTimer: number | null = null
|
||||||
let fabDragState: FabDragState | null = null
|
let fabDragState: FabDragState | null = null
|
||||||
let fabSuppressNextClick = false
|
let fabSuppressNextClick = false
|
||||||
|
let fabSuppressNextClickTimer: number | null = null
|
||||||
let fabPointerFrame = 0
|
let fabPointerFrame = 0
|
||||||
let fabPendingPointerPoint: FabPointerPoint | null = null
|
let fabPendingPointerPoint: FabPointerPoint | null = null
|
||||||
let fabBubblePositionFrame = 0
|
let fabBubblePositionFrame = 0
|
||||||
@@ -179,6 +203,8 @@ let fabLastRandomAction: FabRandomAction | null = null
|
|||||||
let fabRandomActionTimer: number | null = null
|
let fabRandomActionTimer: number | null = null
|
||||||
let fabRandomActionEndTimer: number | null = null
|
let fabRandomActionEndTimer: number | null = null
|
||||||
let stopBubbleListener: (() => void) | null = null
|
let stopBubbleListener: (() => void) | null = null
|
||||||
|
let fabPositionAnchor: FabPositionAnchor | null = null
|
||||||
|
let stopFabTouchMoveGuard: (() => void) | null = null
|
||||||
|
|
||||||
const fabBubbleTimers = new Map<string, number>()
|
const fabBubbleTimers = new Map<string, number>()
|
||||||
|
|
||||||
@@ -195,9 +221,16 @@ function createBubbleId(prefix = 'bubble') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getViewportSize() {
|
function getViewportSize() {
|
||||||
|
const layoutWidth = window.innerWidth || document.documentElement.clientWidth || 0
|
||||||
|
const layoutHeight = window.innerHeight || document.documentElement.clientHeight || 0
|
||||||
|
const visualWidth = window.visualViewport?.width || 0
|
||||||
|
const visualHeight = window.visualViewport?.height || 0
|
||||||
|
|
||||||
|
// 取布局视口和可见视口的较小值,避免两者短暂不同步时把入口计算到屏幕外。
|
||||||
return {
|
return {
|
||||||
height: window.innerHeight || document.documentElement.clientHeight || 0,
|
height:
|
||||||
width: window.innerWidth || document.documentElement.clientWidth || 0,
|
visualHeight > 0 && layoutHeight > 0 ? Math.min(visualHeight, layoutHeight) : visualHeight || layoutHeight,
|
||||||
|
width: visualWidth > 0 && layoutWidth > 0 ? Math.min(visualWidth, layoutWidth) : visualWidth || layoutWidth,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,16 +293,6 @@ function getFabInteractiveBounds(): FabInteractiveBounds {
|
|||||||
return getFallbackFabInteractiveBounds()
|
return getFallbackFabInteractiveBounds()
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFabSize() {
|
|
||||||
const root = getFabRootElement()
|
|
||||||
const rect = root?.getBoundingClientRect()
|
|
||||||
|
|
||||||
return {
|
|
||||||
height: rect?.height || 115,
|
|
||||||
width: rect?.width || 211,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDefaultFabPosition() {
|
function getDefaultFabPosition() {
|
||||||
if (typeof window === 'undefined') return { x: 0, y: 0 }
|
if (typeof window === 'undefined') return { x: 0, y: 0 }
|
||||||
|
|
||||||
@@ -283,35 +306,146 @@ function getDefaultFabPosition() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampFabPosition(position: FabPosition) {
|
function getFabPositionRange(options: FabPositionRangeOptions = {}) {
|
||||||
if (typeof window === 'undefined') return position
|
|
||||||
|
|
||||||
const viewport = getViewportSize()
|
const viewport = getViewportSize()
|
||||||
const bounds = getFabInteractiveBounds()
|
const bounds = options.useOpenBounds ? getFallbackFabInteractiveBounds() : getFabInteractiveBounds()
|
||||||
const minX = -bounds.offsetX
|
const minX = -bounds.offsetX
|
||||||
const minY = -bounds.offsetY
|
const minY = -bounds.offsetY
|
||||||
const maxX = Math.max(minX, viewport.width - bounds.offsetX - bounds.width)
|
const maxX = Math.max(minX, viewport.width - bounds.offsetX - bounds.width)
|
||||||
const maxY = Math.max(minY, viewport.height - bounds.offsetY - bounds.height)
|
const maxY = Math.max(minY, viewport.height - bounds.offsetY - bounds.height)
|
||||||
|
|
||||||
|
return { maxX, maxY, minX, minY }
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampFabPosition(position: FabPosition, options: FabPositionRangeOptions = {}) {
|
||||||
|
if (typeof window === 'undefined') return position
|
||||||
|
|
||||||
|
const range = getFabPositionRange(options)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: Math.min(maxX, Math.max(minX, position.x)),
|
x: Math.min(range.maxX, Math.max(range.minX, position.x)),
|
||||||
y: Math.min(maxY, Math.max(minY, position.y)),
|
y: Math.min(range.maxY, Math.max(range.minY, position.y)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clampFabY(y: number, options: FabPositionRangeOptions = {}) {
|
||||||
|
const range = getFabPositionRange(options)
|
||||||
|
|
||||||
|
return Math.min(range.maxY, Math.max(range.minY, y))
|
||||||
|
}
|
||||||
|
|
||||||
function getCurrentFabPosition() {
|
function getCurrentFabPosition() {
|
||||||
return fabPosition.value || getDefaultFabPosition()
|
return fabPosition.value || getDefaultFabPosition()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isFabNearRightEdge(position = getCurrentFabPosition()) {
|
function getFabRightEdgeOffset(position = getCurrentFabPosition()) {
|
||||||
const viewport = getViewportSize()
|
const viewport = getViewportSize()
|
||||||
const size = getFabSize()
|
const size = getOpenFabSize()
|
||||||
|
|
||||||
return viewport.width - (position.x + size.width) <= FAB_RIGHT_EDGE_DOCK_DISTANCE
|
return viewport.width - (position.x + size.width)
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFabPosition(position: FabPosition) {
|
function getFabYRatio(position: FabPosition, options: FabPositionRangeOptions = {}) {
|
||||||
fabPosition.value = clampFabPosition(position)
|
return getFabFreePositionRatio(position, options).y
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只在用户定位意图变化时更新锚点,窗口缩放时只消费该锚点重新计算位置。
|
||||||
|
function updateFabAnchorFromPosition(position = getCurrentFabPosition(), options: FabPositionRangeOptions = {}) {
|
||||||
|
const rangeOptions = { useOpenBounds: options.useOpenBounds ?? fabDragging.value }
|
||||||
|
const offset = getFabRightEdgeOffset(position)
|
||||||
|
if (offset <= FAB_RIGHT_EDGE_RESIZE_FOLLOW_DISTANCE) {
|
||||||
|
fabPositionAnchor = {
|
||||||
|
mode: 'right',
|
||||||
|
rightOffset: Math.max(0, offset),
|
||||||
|
yRatio: getFabYRatio(position, rangeOptions),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = getFabFreePositionRatio(position, rangeOptions)
|
||||||
|
fabPositionAnchor = {
|
||||||
|
mode: 'free',
|
||||||
|
xRatio: ratio.x,
|
||||||
|
yRatio: ratio.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldFabAutoDock() {
|
||||||
|
return fabPositionAnchor?.mode === 'right' && fabPositionAnchor.rightOffset <= FAB_RIGHT_EDGE_DOCK_DISTANCE
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFabFreePositionRatio(position: FabPosition, options: FabPositionRangeOptions = {}): FabPositionRatio {
|
||||||
|
const range = getFabPositionRange(options)
|
||||||
|
const xRange = range.maxX - range.minX
|
||||||
|
const yRange = range.maxY - range.minY
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: xRange > 0 ? (position.x - range.minX) / xRange : 0,
|
||||||
|
y: yRange > 0 ? (position.y - range.minY) / yRange : 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFabPositionFromRatio(ratio: FabPositionRatio, options: FabPositionRangeOptions = {}) {
|
||||||
|
const range = getFabPositionRange(options)
|
||||||
|
|
||||||
|
return clampFabPosition(
|
||||||
|
{
|
||||||
|
x: range.minX + (range.maxX - range.minX) * ratio.x,
|
||||||
|
y: range.minY + (range.maxY - range.minY) * ratio.y,
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFabYFromRatio(yRatio: number, options: FabPositionRangeOptions = {}) {
|
||||||
|
const range = getFabPositionRange(options)
|
||||||
|
|
||||||
|
return range.minY + (range.maxY - range.minY) * yRatio
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFabPositionFromAnchor(anchor: FabPositionAnchor) {
|
||||||
|
if (anchor.mode === 'right') {
|
||||||
|
const viewport = getViewportSize()
|
||||||
|
const size = getOpenFabSize()
|
||||||
|
|
||||||
|
return clampFabPosition(
|
||||||
|
{
|
||||||
|
x: viewport.width - size.width - anchor.rightOffset,
|
||||||
|
y: getFabYFromRatio(anchor.yRatio, { useOpenBounds: true }),
|
||||||
|
},
|
||||||
|
{ useOpenBounds: true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return getFabPositionFromRatio(
|
||||||
|
{
|
||||||
|
x: anchor.xRatio,
|
||||||
|
y: anchor.yRatio,
|
||||||
|
},
|
||||||
|
{ useOpenBounds: true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenFabPositionForDrag(currentPosition: FabPosition) {
|
||||||
|
if (fabPositionAnchor) return getFabPositionFromAnchor(fabPositionAnchor)
|
||||||
|
|
||||||
|
return clampFabPosition(
|
||||||
|
{
|
||||||
|
...currentPosition,
|
||||||
|
x: Math.min(
|
||||||
|
currentPosition.x,
|
||||||
|
Math.max(0, getViewportSize().width - getOpenFabSize().width - FAB_DEFAULT_RIGHT_OFFSET),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{ useOpenBounds: true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFabPosition(position: FabPosition, options: FabPositionRangeOptions & { syncAnchor?: boolean } = {}) {
|
||||||
|
const rangeOptions = { useOpenBounds: options.useOpenBounds ?? fabDragging.value }
|
||||||
|
|
||||||
|
fabPosition.value = clampFabPosition(position, rangeOptions)
|
||||||
|
if (options.syncAnchor !== false) updateFabAnchorFromPosition(fabPosition.value, rangeOptions)
|
||||||
scheduleFabBubblePositionUpdate()
|
scheduleFabBubblePositionUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,17 +718,24 @@ function teardownFabBubblePositioning() {
|
|||||||
|
|
||||||
function resetFabPosition() {
|
function resetFabPosition() {
|
||||||
fabPosition.value = getDefaultFabPosition()
|
fabPosition.value = getDefaultFabPosition()
|
||||||
|
updateFabAnchorFromPosition(fabPosition.value)
|
||||||
scheduleFabBubblePositionUpdate()
|
scheduleFabBubblePositionUpdate()
|
||||||
if (isFabNearRightEdge()) scheduleFabAutoDock()
|
if (shouldFabAutoDock()) scheduleFabAutoDock()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleWindowResize() {
|
function handleWindowResize() {
|
||||||
updateFabPosition(getCurrentFabPosition())
|
const currentPosition = getCurrentFabPosition()
|
||||||
if (fabDocked.value) {
|
if (fabDocked.value) {
|
||||||
|
const y = fabPositionAnchor ? getFabYFromRatio(fabPositionAnchor.yRatio) : currentPosition.y
|
||||||
fabPosition.value = {
|
fabPosition.value = {
|
||||||
...getCurrentFabPosition(),
|
...currentPosition,
|
||||||
x: getDockedFabX(),
|
x: getDockedFabX(),
|
||||||
|
y: clampFabY(y),
|
||||||
}
|
}
|
||||||
|
} else if (fabPositionAnchor) {
|
||||||
|
updateFabPosition(getFabPositionFromAnchor(fabPositionAnchor), { syncAnchor: false })
|
||||||
|
} else {
|
||||||
|
updateFabPosition(currentPosition)
|
||||||
}
|
}
|
||||||
scheduleFabBubblePositionUpdate()
|
scheduleFabBubblePositionUpdate()
|
||||||
}
|
}
|
||||||
@@ -685,18 +826,36 @@ function clearFabIdleTimer() {
|
|||||||
fabIdleTimer = null
|
fabIdleTimer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearFabSuppressNextClickTimer() {
|
||||||
|
if (fabSuppressNextClickTimer === null) return
|
||||||
|
|
||||||
|
window.clearTimeout(fabSuppressNextClickTimer)
|
||||||
|
fabSuppressNextClickTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function suppressNextFabClick() {
|
||||||
|
fabSuppressNextClick = true
|
||||||
|
clearFabSuppressNextClickTimer()
|
||||||
|
fabSuppressNextClickTimer = window.setTimeout(() => {
|
||||||
|
fabSuppressNextClick = false
|
||||||
|
fabSuppressNextClickTimer = null
|
||||||
|
}, FAB_DRAG_SUPPRESS_CLICK_DELAY)
|
||||||
|
}
|
||||||
|
|
||||||
function scheduleFabAutoDock() {
|
function scheduleFabAutoDock() {
|
||||||
clearFabIdleTimer()
|
clearFabIdleTimer()
|
||||||
if (fabDocked.value || hasKeepOpenFabBubbles.value || fabRandomAction.value || !isFabNearRightEdge()) return
|
if (fabDocked.value || hasKeepOpenFabBubbles.value || fabRandomAction.value || !shouldFabAutoDock()) return
|
||||||
|
|
||||||
fabIdleTimer = window.setTimeout(() => {
|
fabIdleTimer = window.setTimeout(() => {
|
||||||
|
fabIdleTimer = null
|
||||||
|
if (fabDocked.value || hasKeepOpenFabBubbles.value || !shouldFabAutoDock()) return
|
||||||
|
|
||||||
if (fabRandomAction.value) {
|
if (fabRandomAction.value) {
|
||||||
scheduleFabAutoDock()
|
scheduleFabAutoDock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
setFabDocked(true)
|
setFabDocked(true)
|
||||||
fabIdleTimer = null
|
|
||||||
}, FAB_IDLE_DOCK_DELAY)
|
}, FAB_IDLE_DOCK_DELAY)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -765,7 +924,7 @@ function finishFabRandomAction() {
|
|||||||
clearFabRandomActionEndTimer()
|
clearFabRandomActionEndTimer()
|
||||||
fabRandomAction.value = null
|
fabRandomAction.value = null
|
||||||
|
|
||||||
const shouldAutoDock = !fabDocked.value && isFabNearRightEdge()
|
const shouldAutoDock = !fabDocked.value && shouldFabAutoDock()
|
||||||
|
|
||||||
if (shouldAutoDock) {
|
if (shouldAutoDock) {
|
||||||
scheduleFabAutoDock()
|
scheduleFabAutoDock()
|
||||||
@@ -993,15 +1152,19 @@ function setFabDocked(docked: boolean) {
|
|||||||
|
|
||||||
if (docked) {
|
if (docked) {
|
||||||
clearFabIdleTimer()
|
clearFabIdleTimer()
|
||||||
fabPosition.value = {
|
const dockedPosition = {
|
||||||
...currentPosition,
|
...currentPosition,
|
||||||
x: getDockedFabX(),
|
x: getDockedFabX(),
|
||||||
|
y: clampFabY(currentPosition.y),
|
||||||
}
|
}
|
||||||
|
fabPosition.value = dockedPosition
|
||||||
scheduleFabBubblePositionUpdate()
|
scheduleFabBubblePositionUpdate()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updateFabPosition({
|
fabPosition.value = fabPositionAnchor
|
||||||
|
? getFabPositionFromAnchor(fabPositionAnchor)
|
||||||
|
: clampFabPosition({
|
||||||
...currentPosition,
|
...currentPosition,
|
||||||
x: Math.min(
|
x: Math.min(
|
||||||
currentPosition.x,
|
currentPosition.x,
|
||||||
@@ -1009,28 +1172,125 @@ function setFabDocked(docked: boolean) {
|
|||||||
),
|
),
|
||||||
})
|
})
|
||||||
scheduleFabBubblePositionUpdate()
|
scheduleFabBubblePositionUpdate()
|
||||||
|
nextTick(() => {
|
||||||
|
if (fabPositionAnchor) {
|
||||||
|
updateFabPosition(getFabPositionFromAnchor(fabPositionAnchor), { syncAnchor: false })
|
||||||
scheduleFabAutoDock()
|
scheduleFabAutoDock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFabAnchorFromPosition()
|
||||||
|
scheduleFabAutoDock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFabDragState() {
|
||||||
|
fabDragState = null
|
||||||
|
fabDragging.value = false
|
||||||
|
fabPressed.value = false
|
||||||
|
teardownFabTouchMoveGuard()
|
||||||
|
}
|
||||||
|
|
||||||
|
function releaseFabPointerCapture(event: PointerEvent) {
|
||||||
|
try {
|
||||||
|
;(event.currentTarget as HTMLElement).releasePointerCapture?.(event.pointerId)
|
||||||
|
} catch {
|
||||||
|
// 指针捕获可能已被浏览器或 DevTools 中断,状态以组件内清理为准。
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelFabDrag() {
|
||||||
|
const wasDragging = fabDragging.value
|
||||||
|
|
||||||
|
clearFabDragState()
|
||||||
|
if (!wasDragging) {
|
||||||
|
scheduleFabAutoDock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldFabAutoDock()) {
|
||||||
|
scheduleFabAutoDock()
|
||||||
|
} else {
|
||||||
|
clearFabIdleTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function guardFabPointerEvent(event: PointerEvent, options: { preventTouchDefault?: boolean } = {}) {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (
|
||||||
|
options.preventTouchDefault &&
|
||||||
|
(event.pointerType === 'touch' || event.pointerType === 'pen') &&
|
||||||
|
event.cancelable
|
||||||
|
) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupFabTouchMoveGuard() {
|
||||||
|
if (stopFabTouchMoveGuard) return
|
||||||
|
|
||||||
|
const handleTouchMove = (event: TouchEvent) => {
|
||||||
|
if (!fabDragState) return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
if (event.cancelable) event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('touchmove', handleTouchMove, { capture: true, passive: false })
|
||||||
|
stopFabTouchMoveGuard = () => {
|
||||||
|
document.removeEventListener('touchmove', handleTouchMove, { capture: true })
|
||||||
|
stopFabTouchMoveGuard = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function teardownFabTouchMoveGuard() {
|
||||||
|
stopFabTouchMoveGuard?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
function isPressedDragPointer(event: PointerEvent) {
|
||||||
|
if (event.pointerType === 'touch' || event.pointerType === 'pen') {
|
||||||
|
return event.buttons !== 0 || event.pressure > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return event.buttons !== 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFabTriggerPointerDown(event: PointerEvent) {
|
function handleFabTriggerPointerDown(event: PointerEvent) {
|
||||||
|
guardFabPointerEvent(event)
|
||||||
|
if (fabSuppressNextClick) {
|
||||||
|
fabSuppressNextClick = false
|
||||||
|
clearFabSuppressNextClickTimer()
|
||||||
|
}
|
||||||
fabPressed.value = true
|
fabPressed.value = true
|
||||||
pauseFabAutoDock()
|
pauseFabAutoDock()
|
||||||
|
if (event.pointerType === 'touch' || event.pointerType === 'pen') setupFabTouchMoveGuard()
|
||||||
|
|
||||||
const currentPosition = getCurrentFabPosition()
|
const currentPosition = getCurrentFabPosition()
|
||||||
|
const dragStartPosition = fabDocked.value ? getOpenFabPositionForDrag(currentPosition) : currentPosition
|
||||||
fabDragState = {
|
fabDragState = {
|
||||||
pointerId: event.pointerId,
|
pointerId: event.pointerId,
|
||||||
startClientX: event.clientX,
|
startClientX: event.clientX,
|
||||||
startClientY: event.clientY,
|
startClientY: event.clientY,
|
||||||
startX: currentPosition.x,
|
startX: dragStartPosition.x,
|
||||||
startY: currentPosition.y,
|
startY: dragStartPosition.y,
|
||||||
moved: false,
|
moved: false,
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
;(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId)
|
;(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId)
|
||||||
|
} catch {
|
||||||
|
// 指针可能已被浏览器取消,拖拽状态仍按后续 pointermove/pointerup 收敛。
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFabTriggerPointerMove(event: PointerEvent) {
|
function handleFabTriggerPointerMove(event: PointerEvent) {
|
||||||
|
guardFabPointerEvent(event, { preventTouchDefault: true })
|
||||||
updateFabPointer(event)
|
updateFabPointer(event)
|
||||||
if (!fabDragState || fabDragState.pointerId !== event.pointerId) return
|
if (!fabDragState || fabDragState.pointerId !== event.pointerId) return
|
||||||
|
if (!isPressedDragPointer(event)) {
|
||||||
|
releaseFabPointerCapture(event)
|
||||||
|
cancelFabDrag()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const deltaX = event.clientX - fabDragState.startClientX
|
const deltaX = event.clientX - fabDragState.startClientX
|
||||||
const deltaY = event.clientY - fabDragState.startClientY
|
const deltaY = event.clientY - fabDragState.startClientY
|
||||||
@@ -1040,8 +1300,17 @@ function handleFabTriggerPointerMove(event: PointerEvent) {
|
|||||||
|
|
||||||
fabDragState.moved = true
|
fabDragState.moved = true
|
||||||
fabDragging.value = true
|
fabDragging.value = true
|
||||||
fabSuppressNextClick = true
|
if (fabDocked.value) {
|
||||||
fabDocked.value = false
|
fabDocked.value = false
|
||||||
|
fabPosition.value = clampFabPosition(
|
||||||
|
{
|
||||||
|
x: fabDragState.startX,
|
||||||
|
y: fabDragState.startY,
|
||||||
|
},
|
||||||
|
{ useOpenBounds: true },
|
||||||
|
)
|
||||||
|
scheduleFabBubblePositionUpdate()
|
||||||
|
}
|
||||||
updateFabPosition({
|
updateFabPosition({
|
||||||
x: fabDragState.startX + deltaX,
|
x: fabDragState.startX + deltaX,
|
||||||
y: fabDragState.startY + deltaY,
|
y: fabDragState.startY + deltaY,
|
||||||
@@ -1049,19 +1318,30 @@ function handleFabTriggerPointerMove(event: PointerEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleFabTriggerPointerUp(event: PointerEvent) {
|
function handleFabTriggerPointerUp(event: PointerEvent) {
|
||||||
|
guardFabPointerEvent(event)
|
||||||
fabPressed.value = false
|
fabPressed.value = false
|
||||||
|
const dragState = fabDragState
|
||||||
const wasDragging = fabDragging.value
|
const wasDragging = fabDragging.value
|
||||||
|
|
||||||
|
if (wasDragging && dragState?.pointerId === event.pointerId) {
|
||||||
|
updateFabPosition({
|
||||||
|
x: dragState.startX + event.clientX - dragState.startClientX,
|
||||||
|
y: dragState.startY + event.clientY - dragState.startClientY,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fabDragging.value = false
|
fabDragging.value = false
|
||||||
fabDragState = null
|
fabDragState = null
|
||||||
;(event.currentTarget as HTMLElement).releasePointerCapture?.(event.pointerId)
|
teardownFabTouchMoveGuard()
|
||||||
|
releaseFabPointerCapture(event)
|
||||||
|
|
||||||
if (!wasDragging) {
|
if (!wasDragging) {
|
||||||
scheduleFabAutoDock()
|
scheduleFabAutoDock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFabNearRightEdge()) {
|
suppressNextFabClick()
|
||||||
|
if (shouldFabAutoDock()) {
|
||||||
scheduleFabAutoDock()
|
scheduleFabAutoDock()
|
||||||
} else {
|
} else {
|
||||||
clearFabIdleTimer()
|
clearFabIdleTimer()
|
||||||
@@ -1069,12 +1349,40 @@ function handleFabTriggerPointerUp(event: PointerEvent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFabTriggerClick() {
|
function handleFabTriggerPointerCancel(event: PointerEvent) {
|
||||||
if (fabSuppressNextClick) {
|
guardFabPointerEvent(event)
|
||||||
|
fabPressed.value = false
|
||||||
|
|
||||||
|
if (!fabDragState || fabDragState.pointerId !== event.pointerId) return
|
||||||
|
|
||||||
|
releaseFabPointerCapture(event)
|
||||||
|
|
||||||
|
cancelFabDrag()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFabTriggerLostPointerCapture(event: PointerEvent) {
|
||||||
|
if (!fabDragState || fabDragState.pointerId !== event.pointerId) return
|
||||||
|
|
||||||
|
cancelFabDrag()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWindowFabPointerEnd(event: PointerEvent) {
|
||||||
|
if (!fabDragState || fabDragState.pointerId !== event.pointerId) return
|
||||||
|
|
||||||
|
cancelFabDrag()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFabTriggerClick(event: MouseEvent) {
|
||||||
|
event.stopPropagation()
|
||||||
|
if (fabSuppressNextClick && event.detail !== 0) {
|
||||||
fabSuppressNextClick = false
|
fabSuppressNextClick = false
|
||||||
|
clearFabSuppressNextClickTimer()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fabSuppressNextClick = false
|
||||||
|
clearFabSuppressNextClickTimer()
|
||||||
|
|
||||||
if (fabDocked.value) {
|
if (fabDocked.value) {
|
||||||
setFabDocked(false)
|
setFabDocked(false)
|
||||||
return
|
return
|
||||||
@@ -1085,7 +1393,7 @@ function handleFabTriggerClick() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleFabPointerLeave() {
|
function handleFabPointerLeave() {
|
||||||
if (!fabDocked.value && isFabNearRightEdge()) scheduleFabAutoDock()
|
if (!fabDocked.value && shouldFabAutoDock()) scheduleFabAutoDock()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFabPointerEnter() {
|
function handleFabPointerEnter() {
|
||||||
@@ -1096,6 +1404,9 @@ onMounted(() => {
|
|||||||
nextTick(resetFabPosition)
|
nextTick(resetFabPosition)
|
||||||
setAgentAssistantBubbleEntryActive(props.active)
|
setAgentAssistantBubbleEntryActive(props.active)
|
||||||
window.addEventListener('resize', handleWindowResize)
|
window.addEventListener('resize', handleWindowResize)
|
||||||
|
window.visualViewport?.addEventListener('resize', handleWindowResize)
|
||||||
|
window.addEventListener('pointerup', handleWindowFabPointerEnd, { passive: true })
|
||||||
|
window.addEventListener('pointercancel', handleWindowFabPointerEnd, { passive: true })
|
||||||
window.addEventListener('pointermove', handleGlobalFabPointer, { passive: true })
|
window.addEventListener('pointermove', handleGlobalFabPointer, { passive: true })
|
||||||
window.addEventListener('pointerdown', handleGlobalFabPointer, { passive: true })
|
window.addEventListener('pointerdown', handleGlobalFabPointer, { passive: true })
|
||||||
stopBubbleListener = onAgentAssistantBubble(showAgentAssistantBubble)
|
stopBubbleListener = onAgentAssistantBubble(showAgentAssistantBubble)
|
||||||
@@ -1108,7 +1419,7 @@ watch(
|
|||||||
setAgentAssistantBubbleEntryActive(active)
|
setAgentAssistantBubbleEntryActive(active)
|
||||||
|
|
||||||
if (active) {
|
if (active) {
|
||||||
if (isFabNearRightEdge()) scheduleFabAutoDock()
|
if (shouldFabAutoDock()) scheduleFabAutoDock()
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
syncFabBubbleResizeObserver()
|
syncFabBubbleResizeObserver()
|
||||||
syncFabBubblePosition()
|
syncFabBubblePosition()
|
||||||
@@ -1126,15 +1437,20 @@ watch(
|
|||||||
watch([() => props.active, () => props.thinking, fabDocked, fabDragging, fabPressed], syncFabRandomActionSchedule)
|
watch([() => props.active, () => props.thinking, fabDocked, fabDragging, fabPressed], syncFabRandomActionSchedule)
|
||||||
|
|
||||||
onScopeDispose(clearFabIdleTimer)
|
onScopeDispose(clearFabIdleTimer)
|
||||||
|
onScopeDispose(clearFabSuppressNextClickTimer)
|
||||||
onScopeDispose(clearFabRandomAction)
|
onScopeDispose(clearFabRandomAction)
|
||||||
onScopeDispose(resetFabBubbles)
|
onScopeDispose(resetFabBubbles)
|
||||||
onScopeDispose(teardownFabBubblePositioning)
|
onScopeDispose(teardownFabBubblePositioning)
|
||||||
onScopeDispose(clearFabBubbleUndockPositionTimer)
|
onScopeDispose(clearFabBubbleUndockPositionTimer)
|
||||||
|
onScopeDispose(teardownFabTouchMoveGuard)
|
||||||
onScopeDispose(() => {
|
onScopeDispose(() => {
|
||||||
setAgentAssistantBubbleEntryActive(false)
|
setAgentAssistantBubbleEntryActive(false)
|
||||||
stopBubbleListener?.()
|
stopBubbleListener?.()
|
||||||
stopBubbleListener = null
|
stopBubbleListener = null
|
||||||
window.removeEventListener('resize', handleWindowResize)
|
window.removeEventListener('resize', handleWindowResize)
|
||||||
|
window.visualViewport?.removeEventListener('resize', handleWindowResize)
|
||||||
|
window.removeEventListener('pointerup', handleWindowFabPointerEnd)
|
||||||
|
window.removeEventListener('pointercancel', handleWindowFabPointerEnd)
|
||||||
teardownFabPointerTracking()
|
teardownFabPointerTracking()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1211,7 +1527,8 @@ defineExpose({
|
|||||||
@pointerdown="handleFabTriggerPointerDown"
|
@pointerdown="handleFabTriggerPointerDown"
|
||||||
@pointermove="handleFabTriggerPointerMove"
|
@pointermove="handleFabTriggerPointerMove"
|
||||||
@pointerup="handleFabTriggerPointerUp"
|
@pointerup="handleFabTriggerPointerUp"
|
||||||
@pointercancel="handleFabTriggerPointerUp"
|
@pointercancel="handleFabTriggerPointerCancel"
|
||||||
|
@lostpointercapture="handleFabTriggerLostPointerCapture"
|
||||||
@click="handleFabTriggerClick"
|
@click="handleFabTriggerClick"
|
||||||
>
|
>
|
||||||
<span class="agent-assistant-fab__bot" aria-hidden="true">
|
<span class="agent-assistant-fab__bot" aria-hidden="true">
|
||||||
@@ -1294,6 +1611,7 @@ defineExpose({
|
|||||||
inline-size: 5.4rem;
|
inline-size: 5.4rem;
|
||||||
inset-block: auto 0;
|
inset-block: auto 0;
|
||||||
inset-inline: auto 0;
|
inset-inline: auto 0;
|
||||||
|
outline: none;
|
||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
text-align: start;
|
text-align: start;
|
||||||
touch-action: none;
|
touch-action: none;
|
||||||
@@ -1539,7 +1857,6 @@ defineExpose({
|
|||||||
opacity 0.18s ease;
|
opacity 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab__bubble:hover .agent-assistant-fab__bubble-close,
|
|
||||||
.agent-assistant-fab__bubble-close:focus-visible {
|
.agent-assistant-fab__bubble-close:focus-visible {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
@@ -1800,12 +2117,30 @@ defineExpose({
|
|||||||
transform-origin: top center;
|
transform-origin: top center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab.is-bubble-visible .agent-assistant-fab__bubble:hover {
|
@media (hover: hover) and (pointer: fine) {
|
||||||
|
.agent-assistant-fab.is-bubble-visible .agent-assistant-fab__bubble:hover {
|
||||||
box-shadow: var(--app-surface-hover-shadow);
|
box-shadow: var(--app-surface-hover-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__bubble:hover .agent-assistant-fab__bubble-close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__trigger:hover .agent-assistant-fab__bot {
|
||||||
|
filter: drop-shadow(0 0.7rem 0.7rem var(--agent-assistant-robot-shadow-strong));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab__trigger:hover .agent-assistant-fab__bot {
|
@media (hover: none), (pointer: coarse) {
|
||||||
filter: drop-shadow(0 0.7rem 0.7rem var(--agent-assistant-robot-shadow-strong));
|
.agent-assistant-fab__bubble-close {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-assistant-fab__trigger:focus-visible .agent-assistant-fab__bot {
|
||||||
|
filter:
|
||||||
|
drop-shadow(0 0.55rem 0.55rem var(--agent-assistant-robot-shadow))
|
||||||
|
drop-shadow(0 0 0.34rem rgba(var(--v-theme-primary), 0.55));
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-assistant-fab.is-pressed .agent-assistant-fab__bot {
|
.agent-assistant-fab.is-pressed .agent-assistant-fab__bot {
|
||||||
|
|||||||
Reference in New Issue
Block a user