8 Commits a2f059146e ... fed499d867

Author SHA1 Message Date
  tanlie fed499d867 AI 助手添加聊天记录分页功能 3 weeks ago
  tanlie f292ab363f 修复代理模式下 AI 展示问题 3 weeks ago
  tanlie de80fdea6e 修复 AI 助手 404 问题 3 weeks ago
  tanlie ee24789ef5 AI 助手改为 SSE 流式输出 3 weeks ago
  tanlie ff131a99bf 修复 AI 助手缺少 http 导入问题 3 weeks ago
  tanlie aff4206793 AI 助手改为接口请求方式 3 weeks ago
  tanlie f8c1d395a7 修复 AI 助手页面 SCSS 语法错误 3 weeks ago
  tanlie 83c42ab843 新增 AI 助手聊天界面 3 weeks ago
6 changed files with 962 additions and 0 deletions
  1. 16 0
      package-lock.json
  2. 1 0
      package.json
  3. 225 0
      src/mock/index.ts
  4. 6 0
      src/router/index.ts
  5. 21 0
      src/utils/api.ts
  6. 693 0
      src/views/ai-chat/index.vue

+ 16 - 0
package-lock.json

@@ -16,6 +16,7 @@
         "vue-router": "^4.2.0"
         "vue-router": "^4.2.0"
       },
       },
       "devDependencies": {
       "devDependencies": {
+        "@types/node": "^25.5.0",
         "@vitejs/plugin-vue": "^5.0.0",
         "@vitejs/plugin-vue": "^5.0.0",
         "typescript": "^5.3.0",
         "typescript": "^5.3.0",
         "vite": "^5.0.0",
         "vite": "^5.0.0",
@@ -865,6 +866,15 @@
         "@types/lodash": "*"
         "@types/lodash": "*"
       }
       }
     },
     },
+    "node_modules/@types/node": {
+      "version": "25.5.0",
+      "resolved": "https://registry.npmmirror.com/@types/node/-/node-25.5.0.tgz",
+      "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
+      "dev": true,
+      "dependencies": {
+        "undici-types": "~7.18.0"
+      }
+    },
     "node_modules/@types/web-bluetooth": {
     "node_modules/@types/web-bluetooth": {
       "version": "0.0.20",
       "version": "0.0.20",
       "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
       "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz",
@@ -2138,6 +2148,12 @@
         "node": ">=14.17"
         "node": ">=14.17"
       }
       }
     },
     },
