Commit 51518ea7 by pangchong

feat(aiAssistant): 添加全局 AI 智能客服小助手组件

- 在主布局中集成 AiAssistant 组件入口
- 实现悬浮客服入口按钮,可拖动并支持磁力吸边功能
- 开发可拖动的客服对话窗口,支持最小化与新对话功能
- 设计多分类问题导航及上下文相关专属建议栏
- 支持用户输入联想提示与实时消息流式渲染
- 实现消息复制、满意度反馈和快捷操作按钮功能
- 添加机器人思考中的动画提示及输入区快捷指令
- 编写符合主题颜色的样式,支持多种交互动效
- 构建常见问题及回答数据结构,支持关键词匹配与相关问题推荐
parent 4ba6d93e
......@@ -11,11 +11,15 @@
<CommonImportModal ref="globalImportModalRef" />
<CommonUploadModal ref="globalUploadModalRef" />
<CommonDownloadModal ref="globalDownloadModalRef" />
<!-- 全局 AI 智能客服小助手 -->
<AiAssistant />
</div>
</template>
<script setup lang="ts">
import { useAppStore } from '@/store/app/index'
import AiAssistant from './components/AiAssistant/index.vue'
const themeVars = useThemeVars()
const appStore = useAppStore()
......
import type { AiAssistantState, Position } from './types'
import type { ChatMessage, FaqCategory } from '@/layouts/components/AiAssistant/constants'
import { DEFAULT_WELCOME_MESSAGE, HOT_QUESTIONS } from '@/layouts/components/AiAssistant/constants'
// 最大保留问答条数(约 4 轮问答对,超出自动修剪)
const MAX_HISTORY_MESSAGES = 8
export const useAiAssistantStore = defineStore('aiAssistant', {
state: (): AiAssistantState => ({
isOpen: false,
isMinimized: false,
unreadCount: 1,
activeCategory: 'all',
badgePos: { x: 0, y: 0 },
winPos: { x: 0, y: 0 },
messageList: [
{
id: 'msg_welcome',
sender: 'bot',
timestamp: now(),
content: DEFAULT_WELCOME_MESSAGE,
matchedFaqs: HOT_QUESTIONS
}
]
}),
actions: {
setOpen(open: boolean) {
this.isOpen = open
if (open) {
this.unreadCount = 0
}
},
toggleMinimize() {
this.isMinimized = !this.isMinimized
},
setBadgePos(pos: Position) {
this.badgePos = pos
},
setWinPos(pos: Position) {
this.winPos = pos
},
setActiveCategory(category: FaqCategory) {
this.activeCategory = category
},
addMessage(msg: ChatMessage) {
this.messageList.push(msg)
// 自动修剪机制:保留欢迎语,其余对话最多保留最近 8 条(4 轮)
const nonWelcome = this.messageList.filter((m) => m.id !== 'msg_welcome')
if (nonWelcome.length > MAX_HISTORY_MESSAGES) {
const welcomeMsg = this.messageList.find((m) => m.id === 'msg_welcome')
const pruned = nonWelcome.slice(-MAX_HISTORY_MESSAGES)
this.messageList = welcomeMsg ? [welcomeMsg, ...pruned] : pruned
}
},
updateMessageFeedback(msgId: string, feedback: 'helpful' | 'unhelpful') {
const found = this.messageList.find((m) => m.id === msgId)
if (found) {
found.feedback = feedback
}
},
clearMessages() {
this.messageList = [
{
id: 'msg_welcome',
sender: 'bot',
timestamp: now(),
content: DEFAULT_WELCOME_MESSAGE,
matchedFaqs: HOT_QUESTIONS
}
]
}
},
persist: true
})
export * from './types'
import type { ChatMessage, FaqCategory } from '@/layouts/components/AiAssistant/constants'
export interface Position {
x: number
y: number
}
export interface AiAssistantState {
isOpen: boolean
isMinimized: boolean
unreadCount: number
activeCategory: FaqCategory
badgePos: Position
winPos: Position
messageList: ChatMessage[]
}
......@@ -575,6 +575,29 @@ const {
const insertFragmentModalRef = ref<any>(null)
const compareModalRef = ref<any>(null)
const stashModalRef = ref<any>(null)
// 注册 AI 客服快捷动作监听
const eventBus = useEventBus()
onMounted(() => {
eventBus.on('trigger_insert_table', () => handleInsert('TABLE'))
eventBus.on('trigger_insert_graphic', () => handleInsert('GRAPHIC'))
eventBus.on('trigger_insert_signoff', () => handleInsert('SIGNOFF'))
eventBus.on('trigger_insert_selection', () => handleInsert('SELECTION'))
eventBus.on('trigger_insert_template', () => handleInsert('TEMPLATE'))
eventBus.on('trigger_insert_fragment', () => insertFragmentModalRef.value?.open(insertBelow.value))
eventBus.on('trigger_import_xml', () => triggerUpload())
eventBus.on('trigger_export_xml', () => emit('export'))
eventBus.on('trigger_preview', () => emit('preview'))
eventBus.on('trigger_download_html', () => emit('download-html'))
eventBus.on('trigger_stash', () => stashModalRef.value?.open())
eventBus.on('trigger_batch_translate', () => handleTranslate('batch'))
eventBus.on('trigger_extract_translate', () => handleTranslate('extract'))
eventBus.on('trigger_search_translate', () => handleTranslate('search'))
eventBus.on('trigger_compare', () => compareModalRef.value?.open())
eventBus.on('trigger_open_settings', () => {
appStore.settingsOpen = true
})
})
</script>
<style scoped>
......
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