Ver Fonte

优化界面2

chenkun há 2 semanas atrás
pai
commit
da6ded8850

+ 16 - 0
src/api/image.ts

@@ -102,5 +102,21 @@ export const imageApi = {
       project_name: projectName,
       tags: tags
     })
+  },
+
+  // 按分类批量加入任务中心
+  categoryBatchAddToTask(categoryId: string, projectName: string, tags?: string[]): Promise<ApiResponse<{ sub_categories?: string[], image_count: number }>> {
+    return request.post(`${API_PREFIX}/category-batch-add-to-task`, {
+      category_id: categoryId,
+      project_name: projectName,
+      tags: tags
+    })
+  },
+
+  // 按分类批量推送前的预检查
+  categoryBatchCheck(categoryId: string): Promise<ApiResponse<{ sub_categories?: string[], image_count: number }>> {
+    return request.get(`${API_PREFIX}/category-batch-check`, {
+      params: { category_id: categoryId }
+    })
   }
 }

+ 22 - 13
src/views/basic-info/Standard.vue

@@ -45,9 +45,9 @@
           <el-col :span="6">
             <el-form-item label="时效性">
               <el-select v-model="searchForm.validity" placeholder="请选择时效性" clearable style="width: 100%">
-                <el-option label="现行" value="现行" />
-                <el-option label="已废止" value="已废止" />
-                <el-option label="被替代" value="被替代" />
+                <el-option label="现行" value="XH" />
+                <el-option label="废止" value="FZ" />
+                <el-option label="试行" value="SX" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -194,7 +194,7 @@
         <el-table-column prop="validity" label="时效性" width="100">
           <template #default="scope">
             <el-tag :type="getValidityType(scope.row.validity)">
-              {{ scope.row.validity || '现行' }}
+              {{ formatValidity(scope.row.validity) }}
             </el-tag>
           </template>
         </el-table-column>
@@ -327,9 +327,9 @@
           <el-col :span="12">
             <el-form-item label="时效性">
               <el-select v-model="editForm.validity" placeholder="请选择时效性" style="width: 100%">
-                <el-option label="现行" value="现行" />
-                <el-option label="已废止" value="已废止" />
-                <el-option label="被替代" value="被替代" />
+                <el-option label="现行" value="XH" />
+                <el-option label="废止" value="FZ" />
+                <el-option label="试行" value="SX" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -451,7 +451,7 @@
           <span v-else>-</span>
         </el-descriptions-item>
         <el-descriptions-item label="时效性">
-          <el-tag :type="getValidityType(currentItem?.validity)">{{ currentItem?.validity || '现行' }}</el-tag>
+          <el-tag :type="getValidityType(currentItem?.validity)">{{ formatValidity(currentItem?.validity) }}</el-tag>
         </el-descriptions-item>
         <el-descriptions-item label="创建人">{{ currentItem?.creator_name || '-' }}</el-descriptions-item>
         <el-descriptions-item label="创建时间">{{ formatDateTime(currentItem?.created_time) }}</el-descriptions-item>