+    "node_modules/undici-types": {
+      "version": "7.18.2",
+      "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.18.2.tgz",
+      "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+      "dev": true
+    },
     "node_modules/unpipe": {
     "node_modules/unpipe": {
       "version": "1.0.0",
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",
       "resolved": "https://registry.npmmirror.com/unpipe/-/unpipe-1.0.0.tgz",

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "vue-router": "^4.2.0"
     "vue-router": "^4.2.0"
   },
   },
   "devDependencies": {
   "devDependencies": {
+    "@types/node": "^25.5.0",
     "@vitejs/plugin-vue": "^5.0.0",
     "@vitejs/plugin-vue": "^5.0.0",
     "typescript": "^5.3.0",
     "typescript": "^5.3.0",
     "vite": "^5.0.0",
     "vite": "^5.0.0",

+ 225 - 0
src/mock/index.ts

@@ -1,5 +1,100 @@
 import type { MockMethod } from "vite-plugin-mock";
 import type { MockMethod } from "vite-plugin-mock";
 
 
+// 模拟聊天记录存储(内存中)
+interface ChatMessage {
+  id: string;
+  role: 'user' | 'assistant';
+  content: string;
+  time: string;
+}
+
+// 生成模拟历史聊天记录
+const generateMockChatHistory = (): ChatMessage[] => {
+  const history: ChatMessage[] = [
+    {
+      id: '1',
+      role: 'assistant',
+      content: '您好!我是您的 AI 智能助手,有什么可以帮助您的吗?',
+      time: new Date(Date.now() - 86400000 * 2).toLocaleString()
+    },
+    {
+      id: '2',
+      role: 'user',
+      content: '如何管理系统用户?',
+      time: new Date(Date.now() - 86400000 * 2 + 60000).toLocaleString()
+    },
+    {
+      id: '3',
+      role: 'assistant',
+      content: '系统用户管理在「用户管理」菜单中,您可以进行以下操作:\n1. 查看用户列表\n2. 添加新用户\n3. 编辑用户信息\n4. 禁用/启用用户账号',
+      time: new Date(Date.now() - 86400000 * 2 + 120000).toLocaleString()
+    },
+    {
+      id: '4',
+      role: 'user',
+      content: '系统有哪些功能模块?',
+      time: new Date(Date.now() - 86400000).toLocaleString()
+    },
+    {
+      id: '5',
+      role: 'assistant',
+      content: '本系统包含以下功能模块:\n- 仪表盘:数据概览\n- 用户管理:系统用户管理\n- 系统管理:菜单、角色管理\n- 数据统计:报表分析\n- AI 助手:智能问答',
+      time: new Date(Date.now() - 86400000 + 60000).toLocaleString()
+    },
+    {
+      id: '6',
+      role: 'user',
+      content: '如何导出数据报表?',
+      time: new Date(Date.now() - 43200000).toLocaleString()
+    },
+    {
+      id: '7',
+      role: 'assistant',
+      content: '导出数据报表步骤:\n1. 进入「数据统计」页面\n2. 选择查询条件\n3. 点击「导出」按钮\n4. 选择导出格式(Excel/PDF)\n5. 下载生成的报表文件',
+      time: new Date(Date.now() - 43200000 + 60000).toLocaleString()
+    },
+    {
+      id: '8',
+      role: 'user',
+      content: '如何配置菜单权限?',
+      time: new Date(Date.now() - 3600000).toLocaleString()
+    },
+    {
+      id: '9',
+      role: 'assistant',
+      content: '菜单权限配置在「系统管理 > 菜单管理」中:\n1. 创建菜单项\n2. 设置菜单图标和路径\n3. 为角色分配菜单权限\n4. 保存后权限即时生效',
+      time: new Date(Date.now() - 3600000 + 60000).toLocaleString()
+    },
+    {
+      id: '10',
+      role: 'user',
+      content: '如何修改个人资料?',
+      time: new Date(Date.now() - 1800000).toLocaleString()
+    },
+    {
+      id: '11',
+      role: 'assistant',
+      content: '修改个人资料:\n1. 点击右上角用户名\n2. 选择「个人中心」\n3. 编辑个人信息\n4. 点击「保存」按钮',
+      time: new Date(Date.now() - 1800000 + 60000).toLocaleString()
+    },
+    {
+      id: '12',
+      role: 'user',
+      content: '帮助',
+      time: new Date(Date.now() - 900000).toLocaleString()
+    },
+    {
+      id: '13',
+      role: 'assistant',
+      content: '我可以帮您:\n- 解答系统使用问题\n- 提供操作指导\n- 解释功能说明\n请直接输入您的问题!',
+      time: new Date(Date.now() - 900000 + 60000).toLocaleString()
+    }
+  ];
+  return history;
+};
+
+const chatHistory: ChatMessage[] = generateMockChatHistory();
+
 export default [
 export default [
   // 登录接口
   // 登录接口
   {
   {
@@ -101,6 +196,12 @@ export default [
             path: "/statistics",
             path: "/statistics",
             icon: "TrendCharts",
             icon: "TrendCharts",
           },
           },
+          {
+            id: "5",
+            name: "AI 助手",
+            path: "/ai-chat",
+            icon: "ChatDotRound",
+          },
         ],
         ],
       };
       };
     },
     },
@@ -146,4 +247,128 @@ export default [
       };
       };
     },
     },
   },
   },
