Commit 0871b91b by pangchong

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

- 支持悬浮球自由拖拽,拖动时样式动态更新为抓取样式
- 松开时根据屏幕中心自动磁力吸附至左侧或右侧边缘
- 气泡方向根据吸附边自动反转,避免遮挡编辑界面
- 增加悬浮球上下锚点,支持顶部或底部多点吸附
- 对话窗口可拖动,未拖动时保持右下角贴边自适应
- 窗口位置和悬浮球位置在视口变化时自动调整约束
- 添加悬浮球位置和停靠边状态持久化存储支持
- 新增帮助文档条目,介绍悬浮球移动及吸边操作流程
parent 462e6b32
...@@ -1250,6 +1250,38 @@ export const FAQ_DATABASE: FaqItem[] = [ ...@@ -1250,6 +1250,38 @@ export const FAQ_DATABASE: FaqItem[] = [
'展示内容包含 Naive UI 全部语义化颜色(Primary, Info, Success, Warning, Error)、字阶规范、圆角与阴影 Token,并支持一键复制色值。' '展示内容包含 Naive UI 全部语义化颜色(Primary, Info, Success, Warning, Error)、字阶规范、圆角与阴影 Token,并支持一键复制色值。'
], ],
relatedQuestions: ['faq_theme_settings', 'faq_primary_colors'] 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']
} }
] ]
......
import type { FaqItem, FaqCategory, ChatMessage, FaqAction, MatchResult, StepItemParsed } from '../constants' import type { FaqItem, FaqCategory, ChatMessage, FaqAction, MatchResult, StepItemParsed } from '../constants'
import { FAQ_CATEGORIES, FAQ_DATABASE, HOT_QUESTIONS } from '../constants' import { FAQ_CATEGORIES, FAQ_DATABASE, HOT_QUESTIONS } from '../constants'
import { useEditorStore } from '@/store/editor' import { useEditorStore } from '@/store/editor'
import { useAiAssistantStore } from '@/store/aiAssistant' import { useAiAssistantStore, type DockSide, type AnchorY } from '@/store/aiAssistant'
/** /**
* 文本清洗规范化 * 文本清洗规范化
...@@ -219,7 +220,19 @@ export const useAiAssistant = () => { ...@@ -219,7 +220,19 @@ export const useAiAssistant = () => {
const aiStore = useAiAssistantStore() const aiStore = useAiAssistantStore()
// 解构 Pinia 响应式状态 // 解构 Pinia 响应式状态
const { isOpen, isMinimized, unreadCount, activeCategory, badgePos, winPos, messageList } = storeToRefs(aiStore) const {
isOpen,
isMinimized,
unreadCount,
activeCategory,
badgeSide,
badgeAnchorY,
badgeOffsetY,
badgePos,
winPos,
winHasMoved,
messageList
} = storeToRefs(aiStore)
// 本地 UI 交互临时状态 // 本地 UI 交互临时状态
const inputQuery = ref('') const inputQuery = ref('')
...@@ -233,8 +246,51 @@ export const useAiAssistant = () => { ...@@ -233,8 +246,51 @@ export const useAiAssistant = () => {
const badgeRef = ref<HTMLElement | null>(null) const badgeRef = ref<HTMLElement | null>(null)
const windowRef = ref<HTMLElement | null>(null) const windowRef = ref<HTMLElement | null>(null)
const isBadgeSnapping = ref(false) const isBadgeSnapping = ref(false)
const isDraggingBadge = ref(false)
const isDraggingWin = ref(false)
const dragPos = ref({ x: 0, y: 0 })
let snapTimer: ReturnType<typeof setTimeout> | null = null
const isInitialized = ref(false) const isInitialized = ref(false)
// 悬浮球动态定位样式:未拖拽时直接以 right/left 贴边,窗口变化(如开闭控制台)永不脱轨
const badgeStyle = computed(() => {
if (isDraggingBadge.value || isBadgeSnapping.value) {
return {
left: `${dragPos.value.x}px`,
top: `${dragPos.value.y}px`,
right: 'auto',
bottom: 'auto',
touchAction: 'none'
}
}
return {
left: badgeSide.value === 'left' ? '16px' : 'auto',
right: badgeSide.value === 'right' ? '16px' : 'auto',
top: badgeAnchorY.value === 'top' ? `${badgeOffsetY.value}px` : 'auto',
bottom: badgeAnchorY.value === 'bottom' ? `${badgeOffsetY.value}px` : 'auto',
touchAction: 'none'
}
})
// 对话窗口动态定位样式:未手动拖动时保持右下角停靠,自适应窗口变化
const winStyle = computed(() => {
const shadow = `0 12px 36px rgba(0, 0, 0, 0.18), 0 0 1px color-mix(in srgb, ${themeVars.value.primaryColor} 40%, transparent)`
if (isDraggingWin.value || winHasMoved.value) {
return {
left: `${winPos.value.x}px`,
top: `${winPos.value.y}px`,
boxShadow: shadow
}
}
return {
left: badgeSide.value === 'left' ? '24px' : 'auto',
right: badgeSide.value === 'right' ? '24px' : 'auto',
bottom: '24px',
top: 'auto',
boxShadow: shadow
}
})
// 上下文感知推荐:根据当前选中的节点动态计算关联 FAQ // 上下文感知推荐:根据当前选中的节点动态计算关联 FAQ
const contextFaqs = computed<FaqItem[]>(() => { const contextFaqs = computed<FaqItem[]>(() => {
const selectedTag = editorStore.selectedNode?.tagName const selectedTag = editorStore.selectedNode?.tagName
...@@ -245,21 +301,31 @@ export const useAiAssistant = () => { ...@@ -245,21 +301,31 @@ export const useAiAssistant = () => {
// 初始化悬浮球与窗口默认位置 // 初始化悬浮球与窗口默认位置
const initPositions = () => { const initPositions = () => {
const winW = window.innerWidth || 1200 const winW = typeof window !== 'undefined' ? window.innerWidth : 1200
const winH = window.innerHeight || 800 const winH = typeof window !== 'undefined' ? window.innerHeight : 800
if (!badgePos.value.x || !badgePos.value.y) { if (!badgeSide.value) {
aiStore.setBadgePos({ const isLeft = badgePos.value?.x && badgePos.value.x + 120 < winW / 2
x: Math.max(16, winW - 250), aiStore.setBadgeDock({
y: Math.max(16, winH - 140) side: isLeft ? 'left' : 'right',
anchorY: 'bottom',
offsetY: 120
}) })
} }
if (!winPos.value.x || !winPos.value.y) { const badgeW = badgeRef.value?.offsetWidth || 240
aiStore.setWinPos({ const badgeH = badgeRef.value?.offsetHeight || 56
x: Math.max(16, winW - 414), const curX = badgeSide.value === 'left' ? 16 : Math.max(16, winW - badgeW - 16)
y: Math.max(16, winH - 604) const curY =
}) badgeAnchorY.value === 'top' ? badgeOffsetY.value : Math.max(16, winH - badgeOffsetY.value - badgeH)
aiStore.setBadgePos({ x: curX, y: curY })
if (!winHasMoved.value) {
const dialogW = isMinimized.value ? 320 : 390
const dialogH = isMinimized.value ? 52 : 580
const curWinX = badgeSide.value === 'left' ? 24 : Math.max(16, winW - dialogW - 24)
const curWinY = Math.max(16, winH - dialogH - 24)
aiStore.setWinPos({ x: curWinX, y: curWinY })
} }
isInitialized.value = true isInitialized.value = true
...@@ -267,24 +333,43 @@ export const useAiAssistant = () => { ...@@ -267,24 +333,43 @@ export const useAiAssistant = () => {
// 窗口尺寸变化时约束位置在视口内 // 窗口尺寸变化时约束位置在视口内
const handleResize = () => { const handleResize = () => {
const winW = window.innerWidth || 1200 const winW = typeof window !== 'undefined' ? window.innerWidth : 1200
const winH = window.innerHeight || 800 const winH = typeof window !== 'undefined' ? window.innerHeight : 800
const badgeW = badgeRef.value?.offsetWidth || 240 const badgeW = badgeRef.value?.offsetWidth || 240
const badgeH = badgeRef.value?.offsetHeight || 56 const badgeH = badgeRef.value?.offsetHeight || 56
const clampedBadgeX = Math.max(10, Math.min(winW - badgeW - 10, badgePos.value.x))
const clampedBadgeY = Math.max(10, Math.min(winH - badgeH - 10, badgePos.value.y)) // 约束垂直偏移在视口范围内
aiStore.setBadgePos({ x: clampedBadgeX, y: clampedBadgeY }) const maxBadgeOffsetY = Math.max(16, winH - badgeH - 16)
if (badgeOffsetY.value > maxBadgeOffsetY) {
aiStore.setBadgeDock({
side: badgeSide.value,
anchorY: badgeAnchorY.value,
offsetY: maxBadgeOffsetY
})
}
// 响应视口变化,同步最新绝对像素坐标
const curX = badgeSide.value === 'left' ? 16 : Math.max(16, winW - badgeW - 16)
const curY =
badgeAnchorY.value === 'top' ? badgeOffsetY.value : Math.max(16, winH - badgeOffsetY.value - badgeH)
aiStore.setBadgePos({ x: curX, y: curY })
const dialogW = isMinimized.value ? 320 : 390 const dialogW = isMinimized.value ? 320 : 390
const dialogH = isMinimized.value ? 52 : 580 const dialogH = isMinimized.value ? 52 : 580
const clampedWinX = Math.max(10, Math.min(winW - dialogW, winPos.value.x))
const clampedWinY = Math.max(10, Math.min(winH - dialogH, winPos.value.y)) if (!winHasMoved.value) {
const curWinX = badgeSide.value === 'left' ? 24 : Math.max(16, winW - dialogW - 24)
const curWinY = Math.max(16, winH - dialogH - 24)
aiStore.setWinPos({ x: curWinX, y: curWinY })
} else {
const clampedWinX = Math.max(10, Math.min(winW - dialogW - 10, winPos.value.x))
const clampedWinY = Math.max(10, Math.min(winH - dialogH - 10, winPos.value.y))
aiStore.setWinPos({ x: clampedWinX, y: clampedWinY }) aiStore.setWinPos({ x: clampedWinX, y: clampedWinY })
} }
}
// ── 悬浮球拖拽与磁力吸边逻辑 ────────────────────────────────────────────────── // ── 悬浮球拖拽与磁力吸边逻辑 ──────────────────────────────────────────────────
let isDraggingBadge = false
let badgeStartX = 0 let badgeStartX = 0
let badgeStartY = 0 let badgeStartY = 0
let pointerStartX = 0 let pointerStartX = 0
...@@ -292,16 +377,23 @@ export const useAiAssistant = () => { ...@@ -292,16 +377,23 @@ export const useAiAssistant = () => {
let hasMovedBadge = false let hasMovedBadge = false
const startBadgeDrag = (clientX: number, clientY: number) => { const startBadgeDrag = (clientX: number, clientY: number) => {
isDraggingBadge = true if (snapTimer) {
clearTimeout(snapTimer)
snapTimer = null
}
isDraggingBadge.value = true
hasMovedBadge = false hasMovedBadge = false
isBadgeSnapping.value = false isBadgeSnapping.value = false
pointerStartX = clientX pointerStartX = clientX
pointerStartY = clientY pointerStartY = clientY
badgeStartX = badgePos.value.x
badgeStartY = badgePos.value.y const rect = badgeRef.value?.getBoundingClientRect()
badgeStartX = rect ? rect.left : badgePos.value.x
badgeStartY = rect ? rect.top : badgePos.value.y
dragPos.value = { x: badgeStartX, y: badgeStartY }
const onMove = (e: MouseEvent | TouchEvent) => { const onMove = (e: MouseEvent | TouchEvent) => {
if (!isDraggingBadge) return if (!isDraggingBadge.value) return
const curX = 'touches' in e ? e.touches?.[0]?.clientX : e.clientX const curX = 'touches' in e ? e.touches?.[0]?.clientX : e.clientX
const curY = 'touches' in e ? e.touches?.[0]?.clientY : e.clientY const curY = 'touches' in e ? e.touches?.[0]?.clientY : e.clientY
if (curX === undefined || curY === undefined) return if (curX === undefined || curY === undefined) return
...@@ -315,37 +407,57 @@ export const useAiAssistant = () => { ...@@ -315,37 +407,57 @@ export const useAiAssistant = () => {
const winW = window.innerWidth const winW = window.innerWidth
const winH = window.innerHeight const winH = window.innerHeight
const badgeW = badgeRef.value?.offsetWidth || 180 const badgeW = badgeRef.value?.offsetWidth || 240
const badgeH = badgeRef.value?.offsetHeight || 56 const badgeH = badgeRef.value?.offsetHeight || 56
aiStore.setBadgePos({ dragPos.value = {
x: Math.max(8, Math.min(winW - badgeW - 8, badgeStartX + dx)), x: Math.max(8, Math.min(winW - badgeW - 8, badgeStartX + dx)),
y: Math.max(8, Math.min(winH - badgeH - 8, badgeStartY + dy)) y: Math.max(8, Math.min(winH - badgeH - 8, badgeStartY + dy))
}) }
} }
const onEnd = () => { const onEnd = () => {
isDraggingBadge = false
window.removeEventListener('mousemove', onMove) window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onEnd) window.removeEventListener('mouseup', onEnd)
window.removeEventListener('touchmove', onMove) window.removeEventListener('touchmove', onMove)
window.removeEventListener('touchend', onEnd) window.removeEventListener('touchend', onEnd)
if (!hasMovedBadge) { if (!hasMovedBadge) {
isDraggingBadge.value = false
handleToggleOpen() handleToggleOpen()
} else { } else {
const winW = window.innerWidth const winW = window.innerWidth
const badgeW = badgeRef.value?.offsetWidth || 220 const winH = window.innerHeight
const snapX = badgePos.value.x + badgeW / 2 < winW / 2 ? 16 : winW - badgeW - 16 const badgeW = badgeRef.value?.offsetWidth || 240
const badgeH = badgeRef.value?.offsetHeight || 56
const centerX = dragPos.value.x + badgeW / 2
const centerY = dragPos.value.y + badgeH / 2
const newSide: DockSide = centerX < winW / 2 ? 'left' : 'right'
const newAnchorY: AnchorY = centerY < winH / 2 ? 'top' : 'bottom'
const targetX = newSide === 'left' ? 16 : Math.max(16, winW - badgeW - 16)
const newOffsetY =
newAnchorY === 'top'
? Math.max(16, Math.min(winH - badgeH - 16, dragPos.value.y))
: Math.max(16, Math.min(winH - badgeH - 16, winH - dragPos.value.y - badgeH))
const targetY = newAnchorY === 'top' ? newOffsetY : winH - newOffsetY - badgeH
isBadgeSnapping.value = true isBadgeSnapping.value = true
aiStore.setBadgePos({ dragPos.value = { x: targetX, y: targetY }
x: snapX,
y: badgePos.value.y
})
setTimeout(() => { snapTimer = setTimeout(() => {
isBadgeSnapping.value = false isBadgeSnapping.value = false
}, 350) aiStore.setBadgeDock({
side: newSide,
anchorY: newAnchorY,
offsetY: newOffsetY
})
aiStore.setBadgePos({ x: targetX, y: targetY })
isDraggingBadge.value = false
snapTimer = null
}, 320)
} }
} }
...@@ -367,21 +479,23 @@ export const useAiAssistant = () => { ...@@ -367,21 +479,23 @@ export const useAiAssistant = () => {
} }
// ── 对话窗口拖拽逻辑 ────────────────────────────────────────────────────────── // ── 对话窗口拖拽逻辑 ──────────────────────────────────────────────────────────
let isDraggingWin = false
let winStartX = 0 let winStartX = 0
let winStartY = 0 let winStartY = 0
let winPointerStartX = 0 let winPointerStartX = 0
let winPointerStartY = 0 let winPointerStartY = 0
const startWindowDrag = (clientX: number, clientY: number) => { const startWindowDrag = (clientX: number, clientY: number) => {
isDraggingWin = true isDraggingWin.value = true
winPointerStartX = clientX winPointerStartX = clientX
winPointerStartY = clientY winPointerStartY = clientY
winStartX = winPos.value.x
winStartY = winPos.value.y const rect = windowRef.value?.getBoundingClientRect()
winStartX = rect ? rect.left : winPos.value.x
winStartY = rect ? rect.top : winPos.value.y
aiStore.setWinPos({ x: winStartX, y: winStartY })
const onMove = (e: MouseEvent | TouchEvent) => { const onMove = (e: MouseEvent | TouchEvent) => {
if (!isDraggingWin) return if (!isDraggingWin.value) return
const curX = 'touches' in e ? e.touches?.[0]?.clientX : e.clientX const curX = 'touches' in e ? e.touches?.[0]?.clientX : e.clientX
const curY = 'touches' in e ? e.touches?.[0]?.clientY : e.clientY const curY = 'touches' in e ? e.touches?.[0]?.clientY : e.clientY
if (curX === undefined || curY === undefined) return if (curX === undefined || curY === undefined) return
...@@ -389,6 +503,10 @@ export const useAiAssistant = () => { ...@@ -389,6 +503,10 @@ export const useAiAssistant = () => {
const dx = curX - winPointerStartX const dx = curX - winPointerStartX
const dy = curY - winPointerStartY const dy = curY - winPointerStartY
if (Math.hypot(dx, dy) > 4) {
aiStore.setWinHasMoved(true)
}
const winW = window.innerWidth const winW = window.innerWidth
const winH = window.innerHeight const winH = window.innerHeight
const dialogW = isMinimized.value ? 320 : 390 const dialogW = isMinimized.value ? 320 : 390
...@@ -401,7 +519,7 @@ export const useAiAssistant = () => { ...@@ -401,7 +519,7 @@ export const useAiAssistant = () => {
} }
const onEnd = () => { const onEnd = () => {
isDraggingWin = false isDraggingWin.value = false
window.removeEventListener('mousemove', onMove) window.removeEventListener('mousemove', onMove)
window.removeEventListener('mouseup', onEnd) window.removeEventListener('mouseup', onEnd)
window.removeEventListener('touchmove', onMove) window.removeEventListener('touchmove', onMove)
...@@ -698,6 +816,7 @@ export const useAiAssistant = () => { ...@@ -698,6 +816,7 @@ export const useAiAssistant = () => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize) window.removeEventListener('resize', handleResize)
if (snapTimer) clearTimeout(snapTimer)
if (inputTimer) clearTimeout(inputTimer) if (inputTimer) clearTimeout(inputTimer)
if (streamRafId) cancelAnimationFrame(streamRafId) if (streamRafId) cancelAnimationFrame(streamRafId)
}) })
...@@ -708,6 +827,13 @@ export const useAiAssistant = () => { ...@@ -708,6 +827,13 @@ export const useAiAssistant = () => {
isMinimized, isMinimized,
unreadCount, unreadCount,
activeCategory, activeCategory,
badgeSide,
badgeAnchorY,
badgeOffsetY,
badgeStyle,
winStyle,
isDraggingBadge,
isDraggingWin,
inputQuery, inputQuery,
isTyping, isTyping,
isStreaming, isStreaming,
......
...@@ -5,19 +5,20 @@ ...@@ -5,19 +5,20 @@
<div <div
v-if="!isOpen" v-if="!isOpen"
ref="badgeRef" ref="badgeRef"
class="fixed z-40 flex items-center flex-nowrap whitespace-nowrap group cursor-grab active:cursor-grabbing select-none" class="fixed z-40 flex items-center flex-nowrap whitespace-nowrap group select-none"
:class="[isBadgeSnapping ? 'transition-all duration-300 ease-out' : '']" :class="[
:style="{ isDraggingBadge ? 'cursor-grabbing' : 'cursor-grab',
left: `${badgePos.x}px`, isBadgeSnapping ? 'transition-all duration-300 ease-out' : '',
top: `${badgePos.y}px`, badgeSide === 'left' ? 'flex-row-reverse' : 'flex-row'
touchAction: 'none' ]"
}" :style="badgeStyle"
@mousedown="handleBadgeMouseDown" @mousedown="handleBadgeMouseDown"
@touchstart.passive="handleBadgeTouchStart" @touchstart.passive="handleBadgeTouchStart"
> >
<!-- 悬浮小气泡提示语(强制单行不换行) --> <!-- 悬浮小气泡提示语(强制单行不换行) -->
<div <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="{ :style="{
borderColor: `color-mix(in srgb, ${themeVars.primaryColor} 30%, transparent)`, borderColor: `color-mix(in srgb, ${themeVars.primaryColor} 30%, transparent)`,
boxShadow: `0 4px 16px color-mix(in srgb, ${themeVars.primaryColor} 20%, transparent)` boxShadow: `0 4px 16px color-mix(in srgb, ${themeVars.primaryColor} 20%, transparent)`
...@@ -67,11 +68,7 @@ ...@@ -67,11 +68,7 @@
ref="windowRef" ref="windowRef"
class="fixed z-50 flex flex-col rounded-2xl border border-divider bg-card shadow-2xl overflow-hidden text-color1" 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)]']" :class="[isMinimized ? 'h-[52px] w-[320px]' : 'h-[580px] w-[390px] max-h-[calc(100vh-32px)] max-w-[calc(100vw-32px)]']"
:style="{ :style="winStyle"
left: `${winPos.x}px`,
top: `${winPos.y}px`,
boxShadow: `0 12px 36px rgba(0, 0, 0, 0.18), 0 0 1px ${themeVars.primaryColor}40`
}"
> >
<!-- 头部工具栏(按住可拖动窗口) --> <!-- 头部工具栏(按住可拖动窗口) -->
<div <div
...@@ -617,6 +614,10 @@ const { ...@@ -617,6 +614,10 @@ const {
isMinimized, isMinimized,
unreadCount, unreadCount,
activeCategory, activeCategory,
badgeSide,
badgeStyle,
winStyle,
isDraggingBadge,
inputQuery, inputQuery,
isTyping, isTyping,
isStreaming, 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 type { ChatMessage, FaqCategory } from '@/layouts/components/AiAssistant/constants'
import { DEFAULT_WELCOME_MESSAGE, HOT_QUESTIONS } 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', { ...@@ -11,8 +11,12 @@ export const useAiAssistantStore = defineStore('aiAssistant', {
isMinimized: false, isMinimized: false,
unreadCount: 1, unreadCount: 1,
activeCategory: 'all', activeCategory: 'all',
badgeSide: 'right',
badgeAnchorY: 'bottom',
badgeOffsetY: 120,
badgePos: { x: 0, y: 0 }, badgePos: { x: 0, y: 0 },
winPos: { x: 0, y: 0 }, winPos: { x: 0, y: 0 },
winHasMoved: false,
messageList: [ messageList: [
{ {
id: 'msg_welcome', id: 'msg_welcome',
...@@ -33,12 +37,20 @@ export const useAiAssistantStore = defineStore('aiAssistant', { ...@@ -33,12 +37,20 @@ export const useAiAssistantStore = defineStore('aiAssistant', {
toggleMinimize() { toggleMinimize() {
this.isMinimized = !this.isMinimized 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) { setBadgePos(pos: Position) {
this.badgePos = pos this.badgePos = pos
}, },
setWinPos(pos: Position) { setWinPos(pos: Position) {
this.winPos = pos this.winPos = pos
}, },
setWinHasMoved(moved: boolean) {
this.winHasMoved = moved
},
setActiveCategory(category: FaqCategory) { setActiveCategory(category: FaqCategory) {
this.activeCategory = category this.activeCategory = category
}, },
...@@ -70,7 +82,9 @@ export const useAiAssistantStore = defineStore('aiAssistant', { ...@@ -70,7 +82,9 @@ export const useAiAssistantStore = defineStore('aiAssistant', {
] ]
} }
}, },
persist: true persist: {
pick: ['unreadCount', 'activeCategory', 'badgeSide', 'badgeAnchorY', 'badgeOffsetY', 'messageList']
}
}) })
export * from './types' export * from './types'
...@@ -5,12 +5,20 @@ export interface Position { ...@@ -5,12 +5,20 @@ export interface Position {
y: number y: number
} }
export type DockSide = 'left' | 'right'
export type AnchorY = 'top' | 'bottom'
export interface AiAssistantState { export interface AiAssistantState {
isOpen: boolean isOpen: boolean
isMinimized: boolean isMinimized: boolean
unreadCount: number unreadCount: number
activeCategory: FaqCategory activeCategory: FaqCategory
badgeSide: DockSide
badgeAnchorY: AnchorY
badgeOffsetY: number
badgePos: Position badgePos: Position
winPos: Position winPos: Position
winHasMoved: boolean
messageList: ChatMessage[] 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