index.vue 17 KB

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