From 3ca2c89288db1d6cba514ced02c91624ef5fe497 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 09 六月 2026 16:15:56 +0800
Subject: [PATCH] feat(collaborativeApproval): 添加知识库RAG向量检索问答功能

---
 src/views/collaborativeApproval/knowledgeBase/index.vue |  525 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 521 insertions(+), 4 deletions(-)

diff --git a/src/views/collaborativeApproval/knowledgeBase/index.vue b/src/views/collaborativeApproval/knowledgeBase/index.vue
index 43fee33..8d625cf 100644
--- a/src/views/collaborativeApproval/knowledgeBase/index.vue
+++ b/src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -225,18 +225,141 @@
         </div>
       </div>
     </FormDialog>
+
+    <!-- 鏂囦欢绠$悊寮圭獥 -->
+    <FormDialog
+      v-model="filesDialogVisible"
+      title="鏂囦欢绠$悊"
+      :width="'900px'"
+      @close="closeFilesDialog"
+      @confirm="closeFilesDialog"
+      @cancel="closeFilesDialog"
+    >
+      <div class="file-manager">
+        <!-- 鏂囦欢涓婁紶 -->
+        <div class="upload-section">
+          <el-upload
+            :action="uploadUrl"
+            :headers="uploadHeaders"
+            :on-success="handleUploadSuccess"
+            :on-error="handleUploadError"
+            :before-upload="beforeUpload"
+            multiple
+            :show-file-list="false"
+            accept=".txt,.md,.docx,.xlsx,.xls,.pdf"
+          >
+            <el-button type="primary">涓婁紶鏂囦欢</el-button>
+          </el-upload>
+          <el-button
+            type="success"
+            @click="saveFiles"
+            :disabled="uploadedBlobIds.length === 0"
+            :loading="savingFiles"
+            style="margin-left: 10px"
+          >
+            淇濆瓨鏂囦欢鍏宠仈
+          </el-button>
+        </div>
+
+        <!-- 鏂囦欢鍒楄〃涓庡悜閲忓寲鐘舵�� -->
+        <el-table :data="fileList" style="margin-top: 20px" border>
+          <el-table-column prop="fileName" label="鏂囦欢鍚�" show-overflow-tooltip />
+          <el-table-column prop="fileType" label="鏂囦欢绫诲瀷" width="100" />
+          <el-table-column label="鍚戦噺鍖栫姸鎬�" width="120">
+            <template #default="{ row }">
+              <el-tag :type="getStatusType(row.vectorStatus)">
+                {{ getStatusText(row.vectorStatus) }}
+              </el-tag>
+            </template>
+          </el-table-column>
+          <el-table-column prop="chunkCount" label="鍒囩墖鏁�" width="100" align="center" />
+          <el-table-column prop="createTime" label="涓婁紶鏃堕棿" width="180" />
+          <el-table-column label="鎿嶄綔" width="150" align="center">
+            <template #default="{ row }">
+              <el-button
+                v-if="row.vectorStatus === 3"
+                type="text"
+                @click="reprocessFile(row)"
+              >
+                閲嶆柊澶勭悊
+              </el-button>
+              <el-button type="text" @click="deleteFile(row)" style="color: #f56c6c">
+                鍒犻櫎
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+    </FormDialog>
+
+    <!-- 鐭ヨ瘑搴撻棶绛斿脊绐� -->
+    <FormDialog
+      v-model="chatDialogVisible"
+      title="鐭ヨ瘑搴撻棶绛�"
+      :width="'800px'"
+      @close="closeChatDialog"
+      @confirm="closeChatDialog"
+      @cancel="closeChatDialog"
+    >
+      <div class="knowledge-chat">
+        <div class="chat-header">
+          <el-tag type="success">褰撳墠鐭ヨ瘑搴�: {{ currentKnowledgeBase?.title }}</el-tag>
+        </div>
+
+        <!-- 瀵硅瘽鍖哄煙 -->
+        <div class="chat-messages" ref="chatMessagesRef">
+          <div
+            v-for="(msg, index) in messages"
+            :key="index"
+            :class="['message', msg.role]"
+          >
+            <div class="message-role">{{ msg.role === 'user' ? '鎴�' : 'AI鍔╂墜' }}</div>
+            <div class="message-content">{{ msg.content }}</div>
+          </div>
+          <div v-if="chatLoading" class="message assistant">
+            <div class="message-role">AI鍔╂墜</div>
+            <div class="message-content typing">姝e湪鎬濊�冧腑...</div>
+          </div>
+        </div>
+
+        <!-- 杈撳叆妗� -->
+        <div class="chat-input">
+          <el-input
+            v-model="inputQuestion"
+            placeholder="璇疯緭鍏ラ棶棰橈紝鎸夊洖杞﹀彂閫�"
+            @keyup.enter="sendMessage"
+            :disabled="chatLoading"
+          >
+            <template #append>
+              <el-button @click="sendMessage" :loading="chatLoading">鍙戦��</el-button>
+            </template>
+          </el-input>
+        </div>
+      </div>
+    </FormDialog>
   </div>
 </template>
 
 <script setup>
 import { Search } from "@element-plus/icons-vue";
