Ver código fonte

Merge branch 'develop' of http://47.108.150.237:10000/www/taais-web into dev-lzy

# Conflicts:
#	src/views/demo/data/index.vue
#	src/views/taais/homePage/task/index.vue
#	src/views/task/bizProcess/index.vue
ajax 9 meses atrás
pai
commit
e08be5ed31

+ 4 - 0
src/api/modules/ag/model.ts

@@ -27,6 +27,10 @@ export const addModelApi = (data: ModelForm) => {
   return http.post<any>('/ag/model', data, { loading: false })
 }
 
+export const addAlgorithmTaskConfigurationApi = (data: ModelForm) => {
+  return http.post<any>('/task/taskConfiguration/importModel', data, { loading: false })
+}
+
 /**
  * @name 修改算法模型配置
  * @param data data

+ 8 - 0
src/api/modules/demo/video2image.ts

@@ -85,6 +85,14 @@ export const startVideo2imageApi = (id: string | number) => {
   return http.get('/demo/video2image/start/' + id)
 }
 
+/**
+ * @name 终止视频转图片
+ * @returns returns
+ */
+export const stopVideo2imageApi = (id: string | number) => {
+  return http.get('/demo/video2image/stop/' + id)
+}
+
 /**
  * @name 下载压缩包
  * @returns returns

+ 10 - 3
src/api/modules/demo/videoStable.ts

@@ -1,5 +1,4 @@
 import http from '@/api'
-// @ts-expect-error
 import { VideoStableVO, VideoStableForm, VideoStableQuery } from '@/api/interface/demo/videoStable'
 
 /**
@@ -75,6 +74,14 @@ export const startVideoStableApi = (id: String | Number) => {
   return http.get('/demo/videoStable/start/' + id)
 }
 
-export const getCompareImageApi = (taskName: String, idx: String | Number) => {
-  return http.get('/demo/videoStable/compare/' + taskName + '/' + idx)
+export const stopVideoStableApi = (id: String | Number) => {
+  return http.get('/demo/videoStable/stop/' + id)
+}
+
+export const getCompareImageApi = (taskId: String, idx: String | Number) => {
+  return http.get('/demo/videoStable/compare/' + taskId + '/' + idx)
+}
+
+export const getCompareImageCountApi = (taskId: String | Number) => {
+  return http.get('/demo/videoStable/compare/num/' + taskId)
 }

+ 4 - 0
src/api/modules/task/bizProcessNew.ts

@@ -8,6 +8,10 @@ export const listBizProcessApi = (query: any) => {
   return http.get<any[]>('/identification/identificationSubtaskDetails/list', query, { loading: false })
 }
 
+export const getImgList = (query: any) => {
+  return http.get<any[]>('/identification/identificationSubtaskDetails/getImgList', query, { loading: false })
+}
+
 /**
  * @name 查询算法业务处理详细
  * @param id id

+ 19 - 14
src/components/FormDialog/index.vue

@@ -10,12 +10,9 @@
   >
     <ProFrom ref="proFormRef" :items-options="parameter.itemsOptions" :form-options="_options" :model="parameter.model">
       <template #modelAddress="{}">
-        <FileUpload :file-size="4096" :file-type="['pt']" />
+        <FileUpload :file-size="4096" :file-type="['pt']" @update:model-value="setModelAddr" />
       </template>
     </ProFrom>
-    <div class="upload-video-box" v-if="parameter.showVideoUpload">
-      <FileUpload ref="videoUploadRef" :limit="1" :file-size="4096" :file-type="['mp4', 'avi', 'rmvb', 'mov', 'wmv', 'flv']" />
-    </div>
     <template #footer>
       <span class="dialog-footer">
         <el-button type="primary" v-if="parameter.isEdit" :loading="butLoading" @click="handleSubmit">确认</el-button>
@@ -26,12 +23,17 @@
 </template>
 
 <script setup lang="ts" name="FormDialog">
-import { ref, ComputedRef, computed } from 'vue'
+import { ref, ComputedRef, computed, reactive } from 'vue'
 import ProFrom from '@/components/ProForm/index.vue'
 import { ElMessage } from 'element-plus'
 import FileUpload from '@/components/Upload/File.vue'
 // import mittBus from '@/utils/mittBus'
 
+interface EmitEvent {
+  (e: 'update'): void
+}
+const emits = defineEmits<EmitEvent>()
+
 const videoUploadRef = ref<InstanceType<typeof FileUpload> | null>(null)
 
 export interface FormParameterProps {
@@ -45,7 +47,6 @@ export interface FormParameterProps {
   itemsOptions: ProForm.ItemsOptions[] // 动态表单字段配置
   model?: Record<ProForm.FormItem['prop'], ProForm.FormItem['value']> // 表单数据对象
   getTableList?: () => void // 获取表格数据的Api
-  showVideoUpload: boolean // 是否显示视频上传
 }
 
 // dialog状态
@@ -58,8 +59,7 @@ const parameter = ref<FormParameterProps>({
   top: '10vh',
   itemsOptions: [],
   formOptions: {},
-  isEdit: true,
-  showVideoUpload: false
+  isEdit: true
 })
 const _options: ComputedRef<ProForm.FormOptions> = computed(() => {
   const form = {
@@ -69,25 +69,30 @@ const _options: ComputedRef<ProForm.FormOptions> = computed(() => {
   }
   return Object.assign(form, parameter.value.formOptions)
 })
+
+const modelAddr = ref('')
+const setModelAddr = res => {
+  console.log('modelAddr', res)
+  modelAddr.value = res
+}
+
 const proFormRef = ref<InstanceType<typeof ProFrom> | null>(null)
 // 表单提交校验
 const handleSubmit = () => {
   const formEl = proFormRef.value?.proFormRef
   const formModel = proFormRef.value?.formModel
+  formModel.modelAddress = modelAddr.value
+  console.log('formModel', formModel)
 
-  let urlModel = {}
-  if (parameter.value.showVideoUpload) {
-    console.log(videoUploadRef.value.uploadFileListExport)
-    urlModel = { url: videoUploadRef.value?.uploadFileListExport[0].url }
-  }
   butLoading.value = true
   if (!formEl) return
   formEl.validate(valid => {
     if (valid) {
-      parameter.value.api!({ ...formModel, ...parameter.value.model, ...urlModel }).then(res => {
+      parameter.value.api!({ ...formModel, ...parameter.value.model }).then(res => {
         if (res.code == 200) {
           proFormRef.value?.resetForm(formEl)
           ElMessage.success('操作成功')
+          emits('update')
           dialogVisible.value = false
           parameter.value.getTableList && parameter.value.getTableList()
         } else {

+ 6 - 1
src/components/ProForm/index.vue

@@ -25,7 +25,12 @@
               <SelectIcon v-model:icon-value="formModel[item.prop]" />
             </template>
             <template v-else-if="item.compOptions.elTagName === 'file-upload'">
-              <FileUpload v-model:model-value="formModel[item.prop]" v-bind="$attrs" />
+              <FileUpload
+                v-model:model-value="formModel[item.prop]"
+                :file-size="item.compOptions.fileSize"
+                :file-type="item.compOptions.fileType"
+                v-bind="$attrs"
+              />
             </template>
             <template v-else-if="item.compOptions.elTagName === 'img-upload'">
               <uploadImg v-model:image-url="formModel[item.prop]" />

+ 4 - 2
src/components/Upload/File.vue

@@ -65,7 +65,7 @@ interface UploadFileProps {
   text?: string // 按钮文字
   icon?: string
   fileType?: Array<string>
-  uploadApiPath: string // 上传文件服务器地址
+  uploadApiPath?: string // 上传文件服务器地址
 }
 
 // 默认值
@@ -187,10 +187,12 @@ const uploadSuccess = (response: any | undefined, uploadFile: UploadFile) => {
 // 上传结束处理
 const uploadedSuccessfully = () => {
   if (number.value > 0 && uploadList.value.length === number.value) {
+    console.log(_fileList.value)
     _fileList.value = _fileList.value.filter(f => f.url !== undefined).concat(uploadList.value)
+    console.log(_fileList.value)
     uploadList.value = []
     number.value = 0
-    emit('update:modelValue', listToString(_fileList.value))
+    emit('update:modelValue', _fileList.value.length > 0 ? _fileList.value[0].url : '')
     tryHideFullScreenLoading()
   }
   // 监听表单验证

+ 2 - 0
src/typings/ProForm.d.ts

@@ -101,6 +101,8 @@ declare namespace ProForm {
     multiple?: boolean
     disabled?: boolean
     elTagName?: ElTagName
+    fileSize?: number // 文件大小
+    fileType?: string[] // 文件类型
     type?: string
     rangeSeparator?: string // 时间范围分隔符
     startPlaceholder?: string // 开始时间Placeholder

+ 34 - 40
src/views/demo/components/img-maker.vue

@@ -1,6 +1,6 @@
 <template>
   <div>
-    <div style="margin-bottom: 10px">
+    <div style="margin-bottom: 10px" v-show="!isPicOnly">
       <el-button plain type="primary" class="shape-border" @click="drawTypeChange('rectangle')" :disabled="!state.dragMode">标注模式</el-button>
       <el-button plain type="primary" class="shape-border" @click="enableDragMode" :disabled="state.dragMode">移动图片</el-button>
 
@@ -52,6 +52,10 @@ const props = defineProps({
   },
   jsonData: {
     type: Array
+  },
+  isPicOnly: {
+    type: Boolean,
+    default: false
   }
 })
 const state = reactive({
@@ -154,51 +158,15 @@ const loadInit = () => {
   state.canvas.on('mouse:down', mousedown)
   state.canvas.on('mouse:move', mousemove)
   state.canvas.on('mouse:up', mouseup)
-  // // 添加鼠标滚轮缩放功能
-  // state.canvas.on('mouse:wheel', event => {
-  //   const delta = event.delta
-  //   const zoom = state.canvas.getZoom()
-  //   const point = new fabric.Point(event.x, event.y)
-  //
-  //   if (delta > 0) {
-  //     // 放大
-  //     state.canvas.zoomToPoint(point, zoom * 1.1)
-  //     // state.radio *= 1.1
-  //   } else {
-  //     // 缩小
-  //     state.canvas.zoomToPoint(point, zoom * 0.9)
-  //     // state.radio *= 0.9
-  //   }
-  //   state.canvas.renderAll()
-  //   event.e.preventDefault()
-  //   event.e.stopPropagation()
-  // })
-
-  // nice try!
-  // console.log(props.jsonData)
-  // if (props.jsonData && props.jsonData.length > 0) {
-  //   // console.log('load')
-  //   state.canvas.loadFromJSON(props.jsonData, () => {
-  //     let objects = state.canvas.getObjects()
-  //     // console.log(objects)
-  //     objects[0].selectable = false
-  //     for (let i = 1; i < objects.length; i++) {
-  //       objects[i].selectable = true
-  //     }
-  //     state.canvas.renderAll()
-  //   })
-  //   // console.log(state.canvas)
-  //   state.loading = false
-  //   return
-  // }
 
   let imgElement = new Image()
   imgElement.src = props.src
   imgElement.onload = () => {
+    console.log(imgElement.height, imgElement.width)
     // 区域大小/图片原始大小 缩放比例
     state.radio =
       props.cWidth / imgElement.width > props.cHeight / imgElement.height ? props.cHeight / imgElement.height : props.cWidth / imgElement.width
-    // console.log('state.radio', state.radio)
+    console.log('state.radio', state.radio)
 
     // 屏幕分辨率/图片原始大小
     state.realRadioX = props.width / imgElement.width
@@ -218,16 +186,42 @@ const loadInit = () => {
       scaleY: state.radio
     })
     state.canvas.add(imgInstance)
+    console.log('props', props)
+    console.log('state', state)
     if (props.jsonData && props.jsonData.length > 0) {
       for (let i = 0; i < props.jsonData.length; i++) {
         let config = props.jsonData[i]
-        let obj = new fabric.Path(config.pathString, config)
+        let subArr = config.subArr
+        const lW = imgInstance.width * state.radio
+        const lH = imgInstance.height * state.radio
+        let obj = new fabric.Path(
+          'M ' +
+            subArr[1] * lW +
+            ' ' +
+            subArr[2] * lH +
+            ' L ' +
+            subArr[3] * lW +
+            ' ' +
+            subArr[4] * lH +
+            ' L ' +
+            subArr[5] * lW +
+            ' ' +
+            subArr[6] * lH +
+            ' L ' +
+            subArr[7] * lW +
+            ' ' +
+            subArr[8] * lH +
+            ' z',
+          config
+        )
         state.canvas.add(obj)
       }
       state.canvas.renderAll()
     }
     state.radio = state.canvas.getZoom()
     state.loading = false
+    console.log(state.canvas.getObjects())
+    console.log(props.isPicOnly)
     // console.log(state.realRadioX)
     // console.log(state.realRadioY)
   }

+ 311 - 142
src/views/demo/data/index.vue

@@ -2,14 +2,14 @@
   <div class="table-box">
     <ProTable ref="proTable" :columns="columns" row-key="id" :request-api="listDataApi" :init-param="initParam">
       <template #yuan="scope">
-        <el-image style="width: 100px" :src="'/api' + scope.row.url" @click="markImg(scope.row)" />
+        <uploadImg :is-show-data="true" :disabled="true" :image-url="scope.row.url" />
+        <!--        <el-image style="width: 100px" :src="'/api' + scope.row.url" @click="markImg(scope.row)" />-->
       </template>
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <el-button type="primary" v-auth="['demo:data:add']" :icon="CirclePlus" @click="openDialog(1, '数据新增')"> 新增 </el-button>
-        <el-button type="primary" v-auth="['demo:data:import']" :icon="Upload" plain @click="batchAdd"> 导入数据集</el-button>
-        <el-button type="primary" v-auth="['demo:data:export']" :icon="Download" plain @click="downloadFile"> 导出</el-button>
-        <el-button type="primary" v-auth="['system:user:add']" :icon="CirclePlus" @click="dataAmplify()"> 数据增广 </el-button>
+        <el-button type="primary" v-auth="['demo:data:import']" :icon="Upload" plain @click="batchAdd"> 导入数据集 </el-button>
+        <el-button type="primary" v-auth="['demo:data:export']" :icon="Download" plain @click="downloadFile"> 导出 </el-button>
         <el-button
           type="danger"
           v-auth="['demo:data:remove']"
@@ -24,6 +24,7 @@
       <!-- 表格操作 -->
       <template #operation="scope">
         <!-- <el-button type="primary" link :icon="EditPen" v-auth="['demo:data:edit']" @click="openDialog(2, '数据标注', scope.row)"> 标注 </el-button> -->
+        <el-button type="primary" link :icon="EditPen" v-auth="['demo:data:edit']" @click="markImg(scope.row)"> 标注 </el-button>
         <el-button type="primary" link :icon="EditPen" v-auth="['demo:data:edit']" @click="openDialog(2, '数据编辑', scope.row)"> 编辑 </el-button>
         <el-button type="primary" link :icon="View" v-auth="['demo:data:query']" @click="openDialog(3, '数据查看', scope.row)"> 查看 </el-button>
         <el-button type="primary" link :icon="Delete" v-auth="['demo:data:remove']" @click="deleteData(scope.row)"> 删除 </el-button>
@@ -32,15 +33,24 @@
 
     <FormDialog ref="formDialogRef" />
     <ImportPicDataset ref="dialogRef" />
-    <ImgDetect ref="imgDetect" :img="cover" :area="area" :width="width" :height="height" @success="handleImgSuccess"></ImgDetect>
+    <ImgDetect
+      ref="imgDetect"
+      :img="cover"
+      :area="area"
+      :width="width"
+      :height="height"
+      @success="handleImgSuccess"
+      :classes="classes"
+      :json-data="jsonData"
+    >
+    </ImgDetect>
   </div>
 </template>
 
 <script setup lang="tsx" name="Data">
-import { ref, reactive, toRefs } from 'vue'
+import { ref, reactive, toRefs, onMounted } from 'vue'
 import { useHandleData } from '@/hooks/useHandleData'
 import { useDownload } from '@/hooks/useDownload'
-import { useRouter } from 'vue-router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import ProTable from '@/components/ProTable/index.vue'
 import FormDialog from '@/components/FormDialog/index.vue'
@@ -48,11 +58,12 @@ import ImportPicDataset from '@/components/ImportPicDataset/index.vue'
 import { ProTableInstance, ColumnProps } from '@/components/ProTable/interface'
 import { Delete, EditPen, Download, Upload, View, CirclePlus } from '@element-plus/icons-vue'
 // import { fabric } from 'fabric'
-import { useDrawArea } from '@/utils/fabric'
+// import { useDrawArea } from '@/utils/fabric'
+import { getDictsApi } from '@/api/modules/system/dictData'
 import ImgDetect from '../components/img-detect.vue'
+import uploadImg from '@/components/Upload/Img.vue'
 import {
   listDataApi,
-  getFormSelectsApi,
   delDataApi,
   addDataApi,
   updateDataApi,
@@ -61,15 +72,43 @@ import {
   exportDataApi,
   getDataApi
 } from '@/api/modules/demo/data'
+import { listDataApi as listDictDataApi } from '@/api/modules/system/dictData'
+import { uploadPure } from '@/api/modules/upload'
+import http from '@/api'
+
+onMounted(() => {
+  state.cacheData.url = 'http://localhost:9090/profile/upload/2024/08/08/144745610/test.png'
+  // handleImgSuccess({
+  //   data: 'test change data',
+  //   jsonData: 'jsonData12345 test change',
+  //   url: 'testurl'
+  // })
+})
 
 const imgDetect = ref()
 const state = reactive({
   area: '' as string,
   width: 1920 as number,
   height: 1080 as number,
-  cover: ''
+  cover: '',
+  classes: [],
+  //   [
+  //   {
+  //     name: '飞机',
+  //     color: '#ea5413',
+  //     label: 'plane'
+  //   },
+  //   {
+  //     name: '汽车',
+  //     color: '#ff00ff',
+  //     label: 'car'
+  //   }
+  // ],
+  jsonData: [],
+  cacheData: {}
+  // '{"version":"5.3.0","objects":[{"type":"image","version":"5.3.0","originX":"left","originY":"top","left":0,"top":0,"width":918,"height":789,"fill":"rgb(0,0,0)","stroke":null,"strokeWidth":0,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,"scaleX":0.68,"scaleY":0.68,"angle":0,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"cropX":0,"cropY":0,"src":"http://localhost:8848/api/profile/upload/2024/08/08/144745610/3-3.jpg","crossOrigin":null,"filters":[]},{"type":"rect","version":"5.3.0","originX":"left","originY":"top","left":181.38,"top":251.38,"width":244,"height":162,"fill":"rgba(255, 255, 255, 0)","stroke":"#E34F51","strokeWidth":5,"strokeDashArray":null,"strokeLineCap":"butt","strokeDashOffset":0,"strokeLineJoin":"miter","strokeUniform":false,"strokeMiterLimit":4,"scaleX":1,"scaleY":1,"angle":-25,"flipX":false,"flipY":false,"opacity":1,"shadow":null,"visible":true,"backgroundColor":"","fillRule":"nonzero","paintFirst":"fill","globalCompositeOperation":"source-over","skewX":0,"skewY":0,"rx":0,"ry":0}]}'
 })
-const { area, width, height, cover } = toRefs(state)
+const { area, width, height, cover, classes, jsonData } = toRefs(state)
 
 // ProTable 实例
 const proTable = ref<ProTableInstance>()
@@ -77,6 +116,7 @@ const proTable = ref<ProTableInstance>()
 //   return new URL(name, import.meta.url).href
 // }
 
+const initParam = reactive({ type: 1 })
 // 删除数据管理信息
 const deleteData = async (params: any) => {
   await useHandleData(delDataApi, params.id, `删除【${params.name}】数据`)
@@ -85,49 +125,203 @@ const deleteData = async (params: any) => {
 
 // 标注图片
 const markImg = data => {
-  state.cover = '/api' + data.url
-  // area 代表后端的传来的标注数据
-  const area = []
-  if (state.cover != '') {
-    if (area.length != 0) {
-      handleImgSuccess(area)
-    }
-    imgDetect.value.visible = true
-  } else {
+  console.log('data is', data)
+  state.cacheData = data
+  state.jsonData = []
+  if (!data.url || data.url.length === 0) {
     ElMessage.warning('缺失图像,暂时不能进行区域添加!')
+    return
   }
-}
-const initParam = reactive({ type: 1 })
 
-const getList = () => {
-  useDrawArea({
-    src: state.cover,
-    width: state.width,
-    height: state.height,
-    area: state.area
+  listDictDataApi({
+    pageNum: 1,
+    pageSize: 10,
+    dictType: 'class_definition'
   })
-    .then(url => {
-      state.cover = url as string
+    .then(res => {
+      // console.log(res)
+      state.classes = []
+      for (let i = 0; i < res.data.list.length; i++) {
+        state.classes.push({
+          name: res.data.list[i].dictLabel,
+          color: res.data.list[i].cssClass,
+          label: res.data.list[i].dictValue
+        })
+      }
+
+      if (state.cacheData.labelurl && state.cacheData.labelurl.length > 0) {
+        console.log('get label jsonData', state.cacheData.labelurl)
+        http
+          .get<any>(state.cacheData.labelurl)
+          .then(res => {
+            state.jsonData = []
+            console.log(res)
+            let arr = res.replace('\r', '').split('\n')
+            console.log(arr)
+            for (let i = 0; i < arr.length; i++) {
+              let subArr = arr[i].split(' ')
+              // console.log(subArr)
+              let cssVal = '#000000'
+              let label = '-1'
+              for (let j = 0; j < state.classes.length; j++) {
+                if (state.classes[j].label === subArr[0]) {
+                  cssVal = state.classes[j].color
+                  label = state.classes[j].label
+                  break
+                }
+              }
+              state.jsonData.push({
+                subArr: subArr,
+                pathString:
+                  'M ' +
+                  subArr[1] * 1920 +
+                  ' ' +
+                  subArr[2] * 1080 +
+                  ' L ' +
+                  subArr[3] * 1920 +
+                  ' ' +
+                  subArr[4] * 1080 +
+                  ' L ' +
+                  subArr[5] * 1920 +
+                  ' ' +
+                  subArr[6] * 1080 +
+                  ' L ' +
+                  subArr[7] * 1920 +
+                  ' ' +
+                  subArr[8] * 1080 +
+                  ' z',
+                // left: 0,
+                // top: 0,
+                fill: '',
+                stroke: cssVal,
+                strokeWidth: 5,
+                label: label
+              })
+            }
+            // console.log(state.jsonData)
+            imgDetect.value.visible = true
+          })
+          .catch(err => {
+            console.log(err)
+          })
+      } else {
+        imgDetect.value.visible = true
+      }
     })
-    .catch(error => {
-      console.log(error)
+    .catch(err => {
+      console.log(err)
     })
+
+  state.cover = '/api' + data.url
+
+  // area 代表后端的传来的标注数据
+  // console.log(state.cover)
+
+  // const area = []
+  // if (state.cover != '') {
+  //   if (area.length != 0) {
+  //     handleImgSuccess(area)
+  //   }
+  //
+  //   console.log("true???")
+  // } else {
+  //   ElMessage.warning('缺失图像,暂时不能进行区域添加!')
+  // }
 }
+// const getList = () => {
+//   useDrawArea({
+//     src: state.cover,
+//     width: state.width,
+//     height: state.height,
+//     area: state.area
+//   })
+//     .then(url => {
+//       state.cover = url as string
+//     })
+//     .catch(error => {
+//       console.log(error)
+//     })
+// }
 const handleImgSuccess = data => {
-  data.forEach(item => {
-    const area = item.split(';')
-    // try=tly blx=tlx brx=trx bry=bly
-    const tlx = Math.round(Number(area[0]) * 1920)
-    const tly = Math.round(Number(area[1]) * 1080)
-    const trx = tlx + Math.round(Number(area[2]) * 1920)
-    const bly = tly + Math.round(Number(area[3]) * 1080)
-    state.area += `${tlx};${tly};${trx};${tly};${trx};${bly};${tlx};${bly},`
+  // console.log(data)
+  state.jsonData = data['jsonData']
+  state.cover = data['url']
+
+  state.cacheData.labelurl = data['data']
+
+  state.cacheData.increment = 'NONE'
+
+  let arr = state.cacheData.url.split('upload')
+  // console.log(arr)
+  let filenames = arr[arr.length - 1].split('.')
+  filenames[filenames.length - 1] = 'txt'
+  // console.log(filenames.join('.'))
+  let filename = filenames.join('.')
+  filename = filename.replace(/\\/g, '/')
+  if (filename.startsWith('/')) {
+    filename = filename.substring(1)
+  }
+  // console.log(filename)
+
+  labelFile(data['data'], filename).then(res => {
+    // console.log(res)
+    if (res.code === 200) {
+      state.cacheData.labelurl = res.data.url
+      updateDataApi(state.cacheData)
+        .then(res => {
+          // console.log(res)
+          if (res.data) {
+            ElMessage.success('操作成功')
+          }
+        })
+        .catch(err => {
+          console.log(err)
+        })
+    }
   })
-  // console.log('state.area', state.area)
-  state.area.slice(0, -1)
-  getList()
+
+  // data.forEach(item => {
+  //   if (item.startsWith('{')) {
+  //     return
+  //   }
+  //   const area = item.split(';')
+  //   // try=tly blx=tlx brx=trx bry=bly
+  //   // mark: 当前用的应该是左上角定点位置和长宽,应该替换为存储点
+  //   const tlx = Math.round(Number(area[0]) * 1920)
+  //   const tly = Math.round(Number(area[1]) * 1080)
+  //   const trx = tlx + Math.round(Number(area[2]) * 1920)
+  //   const bly = tly + Math.round(Number(area[3]) * 1080)
+  //   state.area += `${tlx};${tly};${trx};${tly};${trx};${bly};${tlx};${bly},`
+  // })
+  // // console.log('state.area', state.area)
+  // state.area.slice(0, -1)
+  // getList()
 }
 
+// 创建并提交标注txt文件
+const labelFile = (data, filename) => {
+  // 创建Blob对象
+  const blob = new Blob([data], { type: 'text/plain' })
+
+  // 创建表单数据并添加文件
+  const formData = new FormData()
+
+  formData.append('file', blob, filename)
+
+  return uploadPure(formData)
+}
+
+const labeledTypeData = [
+  {
+    label: '是',
+    value: true
+  },
+  {
+    label: '否',
+    value: false
+  }
+]
+
 // 批量删除数据管理信息
 const batchDelete = async (ids: string[]) => {
   await useHandleData(delDataApi, ids, '删除所选数据信息')
@@ -138,7 +332,7 @@ const batchDelete = async (ids: string[]) => {
 // 导出数据管理列表
 const downloadFile = async () => {
   ElMessageBox.confirm('确认导出数据管理数据?', '温馨提示', { type: 'warning' }).then(() =>
-    useDownload(exportDataApi, '数据管理压缩文件', proTable.value?.searchParam)
+    useDownload(exportDataApi, '数据管理列表', proTable.value?.searchParam)
   )
 }
 
@@ -154,11 +348,6 @@ const batchAdd = () => {
   dialogRef.value?.acceptParams(params)
 }
 
-const router = useRouter()
-const dataAmplify = () => {
-  router.push(`/data/amplify`)
-}
-
 const formDialogRef = ref<InstanceType<typeof FormDialog> | null>(null)
 // 打开弹框的功能
 const openDialog = async (type: number, title: string, row?: any) => {
@@ -180,9 +369,18 @@ const openDialog = async (type: number, title: string, row?: any) => {
   formDialogRef.value?.openDialog(params)
 }
 
+// 表格配置项
 const columns = reactive<ColumnProps<any>[]>([
   { type: 'selection', fixed: 'left', width: 70 },
   { prop: 'yuan', label: '原图', width: 200 },
+  {
+    prop: 'batchNum',
+    label: '批次号',
+    search: {
+      el: 'input'
+    },
+    width: 120
+  },
   {
     prop: 'name',
     label: '名称',
@@ -191,127 +389,109 @@ const columns = reactive<ColumnProps<any>[]>([
     },
     width: 120
   },
-  // {
-  //   prop: 'dataType',
-  //   label: '数据类型',
-  //   search: {
-  //     el: 'input'
-  //   },
-  //   width: 120
-  // },
-  // {
-  //   prop: 'fileType',
-  //   label: '文件类型',
-  //   search: {
-  //     el: 'input'
-  //   },
-  //   width: 120
-  // },
   {
     prop: 'objectType',
     label: '目标类型',
     search: {
-      el: 'tree-select'
+      el: 'input'
     },
-    width: 120,
-    enum: () => getFormSelectsApi({ field: 'object_type' }),
-    fieldNames: { label: 'key', value: 'value' }
+    width: 120
   },
   {
     prop: 'objectSubtype',
     label: '目标子类型',
     search: {
-      el: 'tree-select'
-    },
-    enum: () => getFormSelectsApi({ field: 'object_subtype' }),
-    fieldNames: { label: 'key', value: 'value' },
-    width: 120
-  },
-  {
-    prop: 'batchNum',
-    label: '批次号',
-    search: {
-      el: 'tree-select'
+      el: 'input'
     },
-    enum: () => getFormSelectsApi({ field: 'batch_num' }),
-    fieldNames: { label: 'key', value: 'value' },
     width: 120
   },
   {
     prop: 'scene',
     label: '场景',
     search: {
-      el: 'tree-select'
+      el: 'input'
     },
-    enum: () => getFormSelectsApi({ field: 'scene' }),
-    fieldNames: { label: 'key', value: 'value' },
     width: 120
   },
   {
     prop: 'dataSource',
     label: '数据源',
     search: {
-      el: 'tree-select'
+      el: 'input'
     },
-    enum: () => getFormSelectsApi({ field: 'data_source' }),
-    fieldNames: { label: 'key', value: 'value' },
     width: 120
   },
   {
-    prop: 'gatherTime',
-    label: '采集时间',
+    prop: 'gatherSpot',
+    label: '采集地点',
     search: {
-      el: 'date-picker',
-      props: { type: 'datetimerange', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
+      el: 'input'
     },
     width: 120
   },
   {
-    prop: 'gatherSpot',
-    label: '采集地点',
+    prop: 'gatherTime',
+    label: '采集时间',
     search: {
-      el: 'tree-select'
+      el: 'date-picker',
+      props: { type: 'datetimerange', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
     },
-    enum: () => getFormSelectsApi({ field: 'gather_spot' }),
-    fieldNames: { label: 'key', value: 'value' },
     width: 120
   },
   {
-    prop: 'increment',
-    label: '扩增方式',
+    prop: 'dataType',
+    label: '数据类型',
+    enum: () => getDictsApi('data_type'),
     search: {
       el: 'tree-select'
     },
-    enum: () => getFormSelectsApi({ field: 'increment' }),
-    fieldNames: { label: 'key', value: 'value' },
+    fieldNames: { label: 'dictLabel', value: 'dictValue' },
     width: 120
   },
+  // {
+  //   prop: 'fileType',
+  //   label: '文件类型',
+  //   search: {
+  //     el: 'input'
+  //   },
+  //   width: 120
+  // },
+  // {
+  //   prop: 'increment',
+  //   label: '扩增方式',
+  //   search: {
+  //     el: 'input'
+  //   },
+  //   width: 120
+  // },
   {
     prop: 'labeled',
     label: '是否标注',
     search: {
-      el: 'tree-select'
+      el: 'input'
     },
-    enum: [
-      { label: '是', value: '是' },
-      { label: '否', value: '否' }
-    ],
-    fieldNames: { label: 'key', value: 'value' },
     width: 120
   },
   { prop: 'operation', label: '操作', width: 230, fixed: 'right' }
 ])
-
 // 表单配置项
 let formItems: ProForm.ItemsOptions[] = []
 const setFormItems = () => {
   formItems = [
     {
-      label: '图',
+      label: '图',
       prop: 'url',
       compOptions: {
         elTagName: 'img-upload',
-        placeholder: '请选择上传图片'
+        placeholder: '请选择上传原图'
+      }
+    },
+    {
+      label: '批次号',
+      prop: 'batchNum',
+      rules: [{ required: true, message: '批次号不能为空', trigger: 'blur' }],
+      compOptions: {
+        placeholder: '请输入批次号'
       }
     },
     {
@@ -322,22 +502,6 @@ const setFormItems = () => {
         placeholder: '请输入名称'
       }
     },
-    // {
-    //   label: '数据类型',
-    //   prop: 'dataType',
-    //   rules: [{ required: true, message: '数据类型不能为空', trigger: 'blur' }],
-    //   compOptions: {
-    //     placeholder: '请输入数据类型'
-    //   }
-    // },
-    // {
-    //   label: '文件类型',
-    //   prop: 'fileType',
-    //   rules: [{ required: true, message: '文件类型不能为空', trigger: 'blur' }],
-    //   compOptions: {
-    //     placeholder: '请输入文件类型'
-    //   }
-    // },
     {
       label: '目标类型',
       prop: 'objectType',
@@ -371,6 +535,14 @@ const setFormItems = () => {
         placeholder: '请输入数据源'
       }
     },
+    {
+      label: '采集地点',
+      prop: 'gatherSpot',
+      rules: [{ required: true, message: '采集地点不能为空', trigger: 'blur' }],
+      compOptions: {
+        placeholder: '请输入采集地点'
+      }
+    },
     {
       label: '采集时间',
       prop: 'gatherTime',
@@ -383,29 +555,26 @@ const setFormItems = () => {
       }
     },
     {
-      label: '采集地点',
-      prop: 'gatherSpot',
-      rules: [{ required: true, message: '采集地点不能为空', trigger: 'blur' }],
+      label: '数据类型',
+      prop: 'dataType',
+      rules: [{ required: true, message: '数据类型不能为空', trigger: 'change' }],
       compOptions: {
-        placeholder: '请输入采集地点'
+        elTagName: 'select',
+        labelKey: 'dictLabel',
+        valueKey: 'dictValue',
+        enum: () => getDictsApi('data_type'),
+        placeholder: '请选择数据类型'
       }
     },
     {
-      label: '批次号',
-      prop: 'batchNum',
-      rules: [{ required: true, message: '批次号不能为空', trigger: 'blur' }],
+      label: '是否标注',
+      prop: 'labeled',
+      rules: [{ required: true, message: '请选择是否标注' }],
       compOptions: {
-        placeholder: '请输入批次号'
+        elTagName: 'radio-group',
+        enum: labeledTypeData
       }
     }
-    // {
-    //   label: '扩增方式',
-    //   prop: 'increment',
-    //   rules: [{ required: true, message: '扩增方式不能为空', trigger: 'blur' }],
-    //   compOptions: {
-    //     placeholder: '请输入扩增方式'
-    //   }
-    // }
   ]
 }
 </script>

+ 118 - 59
src/views/demo/video2image/index.vue

@@ -12,8 +12,8 @@
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <el-button type="primary" v-auth="['demo:video2image:add']" icon="CirclePlus" @click="openDialog(1, '新增')"> 新增 </el-button>
-        <el-button type="primary" v-auth="['demo:video2image:import']" icon="Upload" plain @click="batchAdd" v-if="false"> 导入 </el-button>
-        <el-button type="primary" v-auth="['demo:video2image:export']" icon="Download" plain @click="downloadFile" v-if="false"> 导出 </el-button>
+        <!-- <el-button type="primary" v-auth="['demo:video2image:import']" icon="Upload" plain @click="batchAdd" v-if="false"> 导入 </el-button>
+        <el-button type="primary" v-auth="['demo:video2image:export']" icon="Download" plain @click="downloadFile" v-if="false"> 导出 </el-button> -->
         <el-button
           type="danger"
           v-auth="['demo:video2image:remove']"
@@ -33,10 +33,15 @@
           icon="View"
           v-auth="['demo:video2image:start']"
           @click="startVideo2image(scope.row)"
-          v-if="scope.row.status !== '2'"
+          v-if="scope.row.status == '0' || scope.row.status == '3' || scope.row.status == '4'"
         >
-          图片提取
+          开始
         </el-button>
+        <el-popconfirm title="确定终止此任务吗?" @confirm="stopVideo2image(scope.row)" v-if="scope.row.status == '1'">
+          <template #reference>
+            <el-button type="primary" link icon="Delete"> 终止 </el-button>
+          </template>
+        </el-popconfirm>
         <el-button
           type="primary"
           link
@@ -45,11 +50,20 @@
           @click="dowloadVideo2image(scope.row)"
           v-if="scope.row.status == '2'"
         >
-          下载图片
+          下载
+        </el-button>
+        <!-- <el-button type="primary" link icon="View" v-auth="['demo:video2image:query']" @click="openDialog(3, '查看', scope.row)"> 查看 </el-button> -->
+        <!-- <el-button type="primary" link icon="EditPen" v-auth="['demo:video2image:edit']" @click="openDialog(2, '编辑', scope.row)"> 编辑 </el-button> -->
+        <el-button
+          type="primary"
+          link
+          icon="Delete"
+          v-auth="['demo:video2image:remove']"
+          @click="deleteVideo2image(scope.row)"
+          :disabled="scope.row.status == '1'"
+        >
+          删除
         </el-button>
-        <el-button type="primary" link icon="View" v-auth="['demo:video2image:query']" @click="openDialog(3, '查看', scope.row)"> 查看 </el-button>
-        <el-button type="primary" link icon="EditPen" v-auth="['demo:video2image:edit']" @click="openDialog(2, '编辑', scope.row)"> 编辑 </el-button>
-        <el-button type="primary" link icon="Delete" v-auth="['demo:video2image:remove']" @click="deleteVideo2image(scope.row)"> 删除 </el-button>
       </template>
     </ProTable>
     <FormDialog ref="formDialogRef"></FormDialog>
@@ -64,7 +78,7 @@ import { ElMessageBox, ElMessage } from 'element-plus'
 import ProTable from '@/components/ProTable/index.vue'
 import ImportExcel from '@/components/ImportExcel/index.vue'
 import FormDialog from '@/components/FormDialog/index.vue'
-import { ProTableInstance, ColumnProps } from '@/components/ProTable/interface'
+import { ProTableInstance, ColumnProps, EnumProps } from '@/components/ProTable/interface'
 import {
   listVideo2imageApi,
   delVideo2imageApi,
@@ -75,16 +89,28 @@ import {
   exportVideo2imageApi,
   getVideo2imageApi,
   startVideo2imageApi,
+  stopVideo2imageApi,
   downloadVideo2imageApi
 } from '@/api/modules/demo/video2image'
 
 const startVideo2image = async (params: any) => {
   const res = await startVideo2imageApi(params.id)
   if (res.code === 200) {
-    ElMessage.success('开始视频转图片已经开始,请等待完成!')
+    ElMessage.success('任务已开始,请等待完成!')
   } else {
-    ElMessage.error('开始视频转图片开始失败,请检查!')
+    ElMessage.error('任务开始失败,请检查!')
   }
+  proTable.value?.getTableList()
+}
+
+const stopVideo2image = async (params: any) => {
+  const res = await stopVideo2imageApi(params.id)
+  if (res.code === 200) {
+    ElMessage.success('终止任务成功!')
+  } else {
+    ElMessage.error('终止任务失败,请检查!')
+  }
+  proTable.value?.getTableList()
 }
 
 const dowloadVideo2image = async (params: any) => {
@@ -148,66 +174,99 @@ const openDialog = async (type: number, title: string, row?: any) => {
   formDialogRef.value?.openDialog(params)
 }
 
+const statusEnums: EnumProps[] = [
+  {
+    label: '未开始',
+    value: '0',
+    disabled: false,
+    tagType: 'default'
+  },
+  {
+    label: '进行中',
+    value: '1',
+    disabled: false,
+    tagType: 'primary'
+  },
+  {
+    label: '完成',
+    value: '2',
+    disabled: false,
+    tagType: 'success'
+  },
+  {
+    label: '失败',
+    value: '3',
+    disabled: false,
+    tagType: 'danger'
+  },
+  {
+    label: '已中断',
+    value: '4',
+    disabled: false,
+    tagType: 'default'
+  }
+]
+
 // 表格配置项
 const columns = reactive<ColumnProps<any>[]>([
   { type: 'selection', fixed: 'left', width: 70 },
-  { prop: 'id', label: '主键ID' },
+  { prop: 'id', label: '主键ID', width: 180 },
   {
     prop: 'name',
-    label: '视频名称',
+    label: '任务名称',
     search: {
       el: 'input'
     },
-    width: 120
+    width: 150
   },
   {
     prop: 'status',
     label: '任务状态',
     search: {
-      el: 'input'
+      el: 'select'
     },
-    width: 120
+    tag: true,
+    enum: statusEnums
   },
   {
-    prop: 'outPath',
-    label: '切割好的图片的保存路径',
+    prop: 'fps',
+    label: '切割帧率',
     search: {
       el: 'input'
     },
     width: 120
   },
+  {
+    prop: 'outPath',
+    label: '输出路径',
+    width: 120
+  },
   {
     prop: 'startTime',
     label: '开始时间',
-    search: {
-      el: 'date-picker',
-      props: { type: 'datetimerange', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
-    },
-    width: 120
+    // search: {
+    //   el: 'date-picker',
+    //   props: { type: 'datetimerange', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
+    // },
+    width: 180
   },
   {
     prop: 'endTime',
     label: '结束时间',
-    search: {
-      el: 'date-picker',
-      props: { type: 'datetimerange', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
-    },
-    width: 120
+    // search: {
+    //   el: 'date-picker',
+    //   props: { type: 'datetimerange', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
+    // },
+    width: 180
   },
   {
     prop: 'costSecond',
     label: '耗时',
-    search: {
-      el: 'input'
-    },
     width: 120
   },
   {
     prop: 'log',
     label: '日志',
-    search: {
-      el: 'input'
-    },
     width: 120
   },
   {
@@ -220,40 +279,21 @@ const columns = reactive<ColumnProps<any>[]>([
   },
   {
     prop: 'path',
-    label: '视频本身保存路径',
-    search: {
-      el: 'input'
-    },
-    width: 120
-  },
-  {
-    prop: 'fps',
-    label: '切割帧率,默认1',
-    search: {
-      el: 'input'
-    },
-    width: 120
+    label: '视频路径',
+    width: 150
   },
-  { prop: 'operation', label: '操作', width: 230, fixed: 'right' }
+  { prop: 'operation', label: '操作', width: 170, fixed: 'right' }
 ])
 // 表单配置项
 let itemsOptions: ProForm.ItemsOptions[] = []
 const setItemsOptions = () => {
   itemsOptions = [
     {
-      label: '视频名称',
+      label: '任务名称',
       prop: 'name',
-      rules: [{ required: true, message: '视频名称不能为空', trigger: 'blur' }],
+      rules: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }],
       compOptions: {
-        placeholder: '请输入视频名称'
-      }
-    },
-    {
-      label: '备注',
-      prop: 'remarks',
-      rules: [{ required: true, message: '备注不能为空', trigger: 'blur' }],
-      compOptions: {
-        placeholder: '请输入备注'
+        placeholder: '请输入任务名称'
       }
     },
     {
@@ -273,6 +313,25 @@ const setItemsOptions = () => {
         type: 'number',
         placeholder: '请输入切割帧率,范围:1-60'
       }
+    },
+    {
+      label: '上传视频',
+      prop: 'inputOssId',
+      rules: [{ required: true, message: '视频不能为空', trigger: 'blur' }],
+      compOptions: {
+        elTagName: 'file-upload',
+        fileSize: 4096,
+        fileType: ['mp4', 'avi', 'rmvb', 'mov', 'wmv', 'flv'],
+        placeholder: '请上传视频文件'
+      }
+    },
+    {
+      label: '备注',
+      prop: 'remarks',
+      rules: [{ required: false, message: '备注不能为空', trigger: 'blur' }],
+      compOptions: {
+        placeholder: '请输入备注'
+      }
     }
   ]
 }

+ 265 - 130
src/views/demo/videoStable/index.vue

@@ -4,8 +4,8 @@
       <!-- 表格 header 按钮 -->
       <template #tableHeader="scope">
         <el-button type="primary" v-auth="['demo:videoStable:add']" icon="CirclePlus" @click="openDialog(1, '视频去抖动新增')"> 新增 </el-button>
-        <el-button type="primary" v-auth="['demo:videoStable:import']" icon="Upload" plain @click="batchAdd"> 导入</el-button>
-        <el-button type="primary" v-auth="['demo:videoStable:export']" icon="Download" plain @click="downloadFile"> 导出 </el-button>
+        <!--        <el-button type="primary" v-auth="['demo:videoStable:import']" icon="Upload" plain @click="batchAdd"> 导入</el-button>-->
+        <!--        <el-button type="primary" v-auth="['demo:videoStable:export']" icon="Download" plain @click="downloadFile"> 导出 </el-button>-->
         <el-button
           type="danger"
           v-auth="['demo:videoStable:remove']"
@@ -19,27 +19,51 @@
       </template>
       <!-- 表格操作 -->
       <template #operation="scope">
-        <el-button type="primary" link icon="View" @click="startVideoStable(scope.row)" v-if="scope.row.status !== '2'"> 开始去抖动 </el-button>
-        <el-button type="primary" link icon="View" @click="compareVideoStable(scope.row)" v-if="scope.row.status == '2'"> 图片对比 </el-button>
+        <el-button
+          type="primary"
+          link
+          icon="View"
+          @click="startVideoStable(scope.row)"
+          v-if="scope.row.status == '0' || scope.row.status == '3' || scope.row.status == '4'"
+        >
+          开始
+        </el-button>
+        <el-popconfirm title="确定终止此任务吗?" @confirm="stopVideoStable(scope.row)" v-if="scope.row.status == '1'">
+          <template #reference>
+            <el-button type="primary" link icon="View"> 终止 </el-button>
+          </template>
+        </el-popconfirm>
+        <el-button type="primary" link icon="View" @click="compareVideoStable(scope.row)" v-if="scope.row.status == '2'"> 预览 </el-button>
         <el-button type="primary" link icon="View" v-auth="['demo:videoStable:query']" @click="openDialog(3, '视频去抖动查看', scope.row)">
           查看
         </el-button>
-        <el-button type="primary" link icon="EditPen" v-auth="['demo:videoStable:edit']" @click="openDialog(2, '视频去抖动编辑', scope.row)">
+        <!-- <el-button type="primary" link icon="EditPen" v-auth="['demo:videoStable:edit']" @click="openDialog(2, '视频去抖动编辑', scope.row)">
           编辑
-        </el-button>
+        </el-button> -->
         <el-button type="primary" link icon="Delete" v-auth="['demo:videoStable:remove']" @click="deleteVideoStable(scope.row)"> 删除 </el-button>
       </template>
     </ProTable>
     <FormDialog ref="formDialogRef" />
     <ImportExcel ref="dialogRef" />
-    <el-dialog v-model="dialogVisible" :title="'图片对比: ' + taskName + '第' + imageIdx + '张图片对比'" width="80%">
-      <div class="image-dialog">
-        <el-image :src="'data:image/png;base64,' + imageBase64List.origin" style="width: 45%"></el-image>
-        <el-image :src="'data:image/png;base64,' + imageBase64List.stable" style="width: 45%"></el-image>
+    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="80%">
+      <el-form>
+        <el-form-item label="帧率">
+          <el-select v-model="imageFps" placeholder="选择帧率" style="width: 200px" @change="changeFps">
+            <el-option label="0" value="0"></el-option>
+            <el-option label="5" value="5"></el-option>
+            <el-option label="15" value="15"></el-option>
+            <el-option label="30" value="30"></el-option>
+            <el-option label="60" value="60"></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div class="image-dialog" v-if="imageIdx > 0">
+        <el-image :src="'data:image/png;base64,' + cacheImages[imageIdx].origin" style="width: 45%"></el-image>
+        <el-image :src="'data:image/png;base64,' + cacheImages[imageIdx].stable" style="width: 45%"></el-image>
       </div>
-      <div class="image-dialog-btn">
-        <el-button type="primary" @click="pre_picture" :disabled="imageIdx === 1">上一个</el-button>
-        <el-button type="primary" @click="next_picture">下一个</el-button>
+      <div class="image-dialog-btn" v-if="imageFps == 0">
+        <el-button type="primary" @click="pre_picture" :disabled="imageIdx <= 1">上一个</el-button>
+        <el-button type="primary" @click="next_picture" :disabled="imageIdx >= fileCount">下一个</el-button>
       </div>
     </el-dialog>
   </div>
@@ -53,7 +77,7 @@ import { ElMessageBox, ElMessage } from 'element-plus'
 import ProTable from '@/components/ProTable/index.vue'
 import ImportExcel from '@/components/ImportExcel/index.vue'
 import FormDialog from '@/components/FormDialog/index.vue'
-import { ProTableInstance, ColumnProps } from '@/components/ProTable/interface'
+import { ProTableInstance, ColumnProps, EnumProps } from '@/components/ProTable/interface'
 import {
   listVideoStableApi,
   delVideoStableApi,
@@ -64,48 +88,113 @@ import {
   exportVideoStableApi,
   getVideoStableApi,
   startVideoStableApi,
-  getCompareImageApi
+  stopVideoStableApi,
+  getCompareImageApi,
+  getCompareImageCountApi
 } from '@/api/modules/demo/videoStable'
 
 const dialogVisible = ref(false)
-const taskName = ref('')
-const imageIdx = ref(1)
+const taskId = ref('')
+const imageIdx = ref(0)
 const imageBase64List = ref({
   origin: '',
   stable: ''
 })
+const inFileCount = ref(0)
+const outFileCount = ref(0)
+// 直接缓存所有图片
+const cacheImages = ref({ 0: { origin: '', stable: '' } })
+const dialogTitle = ref('')
+const fileCount = ref(0)
+const imageFps = ref(0)
+const intervalChangeFps: any = ref()
+
+const changeFps = () => {
+  console.log('changeFps')
+  if (intervalChangeFps.value) {
+    clearInterval(intervalChangeFps.value)
+  }
+
+  if (imageFps.value == 0) {
+    return
+  }
+  imageIdx.value = 1
+  intervalChangeFps.value = setInterval(() => {
+    next_picture()
+  }, 1000 / imageFps.value)
+}
 
 const startVideoStable = async (params: any) => {
   const res = await startVideoStableApi(params.id)
   if (res.code === 200) {
-    ElMessage.success('开始去抖动成功')
+    ElMessage.success('任务已经开始,请等待')
+  } else {
+    ElMessage.error('任务开始失败,请检查!')
+  }
+  proTable.value?.getTableList()
+}
+
+const stopVideoStable = async (params: any) => {
+  const res = await stopVideoStableApi(params.id)
+  if (res.code === 200) {
+    ElMessage.success('任务终止成功')
   } else {
-    ElMessage.error('开始去抖动失败')
+    ElMessage.error('任务终止失败!')
   }
   proTable.value?.getTableList()
 }
 
-const loadImageData = async (taskName: string, imageIdx: number) => {
-  const res: any = await getCompareImageApi(taskName, imageIdx)
+const loadImageData = async (taskId: string, imageIdx: number) => {
+  const res: any = await getCompareImageApi(taskId, imageIdx)
   imageBase64List.value.origin = res.origin
   imageBase64List.value.stable = res.stable
 }
 const compareVideoStable = async (params: any) => {
-  taskName.value = params.name
-  imageIdx.value = 1
-  await loadImageData(taskName.value, imageIdx.value)
+  taskId.value = params.id
+  imageIdx.value = 0
 
+  const resCount: any = await getCompareImageCountApi(params.id)
+  if (resCount.code === 200) {
+    inFileCount.value = resCount.data.inFileCount
+    outFileCount.value = resCount.data.outFileCount
+  } else {
+    ElMessage.error('获取图片对比数量失败')
+    return
+  }
   dialogVisible.value = true
+
+  dialogTitle.value = '缓存图片中'
+  fileCount.value = Math.min(inFileCount.value, outFileCount.value)
+  for (let idx = 1; idx <= fileCount.value; idx++) {
+    dialogTitle.value = '缓存图片中: 第' + idx + '张图片 共' + fileCount.value + '张图片'
+    if (cacheImages.value[idx]) {
+      continue
+    }
+    const res: any = await getCompareImageApi(taskId.value, idx)
+    cacheImages.value[idx] = {
+      origin: res.origin,
+      stable: res.stable
+    }
+  }
+  next_picture()
 }
 const next_picture = async () => {
-  imageIdx.value = imageIdx.value + 1
-  await loadImageData(taskName.value, imageIdx.value)
+  if (imageIdx.value < fileCount.value) {
+    if (!cacheImages.value[imageIdx.value + 1]) {
+      await loadImageData(taskId.value, imageIdx.value + 1)
+    }
+    imageIdx.value = imageIdx.value + 1
+  }
+  dialogTitle.value = '预览: 第' + imageIdx.value + '张图片 共' + fileCount.value + '张图片'
 }
 const pre_picture = async () => {
   if (imageIdx.value > 1) {
+    if (!cacheImages.value[imageIdx.value - 1]) {
+      await loadImageData(taskId.value, imageIdx.value - 1)
+    }
     imageIdx.value = imageIdx.value - 1
-    await loadImageData(taskName.value, imageIdx.value)
   }
+  dialogTitle.value = '预览: 第' + imageIdx.value + '张图片 共' + fileCount.value + '张图片'
 }
 
 // ProTable 实例
@@ -113,13 +202,13 @@ const proTable = ref<ProTableInstance>()
 
 // 删除视频去抖动信息
 const deleteVideoStable = async (params: any) => {
-  await useHandleData(delVideoStableApi, params.id, '删除【' + params.id + '】视频去抖动')
+  await useHandleData(delVideoStableApi, params.id, '删除任务【' + params.name + '】?')
   proTable.value?.getTableList()
 }
 
 // 批量删除视频去抖动信息
 const batchDelete = async (ids: string[]) => {
-  await useHandleData(delVideoStableApi, ids, '删除所选视频去抖动信息')
+  await useHandleData(delVideoStableApi, ids, '删除所选任务?')
   proTable.value?.clearSelection()
   proTable.value?.getTableList()
 }
@@ -163,75 +252,132 @@ const openDialog = async (type: number, title: string, row?: any) => {
   }
   formDialogRef.value?.openDialog(params)
 }
+const statusEnums: EnumProps[] = [
+  {
+    label: '未开始',
+    value: '0',
+    disabled: false,
+    tagType: 'default'
+  },
+  {
+    label: '进行中',
+    value: '1',
+    disabled: false,
+    tagType: 'primary'
+  },
+  {
+    label: '完成',
+    value: '2',
+    disabled: false,
+    tagType: 'success'
+  },
+  {
+    label: '失败',
+    value: '3',
+    disabled: false,
+    tagType: 'danger'
+  },
+  {
+    label: '已中断',
+    value: '4',
+    disabled: false,
+    tagType: 'default'
+  }
+]
 
 // 表格配置项
 const columns = reactive<ColumnProps<any>[]>([
   { type: 'selection', fixed: 'left', width: 70 },
-  { prop: 'id', label: '主键ID' },
+  { prop: 'id', label: '主键ID', width: 180 },
   {
     prop: 'name',
     label: '视频名称',
     search: {
       el: 'input'
     },
-    width: 120
+    width: 150
   },
   {
     prop: 'status',
-    label: '任务状态 0未开始 1进行中 2已结束',
+    label: '任务状态',
     search: {
-      el: 'input'
+      el: 'select'
     },
-    width: 120
+    width: 100,
+    tag: true,
+    enum: statusEnums
+  },
+  {
+    prop: 'startTime',
+    label: '开始时间',
+    // search: {
+    //   el: 'date-picker',
+    //   props: {
+    //     type: 'datetimerange',
+    //     valueFormat: 'YYYY-MM-DD HH:mm:ss'
+    //   }
+    // },
+    width: 180
+  },
+  {
+    prop: 'endTime',
+    label: '结束时间',
+    // search: {
+    //   el: 'date-picker',
+    //   props: {
+    //     type: 'datetimerange',
+    //     valueFormat: 'YYYY-MM-DD HH:mm:ss'
+    //   }
+    // },
+    width: 180
   },
   {
-    prop: 'inPath',
-    label: '输入图片集路径',
+    prop: 'costSecond',
+    label: '耗时',
     search: {
       el: 'input'
     },
+    width: 80
+  },
+  {
+    prop: 'log',
+    label: '日志',
     width: 120
   },
+
   {
-    prop: 'outPath',
-    label: '去抖动的图片集路径',
-    search: {
-      el: 'input'
-    },
+    prop: 'block_size',
+    label: '网格大小',
     width: 120
   },
   {
-    prop: 'startTime',
-    label: '开始时间',
-    search: {
-      el: 'date-picker',
-      props: { type: 'datetimerange', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
-    },
+    prop: 'radius',
+    label: '扩散半径',
     width: 120
   },
   {
-    prop: 'endTime',
-    label: '结束时间',
-    search: {
-      el: 'date-picker',
-      props: { type: 'datetimerange', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
-    },
+    prop: 'buffer_size',
+    label: '缓冲区大小',
     width: 120
   },
   {
-    prop: 'costSecond',
-    label: '耗时',
-    search: {
-      el: 'input'
-    },
+    prop: 'cornerquality',
+    label: '角点质量',
+    width: 130
+  },
+  {
+    prop: 'cornerminDistance',
+    label: '角点最小距离',
+    width: 180
+  },
+  {
+    prop: 'lklevel',
+    label: '光流层级',
     width: 120
   },
   {
-    prop: 'log',
-    label: '日志',
-    search: {
-      el: 'input'
-    },
+    prop: 'lkwinSiz',
+    label: '光流窗口大小',
     width: 120
   },
   {
@@ -242,7 +388,12 @@ const columns = reactive<ColumnProps<any>[]>([
     },
     width: 120
   },
-  { prop: 'operation', label: '操作', width: 230, fixed: 'right' }
+  {
+    prop: 'operation',
+    label: '操作',
+    width: 230,
+    fixed: 'right'
+  }
 ])
 // 表单配置项
 let itemsOptions: ProForm.ItemsOptions[] = []
@@ -251,120 +402,104 @@ const setItemsOptions = () => {
     {
       label: '任务名称',
       prop: 'name',
-      rules: [{ required: true, message: '视频名称不能为空', trigger: 'blur' }],
+      rules: [{ required: true, message: '任务名称不能为空', trigger: 'blur' }],
       compOptions: {
-        placeholder: '请输入视频名称'
-      }
-    },
-    {
-      label: '输入路径',
-      prop: 'inPath',
-      rules: [{ required: true, message: '输入图片集路径不能为空', trigger: 'blur' }],
-      compOptions: {
-        type: 'block_size',
-        clearable: true,
-        placeholder: '请输入内容'
-      }
-    },
-    {
-      label: '输出路径',
-      prop: 'outPath',
-      rules: [{ required: true, message: '去抖动的图片集路径不能为空', trigger: 'blur' }],
-      compOptions: {
-        type: 'block_size',
-        clearable: true,
-        placeholder: '请输入内容'
+        placeholder: '请输入任务名称'
       }
     },
     {
-      label: '备注',
-      prop: 'remarks',
-      rules: [
-        {
-          required: false,
-          message: '备注不能为空',
-          trigger: 'blur'
-        }
-      ],
+      label: '图片集压缩包',
+      prop: 'inputOssId',
+      rules: [{ required: true, message: '图片集压缩包不能为空', trigger: 'blur' }],
       compOptions: {
-        placeholder: '请输入备注'
+        elTagName: 'file-upload',
+        fileSize: 4096,
+        fileType: ['zip'],
+        placeholder: '请上传图片集压缩包'
       }
     },
     {
-      label: 'block_size',
+      label: '网格大小',
       prop: 'block_size',
-      rules: [{ required: true, message: 'block_size不能为空', trigger: 'blur' }],
+      rules: [],
       compOptions: {
         type: 'input',
         clearable: true,
-        placeholder: '请输入内容',
-        value: 50
+        placeholder: '默认50'
       }
     },
     {
-      label: 'radius',
+      label: '扩散半径',
       prop: 'radius',
-      rules: [{ required: true, message: 'radius不能为空', trigger: 'blur' }],
+      rules: [],
       compOptions: {
-        type: 'block_size',
+        type: 'input',
         clearable: true,
-        placeholder: '请输入内容',
-        value: 500
+        placeholder: '默认500'
       }
     },
     {
-      label: 'buffer_size',
+      label: '缓冲区大小',
       prop: 'buffer_size',
-      rules: [{ required: true, message: 'buffer_size不能为空', trigger: 'blur' }],
+      rules: [],
       compOptions: {
-        type: 'block_size',
+        type: 'input',
         clearable: true,
-        placeholder: '请输入内容',
-        value: 200
+        placeholder: '默认200'
       }
     },
     {
-      label: 'cornerquality',
+      label: '角点质量',
       prop: 'cornerquality',
-      rules: [{ required: true, message: 'cornerquality不能为空', trigger: 'blur' }],
+      rules: [],
       compOptions: {
-        type: 'block_size',
+        type: 'input',
         clearable: true,
-        placeholder: '请输入内容',
-        value: 0.2
+        placeholder: '默认0.2'
       }
     },
     {
-      label: 'cornerminDistance',
+      label: '角点最小距离',
       prop: 'cornerminDistance',
-      rules: [{ required: true, message: 'cornerminDistance不能为空', trigger: 'blur' }],
+      rules: [],
       compOptions: {
-        type: 'block_size',
+        type: 'input',
         clearable: true,
-        placeholder: '请输入内容',
-        value: 5
+        placeholder: '默认5'
       }
     },
     {
-      label: 'lklevel',
+      label: '光流层级',
       prop: 'lklevel',
-      rules: [{ required: true, message: 'lklevel不能为空', trigger: 'blur' }],
+      rules: [],
       compOptions: {
-        type: 'block_size',
+        type: 'input',
         clearable: true,
-        placeholder: '请输入内容',
-        value: 3
+        placeholder: '默认3'
       }
     },
     {
-      label: 'lkwinSiz',
+      label: '光流窗口大小',
       prop: 'lkwinSiz',
-      rules: [{ required: true, message: 'lkwinSiz不能为空', trigger: 'blur' }],
+      rules: [],
       compOptions: {
-        type: 'block_size',
+        type: 'input',
         clearable: true,
-        placeholder: '请输入内容',
-        value: 15
+        placeholder: '默认15'
+      }
+    },
+    {
+      label: '备注',
+      prop: 'remarks',
+      rules: [
+        {
+          required: false,
+          message: '备注不能为空',
+          trigger: 'blur'
+        }
+      ],
+      compOptions: {
+        placeholder: '请输入备注'
       }
     }
   ]

+ 19 - 5
src/views/taais/homePage/index.vue

@@ -111,12 +111,26 @@ const createTaskNew = () => {
 const columns = reactive<ColumnProps<User.ResUserList>[]>([
   // { type: 'selection', fixed: 'left', width: 70 },
   { prop: 'name', label: '任务名称' },
+  // {
+  //   prop: 'status',
+  //   label: '任务状态',
+  //   tag: true,
+  //   enum: () => getDictsApi('biz_task_status'),
+  //   fieldNames: { label: 'dictLabel', value: 'dictValue' }
+  // },
   {
-    prop: 'status',
-    label: '任务状态',
-    tag: true,
-    enum: () => getDictsApi('biz_task_status'),
-    fieldNames: { label: 'dictLabel', value: 'dictValue' }
+    prop: 'createTime',
+    label: '创建时间'
+    // search: {
+    //   el: 'input'
+    // }
+  },
+  {
+    prop: 'updateTime',
+    label: '更新时间'
+    // search: {
+    //   el: 'input'
+    // }
   },
   { prop: 'operation', label: '操作', width: 230, fixed: 'right' }
 ])

+ 74 - 31
src/views/taais/homePage/task/index.vue

@@ -45,8 +45,8 @@
         <el-container style="display: flex; flex-direction: column">
           <el-container v-for="(item, index) in formData.data" :key="index" style="margin-top: 5px">
             <el-button v-show="formData.data" @click="showDataSelectionDialog(index, true)"> 选择批次 </el-button>
-            <span v-if="!item || item.length === 0" class="span_class" style="font-size: 15px; color: red">未完成</span>
-            <span v-else class="span_class" style="font-size: 15px; color: greenyellow">已选择</span>
+            <span v-if="!item || item.length === 0" class="span_class" style=" font-size: 15px;color: red">未完成</span>
+            <span v-else class="span_class" style=" font-size: 15px;color: greenyellow">已选择</span>
             <el-button v-show="formData.data.length > 1" style="margin-left: 20px" @click="removeDataItem(index, true)"> 删除数据 </el-button>
             <el-button
               v-show="formData.taskType === '2' && index + 1 === formData.data.length"
@@ -65,8 +65,8 @@
         <el-container style="display: flex; flex-direction: column">
           <el-container v-for="(item, index) in formData.testData" :key="index" style="margin-top: 5px">
             <el-button v-show="formData.testData" @click="showDataSelectionDialog(index, false)"> 选择批次 </el-button>
-            <span v-if="!item || item.length === 0" class="span_class" style="font-size: 15px; color: red">未完成</span>
-            <span v-else class="span_class" style="font-size: 15px; color: greenyellow">已选择</span>
+            <span v-if="!item || item.length === 0" class="span_class" style=" font-size: 15px;color: red">未完成</span>
+            <span v-else class="span_class" style=" font-size: 15px;color: greenyellow">已选择</span>
             <el-button v-show="formData.testData.length > 1" style="margin-left: 20px" @click="removeDataItem(index, false)"> 删除数据 </el-button>
             <el-button
               v-show="formData.taskType === '2' && index + 1 === formData.testData.length"
@@ -86,14 +86,14 @@
         <el-button v-show="formData.expandData" style="margin-left: 20px" @click="showExpandDataSuperParameterConfig"> 超参配置 </el-button>
       </el-container>
 
-      <el-form-item style="margin-top: 20px; margin-left: 30px">
+      <el-form-item style=" margin-top: 20px;margin-left: 30px">
         <el-button type="primary" @click="submit"> 提交 </el-button>
 
         <el-button @click="cancel"> 取消 </el-button>
       </el-form-item>
     </dv-border-box1>
 
-    <FormDialog ref="formDialogRef" />
+    <FormDialog ref="formDialogRef" @update="handleUpdateModelList" />
 
     <el-dialog v-model="expandDataDialogVisible" title="数据扩增超参配置">
       <!--      <span style="font-size: 16px; color: darkorange; font-weight: bold"> 数据扩增超参 </span>-->
@@ -145,7 +145,7 @@
           <el-table-column prop="batchSize" label="数量" width="80"> </el-table-column>
         </el-table>
 
-        <el-container style="display: flex; flex-direction: column; justify-content: center; margin-right: 10px; margin-left: 10px">
+        <el-container style=" display: flex; flex-direction: column; justify-content: center; margin-right: 10px;margin-left: 10px">
           <el-button type="primary" :disabled="!canSelect" @click="clickSelectData"> {{ '=>' }} </el-button>
           <el-container style="height: 10px"></el-container>
           <el-button type="primary" :disabled="!canDeselect" @click="clickDeselectData"> {{ '<=' }} </el-button>
@@ -169,7 +169,7 @@
 
 <script setup lang="ts">
 import { reactive, ref, onMounted, computed } from 'vue'
-import { addModelApi } from '@/api/modules/ag/model'
+import { addAlgorithmTaskConfigurationApi } from '@/api/modules/ag/model'
 import { createTaskApi, getAlgorithmOptionApi } from '@/api/modules/task/task'
 import FormDialog from '@/components/FormDialog/index.vue'
 import { useRouter } from 'vue-router'
@@ -345,6 +345,13 @@ const canDeselect = computed(() => {
   return tempDeselectedBatchDataList.value.length > 0
 })
 
+const handleUpdateModelList = () => {
+  listTaskConfigurationApi({ pageNum: 1, pageSize: 10000 }).then(res => {
+    // console.log(res)
+    algorithms = reactive(res.data.list)
+  })
+}
+
 onMounted(() => {
   listTaskConfigurationApi({ pageNum: 1, pageSize: 10000 }).then(res => {
     // console.log(res)
@@ -423,9 +430,33 @@ const showDataSelectionDialog = (index, isTrainData) => {
   tempSelectedBatchDataList.value = reactive([])
   tempDeselectedBatchDataList.value = reactive([])
   selectDataIndex = index
+  fillCurrentDataDialogList(index, isTrainData)
   dataDialogVisible.value = true
 }
 
+const fillCurrentDataDialogList = (index, isTrainData) => {
+  let list = []
+  try {
+    list = bIsTrainData.value ? formData.data[index]?.split(',') : formData.testData[index]?.split(',')
+  } catch (e) {
+    console.log(e)
+  }
+  let selected = new Set(list)
+  batchDataList.value = []
+  selectedBatchDataList.value = []
+
+  batchListDataApi().then(res => {
+    // console.log(res)
+    for (let i = 0; i < res.data.length; i++) {
+      if (selected.has(res.data[i].batchNum)) {
+        selectedBatchDataList.value.push(res.data[i])
+      } else {
+        batchDataList.value.push(res.data[i])
+      }
+    }
+  })
+}
+
 let superParameterFormData = reactive({})
 
 const showSuperParameterConfig = (item, index) => {
@@ -458,18 +489,6 @@ const showAddModelDialog = () => {
       width: 580,
       isEdit: true,
       itemsOptions: [
-        {
-          label: '算法',
-          prop: 'algorithmId',
-          rules: [{ required: true, message: '算法不能为空', trigger: 'blur' }],
-          compOptions: {
-            elTagName: 'select',
-            labelKey: 'name',
-            valueKey: 'id',
-            enum: allAgloData.value,
-            placeholder: '请选择算法'
-          }
-        },
         {
           label: '模型名称',
           prop: 'modelName',
@@ -481,35 +500,59 @@ const showAddModelDialog = () => {
         {
           label: '模型',
           prop: 'modelAddress',
-          rules: [{ required: true, message: '模型不能为空', trigger: 'blur' }],
+          rules: [], //{ required: true, message: '模型不能为空', trigger: 'blur' }
           compOptions: {
             elTagName: 'slot'
           }
         },
         {
-          label: '训练样本数',
-          prop: 'sampleNumber',
+          label: '训练算法地址',
+          prop: 'trainUrl',
+          rules: [{ required: true, message: '训练算法地址不能为空', trigger: 'blur' }],
+          compOptions: {
+            placeholder: '请输入训练算法地址'
+          }
+        },
+        {
+          label: '训练超参配置',
+          prop: 'trainParams',
+          compOptions: {
+            placeholder: '请输入训练超参'
+          }
+        },
+        {
+          label: '验证算法地址',
+          prop: 'valUrl',
+          rules: [{ required: true, message: '验证算法地址不能为空', trigger: 'blur' }],
+          compOptions: {
+            placeholder: '请输入验证算法地址'
+          }
+        },
+        {
+          label: '验证超参配置',
+          prop: 'valParams',
           compOptions: {
-            placeholder: '请输入训练样本数'
+            placeholder: '请输入验证超参'
           }
         },
         {
-          label: '训练循环次数',
-          prop: 'cycleEpoch',
+          label: '测试算法地址',
+          prop: 'testUrl',
+          rules: [{ required: true, message: '测试算法地址不能为空', trigger: 'blur' }],
           compOptions: {
-            placeholder: '请输入训练循环次数'
+            placeholder: '请输入测试算法地址'
           }
         },
         {
-          label: '备注',
-          prop: 'remarks',
+          label: '测试超参配置',
+          prop: 'testParams',
           compOptions: {
-            placeholder: '请输入备注'
+            placeholder: '请输入测试超参'
           }
         }
       ],
       model: {},
-      api: addModelApi
+      api: addAlgorithmTaskConfigurationApi
     }
 
     formDialogRef.value?.openDialog(params)

+ 271 - 26
src/views/task/bizProcess/index.vue

@@ -18,9 +18,9 @@
         >
           批量删除
         </el-button>
-        <el-button type="primary" v-auth="['identification:identificationSubtaskDetails:add']" icon="View" @click="contrastResults()">
-          对比结果
-        </el-button>
+        <el-button type="primary" icon="View" @click="showCompareResult"> 对比结果 </el-button>
+        <!--        <el-button type="primary" icon="View" @click="showValResult(true)"> 验证结果 </el-button>-->
+        <el-button type="primary" icon="View" @click="showValResult(false)"> 测试结果 </el-button>
       </template>
       <!-- 表格操作 -->
       <template #operation="scope">
@@ -31,27 +31,20 @@
           v-auth="['identification:identificationSubtaskDetails:query']"
           @click="openDialog(3, '算法业务处理查看', scope.row)"
         >
-          查看
-        </el-button>
-        <el-button
-          type="primary"
-          link
-          icon="EditPen"
-          v-auth="['identification:identificationSubtaskDetails:edit']"
-          @click="openDialog(2, '算法业务处理编辑', scope.row)"
-        >
-          编辑
+          查看详情
         </el-button>
-        <el-button
-          type="primary"
-          link
-          icon="Delete"
-          v-auth="['identification:identificationSubtaskDetails:remove']"
-          @click="deleteBizProcess(scope.row)"
-        >
-          删除
-        </el-button>
-        <el-button type="primary" link icon="View" v-auth="['identification:identificationSubtaskDetails:query']" @click="viewLog(scope.row)">
+
+        <el-button type="primary" link icon="finished" @click="showResult(scope.row)"> 执行结果 </el-button>
+
+        <el-button type="primary" link icon="Refresh" @click="reRunTask(scope.row)"> 重新执行 </el-button>
+
+        <el-button type="primary" link icon="Refresh" @click="exportData(scope.row)"> 导出结果 </el-button>
+
+        <!--        <el-button type="primary" link icon="EditPen" v-auth="['identification:identificationSubtaskDetails:edit']" @click="openDialog(2, '算法业务处理编辑', scope.row)">-->
+        <!--          编辑-->
+        <!--        </el-button>-->
+        <!--        <el-button type="primary" link icon="Delete" v-auth="['identification:identificationSubtaskDetails:remove']" @click="deleteBizProcess(scope.row)"> 删除 </el-button>-->
+        <el-button type="primary" link icon="document" v-auth="['identification:identificationSubtaskDetails:query']" @click="viewLog(scope.row)">
           查看日志
         </el-button>
       </template>
@@ -124,6 +117,104 @@
         </el-row>
       </div>
     </el-dialog>
+
+    <el-dialog v-model="resultDialogVisible" title="执行结果">
+      <!--      style="width: 70vw"-->
+      <el-card :body-style="{ padding: '0px' }">
+        <div style="padding: 14px">
+          <span>P_curve</span>
+        </div>
+        <el-image :src="PATH_PREFIX + refSelectData.resultPath + '/P_curve.png'"></el-image>
+      </el-card>
+
+      <el-card :body-style="{ padding: '0px' }" style="margin-top: 10px">
+        <div style="padding: 14px">
+          <span>R_curve</span>
+        </div>
+        <el-image :src="PATH_PREFIX + refSelectData.resultPath + '/R_curve.png'"></el-image>
+      </el-card>
+
+      <el-card :body-style="{ padding: '0px' }" style="margin-top: 10px">
+        <div style="padding: 14px">
+          <span>PR_curve</span>
+        </div>
+        <el-image :src="PATH_PREFIX + refSelectData.resultPath + '/PR_curve.png'"></el-image>
+      </el-card>
+
+      <el-card :body-style="{ padding: '0px' }" style="margin-top: 10px">
+        <div style="padding: 14px">
+          <span>F1_curve</span>
+        </div>
+        <el-image :src="PATH_PREFIX + refSelectData.resultPath + '/F1_curve.png'"></el-image>
+      </el-card>
+    </el-dialog>
+
+    <el-dialog v-model="compareDialogVisible" title="执行结果对比">
+      <!--      style="width: 70vw"-->
+      <el-container style="display: flex; flex-direction: row">
+        <el-container v-for="(item, index) in listData" :key="index" style="display: flex; flex-direction: column">
+          <!--          <el-button @click="console.log(item)">test</el-button>-->
+          <span style="font-size: 18px"> {{ item.name }} </span>
+          <el-card :body-style="{ padding: '0px' }" style="margin-top: 3px">
+            <div style="padding: 14px">
+              <span>P_curve</span>
+            </div>
+            <el-image :src="PATH_PREFIX + item.resultPath + '/P_curve.png'"></el-image>
+          </el-card>
+
+          <el-card :body-style="{ padding: '0px' }" style="margin-top: 10px">
+            <div style="padding: 14px">
+              <span>R_curve</span>
+            </div>
+            <el-image :src="PATH_PREFIX + item.resultPath + '/R_curve.png'"></el-image>
+          </el-card>
+
+          <el-card :body-style="{ padding: '0px' }" style="margin-top: 10px">
+            <div style="padding: 14px">
+              <span>PR_curve</span>
+            </div>
+            <el-image :src="PATH_PREFIX + item.resultPath + '/PR_curve.png'"></el-image>
+          </el-card>
+
+          <el-card :body-style="{ padding: '0px' }" style="margin-top: 10px">
+            <div style="padding: 14px">
+              <span>F1_curve</span>
+            </div>
+            <el-image :src="PATH_PREFIX + item.resultPath + '/F1_curve.png'"></el-image>
+          </el-card>
+        </el-container>
+      </el-container>
+    </el-dialog>
+
+    <el-dialog v-model="valDialogVisible" :title="titleMsg" style="width: 85vw; height: 85vh">
+      <!--      style="width: 70vw"-->
+      <el-container style="display: flex; flex-direction: row">
+        <el-container v-for="(item, index) in imgDataList" :key="index" style="display: flex; flex-direction: column">
+          <!--          <el-button @click="console.log(item)">test</el-button>-->
+
+          <el-container v-for="(_item, _index) in item" :key="_index">
+            <span v-if="_item.name" style="width: 80px; margin-right: 20px"> {{ _item.name }} </span>
+            <el-image v-if="_item.srcUrl" :src="_item.srcUrl" style=" width: 200px;height: 200px"></el-image>
+            <span v-if="_item.imgUrl">标签</span>
+            <ImgMaker
+              style=" width: 200px;height: 200px"
+              ref="imgMaker"
+              v-if="_item.imgUrl"
+              :is-pic-only="true"
+              :src="_item.imgUrl"
+              :area="area"
+              :width="200"
+              :height="200"
+              :c-width="200"
+              :c-height="200"
+              :class-def="typeDefs"
+              :json-data="_item.jsonData"
+            ></ImgMaker>
+            <el-image v-if="_item.resUrl" :src="_item.resUrl" style=" width: 200px;height: 200px"></el-image>
+          </el-container>
+        </el-container>
+      </el-container>
+    </el-dialog>
   </div>
 </template>
 
@@ -136,6 +227,7 @@ import ProTable from '@/components/ProTable/index.vue'
 import ImportExcel from '@/components/ImportExcel/index.vue'
 import FormDialog from '@/components/FormDialog/index.vue'
 import { ProTableInstance, ColumnProps } from '@/components/ProTable/interface'
+import ImgMaker from '../../demo/components/img-maker'
 import {
   listBizProcessApi,
   delBizProcessApi,
@@ -145,14 +237,151 @@ import {
   getBizProcessApi,
   getTrainResultApi,
   getVerifyResultApi,
-  getTestResultApi
+  getTestResultApi,
+  getImgList
 } from '@/api/modules/task/bizProcessNew'
 // getTrainResultApi,getVerifyResultApi,getTestResultApi
 import { getSubtaskApi } from '@/api/modules/task/subtask'
-import { getDictsApi } from '@/api/modules/system/dictData'
+import { getDictsApi, listDataApi as listDictDataApi } from '@/api/modules/system/dictData'
 import { useRoute } from 'vue-router'
 import ImagePreview from '@/components/ImagePreview/index.vue'
+import http from '@/api'
 const route = useRoute()
+const PATH_PREFIX = 'api/profile/task'
+let resultDialogVisible = ref(false)
+let refSelectData = ref(reactive({}))
+const reRunTask = row => {
+  http.post<any>('/identification/identificationSubtaskDetails/execute', { taskId: row.id }, { loading: false })
+}
+const typeDefs = ref(reactive([]))
+
+const exportData = row => {
+  http.get<any>('/identification/identificationSubtaskDetails/resultZip', { taskId: row.id }, { loading: false }).then(res => {
+    console.log(res)
+    if (res.code === 200) {
+      window.open(res.msg, '_blank')
+    }
+  })
+}
+
+let imgDataList = ref(reactive([]))
+let titleMsg = ref('')
+let valDialogVisible = ref(false)
+const showValResult = async isVal => {
+  let hasInit = false
+  titleMsg.value = isVal ? '验证结果对比' : '测试结果对比'
+  imgDataList.value = reactive([])
+  for (let i = 0; i < listData.value.length; i++) {
+    if (listData.value[i].name.indexOf(isVal ? '验证' : '测试') !== -1) {
+      console.log(listData.value[i])
+      if (!hasInit) {
+        hasInit = true
+        let res = await getImgList({
+          taskId: listData.value[i].preprocessPath.substring(1).split('/')[0],
+          subPath: 'images'
+        })
+        console.log('res data', res)
+        imgDataList.value.push([])
+        for (let j = 0; j < res.data.length; j++) {
+          let jList = res.data[j].split('.')
+          jList[jList.length - 1] = 'txt'
+          let obj = {
+            name: res.data[j],
+            srcUrl: 'api/profile/task' + listData.value[i].preprocessPath + '/' + res.data[j]
+            // imgUrl: 'api/profile/task' + listData.value[i].preprocessPath + '/images/' + res.data[j],
+            // labelUrl: 'api/profile/task' + listData.value[i].preprocessPath + '/labels/' + jList.join('.')
+          }
+
+          imgDataList.value[imgDataList.value.length - 1].push(obj)
+          if (isVal) {
+            obj.imgUrl = 'api/profile/task' + listData.value[i].preprocessPath + '/images/' + res.data[j]
+            obj.labelUrl = '/profile/task' + listData.value[i].preprocessPath + '/labels/' + jList.join('.')
+            console.log('url is', obj.labelUrl)
+            setDetail(obj)
+          }
+        }
+      }
+      let res = await getImgList({
+        taskId: listData.value[i].preprocessPath.substring(1).split('/')[0],
+        subPath: 'images'
+      })
+      console.log(res.data)
+      imgDataList.value.push([])
+      for (let j = 0; j < res.data.length; j++) {
+        let jList = res.data[j].split('.')
+        jList[jList - 1] = 'txt'
+        imgDataList.value[imgDataList.value.length - 1].push({
+          resUrl: 'api/profile/task' + listData.value[i].resultPath + '/' + res.data[j],
+          name: listData.value[i].name
+        })
+      }
+    }
+  }
+  console.log(imgDataList)
+  valDialogVisible.value = true
+}
+
+const setDetail = obj => {
+  http.get<any>(obj.labelUrl).then(res => {
+    obj.jsonData = []
+    console.log('result', res)
+    let arr = res.split('\r\n')
+    console.log(arr)
+    for (let i = 0; i < arr.length; i++) {
+      let subArr = arr[i].split(' ')
+      // console.log(subArr)
+      let cssVal = '#000000'
+      let label = '-1'
+      for (let j = 0; j < typeDefs.value.length; j++) {
+        if (typeDefs.value[j].label === subArr[0]) {
+          cssVal = typeDefs.value[j].color
+          label = typeDefs.value[j].label
+          break
+        }
+      }
+      const ww = 200
+      const hh = 200
+      obj.jsonData.push({
+        pathString:
+          'M ' +
+          subArr[1] * ww +
+          ' ' +
+          subArr[2] * hh +
+          ' L ' +
+          subArr[3] * ww +
+          ' ' +
+          subArr[4] * hh +
+          ' L ' +
+          subArr[5] * ww +
+          ' ' +
+          subArr[6] * hh +
+          ' L ' +
+          subArr[7] * ww +
+          ' ' +
+          subArr[8] * hh +
+          ' z',
+        // left: 0,
+        // top: 0,
+        fill: '',
+        stroke: cssVal,
+        strokeWidth: 5,
+        label: label
+      })
+    }
+  })
+}
+
+let compareDialogVisible = ref(false)
+let listData = ref(reactive([]))
+const showCompareResult = () => {
+  console.log(listData.value)
+  compareDialogVisible.value = true
+}
+const showResult = row => {
+  refSelectData.value = reactive(row)
+  resultDialogVisible.value = true
+  console.log(row)
+}
 const subTaskId = route.query.id as string
 // ProTable 实例
 const proTable = ref<ProTableInstance>()
@@ -179,10 +408,26 @@ const refreshList = () => {
       subtaskId: subTaskId
     }).then(res => {
       bizProcessList.value = res.data['list'].sort((a, b) => b.index - a.index)
+      listData.value = reactive(res.data['list'].sort((a, b) => b.index - a.index))
     })
   }, 0)
 }
 onMounted(() => {
+  listDictDataApi({
+    pageNum: 1,
+    pageSize: 10,
+    dictType: 'class_definition'
+  }).then(res => {
+    // console.log(res)
+    for (let i = 0; i < res.data.list.length; i++) {
+      typeDefs.value.push({
+        name: res.data.list[i].dictLabel,
+        color: res.data.list[i].cssClass,
+        label: res.data.list[i].dictValue
+      })
+    }
+  })
+
   getSubtaskApi(subTaskId).then(res => {
     taskType.value = res.data.type
     taskStatus.value = res.data.status
@@ -283,7 +528,7 @@ const viewLog = row => {
   //   ElMessage.warning('算法状态为待处理,暂无日志')
   //   return
   // }
-  const url = `/api/profile${row.log}`
+  const url = `/api/profile/task` + row.resultPath + '/log/log.log'
   logShow(url)
   dialogVisible.value = true
   timer2.value = setInterval(() => {

+ 22 - 15
src/views/task/subtask/index.vue

@@ -146,29 +146,36 @@ const columns = reactive<ColumnProps<any>[]>([
   { prop: 'id', label: '主键ID' },
   {
     prop: 'name',
-    label: '任务名称',
+    label: '任务类型',
     search: {
       el: 'input'
     }
   },
   {
-    prop: 'status',
-    label: '任务状态',
-    tag: true,
-    enum: () => getDictsApi('biz_task_status'),
+    prop: 'createTime',
+    label: '创建时间',
     search: {
-      el: 'tree-select'
-    },
-    fieldNames: { label: 'dictLabel', value: 'dictValue' }
-  },
-  {
-    prop: 'type',
-    label: '任务类型',
-    tag: true,
-    enum: () => getDictsApi('biz_ag_type'),
-    fieldNames: { label: 'dictLabel', value: 'dictValue' }
+      el: 'input'
+    }
   },
   // {
+  //   prop: 'status',
+  //   label: '任务状态',
+  //   tag: true,
+  //   enum: () => getDictsApi('biz_task_status'),
+  //   search: {
+  //     el: 'tree-select'
+  //   },
+  //   fieldNames: { label: 'dictLabel', value: 'dictValue' }
+  // },
+  // {
+  //   prop: 'type',
+  //   label: '任务类型',
+  //   tag: true,
+  //   enum: () => getDictsApi('biz_ag_type'),
+  //   fieldNames: { label: 'dictLabel', value: 'dictValue' }
+  // },
+  // {
   //   prop: 'parameters',
   //   label: '调用算法时所用的参数'
   // },