|
@@ -0,0 +1,509 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="kb-manage">
|
|
|
|
|
+ <!-- 左侧:知识库列表 -->
|
|
|
|
|
+ <div class="kb-left-panel">
|
|
|
|
|
+ <KnowledgeBaseList
|
|
|
|
|
+ :list="filteredKBList"
|
|
|
|
|
+ :activeId="activeKBId"
|
|
|
|
|
+ :searchKey="kbSearchKey"
|
|
|
|
|
+ @search="kbSearchKey = $event"
|
|
|
|
|
+ @select="onSelectKB"
|
|
|
|
|
+ @create="onCreateKB"
|
|
|
|
|
+ @rename="onRenameKB"
|
|
|
|
|
+ @delete="onDeleteKB"
|
|
|
|
|
+ @set-tags="onSetTags"
|
|
|
|
|
+ />
|
|
|
|
|
+ </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 || '暂无描述' }}</p>
|
|
|
|
|
+ <div class="kb-detail-meta">
|
|
|
|
|
+ <span>创建时间:{{ activeKB.createdAt }}</span>
|
|
|
|
|
+ <span>更新时间:{{ activeKB.updatedAt }}</span>
|
|
|
|
|
+ <span>
|
|
|
|
|
+ 标签:
|
|
|
|
|
+ <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">无</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"
|
|
|
|
|
+ :metadataFields="metadataFields"
|
|
|
|
|
+ @upload="openUploadDialog"
|
|
|
|
|
+ @detail="openDocumentDetail"
|
|
|
|
|
+ @toggle-status="onToggleDocStatus"
|
|
|
|
|
+ @delete="onDeleteDocument"
|
|
|
|
|
+ @download="onDownloadDocument"
|
|
|
|
|
+ @batch-download="onBatchDownload"
|
|
|
|
|
+ @refresh="loadDocuments"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+ <el-tab-pane label="元数据配置" name="metadata">
|
|
|
|
|
+ <MetadataConfig
|
|
|
|
|
+ :fields="metadataFields"
|
|
|
|
|
+ @create="onCreateField"
|
|
|
|
|
+ @update="onUpdateField"
|
|
|
|
|
+ @delete="onDeleteField"
|
|
|
|
|
+ @toggle="onToggleField"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+ <el-tab-pane label="检索测试" name="search">
|
|
|
|
|
+ <SearchTest :kbId="activeKBId" :documents="currentDocuments" />
|
|
|
|
|
+ </el-tab-pane>
|
|
|
|
|
+ </el-tabs>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ <div v-else class="kb-empty-state">
|
|
|
|
|
+ <i class="el-icon-document"></i>
|
|
|
|
|
+ <p>{{ knowledgeBases.length === 0 ? '加载中...' : '请选择左侧知识库查看详情' }}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 上传文档弹窗 -->
|
|
|
|
|
+ <UploadDocument
|
|
|
|
|
+ :visible="uploadDialogVisible"
|
|
|
|
|
+ :metadataFields="enabledMetadataFields"
|
|
|
|
|
+ @close="uploadDialogVisible = false"
|
|
|
|
|
+ @submit="onUploadDocument"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 文档详情抽屉 -->
|
|
|
|
|
+ <DocumentDetail
|
|
|
|
|
+ :visible="detailDrawerVisible"
|
|
|
|
|
+ :document="selectedDocument"
|
|
|
|
|
+ :metadataFields="enabledMetadataFields"
|
|
|
|
|
+ @close="detailDrawerVisible = false"
|
|
|
|
|
+ @save-meta="onSaveDocMetadata"
|
|
|
|
|
+ @toggle-status="onToggleDocStatus"
|
|
|
|
|
+ @retry-embed="onRetryEmbed"
|
|
|
|
|
+ @delete="onDeleteDocument"
|
|
|
|
|
+ />
|
|
|
|
|
+ </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'
|
|
|
|
|
+import SearchTest from './SearchTest.vue'
|
|
|
|
|
+
|
|
|
|
|
+const TAG_COLORS = [
|
|
|
|
|
+ '#409EFF', '#67C23A', '#E6A23C', '#F56C6C',
|
|
|
|
|
+ '#909399', '#B37FEB', '#13C2C2', '#FA8C16'
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+// [缺] 后端未提供以下接口,前端暂不支持:
|
|
|
|
|
+const GAP_MSG = '该功能暂未开放,请等待后端接口就绪'
|
|
|
|
|
+
|
|
|
|
|
+export default {
|
|
|
|
|
+ name: 'KBManagement',
|
|
|
|
|
+ components: {
|
|
|
|
|
+ KnowledgeBaseList,
|
|
|
|
|
+ DocumentTable,
|
|
|
|
|
+ UploadDocument,
|
|
|
|
|
+ DocumentDetail,
|
|
|
|
|
+ MetadataConfig,
|
|
|
|
|
+ SearchTest
|
|
|
|
|
+ },
|
|
|
|
|
+ data() {
|
|
|
|
|
+ return {
|
|
|
|
|
+ activeKBId: null,
|
|
|
|
|
+ kbSearchKey: '',
|
|
|
|
|
+ activeTab: 'docs',
|
|
|
|
|
+ uploadDialogVisible: false,
|
|
|
|
|
+ detailDrawerVisible: false,
|
|
|
|
|
+ selectedDocument: null,
|
|
|
|
|
+ tagColorMap: {},
|
|
|
|
|
+
|
|
|
|
|
+ // 全部从接口获取
|
|
|
|
|
+ knowledgeBases: [],
|
|
|
|
|
+ allDocuments: [],
|
|
|
|
|
+ metadataFields: []
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ 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() {
|
|
|
|
|
+ if (!this.activeKBId) return []
|
|
|
|
|
+ return this.allDocuments.filter(doc => doc.kbId === this.activeKBId)
|
|
|
|
|
+ },
|
|
|
|
|
+ enabledMetadataFields() {
|
|
|
|
|
+ return this.metadataFields.filter(f => f.enabled)
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+ 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
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ gapWarn(feature) {
|
|
|
|
|
+ console.warn(`[GAP] ${feature}`)
|
|
|
|
|
+ this.$message.warning(GAP_MSG)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ====== 知识库 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()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ 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: '',
|
|
|
|
|
+ status: '',
|
|
|
|
|
+ pageNumber: 1,
|
|
|
|
|
+ pageSize: 999
|
|
|
|
|
+ })
|
|
|
|
|
+ if (res && res.code === 1 && res.data) {
|
|
|
|
|
+ const list = res.data.list || res.data.data || res.data || []
|
|
|
|
|
+ this.allDocuments = list.map(item => ({
|
|
|
|
|
+ id: item.id || item.document_id,
|
|
|
|
|
+ kbId: this.activeKBId,
|
|
|
|
|
+ name: item.name || item.document_name || item.file_name || '',
|
|
|
|
|
+ size: item.size || item.file_size || '-',
|
|
|
|
|
+ type: (item.type || item.file_type || item.name || '').split('.').pop().toLowerCase(),
|
|
|
|
|
+ version: item.version || 1,
|
|
|
|
|
+ status: item.status || 'enabled',
|
|
|
|
|
+ embedStatus: item.indexing_status || item.embedStatus || 'pending',
|
|
|
|
|
+ embedProgress: item.completed_segments !== undefined && item.total_segments
|
|
|
|
|
+ ? Math.round(item.completed_segments / item.total_segments * 100)
|
|
|
|
|
+ : (item.embedProgress || 0),
|
|
|
|
|
+ uploadTime: item.created_at || item.uploadTime || '',
|
|
|
|
|
+ metadata: item.metadata || item.doc_metadata || {}
|
|
|
|
|
+ }))
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ 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: d.size || d.file_size || this.allDocuments[idx].size,
|
|
|
|
|
+ type: (d.type || d.file_type || d.name || '').split('.').pop().toLowerCase(),
|
|
|
|
|
+ version: d.version || this.allDocuments[idx].version,
|
|
|
|
|
+ status: d.status || this.allDocuments[idx].status,
|
|
|
|
|
+ embedStatus: d.indexing_status || d.embedStatus || this.allDocuments[idx].embedStatus,
|
|
|
|
|
+ embedProgress: d.embedProgress || this.allDocuments[idx].embedProgress,
|
|
|
|
|
+ metadata: d.metadata || d.doc_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()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onCreateKB() {
|
|
|
|
|
+ this.gapWarn('创建知识库')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onRenameKB() {
|
|
|
|
|
+ this.gapWarn('更新知识库')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onDeleteKB() {
|
|
|
|
|
+ this.gapWarn('删除知识库')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onSetTags() {
|
|
|
|
|
+ this.gapWarn('设置知识库标签')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ====== 文档操作 ======
|
|
|
|
|
+ openUploadDialog() {
|
|
|
|
|
+ this.gapWarn('上传文档(需先接入OBS获取attachmentid)')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onUploadDocument() {
|
|
|
|
|
+ this.uploadDialogVisible = false
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ loadDocuments() {
|
|
|
|
|
+ this.fetchDocumentList()
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ openDocumentDetail(doc) {
|
|
|
|
|
+ this.selectedDocument = { ...doc }
|
|
|
|
|
+ this.detailDrawerVisible = true
|
|
|
|
|
+ this.fetchDocumentDetail(doc.id)
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onSaveDocMetadata() {
|
|
|
|
|
+ this.gapWarn('设置文档元数据')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async onToggleDocStatus(docId) {
|
|
|
|
|
+ const doc = this.allDocuments.find(d => d.id === docId)
|
|
|
|
|
+ if (!doc) return
|
|
|
|
|
+ const newStatus = doc.status === 'enabled' ? 'disabled' : 'enabled'
|
|
|
|
|
+ const action = newStatus === 'enabled' ? 'enable' : 'disable'
|
|
|
|
|
+
|
|
|
|
|
+ const ok = await this.apiCall(2026052714301016, {
|
|
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
|
|
+ document_ids: [String(docId)],
|
|
|
|
|
+ action
|
|
|
|
|
+ })
|
|
|
|
|
+ if (ok && ok.code === 1) {
|
|
|
|
|
+ doc.status = newStatus
|
|
|
|
|
+ this.$message.success(newStatus === 'enabled' ? '文档已启用' : '文档已禁用')
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async onDeleteDocument(docId) {
|
|
|
|
|
+ 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)
|
|
|
|
|
+ this.$message.success('文档已删除')
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ 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.download_url || res.data.downloadUrl || res.data.url
|
|
|
|
|
+ if (url) window.open(url)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async onBatchDownload(ids) {
|
|
|
|
|
+ const res = await this.apiCall(2026052714301011, {
|
|
|
|
|
+ dataset_id: String(this.activeKBId),
|
|
|
|
|
+ document_ids: ids.map(String)
|
|
|
|
|
+ })
|
|
|
|
|
+ if (res && res.code === 1 && res.data) {
|
|
|
|
|
+ const url = res.data.download_url || res.data.downloadUrl || res.data.url
|
|
|
|
|
+ if (url) window.open(url)
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ onRetryEmbed() {
|
|
|
|
|
+ this.gapWarn('重新向量化')
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // ====== 元数据操作 ======
|
|
|
|
|
+ onCreateField() { this.gapWarn('元数据管理') },
|
|
|
|
|
+ onUpdateField() { this.gapWarn('元数据管理') },
|
|
|
|
|
+ onDeleteField() { this.gapWarn('元数据管理') },
|
|
|
|
|
+ onToggleField() { this.gapWarn('元数据管理') },
|
|
|
|
|
+
|
|
|
|
|
+ // ====== 工具方法 ======
|
|
|
|
|
+ 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>
|