Commit 0871b91b by pangchong

feat(aiAssistant): 实现悬浮球拖拽与磁力吸边功能

- 支持悬浮球自由拖拽,拖动时样式动态更新为抓取样式
- 松开时根据屏幕中心自动磁力吸附至左侧或右侧边缘
- 气泡方向根据吸附边自动反转,避免遮挡编辑界面
- 增加悬浮球上下锚点,支持顶部或底部多点吸附
- 对话窗口可拖动,未拖动时保持右下角贴边自适应
- 窗口位置和悬浮球位置在视口变化时自动调整约束
- 添加悬浮球位置和停靠边状态持久化存储支持
- 新增帮助文档条目,介绍悬浮球移动及吸边操作流程
parent 462e6b32
......@@ -1250,6 +1250,38 @@ export const FAQ_DATABASE: FaqItem[] = [
'展示内容包含 Naive UI 全部语义化颜色(Primary, Info, Success, Warning, Error)、字阶规范、圆角与阴影 Token,并支持一键复制色值。'
],
relatedQuestions: ['faq_theme_settings', 'faq_primary_colors']
},
{
id: 'faq_ai_assistant_drag_dock',
category: 'settings',
categoryName: '设置与快捷键',
title: '如何移动工卡智能助手悬浮球?悬浮球如何磁力吸边?',
keywords: [
'智能助手',
'悬浮球',
'吸边',
'移动助手',
'磁力吸边',
'ai位置',
'ai助手',
'拖动小助手',
'ai',
'znzs',
'xfq',
'xb',
'clxb'
],
synonyms: ['怎么拖动智能小助手', '悬浮客服可以挪动吗', '小助手吸附边缘', '悬浮图标位置调整'],
answer: '智能工卡助手悬浮球支持自由拖动与智能磁力吸边自适应:',
steps: [
'【自由拖动】:按住右下角(或左侧)的智能助手悬浮球,即可在屏幕可视区域内任意拖动位置。',
'【磁力吸附】:松开鼠标或触摸后,悬浮球会自动根据屏幕中线判断,平滑吸附停靠到最近的左侧或右侧边缘(吸边间距 16px)。',
'【自适应窗口与控制台】:悬浮球采用响应式边缘停靠机制,无论打开/关闭浏览器 F12 控制台还是缩放窗口,都会紧贴边缘,永不脱轨或滞留在屏幕中央。'
],
highlights: [
'悬浮球靠右吸附时,气泡与提示语向左侧展开;靠左吸附时自动反转方向(头像在左、气泡向右),确保不遮挡主要编辑界面。'
],
relatedQuestions: ['faq_theme_settings', 'faq_keyboard_shortcuts']
}
]
......
......@@ -5,19 +5,20 @@
<div
v-if="!isOpen"
ref="badgeRef"
class="fixed z-40 flex items-center flex-nowrap whitespace-nowrap group cursor-grab active:cursor-grabbing select-none"
:class="[isBadgeSnapping ? 'transition-all duration-300 ease-out' : '']"
:style="{
left: `${badgePos.x}px`,
top: `${badgePos.y}px`,
touchAction: 'none'
}"
class="fixed z-40 flex items-center flex-nowrap whitespace-nowrap group select-none"
:class="[
isDraggingBadge ? 'cursor-grabbing' : 'cursor-grab',
isBadgeSnapping ? 'transition-all duration-300 ease-out' : '',
badgeSide === 'left' ? 'flex-row-reverse' : 'flex-row'
]"
:style="badgeStyle"
@mousedown="handleBadgeMouseDown"
@touchstart.passive="handleBadgeTouchStart"
>
<!-- 悬浮小气泡提示语(强制单行不换行) -->
<div
class="mr-3 px-3 py-1.5 rounded-full bg-card border border-divider shadow-lg text-xs font-medium text-color1 flex items-center gap-1.5 whitespace-nowrap shrink-0 opacity-90 group-hover:opacity-100 group-hover:scale-105 transition-all duration-200 pointer-events-none"
class="px-3 py-1.5 rounded-full bg-card border border-divider shadow-lg text-xs font-medium text-color1 flex items-center gap-1.5 whitespace-nowrap shrink-0 opacity-90 group-hover:opacity-100 group-hover:scale-105 transition-all duration-200 pointer-events-none"
:class="[badgeSide === 'left' ? 'ml-3' : 'mr-3']"
:style="{
borderColor: `color-mix(in srgb, ${themeVars.primaryColor} 30%, transparent)`,
boxShadow: `0 4px 16px color-mix(in srgb, ${themeVars.primaryColor} 20%, transparent)`
......@@ -67,11 +68,7 @@
ref="windowRef"
class="fixed z-50 flex flex-col rounded-2xl border border-divider bg-card shadow-2xl overflow-hidden text-color1"
:class="[isMinimized ? 'h-[52px] w-[320px]' : 'h-[580px] w-[390px] max-h-[calc(100vh-32px)] max-w-[calc(100vw-32px)]']"
:style="{
left: `${winPos.x}px`,
top: `${winPos.y}px`,
boxShadow: `0 12px 36px rgba(0, 0, 0, 0.18), 0 0 1px ${themeVars.primaryColor}40`
}"
:style="winStyle"
>
<!-- 头部工具栏(按住可拖动窗口) -->
<div
......@@ -617,6 +614,10 @@ const {
isMinimized,
unreadCount,
activeCategory,
badgeSide,
badgeStyle,
winStyle,
isDraggingBadge,
inputQuery,
isTyping,
isStreaming,
......
import type { AiAssistantState, Position } from './types'
import type { AiAssistantState, Position, DockSide, AnchorY } from './types'
import type { ChatMessage, FaqCategory } from '@/layouts/components/AiAssistant/constants'
import { DEFAULT_WELCOME_MESSAGE, HOT_QUESTIONS } from '@/layouts/components/AiAssistant/constants'
......@@ -11,8 +11,12 @@ export const useAiAssistantStore = defineStore('aiAssistant', {
isMinimized: false,
unreadCount: 1,
activeCategory: 'all',
badgeSide: 'right',
badgeAnchorY: 'bottom',
badgeOffsetY: 120,
badgePos: { x: 0, y: 0 },
winPos: { x: 0, y: 0 },
winHasMoved: false,
messageList: [
{
id: 'msg_welcome',
......@@ -33,12 +37,20 @@ export const useAiAssistantStore = defineStore('aiAssistant', {
toggleMinimize() {
this.isMinimized = !this.isMinimized
},
setBadgeDock(dock: { side: DockSide; anchorY?: AnchorY; offsetY?: number }) {
this.badgeSide = dock.side
if (dock.anchorY) this.badgeAnchorY = dock.anchorY
if (typeof dock.offsetY === 'number') this.badgeOffsetY = dock.offsetY
},
setBadgePos(pos: Position) {
this.badgePos = pos
},
setWinPos(pos: Position) {
this.winPos = pos
},
setWinHasMoved(moved: boolean) {
this.winHasMoved = moved
},
setActiveCategory(category: FaqCategory) {
this.activeCategory = category
},
......@@ -70,7 +82,9 @@ export const useAiAssistantStore = defineStore('aiAssistant', {
]
}
},
persist: true
persist: {
pick: ['unreadCount', 'activeCategory', 'badgeSide', 'badgeAnchorY', 'badgeOffsetY', 'messageList']
}
})
export * from './types'
......@@ -5,12 +5,20 @@ export interface Position {
y: number
}
export type DockSide = 'left' | 'right'
export type AnchorY = 'top' | 'bottom'
export interface AiAssistantState {
isOpen: boolean
isMinimized: boolean
unreadCount: number
activeCategory: FaqCategory
badgeSide: DockSide
badgeAnchorY: AnchorY
badgeOffsetY: number
badgePos: Position
winPos: Position
winHasMoved: boolean
messageList: ChatMessage[]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment