chenkun 2 тижнів тому
батько
коміт
dd3faf848c

+ 8 - 2
src/api/document.ts

@@ -132,10 +132,11 @@ export const documentApi = {
   },
 
   // 批量加入任务中心
-  batchAddToTask(ids: string[], projectName: string): Promise<ApiResponse<any>> {
+  batchAddToTask(ids: string[], projectName: string, tags?: string[]): Promise<ApiResponse<any>> {
     return request.post(`${API_PREFIX}/documents/batch-add-to-task`, {
       doc_ids: ids,
-      project_name: projectName
+      project_name: projectName,
+      tags: tags
     })
   },
 
@@ -158,5 +159,10 @@ export const documentApi = {
       content_type: contentType,
       prefix
     })
+  },
+
+  // 获取标签分类树
+  getTagTree(): Promise<ApiResponse<any[]>> {
+    return request.get('/api/v1/sample/tag/tree')
   }
 }

+ 3 - 2
src/api/image.ts

@@ -96,10 +96,11 @@ export const imageApi = {
   },
 
   // 批量加入任务中心
-  batchAddToTask(ids: string[], projectName: string): Promise<ApiResponse<null>> {
+  batchAddToTask(ids: string[], projectName: string, tags?: string[]): Promise<ApiResponse<null>> {
     return request.post(`${API_PREFIX}/batch-add-to-task`, {
       ids: ids,
-      project_name: projectName
+      project_name: projectName,
+      tags: tags
     })
   }
 }

+ 154 - 32
src/views/admin/TaskManagement.vue

@@ -10,25 +10,46 @@
       <el-tabs v-model="activeTab" @tab-change="handleTabChange">
         <el-tab-pane label="信息任务管理" name="data">
           <el-table v-loading="loading" :data="taskList" border stripe style="width: 100%">
-            <el-table-column prop="id" label="任务序号" width="100" />
-            <el-table-column prop="business_id" label="基本信息ID" min-width="180" />
-            <el-table-column prop="name" label="名称" min-width="250" show-overflow-tooltip />
-            <el-table-column prop="task_id" label="任务ID (task_id)" min-width="200">
+            <el-table-column type="index" label="序号" width="70" align="center" />
+            <el-table-column prop="project_name" label="项目名称" min-width="180" />
+            <el-table-column label="任务标签" min-width="150">
+              <template #default="{ row }">
+                <div class="tag-group">
+                  <el-tag 
+                    v-for="tag in parseTags(row.tag)" 
+                    :key="tag" 
+                    size="small" 
+                    class="mr-1 mb-1"
+                  >
+                    {{ tag }}
+                  </el-tag>
+                </div>
+              </template>
+            </el-table-column>
+            <el-table-column prop="file_count" label="文件数量" width="100" align="center" />
+            <el-table-column prop="task_id" label="标注平台任务ID" min-width="200">
               <template #default="{ row }">
                 <el-tag v-if="row.task_id" type="info">{{ row.task_id }}</el-tag>
                 <span v-else class="placeholder">暂无任务ID</span>
               </template>
             </el-table-column>
-            <el-table-column label="操作" width="220" fixed="right">
+            <el-table-column label="操作" width="280" fixed="right">
               <template #default="{ row }">
                 <el-button-group>
+                  <el-button 
+                    size="small" 
+                    type="info" 
+                    @click="handleViewDetails(row)"
+                  >
+                    详情
+                  </el-button>
                   <el-button 
                     size="small" 
                     type="primary" 
                     :disabled="!row.task_id"
                     @click="handleCheckProgress(row)"
                   >
-                    查看进度
+                    进度
                   </el-button>
                   <el-button 
                     size="small" 
@@ -36,7 +57,7 @@
                     :disabled="!row.task_id"
                     @click="handleExportData(row)"
                   >
-                    导出结果
+                    导出
                   </el-button>
                 </el-button-group>
               </template>
@@ -46,44 +67,46 @@
 
         <el-tab-pane label="图片任务管理" name="image">
           <el-table v-loading="loading" :data="taskList" border stripe style="width: 100%">
