Explorar el Código

refactor: 优化文档详情和知识库列表

- DocumentDetail: 嵌入状态支持7种状态显示(parsing/cleaning/splitting/indexing等)
- KnowledgeBaseList: 移除新建/重命名/删除知识库功能(后端无接口)
- UploadDocument: 修复文件列表同步问题
Zachary hace 4 días
padre
commit
c66d998eeb

+ 37 - 18
src/AIManagement/KBM/DocumentDetail.vue

@@ -18,8 +18,6 @@
           <p class="dd-file-meta">
             <span>{{ document.size }}</span>
             <span class="dd-meta-divider">|</span>
-            <span>v{{ document.version }}</span>
-            <span class="dd-meta-divider">|</span>
             <span>{{ document.type ? document.type.toUpperCase() : '-' }}</span>
           </p>
         </div>
@@ -47,25 +45,25 @@
       <div class="dd-section">
         <h4 class="dd-section-title">向量化嵌入</h4>
         <div class="dd-embed-card" :class="'dd-embed--' + document.embedStatus">
-          <div class="dd-embed-icon">
-            <i v-if="document.embedStatus === 'completed'" class="el-icon-circle-check"></i>
-            <i v-else-if="document.embedStatus === 'processing'" class="el-icon-loading"></i>
-            <i v-else-if="document.embedStatus === 'failed'" class="el-icon-circle-close"></i>
-            <i v-else class="el-icon-time"></i>
-          </div>
-          <div class="dd-embed-body">
-            <p class="dd-embed-text">
-              {{ document.embedStatus === 'completed' ? '嵌入完成' : document.embedStatus === 'processing' ? '正在向量化处理中...' : document.embedStatus === 'failed' ? '嵌入失败' : '等待处理' }}
-            </p>
+        <div class="dd-embed-icon">
+          <i v-if="document.embedStatus === 'completed'" class="el-icon-circle-check"></i>
+          <i v-else-if="['parsing','cleaning','splitting','indexing'].includes(document.embedStatus)" class="el-icon-loading"></i>
+          <i v-else-if="document.embedStatus === 'error'" class="el-icon-circle-close"></i>
+          <i v-else class="el-icon-time"></i>
+        </div>
+        <div class="dd-embed-body">
+          <p class="dd-embed-text">
+            {{ embedStatusText }}
+          </p>
             <el-progress
-              v-if="document.embedStatus === 'processing'"
+              v-if="['parsing','cleaning','splitting','indexing'].includes(document.embedStatus)"
               :percentage="document.embedProgress"
               :stroke-width="6"
               :show-text="false"
               style="margin-top: 6px;"
             />
             <el-button
-              v-if="document.embedStatus === 'failed'"
+              v-if="document.embedStatus === 'error'"
               type="text"
               size="mini"
               style="padding: 0; margin-top: 4px;"
