Browse Source

update webapp

tags/v0.3.0
之江实验室 3 years ago
parent
commit
8922323a32
39 changed files with 1235 additions and 851 deletions
  1. +4
    -0
      webapp/.env.mock
  2. +2
    -1
      webapp/.eslintignore
  3. +1
    -0
      webapp/.npmrc
  4. +15
    -0
      webapp/CHANGELOG.md
  5. +9
    -0
      webapp/README.md
  6. +56
    -0
      webapp/mock/api/data/datasets.js
  7. +5
    -0
      webapp/mock/api/data/labelGroup/getList/id.js
  8. +4
    -0
      webapp/mock/mock-map.js
  9. +5
    -2
      webapp/package.json
  10. +7
    -7
      webapp/src/components/Training/jobForm.vue
  11. +44
    -23
      webapp/src/config/index.js
  12. +31
    -1
      webapp/src/layout/BaseLayout.vue
  13. +1
    -1
      webapp/src/layout/DatasetLayout.vue
  14. +0
    -24
      webapp/src/layout/components/AppMain/index.vue
  15. +1
    -1
      webapp/src/layout/components/Feedback/index.vue
  16. +57
    -0
      webapp/src/layout/components/Guideline/index.vue
  17. +1
    -0
      webapp/src/layout/components/index.js
  18. +5
    -1
      webapp/src/settings.js
  19. +2
    -2
      webapp/src/utils/minIO.js
  20. +8
    -0
      webapp/src/utils/utils.js
  21. +6
    -3
      webapp/src/views/algorithm/index.vue
  22. +0
    -1
      webapp/src/views/dataset/annotate/settingContainer/index.vue
  23. +0
    -1
      webapp/src/views/dataset/annotate/thumbContainer/index.vue
  24. +2
    -2
      webapp/src/views/dataset/annotate/workSpaceContainer/index.vue
  25. +6
    -6
      webapp/src/views/dataset/list/action.js
  26. +107
    -153
      webapp/src/views/dataset/list/create-dataset.vue
  27. +130
    -144
      webapp/src/views/dataset/list/edit-dataset.vue
  28. +8
    -0
      webapp/src/views/dataset/style/list.scss
  29. +11
    -0
      webapp/src/views/dataset/util.js
  30. +5
    -1
      webapp/src/views/labelGroup/dynamicField.vue
  31. +6
    -2
      webapp/src/views/model/components/addModelDialog.vue
  32. +6
    -2
      webapp/src/views/model/version.vue
  33. +6
    -2
      webapp/src/views/trainingImage/index.vue
  34. +1
    -0
      webapp/src/views/trainingJob/components/jobDetail.vue
  35. +10
    -21
      webapp/src/views/trainingJob/components/pathSelectDialog.vue
  36. +1
    -0
      webapp/src/views/trainingJob/detail.vue
  37. +20
    -1
      webapp/src/views/trainingJob/utils.js
  38. +98
    -1
      webapp/vue.config.js
  39. +554
    -448
      webapp/yarn.lock

+ 4
- 0
webapp/.env.mock View File

@@ -0,0 +1,4 @@
ENV='development'
VUE_APP_MOCK=true
VUE_APP_BASE_API = ''
VUE_APP_DATA_API = '/mock'

+ 2
- 1
webapp/.eslintignore View File