-            <el-table-column prop="id" label="任务序号" width="100" />
-            <el-table-column label="图片预览" width="120">
+            <el-table-column type="index" label="序号" width="70" align="center" />
+            <el-table-column prop="project_name" label="项目名称" min-width="180" />
+            <el-table-column label="任务标签" min-width="150">
               <template #default="{ row }">
-                <el-image 
-                  v-if="row.image_url"
-                  class="table-image"
-                  :src="row.image_url" 
-                  :preview-src-list="[row.image_url]"
-                  fit="cover"
-                  preview-teleported
-                >
-                  <template #error>
-                    <div class="image-error">
-                      <el-icon><Picture /></el-icon>
-                    </div>
-                  </template>
-                </el-image>
-                <span v-else class="placeholder">无预览图</span>
+                <div class="tag-group">
+                  <el-tag 
+                    v-for="tag in parseTags(row.tag)" 
+                    :key="tag" 
+                    size="small" 
+                    class="mr-1 mb-1"
+                  >
+                    {{ tag }}
+                  </el-tag>
+                </div>
               </template>
             </el-table-column>
-            <el-table-column prop="business_id" label="图片ID" min-width="180" />
-            <el-table-column prop="name" label="名称" min-width="250" show-overflow-tooltip />
-            <el-table-column prop="task_id" label="任务ID (task_id)" min-width="200">
+            <el-table-column prop="file_count" label="文件数量" width="100" align="center" />
+            <el-table-column prop="task_id" label="标注平台任务ID" min-width="200">
               <template #default="{ row }">
                 <el-tag v-if="row.task_id" type="info">{{ row.task_id }}</el-tag>
                 <span v-else class="placeholder">暂无任务ID</span>
               </template>
             </el-table-column>
-            <el-table-column label="操作" width="220" fixed="right">
+            <el-table-column label="操作" width="280" fixed="right">
               <template #default="{ row }">
                 <el-button-group>
+                  <el-button 
+                    size="small" 
+                    type="info" 
+                    @click="handleViewDetails(row)"
+                  >
+                    详情
+                  </el-button>
                   <el-button 
                     size="small" 
                     type="primary" 
                     :disabled="!row.task_id"
                     @click="handleCheckProgress(row)"
                   >
-                    查看进度
+                    进度
                   </el-button>
                   <el-button 
                     size="small" 
@@ -91,7 +114,7 @@
                     :disabled="!row.task_id"
                     @click="handleExportData(row)"
                   >
-                    导出结果
+                    导出
                   </el-button>
                 </el-button-group>
               </template>
@@ -100,6 +123,53 @@
         </el-tab-pane>
       </el-tabs>
     </el-card>
+
+    <!-- 项目详情对话框 -->
+    <el-dialog
+      v-model="detailsVisible"
+      :title="`项目详情: ${currentProject?.project_name}`"
+      width="800px"
+      destroy-on-close
+    >
+      <el-table :data="currentProjectFiles" v-loading="detailsLoading" border stripe style="width: 100%" max-height="500">
+        <el-table-column type="index" label="序号" width="60" align="center" />
+        <el-table-column v-if="activeTab === 'image'" label="图片预览" width="100">
+          <template #default="{ row }">
+            <el-image 
+              v-if="row.image_url"
+              class="detail-table-image"
+              :src="row.image_url" 
+              :preview-src-list="[row.image_url]"
+              fit="cover"
+              preview-teleported
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="name" label="文件名称" min-width="200" show-overflow-tooltip />
+        <el-table-column label="任务标签" min-width="120">
+          <template #default="{ row }">
+            <div class="tag-group">
+              <el-tag 
+                v-for="tag in parseTags(row.tag)" 
+                :key="tag" 
+                size="small" 
+                class="mr-1 mb-1"
+              >
+                {{ tag }}
+              </el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="business_id" label="业务ID" width="180" />
+        <el-table-column prop="annotation_status" label="状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.annotation_status === 'completed' ? 'success' : 'warning'" size="small">
+              {{ row.annotation_status === 'completed' ? '已完成' : '标注中' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
   </div>
 </template>
 
@@ -110,12 +180,16 @@ import { Picture } from '@element-plus/icons-vue'
 import request from '@/api/request'
 
 interface TaskItem {
-  id: number
+  id?: number
   business_id: string
   task_id: string | null
-  project_id: string  // 项目名称
+  project_id: string
+  project_name: string
   type: string
   name: string
+  file_count: number
+  status: string
+  tag?: string
   image_url?: string
 }
 
@@ -130,6 +204,23 @@ const activeTab = ref('data')
 const loading = ref(false)
 const taskList = ref<TaskItem[]>([])
 
+// 详情相关
+const detailsVisible = ref(false)
+const detailsLoading = ref(false)
+const currentProject = ref<TaskItem | null>(null)
+const currentProjectFiles = ref<any[]>([])
+
+// 解析标签
+const parseTags = (tagStr: string | null | undefined): string[] => {
+  if (!tagStr) return []
+  try {
+    const parsed = JSON.parse(tagStr)
+    return Array.isArray(parsed) ? parsed : [parsed]
+  } catch (e) {
+    return [tagStr]
+  }
+}
+
 const loadTasks = async () => {
   loading.value = true
   try {
@@ -149,6 +240,31 @@ const loadTasks = async () => {
   }
 }
 
+// 查看项目详情
+const handleViewDetails = async (row: TaskItem) => {
+  currentProject.value = row
+  detailsVisible.value = true
+  detailsLoading.value = true
+  try {
+    const response = await request.get<any, ApiResponse<any[]>>('/api/v1/sample/tasks/details', {
+      params: { 
+        project_id: row.project_id,
+        type: activeTab.value
+      }
+    })
+    if (response.code === 0) {
+      currentProjectFiles.value = response.data
+    } else {
+      ElMessage.error(response.message || '获取项目详情失败')
+    }
+  } catch (error) {
+    console.error('获取详情出错:', error)
+    ElMessage.error('获取项目详情失败')
+  } finally {
+    detailsLoading.value = false
+  }
+}
+
 const handleTabChange = () => {
   taskList.value = []
   loadTasks()
@@ -266,6 +382,12 @@ onMounted(() => {
   font-size: 20px;
 }
 
+.detail-table-image {
+  width: 60px;
+  height: 45px;
+  border-radius: 4px;
+}
+
 :deep(.el-tabs__header) {
   margin-bottom: 20px;
 }

+ 109 - 3
src/views/documents/Index.vue

@@ -349,11 +349,40 @@
     </el-dialog>
 
     <!-- 加入任务设置弹窗 -->
-    <el-dialog v-model="taskDialogVisible" title="加入标注任务" width="400px">
+    <el-dialog v-model="taskDialogVisible" title="加入标注任务" width="450px">
       <el-form :model="taskForm" :rules="taskRules" ref="taskFormRef" label-width="100px">
         <el-form-item label="项目名称" prop="project_name">
           <el-input v-model="taskForm.project_name" placeholder="请输入项目名称" clearable />
         </el-form-item>
+        <el-form-item label="任务标签" prop="tags">
+          <div class="tag-group">
+            <el-tag
+              v-for="(tag, index) in taskForm.selectedTags"
+              :key="tag.id"
+              closable
+              @close="handleRemoveTag(index)"
+              type="info"
+              class="tag-item"
+            >
+              {{ tag.name }}
+            </el-tag>
+            <el-cascader
+              :key="cascaderKey"
+              v-model="tempTagValue"
+              :options="tagTree"
+              :props="{ checkStrictly: true, emitPath: false }"
+              placeholder="添加标签"
+              @change="handleAddTag"
+              filterable
+              clearable
+              class="tag-adder"
+            >
+              <template #default="{ data }">
+                <span>{{ data.label }}</span>
+              </template>
+            </el-cascader>
+          </div>
+        </el-form-item>
       </el-form>
       <template #footer>
         <span class="dialog-footer">
@@ -801,10 +830,62 @@ const ingestForm = reactive({
 // 任务相关状态
 const taskAdding = ref(false)
 const taskDialogVisible = ref(false)
+const tempTagValue = ref<number | null>(null)
+const cascaderKey = ref(0)
 const taskForm = reactive({
-  project_name: ''
+  project_name: '',
+  selectedTags: [] as { id: number, name: string }[]
 })
 
+// 标签相关
+const tagTree = ref<any[]>([])
+
+const handleAddTag = (val: any) => {
+  if (!val) return
+  const id = val
+  const name = findTagName(tagTree.value, id)
+  if (name && !taskForm.selectedTags.find(t => t.id === id)) {
+    taskForm.selectedTags.push({ id, name })
+  }
+  // 清空选择器并增加 key 以强制重置 (清除搜索缓存等)
+  tempTagValue.value = null
+  cascaderKey.value++
+}
+
+const handleRemoveTag = (index: number) => {
+  taskForm.selectedTags.splice(index, 1)
+}
+
+const findTagName = (nodes: any[], id: number): string | null => {
+  for (const node of nodes) {
+    if (node.value === id) return node.label
+    if (node.children) {
+      const name = findTagName(node.children, id)
+      if (name) return name
+    }
+  }
+  return null
+}
+const fetchTagTree = async () => {
+  try {
+    const res = await documentApi.getTagTree()
+    if (res.code === 200) {
+      tagTree.value = formatTagTree(res.data)
+    }
+  } catch (error) {
+    console.error('获取标签树失败:', error)
+  }
+}
+
+// 格式化标签树以适应 Cascader
+const formatTagTree = (data: any[]) => {
+  return data.map(node => ({
+    value: node.id,
+    label: node.name,
+    children: node.children && node.children.length > 0 ? formatTagTree(node.children) : undefined
+  }))
+}
+
 const taskRules = {
   project_name: [
     { required: true, message: '请输入项目名称', trigger: 'blur' },
@@ -1124,9 +1205,17 @@ const handleBatchAddToTask = async () => {
   }
 
   taskForm.project_name = '' // 重置项目名称
+  taskForm.selectedTags = [] // 重置标签
+  tempTagValue.value = null
   if (taskFormRef.value) {
     taskFormRef.value.clearValidate()
   }
+  
+  // 获取标签树
+  if (tagTree.value.length === 0) {
+    await fetchTagTree()
+  }
+  
   taskDialogVisible.value = true
 }
 
@@ -1141,7 +1230,8 @@ const confirmAddTask = async () => {
 
   taskAdding.value = true
   try {
-    const res = await documentApi.batchAddToTask(selectedIds.value, taskForm.project_name)
+    const tags = taskForm.selectedTags.map(t => t.name)
+    const res = await documentApi.batchAddToTask(selectedIds.value, taskForm.project_name, tags)
 
     if (res.code === 0) {
       ElMessage.success(res.message || '操作成功')
@@ -1681,6 +1771,22 @@ onUnmounted(() => {
 </script>
 
 <style scoped>
+.tag-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+  min-height: 32px;
+}
+
+.tag-item {
+  margin: 2px 0;
+}
+
+.tag-adder {
+  width: 120px;
+}
+
 .documents-container {
   padding: 20px;
 }

+ 109 - 3
src/views/images/Index.vue

@@ -171,11 +171,40 @@
     </el-dialog>
 
     <!-- 加入任务设置弹窗 -->
-    <el-dialog v-model="taskDialogVisible" title="加入标注任务" width="400px">
+    <el-dialog v-model="taskDialogVisible" title="加入标注任务" width="450px">
       <el-form :model="taskForm" :rules="taskRules" ref="taskFormRef" label-width="100px">
         <el-form-item label="项目名称" prop="project_name">
           <el-input v-model="taskForm.project_name" placeholder="请输入项目名称" clearable />
         </el-form-item>
+        <el-form-item label="任务标签" prop="tags">
+          <div class="tag-group">
+            <el-tag
+              v-for="(tag, index) in taskForm.selectedTags"
+              :key="tag.id"
+              closable
+              @close="handleRemoveTag(index)"
+              type="info"
+              class="tag-item"
+            >
+              {{ tag.name }}
+            </el-tag>
+            <el-cascader
+              :key="cascaderKey"
+              v-model="tempTagValue"
+              :options="tagTree"
+              :props="{ checkStrictly: true, emitPath: false }"
+              placeholder="添加标签"
+              @change="handleAddTag"
+              filterable
+              clearable
+              class="tag-adder"
+            >
+              <template #default="{ data }">
+                <span>{{ data.label }}</span>
+              </template>
+            </el-cascader>
+          </div>
+        </el-form-item>
       </el-form>
       <template #footer>
         <span class="dialog-footer">
@@ -194,6 +223,7 @@ import { ref, onMounted, reactive, computed } from 'vue'
 import { Plus, Edit, Delete, Upload, Picture, Folder, FolderOpened, Tickets } from '@element-plus/icons-vue'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { imageApi, type ImageCategory, type ImageItem } from '@/api/image'
+import { documentApi } from '@/api/document'
 import axios from 'axios'
 
 // --- 数据定义 ---
@@ -226,10 +256,61 @@ const selectedIds = ref<string[]>([])
 // 任务相关状态
 const taskAdding = ref(false)
 const taskDialogVisible = ref(false)
+const tempTagValue = ref<number | null>(null)
+const cascaderKey = ref(0)
 const taskForm = reactive({
-  project_name: ''
+  project_name: '',
+  selectedTags: [] as { id: number, name: string }[]
 })
 
+// 标签相关
+const tagTree = ref<any[]>([])
+
+const handleAddTag = (val: any) => {
+  if (!val) return
+  const id = val
+  const name = findTagName(tagTree.value, id)
+  if (name && !taskForm.selectedTags.find(t => t.id === id)) {
+    taskForm.selectedTags.push({ id, name })
+  }
+  // 清空选择器并增加 key 以强制重置 (清除搜索缓存等)
+  tempTagValue.value = null
+  cascaderKey.value++
+}
+
+const handleRemoveTag = (index: number) => {
+  taskForm.selectedTags.splice(index, 1)
+}
+
+const findTagName = (nodes: any[], id: number): string | null => {
+  for (const node of nodes) {
+    if (node.value === id) return node.label
+    if (node.children) {
+      const name = findTagName(node.children, id)
+      if (name) return name
+    }
+  }
+  return null
+}
+const fetchTagTree = async () => {
+  try {
+    const res = await documentApi.getTagTree()
+    if (res.code === 200) {
+      tagTree.value = formatTagTree(res.data)
+    }
+  } catch (error) {
+    console.error('获取标签树失败:', error)
+  }
+}
+
+const formatTagTree = (data: any[]) => {
+  return data.map(node => ({
+    value: node.id,
+    label: node.name,
+    children: node.children && node.children.length > 0 ? formatTagTree(node.children) : undefined
+  }))
+}
+
 const taskRules = {
   project_name: [
     { required: true, message: '请输入项目名称', trigger: 'blur' },
@@ -296,9 +377,17 @@ const handleBatchAddToTask = async () => {
   if (selectedIds.value.length === 0) return
   
   taskForm.project_name = '' // 重置项目名称
+  taskForm.selectedTags = [] // 重置标签
+  tempTagValue.value = null
   if (taskFormRef.value) {
     taskFormRef.value.clearValidate()
   }
+  
+  // 获取标签树
+  if (tagTree.value.length === 0) {
+    await fetchTagTree()
+  }
+  
   taskDialogVisible.value = true
 }
 
@@ -313,7 +402,8 @@ const confirmAddTask = async () => {
 
   taskAdding.value = true
   try {
-    const { code, message } = await imageApi.batchAddToTask(selectedIds.value, taskForm.project_name)
+    const tagNames = taskForm.selectedTags.map(t => t.name)
+    const { code, message } = await imageApi.batchAddToTask(selectedIds.value, taskForm.project_name, tagNames)
     if (code === 0) {
       ElMessage.success(message || '批量加入任务成功')
       selectedIds.value = []
@@ -532,6 +622,22 @@ onMounted(() => {
 </script>
 
 <style scoped>
+.tag-group {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  align-items: center;
+  min-height: 32px;
+}
+
+.tag-item {
+  margin: 2px 0;
+}
+
+.tag-adder {
+  width: 120px;
+}
+
 .image-management {
   height: calc(100vh - 120px);
   background-color: #f5f7fa;