+
+  // 获取聊天记录列表(分页)
+  {
+    url: "/api/upms/ai/chat/history",
+    method: "get",
+    response: ({ query }) => {
+      const page = parseInt(query.page as string) || 1;
+      const size = parseInt(query.size as string) || 10;
+
+      // 计算分页
+      const total = chatHistory.length;
+      const start = (page - 1) * size;
+      const end = start + size;
+      const list = chatHistory.slice(start, end);
+
+      return {
+        code: 200,
+        message: "success",
+        data: {
+          list,
+          total,
+          page,
+          size,
+          hasMore: end < total
+        }
+      };
+    },
+  },
+
+  // AI 对话接口 (POST - 返回 JSON,适用于代理模式)
+  {
+    url: "/api/upms/ai/chat",
+    method: "post",
+    response: ({ body }) => {
+      const { question } = body;
+
+      // AI 回复知识库
+      const knowledgeBase: 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请直接输入您的问题!"
+      };
+
+      // 查找回复
+      let reply = '';
+      for (const key in knowledgeBase) {
+        if (question.includes(key) || key.includes(question)) {
+          reply = knowledgeBase[key];
+          break;
+        }
+      }
+
+      // 默认回复
+      if (!reply) {
+        reply = `感谢您的提问!关于"${question}",我建议您:\n\n1. 查看相关功能模块的说明文档\n2. 联系系统管理员获取帮助\n3. 或者尝试在快捷问题中寻找类似问题\n\n如果您需要更详细的帮助,请联系技术支持。`;
+      }
+
+      return {
+        code: 200,
+        message: "success",
+        data: { reply }
+      };
+    },
+  },
+
+  // AI 对话接口 (GET - SSE 流式输出,适用于 Mock 模式)
+  {
+    url: "/api/upms/ai/chat",
+    method: "get",
+    rawResponse: async (req, res) => {
+      // 从 query 参数获取问题
+      const url = new URL(req.url || '', `http://${req.headers.host}`);
+      const question = url.searchParams.get('question') || '';
+
+      // AI 回复知识库
+      const knowledgeBase: 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请直接输入您的问题!"
+      };
+
+      // 查找回复
+      let reply = '';
+      for (const key in knowledgeBase) {
+        if (question.includes(key) || key.includes(question)) {
+          reply = knowledgeBase[key];
+          break;
+        }
+      }
+
+      // 默认回复
+      if (!reply) {
+        reply = `感谢您的提问!关于"${question}",我建议您:\n\n1. 查看相关功能模块的说明文档\n2. 联系系统管理员获取帮助\n3. 或者尝试在快捷问题中寻找类似问题\n\n如果您需要更详细的帮助,请联系技术支持。`;
+      }
+
+      // 设置 SSE 响应头
+      res.setHeader('Content-Type', 'text/event-stream');
+      res.setHeader('Cache-Control', 'no-cache');
+      res.setHeader('Connection', 'keep-alive');
+
+      // 流式发送回复,每个字延迟 50ms
+      const chars = reply.split('');
+      for (let i = 0; i < chars.length; i++) {
+        const data = {
+          content: chars[i],
+          index: i,
+          total: chars.length,
+          done: i === chars.length - 1
+        };
+        res.write(`data: ${JSON.stringify(data)}\n\n`);
+        await new Promise(resolve => setTimeout(resolve, 50));
+      }
+
+      res.end();
+    },
+  },
 ] as MockMethod[];
 ] as MockMethod[];

+ 6 - 0
src/router/index.ts

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

+ 21 - 0
src/utils/api.ts

@@ -65,6 +65,27 @@ export const fileApi = {
   }
   }
 }
 }
 
 
