| @@ -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: { | |||