|
|
@@ -0,0 +1,792 @@
|
|
|
+<template>
|
|
|
+ <div class="kb-manage">
|
|
|
+ <!-- 左侧:知识库列表 -->
|
|
|
+ <div class="kb-left-panel">
|
|
|
+ <KnowledgeBaseList
|
|
|
+ :list="filteredKBList"
|
|
|
+ :activeId="activeKBId"
|
|
|
+ :searchKey="kbSearchKey"
|
|
|
+ @search="kbSearchKey = $event"
|
|
|
+ @select="onSelectKB"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧:详情与操作区 -->
|
|
|
+ <div class="kb-right-panel">
|
|
|
+ <template v-if="activeKB">
|
|
|
+ <div class="kb-detail-header normal-panel">
|
|
|
+ <div class="kb-detail-info">
|
|
|
+ <h3 class="kb-detail-name">{{ activeKB.name }}</h3>
|
|
|
+ <p class="kb-detail-desc">{{ activeKB.description || $t('暂无描述') }}</p>
|
|
|
+ <div class="kb-detail-meta">
|
|
|
+ <span>{{ $t('创建时间') }}:{{ activeKB.createdAt }}</span>
|
|
|
+ <span>{{ $t('更新时间') }}:{{ activeKB.updatedAt }}</span>
|
|
|
+ <span>
|
|
|
+ {{ $t('标签') }}:
|
|
|
+ <el-tag
|
|
|
+ v-for="tag in activeKB.tags"
|
|
|
+ :key="tag"
|
|
|
+ :color="getTagColor(tag)"
|
|
|
+ size="small"
|
|
|
+ effect="dark"
|
|
|
+ class="kb-tag-inline"
|
|
|
+ >{{ tag }}</el-tag>
|
|
|
+ <span v-if="!activeKB.tags || activeKB.tags.length === 0" class="kb-no-tag">{{ $t('无') }}</span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="kb-tabs-wrapper">
|
|
|
+ <el-tabs v-model="activeTab" type="card" class="kb-tabs">
|
|
|
+ <el-tab-pane label="文档管理" name="docs">
|
|
|
+ <DocumentTable
|
|
|
+ :kb="activeKB"
|
|
|
+ :documents="currentDocuments"
|
|
|
+ :total="docTotal"
|
|
|
+ :pageSize="docPageSize"
|
|
|
+ :currentPage="docPage"
|
|
|
+ @upload="openUploadDialog"
|
|
|
+ @detail="openDocumentDetail"
|
|
|
+ @preview="onPreviewFile"
|
|
|
+ @toggle-status="onToggleDocStatus"
|
|
|
+ @delete="onDeleteDocument"
|
|
|
+ @download="onDownloadDocument"
|
|
|
+ @batch-download="onBatchDownload"
|
|
|
+ @refresh="loadDocuments"
|
|
|
+ @search="onDocSearch"
|
|
|
+ @page-change="onDocPageChange"
|
|
|
+ @size-change="onDocSizeChange"
|
|
|
+ />
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="元数据配置" name="metadata">
|
|
|
+ <MetadataConfig
|
|
|
+ :fields="metadataFields"
|
|
|
+ :sysMetaEnabled="sysMetaEnabled"
|
|
|
+ @create="onCreateField"
|
|
|
+ @update="onUpdateField"
|
|
|
+ @delete="onDeleteField"
|
|
|
+ @toggle-system="onToggleSystemMeta"
|
|
|
+ />
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div v-else class="kb-empty-state">
|
|
|
+ <i class="el-icon-document"></i>
|
|
|
+ <p>{{ knowledgeBases.length === 0 ? $t('加载中') : $t('请选择左侧知识库查看详情') }}</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 上传文档弹窗 -->
|
|
|
+ <UploadDocument
|
|
|
+ :visible="uploadDialogVisible"
|
|
|
+ @close="uploadDialogVisible = false"
|
|
|
+ @submit="onUploadDocument"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 文档详情抽屉 -->
|
|
|
+ <DocumentDetail
|
|
|
+ :visible="detailDrawerVisible"
|
|
|
+ :document="selectedDocument"
|
|
|
+ :metadataFields="metadataFields"
|
|
|
+ @close="detailDrawerVisible = false"
|
|
|
+ @download="onDownloadDocument"
|
|
|
+ @toggle-status="onToggleDocStatus"
|
|
|
+ @delete="onDeleteDocument"
|
|
|
+ @save-meta="onSaveDocMetadata"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import KnowledgeBaseList from './KnowledgeBaseList.vue'
|
|
|
+import DocumentTable from './DocumentTable.vue'
|
|
|
+import UploadDocument from './UploadDocument.vue'
|
|
|
+import DocumentDetail from './DocumentDetail.vue'
|
|
|
+import MetadataConfig from './MetadataConfig.vue'
|
|
|
+
|
|
|
+const TAG_COLORS = [
|
|
|
+ '#409EFF', '#67C23A', '#E6A23C', '#F56C6C',
|
|
|
+ '#909399', '#B37FEB', '#13C2C2', '#FA8C16'
|
|
|
+]
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'KBManagement',
|
|
|
+ components: {
|
|
|
+ KnowledgeBaseList,
|
|
|
+ DocumentTable,
|
|
|
+ UploadDocument,
|
|
|
+ DocumentDetail,
|
|
|
+ MetadataConfig
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ activeKBId: null,
|
|
|
+ kbSearchKey: '',
|
|
|
+ activeTab: 'docs',
|
|
|
+ uploadDialogVisible: false,
|
|
|
+ detailDrawerVisible: false,
|
|
|
+ selectedDocument: null,
|
|
|
+ tagColorMap: {},
|
|
|
+ previewUrl: '',
|
|
|
+
|
|
|
+ // 全部从接口获取
|
|
|
+ knowledgeBases: [],
|
|
|
+ allDocuments: [],
|
|
|
+ metadataFields: [],
|
|
|
+ sysMetaEnabled: true,
|
|
|
+ docPage: 1,
|
|
|
+ docPageSize: 20,
|
|
|
+ docTotal: 0,
|
|
|
+ docKeyword: ''
|
|
|
+ }
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ activeKB() {
|
|
|
+ return this.knowledgeBases.find(kb => kb.id === this.activeKBId) || null
|
|
|
+ },
|
|
|
+ filteredKBList() {
|
|
|
+ if (!this.kbSearchKey) return this.knowledgeBases
|
|
|
+ const keyword = this.kbSearchKey.toLowerCase()
|
|
|
+ return this.knowledgeBases.filter(kb => kb.name.toLowerCase().includes(keyword))
|
|
|
+ },
|
|
|
+ currentDocuments() {
|
|
|
+ return this.allDocuments
|
|
|
+ },
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ this.fetchKBList()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // ====== 通用请求 ======
|
|
|
+ async apiCall(id, content = {}) {
|
|
|
+ try {
|
|
|
+ const res = await this.$api.requested({ id, content })
|
|
|
+ console.log(`[API] id=${id}`, { request: content, response: res })
|
|
|
+ return res
|
|
|
+ } catch (e) {
|
|
|
+ console.error(`[API] id=${id} error`, e)
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // ====== 知识库 API ======
|
|
|
+ async fetchKBList() {
|
|
|
+ const res = await this.apiCall(2026052714301001, { keyword: this.kbSearchKey || '' })
|
|
|
+ if (res && res.code === 1 && res.data) {
|
|
|
+ const list = res.data.data || res.data.list || res.data || []
|
|
|
+ this.knowledgeBases = list.map(item => ({
|
|
|
+ id: item.id || item.dataset_id,
|
|
|
+ name: item.name || item.dataset_name || '',
|
|
|
+ description: item.description || item.desc || '',
|
|
|
+ docCount: item.total_documents || 0,
|
|
|
+ tags: item.tags || [],
|
|
|
+ createdAt: this.formatTimestamp(item.created_at || item.createdAt),
|
|
|
+ updatedAt: this.formatTimestamp(item.updated_at || item.updatedAt)
|
|
|
+ }))
|
|
|
+ if (!this.activeKBId && this.knowledgeBases.length > 0) {
|
|
|
+ this.activeKBId = this.knowledgeBases[0].id
|
|
|
+ this.fetchDocumentList()
|
|
|
+ this.fetchKBTags()
|
|
|
+ this.fetchMetadata()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async fetchKBTags() {
|
|
|
+ if (!this.activeKBId) return
|
|
|
+ const res = await this.apiCall(2026052714301007, { dataset_id: String(this.activeKBId) })
|
|
|
+ if (res && res.code === 1 && res.data) {
|
|
|
+ const tags = res.data.tags || res.data.list || res.data || []
|
|
|
+ const kb = this.knowledgeBases.find(k => k.id === this.activeKBId)
|
|
|
+ if (kb) kb.tags = Array.isArray(tags) ? tags : []
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // ====== 文档 API ======
|
|
|
+ async fetchDocumentList() {
|
|
|
+ if (!this.activeKBId) return
|
|
|
+ const res = await this.apiCall(2026052714301017, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ keyword: this.docKeyword,
|
|
|
+ status: '',
|
|
|
+ pageNumber: this.docPage,
|
|
|
+ pageSize: this.docPageSize
|
|
|
+ })
|
|
|
+ if (res && res.code === 1 && res.data) {
|
|
|
+ const list = res.data.data || res.data.list || res.data || []
|
|
|
+ const total = res.data.total || list.length || 0
|
|
|
+ this.docTotal = total
|
|
|
+ this.allDocuments = Array.isArray(list) ? list.map(item => ({
|
|
|
+ id: item.id || item.document_id,
|
|
|
+ kbId: this.activeKBId,
|
|
|
+ name: item.name || item.document_name || item.file_name || '',
|
|
|
+ size: this.formatSize(this.getFileSize(item)),
|
|
|
+ type: (item.type || item.file_type || item.name || '').split('.').pop().toLowerCase(),
|
|
|
+ version: item.version || 1,
|
|
|
+ status: (item.display_status === 'available' ? 'enabled' : item.display_status) || (item.enabled === false ? 'disabled' : 'enabled'),
|
|
|
+ embedStatus: item.indexing_status || item.embedStatus || 'waiting',
|
|
|
+ embedProgress: item.completed_segments !== undefined && item.total_segments
|
|
|
+ ? Math.round(item.completed_segments / item.total_segments * 100)
|
|
|
+ : (item.embedProgress || 0),
|
|
|
+ uploadTime: this.formatTimestamp(item.created_at || item.createdBy),
|
|
|
+ metadata: this.formatMetadata(item.metadata || item.doc_metadata || [])
|
|
|
+ })) : []
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 获取文件大小(兼容不同接口返回结构)
|
|
|
+ getFileSize(item) {
|
|
|
+ return item.data_source_info?.upload_file?.size
|
|
|
+ || item.data_source_detail_dict?.upload_file?.size
|
|
|
+ || item.size
|
|
|
+ || 0
|
|
|
+ },
|
|
|
+
|
|
|
+ // 格式化元数据为可读格式
|
|
|
+ formatMetadata(metadata) {
|
|
|
+ if (!Array.isArray(metadata)) return metadata
|
|
|
+ const result = {}
|
|
|
+ const labelMap = {
|
|
|
+ 'document_name': '文档名称',
|
|
|
+ 'uploader': '上传者',
|
|
|
+ 'upload_date': '上传时间',
|
|
|
+ 'last_update_date': '更新时间',
|
|
|
+ 'source': '来源'
|
|
|
+ }
|
|
|
+ const sourceMap = {
|
|
|
+ 'file_upload': '文件上传',
|
|
|
+ 'api': 'API导入',
|
|
|
+ 'notion': 'Notion导入',
|
|
|
+ 'web': '网页抓取'
|
|
|
+ }
|
|
|
+ metadata.forEach(item => {
|
|
|
+ const key = item.name
|
|
|
+ let value = item.value
|
|
|
+ // 时间戳转日期
|
|
|
+ if (item.type === 'time' && value) {
|
|
|
+ value = this.formatTimestamp(parseFloat(value))
|
|
|
+ }
|
|
|
+ // 来源转中文
|
|
|
+ if (key === 'source' && sourceMap[value]) {
|
|
|
+ value = sourceMap[value]
|
|
|
+ }
|
|
|
+ const label = labelMap[key] || key
|
|
|
+ result[label] = value
|
|
|
+ })
|
|
|
+ return result
|
|
|
+ },
|
|
|
+
|
|
|
+ async fetchDocumentDetail(docId) {
|
|
|
+ const res = await this.apiCall(2026052714301008, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ document_id: String(docId)
|
|
|
+ })
|
|
|
+ if (res && res.code === 1 && res.data) {
|
|
|
+ const d = res.data
|
|
|
+ const idx = this.allDocuments.findIndex(doc => doc.id === docId)
|
|
|
+ if (idx > -1) {
|
|
|
+ this.allDocuments[idx] = {
|
|
|
+ ...this.allDocuments[idx],
|
|
|
+ name: d.name || d.document_name || this.allDocuments[idx].name,
|
|
|
+ size: this.formatSize(this.getFileSize(d)) || this.allDocuments[idx].size,
|
|
|
+ type: (d.type || d.file_type || d.name || '').split('.').pop().toLowerCase(),
|
|
|
+ version: d.version || this.allDocuments[idx].version,
|
|
|
+ status: (d.display_status === 'available' ? 'enabled' : d.display_status) || (d.enabled === false ? 'disabled' : (d.enabled === true ? 'enabled' : this.allDocuments[idx].status)),
|
|
|
+ embedStatus: d.indexing_status || d.embedStatus || this.allDocuments[idx].embedStatus,
|
|
|
+ embedProgress: d.embedProgress || this.allDocuments[idx].embedProgress,
|
|
|
+ metadata: this.formatMetadata(d.doc_metadata || d.metadata || this.allDocuments[idx].metadata)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.selectedDocument = { ...this.allDocuments[idx] || this.selectedDocument }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async fetchIndexingStatus(docId, batch) {
|
|
|
+ const res = await this.apiCall(2026052714301012, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ batch: batch || String(docId)
|
|
|
+ })
|
|
|
+ if (res && res.code === 1 && res.data) {
|
|
|
+ const doc = this.allDocuments.find(d => d.id === docId)
|
|
|
+ if (doc) {
|
|
|
+ doc.embedStatus = res.data.indexing_status || res.data.status || doc.embedStatus
|
|
|
+ if (res.data.completed_segments !== undefined && res.data.total_segments) {
|
|
|
+ doc.embedProgress = Math.round(res.data.completed_segments / res.data.total_segments * 100)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // ====== 知识库操作 ======
|
|
|
+ onSelectKB(id) {
|
|
|
+ this.activeKBId = id
|
|
|
+ this.activeTab = 'docs'
|
|
|
+ this.fetchDocumentList()
|
|
|
+ this.fetchKBTags()
|
|
|
+ this.fetchMetadata()
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ // ====== 文档操作 ======
|
|
|
+ openUploadDialog() {
|
|
|
+ this.uploadDialogVisible = true
|
|
|
+ },
|
|
|
+
|
|
|
+ async onUploadDocument({ files, metadata }) {
|
|
|
+ if (!files || files.length === 0) return
|
|
|
+ this.uploadDialogVisible = false
|
|
|
+ const loading = this.$loading({ lock: true, text: this.$t('正在上传文档') })
|
|
|
+
|
|
|
+ try {
|
|
|
+ for (const file of files) {
|
|
|
+ // 1. 获取 OBS 上传地址
|
|
|
+ const ext = file.name.split('.').pop().toLowerCase()
|
|
|
+ const obsRes = await this.$api.requested({
|
|
|
+ classname: 'system.attachment.huawei.OBS',
|
|
|
+ method: 'getFileName',
|
|
|
+ content: { filename: file.name, filetype: ext, parentid: '' }
|
|
|
+ })
|
|
|
+ if (!obsRes || !obsRes.data || !obsRes.data.uploadurl) {
|
|
|
+ this.$message.error(this.$t('获取上传地址失败', { name: file.name }))
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ const uploadUrl = obsRes.data.uploadurl
|
|
|
+ const serialfilename = obsRes.data.serialfilename
|
|
|
+
|
|
|
+ // 2. 上传文件到华为云 OBS
|
|
|
+ const config = {
|
|
|
+ headers: ext === 'pdf'
|
|
|
+ ? { 'Content-Type': 'application/pdf' }
|
|
|
+ : { 'Content-Type': 'application/octet-stream' },
|
|
|
+ onUploadProgress: (e) => {
|
|
|
+ if (e.total) loading.text = this.$t('正在上传') + ` ${file.name} (${Math.round(e.loaded / e.total * 100)}%)`
|
|
|
+ }
|
|
|
+ }
|
|
|
+ await this.$upload.hw_upload(uploadUrl, file, config)
|
|
|
+
|
|
|
+ // 3. 生成附件记录,拿到 attachmentid
|
|
|
+ const attachRes = await this.$api.requested({
|
|
|
+ classname: 'system.attachment.huawei.OBS',
|
|
|
+ method: 'uploadSuccess',
|
|
|
+ content: { serialfilename }
|
|
|
+ })
|
|
|
+ if (!attachRes || !attachRes.data) {
|
|
|
+ this.$message.error(this.$t('附件记录创建失败', { name: file.name }))
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ const attachmentId = attachRes.data.attachmentids
|
|
|
+ ? attachRes.data.attachmentids[0]
|
|
|
+ : (attachRes.data.attachmentid || attachRes.data.id)
|
|
|
+
|
|
|
+ // 4. 调用 Dify 接口创建文档
|
|
|
+ const difyRes = await this.apiCall(2026052715313901, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ attachmentid: attachmentId
|
|
|
+ })
|
|
|
+ if (difyRes && difyRes.code === 1) {
|
|
|
+ this.$message.success(this.$t('上传成功', { name: file.name }))
|
|
|
+ } else {
|
|
|
+ this.$message.error(this.$t('创建文档失败', { name: file.name }))
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 刷新文档列表
|
|
|
+ this.fetchDocumentList()
|
|
|
+ } catch (e) {
|
|
|
+ console.error('[Upload] error', e)
|
|
|
+ this.$message.error(this.$t('上传失败') + ': ' + (e.message || ''))
|
|
|
+ } finally {
|
|
|
+ loading.close()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ loadDocuments() {
|
|
|
+ this.fetchDocumentList()
|
|
|
+ },
|
|
|
+
|
|
|
+ onDocSearch(keyword) {
|
|
|
+ this.docKeyword = keyword
|
|
|
+ this.docPage = 1
|
|
|
+ this.fetchDocumentList()
|
|
|
+ },
|
|
|
+
|
|
|
+ onDocPageChange(page) {
|
|
|
+ this.docPage = page
|
|
|
+ this.fetchDocumentList()
|
|
|
+ },
|
|
|
+
|
|
|
+ onDocSizeChange(size) {
|
|
|
+ this.docPageSize = size
|
|
|
+ this.docPage = 1
|
|
|
+ this.fetchDocumentList()
|
|
|
+ },
|
|
|
+
|
|
|
+ // 浏览器可直接预览的文件类型
|
|
|
+ isPreviewable(type) {
|
|
|
+ const previewable = ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'txt', 'html', 'htm']
|
|
|
+ return previewable.includes((type || '').toLowerCase())
|
|
|
+ },
|
|
|
+
|
|
|
+ async onPreviewFile(doc) {
|
|
|
+ const res = await this.apiCall(2026052714301010, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ document_id: String(doc.id)
|
|
|
+ })
|
|
|
+ if (!res || res.code !== 1 || !res.data) {
|
|
|
+ this.$message.error(this.$t('获取文件地址失败'))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const url = res.data.url || res.data.download_url || res.data.downloadUrl
|
|
|
+ if (!url) return
|
|
|
+ const fullUrl = res.difyBaseUrl ? res.difyBaseUrl + url : url
|
|
|
+
|
|
|
+ if (this.isPreviewable(doc.type)) {
|
|
|
+ // 把 as_attachment=true 改为 false 实现浏览器预览
|
|
|
+ const previewUrl = fullUrl.replace('as_attachment=true', 'as_attachment=false')
|
|
|
+ window.open(previewUrl, '_blank')
|
|
|
+ } else {
|
|
|
+ try {
|
|
|
+ await this.$confirm('该文件类型不支持在线预览,是否直接下载?', '提示', {
|
|
|
+ confirmButtonText: '下载',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ })
|
|
|
+ this.triggerDownload(fullUrl, doc.name || '')
|
|
|
+ } catch {
|
|
|
+ // 用户取消
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async openDocumentDetail(doc) {
|
|
|
+ this.selectedDocument = { ...doc }
|
|
|
+ this.detailDrawerVisible = true
|
|
|
+ this.fetchDocumentDetail(doc.id)
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ async onToggleDocStatus(docId, callback) {
|
|
|
+ const idx = this.allDocuments.findIndex(d => d.id === docId)
|
|
|
+ if (idx === -1) return
|
|
|
+ const doc = this.allDocuments[idx]
|
|
|
+ const action = doc.status === 'enabled' ? 'disable' : 'enable'
|
|
|
+
|
|
|
+ const ok = await this.apiCall(2026052714301016, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ document_ids: [String(docId)],
|
|
|
+ action
|
|
|
+ })
|
|
|
+ if (ok && ok.code === 1) {
|
|
|
+ const newStatus = action === 'enable' ? 'enabled' : 'disabled'
|
|
|
+ // 用 $set 触发 Vue 2 响应式更新
|
|
|
+ this.$set(this.allDocuments, idx, { ...doc, status: newStatus })
|
|
|
+ // 同步更新详情抽屉的数据
|
|
|
+ if (this.selectedDocument && this.selectedDocument.id === docId) {
|
|
|
+ this.selectedDocument = { ...this.selectedDocument, status: newStatus }
|
|
|
+ }
|
|
|
+ this.$message.success(action === 'enable' ? '文档已启用' : '文档已禁用')
|
|
|
+ }
|
|
|
+ callback && callback()
|
|
|
+ },
|
|
|
+
|
|
|
+ async onDeleteDocument(docId, callback) {
|
|
|
+ const ok = await this.apiCall(2026052714301009, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ document_id: String(docId)
|
|
|
+ })
|
|
|
+ if (ok && ok.code === 1) {
|
|
|
+ const idx = this.allDocuments.findIndex(d => d.id === docId)
|
|
|
+ if (idx > -1) this.allDocuments.splice(idx, 1)
|
|
|
+ // 更新分页总数
|
|
|
+ if (this.docTotal > 0) this.docTotal--
|
|
|
+ // 更新左侧知识库列表的文档数
|
|
|
+ const kb = this.knowledgeBases.find(k => k.id === this.activeKBId)
|
|
|
+ if (kb && kb.docCount > 0) kb.docCount--
|
|
|
+ this.$message.success(this.$t('文档已删除'))
|
|
|
+ }
|
|
|
+ callback && callback()
|
|
|
+ },
|
|
|
+
|
|
|
+ triggerDownload(url, filename) {
|
|
|
+ const a = document.createElement('a')
|
|
|
+ a.href = url
|
|
|
+ a.download = filename || ''
|
|
|
+ a.style.display = 'none'
|
|
|
+ document.body.appendChild(a)
|
|
|
+ a.click()
|
|
|
+ document.body.removeChild(a)
|
|
|
+ },
|
|
|
+
|
|
|
+ async onDownloadDocument(doc) {
|
|
|
+ const res = await this.apiCall(2026052714301010, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ document_id: String(doc.id)
|
|
|
+ })
|
|
|
+ if (res && res.code === 1 && res.data) {
|
|
|
+ const url = res.data.url || res.data.download_url || res.data.downloadUrl
|
|
|
+ if (url) {
|
|
|
+ const fullUrl = res.difyBaseUrl ? res.difyBaseUrl + url : url
|
|
|
+ this.triggerDownload(fullUrl, doc.name || '')
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async onBatchDownload(ids) {
|
|
|
+ for (const docId of ids) {
|
|
|
+ const doc = this.allDocuments.find(d => d.id === docId)
|
|
|
+ if (doc) {
|
|
|
+ await this.onDownloadDocument(doc)
|
|
|
+ await new Promise(r => setTimeout(r, 300))
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // ====== 元数据 API ======
|
|
|
+ async fetchMetadata() {
|
|
|
+ if (!this.activeKBId) return
|
|
|
+ const [sysRes, customRes] = await Promise.all([
|
|
|
+ this.apiCall(2026052715313903, { dataset_id: String(this.activeKBId), iscustom: false }),
|
|
|
+ this.apiCall(2026052715313903, { dataset_id: String(this.activeKBId), iscustom: true })
|
|
|
+ ])
|
|
|
+ // Dify 类型 → 前端显示类型
|
|
|
+ const difyToFrontend = (t) => {
|
|
|
+ const map = { string: 'text', number: 'number', time: 'date' }
|
|
|
+ return map[t] || 'text'
|
|
|
+ }
|
|
|
+ const fields = []
|
|
|
+ // 系统元数据:data.fields[]
|
|
|
+ if (sysRes && sysRes.code === 1 && sysRes.data) {
|
|
|
+ const list = sysRes.data.fields || []
|
|
|
+ list.forEach(item => {
|
|
|
+ fields.push({
|
|
|
+ id: item.name,
|
|
|
+ name: item.name,
|
|
|
+ key: item.name,
|
|
|
+ type: difyToFrontend(item.type),
|
|
|
+ options: [],
|
|
|
+ isSystem: true,
|
|
|
+ enabled: true
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ // 自定义元数据:data.doc_metadata[]
|
|
|
+ if (customRes && customRes.code === 1 && customRes.data) {
|
|
|
+ this.sysMetaEnabled = customRes.data.built_in_field_enabled !== false
|
|
|
+ const list = customRes.data.doc_metadata || []
|
|
|
+ list.forEach(item => {
|
|
|
+ fields.push({
|
|
|
+ id: item.id,
|
|
|
+ name: item.name,
|
|
|
+ key: item.name,
|
|
|
+ type: difyToFrontend(item.type),
|
|
|
+ options: [],
|
|
|
+ isSystem: false,
|
|
|
+ enabled: true
|
|
|
+ })
|
|
|
+ })
|
|
|
+ }
|
|
|
+ this.metadataFields = fields
|
|
|
+ },
|
|
|
+
|
|
|
+ async onCreateField(form, callback) {
|
|
|
+ // 前端类型 → Dify 类型映射
|
|
|
+ const typeMap = { text: 'string', date: 'time' }
|
|
|
+ const res = await this.apiCall(2026052715313904, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ name: form.name,
|
|
|
+ type: typeMap[form.type] || 'string'
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success(this.$t('元数据创建成功'))
|
|
|
+ this.fetchMetadata()
|
|
|
+ }
|
|
|
+ callback && callback()
|
|
|
+ },
|
|
|
+
|
|
|
+ async onUpdateField({ id, name }, callback) {
|
|
|
+ const res = await this.apiCall(2026052715313906, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ metadata_id: String(id),
|
|
|
+ name
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success(this.$t('元数据更新成功'))
|
|
|
+ this.fetchMetadata()
|
|
|
+ }
|
|
|
+ callback && callback()
|
|
|
+ },
|
|
|
+
|
|
|
+ async onDeleteField(id, callback) {
|
|
|
+ const res = await this.apiCall(2026052715313905, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ metadata_id: String(id)
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success(this.$t('元数据删除成功'))
|
|
|
+ this.fetchMetadata()
|
|
|
+ }
|
|
|
+ callback && callback()
|
|
|
+ },
|
|
|
+
|
|
|
+ async onToggleSystemMeta() {
|
|
|
+ const res = await this.apiCall(2026052715313907, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ enable: !this.sysMetaEnabled
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success(this.sysMetaEnabled ? this.$t('系统元数据已禁用') : this.$t('系统元数据已启用'))
|
|
|
+ this.fetchMetadata()
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ async onSaveDocMetadata({ docId, metadata }) {
|
|
|
+ const metadataArr = Object.entries(metadata).map(([key, value]) => {
|
|
|
+ const field = this.metadataFields.find(f => f.key === key)
|
|
|
+ return {
|
|
|
+ id: field ? field.id : key,
|
|
|
+ name: field ? field.name : key,
|
|
|
+ value: Array.isArray(value) ? value.join(',') : (value || '')
|
|
|
+ }
|
|
|
+ }).filter(item => item.value)
|
|
|
+
|
|
|
+ const res = await this.apiCall(2026053015572501, {
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
+ document_id: String(docId),
|
|
|
+ partial_update: true,
|
|
|
+ metadata: metadataArr
|
|
|
+ })
|
|
|
+ if (res && res.code === 1) {
|
|
|
+ this.$message.success(this.$t('元数据保存成功'))
|
|
|
+ // 重新获取文档详情,确保数据一致
|
|
|
+ this.fetchDocumentDetail(docId)
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ // ====== 工具方法 ======
|
|
|
+ formatSize(bytes) {
|
|
|
+ if (!bytes || bytes <= 0) return '-'
|
|
|
+ if (bytes < 1024) return bytes + ' B'
|
|
|
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
|
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
|
+ },
|
|
|
+ formatTimestamp(ts) {
|
|
|
+ if (!ts) return ''
|
|
|
+ const d = new Date(ts * 1000)
|
|
|
+ const pad = n => String(n).padStart(2, '0')
|
|
|
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
|
|
+ },
|
|
|
+ getTagColor(tag) {
|
|
|
+ if (!this.tagColorMap[tag]) {
|
|
|
+ const idx = Object.keys(this.tagColorMap).length % TAG_COLORS.length
|
|
|
+ this.$set(this.tagColorMap, tag, TAG_COLORS[idx])
|
|
|
+ }
|
|
|
+ return this.tagColorMap[tag]
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.kb-manage {
|
|
|
+ display: flex;
|
|
|
+ height: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-left-panel {
|
|
|
+ width: 280px;
|
|
|
+ min-width: 280px;
|
|
|
+ border-right: 1px solid #e4e7ed;
|
|
|
+ background: #fff;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-right-panel {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: #f0f2f5;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-detail-header {
|
|
|
+ flex-shrink: 0;
|
|
|
+ padding: 16px 20px;
|
|
|
+ background: #fff;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-detail-name {
|
|
|
+ margin: 0 0 8px 0;
|
|
|
+ font-size: 18px;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-detail-desc {
|
|
|
+ margin: 0 0 10px 0;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-detail-meta {
|
|
|
+ display: flex;
|
|
|
+ gap: 24px;
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ align-items: center;
|
|
|
+ flex-wrap: wrap;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-tag-inline { margin: 0 2px; }
|
|
|
+.kb-no-tag { color: #c0c4cc; }
|
|
|
+
|
|
|
+.kb-tabs-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-tabs {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ background: #fff;
|
|
|
+ padding: 0 20px 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-tabs >>> .el-tabs__content {
|
|
|
+ flex: 1;
|
|
|
+ overflow: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-tabs >>> .el-tabs__header {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-empty-state {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ color: #c0c4cc;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-empty-state i {
|
|
|
+ font-size: 64px;
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.kb-empty-state p {
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+</style>
|