|
@@ -0,0 +1,460 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="ai-chat-container">
|
|
|
|
|
+ <el-card class="chat-card">
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <div class="chat-header">
|
|
|
|
|
+ <el-icon class="chat-icon"><ChatDotRound /></el-icon>
|
|
|
|
|
+ <span class="chat-title">AI 智能助手</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 聊天消息区域 -->
|
|
|
|
|
+ <div class="chat-messages" ref="messagesRef">
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(msg, index) in messages"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ :class="['message', msg.role === 'user' ? 'user-message' : 'ai-message']"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="message-avatar">
|
|
|
|
|
+ <el-avatar
|
|
|
|
|
+ :size="40"
|
|
|
|
|
+ :src="msg.role === 'user' ? userAvatar : aiAvatar"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="message-content">
|
|
|
|
|
+ <div class="message-sender">{{ msg.role === 'user' ? '我' : 'AI 助手' }}</div>
|
|
|
|
|
+ <div class="message-text" v-html="formatMessage(msg.content)"></div>
|
|
|
|
|
+ <div class="message-time">{{ msg.time }}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- AI 正在输入提示 -->
|
|
|
|
|
+ <div v-if="isTyping" class="message ai-message typing-message">
|
|
|
|
|
+ <div class="message-avatar">
|
|
|
|
|
+ <el-avatar :size="40" :src="aiAvatar" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="message-content">
|
|
|
|
|
+ <div class="message-sender">AI 助手</div>
|
|
|
|
|
+ <div class="typing-indicator">
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 输入区域 -->
|
|
|
|
|
+ <div class="chat-input-area">
|
|
|
|
|
+ <div class="input-toolbar">
|
|
|
|
|
+ <el-button type="primary" text @click="clearChat">
|
|
|
|
|
+ <el-icon><Delete /></el-icon>清空对话
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button type="primary" text @click="exportChat">
|
|
|
|
|
+ <el-icon><Download /></el-icon>导出记录
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="input-box">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="inputMessage"
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ :rows="3"
|
|
|
|
|
+ placeholder="请输入您的问题,按 Enter 发送,Shift + Enter 换行..."
|
|
|
|
|
+ resize="none"
|
|
|
|
|
+ @keydown.enter.prevent="handleEnter"
|
|
|
|
|
+ />
|
|
|
|
|
+ <div class="input-actions">
|
|
|
|
|
+ <el-button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="large"
|
|
|
|
|
+ :loading="isSending"
|
|
|
|
|
+ :disabled="!inputMessage.trim()"
|
|
|
|
|
+ @click="sendMessage"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-icon><Promotion /></el-icon>
|
|
|
|
|
+ 发送
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 快捷问题卡片 -->
|
|
|
|
|
+ <el-card class="quick-questions">
|
|
|
|
|
+ <template #header>
|
|
|
|
|
+ <span>快捷问题</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <div class="question-tags">
|
|
|
|
|
+ <el-tag
|
|
|
|
|
+ v-for="question in quickQuestions"
|
|
|
|
|
+ :key="question"
|
|
|
|
|
+ class="question-tag"
|
|
|
|
|
+ effect="plain"
|
|
|
|
|
+ @click="selectQuestion(question)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ question }}
|
|
|
|
|
+ </el-tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { ref, nextTick, onMounted } from 'vue'
|
|
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
|
|
+import { ChatDotRound, Delete, Download, Promotion } from '@element-plus/icons-vue'
|
|
|
|
|
+import { useUserStore } from '@/store/user'
|
|
|
|
|
+
|
|
|
|
|
+interface Message {
|
|
|
|
|
+ role: 'user' | 'assistant'
|
|
|
|
|
+ content: string
|
|
|
|
|
+ time: string
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const userStore = useUserStore()
|
|
|
|
|
+const messagesRef = ref<HTMLDivElement>()
|
|
|
|
|
+const inputMessage = ref('')
|
|
|
|
|
+const isSending = ref(false)
|
|
|
|
|
+const isTyping = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+// 头像
|
|
|
|
|
+const userAvatar = 'https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png'
|
|
|
|
|
+const aiAvatar = 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
|
|
|
|
|
+
|
|
|
|
|
+// 消息列表
|
|
|
|
|
+const messages = ref<Message[]>([
|
|
|
|
|
+ {
|
|
|
|
|
+ role: 'assistant',
|
|
|
|
|
+ content: '您好!我是您的 AI 智能助手,有什么可以帮助您的吗?',
|
|
|
|
|
+ time: new Date().toLocaleString()
|
|
|
|
|
+ }
|
|
|
|
|
+])
|
|
|
|
|
+
|
|
|
|
|
+// 快捷问题
|
|
|
|
|
+const quickQuestions = [
|
|
|
|
|
+ '如何管理系统用户?',
|
|
|
|
|
+ '如何配置菜单权限?',
|
|
|
|
|
+ '系统有哪些功能模块?',
|
|
|
|
|
+ '如何导出数据报表?',
|
|
|
|
|
+ '如何修改个人资料?'
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+// 格式化消息(支持简单的换行)
|
|
|
|
|
+const formatMessage = (content: string) => {
|
|
|
|
|
+ return content.replace(/\n/g, '<br>')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 滚动到底部
|
|
|
|
|
+const scrollToBottom = () => {
|
|
|
|
|
+ nextTick(() => {
|
|
|
|
|
+ if (messagesRef.value) {
|
|
|
|
|
+ messagesRef.value.scrollTop = messagesRef.value.scrollHeight
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 发送消息
|
|
|
|
|
+const sendMessage = async () => {
|
|
|
|
|
+ const content = inputMessage.value.trim()
|
|
|
|
|
+ if (!content || isSending.value) return
|
|
|
|
|
+
|
|
|
|
|
+ // 添加用户消息
|
|
|
|
|
+ messages.value.push({
|
|
|
|
|
+ role: 'user',
|
|
|
|
|
+ content,
|
|
|
|
|
+ time: new Date().toLocaleString()
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ inputMessage.value = ''
|
|
|
|
|
+ isSending.value = true
|
|
|
|
|
+ isTyping.value = true
|
|
|
|
|
+ scrollToBottom()
|
|
|
|
|
+
|
|
|
|
|
+ // 模拟 AI 回复
|
|
|
|
|
+ setTimeout(() => {
|
|
|
|
|
+ isTyping.value = false
|
|
|
|
|
+ const reply = generateReply(content)
|
|
|
|
|
+ messages.value.push({
|
|
|
|
|
+ role: 'assistant',
|
|
|
|
|
+ content: reply,
|
|
|
|
|
+ time: new Date().toLocaleString()
|
|
|
|
|
+ })
|
|
|
|
|
+ isSending.value = false
|
|
|
|
|
+ scrollToBottom()
|
|
|
|
|
+ }, 1500)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 生成回复(模拟)
|
|
|
|
|
+const generateReply = (question: string): string => {
|
|
|
|
|
+ const replies: Record<string, string> = {
|
|
|
|
|
+ '如何管理系统用户?': '系统用户管理在"用户管理"菜单中,您可以进行以下操作:\n1. 查看用户列表\n2. 添加新用户\n3. 编辑用户信息\n4. 禁用/启用用户账号',
|
|
|
|
|
+ '如何配置菜单权限?': '菜单权限配置在"系统管理 > 菜单管理"中:\n1. 创建菜单项\n2. 设置菜单图标和路径\n3. 为角色分配菜单权限\n4. 保存后权限即时生效',
|
|
|
|
|
+ '系统有哪些功能模块?': '本系统包含以下功能模块:\n- 仪表盘:数据概览\n- 用户管理:系统用户管理\n- 系统管理:菜单、角色管理\n- 数据统计:报表分析\n- AI 助手:智能问答',
|
|
|
|
|
+ '如何导出数据报表?': '导出数据报表步骤:\n1. 进入"数据统计"页面\n2. 选择查询条件\n3. 点击"导出"按钮\n4. 选择导出格式(Excel/PDF)\n5. 下载生成的报表文件',
|
|
|
|
|
+ '如何修改个人资料?': '修改个人资料:\n1. 点击右上角用户名\n2. 选择"个人中心"\n3. 编辑个人信息\n4. 点击"保存"按钮',
|
|
|
|
|
+ '你好': '您好!很高兴为您服务,请问有什么可以帮助您的?',
|
|
|
|
|
+ '帮助': '我可以帮您:\n- 解答系统使用问题\n- 提供操作指导\n- 解释功能说明\n请直接输入您的问题!'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 模糊匹配
|
|
|
|
|
+ for (const key in replies) {
|
|
|
|
|
+ if (question.includes(key) || key.includes(question)) {
|
|
|
|
|
+ return replies[key]
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 默认回复
|
|
|
|
|
+ return `感谢您的提问!关于"${question}",我建议您:\n\n1. 查看相关功能模块的说明文档\n2. 联系系统管理员获取帮助\n3. 或者尝试在快捷问题中寻找类似问题\n\n如果您需要更详细的帮助,请联系技术支持。`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 选择快捷问题
|
|
|
|
|
+const selectQuestion = (question: string) => {
|
|
|
|
|
+ inputMessage.value = question
|
|
|
|
|
+ sendMessage()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 处理回车键
|
|
|
|
|
+const handleEnter = (e: KeyboardEvent) => {
|
|
|
|
|
+ if (!e.shiftKey) {
|
|
|
|
|
+ sendMessage()
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 清空对话
|
|
|
|
|
+const clearChat = () => {
|
|
|
|
|
+ messages.value = [{
|
|
|
|
|
+ role: 'assistant',
|
|
|
|
|
+ content: '对话已清空,我是您的 AI 智能助手,有什么可以帮助您的吗?',
|
|
|
|
|
+ time: new Date().toLocaleString()
|
|
|
|
|
+ }]
|
|
|
|
|
+ ElMessage.success('对话已清空')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 导出聊天记录
|
|
|
|
|
+const exportChat = () => {
|
|
|
|
|
+ const chatText = messages.value.map(m => {
|
|
|
|
|
+ return `[${m.time}] ${m.role === 'user' ? '我' : 'AI'}: ${m.content}`
|
|
|
|
|
+ }).join('\n\n')
|
|
|
|
|
+
|
|
|
|
|
+ const blob = new Blob([chatText], { type: 'text/plain;charset=utf-8' })
|
|
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
|
|
+ const link = document.createElement('a')
|
|
|
|
|
+ link.href = url
|
|
|
|
|
+ link.download = `聊天记录_${new Date().toLocaleDateString()}.txt`
|
|
|
|
|
+ document.body.appendChild(link)
|
|
|
|
|
+ link.click()
|
|
|
|
|
+ document.body.removeChild(link)
|
|
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
|
|
+
|
|
|
|
|
+ ElMessage.success('聊天记录已导出')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(() => {
|
|
|
|
|
+ scrollToBottom()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.ai-chat-container {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ height: calc(100vh - 140px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chat-card {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-card__body) {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chat-header {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+
|
|
|
|
|
+ .chat-icon {
|
|
|
|
|
+ font-size: 24px;
|
|
|
|
|
+ color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .chat-title {
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chat-messages {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ background-color: #f5f7fa;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.message {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-bottom: 20px;
|
|
|
|
|
+
|
|
|
|
|
+ .message-avatar {
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .message-content {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ max-width: 70%;
|
|
|
|
|
+
|
|
|
|
|
+ .message-sender {
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ color: #909399;
|
|
|
|
|
+ margin-bottom: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .message-text {
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ line-height: 1.6;
|
|
|
|
|
+ word-wrap: break-word;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .message-time {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ color: #c0c4cc;
|
|
|
|
|
+ margin-top: 6px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.user-message {
|
|
|
|
|
+ flex-direction: row-reverse;
|
|
|
|
|
+
|
|
|
|
|
+ .message-content {
|
|
|
|
|
+ .message-text {
|
|
|
|
|
+ background-color: #409eff;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ border-bottom-right-radius: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .message-sender,
|
|
|
|
|
+ .message-time {
|
|
|
|
|
+ text-align: right;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ai-message {
|
|
|
|
|
+ .message-content {
|
|
|
|
|
+ .message-text {
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+ color: #303133;
|
|
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
|
|
+ border-bottom-left-radius: 4px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.typing-message {
|
|
|
|
|
+ .typing-indicator {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 4px;
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+ border: 1px solid #e4e7ed;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ border-bottom-left-radius: 4px;
|
|
|
|
|
+ width: fit-content;
|
|
|
|
|
+
|
|
|
|
|
+ span {
|
|
|
|
|
+ width: 8px;
|
|
|
|
|
+ height: 8px;
|
|
|
|
|
+ background-color: #409eff;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ animation: typing 1.4s infinite ease-in-out both;
|
|
|
|
|
+
|
|
|
|
|
+ &:nth-child(1) {
|
|
|
|
|
+ animation-delay: -0.32s;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ &:nth-child(2) {
|
|
|
|
|
+ animation-delay: -0.16s;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes typing {
|
|
|
|
|
+ 0%, 80%, 100% {
|
|
|
|
|
+ transform: scale(0);
|
|
|
|
|
+ }
|
|
|
|
|
+ 40% {
|
|
|
|
|
+ transform: scale(1);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.chat-input-area {
|
|
|
|
|
+ padding: 20px;
|
|
|
|
|
+ border-top: 1px solid #e4e7ed;
|
|
|
|
|
+ background-color: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.input-toolbar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.input-box {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+
|
|
|
|
|
+ .el-textarea {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .input-actions {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-end;
|
|
|
|
|
+
|
|
|
|
|
+ .el-button {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ min-height: 74px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.quick-questions {
|
|
|
|
|
+ width: 280px;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-card__body) {
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.question-tags {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.question-tag {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ padding: 10px 15px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ transition: all 0.3s;
|
|
|
|
|
+
|
|
|
|
|
+ &:hover {
|
|
|
|
|
+ background-color: #409eff;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ border-color: #409eff;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|