gcz 3 weeks ago
parent
commit
3bc3fb7748

+ 2 - 1
index.html

@@ -2,7 +2,8 @@
 <html lang="zh-CN">
   <head>
     <meta charset="UTF-8" />
-    <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
+    <!-- <link rel="icon" type="image/svg+xml" href="/favicon.ico" /> -->
+    <link rel="icon" type="image/png" href="/logo.png" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>AI助手</title>
   </head>

BIN
logo.png


BIN
src/assets/knowledgeBg.png


BIN
src/assets/newchat.png


+ 69 - 10
src/layout/index.vue

@@ -17,15 +17,20 @@
         class="sidebar-menu"
         router
       >
+        <el-menu-item index="/new-chat" @click="handleNewChat">
+          <img src="../assets/newchat.png" alt="newchat"/>
+          <template #title>新建对话</template>
+        </el-menu-item>
+        
         <el-menu-item index="/ai-chat">
           <img src="../assets/ChatLineRound.png" alt="chat"/>
           <template #title>AI对话</template>
         </el-menu-item>
         
-        <el-menu-item index="/solution">
+        <!-- <el-menu-item index="/solution">
           <img src="../assets/Document.png" alt="solution"/>
           <template #title>方案生成</template>
-        </el-menu-item>
+        </el-menu-item> -->
         
         <el-menu-item index="/doc-search">
           <img src="../assets/Search.png" alt="search"/>
@@ -66,16 +71,16 @@
       </div>
       
       <!-- 用户信息 -->
-      <div class="user-container">
+      <div class="user-container" @click="router.push('/user-info')">
         <el-avatar :size="40" :src="userStore.userInfo.headUrl"/>
         <div v-show="!userStore.isCollapse" class="user-info">
-          <div class="username">{{ userStore.userInfo.username||'' }}</div>
+          <div class="username">{{ userStore.userInfo.realName||userStore.userInfo.username }}</div>
           <div class="phone">{{ hidePhoneNumber(userStore.userInfo.mobile) }}</div>
         </div>
-        <div v-show="!userStore.isCollapse" class="rank">
+        <!-- <div v-show="!userStore.isCollapse" class="rank">
           <span class="rank-number">{{ userStore.userInfo.rank||'100' }}</span>
           <span class="rank-text">使用排名</span>
-        </div>
+        </div> -->
       </div>
     </div>
     
@@ -91,6 +96,8 @@ import { ref, onMounted } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { useUserStore } from '../stores/user'
 import request from '../utils/request' // 确保引入request
+import { ElMessage } from 'element-plus'
+import { get, post,put } from '../utils/request'
 
 const route = useRoute()
 const router = useRouter()
@@ -113,9 +120,9 @@ const getHistoryList = async () => {
     })
     // console.log('res', res);
     