@@ -2,4 +2,5 @@ build/*.js
src/assets
public
dist
src/components/Crud
src/components/Crud
mock

+ 1
- 0
webapp/.npmrc View File

@@ -0,0 +1 @@
package-lock=true

+ 15
- 0
webapp/CHANGELOG.md View File

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


+ 9
- 0
webapp/README.md View File

@@ -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 地址。

## 项目结构

```


+ 56
- 0
webapp/mock/api/data/datasets.js View File

@@ -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
},
}
}

+ 5
- 0
webapp/mock/api/data/labelGroup/getList/id.js View File

@@ -0,0 +1,5 @@
module.exports = {
"code": 200,
"msg": null,
"data": []
}

+ 4
- 0
webapp/mock/mock-map.js View File

@@ -0,0 +1,4 @@
// 定义 RESTful 接口和实际代码的映射
module.exports = {
'GET::/api/data/labelGroup/getList/(\\d+)': '/api/data/labelGroup/getList/id',
};

+ 5
- 2
webapp/package.json View File

@@ -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",


+ 7
- 7
webapp/src/components/Training/jobForm.vue View File

@@ -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"
/>&nbsp;小时
</el-form-item>
@@ -295,7 +295,7 @@
id="delayDeleteTime"
v-model="form.delayDeleteTime"
:min="0"
:max="168"
:max="trainConfig.delayDeleteTimeMax"
:step-strictly="true"
/>&nbsp;小时
<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;
},
},
};


+ 44
- 23
webapp/src/config/index.js View File

@@ -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 表示不限制大小
};

+ 31
- 1
webapp/src/layout/BaseLayout.vue View File

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

+ 1
- 1
webapp/src/layout/DatasetLayout.vue View File

@@ -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">


+ 0
- 24
webapp/src/layout/components/AppMain/index.vue View File

@@ -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">


+ 1
- 1
webapp/src/layout/components/Feedback/index.vue View File

@@ -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;


+ 57
- 0
webapp/src/layout/components/Guideline/index.vue View File

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

+ 1
- 0
webapp/src/layout/components/index.js View File

@@ -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';

+ 5
- 1
webapp/src/settings.js View File

@@ -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/' ,
};

+ 2
- 2
webapp/src/utils/minIO.js View File

@@ -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;


+ 8
- 0
webapp/src/utils/utils.js View File

@@ -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`;
}

+ 6
- 3
webapp/src/views/algorithm/index.vue View File

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

+ 0
- 1
webapp/src/views/dataset/annotate/settingContainer/index.vue View File

@@ -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);



+ 0
- 1
webapp/src/views/dataset/annotate/thumbContainer/index.vue View File

@@ -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);


+ 2
- 2
webapp/src/views/dataset/annotate/workSpaceContainer/index.vue View File

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


+ 6
- 6
webapp/src/views/dataset/list/action.js View File

@@ -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)}>


+ 107
- 153
webapp/src/views/dataset/list/create-dataset.vue View File

@@ -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">
&nbsp;
<span v-if="labelGroupId === null">&nbsp;&nbsp;标签组</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;


+ 130
- 144
webapp/src/views/dataset/list/edit-dataset.vue View File

@@ -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">
&nbsp;
<span v-if="state.model.labelGroupId === null">&nbsp;&nbsp;标签组</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;">
&nbsp;&nbsp;&nbsp;&nbsp;{{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,
};
},
};


+ 8
- 0
webapp/src/views/dataset/style/list.scss View File

@@ -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;
}


+ 11
- 0
webapp/src/views/dataset/util.js View File

@@ -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' },


+ 5
- 1
webapp/src/views/labelGroup/dynamicField.vue View File

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


+ 6
- 2
webapp/src/views/model/components/addModelDialog.vue View File

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

+ 6
- 2
webapp/src/views/model/version.vue View File

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

+ 6
- 2
webapp/src/views/trainingImage/index.vue View File

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

+ 1
- 0
webapp/src/views/trainingJob/components/jobDetail.vue View File

@@ -144,6 +144,7 @@
<!--模型下载Dialog-->
<path-select-dialog
ref="pathSelect"
class-key="ModelDownload"
type="modelDownload"
@chooseDone="chooseDone"
/>


+ 10
- 21
webapp/src/views/trainingJob/components/pathSelectDialog.vue View File

@@ -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,
);


+ 1
- 0
webapp/src/views/trainingJob/detail.vue View File

@@ -161,6 +161,7 @@
<!--断点续训Dialog-->
<path-select-dialog
ref="pathSelect"
class-key="keepTrainDialog"
:type="pathType"
@chooseDone="chooseDone"
@chooseModel="chooseModel"


+ 20
- 1
webapp/src/views/trainingJob/utils.js View File

@@ -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': '暂无模型数据',
},
];

+ 98
- 1
webapp/vue.config.js View File

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


+ 554
- 448
webapp/yarn.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save