Bladeren bron

新增 AI 助手聊天界面

- 创建 AI 聊天页面组件 src/views/ai-chat/index.vue
- 添加 AI 助手到路由配置
- 更新 mock 菜单数据,添加 AI 助手菜单项

功能特性:
- 支持对话式问答交互
- 快捷问题一键发送
- 支持清空和导出聊天记录
- 消息支持 Markdown 格式显示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tanlie 3 weken geleden
bovenliggende
commit
83c42ab843
3 gewijzigde bestanden met toevoegingen van 472 en 0 verwijderingen
  1. 6 0
      src/mock/index.ts
  2. 6 0
      src/router/index.ts
  3. 460 0
      src/views/ai-chat/index.vue

+ 6 - 0
src/mock/index.ts

@@ -101,6 +101,12 @@ export default [
             path: "/statistics",
             icon: "TrendCharts",
           },
+          {
+            id: "5",
+            name: "AI 助手",
+            path: "/ai-chat",
+            icon: "ChatDotRound",
+          },
         ],
       };
     },

+ 6 - 0
src/router/index.ts

@@ -46,6 +46,12 @@ const router = createRouter({
           name: 'Statistics',
           component: () => import('@/views/statistics/index.vue'),
           meta: { title: '数据统计' }
+        },
+        {
+          path: '/ai-chat',
+          name: 'AIChat',
+          component: () => import('@/views/ai-chat/index.vue'),
+          meta: { title: 'AI 助手' }
         }
       ]
     }

+ 460 - 0
src/views/ai-chat/index.vue

@@ -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>