| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form" style="margin-bottom: 20px;"> |
| | | <div> |
| | | <span class="search_title">知识标题:</span> |
| | | <el-input |
| | | v-model="searchForm.title" |
| | | style="width: 240px" |
| | | placeholder="请输入知识标题搜索" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <span class="search_title ml10">知识类型:</span> |
| | | <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px"> |
| | | <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" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | ></PIMTable> |
| | | </div> |
| | | |
| | | <!-- 新增/编辑知识弹窗 --> |
| | | <FormDialog |
| | | v-model="dialogVisible" |
| | | :title="dialogTitle" |
| | | :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="title"> |
| | | <el-input v-model="form.title" placeholder="请输入知识标题" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="知识类型" prop="type"> |
| | | <el-select v-model="form.type" placeholder="请选择知识类型" style="width: 100%"> |
| | | <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> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="适用场景" prop="scenario"> |
| | | <el-input v-model="form.scenario" placeholder="请输入适用场景" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="解决效率" prop="efficiency"> |
| | | <el-select v-model="form.efficiency" placeholder="请选择解决效率" style="width: 100%"> |
| | | <el-option label="显著提升" value="high" /> |
| | | <el-option label="一般提升" value="medium" /> |
| | | <el-option label="轻微提升" value="low" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="问题描述" prop="problem"> |
| | | <el-input |
| | | v-model="form.problem" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请描述遇到的问题" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="解决方案" prop="solution"> |
| | | <el-input |
| | | v-model="form.solution" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请详细描述解决方案" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="关键要点" prop="keyPoints"> |
| | | <el-input |
| | | v-model="form.keyPoints" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请输入关键要点,用逗号分隔" |
| | | /> |
| | | </el-form-item> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="创建人" prop="creator"> |
| | | <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-form-item label="使用次数" prop="usageCount"> |
| | | <el-input-number v-model="form.usageCount" :min="0" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | |
| | | <!-- 查看知识详情弹窗 --> |
| | | <FormDialog |
| | | v-model="viewDialogVisible" |
| | | title="知识详情" |
| | | :width="'900px'" |
| | | @close="closeViewDialog" |
| | | @confirm="handleViewDialogConfirm" |
| | | @cancel="closeViewDialog" |
| | | > |
| | | <div class="knowledge-detail"> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="知识标题" :span="2"> |
| | | <span class="detail-title">{{ currentKnowledge.title }}</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="知识类型"> |
| | | <el-tag :type="getTypeTagType(currentKnowledge.type)"> |
| | | {{ getTypeLabel(currentKnowledge.type) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="适用场景"> |
| | | {{ currentKnowledge.scenario }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="解决效率"> |
| | | <el-tag :type="getEfficiencyTagType(currentKnowledge.efficiency)"> |
| | | {{ getEfficiencyLabel(currentKnowledge.efficiency) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="使用次数"> |
| | | <el-tag type="info">{{ currentKnowledge.usageCount }} 次</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="创建人"> |
| | | {{ currentKnowledge.creator }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="创建时间"> |
| | | {{ currentKnowledge.createTime }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div class="detail-section"> |
| | | <h4>问题描述</h4> |
| | | <div class="detail-content">{{ currentKnowledge.problem }}</div> |
| | | </div> |
| | | |
| | | <div class="detail-section"> |
| | | <h4>解决方案</h4> |
| | | <div class="detail-content">{{ currentKnowledge.solution }}</div> |
| | | </div> |
| | | |
| | | <div class="detail-section"> |
| | | <h4>关键要点</h4> |
| | | <div class="key-points"> |
| | | <el-tag |
| | | v-for="(point, index) in currentKnowledge.keyPoints?.split(',') || []" |
| | | :key="index" |
| | | type="success" |
| | | style="margin-right: 8px; margin-bottom: 8px;" |
| | | > |
| | | {{ point.trim() }} |
| | | </el-tag> |
| | | <!-- Tab页签切换 --> |
| | | <el-tabs v-model="activeTab" class="knowledge-tabs"> |
| | | <!-- 知识库管理Tab --> |
| | | <el-tab-pane label="知识库管理" name="manage"> |
| | | <div class="search_form" style="margin-bottom: 20px;"> |
| | | <div> |
| | | <span class="search_title">知识标题:</span> |
| | | <el-input |
| | | v-model="searchForm.title" |
| | | style="width: 240px" |
| | | placeholder="请输入知识标题搜索" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | <span class="search_title ml10">知识类型:</span> |
| | | <el-select v-model="searchForm.type" clearable @change="handleQuery" style="width: 240px"> |
| | | <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="detail-section"> |
| | | <h4>使用统计</h4> |
| | | <div class="usage-stats"> |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | ></PIMTable> |
| | | </div> |
| | | |
| | | <!-- 新增/编辑知识弹窗 --> |
| | | <FormDialog |
| | | v-model="dialogVisible" |
| | | :title="dialogTitle" |
| | | :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="8"> |
| | | <div class="stat-item"> |
| | | <div class="stat-number">{{ currentKnowledge.usageCount }}</div> |
| | | <div class="stat-label">使用次数</div> |
| | | </div> |
| | | <el-col :span="12"> |
| | | <el-form-item label="知识标题" prop="title"> |
| | | <el-input v-model="form.title" placeholder="请输入知识标题" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-item"> |
| | | <div class="stat-number">{{ getEfficiencyScore(currentKnowledge.efficiency) }}%</div> |
| | | <div class="stat-label">效率提升</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-item"> |
| | | <div class="stat-number">{{ getTimeSaved(currentKnowledge.efficiency) }}</div> |
| | | <div class="stat-label">平均节省时间</div> |
| | | </div> |
| | | <el-col :span="12"> |
| | | <el-form-item label="知识类型" prop="type"> |
| | | <el-select v-model="form.type" placeholder="请选择知识类型" style="width: 100%"> |
| | | <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> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="适用场景" prop="scenario"> |
| | | <el-input v-model="form.scenario" placeholder="请输入适用场景" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="解决效率" prop="efficiency"> |
| | | <el-select v-model="form.efficiency" placeholder="请选择解决效率" style="width: 100%"> |
| | | <el-option label="显著提升" value="high" /> |
| | | <el-option label="一般提升" value="medium" /> |
| | | <el-option label="轻微提升" value="low" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="问题描述" prop="problem"> |
| | | <el-input |
| | | v-model="form.problem" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请描述遇到的问题" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="解决方案" prop="solution"> |
| | | <el-input |
| | | v-model="form.solution" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请详细描述解决方案" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="关键要点" prop="keyPoints"> |
| | | <el-input |
| | | v-model="form.keyPoints" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请输入关键要点,用逗号分隔" |
| | | /> |
| | | </el-form-item> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="创建人" prop="creator"> |
| | | <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-form-item label="使用次数" prop="usageCount"> |
| | | <el-input-number v-model="form.usageCount" :min="0" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | |
| | | <!-- 查看知识详情弹窗 --> |
| | | <FormDialog |
| | | v-model="viewDialogVisible" |
| | | title="知识详情" |
| | | :width="'900px'" |
| | | @close="closeViewDialog" |
| | | @confirm="handleViewDialogConfirm" |
| | | @cancel="closeViewDialog" |
| | | > |
| | | <div class="knowledge-detail"> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="知识标题" :span="2"> |
| | | <span class="detail-title">{{ currentKnowledge.title }}</span> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="知识类型"> |
| | | <el-tag :type="getTypeTagType(currentKnowledge.type)"> |
| | | {{ getTypeLabel(currentKnowledge.type) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="适用场景"> |
| | | {{ currentKnowledge.scenario }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="解决效率"> |
| | | <el-tag :type="getEfficiencyTagType(currentKnowledge.efficiency)"> |
| | | {{ getEfficiencyLabel(currentKnowledge.efficiency) }} |
| | | </el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="使用次数"> |
| | | <el-tag type="info">{{ currentKnowledge.usageCount }} 次</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="创建人"> |
| | | {{ currentKnowledge.creator }} |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="创建时间"> |
| | | {{ currentKnowledge.createTime }} |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div class="detail-section"> |
| | | <h4>问题描述</h4> |
| | | <div class="detail-content">{{ currentKnowledge.problem }}</div> |
| | | </div> |
| | | |
| | | <div class="detail-section"> |
| | | <h4>解决方案</h4> |
| | | <div class="detail-content">{{ currentKnowledge.solution }}</div> |
| | | </div> |
| | | |
| | | <div class="detail-section"> |
| | | <h4>关键要点</h4> |
| | | <div class="key-points"> |
| | | <el-tag |
| | | v-for="(point, index) in currentKnowledge.keyPoints?.split(',') || []" |
| | | :key="index" |
| | | type="success" |
| | | style="margin-right: 8px; margin-bottom: 8px;" |
| | | > |
| | | {{ point.trim() }} |
| | | </el-tag> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="detail-section"> |
| | | <h4>使用统计</h4> |
| | | <div class="usage-stats"> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="8"> |
| | | <div class="stat-item"> |
| | | <div class="stat-number">{{ currentKnowledge.usageCount }}</div> |
| | | <div class="stat-label">使用次数</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-item"> |
| | | <div class="stat-number">{{ getEfficiencyScore(currentKnowledge.efficiency) }}%</div> |
| | | <div class="stat-label">效率提升</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="8"> |
| | | <div class="stat-item"> |
| | | <div class="stat-number">{{ getTimeSaved(currentKnowledge.efficiency) }}</div> |
| | | <div class="stat-label">平均节省时间</div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </FormDialog> |
| | | </el-tab-pane> |
| | | |
| | | <!-- 文件管理Tab --> |
| | | <el-tab-pane label="文件管理" name="files"> |
| | | <div class="file-manage-container"> |
| | | <!-- 知识库选择 --> |
| | | <div class="kb-selector" style="margin-bottom: 20px;"> |
| | | <span class="search_title">选择知识库:</span> |
| | | <el-select v-model="selectedKnowledgeBaseId" placeholder="请选择知识库" style="width: 300px" @change="handleKnowledgeBaseChange"> |
| | | <el-option |
| | | v-for="kb in tableData" |
| | | :key="kb.id" |
| | | :label="kb.title" |
| | | :value="kb.id" |
| | | /> |
| | | </el-select> |
| | | <el-button type="primary" style="margin-left: 10px" @click="refreshFileList" :disabled="!selectedKnowledgeBaseId"> |
| | | 刷新状态 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 文件上传区域 --> |
| | | <div v-if="selectedKnowledgeBaseId" class="upload-section"> |
| | | <el-upload |
| | | :action="uploadUrl" |
| | | :headers="uploadHeaders" |
| | | :on-success="handleUploadSuccess" |
| | | :before-upload="beforeUpload" |
| | | :accept="acceptTypes" |
| | | :file-list="uploadFileList" |
| | | :show-file-list="false" |
| | | name="files" |
| | | multiple |
| | | > |
| | | <el-button type="primary"> |
| | | <el-icon><Upload /></el-icon> |
| | | 上传文件 |
| | | </el-button> |
| | | </el-upload> |
| | | <div class="upload-tip"> |
| | | 支持 docx、xlsx、xls、pdf、txt、md、json、csv 等格式,单文件不超过10MB |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 文件列表 --> |
| | | <div v-if="selectedKnowledgeBaseId" class="file-list-section"> |
| | | <el-table :data="fileList" v-loading="fileLoading" border> |
| | | <el-table-column prop="name" label="文件名" min-width="200" show-overflow-tooltip /> |
| | | <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" align="center"> |
| | | <template #default="{ row }"> |
| | | {{ row.chunkCount || 0 }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="createTime" label="上传时间" width="160" /> |
| | | <el-table-column label="操作" width="200" fixed="right"> |
| | | <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> |
| | | </div> |
| | | |
| | | <!-- 未选择知识库提示 --> |
| | | <el-empty v-if="!selectedKnowledgeBaseId" description="请先选择知识库" /> |
| | | </div> |
| | | </div> |
| | | </FormDialog> |
| | | </el-tab-pane> |
| | | |
| | | <!-- 知识问答Tab --> |
| | | <el-tab-pane label="知识问答" name="chat"> |
| | | <div class="knowledge-chat-container"> |
| | | <!-- 知识库选择 --> |
| | | <div class="kb-selector"> |
| | | <span class="search_title">选择知识库:</span> |
| | | <el-select v-model="chatKnowledgeBaseId" placeholder="请选择知识库" style="width: 300px" @change="handleChatKnowledgeBaseChange"> |
| | | <el-option |
| | | v-for="kb in tableData" |
| | | :key="kb.id" |
| | | :label="kb.title" |
| | | :value="kb.id" |
| | | /> |
| | | </el-select> |
| | | <el-button type="primary" plain style="margin-left: 10px" @click="clearChatHistory" :disabled="!chatKnowledgeBaseId"> |
| | | 清空对话 |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- 聊天区域 --> |
| | | <div class="chat-container"> |
| | | <div class="message-list" ref="messageListRef"> |
| | | <div v-for="msg in chatMessages" :key="msg.id" :class="['message', msg.role]"> |
| | | <div class="message-role"> |
| | | <el-avatar v-if="msg.role === 'user'" :size="28" style="background: #409eff;"> |
| | | <el-icon><User /></el-icon> |
| | | </el-avatar> |
| | | <el-avatar v-else :size="28" style="background: #67c23a;"> |
| | | <el-icon><ChatDotRound /></el-icon> |
| | | </el-avatar> |
| | | <span class="role-text">{{ msg.role === 'user' ? '我' : 'AI助手' }}</span> |
| | | </div> |
| | | <div class="message-content">{{ msg.content }}</div> |
| | | <div class="message-time">{{ msg.createTime }}</div> |
| | | </div> |
| | | <div v-if="streamingContent" class="message assistant"> |
| | | <div class="message-role"> |
| | | <el-avatar :size="28" style="background: #67c23a;"> |
| | | <el-icon><ChatDotRound /></el-icon> |
| | | </el-avatar> |
| | | <span class="role-text">AI助手</span> |
| | | </div> |
| | | <div class="message-content">{{ streamingContent }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 输入区域 --> |
| | | <div class="input-area"> |
| | | <el-input |
| | | v-model="questionInput" |
| | | placeholder="输入问题,基于知识库内容回答..." |
| | | @keyup.enter="sendQuestion" |
| | | :disabled="!chatKnowledgeBaseId || sending" |
| | | /> |
| | | <el-button type="primary" @click="sendQuestion" :loading="sending" :disabled="!chatKnowledgeBaseId"> |
| | | 发送 |
| | | </el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 未选择知识库提示 --> |
| | | <el-empty v-if="!chatKnowledgeBaseId" description="请先选择知识库开始问答" /> |
| | | </div> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, toRefs, getCurrentInstance, computed, watch } from "vue"; |
| | | import { Search, Upload, User, ChatDotRound } 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 } from "@/api/collaborativeApproval/knowledgeBase.js"; |
| | | import { |
| | | listKnowledgeBase, |
| | | delKnowledgeBase, |
| | | addKnowledgeBase, |
| | | updateKnowledgeBase, |
| | | getVectorStatus, |
| | | reprocessVector, |
| | | knowledgeChat |
| | | } 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'; |
| | | |
| | | // 表单验证规则 |
| | | const rules = { |
| | |
| | | |
| | | // 响应式数据 |
| | | const data = reactive({ |
| | | activeTab: 'manage', |
| | | searchForm: { |
| | | title: "", |
| | | type: "", |
| | |
| | | dialogTitle: "", |
| | | dialogType: "add", |
| | | viewDialogVisible: false, |
| | | currentKnowledge: {} |
| | | currentKnowledge: {}, |
| | | // 文件管理相关 |
| | | selectedKnowledgeBaseId: null, |
| | | fileList: [], |
| | | fileLoading: false, |
| | | uploadFileList: [], |
| | | // 知识问答相关 |
| | | chatKnowledgeBaseId: null, |
| | | chatMessages: [], |
| | | questionInput: '', |
| | | sending: false, |
| | | streamingContent: '', |
| | | memoryId: '' |
| | | }); |
| | | |
| | | const { |
| | | activeTab, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | |
| | | dialogTitle, |
| | | dialogType, |
| | | viewDialogVisible, |
| | | currentKnowledge |
| | | currentKnowledge, |
| | | selectedKnowledgeBaseId, |
| | | fileList, |
| | | fileLoading, |
| | | uploadFileList, |
| | | chatKnowledgeBaseId, |
| | | chatMessages, |
| | | questionInput, |
| | | sending, |
| | | streamingContent, |
| | | memoryId |
| | | } = toRefs(data); |
| | | |
| | | // 表单引用 |
| | | const formRef = ref(); |
| | | const messageListRef = ref(); |
| | | // 用户相关 |
| | | const userStore = useUserStore(); |
| | | const userList = ref([]); |
| | | |
| | | // 文件上传相关 |
| | | const uploadUrl = import.meta.env.VITE_APP_BASE_API + '/common/upload'; |
| | | const uploadHeaders = { Authorization: 'Bearer ' + getToken() }; |
| | | const acceptTypes = '.docx,.xlsx,.xls,.pdf,.txt,.md,.json,.csv'; |
| | | const uploadedBlobs = ref([]); |
| | | |
| | | // 表格列配置 |
| | | const tableColumn = ref([ |
| | |
| | | const startAutoRefresh = () => { |
| | | setInterval(() => { |
| | | getList(); |
| | | }, 600000); // 10分钟刷新一次 (10 * 60 * 1000 = 600000ms) |
| | | }, 600000); |
| | | }; |
| | | |
| | | // 查询数据 |
| | |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | 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; |
| | | // 重新查询第1页数据 |
| | | return getList(); |
| | | } |
| | | tableData.value = res.data.records; |
| | |
| | | const oldSize = page.value.size; |
| | | page.value.current = obj.page; |
| | | page.value.size = obj.limit; |
| | | // 如果 size 改变了,重置到第1页,避免当前页超出范围 |
| | | if (oldSize !== obj.limit) { |
| | | page.value.current = 1; |
| | | } |
| | |
| | | dialogType.value = type; |
| | | if (type === "add") { |
| | | dialogTitle.value = "新增知识"; |
| | | // 重置表单,默认创建人为当前用户 |
| | | Object.assign(form.value, { |
| | | title: "", |
| | | type: "", |
| | |
| | | 创建人:${currentKnowledge.value.creator} |
| | | `.trim(); |
| | | |
| | | // 复制到剪贴板 |
| | | navigator.clipboard.writeText(knowledgeText).then(() => { |
| | | ElMessage.success("知识内容已复制到剪贴板"); |
| | | }).catch(() => { |
| | |
| | | |
| | | // 关闭知识表单对话框 |
| | | const closeKnowledgeDialog = () => { |
| | | // 清空表单数据,默认创建人为当前用户 |
| | | Object.assign(form.value, { |
| | | id: undefined, |
| | | title: "", |
| | |
| | | creator: userStore.nickName || "", |
| | | usageCount: 0 |
| | | }); |
| | | // 清除表单验证状态 |
| | | if (formRef.value) { |
| | | formRef.value.clearValidate(); |
| | | } |
| | |
| | | viewDialogVisible.value = false; |
| | | }; |
| | | |
| | | // 处理查看详情对话框确认(执行复制操作) |
| | | // 处理查看详情对话框确认 |
| | | const handleViewDialogConfirm = () => { |
| | | copyKnowledge(); |
| | | closeViewDialog(); |
| | |
| | | try { |
| | | await formRef.value.validate(); |
| | | if (dialogType.value === "add") { |
| | | // 新增知识 |
| | | addKnowledgeBase({...form.value}).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("添加成功"); |
| | |
| | | cancelButtonText: "取消", |
| | | type: "warning", |
| | | }).then(() => { |
| | | // console.log(selectedIds.value); |
| | | delKnowledgeBase(selectedIds.value).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("删除成功"); |
| | |
| | | getList(); |
| | | } |
| | | }) |
| | | }).catch(() => { |
| | | // 用户取消 |
| | | }); |
| | | }).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)) |
| | |
| | | const handleExport = () => { |
| | | proxy.download('/knowledgeBase/export', { ...searchForm.value }, '知识库.xlsx') |
| | | } |
| | | |
| | | // ========== 文件管理相关方法 ========== |
| | | |
| | | // 知识库选择变化 |
| | | const handleKnowledgeBaseChange = () => { |
| | | 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 |
| | | })); |
| | | } catch (error) { |
| | | console.error('获取文件列表失败:', error); |
| | | ElMessage.error('获取文件列表失败'); |
| | | } finally { |
| | | fileLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 上传前校验 |
| | | 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 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 getFileType = (name) => { |
| | | return name?.split('.').pop()?.toLowerCase() || ''; |
| | | }; |
| | | |
| | | // 向量化状态文本 |
| | | 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) => { |
| | | if (row.previewURL) { |
| | | window.open(row.previewURL, '_blank'); |
| | | } else { |
| | | ElMessage.warning('暂无预览链接'); |
| | | } |
| | | }; |
| | | |
| | | // 下载文件 |
| | | const downloadFile = (row) => { |
| | | if (row.downloadURL) { |
| | | window.open(row.downloadURL, '_blank'); |
| | | } else { |
| | | ElMessage.warning('暂无下载链接'); |
| | | } |
| | | }; |
| | | |
| | | // 删除文件 |
| | | const deleteFile = async (row) => { |
| | | try { |
| | | await ElMessageBox.confirm('确定删除该文件吗?删除后将同步删除向量库中的相关数据', '删除确认', { |
| | | confirmButtonText: '确定', |
| | | cancelButtonText: '取消', |
| | | type: 'warning' |
| | | }); |
| | | |
| | | await deleteAttachment([row.id]); |
| | | ElMessage.success('删除成功'); |
| | | refreshFileList(); |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | ElMessage.error('删除失败'); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // 重新向量化文件 |
| | | const revectorFile = async (row) => { |
| | | try { |
| | | await reprocessVector(row.vectorId); |
| | | ElMessage.success('已重新提交向量化任务'); |
| | | refreshFileList(); |
| | | } catch (error) { |
| | | ElMessage.error('重新处理失败'); |
| | | } |
| | | }; |
| | | |
| | | // ========== 知识问答相关方法 ========== |
| | | |
| | | // 知识库选择变化 |
| | | const handleChatKnowledgeBaseChange = () => { |
| | | // 重置会话 |
| | | memoryId.value = 'kb-chat-' + Date.now(); |
| | | chatMessages.value = []; |
| | | questionInput.value = ''; |
| | | streamingContent.value = ''; |
| | | }; |
| | | |
| | | // 清空对话历史 |
| | | const clearChatHistory = () => { |
| | | memoryId.value = 'kb-chat-' + Date.now(); |
| | | chatMessages.value = []; |
| | | streamingContent.value = ''; |
| | | ElMessage.success('对话已清空'); |
| | | }; |
| | | |
| | | // 发送问题 |
| | | const sendQuestion = async () => { |
| | | if (!questionInput.value.trim() || !chatKnowledgeBaseId.value || sending.value) return; |
| | | |
| | | sending.value = true; |
| | | streamingContent.value = ''; |
| | | |
| | | // 添加用户消息 |
| | | const userMsg = { |
| | | id: Date.now(), |
| | | role: 'user', |
| | | content: questionInput.value, |
| | | createTime: new Date().toLocaleString() |
| | | }; |
| | | chatMessages.value.push(userMsg); |
| | | |
| | | const currentQuestion = questionInput.value; |
| | | questionInput.value = ''; |
| | | |
| | | try { |
| | | const response = await knowledgeChat({ |
| | | knowledgeBaseId: chatKnowledgeBaseId.value, |
| | | memoryId: memoryId.value, |
| | | question: currentQuestion |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | throw new Error('请求失败'); |
| | | } |
| | | |
| | | // 流式读取响应 |
| | | const reader = response.body.getReader(); |
| | | const decoder = new TextDecoder(); |
| | | let accumulatedContent = ''; |
| | | |
| | | while (true) { |
| | | const { done, value } = await reader.read(); |
| | | if (done) break; |
| | | |
| | | accumulatedContent += decoder.decode(value, { stream: true }); |
| | | streamingContent.value = accumulatedContent; |
| | | |
| | | // 滚动到底部 |
| | | scrollToBottom(); |
| | | } |
| | | |
| | | // 添加AI回复消息 |
| | | if (accumulatedContent) { |
| | | chatMessages.value.push({ |
| | | id: Date.now(), |
| | | role: 'assistant', |
| | | content: accumulatedContent, |
| | | createTime: new Date().toLocaleString() |
| | | }); |
| | | } |
| | | |
| | | streamingContent.value = ''; |
| | | } catch (error) { |
| | | console.error('问答请求失败:', error); |
| | | ElMessage.error('问答请求失败,请稍后重试'); |
| | | // 移除用户消息 |
| | | chatMessages.value.pop(); |
| | | } finally { |
| | | sending.value = false; |
| | | } |
| | | }; |
| | | |
| | | // 滚动到底部 |
| | | const scrollToBottom = () => { |
| | | nextTick(() => { |
| | | if (messageListRef.value) { |
| | | messageListRef.value.scrollTop = messageListRef.value.scrollHeight; |
| | | } |
| | | }); |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .auto-refresh-info { |
| | | margin-bottom: 15px; |
| | | .knowledge-tabs { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .auto-refresh-info .el-alert { |
| | | /* 文件管理样式 */ |
| | | .file-manage-container { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .upload-section { |
| | | margin-bottom: 20px; |
| | | padding: 20px; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | .upload-tip { |
| | | margin-top: 10px; |
| | | color: #909399; |
| | | font-size: 12px; |
| | | } |
| | | |
| | | .file-list-section { |
| | | margin-top: 20px; |
| | | } |
| | | |
| | | /* 知识问答样式 */ |
| | | .knowledge-chat-container { |
| | | height: calc(100vh - 200px); |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .kb-selector { |
| | | padding: 10px 0; |
| | | border-bottom: 1px solid #eee; |
| | | } |
| | | |
| | | .chat-container { |
| | | flex: 1; |
| | | display: flex; |
| | | flex-direction: column; |
| | | min-height: 400px; |
| | | } |
| | | |
| | | .message-list { |
| | | flex: 1; |
| | | overflow-y: auto; |
| | | padding: 20px; |
| | | background: #f5f7fa; |
| | | border-radius: 8px; |
| | | margin: 10px 0; |
| | | } |
| | | |
| | | .message { |
| | | margin-bottom: 20px; |
| | | padding: 16px; |
| | | border-radius: 12px; |
| | | background: #fff; |
| | | box-shadow: 0 1px 2px rgba(0,0,0,0.1); |
| | | } |
| | | |
| | | .message.user { |
| | | background: #e6f7ff; |
| | | border-left: 3px solid #409eff; |
| | | } |
| | | |
| | | .message.assistant { |
| | | background: #f0f9eb; |
| | | border-left: 3px solid #67c23a; |
| | | } |
| | | |
| | | .message-role { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .role-text { |
| | | margin-left: 8px; |
| | | font-weight: 500; |
| | | color: #606266; |
| | | } |
| | | |
| | | .message-content { |
| | | line-height: 1.6; |
| | | color: #303133; |
| | | white-space: pre-wrap; |
| | | } |
| | | |
| | | .message-time { |
| | | margin-top: 8px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | } |
| | | |
| | | .input-area { |
| | | padding: 15px; |
| | | display: flex; |
| | | gap: 10px; |
| | | background: #fff; |
| | | border-radius: 8px; |
| | | border: 1px solid #eee; |
| | | } |
| | | |
| | | .input-area .el-input { |
| | | flex: 1; |
| | | } |
| | | |
| | | /* 知识详情样式 */ |
| | | .knowledge-detail { |
| | | padding: 20px 0; |
| | | } |
| | |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | </style> |
| | | </style> |