-    if (res.rows) {
-      historyList.value = res.rows || []
-      hasMore.value = res.total > 5
+    if (res.data.rows) {
+      historyList.value = res.data.rows || []
+      hasMore.value = res.data.total > 5
     }
   } catch (error) {
     console.error('获取历史记录失败:', error)
@@ -127,9 +134,55 @@ onMounted(() => {
   getHistoryList()
 })
 
-const handleHistoryClick = (item) => {
+const handleHistoryClick = async (item) => {
   // 处理历史记录点击
   console.log('点击历史记录:', item)
+  console.log('item.relatedType',item.relatedType);
+  console.log('item.relatedType',item.relatedType=='ai.answer');
+  if (!item.relatedType) return;
+
+    switch (item.relatedType) {
+      case 'ai.answer':
+        // router.push(`/ai-chat?historyId=${item.id}`);
+        window.location.href = `/ai-chat?historyId=${item.id}`
+        break;
+      case 'scheme':
+        router.push(`/solution?historyId=${item.id}`);
+        break;
+      case 'file.download':
+        try {
+          window.open(item.fileUrl, '_blank')
+          // 添加文件下载记录
+          await post('/admin/userHistoryLog/saveFileDownloadLog', {
+            relatedId: item.relatedId
+          })
+        } catch (error) {
+          console.log('error', error);
+          ElMessage.error('下载失败,请重试文件地址:'+item.fileUrl)
+        }
+        // try {
+        //   // 调用下载接口
+        //   const response = await get(`admin/file/download/${item.id}`, {}, {
+        //     responseType: 'blob'
+        //   });
+          
+        //   // 创建下载链接
+        //   const blob = new Blob([response], { type: response.type });
+        //   const url = window.URL.createObjectURL(blob);
+        //   const link = document.createElement('a');
+        //   link.href = url;
+        //   link.download = item.fileName || '下载文件';
+        //   document.body.appendChild(link);
+        //   link.click();
+        //   document.body.removeChild(link);
+        //   window.URL.revokeObjectURL(url);
+        // } catch (error) {
+        //   ElMessage.error('文件下载失败');
+        // }
+        break;
+      default:
+        break;
+    }
 }
 
 const handleViewMore = () => {
@@ -141,6 +194,11 @@ const hidePhoneNumber = (phone) => {
   if (!phone) return ''
   return phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1****$3')
 }
+
+const handleNewChat = () => {
+  // router.push('/ai-chat')
+  window.location.href = '/ai-chat'
+}
 </script>
 
 <style lang="scss" scoped>
@@ -307,6 +365,7 @@ const hidePhoneNumber = (phone) => {
   align-items: center;
   background: #F8F8FA;
   border-radius: 16px;
+  cursor: pointer;
   
   .user-info {
     margin-left: 10px;

+ 18 - 6
src/router/index.js

@@ -43,12 +43,24 @@ const routes = [
         component: () => import('../views/History.vue'),
         meta: { title: '历史记录', icon: 'history' }
       },
-      // {
-      //   path: 'user-info',
-      //   name: 'UserInfo',
-      //   component: () => import('../views/UserInfo.vue'),
-      //   meta: { title: '用户信息', icon: 'user' }
-      // },
+      {
+        path: 'user-info',
+        name: 'UserInfo',
+        component: () => import('../views/UserInfo.vue'),
+        meta: { title: '用户信息', icon: 'user' }
+      },
+      {
+        path: 'knowledge-list',
+        name: 'KnowledgeList',
+        component: () => import('../views/knowledgeList.vue'),
+        meta: { title: '知识库列表', icon: 'knowledge' }
+      },
+      {
+        path: 'knowledge-file',
+        name: 'KnowledgeFile',
+        component: () => import('../views/knowledgeFile.vue'),
+        meta: { title: '知识库文件', icon: 'knowledge' }
+      },
 
     ]
   },

+ 1 - 1
src/utils/request.js

@@ -32,7 +32,7 @@ service.interceptors.response.use(
     
     // 这里可以根据后端的数据结构进行调整
     if (res.code === 200||res.code === 0) {
-      return res.data
+      return res
     } else {
       const userStore = useUserStore()
       if (res.code) {

+ 173 - 28
src/views/AIChat.vue

@@ -20,30 +20,30 @@
               <button class="action-btn" @click="copyContent(message.content)">
                 <img src="@/assets/icon-copy.png" alt="复制">
               </button>
-              <button class="action-btn" @click="toggleFavorite(message.id)">
+              <!-- <button class="action-btn" @click="toggleFavorite(message.id)">
                 <img src="@/assets/icon-star.png" alt="收藏">
-              </button>
+              </button> -->
               <button class="action-btn reanswer" @click="reAnswer(message.id)">
                 <img src="@/assets/icon-reanswer.png" alt="重新回答">
                 <span>重新回答</span>
               </button>
             </div>
-            <div class="right-actions">
+            <!-- <div class="right-actions">
               <button class="action-btn" @click="handleLike(message.id, true)">
                 <img src="@/assets/icon-like.png" alt="点赞">
               </button>
               <button class="action-btn" @click="handleLike(message.id, false)">
                 <img src="@/assets/icon-dislike.png" alt="反对">
               </button>
-            </div>
+            </div> -->
           </div>
           <!-- <div class="time">{{ formatTime(message.time) }}</div> -->
         </div>
       </div>
       <div v-if="loading" class="message assistant">
-        <!-- <div class="avatar">
+        <div class="avatar">
           <img :src="assistantAvatar" alt="assistant">
-        </div> -->
+        </div>
         <div class="content">
           <div class="typing">正在输入...</div>
         </div>
@@ -61,7 +61,7 @@
         ></textarea>
         <div class="input-actions">
           <div class="select-wrap">
-            <el-select v-model="selectedModel" class="model-select">
+            <el-select v-model="selectedModel" class="model-select" :disabled="isChating">
               <el-option v-for="model in models" :key="model.id" :value="model.id" :label="model.name">
                 {{ model.name }}
               </el-option>
@@ -80,8 +80,8 @@
           <button 
             v-for="(item, index) in knowledgeList" 
             :key="index"
-            :class="['knowledge-tag', { active: selectedKnowledge.includes(item.id) }]"
-            @click="toggleKnowledge(item.id)"
+            :class="['knowledge-tag', { active: selectedKnowledge.includes(item.datasetId) }]"
+            @click="toggleKnowledge(item.datasetId)"
           >
             {{ item.name }}
           </button>
@@ -94,20 +94,26 @@
 
 <script setup>
 import { ref, onMounted, nextTick } from 'vue'
+import { useRoute } from 'vue-router'
 import userAvatar from '@/assets/user-avatar.png'
 import assistantAvatar from '@/assets/assistant-avatar.png'
-import { post } from '@/utils/request'
+import { post,get } from '@/utils/request'
 import { ElMessage } from 'element-plus'
 import MarkdownIt from 'markdown-it'
 import hljs from 'highlight.js'
 import 'highlight.js/styles/github.css'
+import { useUserStore } from '../stores/user'
 
+const userStore = useUserStore()
+const route = useRoute()
 const messagesRef = ref(null)
 const inputMessage = ref('')
 const loading = ref(false)
 const isChating = ref(false)
 const selectedKnowledge = ref([])
 const selectedModel = ref('DeepSeek')
+const sessionId = ref('')
+const chatId = ref('')
 
 // 对话模型列表
 const models = ref([
@@ -117,12 +123,26 @@ const models = ref([
 ])
 
 // 知识库列表
-const knowledgeList = ref([
-  { id: 1, name: '知识库1' },
-  { id: 2, name: '知识库2' },
-  { id: 3, name: '知识库3' },
-  { id: 4, name: '知识库4' }
-])
+const knowledgeList = ref([])
+
+// 获取知识库列表
+const getKnowledgeList = async () => {
+  try {
+    const userId = userStore.userInfo.id
+    const response = await get('/admin/user/chat/knowInfoList?userId='+userId)
+    // console.log('response', response);
+    if (response.data) {
+      knowledgeList.value = response.data
+      // 默认选中第一个知识库
+      selectedKnowledge.value = [knowledgeList.value[0].datasetId]
+    } else {
+      ElMessage.error('获取知识库列表失败')
+    }
+  } catch (error) {
+    console.error('获取知识库列表失败:', error)
+    ElMessage.error('获取知识库列表失败')
+  }
+}
 
 const messages = ref([])
 
@@ -163,6 +183,10 @@ const scrollToBottom = async () => {
 }
 
 const handleSend = async () => {
+  if (selectedKnowledge.value.length === 0) {
+      ElMessage.warning('请先选择知识库')
+      return
+    }
   const message = inputMessage.value.trim()
   if (!message || loading.value) return
 
@@ -170,6 +194,28 @@ const handleSend = async () => {
     isChating.value = true
   }
 
+  // 如果没有session_id和chat_id,先获取它们
+  if (!sessionId.value || !chatId.value) {
+    try {
+      const response = await post('/admin/user/chat/create', {
+        userId: userStore.userInfo.id,
+        datasetIds: selectedKnowledge.value.join(',')
+      })
+
+      if (response.data) {
+        sessionId.value = response.data.sessionId
+        chatId.value = response.data.chatId
+      } else {
+        ElMessage.error('初始化对话失败')
+        return
+      }
+    } catch (error) {
+      console.error('初始化对话失败:', error)
+      ElMessage.error('初始化对话失败,请重试')
+      return
+    }
+  }
+
   // 添加用户消息
   messages.value.push({
     role: 'user',
@@ -177,7 +223,6 @@ const handleSend = async () => {
     time: new Date()
   })
   
-  // inputMessage.value = ''
   await scrollToBottom()
 
   // 开始流式响应
@@ -196,23 +241,33 @@ const simulateStreamResponse = async () => {
     time: new Date()
   });
 
-  const session_id = '3317760afe5e11efa3130242ac130002'
-  const chat_id = '049da692fe2b11efa7b30242ac130002'
-  const user_id = '123'
+  const token = localStorage.getItem('token')
   
   let accumulatedText = ''; // 用于累积接收到的文本
+
+  // /admin/user/chat/converse
+  // /admin/ragflow/chat/converse
   
-  const eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}/admin/ragflow/chat/converse?chat_id=${chat_id}&question=${encodeURIComponent(inputMessage.value)}&stream=true&session_id=${session_id}&user_id=${user_id}`);
+  //请求ai聊天接口
+  const eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}/admin/user/chat/converse?chat_id=${chatId.value}&question=${encodeURIComponent(inputMessage.value)}&stream=true&session_id=${sessionId.value}&user_id=${userStore.userInfo.id}&token=${token}`);
 
-  eventSource.onmessage = async (event) => {
+  eventSource.onmessage = async (event) => {  
     try {
       const data = JSON.parse(event.data);
       
       if (data.data === true) {
-        console.log('数据流结束');
-        // 在流结束时才进行最终的 markdown 渲染
-        const formattedText = md.render(accumulatedText);
-        messages.value[messageIndex].content = formattedText;
+       
+        // accumulatedText = '<think>' + accumulatedText;
+          console.log('数据流结束',accumulatedText);
+        // 检查是否包含<think>标签
+        if (accumulatedText.startsWith('<think>') && accumulatedText.endsWith('</think>')) {
+          // 如果是think标签内容,直接显示原文
+          messages.value[messageIndex].content = accumulatedText;
+        } else {
+          // 否则进行markdown渲染
+          const formattedText = md.render(accumulatedText);
+          messages.value[messageIndex].content = formattedText;
+        }
         eventSource.close();
         loading.value = false;
         await scrollToBottom();
@@ -220,9 +275,10 @@ const simulateStreamResponse = async () => {
       }
 
       if (data.data && data.data.answer) {
-        // 累积接收到的文本,但不立即进行 markdown 渲染
+        console.log('data.data.answer',data.data.answer);
+        // 累积接收到的文本
         accumulatedText += data.data.answer;
-        // 临时显示原始文本
+        // 临时显示文本
         messages.value[messageIndex].content = accumulatedText;
         await nextTick();
         await scrollToBottom();
@@ -288,7 +344,96 @@ const handleLike = (messageId, isLike) => {
   // TODO: 实现点赞/反对功能
 }
 
+// 获取历史记录
+const getHistoryChat = async (historyId) => {
+  try {
+    const response = await get(`/admin/userHistoryLog/selectById?id=${historyId}`)
+    if (response.data) {
+      const { chatId: historyChatId, sessionId: historySessionId, askList } = response.data
+      chatId.value = historyChatId
+      sessionId.value = historySessionId
+
+      // 清空现有消息
+      messages.value = []
+
+      // 按照 askSeq 排序对话记录
+      const sortedMessages = askList.sort((a, b) => Number(a.askSeq) - Number(b.askSeq))
+
+      // 添加所有对话记录
+      for (const message of sortedMessages) {
+        // 添加用户问题
+        messages.value.push({
+          role: 'user',
+          content: message.askContent,
+          time: new Date(message.createTime)
+        })
+
+        // 处理AI回答
+        try {
+          let answerContent = message.answerContent
+          let formattedAnswer = ''
+
+          // 移除开头的 "data:"
+          if (answerContent.startsWith('data:')) {
+            answerContent = answerContent.substring(5)
+          }
+
+          // 尝试解析第一个 JSON 对象(包含实际回答)
+          try {
+            const firstBrace = answerContent.indexOf('{')
+            const lastBrace = answerContent.indexOf('}{')
+            if (firstBrace !== -1 && lastBrace !== -1) {
+              const jsonStr = answerContent.substring(firstBrace, lastBrace + 1)
+              const jsonData = JSON.parse(jsonStr)
+              if (jsonData.data && jsonData.data.answer) {
+                formattedAnswer = jsonData.data.answer
+              }
+            }
+          } catch (e) {
+            console.warn('解析回答内容失败:', e)
+            formattedAnswer = answerContent
+          }
+
+          // 如果没有成功解析出任何内容,使用原始内容
+          if (!formattedAnswer.trim()) {
+            formattedAnswer = answerContent
+          }
+
+          // 添加AI回答
+          messages.value.push({
+            id: message.id,
+            role: 'assistant',
+            content: md.render(formattedAnswer),
+            time: new Date(message.createTime)
+          })
+        } catch (error) {
+          console.error('处理回答内容失败:', error)
+          // 如果处理失败,直接显示原始内容
+          messages.value.push({
+            id: message.id,
+            role: 'assistant',
+            content: answerContent,
+            time: new Date(message.createTime)
+          })
+        }
+      }
+
+      isChating.value = true
+      await nextTick()
+      scrollToBottom()
+    }
+  } catch (error) {
+    console.error('获取历史记录失败:', error)
+    ElMessage.error('获取历史记录失败')
+  }
+}
+
 onMounted(() => {
+  const historyId = route.query.historyId
+  if (historyId) {
+    getHistoryChat(historyId)
+  }
+  getKnowledgeList()
   scrollToBottom()
 })
 </script>

+ 20 - 23
src/views/DocSearch.vue

@@ -2,7 +2,7 @@
   <div class="doc-search-container">
     <!-- 左侧知识库类型选择 -->
     <div class="sidebar">
-      <h2 class="sidebar-title">知识库</h2>
+      <h2 class="sidebar-title" @click="router.push('/knowledge-list')">知识库</h2>
       <ul class="type-list">
         <!-- <li 
           class="type-item" 
@@ -147,6 +147,11 @@ import { ref, reactive, onMounted } from 'vue'
 import { ElMessage } from 'element-plus'
 import { Search, View, Star, Download, Document, Files, Grid, PictureFilled, Folder } from '@element-plus/icons-vue'
 import { get, post,put } from '../utils/request'
+import { useUserStore } from '../stores/user'
+import { useRoute, useRouter } from 'vue-router'
+const userStore = useUserStore()
+const router = useRouter()
+
 import docTypeJs from '../assets/doc-type-js.png'
 import docTypeSw from '../assets/doc-type-sw.png'
 import docTypeXz from '../assets/doc-type-xz.png'
@@ -171,10 +176,13 @@ const typeOptions = ref([])
 // 添加获取知识库列表的方法
 const getKnowInfoList = async () => {
   try {
-    const response = await get('/admin/knowInfo/getList',{pageNum:1,pageSize:100})
+    // const response = await get('/admin/knowInfo/getList',{pageNum:1,pageSize:100})
+    const userId = userStore.userInfo.id
+    const response = await get('/admin/user/chat/knowInfoList?userId='+userId)
+
     console.log('response', response);
     
-    typeOptions.value = response.rows.map(item => ({
+    typeOptions.value = response.data.map(item => ({ 
       value: item.id,
       label: item.name,
       icon: getIconByType(item.type) // 根据类型获取对应图标
@@ -214,8 +222,8 @@ const handleSearch = async () => {
     }
     
     const response = await get('/admin/knowFile/search', params)
-    searchResults.value = response.rows
-    total.value = Number(response.total)
+    searchResults.value = response.data.rows
+    total.value = Number(response.data.total)
   } catch (error) {
     // 模拟数据
     // searchResults.value = [
@@ -265,31 +273,18 @@ const downloadDocument = async (doc) => {
   // fileUrl是文件的地址,直接下载
   try {
     window.open(doc.fileUrl, '_blank')
+    // 添加文件下载记录
+    await post('/admin/userHistoryLog/saveFileDownloadLog', {
+      relatedId: doc.id
+    })
   } catch (error) {
     console.log('error', error);
     ElMessage.error('下载失败,请重试文件地址:'+doc.fileUrl)
   }
-  // try {
-  //   const response = await get(`/doc/download/${doc.id}`, {
-  //     responseType: 'blob'
-  //   })
-    
-  //   const url = window.URL.createObjectURL(new Blob([response]))
-  //   const link = document.createElement('a')
-  //   link.href = url
-  //   link.download = doc.title
-  //   document.body.appendChild(link)
-  //   link.click()
-  //   document.body.removeChild(link)
-  //   window.URL.revokeObjectURL(url)
-    
-  //   ElMessage.success('下载成功')
-  // } catch (error) {
-  //   ElMessage.error('下载失败,请重试')
-  // }
 }
 
 const toggleFavorite = async (doc) => {
+  console.log('doc', doc);
   try {
     if (doc.isCollect == 0) {
       await post('admin/userCollect/save', {
@@ -306,6 +301,7 @@ const toggleFavorite = async (doc) => {
     }
     doc.isCollect = doc.isCollect == 0 ? 1 : 0
     ElMessage.success(doc.isCollect == 1 ? '收藏成功' : '已取消收藏')
+    handleSearch()
   } catch (error) {
     ElMessage.error('操作失败,请重试')
   }
@@ -357,6 +353,7 @@ const highlightText = (text) => {
     border-bottom: 1px solid $border-lighter;
     text-align: center;
     color: #191A1E;
+    cursor: pointer;
   }
 
   .type-list {

+ 10 - 11
src/views/Favorites.vue

@@ -4,8 +4,8 @@
     <div class="left-panel">
       <el-select v-model="currentType" placeholder="选择收藏类型" class="type-select">
         <el-option label="全部" value="all" />
-        <el-option label="对话" value="ai.answer" />
-        <el-option label="方案" value="scheme" />
+        <!-- <el-option label="对话" value="ai.answer" /> -->
+        <!-- <el-option label="方案" value="scheme" /> -->
         <el-option label="文档" value="file.search" />
       </el-select>
     </div>
@@ -52,7 +52,7 @@
                 v-model="item.checked"
                 @change="handleItemSelect"
               />
-              <div class="item-title">{{ item.fileName }}</div>
+              <div class="item-title">{{ item.name||item.fileName }}</div>
               <div class="item-info">
                 <span class="item-date">{{ formatDate(item.createTime) }}</span>
                 <el-dropdown v-if="!isBatchMode" @command="handleCommand($event, item)">
@@ -210,8 +210,8 @@ const fetchFavorites = async () => {
       pageSize: pageSize.value
     }
     const response = await get('admin/userCollect/pageList', params)
-    favorites.value = response.rows || []
-    total.value = Number(response.total) || 0
+    favorites.value = response.data.rows || []
+    total.value = Number(response.data.total) || 0
   } catch (error) {
     ElMessage.error('获取列表失败')
   } finally {
@@ -323,7 +323,6 @@ const handleRename = async () => {
 
 const removeFavorite = async (item) => {
   console.log('item', item);
-  return
   try {
     await ElMessageBox.confirm(
       '确定要删除这个收藏吗?',
@@ -335,12 +334,12 @@ const removeFavorite = async (item) => {
       }
     )
     
-    await put('admin/userCollect/cancel', {
-      id: item.collectId,
-      relatedType: 'file.search',//related_type : AI问答 : ai.answer 方案:scheme 文件检索: file.search
-      relatedId: item.id,
+    await del('admin/userCollect/deleteById', {
+      id: item.id,
+      // relatedType: 'file.search',//related_type : AI问答 : ai.answer 方案:scheme 文件检索: file.search
+      // relatedId: item.id,
     })
-    ElMessage.success('删除成功')
+    // ElMessage.success('删除成功')
     fetchFavorites()
   } catch (error) {
     if (error !== 'cancel') {

+ 67 - 19
src/views/History.vue

@@ -54,17 +54,18 @@
   
         <!-- 历史列表 -->
         <div class="history-list" v-loading="loading">
-          <template v-if="filteredFavorites.length">
+          <template v-if="filteredHistory.length">
             <div
-              v-for="item in filteredFavorites"
+              v-for="item in filteredHistory"
               :key="item.id"
               class="favorite-item"
             >
-              <div class="item-content">
+              <div class="item-content" @click="handleItemClick(item)">
                 <el-checkbox 
                   v-show="isBatchMode"
                   v-model="item.checked"
                   @change="handleItemSelect"
+                  @click.stop
                 />
                 <div class="item-title">{{ item.relatedType==='ai.answer'? item.askContent : item.fileName }}</div>
                 <div class="item-info">
@@ -173,7 +174,7 @@
   const isBatchMode = ref(false)
   const isAllSelected = ref(false)
   const selectedCount = computed(() => {
-    return filteredFavorites.value.filter(item => item.checked).length
+    return filteredHistory.value.filter(item => item.checked).length
   })
   
   // 重命名相关
@@ -192,7 +193,7 @@
   }
   
   // 计算属性:过滤后的历史列表
-  const filteredFavorites = computed(() => {
+  const filteredHistory = computed(() => {
     let result = history.value.map(item => ({
       ...item,
       checked: item.checked || false
@@ -215,7 +216,7 @@
   })
   
   // 获取历史列表
-  const fetchFavorites = async () => {
+  const fetchHistory = async () => {
     loading.value = true
     try {
       const params = {
@@ -225,10 +226,10 @@
         pageSize: pageSize.value
       }
       const response = await get('admin/userHistoryLog/pageList', params)
-      console.log('response.rows', response.rows);
-      history.value = response.rows || []
+      console.log('response.rows', response.data.rows);
+      history.value = response.data.rows || []
       console.log('history.value', history.value);
-      total.value = Number(response.total) || 0
+      total.value = Number(response.data.total) || 0
     } catch (error) {
       ElMessage.error('获取列表失败')
     } finally {
@@ -265,8 +266,8 @@
   
   const handleItemSelect = () => {
     // 检查当前过滤后的列表是否全部选中
-    isAllSelected.value = filteredFavorites.value.length > 0 && 
-      filteredFavorites.value.every(item => item.checked)
+    isAllSelected.value = filteredHistory.value.length > 0 && 
+      filteredHistory.value.every(item => item.checked)
   }
   
   const handleBatchDelete = async () => {
@@ -292,7 +293,7 @@
       
       await post('/history/batch-delete', { ids: selectedIds })
       ElMessage.success('删除成功')
-      await fetchFavorites()
+      await fetchHistory()
       cancelBatchMode()
     } catch (error) {
       if (error !== 'cancel') {
@@ -330,7 +331,7 @@
       
       ElMessage.success('重命名成功')
       renameDialogVisible.value = false
-      fetchFavorites()
+      fetchHistory()
     } catch (error) {
       if (error !== 'cancel') {
         ElMessage.error('重命名失败')
@@ -356,7 +357,7 @@
         relatedId: item.id,
       })
       ElMessage.success('删除成功')
-      fetchFavorites()
+      fetchHistory()
     } catch (error) {
       if (error !== 'cancel') {
         ElMessage.error('删除失败')
@@ -406,7 +407,7 @@
     currentPage.value = 1 // 搜索时重置为第一页
     searchLoading.value = true
     try {
-      await fetchFavorites()
+      await fetchHistory()
     } finally {
       searchLoading.value = false
     }
@@ -423,18 +424,18 @@
   const handleSizeChange = (val) => {
     pageSize.value = val
     currentPage.value = 1 // 切换每页条数时重置为第一页
-    fetchFavorites()
+    fetchHistory()
   }
   
   const handleCurrentChange = (val) => {
     currentPage.value = val
-    fetchFavorites()
+    fetchHistory()
   }
   
   // 监听类型变化
   watch(currentType, () => {
     currentPage.value = 1 // 切换类型时重置为第一页
-    fetchFavorites()
+    fetchHistory()
   })
   
   // 类型选择相关
@@ -451,8 +452,55 @@
     router.back()
   }
   
+  const handleItemClick = async (item) => {
+    if (!item.relatedType) return;
+
+    switch (item.relatedType) {
+      case 'ai.answer':
+        // router.push(`/ai-chat?historyId=${item.id}`);
+        window.location.href = `/ai-chat?historyId=${item.id}`
+        break;
+      case 'scheme':
+        router.push(`/solution?historyId=${item.id}`);
+        break;
+      case 'file.download':
+          try {
+              window.open(item.fileUrl, '_blank')
+              // 添加文件下载记录
+              await post('/admin/userHistoryLog/saveFileDownloadLog', {
+                relatedId: item.id
+              })
+            } catch (error) {
+              console.log('error', error);
+              ElMessage.error('下载失败,请重试文件地址:'+item.fileUrl)
+            }
+        // try {
+        //   // 调用下载接口
+        //   const response = await get(`admin/file/download/${item.id}`, {}, {
+        //     responseType: 'blob'
+        //   });
+          
+        //   // 创建下载链接
+        //   const blob = new Blob([response], { type: response.type });
+        //   const url = window.URL.createObjectURL(blob);
+        //   const link = document.createElement('a');
+        //   link.href = url;
+        //   link.download = item.fileName || '下载文件';
+        //   document.body.appendChild(link);
+        //   link.click();
+        //   document.body.removeChild(link);
+        //   window.URL.revokeObjectURL(url);
+        // } catch (error) {
+        //   ElMessage.error('文件下载失败');
+        // }
+        break;
+      default:
+        break;
+    }
+  };
+  
   // 初始化
-  fetchFavorites()
+  fetchHistory()
   </script>
   
   <style lang="scss" scoped>

+ 8 - 8
src/views/Login.vue

@@ -3,13 +3,13 @@
     <div class="login-box">
       <div class="login-header">
         <img src="@/assets/logo.png" alt="logo" class="logo">
-        <h2>GPT对话系统</h2>
+        <h2>省信息中心对话系统</h2>
       </div>
       <div class="login-form">
         <div class="form-item">
           <div class="input-with-icon">
-            <img src="@/assets/login-user.png" alt="用户" class="input-icon">
-            <input type="text" v-model="username" placeholder="请输入手机号">
+            <img src="@/assets/login-user.png" alt="账号" class="input-icon">
+            <input type="text" v-model="username" placeholder="请输入号">
           </div>
         </div>
         <div class="form-item">
@@ -101,7 +101,7 @@ const saveLoginInfo = () => {
 
 const handleLogin = async () => {
   if (!username.value || !password.value || !captcha.value) {
-    alert('请输入用户名、密码和验证码')
+    alert('请输入账号、密码和验证码')
     return
   }
 
@@ -116,11 +116,11 @@ const handleLogin = async () => {
     })
     // console.log('res', res)
     const userStore = useUserStore()
-    userStore.setToken(res.token)
+    userStore.setToken(res.data.token)
     
     // 获取用户信息
     const userInfoRes = await get('/admin/sys/user/info')||{}
-    userStore.setUserInfo(userInfoRes)
+    userStore.setUserInfo(userInfoRes.data)
     
     // 保存登录信息(如果勾选了记住密码)
     saveLoginInfo()
@@ -128,8 +128,8 @@ const handleLogin = async () => {
      // 通过接口检查密码是否过期
      const pwdExpireRes = await get('/admin/sys/user/pwdExpire')
     //  console.log('pwdExpireRes', pwdExpireRes);
-     if(pwdExpireRes.isExpire ){
-      ElMessage.error(pwdExpireRes.tipMsg||'密码已过期,请修改密码');
+     if(pwdExpireRes.data.isExpire ){
+      ElMessage.error(pwdExpireRes.data.tipMsg||'密码已过期,请修改密码');
       return
      }  
     

+ 412 - 76
src/views/Solution.vue

@@ -10,34 +10,34 @@
     <div class="chat-messages"  v-show="isChating" ref="messagesRef" :class="{ 'chat-active': isChating }">
       <div v-for="(message, index) in messages" :key="index" 
            :class="['message', message.role === 'user' ? 'user' : 'assistant']">
-        <div class="avatar">
+        <div class="avatar" v-if="message.content">
           <img :src="message.role === 'user' ? userAvatar : assistantAvatar" :alt="message.role">
         </div>
-        <div class="content">
-          <div class="text">{{ message.content }}</div>
+        <div class="content" v-if="message.content">
+          <div class="text markdown-body" v-html="message.content"></div>
           <div class="message-actions" v-if="message.role === 'assistant'">
             <div class="left-actions">
               <button class="action-btn" @click="copyContent(message.content)">
                 <img src="@/assets/icon-copy.png" alt="复制">
               </button>
-              <button class="action-btn" @click="toggleFavorite(message.id)">
+              <!-- <button class="action-btn" @click="toggleFavorite(message.id)">
                 <img src="@/assets/icon-star.png" alt="收藏">
-              </button>
+              </button> -->
               <button class="action-btn reanswer" @click="reAnswer(message.id)">
                 <img src="@/assets/icon-reanswer.png" alt="重新回答">
                 <span>重新回答</span>
               </button>
             </div>
-            <div class="right-actions">
+            <!-- <div class="right-actions">
               <button class="action-btn" @click="handleLike(message.id, true)">
                 <img src="@/assets/icon-like.png" alt="点赞">
               </button>
               <button class="action-btn" @click="handleLike(message.id, false)">
                 <img src="@/assets/icon-dislike.png" alt="反对">
               </button>
-            </div>
+            </div> -->
           </div>
-          <div class="time">{{ formatTime(message.time) }}</div>
+          <!-- <div class="time">{{ formatTime(message.time) }}</div> -->
         </div>
       </div>
       <div v-if="loading" class="message assistant">
@@ -61,7 +61,7 @@
         ></textarea>
         <div class="input-actions">
           <div class="select-wrap">
-            <el-select v-model="selectedModel" class="model-select">
+            <el-select v-model="selectedModel" class="model-select" :disabled="isChating">
               <el-option v-for="model in models" :key="model.id" :value="model.id" :label="model.name">
                 {{ model.name }}
               </el-option>
@@ -73,24 +73,23 @@
           </button>
         </div>
       </div>
-      <!-- 知识库列表 -->
-      <!-- <div class="knowledge-base" v-if="!isChating">
+    </div>
+    <!-- 知识库列表 -->
+    <!-- <div class="knowledge-base" v-if="!isChating">
         <div class="knowledge-tags">
           <button 
             v-for="(item, index) in knowledgeList" 
             :key="index"
-            :class="['knowledge-tag', { active: selectedKnowledge.includes(item.id) }]"
-            @click="toggleKnowledge(item.id)"
+            :class="['knowledge-tag', { active: selectedKnowledge.includes(item.datasetId) }]"
+            @click="toggleKnowledge(item.datasetId)"
           >
             {{ item.name }}
           </button>
         </div>
       </div> -->
-
-    </div>
-
+    
     <!-- 模版区域 -->
-    <div class="template-section">
+    <div class="template-section" v-if="!isChating">
       <div class="template-type">
         <div class="type-title">全文</div>
         <div class="type-list">
@@ -119,23 +118,33 @@
       </div>
     </div>
 
+
   </div>
 </template>
 
 <script setup>
 import { ref, onMounted, nextTick } from 'vue'
-import { useRouter } from 'vue-router'
+import { useRoute,useRouter } from 'vue-router'
 import userAvatar from '@/assets/user-avatar.png'
 import assistantAvatar from '@/assets/assistant-avatar.png'
-
+import { post,get } from '@/utils/request'
+import { ElMessage } from 'element-plus'
+import MarkdownIt from 'markdown-it'
+import hljs from 'highlight.js'
+import 'highlight.js/styles/github.css'
+import { useUserStore } from '../stores/user'
+
+const userStore = useUserStore()
 const router = useRouter()
-
+const route = useRoute()
 const messagesRef = ref(null)
 const inputMessage = ref('')
 const loading = ref(false)
 const isChating = ref(false)
 const selectedKnowledge = ref([])
 const selectedModel = ref('DeepSeek')
+const sessionId = ref('')
+const chatId = ref('')
 
 import iconTemplate1 from '@/assets/icon-template-1.png'
 import iconTemplate2 from '@/assets/icon-template-2.png'
@@ -145,10 +154,35 @@ const templateList1 = ref([
   { id: 2, name: '工作月报', content: '辅助生成内容全面、条理清晰的工作月 报.', icon: iconTemplate2 },
   { id: 3, name: '会议纪要', content: '全面分析市场需求、竞争环境、商业投 入.', icon: iconTemplate3 }
 ])
-
 const templateList2 = ref([
   { id: 1, name: '商业计划书', content: '你是一个企业管理顾问,现在有一家公 司请', icon: iconTemplate1 },
 ])
+const useTemplate = async (id) => {
+  // 根据ID找到对应的模板
+  const template = [...templateList1.value, ...templateList2.value].find(t => t.id === id)
+  if (!template) return
+  
+  try {
+    // 调用create接口
+    const response = await post('/admin/user/chat/create', {
+      userId: userStore.userInfo.id,
+      datasetIds: selectedKnowledge.value.join(','),
+      templateId: template.id
+    })
+
+    if (response.data) {
+      sessionId.value = response.data.sessionId
+      chatId.value = response.data.chatId
+      ElMessage.success('初始化对话成功')
+    } else {
+      ElMessage.error('初始化对话失败')
+      return
+    }
+  } catch (error) {
+    console.error('初始化对话失败:', error)
+    ElMessage.error('初始化对话失败,请重试')
+  }
+}
 
 // 对话模型列表
 const models = ref([
@@ -158,33 +192,45 @@ const models = ref([
 ])
 
 // 知识库列表
-const knowledgeList = ref([
-  { id: 1, name: '知识库1' },
-  { id: 2, name: '知识库2' },
-  { id: 3, name: '知识库3' },
-  { id: 4, name: '知识库4' }
-])
-
-const useTemplate = (id) => {
-  // 根据ID找到对应的模板
-  const template = [...templateList1.value, ...templateList2.value].find(t => t.id === id)
-  if (!template) return
-  
-  // 转到SolutionChat页面,传递完整的模板信息
-  router.push({
-    path: '/solution-chat',
-    query: {
-      template: JSON.stringify({
-        id: template.id,
-        name: template.name,
-        content: template.content
-      })
+const knowledgeList = ref([])
+
+// 获取知识库列表
+const getKnowledgeList = async () => {
+  try {
+    const userId = userStore.userInfo.id
+    const response = await get('/admin/user/chat/knowInfoList?userId='+userId)
+    // console.log('response', response);
+    if (response.data) {
+      knowledgeList.value = response.data
+      // 默认选中第一个知识库
+      selectedKnowledge.value = [knowledgeList.value[0].datasetId]
+    } else {
+      ElMessage.error('获取知识库列表失败')
     }
-  })
+  } catch (error) {
+    console.error('获取知识库列表失败:', error)
+    ElMessage.error('获取知识库列表失败')
+  }
 }
 
 const messages = ref([])
 
+// 创建 markdown-it 实例,配置代码高亮
+const md = new MarkdownIt({
+  html: false,
+  breaks: true,
+  linkify: true,
+  highlight: function (str, lang) {
+    if (lang && hljs.getLanguage(lang)) {
+      try {
+        return `<pre class="hljs"><code>${hljs.highlight(str, { language: lang, ignoreIllegals: true }).value}</code></pre>`
+      } catch (__) {}
+    }
+    // 使用默认的转义
+    return `<pre class="hljs"><code>${md.utils.escapeHtml(str)}</code></pre>`
+  }
+})
+
 const formatTime = (time) => {
   return new Date(time).toLocaleTimeString()
 }
@@ -206,6 +252,10 @@ const scrollToBottom = async () => {
 }
 
 const handleSend = async () => {
+  if (selectedKnowledge.value.length === 0) {
+      ElMessage.warning('请先选择知识库')
+      return
+    }
   const message = inputMessage.value.trim()
   if (!message || loading.value) return
 
@@ -213,6 +263,28 @@ const handleSend = async () => {
     isChating.value = true
   }
 
+  // 如果没有session_id和chat_id,先获取它们
+  if (!sessionId.value || !chatId.value) {
+    try {
+      const response = await post('/admin/user/chat/create', {
+        userId: userStore.userInfo.id,
+        datasetIds: selectedKnowledge.value.join(',')
+      })
+
+      if (response.data) {
+        sessionId.value = response.data.sessionId
+        chatId.value = response.data.chatId
+      } else {
+        ElMessage.error('初始化对话失败')
+        return
+      }
+    } catch (error) {
+      console.error('初始化对话失败:', error)
+      ElMessage.error('初始化对话失败,请重试')
+      return
+    }
+  }
+
   // 添加用户消息
   messages.value.push({
     role: 'user',
@@ -220,41 +292,97 @@ const handleSend = async () => {
     time: new Date()
   })
   
-  inputMessage.value = ''
   await scrollToBottom()
 
   // 开始流式响应
   loading.value = true
-  // TODO: 实现流式响应的API调用
   simulateStreamResponse()
 }
 
-const simulateStreamResponse = () => {
-  const response = '这是一个模拟的AI响应消息。在实际应用中,这里应该调用后端API获取真实的流式响应。'
-  let index = 0
-  const messageObj = {
+const simulateStreamResponse = async () => {
+  loading.value = true;
+  
+  const messageIndex = messages.value.length;
+  messages.value.push({
     id: Date.now(),
     role: 'assistant',
     content: '',
     time: new Date()
-  }
-  messages.value.push(messageObj)
+  });
 
-  const timer = setInterval(() => {
-    if (index < response.length) {
-      messageObj.content += response[index]
-      index++
-      scrollToBottom()
-    } else {
-      clearInterval(timer)
-      loading.value = false
+  const token = localStorage.getItem('token')
+  
+  let accumulatedText = ''; // 用于累积接收到的文本
+
+  // /admin/user/chat/converse
+  // /admin/ragflow/chat/converse
+  
+  //请求ai聊天接口
+  const eventSource = new EventSource(`${import.meta.env.VITE_API_BASE_URL}/admin/user/chat/converse?chat_id=${chatId.value}&question=${encodeURIComponent(inputMessage.value)}&stream=true&session_id=${sessionId.value}&user_id=${userStore.userInfo.id}&token=${token}`);
+
+  eventSource.onmessage = async (event) => {  
+    try {
+      const data = JSON.parse(event.data);
+      
+      if (data.data === true) {
+       
+        // accumulatedText = '<think>' + accumulatedText;
+          console.log('数据流结束',accumulatedText);
+        // 检查是否包含<think>标签
+        if (accumulatedText.startsWith('<think>') && accumulatedText.endsWith('</think>')) {
+          // 如果是think标签内容,直接显示原文
+          messages.value[messageIndex].content = accumulatedText;
+        } else {
+          // 否则进行markdown渲染
+          const formattedText = md.render(accumulatedText);
+          messages.value[messageIndex].content = formattedText;
+        }
+        eventSource.close();
+        loading.value = false;
+        await scrollToBottom();
+        // AI回答结束后跳转到solution-chat页面
+        router.push(`/solution-chat?historyId=${chatId.value}`);
+        return;
+      }
+
+      if (data.data && data.data.answer) {
+        console.log('data.data.answer',data.data.answer);
+        // 累积接收到的文本
+        accumulatedText += data.data.answer;
+        // 临时显示文本
+        messages.value[messageIndex].content = accumulatedText;
+        await nextTick();
+        await scrollToBottom();
+      }
+    } catch (error) {
+      console.error('Parse error:', error);
     }
-  }, 50)
+  };
+
+  eventSource.onerror = (error) => {
+    console.error('EventSource error:', error);
+    eventSource.close();
+    loading.value = false;
+    if (!messages.value[messageIndex].content) {
+      ElMessage.error('对话请求失败,请重试');
+    }
+  };
+
+  eventSource.onopen = () => {
+    console.log('连接已建立');
+    // 清空输入框
+    inputMessage.value = '';
+  };
 }
 
+// 修改复制功能,去除HTML标签
 const copyContent = (content) => {
-  navigator.clipboard.writeText(content)
-  // TODO: 添加复制成功提示
+  // 创建临时元素来去除HTML标签
+  const temp = document.createElement('div')
+  temp.innerHTML = content
+  const plainText = temp.textContent || temp.innerText
+  navigator.clipboard.writeText(plainText)
+  ElMessage.success('复制成功')
 }
 
 const toggleFavorite = (messageId) => {
@@ -262,14 +390,121 @@ const toggleFavorite = (messageId) => {
 }
 
 const reAnswer = (messageId) => {
-  // TODO: 实现重新回答功能
+  // 找到当前消息的索引
+  const currentIndex = messages.value.findIndex(msg => msg.id === messageId);
+  if (currentIndex === -1) return;
+
+  // 找到这条消息之前最近的用户消息
+  let userMessageIndex = currentIndex - 1;
+  while (userMessageIndex >= 0) {
+    if (messages.value[userMessageIndex].role === 'user') {
+      // 获取用户的问题
+      const question = messages.value[userMessageIndex].content;
+      // 设置输入框的内容
+      inputMessage.value = question;
+      // 触发发送
+      handleSend();
+      break;
+    }
+    userMessageIndex--;
+  }
+  scrollToBottom();
 }
 
 const handleLike = (messageId, isLike) => {
   // TODO: 实现点赞/反对功能
 }
 
+// 获取历史记录
+const getHistoryChat = async (historyId) => {
+  try {
+    const response = await get(`/admin/userHistoryLog/selectById?id=${historyId}`)
+    if (response.data) {
+      const { chatId: historyChatId, sessionId: historySessionId, askList } = response.data
+      chatId.value = historyChatId
+      sessionId.value = historySessionId
+
+      // 清空现有消息
+      messages.value = []
+
+      // 按照 askSeq 排序对话记录
+      const sortedMessages = askList.sort((a, b) => Number(a.askSeq) - Number(b.askSeq))
+
+      // 添加所有对话记录
+      for (const message of sortedMessages) {
+        // 添加用户问题
+        messages.value.push({
+          role: 'user',
+          content: message.askContent,
+          time: new Date(message.createTime)
+        })
+
+        // 处理AI回答
+        try {
+          let answerContent = message.answerContent
+          let formattedAnswer = ''
+
+          // 移除开头的 "data:"
+          if (answerContent.startsWith('data:')) {
+            answerContent = answerContent.substring(5)
+          }
+
+          // 尝试解析第一个 JSON 对象(包含实际回答)
+          try {
+            const firstBrace = answerContent.indexOf('{')
+            const lastBrace = answerContent.indexOf('}{')
+            if (firstBrace !== -1 && lastBrace !== -1) {
+              const jsonStr = answerContent.substring(firstBrace, lastBrace + 1)
+              const jsonData = JSON.parse(jsonStr)
+              if (jsonData.data && jsonData.data.answer) {
+                formattedAnswer = jsonData.data.answer
+              }
+            }
+          } catch (e) {
+            console.warn('解析回答内容失败:', e)
+            formattedAnswer = answerContent
+          }
+
+          // 如果没有成功解析出任何内容,使用原始内容
+          if (!formattedAnswer.trim()) {
+            formattedAnswer = answerContent
+          }
+
+          // 添加AI回答
+          messages.value.push({
+            id: message.id,
+            role: 'assistant',
+            content: md.render(formattedAnswer),
+            time: new Date(message.createTime)
+          })
+        } catch (error) {
+          console.error('处理回答内容失败:', error)
+          // 如果处理失败,直接显示原始内容
+          messages.value.push({
+            id: message.id,
+            role: 'assistant',
+            content: answerContent,
+            time: new Date(message.createTime)
+          })
+        }
+      }
+
+      isChating.value = true
+      await nextTick()
+      scrollToBottom()
+    }
+  } catch (error) {
+    console.error('获取历史记录失败:', error)
+    ElMessage.error('获取历史记录失败')
+  }
+}
+
 onMounted(() => {
+  const historyId = route.query.historyId
+  if (historyId) {
+    getHistoryChat(historyId)
+  }
+  getKnowledgeList()
   scrollToBottom()
 })
 </script>
@@ -281,6 +516,9 @@ onMounted(() => {
   flex-direction: column;
   background: url('@/assets/ai-chat-bg.png') no-repeat center center;
   background-size: cover;
+  &:has(.chat-active){
+    background: #fff;
+  }
 
   .welcome-section {
     margin-top: 100px;
@@ -298,7 +536,7 @@ onMounted(() => {
   }
 
   .knowledge-base {
-    max-width: 800px;
+    width: 800px;
     margin: 24px auto 0;
 
     .knowledge-tags {
@@ -309,20 +547,20 @@ onMounted(() => {
 
       .knowledge-tag {
         padding: 8px 16px;
-        border: 1px solid #dcdfe6;
-        border-radius: 20px;
-        background: rgba(255, 255, 255, 0.9);
+        border: 1px solid #DADDE8;
+        border-radius: 15px;
+        background: transparent;
         cursor: pointer;
         transition: all 0.3s;
+        color: #414967;
 
         &:hover {
           background: #f5f7fa;
         }
 
         &.active {
-          background: $primary-color;
-          color: white;
-          border-color: $primary-color;
+          color: #FF7575;
+          border-color: #FF7575;
         }
       }
     }
@@ -331,7 +569,7 @@ onMounted(() => {
   .chat-messages {
     flex: 1;
     overflow-y: auto;
-    padding: 20px;
+    padding: 20px 20%;
     transition: all 0.3s;
 
     &.chat-active {
@@ -340,14 +578,17 @@ onMounted(() => {
 
     .message {
       display: flex;
-      margin-bottom: 20px;
+      margin-bottom: 100px;
       .avatar{
           img{
             width: 40px;
             height: 40px;
           }
         }
-      
+      .text {
+        padding: 10px 20px;
+        font-size: 16px;
+      }
       &.user {
         flex-direction: row-reverse;
         .content {
@@ -355,8 +596,8 @@ onMounted(() => {
           margin-left: 0;
           
           .text {
-            // background: $primary-color;
-            // color: white;
+            background: #FFF5F5;
+            color: #1B1C21;
             border-radius: 10px 2px 10px 10px;
           }
           
@@ -414,15 +655,17 @@ onMounted(() => {
     margin: 0 auto;
     padding: 20px;
     background: rgba(255, 255, 255, 0.9);
+    box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.1);
     backdrop-filter: blur(10px);
     border-radius: 20px;
     min-width: 800px;
     &.chat-active{
       position: fixed;
       bottom: 0;
-      left: 0;
+      left: $sidebar-width;
       right: 0;
       padding: 20px;
+      box-shadow: none;
     }
 
     .input-container {
@@ -501,6 +744,99 @@ onMounted(() => {
       }
     }
   }
+
+  // 添加 markdown 样式
+  .markdown-body {
+    line-height: 1.6;
+    font-size: 14px;
+    
+    p {
+      margin: 8px 0;
+    }
+
+    h1, h2, h3, h4, h5, h6 {
+      margin: 16px 0 8px;
+      font-weight: 600;
+    }
+
+    code {
+      background-color: rgba(175, 184, 193, 0.2);
+      padding: 0.2em 0.4em;
+      border-radius: 6px;
+      font-family: monospace;
+    }
+
+    pre {
+      background-color: #f6f8fa;
+      border-radius: 6px;
+      padding: 16px;
+      overflow: auto;
+      margin: 16px 0;
+
+      code {
+        background-color: transparent;
+        padding: 0;
+        font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+        font-size: 14px;
+        line-height: 1.45;
+        tab-size: 4;
+      }
+
+      &.hljs {
+        background: #f6f8fa;
+        border: 1px solid #e1e4e8;
+      }
+    }
+
+    ul, ol {
+      padding-left: 20px;
+      margin: 8px 0;
+    }
+
+    table {
+      border-collapse: collapse;
+      margin: 8px 0;
+      
+      th, td {
+        border: 1px solid #d0d7de;
+        padding: 6px 13px;
+      }
+
+      th {
+        background-color: #f6f8fa;
+      }
+    }
+
+    blockquote {
+      border-left: 4px solid #d0d7de;
+      padding-left: 16px;
+      margin: 8px 0;
+      color: #656d76;
+    }
+
+    img {
+      max-width: 100%;
+      height: auto;
+    }
+
+    a {
+      color: #0969da;
+      text-decoration: none;
+      
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+
+    code {
+      font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+      font-size: 0.9em;
+      padding: 0.2em 0.4em;
+      margin: 0;
+      background-color: rgba(175, 184, 193, 0.2);
+      border-radius: 6px;
+    }
+  }
 }
 .template-section{
   margin: 24px auto;

+ 178 - 2
src/views/UserInfo.vue

@@ -1,9 +1,185 @@
 <!--
- * @Description: 
+ * @Description: User Profile Page
  * @Author: gcz
  * @Date: 2025-03-10 17:37:39
  * @LastEditors: gcz
- * @LastEditTime: 2025-03-10 17:37:39
+ * @LastEditTime: 2025-03-14 15:41:59
  * @FilePath: \knowledge_userui\src\views\UserInfo.vue
  * @Copyright: Copyright (c) 2016~2025 by gcz, All Rights Reserved. 
 -->
+<template>
+  <div class="user-profile">
+    <div class="profile-card">
+      <div class="avatar-section">
+        <div class="avatar-wrapper" @click="triggerUpload">
+          <el-avatar 
+            :size="100" 
+            :src="userStore.userInfo.headUrl" 
+            class="user-avatar">
+            <img src="@/assets/user-avatar.png" />
+          </el-avatar>
+          <div class="avatar-overlay">
+            <i class="el-icon-camera"></i>
+            <span>更换头像</span>
+          </div>
+        </div>
+        <input
+          type="file"
+          ref="fileInput"
+          style="display: none"
+          accept="image/*"
+          @change="handleAvatarUpload"
+        />
+      </div>
+
+      <div class="user-info">
+        <h2>{{ userStore.userInfo.realName||userStore.userInfo.username }}</h2>
+        <p class="department">{{ userStore.userInfo.deptName }}</p>
+      </div>
+
+      <el-button 
+        type="danger" 
+        class="logout-btn"
+        @click="handleLogout">
+        退出登录
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+import { useRouter } from 'vue-router'
+import { get, post, del, put } from '../utils/request'
+import { useUserStore } from '../stores/user'
+
+const router = useRouter()
+const fileInput = ref(null)
+const userStore = useUserStore()
+
+
+const triggerUpload = () => {
+  fileInput.value.click()
+}
+
+const handleAvatarUpload = async (event) => {
+  const file = event.target.files[0]
+  if (!file) return
+
+  const formData = new FormData()
+  formData.append('file', file)
+
+  try {
+    const token = localStorage.getItem('token') // 获取存储的token
+    // 上传文件
+    const uploadRes = await post('/admin/sys/oss/upload', formData, {
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    })
+    console.log('uploadRes', uploadRes);
+
+    if (uploadRes.data.url) {
+      // 更新用户头像
+      const updateRes = await put('/admin/sys/user/updateHead', {
+        headUrl: uploadRes.data.url
+      })
+      console.log('updateRes', updateRes);
+      if (updateRes.code === 0) {
+        // userInfo.headUrl = uploadRes.data.url
+        // 获取用户信息
+        const userInfoRes = await get('/admin/sys/user/info')||{}
+        userStore.setUserInfo(userInfoRes.data)
+        // userStore.userInfo.headUrl = uploadRes.data.url
+        ElMessage.success('头像更新成功')
+      }
+    }
+  } catch (error) {
+    ElMessage.error('头像上传失败')
+    console.error('Upload error:', error)
+  }
+}
+
+const handleLogout = () => {
+  // 这里添加登出逻辑
+  localStorage.removeItem('token')
+  router.push('/login')
+}
+</script>
+
+<style scoped>
+.user-profile {
+  min-height: 100vh;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: #f5f7fa;
+}
+
+.profile-card {
+  background: white;
+  padding: 40px;
+  border-radius: 16px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+  width: 100%;
+  max-width: 400px;
+  text-align: center;
+}
+
+.avatar-section {
+  margin-bottom: 24px;
+}
+
+.avatar-wrapper {
+  position: relative;
+  display: inline-block;
+  cursor: pointer;
+}
+
+.avatar-overlay {
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  bottom: 0;
+  background: rgba(0, 0, 0, 0.5);
+  border-radius: 50%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  color: white;
+  opacity: 0;
+  transition: opacity 0.3s;
+}
+
+.avatar-wrapper:hover .avatar-overlay {
+  opacity: 1;
+}
+
+.user-avatar {
+  border: 4px solid #fff;
+  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
+}
+
+.user-info {
+  margin-bottom: 32px;
+}
+
+.user-info h2 {
+  margin: 0;
+  font-size: 24px;
+  color: #303133;
+}
+
+.department {
+  color: #909399;
+  margin: 8px 0 0;
+}
+
+.logout-btn {
+  width: 100%;
+  margin-top: 16px;
+}
+</style>

+ 205 - 0
src/views/knowledgeFile.vue

@@ -0,0 +1,205 @@
+<template>
+  <div class="knowledge-file">
+    <el-card class="file-list">
+      <template #header>
+        <div class="card-header">
+          <span>文件列表</span>
+        </div>
+      </template>
+      
+      <!-- 添加搜索表单 -->
+      <div class="search-form">
+        <el-form :inline="true" :model="searchForm">
+          <el-form-item label="文件名称">
+            <el-input v-model="searchForm.fileName" placeholder="请输入文件名称" clearable @keyup.enter="handleSearch" />
+          </el-form-item>
+          <el-form-item label="文件类型">
+            <el-select v-model="searchForm.fileType" placeholder="请选择文件类型" clearable>
+              <el-option v-for="item in fileTypes" :key="item" :label="item" :value="item" />
+            </el-select>
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleSearch">搜索</el-button>
+            <el-button @click="handleReset">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+      
+      <el-table :data="fileList" style="width: 100%">
+        <el-table-column prop="fileName" label="文件名称" />
+        <el-table-column prop="createBy" label="上传人" />
+        <el-table-column prop="createTime" label="上传时间">
+          <template #default="scope">
+            {{ formatDate(scope.row.createTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="120">
+          <template #default="scope">
+            <el-button
+              type="primary"
+              link
+              @click="handleDownload(scope.row)"
+            >
+              下载
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 添加分页组件 -->
+      <div class="pagination-container">
+        <el-pagination
+          v-model:current-page="pagination.pageNum"
+          v-model:page-size="pagination.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="pagination.total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { get,post } from '../utils/request'
+
+const route = useRoute()
+
+const fileList = ref([])
+// 添加搜索表单数据
+const searchForm = ref({
+  fileName: '',
+  fileType: ''
+})
+
+// 添加分页数据
+const pagination = ref({
+  pageNum: 1,
+  pageSize: 10,
+  total: 0
+})
+
+// 文件类型选项
+const fileTypes = ref(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'md', 'sql', 'txt'])
+
+// 获取文件列表
+const getFileList = async () => {
+  try {
+    const konwInfoId = route.query.konwInfoId
+    if (!konwInfoId) {
+      ElMessage.error('知识库ID不能为空')
+      return
+    }
+    console.log('route.query', route.query);
+    const params = {
+      konwInfoId: konwInfoId,
+      fileName: searchForm.value.fileName,
+      fileType: searchForm.value.fileType,
+      pageNum: pagination.value.pageNum,
+      pageSize: pagination.value.pageSize
+    }
+    const res = await get('/admin/knowInfo/getFlieList', params)
+    if (res.code === 0) {
+      fileList.value = res.data.rows || []
+      pagination.value.total = Number(res.data.total) || 0
+    } else {
+      ElMessage.error(res.msg || '获取文件列表失败')
+    }
+  } catch (error) {
+    console.error('获取文件列表失败:', error)
+    ElMessage.error('获取文件列表失败')
+  }
+}
+
+// 下载文件
+const handleDownload = async (item) => {
+  try {
+    window.open(item.fileUrl, '_blank')
+    // 添加文件下载记录
+    await post('/admin/userHistoryLog/saveFileDownloadLog', {
+      relatedId: item.id
+    })
+  } catch (error) {
+    console.log('error', error);
+    ElMessage.error('下载失败,请重试文件地址:'+item.fileUrl)
+  }
+}
+
+// 搜索
+const handleSearch = () => {
+  pagination.value.pageNum = 1 // 搜索时重置到第一页
+  getFileList()
+}
+
+// 重置
+const handleReset = () => {
+  searchForm.value = {
+    fileName: '',
+    fileType: ''
+  }
+  pagination.value.pageNum = 1 // 重置时回到第一页
+  getFileList()
+}
+
+// 处理每页条数变化
+const handleSizeChange = (val) => {
+  pagination.value.pageSize = val
+  getFileList()
+}
+
+// 处理页码变化
+const handleCurrentChange = (val) => {
+  pagination.value.pageNum = val
+  getFileList()
+}
+
+// 格式化日期
+const formatDate = (dateStr) => {
+  if (!dateStr) return ''
+  const date = new Date(dateStr)
+  return date.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit'
+  })
+}
+
+onMounted(() => {
+  getFileList()
+})
+</script>
+
+<style scoped lang="scss">
+.knowledge-file {
+  padding: 20px;
+
+  .file-list {
+    .card-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+    }
+
+    .search-form {
+      margin-bottom: 20px;
+      .el-select{
+        width: 120px;
+      }
+    }
+
+    .pagination-container {
+      margin-top: 20px;
+      display: flex;
+      justify-content: flex-end;
+    }
+  }
+}
+</style>

+ 179 - 0
src/views/knowledgeList.vue

@@ -0,0 +1,179 @@
+<!--
+ * @Description: 
+ * @Author: gcz
+ * @Date: 2025-03-14 11:46:47
+ * @LastEditors: gcz
+ * @LastEditTime: 2025-03-14 14:08:58
+ * @FilePath: \knowledge_userui\src\views\knowledgeList.vue
+ * @Copyright: Copyright (c) 2016~2025 by gcz, All Rights Reserved. 
+-->
+
+<template>
+  <div class="knowledge-list">
+    <div class="search-bar">
+      <el-input
+        v-model="searchForm.name"
+        placeholder="请输入知识库名称"
+        class="search-input"
+        @keyup.enter="handleSearch"
+      >
+        <template #append>
+          <el-button @click="handleSearch">
+            <el-icon><Search /></el-icon>
+          </el-button>
+        </template>
+      </el-input>
+    </div>
+
+    <div class="list-content">
+      <el-card v-for="item in knowledgeList" 
+               :key="item.id" 
+               class="list-item"
+               @click="handleItemClick(item)">
+        <div class="item-title">{{ item.name }}</div>
+        <div class="item-desc">{{ item.knowSnapshot }}</div>
+      </el-card>
+    </div>
+
+    <div class="pagination">
+      <el-pagination
+        v-model:current-page="page.current"
+        v-model:page-size="page.size"
+        :total="page.total"
+        :page-sizes="[10, 20, 30, 50]"
+        layout="total, sizes, prev, pager, next"
+        @size-change="handleSizeChange"
+        @current-change="handleCurrentChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, reactive } from 'vue'
+import { useRouter } from 'vue-router'
+import { Search } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { post,get } from '../utils/request'
+
+const router = useRouter()
+
+const knowledgeList = ref([])
+
+const searchForm = reactive({
+  name: ''
+})
+
+const page = reactive({
+  current: 1,
+  size: 10,
+  total: 0
+})
+
+// 获取知识库列表
+const getKnowledgeList = async () => {
+  try {
+    const params = {
+      name: searchForm.name,
+      pageNum: page.current,
+      pageSize: page.size
+    }
+    const res = await get('/admin/knowInfo/getList', params)
+    console.log('res', res);
+    if (res.code === 0) {
+      knowledgeList.value = res.data.rows
+      page.total = Number(res.data.total)
+    } else {
+      ElMessage.error(res.msg || '获取列表失败')
+    }
+  } catch (error) {
+    console.log('error', error);
+    console.error('获取知识库列表失败:', error)
+  }
+}
+
+// 搜索
+const handleSearch = () => {
+  page.current = 1
+  getKnowledgeList()
+}
+
+// 页码改变
+const handleCurrentChange = (val) => {
+  page.current = val
+  getKnowledgeList()
+}
+
+// 每页条数改变
+const handleSizeChange = (val) => {
+  page.size = val
+  page.current = 1
+  getKnowledgeList()
+}
+
+// 点击列表项
+const handleItemClick = (item) => {
+    console.log('item', item);
+  router.push({
+    name: 'KnowledgeFile',
+    query: {
+        konwInfoId: item.id
+    }
+  })
+}
+
+onMounted(() => {
+  getKnowledgeList()
+})
+</script>
+
+<style scoped lang="scss">
+.knowledge-list {
+  padding: 20px;
+
+  .search-bar {
+    margin-bottom: 20px;
+    .search-input {
+      width: 300px;
+    }
+  }
+
+  .list-content {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+    gap: 20px;
+    margin-bottom: 20px;
+
+    .list-item {
+      cursor: pointer;
+      transition: all 0.3s;
+      background-image: url('@/assets/knowledgeBg.png');
+      background-size: 100% 100%;
+      background-repeat: no-repeat;
+      background-position: center;
+      border-radius: 10px;
+
+      &:hover {
+        transform: translateY(-5px);
+        box-shadow: 0 2px 12px 0 rgba(0,0,0,0.1);
+      }
+
+      .item-title {
+        font-size: 16px;
+        font-weight: bold;
+        margin-bottom: 10px;
+      }
+
+      .item-desc {
+        font-size: 14px;
+        color: #666;
+      }
+    }
+  }
+
+  .pagination {
+    display: flex;
+    justify-content: flex-end;
+  }
+}
+</style>