@@ -200,6 +198,19 @@ export default {
       if (['xlsx', 'xls'].includes(t)) return 'is-excel'
       if (['pptx', 'ppt'].includes(t)) return 'is-ppt'
       return ''
+    },
+    embedStatusText() {
+      if (!this.document) return ''
+      const map = {
+        waiting: '等待处理',
+        parsing: '正在解析文档...',
+        cleaning: '正在清洗内容...',
+        splitting: '正在分段...',
+        indexing: '正在向量化...',
+        completed: '嵌入完成',
+        error: '嵌入失败'
+      }
+      return map[this.document.embedStatus] || '等待处理'
     }
   },
   watch: {
@@ -361,9 +372,13 @@ export default {
 }
 
 .dd-embed--completed { background: #f0f9eb; }
+.dd-embed--parsing { background: #ecf5ff; }
+.dd-embed--cleaning { background: #ecf5ff; }
+.dd-embed--splitting { background: #ecf5ff; }
+.dd-embed--indexing { background: #ecf5ff; }
 .dd-embed--processing { background: #ecf5ff; }
-.dd-embed--failed     { background: #fef0f0; }
-.dd-embed--pending    { background: #fafbfc; }
+.dd-embed--error     { background: #fef0f0; }
+.dd-embed--waiting   { background: #fafbfc; }
 
 .dd-embed-icon {
   font-size: 22px;
@@ -371,9 +386,13 @@ export default {
   margin-top: 2px;
 }
 .dd-embed--completed .dd-embed-icon { color: #67C23A; }
+.dd-embed--parsing .dd-embed-icon { color: #3874F6; }
+.dd-embed--cleaning .dd-embed-icon { color: #3874F6; }
+.dd-embed--splitting .dd-embed-icon { color: #3874F6; }
+.dd-embed--indexing .dd-embed-icon { color: #3874F6; }
 .dd-embed--processing .dd-embed-icon { color: #3874F6; }
-.dd-embed--failed     .dd-embed-icon { color: #F56C6C; }
-.dd-embed--pending    .dd-embed-icon { color: #c0c4cc; }
+.dd-embed--error     .dd-embed-icon { color: #F56C6C; }
+.dd-embed--waiting   .dd-embed-icon { color: #c0c4cc; }
 
 .dd-embed-body { flex: 1; }
 .dd-embed-text { margin: 0; font-size: 13px; color: #303133; }

+ 1 - 229
src/AIManagement/KBM/KnowledgeBaseList.vue

@@ -10,9 +10,6 @@
         clearable
         @input="$emit('search', searchText)"
       />
-      <el-button type="primary" size="small" icon="el-icon-plus" class="kbl-create-btn" @click="openCreateDialog">
-        新建知识库
-      </el-button>
     </div>
 
     <!-- 列表 -->
@@ -23,7 +20,6 @@
         class="kbl-item"
         :class="{ 'kbl-item--active': activeId === kb.id }"
         @click="$emit('select', kb.id)"
-        @contextmenu.prevent="openContextMenu($event, kb)"
       >
         <div class="kbl-item-top">
           <span class="kbl-item-icon"><i class="el-icon-folder-opened"></i></span>
@@ -48,82 +44,10 @@
 
       <div v-if="list.length === 0" class="kbl-empty">
         <p>暂无知识库</p>
-        <p class="kbl-empty-tip">点击上方按钮创建</p>
       </div>
     </div>
 
-    <!-- 右键菜单 -->
-    <div
-      v-show="contextMenu.visible"
-      class="kbl-context-menu"
-      :style="{ left: contextMenu.x + 'px', top: contextMenu.y + 'px' }"
-    >
-      <div class="kbl-menu-item" @click="openRenameDialog"><i class="el-icon-edit"></i> 重命名</div>
-      <div class="kbl-menu-item" @click="openTagDialog"><i class="el-icon-price-tag"></i> 设置标签</div>
-      <div class="kbl-menu-item kbl-menu-item--danger" @click="confirmDelete"><i class="el-icon-delete"></i> 删除</div>
-    </div>
-
-    <!-- 新建/重命名弹窗 -->
-    <el-dialog
-      :title="dialogTitle"
-      :visible.sync="dialogVisible"
-      width="460px"
-      :close-on-click-modal="false"
-      append-to-body
-    >
-      <el-form ref="kbForm" :model="formData" :rules="formRules" label-width="60px" size="small">
-        <el-form-item label="名称" prop="name">
-          <el-input v-model="formData.name" placeholder="请输入知识库名称" maxlength="30" />
-        </el-form-item>
-        <el-form-item label="描述" prop="description">
-          <el-input
-            v-model="formData.description"
-            type="textarea"
-            :rows="3"
-            placeholder="请输入描述(选填)"
-            maxlength="200"
-          />
-        </el-form-item>
-      </el-form>
-      <span slot="footer">
-        <el-button size="small" @click="dialogVisible = false">取 消</el-button>
-        <el-button size="small" type="primary" @click="submitForm">确 定</el-button>
-      </span>
-    </el-dialog>
 
-    <!-- 设置标签弹窗 -->
-    <el-dialog
-      title="设置标签"
-      :visible.sync="tagDialogVisible"
-      width="500px"
-      :close-on-click-modal="false"
-      append-to-body
-    >
-      <div class="kbl-tag-editor">
-        <el-tag
-          v-for="tag in editTags"
-          :key="tag"
-          closable
-          :disable-transitions="false"
-          size="small"
-          @close="removeTag(tag)"
-        >{{ tag }}</el-tag>
-        <el-input
-          v-if="tagInputVisible"
-          ref="tagInput"
-          v-model="tagInputValue"
-          size="mini"
-          class="kbl-tag-input"
-          @keyup.enter.native="addTag"
-          @blur="addTag"
-        />
-        <el-button v-else size="mini" @click="showTagInput">+ 添加标签</el-button>
-      </div>
-      <span slot="footer">
-        <el-button size="small" @click="tagDialogVisible = false">取 消</el-button>
-        <el-button size="small" type="primary" @click="submitTags">确 定</el-button>
-      </span>
-    </el-dialog>
   </div>
 </template>
 
@@ -143,115 +67,11 @@ export default {
   data() {
     return {
       searchText: '',
-      contextMenu: { visible: false, x: 0, y: 0, kb: null },
-      dialogVisible: false,
-      dialogMode: 'create',
-      formData: { name: '', description: '' },
-      formRules: {
-        name: [{ required: true, message: '请输入知识库名称', trigger: 'blur' }]
-      },
-      tagDialogVisible: false,
-      editTags: [],
-      tagInputVisible: false,
-      tagInputValue: '',
-      tagEditKB: null,
       tagColorMap: {}
     }
   },
-  computed: {
-    dialogTitle() {
-      return this.dialogMode === 'create' ? '新建知识库' : '重命名知识库'
-    }
-  },
-  methods: {
-    openCreateDialog() {
-      this.dialogMode = 'create'
-      this.formData = { name: '', description: '' }
-      this.dialogVisible = true
-      this.$nextTick(() => { this.$refs.kbForm && this.$refs.kbForm.clearValidate() })
-    },
-
-    openRenameDialog() {
-      const kb = this.contextMenu.kb
-      this.dialogMode = 'rename'
-      this.formData = { name: kb.name, description: kb.description || '' }
-      this.dialogVisible = true
-      this.hideContextMenu()
-      this.$nextTick(() => { this.$refs.kbForm && this.$refs.kbForm.clearValidate() })
-    },
-
-    submitForm() {
-      this.$refs.kbForm.validate(valid => {
-        if (!valid) return
-        if (this.dialogMode === 'create') {
-          this.$emit('create', { ...this.formData })
-        } else {
-          const kb = this.contextMenu.kb
-          this.$emit('rename', { id: kb.id, name: this.formData.name, description: this.formData.description })
-        }
-        this.dialogVisible = false
-      })
-    },
-
-    confirmDelete() {
-      const kb = this.contextMenu.kb
-      this.hideContextMenu()
-      this.$confirm(`确定删除知识库「${kb.name}」吗?知识库下的所有文档也将被删除。`, '删除确认', {
-        confirmButtonText: '确定删除',
-        cancelButtonText: '取消',
-        type: 'warning'
-      }).then(() => {
-        this.$emit('delete', kb.id)
-      }).catch(() => {})
-    },
-
-    // 标签
-    openTagDialog() {
-      const kb = this.contextMenu.kb
-      this.tagEditKB = kb
-      this.editTags = [...(kb.tags || [])]
-      this.tagDialogVisible = true
-      this.hideContextMenu()
-    },
-
-    showTagInput() {
-      this.tagInputVisible = true
-      this.$nextTick(() => {
-        this.$refs.tagInput && this.$refs.tagInput.$refs.input.focus()
-      })
-    },
-
-    addTag() {
-      const val = this.tagInputValue.trim()
-      if (val && !this.editTags.includes(val)) {
-        this.editTags.push(val)
-      }
-      this.tagInputVisible = false
-      this.tagInputValue = ''
-    },
-
-    removeTag(tag) {
-      const idx = this.editTags.indexOf(tag)
-      if (idx > -1) this.editTags.splice(idx, 1)
-    },
-
-    submitTags() {
-      if (this.tagEditKB) {
-        this.$emit('set-tags', { id: this.tagEditKB.id, tags: this.editTags })
-      }
-      this.tagDialogVisible = false
-    },
-
-    // 右键菜单
-    openContextMenu(e, kb) {
-      this.contextMenu = { visible: true, x: e.clientX, y: e.clientY, kb }
-      document.addEventListener('click', this.hideContextMenu, { once: true })
-    },
-
-    hideContextMenu() {
-      this.contextMenu.visible = false
-    },
 
+  methods: {
     getTagColor(tag) {
       if (!this.tagColorMap[tag]) {
         const i = Object.keys(this.tagColorMap).length % TAG_COLORS.length
@@ -275,10 +95,6 @@ export default {
   border-bottom: 1px solid #e4e7ed;
 }
 
-.kbl-create-btn {
-  width: 100%;
-  margin-top: 8px;
-}
 
 .kbl-list {
   flex: 1;
@@ -367,49 +183,5 @@ export default {
   margin-top: 4px;
 }
 
-/* 右键菜单 */
-.kbl-context-menu {
-  position: fixed;
-  z-index: 3000;
-  background: #fff;
-  border: 1px solid #e4e7ed;
-  border-radius: 4px;
-  box-shadow: 0 2px 12px rgba(0,0,0,0.12);
-  padding: 4px 0;
-  min-width: 140px;
-}
-
-.kbl-menu-item {
-  padding: 8px 16px;
-  font-size: 13px;
-  cursor: pointer;
-  display: flex;
-  align-items: center;
-  gap: 6px;
-}
-
-.kbl-menu-item:hover {
-  background: #f5f7fa;
-}
 
-.kbl-menu-item--danger {
-  color: #F56C6C;
-}
-
-.kbl-menu-item--danger:hover {
-  background: #fef0f0;
-}
-
-/* 标签编辑 */
-.kbl-tag-editor {
-  display: flex;
-  flex-wrap: wrap;
-  gap: 8px;
-  align-items: center;
-  min-height: 32px;
-}
-
-.kbl-tag-input {
-  width: 100px;
-}
 </style>

+ 2 - 0
src/AIManagement/KBM/UploadDocument.vue

@@ -125,11 +125,13 @@ export default {
     },
 
     handleFileChange(file) {
+      this.fileList = this.$refs.upload.uploadFiles
       this.form.files = this.$refs.upload.uploadFiles.map(f => f.raw).filter(Boolean)
     },
 
     handleFileRemove() {
       this.$nextTick(() => {
+        this.fileList = this.$refs.upload.uploadFiles
         this.form.files = this.$refs.upload.uploadFiles.map(f => f.raw).filter(Boolean)
       })
     },