-import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch } from "vue";
+import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch, nextTick } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import PIMTable from "@/components/PIMTable/PIMTable.vue";
 import FormDialog from '@/components/Dialog/FormDialog.vue';
-import { listKnowledgeBase, delKnowledgeBase,addKnowledgeBase,updateKnowledgeBase } from "@/api/collaborativeApproval/knowledgeBase.js";
+import {
+  listKnowledgeBase,
+  delKnowledgeBase,
+  addKnowledgeBase,
+  updateKnowledgeBase,
+  getVectorStatus,
+  reprocessVector,
+  saveKnowledgeBaseFiles,
+  deleteKnowledgeBaseFiles,
+  knowledgeChat
+} from "@/api/collaborativeApproval/knowledgeBase.js";
 import useUserStore from '@/store/modules/user';
 import { userListNoPageByTenantId } from '@/api/system/user.js';
+import { getToken } from "@/utils/auth";
 
 // 琛ㄥ崟楠岃瘉瑙勫垯
 const rules = {
@@ -283,7 +406,17 @@
   dialogTitle: "",
   dialogType: "add",
   viewDialogVisible: false,
-  currentKnowledge: {}
+  currentKnowledge: {},
+  filesDialogVisible: false,
+  currentKnowledgeBase: null,
+  fileList: [],
+  uploadedBlobIds: [],
+  savingFiles: false,
+  chatDialogVisible: false,
+  messages: [],
+  inputQuestion: "",
+  chatLoading: false,
+  memoryId: ""
 });
 
 const {
@@ -297,7 +430,17 @@
   dialogTitle,
   dialogType,
   viewDialogVisible,
-  currentKnowledge
+  currentKnowledge,
+  filesDialogVisible,
+  currentKnowledgeBase,
+  fileList,
+  uploadedBlobIds,
+  savingFiles,
+  chatDialogVisible,
+  messages,
+  inputQuestion,
+  chatLoading,
+  memoryId
 } = toRefs(data);
 
 // 琛ㄥ崟寮曠敤
@@ -305,6 +448,12 @@
 // 鐢ㄦ埛鐩稿叧
 const userStore = useUserStore();
 const userList = ref([]);
