| doc/20260608_知识库RAG向量检索功能前端联调文档.md | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/api/collaborativeApproval/knowledgeBase.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/collaborativeApproval/knowledgeBase/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/views/collaborativeApproval/notificationManagement/summary/index.vue | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
doc/20260608_֪ʶ¿âRAGÏòÁ¿¼ìË÷¹¦ÄÜǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,598 @@ # ç¥è¯åºæä»¶ä¸ä¼ ä¸RAGåéæ£ç´¢åè½å端èè°ææ¡£ ## ä¸ãåè½æ¦è¿° ç¥è¯åºæ¨¡åæ°å¢æä»¶ä¸ä¼ åè½ï¼ä¸ä¼ çæä»¶ä¼å®æ¶è¿å ¥åéåºè¿è¡åçå¤çï¼é åAI模åå®ç°RAGï¼æ£ç´¢å¢å¼ºçæï¼é®çåè½ã ### åè½æ¨¡å 1. **ç¥è¯åºæä»¶ç®¡ç**ï¼ä½¿ç¨ç³»ç»å·²æçéä»¶ç®¡çæºå¶ä¸ä¼ æä»¶ 2. **åéæ£ç´¢å¤ç**ï¼æä»¶å 容èªå¨åçå¹¶åå ¥åéåºï¼Pineconeï¼ 3. **ç¥è¯åºé®ç**ï¼åºäºä¸ä¼ æä»¶å 容è¿è¡æºè½é®ç ## äºãæ°æ®åºåæ´ ### 2.1 æ°å¢ç¥è¯åºæä»¶åéè®°å½è¡¨ ```sql -- ç¥è¯åºæä»¶åéè®°å½è¡¨ï¼ç¨äºè·è¸ªåéåç¶æï¼ CREATE TABLE IF NOT EXISTS knowledge_base_vector ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主é®ID', knowledge_base_id BIGINT NOT NULL COMMENT 'å ³èç¥è¯åºID', storage_blob_id BIGINT NOT NULL COMMENT 'å ³èæä»¶blob ID', file_name VARCHAR(255) NOT NULL COMMENT 'æä»¶åç§°', file_type VARCHAR(50) NOT NULL COMMENT 'æä»¶ç±»å(docx/pdf/xlsx/txtç)', vector_status TINYINT DEFAULT 0 COMMENT 'åéåç¶æ: 0-å¾ å¤ç, 1-å¤çä¸, 2-已宿, 3-失败', vector_error VARCHAR(500) COMMENT 'åéå失败åå ', chunk_count INT DEFAULT 0 COMMENT 'åçæ°é', namespace VARCHAR(100) COMMENT 'åéå½å空é´', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 'å建æ¶é´', create_user INT COMMENT 'å建人', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'æ´æ°æ¶é´', update_user INT COMMENT 'æ´æ°äºº', tenant_id BIGINT COMMENT 'ç§æ·ID', dept_id BIGINT COMMENT 'é¨é¨ID', INDEX idx_knowledge_base_id (knowledge_base_id), INDEX idx_storage_blob_id (storage_blob_id), INDEX idx_vector_status (vector_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ç¥è¯åºæä»¶åéè®°å½è¡¨'; ``` ### 2.2 ä¿®æ¹ç¥è¯åºè¡¨ ```sql -- ç¥è¯åºè¡¨å¢å åæ®µ ALTER TABLE knowledge_base ADD COLUMN file_count INT DEFAULT 0 COMMENT 'æä»¶æ°é', ADD COLUMN total_chunk_count INT DEFAULT 0 COMMENT 'æ»åçæ°é', ADD COLUMN description VARCHAR(500) COMMENT 'ç¥è¯åºæè¿°'; ``` ## ä¸ãæ¥å£è®¾è®¡ ### 3.1 æä»¶ä¸ä¼ ï¼ä½¿ç¨ç³»ç»å·²ææ¥å£ï¼ **æ¥å£å°å**ï¼`POST /common/upload` **è¯·æ±æ¹å¼**ï¼multipart/form-data **请æ±åæ°**ï¼ | åæ°å | ç±»å | å¿ å¡« | 说æ | |--------|------|------|------| | files | MultipartFile[] | æ¯ | ä¸ä¼ çæä»¶å表 | **ååºç»æ**ï¼ ```json { "code": 200, "msg": "æä½æå", "data": [ { "id": 123, "name": "æä½æå.docx", "url": "/profile/upload/20260608/xxx.docx", "previewURL": "/common/preview/xxx?token=yyy", "downloadURL": "/common/download/xxx?token=yyy", "storageAttachmentId": null } ] } ``` ### 3.2 ä¿åç¥è¯åºæä»¶å ³èï¼è§¦ååéåï¼ **éè¦**ï¼ä¸ä¼ 宿åï¼**å¿ é¡»**è°ç¨æ¤æ¥å£æ¥å ³èæä»¶å¹¶è§¦ååéåå¤çã **æ¥å£å°å**ï¼`POST /knowledgeBase/file/save` **请æ±åæ°**ï¼ ```json { "knowledgeBaseId": 10, "storageBlobIds": [123, 124] } ``` | åæ°å | ç±»å | å¿ å¡« | 说æ | |--------|------|------|------| | knowledgeBaseId | Long | æ¯ | ç¥è¯åºID | | storageBlobIds | List<Long> | æ¯ | ä¸ä¼ è¿åçæä»¶blob IDå表 | **ååºç»æ**ï¼ ```json { "code": 200, "msg": "æä½æå" } ``` **è°ç¨æ¶æº**ï¼å¨ `/common/upload` ä¸ä¼ æååï¼ç«å³è°ç¨æ¤æ¥å£ã **注æ**ï¼æ¤æ¥å£ä¼ï¼ 1. ä¿åéä»¶å ³èå° `storage_attachment` 表 2. å建åéè®°å½å° `knowledge_base_vector` 表 3. **弿¥è§¦ååéåå¤ç**ï¼æä»¶åç â åéåµå ¥ â åå ¥Pineconeï¼ --- ### 3.3 æ¥è¯¢åéåç¶æï¼æ¨è使ç¨ï¼ **æ¥å£å°å**ï¼`GET /knowledgeBase/vector/status/{knowledgeBaseId}` **ååºç»æ**ï¼ ```json { "code": 200, "data": [ { "id": 1, "storageBlobId": 123, "fileName": "æä½æå.docx", "fileType": "docx", "vectorStatus": 2, "chunkCount": 15, "namespace": "kb-10", "vectorError": null } ] } ``` ### 3.4 å é¤ç¥è¯åºæä»¶ **æ¥å£å°å**ï¼`DELETE /knowledgeBase/file/delete` **请æ±åæ°**ï¼ ```json { "ids": [1, 2, 3] } ``` **注æ**ï¼æ¤æ¥å£ä¼åæ¶å é¤åéåºä¸çç¸å ³æ°æ®ã **ååºç»æ**ï¼ ```json { "code": 200, "msg": "æä½æå" } ``` ### 3.5 éæ°åéåæä»¶ **æ¥å£å°å**ï¼`POST /knowledgeBase/vector/reprocess/{vectorId}` **ååºç»æ**ï¼ ```json { "code": 200, "msg": "已鿰æäº¤åéåä»»å¡" } ``` ### 3.6 ç¥è¯åºé®çæ¥å£ **æ¥å£å°å**ï¼`POST /ai/knowledge/chat`ï¼æµå¼è¿åï¼ **请æ±åæ°**ï¼ ```json { "knowledgeBaseId": 10, "memoryId": "session-xxx", "question": "å¦ä½æä½å®¡æ¹æµç¨ï¼" } ``` | åæ°å | ç±»å | å¿ å¡« | 说æ | |--------|------|------|------| | knowledgeBaseId | Long | æ¯ | ç¥è¯åºID | | memoryId | String | æ¯ | ä¼è¯IDï¼ç¨äºä¿æä¸ä¸æ | | question | String | æ¯ | ç¨æ·æé®å 容 | **ååºç»æ**ï¼æµå¼è¿å `text/stream;charset=utf-8`ï¼ï¼ ``` æ ¹æ®ç¥è¯åºå 容ï¼å®¡æ¹æµç¨çæä½æ¥éª¤å¦ä¸ï¼ 1. ç»å½ç³»ç»åè¿å ¥å®¡æ¹ç®¡ç模å... ``` ### 3.7 ç¥è¯åºé®çä¼è¯åå² **æ¥å£å°å**ï¼`GET /ai/knowledge/history/{memoryId}` **ååºç»æ**ï¼ ```json { "code": 200, "data": [ { "role": "user", "content": "å¦ä½æä½å®¡æ¹æµç¨ï¼", "createTime": "2026-06-08 10:00:00" }, { "role": "assistant", "content": "æ ¹æ®ç¥è¯åºå 容...", "createTime": "2026-06-08 10:01:00" } ] } ``` ## åãæä»¶ç±»åæ¯æ | æä»¶ç±»å | æ©å±å | 说æ | |----------|--------|------| | Wordææ¡£ | .docx | æ¯æææ¬æå | | Excelè¡¨æ ¼ | .xlsx, .xls | æ¯æè¡¨æ ¼å 容æå | | PDFææ¡£ | .pdf | æ¯æPDFææ¬æå | | ææ¬æä»¶ | .txt, .md, .json, .csv | ç´æ¥è¯»åå 容 | | ä»£ç æä»¶ | .java, .js, .vue, .html, .sqlç | ç´æ¥è¯»åå 容 | **æä»¶å¤§å°éå¶**ï¼åæä»¶æå¤§10MBï¼ç³»ç»é»è®¤éå¶ï¼ ## äºãåéåç¶æè¯´æ | ç¶æå¼ | ç¶æåç§° | 说æ | |--------|----------|------| | 0 | å¾ å¤ç | æä»¶å·²ä¸ä¼ ï¼çå¾ åéåå¤ç | | 1 | å¤çä¸ | æ£å¨è¿è¡åéåçå¤ç | | 2 | 已宿 | åéå宿ï¼å¯è¿è¡æ£ç´¢é®ç | | 3 | 失败 | åéå失败ï¼ééæ°å¤ç | ## å ãå端ç»ä»¶è®¾è®¡ ### 6.1 æä»¶ä¸ä¼ ç»ä»¶ï¼ä½¿ç¨ç³»ç»å·²æä¸ä¼ ï¼ ```vue <template> <div class="knowledge-file-upload"> <!-- ç¥è¯åºéæ© --> <el-select v-model="selectedKnowledgeBase" placeholder="éæ©ç¥è¯åº" style="width: 200px"> <el-option v-for="kb in knowledgeBaseList" :key="kb.id" :label="kb.title" :value="kb.id" /> </el-select> <!-- 使ç¨ç³»ç»å·²æä¸ä¼ ç»ä»¶ --> <el-upload :action="uploadUrl" :headers="uploadHeaders" :on-success="handleUploadSuccess" :before-upload="beforeUpload" :accept="acceptTypes" :limit="10" :file-list="fileList" multiple > <el-button type="primary">ç¹å»ä¸ä¼ </el-button> <template #tip> <div class="el-upload__tip"> æ¯æ docxãxlsxãpdfãtxt çæ ¼å¼ï¼åæä»¶ä¸è¶ è¿10MB </div> </template> </el-upload> </div> </template> <script setup> import { ref } from 'vue' import { getToken } from '@/utils/auth' import request from '@/utils/request' const uploadUrl = '/common/upload' const uploadHeaders = { Authorization: 'Bearer ' + getToken() } const acceptTypes = '.docx,.xlsx,.xls,.pdf,.txt,.md,.json,.csv' const uploadedBlobs = ref([]) const beforeUpload = (file) => { const maxSize = 10 * 1024 * 1024 if (file.size > maxSize) { ElMessage.error('æä»¶å¤§å°ä¸è½è¶ è¿10MB') return false } return true } // ä¸ä¼ æååä¿åæä»¶å ³è并触ååéå const handleUploadSuccess = async (response, file) => { if (response.code === 200) { uploadedBlobs.value.push(...response.data) // è°ç¨ç¥è¯åºæä»¶ä¿åæ¥å£ï¼å ³èæä»¶å¹¶è§¦ååéå await saveKnowledgeBaseFiles() ElMessage.success('æä»¶ä¸ä¼ æåï¼æ£å¨å¤çåéå...') refreshVectorStatus() } else { ElMessage.error(response.msg) } } // ä¿åæä»¶å ³èå°ç¥è¯åºå¹¶è§¦ååéå const saveKnowledgeBaseFiles = async () => { await request.post('/knowledgeBase/file/save', { knowledgeBaseId: selectedKnowledgeBase.value, storageBlobIds: uploadedBlobs.value.map(b => b.id) }) uploadedBlobs.value = [] } </script> ``` ### 6.2 æä»¶å表ç»ä»¶ ```vue <template> <el-table :data="fileList" v-loading="loading"> <el-table-column prop="name" label="æä»¶å" width="200" /> <el-table-column prop="fileType" label="ç±»å" width="80"> <template #default="{ row }"> {{ getFileType(row.name) }} </template> </el-table-column> <el-table-column prop="vectorStatus" label="åéåç¶æ" width="120"> <template #default="{ row }"> <el-tag :type="getVectorStatusType(row.vectorStatus)"> {{ getVectorStatusText(row.vectorStatus) }} </el-tag> </template> </el-table-column> <el-table-column prop="chunkCount" label="åçæ°" width="80" /> <el-table-column prop="createTime" label="ä¸ä¼ æ¶é´" width="160" /> <el-table-column label="æä½" width="180"> <template #default="{ row }"> <el-button type="primary" size="small" link @click="previewFile(row)">é¢è§</el-button> <el-button type="primary" size="small" link @click="downloadFile(row)">ä¸è½½</el-button> <el-button v-if="row.vectorStatus === 3" type="warning" size="small" link @click="revectorFile(row)" >éæ°å¤ç</el-button> <el-button type="danger" size="small" link @click="deleteFile(row)">å é¤</el-button> </template> </el-table-column> </el-table> </template> <script setup> import request from '@/utils/request' const getFileType = (name) => { return name?.split('.').pop() || '' } const getVectorStatusText = (status) => { const statusMap = { 0: 'å¾ å¤ç', 1: 'å¤çä¸', 2: '已宿', 3: '失败' } return statusMap[status] || 'æªç¥' } const getVectorStatusType = (status) => { const typeMap = { 0: 'info', 1: 'warning', 2: 'success', 3: 'danger' } return typeMap[status] || 'info' } const previewFile = (row) => { window.open(row.previewURL, '_blank') } const downloadFile = (row) => { window.open(row.downloadURL, '_blank') } const deleteFile = async (row) => { await request.delete('/knowledgeBase/file/delete', { data: [row.id] }) ElMessage.success('å 餿å') refreshFileList() } const revectorFile = async (row) => { await request.post(`/knowledgeBase/vector/reprocess/${row.vectorId}`) ElMessage.success('已鿰æäº¤åéåä»»å¡') refreshVectorStatus() } </script> ``` ### 6.3 ç¥è¯åºé®çç»ä»¶ ```vue <template> <div class="knowledge-chat"> <!-- ç¥è¯åºéæ© --> <div class="kb-selector"> <el-select v-model="selectedKnowledgeBase" placeholder="éæ©ç¥è¯åº"> <el-option v-for="kb in knowledgeBaseList" :key="kb.id" :label="kb.title" :value="kb.id" /> </el-select> </div> <!-- è天åºå --> <div class="chat-container"> <div class="message-list" ref="messageList"> <div v-for="msg in messages" :key="msg.id" :class="['message', msg.role]"> <div class="message-content">{{ msg.content }}</div> </div> <div v-if="streamingContent" class="message assistant"> <div class="message-content">{{ streamingContent }}</div> </div> </div> <!-- è¾å ¥åºå --> <div class="input-area"> <el-input v-model="question" placeholder="è¾å ¥é®é¢ï¼åºäºç¥è¯åºå 容åç..." @keyup.enter="sendQuestion" /> <el-button type="primary" @click="sendQuestion" :loading="sending">åé</el-button> </div> </div> </div> </template> <script setup> import { ref, nextTick } from 'vue' import { getToken } from '@/utils/auth' const selectedKnowledgeBase = ref(null) const question = ref('') const messages = ref([]) const sending = ref(false) const streamingContent = ref('') const memoryId = ref('kb-chat-' + Date.now()) const sendQuestion = async () => { if (!question.value.trim() || !selectedKnowledgeBase.value) return sending.value = true streamingContent.value = '' messages.value.push({ id: Date.now(), role: 'user', content: question.value }) try { const response = await fetch('/ai/knowledge/chat', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken() }, body: JSON.stringify({ knowledgeBaseId: selectedKnowledgeBase.value, memoryId: memoryId.value, question: question.value }) }) // æµå¼è¯»åååº const reader = response.body.getReader() const decoder = new TextDecoder() while (true) { const { done, value } = await reader.read() if (done) break streamingContent.value += decoder.decode(value, { stream: true }) // æ»å¨å°åºé¨ nextTick(() => { const list = document.querySelector('.message-list') if (list) list.scrollTop = list.scrollHeight }) } messages.value.push({ id: Date.now(), role: 'assistant', content: streamingContent.value }) streamingContent.value = '' } catch (error) { ElMessage.error('é®ç请æ±å¤±è´¥') } finally { sending.value = false question.value = '' } } </script> <style scoped> .knowledge-chat { height: 100%; display: flex; flex-direction: column; } .kb-selector { padding: 10px; border-bottom: 1px solid #eee; } .chat-container { flex: 1; display: flex; flex-direction: column; } .message-list { flex: 1; overflow-y: auto; padding: 10px; } .message { margin-bottom: 10px; padding: 10px; border-radius: 8px; } .message.user { background: #e6f7ff; text-align: right; } .message.assistant { background: #f5f5f5; } .input-area { padding: 10px; display: flex; gap: 10px; } </style> ``` ## ä¸ãä¸å¡æµç¨ ### 7.1 æä»¶ä¸ä¼ æµç¨ï¼éè¦ï¼ ``` å端è°ç¨ /common/upload ä¸ä¼ æä»¶ â è·å StorageBlobVO å表ï¼å å«blobIdãé¢è§URLãä¸è½½URLï¼ â è°ç¨ /knowledgeBase/file/save å ³èæä»¶å°ç¥è¯åº â å端å建åéè®°å½ â 弿¥è§¦ååéåå¤ç â 弿¥ä»»å¡ï¼æåæä»¶ææ¬ â åç â è°ç¨Embedding模åçæåé â åå ¥Pinecone â æ´æ° knowledge_base_vector è¡¨ç¶æä¸ºå·²å®æ ``` **å ³é®ç¹**ï¼ - å¿ é¡»è°ç¨ `/knowledgeBase/file/save` æ¥å£æè½è§¦ååéå - åé忝弿¥å¤ççï¼ä¸ä¼é»å¡è¯·æ± - å端å¯éè¿è½®è¯¢ `/knowledgeBase/vector/status/{knowledgeBaseId}` æ¥çå¤çè¿åº¦ ### 7.2 ç¥è¯åºé®çæµç¨ ``` ç¨æ·æé® â è°ç¨ Embedding 模å对é®é¢åéå â å¨ Pinecone 䏿£ç´¢ï¼å½å空é´: kb-{knowledgeBaseId}) â è·åç¸å ³åçææ¬ â ä½ä¸ºä¸ä¸æ + ç¨æ·é®é¢åç» AI 模å â AI æµå¼çæåç â è¿åå端 ``` ## å «ãææ¯å®ç°è¦ç¹ ### 8.1 ææ¬åççç¥ - **åç大å°**ï¼é»è®¤æ¯ç 500 å符 - **éå 大å°**ï¼é»è®¤ 100 å符éå ï¼ä¿è¯è¯ä¹è¿è´¯ - **åçå æ°æ®**ï¼å 嫿件IDãç¥è¯åºIDãåçç´¢å¼ ### 8.2 åéå½åç©ºé´ æ¯ä¸ªç¥è¯åºä½¿ç¨ç¬ç«å½å空é´ï¼`kb-{knowledgeBaseId}` ## ä¹ã注æäºé¡¹ 1. **æä»¶ä¸ä¼ å¿ é¡»è°ç¨ `/knowledgeBase/file/save` 触ååéå** 2. å 餿件æ¶åæ¥å é¤åéåºä¸çç¸å ³åç 3. 大æä»¶åéåå¯è½èæ¶è¾é¿ï¼å端éè½®è¯¢ç¶æææ¾ç¤ºè¿åº¦ 4. ç¥è¯åºé®çä¾èµåéæ£ç´¢è´¨éï¼å»ºè®®ä¼ååççç¥ 5. ä¸åç¥è¯åºä½¿ç¨ä¸åå½å空é´ï¼é¿å æ°æ®æ··æ· ## åãé误ç 说æ | é误ç | 说æ | |--------|------| | 40001 | æä»¶ç±»å䏿¯æ | | 40002 | æä»¶å¤§å°è¶ åºéå¶ | | 40003 | ç¥è¯åºä¸åå¨ | | 50001 | æä»¶ä¸ä¼ 失败 | | 50002 | æä»¶å 容æå失败 | | 50003 | åéåå¤ç失败 | | 50004 | åéæ£ç´¢å¤±è´¥ | src/api/collaborativeApproval/knowledgeBase.js
@@ -54,7 +54,7 @@ }); } // æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æ // æ¥è¯¢ç¥è¯åºæä»¶åéåç¶æï¼å 嫿件åè¡¨ï¼ export function getVectorStatus(knowledgeBaseId) { return request({ url: `/knowledgeBase/vector/status/${knowledgeBaseId}`, @@ -62,6 +62,24 @@ }); } // ä¿åç¥è¯åºæä»¶å ³èï¼è§¦ååéåï¼ export function saveKnowledgeBaseFiles(data) { return request({ url: "/knowledgeBase/file/save", method: "post", data, }); } // å é¤ç¥è¯åºæä»¶ export function deleteKnowledgeBaseFile(ids) { return request({ url: "/knowledgeBase/file/delete", method: "delete", data: ids, }); } // éæ°åéåæä»¶ export function reprocessVector(vectorId) { return request({ src/views/collaborativeApproval/knowledgeBase/index.vue
@@ -256,6 +256,7 @@ :action="uploadUrl" :headers="uploadHeaders" :on-success="handleUploadSuccess" :on-change="handleFileChange" :before-upload="beforeUpload" :accept="acceptTypes" :file-list="uploadFileList" @@ -398,9 +399,10 @@ updateKnowledgeBase, getVectorStatus, reprocessVector, knowledgeChat knowledgeChat, saveKnowledgeBaseFiles, deleteKnowledgeBaseFile } from "@/api/collaborativeApproval/knowledgeBase.js"; import { attachmentList, createAttachment, deleteAttachment } from "@/api/basicData/storageAttachment.js"; import useUserStore from '@/store/modules/user'; import { userListNoPageByTenantId } from '@/api/system/user.js'; import { getToken } from '@/utils/auth'; @@ -874,33 +876,17 @@ refreshFileList(); }; // å·æ°æä»¶å表 // å·æ°æä»¶å表ï¼ç´æ¥ä½¿ç¨åéç¶ææ¥å£ï¼ const refreshFileList = async () => { if (!selectedKnowledgeBaseId.value) return; fileLoading.value = true; try { // è·åéä»¶å表 const attachmentRes = await attachmentList({ recordType: 'knowledge_base', recordId: selectedKnowledgeBaseId.value, application: 'rag_file' }); // è·ååéåç¶æ const vectorRes = await getVectorStatus(selectedKnowledgeBaseId.value); // åå¹¶æ°æ® const vectorMap = {}; (vectorRes.data || []).forEach(v => { vectorMap[v.storageBlobId] = v; }); fileList.value = (attachmentRes.data || []).map(file => ({ ...file, vectorStatus: vectorMap[file.storageBlobId]?.vectorStatus ?? 0, chunkCount: vectorMap[file.storageBlobId]?.chunkCount ?? 0, vectorId: vectorMap[file.storageBlobId]?.id const res = await getVectorStatus(selectedKnowledgeBaseId.value); fileList.value = (res.data || []).map(item => ({ ...item, name: item.fileName, vectorId: item.id })); } catch (error) { console.error('è·åæä»¶å表失败:', error); @@ -924,23 +910,35 @@ const handleUploadSuccess = async (response, file) => { if (response.code === 200) { uploadedBlobs.value.push(...response.data); await saveAttachment(); ElMessage.success('æä»¶ä¸ä¼ æåï¼æ£å¨å¤çåéå...'); uploadedBlobs.value = []; refreshFileList(); } else { ElMessage.error(response.msg || 'ä¸ä¼ 失败'); } }; // ä¿åéä»¶å ³èå°ç¥è¯åº const saveAttachment = async () => { await createAttachment({ recordType: 'knowledge_base', recordId: selectedKnowledgeBaseId.value, application: 'rag_file', storageBlobDTOs: uploadedBlobs.value.map(b => b.id) }); // ä¸ä¼ æä»¶ååå¤çï¼ç¨äºæ£æµæææä»¶ä¸ä¼ å®æï¼ const handleFileChange = (file, fileList) => { // å½æä»¶ç¶æåå䏿²¡ææ£å¨ä¸ä¼ çæä»¶æ¶ï¼ç»ä¸æäº¤ if (file.status === 'success' && fileList.every(f => f.status === 'success' || f.status === 'fail')) { if (uploadedBlobs.value.length > 0) { saveKnowledgeBaseFilesAndRefresh(); } } }; // ä¿åç¥è¯åºæä»¶å ³è并触ååéå const saveKnowledgeBaseFilesAndRefresh = async () => { try { await saveKnowledgeBaseFiles({ knowledgeBaseId: selectedKnowledgeBaseId.value, storageBlobIds: uploadedBlobs.value.map(b => b.id) }); ElMessage.success('æä»¶ä¸ä¼ æåï¼æ£å¨å¤çåéå...'); } catch (error) { ElMessage.error('ä¿åæä»¶å ³è失败'); } finally { uploadedBlobs.value = []; refreshFileList(); } }; // è·åæä»¶ç±»å @@ -987,7 +985,7 @@ type: 'warning' }); await deleteAttachment([row.id]); await deleteKnowledgeBaseFile([row.id]); ElMessage.success('å 餿å'); refreshFileList(); } catch (error) { src/views/collaborativeApproval/notificationManagement/meetApplication/index.vue
@@ -84,34 +84,24 @@ <el-row :gutter="20"> <el-col :span="12"> <el-form-item label="å¼å§æ¶é´" prop="startTime"> <el-select <el-time-picker v-model="meetingForm.startTime" placeholder="è¯·éæ©å¼å§æ¶é´" value-format="HH:mm" format="HH:mm" style="width: 100%" > <el-option v-for="time in startTimeOptions" :key="time.value" :label="time.label" :value="time.value" /> </el-select> /> </el-form-item> </el-col> <el-col :span="12"> <el-form-item label="ç»ææ¶é´" prop="endTime"> <el-select <el-time-picker v-model="meetingForm.endTime" placeholder="è¯·éæ©ç»ææ¶é´" value-format="HH:mm" format="HH:mm" style="width: 100%" > <el-option v-for="time in endTimeOptions" :key="time.value" :label="time.label" :value="time.value" /> </el-select> /> </el-form-item> </el-col> </el-row> @@ -152,9 +142,9 @@ </template> <script setup> import {ref, reactive, onMounted, computed, watch} from 'vue' import {ref, reactive, onActivated} from 'vue' import {ElMessage} from 'element-plus' import {Plus, Document, Promotion, Bell} from '@element-plus/icons-vue' import {Document, Promotion, Bell} from '@element-plus/icons-vue' import {getRoomEnum, saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js' import {userListNoPageByTenantId} from "@/api/system/user.js"; @@ -205,152 +195,16 @@ // åå·¥å表 const employees = ref([]) // æ¶é´é项ï¼ä»¥åå°æ¶ä¸ºé´éï¼ const timeOptions = ref([]) const getTimeInMinutes = (time) => { if (!time) return -1 const [hour, minute] = time.split(':').map(Number) return hour * 60 + minute } const isToday = (dateText) => { if (!dateText) return false const [year, month, day] = dateText.split('-').map(Number) const now = new Date() return year === now.getFullYear() && month === now.getMonth() + 1 && day === now.getDate() } const validateStartTime = (_rule, value, callback) => { if (!value) { callback() return } if (isToday(meetingForm.meetingDate)) { const now = new Date() const currentMinutes = now.getHours() * 60 + now.getMinutes() if (getTimeInMinutes(value) > currentMinutes) { callback(new Error('å½å¤©å¼å§æ¶é´ä¸è½æäºå½åæ¶é´')) return } } callback() } const validateEndTime = (_rule, value, callback) => { if (!value || !meetingForm.startTime) { callback() return } if (getTimeInMinutes(value) <= getTimeInMinutes(meetingForm.startTime)) { callback(new Error('ç»ææ¶é´å¿ 须大äºå¼å§æ¶é´')) return } callback() } // è¡¨åæ ¡éªè§å const rules = { title: [{required: true, message: '请è¾å ¥ä¼è®®ä¸»é¢', trigger: 'blur'}], roomId: [{required: true, message: 'è¯·éæ©ä¼è®®å®¤', trigger: 'change'}], host: [{required: true, message: '请è¾å ¥ä¸»æäºº', trigger: 'blur'}], meetingDate: [{required: true, message: 'è¯·éæ©ä¼è®®æ¥æ', trigger: 'change'}], startTime: [ {required: true, message: 'è¯·éæ©å¼å§æ¶é´', trigger: 'change'}, {validator: validateStartTime, trigger: 'change'} ], endTime: [ {required: true, message: 'è¯·éæ©ç»ææ¶é´', trigger: 'change'}, {validator: validateEndTime, trigger: 'change'} ], startTime: [{required: true, message: 'è¯·éæ©å¼å§æ¶é´', trigger: 'change'}], endTime: [{required: true, message: 'è¯·éæ©ç»ææ¶é´', trigger: 'change'}], participants: [{required: true, message: 'è¯·éæ©åä¼äººå', trigger: 'change'}] } const startTimeOptions = computed(() => { if (!isToday(meetingForm.meetingDate)) { return timeOptions.value } const now = new Date() const currentMinutes = now.getHours() * 60 + now.getMinutes() return timeOptions.value.filter(item => getTimeInMinutes(item.value) <= currentMinutes) }) const endTimeOptions = computed(() => { if (!meetingForm.startTime) { return timeOptions.value } const startMinutes = getTimeInMinutes(meetingForm.startTime) return timeOptions.value.filter(item => getTimeInMinutes(item.value) > startMinutes) }) // åå§åæ¶é´é项 const initTimeOptions = () => { const options = [] const now = new Date() const currentHour = now.getHours() const currentMinute = now.getMinutes() // meetingDate æ¯ "yyyy-MM-dd" const meetingDate = new Date(meetingForm.meetingDate) const isSameDay = now.getFullYear() === meetingDate.getFullYear() && now.getMonth() === meetingDate.getMonth() && now.getDate() === meetingDate.getDate() console.log('æ¯å¦åä¸å¤©:', isSameDay) for (let hour = 8; hour <= 18; hour++) { // å¼å§æ¶é´å¿ é¡»æäºå½åæ¶é´ if (hour < currentHour && isSameDay) { continue } if (hour === currentHour && currentMinute > 30 && isSameDay) { continue } // æ¯ä¸ªå°æ¶æ·»å 两个éé¡¹ï¼æ´ç¹ååç¹ options.push({ value: `${hour.toString().padStart(2, '0')}:00`, label: `${hour.toString().padStart(2, '0')}:00` }) if (hour < 18) { // 18:00ä¹å没æåç¹é项 options.push({ value: `${hour.toString().padStart(2, '0')}:30`, label: `${hour.toString().padStart(2, '0')}:30` }) } } timeOptions.value = options } watch(() => meetingForm.meetingDate, () => { if (meetingForm.startTime && !startTimeOptions.value.some(item => item.value === meetingForm.startTime)) { meetingForm.startTime = '' } if (meetingForm.endTime && !endTimeOptions.value.some(item => item.value === meetingForm.endTime)) { meetingForm.endTime = '' } if (meetingForm.startTime) { meetingFormRef.value?.validateField('startTime') } if (meetingForm.endTime) { meetingFormRef.value?.validateField('endTime') } initTimeOptions() }) watch(() => meetingForm.startTime, () => { if (meetingForm.endTime && getTimeInMinutes(meetingForm.endTime) <= getTimeInMinutes(meetingForm.startTime)) { meetingForm.endTime = '' } if (meetingForm.endTime) { meetingFormRef.value?.validateField('endTime') } }) // ç¦ç¨æ¥æï¼ç¦ç¨ä»å¤©ä¹åçæ¥æï¼ const disabledDate = (time) => { @@ -389,19 +243,6 @@ // 模ææäº¤æä½ ElMessage.success(`${getCurrentTypeName()}æäº¤æå`) // æ ¹æ®ä¸åç±»åæ§è¡ä¸åæä½ switch (currentType.value) { case 'approval': ElMessage.info('ä¼è®®å·²æäº¤å®¡æ¹æµç¨') break case 'department': ElMessage.info('é¨é¨çº§ä¼è®®ç³è¯·å·²æäº¤') break case 'notification': ElMessage.info('ä¼è®®éç¥å·²åå¸') break } resetForm() }) @@ -410,8 +251,7 @@ } // 页é¢å è½½æ¶åå§å onMounted(() => { initTimeOptions() onActivated(() => { getRoomEnum().then(res => { meetingRooms.value = res.data }) src/views/collaborativeApproval/notificationManagement/meetExamine/index.vue
@@ -184,12 +184,12 @@ </template> <script setup> import {ref, reactive, onMounted} from 'vue' import {ref, reactive, onMounted, onActivated} from 'vue' import {ElMessage, ElMessageBox} from 'element-plus' import Pagination from '@/components/Pagination/index.vue' import {getRoomEnum, getExamineList,saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js' import dayjs from "dayjs"; import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js"; import {userListNoPageByTenantId} from "@/api/system/user.js"; // æ°æ®å表å è½½ç¶æ const loading = ref(false) @@ -235,12 +235,12 @@ let room = roomEnum.value.find(room => it.roomId === room.id) it.location = `${room.name}(${room.location})` let staffs = JSON.parse(it.participants) it.staffCount = staffs.size it.staffCount = staffs.length it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}` it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => { it.participants = staffList.value.filter(staff => staffs.some(id => id == staff.userId)).map(staff => { return { id: staff.id, name: `${staff.staffName}${staff.postName ? ` (${staff.postName})` : ''}` id: staff.userId, name: `${staff.nickName || staff.userName}${staff.dept?.deptName ? ` (${staff.dept.deptName})` : ''}` } }) @@ -342,11 +342,13 @@ // 页é¢å è½½æ¶è·åæ°æ® onMounted(async () => { const [resp1, resp2]= await Promise.all([getRoomEnum(), staffOnJobListPage({current: -1, size: -1, staffState: 1})]) const [resp1, resp2]= await Promise.all([getRoomEnum(), userListNoPageByTenantId()]) roomEnum.value = resp1.data staffList.value = resp2.data.records staffList.value = resp2.data || [] }) await getList() onActivated(() => { getList() }) </script> src/views/collaborativeApproval/notificationManagement/meetIndex/index.vue
@@ -45,7 +45,7 @@ <div class="cell content-cell" :class="[cell.type, `status-${cell.meeting?.status || '0'}`]" :style="{ flex: cell.span-0.2 }" :style="{ flex: cell.span }" @click="viewMeetingDetails(cell)" > <div v-if="cell.type === 'meeting'" class="meeting-content"> @@ -89,7 +89,7 @@ </template> <script setup> import {ref, reactive, onMounted} from 'vue' import {ref, reactive, onMounted, onActivated} from 'vue' import {ElMessage} from 'element-plus' import {getMeetingUseList} from "@/api/collaborativeApproval/meeting.js" import dayjs from "dayjs"; @@ -111,17 +111,17 @@ // æ¯å¦æ¾ç¤ºè¯¦æ å¯¹è¯æ¡ const detailDialogVisible = ref(false) // åå§åæ¶é´æ§½ï¼ä»¥åå°æ¶ä¸ºé´éï¼ä»8:00å°18:00ï¼ // åå§åæ¶é´æ§½ï¼ä»¥åå°æ¶ä¸ºé´éï¼ä»8:00å°19:00ï¼ const initTimeSlots = () => { const slots = [] for (let hour = 8; hour < 18; hour++) { for (let hour = 8; hour < 19; hour++) { // æ¯ä¸ªå°æ¶æ·»å 两个æ¶é´æ®µï¼æ´ç¹ååç¹ slots.push({ label: `${hour.toString().padStart(2, '0')}:00`, value: `${hour.toString().padStart(2, '0')}:00` }) if (hour < 18) { // å°17:30ä¸ºæ¢ if (hour < 19) { // å°18:30ä¸ºæ¢ slots.push({ label: `${hour.toString().padStart(2, '0')}:30`, value: `${hour.toString().padStart(2, '0')}:30` @@ -131,28 +131,36 @@ timeSlots.value = slots } const toMinutes = (time) => { if (!time) return 0 const [h, m] = time.split(':').map(Number) return h * 60 + m } // çæä¼è®®å®¤çæ¶é´åå æ ¼ const generateMeetingCells = (room) => { const cells = [] const meetings = room.meetings || [] const occupiedSlots = new Set() // å¤çæ¯ä¸ªä¼è®® for (const meeting of meetings) { const startMin = toMinutes(meeting.startTime) const endMin = toMinutes(meeting.endTime) const startIdx = timeSlots.value.findIndex(slot => slot.value === meeting.startTime) let endIdx = timeSlots.value.findIndex(slot => slot.value === meeting.endTime) if (endIdx === -1) { endIdx = timeSlots.value.length } console.log('endIdx111', endIdx) if (startIdx !== -1 && endIdx !== -1) { // æ 记被å ç¨çæ¶é´æ®µ // æ¾å°ç¬¬ä¸ä¸ªæ¶é´ > startMin çæ§½ä½ï¼åå¾åé䏿 ¼ let startIdx = timeSlots.value.findIndex(slot => toMinutes(slot.value) > startMin) if (startIdx === -1) startIdx = timeSlots.value.length startIdx = Math.max(0, startIdx - 1) // æ¾å°ç¬¬ä¸ä¸ªæ¶é´ >= endMin çæ§½ä½ï¼æ²¡æ¾å°å延伏尿«å°¾ let endIdx = timeSlots.value.findIndex(slot => toMinutes(slot.value) >= endMin) if (endIdx === -1) endIdx = timeSlots.value.length if (startIdx < endIdx) { for (let i = startIdx; i < endIdx; i++) { occupiedSlots.add(timeSlots.value[i].value) } // å建ä¼è®®åå æ ¼ cells.push({ type: 'meeting', meeting: meeting, @@ -163,11 +171,9 @@ } } // å¤çç©ºé²æ¶é´æ®µ for (let i = 0; i < timeSlots.value.length; i++) { const slot = timeSlots.value[i] if (!occupiedSlots.has(slot.value)) { // æ¥æ¾è¿ç»çç©ºé²æ¶é´æ®µ let span = 1 while (i + span < timeSlots.value.length && !occupiedSlots.has(timeSlots.value[i + span].value)) { @@ -183,14 +189,11 @@ } } // ææ¶é´æåº cells.sort((a, b) => { const timeA = a.startTime || a.time const timeB = b.startTime || b.time return timeSlots.value.findIndex(s => s.value === timeA) - timeSlots.value.findIndex(s => s.value === timeB) return toMinutes(timeA) - toMinutes(timeB) }) console.log('cells', cells) return cells } @@ -219,12 +222,10 @@ // 页é¢å è½½æ¶è·åæ°æ® onMounted(() => { // åå§åæ¶é´æ§½ initTimeSlots() }) // é»è®¤æ¥è¯¢ä»å¤©çæ°æ® const today = new Date() queryForm.date = today.toISOString().split('T')[0] onActivated(() => { handleSearch() }) </script> src/views/collaborativeApproval/notificationManagement/meetPublish/index.vue
@@ -182,12 +182,12 @@ </template> <script setup> import {ref, reactive, onMounted} from 'vue' import {ref, reactive, onMounted, onActivated} from 'vue' import {ElMessage, ElMessageBox} from 'element-plus' import Pagination from '@/components/Pagination/index.vue' import {getRoomEnum, getMeetingPublish,saveMeetingApplication} from '@/api/collaborativeApproval/meeting.js' import dayjs from "dayjs"; import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js"; import {userListNoPageByTenantId} from "@/api/system/user.js"; // æ°æ®å表å è½½ç¶æ const loading = ref(false) @@ -233,13 +233,13 @@ let room = roomEnum.value.find(room => it.roomId === room.id) it.location = `${room.name}(${room.location})` let staffs = JSON.parse(it.participants) it.staffCount = staffs.size it.staffCount = staffs.length it.status = it.publishStatus it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}` it.participants = staffList.value.filter(staff => staffs.some(id=>id === staff.id)).map(staff => { it.participants = staffList.value.filter(staff => staffs.some(id => id == staff.userId)).map(staff => { return { id: staff.id, name: `${staff.staffName}${staff.postName ? ` (${staff.postName})` : ''}` id: staff.userId, name: `${staff.nickName || staff.userName}${staff.dept?.deptName ? ` (${staff.dept.deptName})` : ''}` } }) @@ -340,11 +340,13 @@ // 页é¢å è½½æ¶è·åæ°æ® onMounted(async () => { const [resp1, resp2]= await Promise.all([getRoomEnum(), staffOnJobListPage({current: -1, size: -1, staffState: 1})]) const [resp1, resp2]= await Promise.all([getRoomEnum(), userListNoPageByTenantId()]) roomEnum.value = resp1.data staffList.value = resp2.data.records staffList.value = resp2.data || [] }) await getList() onActivated(() => { getList() }) </script> src/views/collaborativeApproval/notificationManagement/meetSetting/index.vue
@@ -102,7 +102,7 @@ </template> <script setup> import { ref, reactive, onMounted, getCurrentInstance } from 'vue' import { ref, reactive, onActivated, getCurrentInstance } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { Plus } from '@element-plus/icons-vue' import Pagination from '@/components/Pagination/index.vue' @@ -276,8 +276,7 @@ proxy.download('/meeting/export', { ...searchForm }, 'ä¼è®®å®¤è®¾ç½®.xlsx') } // 页é¢å è½½æ¶è·åæ°æ® onMounted(() => { onActivated(() => { getList() }) </script> src/views/collaborativeApproval/notificationManagement/summary/index.vue
@@ -155,13 +155,13 @@ </template> <script setup> import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted, onActivated } from 'vue' import { ElMessage } from 'element-plus' import Pagination from '@/components/Pagination/index.vue' import Editor from '@/components/Editor/index.vue' import { getRoomEnum, getMeetingPublish ,getMeetingMinutesByMeetingId,saveMeetingMinutes} from '@/api/collaborativeApproval/meeting.js' import dayjs from "dayjs" import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js"; import {userListNoPageByTenantId} from "@/api/system/user.js"; // æ°æ®å表å è½½ç¶æ const loading = ref(false) @@ -209,12 +209,12 @@ let room = roomEnum.value.find(room => it.roomId === room.id) it.location = `${room.name}(${room.location})` let staffs = JSON.parse(it.participants) it.staffCount = staffs.size it.staffCount = staffs.length it.meetingTime = `${it.meetingDate} ${dayjs(it.startTime).format('HH:mm:ss')} ~ ${dayjs(it.endTime).format('HH:mm:ss')}` it.participants = staffList.value.filter(staff => staffs.some(id => id === staff.id)).map(staff => { it.participants = staffList.value.filter(staff => staffs.some(id => id == staff.userId)).map(staff => { return { id: staff.id, name: `${staff.staffName}${staff.postName ? ` (${staff.postName})` : ''}` id: staff.userId, name: `${staff.nickName || staff.userName}${staff.dept?.deptName ? ` (${staff.dept.deptName})` : ''}` } }) @@ -337,11 +337,13 @@ // 页é¢å è½½æ¶è·åæ°æ® onMounted(async () => { const [resp1, resp2] = await Promise.all([getRoomEnum(), staffOnJobListPage({current: -1, size: -1, staffState: 1})]) const [resp1, resp2] = await Promise.all([getRoomEnum(), userListNoPageByTenantId()]) roomEnum.value = resp1.data staffList.value = resp2.data.records staffList.value = resp2.data || [] }) await getList() onActivated(() => { getList() }) </script>