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