index.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. <template>
  2. <div class="table-box">
  3. <el-image-viewer v-if="imgViewVisible" :url-list="['/api' + imageUrl]" @close="imgViewVisible = false" />
  4. <ProTable ref="proTable" :columns="columns" row-key="id" :request-api="listDataApi" :init-param="initParam">
  5. <template #yuan="scope">
  6. <el-image style=" width: 200px;height: 200px" :src="'/api' + scope.row.url" @click="loadImg(scope.row.url)"></el-image>
  7. <!-- <uploadImg :is-show-data="true" :disabled="true" :image-url="scope.row.url" @click="loadImg(scope.row.url)"/>-->
  8. <!-- <el-image style="width: 100px" :src="'/api' + scope.row.url" @click="markImg(scope.row)" />-->
  9. </template>
  10. <!-- 表格 header 按钮 -->
  11. <template #tableHeader="scope">
  12. <el-button type="primary" v-auth="['demo:data:add']" :icon="CirclePlus" @click="openDialog(1, '数据新增')"> 新增 </el-button>
  13. <el-button type="primary" v-auth="['demo:data:import']" :icon="Upload" plain @click="batchAdd"> 导入数据集 </el-button>
  14. <el-button type="primary" v-auth="['demo:data:export']" :icon="Download" plain @click="downloadFile(scope.selectedListIds)"> 导出 </el-button>
  15. <el-button type="primary" v-auth="['system:user:add']" :icon="CirclePlus" @click="dataAmplify()"> 数据增广 </el-button>
  16. <el-button
  17. type="danger"
  18. v-auth="['demo:data:remove']"
  19. :icon="Delete"
  20. plain
  21. :disabled="!scope.isSelected"
  22. @click="batchDelete(scope.selectedListIds)"
  23. >
  24. 批量删除
  25. </el-button>
  26. </template>
  27. <!-- 表格操作 -->
  28. <template #operation="scope">
  29. <!-- <el-button type="primary" link :icon="EditPen" v-auth="['demo:data:edit']" @click="openDialog(2, '数据标注', scope.row)"> 标注 </el-button> -->
  30. <el-button type="primary" link :icon="EditPen" v-auth="['demo:data:edit']" @click="markImg(scope.row)"> 标注 </el-button>
  31. <el-button type="primary" link :icon="EditPen" v-auth="['demo:data:edit']" @click="openDialog(2, '数据编辑', scope.row)"> 编辑 </el-button>
  32. <el-button type="primary" link :icon="View" v-auth="['demo:data:query']" @click="openDialog(3, '数据查看', scope.row)"> 查看 </el-button>
  33. <el-button type="primary" link :icon="Delete" v-auth="['demo:data:remove']" @click="deleteData(scope.row)"> 删除 </el-button>
  34. </template>
  35. </ProTable>
  36. <FormDialog ref="formDialogRef" />
  37. <ImportPicDataset ref="dialogRef" />
  38. <ImgDetect
  39. ref="imgDetect"
  40. :img="cover"
  41. :area="area"
  42. :width="width"
  43. :height="height"
  44. @success="handleImgSuccess"
  45. :classes="classes"
  46. :json-data="jsonData"
  47. >
  48. </ImgDetect>
  49. </div>
  50. </template>
  51. <script setup lang="tsx" name="Data">
  52. import { ref, reactive, toRefs, onMounted } from 'vue'
  53. import { useHandleData } from '@/hooks/useHandleData'
  54. import { useDownload } from '@/hooks/useDownload'
  55. import { ElMessage, ElMessageBox } from 'element-plus'
  56. import ProTable from '@/components/ProTable/index.vue'
  57. import FormDialog from '@/components/FormDialog/index.vue'
  58. import ImportPicDataset from '@/components/ImportPicDataset/index.vue'
  59. import { ProTableInstance, ColumnProps } from '@/components/ProTable/interface'
  60. import { Delete, EditPen, Download, Upload, View, CirclePlus } from '@element-plus/icons-vue'
  61. // import { fabric } from 'fabric'
  62. // import { useDrawArea } from '@/utils/fabric'
  63. import { getDictsApi } from '@/api/modules/system/dictData'
  64. import ImgDetect from '../components/img-detect.vue'
  65. import uploadImg from '@/components/Upload/Img.vue'
  66. import {
  67. listDataApi,
  68. delDataApi,
  69. addDataApi,
  70. updateDataApi,
  71. importTemplateApi,
  72. importDataDataApi,
  73. exportDataApi,
  74. getDataApi
  75. } from '@/api/modules/demo/data'
  76. import { listDataApi as listDictDataApi } from '@/api/modules/system/dictData'
  77. import { uploadPure } from '@/api/modules/upload'
  78. import http from '@/api'
  79. import {useRouter} from "vue-router";
  80. onMounted(() => {
  81. state.cacheData.url = 'http://localhost:9090/profile/upload/2024/08/08/144745610/test.png'
  82. // handleImgSuccess({
  83. // data: 'test change data',
  84. // jsonData: 'jsonData12345 test change',
  85. // url: 'testurl'
  86. // })
  87. })
  88. const imageUrl = ref('')
  89. const imgViewVisible = ref(false)
  90. const loadImg = url => {
  91. imageUrl.value = url
  92. imgViewVisible.value = true
  93. }
  94. const imgDetect = ref()
  95. const state = reactive({
  96. area: '' as string,
  97. width: 1920 as number,
  98. height: 1080 as number,
  99. cover: '',
  100. classes: [],
  101. // [
  102. // {
  103. // name: '飞机',
  104. // color: '#ea5413',
  105. // label: 'plane'
  106. // },
  107. // {
  108. // name: '汽车',
  109. // color: '#ff00ff',
  110. // label: 'car'
  111. // }
  112. // ],
  113. jsonData: [],
  114. cacheData: {}
  115. // '{"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}]}'
  116. })
  117. const { area, width, height, cover, classes, jsonData } = toRefs(state)
  118. // ProTable 实例
  119. const proTable = ref<ProTableInstance>()
  120. // const getImageUrl = name => {
  121. // return new URL(name, import.meta.url).href
  122. // }
  123. const initParam = reactive({ type: 1 })
  124. // 删除数据管理信息
  125. const deleteData = async (params: any) => {
  126. await useHandleData(delDataApi, params.id, `删除【${params.name}】数据`)
  127. proTable.value?.getTableList()
  128. }
  129. // 标注图片
  130. const markImg = data => {
  131. console.log('data is', data)
  132. state.cacheData = data
  133. state.jsonData = []
  134. if (!data.url || data.url.length === 0) {
  135. ElMessage.warning('缺失图像,暂时不能进行区域添加!')
  136. return
  137. }
  138. listDictDataApi({
  139. pageNum: 1,
  140. pageSize: 10,
  141. dictType: 'class_definition'
  142. })
  143. .then(res => {
  144. // console.log(res)
  145. state.classes = []
  146. for (let i = 0; i < res.data.list.length; i++) {
  147. state.classes.push({
  148. name: res.data.list[i].dictLabel,
  149. color: res.data.list[i].cssClass,
  150. label: res.data.list[i].dictValue
  151. })
  152. }
  153. if (state.cacheData.labelurl && state.cacheData.labelurl.length > 0) {
  154. console.log('get label jsonData', state.cacheData.labelurl)
  155. http
  156. .get<any>(state.cacheData.labelurl)
  157. .then(res => {
  158. state.jsonData = []
  159. console.log(res)
  160. let arr = res.replace('\r', '').split('\n')
  161. console.log(arr)
  162. for (let i = 0; i < arr.length; i++) {
  163. let subArr = arr[i].split(' ')
  164. // console.log(subArr)
  165. let cssVal = '#000000'
  166. let label = '-1'
  167. for (let j = 0; j < state.classes.length; j++) {
  168. if (state.classes[j].label === subArr[0]) {
  169. cssVal = state.classes[j].color
  170. label = state.classes[j].label
  171. break
  172. }
  173. }
  174. state.jsonData.push({
  175. subArr: subArr,
  176. pathString:
  177. 'M ' +
  178. subArr[1] * 1920 +
  179. ' ' +
  180. subArr[2] * 1080 +
  181. ' L ' +
  182. subArr[3] * 1920 +
  183. ' ' +
  184. subArr[4] * 1080 +
  185. ' L ' +
  186. subArr[5] * 1920 +
  187. ' ' +
  188. subArr[6] * 1080 +
  189. ' L ' +
  190. subArr[7] * 1920 +
  191. ' ' +
  192. subArr[8] * 1080 +
  193. ' z',
  194. // left: 0,
  195. // top: 0,
  196. fill: '',
  197. stroke: cssVal,
  198. strokeWidth: 5,
  199. label: label
  200. })
  201. }
  202. // console.log(state.jsonData)
  203. imgDetect.value.visible = true
  204. })
  205. .catch(err => {
  206. console.log(err)
  207. })
  208. } else {
  209. imgDetect.value.visible = true
  210. }
  211. })
  212. .catch(err => {
  213. console.log(err)
  214. })
  215. state.cover = '/api' + data.url
  216. // area 代表后端的传来的标注数据
  217. // console.log(state.cover)
  218. // const area = []
  219. // if (state.cover != '') {
  220. // if (area.length != 0) {
  221. // handleImgSuccess(area)
  222. // }
  223. //
  224. // console.log("true???")
  225. // } else {
  226. // ElMessage.warning('缺失图像,暂时不能进行区域添加!')
  227. // }
  228. }
  229. // const getList = () => {
  230. // useDrawArea({
  231. // src: state.cover,
  232. // width: state.width,
  233. // height: state.height,
  234. // area: state.area
  235. // })
  236. // .then(url => {
  237. // state.cover = url as string
  238. // })
  239. // .catch(error => {
  240. // console.log(error)
  241. // })
  242. // }
  243. const handleImgSuccess = data => {
  244. // console.log(data)
  245. state.jsonData = data['jsonData']
  246. state.cover = data['url']
  247. state.cacheData.labelurl = data['data']
  248. state.cacheData.increment = 'NONE'
  249. let arr = state.cacheData.url.split('upload')
  250. // console.log(arr)
  251. let filenames = arr[arr.length - 1].split('.')
  252. filenames[filenames.length - 1] = 'txt'
  253. // console.log(filenames.join('.'))
  254. let filename = filenames.join('.')
  255. filename = filename.replace(/\\/g, '/')
  256. if (filename.startsWith('/')) {
  257. filename = filename.substring(1)
  258. }
  259. // console.log(filename)
  260. labelFile(data['data'], filename).then(res => {
  261. // console.log(res)
  262. if (res.code === 200) {
  263. state.cacheData.labelurl = res.data.url
  264. updateDataApi(state.cacheData)
  265. .then(res => {
  266. // console.log(res)
  267. if (res.data) {
  268. ElMessage.success('操作成功')
  269. }
  270. })
  271. .catch(err => {
  272. console.log(err)
  273. })
  274. }
  275. })
  276. // data.forEach(item => {
  277. // if (item.startsWith('{')) {
  278. // return
  279. // }
  280. // const area = item.split(';')
  281. // // try=tly blx=tlx brx=trx bry=bly
  282. // // mark: 当前用的应该是左上角定点位置和长宽,应该替换为存储点
  283. // const tlx = Math.round(Number(area[0]) * 1920)
  284. // const tly = Math.round(Number(area[1]) * 1080)
  285. // const trx = tlx + Math.round(Number(area[2]) * 1920)
  286. // const bly = tly + Math.round(Number(area[3]) * 1080)
  287. // state.area += `${tlx};${tly};${trx};${tly};${trx};${bly};${tlx};${bly},`
  288. // })
  289. // // console.log('state.area', state.area)
  290. // state.area.slice(0, -1)
  291. // getList()
  292. }
  293. // 创建并提交标注txt文件
  294. const labelFile = (data, filename) => {
  295. // 创建Blob对象
  296. const blob = new Blob([data], { type: 'text/plain' })
  297. // 创建表单数据并添加文件
  298. const formData = new FormData()
  299. formData.append('file', blob, filename)
  300. return uploadPure(formData)
  301. }
  302. const labeledTypeData = [
  303. {
  304. label: '是',
  305. value: true
  306. },
  307. {
  308. label: '否',
  309. value: false
  310. }
  311. ]
  312. // 批量删除数据管理信息
  313. const batchDelete = async (ids: string[]) => {
  314. await useHandleData(delDataApi, ids, '删除所选数据信息')
  315. proTable.value?.clearSelection()
  316. proTable.value?.getTableList()
  317. }
  318. // 导出数据管理列表
  319. const downloadFile = async (ids: string[]) => {
  320. proTable.value.searchParam['selectedIds'] = ids
  321. ElMessageBox.confirm('确认导出数据管理数据?', '温馨提示', { type: 'warning' }).then(() =>
  322. useDownload(exportDataApi, '数据管理列表', proTable.value?.searchParam)
  323. )
  324. }
  325. // 批量添加数据管理
  326. const dialogRef = ref<InstanceType<typeof ImportPicDataset> | null>(null)
  327. const batchAdd = () => {
  328. const params = {
  329. title: '数据管理添加数据集',
  330. tempApi: importTemplateApi,
  331. importApi: importDataDataApi,
  332. getTableList: proTable.value?.getTableList
  333. }
  334. dialogRef.value?.acceptParams(params)
  335. }
  336. const router = useRouter()
  337. const dataAmplify = () => {
  338. router.push(`/data/amplify`)
  339. }
  340. const formDialogRef = ref<InstanceType<typeof FormDialog> | null>(null)
  341. // 打开弹框的功能
  342. const openDialog = async (type: number, title: string, row?: any) => {
  343. let res = { data: {} }
  344. if (row?.id) {
  345. res = await getDataApi(row?.id || null)
  346. }
  347. // 重置表单
  348. setFormItems()
  349. const params = {
  350. title,
  351. width: 580,
  352. isEdit: type !== 3,
  353. itemsOptions: formItems,
  354. model: type == 1 ? {} : res.data,
  355. api: type == 1 ? addDataApi : updateDataApi,
  356. getTableList: proTable.value?.getTableList
  357. }
  358. formDialogRef.value?.openDialog(params)
  359. }
  360. // 表格配置项
  361. const columns = reactive<ColumnProps<any>[]>([
  362. { type: 'selection', fixed: 'left', width: 70 },
  363. { prop: 'yuan', label: '原图', width: 200 },
  364. {
  365. prop: 'batchNum',
  366. label: '批次号',
  367. search: {
  368. el: 'input'
  369. },
  370. width: 120
  371. },
  372. {
  373. prop: 'name',
  374. label: '名称',
  375. search: {
  376. el: 'input'
  377. },
  378. width: 120
  379. },
  380. {
  381. prop: 'objectType',
  382. label: '目标类型',
  383. search: {
  384. el: 'input'
  385. },
  386. width: 120
  387. },
  388. {
  389. prop: 'objectSubtype',
  390. label: '目标子类型',
  391. search: {
  392. el: 'input'
  393. },
  394. width: 120
  395. },
  396. {
  397. prop: 'scene',
  398. label: '场景',
  399. search: {
  400. el: 'input'
  401. },
  402. width: 120
  403. },
  404. {
  405. prop: 'dataSource',
  406. label: '数据源',
  407. search: {
  408. el: 'input'
  409. },
  410. width: 120
  411. },
  412. {
  413. prop: 'gatherSpot',
  414. label: '采集地点',
  415. search: {
  416. el: 'input'
  417. },
  418. width: 120
  419. },
  420. {
  421. prop: 'gatherTime',
  422. label: '采集时间',
  423. search: {
  424. el: 'date-picker',
  425. props: { type: 'datetimerange', valueFormat: 'YYYY-MM-DD HH:mm:ss' }
  426. },
  427. width: 120
  428. },
  429. {
  430. prop: 'dataType',
  431. label: '数据类型',
  432. enum: () => getDictsApi('data_type'),
  433. search: {
  434. el: 'tree-select'
  435. },
  436. fieldNames: { label: 'dictLabel', value: 'dictValue' },
  437. width: 120
  438. },
  439. // {
  440. // prop: 'fileType',
  441. // label: '文件类型',
  442. // search: {
  443. // el: 'input'
  444. // },
  445. // width: 120
  446. // },
  447. // {
  448. // prop: 'increment',
  449. // label: '扩增方式',
  450. // search: {
  451. // el: 'input'
  452. // },
  453. // width: 120
  454. // },
  455. {
  456. prop: 'labeled',
  457. label: '是否标注',
  458. search: {
  459. el: 'input'
  460. },
  461. width: 120
  462. },
  463. { prop: 'operation', label: '操作', width: 230, fixed: 'right' }
  464. ])
  465. // 表单配置项
  466. let formItems: ProForm.ItemsOptions[] = []
  467. const setFormItems = () => {
  468. formItems = [
  469. {
  470. label: '原图',
  471. prop: 'url',
  472. compOptions: {
  473. elTagName: 'img-upload',
  474. placeholder: '请选择上传原图'
  475. }
  476. },
  477. {
  478. label: '批次号',
  479. prop: 'batchNum',
  480. rules: [{ required: true, message: '批次号不能为空', trigger: 'blur' }],
  481. compOptions: {
  482. placeholder: '请输入批次号'
  483. }
  484. },
  485. {
  486. label: '名称',
  487. prop: 'name',
  488. rules: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
  489. compOptions: {
  490. placeholder: '请输入名称'
  491. }
  492. },
  493. {
  494. label: '目标类型',
  495. prop: 'objectType',
  496. rules: [{ required: true, message: '目标类型不能为空', trigger: 'blur' }],
  497. compOptions: {
  498. placeholder: '请输入目标类型'
  499. }
  500. },
  501. {
  502. label: '目标子类型',
  503. prop: 'objectSubtype',
  504. rules: [{ required: true, message: '目标子类型不能为空', trigger: 'blur' }],
  505. compOptions: {
  506. placeholder: '请输入目标子类型'
  507. }
  508. },
  509. {
  510. label: '场景',
  511. prop: 'scene',
  512. rules: [{ required: true, message: '场景不能为空', trigger: 'blur' }],
  513. compOptions: {
  514. placeholder: '请输入场景'
  515. }
  516. },
  517. {
  518. label: '数据源',
  519. prop: 'dataSource',
  520. rules: [{ required: true, message: '数据源不能为空', trigger: 'blur' }],
  521. compOptions: {
  522. placeholder: '请输入数据源'
  523. }
  524. },
  525. {
  526. label: '采集地点',
  527. prop: 'gatherSpot',
  528. rules: [{ required: true, message: '采集地点不能为空', trigger: 'blur' }],
  529. compOptions: {
  530. placeholder: '请输入采集地点'
  531. }
  532. },
  533. {
  534. label: '采集时间',
  535. prop: 'gatherTime',
  536. rules: [{ required: true, message: '采集时间不能为空', trigger: 'change' }],
  537. compOptions: {
  538. elTagName: 'date-picker',
  539. type: 'datetime',
  540. valueFormat: 'YYYY-MM-DD HH:mm:ss',
  541. placeholder: '请选择采集时间'
  542. }
  543. },
  544. {
  545. label: '数据类型',
  546. prop: 'dataType',
  547. rules: [{ required: true, message: '数据类型不能为空', trigger: 'change' }],
  548. compOptions: {
  549. elTagName: 'select',
  550. labelKey: 'dictLabel',
  551. valueKey: 'dictValue',
  552. enum: () => getDictsApi('data_type'),
  553. placeholder: '请选择数据类型'
  554. }
  555. },
  556. {
  557. label: '是否标注',
  558. prop: 'labeled',
  559. rules: [{ required: true, message: '请选择是否标注' }],
  560. compOptions: {
  561. elTagName: 'radio-group',
  562. enum: labeledTypeData
  563. }
  564. }
  565. ]
  566. }
  567. </script>