| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <div class="search_form" style="margin-bottom: 20px;"> |
| | | <div> |
| | | <span class="search_title">知识标题:</span> |
| | | <el-input |
| | |
| | | /> |
| | | <span class="search_title ml10">知识类型:</span> |
| | | <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px"> |
| | | <el-option label="合同特批" :value="'contract'" /> |
| | | <el-option label="审批案例" :value="'approval'" /> |
| | | <el-option label="解决方案" :value="'solution'" /> |
| | | <el-option label="经验总结" :value="'experience'" /> |
| | | <el-option label="操作指南" :value="'guide'" /> |
| | | <el-option |
| | | v-for="item in knowledgeTypeOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | <el-button type="primary" @click="handleQuery" style="margin-left: 10px"> |
| | | 搜索 |
| | | </el-button> |
| | | </div> |
| | | <div> |
| | | <el-button @click="handleExport" style="margin-right: 10px">导出</el-button> |
| | | <el-button type="primary" @click="openForm('add')">新增知识</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">删除</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | |
| | | </div> |
| | | |
| | | <!-- 新增/编辑知识弹窗 --> |
| | | <el-dialog |
| | | <FormDialog |
| | | v-model="dialogVisible" |
| | | :title="dialogTitle" |
| | | width="800px" |
| | | :close-on-click-modal="false" |
| | | :width="'800px'" |
| | | @close="closeKnowledgeDialog" |
| | | @confirm="submitForm" |
| | | @cancel="closeKnowledgeDialog" |
| | | > |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> |
| | | <el-row :gutter="20"> |
| | |
| | | <el-col :span="12"> |
| | | <el-form-item label="知识类型" prop="type"> |
| | | <el-select v-model="form.type" placeholder="请选择知识类型" style="width: 100%"> |
| | | <el-option label="合同特批" value="contract" /> |
| | | <el-option label="审批案例" value="approval" /> |
| | | <el-option label="解决方案" value="solution" /> |
| | | <el-option label="经验总结" value="experience" /> |
| | | <el-option label="操作指南" value="guide" /> |
| | | <el-option |
| | | v-for="item in knowledgeTypeOptions" |
| | | :key="item.value" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="创建人" prop="creator"> |
| | | <el-input v-model="form.creator" placeholder="请输入创建人" /> |
| | | <el-select v-model="form.creator" placeholder="请选择创建人" style="width: 100%" filterable> |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.nickName" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="dialogVisible = false">取消</el-button> |
| | | <el-button type="primary" @click="submitForm">确定</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | </FormDialog> |
| | | |
| | | <!-- 查看知识详情弹窗 --> |
| | | <el-dialog |
| | | <FormDialog |
| | | v-model="viewDialogVisible" |
| | | title="知识详情" |
| | | width="900px" |
| | | :close-on-click-modal="false" |
| | | :width="'900px'" |
| | | @close="closeViewDialog" |
| | | @confirm="handleViewDialogConfirm" |
| | | @cancel="closeViewDialog" |
| | | > |
| | | <div class="knowledge-detail"> |
| | | <el-descriptions :column="2" border> |
| | |
| | | <h4>关键要点</h4> |
| | | <div class="key-points"> |
| | | <el-tag |
| | | v-for="(point, index) in currentKnowledge.keyPoints.split(',')" |
| | | v-for="(point, index) in currentKnowledge.keyPoints?.split(',') || []" |
| | | :key="index" |
| | | type="success" |
| | | style="margin-right: 8px; margin-bottom: 8px;" |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </FormDialog> |
| | | |
| | | <template #footer> |
| | | <span class="dialog-footer"> |
| | | <el-button @click="viewDialogVisible = false">关闭</el-button> |
| | | <el-button type="primary" @click="copyKnowledge">复制知识</el-button> |
| | | <el-button type="success" @click="markAsFavorite">收藏</el-button> |
| | | </span> |
| | | </template> |
| | | </el-dialog> |
| | | <!-- 文件管理弹窗 --> |
| | | <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" |
| | | name="files" |
| | | 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> |
| | | <el-button |
| | | v-if="uploadedBlobIds.length > 0" |
| | | type="text" |
| | | @click="clearUploadedFiles" |
| | | style="margin-left: 10px" |
| | | > |
| | | 清空待保存列表 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 待保存的文件列表 --> |
| | | <div v-if="uploadedBlobIds.length > 0" class="uploaded-list"> |
| | | <div class="uploaded-tip"> |
| | | <el-icon style="color: #409eff"><InfoFilled /></el-icon> |
| | | <span>已上传 {{ uploadedBlobIds.length }} 个文件,请点击"保存文件关联"按钮触发向量化处理</span> |
| | | </div> |
| | | </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 label="错误信息" width="200" show-overflow-tooltip> |
| | | <template #default="{ row }"> |
| | | <span v-if="row.vectorError" style="color: #f56c6c">{{ row.vectorError }}</span> |
| | | <span v-else style="color: #909399">-</span> |
| | | </template> |
| | | </el-table-column> |
| | | <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">正在思考中...</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 输入框 --> |
| | | <div class="chat-input"> |
| | | <el-input |
| | | v-model="inputQuestion" |
| | | placeholder="请输入问题,按回车发送(Ctrl+Enter快捷发送)" |
| | | @keyup.enter="sendMessage" |
| | | :disabled="chatLoading" |
| | | > |
| | | <template #append> |
| | | <el-button @click="sendMessage" :loading="chatLoading">发送</el-button> |
| | | </template> |
| | | </el-input> |
| | | <div class="chat-actions"> |
| | | <el-button type="text" size="small" @click="clearMessages">清空对话</el-button> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </FormDialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, toRefs } from "vue"; |
| | | import { Search, InfoFilled } from "@element-plus/icons-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, |
| | | getVectorStatus, |
| | | reprocessVector, |
| | | saveKnowledgeBaseFiles, |
| | | deleteKnowledgeBaseFile, |
| | | 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 = { |
| | |
| | | tableLoading: false, |
| | | page: { |
| | | current: 1, |
| | | size: 100, |
| | | size: 20, |
| | | total: 0, |
| | | }, |
| | | tableData: [], |
| | |
| | | title: "", |
| | | type: "", |
| | | scenario: "", |
| | | efficiency: "medium", |
| | | efficiency: "", |
| | | problem: "", |
| | | solution: "", |
| | | keyPoints: "", |
| | |
| | | dialogTitle: "", |
| | | dialogType: "add", |
| | | viewDialogVisible: false, |
| | | currentKnowledge: {} |
| | | currentKnowledge: {}, |
| | | filesDialogVisible: false, |
| | | currentKnowledgeBase: null, |
| | | fileList: [], |
| | | uploadedBlobIds: [], |
| | | savingFiles: false, |
| | | vectorStatusTimer: null, // 向量化状态轮询定时器 |
| | | chatDialogVisible: false, |
| | | messages: [], |
| | | inputQuestion: "", |
| | | chatLoading: false, |
| | | memoryId: "" |
| | | }); |
| | | |
| | | const { |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | const { |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | selectedIds, |
| | | form, |
| | | dialogVisible, |
| | | dialogTitle, |
| | | dialogType, |
| | | viewDialogVisible, |
| | | currentKnowledge |
| | | currentKnowledge, |
| | | filesDialogVisible, |
| | | currentKnowledgeBase, |
| | | fileList, |
| | | uploadedBlobIds, |
| | | savingFiles, |
| | | vectorStatusTimer, |
| | | chatDialogVisible, |
| | | messages, |
| | | inputQuestion, |
| | | chatLoading, |
| | | memoryId |
| | | } = toRefs(data); |
| | | |
| | | // 表单引用 |
| | | const formRef = ref(); |
| | | // 用户相关 |
| | | 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([ |
| | |
| | | prop: "type", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | const typeMap = { |
| | | contract: "合同特批", |
| | | approval: "审批案例", |
| | | solution: "解决方案", |
| | | experience: "经验总结", |
| | | guide: "操作指南" |
| | | }; |
| | | return typeMap[params] || params; |
| | | return getKnowledgeTypeLabel(params); |
| | | }, |
| | | formatType: (params) => { |
| | | const typeMap = { |
| | | contract: "success", |
| | | approval: "warning", |
| | | solution: "primary", |
| | | experience: "info", |
| | | guide: "danger" |
| | | }; |
| | | return typeMap[params] || "info"; |
| | | return getKnowledgeTypeTagType(params); |
| | | } |
| | | }, |
| | | { |
| | |
| | | }; |
| | | return typeMap[params] || "info"; |
| | | } |
| | | }, |
| | | { |
| | | label: "文件数量", |
| | | prop: "fileCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "切片数量", |
| | | prop: "totalChunkCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "使用次数", |
| | |
| | | } |
| | | }, |
| | | { |
| | | name: "查看", |
| | | name: "文件", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openFilesDialog(row); |
| | | } |
| | | }, |
| | | { |
| | | name: "问答", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openChatDialog(row); |
| | | } |
| | | }, |
| | | { |
| | | name: "详情", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | viewKnowledge(row); |
| | |
| | | } |
| | | ]); |
| | | |
| | | // 模拟数据 |
| | | let mockData = [ |
| | | { |
| | | id: "1", |
| | | title: "特殊合同审批流程优化方案", |
| | | type: "contract", |
| | | scenario: "大额合同快速审批", |
| | | efficiency: "high", |
| | | problem: "大额合同审批流程复杂,审批时间长,影响业务进展", |
| | | solution: "建立绿色通道,对符合条件的合同采用简化审批流程,由部门负责人直接审批,平均审批时间从3天缩短至1天", |
| | | keyPoints: "绿色通道条件,简化流程,审批权限,时间控制", |
| | | creator: "陈志强", |
| | | usageCount: 15, |
| | | createTime: "2025-01-15 10:30:00" |
| | | }, |
| | | { |
| | | id: "2", |
| | | title: "跨部门协作审批经验总结", |
| | | type: "experience", |
| | | scenario: "多部门协作项目", |
| | | efficiency: "medium", |
| | | problem: "跨部门项目审批时,各部门意见不统一,审批进度缓慢", |
| | | solution: "建立项目协调机制,指定项目负责人,定期召开协调会议,统一各方意见后再进行审批", |
| | | keyPoints: "项目协调,定期会议,统一意见,负责人制度", |
| | | creator: "李主管", |
| | | usageCount: 8, |
| | | createTime: "2025-01-14 15:20:00" |
| | | }, |
| | | { |
| | | id: "3", |
| | | title: "紧急采购审批操作指南", |
| | | type: "guide", |
| | | scenario: "紧急采购需求", |
| | | efficiency: "high", |
| | | problem: "紧急采购时审批流程复杂,无法满足紧急需求", |
| | | solution: "制定紧急采购审批标准,明确紧急程度分级,不同级别采用不同审批流程,确保紧急需求得到及时处理", |
| | | keyPoints: "紧急分级,标准制定,流程简化,及时处理", |
| | | creator: "王专员", |
| | | usageCount: 12, |
| | | createTime: "2025-01-13 09:15:00" |
| | | // 监听对话框打开,获取用户列表 |
| | | watch(dialogVisible, (newVal) => { |
| | | if (newVal) { |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data || []; |
| | | }); |
| | | } |
| | | ]; |
| | | |
| | | // 知识标题模板 |
| | | const titleTemplates = [ |
| | | "{type}审批流程优化方案", |
| | | "{scenario}处理经验总结", |
| | | "{type}特殊情况处理指南", |
| | | "{scenario}快速审批方案", |
| | | "{type}标准化操作流程", |
| | | "{scenario}问题解决方案", |
| | | "{type}最佳实践总结", |
| | | "{scenario}效率提升方案" |
| | | ]; |
| | | |
| | | // 知识类型配置 |
| | | const knowledgeTypes = [ |
| | | { type: "contract", label: "合同特批", efficiency: "high" }, |
| | | { type: "approval", label: "审批案例", efficiency: "medium" }, |
| | | { type: "solution", label: "解决方案", efficiency: "high" }, |
| | | { type: "experience", label: "经验总结", efficiency: "medium" }, |
| | | { type: "guide", label: "操作指南", efficiency: "low" } |
| | | ]; |
| | | |
| | | // 场景列表 |
| | | const scenarios = ["大额合同审批", "跨部门协作", "紧急采购", "特殊申请", "流程优化", "问题处理", "标准化建设", "效率提升"]; |
| | | |
| | | // 自动生成新数据 |
| | | const generateNewData = () => { |
| | | const newId = (mockData.length + 1).toString(); |
| | | const now = new Date(); |
| | | const randomType = knowledgeTypes[Math.floor(Math.random() * knowledgeTypes.length)]; |
| | | const randomScenario = scenarios[Math.floor(Math.random() * scenarios.length)]; |
| | | |
| | | // 生成随机标题 |
| | | let title = titleTemplates[Math.floor(Math.random() * titleTemplates.length)]; |
| | | title = title |
| | | .replace('{type}', randomType.label) |
| | | .replace('{scenario}', randomScenario); |
| | | |
| | | const newKnowledge = { |
| | | id: newId, |
| | | title: title, |
| | | type: randomType.type, |
| | | scenario: randomScenario, |
| | | efficiency: randomType.efficiency, |
| | | problem: `在${randomScenario}过程中遇到的问题描述...`, |
| | | solution: `针对${randomScenario}的解决方案和操作步骤...`, |
| | | keyPoints: "关键要点1,关键要点2,关键要点3,关键要点4", |
| | | creator: ["陈志强", "刘雅婷", "王建国", "赵丽华"][Math.floor(Math.random() * 4)], |
| | | usageCount: Math.floor(Math.random() * 20) + 1, |
| | | createTime: now.toLocaleString() |
| | | }; |
| | | |
| | | // 添加到数据开头 |
| | | mockData.unshift(newKnowledge); |
| | | |
| | | // 保持数据量在合理范围内(最多保留30条) |
| | | if (mockData.length > 30) { |
| | | mockData = mockData.slice(0, 30); |
| | | } |
| | | |
| | | console.log(`[${new Date().toLocaleString()}] 自动生成新知识: ${title}`); |
| | | }; |
| | | }); |
| | | |
| | | // 生命周期 |
| | | onMounted(() => { |
| | |
| | | // 开始自动刷新 |
| | | const startAutoRefresh = () => { |
| | | setInterval(() => { |
| | | generateNewData(); |
| | | getList(); |
| | | }, 600000); // 10分钟刷新一次 (10 * 60 * 1000 = 600000ms) |
| | | }; |
| | |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | |
| | | setTimeout(() => { |
| | | let filteredData = [...mockData]; |
| | | |
| | | if (searchForm.value.title) { |
| | | filteredData = filteredData.filter(item => |
| | | item.title.toLowerCase().includes(searchForm.value.title.toLowerCase()) |
| | | ); |
| | | } |
| | | |
| | | if (searchForm.value.type) { |
| | | filteredData = filteredData.filter(item => item.type === searchForm.value.type); |
| | | } |
| | | |
| | | tableData.value = filteredData; |
| | | page.value.total = filteredData.length; |
| | | |
| | | // ✅ GET请求使用params传参 |
| | | listKnowledgeBase({ |
| | | current: page.value.current, |
| | | size: page.value.size, |
| | | title: searchForm.value.title, |
| | | type: searchForm.value.type |
| | | }) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | }, 500); |
| | | page.value.total = res.data.total; |
| | | |
| | | // 如果当前页数超过总页数,重置到第1页并重新查询 |
| | | const maxPage = Math.ceil(res.data.total / page.value.size) || 1; |
| | | if (page.value.current > maxPage && maxPage > 0) { |
| | | page.value.current = 1; |
| | | return getList(); |
| | | } |
| | | |
| | | tableData.value = res.data.records; |
| | | }) |
| | | .catch(err => { |
| | | tableLoading.value = false; |
| | | console.error("查询知识库列表失败:", err); |
| | | }); |
| | | }; |
| | | |
| | | // 分页处理 |
| | | const pagination = (obj) => { |
| | | const oldSize = page.value.size; |
| | | page.value.current = obj.page; |
| | | page.value.size = obj.limit; |
| | | handleQuery(); |
| | | // 如果 size 改变了,重置到第1页,避免当前页超出范围 |
| | | if (oldSize !== obj.limit) { |
| | | page.value.current = 1; |
| | | } |
| | | getList(); |
| | | }; |
| | | |
| | | // 选择变化处理 |
| | |
| | | dialogType.value = type; |
| | | if (type === "add") { |
| | | dialogTitle.value = "新增知识"; |
| | | // 重置表单 |
| | | // 重置表单,默认创建人为当前用户 |
| | | Object.assign(form.value, { |
| | | title: "", |
| | | type: "", |
| | | scenario: "", |
| | | efficiency: "medium", |
| | | efficiency: "", |
| | | problem: "", |
| | | solution: "", |
| | | keyPoints: "", |
| | | creator: "", |
| | | creator: userStore.nickName || "", |
| | | usageCount: 0 |
| | | }); |
| | | } else if (type === "edit" && row) { |
| | | dialogTitle.value = "编辑知识"; |
| | | Object.assign(form.value, { |
| | | id: row.id, |
| | | title: row.title, |
| | | type: row.type, |
| | | scenario: row.scenario, |
| | |
| | | |
| | | // 获取类型标签文本 |
| | | const getTypeLabel = (type) => { |
| | | const typeMap = { |
| | | contract: "合同特批", |
| | | approval: "审批案例", |
| | | solution: "解决方案", |
| | | experience: "经验总结", |
| | | guide: "操作指南" |
| | | }; |
| | | return typeMap[type] || type; |
| | | return getKnowledgeTypeLabel(type); |
| | | }; |
| | | |
| | | // 获取效率标签类型 |
| | |
| | | // 复制知识 |
| | | const copyKnowledge = () => { |
| | | const knowledgeText = ` |
| | | 知识标题:${currentKnowledge.value.title} |
| | | 知识类型:${getTypeLabel(currentKnowledge.value.type)} |
| | | 适用场景:${currentKnowledge.value.scenario} |
| | | 问题描述:${currentKnowledge.value.problem} |
| | | 解决方案:${currentKnowledge.value.solution} |
| | | 关键要点:${currentKnowledge.value.keyPoints} |
| | | 创建人:${currentKnowledge.value.creator} |
| | | 知识标题:${currentKnowledge.value.title} |
| | | 知识类型:${getTypeLabel(currentKnowledge.value.type)} |
| | | 适用场景:${currentKnowledge.value.scenario} |
| | | 问题描述:${currentKnowledge.value.problem} |
| | | 解决方案:${currentKnowledge.value.solution} |
| | | 关键要点:${currentKnowledge.value.keyPoints} |
| | | 创建人:${currentKnowledge.value.creator} |
| | | `.trim(); |
| | | |
| | | |
| | | // 复制到剪贴板 |
| | | navigator.clipboard.writeText(knowledgeText).then(() => { |
| | | ElMessage.success("知识内容已复制到剪贴板"); |
| | |
| | | }); |
| | | }; |
| | | |
| | | // 收藏知识 |
| | | const markAsFavorite = () => { |
| | | // 增加使用次数 |
| | | const index = mockData.findIndex(item => item.id === currentKnowledge.value.id); |
| | | if (index !== -1) { |
| | | mockData[index].usageCount += 1; |
| | | currentKnowledge.value.usageCount += 1; |
| | | // 关闭知识表单对话框 |
| | | const closeKnowledgeDialog = () => { |
| | | // 清空表单数据,默认创建人为当前用户 |
| | | Object.assign(form.value, { |
| | | id: undefined, |
| | | title: "", |
| | | type: "", |
| | | scenario: "", |
| | | efficiency: "", |
| | | problem: "", |
| | | solution: "", |
| | | keyPoints: "", |
| | | creator: userStore.nickName || "", |
| | | usageCount: 0 |
| | | }); |
| | | // 清除表单验证状态 |
| | | if (formRef.value) { |
| | | formRef.value.clearValidate(); |
| | | } |
| | | |
| | | ElMessage.success("已收藏,使用次数+1"); |
| | | dialogVisible.value = false; |
| | | }; |
| | | |
| | | // 关闭查看详情对话框 |
| | | const closeViewDialog = () => { |
| | | viewDialogVisible.value = false; |
| | | }; |
| | | |
| | | // 处理查看详情对话框确认(执行复制操作) |
| | | const handleViewDialogConfirm = () => { |
| | | copyKnowledge(); |
| | | closeViewDialog(); |
| | | }; |
| | | |
| | | // 提交知识表单 |
| | | const submitForm = async () => { |
| | | try { |
| | | await formRef.value.validate(); |
| | | |
| | | |
| | | // ✅ POST请求使用data传参,明确参数结构 |
| | | const formData = { |
| | | title: form.value.title, |
| | | type: form.value.type, |
| | | scenario: form.value.scenario || "", |
| | | efficiency: form.value.efficiency || "", |
| | | problem: form.value.problem, |
| | | solution: form.value.solution, |
| | | keyPoints: form.value.keyPoints || "", |
| | | creator: form.value.creator || "", |
| | | usageCount: form.value.usageCount || 0 |
| | | }; |
| | | |
| | | if (dialogType.value === "add") { |
| | | // 新增知识 |
| | | const newKnowledge = { |
| | | id: (mockData.length + 1).toString(), |
| | | title: form.value.title, |
| | | type: form.value.type, |
| | | scenario: form.value.scenario, |
| | | efficiency: form.value.efficiency, |
| | | problem: form.value.problem, |
| | | solution: form.value.solution, |
| | | keyPoints: form.value.keyPoints, |
| | | creator: form.value.creator, |
| | | usageCount: form.value.usageCount, |
| | | createTime: new Date().toLocaleString() |
| | | }; |
| | | |
| | | mockData.unshift(newKnowledge); |
| | | ElMessage.success("知识创建成功"); |
| | | addKnowledgeBase(formData).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("添加成功"); |
| | | closeKnowledgeDialog(); |
| | | getList(); |
| | | } |
| | | }).catch(err => { |
| | | console.error("添加知识库失败:", err); |
| | | ElMessage.error(err.msg || "添加失败"); |
| | | }); |
| | | } else { |
| | | // 编辑知识 |
| | | const index = mockData.findIndex(item => item.id === selectedIds.value[0]); |
| | | if (index !== -1) { |
| | | Object.assign(mockData[index], { |
| | | title: form.value.title, |
| | | type: form.value.type, |
| | | scenario: form.value.scenario, |
| | | efficiency: form.value.efficiency, |
| | | problem: form.value.problem, |
| | | solution: form.value.solution, |
| | | keyPoints: form.value.keyPoints, |
| | | creator: form.value.creator, |
| | | usageCount: form.value.usageCount |
| | | }); |
| | | ElMessage.success("知识更新成功"); |
| | | } |
| | | // 更新知识 - 添加id参数 |
| | | updateKnowledgeBase({ |
| | | id: form.value.id, |
| | | ...formData |
| | | }).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("更新成功"); |
| | | closeKnowledgeDialog(); |
| | | getList(); |
| | | } |
| | | }).catch(err => { |
| | | console.error("更新知识库失败:", err); |
| | | ElMessage.error(err.msg || "更新失败"); |
| | | }); |
| | | } |
| | | |
| | | dialogVisible.value = false; |
| | | getList(); |
| | | } catch (error) { |
| | | console.error("表单验证失败:", error); |
| | | } |
| | |
| | | ElMessage.warning("请选择要删除的知识"); |
| | | return; |
| | | } |
| | | |
| | | ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", { |
| | | |
| | | ElMessageBox.confirm("选中的内容将被删除,是否确认删除?", "删除", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }).then(() => { |
| | | // 从mockData中删除选中的项 |
| | | selectedIds.value.forEach(id => { |
| | | const index = mockData.findIndex(item => item.id === id); |
| | | if (index !== -1) { |
| | | mockData.splice(index, 1); |
| | | // ✅ DELETE请求使用data传递ID数组 |
| | | delKnowledgeBase(selectedIds.value).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("删除成功"); |
| | | selectedIds.value = []; |
| | | getList(); |
| | | } |
| | | }).catch(err => { |
| | | console.error("删除知识库失败:", err); |
| | | ElMessage.error(err.msg || "删除失败"); |
| | | }); |
| | | |
| | | ElMessage.success("删除成功"); |
| | | selectedIds.value = []; |
| | | getList(); |
| | | }).catch(() => { |
| | | // 用户取消 |
| | | }); |
| | | }; |
| | | |
| | | // 导出 |
| | | const { proxy } = getCurrentInstance() |
| | | const { knowledge_type } = proxy.useDict("knowledge_type") |
| | | |
| | | // 字典工具 |
| | | const knowledgeTypeOptions = computed(() => knowledge_type?.value || []) |
| | | const getKnowledgeTypeLabel = (val) => { |
| | | const item = knowledgeTypeOptions.value.find(i => String(i.value) === String(val)) |
| | | return item ? item.label : val |
| | | } |
| | | const getKnowledgeTypeTagType = (val) => { |
| | | const item = knowledgeTypeOptions.value.find(i => String(i.value) === String(val)) |
| | | return item?.elTagType || "info" |
| | | } |
| | | 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 || []; |
| | | |
| | | // 检查是否有处理中的文件,如果有则启动轮询 |
| | | const hasProcessing = res.data.some(item => item.vectorStatus === 1); |
| | | if (hasProcessing && !vectorStatusTimer.value) { |
| | | startVectorStatusPolling(); |
| | | } else if (!hasProcessing && vectorStatusTimer.value) { |
| | | stopVectorStatusPolling(); |
| | | } |
| | | } catch (error) { |
| | | console.error("加载文件列表失败:", error); |
| | | ElMessage.error("加载文件列表失败"); |
| | | } |
| | | }; |
| | | |
| | | // 开始轮询向量化状态 |
| | | const startVectorStatusPolling = () => { |
| | | vectorStatusTimer.value = setInterval(async () => { |
| | | try { |
| | | const res = await getVectorStatus(currentKnowledgeBase.value.id); |
| | | fileList.value = res.data || []; |
| | | |
| | | // 检查是否还有处理中的文件 |
| | | const hasProcessing = res.data.some(item => item.vectorStatus === 1); |
| | | if (!hasProcessing) { |
| | | stopVectorStatusPolling(); |
| | | ElMessage.success("所有文件向量化处理完成"); |
| | | } |
| | | } catch (error) { |
| | | console.error("轮询向量化状态失败:", error); |
| | | stopVectorStatusPolling(); |
| | | } |
| | | }, 3000); // 每3秒轮询一次 |
| | | }; |
| | | |
| | | // 停止轮询向量化状态 |
| | | const stopVectorStatusPolling = () => { |
| | | if (vectorStatusTimer.value) { |
| | | clearInterval(vectorStatusTimer.value); |
| | | vectorStatusTimer.value = null; |
| | | } |
| | | }; |
| | | |
| | | // 上传前校验 |
| | | 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、md、docx、xlsx、xls、pdf 格式的文件'); |
| | | return false; |
| | | } |
| | | |
| | | const isLt50M = file.size / 1024 / 1024 < 50; |
| | | if (!isLt50M) { |
| | | ElMessage.error('文件大小不能超过 50MB'); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | }; |
| | | |
| | | // 上传成功 |
| | | const handleUploadSuccess = (response, file) => { |
| | | console.log("上传响应:", response); // 调试日志 |
| | | |
| | | if (response.code === 200) { |
| | | // ✅ 后端返回的是 List<StorageBlobVO>,所以data是数组 |
| | | if (Array.isArray(response.data) && response.data.length > 0) { |
| | | // 取数组第一个元素的id |
| | | const blobId = response.data[0].id; |
| | | if (blobId) { |
| | | uploadedBlobIds.value.push(blobId); |
| | | ElMessage.success(`文件 ${file.name} 上传成功`); |
| | | } else { |
| | | console.error("上传响应中未找到id:", response.data[0]); |
| | | ElMessage.error("上传失败: 未获取到文件ID"); |
| | | } |
| | | } else { |
| | | console.error("上传响应格式错误:", response); |
| | | ElMessage.error("上传失败: 响应格式错误"); |
| | | } |
| | | } else { |
| | | ElMessage.error(response.msg || "上传失败"); |
| | | } |
| | | }; |
| | | |
| | | // 上传失败 |
| | | const handleUploadError = (error, file) => { |
| | | ElMessage.error(`文件 ${file.name} 上传失败`); |
| | | }; |
| | | |
| | | // 保存文件关联 |
| | | const saveFiles = async () => { |
| | | // 参数校验 |
| | | if (!currentKnowledgeBase.value?.id) { |
| | | ElMessage.error("知识库信息异常"); |
| | | return; |
| | | } |
| | | |
| | | if (uploadedBlobIds.value.length === 0) { |
| | | ElMessage.warning("请先上传文件"); |
| | | return; |
| | | } |
| | | |
| | | savingFiles.value = true; |
| | | |
| | | try { |
| | | // ✅ POST请求使用data传参,明确参数结构 |
| | | await saveKnowledgeBaseFiles({ |
| | | knowledgeBaseId: currentKnowledgeBase.value.id, // 知识库ID |
| | | storageBlobIds: uploadedBlobIds.value // 文件blob ID数组 |
| | | }); |
| | | |
| | | 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 clearUploadedFiles = () => { |
| | | uploadedBlobIds.value = []; |
| | | ElMessage.success("已清空待保存文件列表"); |
| | | }; |
| | | |
| | | // 删除文件 |
| | | const deleteFile = async (row) => { |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | "确定要删除该文件吗?删除后将无法恢复向量数据", |
| | | "删除确认", |
| | | { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | } |
| | | ); |
| | | |
| | | // ✅ DELETE请求使用data传递ID数组 |
| | | await deleteKnowledgeBaseFile([row.id]); // 注意: row.id是向量记录ID,不是storageBlobId |
| | | 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 = []; |
| | | stopVectorStatusPolling(); // 停止轮询 |
| | | getList(); // 刷新主列表,更新文件数量 |
| | | }; |
| | | |
| | | // ============ 知识库问答相关 ============ |
| | | |
| | | // 生成UUID的fallback方案 |
| | | const generateUUID = () => { |
| | | if (typeof crypto !== 'undefined' && crypto.randomUUID) { |
| | | return crypto.randomUUID(); |
| | | } |
| | | // fallback: 兼容不支持 crypto.randomUUID 的环境 |
| | | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { |
| | | const r = Math.random() * 16 | 0; |
| | | const v = c === 'x' ? r : (r & 0x3 | 0x8); |
| | | return v.toString(16); |
| | | }); |
| | | }; |
| | | |
| | | // 打开问答弹窗 |
| | | const openChatDialog = (row) => { |
| | | currentKnowledgeBase.value = row; |
| | | chatDialogVisible.value = true; |
| | | memoryId.value = generateUUID(); |
| | | messages.value = []; |
| | | inputQuestion.value = ""; |
| | | }; |
| | | |
| | | // 发送消息 |
| | | const sendMessage = async () => { |
| | | // 参数校验 |
| | | if (!inputQuestion.value.trim()) { |
| | | ElMessage.warning("请输入问题"); |
| | | return; |
| | | } |
| | | |
| | | if (!currentKnowledgeBase.value?.id) { |
| | | ElMessage.error("知识库信息异常"); |
| | | return; |
| | | } |
| | | |
| | | const question = inputQuestion.value.trim(); |
| | | |
| | | // 添加用户消息 |
| | | messages.value.push({ |
| | | role: 'user', |
| | | content: question |
| | | }); |
| | | |
| | | inputQuestion.value = ""; |
| | | chatLoading.value = true; |
| | | |
| | | // 滚动到底部 |
| | | await nextTick(); |
| | | scrollToBottom(); |
| | | |
| | | try { |
| | | // ✅ 流式请求使用Fetch API |
| | | const response = await knowledgeChat({ |
| | | knowledgeBaseId: currentKnowledgeBase.value.id, // 知识库ID |
| | | memoryId: memoryId.value, // 会话ID |
| | | question: question // 用户问题 |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | const errorText = await response.text(); |
| | | throw new Error(errorText || '请求失败'); |
| | | } |
| | | |
| | | // ✅ 后端返回 text/stream;charset=utf-8 |
| | | 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, { stream: true }); // ✅ 添加stream选项 |
| | | aiContent += text; |
| | | messages.value[messages.value.length - 1].content = aiContent; |
| | | |
| | | // 滚动到底部 |
| | | await nextTick(); |
| | | scrollToBottom(); |
| | | } |
| | | |
| | | // 如果AI返回空内容,显示提示 |
| | | if (!aiContent.trim()) { |
| | | messages.value[messages.value.length - 1].content = '抱歉,知识库中未找到相关内容,请尝试其他问题。'; |
| | | } |
| | | } catch (error) { |
| | | console.error("问答请求失败:", error); |
| | | ElMessage.error("问答请求失败,请稍后重试"); |
| | | messages.value.push({ |
| | | role: 'assistant', |
| | | content: '抱歉,发生了错误,请稍后重试' |
| | | }); |
| | | } finally { |
| | | chatLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 清空对话 |
| | | const clearMessages = () => { |
| | | ElMessageBox.confirm( |
| | | "确定要清空所有对话记录吗?", |
| | | "清空确认", |
| | | { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | } |
| | | ).then(() => { |
| | | messages.value = []; |
| | | memoryId.value = generateUUID(); // 重新生成会话ID |
| | | ElMessage.success("对话已清空"); |
| | | }).catch(() => { |
| | | // 用户取消 |
| | | }); |
| | | }; |
| | | |
| | | // 滚动到底部 |
| | | const scrollToBottom = () => { |
| | | if (chatMessagesRef.value) { |
| | | chatMessagesRef.value.scrollTop = chatMessagesRef.value.scrollHeight; |
| | | } |
| | | }; |
| | | |
| | | // 关闭问答弹窗 |
| | | const closeChatDialog = () => { |
| | | chatDialogVisible.value = false; |
| | | currentKnowledgeBase.value = null; |
| | | messages.value = []; |
| | | inputQuestion.value = ""; |
| | | }; |
| | | </script> |
| | | |
| | |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | |
| | | /* 文件管理样式 */ |
| | | .file-manager { |
| | | padding: 20px 0; |
| | | } |
| | | |
| | | .upload-section { |
| | | display: flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .uploaded-list { |
| | | margin-top: 16px; |
| | | padding: 12px; |
| | | background: #f0f9ff; |
| | | border-radius: 6px; |
| | | border: 1px solid #b3d8ff; |
| | | } |
| | | |
| | | .uploaded-tip { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | color: #409eff; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | /* 知识库问答样式 */ |
| | | .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; |
| | | } |
| | | |
| | | .chat-actions { |
| | | margin-top: 8px; |
| | | text-align: right; |
| | | } |
| | | |
| | | </style> |