@@ -576,7 +576,7 @@ const editForm = reactive<any>({
   release_date: '',
   document_type: '',
   professional_field: '',
-  validity: '现行',
+  validity: 'XH',
   note: '',
   file_url: '',
   english_name: '',
@@ -681,13 +681,22 @@ const formatDateTime = (date: string | undefined | null) => {
 
 const getValidityType = (validity: string) => {
   switch (validity) {
-    case '现行': return 'success'
-    case '已废止': return 'danger'
-    case '被替代': return 'warning'
+    case 'XH': return 'success'
+    case 'FZ': return 'danger'
+    case 'SX': return 'warning'
     default: return 'success'
   }
 }
 
+const formatValidity = (validity: string) => {
+  const map: Record<string, string> = {
+    'XH': '现行',
+    'FZ': '废止',
+    'SX': '试行'
+  }
+  return map[validity] || validity || '现行'
+}
+
 const proxyPreviewUrl = computed(() => {
   if (!previewUrl.value) return ''
   let baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000'
@@ -792,7 +801,7 @@ const handleAdd = () => {
     editForm[key] = ''
   })
   editForm.id = null
-  editForm.validity = '现行'
+  editForm.validity = 'XH'
   participatingUnitsList.value = ['']
   referenceBasisList.value = ['']
   fileList.value = []

+ 89 - 52
src/views/documents/Index.vue

@@ -87,6 +87,7 @@
       <el-empty v-if="documents.length === 0" description="暂无文档数据" />
       <el-table 
         v-else 
+        ref="multipleTableRef"
         :data="documents" 
         row-key="id"
         style="width: 100%" 
@@ -421,7 +422,7 @@
 
     <!-- 上传文档对话框 -->
     <el-dialog v-model="uploadDialogVisible" title="上传文档" width="500px">
-      <el-form :model="uploadForm" :rules="commonRules" ref="uploadFormRef" label-width="120px">
+      <el-form :model="uploadForm" :rules="commonRules" ref="uploadFormRef" label-width="120px" v-loading="uploadingFile" element-loading-text="正在上传文件到服务器...">
         <el-form-item label="基本信息类型" prop="table_type">
           <el-select v-model="uploadForm.table_type" placeholder="请选择基本信息类型">
             <el-option label="施工标准规范" value="standard" />
@@ -469,8 +470,8 @@
         </el-form-item>
       </el-form>
       <template #footer>
-        <el-button @click="uploadDialogVisible = false">取消</el-button>
-        <el-button type="primary" @click="submitUpload" :loading="submitting">确定</el-button>
+        <el-button @click="uploadDialogVisible = false" :disabled="uploadingFile">取消</el-button>
+        <el-button type="primary" @click="submitUpload" :loading="submitting" :disabled="uploadingFile">确定</el-button>
       </template>
     </el-dialog>
 
@@ -545,9 +546,9 @@
           <el-col :span="12" v-if="editForm.table_type === 'standard'">
             <el-form-item label="* 时效性">
               <el-select v-model="editForm.validity" placeholder="请选择时效性" style="width: 100%">
-                <el-option label="现行" value="现行" />
-                <el-option label="已废止" value="已废止" />
-                <el-option label="被替代" value="被替代" />
+                <el-option label="现行" value="XH" />
+                <el-option label="废止" value="FZ" />
+                <el-option label="试行" value="SX" />
               </el-select>
             </el-form-item>
           </el-col>
@@ -733,7 +734,7 @@
           <el-descriptions-item label="批准部门">{{ currentDoc?.approving_department || '-' }}</el-descriptions-item>
           <el-descriptions-item label="文件类型">{{ currentDoc?.document_type || '-' }}</el-descriptions-item>
           <el-descriptions-item label="专业领域">{{ currentDoc?.professional_field || '-' }}</el-descriptions-item>
-          <el-descriptions-item label="时效性">{{ currentDoc?.validity || '-' }}</el-descriptions-item>
+          <el-descriptions-item label="时效性">{{ getValidityName(currentDoc?.validity) }}</el-descriptions-item>
           <el-descriptions-item label="参编单位">
             <div v-if="currentDoc?.participating_units">
               <div v-for="(unit, idx) in currentDoc.participating_units.split(';')" :key="idx">{{ unit }}</div>
@@ -830,6 +831,7 @@ import { getKnowledgeBases } from '@/api/knowledge-base'
 
 // 状态变量
 const loading = ref(false)
+const uploadingFile = ref(false)
 const submitting = ref(false)
 const uploadRef = ref<any>(null)
 const uploadDialogVisible = ref(false)
@@ -920,6 +922,7 @@ const taskFormRef = ref()
 const ingestFormRef = ref()
 const uploadFormRef = ref()
 const editFormRef = ref()
+const multipleTableRef = ref()
 
 const commonRules = {
   kb_id: [
@@ -1118,6 +1121,15 @@ const getFileIconClass = (row: DocumentItem) => {
   return `icon-${getFileIcon(row)}`
 }
 
+const getValidityName = (val: string | null | undefined) => {
+  const names: Record<string, string> = {
+    XH: '现行',
+    FZ: '废止',
+    SX: '试行'
+  }
+  return names[val || ''] || val || '-'
+}
+
 const getSourceTypeName = (sourceType: string | null | undefined) => {
   const names: Record<string, string> = {
     standard: '施工标准规范',
@@ -1177,18 +1189,43 @@ const confirmIngest = async () => {
   }
   
   ingesting.value = true
-  
   try {
-    // 1. 临时请求选中 ID 的详情数据,确保校验时数据是最新的且完整的
-    const detailPromises = selectedIds.value.map(id => documentApi.getDetail(id))
+    const res = await request.post<ApiResponse>('/api/v1/sample/documents/batch-enter', {
+      ids: selectedIds.value,
+      kb_method: ingestForm.kb_method,
+      chunk_size: ingestForm.chunk_size,
+      separator: ingestForm.separator,
+      table_type: searchQuery.table_type
+    })
+    
+    if (res.code === 0) {
+      ElMessage.success(res.message || '已加入入库队列')
+      selectedIds.value = []
+      multipleTableRef.value?.clearSelection()
+      ingestDialogVisible.value = false
+      fetchDocuments()
+    }
+  } catch (error: any) {
+    if (error !== 'cancel') {
+      console.error('入库失败:', error)
+    }
+  } finally {
+    ingesting.value = false
+  }
+}
+
+// 提取字段检查逻辑
+const checkDocumentsCompleteness = async (ids: string[]) => {
+  loading.value = true
+  try {
+    const detailPromises = ids.map(id => documentApi.getDetail(id))
     const detailResults = await Promise.all(detailPromises)
-    const selectedDocs = detailResults.map(res => res.data)
+    const docs = detailResults.map(res => res.data)
     
-    // 2. 检查基本信息字段完善度
     let isIncomplete = false
     let missingFields: string[] = []
     
-    for (const doc of selectedDocs) {
+    for (const doc of docs) {
       const type = doc.source_type || searchQuery.table_type
       
       if (type === 'standard') {
@@ -1230,34 +1267,11 @@ const confirmIngest = async () => {
           }
         }
       }
-      if (isIncomplete && selectedIds.value.length === 1) break // 如果是单个,直接跳出
-    }
-
-    const startIngest = async () => {
-      try {
-        const res = await request.post<ApiResponse>('/api/v1/sample/documents/batch-enter', {
-          ids: selectedIds.value,
-          kb_method: ingestForm.kb_method,
-          chunk_size: ingestForm.chunk_size,
-          separator: ingestForm.separator,
-          table_type: searchQuery.table_type
-        })
-        
-        if (res.code === 0) {
-          ElMessage.success(res.message || '已加入入库队列')
-          selectedIds.value = []
-          ingestDialogVisible.value = false
-          fetchDocuments()
-        }
-      } catch (error: any) {
-        if (error !== 'cancel') {
-          console.error('入库失败:', error)
-        }
-      }
+      if (isIncomplete && ids.length === 1) break
     }
 
     if (isIncomplete) {
-      ingesting.value = false // 弹窗前先结束 loading,方便用户操作
+      loading.value = false // 弹出确认框前先关闭 loading
       try {
         await ElMessageBox.confirm(
           '该文档基本信息其中某几个字段未补充完善,可能导致无法检索的问题,你是否继续?',
@@ -1268,19 +1282,21 @@ const confirmIngest = async () => {
             type: 'warning',
           }
         )
-        ingesting.value = true // 继续后重新开启 loading
-        await startIngest()
+        return true
       } catch (error) {
-        // 用户取消
+        return false
       }
-    } else {
-      await startIngest()
     }
+    loading.value = false // 检查完成,关闭 loading
+    return true
   } catch (error) {
-    console.error('获取详情或入库失败:', error)
-    ElMessage.error('入库准备失败,请重试')
+    loading.value = false // 出错时也关闭 loading
+    console.error('检查文档完善度失败:', error)
+    ElMessage.error('检查文档完善度失败,请重试')
+    return false
   } finally {
-    ingesting.value = false
+    // 确保 loading 在所有路径下都被正确关闭
+    loading.value = false
   }
 }
 
@@ -1290,8 +1306,12 @@ const handleBatchEnter = async () => {
     return
   }
   
-  // 不再使用 confirm,改为显示入库设置弹窗
-  ingestForm.kb_method = 'length' // 重置为默认值
+  // 先检查字段完善度
+  const canContinue = await checkDocumentsCompleteness(selectedIds.value)
+  if (!canContinue) return
+
+  // 校验通过后,显示入库设置弹窗
+  ingestForm.kb_method = 'length'
   ingestForm.chunk_size = 500
   ingestForm.separator = '。'
   
@@ -1308,14 +1328,21 @@ const handleBatchAddToTask = async () => {
     return
   }
 
-  taskForm.project_name = '' // 重置项目名称
-  taskForm.selectedTags = [] // 重置标签
+  // 检查是否有未入库的数据
+  const unenteredDocs = documents.value.filter(doc => selectedIds.value.includes(doc.id) && !isEntered(doc.whether_to_enter))
+  if (unenteredDocs.length > 0) {
+    ElMessage.warning(`选中数据中包含 ${unenteredDocs.length} 份未入库文档,只有已入库的文档可以加入标注任务。`)
+    return
+  }
+
+  // 显示任务弹窗(填入项目名称和标签),不再进行字段检查
+  taskForm.project_name = ''
+  taskForm.selectedTags = []
   tempTagValue.value = null
   if (taskFormRef.value) {
     taskFormRef.value.clearValidate()
   }
   
-  // 获取标签树
   if (tagTree.value.length === 0) {
     await fetchTagTree()
   }
@@ -1340,6 +1367,7 @@ const confirmAddTask = async () => {
     if (res.code === 0) {
       ElMessage.success(res.message || '操作成功')
       selectedIds.value = []
+      multipleTableRef.value?.clearSelection()
       taskDialogVisible.value = false
       fetchDocuments()
     } else {
@@ -1356,8 +1384,12 @@ const confirmAddTask = async () => {
 const handleSingleEnter = async (doc: DocumentItem | null) => {
   if (!doc) return
   
+  // 先检查字段完善度
+  const canContinue = await checkDocumentsCompleteness([doc.id])
+  if (!canContinue) return
+
   selectedIds.value = [doc.id]
-  ingestForm.kb_method = 'length' // 重置为默认值
+  ingestForm.kb_method = 'length'
   ingestForm.chunk_size = 500
   ingestForm.separator = '。'
   
@@ -1426,6 +1458,7 @@ const handleBatchDelete = async () => {
     if (res.code === 0) {
       ElMessage.success(res.message || '批量删除成功')
       selectedIds.value = []
+      multipleTableRef.value?.clearSelection()
       fetchDocuments()
     } else {
       ElMessage.error(res.message || '批量删除失败')
@@ -1471,6 +1504,7 @@ const handleBatchClear = async () => {
     if (res.code === 0) {
       ElMessage.success(res.message || '数据清空成功')
       selectedIds.value = []
+      multipleTableRef.value?.clearSelection()
       fetchDocuments()
     } else {
       ElMessage.error(res.message || '数据清空失败')
@@ -1614,6 +1648,7 @@ const handleExceed = () => {
 const customUpload = async (options: any) => {
   const { file, onSuccess, onError } = options
   try {
+    uploadingFile.value = true
     // 1. 获取预签名 URL
     const res = await documentApi.getUploadUrl(file.name, file.type || 'application/octet-stream', uploadForm.table_type)
 
@@ -1643,6 +1678,8 @@ const customUpload = async (options: any) => {
     console.error('文件上传失败:', error)
     ElMessage.error(error.message || '文件上传失败')
     onError(error)
+  } finally {
+    uploadingFile.value = false
   }
 }
 

+ 32 - 2
src/views/documents/SearchEngine.vue

@@ -124,7 +124,20 @@
                         </el-col>
                         <el-col :span="1" style="text-align: center; color: #909399;">=</el-col>
                         <el-col :span="11">
+                            <el-select 
+                                v-if="filter.field === 'validity'"
+                                v-model="filter.value"
+                                placeholder="选择时效性"
+                                style="width: 100%"
+                                clearable
+                                @change="handleSearch"
+                            >
+                                <el-option label="现行" value="XH" />
+                                <el-option label="废止" value="FZ" />
+                                <el-option label="试行" value="SX" />
+                            </el-select>
                             <el-input 
+                                v-else
                                 v-model="filter.value" 
                                 placeholder="输入值" 
                                 :disabled="!filter.field"
@@ -391,13 +404,26 @@ const handleKbChange = async () => {
 const docNamesMap = ref<Record<string, string>>({})
 
 const formatMetaInfo = (row: any) => {
+    const getValidityName = (val: string | null | undefined) => {
+        const names: Record<string, string> = {
+            XH: '现行',
+            FZ: '废止',
+            SX: '试行'
+        }
+        return names[val || ''] || val || '-'
+    }
+
     // 优先展示 metadata 字段中的信息
     if (row.metadata) {
         // 如果是 JSON 对象,尝试格式化展示
         if (typeof row.metadata === 'object') {
              const displayParts = []
              for (const [key, value] of Object.entries(row.metadata)) {
-                 const valStr = typeof value === 'object' ? JSON.stringify(value) : String(value)
+                 let valStr = typeof value === 'object' ? JSON.stringify(value) : String(value)
+                 // 如果是时效性字段,转换简写为中文
+                 if (key === 'validity') {
+                     valStr = getValidityName(valStr)
+                 }
                  displayParts.push(`${key}: ${valStr}`)
              }
              if (displayParts.length > 0) return displayParts.join(' | ')
@@ -406,7 +432,11 @@ const formatMetaInfo = (row: any) => {
                 const metaObj = JSON.parse(row.metadata)
                 const displayParts = []
                 for (const [key, value] of Object.entries(metaObj)) {
-                    const valStr = typeof value === 'object' ? JSON.stringify(value) : String(value)
+                    let valStr = typeof value === 'object' ? JSON.stringify(value) : String(value)
+                    // 如果是时效性字段,转换简写为中文
+                    if (key === 'validity') {
+                        valStr = getValidityName(valStr)
+                    }
                     displayParts.push(`${key}: ${valStr}`)
                 }
                 if (displayParts.length > 0) return displayParts.join(' | ')

+ 82 - 2
src/views/images/Index.vue

@@ -7,6 +7,9 @@
           <el-button type="primary" :icon="Plus" @click="handleAddCategory">新增</el-button>
           <el-button :icon="Edit" @click="handleEditSelectedCategory" :disabled="!currentCategory || currentCategory.id === '0'">修改</el-button>
           <el-button type="danger" :icon="Delete" @click="handleDeleteSelectedCategory" :disabled="!currentCategory || currentCategory.id === '0'">删除</el-button>
+          <el-button type="warning" :icon="Tickets" @click="handleCategoryBatchAddToTask" style="width: 100%; margin-left: 0; margin-top: 10px;">
+            分类批量加入标注任务
+          </el-button>
         </div>
         <div class="aside-content">
           <el-tree
@@ -319,6 +322,7 @@ const taskRules = {
 }
 
 const taskFormRef = ref()
+const isCategoryBatch = ref(false)
 
 const categoryForm = reactive({
   id: '',
@@ -376,6 +380,7 @@ const handleSelectionChange = (selection: ImageItem[]) => {
 const handleBatchAddToTask = async () => {
   if (selectedIds.value.length === 0) return
   
+  isCategoryBatch.value = false
   taskForm.project_name = '' // 重置项目名称
   taskForm.selectedTags = [] // 重置标签
   tempTagValue.value = null
@@ -391,6 +396,60 @@ const handleBatchAddToTask = async () => {
   taskDialogVisible.value = true
 }
 
+const handleCategoryBatchAddToTask = async () => {
+  const categoryId = currentCategory.value?.id === '0' ? '' : (currentCategory.value?.id || '')
+  
+  try {
+    loading.value = true
+    const res = await imageApi.categoryBatchCheck(categoryId)
+    loading.value = false
+    
+    if (res.code !== 0) {
+      ElMessage.error(res.message || '检查分类失败')
+      return
+    }
+
+    const { image_count, sub_categories } = res.data
+    if (image_count === 0) {
+      ElMessage.warning('该分类下无图片,无法推送')
+      return
+    }
+
+    let confirmMsg = `该分类下共有 ${image_count} 张图片。`
+    if (sub_categories && sub_categories.length > 0) {
+      confirmMsg += `涉及子分类: ${sub_categories.join(', ')}。`
+    }
+    confirmMsg += '是否继续推送?'
+
+    await ElMessageBox.confirm(confirmMsg, '确认推送', {
+      confirmButtonText: '继续',
+      cancelButtonText: '取消',
+      type: 'info'
+    })
+
+    // 通过后才打开弹窗
+    isCategoryBatch.value = true
+    taskForm.project_name = '' 
+    taskForm.selectedTags = []
+    tempTagValue.value = null
+    if (taskFormRef.value) {
+      taskFormRef.value.clearValidate()
+    }
+    
+    // 获取标签树
+    if (tagTree.value.length === 0) {
+      await fetchTagTree()
+    }
+    
+    taskDialogVisible.value = true
+  } catch (error) {
+    loading.value = false
+    if (error !== 'cancel') {
+      console.error('分类检查异常:', error)
+    }
+  }
+}
+
 const confirmAddTask = async () => {
   if (!taskFormRef.value) return
   
@@ -403,9 +462,30 @@ const confirmAddTask = async () => {
   taskAdding.value = true
   try {
     const tagNames = taskForm.selectedTags.map(t => t.name)
-    const { code, message } = await imageApi.batchAddToTask(selectedIds.value, taskForm.project_name, tagNames)
+    let res
+    
+    if (isCategoryBatch.value) {
+      const categoryId = currentCategory.value?.id === '0' ? '' : (currentCategory.value?.id || '')
+      res = await imageApi.categoryBatchAddToTask(categoryId, taskForm.project_name, tagNames)
+    } else {
+      res = await imageApi.batchAddToTask(selectedIds.value, taskForm.project_name, tagNames)
+    }
+
+    const { code, message, data } = res
     if (code === 0) {
-      ElMessage.success(message || '批量加入任务成功')
+      let successMsg = message || '批量加入任务成功'
+      if (isCategoryBatch.value && data) {
+        if (data.image_count === 0) {
+          ElMessage.warning('该分类下无图片,无法推送')
+          taskAdding.value = false
+          return
+        }
+        if (data.sub_categories && data.sub_categories.length > 0) {
+          successMsg = `成功推送 ${data.image_count} 张图片。涉及子分类: ${data.sub_categories.join(', ')}`
+        }
+      }
+      
+      ElMessage.success(successMsg)
       selectedIds.value = []
       taskDialogVisible.value = false
       fetchImages()