diff --git a/webapp/.env.development b/webapp/.env.development
index 9051227..702f9a5 100644
--- a/webapp/.env.development
+++ b/webapp/.env.development
@@ -7,7 +7,7 @@ VUE_APP_BASE_API = ''
VUE_APP_DATA_API = ''
# minio
-VUE_APP_MINIO_API = ''
+VUE_APP_MINIO_API = '' // 建议使用与当前 web 服务域名的域名
-# atlas
+# atlas 模型炼知
VUE_APP_ATLAS_HOST = ''
diff --git a/webapp/.env.test b/webapp/.env.test
new file mode 100644
index 0000000..5fa6d82
--- /dev/null
+++ b/webapp/.env.test
@@ -0,0 +1,13 @@
+ENV = 'test'
+
+# 默认BASE URL
+VUE_APP_BASE_API = ''
+
+# 数据管理
+VUE_APP_DATA_API = ''
+
+# minio
+VUE_APP_MINIO_API = ''
+
+# atlas
+VUE_APP_ATLAS_HOST = ''
diff --git a/webapp/.eslintrc.js b/webapp/.eslintrc.js
index 02908fc..cc4ae86 100644
--- a/webapp/.eslintrc.js
+++ b/webapp/.eslintrc.js
@@ -53,15 +53,19 @@ module.exports = {
"vue/attribute-hyphenation": "off",
"vue/comment-directive": "off",
"vue/prop-name-casing": "off",
- "vue/max-attributes-per-line": [
- 2,
- {
- singleline: 20,
- multiline: {
- max: 1,
- allowFirstLine: false
- }
- }
- ]
+ "vue/max-attributes-per-line": [2, {
+ singleline: 20,
+ multiline: {
+ max: 1,
+ allowFirstLine: false
+ }}
+ ],
+ "vue/html-indent": ["error", 2, {
+ "attribute": 1,
+ "baseIndent": 1,
+ "closeBracket": 0,
+ "alignAttributesVertically": true,
+ "ignores": []
+ }]
}
};
diff --git a/webapp/CHANGELOG.md b/webapp/CHANGELOG.md
new file mode 100644
index 0000000..c8cbda0
--- /dev/null
+++ b/webapp/CHANGELOG.md
@@ -0,0 +1,26 @@
+## 1.1.0 (2020-10-26)
+
+### Breaking Change
+
+- [数据管理] 导入数据集功能重构。系统提供标准数据集模板,用户按照规范导入数据集文件,实现数据集全功能兼容
+- [训练管理] 支持OneFlow、TensorFlow、Pytorch等主流框架的多机多卡模式分布式训练
+- [训练管理] 训练时支持将已有模型作为训练入参
+- [训练管理] 训练时支持区分训练数据集与验证数据集
+- [训练管理] 训练支持延时启动、定时停止功能
+- [训练管理] 训练日志、运行日志下载功能优化,避免大文件导致的浏览器卡死
+
+### Features
+
+- [数据管理] 将标签和数据集拆分,引入「标签组」统一管理标签
+- [数据管理] 超大数据集操作流程优化。实现超大数据集(40w+文件)的全流程平滑操作
+- [数据管理] 数据集图片手动标注优化。支持标注像素级位置、大小调整,支持常见缩放、拖拽、平移等操作
+- [数据管理] 数据集状态逻辑优化,代码性能优化等
+- [训练管理] 断点续训功能、模型下载功能、模型保存功能支持通过目录树选择模型文件/文件夹
+- [训练管理] 文件上传增加进度条展示
+- [训练管理] 训练创建页,增加运行命令预览功能;训练详情页,增加算法在线编辑跳转功能
+- [训练管理] 镜像管理功能,镜像名称支持自定义;支持镜像的删除、修改等操作
+- [训练管理] 增加训练失败异常信息反馈
+
+### Bug Fixs
+
+- [数据管理] 标注详情里面不同分辨率图片标注位置偏移 bug
\ No newline at end of file
diff --git a/webapp/LICENSE b/webapp/LICENSE
new file mode 100755
index 0000000..cf57587
--- /dev/null
+++ b/webapp/LICENSE
@@ -0,0 +1,211 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
+
+Other dependencies and licenses:
+----------------------------------------------------------------------------------------
+
+Open Source Software Licensed Under the Apache License, Version 2.0:
+The below software in this distribution may have been modified.
+----------------------------------------------------------------------------------------
+1. EL-ADMIN
+Copyright 2019-2020 Zheng Jie
\ No newline at end of file
diff --git a/webapp/README.md b/webapp/README.md
index b31ee0e..b189f9e 100644
--- a/webapp/README.md
+++ b/webapp/README.md
@@ -1,10 +1,76 @@
-# 一站式开发平台-前端
+# 之江天枢-前端
+
+**之江天枢一站式人工智能开源平台**(简称:**之江天枢**),包括海量数据处理、交互式模型构建(包含Notebook和模型可视化)、AI模型高效训练。多维度产品形态满足从开发者到大型企业的不同需求,将提升人工智能技术的研发效率、扩大算法模型的应用范围,进一步构建人工智能生态“朋友圈”。
+
+## 特性
+* 一站式开发
+* 集成先进算法
+* 灵活易用
+* 性能优越
+
+## 预览
+![概览](/public/dubhe_dashboard.png "概览")
+
+## 源码部署
+
+### 1. 下载源码
+
+``` bash
+git clone https://codeup.teambition.com/zhejianglab/dubhe-web.git
+
+# 进入根目录
+cd dubhe-web
+
+```
+### 2. 配置
+
+根据需要修改如下配置文件
+```
+config/index.js
+settings.js
+.env.production
+```
+
+### 3. 构建
+
+``` bash
+# 安装项目依赖
+npm install
+
+# 构建生产环境
+npm run build:prod
+```
+
+### 4. 部署
+
+- 构建完成后会在根目录生成 dist 文件夹,并将该文件夹上传至服务器;
+- 在服务器 nginx.conf 文件中添加如下配置;
+
+``` nginx
+server {
+ listen 80; # 端口
+ server_name localhost; # 域名/外网IP
+
+ location / {
+ root /home/wwwroot/dubhe-web/dist; # dist 文件夹根目录
+ index index.html;
+ try_files $uri $uri/ /index.html;
+ }
+}
+
+```
+
+- 保存 `nginx.conf` 并重启 Nginx 使之生效。
+
## 本地开发
``` bash
-# 进入前端项目根目录
-cd webapp
+# 下载源码
+git clone https://codeup.teambition.com/zhejianglab/dubhe-web.git
+
+# 进入项目根目录
+cd dubhe-web
# 安装依赖
npm install
diff --git a/webapp/package.json b/webapp/package.json
index c73678d..857480b 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -1,6 +1,6 @@
{
"name": "dubhe-web",
- "version": "1.0.0",
+ "version": "1.1.0",
"description": "之江天枢人工智能开源平台",
"author": "zhejianglab",
"keywords": [
@@ -13,6 +13,8 @@
"scripts": {
"dev": "vue-cli-service serve --open",
"build:prod": "vue-cli-service build",
+ "build:test": "vue-cli-service build --mode test",
+ "build:dev": "vue-cli-service build --mode development",
"lint": "eslint --ext .js,.vue src",
"fix": "eslint --fix --ext .js,.vue src",
"lint:style": "stylelint src/**/*.{html,vue,css,sass,scss}",
@@ -54,6 +56,7 @@
"filereader-stream": "^2.0.0",
"jquery": "^3.5.1",
"jquery-contextmenu": "^2.9.1",
+ "js-beautify": "^1.13.0",
"js-cookie": "2.2.0",
"jsencrypt": "^3.0.0-rc.1",
"json2csv": "^5.0.1",
@@ -72,7 +75,9 @@
"v-hotkey": "^0.8.0",
"vee-validate": "^3.3.0",
"vue": "2.6.10",
+ "vue-copy-to-clipboard": "^1.0.3",
"vue-prism-component": "^1.2.0",
+ "vue-prism-editor": "^1.2.2",
"vue-router": "^3.0.2",
"vuex": "3.1.0"
},
diff --git a/webapp/src/App.vue b/webapp/src/App.vue
index a4fd21a..3cf15e0 100644
--- a/webapp/src/App.vue
+++ b/webapp/src/App.vue
@@ -16,7 +16,9 @@
-
+
+
+
diff --git a/webapp/src/api/preparation/annotation.js b/webapp/src/api/preparation/annotation.js
index f064ac2..5edc191 100644
--- a/webapp/src/api/preparation/annotation.js
+++ b/webapp/src/api/preparation/annotation.js
@@ -16,9 +16,9 @@
import request from '@/utils/request';
-export function batchFinishAnnotation(data) {
+export function batchFinishAnnotation(data, datasetId) {
return request({
- url: 'api/data/datasets/files/annotations',
+ url: `api/data/datasets/files/${datasetId}/annotations`,
method: 'post',
data,
});
@@ -33,6 +33,13 @@ export function delAnnotation(id) {
});
}
+export function track(id) {
+ return request({
+ url: `api/data/datasets/files/annotations/auto/track/${id}`,
+ method: 'get',
+ });
+}
+
export function autoAnnotate(ids) {
const data = { datasetIds: ids };
return request({
diff --git a/webapp/src/api/preparation/datalabel.js b/webapp/src/api/preparation/datalabel.js
index 5840b25..390b051 100644
--- a/webapp/src/api/preparation/datalabel.js
+++ b/webapp/src/api/preparation/datalabel.js
@@ -31,6 +31,14 @@ export function createLabel(id, label) {
});
}
+export function editLabel(id, label) {
+ return request({
+ url: `api/data/datasets/labels/${id}`,
+ method: 'put',
+ data: label,
+ });
+}
+
export function getAutoLabels() {
return request({
url: 'api/data/datasets/labels/auto',
diff --git a/webapp/src/api/preparation/dataset.js b/webapp/src/api/preparation/dataset.js
index 0921a3d..02eca08 100644
--- a/webapp/src/api/preparation/dataset.js
+++ b/webapp/src/api/preparation/dataset.js
@@ -32,6 +32,15 @@ export function detail(id) {
});
}
+// 数据集状态(导入数据集轮询使用)
+export function queryDatasetStatus(ids) {
+ return request({
+ url: `/api/data/datasets/status`,
+ method: 'get',
+ params: { datasetIds: ids },
+ });
+}
+
export function add(data) {
return request({
url: 'api/data/datasets',
@@ -57,6 +66,13 @@ export function editDataset(data) {
});
}
+export function topDataset(data) {
+ return request({
+ url: `api/data/datasets/${data.id}/top`,
+ method: 'get',
+ });
+}
+
// 导入自定义数据集
export function addCustomDataset(data) {
return request({
@@ -153,9 +169,9 @@ export function postDataEnhance(datasetId, types = []) {
}
// 指定原始文件,获取增强文件列表
-export function getEnhanceFileList(fileId) {
+export function getEnhanceFileList(datasetId, fileId) {
return request({
- url: `api/data/datasets/${fileId}/enhanceFileList`,
+ url: `api/data/datasets/${datasetId}/${fileId}/enhanceFileList`,
});
}
@@ -173,4 +189,13 @@ export function queryDatasetsCount() {
});
}
+// 查询数据集状态
+export function queryDatasetsProgress(params) {
+ return request({
+ url: `/api/data/datasets/progress`,
+ method: 'get',
+ params,
+ });
+}
+
export default { list, add, del };
diff --git a/webapp/src/api/preparation/labelGroup.js b/webapp/src/api/preparation/labelGroup.js
new file mode 100644
index 0000000..0a6e172
--- /dev/null
+++ b/webapp/src/api/preparation/labelGroup.js
@@ -0,0 +1,89 @@
+/** 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.
+* =============================================================
+*/
+
+import request from '@/utils/request';
+
+// 创建标签组
+export function add(data) {
+ return request({
+ url: `/api/data/labelGroup`,
+ method: 'post',
+ data,
+ });
+}
+
+// 编辑标签组
+export function edit(data) {
+ return request({
+ url: `/api/data/labelGroup/${data.id}`,
+ method: 'put',
+ data,
+ });
+}
+
+// 删除标签组
+export function del(ids) {
+ return request({
+ url: `/api/data/labelGroup`,
+ method: 'delete',
+ data: {ids},
+ });
+}
+
+// 复制标签组
+export function copy(data) {
+ return request({
+ url: `/api/data/labelGroup/copy`,
+ method: 'post',
+ data,
+ });
+}
+
+// 标签组列表分页查询
+export function list(params) {
+ return request({
+ url: `/api/data/labelGroup/query`,
+ method: 'get',
+ params,
+ });
+}
+
+// 标签组列表的简况查询 用于详情页选择标签组列举
+export function getLabelGroupList(params) {
+ return request({
+ url: `/api/data/labelGroup/getList/${params}`,
+ method: 'get',
+ });
+}
+
+// 获取标签组详情
+export function getLabelGroupDetail(id) {
+ return request({
+ url: `/api/data/labelGroup/${id}`,
+ method: 'get',
+ });
+}
+
+export function importLabelGroup(form) {
+ return request.post(`api/data/labelGroup/import`, form, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ },
+ });
+}
+
+export default { list, add, del, edit };
+
diff --git a/webapp/src/api/system/harbor.js b/webapp/src/api/system/harbor.js
index 2b1a6ba..15fe6ce 100644
--- a/webapp/src/api/system/harbor.js
+++ b/webapp/src/api/system/harbor.js
@@ -18,7 +18,7 @@ import request from '@/utils/request';
export function harborProjectNames() {
return request({
- url: `api/v1/ptImage/project`,
+ url: `api/v1/ptImage/imageNameList`,
method: 'get',
});
}
diff --git a/webapp/src/api/system/pod.js b/webapp/src/api/system/pod.js
new file mode 100644
index 0000000..969d5e8
--- /dev/null
+++ b/webapp/src/api/system/pod.js
@@ -0,0 +1,50 @@
+/** 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.
+* =============================================================
+*/
+
+import request from '@/utils/request';
+
+export function getPodLog(params) {
+ return request({
+ url: 'api/v1/pod/log',
+ method: 'get',
+ params,
+ });
+}
+
+export function downloadPodLog(params) {
+ return request({
+ url: 'api/v1/pod/log/download',
+ method: 'get',
+ params,
+ });
+}
+
+export function batchDownloadPodLog(data) {
+ return request({
+ url: 'api/v1/pod/log/download',
+ method: 'post',
+ responseType: 'blob',
+ data,
+ });
+}
+
+export function countPodLogs(podVOList) {
+ return request({
+ url: 'api/v1/pod/log/count',
+ method: 'post',
+ data: { podVOList },
+ });
+}
diff --git a/webapp/src/api/trainingImage/index.js b/webapp/src/api/trainingImage/index.js
index 9e63f59..b04a497 100644
--- a/webapp/src/api/trainingImage/index.js
+++ b/webapp/src/api/trainingImage/index.js
@@ -32,11 +32,27 @@ export function add(data) {
});
}
-export function project() {
+export function edit(data) {
return request({
- url: 'api/v1/ptImage/project',
+ url: 'api/v1/ptImage',
+ method: 'put',
+ data,
+ });
+}
+
+export function del(ids) {
+ return request({
+ url: 'api/v1/ptImage',
+ method: 'delete',
+ data: ids,
+ });
+}
+
+export function imageNameList() {
+ return request({
+ url: 'api/v1/ptImage/imageNameList',
method: 'get',
});
}
-export default { list, add };
+export default { list, add, edit };
diff --git a/webapp/src/api/trainingJob/job.js b/webapp/src/api/trainingJob/job.js
index 7479b3d..08eaf95 100644
--- a/webapp/src/api/trainingJob/job.js
+++ b/webapp/src/api/trainingJob/job.js
@@ -72,17 +72,17 @@ export function getJobList(params) {
});
}
-export function getTrainLog(params) {
+export function getJobDetail(jobId) {
return request({
- url: `api/v1/trainLog`,
+ url: `api/v1/trainJob/jobDetail`,
method: 'get',
- params,
+ params: { id: jobId },
});
}
-export function downloadTrainLog(params) {
+export function getTrainLog(params) {
return request({
- url: `api/v1/trainLog/download`,
+ url: `api/v1/trainLog`,
method: 'get',
params,
});
@@ -109,4 +109,11 @@ export function getGarafanaInfo(jobId) {
});
}
-export default { list, add, edit, del, getGarafanaInfo };
+export function getPods(jobId) {
+ return request({
+ url: `api/v1/trainLog/pod/${jobId}`,
+ method: 'get',
+ });
+}
+
+export default { list, add, edit, del };
diff --git a/webapp/src/assets/styles/atomic.scss b/webapp/src/assets/styles/atomic.scss
index 5a42586..a475124 100644
--- a/webapp/src/assets/styles/atomic.scss
+++ b/webapp/src/assets/styles/atomic.scss
@@ -214,6 +214,10 @@ img.responsive {
color: red;
}
+.success {
+ color: $successColor;
+}
+
.g3 {
color: #333;
}
diff --git a/webapp/src/assets/styles/common.scss b/webapp/src/assets/styles/common.scss
index 59b2e25..014fd4f 100644
--- a/webapp/src/assets/styles/common.scss
+++ b/webapp/src/assets/styles/common.scss
@@ -126,7 +126,6 @@
}
.text {
- height: 19px;
font-size: 14px;
line-height: 19px;
color: rgba(68, 68, 68, 1);
@@ -143,18 +142,6 @@
}
}
}
-
- .fr {
- margin-right: 5%;
- }
-
- .iframe {
- width: 90%;
- height: 500px;
- margin: 40px 5%;
- overflow: auto;
- border: #ccc solid 1px;
- }
}
.eltabs-inlineblock.el-tabs {
@@ -242,3 +229,9 @@
color: $primaryHoverColor;
}
}
+
+.tree-container {
+ height: 500px;
+ margin-top: 20px;
+ overflow-y: scroll;
+}
diff --git a/webapp/src/assets/styles/element-ui.scss b/webapp/src/assets/styles/element-ui.scss
index 4bbdf20..ee18e90 100644
--- a/webapp/src/assets/styles/element-ui.scss
+++ b/webapp/src/assets/styles/element-ui.scss
@@ -44,6 +44,14 @@
}
}
+// el-radio border
+.el-radio.is-bordered {
+ &.is-checked,
+ &:hover {
+ border-color: $primaryBorderColor;
+ }
+}
+
// radio-button
.el-radio-button__inner {
padding: 8px 25px;
@@ -298,3 +306,27 @@
.el-tooltip__popper {
max-width: 50%;
}
+
+.info-alert.is-light {
+ margin-bottom: 20px;
+ background-color: $primaryBg;
+
+ .el-alert__content {
+ width: 100%;
+ }
+
+ .el-alert__icon {
+ color: $primaryColor;
+ }
+
+ .slot-content {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ color: #000;
+
+ a {
+ color: $primaryColor;
+ }
+ }
+}
diff --git a/webapp/src/assets/styles/index.scss b/webapp/src/assets/styles/index.scss
index dae421a..e38b528 100644
--- a/webapp/src/assets/styles/index.scss
+++ b/webapp/src/assets/styles/index.scss
@@ -69,7 +69,6 @@ ol {
a,
a:focus,
a:hover {
- color: inherit;
text-decoration: none;
cursor: pointer;
}
@@ -87,6 +86,10 @@ ol li {
color: $infoColor;
}
+.fontBold {
+ font-weight: bold;
+}
+
.primary-bg {
background-color: $primaryColor;
@@ -100,3 +103,20 @@ p.error-message {
font-size: 12px;
color: $red;
}
+
+// 新手导引
+.v-tour {
+ div.v-step {
+ color: #2e4fde;
+ background: #d8dfff;
+ }
+
+ button.v-step__button {
+ color: #2e4fde;
+ border-color: #2e4fde;
+
+ &:hover {
+ background: #f3f7ff;
+ }
+ }
+}
diff --git a/webapp/src/assets/styles/variables.scss b/webapp/src/assets/styles/variables.scss
index b005c2a..b5c5cc2 100644
--- a/webapp/src/assets/styles/variables.scss
+++ b/webapp/src/assets/styles/variables.scss
@@ -33,6 +33,7 @@ $imageBg: #f8f8f8;
$borderColorBase: #ebeef5;
$borderColorDark: #c0c4cc;
$black: #001529;
+$dark: #323232;
// sidebar
$menuBg: #f3f7ff;
diff --git a/webapp/src/components/BaseModal/index.js b/webapp/src/components/BaseModal/index.js
index 4a40255..5109388 100644
--- a/webapp/src/components/BaseModal/index.js
+++ b/webapp/src/components/BaseModal/index.js
@@ -97,10 +97,10 @@ const BaseModal = {
return (
);
};
diff --git a/webapp/src/components/Crud/CD.operation.vue b/webapp/src/components/Crud/CD.operation.vue
index 492fbd8..81fbd95 100644
--- a/webapp/src/components/Crud/CD.operation.vue
+++ b/webapp/src/components/Crud/CD.operation.vue
@@ -19,6 +19,7 @@
+import { reactive } from '@vue/composition-api';
+import { findAncestorSvg } from '@/utils';
+
+export default {
+ name: 'Drag',
+ props: {
+ width: Number,
+ height: Number,
+ resetOnStart: {
+ type: Boolean,
+ default: false,
+ },
+ onDragStart: Function,
+ onDragMove: Function,
+ onDragEnd: Function,
+ },
+ setup(props) {
+ const { resetOnStart, onDragStart, onDragMove, onDragEnd } = props;
+ const state = reactive({
+ x: undefined,
+ y: undefined,
+ dx: 0,
+ dy: 0,
+ isDragging: false, // 鼠标按下
+ isMoving: true, // 鼠标移动
+ });
+
+ function getPoint(event) {
+ // 容器尺寸
+ const bound = findAncestorSvg(event).getBoundingClientRect();
+ const { clientX, clientY } = event;
+
+ return {
+ x: clientX - bound.left,
+ y: clientY - bound.top,
+ };
+ }
+
+ function dragStart(event) {
+ const point = getPoint(event);
+ const nextState = {
+ isDragging: true,
+ isMoving: false,
+ dx: resetOnStart ? 0 : state.dx,
+ dy: resetOnStart ? 0 : state.dy,
+ x: resetOnStart ? point.x : -state.dx + point.x,
+ y: resetOnStart ? point.y : -state.dy + point.y,
+ };
+ Object.assign(state, nextState);
+ if (typeof onDragStart === 'function') onDragStart(nextState, event);
+ }
+
+ function dragMove(event) {
+ if (!state.isDragging) return;
+ const point = getPoint(event);
+ // 避免无效移动
+ if(Math.abs(point.x - state.x) < 2 && Math.abs(point.y - state.y) < 2) return;
+ const nextState = {
+ isDragging: true,
+ isMoving: true,
+ dx: point.x - state.x,
+ dy: point.y - state.y,
+ };
+ Object.assign(state, nextState);
+ if (typeof onDragMove === 'function') onDragMove(state, event);
+ }
+
+ function dragEnd(event) {
+ const nextState = {
+ isDragging: false,
+ isMoving: false,
+ };
+ const prevState = { ...state };
+ Object.assign(state, nextState);
+ // 传递 prevState
+ if (typeof onDragEnd === 'function') onDragEnd(state, event, {
+ prevState,
+ });
+ }
+
+ return {
+ state,
+ dragStart,
+ dragMove,
+ dragEnd,
+ };
+ },
+
+ render() {
+ const children = this.$scopedSlots.default;
+
+ return (
+
+ {this.state.isDragging &&
+ (
+
+ )}
+ { typeof children === 'function' && (
+ children({
+ state: this.state,
+ dragStart: this.dragStart,
+ dragMove: this.dragMove,
+ dragEnd: this.dragEnd,
+ })
+ ) }
+
+ );
+ },
+};
+
diff --git a/webapp/src/components/Drag/index.js b/webapp/src/components/Drag/index.js
new file mode 100644
index 0000000..010364e
--- /dev/null
+++ b/webapp/src/components/Drag/index.js
@@ -0,0 +1,19 @@
+/** 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.
+* =============================================================
+*/
+
+import Drag from './drag';
+
+export default Drag;
diff --git a/webapp/src/components/Exception/index.vue b/webapp/src/components/Exception/index.vue
index 16000fd..ccbe530 100644
--- a/webapp/src/components/Exception/index.vue
+++ b/webapp/src/components/Exception/index.vue
@@ -38,19 +38,18 @@ export default {
};
diff --git a/webapp/src/components/IconFont/index.js b/webapp/src/components/IconFont/index.js
index dd71921..0070d1a 100644
--- a/webapp/src/components/IconFont/index.js
+++ b/webapp/src/components/IconFont/index.js
@@ -26,7 +26,7 @@
import create from './iconfont';
const IconFont = create({
- scriptUrl: '//at.alicdn.com/t/font_1756495_bycxbb6pz6s.js',
+ scriptUrl: '//at.alicdn.com/t/font_1756495_k4j524i5vng.js',
extraIconProps: { class: 'svg-icon' },
});
diff --git a/webapp/src/components/ImageGallery/index.vue b/webapp/src/components/ImageGallery/index.vue
index 47229c2..7023def 100644
--- a/webapp/src/components/ImageGallery/index.vue
+++ b/webapp/src/components/ImageGallery/index.vue
@@ -53,7 +53,7 @@
:class="rootClass + '__img'"
@click="onClickImg(dataImage)"
>
- {{ imageLabelTag[dataImage.id]['text'] }}
+ {{ imageLabelTag[dataImage.id]['text'] }}
handleCheck(dataImage, checked)" />
{{ basename(dataImage.url) }}
@@ -74,6 +74,7 @@
-
-
diff --git a/webapp/src/components/LogContainer/index.vue b/webapp/src/components/LogContainer/index.vue
new file mode 100644
index 0000000..6534a34
--- /dev/null
+++ b/webapp/src/components/LogContainer/index.vue
@@ -0,0 +1,125 @@
+/** 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.
+* =============================================================
+*/
+
+
+
+
+
+
diff --git a/webapp/src/components/Training/dataSourceSelector.vue b/webapp/src/components/Training/dataSourceSelector.vue
new file mode 100644
index 0000000..dd68f69
--- /dev/null
+++ b/webapp/src/components/Training/dataSourceSelector.vue
@@ -0,0 +1,243 @@
+/** 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.
+* =============================================================
+*/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 使用 OfRecord
+
+
+
+
+
diff --git a/webapp/src/components/Training/jobForm.vue b/webapp/src/components/Training/jobForm.vue
index d1ce0e1..2542f0f 100644
--- a/webapp/src/components/Training/jobForm.vue
+++ b/webapp/src/components/Training/jobForm.vue
@@ -22,28 +22,36 @@
:model="form"
:rules="rules"
label-width="120px"
- class="demo-ruleForm"
- :style="`width: ${widthPercent}%;`"
+ :style="`width: ${widthPercent}%; margin-top: 20px;`"
>
-
+
{{ form.jobName }}
-
+
-
+
-
+
- 我的算法
- 预置算法
+ 我的算法
+ 预置算法
@@ -100,60 +111,81 @@
/>
-
-
-
-
-
+
+
+
+
+
+ 我的模型
+ 预训练模型
+
+
+
-
-
-
-
- 使用 OfRecord
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- CPU
- GPU
+
+ CPU
+ GPU
+
+
+
-
-
-
+
+ {{ spec.specsName }}
-
+ :label="spec.label"
+ :value="spec.label"
+ />
+
+
+
+
+
+
+
+
+
+ 小时
+
+
+ 小时
+
+
+
+
+
+
+ {{ preview }}
+
@@ -198,26 +319,51 @@
{{ form.imageName }}
-
+
+ {{ form.modelName }}
+
+
{{ form.dataSourceName }}
+
+ {{ form.valDataSourceName }}
+
{{ form.runCommand }}
- --{{ key }}={{ form.runParams[key] }}
+ --{{ key }}={{ form.runParams[key] }}
+
+
+ {{ form.trainType === 1 ? '是' : '否' }}
+
+
+ {{ form.resourcesPoolNode }}
+
+
+ {{ delayCreateDelete ? '是' : '否' }}
+
+
+ {{ form.delayCreateTime }} 小时
+
+
+ {{ form.delayDeleteTime }} 小时
- {{ form.resourcesPoolType ? 'GPU' : 'CPU' }}
+ {{ form.resourcesPoolNode }}
- {{ formSpecs && formSpecs.specsName }}
+ {{ formSpecs && formSpecs.label }}
+
+
+
+ {{ preview }}
+
-
- 开始训练
- 清空
-
@@ -225,42 +371,79 @@
-
diff --git a/webapp/src/components/Training/paramPair.vue b/webapp/src/components/Training/paramPair.vue
new file mode 100644
index 0000000..0cd9511
--- /dev/null
+++ b/webapp/src/components/Training/paramPair.vue
@@ -0,0 +1,135 @@
+/** 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.
+* =============================================================
+*/
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/webapp/src/components/Training/runParamForm.vue b/webapp/src/components/Training/runParamForm.vue
index 0bb0e6a..2f08656 100644
--- a/webapp/src/components/Training/runParamForm.vue
+++ b/webapp/src/components/Training/runParamForm.vue
@@ -18,8 +18,8 @@
- key-value
- arguments
+ key-value
+ arguments
-
-
-
-
-
-
-
-
-
- { addP(index) }" />
- { removeP(index) }" />
-
-
-
+
import { stringIsValidPythonVariable } from '@/utils';
+import ParamPair from './paramPair';
export default {
name: 'RunParamForm',
+ components: { ParamPair },
props: {
- id: {
- type: [Number, String],
- default: null,
- },
runParamObj: {
type: Object,
- default: () => {},
+ default: () => ({}),
},
prop: {
type: String,
default: null,
},
- input1Width: {
- type: Number,
- default: 150,
- },
- input2Width: {
- type: Number,
- default: 150,
- },
paramLabelWidth: {
type: String,
default: '100px',
@@ -110,73 +83,34 @@ export default {
},
},
data() {
+ const isInputEmpty = value => {
+ return value === '' || value === null;
+ };
+
+ const keyValidator = (rule, value, callback) => {
+ if (!isInputEmpty(value) && !stringIsValidPythonVariable(value)) {
+ callback(new Error('参数key必须是合法变量名'));
+ } else {
+ callback();
+ }
+ };
+
return {
runParamsList: [],
- errMsg: [],
paramsMode: 1,
paramsArguments: '',
argErrorMsg: null,
- validateKey: (callback, item, index) => {
- // 先校验是不是都为空,若都为空则通过
- const isEmptyKey = this.isInputEmpty(item.key);
- const isEmptyValue = this.isInputEmpty(item.value);
- if (isEmptyKey && isEmptyValue) {
- // 可能之前value有校验错误信息
- this.$refs[this.itemValueId(index)][0].form.clearValidate(this.itemValueId(index));
- callback();
- return;
- }
- // 再校验自己是不是合法的变量名
- if (!stringIsValidPythonVariable(item.key)) {
- callback(new Error('参数key必须是合法变量名'));
- return;
- }
- // 然后和value联合校验
- if (isEmptyKey) {
- callback(new Error('请输入参数key'));
- return;
- } if (isEmptyValue) {
- this.$refs.runParamForm.validateField(this.itemValueId(index));
- } else {
- callback();
-
- }
- },
- validateValue: (callback, item, index) => {
- // 先校验是不是都为空,若都为空则通过
- const isEmptyKey = this.isInputEmpty(item.key);
- const isEmptyValue = this.isInputEmpty(item.value);
- if (isEmptyKey && isEmptyValue) {
- // 可能之前key有校验错误信息
- this.$refs[this.itemKeyId(index)][0].form.clearValidate(this.itemKeyId(index));
- callback();
- return;
- }
- // 输入框格式保证了其类似一定是字符串,只需不传空即可
- // 然后和key联合校验
- if (isEmptyValue) {
- callback();
- return;
- } if (isEmptyKey) {
- this.$refs.runParamForm.validateField(this.itemKeyId(index));
- } else {
- callback();
-
- }
- },
+ // 整体校验规则:对 key 做 python 变量名有效性校验,对 value 不做任何校验
+ keyRule: [{
+ validator: keyValidator,
+ trigger: 'blur',
+ }],
+ paramId: 0,
+ paramRepeatWarning: null,
+ hasError: false,
};
},
watch: {
- id(newValue) {
- if (newValue === null || isNaN(newValue)) {
- /**
- * newValue为null时的一种情况是与el-form组合使用的
- * crud组件触发了cancelCU方法,此时不需更新
- */
- return;
- }
- this.syncListData();
- },
runParamObj() {
this.syncListData();
},
@@ -185,19 +119,12 @@ export default {
this.syncListData();
},
methods: {
- isInputEmpty(value) {
- return value === '' || value === null;
- },
- itemKeyId(index) {
- return `runParamsList.${ index }.key`;
- },
- itemValueId(index) {
- return `runParamsList.${ index }.value`;
- },
addP() {
this.runParamsList.push({
key: '',
value: '',
+ // eslint-disable-next-line no-plusplus
+ id: this.paramId++,
});
},
removeP(i) {
@@ -205,12 +132,17 @@ export default {
this.updateRunParamObj();
},
syncListData() {
- const rpObj = { ...this.runParamObj};
const list = [];
- for (const formKey in rpObj) {
- list.push({
- key: formKey,
- value: typeof (rpObj[formKey]) === 'object' ? JSON.stringify(rpObj[formKey]) : rpObj[formKey],
+ for (const key in this.runParamObj) {
+ const objItem = this.runParamsList.find(p => p.key === key);
+ if (objItem) {
+ objItem.value = this.runParamObj[key];
+ }
+ list.push(objItem || {
+ key,
+ value: this.runParamObj[key],
+ // eslint-disable-next-line no-plusplus
+ id: this.paramId++,
});
}
this.runParamsList = list;
@@ -221,33 +153,59 @@ export default {
this.convertPairsToArgs();
}
},
- handleChange() {
+ handleChange(paramPair) {
+ // 当参数对的值改变时 key 为空,则把对于的 param 删除
+ if (!paramPair.key) {
+ const paramIndex = this.runParamsList.findIndex(p => p.id === paramPair.id);
+ this.runParamsList.splice(paramIndex, 1);
+ }
+ if (!this.runParamsList.length) {
+ this.addP();
+ }
this.updateRunParamObj();
},
+ // 提供修改参数的入口, 如果参数存在则可修改
+ updateParam(key, value) {
+ const param = this.runParamsList.find(p => p.key === key);
+ if (param) {
+ param.value = value;
+ this.updateRunParamObj();
+ }
+ },
updateRunParamObj() {
const obj = {};
- this.runParamsList.forEach(d => {
- if (d.key === '') return;
- obj[d.key] = d.value;
+ const repeatedParams = new Set();
+ this.runParamsList.forEach(param => {
+ // 当 key 为空或者已存在相同 key 时,不加入数值
+ if (!param.key) {
+ return;
+ }
+ if (obj[param.key] !== undefined) {
+ repeatedParams.add(param.key);
+ return;
+ }
+ obj[param.key] = param.value;
});
+ if (repeatedParams.size) {
+ this.paramRepeatWarning && this.paramRepeatWarning.close();
+ this.paramRepeatWarning = this.$message.warning(`参数 ${[...repeatedParams].join(', ')} 有重复, 将取用第一个值。`);
+ }
this.$emit('updateRunParams', obj);
},
- goValid() {
+ validate() {
// 单独校验
let valid = true;
- this.errMsg = [];
- this.runParamsList.forEach((item, index) => {
- if (this.isInputEmpty(item.key)) {
- if (!this.isInputEmpty(item.value)) {
- valid = false;
- }
- } else if (!stringIsValidPythonVariable(item.key)) {
- valid = false;
- this.$nextTick(() => {
- this.errMsg[index] = '参数key必须是合法变量名';
- });
- }
- });
+ const validCallback = pairValid => {
+ valid = valid && pairValid;
+ };
+
+ // eslint-disable-next-line no-plusplus
+ for (let i = 0; i < this.runParamsList.length; i++) {
+ this.paramsMode === 1 && this.$refs.paramPairs[i].validate(validCallback);
+ };
+
+ valid = valid && !this.hasError;
+
return valid;
},
onParamsModeChange(value) {
@@ -265,33 +223,41 @@ export default {
const paramsList = this.paramsArguments.split(' ');
const pairList = [];
const re = /^--(.+)=(.*)$/;
+ this.hasError = false;
+ // 先使用正则进行匹配
paramsList.forEach(arg => {
const group = re.exec(arg);
if (group) {
pairList.push({
key: group[1],
value: group[2],
+ // eslint-disable-next-line no-plusplus
+ id: this.paramId++,
});
} else if (arg) {
this.$nextTick(() => {
this.argErrorMsg = `参数'${arg}'不合法,请检查运行参数`;
});
this.paramsMode = 2;
-
+ this.hasError = true;
}
});
+ if (this.hasError) return;
+ // 其次做参数名验证
pairList.forEach(pair => {
if (!stringIsValidPythonVariable(pair.key)) {
this.$nextTick(() => {
this.argErrorMsg = `参数名'${pair.key}'不是合法参数,请检查运行参数`;
});
this.paramsMode = 2;
-
+ this.hasError = true;
}
});
+ if (this.hasError) return;
// 参数为空时增加一个空参数
if (!pairList.length) {
- pairList.push({ key: '', value: '' });
+ // eslint-disable-next-line no-plusplus
+ pairList.push({ key: '', value: '', id: this.paramId++ });
}
this.runParamsList = pairList;
this.updateRunParamObj();
@@ -308,13 +274,20 @@ export default {
this.paramsArguments = args;
},
reset() {
- this.errMsg = [];
this.argErrorMsg = null;
this.paramsMode = 1;
this.paramsArguments = '';
- this.runParamsList = [{ key: '', value: '' }];
- this.$refs.runParamForm.clearValidate();
+ // eslint-disable-next-line no-plusplus
+ this.runParamsList = [{ key: '', value: '', id: this.paramId++ }];
},
},
};
+
\ No newline at end of file
diff --git a/webapp/src/components/Training/saveModelDialog.vue b/webapp/src/components/Training/saveModelDialog.vue
index 8a5bb9c..9f2daec 100644
--- a/webapp/src/components/Training/saveModelDialog.vue
+++ b/webapp/src/components/Training/saveModelDialog.vue
@@ -33,7 +33,7 @@
-
+
新建模型
@@ -210,6 +210,12 @@ export default {
this.createAlgorithmUsage(value);
}
},
+ formatVersion(item) {
+ if (item.versionNum) {
+ return `${item.name} (V${(Number(item.versionNum.substr(1)) + 1).toString().padStart(4, '0')})`;
+ }
+ return `${item.name} (V0001)`;
+ },
// op
doSaveModel() {
this.$refs.modelForm.validate(valid => {
diff --git a/webapp/src/components/UploadForm/form.js b/webapp/src/components/UploadForm/form.js
index e59ae52..65d45cb 100644
--- a/webapp/src/components/UploadForm/form.js
+++ b/webapp/src/components/UploadForm/form.js
@@ -43,16 +43,12 @@ export default {
},
limit: {
type: Number,
- default: 1000,
+ default: 5000,
},
showFileCount: {
type: Boolean,
default: true,
},
- wordShow: {
- type: Boolean,
- default: true,
- },
},
data() {
return {
@@ -130,6 +126,7 @@ export default {
},
onRemove(file, fileList) {
this.lenOfFileList = fileList.length;
+ this.$attrs['on-remove'] && this.$attrs['on-remove'](file, fileList);
},
cancelUpload() {
if (this.source) {
@@ -163,7 +160,7 @@ export default {
class='upload-field'
limit={this.limit}
multiple
- list-type='picture'
+ list-type={this.lenOfFileList>100? 'text' : 'picture'}
auto-upload={false}
disabled={this.uploading}
{...uploadProps}
@@ -180,7 +177,7 @@ export default {
{
this.showFileCount && (
- this.wordShow ? 已选择{ this.lenOfFileList }张 : null
+ 已选择{ this.lenOfFileList }张
)
}
diff --git a/webapp/src/components/UploadForm/inline.vue b/webapp/src/components/UploadForm/inline.vue
index 6282309..29f3b20 100644
--- a/webapp/src/components/UploadForm/inline.vue
+++ b/webapp/src/components/UploadForm/inline.vue
@@ -69,7 +69,7 @@ export default {
}
state.uploading = true;
- ctx.emit('uploadStart');
+ ctx.emit('uploadStart', files);
const uploadReqeust = request || minIOUpload;
// 开始调用上传接口
return uploadReqeust({ ...props.params, fileList: renameFileList, transformFile }, callback)
diff --git a/webapp/src/components/UploadProgress/index.vue b/webapp/src/components/UploadProgress/index.vue
new file mode 100644
index 0000000..8bbe26e
--- /dev/null
+++ b/webapp/src/components/UploadProgress/index.vue
@@ -0,0 +1,93 @@
+/** 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.
+* =============================================================
+*/
+
+// 仅支持line-upload上传文件,线性进度条
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webapp/src/components/svg/brush/Brush.js b/webapp/src/components/svg/brush/Brush.js
new file mode 100644
index 0000000..e5d7c2a
--- /dev/null
+++ b/webapp/src/components/svg/brush/Brush.js
@@ -0,0 +1,217 @@
+/** 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.
+* =============================================================
+*/
+
+import cx from 'classnames';
+import Vue from 'vue';
+import { reactive } from '@vue/composition-api';
+
+import Drag from '@/components/Drag';
+import Group from '../group';
+import BrushSelection from './BrushSelection';
+
+export default {
+ name: 'Brush',
+ components: {
+ Group,
+ },
+ props: {
+ stageWidth: Number,
+ stageHeight: Number,
+ className: String,
+ onBrushStart: Function,
+ onBrushMove: Function,
+ onBrushEnd: Function,
+ transformZoom: Function,
+ left: {
+ type: Number,
+ default: 0,
+ },
+ top: {
+ type: Number,
+ default: 0,
+ },
+ brushSelectionStyle: {
+ type: Object,
+ default: () => ({
+ fill: 'rgba(102, 181, 245, 0.1)',
+ stroke: 'rgba(102, 181, 245, 1)',
+ strokeWidth: 1,
+ }),
+ },
+ },
+
+ setup(props){
+ const { onBrushStart, onBrushMove, left, top, onChange, onBrushEnd, transformZoom } = props;
+ const state = reactive({
+ start: { x: 0, y: 0 },
+ end: { x: 0, y: 0 },
+ extent: { x0: 0, x1: 0, y0: 0, y1: 0 },
+ isBrushing: false,
+ });
+ const getWidth = () => {
+ return Math.abs(state.extent.x1 - state.extent.x0);
+ };
+
+ const getHeight = () => {
+ return Math.abs(state.extent.y1 - state.extent.y0);
+ };
+
+ const getExtent = (start, end) => {
+ const x0 = Math.min(start.x, end.x);
+ const x1 = Math.max(start.x, end.x);
+ const y0 = Math.min(start.y, end.y);
+ const y1 = Math.max(start.y, end.y);
+
+ return {
+ x0,
+ x1,
+ y0,
+ y1,
+ };
+ };
+
+ const update = (updater, callback) => {
+ Object.assign(state, updater(state));
+ Vue.nextTick(() => {
+ if(callback) {
+ callback(state);
+ }
+ if(onChange) {
+ onChange(state);
+ }
+ });
+ };
+
+ const handleDragStart = (draw, event) => {
+ const start = transformZoom({
+ x: draw.x + draw.dx - left,
+ y: draw.y + draw.dy - top,
+ });
+ if (onBrushStart) {
+ onBrushStart(start, event);
+ }
+
+ update(prevBrush => ({
+ ...prevBrush,
+ start,
+ end: undefined,
+ extent: {
+ x0: -1,
+ x1: -1,
+ y0: -1,
+ y1: -1,
+ },
+ isBrushing: true,
+ }));
+ };
+
+ const handleDragMove = (draw, event) => {
+ if (!draw.isDragging) return;
+ const end = transformZoom({
+ x: draw.x + draw.dx - left,
+ y: draw.y + draw.dy - top,
+ });
+
+ update(prevBrush => {
+ const { start } = prevBrush;
+ const extent = getExtent(start, end);
+ return {
+ ...prevBrush,
+ end,
+ extent,
+ };
+ }, (nextState) => {
+ // 回调
+ typeof onBrushMove === 'function' && onBrushMove(nextState, event);
+ });
+ };
+
+
+ const handleDragEnd = (draw, event, options = {}) => {
+ update(prevBrush => ({
+ ...prevBrush,
+ isBrushing: false,
+ }), state => onBrushEnd(state, event, options));
+ };
+
+ return {
+ state,
+ getWidth,
+ getHeight,
+ update,
+ handleDragStart,
+ handleDragMove,
+ handleDragEnd,
+ getExtent,
+ };
+ },
+
+ render(h) {
+ const { stageWidth, stageHeight, className, left, top, brushSelectionStyle } = this;
+ const { start, end, isBrushing } = this.state;
+
+ const width = this.getWidth();
+ const height = this.getHeight();
+
+ const dragProps = {
+ props: {
+ width: stageWidth,
+ height: stageHeight,
+ resetOnStart: true,
+ onDragStart: this.handleDragStart,
+ onDragMove: this.handleDragMove,
+ onDragEnd: this.handleDragEnd,
+ },
+ };
+
+ return (
+
+ {/* overlay */}
+
+ {
+ (drag) => (
+
+ )
+ }
+
+ {start && end && !!isBrushing && (
+
+
+
+ )}
+
+ );
+ },
+};
diff --git a/webapp/src/components/svg/brush/BrushCorner.js b/webapp/src/components/svg/brush/BrushCorner.js
new file mode 100644
index 0000000..9aa3d01
--- /dev/null
+++ b/webapp/src/components/svg/brush/BrushCorner.js
@@ -0,0 +1,227 @@
+/** 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.
+* =============================================================
+*/
+
+import Drag from '@/components/Drag';
+import { chroma } from '@/utils';
+
+export default {
+ name: 'BrushCorner',
+ props: {
+ annotate: Object,
+ transformer: Object,
+ currentAnnotationId: String,
+ stageWidth: Number,
+ stageHeight: Number,
+ type: String,
+ scale: {
+ type: Number,
+ default: 1,
+ },
+ x: Number,
+ y: Number,
+ width: Number,
+ height: Number,
+ handleBrushStart: Function,
+ updateBrush: Function,
+ updateBrushEnd: Function,
+ getZoom: Function,
+ },
+
+ setup(props) {
+ const { updateBrush, updateBrushEnd, type, scale, handleBrushStart, getZoom } = props;
+
+ const handleDragStart = (drag, event) => {
+ // 开始拖拽是选中当前标注
+ if(handleBrushStart) {
+ handleBrushStart(drag, event);
+ }
+ };
+
+ const handleDragMove = (drag) => {
+ if (!drag.isDragging) return;
+ const { zoom } = getZoom();
+ updateBrush(prevBrush => {
+ const { start, end } = prevBrush;
+ let nextState = {};
+
+ let moveX = 0;
+ let moveY = 0;
+
+ const _scale = scale * zoom;
+
+ const xMax = Math.max(start.x, end.x);
+ const xMin = Math.min(start.x, end.x);
+ const yMax = Math.max(start.y, end.y);
+ const yMin = Math.min(start.y, end.y);
+
+ switch (type) {
+ case 'topRight':
+ moveX = xMax + drag.dx / _scale;
+ moveY = yMin + drag.dy / _scale;
+
+ nextState = {
+ ...prevBrush,
+ activeHandle: type,
+ extent: {
+ ...prevBrush.extent,
+ x0: Math.max(Math.min(moveX, start.x), prevBrush.bounds.x0),
+ x1: Math.min(Math.max(moveX, start.x), prevBrush.bounds.x1),
+ y0: Math.max(Math.min(moveY, end.y), prevBrush.bounds.y0),
+ y1: Math.min(Math.max(moveY, end.y), prevBrush.bounds.y1),
+ },
+ };
+ break;
+ case 'topLeft':
+ moveX = xMin + drag.dx / _scale;
+ moveY = yMin + drag.dy / _scale;
+
+ nextState = {
+ ...prevBrush,
+ activeHandle: type,
+ extent: {
+ ...prevBrush.extent,
+ x0: Math.max(Math.min(moveX, end.x), prevBrush.bounds.x0),
+ x1: Math.min(Math.max(moveX, end.x), prevBrush.bounds.x1),
+ y0: Math.max(Math.min(moveY, end.y), prevBrush.bounds.y0),
+ y1: Math.min(Math.max(moveY, end.y), prevBrush.bounds.y1),
+ },
+ };
+ break;
+ case 'bottomLeft':
+ moveX = xMin + drag.dx / _scale;
+ moveY = yMax + drag.dy / _scale;
+
+ nextState = {
+ ...prevBrush,
+ activeHandle: type,
+ extent: {
+ ...prevBrush.extent,
+ x0: Math.max(Math.min(moveX, end.x), prevBrush.bounds.x0),
+ x1: Math.min(Math.max(moveX, end.x), prevBrush.bounds.x1),
+ y0: Math.max(Math.min(moveY, start.y), prevBrush.bounds.y0),
+ y1: Math.min(Math.max(moveY, start.y), prevBrush.bounds.y1),
+ },
+ };
+ break;
+ case 'bottomRight':
+ moveX = xMax + drag.dx / _scale;
+ moveY = yMax + drag.dy / _scale;
+ nextState = {
+ ...prevBrush,
+ activeHandle: type,
+ extent: {
+ ...prevBrush.extent,
+ x0: Math.max(Math.min(moveX, start.x), prevBrush.bounds.x0),
+ x1: Math.min(Math.max(moveX, start.x), prevBrush.bounds.x1),
+ y0: Math.max(Math.min(moveY, start.y), prevBrush.bounds.y0),
+ y1: Math.min(Math.max(moveY, start.y), prevBrush.bounds.y1),
+ },
+ };
+ break;
+ default:
+ break;
+ }
+ return nextState;
+ });
+ };
+
+ const handleDragEnd = () => {
+ updateBrushEnd(prevBrush => {
+ const { start, end, extent } = { ...prevBrush };
+ start.x = Math.min(extent.x0, extent.x1);
+ start.y = Math.min(extent.y0, extent.y0);
+ end.x = Math.max(extent.x0, extent.x1);
+ end.y = Math.max(extent.y0, extent.y1);
+ const nextBrush = {
+ ...prevBrush,
+ start,
+ end,
+ activeHandle: undefined,
+ isBrushing: false,
+ domain: {
+ x0: Math.min(start.x, end.x),
+ x1: Math.max(start.x, end.x),
+ y0: Math.min(start.y, end.y),
+ y1: Math.max(start.y, end.y),
+ },
+ };
+ return nextBrush;
+ });
+ };
+
+ return {
+ handleDragStart,
+ handleDragMove,
+ handleDragEnd,
+ };
+ },
+
+ render(h) {
+ const { annotate, transformer, currentAnnotationId, stageWidth, stageHeight, type, x, y, width, height } = this;
+
+ const cursor = type === 'topLeft' || type === 'bottomRight' ? 'nwse-resize' : 'nesw-resize';
+
+ let transform = null;
+ if(annotate.id === transformer.id) {
+ transform = `translate(${transformer.dx}, ${transformer.dy})`;
+ }
+
+ const { data = {} } = annotate;
+ const { color } = data;
+ const defaultFill = 'rgba(102, 181, 245, 0.1)';
+ const bgColor = color || defaultFill;
+ const isActive = currentAnnotationId === annotate.id;
+ const colorAlpha = isActive ? 1 : 0;
+ const fillColor = chroma(bgColor).alpha(colorAlpha);
+
+ const dragProps = {
+ props: {
+ width: stageWidth,
+ height: stageHeight,
+ resetOnStart: true,
+ onDragStart: this.handleDragStart,
+ onDragMove: this.handleDragMove,
+ onDragEnd: this.handleDragEnd,
+ },
+ };
+
+ const style = {
+ cursor,
+ };
+
+ return (
+
+ {
+ (drag) => (
+
+ )
+ }
+
+ );
+ },
+};
diff --git a/webapp/src/components/svg/brush/BrushHandle.js b/webapp/src/components/svg/brush/BrushHandle.js
new file mode 100644
index 0000000..d12d456
--- /dev/null
+++ b/webapp/src/components/svg/brush/BrushHandle.js
@@ -0,0 +1,193 @@
+/** 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.
+* =============================================================
+*/
+
+import Drag from '@/components/Drag';
+
+export default {
+ name: 'BrushHandle',
+ props: {
+ stageWidth: Number,
+ stageHeight: Number,
+ type: String,
+ scale: {
+ type: Number,
+ default: 1,
+ },
+ handle: {
+ type: Object,
+ default: () => ({ x: 0, y: 0, width: 0, height: 0 }),
+ },
+ handleBrushStart: Function,
+ updateBrush: Function,
+ updateBrushEnd: Function,
+ getZoom: Function,
+ },
+
+ // todo: 鼠标离开画布没有释放
+ setup(props) {
+ const { updateBrush, updateBrushEnd, type, scale, handleBrushStart, getZoom } = props;
+
+ const handleDragStart = (drag, event) => {
+ // 开始拖拽是选中当前标注
+ if(handleBrushStart) {
+ handleBrushStart(drag, event);
+ }
+ };
+
+ const handleDragMove = (drag) => {
+ if (!drag.isDragging) return;
+ const { zoom } = getZoom();
+ updateBrush(prevBrush => {
+ const { start, end } = prevBrush;
+ let nextState = {};
+ let move = 0;
+ const _scale = scale * zoom;
+
+ const xMax = Math.max(start.x, end.x);
+ const xMin = Math.min(start.x, end.x);
+ const yMax = Math.max(start.y, end.y);
+ const yMin = Math.min(start.y, end.y);
+
+ switch (type) {
+ case 'right':
+ move = xMax + drag.dx / _scale;
+ nextState = {
+ ...prevBrush,
+ activeHandle: type,
+ extent: {
+ ...prevBrush.extent,
+ x0: Math.max(Math.min(move, start.x), prevBrush.bounds.x0),
+ x1: Math.min(Math.max(move, start.x), prevBrush.bounds.x1),
+ },
+ };
+ break;
+ case 'left':
+ move = xMin + drag.dx / _scale;
+ nextState = {
+ ...prevBrush,
+ activeHandle: type,
+ extent: {
+ ...prevBrush.extent,
+ x0: Math.min(move, end.x),
+ x1: Math.max(move, end.x),
+ },
+ };
+ break;
+ case 'top':
+ move = yMin + drag.dy / _scale;
+ nextState = {
+ ...prevBrush,
+ activeHandle: type,
+ extent: {
+ ...prevBrush.extent,
+ y0: Math.min(move, end.y),
+ y1: Math.max(move, end.y),
+ },
+ };
+ break;
+ case 'bottom':
+ move = yMax + drag.dy / _scale;
+ nextState = {
+ ...prevBrush,
+ activeHandle: type,
+ extent: {
+ ...prevBrush.extent,
+ y0: Math.min(move, start.y),
+ y1: Math.max(move, start.y),
+ },
+ };
+ break;
+ default:
+ break;
+ }
+ return nextState;
+ });
+ };
+
+ const handleDragEnd = () => {
+ updateBrushEnd(prevBrush => {
+ const { start, end, extent } = { ...prevBrush };
+ start.x = Math.min(extent.x0, extent.x1);
+ start.y = Math.min(extent.y0, extent.y0);
+ end.x = Math.max(extent.x0, extent.x1);
+ end.y = Math.max(extent.y0, extent.y1);
+ const nextBrush = {
+ ...prevBrush,
+ start,
+ end,
+ activeHandle: undefined,
+ isBrushing: false,
+ domain: {
+ x0: Math.min(start.x, end.x),
+ x1: Math.max(start.x, end.x),
+ y0: Math.min(start.y, end.y),
+ y1: Math.max(start.y, end.y),
+ },
+ };
+ return nextBrush;
+ });
+ };
+
+ return {
+ handleDragStart,
+ handleDragMove,
+ handleDragEnd,
+ };
+ },
+
+ render(h) {
+ const { stageWidth, stageHeight, handle, type } = this;
+ const { x, y, width, height } = handle;
+
+ const cursor = type === 'right' || type === 'left' ? 'ew-resize' : 'ns-resize';
+
+ const dragProps = {
+ props: {
+ width: stageWidth,
+ height: stageHeight,
+ resetOnStart: true,
+ onDragStart: this.handleDragStart,
+ onDragMove: this.handleDragMove,
+ onDragEnd: this.handleDragEnd,
+ },
+ };
+
+ const style = {
+ cursor,
+ };
+
+ return (
+
+ {
+ (drag) => (
+
+ )
+ }
+
+ );
+ },
+};
diff --git a/webapp/src/components/svg/brush/BrushSelection.js b/webapp/src/components/svg/brush/BrushSelection.js
new file mode 100644
index 0000000..eeca76c
--- /dev/null
+++ b/webapp/src/components/svg/brush/BrushSelection.js
@@ -0,0 +1,55 @@
+/** 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.
+* =============================================================
+*/
+
+export default {
+ name: 'BrushSelection',
+ props: {
+ stageWidth: Number,
+ stageHeight: Number,
+ width: Number,
+ height: Number,
+ updateBrush: Function,
+ brush: Object,
+ onBrushStart: Function,
+ onBrushEnd: Function,
+ disableDraggingSelection: {
+ type: Boolean,
+ default: false,
+ },
+ selectionStyle: {
+ type: Object,
+ },
+ },
+
+ render(h) {
+ const { width, height, brush, disableDraggingSelection, selectionStyle } = this;
+
+ return (
+
+ );
+ },
+};
diff --git a/webapp/src/components/svg/brush/index.js b/webapp/src/components/svg/brush/index.js
new file mode 100644
index 0000000..57a5e31
--- /dev/null
+++ b/webapp/src/components/svg/brush/index.js
@@ -0,0 +1,19 @@
+/** 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.
+* =============================================================
+*/
+
+export { default as Brush } from './Brush';
+export { default as BrushHandle } from './BrushHandle';
+export { default as BrushCorner } from './BrushCorner';
diff --git a/webapp/src/components/svg/group/index.js b/webapp/src/components/svg/group/index.js
new file mode 100644
index 0000000..e4037e4
--- /dev/null
+++ b/webapp/src/components/svg/group/index.js
@@ -0,0 +1,42 @@
+/** 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.
+* =============================================================
+*/
+
+import cx from 'classnames';
+
+export default {
+ name: 'Group',
+ functional: true,
+ render(h, context) {
+ const { props, children } = context;
+ const {
+ top = 0,
+ left = 0,
+ transform,
+ className,
+ ...otherProps
+ } = props;
+
+ return (
+
+ {children}
+
+ );
+ },
+};
diff --git a/webapp/src/components/svg/index.js b/webapp/src/components/svg/index.js
new file mode 100644
index 0000000..f2b4568
--- /dev/null
+++ b/webapp/src/components/svg/index.js
@@ -0,0 +1,18 @@
+/** 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.
+* =============================================================
+*/
+
+export { default as Group } from './group';
+export * from './brush';
diff --git a/webapp/src/config/index.js b/webapp/src/config/index.js
index 406f934..6255f1c 100644
--- a/webapp/src/config/index.js
+++ b/webapp/src/config/index.js
@@ -18,7 +18,7 @@ module.exports = {
minIO: {
development: {
config: {
- endPoint: '10.5.26.234',
+ endPoint: '', // MinIO 服务地址
port: 9000,
useSSL: false,
},
@@ -26,7 +26,7 @@ module.exports = {
},
test: {
config: {
- endPoint: '10.5.26.234',
+ endPoint: '',
port: 9000,
useSSL: false,
},
@@ -34,7 +34,7 @@ module.exports = {
},
production: {
config: {
- endPoint: '121.41.72.89',
+ endPoint: '',
port: 9000,
useSSL: false,
},
diff --git a/webapp/src/hooks/brush/useBrush.js b/webapp/src/hooks/brush/useBrush.js
index 25e45e4..567d1e8 100644
--- a/webapp/src/hooks/brush/useBrush.js
+++ b/webapp/src/hooks/brush/useBrush.js
@@ -21,26 +21,53 @@ function useBrush() {
const state = reactive({
start: undefined,
end: undefined,
+ extent: undefined,
isBrushing: false,
});
+ function getExtent(start, end) {
+ const x0 = Math.min(start.x, end.x);
+ const x1 = Math.max(start.x, end.x);
+ const y0 = Math.min(start.y, end.y);
+ const y1 = Math.max(start.y, end.y);
+
+ return {
+ x0,
+ x1,
+ y0,
+ y1,
+ };
+ }
+
function onBrushStart({ x, y }) {
Object.assign(state, {
start: { x, y },
isBrushing: true,
end: undefined,
+ extent: undefined,
});
}
function onBrushMove({ x, y }) {
+ const extent = getExtent(state.start, {x, y});
Object.assign(state, {
end: { x, y },
+ extent,
});
}
function onBrushEnd() {
+ const { extent } = state;
Object.assign(state, {
isBrushing: false,
+ start: {
+ x: extent.x0,
+ y: extent.y0,
+ },
+ end: {
+ x: extent.x1,
+ y: extent.y1,
+ },
});
}
@@ -48,15 +75,26 @@ function useBrush() {
Object.assign(state, {
start: undefined,
end: undefined,
+ extent: undefined,
isBrushing: false,
});
}
+ function updateBrush(updater, callback) {
+ const newState = updater(state);
+ Object.assign(state, newState);
+ if(typeof callback === 'function') {
+ callback(state);
+ }
+ }
+
return ({
brush: state,
+ getExtent,
onBrushStart,
onBrushMove,
onBrushEnd,
+ updateBrush,
onBrushReset,
});
}
diff --git a/webapp/src/hooks/zoom/useZoom.js b/webapp/src/hooks/zoom/useZoom.js
index 25356ec..e655ab8 100644
--- a/webapp/src/hooks/zoom/useZoom.js
+++ b/webapp/src/hooks/zoom/useZoom.js
@@ -66,8 +66,13 @@ function useZoom(initialZoom, wrapperRef, options = {
updateZoom({ newZoom: 1, zoom: 1, zoomX: 0, zoomY: 0 });
}
+ function getZoom(){
+ return state;
+ }
+
return ({
zoom: state,
+ getZoom,
setZoom,
zoomIn,
zoomOut,
diff --git a/webapp/src/layout/BaseLayout.vue b/webapp/src/layout/BaseLayout.vue
index 7f6421d..9bfd700 100644
--- a/webapp/src/layout/BaseLayout.vue
+++ b/webapp/src/layout/BaseLayout.vue
@@ -91,7 +91,8 @@ export default {
} else {
// 不存在历史记录
// 或者新开 Tab
- if (!window.history.length || window.history.length === 1) {
+ // chrome 新开tab页面历史记录为 2
+ if (!window.history.length || window.history.length <= 2) {
this.$router.push('/');
return;
}
diff --git a/webapp/src/layout/components/AppMain/index.vue b/webapp/src/layout/components/AppMain/index.vue
index eb8ef3e..367ea94 100644
--- a/webapp/src/layout/components/AppMain/index.vue
+++ b/webapp/src/layout/components/AppMain/index.vue
@@ -59,6 +59,7 @@ export default {
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;
}
diff --git a/webapp/src/layout/components/Feedback/index.vue b/webapp/src/layout/components/Feedback/index.vue
index b5b34e8..40fa179 100644
--- a/webapp/src/layout/components/Feedback/index.vue
+++ b/webapp/src/layout/components/Feedback/index.vue
@@ -33,15 +33,15 @@
-
+
diff --git a/webapp/src/store/modules/dataset.js b/webapp/src/store/modules/dataset.js
index fc9505a..a45b239 100644
--- a/webapp/src/store/modules/dataset.js
+++ b/webapp/src/store/modules/dataset.js
@@ -20,6 +20,7 @@
const state = {
activePanel: 0,
+ activePanelLabelGroup: 0,
};
const mutations = {
@@ -29,6 +30,12 @@ const mutations = {
RESET_PANEL: (state) => {
state.activePanel = 0;
},
+ TOGGLE_PANEL_LABEL_GROUP: (state, panel) => {
+ state.activePanelLabelGroup = panel;
+ },
+ RESET_PANEL_LABEL_GROUP: (state) => {
+ state.activePanelLabelGroup = 0;
+ },
};
const actions = {
@@ -38,6 +45,12 @@ const actions = {
resetPanel({ commit }) {
commit('RESET_PANEL');
},
+ togglePanelLabelGroup({ commit }, panel) {
+ commit('TOGGLE_PANEL_LABEL_GROUP', panel);
+ },
+ resetPanelLabelGroup({ commit }) {
+ commit('RESET_PANEL_LABEL_GROUP');
+ },
};
export default {
diff --git a/webapp/src/utils/base.js b/webapp/src/utils/base.js
index d5730b1..adc8772 100644
--- a/webapp/src/utils/base.js
+++ b/webapp/src/utils/base.js
@@ -15,9 +15,17 @@
*/
import { format, parseISO, isDate } from 'date-fns';
-import { isEqual, isPlainObject } from 'lodash';
+import { isEqual, isPlainObject, isNil, findIndex, findLastIndex } from 'lodash';
import { nanoid } from 'nanoid';
+const chroma = require('chroma-js');
+
+export const duplicate = (arr, callback) => {
+ const index = findIndex(arr, callback);
+ const lastIndex = findLastIndex(arr, callback);
+ return index !== lastIndex;
+};
+
// 合并多个属性
export function mergeProps(...args) {
const props = {};
@@ -144,3 +152,19 @@ export const identity = d => d;
export const isEqualByProp = (arr1, arr2, prop) => {
return isEqual(arr1.map(d => d[prop]), arr2.map(d => d[prop]));
};
+
+// 根据背景色深浅来设置颜色
+export const colorByLuminance = (color) => {
+ if(isNil(color) || color === '') {
+ return '#333';
+ }
+ const colorMap = {
+ dark: '#333',
+ light: '#fff',
+ };
+ const luminance = chroma(color).luminance();
+ const theme = luminance < 0.5 ? 'light' : 'dark';
+ return colorMap[theme];
+};
+
+export { chroma };
diff --git a/webapp/src/utils/event.js b/webapp/src/utils/event.js
index c89e9c4..a5c80cd 100644
--- a/webapp/src/utils/event.js
+++ b/webapp/src/utils/event.js
@@ -25,10 +25,10 @@ export const getCursorPosition = (el, event, options = {}) => {
};
// 根据 d3-zoom 获取缩放后的相对位置
-export const getZoomPosition = (el, originPosition = []) => {
+export const getZoomPosition = (el, {x, y}) => {
const transform = zoomTransform(el);
// const invertPosition = transform.invert(originPosition)
- return [originPosition[0] / transform.k, originPosition[1] / transform.k];
+ return { x: x / transform.k, y: y / transform.k };
// return invertPosition
};
@@ -60,6 +60,22 @@ export const generateBbox = (brush) => {
};
};
+// Bbox 转为 extent
+export const bbox2Extent = bbox => ({
+ x0: bbox.x,
+ y0: bbox.y,
+ x1: bbox.x + bbox.width,
+ y1: bbox.y + bbox.height,
+});
+
+// 将 extent 转为 bbox
+export const extent2Bbox = extent => ({
+ x: extent.x0,
+ y: extent.y0,
+ width: extent.x1 - extent.x0,
+ height: extent.y1 - extent.y0,
+});
+
// 解析bbox
export const parseBbox = (bbox = []) => {
if (!bbox.length) return null;
@@ -97,3 +113,32 @@ export function getStyle(el, property) {
.getPropertyValue(property)
.replace('px', '');
}
+
+/**
+ * 向上找到原始 svg 元素
+ * @param {[type]} node [节点]
+ * @param {[type]} event [事件对象]
+ */
+// eslint-disable-next-line
+export const findAncestorSvg = (node, event) => {
+ // 检测是否有参数传入
+ if (!node) return null;
+
+ // 如果只有一个参数
+ if (node.target) {
+ event = null;
+ // 当前元素的 svg 包裹元素
+ node = node.target.ownerSVGElement;
+ }
+ // 向上一直遍历,直到找到 svg 元素
+ while (node.ownerSVGElement) {
+ node = node.ownerSVGElement;
+ }
+
+ return node;
+};
+
+export const raise = (arr, raiseIndex) => {
+ return ([...arr.slice(0, raiseIndex), ...arr.slice(raiseIndex + 1), arr[raiseIndex]]);
+};
+
diff --git a/webapp/src/utils/request.js b/webapp/src/utils/request.js
index 440d52d..4c4c38e 100644
--- a/webapp/src/utils/request.js
+++ b/webapp/src/utils/request.js
@@ -71,6 +71,10 @@ service.interceptors.request.use(
service.interceptors.response.use(
response => {
const res = response.data;
+ // 如果请求的返回类型是流,则直接返回 data
+ if (response.config.responseType === 'blob') {
+ return res;
+ }
// if the custom code is not 200, it is judged as an error.
if (res.code !== 200) {
if (isWhiteList(response.config.url)) {
diff --git a/webapp/src/utils/utils.js b/webapp/src/utils/utils.js
index a29e555..fca1c16 100644
--- a/webapp/src/utils/utils.js
+++ b/webapp/src/utils/utils.js
@@ -18,6 +18,8 @@
* utils, 通用方法
*/
+import { nanoid } from 'nanoid';
+
/**
* Parse the time to string
* @param {(Object|string|number)} time
@@ -252,5 +254,92 @@ export function stringIsValidPythonVariable(str) {
}
const pattern = /^[_a-zA-Z][_a-zA-Z0-9]*$/;
return pattern.test(str);
+}
+
+
+const _toTree = (data) => {
+ const result = [];
+ if (!Array.isArray(data)) {
+ return result;
+ }
+ data.forEach(item => {
+ delete item.children;
+ });
+ const map = {};
+ data.forEach(item => {
+ map[item.id] = item;
+ });
+ data.forEach(item => {
+ const parent = map[item.pid];
+ if (parent) {
+ (parent.children || (parent.children = [])).push(item);
+ } else {
+ result.push(item);
+ }
+ });
+ return result;
+};
+
+/**
+ * minio数据转成树形结构
+ * @param {string} filepath minio的路径
+ * @returns {list} [treeList, expandedKeys] 树形结构,和默认展开的元素
+ */
+export const getTreeListFromFilepath = async (filepath) => {
+ // 1,获取minio的数据
+ const tmp = await window.minioClient.listObjects(filepath);
+ if(!tmp || !tmp.length){
+ return [[], []];
+ }
+ const minioList = [];
+ for (const item of tmp) {
+ minioList.push(item.name.replace(filepath, ""));
+ }
+ // 2 转成平级数据
+ const dataList = [];
+ const keyList = []; // 去重用
+ for (const filename of minioList) {
+ const list = filename.split("/");
+ list.forEach((item, index) => {
+ const p = {
+ pid: index === 0 ? 9999 : `${index - 1}_${list[index - 1]}`,
+ id: `${index}_${item}`,
+ name: item,
+ originPath: filepath + list.slice(0,index+1).join('/'),
+ isFile: index === list.length-1,
+ };
+ const key = `${p.pid}_${p.id}`;
+ if (keyList.indexOf(key) === -1) {
+ keyList.push(key);
+ dataList.push(p);
+ }
+ });
+ }
+ // 2.1 最外层单独封装一层
+ const tmp2 = filepath.split('/');
+ const wrapperNodeName = tmp2[tmp2.length-2];
+ const wrapperNode = {
+ pid: 0,
+ id: 9999,
+ name: wrapperNodeName,
+ originPath: filepath,
+ isFile: false,
+ };
+ dataList.push(wrapperNode);
+ // 3 转成树形结构
+ const treeList = _toTree([].concat(dataList));
+ // 4 显示默认展开的层级,默认二级
+ const expandedKeys = [];
+ for (const item of treeList) {
+ expandedKeys.push(item.id);
+ for(const item2 of item.children){
+ expandedKeys.push(item2.id);
+ }
+ }
+ // 返回数据
+ return [treeList, expandedKeys];
+};
+export function getUniqueId() {
+ return parseTime(new Date(), '{y}{m}{d}{h}{i}{s}{S}') + nanoid(4);
}
diff --git a/webapp/src/utils/validate.js b/webapp/src/utils/validate.js
index 890cf7b..118e87e 100644
--- a/webapp/src/utils/validate.js
+++ b/webapp/src/utils/validate.js
@@ -18,6 +18,7 @@
* validate,校验函数
*/
+import { isPlainObject } from 'lodash';
import { ValidationProvider, ValidationObserver, extend } from 'vee-validate';
import { required } from 'vee-validate/dist/rules';
@@ -268,3 +269,29 @@ export function validateRunCommand(rule, value, callback) {
callback(new Error('请输入正确的启动命令'));
}
}
+
+// 校验标签组基本方法
+export const validateLabelsUtil = (value) => {
+ if(!isPlainObject(value)) {
+ return '标签不能为空';
+ }
+ if(!value.name) {
+ return '标签名称不能为空';
+ }
+ if(!value.color) {
+ return '标签颜色不能为空';
+ }
+ if(!/^#[0-9A-F]{6}$/i.test(value.color)) {
+ return '标签颜色格式不对';
+ }
+ return '';
+};
+
+export function validateLabel(rule, value, callback) {
+ const validateResult = validateLabelsUtil(value);
+ if(validateResult !== '') {
+ callback(new Error(validateResult));
+ return;
+ }
+ callback();
+}
diff --git a/webapp/src/views/algorithm/index.vue b/webapp/src/views/algorithm/index.vue
index 7ec82a4..5e00ac8 100644
--- a/webapp/src/views/algorithm/index.vue
+++ b/webapp/src/views/algorithm/index.vue
@@ -21,6 +21,7 @@
-
-
+
+
@@ -82,19 +84,19 @@
- 在线编辑
- 创建训练任务
- 下载
- fork
+ 在线编辑
+ 创建训练任务
+ 下载
+ fork
更多
-
+
fork
-
+
删除
@@ -124,6 +126,7 @@
>
源代码包:
- 下载
+ 下载
- 算法上传中...
+
+
+
+
+
+
-
+
-
+
diff --git a/webapp/src/views/dataset/annotate/index.vue b/webapp/src/views/dataset/annotate/index.vue
index 84fa6f6..fc006f6 100644
--- a/webapp/src/views/dataset/annotate/index.vue
+++ b/webapp/src/views/dataset/annotate/index.vue
@@ -30,7 +30,7 @@
:isTrack="isTrack"
:state="state"
:currentImg="currentImg"
- :handleBrushEnd="handleBrushEnd"
+ :drawBboxEnd="drawBboxEnd"
:createLabel="createLabel"
:queryLabels="queryLabels"
:updateState="updateState"
@@ -65,8 +65,8 @@ import { isEmpty, isFunction, omit, isNil } from 'lodash';
import { detail, detectFileList, queryFileOffset, queryDataEnhanceList, getEnhanceFileList } from '@/api/preparation/dataset';
import request from '@/utils/request';
-import { generateUuid, generateBbox, endsWith, replace, remove, AssertError } from '@/utils';
-import { parseAnnotation, labelsSymbol, enhanceSymbol, stringifyAnnotations, annotationMap, transformFiles } from '../util';
+import { generateUuid, generateBbox, bbox2Extent, extent2Bbox, endsWith, replace, remove, AssertError } from '@/utils';
+import { parseAnnotation, labelsSymbol, enhanceSymbol, stringifyAnnotations, annotationMap, transformFiles, withExtent } from '../util';
import ThumbContainer from './thumbContainer';
import WorkSpaceContainer from './workSpaceContainer';
@@ -89,6 +89,9 @@ export default {
const { params = {}} = $route;
const workspaceRef = ref(null);
+ // 加载下一页,避免重复加载
+ const loadNextPageFlag = ref(false);
+
// 标注类型
const isTrack = $route.name.startsWith('TrackDataset');
// const isAnnotation = meta.type === 'annotate'
@@ -102,6 +105,7 @@ export default {
hasMore: true, // 是否有更多列表
datasetId: Number(params.datasetId),
currentImgId: Number(params.fileId) || undefined, // 当前图片 id
+ rawAnnotations: [], // 原始标注集合
annotations: [], // 标注集合
fileInfo: null, // 文件信息
fileId: Number($route.params.fileId),
@@ -177,8 +181,15 @@ export default {
};
// 根据异步结果更新状态
- const updateState = (nextState) => {
- Object.assign(state, nextState);
+ const updateState = (params) => {
+ // 区分函数式更新和对象更新
+ if(typeof params === 'function') {
+ const next = params(state);
+ Object.assign(state, next);
+ return;
+ }
+ // 普通更新
+ Object.assign(state, params);
};
// 根据 labelId 获取标签颜色
@@ -236,6 +247,7 @@ export default {
const clearHistory = () => {
updateState({
history: [],
+ rawAnnotations: [],
annotations: [],
fileInfo: null, // 当前文件信息
lastSelectedLabel: undefined,
@@ -259,7 +271,13 @@ export default {
// 当到下边界只有 2 张图片时,请求下一页数据
// 仍然有下页
if (index + 2 >= fileList.value.length && state.hasMore) {
- queryNextPage({ offset: state.offset, type: state.fileFilterType });
+ // 避免重复加载
+ if(loadNextPageFlag.value === false) {
+ loadNextPageFlag.value = true;
+ queryNextPage({ offset: state.offset, type: state.fileFilterType }).then(() => {
+ loadNextPageFlag.value = false;
+ });
+ }
}
};
@@ -286,7 +304,7 @@ export default {
// 请求指定图片信息
const queryFile = async(id) => {
- const file = await request(`api/data/datasets/files/${id}/info`) || {};
+ const file = await request(`api/data/datasets/files/${params.datasetId}/${id}/info`) || {};
return file;
};
@@ -319,7 +337,7 @@ export default {
// 保存标注
const saveAnnotation = async(data) => {
- await request.post(`api/data/datasets/files/${state.currentImgId}/annotations`, data).then(() => {
+ await request.post(`api/data/datasets/files/${params.datasetId}/${state.currentImgId}/annotations`, data).then(() => {
// 清空历史记录
Object.assign(state, { history: [] });
Message.success({ message: '保存成功', duration: 800 });
@@ -328,7 +346,7 @@ export default {
// 人工确认标注
const confirmAnnotation = async(data) => {
- await request.post(`api/data/datasets/files/${state.currentImgId}/annotations/finish`, data).then(() => {
+ await request.post(`api/data/datasets/files/${params.datasetId}/${state.currentImgId}/annotations/finish`, data).then(() => {
// 清空历史记录
Object.assign(state, { history: [] });
// todo: 更新列表
@@ -352,15 +370,71 @@ export default {
});
};
+ // 将绝对路径映射为相对图片路径
+ const mapBrushToBbox = annotation => {
+ const { bbox } = annotation.data;
+
+ const { dimension } = workspaceRef.value;
+ // 临时变量
+ let temp_bbox = {};
+ // 解析 bbox 值
+ const _bbox = {};
+ // 当图片缩放比例小于1,当前画布尺寸会超过图片,需要截取空白尺寸
+ if (dimension.scale < 1) {
+ const padding = {
+ width: dimension.svg.width - dimension.img.width * dimension.scale,
+ height: dimension.svg.height - dimension.img.height * dimension.scale,
+ };
+ Object.assign(temp_bbox, {
+ ...bbox,
+ x: bbox.x - padding.width / 2,
+ // 垂直反向偏移
+ y: bbox.y - padding.height / 2,
+ });
+ } else {
+ temp_bbox = bbox;
+ }
+ for (const k in temp_bbox) {
+ // 根据图片缩放比例进行调整
+ _bbox[k] = temp_bbox[k] / (dimension.scale || 1);
+ }
+
+ const updatedAnnotation = {
+ ...annotation,
+ data: {
+ ...annotation.data,
+ bbox: _bbox,
+ extent: bbox2Extent(_bbox),
+ },
+ };
+
+ return updatedAnnotation;
+ };
+
+ // 保存的时候生成新的位置信息
+ const rescale = (annotation) => {
+ const { extent } = annotation.data;
+
+ const updatedAnnotation = {
+ ...annotation,
+ data: {
+ ...annotation.data,
+ bbox: extent2Bbox(extent),
+ },
+ };
+ // _type 仅供绘画使用
+ return omit(updatedAnnotation, ['__type']);
+ };
+
// 手动画框结束
- const handleBrushEnd = (brush) => {
+ const drawBboxEnd = (brush) => {
const bbox = generateBbox(brush);
// 记录上一次选中的 selectLabel
const otherProps = state.lastSelectedLabel ? {
categoryId: state.lastSelectedLabel,
color: getColorLabel(state.lastSelectedLabel),
} : {};
- const annotation = {
+ const rawAnnotation = {
id: generateUuid(),
__type: 0, // 标识为新创建的标注
data: {
@@ -369,6 +443,9 @@ export default {
...otherProps,
},
};
+
+ // todo: 转换成标准地址(extent/bbox)
+ const annotation = mapBrushToBbox(rawAnnotation);
// 更新框选位置坐标
const newAnnotation = (state.annotations || []).concat(annotation);
Object.assign(state, {
@@ -386,48 +463,6 @@ export default {
return true;
};
- // 保存的时候生成新的位置信息
- const rescale = (annotation) => {
- const { __type } = annotation;
- const { bbox } = annotation.data;
- const { dimension } = workspaceRef.value;
- // 临时变量
- let temp_bbox = {};
- // 解析 bbox 值
- const _bbox = {};
- if (__type === 0) {
- // 当图片缩放比例小于1,当前画布尺寸会超过图片,需要截取空白尺寸
- if (dimension.scale < 1) {
- const padding = {
- width: dimension.svg.width - dimension.img.width * dimension.scale,
- height: dimension.svg.height - dimension.img.height * dimension.scale,
- };
- Object.assign(temp_bbox, {
- ...bbox,
- x: bbox.x - padding.width / 2,
- // 垂直反向偏移
- // y: bbox.y - padding.height / 2
- });
- } else {
- temp_bbox = bbox;
- }
- for (const k in temp_bbox) {
- // 根据图片缩放比例进行调整
- _bbox[k] = temp_bbox[k] / (dimension.scale || 1);
- }
- }
-
- const updatedAnnotation = {
- ...annotation,
- data: {
- ...annotation.data,
- bbox: __type === 0 ? _bbox : bbox,
- },
- };
- // _type 仅供绘画使用
- return omit(updatedAnnotation, ['__type']);
- };
-
// 保存标注
const handleSave = () => {
const isValid = state.annotations.every(checkAnnotationValid);
@@ -580,6 +615,8 @@ export default {
let { result: files } = rawFile.value;
const { __offset__, page = {}} = rawFile.value;
+ // 同步当前文件的偏移
+ state.offset = __offset__;
// 自定义分页
// 当前条数小于每页可返回的总条数,向上补齐
const availableSize = Math.min(page.size, page.total);
@@ -610,15 +647,16 @@ export default {
updateState(nextState);
// 根据第一个文件是否携带数据增强结果来决定是否展示
- const firstEnhanceList = await getEnhanceFileList(firstFile.id);
+ const firstEnhanceList = await getEnhanceFileList(params.datasetId, firstFile.id);
// 更新当前图片
const { file, annotations } = await updateImageInfo(activeFileId, labels);
updateState({
currentImgId: file.id,
fileInfo: file,
- annotations,
- hasEnhanceRecord: firstEnhanceList.length > 0,
+ rawAnnotations: annotations,
+ annotations: withExtent(annotations),
+ hasEnhanceRecord: !isNil(firstEnhanceList),
});
});
@@ -632,6 +670,7 @@ export default {
watch(() => [state.currentImgId, state.timestamp], async() => {
const imgId = state.currentImgId;
updateState({
+ rawAnnotations: [],
annotations: [],
fileInfo: null,
});
@@ -646,7 +685,8 @@ export default {
gotoFileDetail(imgId);
// 清理数据
updateState({
- annotations,
+ rawAnnotations: annotations,
+ annotations: withExtent(annotations),
fileInfo: file,
});
}
@@ -663,7 +703,7 @@ export default {
currentImg,
handleSelection,
handleBrushStart,
- handleBrushEnd,
+ drawBboxEnd,
handleSave,
handleConfirm,
gotoFileDetail,
diff --git a/webapp/src/views/dataset/annotate/settingContainer/annotations.vue b/webapp/src/views/dataset/annotate/settingContainer/annotations.vue
index d945706..74952b4 100644
--- a/webapp/src/views/dataset/annotate/settingContainer/annotations.vue
+++ b/webapp/src/views/dataset/annotate/settingContainer/annotations.vue
@@ -102,7 +102,7 @@ export default {
};
const withEdit = (item, isEdit = false) => {
- const { categoryId, track_id } = item.data;
+ const { categoryId, track_id } = item.data || {};
// 获取到分类标签名
const labelName = rLabels.value[categoryId];
const labelNameTxt = labelName ? `${labelName}_` : '';
diff --git a/webapp/src/views/dataset/annotate/settingContainer/enhance.vue b/webapp/src/views/dataset/annotate/settingContainer/enhance.vue
index 67c59d9..bd2a060 100644
--- a/webapp/src/views/dataset/annotate/settingContainer/enhance.vue
+++ b/webapp/src/views/dataset/annotate/settingContainer/enhance.vue
@@ -151,7 +151,7 @@ export default {
watch(() => props.fileId, async(next) => {
if (next) {
- const enhanceFileList = await getEnhanceFileList(next);
+ const enhanceFileList = await getEnhanceFileList(props.datasetId,next);
const isOrigin = !!enhanceFileList.length; // 被增强
Object.assign(state, {
isOrigin,
diff --git a/webapp/src/views/dataset/annotate/settingContainer/index.vue b/webapp/src/views/dataset/annotate/settingContainer/index.vue
index 4c82d48..fbfacf8 100644
--- a/webapp/src/views/dataset/annotate/settingContainer/index.vue
+++ b/webapp/src/views/dataset/annotate/settingContainer/index.vue
@@ -17,13 +17,35 @@
+
+
+ {{ state.datasetInfo.value.labelGroupName }}
+
+ 查看详情
+
+
+
-
+
import { Message } from 'element-ui';
-import { inject, reactive, onMounted, computed } from '@vue/composition-api';
+import { inject, watch, reactive, onMounted, computed } from '@vue/composition-api';
import { isNil } from 'lodash';
-import { getAutoLabels } from '@/api/preparation/datalabel';
+import { getAutoLabels, editLabel } from '@/api/preparation/datalabel';
import { labelsSymbol } from '@/views/dataset/util';
import SelectLabel from './selectLabel';
@@ -90,6 +112,7 @@ export default {
const { createLabel, updateState, queryLabels } = props;
const api = reactive({
newLabel: undefined,
+ currentAnnotationId: undefined,
});
// 当前所有标签信息
const labels = inject(labelsSymbol);
@@ -103,21 +126,13 @@ export default {
});
};
- const addLabel = (label) => {
- api.newLabel = label;
+ // 编辑标签
+ const edit = (labelId, data) => {
+ return editLabel(labelId, data).then(refreshLabel);
};
- const handleLabelChange = value => {
- // 新建标签
- if (!isNil(value)) {
- // 如果不是系统标签,才会选择新建
- if (api.systemLabels.findIndex(d => d.value === value) === -1) {
- addLabel(value);
- } else {
- const systemLabel = api.systemLabels.find(d => d.value === value) || {};
- systemLabel.label && addLabel(systemLabel.label);
- }
- }
+ const addLabel = (label) => {
+ api.newLabel = label;
};
const postLabel = () => {
@@ -131,6 +146,24 @@ export default {
api.newLabel = undefined;
refreshLabel();
});
+ } else {
+ Message.warning('请选择标签');
+ }
+ };
+
+ const handleLabelChange = (value, callback) => {
+ // 新建标签
+ if (!isNil(value)) {
+ // 如果不是系统标签,才会选择新建
+ if (api.systemLabels.findIndex(d => d.value === value) === -1) {
+ addLabel(value);
+ // 新建标签直接触发创建
+ postLabel();
+ typeof callback === 'function' && callback();
+ } else {
+ const systemLabel = api.systemLabels.find(d => d.value === value) || {};
+ systemLabel.label && addLabel(systemLabel.label);
+ }
}
};
@@ -176,8 +209,14 @@ export default {
updateState(newState);
};
- // 使用的是预置标签时type大于1,目前自定义标签type为0,自动标注标签为1
- const isPresetLabel = computed(() => labels.value && labels.value[0] && labels.value[0].type > 1);
+ // labelGroupType 标签组类型:0: private 私有标签组, 1:public 公开标签组
+ const isPresetLabel = computed(() => props.state.labelGroupType === 1);
+
+ watch(() => props.state, (next) => {
+ if ('currentAnnotationId' in next) {
+ api.currentAnnotationId = next.currentAnnotationId || [];
+ }
+ });
onMounted(() => {
getSystemLabel();
@@ -190,6 +229,8 @@ export default {
toggleShowId,
labels,
postLabel,
+ addLabel,
+ edit,
handleLabelChange,
isPresetLabel,
};
diff --git a/webapp/src/views/dataset/annotate/settingContainer/labelList/edit.vue b/webapp/src/views/dataset/annotate/settingContainer/labelList/edit.vue
new file mode 100644
index 0000000..17bae7f
--- /dev/null
+++ b/webapp/src/views/dataset/annotate/settingContainer/labelList/edit.vue
@@ -0,0 +1,135 @@
+/** 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.
+* =============================================================
+*/
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 确定
+
+
+
+
+
+
diff --git a/webapp/src/views/dataset/annotate/settingContainer/labelList.vue b/webapp/src/views/dataset/annotate/settingContainer/labelList/index.vue
similarity index 53%
rename from webapp/src/views/dataset/annotate/settingContainer/labelList.vue
rename to webapp/src/views/dataset/annotate/settingContainer/labelList/index.vue
index 46904d7..2af9eae 100644
--- a/webapp/src/views/dataset/annotate/settingContainer/labelList.vue
+++ b/webapp/src/views/dataset/annotate/settingContainer/labelList/index.vue
@@ -23,7 +23,21 @@
- {{ item.name }}
+ handleEditAnnotation(item, event)"
+ >
+ {{ item.name }}
+
+
@@ -32,34 +46,44 @@
+
diff --git a/webapp/src/views/dataset/annotate/settingContainer/selectLabel.vue b/webapp/src/views/dataset/annotate/settingContainer/selectLabel.vue
index fe9a4ad..47d1677 100644
--- a/webapp/src/views/dataset/annotate/settingContainer/selectLabel.vue
+++ b/webapp/src/views/dataset/annotate/settingContainer/selectLabel.vue
@@ -22,6 +22,7 @@
确定
@@ -37,7 +38,7 @@
diff --git a/webapp/src/views/dataset/annotate/workSpaceContainer/score.js b/webapp/src/views/dataset/annotate/workSpaceContainer/score.js
index 4c4bc58..c41ae6b 100644
--- a/webapp/src/views/dataset/annotate/workSpaceContainer/score.js
+++ b/webapp/src/views/dataset/annotate/workSpaceContainer/score.js
@@ -15,12 +15,10 @@
*/
import { isNil } from 'lodash';
-import { addSuffix } from '@/utils';
+import { addSuffix, colorByLuminance, chroma } from '@/utils';
import { defaultColor } from './bbox';
-const chroma = require('chroma-js');
-
// 分数最小宽度
const MinWidth = 48;
@@ -29,39 +27,28 @@ export default {
functional: true,
props: {
annotate: Object,
- scale: {
- type: Number,
- },
- imgBoundingLeft: Number,
+ offset: Function,
+ transformer: Object,
+ brush: Object,
+ currentAnnotationId: String,
},
render(h, context) {
const { props } = context;
const {
annotate = {},
- imgBoundingLeft,
+ offset,
+ transformer,
+ brush,
} = props;
- const { data = {}, __type } = annotate;
+ const { data = {}, id } = annotate;
const { bbox, color = defaultColor, score = 1 } = data;
- if (isNil(bbox)) return null;
- // 是否为草稿模式
- const isDraft = __type === 0;
- const paddingLeft = (props.scale < 1 && !isNil(imgBoundingLeft))
- ? imgBoundingLeft
- : 0;
+ // 当前在拖拽中不展示
+ if(props.currentAnnotationId === id && brush.isBrushing) return null;
- const pos = isDraft ? {
- x: bbox.x,
- y: bbox.y,
- width: bbox.width,
- height: bbox.height,
- } : {
- x: bbox.x * props.scale + paddingLeft,
- y: bbox.y * props.scale,
- width: bbox.width * props.scale,
- height: bbox.height * props.scale,
- };
+ if (isNil(bbox)) return null;
+ const pos = offset(props.annotate);
const style = {
width: addSuffix(pos.width),
@@ -70,8 +57,14 @@ export default {
minWidth: addSuffix(MinWidth),
};
+ // 匹配当前标注
+ if(annotate.id === transformer.id) {
+ style.transform = `translate(${transformer.dx}px, ${transformer.dy}px)`;
+ }
+
const boxStyle = {
backgroundColor: chroma(color).alpha(0.8),
+ color: colorByLuminance(color),
};
return (
diff --git a/webapp/src/views/dataset/annotate/workSpaceContainer/tag.js b/webapp/src/views/dataset/annotate/workSpaceContainer/tag.js
index bc365bd..7752823 100644
--- a/webapp/src/views/dataset/annotate/workSpaceContainer/tag.js
+++ b/webapp/src/views/dataset/annotate/workSpaceContainer/tag.js
@@ -15,66 +15,61 @@
*/
import { isNil } from 'lodash';
-import { addSuffix } from '@/utils';
+import { addSuffix, chroma, colorByLuminance } from '@/utils';
import { defaultColor } from './bbox';
-const chroma = require('chroma-js');
-
export default {
name: 'Tag',
functional: true,
props: {
annotate: Object,
- scale: {
- type: Number,
- },
- imgBoundingLeft: Number,
+ offset: Function,
+ transformer: Object,
getLabelName: Function,
+ brush: Object,
+ currentAnnotationId: String,
},
render(h, context) {
const { props } = context;
const {
annotate = {},
- imgBoundingLeft,
getLabelName,
+ offset,
+ transformer,
+ brush,
} = props;
- const { data = {}, __type } = annotate;
+ const { data = {}, id } = annotate;
const { bbox, color = defaultColor } = data;
+
+ // 当前在拖拽中不展示
+ if(props.currentAnnotationId === id && brush.isBrushing) return null;
+
if (isNil(bbox)) return null;
// 是否为草稿模式
- const isDraft = __type === 0;
- const paddingLeft = (props.scale < 1 && !isNil(imgBoundingLeft))
- ? imgBoundingLeft
- : 0;
-
- const pos = isDraft ? {
- x: bbox.x,
- y: bbox.y,
- width: bbox.width,
- height: bbox.height,
- } : {
- x: bbox.x * props.scale + paddingLeft,
- y: bbox.y * props.scale,
- width: bbox.width * props.scale,
- height: bbox.height * props.scale,
- };
+ const pos = offset(props.annotate);
const style = {
width: addSuffix(pos.width),
left: addSuffix(pos.x),
top: addSuffix(pos.y),
+ color: colorByLuminance(color),
};
+ // 匹配当前标注
+ if(annotate.id === transformer.id) {
+ style.transform = `translate(${transformer.dx}px, ${transformer.dy}px)`;
+ }
+
const tagColor = chroma(color).alpha(0.8).toString();
const tagName = getLabelName(data.categoryId);
if (!tagName) return null;
return (
- {tagName}
+ {tagName}
);
},
diff --git a/webapp/src/views/dataset/classify.vue b/webapp/src/views/dataset/classify.vue
index d645b98..ab52e5b 100644
--- a/webapp/src/views/dataset/classify.vue
+++ b/webapp/src/views/dataset/classify.vue
@@ -29,12 +29,12 @@