@@ -1,3 +1,9 @@ | |||||
## 2.1.0 (2021-12-22) | |||||
### Breaking Change | |||||
- [自动机器学习] 新增自动机器学习模块 | |||||
## 2.0.0 (2021-08-30) | ## 2.0.0 (2021-08-30) | ||||
### Breaking Change | ### Breaking Change | ||||
@@ -8,5 +8,5 @@ if (process.env.NODE_ENV === "production") { | |||||
} | } | ||||
module.exports = { | module.exports = { | ||||
plugins, | plugins, | ||||
presets: ["@vue/app"], | |||||
presets: [["@vue/app",{ useBuiltIns: "entry" }]], | |||||
}; | }; |
@@ -1,6 +1,6 @@ | |||||
{ | { | ||||
"name": "dubhe-web", | "name": "dubhe-web", | ||||
"version": "2.0.0", | |||||
"version": "2.1.0", | |||||
"description": "之江天枢人工智能开源平台", | "description": "之江天枢人工智能开源平台", | ||||
"author": "zhejianglab", | "author": "zhejianglab", | ||||
"keywords": [ | "keywords": [ | ||||
@@ -41,6 +41,8 @@ | |||||
"url": "git@codeup.teambition.com:zhejianglab/dubhe-web.git" | "url": "git@codeup.teambition.com:zhejianglab/dubhe-web.git" | ||||
}, | }, | ||||
"dependencies": { | "dependencies": { | ||||
"@antv/g2plot": "^2.3.17", | |||||
"@opd/g2plot-vue": "3.1.12", | |||||
"@riophae/vue-treeselect": "0.1.0", | "@riophae/vue-treeselect": "0.1.0", | ||||
"@vue/babel-plugin-transform-vue-jsx": "^1.1.2", | "@vue/babel-plugin-transform-vue-jsx": "^1.1.2", | ||||
"@vue/composition-api": "^1.0.0-rc.1", | "@vue/composition-api": "^1.0.0-rc.1", | ||||
@@ -51,6 +53,8 @@ | |||||
"chroma-js": "^2.1.0", | "chroma-js": "^2.1.0", | ||||
"classnames": "^2.2.6", | "classnames": "^2.2.6", | ||||
"clipboard": "^2.0.6", | "clipboard": "^2.0.6", | ||||
"codemirror": "^5.60.0", | |||||
"core-js": "^3.9.1", | |||||
"d3": "^5.16.0", | "d3": "^5.16.0", | ||||
"d3-selection": "^1.4.1", | "d3-selection": "^1.4.1", | ||||
"d3-zoom": "^1.8.3", | "d3-zoom": "^1.8.3", | ||||
@@ -68,6 +72,8 @@ | |||||
"jquery-contextmenu": "^2.9.1", | "jquery-contextmenu": "^2.9.1", | ||||
"js-beautify": "^1.13.0", | "js-beautify": "^1.13.0", | ||||
"js-cookie": "2.2.0", | "js-cookie": "2.2.0", | ||||
"js-yaml": "^4.0.0", | |||||
"jschardet": "^2.2.1", | |||||
"jsencrypt": "^3.0.0-rc.1", | "jsencrypt": "^3.0.0-rc.1", | ||||
"json2csv": "^5.0.1", | "json2csv": "^5.0.1", | ||||
"lodash": "^4.17.15", | "lodash": "^4.17.15", | ||||
@@ -87,6 +93,7 @@ | |||||
"screenfull": "^5.0.2", | "screenfull": "^5.0.2", | ||||
"stream-to-array": "^2.3.0", | "stream-to-array": "^2.3.0", | ||||
"streamsaver": "^2.0.4", | "streamsaver": "^2.0.4", | ||||
"url-join": "^4.0.1", | |||||
"v-click-outside": "^3.0.1", | "v-click-outside": "^3.0.1", | ||||
"v-hotkey": "^0.8.0", | "v-hotkey": "^0.8.0", | ||||
"vee-validate": "^3.3.0", | "vee-validate": "^3.3.0", | ||||
@@ -0,0 +1,292 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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'; | |||||
import { API_MODULE_NAME } from '@/config'; | |||||
export function list(params) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment`, | |||||
method: 'get', | |||||
params, | |||||
}); | |||||
} | |||||
// 创建/保存实验 | |||||
export function createExperiment(data) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment`, | |||||
method: 'post', | |||||
data, | |||||
}); | |||||
} | |||||
// 编辑实验 | |||||
export function editExperiment(data) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment`, | |||||
method: 'put', | |||||
data, | |||||
}); | |||||
} | |||||
// 查询实验详情的概览 | |||||
export function expDetailOverview(experimentId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 查询实验详情 | |||||
export function expDetail(experimentId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/info`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 查询阶段概览 | |||||
export function expStageInfo(experimentId, stageOrder) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 查询阶段实验参数 | |||||
export function expStageParam(experimentId, stageOrder) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/param`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 查询阶段运行中参数 | |||||
export function expStageRuntimeParam(experimentId, stageOrder) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/runtime/param`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 修改阶段运行参数之trial并发数 | |||||
export function updateConcurrentNum(experimentId, stageOrder, trialConcurrentNum) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/update/ConcurrentNum`, | |||||
method: 'put', | |||||
data: { experimentId, stageOrder, trialConcurrentNum }, | |||||
}); | |||||
} | |||||
// 修改阶段运行参数之trial最大值 | |||||
export function updateMaxTrialNum(experimentId, stageOrder, maxTrialNum) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/update/MaxTrialNum`, | |||||
method: 'put', | |||||
data: { experimentId, stageOrder, maxTrialNum }, | |||||
}); | |||||
} | |||||
// 修改阶段运行参数之最大运行时间 | |||||
export function updateMaxExecDuration( | |||||
experimentId, | |||||
stageOrder, | |||||
maxExecDuration, | |||||
maxExecDurationUnit | |||||
) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/update/MaxExecDuration`, | |||||
method: 'put', | |||||
data: { experimentId, stageOrder, maxExecDuration, maxExecDurationUnit }, | |||||
}); | |||||
} | |||||
// 查询阶段trial精度最高5条 | |||||
export function expStageTrialRep(experimentId, stageOrder) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/trial/rep`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 查询实验配置信息 | |||||
export function expYaml(experimentId, stageOrder) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/yaml`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 修改实验配置yaml | |||||
export function updateExpYaml(experimentId, stageOrder, yaml) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/update/yaml`, | |||||
method: 'put', | |||||
data: { yaml, experimentId, stageOrder }, | |||||
}); | |||||
} | |||||
// 查询阶段trialsList列表 | |||||
export function expStageTrialList({ experimentId, stageOrder, current = 1, size = 1, ...args }) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/trial/${experimentId}/${stageOrder}/list`, | |||||
method: 'get', | |||||
params: { | |||||
experimentId, | |||||
stageOrder, | |||||
current, | |||||
size, | |||||
...args, | |||||
}, | |||||
}); | |||||
} | |||||
// 查询阶段运行标准输出数据 | |||||
export function expStageAccuracy(experimentId, stageOrder) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/best/accuracy`, | |||||
method: 'get', | |||||
params: { | |||||
experimentId, | |||||
stageOrder, | |||||
}, | |||||
}); | |||||
} | |||||
// 查询多trial图数据 | |||||
export function expStageTrialData(experimentId, stageOrder, params) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/trialData`, | |||||
method: 'get', | |||||
params, | |||||
}); | |||||
} | |||||
// 查询阶段运行中间值 | |||||
export function expStageIntermediate(experimentId, stageOrder, trialIds = null) { | |||||
const params = { | |||||
experimentId, | |||||
stageOrder, | |||||
trialIds, | |||||
}; | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/intermediate/accuracy`, | |||||
method: 'get', | |||||
params, | |||||
}); | |||||
} | |||||
export function expStageRuntime(experimentId, stageOrder) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/runTime`, | |||||
method: 'get', | |||||
params: { | |||||
experimentId, | |||||
stageOrder, | |||||
}, | |||||
}); | |||||
} | |||||
// 启动实验 | |||||
export function startExp(experimentId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/start`, | |||||
method: 'put', | |||||
}); | |||||
} | |||||
// 暂停实验 | |||||
export function pauseExp(experimentId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/pause`, | |||||
method: 'put', | |||||
}); | |||||
} | |||||
// 删除实验 | |||||
export function deleteExp(experimentId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}`, | |||||
method: 'delete', | |||||
}); | |||||
} | |||||
// 查询searchspace内容 | |||||
export function getSearchSpace(experimentId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/searchSpace`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 查询best selected space内容 | |||||
export function getSelectedSpace(experimentId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/bestSelectedSpace`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 查询实验config | |||||
export function getExpConfig(experimentId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/${experimentId}/config`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 查询实验总日志 | |||||
export function getExpLog(params) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/${params.experimentId}/logs`, | |||||
method: 'get', | |||||
params, | |||||
}); | |||||
} | |||||
// 查询trial日志详情 | |||||
export function trialLog(trialId, startLine = 1, lines = 50) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/trial/trialLog`, | |||||
method: 'get', | |||||
params: { | |||||
trialId, | |||||
startLine, | |||||
lines, | |||||
}, | |||||
}); | |||||
} | |||||
/** | |||||
* /api/ {version} /tadl /experiment/{experimentId}/{stageOrder}/ {trialId} /model | |||||
*/ | |||||
// 下载模型 | |||||
export function downloadModel(experimentId, stageOrder, trialId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/stage/${experimentId}/${stageOrder}/trial/${trialId}/model`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
// 获取资源配置 | |||||
export function getResources(params) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/experiment/resource`, | |||||
method: 'get', | |||||
params, | |||||
}); | |||||
} |
@@ -0,0 +1,106 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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'; | |||||
import { API_MODULE_NAME } from '@/config'; | |||||
// 算法解压 | |||||
export function unpackZip(params) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/unzip`, | |||||
method: 'get', | |||||
params, | |||||
}); | |||||
} | |||||
export function parseYamlParams(params) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/yaml`, | |||||
method: 'get', | |||||
params, | |||||
}); | |||||
} | |||||
export function getStrategyList(params) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/query`, | |||||
method: 'get', | |||||
params, | |||||
}); | |||||
} | |||||
export function getVersionList(id) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/${id}/list`, | |||||
method: 'get', | |||||
}); | |||||
} | |||||
export function uploadStrategy(data) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/upload`, | |||||
method: 'post', | |||||
data, | |||||
}); | |||||
} | |||||
export function updateStrategy(data) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/update`, | |||||
method: 'post', | |||||
data, | |||||
}); | |||||
} | |||||
export function getNextVersion(algorithmId) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/${algorithmId} /next/version`, | |||||
method: 'get', | |||||
params: { algorithmId }, | |||||
}); | |||||
} | |||||
export function versionRelease(data) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/push/version`, | |||||
method: 'post', | |||||
data, | |||||
}); | |||||
} | |||||
export function shiftVersion(data) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/version/switch`, | |||||
method: 'put', | |||||
data, | |||||
}); | |||||
} | |||||
export function checkStrategy(params, id) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm/${id}/query`, | |||||
method: 'get', | |||||
params, | |||||
}); | |||||
} | |||||
export function deleteVersion(data) { | |||||
return request({ | |||||
url: `/${API_MODULE_NAME.TADL}/algorithm`, | |||||
method: 'delete', | |||||
data, | |||||
}); | |||||
} |
@@ -180,6 +180,10 @@ | |||||
margin-left: 4px; | margin-left: 4px; | ||||
} | } | ||||
.mr-4 { | |||||
margin-right: 4px; | |||||
} | |||||
.ml-10 { | .ml-10 { | ||||
margin-left: 10px; | margin-left: 10px; | ||||
} | } | ||||
@@ -204,6 +208,10 @@ | |||||
margin-left: auto; | margin-left: auto; | ||||
} | } | ||||
.mb-0 { | |||||
margin-bottom: 0; | |||||
} | |||||
.mb-50 { | .mb-50 { | ||||
margin-bottom: 50px; | margin-bottom: 50px; | ||||
} | } | ||||
@@ -250,6 +258,10 @@ | |||||
margin-bottom: auto; | margin-bottom: auto; | ||||
} | } | ||||
.w-80 { | |||||
width: 80px; | |||||
} | |||||
.w-100 { | .w-100 { | ||||
width: 100px; | width: 100px; | ||||
} | } | ||||
@@ -411,3 +423,7 @@ img.responsive { | |||||
width: 100vw; | width: 100vw; | ||||
height: 100vh; | height: 100vh; | ||||
} | } | ||||
.multiple-lines { | |||||
@include multiple-lines; | |||||
} |
@@ -264,6 +264,10 @@ pre { | |||||
color: $infoColor; | color: $infoColor; | ||||
} | } | ||||
.CodeMirror-lint-tooltip { | |||||
z-index: 10000 !important; | |||||
} | |||||
.app-result-content { | .app-result-content { | ||||
padding: 24px 40px; | padding: 24px 40px; | ||||
margin-top: 24px; | margin-top: 24px; | ||||
@@ -339,3 +339,29 @@ | |||||
} | } | ||||
} | } | ||||
} | } | ||||
.el-tooltip__popper.is-light { | |||||
border: none; | |||||
box-shadow: rgba(0, 0, 0, 0.15) 0 2px 8px 0; | |||||
.popper__arrow { | |||||
border: none; | |||||
} | |||||
} | |||||
.el-tabs-large .el-tabs__nav .el-tabs__item { | |||||
font-size: 16px; | |||||
} | |||||
.el-card__footer { | |||||
padding-top: 20px; | |||||
margin-top: 8px; | |||||
border-top: 1px solid #f0f0f0; | |||||
} | |||||
.el-form-item-explain { | |||||
min-height: 24px; | |||||
font-size: 14px; | |||||
line-height: 1.5715; | |||||
color: rgba(0, 0, 0, 0.45); | |||||
} |
@@ -21,6 +21,7 @@ | |||||
@import 'element-ui'; | @import 'element-ui'; | ||||
@import 'sidebar'; | @import 'sidebar'; | ||||
@import 'common'; | @import 'common'; | ||||
@import url('//at.alicdn.com/t/font_1756495_ohftzv0cq9c.css'); | |||||
@media screen and (max-width: 768px) { | @media screen and (max-width: 768px) { | ||||
.mb-dn { | .mb-dn { | ||||
@@ -93,6 +94,11 @@ ol li { | |||||
color: $primaryColor; | color: $primaryColor; | ||||
} | } | ||||
.disabled { | |||||
color: $disableColor; | |||||
cursor: not-allowed; | |||||
} | |||||
.infoColor { | .infoColor { | ||||
color: $infoColor; | color: $infoColor; | ||||
} | } | ||||
@@ -43,6 +43,13 @@ | |||||
white-space: nowrap; | white-space: nowrap; | ||||
} | } | ||||
@mixin multiple-lines { | |||||
display: -webkit-box; | |||||
overflow: hidden; | |||||
text-overflow: ellipsis; | |||||
-webkit-box-orient: vertical; | |||||
} | |||||
@mixin relative { | @mixin relative { | ||||
position: relative; | position: relative; | ||||
width: 100%; | width: 100%; | ||||
@@ -1,18 +1,10 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<el-table ref="table" v-loading="loading" :data="data" v-bind="attrs" v-on="$listeners"> | <el-table ref="table" v-loading="loading" :data="data" v-bind="attrs" v-on="$listeners"> | ||||
@@ -68,6 +60,7 @@ | |||||
@click.native="() => runFunc(moreOp.func, scope.row)" | @click.native="() => runFunc(moreOp.func, scope.row)" | ||||
> | > | ||||
<el-button | <el-button | ||||
v-click-once="moreOp.clickOnceTime || 1000" | |||||
type="text" | type="text" | ||||
:disabled="getOperationStatus('disabled', 'disableFunc', moreOp, scope.row)" | :disabled="getOperationStatus('disabled', 'disableFunc', moreOp, scope.row)" | ||||
> | > | ||||
@@ -81,6 +74,7 @@ | |||||
v-else-if="!getOperationStatus('hide', 'hideFunc', operation, scope.row)" | v-else-if="!getOperationStatus('hide', 'hideFunc', operation, scope.row)" | ||||
:id="operation.label + scope.$index" | :id="operation.label + scope.$index" | ||||
:key="operation.key" | :key="operation.key" | ||||
v-click-once="operation.clickOnceTime || 1000" | |||||
type="text" | type="text" | ||||
:disabled="getOperationStatus('disabled', 'disableFunc', operation, scope.row)" | :disabled="getOperationStatus('disabled', 'disableFunc', operation, scope.row)" | ||||
@click.stop="() => runFunc(operation.func, scope.row)" | @click.stop="() => runFunc(operation.func, scope.row)" | ||||
@@ -94,6 +88,13 @@ | |||||
<el-tag v-else-if="column.type === 'tag'" v-bind="getTagAttrs(column, scope.row)">{{ | <el-tag v-else-if="column.type === 'tag'" v-bind="getTagAttrs(column, scope.row)">{{ | ||||
getContent(column, scope.row) | getContent(column, scope.row) | ||||
}}</el-tag> | }}</el-tag> | ||||
<!-- link 列 --> | |||||
<el-link | |||||
v-else-if="column.type === 'link'" | |||||
type="primary" | |||||
@click="runFunc(column.func, scope.row)" | |||||
>{{ getContent(column, scope.row) }}</el-link | |||||
> | |||||
<!-- 其他展示列 --> | <!-- 其他展示列 --> | ||||
<span v-else> | <span v-else> | ||||
{{ getContent(column, scope.row) }} | {{ getContent(column, scope.row) }} | ||||
@@ -107,7 +108,7 @@ | |||||
<script> | <script> | ||||
import { computed, ref } from '@vue/composition-api'; | import { computed, ref } from '@vue/composition-api'; | ||||
import { parseTime, noop, runFunc } from '@/utils'; | |||||
import { parseTime, noop, runFunc, restProps } from '@/utils'; | |||||
import DropdownHeader from '@/components/DropdownHeader'; | import DropdownHeader from '@/components/DropdownHeader'; | ||||
const defaultColunmDefinition = { | const defaultColunmDefinition = { | ||||
@@ -232,7 +233,7 @@ export default { | |||||
if (typeof column.tagAttrFunc === 'function') { | if (typeof column.tagAttrFunc === 'function') { | ||||
return column.tagAttrFunc(row[column.prop], row); | return column.tagAttrFunc(row[column.prop], row); | ||||
} | } | ||||
const tagAttr = column.tagAttr || {}; | |||||
const tagAttr = restProps(column.tagAttr || {}, row); | |||||
const tagMap = column.tagMap || {}; | const tagMap = column.tagMap || {}; | ||||
return { | return { | ||||
type: tagMap[row[column.prop]], | type: tagMap[row[column.prop]], | ||||
@@ -270,6 +271,7 @@ export default { | |||||
// Utils 方法 | // Utils 方法 | ||||
parseTime, | parseTime, | ||||
runFunc, | runFunc, | ||||
restProps, | |||||
}; | }; | ||||
}, | }, | ||||
}; | }; | ||||
@@ -0,0 +1,42 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-tooltip v-bind="mergedAttrs"> | |||||
<i class="primary f18 vm" :class="[icon]" /> | |||||
</el-tooltip> | |||||
</template> | |||||
<script> | |||||
import { computed } from '@vue/composition-api'; | |||||
const defaultAttr = { | |||||
effect: 'dark', | |||||
placement: 'top', | |||||
}; | |||||
export default { | |||||
name: 'BaseTooltip', | |||||
props: { | |||||
icon: { | |||||
type: String, | |||||
default: 'el-icon-warning-outline', | |||||
}, | |||||
}, | |||||
setup(props, ctx) { | |||||
const mergedAttrs = computed(() => ({ | |||||
...defaultAttr, | |||||
...ctx.attrs, | |||||
})); | |||||
return { | |||||
mergedAttrs, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,66 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="description-items"> | |||||
<table> | |||||
<tbody> | |||||
<tr v-for="(row, index) in columns" :key="index" class="descriptions-row"> | |||||
<DescriptionItem | |||||
v-for="col in row" | |||||
:key="col[labelBy]" | |||||
class="description-item" | |||||
:col="col" | |||||
v-bind="attrs" | |||||
> | |||||
<template v-for="(_, name) in $slots" v-slot:[name]> | |||||
<slot :name="name" /> | |||||
</template> | |||||
</DescriptionItem> | |||||
</tr> | |||||
</tbody> | |||||
</table> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { computed } from '@vue/composition-api'; | |||||
import DescriptionItem from './item'; | |||||
export default { | |||||
name: 'Description', | |||||
components: { | |||||
DescriptionItem, | |||||
}, | |||||
props: { | |||||
columns: { | |||||
type: Array, | |||||
default: () => [], | |||||
}, | |||||
labelBy: String, | |||||
}, | |||||
setup(props, ctx) { | |||||
const attrs = computed(() => ctx.attrs); | |||||
return { | |||||
attrs, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.descriptions-row { | |||||
line-height: 1.5; | |||||
} | |||||
.description-items { | |||||
table { | |||||
width: 100%; | |||||
table-layout: fixed; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,50 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<td :colspan="col.span || 1" class="description-item"> | |||||
<span class="description-item-label">{{ col[labelBy] }}:</span> | |||||
<span v-if="state.content" class="description-item-content">{{ state.content }}</span> | |||||
<slot v-else :name="col[labelBy]"></slot> | |||||
</td> | |||||
</template> | |||||
<script> | |||||
import { reactive, watch } from '@vue/composition-api'; | |||||
export default { | |||||
name: 'DescriptionItem', | |||||
props: { | |||||
col: Object, | |||||
data: Object, | |||||
contentBy: { | |||||
type: String, | |||||
default: 'content', | |||||
}, | |||||
labelBy: { | |||||
type: String, | |||||
default: 'label', | |||||
}, | |||||
}, | |||||
setup(props) { | |||||
const state = reactive({ | |||||
content: props.col[props.contentBy] || null, | |||||
}); | |||||
watch( | |||||
() => props.col, | |||||
(next) => { | |||||
state.content = next[props.contentBy]; | |||||
} | |||||
); | |||||
return { | |||||
state, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> |
@@ -26,7 +26,7 @@ | |||||
import create from './iconfont'; | import create from './iconfont'; | ||||
const IconFont = create({ | const IconFont = create({ | ||||
scriptUrl: '//at.alicdn.com/t/font_1756495_hq281r3cld4.js', | |||||
scriptUrl: '//at.alicdn.com/t/font_1756495_ohftzv0cq9c.js', | |||||
extraIconProps: { class: 'svg-icon' }, | extraIconProps: { class: 'svg-icon' }, | ||||
}); | }); | ||||
@@ -1,18 +1,10 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<ValidationObserver ref="observerRef"> | <ValidationObserver ref="observerRef"> | ||||
@@ -24,7 +16,7 @@ | |||||
:title="props.title" | :title="props.title" | ||||
@show="onShow" | @show="onShow" | ||||
> | > | ||||
<ValidationProvider v-slot="{ errors }" :rules="rules" :name="label"> | |||||
<ValidationProvider ref="providerRef" v-slot="{ errors }" :rules="rules" :name="label"> | |||||
<el-input | <el-input | ||||
ref="inputRef" | ref="inputRef" | ||||
v-model.trim="state.value" | v-model.trim="state.value" | ||||
@@ -32,20 +24,24 @@ | |||||
placeholder="" | placeholder="" | ||||
:type="inputType" | :type="inputType" | ||||
@keyup.enter.native="handleOk" | @keyup.enter.native="handleOk" | ||||
/> | |||||
> | |||||
<template v-for="(_, name) in $slots" v-slot:[name]> | |||||
<slot :name="name" /> | |||||
</template> | |||||
</el-input> | |||||
<p class="error-message" style="margin-top: 4px;">{{ errors[0] }}</p> | <p class="error-message" style="margin-top: 4px;">{{ errors[0] }}</p> | ||||
</ValidationProvider> | </ValidationProvider> | ||||
<div class="tc" style="margin-top: 8px;"> | <div class="tc" style="margin-top: 8px;"> | ||||
<el-button @click="handleCancel">取消</el-button> | <el-button @click="handleCancel">取消</el-button> | ||||
<el-button type="primary" @click="handleOk">确定</el-button> | <el-button type="primary" @click="handleOk">确定</el-button> | ||||
</div> | </div> | ||||
<i slot="reference" class="el-icon-edit primary cp dib" /> | |||||
<i slot="reference" :class="triggerClass" /> | |||||
</el-popover> | </el-popover> | ||||
</ValidationObserver> | </ValidationObserver> | ||||
</template> | </template> | ||||
<script> | <script> | ||||
import Vue from 'vue'; | |||||
import { reactive, ref, watch } from '@vue/composition-api'; | |||||
import { reactive, ref, watch, computed, nextTick } from '@vue/composition-api'; | |||||
import cx from 'classnames'; | |||||
export default { | export default { | ||||
name: 'Edit', | name: 'Edit', | ||||
@@ -72,11 +68,20 @@ export default { | |||||
type: String, | type: String, | ||||
default: '名称', // 错误展示字段 | default: '名称', // 错误展示字段 | ||||
}, | }, | ||||
disabled: { | |||||
type: Boolean, | |||||
default: false, | |||||
}, | |||||
// 修改前校验 | |||||
beforeChange: { | |||||
type: Function, | |||||
}, | |||||
}, | }, | ||||
setup(props, ctx) { | setup(props, ctx) { | ||||
const { valueBy } = props; | |||||
const { valueBy, beforeChange } = props; | |||||
const observerRef = ref(null); | const observerRef = ref(null); | ||||
const inputRef = ref(null); | const inputRef = ref(null); | ||||
const providerRef = ref(null); | |||||
const state = reactive({ | const state = reactive({ | ||||
visible: false, | visible: false, | ||||
@@ -97,24 +102,40 @@ export default { | |||||
if (!success) { | if (!success) { | ||||
return; | return; | ||||
} | } | ||||
// 判断是否发生过变更 | |||||
if (String(state.value) !== String(props.row[valueBy])) { | |||||
ctx.emit('handleOk', state.value, props.row); | |||||
if (typeof beforeChange === 'function') { | |||||
beforeChange(state.value, props.row, providerRef, { valueBy }) | |||||
.then(() => { | |||||
// TODO: 判断是否发生过变更 | |||||
ctx.emit('handleOk', state.value, props.row, { valueBy }); | |||||
handleCancel(); | |||||
}) | |||||
.catch((err) => { | |||||
console.error(err); | |||||
}); | |||||
} else { | |||||
ctx.emit('handleOk', state.value, props.row, { valueBy }); | |||||
handleCancel(); | |||||
} | } | ||||
handleCancel(); | |||||
}); | }); | ||||
}; | }; | ||||
const onShow = () => { | const onShow = () => { | ||||
// onShow 的时候重置 | // onShow 的时候重置 | ||||
state.value = props.row[valueBy]; | state.value = props.row[valueBy]; | ||||
Vue.nextTick(() => { | |||||
nextTick(() => { | |||||
const input = | const input = | ||||
(inputRef && inputRef.value.$refs.input) || (inputRef && inputRef.value.$refs.textarea); | (inputRef && inputRef.value.$refs.input) || (inputRef && inputRef.value.$refs.textarea); | ||||
input && input.focus(); | input && input.focus(); | ||||
}); | }); | ||||
}; | }; | ||||
const triggerClass = computed(() => | |||||
cx('el-icon-edit primary cp dib', { | |||||
disabled: !!props.disabled, | |||||
pen: !!props.disabled, | |||||
}) | |||||
); | |||||
watch( | watch( | ||||
() => props.row, | () => props.row, | ||||
(next) => { | (next) => { | ||||
@@ -129,8 +150,10 @@ export default { | |||||
state, | state, | ||||
inputRef, | inputRef, | ||||
observerRef, | observerRef, | ||||
providerRef, | |||||
handleOk, | handleOk, | ||||
handleCancel, | handleCancel, | ||||
triggerClass, | |||||
onShow, | onShow, | ||||
}; | }; | ||||
}, | }, | ||||
@@ -1,18 +1,10 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<div class="pro-table-header flex py-4"> | <div class="pro-table-header flex py-4"> | ||||
@@ -38,6 +30,7 @@ | |||||
>{{ deleteTitle }}</el-button | >{{ deleteTitle }}</el-button | ||||
> | > | ||||
</slot> | </slot> | ||||
<i v-if="loading" class="el-icon-loading" /> | |||||
</span> | </span> | ||||
<span class="header-right ml-auto"> | <span class="header-right ml-auto"> | ||||
<slot name="right"> | <slot name="right"> | ||||
@@ -102,6 +95,11 @@ export default { | |||||
type: Object, | type: Object, | ||||
default: () => ({}), | default: () => ({}), | ||||
}, | }, | ||||
// 是否展示 loading 图标 | |||||
loading: { | |||||
type: Boolean, | |||||
default: false, | |||||
}, | |||||
}, | }, | ||||
setup(props, { emit }) { | setup(props, { emit }) { | ||||
// 点击创建按钮,抛出创建事件 | // 点击创建按钮,抛出创建事件 | ||||
@@ -1,18 +1,10 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<div class="pro-table-container"> | <div class="pro-table-container"> | ||||
@@ -25,6 +17,7 @@ | |||||
:delete-title="deleteTitle" | :delete-title="deleteTitle" | ||||
:form-items="mergedFormItems" | :form-items="mergedFormItems" | ||||
:form-model="state.queryFormModel" | :form-model="state.queryFormModel" | ||||
:loading="headerLoading" | |||||
@create="onCreate" | @create="onCreate" | ||||
@delete="onDelete" | @delete="onDelete" | ||||
> | > | ||||
@@ -64,7 +57,7 @@ | |||||
</slot> | </slot> | ||||
<BaseTable | <BaseTable | ||||
ref="table" | ref="table" | ||||
v-loading="state.loading" | |||||
v-loading="tableLoading" | |||||
v-bind="tableAttrs" | v-bind="tableAttrs" | ||||
:columns="mergedColumns" | :columns="mergedColumns" | ||||
:data="state.data" | :data="state.data" | ||||
@@ -77,7 +70,7 @@ | |||||
</template> | </template> | ||||
</BaseTable> | </BaseTable> | ||||
<el-pagination | <el-pagination | ||||
v-if="showPagination" | |||||
v-if="pageShow" | |||||
v-bind="mergedPageAttrs" | v-bind="mergedPageAttrs" | ||||
:style="`text-align:${pageAlign}; margin-top: 8px;`" | :style="`text-align:${pageAlign}; margin-top: 8px;`" | ||||
@size-change="onSizeChange" | @size-change="onSizeChange" | ||||
@@ -182,6 +175,10 @@ export default { | |||||
type: Object, | type: Object, | ||||
default: () => ({}), | default: () => ({}), | ||||
}, | }, | ||||
// 查询之前的回调方法,如果返回 false 则停止请求 | |||||
beforeListFn: Function, | |||||
// 查询之后的回调方法,入参为当前查询结果 | |||||
afterListFn: Function, | |||||
// 删除数据方法 | // 删除数据方法 | ||||
delRequest: Function, | delRequest: Function, | ||||
// 调用默认删除接口时用于获取 ID 字段 | // 调用默认删除接口时用于获取 ID 字段 | ||||
@@ -189,6 +186,16 @@ export default { | |||||
type: String, | type: String, | ||||
default: 'id', | default: 'id', | ||||
}, | }, | ||||
// 区分在表格上展示 loading 还是在头部展示 loading。table - 表格; header - 头部。 | |||||
loadingType: { | |||||
type: String, | |||||
default: 'table', | |||||
}, | |||||
// 是否在渲染之后立刻请求数据 | |||||
refreshImmediate: { | |||||
type: Boolean, | |||||
default: true, | |||||
}, | |||||
}, | }, | ||||
setup(props, ctx) { | setup(props, ctx) { | ||||
const { formItems, paginationAttrs, deleteDisabled, columns } = toRefs(props); | const { formItems, paginationAttrs, deleteDisabled, columns } = toRefs(props); | ||||
@@ -201,7 +208,7 @@ export default { | |||||
data: [], // 表格数据 | data: [], // 表格数据 | ||||
selectedRows: [], // 表格多选行 | selectedRows: [], // 表格多选行 | ||||
loading: false, // 表格 loading 状态 | loading: false, // 表格 loading 状态 | ||||
queryObj: {}, // 其他查询对象 | |||||
paginationVisible: false, // 需要在请求之后展示分页,避免分页页码提前设置之后无法正确展示 | |||||
}); | }); | ||||
// 搜索 | // 搜索 | ||||
@@ -254,6 +261,10 @@ export default { | |||||
// 数据请求 | // 数据请求 | ||||
const refresh = async (queryObj) => { | const refresh = async (queryObj) => { | ||||
if (typeof listRequest === 'function') { | if (typeof listRequest === 'function') { | ||||
if (typeof props.beforeListFn === 'function') { | |||||
const res = props.beforeListFn(); | |||||
if (res === false) return; | |||||
} | |||||
state.loading = true; | state.loading = true; | ||||
const { currentPage, pageSize } = pagination; | const { currentPage, pageSize } = pagination; | ||||
// 清除空的查询参数 | // 清除空的查询参数 | ||||
@@ -268,7 +279,6 @@ export default { | |||||
size: pageSize, | size: pageSize, | ||||
sort: sortInfo.sort || undefined, | sort: sortInfo.sort || undefined, | ||||
order: sortInfo.order || undefined, | order: sortInfo.order || undefined, | ||||
...state.queryObj, | |||||
...props.listOptions, | ...props.listOptions, | ||||
...queryObj, | ...queryObj, | ||||
}).finally(() => { | }).finally(() => { | ||||
@@ -281,6 +291,10 @@ export default { | |||||
} | } | ||||
setPagination(page); | setPagination(page); | ||||
state.data = result; | state.data = result; | ||||
state.paginationVisible = true; | |||||
if (typeof props.afterListFn === 'function') { | |||||
props.afterListFn(result); | |||||
} | |||||
} | } | ||||
}; | }; | ||||
// 数据查询 | // 数据查询 | ||||
@@ -319,6 +333,7 @@ export default { | |||||
const onSelectionChange = (selections) => { | const onSelectionChange = (selections) => { | ||||
state.selectedRows = selections; | state.selectedRows = selections; | ||||
}; | }; | ||||
const pageShow = computed(() => props.showPagination && state.paginationVisible); | |||||
// 列定义预处理 | // 列定义预处理 | ||||
const mergedColumns = computed(() => { | const mergedColumns = computed(() => { | ||||
@@ -326,7 +341,7 @@ export default { | |||||
// 为下拉表头绑定默认查询方法 | // 为下拉表头绑定默认查询方法 | ||||
if (column.dropdownList && typeof column.func !== 'function') { | if (column.dropdownList && typeof column.func !== 'function') { | ||||
column.func = (value) => { | column.func = (value) => { | ||||
state.queryObj[column.prop] = value; | |||||
state.queryFormModel[column.prop] = value; | |||||
query(); | query(); | ||||
}; | }; | ||||
} | } | ||||
@@ -384,8 +399,18 @@ export default { | |||||
refresh(); | refresh(); | ||||
}; | }; | ||||
const tableLoading = computed(() => { | |||||
return state.loading && props.loadingType === 'table'; | |||||
}); | |||||
const headerLoading = computed(() => { | |||||
return state.loading && props.loadingType === 'header'; | |||||
}); | |||||
// 渲染后调用一次查询 | // 渲染后调用一次查询 | ||||
onMounted(query); | |||||
if (props.refreshImmediate) { | |||||
onMounted(query); | |||||
} | |||||
return { | return { | ||||
state, | state, | ||||
@@ -401,19 +426,24 @@ export default { | |||||
refresh, | refresh, | ||||
query, | query, | ||||
setQuery, | |||||
resetQuery, | resetQuery, | ||||
setSort, | setSort, | ||||
sortInfo, | |||||
onSizeChange, | onSizeChange, | ||||
pagination, | pagination, | ||||
setPagination, | setPagination, | ||||
onPageChange, | onPageChange, | ||||
onSortChange, | onSortChange, | ||||
onSelectionChange, | onSelectionChange, | ||||
pageShow, | |||||
mergedPageAttrs, | mergedPageAttrs, | ||||
mergedColumns, | mergedColumns, | ||||
mergedFormItems, | mergedFormItems, | ||||
slotLeft, | slotLeft, | ||||
tableLoading, | |||||
headerLoading, | |||||
}; | }; | ||||
}, | }, | ||||
}; | }; | ||||
@@ -0,0 +1,43 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="el-statistic"> | |||||
<div v-if="title" class="el-statistic-title">{{ title }}</div> | |||||
<div class="el-statistic-content"> | |||||
<span class="el-statistic-content-value">{{ value }}</span> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'Statistic', | |||||
props: { | |||||
title: String, | |||||
value: [String, Number], | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.el-statistic-title { | |||||
margin-bottom: 4px; | |||||
color: rgba(0, 0, 0, 0.45); | |||||
font-size: 14px; | |||||
} | |||||
.el-statistic-content { | |||||
color: rgba(0, 0, 0, 0.85); | |||||
font-size: 24px; | |||||
} | |||||
.el-statistic-content-value { | |||||
display: inline-block; | |||||
direction: ltr; | |||||
} | |||||
</style> |
@@ -1,18 +1,10 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<!--训练管理页面-保存模型Dialog--> | <!--训练管理页面-保存模型Dialog--> | ||||
@@ -165,6 +157,7 @@ const defaultModelForm = { | |||||
const typeMap = { | const typeMap = { | ||||
training: 1, | training: 1, | ||||
optimize: 2, | optimize: 2, | ||||
tadl: 4, | |||||
}; | }; | ||||
export default { | export default { | ||||
@@ -0,0 +1,133 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="yaml-editor"> | |||||
<textarea ref="textarea" /> | |||||
<div class="tips"> | |||||
<p v-if="readOnly">当前编辑器为只读状态</p> | |||||
<p>tips: 编辑代码时请注意代码格式. 比如缩进及没用的换行, 以免影响提交数据</p> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { onMounted, ref, watch } from '@vue/composition-api'; | |||||
import { Message } from 'element-ui'; | |||||
import CodeMirror from 'codemirror'; | |||||
import 'codemirror/addon/lint/lint.css'; | |||||
import 'codemirror/lib/codemirror.css'; | |||||
import 'codemirror/theme/monokai.css'; | |||||
import 'codemirror/mode/yaml/yaml'; | |||||
import 'codemirror/addon/lint/lint'; | |||||
import 'codemirror/addon/lint/yaml-lint'; | |||||
window.jsyaml = require('js-yaml'); | |||||
const yaml = require('js-yaml'); | |||||
export default { | |||||
name: 'YamlEditor', | |||||
props: { | |||||
value: { | |||||
type: String, | |||||
required: true, | |||||
}, | |||||
readOnly: { | |||||
type: Boolean, | |||||
default: false, | |||||
}, | |||||
}, | |||||
setup(props, ctx) { | |||||
const yamlEditor = ref(null); | |||||
const textarea = ref(null); | |||||
const getValue = () => { | |||||
return yamlEditor.value.getValue(); | |||||
}; | |||||
const setValue = () => { | |||||
yamlEditor.value.setOption('readOnly', props.readOnly); | |||||
yamlEditor.value.setValue(props.value); | |||||
}; | |||||
// 代码语法校验 | |||||
const codeValid = () => { | |||||
try { | |||||
yaml.load(getValue()); | |||||
return true; | |||||
} catch (e) { | |||||
Message.error(e.reason || '代码语法错误'); | |||||
return false; | |||||
} | |||||
}; | |||||
// 编辑器初始化 | |||||
const initYamlEditor = () => { | |||||
yamlEditor.value = CodeMirror.fromTextArea(textarea.value, { | |||||
lineNumbers: true, // 显示行号 | |||||
mode: 'text/x-yaml', // 语法model | |||||
gutters: ['CodeMirror-lint-markers'], // 语法检查器 | |||||
theme: 'monokai', // 编辑器主题 | |||||
lint: true, // 开启语法检查 | |||||
}); | |||||
setValue(); | |||||
yamlEditor.value.on('change', (cm) => { | |||||
ctx.emit('changed', cm.getValue()); | |||||
ctx.emit('input', cm.getValue()); | |||||
}); | |||||
yamlEditor.value.on('blur', (cm) => { | |||||
ctx.emit('blur', cm.getValue()); | |||||
}); | |||||
}; | |||||
watch( | |||||
() => props.value, | |||||
(next) => { | |||||
if (next !== getValue()) { | |||||
yamlEditor.value.setValue(props.value); | |||||
} | |||||
} | |||||
); | |||||
onMounted(initYamlEditor); | |||||
return { | |||||
textarea, | |||||
yamlEditor, | |||||
getValue, | |||||
setValue, | |||||
codeValid, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
::v-deep.yaml-editor { | |||||
height: 100%; | |||||
.CodeMirror { | |||||
height: 100%; | |||||
border-radius: 5px 5px 0 0; | |||||
} | |||||
} | |||||
.tips { | |||||
font-size: 14px; | |||||
color: rgb(179, 175, 175); | |||||
background: #272822; | |||||
border-radius: 0 0 5px 5px; | |||||
p { | |||||
margin: 0 10px; | |||||
} | |||||
} | |||||
</style> |
@@ -30,6 +30,7 @@ export const API_MODULE_NAME = { | |||||
ATLAS: 'measure', // 模型炼知 | ATLAS: 'measure', // 模型炼知 | ||||
K8S: 'k8s', // K8S | K8S: 'k8s', // K8S | ||||
DCM: 'dcm', // 医学dcm | DCM: 'dcm', // 医学dcm | ||||
TADL: 'tadl', // TADL | |||||
DUBHE_PRO: 'terminal', // 天枢专业版 | DUBHE_PRO: 'terminal', // 天枢专业版 | ||||
}; | }; | ||||
@@ -25,3 +25,4 @@ export * from './dict'; | |||||
export * from './localStorage'; | export * from './localStorage'; | ||||
export * from './pagination'; | export * from './pagination'; | ||||
export * from './sort'; | export * from './sort'; | ||||
export * from './keepPageInfo'; |
@@ -0,0 +1,55 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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 { nextTick } from '@vue/composition-api'; | |||||
import { noop } from '@/utils'; | |||||
import store from '@/store'; | |||||
const assert = require('assert'); | |||||
/** | |||||
* 支持使用 VUEX 来存储分页、排序等信息 | |||||
* @param {String} pageInfoGetter 用于获取 store 中 pageInfo 的 getter 字符串 | |||||
* @param {String} updateAction 用于设置 store 中 pageInfo 的 action 字符串 | |||||
* @param {Function} pageInfoSetter 对获取到的分页数据进行设置应用 | |||||
* @param {Function} afterEnter 完成进入页面后调用 | |||||
*/ | |||||
export function useKeepPageInfo({ | |||||
pageInfoGetter, | |||||
updateAction, | |||||
pageInfoSetter = noop, | |||||
afterEnter = noop, | |||||
} = {}) { | |||||
assert(pageInfoGetter, '必须传入对应的 getter 名'); | |||||
assert(updateAction, '必须传入对应的 action 名'); | |||||
const pageEnter = (keepPageInfos) => { | |||||
if (keepPageInfos) { | |||||
pageInfoSetter(store.getters[pageInfoGetter]); | |||||
} | |||||
nextTick(afterEnter); | |||||
}; | |||||
const updatePageInfo = (info) => { | |||||
store.dispatch(updateAction, info); | |||||
}; | |||||
return { | |||||
pageEnter, | |||||
updatePageInfo, | |||||
}; | |||||
} |
@@ -26,7 +26,7 @@ import store from '@/store'; | |||||
export function useMapGetters(getters) { | export function useMapGetters(getters) { | ||||
const map = reactive({}); | const map = reactive({}); | ||||
for (const getter of getters) { | for (const getter of getters) { | ||||
map[getter] = store.getters[getter]; | |||||
Object.assign(map, { [getter]: store.getters[getter] }); | |||||
} | } | ||||
return map; | return map; | ||||
} | } |
@@ -1,18 +1,10 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<BaseLayout | <BaseLayout | ||||
@@ -54,6 +46,16 @@ export default { | |||||
border: 1px solid $borderColor; | border: 1px solid $borderColor; | ||||
} | } | ||||
.app-container.fullWidth { | |||||
padding-right: 0; | |||||
padding-left: 0; | |||||
.app-page-header { | |||||
padding-bottom: 0; | |||||
border: none; | |||||
} | |||||
} | |||||
.app-page-header-title { | .app-page-header-title { | ||||
margin-right: 12px; | margin-right: 12px; | ||||
margin-bottom: 0; | margin-bottom: 0; | ||||
@@ -73,6 +75,29 @@ export default { | |||||
margin: 16px 0 0; | margin: 16px 0 0; | ||||
} | } | ||||
.app-page-contaniner-extra { | |||||
min-width: 300px; | |||||
margin-left: 88px; | |||||
text-align: right; | |||||
} | |||||
.app-page-header-footer { | |||||
margin-top: 20px; | |||||
.el-tabs__header { | |||||
margin-bottom: 0; | |||||
.el-tabs__nav-wrap::after { | |||||
width: 0; | |||||
} | |||||
} | |||||
} | |||||
.profile-advance { | |||||
display: flex; | |||||
justify-content: space-between; | |||||
} | |||||
.app-page-form-steps-desc { | .app-page-form-steps-desc { | ||||
padding: 0 56px; | padding: 0 56px; | ||||
color: rgba(0, 0, 0, 0.55); | color: rgba(0, 0, 0, 0.55); | ||||
@@ -16,8 +16,7 @@ | |||||
import { api_version, api_prefix } from '../../config'; | import { api_version, api_prefix } from '../../config'; | ||||
import { findMatchRule, isURL } from './util'; | import { findMatchRule, isURL } from './util'; | ||||
// eslint-disable-next-line import/no-extraneous-dependencies | |||||
const url = require('url'); | |||||
const urljoin = require('url-join'); | |||||
const { VUE_APP_DATA_API, VUE_APP_VISUAL_API, VUE_APP_BASE_API } = process.env; | const { VUE_APP_DATA_API, VUE_APP_VISUAL_API, VUE_APP_BASE_API } = process.env; | ||||
@@ -27,14 +26,14 @@ const fullPrefix = `${api_prefix}/${api_version}`; | |||||
const rules = [ | const rules = [ | ||||
{ | { | ||||
match: /^\/data/, | match: /^\/data/, | ||||
host: url.resolve(VUE_APP_DATA_API, fullPrefix), | |||||
host: urljoin(VUE_APP_DATA_API, fullPrefix), | |||||
}, | }, | ||||
{ | { | ||||
match: /^\/visual\/api/, | match: /^\/visual\/api/, | ||||
host: VUE_APP_VISUAL_API, | host: VUE_APP_VISUAL_API, | ||||
}, | }, | ||||
{ | { | ||||
host: url.resolve(VUE_APP_BASE_API, fullPrefix), | |||||
host: urljoin(VUE_APP_BASE_API, fullPrefix), | |||||
}, | }, | ||||
]; | ]; | ||||
@@ -0,0 +1,50 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
const state = { | |||||
experimentPageInfo: { | |||||
current: 1, | |||||
pageSize: 10, | |||||
sort: { sort: null, order: null }, | |||||
query: {}, | |||||
}, | |||||
}; | |||||
const mutations = { | |||||
UPDATE_EXPERIMENT_PAGE_INFO(state, pageInfo) { | |||||
state.experimentPageInfo = pageInfo; | |||||
}, | |||||
}; | |||||
const actions = { | |||||
updateExperimentPageInfo({ commit }, pageInfo) { | |||||
commit('UPDATE_EXPERIMENT_PAGE_INFO', pageInfo); | |||||
}, | |||||
}; | |||||
const getters = { | |||||
pageInfo() { | |||||
return state.experimentPageInfo; | |||||
}, | |||||
}; | |||||
export default { | |||||
namespaced: true, | |||||
state, | |||||
mutations, | |||||
actions, | |||||
getters, | |||||
}; |
@@ -19,11 +19,10 @@ import Config from '@/settings'; | |||||
import { getToken } from '@/utils/auth'; | import { getToken } from '@/utils/auth'; | ||||
import store from '@/store/modules/Visual/layout'; | import store from '@/store/modules/Visual/layout'; | ||||
// eslint-disable-next-line import/no-extraneous-dependencies | |||||
const url = require('url'); | |||||
const urljoin = require('url-join'); | |||||
const service = axios.create({ | const service = axios.create({ | ||||
baseURL: url.resolve(process.env.VUE_APP_VISUAL_API, '/visual'), | |||||
baseURL: urljoin(process.env.VUE_APP_VISUAL_API, '/visual'), | |||||
timeout: Config.timeout, // 请求超时时间 | timeout: Config.timeout, // 请求超时时间 | ||||
withCredentials: true, | withCredentials: true, | ||||
}); | }); | ||||
@@ -27,6 +27,7 @@ import { | |||||
values, | values, | ||||
minBy, | minBy, | ||||
maxBy, | maxBy, | ||||
isFunction, | |||||
} from 'lodash'; | } from 'lodash'; | ||||
import { nanoid } from 'nanoid'; | import { nanoid } from 'nanoid'; | ||||
@@ -55,6 +56,24 @@ export function mergeProps(...args) { | |||||
return props; | return props; | ||||
} | } | ||||
// 判断参数是否为函数 | |||||
export const callOrValue = (maybeFn, ...data) => { | |||||
if (isFunction(maybeFn)) { | |||||
return maybeFn(...data); | |||||
} | |||||
return maybeFn; | |||||
}; | |||||
/** | |||||
* 解析对象,解析其中的函数,并传递参数进去 | |||||
*/ | |||||
export const restProps = (rest, ...data) => { | |||||
return Object.keys(rest).reduce((ret, cur) => { | |||||
ret[cur] = callOrValue(rest[cur], ...data); | |||||
return ret; | |||||
}, {}); | |||||
}; | |||||
// 生成唯一 id | // 生成唯一 id | ||||
export const generateUuid = (count = 6) => nanoid(count); | export const generateUuid = (count = 6) => nanoid(count); | ||||
@@ -164,6 +183,8 @@ export const leadingZero = (num, targetLength = 2, char = '0') => { | |||||
// scale 放大倍数,length: 保留小数点位数 | // scale 放大倍数,length: 保留小数点位数 | ||||
// 0.5122 => 51 | // 0.5122 => 51 | ||||
export const toFixed = (num, scale = 2, length = 2) => { | export const toFixed = (num, scale = 2, length = 2) => { | ||||
// eslint-disable-next-line no-restricted-globals | |||||
if (isNaN(num)) return 0; | |||||
// eslint-disable-next-line | // eslint-disable-next-line | ||||
return Math.floor(num * Math.pow(10, scale + length)) / Math.pow(10, length); | return Math.floor(num * Math.pow(10, scale + length)) / Math.pow(10, length); | ||||
}; | }; | ||||
@@ -71,6 +71,7 @@ export const RESOURCES_MODULE_ENUM = { | |||||
NOTEBOOK: 1, | NOTEBOOK: 1, | ||||
TRAIN: 2, | TRAIN: 2, | ||||
SERVING: 3, | SERVING: 3, | ||||
TADL: 4, | |||||
}; | }; | ||||
// 资源类型名称 | // 资源类型名称 | ||||
@@ -116,7 +117,11 @@ export const defaultProcessColors = [ | |||||
// 系统管理员ID | // 系统管理员ID | ||||
export const ADMIN_ROLE_ID = 1; | export const ADMIN_ROLE_ID = 1; | ||||
// 时间常量 | |||||
// 时间常量(毫秒) | |||||
export const ONE_MINUTE = 1000 * 60; | export const ONE_MINUTE = 1000 * 60; | ||||
export const ONE_HOUR = ONE_MINUTE * 60; | export const ONE_HOUR = ONE_MINUTE * 60; | ||||
export const ONE_DAY = ONE_HOUR * 24; | |||||
export const ONE_WEEK = ONE_DAY * 7; |
@@ -20,10 +20,10 @@ | |||||
import { nanoid } from 'nanoid'; | import { nanoid } from 'nanoid'; | ||||
import { Message } from 'element-ui'; | import { Message } from 'element-ui'; | ||||
import { isNil } from 'lodash'; | |||||
import Config from '@/settings'; | import Config from '@/settings'; | ||||
import FileFilter from '@/components/UploadForm/FileFilter'; | import FileFilter from '@/components/UploadForm/FileFilter'; | ||||
import { isNil } from 'lodash'; | |||||
/** | /** | ||||
* Parse the time to string | * Parse the time to string | ||||
@@ -552,6 +552,33 @@ export const runFunc = (func, ...args) => { | |||||
} | } | ||||
}; | }; | ||||
/** | |||||
* 从单一数据源对象中获取值匹配的指定属性 | |||||
* @param {*} map 单一数据源对象,必须有 value 属性 | |||||
* @param {*} value 用于匹配的值 | |||||
* @param {*} key 指定属性名 | |||||
* @returns 匹配的指定属性值 | |||||
*/ | |||||
export const getValueFromMap = (map, value, key) => { | |||||
const selectedObj = Object.values(map).find((obj) => obj.value === value); | |||||
if (isNil(selectedObj)) return selectedObj; | |||||
return key ? selectedObj[key] : selectedObj; | |||||
}; | |||||
/** | |||||
* 对象与对象之间相同属性赋值 | |||||
* @param {Object} target 目标对象 | |||||
* @param {Object} source 数据源 | |||||
* @param {Function} validate 用于自定义判断属性值所需条件 | |||||
*/ | |||||
export function propertyAssign(target, source, validate = isNil) { | |||||
Object.keys(target).forEach((key) => { | |||||
if (validate(source[key])) { | |||||
target[key] = source[key]; | |||||
} | |||||
}); | |||||
} | |||||
// 格式化时长 | // 格式化时长 | ||||
export function durationTrans(time) { | export function durationTrans(time) { | ||||
let duration = ''; | let duration = ''; | ||||
@@ -29,6 +29,8 @@ const isValidName = (value) => { | |||||
return /^[\u4E00-\u9FA5\w-]+$/.test(value) && value.length <= 50; | return /^[\u4E00-\u9FA5\w-]+$/.test(value) && value.length <= 50; | ||||
}; | }; | ||||
const isValidInteger = (value) => /^[1-9]\d*$/.test(value); | |||||
const isValidNameWithHyphen = (value) => { | const isValidNameWithHyphen = (value) => { | ||||
return /^[\u4E00-\u9FA5A-Za-z0-9_-]+$/.test(value); | return /^[\u4E00-\u9FA5A-Za-z0-9_-]+$/.test(value); | ||||
}; | }; | ||||
@@ -85,6 +87,13 @@ extend('validateLesionId', { | |||||
}, | }, | ||||
}); | }); | ||||
extend('validInteger', { | |||||
validate: isValidInteger, | |||||
message: (_, params) => { | |||||
return `${params._field_}只支持正整数`; | |||||
}, | |||||
}); | |||||
export { ValidationProvider, ValidationObserver }; | export { ValidationProvider, ValidationObserver }; | ||||
/** | /** | ||||
@@ -1,152 +1,154 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<BaseModal | <BaseModal | ||||
:key="state.formKey" | :key="state.formKey" | ||||
title="创建数据集" | title="创建数据集" | ||||
width="630px" | width="630px" | ||||
:okText="okText" | |||||
:loading="state.loading" | :loading="state.loading" | ||||
:visible="state.visible" | :visible="state.visible" | ||||
@change="handleClose" | @change="handleClose" | ||||
@ok="handleOk" | @ok="handleOk" | ||||
> | > | ||||
<el-form ref="formRef" :model="state.form" :rules="rules" label-width="100px"> | |||||
<el-form-item label="数据集名称" prop="name"> | |||||
<el-input v-model="state.form.name" placeholder="数据集名称不能超过50字" maxlength="50" /> | |||||
</el-form-item> | |||||
<el-form-item label="数据类型" prop="dataType"> | |||||
<InfoRadio | |||||
v-model="state.form.dataType" | |||||
:dataSource="dataTypeList" | |||||
:transformOptions="transformOptions" | |||||
type="button" | |||||
@change="handleDataTypeChange" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="模型类型" prop="annotateType"> | |||||
<div | |||||
v-if="state.form.dataType !== dataTypeCodeMap.CUSTOM" | |||||
class="image-select flex flex-wrap" | |||||
> | |||||
<el-radio-group v-model="state.datasetRadio" class="my-20 pl-16" @change="onDatasetRadioChange"> | |||||
<el-radio :label="0">新建数据集</el-radio> | |||||
<el-radio :label="1">导入已有数据集</el-radio> | |||||
</el-radio-group> | |||||
<template v-if="!Boolean(state.datasetRadio)"> | |||||
<el-form ref="formRef" :model="state.form" :rules="rules" label-width="100px"> | |||||
<el-form-item label="数据集名称" prop="name"> | |||||
<el-input v-model="state.form.name" placeholder="数据集名称不能超过50字" maxlength="50" /> | |||||
</el-form-item> | |||||
<el-form-item label="数据类型" prop="dataType"> | |||||
<InfoRadio | |||||
v-model="state.form.dataType" | |||||
:dataSource="dataTypeList" | |||||
:transformOptions="transformOptions" | |||||
type="button" | |||||
@change="handleDataTypeChange" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="模型类型" prop="annotateType"> | |||||
<div | <div | ||||
v-for="item in annotationList" | |||||
:key="item.code" | |||||
:class="getImageKlass(item)" | |||||
@click="selectAnnotationType(item)" | |||||
v-if="state.form.dataType !== dataTypeCodeMap.CUSTOM" | |||||
class="image-select flex flex-wrap" | |||||
> | > | ||||
<div class="image-title">{{ item.name }}</div> | |||||
<img class="pic responsive" :src="getImgUrl(item)" /> | |||||
<span> | |||||
<i class="check-icon" /> | |||||
</span> | |||||
<div | |||||
v-for="item in annotationList" | |||||
:key="item.code" | |||||
:class="getImageKlass(item)" | |||||
@click="selectAnnotationType(item)" | |||||
> | |||||
<div class="image-title">{{ item.name }}</div> | |||||
<img class="pic responsive" :src="getImgUrl(item)" /> | |||||
<span> | |||||
<i class="check-icon" /> | |||||
</span> | |||||
</div> | |||||
</div> | </div> | ||||
</div> | |||||
<div v-else> | |||||
<el-select | |||||
v-model="state.customAnnotationType" | |||||
@change="(val) => selectCustomAnnotationType(val)" | |||||
<div v-else> | |||||
<el-select | |||||
v-model="state.customAnnotationType" | |||||
@change="(val) => selectCustomAnnotationType(val)" | |||||
> | |||||
<el-option | |||||
v-for="item in allAnnotationList" | |||||
:key="item.value" | |||||
:label="item.label" | |||||
:value="item.value" | |||||
/> | |||||
</el-select> | |||||
</div> | |||||
<el-input :value="state.form.annotateType" class="dn" /> | |||||
</el-form-item> | |||||
<div style="position: relative; top: -10px; margin-left: 100px;"> | |||||
更多标注类型说明参考<a | |||||
target="_blank" | |||||
type="primary" | |||||
:underline="false" | |||||
class="primary" | |||||
:href="`${VUE_APP_DOCS_URL}module/dataset/intro`" | |||||
>官方文档</a | |||||
> | > | ||||
<el-option | |||||
v-for="item in allAnnotationList" | |||||
:key="item.value" | |||||
:label="item.label" | |||||
:value="item.value" | |||||
/> | |||||
</el-select> | |||||
</div> | </div> | ||||
<el-input :value="state.form.annotateType" class="dn" /> | |||||
</el-form-item> | |||||
<div style="position: relative; top: -10px; margin-left: 100px;"> | |||||
更多标注类型说明参考<a | |||||
target="_blank" | |||||
type="primary" | |||||
:underline="false" | |||||
class="primary" | |||||
:href="`${VUE_APP_DOCS_URL}module/dataset/intro`" | |||||
>官方文档</a | |||||
> | |||||
</div> | |||||
<el-form-item v-if="showlabelGroup" label="标签组" style="height: 32px;" prop="labelGroup"> | |||||
<el-cascader | |||||
v-model="state.form.labelGroup" | |||||
clearable | |||||
placeholder="标签组" | |||||
:options="labelGroupOptions" | |||||
:props="{ expandTrigger: 'hover' }" | |||||
:show-all-levels="false" | |||||
filterable | |||||
popper-class="group-cascader" | |||||
style="width: 100%; line-height: 32px;" | |||||
@change="handleGroupChange" | |||||
> | |||||
<div slot="empty"> | |||||
<span>没有找到标签组?去</span> | |||||
<a | |||||
<el-form-item v-if="showlabelGroup" label="标签组" style="height: 32px;" prop="labelGroup"> | |||||
<el-cascader | |||||
v-model="state.form.labelGroup" | |||||
clearable | |||||
placeholder="标签组" | |||||
:options="labelGroupOptions" | |||||
:props="{ expandTrigger: 'hover' }" | |||||
:show-all-levels="false" | |||||
filterable | |||||
popper-class="group-cascader" | |||||
style="width: 100%; line-height: 32px;" | |||||
@change="handleGroupChange" | |||||
> | |||||
<div slot="empty"> | |||||
<span>没有找到标签组?去</span> | |||||
<a | |||||
target="_blank" | |||||
type="primary" | |||||
:underline="false" | |||||
class="primary" | |||||
:href="`/data/labelgroup/create`" | |||||
> | |||||
新建标签组 | |||||
</a> | |||||
<span>页面创建</span> | |||||
</div> | |||||
</el-cascader> | |||||
<div style="position: relative; top: -33px; right: 30px; float: right;"> | |||||
<el-link | |||||
v-if="state.form.labelGroupId !== null" | |||||
target="_blank" | target="_blank" | ||||
type="primary" | type="primary" | ||||
:underline="false" | :underline="false" | ||||
class="primary" | |||||
:href="`/data/labelgroup/create`" | |||||
class="vm" | |||||
:href="`/data/labelgroup/detail?id=${state.form.labelGroupId}`" | |||||
> | > | ||||
新建标签组 | |||||
</a> | |||||
<span>页面创建</span> | |||||
查看详情 | |||||
</el-link> | |||||
</div> | </div> | ||||
</el-cascader> | |||||
<div style="position: relative; top: -33px; right: 30px; float: right;"> | |||||
<el-link | |||||
v-if="state.form.labelGroupId !== null" | |||||
</el-form-item> | |||||
<div | |||||
v-if="state.form.labelGroupId === null && showlabelGroup" | |||||
style="position: relative; top: -10px; margin-left: 100px;" | |||||
> | |||||
<span>标签组需要在</span> | |||||
<a | |||||
target="_blank" | target="_blank" | ||||
type="primary" | type="primary" | ||||
:underline="false" | :underline="false" | ||||
class="vm" | |||||
:href="`/data/labelgroup/detail?id=${state.form.labelGroupId}`" | |||||
class="primary" | |||||
:href="`/data/labelgroup/create`" | |||||
> | > | ||||
查看详情 | |||||
</el-link> | |||||
新建标签组 | |||||
</a> | |||||
<span>页面创建</span> | |||||
</div> | </div> | ||||
</el-form-item> | |||||
<div | |||||
v-if="state.form.labelGroupId === null && showlabelGroup" | |||||
style="position: relative; top: -10px; margin-left: 100px;" | |||||
> | |||||
<span>标签组需要在</span> | |||||
<a | |||||
target="_blank" | |||||
type="primary" | |||||
:underline="false" | |||||
class="primary" | |||||
:href="`/data/labelgroup/create`" | |||||
> | |||||
新建标签组 | |||||
</a> | |||||
<span>页面创建</span> | |||||
</div> | |||||
<el-form-item label="数据集描述"> | |||||
<el-input | |||||
v-model="state.form.remark" | |||||
type="textarea" | |||||
placeholder="数据集描述长度不能超过100字" | |||||
maxlength="100" | |||||
rows="3" | |||||
show-word-limit | |||||
/> | |||||
</el-form-item> | |||||
</el-form> | |||||
<el-form-item label="数据集描述"> | |||||
<el-input | |||||
v-model="state.form.remark" | |||||
type="textarea" | |||||
placeholder="数据集描述长度不能超过100字" | |||||
maxlength="100" | |||||
rows="3" | |||||
show-word-limit | |||||
/> | |||||
</el-form-item> | |||||
</el-form> | |||||
</template> | |||||
<template v-else> | |||||
<ImportDataset /> | |||||
</template> | |||||
</BaseModal> | </BaseModal> | ||||
</template> | </template> | ||||
<script> | <script> | ||||
@@ -170,6 +172,7 @@ import { validateName } from '@/utils/validate'; | |||||
import { getLabelGroupList } from '@/api/preparation/labelGroup'; | import { getLabelGroupList } from '@/api/preparation/labelGroup'; | ||||
import { add } from '@/api/preparation/dataset'; | import { add } from '@/api/preparation/dataset'; | ||||
import ImportDataset from './import-dataset'; | |||||
const annotationByDataType = annotationBy('dataType'); | const annotationByDataType = annotationBy('dataType'); | ||||
@@ -192,6 +195,7 @@ export default { | |||||
components: { | components: { | ||||
BaseModal, | BaseModal, | ||||
InfoRadio, | InfoRadio, | ||||
ImportDataset, | |||||
}, | }, | ||||
props: { | props: { | ||||
visible: Boolean, | visible: Boolean, | ||||
@@ -232,6 +236,7 @@ export default { | |||||
visible: props.visible, | visible: props.visible, | ||||
loading: false, // 数据集创建进行中 | loading: false, // 数据集创建进行中 | ||||
customAnnotationType: null, | customAnnotationType: null, | ||||
datasetRadio: 0, | |||||
}); | }); | ||||
const labelGroupOptions = ref(initialLabelGroupOptions); | const labelGroupOptions = ref(initialLabelGroupOptions); | ||||
@@ -255,6 +260,7 @@ export default { | |||||
() => | () => | ||||
enableLabelGroup(state.form.annotateType) && state.form.dataType !== dataTypeCodeMap.CUSTOM | enableLabelGroup(state.form.annotateType) && state.form.dataType !== dataTypeCodeMap.CUSTOM | ||||
); | ); | ||||
const okText = computed(() => (state.datasetRadio ? '知道了' : '确定')); | |||||
const setForm = (params) => | const setForm = (params) => | ||||
Object.assign(state, { | Object.assign(state, { | ||||
@@ -388,12 +394,13 @@ export default { | |||||
type: 0, | type: 0, | ||||
}, | }, | ||||
loading: false, | loading: false, | ||||
datasetRadio: 0, | |||||
}); | }); | ||||
toggleVisible(); | toggleVisible(); | ||||
onResetFresh(); | onResetFresh(); | ||||
}; | }; | ||||
const handleOk = () => { | |||||
const onSubmitDataset = () => { | |||||
formRef.value.validate((valid) => { | formRef.value.validate((valid) => { | ||||
if (!valid) return; | if (!valid) return; | ||||
const params = omit(state.form, ['labelGroup']); | const params = omit(state.form, ['labelGroup']); | ||||
@@ -411,6 +418,14 @@ export default { | |||||
}); | }); | ||||
}; | }; | ||||
const handleOk = () => { | |||||
state.datasetRadio ? toggleVisible() : onSubmitDataset(); | |||||
}; | |||||
const onDatasetRadioChange = () => { | |||||
resetForm(); | |||||
}; | |||||
watch( | watch( | ||||
() => props.visible, | () => props.visible, | ||||
(next) => { | (next) => { | ||||
@@ -445,7 +460,9 @@ export default { | |||||
allAnnotationList, | allAnnotationList, | ||||
handleClose, | handleClose, | ||||
handleOk, | handleOk, | ||||
okText, | |||||
rules, | rules, | ||||
onDatasetRadioChange, | |||||
showlabelGroup, | showlabelGroup, | ||||
}; | }; | ||||
}, | }, | ||||
@@ -1,341 +1,77 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<BaseModal | |||||
:key="state.formKey" | |||||
title="导入本地数据集" | |||||
width="600px" | |||||
center | |||||
:loading="state.loading" | |||||
:visible="state.visible" | |||||
@change="handleClose" | |||||
@ok="handleOk" | |||||
> | |||||
<el-form ref="formRef" :model="state.form" :rules="rules" label-width="100px"> | |||||
<el-alert class="info-alert" type="warning" show-icon :closable="false"> | |||||
<div slot="title" class="slot-content"> | |||||
<div>数据集创建完毕后,需要使用脚本工具上传本地已有数据集</div> | |||||
<a :href="`${VUE_APP_DOCS_URL}module/dataset/util`" target="_blank">使用文档</a> | |||||
</div> | |||||
</el-alert> | |||||
<el-form-item label="数据集名称" prop="name"> | |||||
<el-input v-model="state.form.name" placeholder="数据集名称不能超过50字" maxlength="50" /> | |||||
</el-form-item> | |||||
<el-form-item label="数据集来源" prop="sourceType"> | |||||
<InfoRadio v-model="state.form.sourceType" :dataSource="sourceTypeList" /> | |||||
<div> | |||||
标准数据集是指天枢平台预置支持的部分数据集类型, | |||||
<a | |||||
target="_blank" | |||||
type="primary" | |||||
:underline="false" | |||||
class="primary" | |||||
:href="`${VUE_APP_DOCS_URL}module/dataset/intro`" | |||||
>详细参考</a | |||||
> | |||||
</div> | |||||
</el-form-item> | |||||
<el-form-item v-if="!sourceByCustom" label="数据类型" prop="dataType"> | |||||
<InfoRadio | |||||
v-model="state.form.dataType" | |||||
:dataSource="dataTypeList" | |||||
:transformOptions="transformOptions" | |||||
type="button" | |||||
@change="handleDataTypeChange" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item v-if="!sourceByCustom" label="标注类型" prop="annotateType"> | |||||
<InfoSelect | |||||
v-model="state.form.annotateType" | |||||
placeholder="标注类型" | |||||
:dataSource="annotationList" | |||||
width="200px" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item v-else label="模型类型" prop="annotateType"> | |||||
<InfoSelect | |||||
v-model="state.form.annotateType" | |||||
placeholder="模型类型" | |||||
:dataSource="allAnnotationList" | |||||
width="200px" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="数据集描述"> | |||||
<el-input | |||||
v-model="state.form.remark" | |||||
type="textarea" | |||||
placeholder="数据集描述长度不能超过100字" | |||||
maxlength="100" | |||||
rows="3" | |||||
show-word-limit | |||||
/> | |||||
</el-form-item> | |||||
</el-form> | |||||
</BaseModal> | |||||
<div> | |||||
<div class="flex flex-between bg"> | |||||
<span> | |||||
<i class="el-icon-warning" style="color: #3253d6;" /> | |||||
天枢命令行工具支持导入本地已有自定义数据集、标准数据集 | |||||
</span> | |||||
<a | |||||
class="primary" | |||||
href="http://docs.tianshu.org.cn/docs/module/dataset/cli/new" | |||||
target="_blank" | |||||
> | |||||
使用文档 | |||||
</a> | |||||
</div> | |||||
<div v-for="item in datasetCode" :key="item.id"> | |||||
<span class="db mb-10 mt-20">{{ item.text }}</span> | |||||
<pre class="code flex flex-vertical-align flex-between"> | |||||
<code class="text ellipsis">{{item.code}}</code> | |||||
<copy-to-clipboard :text="item.code" @copy="handleCopy"> | |||||
<i class="el-icon-copy-document pointer copy" /> | |||||
</copy-to-clipboard> | |||||
</pre> | |||||
</div> | |||||
</div> | |||||
</template> | </template> | ||||
<script> | <script> | ||||
import { reactive, watch, ref, computed } from '@vue/composition-api'; | |||||
import { Message } from 'element-ui'; | import { Message } from 'element-ui'; | ||||
import { omit } from 'lodash'; | |||||
import BaseModal from '@/components/BaseModal'; | |||||
import InfoRadio from '@/components/InfoRadio'; | |||||
import InfoSelect from '@/components/InfoSelect'; | |||||
import { validateName } from '@/utils/validate'; | |||||
import { | |||||
annotationBy, | |||||
dataTypeMap, | |||||
dataTypeCodeMap, | |||||
annotationCodeMap, | |||||
annotationMap, | |||||
transformMapToList, | |||||
} from '@/views/dataset/util'; | |||||
import { add } from '@/api/preparation/dataset'; | |||||
const annotationByDataType = annotationBy('dataType'); | |||||
import CopyToClipboard from 'vue-copy-to-clipboard'; | |||||
import { datasetCode } from '../util'; | |||||
export default { | export default { | ||||
name: 'ImportDataset', | name: 'ImportDataset', | ||||
components: { | components: { | ||||
BaseModal, | |||||
InfoRadio, | |||||
InfoSelect, | |||||
CopyToClipboard, | |||||
}, | }, | ||||
props: { | |||||
visible: { | |||||
type: Boolean, | |||||
default: false, | |||||
}, | |||||
toggleVisible: { | |||||
type: Function, | |||||
}, | |||||
onResetFresh: { | |||||
type: Function, | |||||
}, | |||||
}, | |||||
setup(props) { | |||||
const { toggleVisible, onResetFresh } = props; | |||||
const initialForm = { | |||||
name: '', | |||||
dataType: 0, | |||||
annotateType: null, | |||||
remark: '', | |||||
loading: false, | |||||
sourceType: 0, | |||||
}; | |||||
const formRef = ref(null); | |||||
// 标准数据集白名单:图像分类、目标检测、语义分割 | |||||
// 文本分类 | |||||
// 音频分类 | |||||
const stdAnnotateType = [ | |||||
annotationCodeMap.ANNOTATE, | |||||
annotationCodeMap.CLASSIFY, | |||||
annotationCodeMap.SEGMENTATION, | |||||
annotationCodeMap.TEXTCLASSIFY, | |||||
annotationCodeMap.AUDIOCLASSIFY, | |||||
]; | |||||
const rules = { | |||||
name: [ | |||||
{ | |||||
required: true, | |||||
message: '请输入数据集名称', | |||||
trigger: ['change', 'blur'], | |||||
}, | |||||
{ validator: validateName, trigger: ['change', 'blur'] }, | |||||
], | |||||
sourceType: [{ required: true, message: '请选择数据集来源', trigger: 'change' }], | |||||
dataType: [{ required: true, message: '请选择数据类型', trigger: 'change' }], | |||||
annotateType: [{ required: true, message: '请选择模型类型', trigger: ['change', 'blur'] }], | |||||
}; | |||||
const state = reactive({ | |||||
form: initialForm, | |||||
formKey: 1, | |||||
visible: props.visible, | |||||
loading: false, // 数据集创建进行中 | |||||
}); | |||||
const sourceTypeList = [ | |||||
{ | |||||
label: '自定义数据集', | |||||
value: 0, | |||||
}, | |||||
{ | |||||
label: '标准数据集', | |||||
value: 1, | |||||
}, | |||||
]; | |||||
// 是否为自定义来源 | |||||
const sourceByCustom = computed(() => state.form.sourceType === 0); | |||||
const dataTypeList = computed(() => { | |||||
const transformed = transformMapToList( | |||||
omit(dataTypeMap, [dataTypeCodeMap.TABLE, dataTypeCodeMap.CUSTOM, dataTypeCodeMap.VIDEO]) | |||||
); | |||||
return transformed.map((d) => ({ | |||||
...d, | |||||
value: Number(d.value), | |||||
})); | |||||
}); | |||||
const annotationList = computed(() => | |||||
annotationByDataType(state.form.dataType) | |||||
.filter((d) => stdAnnotateType.includes(d.code)) | |||||
.map((d) => ({ | |||||
value: d.code, | |||||
label: d.name, | |||||
})) | |||||
); | |||||
const allAnnotationList = computed(() => { | |||||
return Object.keys(annotationMap).map((d) => ({ | |||||
label: annotationMap[d].name, | |||||
value: annotationMap[d].code, | |||||
code: annotationMap[d].code, | |||||
})); | |||||
}); | |||||
const setForm = (params) => | |||||
Object.assign(state, { | |||||
form: { | |||||
...state.form, | |||||
...params, | |||||
}, | |||||
}); | |||||
// 更新加载状态 | |||||
const setLoading = (loading) => Object.assign(state, { loading }); | |||||
// 重置状态(reactive mutate 原始对象) | |||||
const resetForm = () => | |||||
Object.assign(state, { | |||||
form: { | |||||
name: '', | |||||
dataType: 0, | |||||
sourceType: 0, | |||||
annotateType: 2, | |||||
remark: '', | |||||
}, | |||||
}); | |||||
const handleDataTypeChange = () => { | |||||
// 默认定位到第一个标注场景 | |||||
if (annotationList.value.length) { | |||||
setForm({ | |||||
annotateType: annotationList.value[0].value, | |||||
}); | |||||
} | |||||
}; | |||||
const selectAnnotationType = (item) => { | |||||
if (item.code === Number(state.form.annotateType)) return; | |||||
setForm({ | |||||
annotateType: item.code, | |||||
}); | |||||
setup() { | |||||
const handleCopy = () => { | |||||
Message.success('复制成功'); | |||||
}; | }; | ||||
const handleClose = () => { | |||||
Object.assign(state, { | |||||
formKey: state.formKey + 1, | |||||
// reactive mutate 原始对象 | |||||
form: { | |||||
name: '', | |||||
dataType: 0, | |||||
sourceType: 0, | |||||
annotateType: 2, | |||||
remark: '', | |||||
}, | |||||
loading: false, | |||||
}); | |||||
toggleVisible(false); | |||||
onResetFresh(); | |||||
}; | |||||
const handleOk = () => { | |||||
formRef.value.validate((valid) => { | |||||
if (!valid) return; | |||||
const params = { | |||||
type: 0, | |||||
import: true, | |||||
name: state.form.name, | |||||
remark: state.form.remark, | |||||
annotateType: state.form.annotateType, | |||||
}; | |||||
// 区分自定义数据集、标注数据集 | |||||
state.form.sourceType === 0 | |||||
? Object.assign(params, { | |||||
dataType: dataTypeCodeMap.CUSTOM, | |||||
}) | |||||
: Object.assign(params, { | |||||
dataType: state.form.dataType, | |||||
}); | |||||
setLoading(true); | |||||
add(params) | |||||
.then(() => { | |||||
Message.success('数据集创建成功,请下载数据集脚本工具进行下一步操作'); | |||||
resetForm(); | |||||
toggleVisible(false); | |||||
}) | |||||
.finally(() => { | |||||
setLoading(false); | |||||
}); | |||||
}); | |||||
}; | |||||
const transformOptions = (list) => { | |||||
return list.map((d) => ({ | |||||
...d, | |||||
label: d.label, | |||||
value: Number(d.value), | |||||
})); | |||||
}; | |||||
watch( | |||||
() => props.visible, | |||||
(next) => { | |||||
Object.assign(state, { | |||||
visible: next, | |||||
}); | |||||
} | |||||
); | |||||
return { | return { | ||||
VUE_APP_DOCS_URL: process.env.VUE_APP_DOCS_URL, | |||||
rules, | |||||
state, | |||||
formRef, | |||||
sourceTypeList, | |||||
sourceByCustom, | |||||
dataTypeList, | |||||
annotationList, | |||||
allAnnotationList, | |||||
transformOptions, | |||||
handleDataTypeChange, | |||||
selectAnnotationType, | |||||
handleClose, | |||||
handleOk, | |||||
datasetCode, | |||||
handleCopy, | |||||
}; | }; | ||||
}, | }, | ||||
}; | }; | ||||
</script> | </script> | ||||
<style lang="scss" scoped> | |||||
@import '@/assets/styles/variables.scss'; | |||||
.bg { | |||||
padding: 10px 20px; | |||||
background: #eef8ff; | |||||
} | |||||
.code { | |||||
height: 40px; | |||||
padding: 0 20px; | |||||
background: #ebedf0; | |||||
} | |||||
.copy { | |||||
font-size: 18px; | |||||
color: $primaryColor; | |||||
} | |||||
</style> |
@@ -1,18 +1,10 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<div class="app-container"> | <div class="app-container"> | ||||
@@ -29,15 +21,6 @@ | |||||
> | > | ||||
创建数据集 | 创建数据集 | ||||
</el-button> | </el-button> | ||||
<el-button | |||||
slot="left" | |||||
class="filter-item" | |||||
icon="el-icon-upload" | |||||
round | |||||
@click="toggleImportDatasetEvent" | |||||
> | |||||
导入数据集 | |||||
</el-button> | |||||
<span slot="right"> | <span slot="right"> | ||||
<!-- 搜索 --> | <!-- 搜索 --> | ||||
<el-input | <el-input | ||||
@@ -69,12 +52,6 @@ | |||||
:toggleVisible="closeCreateDatasetForm" | :toggleVisible="closeCreateDatasetForm" | ||||
:onResetFresh="onResetFresh" | :onResetFresh="onResetFresh" | ||||
/> | /> | ||||
<!--导入自定义数据集表单组件--> | |||||
<ImportDataset | |||||
:visible="importDatasetVisible" | |||||
:toggleVisible="toggleImportDataset" | |||||
:onResetFresh="onResetFresh" | |||||
/> | |||||
<!--单独导入数据表单组件--> | <!--单独导入数据表单组件--> | ||||
<UploadDataFile | <UploadDataFile | ||||
:row="importRow" | :row="importRow" | ||||
@@ -312,6 +289,16 @@ import { isNil, omit, findKey } from 'lodash'; | |||||
import { mapState } from 'vuex'; | import { mapState } from 'vuex'; | ||||
import CopyToClipboard from 'vue-copy-to-clipboard'; | import CopyToClipboard from 'vue-copy-to-clipboard'; | ||||
import CRUD, { presenter, header, form, crud } from '@crud/crud'; | |||||
import rrOperation from '@crud/RR.operation'; | |||||
import cdOperation from '@crud/CD.operation'; | |||||
import { | |||||
publish, | |||||
autoAnnotate, | |||||
annotateStatus, | |||||
delAnnotation, | |||||
track, | |||||
} from '@/api/preparation/annotation'; | |||||
import crudDataset, { | import crudDataset, { | ||||
editDataset, | editDataset, | ||||
detail, | detail, | ||||
@@ -320,16 +307,6 @@ import crudDataset, { | |||||
queryDatasetsProgress, | queryDatasetsProgress, | ||||
queryDatasetStatus, | queryDatasetStatus, | ||||
} from '@/api/preparation/dataset'; | } from '@/api/preparation/dataset'; | ||||
import { | |||||
publish, | |||||
autoAnnotate, | |||||
annotateStatus, | |||||
delAnnotation, | |||||
track, | |||||
} from '@/api/preparation/annotation'; | |||||
import CRUD, { presenter, header, form, crud } from '@crud/crud'; | |||||
import rrOperation from '@crud/RR.operation'; | |||||
import cdOperation from '@crud/CD.operation'; | |||||
import datePickerMixin from '@/mixins/datePickerMixin'; | import datePickerMixin from '@/mixins/datePickerMixin'; | ||||
import { | import { | ||||
@@ -361,7 +338,6 @@ import CreateDataset from './create-dataset'; | |||||
import Status from './status'; | import Status from './status'; | ||||
import Action from './action'; | import Action from './action'; | ||||
import Publish from './publish'; | import Publish from './publish'; | ||||
import ImportDataset from './import-dataset'; | |||||
import DataEnhance from './data-enhance'; | import DataEnhance from './data-enhance'; | ||||
import EditDataset from './edit-dataset'; | import EditDataset from './edit-dataset'; | ||||
import UploadDataFile from './upload-datafile'; | import UploadDataFile from './upload-datafile'; | ||||
@@ -396,7 +372,6 @@ export default { | |||||
BaseModal, | BaseModal, | ||||
CreateDataset, | CreateDataset, | ||||
Publish, | Publish, | ||||
ImportDataset, | |||||
TenantSelector, | TenantSelector, | ||||
Status, | Status, | ||||
Action, | Action, | ||||
@@ -434,7 +409,6 @@ export default { | |||||
chosenDatasetStatus: 0, | chosenDatasetStatus: 0, | ||||
createDatasetVisible: false, // 创建数据集对话框 | createDatasetVisible: false, // 创建数据集对话框 | ||||
uploadDataFileVisible: false, // 单独导入数据文件的对话框 | uploadDataFileVisible: false, // 单独导入数据文件的对话框 | ||||
importDatasetVisible: false, // 导入自定义数据集对话框 | |||||
enhanceKey: 1000, | enhanceKey: 1000, | ||||
editKey: 1, | editKey: 1, | ||||
currentRow: null, | currentRow: null, | ||||
@@ -685,13 +659,6 @@ export default { | |||||
closeCreateDatasetForm() { | closeCreateDatasetForm() { | ||||
this.createDatasetVisible = false; | this.createDatasetVisible = false; | ||||
}, | }, | ||||
// 导入自定义数据集表单显隐切换 | |||||
toggleImportDataset(visible) { | |||||
this.importDatasetVisible = isNil(visible) ? !this.importDatasetVisible : visible; | |||||
}, | |||||
toggleImportDatasetEvent() { | |||||
return this.toggleImportDataset(); | |||||
}, | |||||
handleCopy(text, result, row) { | handleCopy(text, result, row) { | ||||
this.$set(row, 'copySuccess', false); | this.$set(row, 'copySuccess', false); | ||||
Object.assign(row, { | Object.assign(row, { | ||||
@@ -1,18 +1,10 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | <template> | ||||
<el-dialog | <el-dialog | ||||
@@ -24,6 +16,15 @@ | |||||
:title="state.title" | :title="state.title" | ||||
@close="handleClose" | @close="handleClose" | ||||
> | > | ||||
<div class="flex flex-between py-10 px-20 mb-20" style="background: #eef8ff;"> | |||||
<span> | |||||
<i class="el-icon-warning" style="color: #3253d6;" /> | |||||
单次上传大量文件(2000+)建议下载安装天枢命令行工具 | |||||
</span> | |||||
<a class="primary" href="http://docs.tianshu.org.cn/docs/module/dataset/cli" target="_blank"> | |||||
使用文档 | |||||
</a> | |||||
</div> | |||||
<!--选择上传的文件--> | <!--选择上传的文件--> | ||||
<div v-show="state.uploadStep === 0"> | <div v-show="state.uploadStep === 0"> | ||||
<upload-inline | <upload-inline | ||||
@@ -104,6 +105,7 @@ | |||||
import { last } from 'lodash'; | import { last } from 'lodash'; | ||||
import { reactive, watch, computed, nextTick } from '@vue/composition-api'; | import { reactive, watch, computed, nextTick } from '@vue/composition-api'; | ||||
import { Message } from 'element-ui'; | |||||
import { toFixed } from '@/utils'; | import { toFixed } from '@/utils'; | ||||
import UploadInline from '@/components/UploadForm/inline'; | import UploadInline from '@/components/UploadForm/inline'; | ||||
import { | import { | ||||
@@ -113,7 +115,6 @@ import { | |||||
dataTypeCodeMap, | dataTypeCodeMap, | ||||
} from '@/views/dataset/util'; | } from '@/views/dataset/util'; | ||||
import { submit, submitVideo } from '@/api/preparation/datafile'; | import { submit, submitVideo } from '@/api/preparation/datafile'; | ||||
import { Message } from 'element-ui'; | |||||
// 每次最多上传的文件数量 | // 每次最多上传的文件数量 | ||||
const MAX_FILE_COUNT = 200; | const MAX_FILE_COUNT = 200; | ||||
@@ -14,8 +14,8 @@ | |||||
* ============================================================= | * ============================================================= | ||||
*/ | */ | ||||
import { parseBbox, flatBbox, generateUuid, pos2Array, rawArr2Pos } from '@/utils'; | |||||
import { isNil, pick } from 'lodash'; | import { isNil, pick } from 'lodash'; | ||||
import { parseBbox, flatBbox, generateUuid, pos2Array, rawArr2Pos } from '@/utils'; | |||||
import { bucketName, bucketHost } from '@/utils/minIO'; | import { bucketName, bucketHost } from '@/utils/minIO'; | ||||
const assert = require('assert'); | const assert = require('assert'); | ||||
@@ -581,3 +581,17 @@ export const getIcon = (ext) => { | |||||
const reg = /^(mp4|avi|mkv|mov|wmv|bmp|jpeg|jpg|png|txt|zip|dir|mp3)$/; | const reg = /^(mp4|avi|mkv|mov|wmv|bmp|jpeg|jpg|png|txt|zip|dir|mp3)$/; | ||||
return reg.test(ext) ? ext : 'file'; | return reg.test(ext) ? ext : 'file'; | ||||
}; | }; | ||||
// 导入数据集脚本 | |||||
export const datasetCode = [ | |||||
{ | |||||
id: 0, | |||||
text: '导入自定义数据集', | |||||
code: 'ts-cli dataset import --type=custom --source /Users/myDataset --annotation_type=custom', | |||||
}, | |||||
{ | |||||
id: 1, | |||||
text: '导入标准数据集', | |||||
code: 'ts-cli dataset import --type=ImageClassify --source /Users/myDataset', | |||||
}, | |||||
]; |
@@ -1,18 +1,9 @@ | |||||
/* | |||||
* Copyright 2019-2020 Zheng Jie | |||||
* | |||||
* 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. | |||||
*/ | |||||
/* * Copyright 2019-2020 Zheng Jie * * Licensed under the Apache License, Version 2.0 (the | |||||
"License"); * you may not use this file except in compliance with the License. * You may obtain a | |||||
copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by | |||||
applicable law or agreed to in writing, software * distributed under the License is distributed on | |||||
an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See | |||||
the License for the specific language governing permissions and * limitations under the License. */ | |||||
<template> | <template> | ||||
<div class="app-container"> | <div class="app-container"> | ||||
@@ -242,14 +233,14 @@ | |||||
<script> | <script> | ||||
import Treeselect from '@riophae/vue-treeselect'; | import Treeselect from '@riophae/vue-treeselect'; | ||||
import Editor from '@/components/editor'; | |||||
import { validateName, validateString, validateJSON, hasPermission } from '@/utils'; | |||||
import crudMenu, { getMenusTree } from '@/api/system/menu'; | |||||
import { iconList } from '@/components/IconFont/iconfont'; | |||||
import CRUD, { presenter, header, form, crud } from '@crud/crud'; | import CRUD, { presenter, header, form, crud } from '@crud/crud'; | ||||
import rrOperation from '@crud/RR.operation'; | import rrOperation from '@crud/RR.operation'; | ||||
import cdOperation from '@crud/CD.operation'; | import cdOperation from '@crud/CD.operation'; | ||||
import udOperation from '@crud/UD.operation'; | import udOperation from '@crud/UD.operation'; | ||||
import Editor from '@/components/editor'; | |||||
import { validateName, validateString, validateJSON, hasPermission } from '@/utils'; | |||||
import crudMenu, { getMenusTree } from '@/api/system/menu'; | |||||
import { iconList } from '@/components/IconFont/iconfont'; | |||||
import datePickerMixin from '@/mixins/datePickerMixin'; | import datePickerMixin from '@/mixins/datePickerMixin'; | ||||
import BaseModal from '@/components/BaseModal'; | import BaseModal from '@/components/BaseModal'; | ||||
@@ -271,7 +262,14 @@ const defaultForm = { | |||||
hidden: false, | hidden: false, | ||||
type: 0, | type: 0, | ||||
permission: null, | permission: null, | ||||
extConfig: '{}', | |||||
extConfig: '', | |||||
}; | |||||
const validateExtConfig = (rule, value, callback) => { | |||||
if (value === '') callback(); | |||||
else { | |||||
validateJSON(rule, value, callback); | |||||
} | |||||
}; | }; | ||||
export default { | export default { | ||||
@@ -315,7 +313,7 @@ export default { | |||||
], | ], | ||||
pid: [{ required: true, message: '请选择上级菜单', trigger: 'blur' }], | pid: [{ required: true, message: '请选择上级菜单', trigger: 'blur' }], | ||||
layout: [{ required: true, message: '请选择页面布局', trigger: 'blur' }], | layout: [{ required: true, message: '请选择页面布局', trigger: 'blur' }], | ||||
extConfig: [{ validator: validateJSON, trigger: 'change' }], | |||||
extConfig: [{ validator: validateExtConfig, trigger: 'change' }], | |||||
}, | }, | ||||
}; | }; | ||||
}, | }, | ||||
@@ -19,6 +19,7 @@ export const moduleMap = { | |||||
1: 'notebook', | 1: 'notebook', | ||||
2: 'train', | 2: 'train', | ||||
3: 'serving', | 3: 'serving', | ||||
4: 'tadl', | |||||
}; | }; | ||||
const resourcesPoolTypeMap = { | const resourcesPoolTypeMap = { | ||||
@@ -0,0 +1,34 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<component :is="type" v-bind="chartConfig" :data="chartData" /> | |||||
</template> | |||||
<script> | |||||
import { LineChart, ColumnChart, ScatterChart } from '@opd/g2plot-vue'; | |||||
export default { | |||||
name: 'Chart', | |||||
components: { | |||||
LineChart, | |||||
ColumnChart, | |||||
ScatterChart, | |||||
}, | |||||
props: { | |||||
type: String, | |||||
chartConfig: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
chartData: { | |||||
type: Array, | |||||
default: () => [], | |||||
}, | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,34 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-card> | |||||
<div slot="header"> | |||||
<span>{{ title }}</span> | |||||
<slot name="action" /> | |||||
</div> | |||||
<Chart :type="type" :chartData="chartData" :chartConfig="chartConfig" /> | |||||
<slot name="footer" /> | |||||
</el-card> | |||||
</template> | |||||
<script> | |||||
import Chart from './chart'; | |||||
export default { | |||||
name: 'ChartCard', | |||||
components: { | |||||
Chart, | |||||
}, | |||||
props: { | |||||
title: String, | |||||
type: String, | |||||
chartData: Array, | |||||
chartConfig: Object, | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,59 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="experiment-config"> | |||||
<div> | |||||
<el-button class="left-round-button" @click="toggleSearchSpace">Search Space</el-button> | |||||
</div> | |||||
<div> | |||||
<el-button class="left-round-button" @click="toggleSelectedSpace"> | |||||
Best Selected Space | |||||
</el-button> | |||||
</div> | |||||
<!-- TODO --> | |||||
<!-- <div> | |||||
<el-button class="left-round-button" @click="toggleExpConfig">Experiment Config</el-button> | |||||
</div> --> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
export default { | |||||
name: 'Config', | |||||
props: { | |||||
toggleSearchSpace: { | |||||
type: Function, | |||||
default: () => ({}), | |||||
}, | |||||
toggleSelectedSpace: { | |||||
type: Function, | |||||
default: () => ({}), | |||||
}, | |||||
toggleExpConfig: { | |||||
type: Function, | |||||
default: () => ({}), | |||||
}, | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.experiment-config { | |||||
position: fixed; | |||||
right: -18px; | |||||
top: 100px; | |||||
z-index: 1000; | |||||
.left-round-button { | |||||
margin-bottom: 12px; | |||||
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.08); | |||||
border-radius: 16px 0 0 16px; | |||||
text-align: left; | |||||
min-width: 160px; | |||||
padding-left: 16px; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,230 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="detail-container"> | |||||
<div class="app-page-header-title">实验详情</div> | |||||
<div class="mt-50 flex flex-between"> | |||||
<div class="flex status-box"> | |||||
<div class="mr-10 my-auto"> | |||||
状态:<span :style="statusColor">{{ statusName }}</span> | |||||
</div> | |||||
<div v-if="!isFinished" class="mx-10 my-auto"> | |||||
当前阶段: | |||||
<span class="primary">{{ stageName }}</span> | |||||
</div> | |||||
<template v-else> | |||||
<div class="mx-10 my-auto"> | |||||
最佳精度: | |||||
<span class="primary">{{ detail.bestAccuracy.toFixed(2) }}</span> | |||||
</div> | |||||
<div class="mx-10 my-auto"> | |||||
TRIAL-ID: | |||||
<span class="primary">{{ detail.bestTrialSequence }}</span> | |||||
</div> | |||||
</template> | |||||
</div> | |||||
<div class="flex f1 flex-end"> | |||||
<el-button | |||||
type="text" | |||||
class="primary mr-10" | |||||
icon="el-icon-refresh-right" | |||||
@click="refresh" | |||||
/> | |||||
<div class="app-page-header-extra"> | |||||
<el-dropdown v-show="state.activeTab === stageName" @command="command"> | |||||
<div class="primary mr-10 rel"> | |||||
{{ enableAutoRefresh ? `每${refreshTime}s刷新` : '定时刷新已关闭' }} | |||||
<i class="el-icon-arrow-down el-icon--right" /> | |||||
</div> | |||||
<el-dropdown-menu slot="dropdown"> | |||||
<el-dropdown-item | |||||
v-for="item in refreshControls" | |||||
:key="item.value" | |||||
:icon="item.icon" | |||||
:command="item.value" | |||||
> | |||||
{{ item.label }} | |||||
</el-dropdown-item> | |||||
</el-dropdown-menu> | |||||
</el-dropdown> | |||||
<el-button v-if="enablePause" type="primary" @click="pause"> | |||||
暂停实验 | |||||
</el-button> | |||||
<el-button v-if="enableStart" type="primary" @click="start"> | |||||
启动实验 | |||||
</el-button> | |||||
<el-button v-if="isFinished" type="primary" @click="saveModel"> | |||||
保存模型 | |||||
</el-button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="flex flex-between mt-50"> | |||||
<Description :columns="infoList" /> | |||||
<el-button style="margin: auto auto 0 auto" @click="changeToLog">查看日志</el-button> | |||||
</div> | |||||
<SaveModelDialog ref="saveModelRef" type="tadl" /> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { Message } from 'element-ui'; | |||||
import { reactive, computed, watch, ref, onBeforeUnmount } from '@vue/composition-api'; | |||||
import Description from '@/components/Description'; | |||||
import { parseTime } from '@/utils'; | |||||
import { pauseExp, startExp } from '@/api/tadl'; | |||||
import SaveModelDialog from '@/components/Training/saveModelDialog'; | |||||
import { | |||||
refreshControls, | |||||
runTimeFormatter, | |||||
getModelByCode, | |||||
getExpByCode, | |||||
getStageName, | |||||
STAGE_SEQUENCE, | |||||
} from '../../util'; | |||||
export default { | |||||
name: 'DetailDashboard', | |||||
components: { | |||||
Description, | |||||
SaveModelDialog, | |||||
}, | |||||
props: { | |||||
saveRefreshTime: Function, | |||||
refreshTime: Number, | |||||
refresh: Function, | |||||
updateState: Function, | |||||
detail: Object, | |||||
isFinished: Boolean, | |||||
inProgress: Boolean, | |||||
enablePause: Boolean, | |||||
enableStart: Boolean, | |||||
activePath: Array, | |||||
command: Function, | |||||
}, | |||||
setup(props) { | |||||
const { updateState, refresh, command } = props; | |||||
const saveModelRef = ref(null); | |||||
const state = reactive({ | |||||
activeTab: props.activePath[0], | |||||
prevActiveTab: props.activePath[0], | |||||
}); | |||||
const changeToLog = () => { | |||||
Object.assign(state, { | |||||
prevActiveTab: null, | |||||
activeTab: null, | |||||
}); | |||||
command(0); // 关闭自动刷新 | |||||
updateState({ activePath: ['LOG', 'algrithom'], activeStage: '' }); | |||||
}; | |||||
const pause = async () => { | |||||
await pauseExp(props.detail.id).then(() => { | |||||
Message.success('实验已暂停'); | |||||
refresh(); | |||||
command(0); // 关闭自动刷新 | |||||
}); | |||||
}; | |||||
const start = async () => { | |||||
await startExp(props.detail.id).then(() => { | |||||
Message.success('实验启动中'); | |||||
refresh(); | |||||
command(0); // 关闭自动刷新 | |||||
}); | |||||
}; | |||||
const statusName = computed(() => getExpByCode(props.detail.status, 'label')); | |||||
const stageName = computed(() => getStageName(props.detail.runStage)); | |||||
const statusColor = computed(() => ({ color: getExpByCode(props.detail.status, 'bgColor') })); | |||||
const enableAutoRefresh = computed(() => props.refreshTime > 0); | |||||
const showBestAccuracy = computed(() => props.detail.runStage === STAGE_SEQUENCE.RETRAIN); | |||||
const infoList = computed(() => { | |||||
const runingTime = props.inProgress | |||||
? { label: '运行时间', content: runTimeFormatter(props.detail.runTime) } | |||||
: { label: '结束时间', content: parseTime(props.detail.endTime) }; | |||||
return [ | |||||
[ | |||||
{ label: '实验名称', content: props.detail.name }, | |||||
{ label: '实验 ID', content: props.detail.id }, | |||||
{ label: '模型类别', content: getModelByCode(props.detail.modelType, 'label') }, | |||||
], | |||||
[ | |||||
{ label: '算法名称', content: props.detail.algorithmName }, | |||||
{ label: '算法版本', content: props.detail.algorithmVersion }, | |||||
{ label: '创 建 人', content: props.detail.createUser }, | |||||
], | |||||
[ | |||||
{ label: '开始时间', content: parseTime(props.detail.startTime) }, | |||||
runingTime, | |||||
{ label: '实验描述', content: props.detail.description, span: 2 }, | |||||
], | |||||
]; | |||||
}); | |||||
const saveModel = () => { | |||||
const modelParams = { | |||||
algorithmId: props.detail.algorithmId, | |||||
modelAddress: props.detail.bestCheckpointPath, | |||||
}; | |||||
saveModelRef.value.show(modelParams); | |||||
}; | |||||
watch( | |||||
() => props.activePath, | |||||
(next) => { | |||||
if (next && next.length) { | |||||
Object.assign(state, { | |||||
activeTab: next[0], | |||||
prevActiveTab: next[0], | |||||
}); | |||||
} | |||||
} | |||||
); | |||||
onBeforeUnmount(() => command(0)); | |||||
return { | |||||
state, | |||||
saveModelRef, | |||||
statusColor, | |||||
changeToLog, | |||||
statusName, | |||||
stageName, | |||||
refreshControls, | |||||
enableAutoRefresh, | |||||
showBestAccuracy, | |||||
pause, | |||||
start, | |||||
infoList, | |||||
saveModel, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.detail-container { | |||||
padding: 32px; | |||||
background: #fff; | |||||
box-shadow: 0px 2px 7px 0px rgba(209, 209, 217, 0.5); | |||||
} | |||||
.description-items { | |||||
width: 100%; | |||||
} | |||||
.status-box { | |||||
div { | |||||
margin-right: 72px; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,38 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-card class="app-content-section" shadow="never"> | |||||
<div class="app-content-title mb-20">概览</div> | |||||
<div class="flex flex-vertical-align"> | |||||
<div v-if="!isOneTrial" style="width: 100%"> | |||||
<TrialStat :info="info" /> | |||||
</div> | |||||
<div v-else-if="stage !== 'RETRAIN'" style="width: 100%"> | |||||
<SingleTrialStat /> | |||||
</div> | |||||
</div> | |||||
</el-card> | |||||
</template> | |||||
<script> | |||||
import TrialStat from './stat'; | |||||
import SingleTrialStat from './singleTrialStat'; | |||||
export default { | |||||
name: 'General', | |||||
components: { | |||||
TrialStat, | |||||
SingleTrialStat, | |||||
}, | |||||
props: { | |||||
info: Object, | |||||
stage: String, | |||||
isOneTrial: Boolean, | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,181 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-card shadow="never" class="rel app-content-section"> | |||||
<div class="app-content-title flex flex-between" style="margin: 12px"> | |||||
<span>当前阶段实验参数</span> | |||||
<InfoRadio | |||||
v-model="state.activeParamType" | |||||
type="button" | |||||
:dataSource="paramType" | |||||
@change="handleParamChange" | |||||
/> | |||||
</div> | |||||
<Description v-if="state.activeParamType === 0" :columns="paramList" :data="param"> | |||||
<template v-slot:数据集> | |||||
<el-link type="primary" @click="gotoDataset">{{ datasetName }}</el-link> | |||||
</template> | |||||
</Description> | |||||
<div v-else> | |||||
<YamlEditor ref="yamlRef" :value="state.yaml" @blur="onYamlChange" /> | |||||
<el-button type="primary" class="mt-10" @click="saveParamChange">保存修改</el-button> | |||||
</div> | |||||
</el-card> | |||||
</template> | |||||
<script> | |||||
import yaml from 'js-yaml'; | |||||
import { reactive, computed, ref, watch } from '@vue/composition-api'; | |||||
import { Message, MessageBox } from 'element-ui'; | |||||
import YamlEditor from '@/components/YamlEditor'; | |||||
import InfoRadio from '@/components/InfoRadio'; | |||||
import Description from '@/components/Description'; | |||||
import { propertyAssign, parseTime } from '@/utils'; | |||||
import { updateExpYaml, expYaml } from '@/api/tadl'; | |||||
import { runTimeFormatter, getStageOrder } from '../../util'; | |||||
import { isNull, underlineShiftHump } from '../../strategy/util'; | |||||
export default { | |||||
name: 'ExpParameter', | |||||
components: { | |||||
YamlEditor, | |||||
InfoRadio, | |||||
Description, | |||||
}, | |||||
props: { | |||||
experimentId: String, | |||||
stage: String, | |||||
param: Object, | |||||
progress: Number, | |||||
}, | |||||
setup(props, ctx) { | |||||
const { $router } = ctx.root; | |||||
const stageOrder = getStageOrder(props.stage); | |||||
const state = reactive({ | |||||
activeParamType: 0, | |||||
yamlNotSaved: true, | |||||
yaml: '', | |||||
}); | |||||
const yamlRef = ref(null); | |||||
const paramType = [ | |||||
{ | |||||
label: '查看模式', | |||||
value: 0, | |||||
}, | |||||
{ | |||||
label: '编辑模式', | |||||
value: 1, | |||||
}, | |||||
]; | |||||
const datasetName = computed(() => props.param.datasetName); | |||||
const paramList = computed(() => { | |||||
const runingTime = | |||||
props.progress === 0 | |||||
? { label: '运行时间', content: runTimeFormatter(props.param.runTime) || '暂无数据' } | |||||
: { label: '结束时间', content: parseTime(props.param.endTime) || '暂无数据' }; | |||||
return [ | |||||
[ | |||||
{ label: '数据集' }, | |||||
{ label: '资 源', content: props.param.resourceName }, | |||||
{ label: '算法入口', content: props.param.executeScript }, | |||||
], | |||||
[ | |||||
{ label: '开始时间', content: parseTime(props.param.startTime) || '暂无数据', span: 2 }, | |||||
{ ...runingTime, span: 2 }, | |||||
], | |||||
]; | |||||
}); | |||||
const gotoDataset = () => { | |||||
$router.push({ path: `/data/datasets/${props.param.datasetId}/version` }); | |||||
}; | |||||
const saveParamChange = async () => { | |||||
updateExpYaml(props.experimentId, getStageOrder(props.stage), state.yaml) | |||||
.then(() => { | |||||
Message.success('保存成功'); | |||||
state.yamlNotSaved = false; | |||||
}) | |||||
.catch((err) => { | |||||
Message.error(err.message); | |||||
}); | |||||
}; | |||||
const handleParamChange = async (value) => { | |||||
if (state.activeParamType === 0 && state.yamlNotSaved) { | |||||
await MessageBox.confirm('是否保存当前修改?', '提示', { | |||||
confirmButtonText: '确定', | |||||
cancelButtonText: '取消', | |||||
type: 'warning', | |||||
}) | |||||
.then(() => { | |||||
saveParamChange(); | |||||
}) | |||||
.catch(() => { | |||||
Message.info('当前修改未保存'); | |||||
state.yamlNotSaved = false; | |||||
}); | |||||
state.yamlNotSaved = true; | |||||
state.activeParamType = value; | |||||
} | |||||
}; | |||||
// 直接编辑 Yaml 内容后触发解析 | |||||
const onYamlChange = (yamlValue) => { | |||||
state.yamlNotSaved = true; | |||||
try { | |||||
const yamlLoad = yaml.load(yamlValue); | |||||
if (!yamlLoad) return; | |||||
propertyAssign(state, underlineShiftHump(yamlLoad), (val) => !isNull(val)); | |||||
state.yaml = yamlValue; | |||||
} catch (err) { | |||||
console.error(err); | |||||
if (err.name === 'YAMLException') { | |||||
Message.error('Yaml 解析错误,请检查'); | |||||
} else { | |||||
throw err; | |||||
} | |||||
} | |||||
}; | |||||
watch( | |||||
() => state.activeParamType, | |||||
async (next) => { | |||||
if (next === 1) { | |||||
state.yaml = await expYaml(props.experimentId, stageOrder); | |||||
} | |||||
} | |||||
); | |||||
return { | |||||
yamlRef, | |||||
state, | |||||
paramType, | |||||
gotoDataset, | |||||
datasetName, | |||||
paramList, | |||||
handleParamChange, | |||||
saveParamChange, | |||||
onYamlChange, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.description-items { | |||||
max-width: 80%; | |||||
} | |||||
</style> |
@@ -0,0 +1,282 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="rel app-content-section run-parameter-card"> | |||||
<div class="app-content-title mb-20">当前阶段运行参数</div> | |||||
<el-form ref="form" :model="state.model" label-position="top"> | |||||
<el-row :gutter="16" class="mb-50"> | |||||
<div class="flex flex-between"> | |||||
<div>持续时间</div> | |||||
<div>当前阶段最长持续时间</div> | |||||
</div> | |||||
<div class="flex flex-between"> | |||||
<div class="el-form-item-explain"> | |||||
<span class="primary">{{ duration }}</span> / {{ maxExecDurationStr }} | |||||
</div> | |||||
<div> | |||||
<span>{{ maxExecDurationStr }}</span> | |||||
<Edit | |||||
class="edit-icon" | |||||
:row="state.rawModel" | |||||
valueBy="maxExecDuration" | |||||
title="修改最长持续时间" | |||||
rules="required|validInteger" | |||||
label="时间" | |||||
:beforeChange="validateParam" | |||||
@handleOk="handleMaxExecDurationChange" | |||||
> | |||||
<el-select | |||||
slot="append" | |||||
v-model="state.rawModel.maxExecDurationUnit" | |||||
placeholder="请选择" | |||||
> | |||||
<el-option | |||||
v-for="item in timeFmts" | |||||
:key="item.value" | |||||
:value="item.value" | |||||
:label="item.label" | |||||
/> | |||||
</el-select> | |||||
</Edit> | |||||
</div> | |||||
</div> | |||||
<el-progress :percentage="execDurPercent" :show-text="false"></el-progress> | |||||
</el-row> | |||||
<el-row :gutter="16" class="mb-50"> | |||||
<div class="flex flex-between"> | |||||
<div>Trial数量</div> | |||||
<div>当前阶段最大Trial数量</div> | |||||
</div> | |||||
<div class="flex flex-between"> | |||||
<div class="el-form-item-explain"> | |||||
<span class="primary">{{ state.model.trialNum }}</span> / | |||||
{{ state.model.maxTrialNum }} | |||||
</div> | |||||
<div> | |||||
<span>{{ state.model.maxTrialNum }}</span> | |||||
<Edit | |||||
class="edit-icon" | |||||
:row="state.model" | |||||
:disabled="isOneTrial" | |||||
valueBy="maxTrialNum" | |||||
title="修改最大 Trial 数量" | |||||
rules="required|validInteger" | |||||
label="Trial 数量" | |||||
:beforeChange="validateParam" | |||||
@handleOk="handleMaxTrialNumChange" | |||||
/> | |||||
</div> | |||||
</div> | |||||
<el-progress :percentage="trialPercent" :show-text="false"></el-progress> | |||||
</el-row> | |||||
<el-row :gutter="16"> | |||||
<div class="flex flex-between"> | |||||
<div></div> | |||||
<div>Trial并发数</div> | |||||
</div> | |||||
<div class="flex flex-end"> | |||||
<span>{{ state.model.trialConcurrentNum }}</span> | |||||
<Edit | |||||
class="edit-icon" | |||||
:row="state.model" | |||||
:disabled="isOneTrial" | |||||
valueBy="trialConcurrentNum" | |||||
title="修改 Trial 并发数" | |||||
rules="required|validInteger" | |||||
label="最大 Trial 数量" | |||||
:beforeChange="validateParam" | |||||
@handleOk="handleConcurrentNumChange" | |||||
/> | |||||
</div> | |||||
</el-row> | |||||
</el-form> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { reactive, computed, watch } from '@vue/composition-api'; | |||||
import { Message } from 'element-ui'; | |||||
import { pick } from 'lodash'; | |||||
import Edit from '@/components/InlineTableEdit'; | |||||
import { toFixed } from '@/utils'; | |||||
import { updateConcurrentNum, updateMaxTrialNum, updateMaxExecDuration } from '@/api/tadl'; | |||||
import { runTimeFormatter, timeFmts, parseRunTime, getStageOrder } from '../../util'; | |||||
export default { | |||||
name: 'ExpRunParameter', | |||||
components: { | |||||
Edit, | |||||
}, | |||||
props: { | |||||
param: Object, | |||||
isOneTrial: Boolean, | |||||
experimentId: String, | |||||
stage: String, | |||||
refresh: Function, | |||||
}, | |||||
setup(props) { | |||||
const stageOrder = computed(() => { | |||||
return getStageOrder(props.stage); | |||||
}); | |||||
// TODO: 修改单位不直接修改值,只有点击确认才修改 | |||||
const buildParams = (param) => | |||||
pick(param, ['maxExecDurationUnit', 'maxExecDuration', 'maxTrialNum', 'trialConcurrentNum']); | |||||
const state = reactive({ | |||||
model: props.param, | |||||
rawModel: buildParams(props.param), | |||||
}); | |||||
const maxExecDurationStr = computed(() => { | |||||
const newVal = state.model.maxExecDuration + state.model.maxExecDurationUnit; | |||||
return newVal; | |||||
}); | |||||
const maxExecDuration = computed(() => | |||||
parseRunTime(state.model.maxExecDuration, state.model.maxExecDurationUnit) | |||||
); | |||||
const duration = computed(() => runTimeFormatter(state.model.runTime) || 0); | |||||
const execDurPercent = computed(() => { | |||||
return Math.min(100, toFixed(state.model.runTime / maxExecDuration.value, 2, 0)); | |||||
}); | |||||
const trialPercent = computed(() => { | |||||
return Math.min(100, toFixed(state.model.trialNum / state.model.maxTrialNum, 2, 0)); | |||||
}); | |||||
// 当前校验阶段最长持续时间和最大 trial 数量 | |||||
const applyErrors = (value, row, options) => { | |||||
// 获取修改类型 | |||||
const { valueBy } = options; | |||||
const errors = []; | |||||
if (valueBy === 'maxExecDuration') { | |||||
const changeTime = parseRunTime(value, row.maxExecDurationUnit); | |||||
const curTime = state.model.runTime; | |||||
if (changeTime <= curTime) { | |||||
errors.push('修改后时间不能小于当前运行时间'); | |||||
} | |||||
} else if (valueBy === 'maxTrialNum') { | |||||
if (value < state.model.trialNum) { | |||||
errors.push('修改后最大 trial 数量不能小于当前运行中数量'); | |||||
} | |||||
} else if (valueBy === 'trialConcurrentNum') { | |||||
if (value > state.model.maxTrialNum) { | |||||
errors.push('修改后 trial 并发数量不能大于总的 trial 数量'); | |||||
} | |||||
} | |||||
return errors; | |||||
}; | |||||
// 校验参数合理性 | |||||
const validateParam = (value, row, provider, options = {}) => { | |||||
const errors = applyErrors(value, row, options); | |||||
return new Promise((resolve, reject) => { | |||||
provider.value.applyResult({ | |||||
errors, | |||||
valid: false, // boolean state | |||||
failedRules: {}, // should be empty since this is a manual error. | |||||
}); | |||||
if (errors.length === 0) { | |||||
resolve(true); | |||||
} else { | |||||
reject(errors); | |||||
} | |||||
}); | |||||
}; | |||||
// 运行参数变更 | |||||
const handleParamChange = (value, row, { valueBy }) => { | |||||
const next = { ...state.model, ...row, ...{ [valueBy]: value } }; | |||||
Object.assign(state, { | |||||
model: next, | |||||
rawModel: buildParams(next), | |||||
}); | |||||
}; | |||||
// trial最大并发数变更 | |||||
const handleConcurrentNumChange = (value, row, { valueBy }) => { | |||||
updateConcurrentNum(props.experimentId, stageOrder.value, value).then(() => { | |||||
Message.success('修改运行参数成功'); | |||||
handleParamChange(value, row, { valueBy }); | |||||
props.refresh(); | |||||
}); | |||||
}; | |||||
// trial最大数量变更 | |||||
const handleMaxTrialNumChange = (value, row, { valueBy }) => { | |||||
updateMaxTrialNum(props.experimentId, stageOrder.value, value).then(() => { | |||||
Message.success('修改运行参数成功'); | |||||
handleParamChange(value, row, { valueBy }); | |||||
}); | |||||
props.refresh(); | |||||
}; | |||||
// 最大运行时间变更 | |||||
const handleMaxExecDurationChange = (value, row, { valueBy }) => { | |||||
updateMaxExecDuration( | |||||
props.experimentId, | |||||
stageOrder.value, | |||||
value, | |||||
row.maxExecDurationUnit | |||||
).then(() => { | |||||
Message.success('修改运行参数成功'); | |||||
handleParamChange(value, row, { valueBy }); | |||||
}); | |||||
}; | |||||
watch( | |||||
() => props.param, | |||||
(next) => { | |||||
if (next) { | |||||
Object.assign(state, { | |||||
model: next, | |||||
rawModel: buildParams(next), | |||||
}); | |||||
} | |||||
} | |||||
); | |||||
return { | |||||
state, | |||||
maxExecDurationStr, | |||||
maxExecDuration, | |||||
duration, | |||||
execDurPercent, | |||||
trialPercent, | |||||
timeFmts, | |||||
handleConcurrentNumChange, | |||||
handleMaxTrialNumChange, | |||||
handleMaxExecDurationChange, | |||||
validateParam, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.run-parameter-card { | |||||
border: 1px solid#DFE1E5; | |||||
padding: 32px; | |||||
::v-deep .el-form-item { | |||||
margin-bottom: 0; | |||||
} | |||||
::v-deep .el-form-item__label { | |||||
padding-bottom: 0; | |||||
} | |||||
} | |||||
.ptr { | |||||
padding-top: 22px; | |||||
} | |||||
</style> | |||||
<style lang="scss"> | |||||
.el-select .el-input { | |||||
min-width: 80px; | |||||
} | |||||
</style> |
@@ -0,0 +1,134 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<BaseModal | |||||
:visible="state.visible" | |||||
title="保存模型" | |||||
:loading="state.loading" | |||||
@change="handleClose" | |||||
@cancel="handleClose" | |||||
@ok="handleSave" | |||||
> | |||||
<el-form ref="saveForm" :model="state.saveForm" label-width="80px"> | |||||
<el-form-item label="模型名称" prop="modelName"> | |||||
<el-input | |||||
v-model="state.saveForm.modelName" | |||||
placeholder="请输入模型名称" | |||||
maxlength="50" | |||||
show-word-limit | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="框架" prop="frameType"> | |||||
<el-input v-model="state.saveForm.frameType" disabled /> | |||||
</el-form-item> | |||||
<el-form-item label="模型格式" prop="modelType"> | |||||
<el-select | |||||
v-model="state.saveForm.modelType" | |||||
placeholder="请选择模型格式" | |||||
filterable | |||||
style="width: 300px;" | |||||
disabled | |||||
> | |||||
</el-select> | |||||
</el-form-item> | |||||
<el-form-item label="模型类别" prop="modelClassName"> | |||||
<el-select v-model="state.saveForm.modelClassName" disabled></el-select> | |||||
</el-form-item> | |||||
<el-form-item label="描述" prop="modelDescription"> | |||||
<el-input | |||||
v-model.trim="state.saveForm.modelDescription" | |||||
type="textarea" | |||||
placeholder="请输入模型描述" | |||||
maxlength="255" | |||||
show-word-limit | |||||
/> | |||||
</el-form-item> | |||||
</el-form> | |||||
</BaseModal> | |||||
</template> | |||||
<script> | |||||
import { reactive, watch, nextTick } from '@vue/composition-api'; | |||||
import { Message } from 'element-ui'; | |||||
import BaseModal from '@/components/BaseModal'; | |||||
import { add as saveModel } from '@/api/model/model'; | |||||
import { getModelByCode } from '../../util'; | |||||
export default { | |||||
name: 'SaveModelModal', | |||||
components: { BaseModal }, | |||||
props: { | |||||
detail: Object, | |||||
}, | |||||
setup(props) { | |||||
const state = reactive({ | |||||
saveForm: { | |||||
modelName: props.detail.name, | |||||
modelClassName: getModelByCode(props.detail.modelType, 'label'), | |||||
modelDescription: '', | |||||
frameType: 'pytorch', | |||||
modelType: 'pth', | |||||
}, | |||||
visible: false, | |||||
loading: false, | |||||
}); | |||||
const handleClose = () => { | |||||
state.visible = false; | |||||
state.saveForm = { | |||||
modelName: props.detail.name, | |||||
modelClassName: getModelByCode(props.detail.modelType, 'label'), | |||||
modelDescription: '', | |||||
frameType: 'pytorch', | |||||
modelType: 'pth', | |||||
}; | |||||
}; | |||||
const handleShow = () => { | |||||
state.visible = true; | |||||
}; | |||||
const handleSave = () => { | |||||
saveModel({ | |||||
modelClassName: getModelByCode(props.detail.modelType, 'label'), | |||||
modelDescription: state.saveForm.modelDescription, | |||||
name: state.saveForm.modelName, | |||||
modelSource: 1, | |||||
frameType: 3, | |||||
modelType: 8, | |||||
}).then(() => { | |||||
Message.success('保存成功'); | |||||
handleClose(); | |||||
}); | |||||
}; | |||||
watch( | |||||
() => props.detail, | |||||
() => { | |||||
nextTick(() => { | |||||
state.saveForm = { | |||||
modelName: props.detail.name, | |||||
modelClassName: getModelByCode(props.detail.modelType, 'label'), | |||||
modelDescription: '', | |||||
frameType: 'pytorch', | |||||
modelType: 'pth', | |||||
}; | |||||
}); | |||||
} | |||||
); | |||||
return { | |||||
state, | |||||
handleClose, | |||||
handleShow, | |||||
handleSave, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,63 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="flex flex-wrap" style="margin: 0 10%;"> | |||||
<div v-for="item in stats" :key="item.label" style="width: 50%; margin-bottom: 20px;"> | |||||
<Statistic | |||||
:title="item.label" | |||||
:value="item.value" | |||||
class="styledBorder" | |||||
:style="{ borderLeftColor: item.color }" | |||||
/> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import Statistic from '@/components/Statistic'; | |||||
export default { | |||||
name: 'SingleTrialStat', | |||||
components: { | |||||
Statistic, | |||||
}, | |||||
props: { | |||||
info: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
}, | |||||
setup() { | |||||
const stats = [ | |||||
{ value: 1, label: 'trial 数', color: '#52C41A' }, | |||||
{ value: 0.98, label: '最佳精度', color: '#F5222D' }, | |||||
]; | |||||
return { | |||||
stats, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.styledBorder { | |||||
padding-left: 10px; | |||||
border-left-width: 4px; | |||||
border-left-style: solid; | |||||
margin-bottom: 40px; | |||||
::v-deep { | |||||
.el-statistic-title { | |||||
font-size: 16px; | |||||
} | |||||
.el-statistic-content { | |||||
font-size: 36px; | |||||
line-height: 54px; | |||||
} | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,69 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="flex flex-wrap" style="margin: 0 10%;"> | |||||
<div v-for="item in stats" :key="item.label" style="width: 50%; margin-bottom: 20px;"> | |||||
<Statistic | |||||
:title="item.label" | |||||
:value="item.value" | |||||
class="styledBorder" | |||||
:style="{ borderLeftColor: item.color }" | |||||
/> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { computed } from '@vue/composition-api'; | |||||
import Statistic from '@/components/Statistic'; | |||||
import { TRIAL_STATUS_MAP } from '../../util'; | |||||
export default { | |||||
name: 'TrialStat', | |||||
components: { | |||||
Statistic, | |||||
}, | |||||
props: { | |||||
info: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
}, | |||||
setup(props) { | |||||
const stats = computed(() => | |||||
Object.keys(TRIAL_STATUS_MAP).map((key) => ({ | |||||
label: TRIAL_STATUS_MAP[key].label, | |||||
value: props.info[key], | |||||
color: TRIAL_STATUS_MAP[key].bgColor, | |||||
})) | |||||
); | |||||
return { | |||||
stats, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.styledBorder { | |||||
padding-left: 10px; | |||||
border-left-width: 4px; | |||||
border-left-style: solid; | |||||
margin-bottom: 40px; | |||||
::v-deep { | |||||
.el-statistic-title { | |||||
font-size: 16px; | |||||
} | |||||
.el-statistic-content { | |||||
font-size: 36px; | |||||
line-height: 54px; | |||||
} | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,98 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-card shadow="never" class="rel app-content-section trials-card"> | |||||
<div class="app-content-title mb-20">Trials(最新 5 条)</div> | |||||
<BaseTable :columns="columns" :data="state.list" /> | |||||
<div class="mt-10"><el-link @click="changeToTrialsList">查看全部</el-link></div> | |||||
</el-card> | |||||
</template> | |||||
<script> | |||||
import { reactive, onMounted, computed } from '@vue/composition-api'; | |||||
import { expStageTrialRep } from '@/api/tadl'; | |||||
import BaseTable from '@/components/BaseTable'; | |||||
import { getStageOrder, getTrialByCode, runTimeFormatter } from '../../util'; | |||||
export default { | |||||
name: 'Trials', | |||||
components: { | |||||
BaseTable, | |||||
}, | |||||
props: { | |||||
stage: String, | |||||
changeTab: Function, | |||||
}, | |||||
setup(props, ctx) { | |||||
const { $route } = ctx.root; | |||||
const { params = {} } = $route; | |||||
const { experimentId } = params; | |||||
const { changeTab } = props; | |||||
const stageOrder = getStageOrder(props.stage); | |||||
const state = reactive({ | |||||
list: [], | |||||
}); | |||||
const changeToTrialsList = () => { | |||||
changeTab({ name: 'trials' }); | |||||
}; | |||||
const columns = computed(() => [ | |||||
{ | |||||
prop: 'sequence', | |||||
label: 'Run', | |||||
formatter: (value) => `RUN ${value}`, | |||||
}, | |||||
{ | |||||
prop: 'trialId', | |||||
label: 'Trial Id', | |||||
}, | |||||
{ | |||||
prop: 'status', | |||||
label: '状态', | |||||
type: 'tag', | |||||
tagAttr: { | |||||
style: (col) => ({ | |||||
color: getTrialByCode(col.status, 'bgColor'), | |||||
borderColor: getTrialByCode(col.status, 'bgColor'), | |||||
}), | |||||
}, | |||||
formatter: (value) => getTrialByCode(value, 'label'), | |||||
}, | |||||
{ | |||||
prop: 'runTime', | |||||
label: '持续时间', | |||||
formatter: runTimeFormatter, | |||||
}, | |||||
{ | |||||
prop: 'value', | |||||
label: 'accuracy', | |||||
}, | |||||
]); | |||||
onMounted(async () => { | |||||
const data = await expStageTrialRep(experimentId, stageOrder); | |||||
state.list = data; | |||||
}); | |||||
return { | |||||
state, | |||||
changeToTrialsList, | |||||
columns, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.trials-card { | |||||
height: 400px; | |||||
} | |||||
</style> |
@@ -0,0 +1,383 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-card shadow="never" class="rel app-content-section trials-card"> | |||||
<div class="app-content-title mb-20">Trials</div> | |||||
<ProTable | |||||
ref="proTable" | |||||
:showCreate="false" | |||||
:columns="columns" | |||||
:list-request="list" | |||||
:list-options="listOptions" | |||||
showRefresh | |||||
> | |||||
<div slot="left"> | |||||
<el-button :disabled="contrastDisabled" @click="showContrast"> | |||||
{{ contrastTitle }} | |||||
</el-button> | |||||
</div> | |||||
</ProTable> | |||||
<!-- 保存制品弹窗 --> | |||||
<BaseModal | |||||
:key="`prod${state.prodKey}`" | |||||
:visible="state.actionModal.show && state.actionModal.type === 'prod'" | |||||
title="保存制品" | |||||
:loading="state.actionModal.showOkLoading" | |||||
@change="handleCancel" | |||||
@ok="saveProd" | |||||
> | |||||
<el-form ref="saveForm" :model="state.saveForm" label-width="80px"> | |||||
<el-form-item label="制品名称" prop="prodName"> | |||||
<el-input | |||||
v-model.trim="state.saveForm.prodName" | |||||
placeholder="制品名称长度不能超过50字" | |||||
maxlength="50" | |||||
show-word-limit | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="描述" prop="description"> | |||||
<el-input | |||||
v-model.trim="state.saveForm.description" | |||||
type="textarea" | |||||
placeholder="制品描述长度不能超过100字" | |||||
maxlength="100" | |||||
rows="3" | |||||
show-word-limit | |||||
/> | |||||
</el-form-item> | |||||
</el-form> | |||||
</BaseModal> | |||||
<!-- trials对比弹窗 --> | |||||
<BaseModal | |||||
:key="`visual${state.visualKey}`" | |||||
:visible="state.actionModal.show && state.actionModal.type === 'visual'" | |||||
:title="visualTitle" | |||||
:loading="state.actionModal.showOkLoading" | |||||
:showCancel="false" | |||||
@change="handleCancel" | |||||
@ok="handleCancel" | |||||
> | |||||
<div v-if="!isEmpty(state.contrastChartConfig) && !isEmpty(state.contrastChartData)"> | |||||
<Chart | |||||
type="LineChart" | |||||
:chartConfig="state.contrastChartConfig" | |||||
:chartData="state.contrastChartData" | |||||
style="height: 400px" | |||||
/> | |||||
</div> | |||||
<div v-else>获取绘图数据失败</div> | |||||
</BaseModal> | |||||
<!-- 查看日志弹窗 --> | |||||
<BaseModal | |||||
:key="`log${state.logKey}`" | |||||
class="trialLogModal" | |||||
:visible="state.actionModal.show && state.actionModal.type === 'log'" | |||||
:loading="state.actionModal.showOkLoading" | |||||
title="trial日志" | |||||
width="50" | |||||
:showCancel="false" | |||||
@change="handleCancel" | |||||
@ok="handleCancel" | |||||
> | |||||
<PodLogContainer ref="podLogContainer" :pod="logOptions" /> | |||||
</BaseModal> | |||||
<!-- 查看参数弹窗 --> | |||||
<BaseModal | |||||
:key="`param${state.paramKey}`" | |||||
:visible="state.actionModal.show && state.actionModal.type === 'param'" | |||||
:loading="state.actionModal.showOkLoading" | |||||
title="trial参数" | |||||
:showCancel="false" | |||||
@change="handleCancel" | |||||
@ok="handleCancel" | |||||
> | |||||
参数 | |||||
</BaseModal> | |||||
</el-card> | |||||
</template> | |||||
<script> | |||||
import { reactive, computed, ref, watch } from '@vue/composition-api'; | |||||
import { Message } from 'element-ui'; | |||||
import { isEmpty } from 'lodash'; | |||||
import { expStageTrialList as list, expStageIntermediate } from '@/api/tadl'; | |||||
import { getPodLog } from '@/api/system/pod'; | |||||
import ProTable from '@/components/ProTable'; | |||||
import BaseModal from '@/components/BaseModal'; | |||||
import PodLogContainer from '@/components/LogContainer/podLogContainer'; | |||||
import { | |||||
getStageOrder, | |||||
getTrialByCode, | |||||
runTimeFormatter, | |||||
extractSeriesData, | |||||
TRIAL_STATUS_MAP, | |||||
} from '../../util'; | |||||
import Chart from './chart'; | |||||
import { allTrialStatusList } from '../util'; | |||||
export default { | |||||
name: 'TrialsList', | |||||
components: { | |||||
ProTable, | |||||
BaseModal, | |||||
Chart, | |||||
PodLogContainer, | |||||
}, | |||||
props: { | |||||
stage: String, | |||||
activeTab: String, | |||||
contrastTitle: String, | |||||
createUserId: Number, | |||||
}, | |||||
setup(props, ctx) { | |||||
const { $route } = ctx.root; | |||||
const { params = {} } = $route; | |||||
const { experimentId } = params; | |||||
const stageOrder = getStageOrder(props.stage); | |||||
const podLogContainer = ref(null); | |||||
const listOptions = computed(() => { | |||||
return { | |||||
experimentId, | |||||
stageOrder, | |||||
}; | |||||
}); | |||||
const defaultConfig = { | |||||
autoFit: true, | |||||
xField: null, | |||||
yField: null, | |||||
seriesField: null, | |||||
smooth: false, // 平滑曲线 | |||||
xAxis: { | |||||
title: { | |||||
text: 'sequence', | |||||
spacing: 30, | |||||
style: { | |||||
fontSize: 20, | |||||
}, | |||||
}, | |||||
}, | |||||
yAxis: { | |||||
title: { | |||||
text: '中间值', | |||||
autoRotate: false, | |||||
textStyle: { | |||||
fontSize: 20, | |||||
width: 20, | |||||
}, | |||||
position: 'center', | |||||
}, | |||||
}, | |||||
}; | |||||
const proTable = ref(null); | |||||
const state = reactive({ | |||||
isContrast: true, | |||||
contrastChartConfig: {}, | |||||
contrastChartData: [], | |||||
saveForm: {}, | |||||
actionModal: { | |||||
show: false, | |||||
row: undefined, | |||||
showOkLoading: false, | |||||
type: null, | |||||
}, | |||||
logKey: 1, | |||||
paramKey: 1, | |||||
prodKey: 1, | |||||
visualKey: 1, | |||||
activePod: '', | |||||
}); | |||||
const contrastDisabled = computed(() => proTable.value?.state.selectedRows.length <= 1); | |||||
const visualTitle = computed(() => (state.isContrast ? 'trials对比' : '')); | |||||
const showActionModal = (row, type) => { | |||||
Object.assign(state, { | |||||
actionModal: { | |||||
show: true, | |||||
row, | |||||
showOkLoading: false, | |||||
type, | |||||
}, | |||||
}); | |||||
}; | |||||
const resetLogger = () => { | |||||
setTimeout(() => { | |||||
podLogContainer.value.reset(true); | |||||
}, 0); | |||||
}; | |||||
const logOptions = computed(() => { | |||||
return { | |||||
podName: state.actionModal.row?.podName, | |||||
namespace: `namespace-${props.createUserId}`, | |||||
}; | |||||
}); | |||||
const showLog = async (row) => { | |||||
showActionModal(row, 'log'); | |||||
resetLogger(); | |||||
}; | |||||
const showVisual = async (row, isContrast = true) => { | |||||
showActionModal(row, 'visual'); | |||||
const contrastRowIds = row.map((d) => d.id); | |||||
const contrastMetric = await expStageIntermediate(experimentId, stageOrder, contrastRowIds); | |||||
Object.assign(state, { | |||||
isContrast, | |||||
contrastChartData: extractSeriesData(contrastMetric), | |||||
contrastChartConfig: { | |||||
...defaultConfig, | |||||
...contrastMetric.config, | |||||
xAxis: { title: { text: contrastMetric.config.xFieldName } }, | |||||
yAxis: { title: { text: contrastMetric.config.yFieldName } }, | |||||
}, | |||||
}); | |||||
}; | |||||
const showSingleVisual = (row) => { | |||||
showVisual([{ ...row }], false); | |||||
}; | |||||
const showContrast = () => { | |||||
showVisual(proTable.value?.state.selectedRows); | |||||
}; | |||||
const resetActionModal = () => { | |||||
const keyName = state.actionModal.type.concat('Key'); | |||||
state[keyName] += 1; | |||||
Object.assign(state, { | |||||
actionModal: { | |||||
show: false, | |||||
row: undefined, | |||||
showOkLoading: false, | |||||
type: null, | |||||
}, | |||||
}); | |||||
}; | |||||
const handleCancel = () => { | |||||
resetActionModal(); | |||||
}; | |||||
const saveProd = () => { | |||||
Message.info(state.saveForm, 400); | |||||
handleCancel(); | |||||
}; | |||||
const columns = computed(() => [ | |||||
{ | |||||
prop: 'selections', | |||||
type: 'selection', | |||||
}, | |||||
{ | |||||
prop: 'sequence', | |||||
label: 'Sequence', | |||||
}, | |||||
{ | |||||
prop: 'status', | |||||
label: '状态', | |||||
type: 'tag', | |||||
tagAttr: { | |||||
style: (col) => ({ | |||||
color: getTrialByCode(col.status, 'bgColor'), | |||||
borderColor: getTrialByCode(col.status, 'bgColor'), | |||||
}), | |||||
}, | |||||
formatter: (value) => getTrialByCode(value, 'label'), | |||||
dropdownList: allTrialStatusList, | |||||
}, | |||||
{ | |||||
prop: 'executeScript', | |||||
label: '算法文件', | |||||
}, | |||||
{ | |||||
prop: 'value', | |||||
label: 'accuracy', | |||||
width: '120px', | |||||
}, | |||||
{ | |||||
prop: 'runTime', | |||||
label: '持续时间', | |||||
formatter: runTimeFormatter, | |||||
width: '240px', | |||||
}, | |||||
{ | |||||
prop: 'startTime', | |||||
label: '开始时间', | |||||
width: '240px', | |||||
type: 'time', | |||||
}, | |||||
{ | |||||
prop: 'resourceName', | |||||
label: '计算资源', | |||||
}, | |||||
{ | |||||
label: '操作', | |||||
type: 'operation', | |||||
width: '370px', | |||||
fixed: 'right', | |||||
operations: [ | |||||
{ | |||||
label: '可视化', | |||||
func: showSingleVisual, | |||||
}, | |||||
{ | |||||
label: '查看日志', | |||||
func: showLog, | |||||
hideFunc(row) { | |||||
// 待运行无podname故不可查询k8s日志 | |||||
return [TRIAL_STATUS_MAP.toRun.value, TRIAL_STATUS_MAP.waiting.value].includes( | |||||
row.status | |||||
); | |||||
}, | |||||
}, | |||||
], | |||||
}, | |||||
]); | |||||
watch( | |||||
() => props.activeTab, | |||||
() => { | |||||
proTable.value.refresh(); | |||||
} | |||||
); | |||||
return { | |||||
contrastDisabled, | |||||
visualTitle, | |||||
experimentId, | |||||
stageOrder, | |||||
list, | |||||
listOptions, | |||||
state, | |||||
columns, | |||||
handleCancel, | |||||
showContrast, | |||||
proTable, | |||||
isEmpty, | |||||
saveProd, | |||||
getPodLog, | |||||
logOptions, | |||||
podLogContainer, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss"> | |||||
.trialLogModal { | |||||
.prism-content { | |||||
max-height: 350px; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,513 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="app-container greyBg"> | |||||
<div class="detail-header"> | |||||
<DetailDashboard | |||||
:activePath="state.activePath" | |||||
:detail="state.detail" | |||||
:isFinished="isFinished" | |||||
:inProgress="inProgress" | |||||
:enablePause="enablePause" | |||||
:enableStart="enableStart" | |||||
:refreshTime="state.refreshTime" | |||||
:saveRefreshTime="saveRefreshTime" | |||||
:updateState="updateState" | |||||
:refresh="refresh" | |||||
:command="command" | |||||
/> | |||||
<Config | |||||
:toggleSearchSpace="toggleSearchSpace" | |||||
:toggleSelectedSpace="toggleSelectedSpace" | |||||
:toggleExpConfig="toggleExpConfig" | |||||
/> | |||||
</div> | |||||
<el-drawer title="Search Space" :visible.sync="state.searchSpaceVisible"> | |||||
<TextEditor :txt="state.searchSpace" class="my-auto f1" style="max-height: unset" /> | |||||
</el-drawer> | |||||
<el-drawer title="Best Selected Space" :visible.sync="state.selectedSpaceVisible"> | |||||
<TextEditor :txt="state.selectedSpace" class="my-auto f1" style="max-height: unset" /> | |||||
</el-drawer> | |||||
<el-drawer title="Experiment Config" :visible.sync="state.expConfigVisible"> | |||||
<TextEditor :txt="state.expConfig" class="my-auto f1" style="max-height: unset" /> | |||||
</el-drawer> | |||||
<div class="stage-content"> | |||||
<el-tabs | |||||
v-model="state.activeStage" | |||||
class="stage-tabs el-tabs-large" | |||||
type="card" | |||||
@tab-click="changeTab" | |||||
> | |||||
<el-tab-pane label="TRAIN" name="TRAIN" /> | |||||
<el-tab-pane label="SELECT" name="SELECT" /> | |||||
<el-tab-pane label="RETRAIN" name="RETRAIN" /> | |||||
</el-tabs> | |||||
<el-card v-if="state.activePath[0] === 'LOG'"> | |||||
<div class="mb-10">实验日志</div> | |||||
<LogContainer | |||||
ref="logContainer" | |||||
class="mt-20" | |||||
:log-getter="getExpLog" | |||||
:options="logOptions" | |||||
:log-lines="50" | |||||
/> | |||||
</el-card> | |||||
<Stage | |||||
v-else | |||||
:activePath="state.activePath" | |||||
:detail="state.detail" | |||||
:experimentId="experimentId" | |||||
:configMap="state.configMap" | |||||
:info="stageInfo" | |||||
:param="stageParam" | |||||
:runParam="stageRunParam" | |||||
:metric="stageMetric" | |||||
:updateState="updateState" | |||||
:refresh="refresh" | |||||
/> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { reactive, computed, onMounted, watch, ref } from '@vue/composition-api'; | |||||
import { useLocalStorage } from '@/hooks'; | |||||
import { | |||||
expDetailOverview, | |||||
expStageInfo, | |||||
expStageParam, | |||||
expStageRuntimeParam, | |||||
getSearchSpace, | |||||
getSelectedSpace, | |||||
getExpConfig, | |||||
getExpLog, | |||||
expYaml, | |||||
expStageAccuracy, | |||||
expStageIntermediate, | |||||
expStageRuntime, | |||||
} from '@/api/tadl'; | |||||
import TextEditor from '@/components/textEditor'; | |||||
import LogContainer from '@/components/LogContainer'; | |||||
import { | |||||
expInprogress, | |||||
expIsFinished, | |||||
expEnablePause, | |||||
getStageName, | |||||
getStageOrder, | |||||
expEnableStart, | |||||
extractData, | |||||
extractScatterData, | |||||
extractSeriesData, | |||||
} from '../util'; | |||||
import DetailDashboard from './components/detailDashboard'; | |||||
import Config from './components/config'; | |||||
import Stage from './stage'; | |||||
export default { | |||||
name: 'DetailContainer', | |||||
components: { | |||||
DetailDashboard, | |||||
Config, | |||||
TextEditor, | |||||
Stage, | |||||
LogContainer, | |||||
}, | |||||
setup(props, ctx) { | |||||
const { $route } = ctx.root; | |||||
const { params = {} } = $route; | |||||
const [refreshTime, saveRefreshTime] = useLocalStorage('refreshTime', 10); | |||||
const { experimentId } = params; | |||||
const logContainer = ref(null); | |||||
const state = reactive({ | |||||
activePath: ['TRAIN', 'general'], | |||||
detail: {}, | |||||
loading: false, | |||||
error: null, | |||||
activeStage: 'TRAIN', // 当前所处阶段 | |||||
prevActiveStage: 'TRAIN', | |||||
stageInfoMap: {}, | |||||
stageParamMap: {}, | |||||
stageRunParamMap: {}, | |||||
configMap: {}, // 实验配置参数 | |||||
stageYamlMap: {}, // 分阶段 yaml 配置 | |||||
stageMetricMap: {}, // 基本Metric 展示 | |||||
refreshTime, // 刷新时间 | |||||
searchSpace: '', | |||||
selectedSpace: '', | |||||
expConfig: '', | |||||
searchSpaceVisible: false, | |||||
selectedSpaceVisible: false, | |||||
expConfigVisible: false, | |||||
algrithomLog: '', | |||||
systemLog: '', | |||||
}); | |||||
// 判断实验状态 | |||||
const isFinished = computed(() => expIsFinished(state.detail.status)); | |||||
const inProgress = computed(() => expInprogress(state.detail.status)); | |||||
const enablePause = computed(() => expEnablePause(state.detail.status)); | |||||
const enableStart = computed(() => expEnableStart(state.detail.status)); | |||||
const activeStageName = computed(() => state.activePath[0]); | |||||
// 阶段概览 | |||||
const stageInfo = computed(() => state.stageInfoMap[activeStageName.value]); | |||||
// 阶段参数概览 | |||||
const stageParam = computed(() => state.stageParamMap[activeStageName.value]); | |||||
// 阶段运行参数 | |||||
const stageRunParam = computed(() => state.stageRunParamMap[activeStageName.value]); | |||||
// 分阶段 yaml 配置 | |||||
const stageYaml = computed(() => state.stageYamlMap[activeStageName.value]); | |||||
// 阶段输出数据 | |||||
const stageMetric = computed(() => state.stageMetricMap[activeStageName.value]); | |||||
const updateState = (params) => { | |||||
return new Promise((resolve) => { | |||||
// 区分函数式更新和对象更新 | |||||
if (typeof params === 'function') { | |||||
const next = params(state); | |||||
Object.assign(state, next); | |||||
resolve(state); | |||||
} | |||||
// 普通更新 | |||||
Object.assign(state, params); | |||||
resolve(state); | |||||
}); | |||||
}; | |||||
// 查询实验详情 | |||||
const queryExpDetail = async () => { | |||||
const detail = await expDetailOverview(experimentId); | |||||
updateState({ | |||||
detail, | |||||
activeStage: getStageName(detail.runStage || 1), | |||||
activePath: [getStageName(detail.runStage || 1), 'general'], | |||||
}); | |||||
return detail; | |||||
}; | |||||
// 更新各个阶段详情 | |||||
const updateStateBy = (stageName, key, value) => { | |||||
updateState((state) => { | |||||
const next = { | |||||
...state[key], | |||||
[stageName]: value, | |||||
}; | |||||
return { | |||||
...state, | |||||
[key]: next, | |||||
}; | |||||
}); | |||||
}; | |||||
// 查询实验阶段信息 | |||||
const queryExpStageInfo = async ({ stageOrder }) => { | |||||
const stageInfo = await expStageInfo(experimentId, stageOrder); | |||||
const stageName = getStageName(stageOrder); | |||||
updateStateBy(stageName, 'stageInfoMap', stageInfo); | |||||
}; | |||||
// 查询实验阶段运行参数 | |||||
const queryExpStageParam = async ({ stageOrder }) => { | |||||
const stageParam = await expStageParam(experimentId, stageOrder); | |||||
const stageName = getStageName(stageOrder); | |||||
updateStateBy(stageName, 'stageParamMap', stageParam); | |||||
}; | |||||
// 查询实验参数 | |||||
const queryExpStageRuntimeParam = async ({ stageOrder }) => { | |||||
const stageRunParam = await expStageRuntimeParam(experimentId, stageOrder); | |||||
const stageName = getStageName(stageOrder); | |||||
updateStateBy(stageName, 'stageRunParamMap', stageRunParam); | |||||
}; | |||||
const queryExpYaml = async ({ stageOrder }) => { | |||||
const stageYaml = await expYaml(experimentId, stageOrder); | |||||
const stageName = getStageName(stageOrder); | |||||
updateStateBy(stageName, 'stageYamlMap', stageYaml); | |||||
}; | |||||
const defaultConfig = { | |||||
autoFit: true, | |||||
seriesField: null, | |||||
smooth: false, // 平滑曲线 | |||||
meta: { | |||||
value: { | |||||
// max: 21, // 坐标轴限定值范围 | |||||
}, | |||||
}, | |||||
xAxis: { | |||||
title: { | |||||
text: 'x轴', | |||||
spacing: 30, | |||||
style: { | |||||
fontSize: 20, | |||||
}, | |||||
}, | |||||
}, | |||||
yAxis: { | |||||
title: { | |||||
text: 'y轴', | |||||
style: { | |||||
fontSize: 20, | |||||
}, | |||||
}, | |||||
}, | |||||
}; | |||||
const scatterConfig = { | |||||
regressionLine: { | |||||
type: 'loess', | |||||
}, | |||||
}; | |||||
// 查询最佳精度图数据 | |||||
// 查询运行中间值图数据 | |||||
// 查询运行时间图数据 | |||||
const queryStageMetric = async ({ stageOrder }) => { | |||||
const rawAccuracy = await expStageAccuracy(experimentId, stageOrder); | |||||
const rawIntermediate = await expStageIntermediate(experimentId, stageOrder); | |||||
const rawRuntime = await expStageRuntime(experimentId, stageOrder); | |||||
const stageName = getStageName(stageOrder); | |||||
updateStateBy(stageName, 'stageMetricMap', { | |||||
accuracyData: extractData(rawAccuracy), | |||||
accuracyConfig: { | |||||
...defaultConfig, | |||||
...rawAccuracy.config, | |||||
xAxis: { title: { text: rawAccuracy.config.xFieldName }, tickInterval: 1 }, | |||||
yAxis: { title: { text: rawAccuracy.config.yFieldName } }, | |||||
}, | |||||
accuracyScatterData: extractScatterData(rawAccuracy), | |||||
accuracyScatterConfig: { | |||||
...defaultConfig, | |||||
...scatterConfig, | |||||
...rawAccuracy.config, | |||||
xAxis: { title: { text: rawAccuracy.config.xFieldName }, tickInterval: 1 }, | |||||
yAxis: { title: { text: rawAccuracy.config.yFieldName }, min: 0 }, | |||||
}, | |||||
intermediateData: extractSeriesData(rawIntermediate), | |||||
intermediateConfig: { | |||||
...defaultConfig, | |||||
...rawIntermediate.config, | |||||
xAxis: { title: { text: rawIntermediate.config.xFieldName }, tickInterval: 1 }, | |||||
yAxis: { title: { text: rawIntermediate.config.yFieldName } }, | |||||
}, | |||||
runtimeData: extractData(rawRuntime), | |||||
runtimeConfig: { | |||||
...defaultConfig, | |||||
...rawRuntime.config, | |||||
xAxis: { title: { text: 'trial' }, tickInterval: 1 }, | |||||
yAxis: { title: { text: '运行时间/min' } }, | |||||
}, | |||||
}); | |||||
}; | |||||
const queryStageInfo = (params) => { | |||||
Promise.all([ | |||||
queryExpStageInfo(params), | |||||
queryExpStageParam(params), | |||||
queryExpStageRuntimeParam(params), | |||||
queryExpYaml(params), | |||||
queryStageMetric(params), | |||||
]); | |||||
}; | |||||
const refresh = async () => { | |||||
const { runStage } = await queryExpDetail(); | |||||
queryStageInfo({ stageOrder: runStage || 1 }); | |||||
}; | |||||
// TODO | |||||
// 获取实验相关配置 | |||||
// const queryExpConfig = async () => { | |||||
// const configMap = await getExpConfig(experimentId); | |||||
// updateState((state) => { | |||||
// return { | |||||
// ...state, | |||||
// configMap, | |||||
// }; | |||||
// }); | |||||
// }; | |||||
const toggleSearchSpace = async () => { | |||||
const result = await getSearchSpace(experimentId).then((res) => JSON.parse(res.fileStr)); | |||||
state.searchSpaceVisible = !state.searchSpaceVisible; | |||||
state.searchSpace = result; | |||||
}; | |||||
const toggleSelectedSpace = async () => { | |||||
const result = await getSelectedSpace(experimentId).then((res) => JSON.parse(res.fileStr)); | |||||
state.selectedSpaceVisible = !state.selectedSpaceVisible; | |||||
state.selectedSpace = result; | |||||
}; | |||||
const toggleExpConfig = async () => { | |||||
const result = await getExpConfig(experimentId).then((res) => JSON.parse(res.fileStr)); | |||||
state.expConfigVisible = !state.expConfigVisible; | |||||
state.expConfig = result; | |||||
}; | |||||
const logOptions = computed(() => { | |||||
return { | |||||
experimentId, | |||||
}; | |||||
}); | |||||
const resetLogger = () => { | |||||
setTimeout(() => { | |||||
logContainer.value.reset(true); | |||||
}, 0); | |||||
}; | |||||
const setRefresher = (time) => { | |||||
if (time > 0) { | |||||
return setInterval(() => { | |||||
refresh(); | |||||
}, time * 1000); | |||||
} | |||||
return false; | |||||
}; | |||||
let refresher = setRefresher(props.refreshTime); | |||||
const command = (cmd) => { | |||||
saveRefreshTime(cmd); | |||||
updateState({ refreshTime: cmd }); | |||||
clearInterval(refresher); | |||||
refresher = setRefresher(cmd); | |||||
}; | |||||
const changeTab = (tab) => { | |||||
Object.assign(state, { | |||||
prevActiveStage: tab.name, | |||||
}); | |||||
command(0); | |||||
updateState({ activePath: [tab.name, 'general'] }); | |||||
}; | |||||
// 监听阶段变更 | |||||
watch( | |||||
() => state.activePath[0], | |||||
(next) => { | |||||
if (next === 'LOG') { | |||||
resetLogger(); | |||||
return; | |||||
} | |||||
const stageOrder = getStageOrder(next); | |||||
queryStageInfo({ stageOrder }); | |||||
} | |||||
); | |||||
onMounted(() => { | |||||
refresh(); | |||||
// queryExpConfig(); | |||||
}); | |||||
return { | |||||
state, | |||||
isFinished, | |||||
inProgress, | |||||
enablePause, | |||||
enableStart, | |||||
updateState, | |||||
stageInfo, | |||||
stageParam, | |||||
stageRunParam, | |||||
stageYaml, | |||||
stageMetric, | |||||
refresh, | |||||
saveRefreshTime, | |||||
experimentId, | |||||
toggleSearchSpace, | |||||
toggleSelectedSpace, | |||||
toggleExpConfig, | |||||
getExpLog, | |||||
logOptions, | |||||
logContainer, | |||||
command, | |||||
changeTab, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss"> | |||||
@import '@/assets/styles/variables.scss'; | |||||
.stage-content { | |||||
margin: 30px 0; | |||||
.stage-tabs { | |||||
.el-tabs__header { | |||||
border: none; | |||||
margin: 0; | |||||
} | |||||
.el-tabs__nav { | |||||
border: none; | |||||
} | |||||
.el-tabs__item.is-active { | |||||
background-color: #fff; | |||||
color: $primaryColor; | |||||
} | |||||
.el-tabs__item { | |||||
background-color: $primaryColor; | |||||
color: #fff; | |||||
margin-left: 2px; | |||||
margin-right: 10px; | |||||
border-top-left-radius: 6px; | |||||
border-top-right-radius: 6px; | |||||
} | |||||
} | |||||
.app-content-section { | |||||
margin-bottom: 30px; | |||||
} | |||||
.stage-card { | |||||
.el-tabs__header { | |||||
margin: 0; | |||||
} | |||||
.el-tabs__nav-wrap { | |||||
background-color: #fff; | |||||
padding-left: 16px; | |||||
margin-bottom: 10px; | |||||
&::after { | |||||
height: 0; | |||||
} | |||||
} | |||||
} | |||||
} | |||||
.app-content-title { | |||||
color: rgba(0, 0, 0, 0.85); | |||||
font-weight: 500; | |||||
font-size: 18px; | |||||
line-height: 28px; | |||||
overflow: hidden; | |||||
text-overflow: ellipsis; | |||||
white-space: nowrap; | |||||
} | |||||
.app-descriptions-header { | |||||
display: flex; | |||||
align-items: center; | |||||
margin-bottom: 20px; | |||||
.app-descriptions-title { | |||||
flex: auto; | |||||
overflow: hidden; | |||||
color: rgba(0, 0, 0, 0.85); | |||||
font-weight: 700; | |||||
font-size: 16px; | |||||
line-height: 1.5715; | |||||
white-space: nowrap; | |||||
text-overflow: ellipsis; | |||||
} | |||||
} | |||||
.app-container { | |||||
padding: 30px 32px; | |||||
.detail-header { | |||||
box-shadow: 0px 2px 7px 0px rgba(209, 209, 217, 0.5); | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,141 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-tabs v-model="state.activeTab" class="stage-card el-tabs-large" @tab-click="changeTab"> | |||||
<el-tab-pane label="概 览" name="general"> | |||||
<Parameter :param="param" :progress="progress" :experimentId="experimentId" :stage="stage" /> | |||||
<el-row :gutter="16"> | |||||
<el-col :span="12"> | |||||
<General :info="info" :stage="stage" :isOneTrial="isOneTrial" /> | |||||
</el-col> | |||||
<el-col :span="12"> | |||||
<RunParameter | |||||
:param="runParam" | |||||
:isOneTrial="isOneTrial" | |||||
:experimentId="experimentId" | |||||
:stage="stage" | |||||
:refresh="refresh" | |||||
/> | |||||
</el-col> | |||||
</el-row> | |||||
</el-tab-pane> | |||||
<el-tab-pane label="Trial 列表" name="trials"> | |||||
<el-row :gutter="20" class="mb-20"> | |||||
<el-col :span="12"> | |||||
<ChartCard | |||||
title="最佳精度" | |||||
type="ScatterChart" | |||||
:chartConfig="metric.accuracyScatterConfig" | |||||
:chartData="metric.accuracyScatterData" | |||||
/> | |||||
</el-col> | |||||
<el-col :span="12"> | |||||
<ChartCard | |||||
title="运行中间值" | |||||
type="LineChart" | |||||
:chartConfig="metric.intermediateConfig" | |||||
:chartData="metric.intermediateData" | |||||
/> | |||||
</el-col> | |||||
</el-row> | |||||
<el-row :gutter="20" class="mb-20"> | |||||
<el-col :span="12"> | |||||
<ChartCard | |||||
title="运行时间" | |||||
type="ColumnChart" | |||||
:chartConfig="metric.runtimeConfig" | |||||
:chartData="metric.runtimeData" | |||||
/> | |||||
</el-col> | |||||
</el-row> | |||||
<div class="dib"> | |||||
<TrialsList | |||||
:stage="stage" | |||||
:activeTab="state.activeTab" | |||||
contrastTitle="trial对比" | |||||
:createUserId="detail.createUserId" | |||||
/> | |||||
</div> | |||||
</el-tab-pane> | |||||
</el-tabs> | |||||
</template> | |||||
<script> | |||||
import { reactive } from '@vue/composition-api'; | |||||
import General from './components/general'; | |||||
import Parameter from './components/parameter'; | |||||
import RunParameter from './components/runParameter'; | |||||
import TrialsList from './components/trialsList'; | |||||
import ChartCard from './components/chartCard'; | |||||
export default { | |||||
name: 'TRAIN', | |||||
components: { | |||||
General, | |||||
Parameter, | |||||
RunParameter, | |||||
TrialsList, | |||||
ChartCard, | |||||
}, | |||||
props: { | |||||
activeTab: String, | |||||
stage: String, | |||||
// 阶段概览 | |||||
info: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
detail: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
experimentId: String, | |||||
// 阶段输出度量 | |||||
metric: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
// 参数 | |||||
param: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
// 运行参数 | |||||
runParam: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
updateState: Function, | |||||
// 实验阶段 | |||||
progress: Number, | |||||
// 是否为单一 trial | |||||
isOneTrial: Boolean, | |||||
refresh: Function, | |||||
}, | |||||
setup(props) { | |||||
const state = reactive({ | |||||
activeTab: props.activeTab, | |||||
}); | |||||
const changeTab = (tab) => { | |||||
if (tab.name === state.prevActiveTab) return; | |||||
Object.assign(state, { | |||||
activeTab: tab.name, | |||||
prevActiveTab: tab.name, | |||||
}); | |||||
props.updateState({ activePath: ['RETRAIN', tab.name] }); | |||||
}; | |||||
return { | |||||
changeTab, | |||||
state, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,130 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-tabs v-model="state.activeTab" class="stage-card el-tabs-large" @tab-click="changeTab"> | |||||
<el-tab-pane label="概 览" name="general"> | |||||
<Parameter :param="param" :progress="progress" :experimentId="experimentId" :stage="stage" /> | |||||
<el-row :gutter="16"> | |||||
<el-col :span="12"> | |||||
<General :info="info" :stage="stage" :isOneTrial="isOneTrial" /> | |||||
</el-col> | |||||
<el-col :span="12"> | |||||
<RunParameter | |||||
:param="runParam" | |||||
:experimentId="experimentId" | |||||
:stage="stage" | |||||
:refresh="refresh" | |||||
/> | |||||
</el-col> | |||||
</el-row> | |||||
</el-tab-pane> | |||||
<el-tab-pane label="Trial 列表" name="trials"> | |||||
<el-row :gutter="20" class="mb-20"> | |||||
<el-col :span="12"> | |||||
<ChartCard | |||||
title="运行中间值" | |||||
type="LineChart" | |||||
:chartConfig="metric.intermediateConfig" | |||||
:chartData="metric.intermediateData" | |||||
/> | |||||
</el-col> | |||||
<el-col :span="12"> | |||||
<ChartCard | |||||
title="运行时间" | |||||
type="ColumnChart" | |||||
:chartConfig="metric.runtimeConfig" | |||||
:chartData="metric.runtimeData" | |||||
/> | |||||
</el-col> | |||||
</el-row> | |||||
<div class="dib"> | |||||
<TrialsList | |||||
:stage="stage" | |||||
:activeTab="state.activeTab" | |||||
contrastTitle="trial对比" | |||||
:createUserId="detail.createUserId" | |||||
/> | |||||
</div> | |||||
</el-tab-pane> | |||||
</el-tabs> | |||||
</template> | |||||
<script> | |||||
import { reactive } from '@vue/composition-api'; | |||||
import General from './components/general'; | |||||
import Parameter from './components/parameter'; | |||||
import RunParameter from './components/runParameter'; | |||||
import TrialsList from './components/trialsList'; | |||||
import ChartCard from './components/chartCard'; | |||||
export default { | |||||
name: 'SELECT', | |||||
components: { | |||||
General, | |||||
Parameter, | |||||
RunParameter, | |||||
TrialsList, | |||||
ChartCard, | |||||
}, | |||||
props: { | |||||
activeTab: String, | |||||
stage: String, | |||||
// 阶段概览 | |||||
info: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
detail: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
experimentId: String, | |||||
// 阶段输出度量 | |||||
metric: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
// 参数 | |||||
param: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
// 运行参数 | |||||
runParam: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
updateState: Function, | |||||
// 实验阶段 | |||||
progress: Number, | |||||
// 是否为单一 trial | |||||
isOneTrial: Boolean, | |||||
refresh: Function, | |||||
}, | |||||
setup(props) { | |||||
const state = reactive({ | |||||
activeTab: props.activeTab, | |||||
}); | |||||
const changeTab = (tab) => { | |||||
if (tab.name === state.prevActiveTab) return; | |||||
Object.assign(state, { | |||||
activeTab: tab.name, | |||||
prevActiveTab: tab.name, | |||||
}); | |||||
props.updateState({ activePath: ['SELECT', tab.name] }); | |||||
}; | |||||
return { | |||||
changeTab, | |||||
state, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,71 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-card> | |||||
<component | |||||
:is="stage" | |||||
:stage="stage" | |||||
:activeTab="activeTab" | |||||
:config="config" | |||||
:isOneTrial="isOneTrial" | |||||
:progress="stageProgress" | |||||
:refresh="refresh" | |||||
:detail="detail" | |||||
v-bind="attrs" | |||||
/> | |||||
</el-card> | |||||
</template> | |||||
<script> | |||||
import { computed } from '@vue/composition-api'; | |||||
import { getStageOrder } from '../util'; | |||||
import TRAIN from './train'; | |||||
import SELECT from './select'; | |||||
import RETRAIN from './retrain'; | |||||
export default { | |||||
name: 'Stage', | |||||
components: { | |||||
TRAIN, | |||||
SELECT, | |||||
RETRAIN, | |||||
}, | |||||
props: { | |||||
activePath: [Array, String], | |||||
detail: Object, | |||||
configMap: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
refresh: Function, | |||||
}, | |||||
setup(props, ctx) { | |||||
const stage = computed(() => props.activePath[0]); | |||||
const attrs = computed(() => ctx.attrs); | |||||
const activeTab = computed(() => props.activePath[1]); | |||||
const sequence = computed(() => getStageOrder(stage)); | |||||
// 0: 当前阶段,- 已完成,+ 未完成 | |||||
const stageProgress = computed(() => sequence - props.detail.stageOrder || 0); | |||||
// 算法配置 | |||||
const config = computed(() => props.configMap[stage.value.toLowerCase()]); | |||||
// 是否为单一 trial | |||||
const isOneTrial = computed(() => config.maxTrialNum === 1); | |||||
return { | |||||
stage, | |||||
attrs, | |||||
sequence, | |||||
activeTab, | |||||
stageProgress, | |||||
config, | |||||
isOneTrial, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,144 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-tabs v-model="state.activeTab" class="stage-card el-tabs-large" @tab-click="changeTab"> | |||||
<el-tab-pane label="概 览" name="general"> | |||||
<Parameter :param="param" :progress="progress" :experimentId="experimentId" :stage="stage" /> | |||||
<el-row :gutter="16"> | |||||
<el-col :span="12"> | |||||
<General :info="info" :stage="stage" :isOneTrial="isOneTrial" /> | |||||
</el-col> | |||||
<el-col :span="12"> | |||||
<RunParameter | |||||
:param="runParam" | |||||
:isOneTrial="isOneTrial" | |||||
:experimentId="experimentId" | |||||
:stage="stage" | |||||
:refresh="refresh" | |||||
/> | |||||
</el-col> | |||||
</el-row> | |||||
</el-tab-pane> | |||||
<el-tab-pane label="Trial 列表" name="trials"> | |||||
<el-row :gutter="20" class="mb-20"> | |||||
<el-col :span="12"> | |||||
<ChartCard | |||||
title="最佳精度" | |||||
type="ScatterChart" | |||||
:chartConfig="metric.accuracyScatterConfig" | |||||
:chartData="metric.accuracyScatterData" | |||||
/> | |||||
</el-col> | |||||
<el-col :span="12"> | |||||
<ChartCard | |||||
title="运行中间值" | |||||
type="LineChart" | |||||
:chartConfig="metric.intermediateConfig" | |||||
:chartData="metric.intermediateData" | |||||
/> | |||||
</el-col> | |||||
</el-row> | |||||
<el-row :gutter="20" class="mb-20"> | |||||
<el-col :span="12"> | |||||
<ChartCard | |||||
title="运行时间" | |||||
type="ColumnChart" | |||||
:chartConfig="metric.runtimeConfig" | |||||
:chartData="metric.runtimeData" | |||||
/> | |||||
</el-col> | |||||
</el-row> | |||||
<div class="dib"> | |||||
<TrialsList | |||||
:stage="stage" | |||||
:activeTab="state.activeTab" | |||||
contrastTitle="trial对比" | |||||
:createUserId="detail.createUserId" | |||||
/> | |||||
</div> | |||||
</el-tab-pane> | |||||
</el-tabs> | |||||
</template> | |||||
<script> | |||||
import { reactive } from '@vue/composition-api'; | |||||
import General from './components/general'; | |||||
import Parameter from './components/parameter'; | |||||
import RunParameter from './components/runParameter'; | |||||
import TrialsList from './components/trialsList'; | |||||
import ChartCard from './components/chartCard'; | |||||
export default { | |||||
name: 'TRAIN', | |||||
components: { | |||||
General, | |||||
Parameter, | |||||
RunParameter, | |||||
TrialsList, | |||||
ChartCard, | |||||
}, | |||||
props: { | |||||
activeTab: String, | |||||
stage: String, | |||||
// 阶段概览 | |||||
info: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
detail: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
experimentId: String, | |||||
// 阶段输出度量 | |||||
metric: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
// 参数 | |||||
param: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
// 运行参数 | |||||
runParam: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
updateState: Function, | |||||
// 实验阶段 | |||||
progress: Number, | |||||
// 是否为单一 trial | |||||
isOneTrial: Boolean, | |||||
// 算法配置 | |||||
config: Object, | |||||
refresh: Function, | |||||
}, | |||||
setup(props) { | |||||
const state = reactive({ | |||||
activeTab: props.activeTab, | |||||
prevActiveTab: props.activeTab, | |||||
}); | |||||
const changeTab = (tab) => { | |||||
if (tab.name === state.prevActiveTab) return; | |||||
Object.assign(state, { | |||||
activeTab: tab.name, | |||||
prevActiveTab: tab.name, | |||||
}); | |||||
props.updateState({ activePath: ['TRAIN', tab.name] }); | |||||
}; | |||||
return { | |||||
changeTab, | |||||
state, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,24 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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 { TRIAL_STATUS_MAP } from '../util'; | |||||
export const allTrialStatusList = [{ label: '全部', value: null }].concat( | |||||
Object.values(TRIAL_STATUS_MAP).map((status) => ({ | |||||
label: status.label, | |||||
value: status.value, | |||||
})) | |||||
); |
@@ -0,0 +1,406 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> | |||||
<div class="area-title">基本信息</div> | |||||
<el-form-item label="实验名称" prop="name"> | |||||
<el-input | |||||
v-model.trim="form.name" | |||||
maxlength="32" | |||||
show-word-limit | |||||
placeholder="请输入实验名称" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="实验描述" prop="description"> | |||||
<el-input | |||||
v-model="form.description" | |||||
type="textarea" | |||||
:rows="4" | |||||
maxlength="200" | |||||
show-word-limit | |||||
placeholder="请输入实验描述" | |||||
/> | |||||
</el-form-item> | |||||
<div class="area-title">搜索策略</div> | |||||
<el-form-item ref="algorithmVersionIdRef" label="选择搜索策略" prop="algorithmVersionId"> | |||||
<el-select | |||||
v-model="form.algorithmId" | |||||
placeholder="spos" | |||||
clearable | |||||
@change="onAlgorithmIdChange" | |||||
> | |||||
<el-option | |||||
v-for="item in algorithmList" | |||||
:key="item.id" | |||||
:value="item.id" | |||||
:label="item.name" | |||||
/> | |||||
</el-select> | |||||
<el-select | |||||
v-model="form.algorithmVersionId" | |||||
placeholder="选择版本" | |||||
clearable | |||||
@change="onAlgorithmVersionChange" | |||||
> | |||||
<el-option | |||||
v-for="item in algorithmVersionList" | |||||
:key="item.id" | |||||
:value="item.id" | |||||
:label="item.versionName || '最新'" | |||||
/> | |||||
</el-select> | |||||
<BaseTooltip content="只支持预置搜索策略" /> | |||||
</el-form-item> | |||||
<el-tabs v-model="stageTab" class="eltabs-inlineblock mb-20" @tab-click="onTabsChange"> | |||||
<el-tab-pane label="TRAIN" :name="String(STAGE_SEQUENCE.TRAIN)" /> | |||||
<el-tab-pane label="SELECT" :name="String(STAGE_SEQUENCE.SELECT)" /> | |||||
<el-tab-pane label="RETRAIN" :name="String(STAGE_SEQUENCE.RETRAIN)" /> | |||||
</el-tabs> | |||||
<transition-group ref="tabPane" :name="transition" tag="div"> | |||||
<TadlStageForm | |||||
v-show="stageTab === String(STAGE_SEQUENCE.TRAIN)" | |||||
ref="trainStageForm" | |||||
:key="STAGE_SEQUENCE.TRAIN" | |||||
class="tab-form" | |||||
:model="form.stage[0]" | |||||
:use-gpu="useGpu" | |||||
@resource-change="onResourceChange" | |||||
/> | |||||
<TadlStageForm | |||||
v-show="stageTab === String(STAGE_SEQUENCE.SELECT)" | |||||
ref="selectStageForm" | |||||
:key="STAGE_SEQUENCE.SELECT" | |||||
class="tab-form" | |||||
:model="form.stage[1]" | |||||
:use-gpu="useGpu" | |||||
@resource-change="onResourceChange" | |||||
/> | |||||
<TadlStageForm | |||||
v-show="stageTab === String(STAGE_SEQUENCE.RETRAIN)" | |||||
:key="STAGE_SEQUENCE.RETRAIN" | |||||
ref="retrainStageForm" | |||||
class="tab-form" | |||||
:model="form.stage[2]" | |||||
:use-gpu="useGpu" | |||||
@resource-change="onResourceChange" | |||||
/> | |||||
</transition-group> | |||||
</el-form> | |||||
</template> | |||||
<script> | |||||
import { Message } from 'element-ui'; | |||||
import { nextTick, reactive, ref, toRefs, watch } from '@vue/composition-api'; | |||||
import { isNil } from 'lodash'; | |||||
import { getStrategyList, checkStrategy } from '@/api/tadl/strategy'; | |||||
import BaseTooltip from '@/components/BaseTooltip'; | |||||
import { validateNameWithHyphen } from '@/utils'; | |||||
import TadlStageForm from './tadlStageForm'; | |||||
import { defaultStageForm } from '../utils'; | |||||
import { STAGE_SEQUENCE } from '../../util'; | |||||
const defaultForm = { | |||||
id: null, // 实验 ID | |||||
modelType: null, // 模型类型 | |||||
name: null, // 实验名称 | |||||
description: null, // 实验描述 | |||||
algorithmId: null, // 算法 ID | |||||
algorithmVersionId: null, // 算法版本名称 | |||||
stage: [], // 阶段信息 | |||||
}; | |||||
export default { | |||||
name: 'TadlForm', | |||||
components: { | |||||
BaseTooltip, | |||||
TadlStageForm, | |||||
}, | |||||
setup() { | |||||
// 表单 ref | |||||
const formRef = ref(null); | |||||
const trainStageForm = ref(null); | |||||
const selectStageForm = ref(null); | |||||
const retrainStageForm = ref(null); | |||||
const algorithmVersionIdRef = ref(null); | |||||
const state = reactive({ | |||||
stageTab: String(STAGE_SEQUENCE.TRAIN), | |||||
transition: 'tabRight', | |||||
algorithmList: [], | |||||
algorithmVersionList: [], | |||||
useGpu: false, | |||||
}); | |||||
// 表单值 | |||||
const form = reactive({ ...defaultForm }); | |||||
// 枚举stage表单ref | |||||
const stageRefs = { | |||||
[STAGE_SEQUENCE.TRAIN]: trainStageForm, | |||||
[STAGE_SEQUENCE.SELECT]: selectStageForm, | |||||
[STAGE_SEQUENCE.RETRAIN]: retrainStageForm, | |||||
}; | |||||
// rules | |||||
const rules = { | |||||
name: [ | |||||
{ required: true, message: '请输入实验名称', trigger: 'blur' }, | |||||
{ | |||||
max: 32, | |||||
message: '长度在 32 个字符以内', | |||||
trigger: 'blur', | |||||
}, | |||||
{ | |||||
validator: validateNameWithHyphen, | |||||
trigger: ['blur', 'change'], | |||||
}, | |||||
], | |||||
algorithmVersionId: [ | |||||
{ | |||||
required: true, | |||||
trigger: 'manual', | |||||
validator: (rule, value, callback) => { | |||||
if (!form.algorithmId) { | |||||
callback(new Error('请选择搜索策略')); | |||||
} | |||||
if (!form.algorithmVersionId) { | |||||
callback(new Error('请选择策略版本')); | |||||
} | |||||
callback(); | |||||
}, | |||||
}, | |||||
], | |||||
}; | |||||
// 算法选择处理 | |||||
const onAlgorithmIdChange = (id, keepValue) => { | |||||
const algorithm = state.algorithmList.find((algorithm) => algorithm.id === id); | |||||
if (!algorithm) { | |||||
state.algorithmVersionList = []; | |||||
form.algorithmVersionId = null; | |||||
form.modelType = null; | |||||
return; | |||||
} | |||||
state.algorithmVersionList = algorithm.algorithmVersionVOList.filter( | |||||
(version) => version.versionName | |||||
); | |||||
state.useGpu = algorithm.gpu; | |||||
form.modelType = algorithm.modelType; | |||||
if (!keepValue || !form.algorithmVersionId) { | |||||
form.algorithmVersionId = null; | |||||
return; | |||||
} | |||||
const version = state.algorithmVersionList.find( | |||||
(version) => version.id === form.algorithmVersionId | |||||
); | |||||
if (!version) { | |||||
form.algorithmVersionId = null; | |||||
Message.warning('原有策略版本不存在,请重新选择'); | |||||
} | |||||
}; | |||||
// 获取算法列表 | |||||
const getStrategyInfo = async (keepValue = false) => { | |||||
state.algorithmList = await getStrategyList(); | |||||
if (!keepValue || !form.algorithmId) { | |||||
form.algorithmId = form.algorithmVersionId = null; | |||||
} else { | |||||
const algorithm = state.algorithmList.find((info) => info.id === form.algorithmId); | |||||
if (!algorithm) { | |||||
Message.warning('原有策略不存在,请重新选择'); | |||||
form.algorithmId = form.algorithmVersionId = null; | |||||
return; | |||||
} | |||||
onAlgorithmIdChange(algorithm.id, true); | |||||
} | |||||
}; | |||||
const onAlgorithmVersionChange = async (algorithmVersionId) => { | |||||
if (algorithmVersionId) { | |||||
// 查询查看接口, 回填阶段值 | |||||
const { stage } = await checkStrategy({ algorithmVersionId }, form.algorithmId); | |||||
stage.forEach((order, index) => { | |||||
// 由子表单负责确保不会将无用字段带入 | |||||
Object.assign(form.stage[index], defaultStageForm, order); | |||||
nextTick(() => { | |||||
stageRefs[order.stageOrder].value.initForm(); | |||||
}); | |||||
}); | |||||
} | |||||
algorithmVersionIdRef.value.validate('manual'); | |||||
}; | |||||
// 表单入口 | |||||
const initForm = async (originForm = {}) => { | |||||
// 普通字段赋值 | |||||
Object.keys(form).forEach((key) => { | |||||
if (!isNil(originForm[key])) { | |||||
form[key] = originForm[key]; | |||||
} | |||||
}); | |||||
// stage 数组非引用赋值 + 默认值 | |||||
form.stage = []; | |||||
if (originForm.stage) { | |||||
// 如果原表单有 stage 数组,则直接赋值 | |||||
for (const stage of originForm.stage) { | |||||
form.stage.push({ | |||||
...defaultStageForm, | |||||
...stage, | |||||
}); | |||||
} | |||||
} else { | |||||
form.stage = [ | |||||
{ ...defaultStageForm, stageOrder: STAGE_SEQUENCE.TRAIN }, | |||||
{ ...defaultStageForm, stageOrder: STAGE_SEQUENCE.SELECT }, | |||||
{ ...defaultStageForm, stageOrder: STAGE_SEQUENCE.RETRAIN }, | |||||
]; | |||||
} | |||||
// 获取表单选项数据 | |||||
await getStrategyInfo(true); | |||||
// 算法信息查询完成后,需要根据所选算法中的 gpu 字段才能确定子表单的 props | |||||
// 如果算法信息不存在,由于必须重新选择算法,因此不再进行数据初始化工作 | |||||
if (form.algorithmId && form.algorithmVersionId) { | |||||
if (originForm.stage) { | |||||
nextTick(() => { | |||||
trainStageForm.value.initForm(); | |||||
selectStageForm.value.initForm(); | |||||
retrainStageForm.value.initForm(); | |||||
}); | |||||
} else { | |||||
// 从 搜索策略 创建实验时,需要查询所选算法版本的阶段信息 | |||||
onAlgorithmVersionChange(form.algorithmVersionId); | |||||
} | |||||
} | |||||
}; | |||||
// 表单校验出口 | |||||
const validate = (resolve, reject) => { | |||||
let valid = true; | |||||
formRef.value.validate((isValid) => { | |||||
valid = valid && isValid; | |||||
}); | |||||
// 子表单校验 | |||||
Object.keys(stageRefs).forEach((stage, index) => { | |||||
if (!valid) return; | |||||
stageRefs[stage].value.validate( | |||||
(stageForm) => { | |||||
// 过滤掉后端返回的多余参数 | |||||
form.stage[index] = { | |||||
...stageForm, | |||||
algorithmStageId: stageForm.algorithmStageId || stageForm.id, | |||||
stageName: stageForm.stageName || stageForm.name, | |||||
}; | |||||
}, | |||||
() => { | |||||
valid = false; | |||||
state.stageTab = String(stage); | |||||
} | |||||
); | |||||
}); | |||||
if (valid) { | |||||
if (typeof resolve === 'function') { | |||||
return resolve(form); | |||||
} | |||||
return true; | |||||
} | |||||
if (typeof reject === 'function') { | |||||
return reject(form); | |||||
} | |||||
return false; | |||||
}; | |||||
// 清空表单校验方法 | |||||
const clearValidate = (...args) => { | |||||
formRef.value.clearValidate(...args); | |||||
trainStageForm.value.clearValidate(); | |||||
selectStageForm.value.clearValidate(); | |||||
retrainStageForm.value.clearValidate(); | |||||
}; | |||||
// 资源变更 | |||||
const onResourceChange = (resource) => { | |||||
Object.values(stageRefs).forEach((ref) => { | |||||
ref.value.setDefaultResource(resource); | |||||
}); | |||||
}; | |||||
const onTabsChange = () => { | |||||
// 切换 tab 时,需要更新 yaml 组件才能正常展示内容 | |||||
stageRefs[state.stageTab].value.setYamlValue(); | |||||
}; | |||||
watch( | |||||
() => state.stageTab, | |||||
(next, prev) => { | |||||
Object.assign(state, { | |||||
transition: Number(next) > Number(prev) ? 'tabRight' : 'tabLeft', | |||||
}); | |||||
} | |||||
); | |||||
return { | |||||
STAGE_SEQUENCE, | |||||
formRef, | |||||
trainStageForm, | |||||
selectStageForm, | |||||
retrainStageForm, | |||||
algorithmVersionIdRef, | |||||
form, | |||||
rules, | |||||
...toRefs(state), | |||||
initForm, | |||||
validate, | |||||
clearValidate, | |||||
onTabsChange, | |||||
onAlgorithmIdChange, | |||||
onAlgorithmVersionChange, | |||||
onResourceChange, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
@import '../style'; | |||||
.tab-form { | |||||
float: left; | |||||
width: 100%; | |||||
} | |||||
.tabRight-enter, | |||||
.tabLeft-leave-to { | |||||
position: absolute; | |||||
opacity: 0; | |||||
transform: translateX(100%); | |||||
} | |||||
.tabRight-leave-to, | |||||
.tabLeft-enter { | |||||
position: absolute; | |||||
opacity: 0; | |||||
transform: translateX(-100%); | |||||
} | |||||
.tabRight-enter-active, | |||||
.tabRight-leave-active, | |||||
.tabLeft-enter-active, | |||||
.tabLeft-leave-active { | |||||
transition: all 0.6s ease; | |||||
} | |||||
</style> |
@@ -0,0 +1,523 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> | |||||
<el-form-item label="资源配置" prop="resourceId"> | |||||
<div class="resources-container"> | |||||
<span v-if="baseResourceList.length === 0" class="empty-text"> | |||||
暂无数据 | |||||
</span> | |||||
<el-radio-group | |||||
v-model="form.resourceId" | |||||
class="flex flex-col" | |||||
@change="baseResourceChange" | |||||
> | |||||
<el-radio v-for="resource of baseResourceList" :key="resource.id" :label="resource.id">{{ | |||||
resource.specsName | |||||
}}</el-radio> | |||||
</el-radio-group> | |||||
<el-button | |||||
v-if="baseResourceList.length !== 0" | |||||
type="text" | |||||
class="db" | |||||
@click="selectOtherResource" | |||||
>选择其他</el-button | |||||
> | |||||
</div> | |||||
</el-form-item> | |||||
<el-form-item label="数据集" prop="datasetVersion"> | |||||
<el-input | |||||
v-model="form.datasetName" | |||||
placeholder="数据集名称" | |||||
disabled | |||||
style="width: 200px;" | |||||
/> | |||||
<el-input | |||||
v-model="form.datasetVersion" | |||||
placeholder="数据集版本" | |||||
disabled | |||||
style="width: 200px;" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="运行参数" prop="runParams"> | |||||
<div class="yaml"> | |||||
<YamlEditor ref="yamlRef" :value="form.yaml" @blur="onYamlChange" /> | |||||
</div> | |||||
</el-form-item> | |||||
<el-button type="text" @click="showMore = !showMore" | |||||
><i :class="showMore ? 'el-icon-arrow-up' : 'el-icon-arrow-down'" />{{ | |||||
showMore ? '收起' : '展开' | |||||
}}更多设置</el-button | |||||
> | |||||
<el-collapse-transition> | |||||
<div v-if="showMore"> | |||||
<div class="area-title">实验终止条件</div> | |||||
<el-form-item label="最大 Trial 次数" prop="maxTrialNum"> | |||||
<el-input | |||||
v-model.number="form.maxTrialNum" | |||||
class="w-200" | |||||
@change="changeYamlParams('maxTrialNum')" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="当前阶段最大运行时间" prop="maxExecDuration"> | |||||
<el-input | |||||
v-model="form.maxExecDuration" | |||||
class="w-200 input-suffix" | |||||
@change="onMaxExecDurationChange" | |||||
> | |||||
<template #append> | |||||
<el-select | |||||
v-model="form.maxExecDurationUnit" | |||||
class="w-80" | |||||
@change="changeYamlParams('maxExecDuration')" | |||||
> | |||||
<el-option | |||||
v-for="time in timeFmts" | |||||
:key="time.value" | |||||
:value="time.value" | |||||
:label="time.label" | |||||
/> | |||||
</el-select> | |||||
</template> | |||||
</el-input> | |||||
</el-form-item> | |||||
<div class="area-title">其他配置</div> | |||||
<el-form-item label="Trial 并发数量" prop="trialConcurrentNum"> | |||||
<el-input | |||||
v-model.number="form.trialConcurrentNum" | |||||
class="w-200" | |||||
@change="changeYamlParams('trialConcurrentNum')" | |||||
/> | |||||
</el-form-item> | |||||
</div> | |||||
</el-collapse-transition> | |||||
<BaseModal | |||||
:visible.sync="resourceVisible" | |||||
title="资源配置" | |||||
:showCancel="false" | |||||
@ok="onResourceSelected" | |||||
@close="onResourceClose" | |||||
> | |||||
<BaseTable | |||||
:columns="otherResourceColumns" | |||||
:data="resourceList" | |||||
:highlight-current-row="false" | |||||
> | |||||
<template #radio="scope"> | |||||
<el-radio v-model="selectedOtherResource" :label="scope.row.id"> </el-radio> | |||||
</template> | |||||
</BaseTable> | |||||
<el-pagination | |||||
layout="prev, pager, next" | |||||
:page-size="resourcePageInfo.size" | |||||
:total="resourcePageInfo.total" | |||||
:current-page="resourcePageInfo.current" | |||||
@current-change="onResourcePageChange" | |||||
/> | |||||
</BaseModal> | |||||
</el-form> | |||||
</template> | |||||
<script> | |||||
import { computed, nextTick, reactive, ref, toRefs } from '@vue/composition-api'; | |||||
import yaml from 'js-yaml'; | |||||
import { isNil } from 'lodash'; | |||||
import { Message } from 'element-ui'; | |||||
import BaseModal from '@/components/BaseModal'; | |||||
import BaseTable from '@/components/BaseTable'; | |||||
import YamlEditor from '@/components/YamlEditor'; | |||||
import { list as getResources } from '@/api/system/resources'; | |||||
import { propertyAssign, RESOURCES_MODULE_ENUM } from '@/utils'; | |||||
import { defaultStageForm, otherResourceColumns } from '../utils'; | |||||
import { timeFmts } from '../../util'; | |||||
import { isNull, underlineShiftHump, modifyTime } from '../../strategy/util'; | |||||
const useResources = ({ props, form }, { emit }) => { | |||||
const state = reactive({ | |||||
baseResourceList: [], // 资源配置简易列表 | |||||
resourceList: [], // 资源配置分页列表 | |||||
resourceVisible: false, // 资源配置弹窗 | |||||
selectedOtherResource: null, // 资源弹窗中的资源 | |||||
}); | |||||
// 资源配置 | |||||
// 分页 | |||||
const resourcePageInfo = reactive({ | |||||
current: 1, | |||||
size: 5, | |||||
total: 0, | |||||
}); | |||||
const setResourcePage = (pageInfo) => { | |||||
Object.assign(resourcePageInfo, pageInfo); | |||||
}; | |||||
const baseResourceParam = computed(() => { | |||||
return { | |||||
module: RESOURCES_MODULE_ENUM.TADL, | |||||
resourcesPoolType: props.useGpu ? 1 : 0, | |||||
multiGpu: props.useGpu ? props.model.multiGpu : undefined, | |||||
current: resourcePageInfo.current, | |||||
size: resourcePageInfo.size, | |||||
}; | |||||
}); | |||||
// 获取资源列表 | |||||
const getBaseResourceList = async () => { | |||||
const { result } = await getResources({ | |||||
...baseResourceParam.value, | |||||
current: 1, | |||||
}); | |||||
state.baseResourceList = result; | |||||
}; | |||||
const getResourceList = async () => { | |||||
const { result, page } = await getResources({ | |||||
...baseResourceParam.value, | |||||
}); | |||||
state.resourceList = result; | |||||
setResourcePage(page); | |||||
}; | |||||
const onResourcePageChange = (page) => { | |||||
setResourcePage({ | |||||
current: page, | |||||
}); | |||||
getResourceList(); | |||||
}; | |||||
// 选择其他 | |||||
const selectOtherResource = () => { | |||||
state.selectedOtherResource = form.resourceId; | |||||
state.resourceVisible = true; | |||||
getResourceList(); | |||||
}; | |||||
// 如果选中的弹窗表格里选中的值没有在baseResource, 展示在baseResource | |||||
const onResourceSelected = () => { | |||||
if (state.selectedOtherResource) { | |||||
const resource = state.resourceList.find((r) => r.id === state.selectedOtherResource); | |||||
const baseResource = state.baseResourceList.find( | |||||
(base) => base.id === state.selectedOtherResource | |||||
); | |||||
if (baseResource === undefined && resource !== undefined) { | |||||
state.baseResourceList.unshift(resource); | |||||
} | |||||
form.resourceId = state.selectedOtherResource; | |||||
form.resourceName = resource?.specsName || null; | |||||
emit('resource-change', resource); | |||||
} | |||||
state.resourceVisible = false; | |||||
}; | |||||
const onResourceClose = () => { | |||||
setResourcePage({ | |||||
current: 1, | |||||
}); | |||||
}; | |||||
const baseResourceChange = (id) => { | |||||
const resource = state.baseResourceList.find((item) => item.id === id); | |||||
form.resourceName = resource?.specsName || null; | |||||
emit('resource-change', resource); | |||||
}; | |||||
// 当一个阶段选择了资源配置规格后,其他阶段自动填充默认值 | |||||
const setDefaultResource = (resource) => { | |||||
if (!form.resourceId) { | |||||
const baseResource = state.baseResourceList.find((base) => base.id === resource.id); | |||||
if (!baseResource) { | |||||
state.baseResourceList.unshift(resource); | |||||
} | |||||
form.resourceId = resource.id; | |||||
form.resourceName = resource.specsName; | |||||
} | |||||
}; | |||||
return { | |||||
state, | |||||
setResourcePage, | |||||
resourcePageInfo, | |||||
onResourcePageChange, | |||||
baseResourceChange, | |||||
getBaseResourceList, | |||||
selectOtherResource, | |||||
onResourceSelected, | |||||
onResourceClose, | |||||
setDefaultResource, | |||||
}; | |||||
}; | |||||
export default { | |||||
name: 'TadlStageForm', | |||||
components: { | |||||
BaseModal, | |||||
BaseTable, | |||||
YamlEditor, | |||||
}, | |||||
props: { | |||||
model: Object, | |||||
useGpu: { | |||||
type: Boolean, | |||||
default: undefined, | |||||
}, | |||||
}, | |||||
setup(props, ctx) { | |||||
// 表单 ref | |||||
const formRef = ref(null); | |||||
const yamlRef = ref(null); | |||||
// 表单 | |||||
const form = reactive({ ...defaultStageForm }); | |||||
const rules = { | |||||
resourceId: [{ required: true, message: '请选择资源配置', trigger: 'manual' }], | |||||
maxExecDuration: [ | |||||
{ | |||||
required: true, | |||||
validator: (rule, value, callback) => { | |||||
if (!value) { | |||||
callback(new Error('请输入时间')); | |||||
} | |||||
// eslint-disable-next-line no-restricted-globals | |||||
if (isNaN(Number(value))) { | |||||
callback(new Error('时间为数值')); | |||||
} | |||||
if (Number(value) <= 0) { | |||||
callback(new Error('时间需要大于 0')); | |||||
} | |||||
if (!form.maxExecDurationUnit) { | |||||
callback(new Error('请选择时间单位')); | |||||
} | |||||
callback(); | |||||
}, | |||||
trigger: 'blur', | |||||
}, | |||||
], | |||||
maxTrialNum: [ | |||||
{ required: true, message: '请输入最大Trial次数', trigger: ['blur', 'change'] }, | |||||
{ type: 'number', message: '所填必须为数字' }, | |||||
{ | |||||
validator: (rule, value, callback) => { | |||||
if (!value && value !== 0) { | |||||
callback(); | |||||
} | |||||
if (value <= 0) { | |||||
callback(new Error('最大Trial次数需要大于 0')); | |||||
} | |||||
callback(); | |||||
}, | |||||
trigger: ['blur', 'change'], | |||||
}, | |||||
], | |||||
trialConcurrentNum: [ | |||||
{ required: true, message: '请输入Trial并发数量', trigger: ['blur', 'change'] }, | |||||
{ type: 'number', message: '所填必须为数字' }, | |||||
{ | |||||
validator: (rule, value, callback) => { | |||||
if (!value && value !== 0) { | |||||
callback(); | |||||
} | |||||
if (value <= 0) { | |||||
callback(new Error('Trial并发数量需要大于 0')); | |||||
} | |||||
callback(); | |||||
}, | |||||
trigger: ['blur', 'change'], | |||||
}, | |||||
], | |||||
}; | |||||
const state = reactive({ | |||||
showMore: false, | |||||
}); | |||||
// 更新 Yaml 编辑器文本。切换 tab 页时需要手动更新才能正常显示 | |||||
const setYamlValue = () => { | |||||
nextTick(() => { | |||||
yamlRef.value.setValue(); | |||||
}); | |||||
}; | |||||
// 用于yaml改动进行的一些列联动效果 | |||||
const changeYamlParams = (field) => { | |||||
try { | |||||
const yamlLoad = yaml.load(yamlRef.value.getValue() || form.yaml); | |||||
if (!yamlLoad) return; | |||||
const underScoreField = field.replace(/([A-Z])/g, '_$1').toLowerCase(); | |||||
switch (field) { | |||||
case 'maxExecDuration': | |||||
if (!isNull(form.maxExecDuration) && !isNull(form.maxExecDurationUnit)) { | |||||
yamlLoad[underScoreField] = `${form.maxExecDuration}${form.maxExecDurationUnit}`; | |||||
} | |||||
break; | |||||
default: | |||||
if (!isNull(form[field])) { | |||||
yamlLoad[underScoreField] = form[field]; | |||||
} | |||||
} | |||||
form.yaml = yaml.dump(yamlLoad); | |||||
} catch (err) { | |||||
console.error(err); | |||||
if (err.name === 'YAMLException') { | |||||
Message.error('Yaml 解析错误,请检查'); | |||||
} else { | |||||
throw err; | |||||
} | |||||
} | |||||
}; | |||||
// 直接编辑 Yaml 内容后触发解析 | |||||
const onYamlChange = (yamlValue) => { | |||||
try { | |||||
const yamlLoad = yaml.load(yamlValue); | |||||
if (!yamlLoad) return; | |||||
propertyAssign(form, underlineShiftHump(yamlLoad), (val) => !isNull(val)); | |||||
if ('max_exec_duration' in yamlLoad) { | |||||
[form.maxExecDuration, form.maxExecDurationUnit] = modifyTime(yamlLoad.max_exec_duration); | |||||
} | |||||
form.yaml = yamlValue; | |||||
} catch (err) { | |||||
console.error(err); | |||||
if (err.name === 'YAMLException') { | |||||
Message.error('Yaml 解析错误,请检查'); | |||||
} else { | |||||
throw err; | |||||
} | |||||
} | |||||
}; | |||||
// 最大运行时间 | |||||
const onMaxExecDurationChange = (value) => { | |||||
// 先移除非数字和小数点字符,然后调用系统浮点数解析 | |||||
const float = parseFloat(value.replace(/[^\d.]/g, '')); | |||||
form.maxExecDuration = Number.isNaN(float) ? 0 : float; | |||||
changeYamlParams('maxExecDuration'); | |||||
}; | |||||
// 资源配置 | |||||
const { | |||||
state: resourceState, | |||||
setResourcePage, | |||||
resourcePageInfo, | |||||
onResourcePageChange, | |||||
baseResourceChange, | |||||
getBaseResourceList, | |||||
selectOtherResource, | |||||
onResourceSelected, | |||||
onResourceClose, | |||||
setDefaultResource, | |||||
} = useResources( | |||||
{ | |||||
props, | |||||
form, | |||||
}, | |||||
ctx | |||||
); | |||||
const initForm = async () => { | |||||
setResourcePage({ current: 1 }); | |||||
Object.keys(defaultStageForm).forEach((key) => { | |||||
form[key] = isNil(props.model[key]) ? defaultStageForm[key] : props.model[key]; | |||||
}); | |||||
await getBaseResourceList(); | |||||
// 如果修改实验时,原资源规格不在第一页,那么组装一个资源规格到列表顶部 | |||||
if ( | |||||
form.resourceId && | |||||
form.resourceName && | |||||
!resourceState.baseResourceList.find((resource) => resource.id === form.resourceId) | |||||
) { | |||||
resourceState.baseResourceList.unshift({ | |||||
id: form.resourceId, | |||||
specsName: form.resourceName, | |||||
}); | |||||
} | |||||
}; | |||||
const validate = (resolve, reject) => { | |||||
let valid = true; | |||||
formRef.value.validate((isValid) => { | |||||
valid = valid && isValid; | |||||
}); | |||||
if (valid) { | |||||
if (typeof resolve === 'function') { | |||||
return resolve(form); | |||||
} | |||||
return true; | |||||
} | |||||
if (typeof reject === 'function') { | |||||
return reject(form); | |||||
} | |||||
return false; | |||||
}; | |||||
const clearValidate = (...args) => { | |||||
formRef.value.clearValidate(...args); | |||||
}; | |||||
return { | |||||
timeFmts, | |||||
formRef, | |||||
yamlRef, | |||||
form, | |||||
rules, | |||||
...toRefs(state), | |||||
...toRefs(resourceState), | |||||
initForm, | |||||
validate, | |||||
clearValidate, | |||||
setYamlValue, | |||||
resourcePageInfo, | |||||
selectOtherResource, | |||||
onResourceSelected, | |||||
onResourcePageChange, | |||||
onResourceClose, | |||||
setDefaultResource, | |||||
otherResourceColumns, | |||||
changeYamlParams, | |||||
onYamlChange, | |||||
baseResourceChange, | |||||
onMaxExecDurationChange, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
@import '@/assets/styles/variables.scss'; | |||||
@import '../style'; | |||||
.yaml { | |||||
height: 300px; | |||||
line-height: 18px; | |||||
} | |||||
.pb-22 { | |||||
padding-bottom: 22px; | |||||
} | |||||
.empty-text { | |||||
color: $infoColor; | |||||
} | |||||
.resources-container { | |||||
padding: 0 9px; | |||||
border: 1px solid #bbb; | |||||
.el-radio { | |||||
margin-top: 9px; | |||||
} | |||||
} | |||||
::v-deep .input-suffix .el-input-group__append { | |||||
color: $labelColor; | |||||
background: white; | |||||
} | |||||
</style> |
@@ -0,0 +1,170 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div id="form-page-wrapper" class="app-container"> | |||||
<TadlForm ref="formRef" /> | |||||
<div id="btns-wrapper"> | |||||
<!-- 新建实验 --> | |||||
<template v-if="isCreate"> | |||||
<el-button :loading="loadingState.save" @click="doSave">保存设置</el-button> | |||||
<el-button :loading="loadingState.create" type="primary" @click="doCreate" | |||||
>立即创建</el-button | |||||
> | |||||
</template> | |||||
<!-- 保存实验 --> | |||||
<template v-if="isSave"> | |||||
<el-button :loading="loadingState.save" type="primary" @click="doSave" | |||||
>保存设置,确认返回</el-button | |||||
> | |||||
</template> | |||||
<!-- 修改实验 --> | |||||
<template v-if="isEdit"> | |||||
<el-button @click="doCancel">取消</el-button> | |||||
<el-button :loading="loadingState.edit" type="primary" @click="doEdit">确定修改</el-button> | |||||
</template> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { computed, nextTick, reactive, ref } from '@vue/composition-api'; | |||||
import { Message, MessageBox } from 'element-ui'; | |||||
import { createExperiment, editExperiment } from '@/api/tadl'; | |||||
import { updateTitle } from '@/utils'; | |||||
import TadlForm from './components/tadlForm'; | |||||
const title = { | |||||
create: '创建实验', | |||||
save: '保存实验', | |||||
edit: '修改实验', | |||||
}; | |||||
export default { | |||||
name: 'TadlFormPage', | |||||
components: { TadlForm }, | |||||
beforeRouteEnter(to, from, next) { | |||||
const newTitle = title[to.params.formType || 'create']; | |||||
// 修改 navbar 中的 title | |||||
to.meta.title = newTitle; | |||||
// 修改页面 title | |||||
updateTitle(newTitle); | |||||
next(); | |||||
}, | |||||
setup(props, { root }) { | |||||
// 表单组件 ref | |||||
const formRef = ref(null); | |||||
// 表单类型:新建实验-create / 保存实验-save / 修改实验-edit | |||||
const formType = ref(root.$route.params.formType || 'create'); | |||||
const isCreate = computed(() => ['create', 'strategy'].includes(formType.value)); // 包括搜索策略中的创建实验 | |||||
const isSave = computed(() => formType.value === 'save'); | |||||
const isEdit = computed(() => formType.value === 'edit'); | |||||
// 不同按钮的 loading 状态 | |||||
const loadingState = reactive({ | |||||
create: false, | |||||
save: false, | |||||
edit: false, | |||||
}); | |||||
switch (formType.value) { | |||||
case 'edit': | |||||
case 'save': | |||||
case 'strategy': // 搜索策略中的创建实验 | |||||
nextTick(() => { | |||||
formRef.value.initForm(root.$route.params.formParams); | |||||
}); | |||||
break; | |||||
case 'create': | |||||
default: | |||||
nextTick(() => { | |||||
formRef.value.initForm(); | |||||
}); | |||||
break; | |||||
} | |||||
// 提交新建 | |||||
const doCreate = () => { | |||||
formRef.value.validate((form) => { | |||||
form.start = true; // 用于区分创建/保存实验 | |||||
loadingState.create = true; | |||||
createExperiment(form) | |||||
.then(() => { | |||||
Message.success(`实验创建成功`); | |||||
root.$router.push({ name: 'TadlList' }); | |||||
}) | |||||
.finally(() => { | |||||
loadingState.create = false; | |||||
}); | |||||
}); | |||||
}; | |||||
// 提交保存 | |||||
const doSave = () => { | |||||
formRef.value.validate((form) => { | |||||
form.start = false; // 用于区分创建/保存实验 | |||||
loadingState.save = true; | |||||
createExperiment(form) | |||||
.then(() => { | |||||
Message.success(`实验保存成功`); | |||||
root.$router.push({ name: 'TadlList' }); | |||||
}) | |||||
.finally(() => { | |||||
loadingState.save = false; | |||||
}); | |||||
}); | |||||
}; | |||||
// 提交修改 | |||||
const doEdit = () => { | |||||
formRef.value.validate((form) => { | |||||
loadingState.edit = true; | |||||
editExperiment(form) | |||||
.then(() => { | |||||
Message.success(`实验编辑成功`); | |||||
root.$router.push({ name: 'TadlList' }); | |||||
}) | |||||
.finally(() => { | |||||
loadingState.edit = false; | |||||
}); | |||||
}); | |||||
}; | |||||
// 取消 | |||||
const doCancel = () => { | |||||
MessageBox.confirm('取消将丢失所有信息', '确认').then(() => { | |||||
root.$router.back(); | |||||
}); | |||||
}; | |||||
return { | |||||
formRef, | |||||
isCreate, | |||||
isSave, | |||||
isEdit, | |||||
loadingState, | |||||
doCreate, | |||||
doSave, | |||||
doEdit, | |||||
doCancel, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
#form-page-wrapper { | |||||
max-width: 1400px; | |||||
margin-top: 50px; | |||||
} | |||||
#btns-wrapper { | |||||
margin: 50px 120px; | |||||
} | |||||
</style> |
@@ -0,0 +1,21 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
.area-title { | |||||
padding: 12px 15px; | |||||
margin-bottom: 20px; | |||||
border-left: 4px solid #333; | |||||
} |
@@ -0,0 +1,48 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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 const defaultStageForm = { | |||||
// 从算法中获得的 stage 使用 id/name 字段 | |||||
// 从实验中获得的 stage 使用 algorithmStageId/stageName 字段 | |||||
// 在提交实验时需要统一为 algorithmStageId/stageName 字段 | |||||
id: null, // 阶段 ID | |||||
algorithmStageId: null, | |||||
name: null, // 阶段名 | |||||
stageName: null, | |||||
stageOrder: null, // 阶段序号 | |||||
datasetName: null, // 数据集名称 | |||||
datasetVersion: null, // 数据集版本 | |||||
resourceId: null, // 资源 ID | |||||
resourceName: null, // 资源名称 | |||||
yaml: '', // yaml 运行参数 | |||||
maxTrialNum: 10, // 最大 Trial 次数 | |||||
multiGpu: undefined, // 是否使用多卡 | |||||
trialConcurrentNum: 1, // Trial 并发数量 | |||||
maxExecDuration: 0, // 最大运行时间 | |||||
maxExecDurationUnit: 'min', // 最大运行时间单位 | |||||
}; | |||||
// 弹窗其他资源表格列定义 | |||||
export const otherResourceColumns = [ | |||||
{ | |||||
prop: 'radio', | |||||
width: '50px', | |||||
}, | |||||
{ | |||||
prop: 'specsName', | |||||
label: '名称', | |||||
}, | |||||
]; |
@@ -0,0 +1,127 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="list-status-container"> | |||||
<el-tooltip placement="top" enterable effect="light"> | |||||
<template #content> | |||||
<div class="flex"> | |||||
<template v-for="(stage, index) in stages"> | |||||
<div :key="'status' + index" class="stage-status-block"> | |||||
<div | |||||
class="status-circle" | |||||
:style=" | |||||
`background-color: ${getValueFromMap(STAGE_STATUS_MAP, stage.status, 'bgColor')}` | |||||
" | |||||
/> | |||||
<div class="f18">{{ stage.stageName }}</div> | |||||
<div>{{ stage.endTime && parseTime(stage.endTime) }}</div> | |||||
</div> | |||||
</template> | |||||
</div> | |||||
</template> | |||||
<span class="status-info"> | |||||
<template v-for="(stage, index) in stages"> | |||||
<span v-if="index !== 0" :key="'line' + index" class="split-line" /> | |||||
<span | |||||
:key="'status' + index" | |||||
class="status-circle" | |||||
:style=" | |||||
`background-color: ${getValueFromMap(STAGE_STATUS_MAP, stage.status, 'bgColor')}` | |||||
" | |||||
/> | |||||
</template> | |||||
</span> | |||||
</el-tooltip> | |||||
<span class="status-text"> | |||||
{{ getValueFromMap(EXPERIMENT_STATUS_MAP, status, 'label') }} | |||||
</span> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { getValueFromMap, parseTime } from '@/utils'; | |||||
import { EXPERIMENT_STATUS_MAP, STAGE_STATUS_MAP } from '../../util'; | |||||
export default { | |||||
name: 'ListStatus', | |||||
props: { | |||||
stages: { | |||||
type: Array, | |||||
default: () => [], | |||||
}, | |||||
status: { | |||||
type: Number, | |||||
required: true, | |||||
}, | |||||
}, | |||||
setup() { | |||||
return { | |||||
getValueFromMap, | |||||
parseTime, | |||||
EXPERIMENT_STATUS_MAP, | |||||
STAGE_STATUS_MAP, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.list-status-container, | |||||
.status-info { | |||||
display: flex; | |||||
align-items: center; | |||||
} | |||||
.status-circle { | |||||
display: inline-block; | |||||
width: 6px; | |||||
height: 6px; | |||||
border-radius: 50%; | |||||
} | |||||
.split-line { | |||||
display: inline-block; | |||||
width: 7px; | |||||
height: 0; | |||||
border-top: 2px #bbb solid; | |||||
} | |||||
.status-text { | |||||
margin-left: 5px; | |||||
} | |||||
.stage-status-block { | |||||
display: flex; | |||||
flex-direction: column; | |||||
align-items: center; | |||||
width: 120px; | |||||
&::before { | |||||
display: inline-block; | |||||
width: 100px; | |||||
margin: 7px 60px -7px -60px; | |||||
content: ''; | |||||
border-bottom: 2px solid #bbb; | |||||
} | |||||
&:first-child { | |||||
padding-top: 2px; | |||||
&::before { | |||||
display: none; | |||||
} | |||||
} | |||||
.status-circle { | |||||
width: 12px; | |||||
height: 12px; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,220 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="app-container"> | |||||
<ProTable | |||||
ref="proTable" | |||||
create-title="创建实验" | |||||
:columns="columns" | |||||
:form-items="listQueryFormItems" | |||||
:list-request="list" | |||||
:before-list-fn="beforeListFn" | |||||
:after-list-fn="afterListFn" | |||||
show-refresh | |||||
loading-type="header" | |||||
:refresh-immediate="false" | |||||
:table-attrs="tableAttrs" | |||||
@add="onCreate" | |||||
> | |||||
<template #status="scope"> | |||||
<div class="flex"> | |||||
<ListStatus :stages="scope.row.stages" :status="scope.row.status" /> | |||||
<MsgPopover | |||||
v-if="scope.row.statusDetail" | |||||
:status-detail="scope.row.statusDetail" | |||||
class="ml-4" | |||||
/> | |||||
</div> | |||||
</template> | |||||
</ProTable> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { computed, nextTick, onUnmounted, reactive, ref } from '@vue/composition-api'; | |||||
import { Message, MessageBox } from 'element-ui'; | |||||
import ProTable from '@/components/ProTable'; | |||||
import MsgPopover from '@/components/MsgPopover'; | |||||
import { list, expDetail, pauseExp, startExp, deleteExp } from '@/api/tadl'; | |||||
import { getValueFromMap, Constant } from '@/utils'; | |||||
import { useKeepPageInfo } from '@/hooks'; | |||||
import { MODEL_TYPE_ENUM } from '../util'; | |||||
import ListStatus from './components/listStatus'; | |||||
import { getListColumns, listQueryFormItems, needPoll } from './util'; | |||||
export default { | |||||
name: 'TadlList', | |||||
components: { | |||||
ProTable, | |||||
ListStatus, | |||||
MsgPopover, | |||||
}, | |||||
beforeRouteEnter(to, from, next) { | |||||
// 如果不是从记录页返回到列表页的,页码重置为 1 | |||||
if (!['ExperimentDetail'].includes(from.name)) { | |||||
next((vm) => vm.pageEnter(false)); | |||||
return; | |||||
} | |||||
// 从记录页返回时保留页码和排序状态 | |||||
next((vm) => vm.pageEnter(true)); | |||||
}, | |||||
setup(props, { root }) { | |||||
// proTable ref | |||||
const proTable = ref(null); | |||||
const defaultSort = reactive({ prop: undefined, order: undefined }); | |||||
const setDefaultSort = (sort) => { | |||||
Object.assign(defaultSort, sort); | |||||
}; | |||||
// 创建按钮跳转表单页 | |||||
const onCreate = () => { | |||||
root.$router.push({ name: 'TadlForm', params: { formType: 'create' } }); | |||||
}; | |||||
const modelTypeFormatter = (modelType) => { | |||||
return getValueFromMap(MODEL_TYPE_ENUM, modelType, 'label'); | |||||
}; | |||||
// 列操作方法 | |||||
// 查看详情 | |||||
const toDetail = (row) => { | |||||
root.$router.push({ | |||||
path: `/tadl/experiment/${row.id}`, | |||||
}); | |||||
}; | |||||
// 开始运行 | |||||
const doStart = async (row) => { | |||||
await startExp(row.id); | |||||
Message.success('实验启动成功'); | |||||
proTable.value.refresh(); | |||||
}; | |||||
// 编辑 | |||||
const doEdit = async (row) => { | |||||
const formParams = await expDetail(row.id); | |||||
root.$router.push({ | |||||
name: 'TadlForm', | |||||
params: { | |||||
formType: 'edit', | |||||
formParams, | |||||
}, | |||||
}); | |||||
}; | |||||
// 删除 | |||||
const doDelete = (row) => { | |||||
MessageBox.confirm('确认删除该实验', '确认').then(async () => { | |||||
await deleteExp(row.id); | |||||
Message.success('实验删除成功'); | |||||
proTable.value.refresh(); | |||||
}); | |||||
}; | |||||
// 暂停 | |||||
const doPause = (row) => { | |||||
MessageBox.confirm('确认暂停该实验', '确认').then(async () => { | |||||
await pauseExp(row.id); | |||||
Message.success('实验暂停成功'); | |||||
proTable.value.refresh(); | |||||
}); | |||||
}; | |||||
// 获取列定义 | |||||
const columns = computed(() => { | |||||
return getListColumns({ | |||||
toDetail, | |||||
doStart, | |||||
doEdit, | |||||
doDelete, | |||||
doPause, | |||||
modelTypeFormatter, | |||||
}); | |||||
}); | |||||
const afterEnter = () => { | |||||
proTable.value.refresh(); | |||||
}; | |||||
const pageInfoSetter = ({ current, pageSize, sort: { sort, order }, query }) => { | |||||
setDefaultSort({ prop: sort, order: Constant.tableSortMap2Element[order] }); | |||||
nextTick(() => { | |||||
proTable.value.setPagination({ current, size: pageSize }); | |||||
proTable.value.setSort({ sort, order }); | |||||
proTable.value.setQuery(query); | |||||
}); | |||||
}; | |||||
const { pageEnter, updatePageInfo } = useKeepPageInfo({ | |||||
afterEnter, | |||||
pageInfoGetter: 'tadl/pageInfo', | |||||
updateAction: 'tadl/updateExperimentPageInfo', | |||||
pageInfoSetter, | |||||
}); | |||||
// 判断是否轮询 | |||||
const keepPoll = ref(true); | |||||
let timeoutId; | |||||
onUnmounted(() => { | |||||
keepPoll.value = false; | |||||
}); | |||||
const beforeListFn = () => { | |||||
if (timeoutId) { | |||||
clearTimeout(timeoutId); | |||||
} | |||||
}; | |||||
const afterListFn = (exps) => { | |||||
if (exps.some((exp) => needPoll(exp))) { | |||||
timeoutId = setTimeout(() => { | |||||
if (keepPoll.value) { | |||||
proTable.value.refresh(); | |||||
} | |||||
}, 3000); | |||||
} | |||||
const { currentPage: current, pageSize } = proTable.value.pagination; | |||||
updatePageInfo({ | |||||
current, | |||||
pageSize, | |||||
sort: { ...proTable.value.sortInfo }, | |||||
query: { ...proTable.value.state.queryFormModel }, | |||||
}); | |||||
}; | |||||
const tableAttrs = computed(() => ({ defaultSort })); | |||||
return { | |||||
proTable, | |||||
tableAttrs, | |||||
onCreate, | |||||
columns, | |||||
listQueryFormItems, | |||||
list, | |||||
beforeListFn, | |||||
afterListFn, | |||||
pageEnter, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
::v-deep .name-col .el-link { | |||||
max-width: 100%; | |||||
span { | |||||
overflow: hidden; | |||||
text-overflow: ellipsis; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,175 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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 { runTimeFormatter, EXPERIMENT_STATUS_MAP, MODEL_TYPE_ENUM } from '../util'; | |||||
const allExperimentStatusList = [{ label: '全部', value: null }].concat( | |||||
Object.values(EXPERIMENT_STATUS_MAP).map((status) => ({ | |||||
label: status.label, | |||||
value: status.value, | |||||
})) | |||||
); | |||||
const allModelTypeList = [{ label: '全部', value: null }].concat( | |||||
Object.values(MODEL_TYPE_ENUM).map((status) => ({ | |||||
label: status.label, | |||||
value: status.value, | |||||
})) | |||||
); | |||||
// 获取列表页表格列定义 | |||||
export function getListColumns({ | |||||
toDetail, | |||||
doStart, | |||||
doEdit, | |||||
doDelete, | |||||
doPause, | |||||
modelTypeFormatter, | |||||
}) { | |||||
return [ | |||||
{ | |||||
label: 'ID', | |||||
prop: 'id', | |||||
width: 80, | |||||
sortable: 'custom', | |||||
fixed: true, | |||||
}, | |||||
{ | |||||
label: '名称', | |||||
prop: 'name', | |||||
fixed: true, | |||||
type: 'link', | |||||
func: toDetail, | |||||
className: 'name-col', | |||||
}, | |||||
{ | |||||
label: '状态', | |||||
prop: 'status', | |||||
minWidth: '120px', | |||||
showOverflowTooltip: false, | |||||
dropdownList: allExperimentStatusList, | |||||
}, | |||||
{ | |||||
label: '模型类别', | |||||
prop: 'modelType', | |||||
minWidth: '110px', | |||||
formatter: modelTypeFormatter, | |||||
dropdownList: allModelTypeList, | |||||
}, | |||||
{ | |||||
label: '开始时间', | |||||
prop: 'startTime', | |||||
type: 'time', | |||||
minWidth: '160px', | |||||
sortable: 'custom', | |||||
}, | |||||
{ | |||||
label: '运行时间', | |||||
prop: 'runTime', | |||||
formatter: runTimeFormatter, | |||||
}, | |||||
{ | |||||
label: '创建人', | |||||
prop: 'createUser', | |||||
}, | |||||
{ | |||||
label: '描述', | |||||
prop: 'description', | |||||
minWidth: '200px', | |||||
}, | |||||
{ | |||||
label: '操作', | |||||
type: 'operation', | |||||
width: '370px', | |||||
fixed: 'right', | |||||
operationLimit: 7, | |||||
operations: [ | |||||
{ | |||||
label: '开始运行', | |||||
func: doStart, | |||||
hideFunc(row) { | |||||
// 待运行展示开始运行 | |||||
return ![ | |||||
EXPERIMENT_STATUS_MAP.TO_RUN.value, | |||||
EXPERIMENT_STATUS_MAP.FAILED.value, | |||||
EXPERIMENT_STATUS_MAP.PAUSED.value, | |||||
].includes(row.status); | |||||
}, | |||||
clickOnceTime: 3000, | |||||
}, | |||||
{ | |||||
label: '编辑', | |||||
func: doEdit, | |||||
hideFunc(row) { | |||||
// 待运行展示编辑 | |||||
return row.status !== EXPERIMENT_STATUS_MAP.TO_RUN.value; | |||||
}, | |||||
}, | |||||
{ | |||||
label: '删除', | |||||
func: doDelete, | |||||
hideFunc(row) { | |||||
// 已完成、已暂停、运行失败 展示删除 | |||||
return ![ | |||||
EXPERIMENT_STATUS_MAP.FINISHED.value, | |||||
EXPERIMENT_STATUS_MAP.PAUSED.value, | |||||
EXPERIMENT_STATUS_MAP.FAILED.value, | |||||
].includes(row.status); | |||||
}, | |||||
}, | |||||
{ | |||||
label: '暂停', | |||||
func: doPause, | |||||
hideFunc(row) { | |||||
// 运行中、等待中 展示暂停 | |||||
return ![ | |||||
EXPERIMENT_STATUS_MAP.RUNNING.value, | |||||
EXPERIMENT_STATUS_MAP.WAITING.value, | |||||
].includes(row.status); | |||||
}, | |||||
}, | |||||
], | |||||
}, | |||||
]; | |||||
} | |||||
// 列表页查询表单项 | |||||
export const listQueryFormItems = [ | |||||
{ | |||||
prop: 'name', | |||||
placeholder: '输入名称或ID查询', | |||||
class: 'w-200', | |||||
change: 'query', | |||||
}, | |||||
{ | |||||
type: 'button', | |||||
btnText: '重置', | |||||
func: 'resetQuery', | |||||
}, | |||||
{ | |||||
type: 'button', | |||||
btnText: '搜索', | |||||
btnType: 'primary', | |||||
func: 'query', | |||||
}, | |||||
]; | |||||
// 判断实验是否需要轮询 | |||||
export function needPoll(exp) { | |||||
return [EXPERIMENT_STATUS_MAP.WAITING.value, EXPERIMENT_STATUS_MAP.RUNNING.value].includes( | |||||
exp.status | |||||
); | |||||
} |
@@ -0,0 +1,559 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div> | |||||
<div class="tabs"> | |||||
<el-tabs v-model="active" class="eltabs-inlineblock" @tab-click="handleClick"> | |||||
<el-tab-pane id="tab_0" label="基本配置" name="page" /> | |||||
<el-tab-pane id="tab_1" label="运行参数" name="params" /> | |||||
</el-tabs> | |||||
</div> | |||||
<el-form | |||||
v-show="active === 'page'" | |||||
ref="formRef" | |||||
:model="form" | |||||
:disabled="type === 'check'" | |||||
:rules="rules" | |||||
label-width="150px" | |||||
class="form" | |||||
> | |||||
<!-- 创建阶段 --> | |||||
<template v-if="steps === 0"> | |||||
<el-form-item label="默认指标" prop="default_metric"> | |||||
<el-input | |||||
id="default_metric" | |||||
v-model="createForm.default_metric" | |||||
placeholder="由上传的算法文件生成" | |||||
disabled | |||||
style="width: 200px;" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="GPU" prop="gpu"> | |||||
<el-switch | |||||
id="gpu" | |||||
v-model="createForm.gpu" | |||||
:active-value="true" | |||||
:inactive-value="false" | |||||
disabled | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="算法类型" prop="alg_type"> | |||||
<el-radio-group v-model="createForm.alg_type" disabled> | |||||
<el-radio label="NAS" border class="mr-0">NAS</el-radio> | |||||
</el-radio-group> | |||||
</el-form-item> | |||||
<el-form-item label="运行环境"> | |||||
<el-input | |||||
id="imageId" | |||||
v-model="createForm.platform" | |||||
placeholder="由上传的算法文件生成" | |||||
disabled | |||||
style="width: 200px;" | |||||
/> | |||||
<el-input | |||||
id="imagePath" | |||||
v-model="createForm.platform_version" | |||||
placeholder="由上传的算法文件生成" | |||||
disabled | |||||
style="width: 200px;" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item v-if="createForm.alg_type === 'NAS'" label="ONE_SHOT" prop="one_shot"> | |||||
<el-switch | |||||
id="one_shot" | |||||
v-model="createForm.one_shot" | |||||
:active-value="true" | |||||
:inactive-value="false" | |||||
disabled | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="算法描述" prop="description"> | |||||
<el-input | |||||
id="description" | |||||
v-model="createForm.description" | |||||
type="textarea" | |||||
:rows="3" | |||||
maxlength="256" | |||||
show-word-limit | |||||
placeholder | |||||
style="width: 400px;" | |||||
/> | |||||
</el-form-item> | |||||
</template> | |||||
<!-- 配置阶段 --> | |||||
<template v-else> | |||||
<el-form-item label="支持多卡训练"> | |||||
<el-switch | |||||
id="multi_gpu" | |||||
v-model="pageForm.multi_gpu" | |||||
:active-value="true" | |||||
:inactive-value="false" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item ref="datasetId" label="数据集" prop="datasetId"> | |||||
<InfoSelect | |||||
v-model="pageForm.dataset_id" | |||||
style="display: inline-block;" | |||||
width="200px" | |||||
placeholder="请选择数据集" | |||||
:dataSource="datasetIdList" | |||||
value-key="id" | |||||
label-key="name" | |||||
filterable | |||||
@change="onDatasetChange" | |||||
/> | |||||
<InfoSelect | |||||
v-model="pageForm.dataset_path" | |||||
style="display: inline-block;" | |||||
width="200px" | |||||
placeholder="请选择数据集版本" | |||||
:dataSource="datasetVersionList" | |||||
value-key="versionUrl" | |||||
label-key="versionName" | |||||
filterable | |||||
@change="onDatasetVersionChange" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="运行命令"> | |||||
<el-input | |||||
id="pythonVersion" | |||||
v-model="pageForm.python_version" | |||||
placeholder="由上传文件生成" | |||||
disabled | |||||
style="width: 200px;" | |||||
/> | |||||
<el-input | |||||
id="executeScript" | |||||
v-model="pageForm.execute_script" | |||||
placeholder="由上传文件生成" | |||||
disabled | |||||
style="width: 200px;" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item ref="maxExecDuration" label="现阶段最大运行时间" prop="maxExecDuration"> | |||||
<el-input | |||||
id="maxExecDuration" | |||||
v-model="pageForm.max_exec_duration" | |||||
placeholder="请输入时间" | |||||
clearable | |||||
style="width: 200px;" | |||||
@change="onMaxExecDurationChange" | |||||
/> | |||||
<InfoSelect | |||||
v-model="pageForm.max_exec_duration_unit" | |||||
style="display: inline-block;" | |||||
width="190" | |||||
placeholder="请选择时间单位" | |||||
:dataSource="timeFmts" | |||||
value-key="value" | |||||
label-key="label" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="最大Trial次数" prop="max_trial_num"> | |||||
<el-input | |||||
v-model.number="pageForm.max_trial_num" | |||||
placeholder="请输入最大Trial次数" | |||||
clearable | |||||
style="width: 200px;" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="Trial并发数量" prop="trial_concurrent_num"> | |||||
<el-input | |||||
v-model.number="pageForm.trial_concurrent_num" | |||||
placeholder="请输入Trial并发数量" | |||||
clearable | |||||
style="width: 200px;" | |||||
/> | |||||
</el-form-item> | |||||
</template> | |||||
</el-form> | |||||
<div v-show="active === 'params'" style="position: relative; height: 500px;"> | |||||
<YamlEditor ref="yamlRef" :value="yamlValue" :read-only="steps === 0 || type === 'check'" /> | |||||
</div> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import yaml from 'js-yaml'; | |||||
import { Message } from 'element-ui'; | |||||
import { computed, nextTick, reactive, toRefs } from '@vue/composition-api'; | |||||
import { getPublishedDatasets, getDatasetVersions } from '@/api/preparation/dataset'; | |||||
import { parseYamlParams } from '@/api/tadl/strategy'; | |||||
import InfoSelect from '@/components/InfoSelect'; | |||||
import YamlEditor from '@/components/YamlEditor/index'; | |||||
import { propertyAssign } from '@/utils'; | |||||
import { timeFmts, getModelByCode } from '../../util'; | |||||
import { modifyTime, isNull } from '../util'; | |||||
const defaultCreateForm = { | |||||
default_metric: null, // 默认指标 | |||||
alg_type: 'NAS', // 算法类型 | |||||
platform: null, // 框架名称 | |||||
platform_version: null, // 框架版本 | |||||
gpu: false, // 是否支持gpu计算 | |||||
one_shot: false, // 是否oneshot | |||||
description: null, // 算法描述 | |||||
}; | |||||
const defaultPageForm = { | |||||
multi_gpu: false, // 是否支持多卡 | |||||
dataset_id: null, // 数据集id | |||||
dataset_name: null, // 数据集名称 | |||||
dataset_path: null, // 数据集路径 | |||||
dataset_version: null, // 数据集版本 | |||||
python_version: null, // python版本 | |||||
execute_script: null, // 算法启动文件 | |||||
max_trial_num: null, // 最大trial次数 | |||||
max_exec_duration: null, // 当前阶段最大运行时间 | |||||
max_exec_duration_unit: null, // 最大时间单位 | |||||
trial_concurrent_num: null, // trial并发数量 | |||||
}; | |||||
export default { | |||||
name: 'CreatePageForm', | |||||
components: { YamlEditor, InfoSelect }, | |||||
props: { | |||||
// 用于第一阶段请求参数 | |||||
baseForm: { | |||||
type: Object, | |||||
default: () => ({}), | |||||
}, | |||||
// 阶段值 | |||||
steps: { | |||||
type: Number, | |||||
default: 0, | |||||
}, | |||||
// create/edit/check | |||||
type: { | |||||
type: String, | |||||
default: 'create', | |||||
}, | |||||
// 算法上传路径,创建时需要由此路径获取 yaml | |||||
zipPath: { | |||||
type: String, | |||||
}, | |||||
}, | |||||
setup(props, ctx) { | |||||
const data = reactive({ | |||||
active: 'page', | |||||
yamlValue: '', | |||||
yamlParams: {}, | |||||
datasetIdList: [], | |||||
datasetVersionList: [], | |||||
valueForm: {}, // 用于存入外部传进的form值 | |||||
}); | |||||
const pageForm = reactive({ ...defaultPageForm }); | |||||
const createForm = reactive({ ...defaultCreateForm }); | |||||
const refs = reactive({ | |||||
yamlRef: null, | |||||
formRef: null, | |||||
datasetId: null, | |||||
maxExecDuration: null, | |||||
}); | |||||
const rules = { | |||||
description: [{ required: true, message: '请输入算法描述', trigger: ['blur', 'change'] }], | |||||
datasetId: [ | |||||
{ | |||||
required: true, | |||||
trigger: 'manual', | |||||
validator: (rule, value, callback) => { | |||||
if (!pageForm.dataset_id) { | |||||
callback(new Error('请选择数据集')); | |||||
} | |||||
if (!pageForm.dataset_path) { | |||||
callback(new Error('请选择数据集版本')); | |||||
} | |||||
callback(); | |||||
}, | |||||
}, | |||||
], | |||||
maxExecDuration: [ | |||||
{ | |||||
required: true, | |||||
validator: (rule, value, callback) => { | |||||
if (!pageForm.max_exec_duration) { | |||||
callback(new Error('请输入时间')); | |||||
} | |||||
// eslint-disable-next-line no-restricted-globals | |||||
if (isNaN(Number(pageForm.max_exec_duration))) { | |||||
callback(new Error('时间为数值')); | |||||
} | |||||
if (Number(pageForm.max_exec_duration) <= 0) { | |||||
callback(new Error('时间需要大于 0')); | |||||
} | |||||
if (!pageForm.max_exec_duration_unit) { | |||||
callback(new Error('请选择时间单位')); | |||||
} | |||||
callback(); | |||||
}, | |||||
trigger: 'blur', | |||||
}, | |||||
], | |||||
max_trial_num: [ | |||||
{ required: true, message: '请输入最大Trial次数', trigger: ['blur', 'change'] }, | |||||
{ type: 'number', message: '所填必须为数字' }, | |||||
{ | |||||
validator: (rule, value, callback) => { | |||||
if (!value && value !== 0) { | |||||
callback(); | |||||
} | |||||
if (value <= 0) { | |||||
callback(new Error('最大Trial次数需要大于 0')); | |||||
} | |||||
callback(); | |||||
}, | |||||
trigger: ['blur', 'change'], | |||||
}, | |||||
], | |||||
trial_concurrent_num: [ | |||||
{ required: true, message: '请输入Trial并发数量', trigger: ['blur', 'change'] }, | |||||
{ type: 'number', message: '所填必须为数字' }, | |||||
{ | |||||
validator: (rule, value, callback) => { | |||||
if (!value && value !== 0) { | |||||
callback(); | |||||
} | |||||
if (value <= 0) { | |||||
callback(new Error('Trial并发数量需要大于 0')); | |||||
} | |||||
callback(); | |||||
}, | |||||
trigger: ['blur', 'change'], | |||||
}, | |||||
], | |||||
}; | |||||
const form = computed(() => (props.steps === 0 ? createForm : pageForm)); | |||||
// 数据集 | |||||
const getDatasetVersion = async (datasetId, keepValue = false) => { | |||||
data.datasetVersionList = await getDatasetVersions(datasetId); | |||||
if (keepValue && pageForm.dataset_path) { | |||||
const version = data.datasetVersionList.find( | |||||
(version) => version.versionUrl === pageForm.dataset_path | |||||
); | |||||
if (!version) { | |||||
pageForm.dataset_path = null; | |||||
Message.warning('原有数据集版本不存在,请重新选择'); | |||||
} | |||||
} | |||||
}; | |||||
const getDataset = async (keepValue = false) => { | |||||
data.datasetIdList = ( | |||||
await getPublishedDatasets({ | |||||
size: 1000, | |||||
annotateType: getModelByCode(data.valueForm.model_type, 'label'), | |||||
}) | |||||
).result; | |||||
if (!keepValue || !pageForm.dataset_id) { | |||||
pageForm.dataset_path = null; | |||||
} else { | |||||
const dataset = data.datasetIdList.find((dataset) => dataset.id === pageForm.dataset_id); | |||||
if (!dataset) { | |||||
Message.warning('原有数据集不存在,请重新选择'); | |||||
pageForm.dataset_id = pageForm.dataset_path = pageForm.dataset_version = null; | |||||
return; | |||||
} | |||||
getDatasetVersion(dataset.id, true); | |||||
} | |||||
}; | |||||
const onDatasetChange = (datasetId) => { | |||||
pageForm.dataset_path = pageForm.dataset_version = pageForm.dataset_name = null; | |||||
data.datasetVersionList = []; | |||||
if (!datasetId) return; | |||||
getDatasetVersion(datasetId); | |||||
const selectedDataset = data.datasetIdList.find((i) => i.id === datasetId); | |||||
pageForm.dataset_name = selectedDataset.name; | |||||
}; | |||||
const onDatasetVersionChange = () => { | |||||
const version = data.datasetVersionList.find( | |||||
(version) => version.versionUrl === pageForm.dataset_path | |||||
); | |||||
pageForm.dataset_version = version ? version.versionName : null; | |||||
refs.datasetId.validate('manual'); | |||||
}; | |||||
// 最大运行时间 | |||||
const onMaxExecDurationChange = (value) => { | |||||
// 先移除非数字和小数点字符,然后调用系统浮点数解析 | |||||
const float = parseFloat(value.replace(/[^\d.]/g, '')); | |||||
pageForm.max_exec_duration = Number.isNaN(float) ? 0 : float; | |||||
}; | |||||
// yaml语法转换 | |||||
const yamlLoad = () => { | |||||
try { | |||||
// 将yaml字符转换成yaml对象格式 | |||||
data.yamlParams = yaml.load(refs.yamlRef.getValue() || data.yamlValue); | |||||
propertyAssign(form.value, data.yamlParams, (val) => !isNull(val)); | |||||
if ('command' in data.yamlParams) | |||||
[pageForm.python_version, pageForm.execute_script] = data.yamlParams.command.split(' '); | |||||
if ('max_exec_duration' in data.yamlParams) | |||||
[pageForm.max_exec_duration, pageForm.max_exec_duration_unit] = modifyTime( | |||||
data.yamlParams.max_exec_duration | |||||
); | |||||
} catch (err) { | |||||
console.error(err); | |||||
throw err; | |||||
} | |||||
}; | |||||
// 初始解析yaml | |||||
const getYaml = async () => { | |||||
data.yamlValue = await parseYamlParams({ | |||||
algorithm: props.steps ? data.valueForm.name : props.baseForm.name || undefined, | |||||
zipPath: props.zipPath || undefined, | |||||
stageOrder: props.steps, | |||||
versionName: props.steps | |||||
? data.valueForm.version_name | |||||
: props.baseForm.version_name || undefined, | |||||
}); | |||||
yamlLoad(); | |||||
if (!props.steps) { | |||||
// 用于回填模型类别 | |||||
ctx.emit('yaml-loaded', { | |||||
modelType: data.yamlParams?.model_type || 'ImageClassify', | |||||
name: data.yamlParams.alg_name, | |||||
}); | |||||
} | |||||
}; | |||||
// 外部调用传值 | |||||
const initForm = (originForm = {}) => { | |||||
// 保存外部传入的值 | |||||
data.valueForm = originForm; | |||||
if (props.steps) { | |||||
getDataset(true); | |||||
const order = originForm.stage.find((s) => s.stage_order === props.steps); | |||||
if (order) { | |||||
propertyAssign(form.value, order, (val) => !isNull(val)); | |||||
data.yamlValue = order.yaml; | |||||
data.yamlParams = yaml.load(data.yamlValue); | |||||
} else { | |||||
getYaml(); | |||||
} | |||||
} else { | |||||
propertyAssign(form.value, originForm, (val) => !isNull(val)); | |||||
data.yamlValue = originForm.yaml; | |||||
data.yamlParams = yaml.load(data.yamlValue); | |||||
} | |||||
}; | |||||
const shiftBasePage = () => { | |||||
nextTick(() => { | |||||
refs.yamlRef.codeValid() ? yamlLoad() : (data.active = 'params'); | |||||
}); | |||||
}; | |||||
const shiftYamlParams = () => { | |||||
propertyAssign(data.yamlParams, form.value, (val) => !isNull(val)); | |||||
if (props.steps) { | |||||
if (!isNull(pageForm.python_version) && !isNull(pageForm.execute_script)) { | |||||
data.yamlParams.command = `${pageForm.python_version} ${pageForm.execute_script}`; | |||||
} | |||||
if (!isNull(pageForm.max_exec_duration) && !isNull(pageForm.max_exec_duration_unit)) { | |||||
data.yamlParams.max_exec_duration = `${pageForm.max_exec_duration}${pageForm.max_exec_duration_unit}`; | |||||
} | |||||
} | |||||
// 将yaml对象格式转成字符串 | |||||
data.yamlValue = yaml.dump(data.yamlParams); | |||||
nextTick(() => { | |||||
refs.yamlRef.setValue(); | |||||
}); | |||||
}; | |||||
const handleClick = () => { | |||||
data.active === 'page' ? shiftBasePage() : shiftYamlParams(); | |||||
nextTick(() => { | |||||
ctx.emit('tabs-change', data.active); | |||||
}); | |||||
}; | |||||
const getFormValue = () => { | |||||
return [form.value, data.yamlValue]; | |||||
}; | |||||
// 表单校验方法 | |||||
const validateForm = (resolve, reject) => { | |||||
shiftYamlParams(); // 单击下一步时需要转换 | |||||
refs.formRef.validate((isValid) => { | |||||
if (isValid) { | |||||
if (typeof resolve === 'function') { | |||||
return resolve(form.value, data.yamlValue); | |||||
} | |||||
return true; | |||||
} | |||||
if (typeof reject === 'function') { | |||||
return reject(form.value); | |||||
} | |||||
return false; | |||||
}); | |||||
}; | |||||
// 清空表单 | |||||
const clearValidate = () => { | |||||
refs.formRef.clearValidate(); | |||||
}; | |||||
// 重置表单 | |||||
const resetForm = () => { | |||||
Object.assign(createForm, defaultCreateForm); | |||||
Object.assign(pageForm, defaultPageForm); | |||||
data.active = 'page'; | |||||
data.yamlValue = ''; | |||||
data.yamlParams = {}; | |||||
nextTick(() => { | |||||
clearValidate(); | |||||
ctx.emit('tabs-change', data.active); | |||||
}); | |||||
}; | |||||
return { | |||||
...toRefs(data), | |||||
...toRefs(refs), | |||||
createForm, | |||||
pageForm, | |||||
form, | |||||
rules, | |||||
handleClick, | |||||
onDatasetChange, | |||||
onDatasetVersionChange, | |||||
onMaxExecDurationChange, | |||||
getYaml, | |||||
initForm, | |||||
getFormValue, | |||||
validateForm, | |||||
resetForm, | |||||
timeFmts, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.form { | |||||
margin-left: 20px; | |||||
} | |||||
.tabs { | |||||
margin-bottom: 20px; | |||||
text-align: center; | |||||
} | |||||
.el-radio.is-bordered { | |||||
width: 100px; | |||||
height: 35px; | |||||
padding: 10px 0; | |||||
text-align: center; | |||||
} | |||||
</style> |
@@ -0,0 +1,101 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="wrapper"> | |||||
<BaseModal | |||||
:visible.sync="visible" | |||||
title="发布搜索策略" | |||||
:loading="releasing" | |||||
@cancel="visible = false" | |||||
@ok="onVersionRelease" | |||||
> | |||||
<el-form ref="formRef" :model="form" label-width="100px"> | |||||
<el-form-item label="策略名称"> | |||||
<el-input | |||||
id="name" | |||||
v-model.trim="form.name" | |||||
placeholder | |||||
disabled | |||||
maxlength="50" | |||||
show-word-limit | |||||
style="width: 300px;" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="当前版本"> | |||||
<el-input | |||||
id="currentVersion" | |||||
v-model="form.currentVersion" | |||||
style="width: 200px;" | |||||
disabled | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="下一版本"> | |||||
<el-input id="nextVersion" v-model="form.nextVersion" disabled style="width: 200px;"> | |||||
</el-input> | |||||
</el-form-item> | |||||
</el-form> | |||||
</BaseModal> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { Message } from 'element-ui'; | |||||
import { reactive, ref } from '@vue/composition-api'; | |||||
import { versionRelease } from '@/api/tadl/strategy'; | |||||
import BaseModal from '@/components/BaseModal'; | |||||
const defaultForm = { | |||||
name: null, | |||||
currentVersion: null, | |||||
nextVersion: null, | |||||
}; | |||||
export default { | |||||
name: 'ReleaseDialog', | |||||
components: { BaseModal }, | |||||
setup(props, ctx) { | |||||
const formRef = ref(null); | |||||
const form = reactive({ ...defaultForm }); | |||||
const visible = ref(false); | |||||
const releasing = ref(false); | |||||
const handleShow = (info) => { | |||||
visible.value = true; | |||||
Object.assign(form, info); | |||||
}; | |||||
// 版本发布 | |||||
const onVersionRelease = () => { | |||||
formRef.value.validate((valid) => { | |||||
if (valid) { | |||||
releasing.value = true; | |||||
versionRelease(form) | |||||
.then(() => { | |||||
ctx.emit('release-success'); | |||||
Message.success('版本发布成功'); | |||||
visible.value = false; | |||||
}) | |||||
.finally(() => { | |||||
releasing.value = false; | |||||
}); | |||||
} | |||||
}); | |||||
}; | |||||
return { | |||||
formRef, | |||||
form, | |||||
visible, | |||||
releasing, | |||||
handleShow, | |||||
onVersionRelease, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> |
@@ -0,0 +1,514 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<el-drawer | |||||
ref="drawerRef" | |||||
:title="title" | |||||
:before-close="handleClose" | |||||
:visible.sync="visible" | |||||
size="40%" | |||||
> | |||||
<!-- 阶段步骤条 --> | |||||
<el-steps :active="active" finish-status="success" align-center> | |||||
<el-step title="创建策略"></el-step> | |||||
<el-step title="TRAIN阶段配置"></el-step> | |||||
<el-step title="SELECT阶段配置"></el-step> | |||||
<el-step title="RETRAIN配置"></el-step> | |||||
</el-steps> | |||||
<!-- 基本配置表单 --> | |||||
<el-form | |||||
v-if="active === 0" | |||||
ref="baseFormRef" | |||||
:rules="rules" | |||||
:model="baseForm" | |||||
:disabled="type === 'check'" | |||||
label-width="150px" | |||||
class="form" | |||||
> | |||||
<el-form-item | |||||
v-if="type === 'create'" | |||||
ref="algorithmPathRef" | |||||
label="上传算法文件" | |||||
prop="algorithm_path" | |||||
> | |||||
<upload-inline | |||||
ref="uploadRef" | |||||
action="fakeApi" | |||||
accept=".zip" | |||||
list-type="text" | |||||
:acceptSize="algorithmConfig.uploadFileAcceptSize" | |||||
:acceptSizeFormat="uploadSizeFomatter" | |||||
:params="uploadParams" | |||||
:show-file-count="false" | |||||
:auto-upload="true" | |||||
:hash="false" | |||||
:filters="uploadFilters" | |||||
:limit="1" | |||||
:on-remove="onFileRemove" | |||||
@uploadStart="uploadStart" | |||||
@uploadSuccess="uploadSuccess" | |||||
@uploadError="uploadError" | |||||
/> | |||||
<upload-progress | |||||
v-if="uploading" | |||||
:progress="progress" | |||||
:status="uploadStatus" | |||||
:size="size" | |||||
@onSetProgress="onSetProgress" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="策略名称" prop="name"> | |||||
<el-input | |||||
id="name" | |||||
v-model.trim="baseForm.name" | |||||
placeholder="由算法解析自动获取" | |||||
maxlength="50" | |||||
show-word-limit | |||||
disabled | |||||
style="width: 200px;" | |||||
/> | |||||
</el-form-item> | |||||
<el-form-item label="模型类别" prop="model_type"> | |||||
<el-select | |||||
id="modelType" | |||||
v-model="baseForm.model_type" | |||||
placeholder="由算法解析自动获取" | |||||
clearable | |||||
disabled | |||||
> | |||||
<el-option | |||||
v-for="item in MODEL_TYPE_ENUM" | |||||
:key="item.value" | |||||
:label="item.label" | |||||
:value="item.value" | |||||
/> | |||||
</el-select> | |||||
</el-form-item> | |||||
</el-form> | |||||
<!-- 阶段配置表单组件 --> | |||||
<CreatePageForm | |||||
ref="createPageFormRef" | |||||
:base-form="baseForm" | |||||
:zip-path="zipPath" | |||||
:steps="active" | |||||
:type="type" | |||||
@tabs-change="(tab) => (buttonShow = tab === 'page')" | |||||
@yaml-loaded="onYamlLoaded" | |||||
/> | |||||
<!-- 操作按钮 --> | |||||
<div v-if="buttonShow" class="operation"> | |||||
<el-button :disabled="submitting" @click="handleBack">{{ backButtonName }}</el-button> | |||||
<el-button type="primary" :loading="submitting" @click="handleNext">{{ | |||||
nextButtonName | |||||
}}</el-button> | |||||
</div> | |||||
</el-drawer> | |||||
</template> | |||||
<script> | |||||
import { Message, MessageBox } from 'element-ui'; | |||||
import { reactive, toRefs, computed, nextTick, ref } from '@vue/composition-api'; | |||||
import { | |||||
uploadSizeFomatter, | |||||
invalidFileNameChar, | |||||
propertyAssign, | |||||
validateName, | |||||
getUniqueId, | |||||
} from '@/utils'; | |||||
import { algorithmConfig } from '@/config'; | |||||
import { unpackZip, uploadStrategy, updateStrategy, checkStrategy } from '@/api/tadl/strategy'; | |||||
import UploadInline from '@/components/UploadForm/inline'; | |||||
import UploadProgress from '@/components/UploadProgress'; | |||||
import { useMapGetters } from '@/hooks'; | |||||
import CreatePageForm from './CreatePageForm'; | |||||
import { MODEL_TYPE_ENUM } from '../../util'; | |||||
import { underlineShiftHump, humpShiftUnderline, isNull } from '../util'; | |||||
const defaultForm = { | |||||
name: null, // 算法名称 | |||||
model_type: null, // 模型类别 | |||||
algorithm_path: null, // 算法路径 | |||||
}; | |||||
const useUpload = ({ customOnRemove, customUploadSuccess } = {}) => { | |||||
const state = reactive({ | |||||
uploadParams: { objectPath: null }, // 对象存储路径 | |||||
size: 0, // 文件大小 | |||||
progress: 0, // 上传进度 | |||||
uploadFilters: [invalidFileNameChar], // 文件校验 | |||||
uploading: false, | |||||
}); | |||||
const uploadRef = ref(null); | |||||
// 上传状态 | |||||
const uploadStatus = computed(() => { | |||||
state.progress === 100 ? 'success' : null; | |||||
}); | |||||
const { user } = useMapGetters(['user']); | |||||
// 生成随机临时文件夹路径 | |||||
const updateObjectPath = () => { | |||||
state.uploadParams.objectPath = `upload-temp/${user.id}/${getUniqueId()}`; | |||||
}; | |||||
// 移除文件 | |||||
const onRemove = () => { | |||||
state.uploading = false; | |||||
if (typeof customOnRemove === 'function') { | |||||
customOnRemove(); | |||||
} | |||||
}; | |||||
// 开始上传 | |||||
const uploadStart = (files) => { | |||||
updateObjectPath(); | |||||
state.uploading = true; | |||||
state.size = files.size; | |||||
state.progress = 0; | |||||
}; | |||||
// 上传成功 | |||||
const uploadSuccess = (res) => { | |||||
state.progress = 100; | |||||
setTimeout(() => { | |||||
state.uploading = false; | |||||
}, 1000); | |||||
if (typeof customUploadSuccess === 'function') { | |||||
customUploadSuccess(res); | |||||
} | |||||
}; | |||||
// 上传失败 | |||||
const uploadError = () => { | |||||
Message.error('上传文件失败'); | |||||
state.uploading = false; | |||||
}; | |||||
// 进度更新 | |||||
const onSetProgress = (val) => { | |||||
state.progress += val; | |||||
}; | |||||
return { | |||||
...toRefs(state), | |||||
uploadRef, | |||||
uploadStatus, | |||||
updateObjectPath, | |||||
onRemove, | |||||
uploadStart, | |||||
uploadSuccess, | |||||
uploadError, | |||||
onSetProgress, | |||||
}; | |||||
}; | |||||
// 表单类型与中文操作对应关系 | |||||
const TYPE_MAP = { | |||||
create: '上传', | |||||
edit: '编辑', | |||||
check: '查看', | |||||
}; | |||||
export default { | |||||
name: 'StrategyDrawer', | |||||
components: { CreatePageForm, UploadInline, UploadProgress }, | |||||
setup(props, ctx) { | |||||
// 头部表单 | |||||
const baseForm = reactive({ ...defaultForm }); | |||||
const refs = reactive({ | |||||
baseFormRef: null, | |||||
algorithmPathRef: null, | |||||
createPageFormRef: null, | |||||
}); | |||||
const data = reactive({ | |||||
visible: false, | |||||
type: 'create', | |||||
active: 0, | |||||
buttonShow: true, | |||||
form: {}, // 用于存储表单数据 | |||||
submitting: false, | |||||
zipPath: null, // 存储上传的算法路径,创建算法时需要由此路径获取 yaml 信息 | |||||
}); | |||||
const title = computed(() => { | |||||
const action = TYPE_MAP[data.type] || ''; | |||||
const strategyName = data.form.name ? ` - ${data.form.name}` : ''; | |||||
return `${action}搜索策略${strategyName}`; | |||||
}); | |||||
const rules = { | |||||
name: [ | |||||
{ required: true, message: '请输入策略名称', trigger: ['change', 'blur'] }, | |||||
{ validator: validateName, trigger: ['change', 'blur'] }, | |||||
], | |||||
model_type: [{ required: true, message: '请选择模型类别', trigger: 'change' }], | |||||
algorithm_path: [{ required: true, message: '请上传算法文件', trigger: ['blur', 'manual'] }], | |||||
}; | |||||
// 外部显示 | |||||
const handleShow = async (type, { algorithmVersionId, id }) => { | |||||
data.type = type; | |||||
data.visible = true; | |||||
if (type !== 'create') { | |||||
const params = await checkStrategy({ algorithmVersionId }, id); | |||||
data.form = humpShiftUnderline(params); | |||||
if (data.active === 0) { | |||||
propertyAssign(baseForm, data.form, (val) => !isNull(val)); | |||||
} | |||||
nextTick(() => { | |||||
refs.createPageFormRef.initForm(data.form); | |||||
}); | |||||
} | |||||
}; | |||||
const resetBaseForm = () => { | |||||
Object.assign(baseForm, defaultForm); | |||||
nextTick(() => { | |||||
refs.baseFormRef && refs.baseFormRef.clearValidate(); | |||||
}); | |||||
}; | |||||
const initState = async () => { | |||||
refs.createPageFormRef.resetForm(); | |||||
data.active = 0; | |||||
data.form = {}; | |||||
data.visible = false; | |||||
}; | |||||
// 上传算法 | |||||
// 文件移除处理 | |||||
const onFileRemove = () => { | |||||
baseForm.algorithm_path = null; | |||||
refs.algorithmPathRef.validate('manual'); | |||||
}; | |||||
// 上传成功处理 | |||||
const customUploadSuccess = (res) => { | |||||
const algorithmPath = res[0].data.objectName; | |||||
baseForm.algorithm_path = algorithmPath; | |||||
data.zipPath = algorithmPath; | |||||
refs.algorithmPathRef.validate('manual'); | |||||
// 文件上传成功后反馈给后端 | |||||
unpackZip({ zipPath: algorithmPath }).then(() => { | |||||
nextTick(() => { | |||||
// 文件解析成功后将基本配置数据传入 | |||||
refs.createPageFormRef.getYaml(); | |||||
// 上传后清空 form,所有数据由解析算法后获取 | |||||
data.form = {}; | |||||
}); | |||||
}); | |||||
}; | |||||
const { | |||||
uploadRef, | |||||
uploadParams, | |||||
size, | |||||
progress, | |||||
uploadFilters, | |||||
uploadStatus, | |||||
uploading, | |||||
updateObjectPath, | |||||
onRemove, | |||||
uploadStart, | |||||
onSetProgress, | |||||
uploadSuccess, | |||||
uploadError, | |||||
} = useUpload({ customOnRemove: onFileRemove, customUploadSuccess }); | |||||
const handleClose = () => { | |||||
MessageBox.confirm('关闭弹窗数据会消失,是否继续', '提示', { | |||||
confirmButtonText: '确定', | |||||
cancelButtonText: '取消', | |||||
type: 'warning', | |||||
}).then(() => { | |||||
if (!data.active && data.type === 'create') { | |||||
uploadRef.value.formRef.reset(); | |||||
data.loading = false; | |||||
resetBaseForm(); | |||||
} | |||||
initState(); | |||||
}); | |||||
}; | |||||
// buttons | |||||
// 上一步按钮名 | |||||
const backButtonName = computed(() => { | |||||
return data.active ? '上一步' : '取消'; | |||||
}); | |||||
// 下一步按钮名 | |||||
const nextButtonName = computed(() => { | |||||
if (data.active !== 3) { | |||||
return '下一步'; | |||||
} | |||||
return data.type === 'check' ? '关闭' : '确定'; | |||||
}); | |||||
// 上一步 | |||||
const handleBack = () => { | |||||
if (!data.active) { | |||||
handleClose(); | |||||
} else { | |||||
const [params, yaml] = refs.createPageFormRef.getFormValue(); | |||||
params.stage_order = data.active; | |||||
if (data.form.stage[params.stage_order - 1]) { | |||||
Object.assign(data.form.stage[params.stage_order - 1], { ...params, yaml }); | |||||
} else { | |||||
data.form.stage[params.stage_order - 1] = { ...params, yaml }; | |||||
} | |||||
data.active -= 1; | |||||
if (data.active === 0) { | |||||
propertyAssign(baseForm, data.form, (val) => !isNull(val)); | |||||
} | |||||
nextTick(() => { | |||||
refs.createPageFormRef.initForm(data.form); | |||||
}); | |||||
} | |||||
}; | |||||
// 策略基础表单校验 | |||||
const submitBaseForm = () => { | |||||
let baseValid = true; | |||||
let configValid = true; | |||||
// 基本信息表单校验 | |||||
refs.baseFormRef.validate((valid) => { | |||||
baseValid = valid && baseValid; | |||||
if (valid) { | |||||
Object.assign(data.form, baseForm); | |||||
} | |||||
}); | |||||
// 基本配置表单校验 | |||||
refs.createPageFormRef.validateForm( | |||||
(form, yaml) => { | |||||
Object.assign(data.form, form, { yaml }); | |||||
if (data.form.stage === undefined) data.form.stage = []; | |||||
}, | |||||
() => { | |||||
configValid = false; | |||||
} | |||||
); | |||||
if (baseValid && configValid) { | |||||
resetBaseForm(); | |||||
refs.createPageFormRef.resetForm(); | |||||
nextTick(() => { | |||||
data.active += 1; | |||||
nextTick(() => { | |||||
refs.createPageFormRef.initForm(data.form); | |||||
}); | |||||
}); | |||||
} | |||||
}; | |||||
// 阶段表单校验 | |||||
const submitStageForm = () => { | |||||
refs.createPageFormRef.validateForm((form, yaml) => { | |||||
form.stage_order = data.active; | |||||
if (data.form.stage[form.stage_order - 1]) { | |||||
Object.assign(data.form.stage[form.stage_order - 1], { ...form, yaml }); | |||||
} else { | |||||
data.form.stage[form.stage_order - 1] = { ...form, yaml }; | |||||
} | |||||
if (data.active !== 3) { | |||||
refs.createPageFormRef.resetForm(); | |||||
data.active += 1; | |||||
nextTick(() => { | |||||
refs.createPageFormRef.initForm(data.form); | |||||
}); | |||||
} else { | |||||
if (data.type === 'check') { | |||||
initState(); | |||||
return; | |||||
} | |||||
if (data.submitting) return; | |||||
data.submitting = true; | |||||
const apiFunction = data.type === 'create' ? uploadStrategy : updateStrategy; | |||||
data.form.zipPath = data.zipPath; | |||||
apiFunction(underlineShiftHump(data.form)) | |||||
.then(() => { | |||||
initState(); | |||||
ctx.emit('submit-success'); | |||||
Message.success(`${TYPE_MAP[data.type]}成功`); | |||||
}) | |||||
.finally(() => { | |||||
data.submitting = false; | |||||
}); | |||||
} | |||||
}); | |||||
}; | |||||
// 下一步 | |||||
const handleNext = async () => { | |||||
if (data.active === 0) { | |||||
submitBaseForm(); | |||||
} else { | |||||
submitStageForm(); | |||||
} | |||||
}; | |||||
const onYamlLoaded = ({ modelType, name }) => { | |||||
baseForm.model_type = MODEL_TYPE_ENUM[modelType].value; | |||||
baseForm.name = name; | |||||
}; | |||||
updateObjectPath(); | |||||
return { | |||||
...toRefs(data), | |||||
...toRefs(refs), | |||||
title, | |||||
baseForm, | |||||
rules, | |||||
handleClose, | |||||
handleShow, | |||||
backButtonName, | |||||
nextButtonName, | |||||
handleNext, | |||||
handleBack, | |||||
// upload | |||||
uploadRef, | |||||
uploadParams, | |||||
size, | |||||
progress, | |||||
uploadFilters, | |||||
uploadStatus, | |||||
uploading, | |||||
onFileRemove: onRemove, | |||||
uploadStart, | |||||
onSetProgress, | |||||
uploadSuccess, | |||||
uploadError, | |||||
onYamlLoaded, | |||||
// 外部引入 | |||||
algorithmConfig, | |||||
uploadSizeFomatter, | |||||
MODEL_TYPE_ENUM, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
.form { | |||||
margin: 30px 0 0 20px; | |||||
} | |||||
.operation { | |||||
margin-bottom: 20px; | |||||
text-align: center; | |||||
} | |||||
</style> |
@@ -0,0 +1,329 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. All Rights Reserved. * * Licensed under the Apache License, | |||||
Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * | |||||
You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless | |||||
required by applicable law or agreed to in writing, software * distributed under the License is | |||||
distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | |||||
implied. * See the License for the specific language governing permissions and * limitations under | |||||
the License. * ============================================================= */ | |||||
<template> | |||||
<div class="app-container"> | |||||
<el-row v-loading="loading" class="card-row"> | |||||
<!-- 非管理员不能上传和编辑 --> | |||||
<el-col v-if="isAdmin" :xs="12" :sm="12" :lg="6" :xl="4" class="card-col"> | |||||
<el-card shadow="always"> | |||||
<div class="upload flex flex-center" @click="onDrawerShow('create')"> | |||||
<span> | |||||
<i class="el-icon-plus"></i> | |||||
上传搜索策略 | |||||
</span> | |||||
</div> | |||||
</el-card> | |||||
</el-col> | |||||
<el-col | |||||
v-for="item in algorithmList" | |||||
:key="item.id" | |||||
:xs="12" | |||||
:sm="12" | |||||
:lg="6" | |||||
:xl="4" | |||||
class="card-col" | |||||
> | |||||
<el-card shadow="hover"> | |||||
<!-- 卡片头部 --> | |||||
<div class="flex flex-between card-title"> | |||||
<label>{{ item.name }}</label> | |||||
<el-dropdown v-if="item.algorithmVersionVOList.length > 1" @command="onVersionChange"> | |||||
<span class="el-dropdown-link"> | |||||
{{ item.selectedVersionName }}<i class="el-icon-arrow-down el-icon--right"></i> | |||||
</span> | |||||
<el-dropdown-menu slot="dropdown"> | |||||
<el-dropdown-item v-for="v in item.algorithmVersionVOList" :key="v.id" :command="v"> | |||||
{{ v.versionName || '最新' }} | |||||
<el-button | |||||
v-if="v.versionName" | |||||
class="dropdown-del-btn" | |||||
type="text" | |||||
@click.stop="doDelete(item, v)" | |||||
><i class="el-icon-close" | |||||
/></el-button> | |||||
</el-dropdown-item> | |||||
</el-dropdown-menu> | |||||
</el-dropdown> | |||||
</div> | |||||
<!-- 标签 --> | |||||
<div class="tag"> | |||||
<i class="el-icon-price-tag tag-icon" /> | |||||
<el-tag class="ml-10">{{ item.algType }}</el-tag> | |||||
</div> | |||||
<!-- 文本内容 --> | |||||
<p class="introduce multiple-lines"> | |||||
{{ item.description }} | |||||
</p> | |||||
<el-divider /> | |||||
<!-- 卡片操作按钮 --> | |||||
<div class="operation-wrapper flex flex-around"> | |||||
<!-- 非管理员不能上传和编辑 --> | |||||
<template v-if="isAdmin"> | |||||
<el-tooltip effect="dark" :content="getEditContent(item)" placement="bottom"> | |||||
<i | |||||
class="cp iconfont icon-bianji" | |||||
:class="{ 'i-disabled': item.isReleased }" | |||||
@click.stop="onDrawerShow('edit', item)" | |||||
/> | |||||
</el-tooltip> | |||||
<el-divider direction="vertical" /> | |||||
</template> | |||||
<el-tooltip effect="dark" content="查看搜索策略" placement="bottom"> | |||||
<i class="cp iconfont icon-chaxun" @click.stop="onDrawerShow('check', item)" /> | |||||
</el-tooltip> | |||||
<el-divider direction="vertical" /> | |||||
<el-tooltip effect="dark" :content="getCreateContent(item)" placement="bottom"> | |||||
<i | |||||
class="cp iconfont icon-shiyanguanli" | |||||
:class="{ 'i-disabled': !item.isReleased }" | |||||
@click.stop="doCreate(item)" | |||||
/> | |||||
</el-tooltip> | |||||
<!-- 非管理员不能发布和删除 --> | |||||
<template v-if="isAdmin"> | |||||
<el-divider direction="vertical" /> | |||||
<el-tooltip effect="dark" :content="getReleaseContent(item)" placement="bottom"> | |||||
<i | |||||
class="cp iconfont icon-fabu" | |||||
:class="{ 'i-disabled': item.isReleased }" | |||||
@click.stop="doRelease(item)" | |||||
/> | |||||
</el-tooltip> | |||||
<el-divider direction="vertical" /> | |||||
<el-tooltip effect="dark" content="删除算法" placement="bottom"> | |||||
<i class="cp iconfont icon-shanchu" @click.stop="doDelete(item)" /> | |||||
</el-tooltip> | |||||
</template> | |||||
</div> | |||||
</el-card> | |||||
</el-col> | |||||
</el-row> | |||||
<!-- 上传/编辑/查看抽屉 --> | |||||
<StrategyDrawer ref="strategyDrawer" @submit-success="submitSuccess" /> | |||||
<!-- 发布搜索策略弹窗 --> | |||||
<ReleaseDialog ref="releaseDialog" @release-success="releaseSuccess" /> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { reactive, toRefs, onMounted } from '@vue/composition-api'; | |||||
import { Message, MessageBox } from 'element-ui'; | |||||
import { getStrategyList, getNextVersion, shiftVersion, deleteVersion } from '@/api/tadl/strategy'; | |||||
import { useMapGetters } from '@/hooks'; | |||||
import StrategyDrawer from './components/StrategyDrawer'; | |||||
import ReleaseDialog from './components/ReleaseDialog'; | |||||
const useGetAlgorithms = () => { | |||||
const state = reactive({ | |||||
algorithmList: [], // 接口获取的算法列表 | |||||
loading: false, | |||||
}); | |||||
// 列表刷新及搜索 | |||||
const refreshList = async (content) => { | |||||
state.loading = true; | |||||
state.algorithmList = await getStrategyList({ content }).finally(() => { | |||||
state.loading = false; | |||||
}); | |||||
// 为算法添加当前版本信息 | |||||
state.algorithmList.forEach((algorithm) => { | |||||
const selectedVersion = algorithm.algorithmVersionVOList.find( | |||||
(v) => v.id === algorithm.algorithmVersionId | |||||
); | |||||
if (selectedVersion) { | |||||
algorithm.selectedVersionName = selectedVersion.versionName || '最新'; | |||||
algorithm.isReleased = selectedVersion.versionName !== null; | |||||
} else { | |||||
algorithm.selectedVersionName = '选择版本'; | |||||
algorithm.isReleased = false; | |||||
} | |||||
}); | |||||
}; | |||||
return { | |||||
...toRefs(state), | |||||
refreshList, | |||||
}; | |||||
}; | |||||
export default { | |||||
name: 'Strategy', | |||||
components: { StrategyDrawer, ReleaseDialog }, | |||||
setup(props, ctx) { | |||||
const refs = reactive({ | |||||
strategyDrawer: null, | |||||
releaseDialog: null, | |||||
}); | |||||
const { isAdmin } = useMapGetters(['isAdmin']); | |||||
const { algorithmList, loading, refreshList } = useGetAlgorithms(); | |||||
const onDrawerShow = async (type, item = {}) => { | |||||
if (!(type === 'edit' && item.isReleased)) { | |||||
refs.strategyDrawer.handleShow(type, item); | |||||
} | |||||
}; | |||||
const doRelease = async (info) => { | |||||
if (info.isReleased) return; | |||||
const version = info.algorithmVersionVOList.find((v) => v.id === info.algorithmVersionId); | |||||
if (version) { | |||||
const releaseObj = await getNextVersion(version.algorithmId); | |||||
refs.releaseDialog.handleShow(releaseObj); | |||||
} | |||||
}; | |||||
const onVersionChange = ({ algorithmId, id }) => { | |||||
shiftVersion({ algorithmId, algorithmVersionId: id }).then(() => refreshList()); | |||||
}; | |||||
const submitSuccess = () => { | |||||
refreshList(); | |||||
}; | |||||
const releaseSuccess = () => { | |||||
refreshList(); | |||||
}; | |||||
const doCreate = ({ id, algorithmVersionId, isReleased }) => { | |||||
if (!isReleased) return; | |||||
ctx.root.$router.push({ | |||||
name: 'TadlForm', | |||||
params: { | |||||
formType: 'strategy', | |||||
formParams: { | |||||
algorithmId: id, | |||||
algorithmVersionId, | |||||
}, | |||||
}, | |||||
}); | |||||
}; | |||||
const doDelete = ({ name, id }, version) => { | |||||
MessageBox.confirm( | |||||
version | |||||
? `确认删除算法${name}的${version.versionName}版本?` | |||||
: `删除算法${name}会同时删除其所有版本,是否确认?`, | |||||
'确认' | |||||
).then(async () => { | |||||
await deleteVersion({ | |||||
algorithmId: id, | |||||
algorithmVersionId: version ? version.id : undefined, | |||||
}); | |||||
Message.success('算法删除成功!'); | |||||
refreshList(); | |||||
}); | |||||
}; | |||||
const getEditContent = ({ isReleased }) => | |||||
isReleased ? '编辑只可用于最新版本' : '编辑搜索策略'; | |||||
const getReleaseContent = ({ isReleased }) => (isReleased ? '发布只可用于最新版本' : '发布'); | |||||
const getCreateContent = ({ isReleased }) => | |||||
isReleased ? '创建实验' : '只有已发布版本才能创建实验'; | |||||
onMounted(() => { | |||||
refreshList(); | |||||
}); | |||||
return { | |||||
...toRefs(refs), | |||||
isAdmin, | |||||
algorithmList, | |||||
loading, | |||||
submitSuccess, | |||||
releaseSuccess, | |||||
onDrawerShow, | |||||
doRelease, | |||||
onVersionChange, | |||||
doCreate, | |||||
doDelete, | |||||
getEditContent, | |||||
getReleaseContent, | |||||
getCreateContent, | |||||
}; | |||||
}, | |||||
}; | |||||
</script> | |||||
<style lang="scss" scoped> | |||||
@import '@/assets/styles/variables.scss'; | |||||
.i-disabled { | |||||
color: #909399; | |||||
cursor: not-allowed; | |||||
} | |||||
.divider { | |||||
height: 12px; | |||||
margin-bottom: 20px; | |||||
background: #f5f7fa; | |||||
} | |||||
.card-col { | |||||
padding: 0 10px; | |||||
margin: 10px 0; | |||||
.upload { | |||||
height: 175px; | |||||
color: #88898a; | |||||
cursor: pointer; | |||||
} | |||||
.card-title { | |||||
height: 20px; | |||||
margin-bottom: 10px; | |||||
} | |||||
.tag { | |||||
height: 23px; | |||||
margin: 10px 0; | |||||
.tag-icon { | |||||
font-size: 18px; | |||||
color: #000; | |||||
transform: rotate(-30deg); | |||||
} | |||||
} | |||||
.introduce { | |||||
height: 65px; | |||||
font-size: 14px; | |||||
color: rgba(146, 146, 146, 100); | |||||
-webkit-line-clamp: 4; | |||||
} | |||||
.el-divider--horizontal { | |||||
margin: 10px 0; | |||||
} | |||||
.el-dropdown-link { | |||||
color: #2e4fde; | |||||
cursor: pointer; | |||||
} | |||||
.el-icon-arrow-down { | |||||
font-size: 12px; | |||||
} | |||||
} | |||||
.dropdown-del-btn { | |||||
float: right; | |||||
margin: 0 -10px 0 10px; | |||||
line-height: 18px; | |||||
color: $infoColor; | |||||
:hover { | |||||
color: $primaryHoverColor; | |||||
} | |||||
} | |||||
</style> |
@@ -0,0 +1,75 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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. | |||||
* ============================================================= | |||||
*/ | |||||
// 分割时间数值和单位名称 | |||||
const TIME_UNIT_RE = /^([\d.]+)([a-z]+)$/; | |||||
export function modifyTime(time) { | |||||
return time.match(TIME_UNIT_RE).slice(1, 3); | |||||
} | |||||
// 判断数据类型 | |||||
export function typeOf(type) { | |||||
return Object.prototype.toString.call(type).slice(8, -1); | |||||
} | |||||
// 对象属性名下划线转驼峰 | |||||
export function underlineShiftHump(obj) { | |||||
const newObj = {}; | |||||
Object.keys(obj).forEach((key) => { | |||||
const keyArr = key.split('_'); | |||||
let newKey = keyArr[0]; | |||||
keyArr.forEach((item, index) => { | |||||
if (index) newKey += item.slice(0, 1).toUpperCase() + item.slice(1); | |||||
}); | |||||
newObj[newKey] = obj[key]; | |||||
if (typeOf(obj[key]) === 'Array') { | |||||
obj[key].forEach((item, index) => { | |||||
if (typeOf(item) === 'Object') newObj[newKey][index] = underlineShiftHump(item); | |||||
}); | |||||
} | |||||
if (typeOf(obj[key]) === 'Object') { | |||||
newObj[newKey] = underlineShiftHump(obj[key]); | |||||
} | |||||
}); | |||||
return newObj; | |||||
} | |||||
// 对象属性名驼峰转下划线 | |||||
export function humpShiftUnderline(obj) { | |||||
const newObj = {}; | |||||
Object.keys(obj).forEach((key) => { | |||||
const newKey = key.replace(/([A-Z])/g, '_$1').toLowerCase(); | |||||
newObj[newKey] = obj[key]; | |||||
if (typeOf(obj[key]) === 'Array') { | |||||
obj[key].forEach((item, index) => { | |||||
if (typeOf(item) === 'Object') newObj[newKey][index] = humpShiftUnderline(item); | |||||
}); | |||||
} | |||||
if (typeOf(obj[key]) === 'Object') { | |||||
newObj[newKey] = humpShiftUnderline(obj[key]); | |||||
} | |||||
}); | |||||
return newObj; | |||||
} | |||||
// 判断值是否为空或空字符 | |||||
export const isNull = (value) => { | |||||
return ( | |||||
value === null || | |||||
value === undefined || | |||||
(typeof value === 'string' && value.trim().length === 0) | |||||
); | |||||
}; |
@@ -0,0 +1,184 @@ | |||||
/** Copyright 2020 Tianshu AI Platform. 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 { isNil, invert } from 'lodash'; | |||||
import { ONE_DAY, ONE_HOUR, ONE_MINUTE, getValueFromMap } from '@/utils'; | |||||
import { expStageAccuracy, expStageIntermediate } from '@/api/tadl'; | |||||
// 实验状态枚举值 | |||||
export const EXPERIMENT_STATUS_MAP = { | |||||
TO_RUN: { value: 101, label: '待运行', bgColor: '#BFBFBF' }, | |||||
WAITING: { value: 102, label: '等待中', bgColor: '#409EFF' }, | |||||
RUNNING: { value: 103, label: '运行中', bgColor: '#1890FF' }, | |||||
PAUSED: { value: 104, label: '已暂停', bgColor: '#409EFF' }, | |||||
FINISHED: { value: 202, label: '已完成', bgColor: '#52C41A' }, | |||||
FAILED: { value: 203, label: '运行失败', bgColor: '#F5222D' }, | |||||
}; | |||||
// 成功的实验 | |||||
export const expIsFinished = (code) => [202].includes(code); | |||||
// 进行中实验 | |||||
export const expInprogress = (code) => [101, 102, 103, 104, 105].includes(code); | |||||
// 可暂停实验 | |||||
export const expEnablePause = (code) => [101, 102, 103].includes(code); | |||||
// 可停止实验 | |||||
export const expEnableStop = (code) => [102, 103, 104].includes(code); | |||||
// 可启动实验 | |||||
export const expEnableStart = (code) => [101, 104, 203].includes(code); | |||||
// Trial 状态枚举值 | |||||
export const TRIAL_STATUS_MAP = { | |||||
toRun: { value: 101, label: '待运行', bgColor: '#BFBFBF' }, | |||||
waiting: { value: 102, label: '等待中', bgColor: '#409EFF' }, | |||||
running: { value: 103, label: '运行中', bgColor: '#1890FF' }, | |||||
finished: { value: 201, label: '已完成', bgColor: '#52C41A' }, | |||||
failed: { value: 202, label: '运行失败', bgColor: '#F5222D' }, | |||||
// unknown: { value: 203, label: '未知', bgColor: '#409EFF' }, | |||||
}; | |||||
// 阶段状态枚举值 | |||||
export const STAGE_STATUS_MAP = { | |||||
TO_RUN: { value: 101, label: '待运行', bgColor: '#BFBFBF' }, | |||||
RUNNING: { value: 102, label: '运行中', bgColor: '#1890FF' }, | |||||
FINISHED: { value: 201, label: '已完成', bgColor: '#52C41A' }, | |||||
FAILED: { value: 202, label: '运行失败', bgColor: '#F5222D' }, | |||||
}; | |||||
// 阶段顺序 | |||||
export const STAGE_SEQUENCE = { | |||||
TRAIN: 1, | |||||
SELECT: 2, | |||||
RETRAIN: 3, | |||||
}; | |||||
// 模型类型枚举值 | |||||
export const MODEL_TYPE_ENUM = { | |||||
ImageClassify: { value: 101, label: '图像分类' }, // 图像分类 | |||||
TextClassify: { value: 301, label: '文本分类' }, // 文本分类 | |||||
}; | |||||
// 提供获取模型字段基类方法 | |||||
export const getExpByCode = (value, key) => getValueFromMap(EXPERIMENT_STATUS_MAP, value, key); | |||||
// 提供获取模型字段基类方法 | |||||
export const getModelByCode = (value, key) => getValueFromMap(MODEL_TYPE_ENUM, value, key); | |||||
// 提供获取 Trial 基类方法 | |||||
export const getTrialByCode = (value, key) => getValueFromMap(TRIAL_STATUS_MAP, value, key); | |||||
// 根据阶段 order 获取名称 | |||||
export const getStageName = (stageOrder) => invert(STAGE_SEQUENCE)[stageOrder]; | |||||
export const getStageOrder = (stageName) => STAGE_SEQUENCE[stageName]; | |||||
// 刷新频率 | |||||
export const refreshControls = [ | |||||
{ icon: 'el-icon-remove-outline', label: '关闭自动刷新', value: 0 }, | |||||
{ icon: 'el-icon-timer', label: '每 10s 刷新', value: 10 }, | |||||
{ icon: 'el-icon-timer', label: '每 20s 刷新', value: 20 }, | |||||
{ icon: 'el-icon-timer', label: '每 30s 刷新', value: 30 }, | |||||
{ icon: 'el-icon-timer', label: '每 60s 刷新', value: 60 }, | |||||
]; | |||||
// 时间格式 | |||||
export const timeFmts = [ | |||||
{ label: 'day', value: 'day' }, | |||||
{ label: 'hour', value: 'hour' }, | |||||
{ label: 'min', value: 'min' }, | |||||
]; | |||||
/** | |||||
* 运行时间格式化 | |||||
* @param {Number} ms 运行毫秒数 | |||||
* @returns {String} 返回格式化的时间 | |||||
*/ | |||||
export const runTimeFormatter = (ms) => { | |||||
let day; | |||||
let hour; | |||||
let minute; | |||||
if (ms > ONE_DAY) { | |||||
day = Math.floor(ms / ONE_DAY); | |||||
ms %= ONE_DAY; | |||||
} | |||||
if (ms > ONE_HOUR) { | |||||
hour = Math.floor(ms / ONE_HOUR); | |||||
ms %= ONE_HOUR; | |||||
} | |||||
if (ms > ONE_MINUTE) { | |||||
minute = Math.floor(ms / ONE_MINUTE); | |||||
} | |||||
const dayStr = isNil(day) ? '' : `${day}day `; | |||||
const hourStr = isNil(hour) && !dayStr ? '' : `${hour || 0}hour `; | |||||
const minStr = isNil(minute) && !hourStr ? '' : `${minute || 0}min`; | |||||
return `${dayStr}${hourStr}${minStr}`; | |||||
}; | |||||
// 根据时间单位解析时间 | |||||
export const parseRunTime = (time, unit) => { | |||||
const unitMap = { | |||||
day: ONE_DAY, | |||||
hour: ONE_HOUR, | |||||
min: ONE_MINUTE, | |||||
}; | |||||
return time * unitMap[unit] || 0; | |||||
}; | |||||
// 提取图数据的data | |||||
export const extractData = (raw) => { | |||||
const { xField, yField } = raw.config; | |||||
const data = raw.data.map((point) => { | |||||
return { | |||||
[xField]: String(point[xField]), | |||||
[yField]: point[yField], | |||||
}; | |||||
}); | |||||
return data.flat(); // 将二维数组压平成一维数组 | |||||
}; | |||||
export const extractScatterData = (raw) => { | |||||
const { xField, yField } = raw.config; | |||||
const data = raw.data.map((point) => { | |||||
return { | |||||
[xField]: point[xField], | |||||
[yField]: point[yField], | |||||
}; | |||||
}); | |||||
return data.flat(); // 将二维数组压平成一维数组 | |||||
}; | |||||
export const extractSeriesData = (raw) => { | |||||
const { xField, yField, seriesField } = raw.config; | |||||
const data = raw.data.map((d) => { | |||||
return d.list.map((point) => { | |||||
return { | |||||
[xField]: String(point[xField]), | |||||
[yField]: point[yField], | |||||
[seriesField]: d[seriesField], | |||||
}; | |||||
}); | |||||
}); | |||||
return data.flat(); // 将二维数组压平成一维数组 | |||||
}; | |||||
const metricMap = { | |||||
accuracy: expStageAccuracy, | |||||
intermediate: expStageIntermediate, | |||||
}; | |||||
// 根据指定metric获取数据 | |||||
export const fetchMetric = async (experimentId, stageOrder, metricStr) => { | |||||
const metric = await metricMap[metricStr](experimentId, stageOrder); | |||||
return metric; | |||||
}; |
@@ -1,240 +1,82 @@ | |||||
/* | |||||
* Copyright 2019-2020 Zheng Jie | |||||
* | |||||
* 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. | |||||
*/ | |||||
/* * Copyright 2019-2020 Zheng Jie * * Licensed under the Apache License, Version 2.0 (the | |||||
"License"); * you may not use this file except in compliance with the License. * You may obtain a | |||||
copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by | |||||
applicable law or agreed to in writing, software * distributed under the License is distributed on | |||||
an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See | |||||
the License for the specific language governing permissions and * limitations under the License. */ | |||||
<template> | <template> | ||||
<div class="app-container"> | <div class="app-container"> | ||||
<el-card class="box-card"> | <el-card class="box-card"> | ||||
<!-- 个人信息 --> | |||||
<img :src="user.avatar" title="点击上传头像" class="avatar" @click="openUploadDialog" /> | |||||
<div class="info-row"> | |||||
<div class="info-label">登录账号</div> | |||||
<div class="info-text">{{ user.username }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户昵称</div> | |||||
<div class="info-text">{{ user.nickName }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户性别</div> | |||||
<div class="info-text">{{ user.sex }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">手机号码</div> | |||||
<div class="info-text">{{ user.phone }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户邮箱</div> | |||||
<div class="info-text">{{ user.email }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户角色</div> | |||||
<div class="info-text">{{ userRoles }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户设置</div> | |||||
<div class="info-text"> | |||||
<el-button type="text" @click="infoDialog = true">修改信息</el-button> | |||||
<el-button type="text" @click="$refs.pass.dialog = true">修改密码</el-button> | |||||
<el-button type="text" @click="$refs.email.dialog = true">修改邮箱</el-button> | |||||
</div> | |||||
</div> | |||||
<el-tabs tab-position="left" style="height: 400px;"> | |||||
<el-tab-pane> | |||||
<span slot="label"><i class="el-icon-user"></i> 基本设置</span> | |||||
<user-info></user-info> | |||||
</el-tab-pane> | |||||
<el-tab-pane> | |||||
<span slot="label"><i class="el-icon-setting"></i> 开发者信息</span> | |||||
<div style="margin-left: 30px;"> | |||||
<h4 class="my-10">Token</h4> | |||||
<span>当前用户的唯一登录信息,你可以在命令行里面使用,完成用户鉴权</span> | |||||
<pre class="code flex flex-vertical-align flex-between"> | |||||
<code class="text ellipsis">{{getToken()}}</code> | |||||
<copy-to-clipboard :text="getToken()" @copy="handleCopy"> | |||||
<i class="el-icon-copy-document pointer copy" /> | |||||
</copy-to-clipboard> | |||||
</pre> | |||||
</div> | |||||
</el-tab-pane> | |||||
</el-tabs> | |||||
</el-card> | </el-card> | ||||
<UploadForm | |||||
action="fakeApi" | |||||
title="上传头像" | |||||
:visible="uploadDialogVisible" | |||||
:toggleVisible="handleClose" | |||||
:params="uploadParams" | |||||
:multiple="false" | |||||
:limit="1" | |||||
:showFileCount="false" | |||||
:filters="uploadFilters" | |||||
@uploadSuccess="uploadSuccess" | |||||
@uploadError="uploadError" | |||||
/> | |||||
<BaseModal | |||||
:visible.sync="infoDialog" | |||||
title="修改信息" | |||||
width="450px" | |||||
@cancel="infoDialog = false" | |||||
@open="onDialogOpen" | |||||
@ok="doSubmit" | |||||
> | |||||
<el-form ref="form" :model="form" :rules="rules" style="margin-top: 10px;" label-width="65px"> | |||||
<el-form-item label="昵称" prop="nickName"> | |||||
<el-input v-model="form.nickName" style="width: 40%;" /> | |||||
<span style="margin-left: 10px; color: #c0c0c0;">用户昵称不作为登录使用</span> | |||||
</el-form-item> | |||||
<el-form-item label="手机号" prop="phone"> | |||||
<el-input v-model="form.phone" style="width: 40%;" /> | |||||
<span style="margin-left: 10px; color: #c0c0c0;">一个手机号只能注册一个用户</span> | |||||
</el-form-item> | |||||
<el-form-item label="性别"> | |||||
<el-radio-group v-model="form.sex" style="width: 178px;"> | |||||
<el-radio label="男">男</el-radio> | |||||
<el-radio label="女">女</el-radio> | |||||
</el-radio-group> | |||||
</el-form-item> | |||||
</el-form> | |||||
</BaseModal> | |||||
<updateEmail ref="email" :email="user.email" /> | |||||
<updatePass ref="pass" /> | |||||
</div> | </div> | ||||
</template> | </template> | ||||
<script> | <script> | ||||
import { mapGetters } from 'vuex'; | |||||
import store from '@/store'; | |||||
import { bucketName, bucketHost } from '@/utils/minIO'; | |||||
import { validateName } from '@/utils/validate'; | |||||
import { invalidFileNameChar } from '@/utils'; | |||||
import { updateAvatar } from '@/api/user'; | |||||
import BaseModal from '@/components/BaseModal'; | |||||
import UploadForm from '@/components/UploadForm'; | |||||
import updateEmail from './components/updateEmail'; | |||||
import updatePass from './components/updatePass'; | |||||
import { Message } from 'element-ui'; | |||||
import CopyToClipboard from 'vue-copy-to-clipboard'; | |||||
import { getToken } from '@/utils/auth'; | |||||
import UserInfo from './userInfo.vue'; | |||||
export default { | export default { | ||||
name: 'Center', | name: 'Center', | ||||
components: { BaseModal, UploadForm, updatePass, updateEmail }, | |||||
data() { | |||||
components: { UserInfo, CopyToClipboard }, | |||||
setup() { | |||||
const handleCopy = () => { | |||||
Message.success('复制成功'); | |||||
}; | |||||
return { | return { | ||||
saveLoading: false, | |||||
infoDialog: false, | |||||
uploadDialogVisible: false, | |||||
form: { | |||||
id: '', | |||||
nickName: '', | |||||
sex: '', | |||||
phone: '', | |||||
}, | |||||
rules: { | |||||
nickName: [ | |||||
{ required: true, message: '请输入用户昵称', trigger: 'blur' }, | |||||
{ validator: validateName, trigger: 'blur' }, | |||||
], | |||||
phone: [ | |||||
{ required: true, message: '请输入手机号码', trigger: 'blur' }, | |||||
{ | |||||
pattern: /^1\d{10}$/, | |||||
message: '请输入正确的11位手机号码', | |||||
trigger: ['blur', 'change'], | |||||
}, | |||||
], | |||||
}, | |||||
uploadFilters: [invalidFileNameChar], | |||||
handleCopy, | |||||
getToken, | |||||
}; | }; | ||||
}, | }, | ||||
computed: { | |||||
...mapGetters(['user']), | |||||
userRoles() { | |||||
const roles = this.user.roles || []; | |||||
const names = roles.map((role) => role.name); | |||||
return names.join(' ') || '-'; | |||||
}, | |||||
uploadParams() { | |||||
return { | |||||
objectPath: `avatar/${this.user.id}`, // 对象存储路径 | |||||
}; | |||||
}, | |||||
}, | |||||
created() { | |||||
store.dispatch('GetUserInfo').then(() => {}); | |||||
}, | |||||
methods: { | |||||
uploadSuccess(res) { | |||||
if (!res.length) return; | |||||
const filePath = res[0].data.objectName; | |||||
updateAvatar({ | |||||
path: `${bucketName}/${filePath}`, | |||||
realName: `${bucketHost}/${bucketName}/${filePath}`, | |||||
}).then(() => { | |||||
this.$notify({ | |||||
title: '头像修改成功', | |||||
type: 'success', | |||||
duration: 2500, | |||||
}); | |||||
store.dispatch('GetUserInfo').then(() => {}); | |||||
}); | |||||
}, | |||||
uploadError() { | |||||
this.$message({ | |||||
message: '头像修改失败', | |||||
type: 'error', | |||||
}); | |||||
}, | |||||
openUploadDialog() { | |||||
this.uploadDialogVisible = true; | |||||
}, | |||||
handleClose() { | |||||
this.uploadDialogVisible = false; | |||||
}, | |||||
onDialogOpen() { | |||||
this.form = { | |||||
id: this.user.id, | |||||
nickName: this.user.nickName, | |||||
sex: this.user.sex, | |||||
phone: this.user.phone, | |||||
}; | |||||
}, | |||||
doSubmit() { | |||||
if (this.$refs.form) { | |||||
this.$refs.form.validate((valid) => { | |||||
if (valid) { | |||||
this.saveLoading = true; | |||||
store | |||||
.dispatch('UpdateUserInfo', this.form) | |||||
.then(() => { | |||||
this.editSuccessNotify(); | |||||
this.saveLoading = false; | |||||
this.infoDialog = false; | |||||
}) | |||||
.catch(() => { | |||||
this.saveLoading = false; | |||||
this.infoDialog = false; | |||||
}); | |||||
} | |||||
}); | |||||
} | |||||
}, | |||||
}, | |||||
}; | }; | ||||
</script> | </script> | ||||
<style rel="stylesheet/scss" lang="scss"> | |||||
.avatar { | |||||
display: block; | |||||
width: 120px; | |||||
height: 120px; | |||||
margin: 20px 0; | |||||
cursor: pointer; | |||||
border-radius: 50%; | |||||
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.31), 0 1px 3px rgba(0, 0, 0, 0.08); | |||||
<style rel="stylesheet/scss" lang="scss" scoped> | |||||
@import '@/assets/styles/variables.scss'; | |||||
::v-deep.el-tabs--left { | |||||
.el-tabs__item.is-left { | |||||
text-align: left; | |||||
} | |||||
.el-tabs__item.is-active { | |||||
color: #1f89fc; | |||||
background: #e6f7ff; | |||||
} | |||||
} | } | ||||
.info-row { | |||||
display: flex; | |||||
height: 32px; | |||||
font-size: 14px; | |||||
line-height: 32px; | |||||
.code { | |||||
width: 80%; | |||||
height: 40px; | |||||
padding: 0 20px; | |||||
margin-top: 20px; | |||||
background: #ebedf0; | |||||
} | } | ||||
.info-label { | |||||
width: 100px; | |||||
.copy { | |||||
font-size: 18px; | |||||
color: $primaryColor; | |||||
} | } | ||||
</style> | </style> |
@@ -0,0 +1,231 @@ | |||||
/* * Copyright 2019-2020 Zheng Jie * * Licensed under the Apache License, Version 2.0 (the | |||||
"License"); * you may not use this file except in compliance with the License. * You may obtain a | |||||
copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by | |||||
applicable law or agreed to in writing, software * distributed under the License is distributed on | |||||
an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See | |||||
the License for the specific language governing permissions and * limitations under the License. */ | |||||
<template> | |||||
<div style="margin-left: 30px;"> | |||||
<div class="box-card"> | |||||
<!-- 个人信息 --> | |||||
<img :src="user.avatar" title="点击上传头像" class="avatar" @click="openUploadDialog" /> | |||||
<div class="info-row"> | |||||
<div class="info-label">登录账号</div> | |||||
<div class="info-text">{{ user.username }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户昵称</div> | |||||
<div class="info-text">{{ user.nickName }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户性别</div> | |||||
<div class="info-text">{{ user.sex }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">手机号码</div> | |||||
<div class="info-text">{{ user.phone }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户邮箱</div> | |||||
<div class="info-text">{{ user.email }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户角色</div> | |||||
<div class="info-text">{{ userRoles }}</div> | |||||
</div> | |||||
<div class="info-row"> | |||||
<div class="info-label">用户设置</div> | |||||
<div class="info-text"> | |||||
<el-button type="text" @click="infoDialog = true">修改信息</el-button> | |||||
<el-button type="text" @click="$refs.pass.dialog = true">修改密码</el-button> | |||||
<el-button type="text" @click="$refs.email.dialog = true">修改邮箱</el-button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<UploadForm | |||||
action="fakeApi" | |||||
title="上传头像" | |||||
:visible="uploadDialogVisible" | |||||
:toggleVisible="handleClose" | |||||
:params="uploadParams" | |||||
:multiple="false" | |||||
:limit="1" | |||||
:showFileCount="false" | |||||
:filters="uploadFilters" | |||||
@uploadSuccess="uploadSuccess" | |||||
@uploadError="uploadError" | |||||
/> | |||||
<BaseModal | |||||
:visible.sync="infoDialog" | |||||
title="修改信息" | |||||
width="450px" | |||||
@cancel="infoDialog = false" | |||||
@open="onDialogOpen" | |||||
@ok="doSubmit" | |||||
> | |||||
<el-form ref="form" :model="form" :rules="rules" style="margin-top: 10px;" label-width="65px"> | |||||
<el-form-item label="昵称" prop="nickName"> | |||||
<el-input v-model="form.nickName" style="width: 40%;" /> | |||||
<span style="margin-left: 10px; color: #c0c0c0;">用户昵称不作为登录使用</span> | |||||
</el-form-item> | |||||
<el-form-item label="手机号" prop="phone"> | |||||
<el-input v-model="form.phone" style="width: 40%;" /> | |||||
<span style="margin-left: 10px; color: #c0c0c0;">一个手机号只能注册一个用户</span> | |||||
</el-form-item> | |||||
<el-form-item label="性别"> | |||||
<el-radio-group v-model="form.sex" style="width: 178px;"> | |||||
<el-radio label="男">男</el-radio> | |||||
<el-radio label="女">女</el-radio> | |||||
</el-radio-group> | |||||
</el-form-item> | |||||
</el-form> | |||||
</BaseModal> | |||||
<updateEmail ref="email" :email="user.email" /> | |||||
<updatePass ref="pass" /> | |||||
</div> | |||||
</template> | |||||
<script> | |||||
import { mapGetters } from 'vuex'; | |||||
import store from '@/store'; | |||||
import { bucketName, bucketHost } from '@/utils/minIO'; | |||||
import { validateName } from '@/utils/validate'; | |||||
import { invalidFileNameChar } from '@/utils'; | |||||
import { updateAvatar } from '@/api/user'; | |||||
import BaseModal from '@/components/BaseModal'; | |||||
import UploadForm from '@/components/UploadForm'; | |||||
import updateEmail from './components/updateEmail'; | |||||
import updatePass from './components/updatePass'; | |||||
export default { | |||||
name: 'UserInfo', | |||||
components: { BaseModal, UploadForm, updatePass, updateEmail }, | |||||
data() { | |||||
return { | |||||
saveLoading: false, | |||||
infoDialog: false, | |||||
uploadDialogVisible: false, | |||||
form: { | |||||
id: '', | |||||
nickName: '', | |||||
sex: '', | |||||
phone: '', | |||||
}, | |||||
rules: { | |||||
nickName: [ | |||||
{ required: true, message: '请输入用户昵称', trigger: 'blur' }, | |||||
{ validator: validateName, trigger: 'blur' }, | |||||
], | |||||
phone: [ | |||||
{ required: true, message: '请输入手机号码', trigger: 'blur' }, | |||||
{ | |||||
pattern: /^1\d{10}$/, | |||||
message: '请输入正确的11位手机号码', | |||||
trigger: ['blur', 'change'], | |||||
}, | |||||
], | |||||
}, | |||||
uploadFilters: [invalidFileNameChar], | |||||
}; | |||||
}, | |||||
computed: { | |||||
...mapGetters(['user']), | |||||
userRoles() { | |||||
const roles = this.user.roles || []; | |||||
const names = roles.map((role) => role.name); | |||||
return names.join(' ') || '-'; | |||||
}, | |||||
uploadParams() { | |||||
return { | |||||
objectPath: `avatar/${this.user.id}`, // 对象存储路径 | |||||
}; | |||||
}, | |||||
}, | |||||
created() { | |||||
store.dispatch('GetUserInfo').then(() => {}); | |||||
}, | |||||
methods: { | |||||
uploadSuccess(res) { | |||||
if (!res.length) return; | |||||
const filePath = res[0].data.objectName; | |||||
updateAvatar({ | |||||
path: `${bucketName}/${filePath}`, | |||||
realName: `${bucketHost}/${bucketName}/${filePath}`, | |||||
}).then(() => { | |||||
this.$notify({ | |||||
title: '头像修改成功', | |||||
type: 'success', | |||||
duration: 2500, | |||||
}); | |||||
store.dispatch('GetUserInfo').then(() => {}); | |||||
}); | |||||
}, | |||||
uploadError() { | |||||
this.$message({ | |||||
message: '头像修改失败', | |||||
type: 'error', | |||||
}); | |||||
}, | |||||
openUploadDialog() { | |||||
this.uploadDialogVisible = true; | |||||
}, | |||||
handleClose() { | |||||
this.uploadDialogVisible = false; | |||||
}, | |||||
onDialogOpen() { | |||||
this.form = { | |||||
id: this.user.id, | |||||
nickName: this.user.nickName, | |||||
sex: this.user.sex, | |||||
phone: this.user.phone, | |||||
}; | |||||
}, | |||||
doSubmit() { | |||||
if (this.$refs.form) { | |||||
this.$refs.form.validate((valid) => { | |||||
if (valid) { | |||||
this.saveLoading = true; | |||||
store | |||||
.dispatch('UpdateUserInfo', this.form) | |||||
.then(() => { | |||||
this.editSuccessNotify(); | |||||
this.saveLoading = false; | |||||
this.infoDialog = false; | |||||
}) | |||||
.catch(() => { | |||||
this.saveLoading = false; | |||||
this.infoDialog = false; | |||||
}); | |||||
} | |||||
}); | |||||
} | |||||
}, | |||||
}, | |||||
}; | |||||
</script> | |||||
<style rel="stylesheet/scss" lang="scss"> | |||||
.avatar { | |||||
display: block; | |||||
width: 120px; | |||||
height: 120px; | |||||
margin: 0 0 20px; | |||||
cursor: pointer; | |||||
border-radius: 50%; | |||||
box-shadow: 0 4px 6px rgba(50, 50, 93, 0.31), 0 1px 3px rgba(0, 0, 0, 0.08); | |||||
} | |||||
.info-row { | |||||
display: flex; | |||||
height: 32px; | |||||
font-size: 14px; | |||||
line-height: 32px; | |||||
} | |||||
.info-label { | |||||
width: 100px; | |||||
} | |||||
</style> |
@@ -2,6 +2,199 @@ | |||||
# yarn lockfile v1 | # yarn lockfile v1 | ||||
"@antv/adjust@^0.2.1": | |||||
version "0.2.3" | |||||
resolved "https://registry.yarnpkg.com/@antv/adjust/-/adjust-0.2.3.tgz#c3884a680c3264cc125d7f2ab5398e8a1c0b9401" | |||||
integrity sha512-rihqcCdS7piQnK1nRlCvbIaj2QeaqghxINXiMpTJp+0c9cKlTUwL7/2r+gv9YN5R0P1WzSHTmK2Sn+bQCJDo0Q== | |||||
dependencies: | |||||
"@antv/util" "~2.0.0" | |||||
tslib "^1.10.0" | |||||
"@antv/attr@^0.3.1": | |||||
version "0.3.2" | |||||
resolved "https://registry.yarnpkg.com/@antv/attr/-/attr-0.3.2.tgz#e5866b64870c62f3a9c25b8a61f654ba2bfda051" | |||||
integrity sha512-31PfcVKeQdPBmr/QD+IC0NB/FbdtVKOXBCNMepFc5/dEs7jphmgG1V4tfAJmcXIHubCTHOjpscTrDIvoKSGvMQ== | |||||
dependencies: | |||||
"@antv/color-util" "^2.0.1" | |||||
"@antv/util" "~2.0.0" | |||||
tslib "^1.10.0" | |||||
"@antv/color-util@^2.0.1", "@antv/color-util@^2.0.2": | |||||
version "2.0.6" | |||||
resolved "https://registry.yarnpkg.com/@antv/color-util/-/color-util-2.0.6.tgz#5e129bb9ce3f2b9309b52102b3dc929430ccc016" | |||||
integrity sha512-KnPEaAH+XNJMjax9U35W67nzPI+QQ2x27pYlzmSIWrbj4/k8PGrARXfzDTjwoozHJY8qG62Z+Ww6Alhu2FctXQ== | |||||
dependencies: | |||||
"@antv/util" "^2.0.9" | |||||
tslib "^2.0.3" | |||||
"@antv/component@^0.8.7": | |||||
version "0.8.13" | |||||
resolved "https://registry.yarnpkg.com/@antv/component/-/component-0.8.13.tgz#7fa57ec01ff4fd8048979d6daa84bd53ee7167a8" | |||||
integrity sha512-VCCVQUA9/Scxlrc8N8VDY/hTCvpQwoHmSqgu15viYpyDMx8lBuzrWOdBy2IbHMbjrPFwMw7wOCmAGQL76yeFQA== | |||||
dependencies: | |||||
"@antv/dom-util" "~2.0.1" | |||||
"@antv/g-base" "0.5.6" | |||||
"@antv/matrix-util" "^3.1.0-beta.1" | |||||
"@antv/path-util" "~2.0.7" | |||||
"@antv/scale" "~0.3.1" | |||||
"@antv/util" "~2.0.0" | |||||
fecha "~4.2.0" | |||||
tslib "^2.0.3" | |||||
"@antv/coord@^0.3.0": | |||||
version "0.3.1" | |||||
resolved "https://registry.yarnpkg.com/@antv/coord/-/coord-0.3.1.tgz#982e261d8a1e06a198eb518ea7acc20ed875a019" | |||||
integrity sha512-rFE94C8Xzbx4xmZnHh2AnlB3Qm1n5x0VT3OROy257IH6Rm4cuzv1+tZaUBATviwZd99S+rOY9telw/+6C9GbRw== | |||||
dependencies: | |||||
"@antv/matrix-util" "^3.1.0-beta.2" | |||||
"@antv/util" "~2.0.12" | |||||
tslib "^2.1.0" | |||||
"@antv/dom-util@^2.0.2", "@antv/dom-util@~2.0.1": | |||||
version "2.0.3" | |||||
resolved "https://registry.yarnpkg.com/@antv/dom-util/-/dom-util-2.0.3.tgz#cbd158b1c88e0e8a4d865871a5969b1190554ff5" | |||||
integrity sha512-dUHsUT4U9X1T1/Y9bH3jRMe0MzvWJk2jSQm1vm3w9NX+Ra0ftq5VUBiGTNbthm3nFwG0fFFjip904rYjl50g4A== | |||||
dependencies: | |||||
tslib "^2.0.3" | |||||
"@antv/event-emitter@^0.1.1", "@antv/event-emitter@^0.1.2", "@antv/event-emitter@~0.1.0": | |||||
version "0.1.2" | |||||
resolved "https://registry.yarnpkg.com/@antv/event-emitter/-/event-emitter-0.1.2.tgz#a17b7cb86e6d071880dc6bfb232756f88624ecbc" | |||||
integrity sha512-6C6NJOdoNVptCr5y9BVOhKkCgW7LFs/SpcRyAExUeSjAm0zJqcqNkSIRGsXYhj4PJI+CZICHzGwwiSnIsE68Ug== | |||||
"@antv/g-base@0.5.6": | |||||
version "0.5.6" | |||||
resolved "https://registry.yarnpkg.com/@antv/g-base/-/g-base-0.5.6.tgz#d96da5fbf6c5f8b073072751e15e5eec70b393fc" | |||||
integrity sha512-szxqFQ/xdCnfaeSEEC2kVjXdKxJnvKKJNT0MvaOG3UXOfsjPDLgb3IKLr+bU3sLvTAQfPhsbtYh7mWb03+mGjA== | |||||
dependencies: | |||||
"@antv/event-emitter" "^0.1.1" | |||||
"@antv/g-math" "^0.1.6" | |||||
"@antv/matrix-util" "^3.1.0-beta.1" | |||||
"@antv/path-util" "~2.0.5" | |||||
"@antv/util" "~2.0.0" | |||||
"@types/d3-timer" "^2.0.0" | |||||
d3-ease "^1.0.5" | |||||
d3-interpolate "^1.3.2" | |||||
d3-timer "^1.0.9" | |||||
detect-browser "^5.1.0" | |||||
tslib "^2.0.3" | |||||
"@antv/g-base@^0.5.3", "@antv/g-base@~0.5.6": | |||||
version "0.5.9" | |||||
resolved "https://registry.yarnpkg.com/@antv/g-base/-/g-base-0.5.9.tgz#58d0e11d85157ada1408fbdf24f4f468f40e59cd" | |||||
integrity sha512-IAzuCLRmz9cKCWUKR3cKWgLZ/6OQYpTCIOgxAP8Bc+HRw0mu8iC3OTz+tWKGv9L8unpvCvpQd1H+OTTjdg/TpQ== | |||||
dependencies: | |||||
"@antv/event-emitter" "^0.1.1" | |||||
"@antv/g-math" "^0.1.6" | |||||
"@antv/matrix-util" "^3.1.0-beta.1" | |||||
"@antv/path-util" "~2.0.5" | |||||
"@antv/util" "~2.0.0" | |||||
"@types/d3-timer" "^2.0.0" | |||||
d3-ease "^1.0.5" | |||||
d3-interpolate "^1.3.2" | |||||
d3-timer "^1.0.9" | |||||
detect-browser "^5.1.0" | |||||
tslib "^2.0.3" | |||||
"@antv/g-canvas@~0.5.10": | |||||
version "0.5.10" | |||||
resolved "https://registry.yarnpkg.com/@antv/g-canvas/-/g-canvas-0.5.10.tgz#ad19a1dcd19edd12d29539e1dc5b521585b437c6" | |||||
integrity sha512-U454VM7TlO/y1fYP9B9Fbj4QCl/CjQDxaCAHzg8SKq5FecSUseh7Gjliv4YMb3QAb9UCaNx0RUpobUCFBZgLhg== | |||||
dependencies: | |||||
"@antv/g-base" "^0.5.3" | |||||
"@antv/g-math" "^0.1.6" | |||||
"@antv/matrix-util" "^3.1.0-beta.1" | |||||
"@antv/path-util" "~2.0.5" | |||||
"@antv/util" "~2.0.0" | |||||
gl-matrix "^3.0.0" | |||||
tslib "^2.0.3" | |||||
"@antv/g-math@^0.1.6": | |||||
version "0.1.7" | |||||
resolved "https://registry.yarnpkg.com/@antv/g-math/-/g-math-0.1.7.tgz#6ec2769269f7ccb67e58140d5739df74046cc04e" | |||||
integrity sha512-xGyXaloD1ynfp7gS4VuV+MjSptZIwHvLHr8ekXJSFAeWPYLu84yOW2wOZHDdp1bzDAIuRv6xDBW58YGHrWsFcA== | |||||
dependencies: | |||||
"@antv/util" "~2.0.0" | |||||
gl-matrix "^3.0.0" | |||||
"@antv/g-svg@~0.5.6": | |||||
version "0.5.6" | |||||
resolved "https://registry.yarnpkg.com/@antv/g-svg/-/g-svg-0.5.6.tgz#70b2fa980c431b39ad3c5b4b53e36a1d60957d65" | |||||
integrity sha512-Xve1EUGk4HMbl2nq4ozR4QLh6GyoZ8Xw/+9kHYI4B5P2lIUQU95MuRsaLFfW5NNpZDx85ZeH97tqEmC9L96E7A== | |||||
dependencies: | |||||
"@antv/g-base" "^0.5.3" | |||||
"@antv/g-math" "^0.1.6" | |||||
"@antv/util" "~2.0.0" | |||||
detect-browser "^5.0.0" | |||||
tslib "^2.0.3" | |||||
"@antv/g2@^4.1.0": | |||||
version "4.1.19" | |||||
resolved "https://registry.yarnpkg.com/@antv/g2/-/g2-4.1.19.tgz#d0da4a883e7674db463fd090cac7498237916730" | |||||
integrity sha512-dRO32/8TcdsalkQ1f4IHywLsfFNj2+seun+RzdGsV7B9yvITFeZN6PQlscCzCUuE0KljMIbCHpVhuLzp3bWR6A== | |||||
dependencies: | |||||
"@antv/adjust" "^0.2.1" | |||||
"@antv/attr" "^0.3.1" | |||||
"@antv/color-util" "^2.0.2" | |||||
"@antv/component" "^0.8.7" | |||||
"@antv/coord" "^0.3.0" | |||||
"@antv/dom-util" "^2.0.2" | |||||
"@antv/event-emitter" "~0.1.0" | |||||
"@antv/g-base" "~0.5.6" | |||||
"@antv/g-canvas" "~0.5.10" | |||||
"@antv/g-svg" "~0.5.6" | |||||
"@antv/matrix-util" "^3.1.0-beta.1" | |||||
"@antv/path-util" "^2.0.3" | |||||
"@antv/scale" "^0.3.7" | |||||
"@antv/util" "~2.0.5" | |||||
tslib "^2.0.0" | |||||
"@antv/g2plot@^2.3.17": | |||||
version "2.3.24" | |||||
resolved "https://registry.yarnpkg.com/@antv/g2plot/-/g2plot-2.3.24.tgz#92d8a6157a6e6d8999ea5072f57ebe5c73473c16" | |||||
integrity sha512-JP/8l72iOJSFuuZHe7nYvNqkwgDg5f6W5DbXXZiLOiXKBWwFTpiNiAvKI4F5yJBSYcxPcgz+hKHrEBc8jO5K5g== | |||||
dependencies: | |||||
"@antv/event-emitter" "^0.1.2" | |||||
"@antv/g2" "^4.1.0" | |||||
d3-hierarchy "^2.0.0" | |||||
d3-regression "^1.3.5" | |||||
pdfast "^0.2.0" | |||||
size-sensor "^1.0.1" | |||||
tslib "^2.0.3" | |||||
"@antv/matrix-util@^3.1.0-beta.1", "@antv/matrix-util@^3.1.0-beta.2": | |||||
version "3.1.0-beta.2" | |||||
resolved "https://registry.yarnpkg.com/@antv/matrix-util/-/matrix-util-3.1.0-beta.2.tgz#b4afafb70dbdf52affca308d3546c8a090fd23ca" | |||||
integrity sha512-Efwp0ZHxVDK/8RUa/RRWN7HKFHJmjn7Oq5HaNBbCmsxd7JTla3Zsoq1AZrjWMDlq0lplo77urclwI+XIW8NEHw== | |||||
dependencies: | |||||
"@antv/util" "^2.0.9" | |||||
gl-matrix "^3.3.0" | |||||
tslib "^1.10.0" | |||||
"@antv/path-util@^2.0.3", "@antv/path-util@~2.0.5", "@antv/path-util@~2.0.7": | |||||
version "2.0.9" | |||||
resolved "https://registry.yarnpkg.com/@antv/path-util/-/path-util-2.0.9.tgz#976e4a3cfb6219767a602d297b205c88d66d7b2c" | |||||
integrity sha512-kunEz4dNheQMVn4rVFsoBDx+n9Knfi3uRLvDk9SojZAqpninsjFhdoiYtbExwJGz1FYGtiV10Y6N1tp73kZFcg== | |||||
dependencies: | |||||
"@antv/util" "^2.0.9" | |||||
tslib "^2.0.3" | |||||
"@antv/scale@^0.3.7", "@antv/scale@~0.3.1": | |||||
version "0.3.11" | |||||
resolved "https://registry.yarnpkg.com/@antv/scale/-/scale-0.3.11.tgz#3ce10445e108a9bc208840c3394507f0cfb44e68" | |||||
integrity sha512-nARq88j77tADJZ+TqUgrZnJAXuVf5U553TOGrhJ5aL8eEkPxn6ZoroT78oK09zojrjeRPqOSGJl7Md3fmHk/kg== | |||||
dependencies: | |||||
"@antv/util" "~2.0.3" | |||||
fecha "~4.2.0" | |||||
tslib "^2.0.0" | |||||
"@antv/util@^2.0.9", "@antv/util@~2.0.0", "@antv/util@~2.0.12", "@antv/util@~2.0.3", "@antv/util@~2.0.5": | |||||
version "2.0.14" | |||||
resolved "https://registry.yarnpkg.com/@antv/util/-/util-2.0.14.tgz#1ac8c4f790beaf6572daecf62df6aa55fa0a31df" | |||||
integrity sha512-iwM4XKRzW7pbBnMnSGKqcNGo3FdDzMGbRojAiMQ2KC0bTwtLEphQ+hYWa1c+O9BuHtcMkVvTVDylHNESL5vE5g== | |||||
dependencies: | |||||
tslib "^2.0.3" | |||||
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.12.13": | "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.12.13": | ||||
version "7.12.13" | version "7.12.13" | ||||
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" | resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658" | ||||
@@ -964,6 +1157,14 @@ | |||||
"@nodelib/fs.scandir" "2.1.4" | "@nodelib/fs.scandir" "2.1.4" | ||||
fastq "^1.6.0" | fastq "^1.6.0" | ||||
"@opd/g2plot-vue@3.1.12": | |||||
version "3.1.12" | |||||
resolved "https://registry.yarnpkg.com/@opd/g2plot-vue/-/g2plot-vue-3.1.12.tgz#3d8b8a8a478d998808427873c5fbc10d59588070" | |||||
integrity sha512-Rp5ieCbTVe2CZgBBGhKowj7OrkI0Y3BE784kE5bYF6Ol0ALcnj+v8Z1W1NBg+lHZrkisMH1zhFDCWNQX5ObSNg== | |||||
dependencies: | |||||
core-js "^3.9.1" | |||||
vue-demi "^0.7.4" | |||||
"@riophae/vue-treeselect@0.1.0": | "@riophae/vue-treeselect@0.1.0": | ||||
version "0.1.0" | version "0.1.0" | ||||
resolved "https://registry.npmjs.org/@riophae/vue-treeselect/-/vue-treeselect-0.1.0.tgz#39bb5b6757047008e27b6cddf33efde5d94c6efc" | resolved "https://registry.npmjs.org/@riophae/vue-treeselect/-/vue-treeselect-0.1.0.tgz#39bb5b6757047008e27b6cddf33efde5d94c6efc" | ||||
@@ -1010,6 +1211,11 @@ | |||||
remark "^13.0.0" | remark "^13.0.0" | ||||
unist-util-find-all-after "^3.0.2" | unist-util-find-all-after "^3.0.2" | ||||
"@types/d3-timer@^2.0.0": | |||||
version "2.0.0" | |||||
resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-2.0.0.tgz#9901bb02af38798764674df17d66b07329705632" | |||||
integrity sha512-l6stHr1VD1BWlW6u3pxrjLtJfpPZq9I3XmKIQtq7zHM/s6fwEtI1Yn6Sr5/jQTrUDCC5jkS6gWqlFGCDArDqNg== | |||||
"@types/glob@^7.1.1": | "@types/glob@^7.1.1": | ||||
version "7.1.3" | version "7.1.3" | ||||
resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" | resolved "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" | ||||
@@ -1792,6 +1998,11 @@ argparse@^1.0.7: | |||||
dependencies: | dependencies: | ||||
sprintf-js "~1.0.2" | sprintf-js "~1.0.2" | ||||
argparse@^2.0.1: | |||||
version "2.0.1" | |||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" | |||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== | |||||
arr-diff@^2.0.0: | arr-diff@^2.0.0: | ||||
version "2.0.0" | version "2.0.0" | ||||
resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" | resolved "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" | ||||
@@ -3108,6 +3319,11 @@ code-point-at@^1.0.0: | |||||
resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" | resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" | ||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= | integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= | ||||
codemirror@^5.60.0: | |||||
version "5.62.0" | |||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.62.0.tgz#e9ecd012e6f9eaf2e05ff4a449ff750f51619e22" | |||||
integrity sha512-Xnl3304iCc8nyVZuRkzDVVwc794uc9QNX0UcPGeNic1fbzkSrO4l4GVXho9tRNKBgPYZXgocUqXyfIv3BILhCQ== | |||||
codepage@~1.14.0: | codepage@~1.14.0: | ||||
version "1.14.0" | version "1.14.0" | ||||
resolved "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99" | resolved "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz#8cbe25481323559d7d307571b0fff91e7a1d2f99" | ||||
@@ -3425,6 +3641,11 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.5.7, core-js@^2.6.5: | |||||
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" | resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" | ||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== | integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== | ||||
core-js@^3.9.1: | |||||
version "3.15.1" | |||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.15.1.tgz#6c08ab88abdf56545045ccf5fd81f47f407e7f1a" | |||||
integrity sha512-h8VbZYnc9pDzueiS2610IULDkpFFPunHwIpl8yRwFahAEEdSpHlTy3h3z3rKq5h11CaUdBEeRViu9AYvbxiMeg== | |||||
core-util-is@1.0.2, core-util-is@~1.0.0: | core-util-is@1.0.2, core-util-is@~1.0.0: | ||||
version "1.0.2" | version "1.0.2" | ||||
resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" | resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" | ||||
@@ -3805,9 +4026,9 @@ d3-dsv@1: | |||||
iconv-lite "0.4" | iconv-lite "0.4" | ||||
rw "1" | rw "1" | ||||
d3-ease@1: | |||||
d3-ease@1, d3-ease@^1.0.5: | |||||
version "1.0.7" | version "1.0.7" | ||||
resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" | |||||
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.7.tgz#9a834890ef8b8ae8c558b2fe55bd57f5993b85e2" | |||||
integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ== | integrity sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ== | ||||
d3-fetch@1: | d3-fetch@1: | ||||
@@ -3844,9 +4065,14 @@ d3-hierarchy@1: | |||||
resolved "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" | resolved "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz#2f6bee24caaea43f8dc37545fa01628559647a83" | ||||
integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== | integrity sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ== | ||||
d3-interpolate@1: | |||||
d3-hierarchy@^2.0.0: | |||||
version "2.0.0" | |||||
resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-2.0.0.tgz#dab88a58ca3e7a1bc6cab390e89667fcc6d20218" | |||||
integrity sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw== | |||||
d3-interpolate@1, d3-interpolate@^1.3.2: | |||||
version "1.4.0" | version "1.4.0" | ||||
resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" | |||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.4.0.tgz#526e79e2d80daa383f9e0c1c1c7dcc0f0583e987" | |||||
integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== | integrity sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA== | ||||
dependencies: | dependencies: | ||||
d3-color "1" | d3-color "1" | ||||
@@ -3871,6 +4097,11 @@ d3-random@1: | |||||
resolved "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291" | resolved "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz#2833be7c124360bf9e2d3fd4f33847cfe6cab291" | ||||
integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ== | integrity sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ== | ||||
d3-regression@^1.3.5: | |||||
version "1.3.9" | |||||
resolved "https://registry.yarnpkg.com/d3-regression/-/d3-regression-1.3.9.tgz#61c34acb9b6bbd9172ede89f05d0b7fbd57ccdc0" | |||||
integrity sha512-PoMpToIvxSnVpgAZTCERVseRend40JIBICJxwATJ/T4laWGaI5dpRdRxrPITxD8hk8W455fKonVChwSmDyWEyg== | |||||
d3-scale-chromatic@1: | d3-scale-chromatic@1: | ||||
version "1.5.0" | version "1.5.0" | ||||
resolved "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98" | resolved "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz#54e333fc78212f439b14641fb55801dd81135a98" | ||||
@@ -3915,9 +4146,9 @@ d3-time@1: | |||||
resolved "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" | resolved "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz#b1e19d307dae9c900b7e5b25ffc5dcc249a8a0f1" | ||||
integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== | integrity sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA== | ||||
d3-timer@1: | |||||
d3-timer@1, d3-timer@^1.0.9: | |||||
version "1.0.10" | version "1.0.10" | ||||
resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" | |||||
resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.10.tgz#dfe76b8a91748831b13b6d9c793ffbd508dd9de5" | |||||
integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== | integrity sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw== | ||||
d3-transition@1: | d3-transition@1: | ||||
@@ -4224,6 +4455,11 @@ destroy@~1.0.4: | |||||
resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" | resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" | ||||
integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= | ||||
detect-browser@^5.0.0, detect-browser@^5.1.0: | |||||
version "5.2.0" | |||||
resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.2.0.tgz#c9cd5afa96a6a19fda0bbe9e9be48a6b6e1e9c97" | |||||
integrity sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA== | |||||
detect-indent@^4.0.0: | detect-indent@^4.0.0: | ||||
version "4.0.0" | version "4.0.0" | ||||
resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" | resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" | ||||
@@ -5284,6 +5520,11 @@ fb-watchman@^2.0.0: | |||||
dependencies: | dependencies: | ||||
bser "2.1.1" | bser "2.1.1" | ||||
fecha@~4.2.0: | |||||
version "4.2.1" | |||||
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" | |||||
integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== | |||||
fflate@^0.3.8: | fflate@^0.3.8: | ||||
version "0.3.11" | version "0.3.11" | ||||
resolved "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz#2c440d7180fdeb819e64898d8858af327b042a5d" | resolved "https://registry.npmjs.org/fflate/-/fflate-0.3.11.tgz#2c440d7180fdeb819e64898d8858af327b042a5d" | ||||
@@ -5792,6 +6033,11 @@ git-raw-commits@^2.0.0: | |||||
split2 "^3.0.0" | split2 "^3.0.0" | ||||
through2 "^4.0.0" | through2 "^4.0.0" | ||||
gl-matrix@^3.0.0, gl-matrix@^3.3.0: | |||||
version "3.3.0" | |||||
resolved "https://registry.yarnpkg.com/gl-matrix/-/gl-matrix-3.3.0.tgz#232eef60b1c8b30a28cbbe75b2caf6c48fd6358b" | |||||
integrity sha512-COb7LDz+SXaHtl/h4LeaFcNdJdAQSDeVqjiIihSXNrkWObZLhDI4hIkZC11Aeqp7bcE72clzB0BnDXr2SmslRA== | |||||
glob-base@^0.3.0: | glob-base@^0.3.0: | ||||
version "0.3.0" | version "0.3.0" | ||||
resolved "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" | resolved "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" | ||||
@@ -7585,11 +7831,23 @@ js-yaml@^3.13.1, js-yaml@^3.7.0, js-yaml@^3.9.1: | |||||
argparse "^1.0.7" | argparse "^1.0.7" | ||||
esprima "^4.0.0" | esprima "^4.0.0" | ||||
js-yaml@^4.0.0: | |||||
version "4.1.0" | |||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" | |||||
integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== | |||||
dependencies: | |||||
argparse "^2.0.1" | |||||
jsbn@~0.1.0: | jsbn@~0.1.0: | ||||
version "0.1.1" | version "0.1.1" | ||||
resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" | resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" | ||||
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= | integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= | ||||
jschardet@^2.2.1: | |||||
version "2.3.0" | |||||
resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-2.3.0.tgz#06e2636e16c8ada36feebbdc08aa34e6a9b3ff75" | |||||
integrity sha512-6I6xT7XN/7sBB7q8ObzKbmv5vN+blzLcboDE1BNEsEfmRXJValMxO6OIRT69ylPBRemS3rw6US+CMCar0OBc9g== | |||||
jsdom@^11.5.1: | jsdom@^11.5.1: | ||||
version "11.12.0" | version "11.12.0" | ||||
resolved "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" | resolved "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" | ||||
@@ -9521,6 +9779,11 @@ pbkdf2@^3.0.3: | |||||
safe-buffer "^5.0.1" | safe-buffer "^5.0.1" | ||||
sha.js "^2.4.8" | sha.js "^2.4.8" | ||||
pdfast@^0.2.0: | |||||
version "0.2.0" | |||||
resolved "https://registry.yarnpkg.com/pdfast/-/pdfast-0.2.0.tgz#8cbc556e1bf2522177787c0de2e0d4373ba885c9" | |||||
integrity sha1-jLxVbhvyUiF3eHwN4uDUNzuohck= | |||||
performance-now@^2.1.0: | performance-now@^2.1.0: | ||||
version "2.1.0" | version "2.1.0" | ||||
resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" | resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" | ||||
@@ -11220,6 +11483,11 @@ sisteransi@^0.1.1: | |||||
resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce" | resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-0.1.1.tgz#5431447d5f7d1675aac667ccd0b865a4994cb3ce" | ||||
integrity sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g== | integrity sha512-PmGOd02bM9YO5ifxpw36nrNMBTptEtfRl4qUYl9SndkolplkrZZOW7PGHjrZL53QvMVj9nQ+TKqUnRsw4tJa4g== | ||||
size-sensor@^1.0.1: | |||||
version "1.0.1" | |||||
resolved "https://registry.yarnpkg.com/size-sensor/-/size-sensor-1.0.1.tgz#f84e46206d3e259faff1d548e4b3beca93219dbb" | |||||
integrity sha512-QTy7MnuugCFXIedXRpUSk9gUnyNiaxIdxGfUjr8xxXOqIB3QvBUYP9+b51oCg2C4dnhaeNk/h57TxjbvoJrJUA== | |||||
slash@^1.0.0: | slash@^1.0.0: | ||||
version "1.0.0" | version "1.0.0" | ||||
resolved "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" | resolved "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" | ||||
@@ -12221,6 +12489,11 @@ tslib@^1.10.0, tslib@^1.9.0: | |||||
resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" | resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" | ||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== | integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== | ||||
tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0: | |||||
version "2.3.0" | |||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e" | |||||
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg== | |||||
tslib@^2.2.0: | tslib@^2.2.0: | ||||
version "2.2.0" | version "2.2.0" | ||||
resolved "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" | resolved "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" | ||||
@@ -12446,6 +12719,11 @@ urix@^0.1.0: | |||||
resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" | resolved "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" | ||||
integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= | integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= | ||||
url-join@^4.0.1: | |||||
version "4.0.1" | |||||
resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" | |||||
integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== | |||||
url-loader@^1.1.2: | url-loader@^1.1.2: | ||||
version "1.1.2" | version "1.1.2" | ||||
resolved "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8" | resolved "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz#b971d191b83af693c5e3fea4064be9e1f2d7f8d8" | ||||
@@ -12630,6 +12908,11 @@ vue-copy-to-clipboard@^1.0.3: | |||||
dependencies: | dependencies: | ||||
copy-to-clipboard "^3.3.1" | copy-to-clipboard "^3.3.1" | ||||
vue-demi@^0.7.4: | |||||
version "0.7.5" | |||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.7.5.tgz#88dee7492fc99a0f911ff03fc02c658fa2a79af8" | |||||
integrity sha512-eFSQSvbQdY7C9ujOzvM6tn7XxwLjn0VQDXQsiYBLBwf28Na+2nTQR4BBBcomhmdP6mmHlBKAwarq6a0BPG87hQ== | |||||
vue-eslint-parser@^2.0.3: | vue-eslint-parser@^2.0.3: | ||||
version "2.0.3" | version "2.0.3" | ||||
resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1" | resolved "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-2.0.3.tgz#c268c96c6d94cfe3d938a5f7593959b0ca3360d1" | ||||