+// AI 对话 API
+export const aiApi = {
+  // 发送对话消息
+  chat(question: string) {
+    return http.post<{ reply: string }>('/upms/ai/chat', { question })
+  },
+  // 获取聊天记录(分页)
+  getHistory(params: { page?: number; size?: number }) {
+    return http.get<{
+      list: Array<{
+        id: string
+        role: 'user' | 'assistant'
+        content: string
+        time: string
+      }>
+      total: number
+      hasMore: boolean
+    }>('/upms/ai/chat/history', params)
+  }
+}
+
 // 通用 API 示例
 // 通用 API 示例
 export const commonApi = {
 export const commonApi = {
   // 获取字典数据
   // 获取字典数据

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

@@ -0,0 +1,693 @@
+<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" @scroll="handleScroll">
+        <!-- 加载更多提示 -->
+        <div v-if="isLoadingHistory" class="loading-more">
+          <el-icon class="loading-icon"><Loading /></el-icon>
+          <span>加载历史记录中...</span>
+        </div>
+        <div v-else-if="!hasMoreHistory" class="no-more-history">
+          <span>—— 没有更多记录了 ——</span>
+        </div>
+
+        <div
+          v-for="(msg, index) in displayMessages"
+          :key="msg.id || 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, computed } from 'vue'
+import { ElMessage } from 'element-plus'
+import { ChatDotRound, Delete, Download, Promotion, Loading } from '@element-plus/icons-vue'
+import { useUserStore } from '@/store/user'
+import { http } from '@/utils/request'
+
+interface Message {
+  id?: string
+  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 historyPage = ref(1)
+const historySize = 10
+const isLoadingHistory = ref(false)
+const hasMoreHistory = ref(true)
+const historyMessages = ref<Message[]>([])
+
+// 当前会话消息(最多保留10条)
+const currentMessages = ref<Message[]>([
+  {
+    role: 'assistant',
+    content: '您好!我是您的 AI 智能助手,有什么可以帮助您的吗?',
+    time: new Date().toLocaleString()
+  }
+])
+
+// 合并显示的消息(历史 + 当前)
+const displayMessages = computed(() => {
+  return [...historyMessages.value, ...currentMessages.value]
+})
+
+// 快捷问题
+const quickQuestions = [
+  '如何管理系统用户?',
+  '如何配置菜单权限?',
+  '系统有哪些功能模块?',
+  '如何导出数据报表?',
+  '如何修改个人资料?'
+]
+
+// 格式化消息(支持简单的换行)
+const formatMessage = (content: string) => {
+  return content.replace(/\n/g, '<br>')
+}
+
+// 滚动到底部
+const scrollToBottom = () => {
+  nextTick(() => {
+    if (messagesRef.value) {
+      messagesRef.value.scrollTop = messagesRef.value.scrollHeight
+    }
+  })
+}
+
+// 记录滚动位置
+let scrollHeightBeforeLoad = 0
+
+// 加载历史聊天记录
+const loadHistory = async () => {
+  if (isLoadingHistory.value || !hasMoreHistory.value) return
+
+  isLoadingHistory.value = true
+
+  // 记录当前滚动高度
+  if (messagesRef.value) {
+    scrollHeightBeforeLoad = messagesRef.value.scrollHeight
+  }
+
+  try {
+    const res = await http.get<{
+      list: Message[]
+      total: number
+      hasMore: boolean
+    }>('/upms/ai/chat/history', {
+      page: historyPage.value,
+      size: historySize
+    })
+
+    if (res.list && res.list.length > 0) {
+      // 将新加载的历史记录添加到前面
+      historyMessages.value = [...res.list, ...historyMessages.value]
+      historyPage.value++
+      hasMoreHistory.value = res.hasMore
+
+      // 保持滚动位置
+      nextTick(() => {
+        if (messagesRef.value) {
+          const newScrollHeight = messagesRef.value.scrollHeight
+          messagesRef.value.scrollTop = newScrollHeight - scrollHeightBeforeLoad
+        }
+      })
+    } else {
+      hasMoreHistory.value = false
+    }
+  } catch (error) {
+    console.error('加载历史记录失败:', error)
+  } finally {
+    isLoadingHistory.value = false
+  }
+}
+
+// 处理滚动事件
+const handleScroll = () => {
+  if (!messagesRef.value) return
+
+  const { scrollTop } = messagesRef.value
+
+  // 当滚动到顶部时,加载更多历史记录
+  if (scrollTop <= 10 && !isLoadingHistory.value && hasMoreHistory.value) {
+    loadHistory()
+  }
+}
+
+// 发送消息
+const sendMessage = async () => {
+  const content = inputMessage.value.trim()
+  if (!content || isSending.value) return
+
+  // 添加用户消息到当前会话
+  currentMessages.value.push({
+    role: 'user',
+    content,
+    time: new Date().toLocaleString()
+  })
+
+  // 只保留最近10条当前会话记录
+  if (currentMessages.value.length > 10) {
+    currentMessages.value = currentMessages.value.slice(-10)
+  }
+
+  inputMessage.value = ''
+  isSending.value = true
+  isTyping.value = true
+  scrollToBottom()
+
+  // 调用 AI 接口获取流式回复
+  try {
+    await sendChatMessage(content)
+  } catch (error) {
+    console.error('AI 对话失败:', error)
+  } finally {
+    isTyping.value = false
+    isSending.value = false
+    scrollToBottom()
+  }
+}
+
+// 打字机效果显示文本
+const typeWriterEffect = async (text: string, delay: number = 30) => {
+  // 添加一个空的 AI 消息
+  currentMessages.value.push({
+    role: 'assistant',
+    content: '',
+    time: new Date().toLocaleString()
+  })
+
+  const msgIndex = currentMessages.value.length - 1
+
+  for (let i = 0; i < text.length; i++) {
+    if (currentMessages.value[msgIndex]) {
+      currentMessages.value[msgIndex].content += text[i]
+      scrollToBottom()
+    }
+    await new Promise(resolve => setTimeout(resolve, delay))
+  }
+
+  // 只保留最近10条当前会话记录
+  if (currentMessages.value.length > 10) {
+    currentMessages.value = currentMessages.value.slice(-10)
+  }
+}
+
+// AI 对话接口 (同时支持 SSE 和普通 JSON)
+const sendChatMessage = async (question: string): Promise<void> => {
+  try {
+    // 先尝试普通 POST 请求(适用于代理模式,后端返回 JSON)
+    const response = await fetch(`/api/upms/ai/chat`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+      },
+      body: JSON.stringify({ question })
+    })
+
+    // 检查响应类型
+    const contentType = response.headers.get('content-type') || ''
+
+    if (contentType.includes('text/event-stream')) {
+      // SSE 流式响应(Mock 模式)
+      await handleSSEResponse(response)
+    } else {
+      // JSON 响应(代理模式)
+      const data = await response.json()
+      const reply = data.data?.reply || data.reply || '暂无回复'
+      // 使用打字机效果显示
+      await typeWriterEffect(reply, 30)
+    }
+  } catch (error) {
+    console.error('AI 请求失败:', error)
+    // 尝试 SSE 方式(兼容旧版 Mock)
+    try {
+      await handleSSEFallback(question)
+    } catch (sseError) {
+      console.error('SSE 也失败了:', sseError)
+      // 添加错误消息
+      currentMessages.value.push({
+        role: 'assistant',
+        content: '抱歉,AI 服务暂时不可用,请稍后再试。',
+        time: new Date().toLocaleString()
+      })
+    }
+  }
+}
+
+// 处理 SSE 流响应
+const handleSSEResponse = async (response: Response): Promise<void> => {
+  const reader = response.body?.getReader()
+  if (!reader) throw new Error('无法读取响应流')
+
+  // 添加一个空的 AI 消息
+  currentMessages.value.push({
+    role: 'assistant',
+    content: '',
+    time: new Date().toLocaleString()
+  })
+
+  const msgIndex = currentMessages.value.length - 1
+  const decoder = new TextDecoder()
+  let buffer = ''
+
+  while (true) {
+    const { done, value } = await reader.read()
+    if (done) break
+
+    buffer += decoder.decode(value, { stream: true })
+    const lines = buffer.split('\n')
+    buffer = lines.pop() || ''
+
+    for (const line of lines) {
+      if (line.startsWith('data: ')) {
+        try {
+          const data = JSON.parse(line.slice(6))
+          if (currentMessages.value[msgIndex]) {
+            currentMessages.value[msgIndex].content += data.content
+            scrollToBottom()
+          }
+        } catch (e) {
+          // 忽略解析错误
+        }
+      }
+    }
+  }
+
+  // 只保留最近10条当前会话记录
+  if (currentMessages.value.length > 10) {
+    currentMessages.value = currentMessages.value.slice(-10)
+  }
+}
+
+// SSE 降级方案(使用 EventSource)
+const handleSSEFallback = (question: string): Promise<void> => {
+  return new Promise((resolve, reject) => {
+    // 添加一个空的 AI 消息
+    currentMessages.value.push({
+      role: 'assistant',
+      content: '',
+      time: new Date().toLocaleString()
+    })
+
+    const msgIndex = currentMessages.value.length - 1
+    const eventSource = new EventSource(`/api/upms/ai/chat?question=${encodeURIComponent(question)}`)
+
+    eventSource.onmessage = (event) => {
+      try {
+        const data = JSON.parse(event.data)
+        if (currentMessages.value[msgIndex]) {
+          currentMessages.value[msgIndex].content += data.content
+          scrollToBottom()
+        }
+        if (data.done) {
+          eventSource.close()
+          // 只保留最近10条当前会话记录
+          if (currentMessages.value.length > 10) {
+            currentMessages.value = currentMessages.value.slice(-10)
+          }
+          resolve()
+        }
+      } catch (error) {
+        console.error('解析 SSE 数据失败:', error)
+      }
+    }
+
+    eventSource.onerror = (error) => {
+      eventSource.close()
+      if (currentMessages.value[msgIndex]?.content) {
+        // 只保留最近10条当前会话记录
+        if (currentMessages.value.length > 10) {
+          currentMessages.value = currentMessages.value.slice(-10)
+        }
+        resolve()
+      } else {
+        reject(error)
+      }
+    }
+  })
+}
+
+// 选择快捷问题
+const selectQuestion = (question: string) => {
+  inputMessage.value = question
+  sendMessage()
+}
+
+// 处理回车键
+const handleEnter = (e: KeyboardEvent) => {
+  if (!e.shiftKey) {
+    sendMessage()
+  }
+}
+
+// 清空对话
+const clearChat = () => {
+  currentMessages.value = [{
+    role: 'assistant',
+    content: '对话已清空,我是您的 AI 智能助手,有什么可以帮助您的吗?',
+    time: new Date().toLocaleString()
+  }]
+  ElMessage.success('对话已清空')
+}
+
+// 导出聊天记录
+const exportChat = () => {
+  const allMessages = [...historyMessages.value, ...currentMessages.value]
+  const chatText = allMessages.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(() => {
+  // 初始加载历史记录
+  loadHistory()
+})
+</script>
+
+<style scoped>
+.ai-chat-container {
+  display: flex;
+  gap: 20px;
+  height: calc(100vh - 140px);
+}
+
+.chat-card {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+}
+
+.chat-card :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;
+}
+
+.loading-more, .no-more-history {
+  text-align: center;
+  padding: 15px 0;
+  color: #909399;
+  font-size: 13px;
+}
+
+.loading-icon {
+  animation: rotating 1s linear infinite;
+  margin-right: 8px;
+}
+
+@keyframes rotating {
+  from { transform: rotate(0deg); }
+  to { transform: rotate(360deg); }
+}
+
+.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;
+}
+
+.user-message .message-text {
+  background-color: #409eff;
+  color: #fff;
+  border-bottom-right-radius: 4px;
+}
+
+.user-message .message-sender,
+.user-message .message-time {
+  text-align: right;
+}
+
+.ai-message .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;
+}
+
+.typing-indicator span {
+  width: 8px;
+  height: 8px;
+  background-color: #409eff;
+  border-radius: 50%;
+  animation: typing 1.4s infinite ease-in-out both;
+}
+
+.typing-indicator span:nth-child(1) {
+  animation-delay: -0.32s;
+}
+
+.typing-indicator span: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;
+}
+
+.input-box .el-textarea {
+  flex: 1;
+}
+
+.input-actions {
+  display: flex;
+  align-items: flex-end;
+}
+
+.input-actions .el-button {
+  height: 100%;
+  min-height: 74px;
+}
+
+.quick-questions {
+  width: 280px;
+  flex-shrink: 0;
+}
+
+.quick-questions :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;
+}
+
+.question-tag:hover {
+  background-color: #409eff;
+  color: #fff;
+  border-color: #409eff;
+}
+</style>