@@ -0,0 +1,4 @@ | |||
ENV='development' | |||
VUE_APP_MOCK=true | |||
VUE_APP_BASE_API = '' | |||
VUE_APP_DATA_API = '/mock' |
@@ -2,4 +2,5 @@ build/*.js | |||
src/assets | |||
public | |||
dist | |||
src/components/Crud | |||
src/components/Crud | |||
mock |
@@ -0,0 +1 @@ | |||
package-lock=true |
@@ -1,3 +1,18 @@ | |||
## 0.2.1 (2020-11-16) | |||
### Features | |||
- 页面布局中的footer可配置 | |||
- 新增前端开发时mock后端接口的功能 | |||
- [数据管理] 创建数据集时选择标签组、标签组管理查看标签详情体验优化 | |||
- [训练管理] 提取前端参数配置到公共配置文件 (config/index.js) | |||
### Bug Fixs | |||
- 锁定Element UI版本,修复其新版不兼容升级导致的功能异常 | |||
- [训练管理] 分布式训练默认节点数调整, 节点数下限改为2 | |||
- [训练管理] 修复模型下载、模型保存、断点续训目录树弹窗loading效果 | |||
## 0.2.0 (2020-10-26) | |||
### Breaking Change | |||
@@ -79,6 +79,15 @@ npm install | |||
npm run dev | |||
``` | |||
## 接口 Mock | |||
当前项目自动集成了接口 mock 服务,用户可以通过 `npm run mock` 启动数据 mock 服务。 | |||
- 普通接口:在 `mock` 目录下创建根据请求 url 创建对应文件,比如请求路径是`api/data/datasets`,在就直接创建 `mock/api/data/datasets.js` 文件,并导出 mock 文件 | |||
- RESTful 风格接口:在 `mock/mock-map` 文件下创建对应的文件 map, key 为符合[path-to-regexp](https://github.com/pillarjs/path-to-regexp) 风格的路径,value 为对应的实际 mock 文件地址 | |||
如果用户未创建 mock 文件,请求会转发到 `development` 环境指定的 api 地址。 | |||
## 项目结构 | |||
``` | |||
@@ -0,0 +1,56 @@ | |||
module.exports = { | |||
"code": 200, | |||
"msg": null, | |||
"data": { | |||
"result": [{ | |||
"id": 56, | |||
"name": "bag_data", | |||
"remark": "不可删除,不可删除", | |||
"type": 0, | |||
"uri": null, | |||
"dataType": 0, | |||
"annotateType": 2, | |||
"status": 104, | |||
"createTime": "2020-10-21 15:39:01", | |||
"updateTime": "2020-10-22 14:13:10", | |||
"team": null, | |||
"createUser": null, | |||
"updateUser": null, | |||
"progress": null, | |||
"currentVersionName": null, | |||
"decompressState": 0, | |||
"labelGroupId": 1, | |||
"labelGroupName": "COCO", | |||
"labelGroupType": 1, | |||
"import": false, | |||
"top": true | |||
}, { | |||
"id": 346, | |||
"name": "test432", | |||
"remark": "test432", | |||
"type": 0, | |||
"uri": null, | |||
"dataType": 0, | |||
"annotateType": 1, | |||
"status": 101, | |||
"createTime": "2020-10-27 15:20:58", | |||
"updateTime": "2020-10-27 15:20:58", | |||
"team": null, | |||
"createUser": null, | |||
"updateUser": null, | |||
"progress": null, | |||
"currentVersionName": null, | |||
"decompressState": 0, | |||
"labelGroupId": 468, | |||
"labelGroupName": "test432", | |||
"labelGroupType": 0, | |||
"import": false, | |||
"top": false | |||
}], | |||
"page": { | |||
"size": 10, | |||
"current": 1, | |||
"total": 218 | |||
}, | |||
} | |||
} |
@@ -0,0 +1,5 @@ | |||
module.exports = { | |||
"code": 200, | |||
"msg": null, | |||
"data": [] | |||
} |
@@ -0,0 +1,4 @@ | |||
// 定义 RESTful 接口和实际代码的映射 | |||
module.exports = { | |||
'GET::/api/data/labelGroup/getList/(\\d+)': '/api/data/labelGroup/getList/id', | |||
}; |
@@ -1,6 +1,6 @@ | |||
{ | |||
"name": "dubhe-web", | |||
"version": "0.2.0", | |||
"version": "0.2.1", | |||
"description": "之江天枢人工智能开源平台", | |||
"author": "zhejianglab", | |||
"keywords": [ | |||
@@ -11,6 +11,7 @@ | |||
"人工智能" | |||
], | |||
"scripts": { | |||
"mock": "vue-cli-service serve --mode mock --open", | |||
"dev": "vue-cli-service serve --open", | |||
"build:prod": "vue-cli-service build", | |||
"build:test": "vue-cli-service build --mode test", | |||
@@ -51,7 +52,7 @@ | |||
"date-fns": "^2.13.0", | |||
"echarts": "4.2.1", | |||
"echarts-gl": "^1.1.1", | |||
"element-ui": "^2.13.2", | |||
"element-ui": "2.13.2", | |||
"file-saver": "^2.0.2", | |||
"filereader-stream": "^2.0.0", | |||
"jquery": "^3.5.1", | |||
@@ -66,6 +67,7 @@ | |||
"normalize.css": "7.0.0", | |||
"nprogress": "0.2.0", | |||
"p-map": "^4.0.0", | |||
"path-to-regexp": "^6.2.0", | |||
"prismjs": "^1.20.0", | |||
"promise.allsettled": "^1.0.2", | |||
"qs": "^6.9.1", | |||
@@ -105,6 +107,7 @@ | |||
"eslint-plugin-import": "^2.20.2", | |||
"eslint-plugin-prettier": "^2.3.1", | |||
"eslint-plugin-vue": "^6.2.2", | |||
"express-http-proxy": "^1.6.2", | |||
"html-webpack-plugin": "3.2.0", | |||
"husky": "^4.2.5", | |||
"less": "^3.11.3", | |||
@@ -222,8 +222,8 @@ | |||
<el-input-number | |||
id="resourcesPoolNode" | |||
v-model="form.resourcesPoolNode" | |||
:min="1" | |||
:max="8" | |||
:min="2" | |||
:max="trainConfig.trainNodeMax" | |||
:step-strictly="true" | |||
/> | |||
<el-tooltip effect="dark" content="请确保代码中包含“num_nodes”参数和“node_ips”参数用于接收分布式相关参数" placement="top"> | |||
@@ -282,7 +282,7 @@ | |||
id="delayCreateTime" | |||
v-model="form.delayCreateTime" | |||
:min="0" | |||
:max="168" | |||
:max="trainConfig.delayCreateTimeMax" | |||
:step-strictly="true" | |||
/> 小时 | |||
</el-form-item> | |||
@@ -295,7 +295,7 @@ | |||
id="delayDeleteTime" | |||
v-model="form.delayDeleteTime" | |||
:min="0" | |||
:max="168" | |||
:max="trainConfig.delayDeleteTimeMax" | |||
:step-strictly="true" | |||
/> 小时 | |||
<el-tooltip effect="dark" content="选择 0 表示不限制训练时长" placement="top"> | |||
@@ -374,6 +374,7 @@ import { list as getAlgorithmList } from '@/api/algorithm/algorithm'; | |||
import { harborProjectNames, harborImageNames } from '@/api/system/harbor'; | |||
import { list as getModelName } from '@/api/model/model'; | |||
import { list as getModelTag } from '@/api/model/modelVersion'; | |||
import { trainConfig } from '@/config'; | |||
import RunParamForm from './runParamForm'; | |||
import DataSourceSelector from './dataSourceSelector'; | |||
@@ -442,6 +443,7 @@ export default { | |||
dictReady: false, | |||
delayCreateDelete: false, | |||
selectedAlgorithm: null, | |||
trainConfig, | |||
form: { ...defaultForm }, | |||
rules: { | |||
@@ -814,9 +816,7 @@ export default { | |||
this.onResourcesPoolTypeChange(); | |||
}, | |||
onTrainTypeChange(trainType) { | |||
if (trainType === 0) { | |||
this.form.resourcesPoolNode = 1; | |||
} | |||
this.form.resourcesPoolNode = trainType === 0 ? 1 : 2; | |||
}, | |||
}, | |||
}; | |||
@@ -14,31 +14,52 @@ | |||
* ============================================================= | |||
*/ | |||
module.exports = { | |||
minIO: { | |||
development: { | |||
config: { | |||
endPoint: '', // MinIO 服务地址 | |||
port: 9000, | |||
useSSL: false, | |||
}, | |||
bucketName: 'dubhe-dev', | |||
// minIO 参数配置 | |||
export const minIO = { | |||
development: { | |||
config: { | |||
endPoint: '', // MinIO 服务地址 | |||
port: 9000, | |||
useSSL: false, | |||
}, | |||
test: { | |||
config: { | |||
endPoint: '', | |||
port: 9000, | |||
useSSL: false, | |||
}, | |||
bucketName: 'dubhe-test', | |||
bucketName: 'dubhe-dev', | |||
}, | |||
test: { | |||
config: { | |||
endPoint: '', | |||
port: 9000, | |||
useSSL: false, | |||
}, | |||
production: { | |||
config: { | |||
endPoint: '', | |||
port: 9000, | |||
useSSL: false, | |||
}, | |||
bucketName: 'dubhe-prod', | |||
bucketName: 'dubhe-test', | |||
}, | |||
production: { | |||
config: { | |||
endPoint: '', | |||
port: 9000, | |||
useSSL: false, | |||
}, | |||
bucketName: 'dubhe-prod', | |||
}, | |||
}; | |||
// 训练管理模块参数配置 | |||
export const trainConfig = { | |||
trainNodeMax: Infinity, // 分布式训练节点上限 | |||
delayCreateTimeMax: 168, // 延时启动时间上限 | |||
delayDeleteTimeMax: 168, // 训练时长上限 | |||
}; | |||
// 算法管理参数配置 | |||
export const algorithmConfig = { | |||
uploadFileAcceptSize: 1024, // 上传算法文件大小限制,单位为 MB,0 表示不限制大小 | |||
}; | |||
// 镜像管理参数配置 | |||
export const imageConfig = { | |||
uploadFileAcceptSize: 0, // 上传镜像文件大小限制,单位为 MB,0 表示不限制大小 | |||
}; | |||
// 模型管理模块参数配置 | |||
export const modelConfig = { | |||
uploadFileAcceptSize: 0, // 上传模型文件大小限制,单位为 MB,0 表示不限制大小 | |||
}; |
@@ -26,11 +26,19 @@ | |||
</template> | |||
<template v-slot:right> | |||
<slot name="right-options" /> | |||
<Guideline /> | |||
<Feedback /> | |||
</template> | |||
</navbar> | |||
</div> | |||
<app-main /> | |||
<div v-if="$store.state.settings.showFooter && showFooter" id="el-main-footer"> | |||
<span> {{ $store.state.settings.footerTxt }} </span> | |||
<template v-if="$store.state.settings.caseNumber"> | |||
<span>⋅</span> | |||
<a href="/" target="_blank">{{ $store.state.settings.caseNumber }}</a> | |||
</template> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
@@ -38,7 +46,7 @@ | |||
<script> | |||
import { mapState } from 'vuex'; | |||
import ResizeMixin from './mixin/ResizeHandler'; | |||
import { AppMain, Navbar, Sidebar, Feedback } from './components'; | |||
import { AppMain, Navbar, Sidebar, Guideline, Feedback } from './components'; | |||
export default { | |||
name: 'BaseLayout', | |||
@@ -46,6 +54,7 @@ export default { | |||
AppMain, | |||
Navbar, | |||
Sidebar, | |||
Guideline, | |||
Feedback, | |||
}, | |||
mixins: [ResizeMixin], | |||
@@ -62,6 +71,10 @@ export default { | |||
type: Boolean, | |||
default: true, | |||
}, | |||
showFooter: { | |||
type: Boolean, | |||
default: true, | |||
}, | |||
}, | |||
computed: { | |||
...mapState({ | |||
@@ -151,4 +164,21 @@ export default { | |||
.mobile .fixed-header { | |||
width: 100%; | |||
} | |||
#el-main-footer { | |||
position: fixed; | |||
bottom: 0; | |||
z-index: 99; | |||
width: 100%; | |||
height: 33px; | |||
padding: 10px 6px 0 6px; | |||
overflow: hidden; | |||
font-family: Arial, sans-serif !important; | |||
font-size: 0.7rem !important; | |||
color: #7a8b9a; | |||
letter-spacing: 0.8px; | |||
pointer-events: none; | |||
background: none repeat scroll 0 0 white; | |||
border-top: 1px solid #e7eaec; | |||
} | |||
</style> |
@@ -15,7 +15,7 @@ | |||
*/ | |||
<template> | |||
<BaseLayout :showBack="true" :showSidebar="false"> | |||
<BaseLayout :showBack="true" :showSidebar="false" :showFooter="false"> | |||
<div slot="left-options" style="margin-left: 10px;"> | |||
<el-tooltip effect="dark" placement="bottom-start"> | |||
<div slot="content"> | |||
@@ -17,13 +17,6 @@ | |||
<template> | |||
<section class="app-main"> | |||
<router-view :key="$route.path" /> | |||
<div v-if="$store.state.settings.showFooter" id="el-main-footer"> | |||
<span> {{ $store.state.settings.footerTxt }} </span> | |||
<template v-if="$store.state.settings.caseNumber"> | |||
<span>⋅</span> | |||
<a href="/" target="_blank">{{ $store.state.settings.caseNumber }}</a> | |||
</template> | |||
</div> | |||
</section> | |||
</template> | |||
@@ -46,23 +39,6 @@ export default { | |||
.fixed-header + .app-main { | |||
padding-top: 50px; | |||
} | |||
#el-main-footer { | |||
position: fixed; | |||
bottom: 0; | |||
z-index: 99; | |||
width: 100%; | |||
height: 33px; | |||
padding: 10px 6px 0 6px; | |||
overflow: hidden; | |||
font-family: Arial, sans-serif !important; | |||
font-size: 0.7rem !important; | |||
color: #7a8b9a; | |||
letter-spacing: 0.8px; | |||
pointer-events: none; | |||
background: none repeat scroll 0 0 white; | |||
border-top: 1px solid #e7eaec; | |||
} | |||
</style> | |||
<style lang="scss"> | |||
@@ -106,7 +106,7 @@ export default { | |||
@import "~@/assets/styles/variables.scss"; | |||
.feedback { | |||
margin-right: 10px; | |||
margin-right: 20px; | |||
font-size: 14px; | |||
line-height: $navBarHeight; | |||
color: $infoColor; | |||
@@ -0,0 +1,57 @@ | |||
/** Copyright 2020 Zhejiang Lab. All Rights Reserved. | |||
* | |||
* Licensed under the Apache License, Version 2.0 (the "License"); | |||
* you may not use this file except in compliance with the License. | |||
* You may obtain a copy of the License at | |||
* | |||
* http://www.apache.org/licenses/LICENSE-2.0 | |||
* | |||
* Unless required by applicable law or agreed to in writing, software | |||
* distributed under the License is distributed on an "AS IS" BASIS, | |||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
* See the License for the specific language governing permissions and | |||
* limitations under the License. | |||
* ============================================================= | |||
*/ | |||
<template> | |||
<div class="doc-link" > | |||
<a class="link-action" target="_blank" :href="DocLink"> | |||
使用文档 | |||
<IconFont type="externallink" /> | |||
</a> | |||
</div> | |||
</template> | |||
<script> | |||
import { DocLink } from '@/settings'; | |||
export default { | |||
name: 'Guideline', | |||
setup() { | |||
return { | |||
DocLink, | |||
}; | |||
}, | |||
}; | |||
</script> | |||
<style lang="scss"> | |||
@import "~@/assets/styles/variables.scss"; | |||
.doc-link { | |||
margin-right: 20px; | |||
font-size: 14px; | |||
line-height: $navBarHeight; | |||
cursor: pointer; | |||
} | |||
.link-action { | |||
display: block; | |||
text-align: center; | |||
color: $infoColor; | |||
&:hover { | |||
color: $primaryColor; | |||
} | |||
} | |||
</style> |
@@ -17,4 +17,5 @@ | |||
export { default as AppMain } from './AppMain'; | |||
export { default as Navbar } from './Navbar'; | |||
export { default as Sidebar } from './Sidebar'; | |||
export { default as Guideline } from './Guideline'; | |||
export { default as Feedback } from './Feedback'; |
@@ -58,9 +58,13 @@ module.exports = { | |||
/** | |||
* RSA公钥 | |||
*/ | |||
publicKey: 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANL378k3RiZHWx5AfJqdH9xRNBmD9wGD2iRe41HdTNF8RUhNnHit5NpMNtGL0NPTSSpPjjI1kJfVorRvaQerUgkCAwEAAQ==', | |||
publicKey: '', | |||
/** | |||
* 用户社区 | |||
*/ | |||
Community: 'http://www.aiiaos.cn/index.php?s=/forum/index/forum/id/45.html', | |||
/** | |||
* 使用文档 | |||
*/ | |||
DocLink: 'http://docs.dubhe.ai/docs/' , | |||
}; |
@@ -16,10 +16,10 @@ | |||
import { getMinIOAuth } from '@/api/auth'; | |||
import { decrypt } from '@/utils/rsaEncrypt'; | |||
import { minIO } from '@/config'; | |||
const Minio = require('minio'); | |||
const toArray = require('stream-to-array'); | |||
const Config = require('@/config'); | |||
const env = process.env.NODE_ENV || 'development'; | |||
@@ -36,7 +36,7 @@ const makeBucket = (client, bucketName) => { | |||
}); | |||
}; | |||
const minIOConfig = Config.minIO[env]; | |||
const minIOConfig = minIO[env]; | |||
// 导出 bucketName | |||
export const {bucketName} = minIOConfig; | |||
@@ -343,3 +343,11 @@ export const getTreeListFromFilepath = async (filepath) => { | |||
export function getUniqueId() { | |||
return parseTime(new Date(), '{y}{m}{d}{h}{i}{s}{S}') + nanoid(4); | |||
} | |||
// 以 MB 为入参单位,格式化上传大小文本 | |||
export function uploadSizeFomatter(size) { | |||
if (size >= 1024) { | |||
return `${size / 1024} GB`; | |||
} | |||
return `${size} MB`; | |||
} |
@@ -181,8 +181,8 @@ | |||
ref="upload" | |||
action="fakeApi" | |||
accept=".zip" | |||
:acceptSize="1024" | |||
:acceptSizeFormat="(size) => `${size/1024} GB`" | |||
:acceptSize="algorithmConfig.uploadFileAcceptSize" | |||
:acceptSizeFormat="uploadSizeFomatter" | |||
list-type="text" | |||
:show-file-count="false" | |||
:params="uploadParams" | |||
@@ -263,7 +263,7 @@ | |||
</template> | |||
<script> | |||
import { downloadZipFromObjectPath, validateNameWithHyphen, getUniqueId } from '@/utils'; | |||
import { downloadZipFromObjectPath, validateNameWithHyphen, getUniqueId, uploadSizeFomatter } from '@/utils'; | |||
import CRUD, { presenter, header, form, crud } from '@crud/crud'; | |||
import cdOperation from '@crud/CD.operation'; | |||
import rrOperation from '@crud/RR.operation'; | |||
@@ -275,6 +275,7 @@ import BaseModal from '@/components/BaseModal'; | |||
import AlgorithmDetail from '@/components/Training/algorithmDetail'; | |||
import UploadInline from '@/components/UploadForm/inline'; | |||
import UploadProgress from '@/components/UploadProgress'; | |||
import { algorithmConfig } from '@/config'; | |||
const defaultForm = { | |||
id: null, | |||
@@ -366,6 +367,7 @@ export default { | |||
uploading: false, | |||
progress: 0, | |||
size: 0, | |||
algorithmConfig, | |||
customColors: [ | |||
{color: '#909399', percentage: 40}, | |||
{color: '#e6a23c', percentage: 80}, | |||
@@ -577,6 +579,7 @@ export default { | |||
noteBookName, | |||
}}); | |||
}, | |||
uploadSizeFomatter, | |||
}, | |||
}; | |||
</script> |
@@ -240,7 +240,6 @@ export default { | |||
<style lang="scss"> | |||
.workspace-settings { | |||
padding: 28px 28px 0; | |||
margin-bottom: 33px; | |||
overflow-y: auto; | |||
background-color: rgb(242, 242, 242); | |||
@@ -260,7 +260,6 @@ export default { | |||
flex-direction: column; | |||
width: 160px; | |||
padding-top: 20px; | |||
margin-bottom: 34px; | |||
text-align: center; | |||
background: #fff; | |||
box-shadow: 2px 0 6px 0 rgba(0, 0, 0, 0.15); | |||
@@ -171,7 +171,7 @@ import BrushTip from './brushTip'; | |||
const addEventListener = require('add-dom-event-listener'); | |||
const FooterHeight = 32; | |||
const FooterHeight = 0; | |||
// 侧边栏宽度 | |||
export const ThumbWidth = 160; | |||
@@ -881,7 +881,7 @@ export default { | |||
top: 0; | |||
left: 0; | |||
width: 100%; | |||
height: calc(100vh - 130px); | |||
height: calc(100vh - 50px - 48px); | |||
} | |||
.annotation-score-group { | |||
@@ -14,7 +14,7 @@ | |||
* ============================================================= | |||
*/ | |||
import { statusCodeMap } from '../util'; | |||
import { statusCodeMap, dataTypeCodeMap } from '../util'; | |||
export default { | |||
name: 'DatasetAction', | |||
@@ -59,7 +59,7 @@ export default { | |||
// 查看标注按钮在 自动标注中 未采样 采样中 采样失败 目标跟踪中 数据增强中 目标跟踪失败 时不显示, 此外,类型为视频时,自动标注完成也不可查看(此时下游会进行目标跟踪) | |||
let showCheckButton = !['AUTO_ANNOTATING', 'UNSAMPLED', 'SAMPLING', 'SAMPLE_FAILED', 'TRACKING', 'ENHANCING', 'TRACK_FAILED'].includes(statusCodeMap[row.status]); | |||
if (row.dataType === 1 && statusCodeMap[row.status] === 'AUTO_ANNOTATED') { | |||
if (row.dataType === dataTypeCodeMap.VIDEO && statusCodeMap[row.status] === 'AUTO_ANNOTATED') { | |||
showCheckButton = false; | |||
} | |||
// 查看标注按钮 | |||
@@ -113,7 +113,7 @@ export default { | |||
// 当类型为视频时,状态为标注完成、目标跟踪完成时显示发布按钮,其余状态不显示发布按钮 | |||
// 当类型为图片时,状态为自动标注完成时显示有弹窗确认的发布按钮,为标注完成时显示发布按钮,其余状态不显示发布按钮 | |||
if (row.dataType === 1) { | |||
if (row.dataType === dataTypeCodeMap.VIDEO) { | |||
if (['ANNOTATED', 'TRACK_SUCCEED'].includes(statusCodeMap[row.status])) { | |||
showPublishButton = true; | |||
publishButton = publishDialogButton; | |||
@@ -135,7 +135,7 @@ export default { | |||
); | |||
// 类型为视频时,当状态为未采样时才可导入,其余状态不可导入 | |||
// 类型为图片时,自动标注中、数据增强中 目标跟踪失败 不可导入,其余状态均可导入 | |||
if (row.dataType === 1) { | |||
if (row.dataType === dataTypeCodeMap.VIDEO) { | |||
if (statusCodeMap[row.status] === 'UNSAMPLED') { | |||
showUploadButton = true; | |||
} | |||
@@ -144,7 +144,7 @@ export default { | |||
} | |||
// 当标注完成、目标跟踪完成,以及非视频的自动标注完成时显示重新自动标注按钮 (若为视频此时下游会进行目标跟踪) | |||
let showReAutoButton = ['ANNOTATED', 'TRACK_SUCCEED'].includes(statusCodeMap[row.status]) || (statusCodeMap[row.status] === 'AUTO_ANNOTATED' && row.dataType === 0); | |||
let showReAutoButton = ['ANNOTATED', 'TRACK_SUCCEED'].includes(statusCodeMap[row.status]) || (statusCodeMap[row.status] === 'AUTO_ANNOTATED' && row.dataType === dataTypeCodeMap.IMAGE); | |||
// 重新自动标注按钮 | |||
const reAutoButton = ( | |||
<el-popconfirm | |||
@@ -180,7 +180,7 @@ export default { | |||
// 展示数据增强入口 | |||
// 当数据类型为图片,并且状态为自动标注完成、标注完成展示数据增强入口 | |||
let showAugmentButton = row.dataType === 0 && ['AUTO_ANNOTATED', 'ANNOTATED'].includes(statusCodeMap[row.status]); | |||
let showAugmentButton = row.dataType === dataTypeCodeMap.IMAGE && ['AUTO_ANNOTATED', 'ANNOTATED'].includes(statusCodeMap[row.status]); | |||
// 数据增强按钮 | |||
const augmentButton = ( | |||
<el-button {...btnProps} onClick={() => dataEnhance(row)}> | |||
@@ -50,79 +50,51 @@ | |||
v-model="form.annotateType" | |||
placeholder="标注类型" | |||
:dataSource="annotationList" | |||
:disabled="form.dataType === 1" | |||
:disabled="form.dataType === dataTypeCodeMap.VIDEO" | |||
@change="handleAnnotateTypeChange" | |||
/> | |||
</el-form-item> | |||
<el-form-item label="标签组" prop="labelGroupId"> | |||
<div class="label-input"> | |||
<el-popover | |||
ref="popover" | |||
v-model="popoverVisible" | |||
placement="top" | |||
trigger="click" | |||
popper-class="label-group-popover" | |||
> | |||
<div class="add-label-tag"> | |||
<el-tabs v-model="labelGroupTab" type="border-card"> | |||
<el-tab-pane label="自定义标签组" name="custom"> | |||
<el-select | |||
v-model="customLabelGroupId" | |||
filterable | |||
placeholder="请选择" | |||
popper-class="label-group-select" | |||
@change="handleCustomId" | |||
> | |||
<el-option | |||
v-for="item in customLabelGroups" | |||
:key="item.labelGroupId" | |||
:label="item.name" | |||
:value="item.labelGroupId" | |||
> | |||
</el-option> | |||
</el-select> | |||
</el-tab-pane> | |||
<el-tab-pane label="预置标签组" name="system" :disabled="!systemLabelEnabled"> | |||
<el-select | |||
v-model="systemLabelGroupId" | |||
filterable | |||
placeholder="请选择" | |||
@change="handleSystemId" | |||
> | |||
<el-option | |||
v-for="item in systemLabelGroups" | |||
:key="item.labelGroupId" | |||
:label="item.name" | |||
:value="item.labelGroupId" | |||
:disabled="!optionEnabled(item.labelGroupId, form.annotateType)" | |||
> | |||
</el-option> | |||
</el-select> | |||
</el-tab-pane> | |||
</el-tabs> | |||
</div> | |||
<el-button slot="reference" type="text"> | |||
| |||
<span v-if="labelGroupId === null"> 标签组</span> | |||
<el-tag v-else closable @close="handleRemoveLabelGroup()"> | |||
{{labelGroupName}} | |||
</el-tag> | |||
</el-button> | |||
</el-popover> | |||
<el-form-item label="标签组" style="height: 32px;"> | |||
<el-cascader | |||
v-model="chosenGroup" | |||
clearable | |||
placeholder="标签组" | |||
:options="labelGroupOptions" | |||
:props="{expandTrigger: 'hover'}" | |||
:show-all-levels="false" | |||
filterable | |||
popper-class="group-cascader" | |||
style="width:100%; line-height:32px;" | |||
@change="handleGroupChange" | |||
> | |||
<div slot="empty"> | |||
<span>没有找到标签组?去</span> | |||
<a | |||
target="_blank" | |||
type="primary" | |||
:underline="false" | |||
class="primary" | |||
:href="`/data/labelgroup/create`" | |||
> | |||
新建标签组 | |||
</a> | |||
<span>页面创建</span> | |||
</div> | |||
</el-cascader> | |||
<div style="position: relative; float: right; top: -33px; right: 30px;"> | |||
<el-link | |||
v-if="labelGroupId !== null" | |||
v-if="chosenGroupId !== null" | |||
target="_blank" | |||
type="primary" | |||
:underline="false" | |||
class="vm" | |||
:href="`/data/labelgroup/detail?id=${labelGroupId}`" | |||
style="float: right; margin-right: 8px;" | |||
:href="`/data/labelgroup/detail?id=${chosenGroupId}`" | |||
> | |||
查看详情 | |||
</el-link> | |||
</div> | |||
</el-link> | |||
</div> | |||
</el-form-item> | |||
<div v-if="labelGroupId === null" style=" position: relative; top: -12px; left: 118px;"> | |||
<div v-if="chosenGroupId === null" style=" position: relative; top: -12px; left: 116px;"> | |||
<span>标签组需要在</span> | |||
<a | |||
target="_blank" | |||
@@ -169,7 +141,7 @@ | |||
/> | |||
<!--上传视频时显示帧间隔设置--> | |||
<el-form | |||
v-if="form.dataType === 1" | |||
v-if="form.dataType === dataTypeCodeMap.VIDEO" | |||
ref="formStep1" | |||
:model="step1Form" | |||
label-width="100px" | |||
@@ -192,7 +164,7 @@ | |||
<div v-if="activeStep === 2 && skipUpload !== true"> | |||
<!--上传图片进度条--> | |||
<el-progress | |||
v-if="form.dataType !== 1" | |||
v-if="form.dataType !== dataTypeCodeMap.VIDEO" | |||
type="circle" | |||
:percentage="uploadPercent" | |||
:status="uploadStatus" | |||
@@ -230,7 +202,9 @@ import { getLabelGroupList } from '@/api/preparation/labelGroup'; | |||
import { | |||
getImgFromMinIO, | |||
annotationMap, | |||
annotationCodeMap, | |||
dataTypeMap, | |||
dataTypeCodeMap, | |||
withDimensionFile, | |||
trackUploadProps, | |||
} from '@/views/dataset/util'; | |||
@@ -282,15 +256,12 @@ export default { | |||
}, | |||
data() { | |||
return { | |||
dataTypeCodeMap, | |||
chosenDatasetId: 0, // 当前数据集id | |||
activeStep: 0, // 当前的step | |||
actionKey: 1, | |||
// customLabelEnabled: true, // 自定义标签组可用性 | |||
systemLabelEnabled: true, // 预置标签组可用性 | |||
uploadPercent: 0, | |||
uploadStatus: undefined, | |||
skipUpload: false, // 跳过上传 | |||
popoverVisible: false, | |||
rules: { | |||
name: [ | |||
{ required: true, message: '请输入数据集名称', trigger: ['change', 'blur'] }, | |||
@@ -309,13 +280,20 @@ export default { | |||
step1Form: { | |||
frameInterval: defaultFrameInterval, // 默认值 | |||
}, | |||
labelGroupTab: "custom", | |||
labelGroupName: null, | |||
labelGroupId: null, | |||
customLabelGroupId: null, | |||
systemLabelGroupId: null, | |||
customLabelGroups: [], | |||
systemLabelGroups: [], | |||
chosenGroupId: null, | |||
chosenGroup: null, | |||
labelGroupOptions: [{ | |||
value: 'custom', | |||
label: '自定义标签组', | |||
disabled: false, | |||
children: [], | |||
}, | |||
{ | |||
value: 'system', | |||
label: '预置标签组', | |||
disabled: false, | |||
children: [], | |||
}], | |||
}; | |||
}, | |||
computed: { | |||
@@ -326,7 +304,7 @@ export default { | |||
uploadParams() { | |||
// 是否为视频数据类类型 | |||
const isVideo = | |||
this.importRow?.dataType === 1 || this.form.dataType === 1; | |||
this.importRow?.dataType === dataTypeCodeMap.VIDEO || this.form.dataType === dataTypeCodeMap.VIDEO; | |||
const dir = isVideo ? `video` : `origin`; | |||
return { | |||
datasetId: this.chosenDatasetId, | |||
@@ -335,7 +313,7 @@ export default { | |||
}, | |||
// 新建数据集(视频)上传组件参数 | |||
optionCreateProps() { | |||
const props = this.form.dataType === 1 ? trackUploadProps : {}; | |||
const props = this.form.dataType === dataTypeCodeMap.VIDEO ? trackUploadProps : {}; | |||
return props; | |||
}, | |||
annotationList() { | |||
@@ -348,10 +326,10 @@ export default { | |||
// 如果是视频,只能用目标跟踪 | |||
return rawAnnotationList.map(d => { | |||
let disabled = false; | |||
if (this.form.dataType === 0) { | |||
disabled = d.value === 5; | |||
} else if (this.form.dataType === 1) { | |||
disabled = d.value !== 5; | |||
if (this.form.dataType === dataTypeCodeMap.IMAGE) { | |||
disabled = d.value === annotationCodeMap.TRACK; | |||
} else if (this.form.dataType === dataTypeCodeMap.VIDEO) { | |||
disabled = d.value !== annotationCodeMap.TRACK; | |||
} | |||
return { | |||
...d, | |||
@@ -375,47 +353,33 @@ export default { | |||
this.crud.toQuery(); | |||
getLabelGroupList(1).then(res => { | |||
res.forEach((item) => { | |||
this.systemLabelGroups.push({ | |||
labelGroupId: item.id, | |||
name: item.name, | |||
this.labelGroupOptions[1].children.push({ | |||
value: item.id, | |||
label: item.name, | |||
disabled: false, | |||
}); | |||
}); | |||
}); | |||
getLabelGroupList(0).then(res => { | |||
res.forEach((item) => { | |||
this.customLabelGroups.push({ | |||
labelGroupId: item.id, | |||
name: item.name, | |||
this.labelGroupOptions[0].children.push({ | |||
value: item.id, | |||
label: item.name, | |||
disabled: false, | |||
}); | |||
}); | |||
}); | |||
}, | |||
methods: { | |||
handleCustomId() { | |||
this.popoverVisible = false; | |||
this.labelGroupId = this.customLabelGroupId; | |||
this.systemLabelGroupId = null; | |||
this.labelGroupName = this.customLabelGroups.find(d => d.labelGroupId === this.labelGroupId).name; | |||
}, | |||
handleSystemId() { | |||
this.popoverVisible = false; | |||
this.labelGroupId = this.systemLabelGroupId; | |||
this.customLabelGroupId = null; | |||
this.labelGroupName = this.systemLabelGroups.find(d => d.labelGroupId === this.labelGroupId).name; | |||
}, | |||
handleRemoveLabelGroup() { | |||
this.labelGroupId = null; | |||
this.customLabelGroupId = null; | |||
this.systemLabelGroupId = null; | |||
this.$refs.popover.doClose(); | |||
}, | |||
optionEnabled(labelGroupId, annotateType) { | |||
// 目标检测(1)目标跟踪(5)可以使用预置标签组COCO | |||
if([1, 5].includes(annotateType)) { | |||
return labelGroupId === 1; | |||
} | |||
return true; | |||
handleGroupChange(val) { | |||
if(val.length === 0) { | |||
this.chosenGroup = null; | |||
this.chosenGroupId = null; | |||
} else { | |||
this.chosenGroup = val; | |||
// eslint-disable-next-line prefer-destructuring | |||
this.chosenGroupId = val[1]; | |||
} | |||
}, | |||
// 重置创建数据集表单 | |||
@@ -423,10 +387,8 @@ export default { | |||
// 清理第一步表单 | |||
this.$refs.form?.resetFields(); | |||
// 清除标签组 | |||
this.labelGroupId = null; | |||
this.systemLabelEnabled = true; | |||
this.systemLabelGroupId = null; | |||
this.customLabelGroupId = null; | |||
this.chosenGroup = null; | |||
this.chosenGroupId = null; | |||
// 清理上传表单 | |||
this.$refs.initFileUploadForm?.$refs?.formRef.reset(); | |||
this.crud.cancelCU(); | |||
@@ -441,50 +403,42 @@ export default { | |||
this.videoUploadProgress = 0; | |||
}, | |||
// step0 标签选择框刷新 | |||
handleLabelHide() { | |||
this.actionKey += 1; | |||
}, | |||
// step0 改变数据类型 | |||
handleDataTypeChange(dataType) { | |||
// 数据类型选中为视频时,标注类型自动切换为目标跟踪,同时清除不符合类型的标签组 | |||
if (dataType === 1) { | |||
this.form.annotateType = 5; | |||
this.handleAnnotateTypeChange(5); | |||
if (dataType === dataTypeCodeMap.VIDEO) { | |||
this.form.annotateType = annotationCodeMap.TRACK; | |||
this.handleAnnotateTypeChange(annotationCodeMap.TRACK); | |||
} else { | |||
// 数据类型选中为其他时 去除限制 | |||
this.form.annotateType = undefined; | |||
this.systemLabelEnabled = true; | |||
this.labelGroupOptions[1].disabled = false; | |||
this.labelGroupOptions[1].children.forEach( item => {item.disabled = false;}); | |||
} | |||
}, | |||
// step0 改变标注类型 | |||
handleAnnotateTypeChange(annotateType) { | |||
// 更改标注类型会清除不符合条件的标签组 | |||
// 目标检测(1) 目标跟踪(5) 可以选中预置标签组中的Coco(id=1) | |||
if ([1, 5].includes(annotateType)) { | |||
if(this.labelGroupId !== 1 && this.labelGroupId === this.systemLabelGroupId) { | |||
this.systemLabelEnabled = true; | |||
this.labelGroupId = null; | |||
this.systemLabelGroupId = null; | |||
} | |||
} | |||
// 图像分类(2)可以选中预置标签组Coco(id=1)和ImageNet(id=2) | |||
if (annotateType === 2) { | |||
if(![1, 2].includes(this.labelGroupId) && this.labelGroupId === this.systemLabelGroupId) { | |||
this.systemLabelEnabled = true; | |||
this.labelGroupId = null; | |||
this.systemLabelGroupId = null; | |||
// 目标检测和目标跟踪可以选中预置标签组中的Coco(id=1) | |||
if ([annotationCodeMap.ANNOTATE, annotationCodeMap.TRACK].includes(annotateType)) { | |||
if(this.chosenGroupId !== 1){ | |||
this.chosenGroup = null; | |||
this.chosenGroupId = null; | |||
} | |||
this.labelGroupOptions[1].disabled = false; | |||
this.labelGroupOptions[1].children.forEach( item => { | |||
// 此处1是预置的coco标签组固定id为1 | |||
if(item.value === 1){ | |||
item.disabled = false; | |||
} else { | |||
item.disabled = true; | |||
} | |||
}); | |||
} else { | |||
// 其余可以使用任意标签组 | |||
this.labelGroupOptions[1].disabled = false; | |||
this.labelGroupOptions[1].children.forEach(item => {item.disabled = false;}); | |||
} | |||
// 其余不可以使用预置标签组 | |||
if (![1, 2, 5].includes(annotateType)) { | |||
if( this.labelGroupId === this.systemLabelGroupId) { | |||
this.systemLabelGroupId = null; | |||
this.labelGroupId = null; | |||
this.labelGroupName = null; | |||
this.systemLabelEnabled = false; | |||
this.labelGroupTab = "custom"; | |||
} | |||
} | |||
}, | |||
// step0 创建数据集调用 | |||
createDataset() { | |||
@@ -494,7 +448,7 @@ export default { | |||
return; | |||
} | |||
this.crud.status.add = CRUD.STATUS.PROCESSING; | |||
this.crud.form.labelGroupId = this.labelGroupId; | |||
this.crud.form.labelGroupId = this.chosenGroupId; | |||
this.crud.crudMethod | |||
.add(this.crud.form) | |||
.then(res => { | |||
@@ -523,10 +477,10 @@ export default { | |||
// 点击导入操作 | |||
const { dataType } = datasetInfo || {}; | |||
// 文件上传 | |||
if (dataType === 0) { | |||
if (dataType === dataTypeCodeMap.IMAGE) { | |||
return submit(datasetId, files); | |||
} | |||
if (dataType === 1) { | |||
if (dataType === dataTypeCodeMap.VIDEO) { | |||
return submitVideo(datasetId, { | |||
frameInterval: this.step1Form.frameInterval, | |||
url: files[0].url, | |||
@@ -540,7 +494,7 @@ export default { | |||
this.activeStep+=1; | |||
} | |||
// 视频上传完毕 | |||
if (this.form.dataType === 1) { | |||
if (this.form.dataType === dataTypeCodeMap.VIDEO) { | |||
this.videoUploadProgress = 100; | |||
} | |||
const files = getImgFromMinIO(res); | |||
@@ -588,7 +542,7 @@ export default { | |||
// step2 进度格式化 | |||
formatProgress(percentage) { | |||
let formatTxt = `${percentage}%`; | |||
if (this.form.dataType === 1) { | |||
if (this.form.dataType === dataTypeCodeMap.VIDEO) { | |||
formatTxt = this.videoUploadProgress === 100 ? `100%` : `上传中...`; | |||
} | |||
return formatTxt; | |||
@@ -42,72 +42,46 @@ | |||
disabled | |||
/> | |||
</el-form-item> | |||
<el-form-item v-if="!state.model.import" label="标签组" prop="labelGroupId"> | |||
<div v-if="editable" class="label-input"> | |||
<el-popover | |||
ref="popoverRef" | |||
v-model="state.popoverVisible" | |||
placement="top" | |||
trigger="click" | |||
popper-class="label-group-popover" | |||
<el-form-item v-if="!state.model.import" label="标签组" style="height: 32px;"> | |||
<div v-if="editable"> | |||
<el-cascader | |||
v-model="state.chosenGroup" | |||
placeholder="标签组" | |||
:options="state.labelGroupOptions" | |||
:props="{expandTrigger: 'hover'}" | |||
:show-all-levels="false" | |||
filterable | |||
:clearable="deletable" | |||
popper-class="group-cascader" | |||
style="width:100%; line-height:32px;" | |||
@change="handleGroupChange" | |||
> | |||
<div class="add-label-tag"> | |||
<el-tabs v-model="state.labelGroupTab" type="border-card"> | |||
<el-tab-pane label="自定义标签组" name="custom"> | |||
<el-select | |||
v-model="state.customLabelGroupId" | |||
filterable | |||
placeholder="请选择" | |||
popper-class="label-group-select" | |||
@change="handleCustomId" | |||
> | |||
<el-option | |||
v-for="item in customLabelGroups" | |||
:key="item.labelGroupId" | |||
:label="item.name" | |||
:value="item.labelGroupId" | |||
> | |||
</el-option> | |||
</el-select> | |||
</el-tab-pane> | |||
<el-tab-pane label="预置标签组" name="system" :disabled="!systemLabelEnabled"> | |||
<el-select | |||
v-model="state.systemLabelGroupId" | |||
filterable | |||
placeholder="请选择" | |||
@change="handleSystemId" | |||
> | |||
<el-option | |||
v-for="item in systemLabelGroups" | |||
:key="item.labelGroupId" | |||
:label="item.name" | |||
:value="item.labelGroupId" | |||
:disabled="!optionEnabled(item.labelGroupId, state.model.annotateType)" | |||
> | |||
</el-option> | |||
</el-select> | |||
</el-tab-pane> | |||
</el-tabs> | |||
<div slot="empty"> | |||
<span>没有找到标签组?去</span> | |||
<a | |||
target="_blank" | |||
type="primary" | |||
:underline="false" | |||
class="primary" | |||
:href="`/data/labelgroup/create`" | |||
> | |||
新建标签组 | |||
</a> | |||
<span>页面创建</span> | |||
</div> | |||
<el-button slot="reference" type="text"> | |||
| |||
<span v-if="state.model.labelGroupId === null"> 标签组</span> | |||
<el-tag v-else :closable="deletable" @close="handleRemoveLabelGroup()"> | |||
{{state.model.labelGroupName}} | |||
</el-tag> | |||
</el-button> | |||
</el-popover> | |||
<el-link | |||
v-if="state.model.labelGroupId !== null" | |||
target="_blank" | |||
type="primary" | |||
:underline="false" | |||
class="vm" | |||
:href="`/data/labelgroup/detail?id=${state.model.labelGroupId}`" | |||
style="float: right; margin-right: 8px;" | |||
> | |||
查看详情 | |||
</el-link> | |||
</el-cascader> | |||
<div style="position: relative; float: right; top: -33px; right: 30px;"> | |||
<el-link | |||
v-if="state.chosenGroupId !== null" | |||
target="_blank" | |||
type="primary" | |||
:underline="false" | |||
class="vm" | |||
:href="`/data/labelgroup/detail?id=${state.chosenGroupId}`" | |||
> | |||
查看详情 | |||
</el-link> | |||
</div> | |||
</div> | |||
<div v-else class="label-input" style="color: #c0c4cc; background-color: #f5f7fa;"> | |||
{{state.model.labelGroupName}} | |||
@@ -124,6 +98,19 @@ | |||
</el-link> | |||
</div> | |||
</el-form-item> | |||
<div v-if="state.chosenGroupId === null" style=" position: relative; top: -12px; left: 116px;"> | |||
<span>标签组需要在</span> | |||
<a | |||
target="_blank" | |||
type="primary" | |||
:underline="false" | |||
class="primary" | |||
:href="`/data/labelgroup/create`" | |||
> | |||
新建标签组 | |||
</a> | |||
<span>页面创建</span> | |||
</div> | |||
<el-form-item label="数据集描述" prop="remark"> | |||
<el-input | |||
v-model="state.model.remark" | |||
@@ -140,12 +127,12 @@ | |||
<script> | |||
import {isNil} from 'lodash'; | |||
import { watch, reactive, computed, ref, onMounted } from '@vue/composition-api'; | |||
import { watch, reactive, computed, onMounted } from '@vue/composition-api'; | |||
import BaseModal from '@/components/BaseModal'; | |||
import InfoSelect from '@/components/InfoSelect'; | |||
import { validateName } from '@/utils/validate'; | |||
import { annotationMap, dataTypeMap, statusCodeMap } from '@/views/dataset/util'; | |||
import { annotationMap, annotationCodeMap, dataTypeMap, dataTypeCodeMap, statusCodeMap } from '@/views/dataset/util'; | |||
import { getLabelGroupList } from '@/api/preparation/labelGroup'; | |||
export default { | |||
@@ -165,7 +152,6 @@ export default { | |||
}, | |||
handleCancel: Function, | |||
handleOk: Function, | |||
goLabelGroupDetail: Function, | |||
row: { | |||
type: Object, | |||
default: () => {}, | |||
@@ -173,9 +159,6 @@ export default { | |||
}, | |||
setup(props, { refs }) { | |||
const { handleOk } = props; | |||
const popoverRef = ref(null); | |||
const systemLabelGroups = []; | |||
const customLabelGroups = []; | |||
const rules= { | |||
name: [ | |||
@@ -199,14 +182,20 @@ export default { | |||
const state = reactive({ | |||
model: buildModel(props.row), | |||
popoverVisible: false, | |||
labelGroupTab: "custom", | |||
customLabelGroupId: null, | |||
systeomLabelGroupId: null, | |||
}); | |||
const systemLabelEnabled = computed(() => { | |||
return props.row.annotateType !== 5; | |||
chosenGroupId: null, | |||
chosenGroup: null, | |||
labelGroupOptions: [{ | |||
value: 'custom', | |||
label: '自定义标签组', | |||
disabled: false, | |||
children: [], | |||
}, | |||
{ | |||
value: 'system', | |||
label: '预置标签组', | |||
disabled: false, | |||
children: [], | |||
}], | |||
}); | |||
const deletable = computed(() => { | |||
@@ -230,10 +219,10 @@ export default { | |||
// 如果是视频,只能用目标跟踪 | |||
return rawAnnotationList.map(d => { | |||
let disabled = false; | |||
if (state.model.dataType === 0) { | |||
disabled = d.value === 5; | |||
} else if (state.model.dataType === 1) { | |||
disabled = d.value !== 5; | |||
if (state.model.dataType === dataTypeCodeMap.IMAGE) { | |||
disabled = d.value === annotationCodeMap.TRACK; | |||
} else if (state.model.dataType === dataTypeCodeMap.VIDEO) { | |||
disabled = d.value !== annotationCodeMap.TRACK; | |||
} | |||
return { | |||
...d, | |||
@@ -247,6 +236,7 @@ export default { | |||
}); | |||
const handleEditDataset = () => { | |||
state.model.labelGroupId = state.chosenGroupId; | |||
refs.form.validate(valid => { | |||
if (!valid) { | |||
return false; | |||
@@ -255,72 +245,75 @@ export default { | |||
return null; | |||
}); | |||
}; | |||
const handleCustomId = () => { | |||
Object.assign(state, { | |||
popoverVisible: false, | |||
systemLabelGroupId: null, | |||
model: { | |||
...state.model, | |||
labelGroupId: state.customLabelGroupId, | |||
labelGroupName: customLabelGroups.find(d => d.labelGroupId === state.customLabelGroupId).name, | |||
}, | |||
}); | |||
}; | |||
const handleSystemId = () => { | |||
Object.assign(state, { | |||
popoverVisible: false, | |||
customLabelGroupId: null, | |||
model: { | |||
...state.model, | |||
labelGroupId: state.systemLabelGroupId, | |||
labelGroupName: systemLabelGroups.find(d => d.labelGroupId === state.systemLabelGroupId).name, | |||
}, | |||
}); | |||
}; | |||
const handleRemoveLabelGroup = () => { | |||
Object.assign(state, { | |||
customLabelGroupId: null, | |||
systemLabelGroupId: null, | |||
model: { | |||
...state.model, | |||
labelGroupId: null, | |||
}, | |||
}); | |||
popoverRef.value.doClose(); | |||
}; | |||
const optionEnabled = (labelGroupId, annotateType) => { | |||
if(annotateType === 1) { | |||
return labelGroupId === 1; | |||
} | |||
if(annotateType === 5) { | |||
return false; | |||
const handleGroupChange = (val) => { | |||
if(val.length === 0) { | |||
state.chosenGroup = null; | |||
state.chosenGroupId = null; | |||
} else { | |||
state.chosenGroup = val; | |||
// eslint-disable-next-line prefer-destructuring | |||
state.chosenGroupId = val[1]; | |||
} | |||
return true; | |||
}; | |||
onMounted(() => { | |||
getLabelGroupList(1).then(res => res.forEach((item) => { | |||
systemLabelGroups.push({ | |||
labelGroupId: item.id, | |||
name: item.name, | |||
getLabelGroupList(1).then(res => { | |||
res.forEach((item) => { | |||
state.labelGroupOptions[1].children.push({ | |||
value: item.id, | |||
label: item.name, | |||
disabled: false, | |||
}); | |||
}); | |||
})); | |||
getLabelGroupList(0).then(res => res.forEach((item) => { | |||
customLabelGroups.push({ | |||
labelGroupId: item.id, | |||
name: item.name, | |||
}); | |||
getLabelGroupList(0).then(res => { | |||
res.forEach((item) => { | |||
state.labelGroupOptions[0].children.push({ | |||
value: item.id, | |||
label: item.name, | |||
disabled: false, | |||
}); | |||
}); | |||
})); | |||
}); | |||
}); | |||
watch(() => props.row, (next) => { | |||
Object.assign(state, { | |||
model: { ...state.model, ...next }, | |||
}); | |||
// 图像分类可任意选择 | |||
if(next?.annotateType === annotationCodeMap.CLASSIFY) { | |||
state.labelGroupOptions[1].disabled = false; | |||
state.labelGroupOptions[1].children.forEach( item => {item.disabled = false;}); | |||
} | |||
// 目标检测和目标跟踪 在预置标签组中只可选择coco | |||
if([annotationCodeMap.ANNOTATE, annotationCodeMap.TRACK].includes(next?.annotateType)) { | |||
if(state.chosenGroupId !== 1) { | |||
state.chosenGroup = null; | |||
state.chosenGroupId = null; | |||
} | |||
state.labelGroupOptions[1].disabled = false; | |||
state.labelGroupOptions[1].children.forEach( item => { | |||
if(item.value === 1){ | |||
item.disabled = false; | |||
} else { | |||
item.disabled = true; | |||
} | |||
}); | |||
} | |||
// 读取数据集已有标签组 | |||
if(!isNil(next?.labelGroupId)) { | |||
state.chosenGroupId = next.labelGroupId; | |||
if(next.labelGroupType === 0) { | |||
state.chosenGroup = ['custom', next.labelGroupId]; | |||
} else { | |||
state.chosenGroup = ['system', next.labelGroupId]; | |||
} | |||
} else { | |||
state.chosenGroupId = null; | |||
state.chosenGroup = null; | |||
} | |||
}); | |||
return { | |||
@@ -328,17 +321,10 @@ export default { | |||
state, | |||
deletable, | |||
editable, | |||
systemLabelEnabled, | |||
optionEnabled, | |||
systemLabelGroups, | |||
customLabelGroups, | |||
handleCustomId, | |||
handleSystemId, | |||
handleRemoveLabelGroup, | |||
handleGroupChange, | |||
handleEditDataset, | |||
dataTypeList, | |||
annotationList, | |||
popoverRef, | |||
}; | |||
}, | |||
}; | |||
@@ -16,6 +16,14 @@ | |||
@import "~@/assets/styles/variables.scss"; | |||
.group-cascader{ | |||
.el-cascader-menu__wrap{ | |||
max-height: 300px; | |||
min-height: 100px; | |||
max-width: 280px; | |||
} | |||
} | |||
.table-top-row { | |||
background-color: $menuBg !important; | |||
} | |||
@@ -193,6 +193,11 @@ export const dataTypeMap = { | |||
1: '视频', | |||
}; | |||
export const dataTypeCodeMap = { | |||
'IMAGE': 0, | |||
'VIDEO': 1, | |||
}; | |||
// 文件状态 | |||
export const fileTypeEnum = { | |||
0: { label: '全部', abbr: '全部' }, | |||
@@ -217,6 +222,12 @@ export const fileCodeMap = { | |||
'COMPLETED': 302, | |||
}; | |||
export const annotationCodeMap = { | |||
'ANNOTATE': 1, | |||
'CLASSIFY': 2, | |||
'TRACK': 5, | |||
}; | |||
export const annotationMap = { | |||
1: { name: '目标检测', urlPrefix: 'annotate', component: 'AnnotateDataset' }, | |||
2: { name: '图像分类', urlPrefix: 'classify', component: 'Classify' }, | |||
@@ -24,7 +24,7 @@ | |||
:prop="'labels.' + index" | |||
:rules="rules" | |||
> | |||
<div class="flex"> | |||
<div v-if="addAble" class="flex"> | |||
<InfoSelect | |||
:value="list[index].id || list[index].name" | |||
style="width: 200px; margin-right: 10px;" | |||
@@ -57,6 +57,10 @@ | |||
/> | |||
</span> | |||
</div> | |||
<div v-else class="flex"> | |||
<el-input v-model="list[index].name" style="width: 200px; margin-right: 10px;" disabled/> | |||
<el-color-picker v-model="list[index].color" disabled size="small" /> | |||
</div> | |||
</el-form-item> | |||
</div> | |||
</template> | |||
@@ -91,7 +91,8 @@ | |||
v-if="refreshFlag" | |||
action="fakeApi" | |||
accept=".zip,.pb,.h5,.ckpt,.pkl,.pth,.weight,.caffemodel,.pt" | |||
:acceptSize="0" | |||
:acceptSize="modelConfig.uploadFileAcceptSize" | |||
:acceptSizeFormat="uploadSizeFomatter" | |||
list-type="text" | |||
:limit="1" | |||
:multiple="false" | |||
@@ -127,7 +128,8 @@ import { add as addModel } from '@/api/model/model'; | |||
import { list as getAlgorithmUsages, add as addAlgorithmUsage } from '@/api/algorithm/algorithmUsage'; | |||
import UploadInline from '@/components/UploadForm/inline'; | |||
import UploadProgress from '@/components/UploadProgress'; | |||
import { getUniqueId, validateNameWithHyphen } from '@/utils'; | |||
import { getUniqueId, validateNameWithHyphen, uploadSizeFomatter } from '@/utils'; | |||
import { modelConfig } from '@/config'; | |||
const defaultForm = { | |||
name: null, | |||
@@ -195,6 +197,7 @@ export default { | |||
{color: '#e6a23c', percentage: 80}, | |||
{color: '#67c23a', percentage: 100}, | |||
], | |||
modelConfig, | |||
}; | |||
}, | |||
computed: { | |||
@@ -309,6 +312,7 @@ export default { | |||
updateImagePath() { | |||
this.uploadParams.objectPath = `upload-temp/${this.user.id}/${getUniqueId()}`; | |||
}, | |||
uploadSizeFomatter, | |||
}, | |||
}; | |||
</script> |
@@ -74,7 +74,8 @@ | |||
ref="upload" | |||
action="fakeApi" | |||
accept=".zip, .pb, .h5, .ckpt, .pkl, .pth, .weight, .caffemodel, .pt" | |||
:acceptSize="0" | |||
:acceptSize="modelConfig.uploadFileAcceptSize" | |||
:acceptSizeFormat="uploadSizeFomatter" | |||
list-type="text" | |||
:limit="1" | |||
:multiple="false" | |||
@@ -108,7 +109,8 @@ import cdOperation from '@crud/CD.operation'; | |||
import pagination from '@crud/Pagination'; | |||
import UploadInline from '@/components/UploadForm/inline'; | |||
import UploadProgress from '@/components/UploadProgress'; | |||
import { getUniqueId, downloadZipFromObjectPath } from '@/utils'; | |||
import { getUniqueId, downloadZipFromObjectPath, uploadSizeFomatter } from '@/utils'; | |||
import { modelConfig } from '@/config'; | |||
const defaultForm = { | |||
parentId: null, | |||
@@ -157,6 +159,7 @@ export default { | |||
{color: '#e6a23c', percentage: 80}, | |||
{color: '#67c23a', percentage: 100}, | |||
], | |||
modelConfig, | |||
}; | |||
}, | |||
computed: { | |||
@@ -254,6 +257,7 @@ export default { | |||
() => {}, | |||
); | |||
}, | |||
uploadSizeFomatter, | |||
}, | |||
}; | |||
</script> |
@@ -129,7 +129,8 @@ | |||
action="fakeApi" | |||
accept=".zip,.tar,.rar,.gz" | |||
list-type="text" | |||
:acceptSize="0" | |||
:acceptSize="imageConfig.uploadFileAcceptSize" | |||
:acceptSizeFormat="uploadSizeFomatter" | |||
:params="uploadParams" | |||
:show-file-count="false" | |||
:auto-upload="true" | |||
@@ -182,11 +183,12 @@ import rrOperation from '@crud/RR.operation'; | |||
import pagination from '@crud/Pagination'; | |||
import CRUD, { presenter, header, form, crud } from '@crud/crud'; | |||
import trainingImageApi, { imageNameList, del } from '@/api/trainingImage/index'; | |||
import { getUniqueId } from '@/utils'; | |||
import { getUniqueId, uploadSizeFomatter } from '@/utils'; | |||
import BaseModal from '@/components/BaseModal'; | |||
import UploadInline from '@/components/UploadForm/inline'; | |||
import DropdownHeader from '@/components/DropdownHeader'; | |||
import UploadProgress from '@/components/UploadProgress'; | |||
import { imageConfig } from '@/config'; | |||
const defaultForm = { | |||
imageName: null, | |||
@@ -292,6 +294,7 @@ export default { | |||
loading: false, | |||
isEdit: false, | |||
prefabricate: true, | |||
imageConfig, | |||
}; | |||
}, | |||
computed: { | |||
@@ -422,6 +425,7 @@ export default { | |||
}, | |||
); | |||
}, | |||
uploadSizeFomatter, | |||
}, | |||
}; | |||
</script> |
@@ -144,6 +144,7 @@ | |||
<!--模型下载Dialog--> | |||
<path-select-dialog | |||
ref="pathSelect" | |||
class-key="ModelDownload" | |||
type="modelDownload" | |||
@chooseDone="chooseDone" | |||
/> | |||
@@ -18,6 +18,7 @@ | |||
<!--训练管理页面-断点续训Dialog--> | |||
<BaseModal | |||
:visible.sync="visible" | |||
:class="classKey" | |||
:title="title" | |||
width="600px" | |||
@open="onDialogOpen" | |||
@@ -48,11 +49,16 @@ import { Loading } from 'element-ui'; | |||
import BaseModal from '@/components/BaseModal'; | |||
import { getTreeListFromFilepath } from '@/utils'; | |||
import { resumeTrain } from '@/api/trainingJob/job'; | |||
import { modelOfficial } from '../utils'; | |||
export default { | |||
name: 'JobResumeDialog', | |||
components: { BaseModal }, | |||
props: { | |||
classKey: { | |||
type: String, | |||
default: '', | |||
}, | |||
type: { | |||
type: String, | |||
default: 'jobResume', | |||
@@ -79,7 +85,7 @@ export default { | |||
}; | |||
}, | |||
methods: { | |||
async show(item) { | |||
show(item) { | |||
this.path = item.resumePath; | |||
this.id = item.id; | |||
this.fileName = item.fileName; | |||
@@ -90,25 +96,8 @@ export default { | |||
this.visible = true; | |||
}, | |||
getCentext(type='', num) { | |||
const ctxArr = [ | |||
{ | |||
'jobResume':'断点续训', | |||
'modelDownload':'模型下载', | |||
'modelSelect':'模型选择', | |||
}, | |||
{ | |||
'jobResume':'请选择从哪里开始继续训练', | |||
'modelDownload': '请选择需要下载的模型文件目录', | |||
'modelSelect': '请选择要保存的模型', | |||
}, | |||
{ | |||
'jobResume':'暂无数据,无法断点续训', | |||
'modelDownload': '暂无数据', | |||
'modelSelect': '暂无模型数据', | |||
}, | |||
]; | |||
if(ctxArr[num][type]){ | |||
return ctxArr[num][type]; | |||
if(modelOfficial[num][type]){ | |||
return modelOfficial[num][type]; | |||
} | |||
}, | |||
// handle | |||
@@ -117,7 +106,7 @@ export default { | |||
this.treeList = []; | |||
}, | |||
async onDialogOpened() { | |||
const loadingInstance = Loading.service({ target: '.el-dialog__body' }); | |||
const loadingInstance = Loading.service({ target: `.${this.classKey} .el-dialog__body` }); | |||
[this.treeList, this.defaultExpandedKeys] = await getTreeListFromFilepath( | |||
this.path, | |||
); | |||
@@ -161,6 +161,7 @@ | |||
<!--断点续训Dialog--> | |||
<path-select-dialog | |||
ref="pathSelect" | |||
class-key="keepTrainDialog" | |||
:type="pathType" | |||
@chooseDone="chooseDone" | |||
@chooseModel="chooseModel" | |||
@@ -23,4 +23,23 @@ export const trainingStatusMap = { | |||
4: { tagMap: 'info', statusMap: 'done' }, | |||
5: { statusMap: 'done' }, | |||
7: { tagMap: 'danger', statusMap: 'done' }, | |||
}; | |||
}; | |||
// 目录树弹窗文案 | |||
export const modelOfficial = [ | |||
{ | |||
'jobResume':'断点续训', | |||
'modelDownload':'模型下载', | |||
'modelSelect':'模型选择', | |||
}, | |||
{ | |||
'jobResume':'请选择从哪里开始继续训练', | |||
'modelDownload': '请选择需要下载的模型文件目录', | |||
'modelSelect': '请选择要保存的模型', | |||
}, | |||
{ | |||
'jobResume':'暂无数据,无法断点续训', | |||
'modelDownload': '暂无数据', | |||
'modelSelect': '暂无模型数据', | |||
}, | |||
]; |
@@ -1,6 +1,11 @@ | |||
// eslint-disable-next-line import/no-extraneous-dependencies | |||
/*eslint-disable*/ | |||
const path = require('path'); | |||
const sass = require('sass'); | |||
const Promise = require('bluebird'); | |||
const fs = require('fs-extra'); | |||
const { match } = require('path-to-regexp'); | |||
const proxy = require('express-http-proxy'); | |||
const defaultSettings = require('./src/settings.js'); | |||
function resolve(dir) { | |||
@@ -24,6 +29,98 @@ module.exports = { | |||
warnings: false, | |||
errors: true, | |||
}, | |||
before (app){ | |||
function requireUncached(module) { | |||
try { | |||
// 删除缓存,动态加载 | |||
delete require.cache[require.resolve(module)]; | |||
return require(module); | |||
} catch (e) { | |||
console.log(`can't load module in ${module}`); | |||
return false | |||
} | |||
} | |||
// 根据 mock 请求发送响应 | |||
function sendValue(req, res, value) { | |||
if (typeof value === 'function') { | |||
value = value(req, res); | |||
} | |||
if (value.$$header) { | |||
Object.keys(value.$$header).forEach(key => { | |||
res.setHeader(key, value.$$header[key]); | |||
}); | |||
} | |||
const delay = value.$$delay || 0; | |||
delete value.$$header; | |||
delete value.$$delay; | |||
Promise.delay(delay, value).then(result => { | |||
res.send(result); | |||
}); | |||
} | |||
// 分解mockPath | |||
const splitUrl = resouce => { | |||
const splitUrl = resouce.split('::'); | |||
let verb = 'get', url = ''; | |||
if(splitUrl.length > 2) { | |||
throw new Error('url 格式不对'); | |||
} | |||
if(splitUrl.length === 2) { | |||
[verb, url] = splitUrl | |||
verb = splitUrl[0].toLowerCase(); | |||
url = splitUrl[1]; | |||
}else if(splitUrl.length === 1){ | |||
verb = 'get'; | |||
url = splitUrl[0]; | |||
} | |||
return [verb, url]; | |||
} | |||
// 处理 restful mock 接口 | |||
const mockMap = require(path.join(__dirname, 'mock/mock-map')); | |||
// 根据用户是否添加 mock 文件来决定走本地 mock 或者转发到 dev 接口 | |||
app.use('/mock', proxy(process.env.VUE_APP_BASE_API, { | |||
filter: function(req, res){ | |||
// 是否匹配到本地 rest 风格 api mockUrl | |||
const matchRESTApi = Object.keys(mockMap).findIndex(d => { | |||
const [,uri] = splitUrl(d); | |||
const matcher = match(uri, { decode: decodeURIComponent }) | |||
return matcher(req.path) | |||
}) > -1 | |||
// 如果匹配到 restApi 走本地 mock | |||
if(matchRESTApi) return false | |||
// 其他路径 | |||
const mockPath = path.join(__dirname, 'mock', req.path); | |||
const value = requireUncached(mockPath); | |||
return value === false | |||
} | |||
})); | |||
// 对于每个 mock 请求,require mock 文件夹下的对应路径文件,并返回响应 | |||
Object.keys(mockMap).forEach(mockPath => { | |||
const [verb, uri] = splitUrl(mockPath); | |||
app[verb](path.posix.join('/mock', uri), function(req, res) { | |||
const value = requireUncached(path.join(__dirname, 'mock', mockMap[mockPath])) | |||
sendValue(req, res, value) | |||
}) | |||
}) | |||
app.all('/mock/*', function(req, res) { | |||
const mockPath = path.join(__dirname, req.path) | |||
const value = requireUncached(mockPath) | |||
if (value) { | |||
sendValue(req, res, value) | |||
} else { | |||
res.sendStatus(404) | |||
} | |||
}) | |||
}, | |||
}, | |||
css: { | |||
loaderOptions: { | |||