@@ -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 | src/assets | ||||
public | public | ||||
dist | 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) | ## 0.2.0 (2020-10-26) | ||||
### Breaking Change | ### Breaking Change | ||||
@@ -79,6 +79,15 @@ npm install | |||||
npm run dev | 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", | "name": "dubhe-web", | ||||
"version": "0.2.0", | |||||
"version": "0.2.1", | |||||
"description": "之江天枢人工智能开源平台", | "description": "之江天枢人工智能开源平台", | ||||
"author": "zhejianglab", | "author": "zhejianglab", | ||||
"keywords": [ | "keywords": [ | ||||
@@ -11,6 +11,7 @@ | |||||
"人工智能" | "人工智能" | ||||
], | ], | ||||
"scripts": { | "scripts": { | ||||
"mock": "vue-cli-service serve --mode mock --open", | |||||
"dev": "vue-cli-service serve --open", | "dev": "vue-cli-service serve --open", | ||||
"build:prod": "vue-cli-service build", | "build:prod": "vue-cli-service build", | ||||
"build:test": "vue-cli-service build --mode test", | "build:test": "vue-cli-service build --mode test", | ||||
@@ -51,7 +52,7 @@ | |||||
"date-fns": "^2.13.0", | "date-fns": "^2.13.0", | ||||
"echarts": "4.2.1", | "echarts": "4.2.1", | ||||
"echarts-gl": "^1.1.1", | "echarts-gl": "^1.1.1", | ||||
"element-ui": "^2.13.2", | |||||
"element-ui": "2.13.2", | |||||
"file-saver": "^2.0.2", | "file-saver": "^2.0.2", | ||||
"filereader-stream": "^2.0.0", | "filereader-stream": "^2.0.0", | ||||
"jquery": "^3.5.1", | "jquery": "^3.5.1", | ||||
@@ -66,6 +67,7 @@ | |||||
"normalize.css": "7.0.0", | "normalize.css": "7.0.0", | ||||
"nprogress": "0.2.0", | "nprogress": "0.2.0", | ||||
"p-map": "^4.0.0", | "p-map": "^4.0.0", | ||||
"path-to-regexp": "^6.2.0", | |||||
"prismjs": "^1.20.0", | "prismjs": "^1.20.0", | ||||
"promise.allsettled": "^1.0.2", | "promise.allsettled": "^1.0.2", | ||||
"qs": "^6.9.1", | "qs": "^6.9.1", | ||||
@@ -105,6 +107,7 @@ | |||||
"eslint-plugin-import": "^2.20.2", | "eslint-plugin-import": "^2.20.2", | ||||
"eslint-plugin-prettier": "^2.3.1", | "eslint-plugin-prettier": "^2.3.1", | ||||
"eslint-plugin-vue": "^6.2.2", | "eslint-plugin-vue": "^6.2.2", | ||||
"express-http-proxy": "^1.6.2", | |||||
"html-webpack-plugin": "3.2.0", | "html-webpack-plugin": "3.2.0", | ||||
"husky": "^4.2.5", | "husky": "^4.2.5", | ||||
"less": "^3.11.3", | "less": "^3.11.3", | ||||
@@ -222,8 +222,8 @@ | |||||
<el-input-number | <el-input-number | ||||
id="resourcesPoolNode" | id="resourcesPoolNode" | ||||
v-model="form.resourcesPoolNode" | v-model="form.resourcesPoolNode" | ||||
:min="1" | |||||
:max="8" | |||||
:min="2" | |||||
:max="trainConfig.trainNodeMax" | |||||
:step-strictly="true" | :step-strictly="true" | ||||
/> | /> | ||||
<el-tooltip effect="dark" content="请确保代码中包含“num_nodes”参数和“node_ips”参数用于接收分布式相关参数" placement="top"> | <el-tooltip effect="dark" content="请确保代码中包含“num_nodes”参数和“node_ips”参数用于接收分布式相关参数" placement="top"> | ||||
@@ -282,7 +282,7 @@ | |||||
id="delayCreateTime" | id="delayCreateTime" | ||||
v-model="form.delayCreateTime" | v-model="form.delayCreateTime" | ||||
:min="0" | :min="0" | ||||
:max="168" | |||||
:max="trainConfig.delayCreateTimeMax" | |||||
:step-strictly="true" | :step-strictly="true" | ||||
/> 小时 | /> 小时 | ||||
</el-form-item> | </el-form-item> | ||||
@@ -295,7 +295,7 @@ | |||||
id="delayDeleteTime" | id="delayDeleteTime" | ||||
v-model="form.delayDeleteTime" | v-model="form.delayDeleteTime" | ||||
:min="0" | :min="0" | ||||
:max="168" | |||||
:max="trainConfig.delayDeleteTimeMax" | |||||
:step-strictly="true" | :step-strictly="true" | ||||
/> 小时 | /> 小时 | ||||
<el-tooltip effect="dark" content="选择 0 表示不限制训练时长" placement="top"> | <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 { harborProjectNames, harborImageNames } from '@/api/system/harbor'; | ||||
import { list as getModelName } from '@/api/model/model'; | import { list as getModelName } from '@/api/model/model'; | ||||
import { list as getModelTag } from '@/api/model/modelVersion'; | import { list as getModelTag } from '@/api/model/modelVersion'; | ||||
import { trainConfig } from '@/config'; | |||||
import RunParamForm from './runParamForm'; | import RunParamForm from './runParamForm'; | ||||
import DataSourceSelector from './dataSourceSelector'; | import DataSourceSelector from './dataSourceSelector'; | ||||
@@ -442,6 +443,7 @@ export default { | |||||
dictReady: false, | dictReady: false, | ||||
delayCreateDelete: false, | delayCreateDelete: false, | ||||
selectedAlgorithm: null, | selectedAlgorithm: null, | ||||
trainConfig, | |||||
form: { ...defaultForm }, | form: { ...defaultForm }, | ||||
rules: { | rules: { | ||||
@@ -814,9 +816,7 @@ export default { | |||||
this.onResourcesPoolTypeChange(); | this.onResourcesPoolTypeChange(); | ||||
}, | }, | ||||
onTrainTypeChange(trainType) { | 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> | ||||
<template v-slot:right> | <template v-slot:right> | ||||
<slot name="right-options" /> | <slot name="right-options" /> | ||||
<Guideline /> | |||||
<Feedback /> | <Feedback /> | ||||
</template> | </template> | ||||
</navbar> | </navbar> | ||||
</div> | </div> | ||||
<app-main /> | <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> | ||||
</div> | </div> | ||||
</template> | </template> | ||||
@@ -38,7 +46,7 @@ | |||||
<script> | <script> | ||||
import { mapState } from 'vuex'; | import { mapState } from 'vuex'; | ||||
import ResizeMixin from './mixin/ResizeHandler'; | import ResizeMixin from './mixin/ResizeHandler'; | ||||
import { AppMain, Navbar, Sidebar, Feedback } from './components'; | |||||
import { AppMain, Navbar, Sidebar, Guideline, Feedback } from './components'; | |||||
export default { | export default { | ||||
name: 'BaseLayout', | name: 'BaseLayout', | ||||
@@ -46,6 +54,7 @@ export default { | |||||
AppMain, | AppMain, | ||||
Navbar, | Navbar, | ||||
Sidebar, | Sidebar, | ||||
Guideline, | |||||
Feedback, | Feedback, | ||||
}, | }, | ||||
mixins: [ResizeMixin], | mixins: [ResizeMixin], | ||||
@@ -62,6 +71,10 @@ export default { | |||||
type: Boolean, | type: Boolean, | ||||
default: true, | default: true, | ||||
}, | }, | ||||
showFooter: { | |||||
type: Boolean, | |||||
default: true, | |||||
}, | |||||
}, | }, | ||||
computed: { | computed: { | ||||
...mapState({ | ...mapState({ | ||||
@@ -151,4 +164,21 @@ export default { | |||||
.mobile .fixed-header { | .mobile .fixed-header { | ||||
width: 100%; | 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> | </style> |
@@ -15,7 +15,7 @@ | |||||
*/ | */ | ||||
<template> | <template> | ||||
<BaseLayout :showBack="true" :showSidebar="false"> | |||||
<BaseLayout :showBack="true" :showSidebar="false" :showFooter="false"> | |||||
<div slot="left-options" style="margin-left: 10px;"> | <div slot="left-options" style="margin-left: 10px;"> | ||||
<el-tooltip effect="dark" placement="bottom-start"> | <el-tooltip effect="dark" placement="bottom-start"> | ||||
<div slot="content"> | <div slot="content"> | ||||
@@ -17,13 +17,6 @@ | |||||
<template> | <template> | ||||
<section class="app-main"> | <section class="app-main"> | ||||
<router-view :key="$route.path" /> | <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> | </section> | ||||
</template> | </template> | ||||
@@ -46,23 +39,6 @@ export default { | |||||
.fixed-header + .app-main { | .fixed-header + .app-main { | ||||
padding-top: 50px; | 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> | ||||
<style lang="scss"> | <style lang="scss"> | ||||
@@ -106,7 +106,7 @@ export default { | |||||
@import "~@/assets/styles/variables.scss"; | @import "~@/assets/styles/variables.scss"; | ||||
.feedback { | .feedback { | ||||
margin-right: 10px; | |||||
margin-right: 20px; | |||||
font-size: 14px; | font-size: 14px; | ||||
line-height: $navBarHeight; | line-height: $navBarHeight; | ||||
color: $infoColor; | 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 AppMain } from './AppMain'; | ||||
export { default as Navbar } from './Navbar'; | export { default as Navbar } from './Navbar'; | ||||
export { default as Sidebar } from './Sidebar'; | export { default as Sidebar } from './Sidebar'; | ||||
export { default as Guideline } from './Guideline'; | |||||
export { default as Feedback } from './Feedback'; | export { default as Feedback } from './Feedback'; |
@@ -58,9 +58,13 @@ module.exports = { | |||||
/** | /** | ||||
* RSA公钥 | * RSA公钥 | ||||
*/ | */ | ||||
publicKey: 'MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANL378k3RiZHWx5AfJqdH9xRNBmD9wGD2iRe41HdTNF8RUhNnHit5NpMNtGL0NPTSSpPjjI1kJfVorRvaQerUgkCAwEAAQ==', | |||||
publicKey: '', | |||||
/** | /** | ||||
* 用户社区 | * 用户社区 | ||||
*/ | */ | ||||
Community: 'http://www.aiiaos.cn/index.php?s=/forum/index/forum/id/45.html', | 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 { getMinIOAuth } from '@/api/auth'; | ||||
import { decrypt } from '@/utils/rsaEncrypt'; | import { decrypt } from '@/utils/rsaEncrypt'; | ||||
import { minIO } from '@/config'; | |||||
const Minio = require('minio'); | const Minio = require('minio'); | ||||
const toArray = require('stream-to-array'); | const toArray = require('stream-to-array'); | ||||
const Config = require('@/config'); | |||||
const env = process.env.NODE_ENV || 'development'; | 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 | // 导出 bucketName | ||||
export const {bucketName} = minIOConfig; | export const {bucketName} = minIOConfig; | ||||
@@ -343,3 +343,11 @@ export const getTreeListFromFilepath = async (filepath) => { | |||||
export function getUniqueId() { | export function getUniqueId() { | ||||
return parseTime(new Date(), '{y}{m}{d}{h}{i}{s}{S}') + nanoid(4); | 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" | ref="upload" | ||||
action="fakeApi" | action="fakeApi" | ||||
accept=".zip" | accept=".zip" | ||||
:acceptSize="1024" | |||||
:acceptSizeFormat="(size) => `${size/1024} GB`" | |||||
:acceptSize="algorithmConfig.uploadFileAcceptSize" | |||||
:acceptSizeFormat="uploadSizeFomatter" | |||||
list-type="text" | list-type="text" | ||||
:show-file-count="false" | :show-file-count="false" | ||||
:params="uploadParams" | :params="uploadParams" | ||||
@@ -263,7 +263,7 @@ | |||||
</template> | </template> | ||||
<script> | <script> | ||||
import { downloadZipFromObjectPath, validateNameWithHyphen, getUniqueId } from '@/utils'; | |||||
import { downloadZipFromObjectPath, validateNameWithHyphen, getUniqueId, uploadSizeFomatter } from '@/utils'; | |||||
import CRUD, { presenter, header, form, crud } from '@crud/crud'; | import CRUD, { presenter, header, form, crud } from '@crud/crud'; | ||||
import cdOperation from '@crud/CD.operation'; | import cdOperation from '@crud/CD.operation'; | ||||
import rrOperation from '@crud/RR.operation'; | import rrOperation from '@crud/RR.operation'; | ||||
@@ -275,6 +275,7 @@ import BaseModal from '@/components/BaseModal'; | |||||
import AlgorithmDetail from '@/components/Training/algorithmDetail'; | import AlgorithmDetail from '@/components/Training/algorithmDetail'; | ||||
import UploadInline from '@/components/UploadForm/inline'; | import UploadInline from '@/components/UploadForm/inline'; | ||||
import UploadProgress from '@/components/UploadProgress'; | import UploadProgress from '@/components/UploadProgress'; | ||||
import { algorithmConfig } from '@/config'; | |||||
const defaultForm = { | const defaultForm = { | ||||
id: null, | id: null, | ||||
@@ -366,6 +367,7 @@ export default { | |||||
uploading: false, | uploading: false, | ||||
progress: 0, | progress: 0, | ||||
size: 0, | size: 0, | ||||
algorithmConfig, | |||||
customColors: [ | customColors: [ | ||||
{color: '#909399', percentage: 40}, | {color: '#909399', percentage: 40}, | ||||
{color: '#e6a23c', percentage: 80}, | {color: '#e6a23c', percentage: 80}, | ||||
@@ -577,6 +579,7 @@ export default { | |||||
noteBookName, | noteBookName, | ||||
}}); | }}); | ||||
}, | }, | ||||
uploadSizeFomatter, | |||||
}, | }, | ||||
}; | }; | ||||
</script> | </script> |
@@ -240,7 +240,6 @@ export default { | |||||
<style lang="scss"> | <style lang="scss"> | ||||
.workspace-settings { | .workspace-settings { | ||||
padding: 28px 28px 0; | padding: 28px 28px 0; | ||||
margin-bottom: 33px; | |||||
overflow-y: auto; | overflow-y: auto; | ||||
background-color: rgb(242, 242, 242); | background-color: rgb(242, 242, 242); | ||||
@@ -260,7 +260,6 @@ export default { | |||||
flex-direction: column; | flex-direction: column; | ||||
width: 160px; | width: 160px; | ||||
padding-top: 20px; | padding-top: 20px; | ||||
margin-bottom: 34px; | |||||
text-align: center; | text-align: center; | ||||
background: #fff; | background: #fff; | ||||
box-shadow: 2px 0 6px 0 rgba(0, 0, 0, 0.15); | 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 addEventListener = require('add-dom-event-listener'); | ||||
const FooterHeight = 32; | |||||
const FooterHeight = 0; | |||||
// 侧边栏宽度 | // 侧边栏宽度 | ||||
export const ThumbWidth = 160; | export const ThumbWidth = 160; | ||||
@@ -881,7 +881,7 @@ export default { | |||||
top: 0; | top: 0; | ||||
left: 0; | left: 0; | ||||
width: 100%; | width: 100%; | ||||
height: calc(100vh - 130px); | |||||
height: calc(100vh - 50px - 48px); | |||||
} | } | ||||
.annotation-score-group { | .annotation-score-group { | ||||
@@ -14,7 +14,7 @@ | |||||
* ============================================================= | * ============================================================= | ||||
*/ | */ | ||||
import { statusCodeMap } from '../util'; | |||||
import { statusCodeMap, dataTypeCodeMap } from '../util'; | |||||
export default { | export default { | ||||
name: 'DatasetAction', | name: 'DatasetAction', | ||||
@@ -59,7 +59,7 @@ export default { | |||||
// 查看标注按钮在 自动标注中 未采样 采样中 采样失败 目标跟踪中 数据增强中 目标跟踪失败 时不显示, 此外,类型为视频时,自动标注完成也不可查看(此时下游会进行目标跟踪) | // 查看标注按钮在 自动标注中 未采样 采样中 采样失败 目标跟踪中 数据增强中 目标跟踪失败 时不显示, 此外,类型为视频时,自动标注完成也不可查看(此时下游会进行目标跟踪) | ||||
let showCheckButton = !['AUTO_ANNOTATING', 'UNSAMPLED', 'SAMPLING', 'SAMPLE_FAILED', 'TRACKING', 'ENHANCING', 'TRACK_FAILED'].includes(statusCodeMap[row.status]); | 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; | 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])) { | if (['ANNOTATED', 'TRACK_SUCCEED'].includes(statusCodeMap[row.status])) { | ||||
showPublishButton = true; | showPublishButton = true; | ||||
publishButton = publishDialogButton; | publishButton = publishDialogButton; | ||||
@@ -135,7 +135,7 @@ export default { | |||||
); | ); | ||||
// 类型为视频时,当状态为未采样时才可导入,其余状态不可导入 | // 类型为视频时,当状态为未采样时才可导入,其余状态不可导入 | ||||
// 类型为图片时,自动标注中、数据增强中 目标跟踪失败 不可导入,其余状态均可导入 | // 类型为图片时,自动标注中、数据增强中 目标跟踪失败 不可导入,其余状态均可导入 | ||||
if (row.dataType === 1) { | |||||
if (row.dataType === dataTypeCodeMap.VIDEO) { | |||||
if (statusCodeMap[row.status] === 'UNSAMPLED') { | if (statusCodeMap[row.status] === 'UNSAMPLED') { | ||||
showUploadButton = true; | 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 = ( | const reAutoButton = ( | ||||
<el-popconfirm | <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 = ( | const augmentButton = ( | ||||
<el-button {...btnProps} onClick={() => dataEnhance(row)}> | <el-button {...btnProps} onClick={() => dataEnhance(row)}> | ||||
@@ -50,79 +50,51 @@ | |||||
v-model="form.annotateType" | v-model="form.annotateType" | ||||
placeholder="标注类型" | placeholder="标注类型" | ||||
:dataSource="annotationList" | :dataSource="annotationList" | ||||
:disabled="form.dataType === 1" | |||||
:disabled="form.dataType === dataTypeCodeMap.VIDEO" | |||||
@change="handleAnnotateTypeChange" | @change="handleAnnotateTypeChange" | ||||
/> | /> | ||||
</el-form-item> | </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 | <el-link | ||||
v-if="labelGroupId !== null" | |||||
v-if="chosenGroupId !== null" | |||||
target="_blank" | target="_blank" | ||||
type="primary" | type="primary" | ||||
:underline="false" | :underline="false" | ||||
class="vm" | 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> | </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> | <span>标签组需要在</span> | ||||
<a | <a | ||||
target="_blank" | target="_blank" | ||||
@@ -169,7 +141,7 @@ | |||||
/> | /> | ||||
<!--上传视频时显示帧间隔设置--> | <!--上传视频时显示帧间隔设置--> | ||||
<el-form | <el-form | ||||
v-if="form.dataType === 1" | |||||
v-if="form.dataType === dataTypeCodeMap.VIDEO" | |||||
ref="formStep1" | ref="formStep1" | ||||
:model="step1Form" | :model="step1Form" | ||||
label-width="100px" | label-width="100px" | ||||
@@ -192,7 +164,7 @@ | |||||
<div v-if="activeStep === 2 && skipUpload !== true"> | <div v-if="activeStep === 2 && skipUpload !== true"> | ||||
<!--上传图片进度条--> | <!--上传图片进度条--> | ||||
<el-progress | <el-progress | ||||
v-if="form.dataType !== 1" | |||||
v-if="form.dataType !== dataTypeCodeMap.VIDEO" | |||||
type="circle" | type="circle" | ||||
:percentage="uploadPercent" | :percentage="uploadPercent" | ||||
:status="uploadStatus" | :status="uploadStatus" | ||||
@@ -230,7 +202,9 @@ import { getLabelGroupList } from '@/api/preparation/labelGroup'; | |||||
import { | import { | ||||
getImgFromMinIO, | getImgFromMinIO, | ||||
annotationMap, | annotationMap, | ||||
annotationCodeMap, | |||||
dataTypeMap, | dataTypeMap, | ||||
dataTypeCodeMap, | |||||
withDimensionFile, | withDimensionFile, | ||||
trackUploadProps, | trackUploadProps, | ||||
} from '@/views/dataset/util'; | } from '@/views/dataset/util'; | ||||
@@ -282,15 +256,12 @@ export default { | |||||
}, | }, | ||||
data() { | data() { | ||||
return { | return { | ||||
dataTypeCodeMap, | |||||
chosenDatasetId: 0, // 当前数据集id | chosenDatasetId: 0, // 当前数据集id | ||||
activeStep: 0, // 当前的step | activeStep: 0, // 当前的step | ||||
actionKey: 1, | |||||
// customLabelEnabled: true, // 自定义标签组可用性 | |||||
systemLabelEnabled: true, // 预置标签组可用性 | |||||
uploadPercent: 0, | uploadPercent: 0, | ||||
uploadStatus: undefined, | uploadStatus: undefined, | ||||
skipUpload: false, // 跳过上传 | skipUpload: false, // 跳过上传 | ||||
popoverVisible: false, | |||||
rules: { | rules: { | ||||
name: [ | name: [ | ||||
{ required: true, message: '请输入数据集名称', trigger: ['change', 'blur'] }, | { required: true, message: '请输入数据集名称', trigger: ['change', 'blur'] }, | ||||
@@ -309,13 +280,20 @@ export default { | |||||
step1Form: { | step1Form: { | ||||
frameInterval: defaultFrameInterval, // 默认值 | 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: { | computed: { | ||||
@@ -326,7 +304,7 @@ export default { | |||||
uploadParams() { | uploadParams() { | ||||
// 是否为视频数据类类型 | // 是否为视频数据类类型 | ||||
const isVideo = | 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`; | const dir = isVideo ? `video` : `origin`; | ||||
return { | return { | ||||
datasetId: this.chosenDatasetId, | datasetId: this.chosenDatasetId, | ||||
@@ -335,7 +313,7 @@ export default { | |||||
}, | }, | ||||
// 新建数据集(视频)上传组件参数 | // 新建数据集(视频)上传组件参数 | ||||
optionCreateProps() { | optionCreateProps() { | ||||
const props = this.form.dataType === 1 ? trackUploadProps : {}; | |||||
const props = this.form.dataType === dataTypeCodeMap.VIDEO ? trackUploadProps : {}; | |||||
return props; | return props; | ||||
}, | }, | ||||
annotationList() { | annotationList() { | ||||
@@ -348,10 +326,10 @@ export default { | |||||
// 如果是视频,只能用目标跟踪 | // 如果是视频,只能用目标跟踪 | ||||
return rawAnnotationList.map(d => { | return rawAnnotationList.map(d => { | ||||
let disabled = false; | 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 { | return { | ||||
...d, | ...d, | ||||
@@ -375,47 +353,33 @@ export default { | |||||
this.crud.toQuery(); | this.crud.toQuery(); | ||||
getLabelGroupList(1).then(res => { | getLabelGroupList(1).then(res => { | ||||
res.forEach((item) => { | 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 => { | getLabelGroupList(0).then(res => { | ||||
res.forEach((item) => { | 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: { | 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.$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.$refs.initFileUploadForm?.$refs?.formRef.reset(); | ||||
this.crud.cancelCU(); | this.crud.cancelCU(); | ||||
@@ -441,50 +403,42 @@ export default { | |||||
this.videoUploadProgress = 0; | this.videoUploadProgress = 0; | ||||
}, | }, | ||||
// step0 标签选择框刷新 | |||||
handleLabelHide() { | |||||
this.actionKey += 1; | |||||
}, | |||||
// step0 改变数据类型 | // step0 改变数据类型 | ||||
handleDataTypeChange(dataType) { | 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 { | } else { | ||||
// 数据类型选中为其他时 去除限制 | |||||
this.form.annotateType = undefined; | this.form.annotateType = undefined; | ||||
this.systemLabelEnabled = true; | |||||
this.labelGroupOptions[1].disabled = false; | |||||
this.labelGroupOptions[1].children.forEach( item => {item.disabled = false;}); | |||||
} | } | ||||
}, | }, | ||||
// step0 改变标注类型 | // step0 改变标注类型 | ||||
handleAnnotateTypeChange(annotateType) { | 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 创建数据集调用 | // step0 创建数据集调用 | ||||
createDataset() { | createDataset() { | ||||
@@ -494,7 +448,7 @@ export default { | |||||
return; | return; | ||||
} | } | ||||
this.crud.status.add = CRUD.STATUS.PROCESSING; | this.crud.status.add = CRUD.STATUS.PROCESSING; | ||||
this.crud.form.labelGroupId = this.labelGroupId; | |||||
this.crud.form.labelGroupId = this.chosenGroupId; | |||||
this.crud.crudMethod | this.crud.crudMethod | ||||
.add(this.crud.form) | .add(this.crud.form) | ||||
.then(res => { | .then(res => { | ||||
@@ -523,10 +477,10 @@ export default { | |||||
// 点击导入操作 | // 点击导入操作 | ||||
const { dataType } = datasetInfo || {}; | const { dataType } = datasetInfo || {}; | ||||
// 文件上传 | // 文件上传 | ||||
if (dataType === 0) { | |||||
if (dataType === dataTypeCodeMap.IMAGE) { | |||||
return submit(datasetId, files); | return submit(datasetId, files); | ||||
} | } | ||||
if (dataType === 1) { | |||||
if (dataType === dataTypeCodeMap.VIDEO) { | |||||
return submitVideo(datasetId, { | return submitVideo(datasetId, { | ||||
frameInterval: this.step1Form.frameInterval, | frameInterval: this.step1Form.frameInterval, | ||||
url: files[0].url, | url: files[0].url, | ||||
@@ -540,7 +494,7 @@ export default { | |||||
this.activeStep+=1; | this.activeStep+=1; | ||||
} | } | ||||
// 视频上传完毕 | // 视频上传完毕 | ||||
if (this.form.dataType === 1) { | |||||
if (this.form.dataType === dataTypeCodeMap.VIDEO) { | |||||
this.videoUploadProgress = 100; | this.videoUploadProgress = 100; | ||||
} | } | ||||
const files = getImgFromMinIO(res); | const files = getImgFromMinIO(res); | ||||
@@ -588,7 +542,7 @@ export default { | |||||
// step2 进度格式化 | // step2 进度格式化 | ||||
formatProgress(percentage) { | formatProgress(percentage) { | ||||
let formatTxt = `${percentage}%`; | let formatTxt = `${percentage}%`; | ||||
if (this.form.dataType === 1) { | |||||
if (this.form.dataType === dataTypeCodeMap.VIDEO) { | |||||
formatTxt = this.videoUploadProgress === 100 ? `100%` : `上传中...`; | formatTxt = this.videoUploadProgress === 100 ? `100%` : `上传中...`; | ||||
} | } | ||||
return formatTxt; | return formatTxt; | ||||
@@ -42,72 +42,46 @@ | |||||
disabled | disabled | ||||
/> | /> | ||||
</el-form-item> | </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> | </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> | ||||
<div v-else class="label-input" style="color: #c0c4cc; background-color: #f5f7fa;"> | <div v-else class="label-input" style="color: #c0c4cc; background-color: #f5f7fa;"> | ||||
{{state.model.labelGroupName}} | {{state.model.labelGroupName}} | ||||
@@ -124,6 +98,19 @@ | |||||
</el-link> | </el-link> | ||||
</div> | </div> | ||||
</el-form-item> | </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-form-item label="数据集描述" prop="remark"> | ||||
<el-input | <el-input | ||||
v-model="state.model.remark" | v-model="state.model.remark" | ||||
@@ -140,12 +127,12 @@ | |||||
<script> | <script> | ||||
import {isNil} from 'lodash'; | 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 BaseModal from '@/components/BaseModal'; | ||||
import InfoSelect from '@/components/InfoSelect'; | import InfoSelect from '@/components/InfoSelect'; | ||||
import { validateName } from '@/utils/validate'; | 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'; | import { getLabelGroupList } from '@/api/preparation/labelGroup'; | ||||
export default { | export default { | ||||
@@ -165,7 +152,6 @@ export default { | |||||
}, | }, | ||||
handleCancel: Function, | handleCancel: Function, | ||||
handleOk: Function, | handleOk: Function, | ||||
goLabelGroupDetail: Function, | |||||
row: { | row: { | ||||
type: Object, | type: Object, | ||||
default: () => {}, | default: () => {}, | ||||
@@ -173,9 +159,6 @@ export default { | |||||
}, | }, | ||||
setup(props, { refs }) { | setup(props, { refs }) { | ||||
const { handleOk } = props; | const { handleOk } = props; | ||||
const popoverRef = ref(null); | |||||
const systemLabelGroups = []; | |||||
const customLabelGroups = []; | |||||
const rules= { | const rules= { | ||||
name: [ | name: [ | ||||
@@ -199,14 +182,20 @@ export default { | |||||
const state = reactive({ | const state = reactive({ | ||||
model: buildModel(props.row), | 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(() => { | const deletable = computed(() => { | ||||
@@ -230,10 +219,10 @@ export default { | |||||
// 如果是视频,只能用目标跟踪 | // 如果是视频,只能用目标跟踪 | ||||
return rawAnnotationList.map(d => { | return rawAnnotationList.map(d => { | ||||
let disabled = false; | 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 { | return { | ||||
...d, | ...d, | ||||
@@ -247,6 +236,7 @@ export default { | |||||
}); | }); | ||||
const handleEditDataset = () => { | const handleEditDataset = () => { | ||||
state.model.labelGroupId = state.chosenGroupId; | |||||
refs.form.validate(valid => { | refs.form.validate(valid => { | ||||
if (!valid) { | if (!valid) { | ||||
return false; | return false; | ||||
@@ -255,72 +245,75 @@ export default { | |||||
return null; | 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(() => { | 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) => { | watch(() => props.row, (next) => { | ||||
Object.assign(state, { | Object.assign(state, { | ||||
model: { ...state.model, ...next }, | 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 { | return { | ||||
@@ -328,17 +321,10 @@ export default { | |||||
state, | state, | ||||
deletable, | deletable, | ||||
editable, | editable, | ||||
systemLabelEnabled, | |||||
optionEnabled, | |||||
systemLabelGroups, | |||||
customLabelGroups, | |||||
handleCustomId, | |||||
handleSystemId, | |||||
handleRemoveLabelGroup, | |||||
handleGroupChange, | |||||
handleEditDataset, | handleEditDataset, | ||||
dataTypeList, | dataTypeList, | ||||
annotationList, | annotationList, | ||||
popoverRef, | |||||
}; | }; | ||||
}, | }, | ||||
}; | }; | ||||
@@ -16,6 +16,14 @@ | |||||
@import "~@/assets/styles/variables.scss"; | @import "~@/assets/styles/variables.scss"; | ||||
.group-cascader{ | |||||
.el-cascader-menu__wrap{ | |||||
max-height: 300px; | |||||
min-height: 100px; | |||||
max-width: 280px; | |||||
} | |||||
} | |||||
.table-top-row { | .table-top-row { | ||||
background-color: $menuBg !important; | background-color: $menuBg !important; | ||||
} | } | ||||
@@ -193,6 +193,11 @@ export const dataTypeMap = { | |||||
1: '视频', | 1: '视频', | ||||
}; | }; | ||||
export const dataTypeCodeMap = { | |||||
'IMAGE': 0, | |||||
'VIDEO': 1, | |||||
}; | |||||
// 文件状态 | // 文件状态 | ||||
export const fileTypeEnum = { | export const fileTypeEnum = { | ||||
0: { label: '全部', abbr: '全部' }, | 0: { label: '全部', abbr: '全部' }, | ||||
@@ -217,6 +222,12 @@ export const fileCodeMap = { | |||||
'COMPLETED': 302, | 'COMPLETED': 302, | ||||
}; | }; | ||||
export const annotationCodeMap = { | |||||
'ANNOTATE': 1, | |||||
'CLASSIFY': 2, | |||||
'TRACK': 5, | |||||
}; | |||||
export const annotationMap = { | export const annotationMap = { | ||||
1: { name: '目标检测', urlPrefix: 'annotate', component: 'AnnotateDataset' }, | 1: { name: '目标检测', urlPrefix: 'annotate', component: 'AnnotateDataset' }, | ||||
2: { name: '图像分类', urlPrefix: 'classify', component: 'Classify' }, | 2: { name: '图像分类', urlPrefix: 'classify', component: 'Classify' }, | ||||
@@ -24,7 +24,7 @@ | |||||
:prop="'labels.' + index" | :prop="'labels.' + index" | ||||
:rules="rules" | :rules="rules" | ||||
> | > | ||||
<div class="flex"> | |||||
<div v-if="addAble" class="flex"> | |||||
<InfoSelect | <InfoSelect | ||||
:value="list[index].id || list[index].name" | :value="list[index].id || list[index].name" | ||||
style="width: 200px; margin-right: 10px;" | style="width: 200px; margin-right: 10px;" | ||||
@@ -57,6 +57,10 @@ | |||||
/> | /> | ||||
</span> | </span> | ||||
</div> | </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> | </el-form-item> | ||||
</div> | </div> | ||||
</template> | </template> | ||||
@@ -91,7 +91,8 @@ | |||||
v-if="refreshFlag" | v-if="refreshFlag" | ||||
action="fakeApi" | action="fakeApi" | ||||
accept=".zip,.pb,.h5,.ckpt,.pkl,.pth,.weight,.caffemodel,.pt" | accept=".zip,.pb,.h5,.ckpt,.pkl,.pth,.weight,.caffemodel,.pt" | ||||
:acceptSize="0" | |||||
:acceptSize="modelConfig.uploadFileAcceptSize" | |||||
:acceptSizeFormat="uploadSizeFomatter" | |||||
list-type="text" | list-type="text" | ||||
:limit="1" | :limit="1" | ||||
:multiple="false" | :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 { list as getAlgorithmUsages, add as addAlgorithmUsage } from '@/api/algorithm/algorithmUsage'; | ||||
import UploadInline from '@/components/UploadForm/inline'; | import UploadInline from '@/components/UploadForm/inline'; | ||||
import UploadProgress from '@/components/UploadProgress'; | import UploadProgress from '@/components/UploadProgress'; | ||||
import { getUniqueId, validateNameWithHyphen } from '@/utils'; | |||||
import { getUniqueId, validateNameWithHyphen, uploadSizeFomatter } from '@/utils'; | |||||
import { modelConfig } from '@/config'; | |||||
const defaultForm = { | const defaultForm = { | ||||
name: null, | name: null, | ||||
@@ -195,6 +197,7 @@ export default { | |||||
{color: '#e6a23c', percentage: 80}, | {color: '#e6a23c', percentage: 80}, | ||||
{color: '#67c23a', percentage: 100}, | {color: '#67c23a', percentage: 100}, | ||||
], | ], | ||||
modelConfig, | |||||
}; | }; | ||||
}, | }, | ||||
computed: { | computed: { | ||||
@@ -309,6 +312,7 @@ export default { | |||||
updateImagePath() { | updateImagePath() { | ||||
this.uploadParams.objectPath = `upload-temp/${this.user.id}/${getUniqueId()}`; | this.uploadParams.objectPath = `upload-temp/${this.user.id}/${getUniqueId()}`; | ||||
}, | }, | ||||
uploadSizeFomatter, | |||||
}, | }, | ||||
}; | }; | ||||
</script> | </script> |
@@ -74,7 +74,8 @@ | |||||
ref="upload" | ref="upload" | ||||
action="fakeApi" | action="fakeApi" | ||||
accept=".zip, .pb, .h5, .ckpt, .pkl, .pth, .weight, .caffemodel, .pt" | accept=".zip, .pb, .h5, .ckpt, .pkl, .pth, .weight, .caffemodel, .pt" | ||||
:acceptSize="0" | |||||
:acceptSize="modelConfig.uploadFileAcceptSize" | |||||
:acceptSizeFormat="uploadSizeFomatter" | |||||
list-type="text" | list-type="text" | ||||
:limit="1" | :limit="1" | ||||
:multiple="false" | :multiple="false" | ||||
@@ -108,7 +109,8 @@ import cdOperation from '@crud/CD.operation'; | |||||
import pagination from '@crud/Pagination'; | import pagination from '@crud/Pagination'; | ||||
import UploadInline from '@/components/UploadForm/inline'; | import UploadInline from '@/components/UploadForm/inline'; | ||||
import UploadProgress from '@/components/UploadProgress'; | import UploadProgress from '@/components/UploadProgress'; | ||||
import { getUniqueId, downloadZipFromObjectPath } from '@/utils'; | |||||
import { getUniqueId, downloadZipFromObjectPath, uploadSizeFomatter } from '@/utils'; | |||||
import { modelConfig } from '@/config'; | |||||
const defaultForm = { | const defaultForm = { | ||||
parentId: null, | parentId: null, | ||||
@@ -157,6 +159,7 @@ export default { | |||||
{color: '#e6a23c', percentage: 80}, | {color: '#e6a23c', percentage: 80}, | ||||
{color: '#67c23a', percentage: 100}, | {color: '#67c23a', percentage: 100}, | ||||
], | ], | ||||
modelConfig, | |||||
}; | }; | ||||
}, | }, | ||||
computed: { | computed: { | ||||
@@ -254,6 +257,7 @@ export default { | |||||
() => {}, | () => {}, | ||||
); | ); | ||||
}, | }, | ||||
uploadSizeFomatter, | |||||
}, | }, | ||||
}; | }; | ||||
</script> | </script> |
@@ -129,7 +129,8 @@ | |||||
action="fakeApi" | action="fakeApi" | ||||
accept=".zip,.tar,.rar,.gz" | accept=".zip,.tar,.rar,.gz" | ||||
list-type="text" | list-type="text" | ||||
:acceptSize="0" | |||||
:acceptSize="imageConfig.uploadFileAcceptSize" | |||||
:acceptSizeFormat="uploadSizeFomatter" | |||||
:params="uploadParams" | :params="uploadParams" | ||||
:show-file-count="false" | :show-file-count="false" | ||||
:auto-upload="true" | :auto-upload="true" | ||||
@@ -182,11 +183,12 @@ import rrOperation from '@crud/RR.operation'; | |||||
import pagination from '@crud/Pagination'; | import pagination from '@crud/Pagination'; | ||||
import CRUD, { presenter, header, form, crud } from '@crud/crud'; | import CRUD, { presenter, header, form, crud } from '@crud/crud'; | ||||
import trainingImageApi, { imageNameList, del } from '@/api/trainingImage/index'; | import trainingImageApi, { imageNameList, del } from '@/api/trainingImage/index'; | ||||
import { getUniqueId } from '@/utils'; | |||||
import { getUniqueId, uploadSizeFomatter } from '@/utils'; | |||||
import BaseModal from '@/components/BaseModal'; | import BaseModal from '@/components/BaseModal'; | ||||
import UploadInline from '@/components/UploadForm/inline'; | import UploadInline from '@/components/UploadForm/inline'; | ||||
import DropdownHeader from '@/components/DropdownHeader'; | import DropdownHeader from '@/components/DropdownHeader'; | ||||
import UploadProgress from '@/components/UploadProgress'; | import UploadProgress from '@/components/UploadProgress'; | ||||
import { imageConfig } from '@/config'; | |||||
const defaultForm = { | const defaultForm = { | ||||
imageName: null, | imageName: null, | ||||
@@ -292,6 +294,7 @@ export default { | |||||
loading: false, | loading: false, | ||||
isEdit: false, | isEdit: false, | ||||
prefabricate: true, | prefabricate: true, | ||||
imageConfig, | |||||
}; | }; | ||||
}, | }, | ||||
computed: { | computed: { | ||||
@@ -422,6 +425,7 @@ export default { | |||||
}, | }, | ||||
); | ); | ||||
}, | }, | ||||
uploadSizeFomatter, | |||||
}, | }, | ||||
}; | }; | ||||
</script> | </script> |
@@ -144,6 +144,7 @@ | |||||
<!--模型下载Dialog--> | <!--模型下载Dialog--> | ||||
<path-select-dialog | <path-select-dialog | ||||
ref="pathSelect" | ref="pathSelect" | ||||
class-key="ModelDownload" | |||||
type="modelDownload" | type="modelDownload" | ||||
@chooseDone="chooseDone" | @chooseDone="chooseDone" | ||||
/> | /> | ||||
@@ -18,6 +18,7 @@ | |||||
<!--训练管理页面-断点续训Dialog--> | <!--训练管理页面-断点续训Dialog--> | ||||
<BaseModal | <BaseModal | ||||
:visible.sync="visible" | :visible.sync="visible" | ||||
:class="classKey" | |||||
:title="title" | :title="title" | ||||
width="600px" | width="600px" | ||||
@open="onDialogOpen" | @open="onDialogOpen" | ||||
@@ -48,11 +49,16 @@ import { Loading } from 'element-ui'; | |||||
import BaseModal from '@/components/BaseModal'; | import BaseModal from '@/components/BaseModal'; | ||||
import { getTreeListFromFilepath } from '@/utils'; | import { getTreeListFromFilepath } from '@/utils'; | ||||
import { resumeTrain } from '@/api/trainingJob/job'; | import { resumeTrain } from '@/api/trainingJob/job'; | ||||
import { modelOfficial } from '../utils'; | |||||
export default { | export default { | ||||
name: 'JobResumeDialog', | name: 'JobResumeDialog', | ||||
components: { BaseModal }, | components: { BaseModal }, | ||||
props: { | props: { | ||||
classKey: { | |||||
type: String, | |||||
default: '', | |||||
}, | |||||
type: { | type: { | ||||
type: String, | type: String, | ||||
default: 'jobResume', | default: 'jobResume', | ||||
@@ -79,7 +85,7 @@ export default { | |||||
}; | }; | ||||
}, | }, | ||||
methods: { | methods: { | ||||
async show(item) { | |||||
show(item) { | |||||
this.path = item.resumePath; | this.path = item.resumePath; | ||||
this.id = item.id; | this.id = item.id; | ||||
this.fileName = item.fileName; | this.fileName = item.fileName; | ||||
@@ -90,25 +96,8 @@ export default { | |||||
this.visible = true; | this.visible = true; | ||||
}, | }, | ||||
getCentext(type='', num) { | 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 | // handle | ||||
@@ -117,7 +106,7 @@ export default { | |||||
this.treeList = []; | this.treeList = []; | ||||
}, | }, | ||||
async onDialogOpened() { | 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.treeList, this.defaultExpandedKeys] = await getTreeListFromFilepath( | ||||
this.path, | this.path, | ||||
); | ); | ||||
@@ -161,6 +161,7 @@ | |||||
<!--断点续训Dialog--> | <!--断点续训Dialog--> | ||||
<path-select-dialog | <path-select-dialog | ||||
ref="pathSelect" | ref="pathSelect" | ||||
class-key="keepTrainDialog" | |||||
:type="pathType" | :type="pathType" | ||||
@chooseDone="chooseDone" | @chooseDone="chooseDone" | ||||
@chooseModel="chooseModel" | @chooseModel="chooseModel" | ||||
@@ -23,4 +23,23 @@ export const trainingStatusMap = { | |||||
4: { tagMap: 'info', statusMap: 'done' }, | 4: { tagMap: 'info', statusMap: 'done' }, | ||||
5: { statusMap: 'done' }, | 5: { statusMap: 'done' }, | ||||
7: { tagMap: 'danger', 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 path = require('path'); | ||||
const sass = require('sass'); | 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'); | const defaultSettings = require('./src/settings.js'); | ||||
function resolve(dir) { | function resolve(dir) { | ||||
@@ -24,6 +29,98 @@ module.exports = { | |||||
warnings: false, | warnings: false, | ||||
errors: true, | 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: { | css: { | ||||
loaderOptions: { | loaderOptions: { | ||||