+// 鑱婂ぉ娑堟伅瀹瑰櫒寮曠敤
+const chatMessagesRef = ref();
+
+// 鏂囦欢涓婁紶鐩稿叧
+const uploadUrl = import.meta.env.VITE_APP_BASE_API + "/common/upload";
+const uploadHeaders = { Authorization: "Bearer " + getToken() };
 
 // 琛ㄦ牸鍒楅厤缃�
 const tableColumn = ref([
@@ -352,6 +501,18 @@
     }
   },
   {
+    label: "鏂囦欢鏁伴噺",
+    prop: "fileCount",
+    width: 100,
+    align: "center"
+  },
+  {
+    label: "鍒囩墖鏁伴噺",
+    prop: "totalChunkCount",
+    width: 100,
+    align: "center"
+  },
+  {
     label: "浣跨敤娆℃暟",
     prop: "usageCount",
     width: 100,
@@ -379,6 +540,20 @@
         type: "text",
         clickFun: (row) => {
           openForm("edit", row);
+        }
+      },
+      {
+        name: "鏂囦欢",
+        type: "text",
+        clickFun: (row) => {
+          openFilesDialog(row);
+        }
+      },
+      {
+        name: "闂瓟",
+        type: "text",
+        clickFun: (row) => {
+          openChatDialog(row);
         }
       },
       {
@@ -680,6 +855,260 @@
 const handleExport = () => {
   proxy.download('/knowledgeBase/export', { ...searchForm.value }, '鐭ヨ瘑搴�.xlsx')
 }
+
+// ============ 鏂囦欢绠$悊鐩稿叧 ============
+
+// 鎵撳紑鏂囦欢绠$悊寮圭獥
+const openFilesDialog = (row) => {
+  currentKnowledgeBase.value = row;
+  filesDialogVisible.value = true;
+  loadFileList();
+};
+
+// 鍔犺浇鏂囦欢鍒楄〃
+const loadFileList = async () => {
+  if (!currentKnowledgeBase.value?.id) return;
+
+  try {
+    const res = await getVectorStatus(currentKnowledgeBase.value.id);
+    fileList.value = res.data || [];
+  } catch (error) {
+    console.error("鍔犺浇鏂囦欢鍒楄〃澶辫触:", error);
+    ElMessage.error("鍔犺浇鏂囦欢鍒楄〃澶辫触");
+  }
+};
+
+// 涓婁紶鍓嶆牎楠�
+const beforeUpload = (file) => {
+  const allowedTypes = ['.txt', '.md', '.docx', '.xlsx', '.xls', '.pdf'];
+  const fileName = file.name.toLowerCase();
+  const isAllowed = allowedTypes.some(type => fileName.endsWith(type));
+
+  if (!isAllowed) {
+    ElMessage.error('鍙敮鎸� txt銆乵d銆乨ocx銆亁lsx銆亁ls銆乸df 鏍煎紡鐨勬枃浠�');
+    return false;
+  }
+
+  const isLt50M = file.size / 1024 / 1024 < 50;
+  if (!isLt50M) {
+    ElMessage.error('鏂囦欢澶у皬涓嶈兘瓒呰繃 50MB');
+    return false;
+  }
+
+  return true;
+};
+
+// 涓婁紶鎴愬姛
+const handleUploadSuccess = (response, file) => {
+  if (response.code === 200) {
+    uploadedBlobIds.value.push(response.data.id);
+    ElMessage.success(`鏂囦欢 ${file.name} 涓婁紶鎴愬姛`);
+  } else {
+    ElMessage.error(response.msg || "涓婁紶澶辫触");
+  }
+};
+
+// 涓婁紶澶辫触
+const handleUploadError = (error, file) => {
+  ElMessage.error(`鏂囦欢 ${file.name} 涓婁紶澶辫触`);
+};
+
+// 淇濆瓨鏂囦欢鍏宠仈
+const saveFiles = async () => {
+  if (uploadedBlobIds.value.length === 0) {
+    ElMessage.warning("璇峰厛涓婁紶鏂囦欢");
+    return;
+  }
+
+  savingFiles.value = true;
+  try {
+    await saveKnowledgeBaseFiles({
+      knowledgeBaseId: currentKnowledgeBase.value.id,
+      storageBlobIds: uploadedBlobIds.value
+    });
+
+    ElMessage.success("鏂囦欢鍏宠仈淇濆瓨鎴愬姛锛屾鍦ㄥ悗鍙板鐞嗗悜閲忓寲");
+    uploadedBlobIds.value = [];
+
+    // 寤惰繜鍒锋柊鏂囦欢鍒楄〃锛岀粰鍚庡彴澶勭悊鏃堕棿
+    setTimeout(() => {
+      loadFileList();
+    }, 1000);
+  } catch (error) {
+    console.error("淇濆瓨鏂囦欢鍏宠仈澶辫触:", error);
+    ElMessage.error("淇濆瓨鏂囦欢鍏宠仈澶辫触");
+  } finally {
+    savingFiles.value = false;
+  }
+};
+
+// 閲嶆柊澶勭悊鍚戦噺鍖栫殑鏂囦欢
+const reprocessFile = async (row) => {
+  try {
+    await reprocessVector(row.id);
+    ElMessage.success("宸查噸鏂版彁浜ゅ悜閲忓寲浠诲姟");
+    // 寤惰繜鍒锋柊
+    setTimeout(() => {
+      loadFileList();
+    }, 1000);
+  } catch (error) {
+    console.error("閲嶆柊澶勭悊澶辫触:", error);
+    ElMessage.error("閲嶆柊澶勭悊澶辫触");
+  }
+};
+
+// 鍒犻櫎鏂囦欢
+const deleteFile = async (row) => {
+  try {
+    await ElMessageBox.confirm(
+      "纭畾瑕佸垹闄よ鏂囦欢鍚楋紵鍒犻櫎鍚庡皢鏃犳硶鎭㈠鍚戦噺鏁版嵁",
+      "鍒犻櫎纭",
+      {
+        confirmButtonText: "纭畾",
+        cancelButtonText: "鍙栨秷",
+        type: "warning"
+      }
+    );
+
+    await deleteKnowledgeBaseFiles([row.id]);
+    ElMessage.success("鍒犻櫎鎴愬姛");
+    loadFileList();
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error("鍒犻櫎鏂囦欢澶辫触:", error);
+      ElMessage.error("鍒犻櫎鏂囦欢澶辫触");
+    }
+  }
+};
+
+// 鐘舵�佹枃鏈槧灏�
+const getStatusText = (status) => {
+  const map = {
+    0: '寰呭鐞�',
+    1: '澶勭悊涓�',
+    2: '宸插畬鎴�',
+    3: '澶辫触'
+  };
+  return map[status] || '鏈煡';
+};
+
+// 鐘舵�佹爣绛剧被鍨嬫槧灏�
+const getStatusType = (status) => {
+  const map = {
+    0: 'info',
+    1: 'warning',
+    2: 'success',
+    3: 'danger'
+  };
+  return map[status] || 'info';
+};
+
+// 鍏抽棴鏂囦欢绠$悊寮圭獥
+const closeFilesDialog = () => {
+  filesDialogVisible.value = false;
+  currentKnowledgeBase.value = null;
+  fileList.value = [];
+  uploadedBlobIds.value = [];
+  getList(); // 鍒锋柊涓诲垪琛紝鏇存柊鏂囦欢鏁伴噺
+};
+
+// ============ 鐭ヨ瘑搴撻棶绛旂浉鍏� ============
+
+// 鎵撳紑闂瓟寮圭獥
+const openChatDialog = (row) => {
+  currentKnowledgeBase.value = row;
+  chatDialogVisible.value = true;
+  memoryId.value = crypto.randomUUID();
+  messages.value = [];
+  inputQuestion.value = "";
+};
+
+// 鍙戦�佹秷鎭�
+const sendMessage = async () => {
+  if (!inputQuestion.value.trim()) {
+    ElMessage.warning("璇疯緭鍏ラ棶棰�");
+    return;
+  }
+
+  const question = inputQuestion.value.trim();
+
+  // 娣诲姞鐢ㄦ埛娑堟伅
+  messages.value.push({
+    role: 'user',
+    content: question
+  });
+
+  inputQuestion.value = "";
+  chatLoading.value = true;
+
+  // 婊氬姩鍒板簳閮�
+  await nextTick();
+  scrollToBottom();
+
+  try {
+    // 娴佸紡璇锋眰
+    const response = await fetch('/api/ai/knowledge/chat', {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        'Authorization': 'Bearer ' + getToken()
+      },
+      body: JSON.stringify({
+        knowledgeBaseId: currentKnowledgeBase.value.id,
+        memoryId: memoryId.value,
+        question: question
+      })
+    });
+
+    if (!response.ok) {
+      throw new Error('璇锋眰澶辫触');
+    }
+
+    // 澶勭悊SSE娴佸紡鍝嶅簲
+    const reader = response.body.getReader();
+    const decoder = new TextDecoder();
+    let aiContent = '';
+
+    messages.value.push({ role: 'assistant', content: '' });
+
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done) break;
+
+      const text = decoder.decode(value);
+      aiContent += text;
+      messages.value[messages.value.length - 1].content = aiContent;
+
+      // 婊氬姩鍒板簳閮�
+      await nextTick();
+      scrollToBottom();
+    }
+  } catch (error) {
+    console.error("闂瓟璇锋眰澶辫触:", error);
+    ElMessage.error("闂瓟璇锋眰澶辫触锛岃绋嶅悗閲嶈瘯");
+    messages.value.push({
+      role: 'assistant',
+      content: '鎶辨瓑锛屽彂鐢熶簡閿欒锛岃绋嶅悗閲嶈瘯'
+    });
+  } finally {
+    chatLoading.value = false;
+  }
+};
+
+// 婊氬姩鍒板簳閮�
+const scrollToBottom = () => {
+  if (chatMessagesRef.value) {
+    chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight;
+  }
+};
+
+// 鍏抽棴闂瓟寮圭獥
+const closeChatDialog = () => {
+  chatDialogVisible.value = false;
+  currentKnowledgeBase.value = null;
+  messages.value = [];
+  inputQuestion.value = "";
+};
 </script>
 
 <style scoped>
