| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 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" v-hasPermi="['collaborativeApproval:knowledgeBase:remove']">å é¤</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> |
| | | </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" |
| | | :on-change="handleFileChange" |
| | | :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)" v-hasPermi="['collaborativeApproval:knowledgeBase:remove']">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <!-- æªéæ©ç¥è¯åºæç¤º --> |
| | | <el-empty v-if="!selectedKnowledgeBaseId" description="请å
éæ©ç¥è¯åº" /> |
| | | </div> |
| | | </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, 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, |
| | | getVectorStatus, |
| | | reprocessVector, |
| | | knowledgeChat, |
| | | saveKnowledgeBaseFiles, |
| | | deleteKnowledgeBaseFile |
| | | } 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 = { |
| | | title: [ |
| | | { required: true, message: "请è¾å
¥ç¥è¯æ é¢", trigger: "blur" } |
| | | ], |
| | | type: [ |
| | | { required: true, message: "è¯·éæ©ç¥è¯ç±»å", trigger: "change" } |
| | | ], |
| | | problem: [ |
| | | { required: true, message: "请æè¿°éå°çé®é¢", trigger: "blur" } |
| | | ], |
| | | solution: [ |
| | | { required: true, message: "è¯·è¯¦ç»æè¿°è§£å³æ¹æ¡", trigger: "blur" } |
| | | ] |
| | | }; |
| | | |
| | | // ååºå¼æ°æ® |
| | | const data = reactive({ |
| | | activeTab: 'manage', |
| | | searchForm: { |
| | | title: "", |
| | | type: "", |
| | | }, |
| | | tableLoading: false, |
| | | page: { |
| | | current: 1, |
| | | size: 20, |
| | | total: 0, |
| | | }, |
| | | tableData: [], |
| | | selectedIds: [], |
| | | form: { |
| | | title: "", |
| | | type: "", |
| | | scenario: "", |
| | | efficiency: "", |
| | | problem: "", |
| | | solution: "", |
| | | keyPoints: "", |
| | | creator: "", |
| | | usageCount: 0 |
| | | }, |
| | | dialogVisible: false, |
| | | dialogTitle: "", |
| | | dialogType: "add", |
| | | viewDialogVisible: false, |
| | | currentKnowledge: {}, |
| | | // æä»¶ç®¡çç¸å
³ |
| | | selectedKnowledgeBaseId: null, |
| | | fileList: [], |
| | | fileLoading: false, |
| | | uploadFileList: [], |
| | | // ç¥è¯é®çç¸å
³ |
| | | chatKnowledgeBaseId: null, |
| | | chatMessages: [], |
| | | questionInput: '', |
| | | sending: false, |
| | | streamingContent: '', |
| | | memoryId: '' |
| | | }); |
| | | |
| | | const { |
| | | activeTab, |
| | | searchForm, |
| | | tableLoading, |
| | | page, |
| | | tableData, |
| | | selectedIds, |
| | | form, |
| | | dialogVisible, |
| | | dialogTitle, |
| | | dialogType, |
| | | viewDialogVisible, |
| | | 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([ |
| | | { |
| | | label: "ç¥è¯æ é¢", |
| | | prop: "title", |
| | | showOverflowTooltip: true, |
| | | }, |
| | | { |
| | | label: "ç¥è¯ç±»å", |
| | | prop: "type", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | return getKnowledgeTypeLabel(params); |
| | | }, |
| | | formatType: (params) => { |
| | | return getKnowledgeTypeTagType(params); |
| | | } |
| | | }, |
| | | { |
| | | label: "éç¨åºæ¯", |
| | | prop: "scenario", |
| | | width: 150, |
| | | showOverflowTooltip: true, |
| | | }, |
| | | { |
| | | label: "è§£å³æç", |
| | | prop: "efficiency", |
| | | dataType: "tag", |
| | | formatData: (params) => { |
| | | const efficiencyMap = { |
| | | high: "æ¾èæå", |
| | | medium: "ä¸è¬æå", |
| | | low: "轻微æå" |
| | | }; |
| | | return efficiencyMap[params] || params; |
| | | }, |
| | | formatType: (params) => { |
| | | const typeMap = { |
| | | high: "success", |
| | | medium: "warning", |
| | | low: "info" |
| | | }; |
| | | return typeMap[params] || "info"; |
| | | } |
| | | }, |
| | | { |
| | | label: "ä½¿ç¨æ¬¡æ°", |
| | | prop: "usageCount", |
| | | width: 100, |
| | | align: "center" |
| | | }, |
| | | { |
| | | label: "å建人", |
| | | prop: "creator", |
| | | width: 120, |
| | | }, |
| | | { |
| | | label: "å建æ¶é´", |
| | | prop: "createTime", |
| | | width: 180, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 200, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | } |
| | | }, |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | viewKnowledge(row); |
| | | } |
| | | } |
| | | ] |
| | | } |
| | | ]); |
| | | |
| | | // çå¬å¯¹è¯æ¡æå¼ï¼è·åç¨æ·å表 |
| | | watch(dialogVisible, (newVal) => { |
| | | if (newVal) { |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data || []; |
| | | }); |
| | | } |
| | | }); |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | getList(); |
| | | startAutoRefresh(); |
| | | }); |
| | | |
| | | // å¼å§èªå¨å·æ° |
| | | const startAutoRefresh = () => { |
| | | setInterval(() => { |
| | | getList(); |
| | | }, 600000); |
| | | }; |
| | | |
| | | // æ¥è¯¢æ°æ® |
| | | const handleQuery = () => { |
| | | page.value.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | listKnowledgeBase({...page.value, ...searchForm.value}) |
| | | .then(res => { |
| | | tableLoading.value = false; |
| | | page.value.total = res.data.total; |
| | | 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; |
| | | }) |
| | | }; |
| | | |
| | | // å页å¤ç |
| | | const pagination = (obj) => { |
| | | const oldSize = page.value.size; |
| | | page.value.current = obj.page; |
| | | page.value.size = obj.limit; |
| | | if (oldSize !== obj.limit) { |
| | | page.value.current = 1; |
| | | } |
| | | getList(); |
| | | }; |
| | | |
| | | // éæ©ååå¤ç |
| | | const handleSelectionChange = (selection) => { |
| | | selectedIds.value = selection.map(item => item.id); |
| | | }; |
| | | |
| | | // æå¼è¡¨å |
| | | const openForm = (type, row = null) => { |
| | | dialogType.value = type; |
| | | if (type === "add") { |
| | | dialogTitle.value = "æ°å¢ç¥è¯"; |
| | | Object.assign(form.value, { |
| | | title: "", |
| | | type: "", |
| | | scenario: "", |
| | | efficiency: "", |
| | | problem: "", |
| | | solution: "", |
| | | keyPoints: "", |
| | | 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, |
| | | efficiency: row.efficiency, |
| | | problem: row.problem, |
| | | solution: row.solution, |
| | | keyPoints: row.keyPoints, |
| | | creator: row.creator, |
| | | usageCount: row.usageCount |
| | | }); |
| | | } |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | // æ¥çç¥è¯è¯¦æ
|
| | | const viewKnowledge = (row) => { |
| | | currentKnowledge.value = { ...row }; |
| | | viewDialogVisible.value = true; |
| | | }; |
| | | |
| | | // è·åç±»åæ ç¾ç±»å |
| | | const getTypeTagType = (type) => { |
| | | const typeMap = { |
| | | contract: "success", |
| | | approval: "warning", |
| | | solution: "primary", |
| | | experience: "info", |
| | | guide: "danger" |
| | | }; |
| | | return typeMap[type] || "info"; |
| | | }; |
| | | |
| | | // è·åç±»åæ ç¾ææ¬ |
| | | const getTypeLabel = (type) => { |
| | | return getKnowledgeTypeLabel(type); |
| | | }; |
| | | |
| | | // è·åæçæ ç¾ç±»å |
| | | const getEfficiencyTagType = (efficiency) => { |
| | | const typeMap = { |
| | | high: "success", |
| | | medium: "warning", |
| | | low: "info" |
| | | }; |
| | | return typeMap[efficiency] || "info"; |
| | | }; |
| | | |
| | | // è·åæçæ ç¾ææ¬ |
| | | const getEfficiencyLabel = (efficiency) => { |
| | | const efficiencyMap = { |
| | | high: "æ¾èæå", |
| | | medium: "ä¸è¬æå", |
| | | low: "轻微æå" |
| | | }; |
| | | return efficiencyMap[efficiency] || efficiency; |
| | | }; |
| | | |
| | | // è·åæçæåç¾åæ¯ |
| | | const getEfficiencyScore = (efficiency) => { |
| | | const scoreMap = { |
| | | high: 40, |
| | | medium: 25, |
| | | low: 15 |
| | | }; |
| | | return scoreMap[efficiency] || 0; |
| | | }; |
| | | |
| | | // è·åå¹³åèçæ¶é´ |
| | | const getTimeSaved = (efficiency) => { |
| | | const timeMap = { |
| | | high: "2-3天", |
| | | medium: "1-2天", |
| | | low: "0.5-1天" |
| | | }; |
| | | return timeMap[efficiency] || "æªç¥"; |
| | | }; |
| | | |
| | | // å¤å¶ç¥è¯ |
| | | 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} |
| | | `.trim(); |
| | | |
| | | navigator.clipboard.writeText(knowledgeText).then(() => { |
| | | ElMessage.success("ç¥è¯å
容已å¤å¶å°åªè´´æ¿"); |
| | | }).catch(() => { |
| | | ElMessage.error("å¤å¶å¤±è´¥ï¼è¯·æå¨å¤å¶"); |
| | | }); |
| | | }; |
| | | |
| | | // å
³éç¥è¯è¡¨åå¯¹è¯æ¡ |
| | | 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(); |
| | | } |
| | | dialogVisible.value = false; |
| | | }; |
| | | |
| | | // å
³éæ¥ç详æ
å¯¹è¯æ¡ |
| | | const closeViewDialog = () => { |
| | | viewDialogVisible.value = false; |
| | | }; |
| | | |
| | | // å¤çæ¥ç详æ
å¯¹è¯æ¡ç¡®è®¤ |
| | | const handleViewDialogConfirm = () => { |
| | | copyKnowledge(); |
| | | closeViewDialog(); |
| | | }; |
| | | |
| | | // æäº¤ç¥è¯è¡¨å |
| | | const submitForm = async () => { |
| | | try { |
| | | await formRef.value.validate(); |
| | | if (dialogType.value === "add") { |
| | | addKnowledgeBase({...form.value}).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("æ·»å æå"); |
| | | closeKnowledgeDialog(); |
| | | getList(); |
| | | } |
| | | }).catch(err => { |
| | | ElMessage.error(err.msg); |
| | | }) |
| | | } else { |
| | | updateKnowledgeBase({...form.value}).then(res => { |
| | | if(res.code == 200){ |
| | | ElMessage.success("æ´æ°æå"); |
| | | closeKnowledgeDialog(); |
| | | getList(); |
| | | } |
| | | }).catch(err => { |
| | | ElMessage.error(err.msg); |
| | | }) |
| | | } |
| | | } catch (error) { |
| | | console.error("表åéªè¯å¤±è´¥:", error); |
| | | } |
| | | }; |
| | | |
| | | // å é¤ç¥è¯ |
| | | const handleDelete = () => { |
| | | if (selectedIds.value.length === 0) { |
| | | ElMessage.warning("è¯·éæ©è¦å é¤çç¥è¯"); |
| | | return; |
| | | } |
| | | |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å é¤ï¼æ¯å¦ç¡®è®¤å é¤ï¼", "å é¤", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }).then(() => { |
| | | delKnowledgeBase(selectedIds.value).then(res => { |
| | | if(res.code == 200){ |
| | | 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 handleKnowledgeBaseChange = () => { |
| | | refreshFileList(); |
| | | }; |
| | | |
| | | // å·æ°æä»¶å表ï¼ç´æ¥ä½¿ç¨åéç¶ææ¥å£ï¼ |
| | | const refreshFileList = async () => { |
| | | if (!selectedKnowledgeBaseId.value) return; |
| | | |
| | | fileLoading.value = true; |
| | | try { |
| | | const res = await getVectorStatus(selectedKnowledgeBaseId.value); |
| | | fileList.value = (res.data || []).map(item => ({ |
| | | ...item, |
| | | name: item.fileName, |
| | | vectorId: item.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); |
| | | } else { |
| | | ElMessage.error(response.msg || 'ä¸ä¼ 失败'); |
| | | } |
| | | }; |
| | | |
| | | // ä¸ä¼ æä»¶ååå¤çï¼ç¨äºæ£æµæææä»¶ä¸ä¼ å®æï¼ |
| | | 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(); |
| | | } |
| | | }; |
| | | |
| | | // è·åæä»¶ç±»å |
| | | 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 deleteKnowledgeBaseFile([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> |
| | | .knowledge-tabs { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | /* æä»¶ç®¡çæ ·å¼ */ |
| | | .file-manage-container { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .upload-section { |
| | | margin-bottom: 20px; |
| | | padding: 20px; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .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; |
| | | } |
| | | |
| | | .detail-title { |
| | | font-size: 18px; |
| | | font-weight: bold; |
| | | color: #303133; |
| | | } |
| | | |
| | | .detail-section { |
| | | margin-top: 24px; |
| | | } |
| | | |
| | | .detail-section h4 { |
| | | margin: 0 0 12px 0; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | color: #303133; |
| | | border-left: 4px solid #409eff; |
| | | padding-left: 12px; |
| | | } |
| | | |
| | | .detail-content { |
| | | background: #f8f9fa; |
| | | padding: 16px; |
| | | border-radius: 6px; |
| | | line-height: 1.6; |
| | | color: #606266; |
| | | white-space: pre-wrap; |
| | | } |
| | | |
| | | .key-points { |
| | | display: flex; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .usage-stats { |
| | | margin-top: 16px; |
| | | } |
| | | |
| | | .stat-item { |
| | | text-align: center; |
| | | padding: 20px; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .stat-number { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .stat-label { |
| | | font-size: 14px; |
| | | color: #909399; |
| | | } |
| | | </style> |