@@ -755,4 +1184,92 @@
   font-size: 14px;
   color: #909399;
 }
+
+/* 鏂囦欢绠$悊鏍峰紡 */
+.file-manager {
+  padding: 20px 0;
+}
+
+.upload-section {
+  display: flex;
+  align-items: center;
+}
+
+/* 鐭ヨ瘑搴撻棶绛旀牱寮� */
+.knowledge-chat {
+  display: flex;
+  flex-direction: column;
+  height: 500px;
+}
+
+.chat-header {
+  margin-bottom: 16px;
+}
+
+.chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+  background: #f5f7fa;
+  border-radius: 8px;
+  margin-bottom: 16px;
+}
+
+.message {
+  margin-bottom: 16px;
+  max-width: 80%;
+}
+
+.message.user {
+  margin-left: auto;
+  text-align: right;
+}
+
+.message.assistant {
+  margin-right: auto;
+}
+
+.message-role {
+  font-size: 12px;
+  color: #909399;
+  margin-bottom: 4px;
+}
+
+.message-content {
+  display: inline-block;
+  padding: 10px 14px;
+  border-radius: 8px;
+  line-height: 1.6;
+  word-wrap: break-word;
+  white-space: pre-wrap;
+}
+
+.message.user .message-content {
+  background: #409eff;
+  color: white;
+}
+
+.message.assistant .message-content {
+  background: white;
+  color: #303133;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.typing {
+  animation: typing 1.5s infinite;
+}
+
+@keyframes typing {
+  0%, 50%, 100% {
+    opacity: 1;
+  }
+  25%, 75% {
+    opacity: 0.5;
+  }
+}
+
+.chat-input {
+  margin-top: auto;
+}
+
 </style>

--
Gitblit v1.9.3