| @@ -0,0 +1,12 @@ | |||
| {{template "base/head" .}} | |||
| <link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-modelmanage-local-create-1.css?v={{MD5 AppVer}}" /> | |||
| <style></style> | |||
| {{$repository := .Repository.ID}} | |||
| <div class="repository release dataset-list view"> | |||
| {{template "repo/header" .}} | |||
| <div class="ui container"> | |||
| <div id="__vue-root"></div> | |||
| </div> | |||
| </div> | |||
| <script src="{{StaticUrlPrefix}}/js/vp-modelmanage-local-create-1.js?v={{MD5 AppVer}}"></script> | |||
| {{template "base/footer" .}} | |||
| @@ -0,0 +1,12 @@ | |||
| {{template "base/head" .}} | |||
| <link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-modelmanage-local-create-2.css?v={{MD5 AppVer}}" /> | |||
| <style></style> | |||
| {{$repository := .Repository.ID}} | |||
| <div class="repository release dataset-list view"> | |||
| {{template "repo/header" .}} | |||
| <div class="ui container"> | |||
| <div id="__vue-root"></div> | |||
| </div> | |||
| </div> | |||
| <script src="{{StaticUrlPrefix}}/js/vp-modelmanage-local-create-2.js?v={{MD5 AppVer}}"></script> | |||
| {{template "base/footer" .}} | |||
| @@ -0,0 +1,12 @@ | |||
| {{template "base/head" .}} | |||
| <link rel="stylesheet" href="{{StaticUrlPrefix}}/css/vp-modelmanage-local-create-1.css?v={{MD5 AppVer}}" /> | |||
| <style></style> | |||
| {{$repository := .Repository.ID}} | |||
| <div class="repository release dataset-list view"> | |||
| {{template "repo/header" .}} | |||
| <div class="ui container"> | |||
| <div id="__vue-root"></div> | |||
| </div> | |||
| </div> | |||
| <script src="{{StaticUrlPrefix}}/js/vp-modelmanage-local-create-1.js?v={{MD5 AppVer}}"></script> | |||
| {{template "base/footer" .}} | |||
| @@ -25,6 +25,23 @@ | |||
| border-bottom-left-radius: 4px; | |||
| box-shadow: 0 2px 3px 0 rgb(34 36 38 / 15%); | |||
| } | |||
| .m-blue-btn { | |||
| background-color: rgb(22, 132, 252) !important; | |||
| } | |||
| .m-blue-btn:hover { | |||
| background-color: #66b1ff !important; | |||
| color: #fff; | |||
| } | |||
| .m-blue-btn:focus { | |||
| background-color: #66b1ff !important; | |||
| color: #fff; | |||
| } | |||
| .m-blue-btn:active { | |||
| background-color: #3a8ee6 !important; | |||
| color: #fff; | |||
| } | |||
| </style> | |||
| <link rel="stylesheet" href="/self/ztree/css/zTreeStyle/zTreeStyle.css" type="text/css"> | |||
| @@ -57,8 +74,10 @@ | |||
| </div> | |||
| <div class="column right aligned"> | |||
| <!-- --> | |||
| <a class="ui button {{if .Permission.CanWrite $.UnitTypeModelManage}} blue m-blue-btn {{else}} disabled {{end}}" | |||
| href="{{.RepoLink}}/modelmanage/create_local_model_tmpl1">导入本地模型</a> | |||
| <a class="ui button {{if .Permission.CanWrite $.UnitTypeModelManage}} green {{else}} disabled {{end}}" | |||
| onclick="showcreate(this)">{{$.i18n.Tr "repo.model.manage.import_new_model"}}</a> | |||
| onclick="showcreate(this)">导入线上模型</a> | |||
| </div> | |||
| </div> | |||
| {{if eq $.MODEL_COUNT 0}} | |||
| @@ -23,6 +23,7 @@ | |||
| <i class="el-icon-arrow-right"></i> | |||
| </div> | |||
| <!-- <i class="el-icon-time"></i> --> | |||
| <span :class="scope.row.modelType == '1' ? 'm-local' : 'm-online'">{{ scope.row.modelType == '1' ? '本地' : '线上' }}</span> | |||
| <a | |||
| class="text-over" | |||
| :href="showinfoHref + scope.row.name" | |||
| @@ -136,7 +137,7 @@ | |||
| > | |||
| <template slot-scope="scope"> | |||
| <div class="space-around"> | |||
| <a | |||
| <!--<a | |||
| :style="{ | |||
| visibility: !scope.row.Children ? 'visible' : 'hidden', | |||
| }" | |||
| @@ -149,6 +150,12 @@ | |||
| ) | |||
| " | |||
| >{{ i18n.model_create_new_ver }}</a | |||
| >--> | |||
| <a | |||
| v-if="scope.row.modelType == 1" | |||
| :href="url + 'create_local_model_tmpl1?type=1&name=' + scope.row.name + '&id=' + scope.row.id" | |||
| :class="{ disabled: !scope.row.isCanOper }" | |||
| >{{ '修改' }}</a | |||
| > | |||
| <a | |||
| :href="loadhref + scope.row.id" | |||
| @@ -223,9 +230,10 @@ export default { | |||
| let tableData; | |||
| tableData = res.data; | |||
| for (let i = 0; i < tableData.length; i++) { | |||
| trainTaskInfo = JSON.parse(tableData[i].trainTaskInfo); | |||
| trainTaskInfo = JSON.parse(tableData[i].trainTaskInfo || '{}'); | |||
| tableData[i].engineName = this.getEngineName(tableData[i]); | |||
| tableData[i].computeResource = trainTaskInfo.ComputeResource; | |||
| tableData[i].computeResource = tableData[i].type == '0' ? 'CPU/GPU' : 'NPU'; | |||
| tableData[i].cName = tableData[i].name; | |||
| tableData[i].rowKey = tableData[i].id + Math.random(); | |||
| tableData[i].name = ""; | |||
| @@ -483,13 +491,14 @@ export default { | |||
| let trainTaskInfo; | |||
| this.tableData = res.data.data; | |||
| for (let i = 0; i < this.tableData.length; i++) { | |||
| trainTaskInfo = JSON.parse(this.tableData[i].trainTaskInfo); | |||
| trainTaskInfo = JSON.parse(this.tableData[i].trainTaskInfo || '{}'); | |||
| this.tableData[i].cName = this.tableData[i].name; | |||
| this.tableData[i].rowKey = this.tableData[i].id + Math.random(); | |||
| this.tableData[i].engineName = this.getEngineName( | |||
| this.tableData[i] | |||
| ); | |||
| this.tableData[i].computeResource = trainTaskInfo.ComputeResource; | |||
| // this.tableData[i].computeResource = trainTaskInfo.ComputeResource; | |||
| this.tableData[i].computeResource = this.tableData[i].type == '0' ? 'CPU/GPU' : 'NPU'; | |||
| this.tableData[i].hasChildren = | |||
| res.data.data[i].versionCount === 1 ? false : true; | |||
| if (this.tableData[i].status !== 1) { | |||
| @@ -615,6 +624,24 @@ export default { | |||
| white-space: nowrap; | |||
| } | |||
| .m-local { | |||
| background-color: rgb(22, 132, 252); | |||
| color: white; | |||
| padding: 2px 3px; | |||
| border-radius: 4px; | |||
| font-size: 12px; | |||
| margin-right: 2px; | |||
| } | |||
| .m-online { | |||
| background-color: rgb(91, 185, 115); | |||
| color: white; | |||
| padding: 2px 3px; | |||
| border-radius: 4px; | |||
| font-size: 12px; | |||
| margin-right: 2px; | |||
| } | |||
| .el-icon-arrow-right { | |||
| font-family: element-icons !important; | |||
| speak: none; | |||
| @@ -0,0 +1,48 @@ | |||
| import service from "../service"; | |||
| // 上传文件1: 获取文件chunks信息 | |||
| // params: { md5, type: 0-CPU/GPU,1-NPU, file_name, scene: ''-数据集,'model'-模型 } | |||
| // return: uploadID, uuid, uploaded, chunks, attachID, datasetID(), datasetName(), fileName | |||
| export const getChunks = (params) => { | |||
| return service({ | |||
| url: `/attachments/get_chunks`, | |||
| method: 'get', | |||
| params, | |||
| data: {}, | |||
| }); | |||
| }; | |||
| // 上传文件2: 上传新文件 | |||
| // params: { totalChunkCounts, md5, size, fileType, type, file_name, scene=model, modeluuid=xxxx } | |||
| // return: uploadID, uuid | |||
| export const getNewMultipart = (params) => { | |||
| return service({ | |||
| url: `/attachments/new_multipart`, | |||
| method: 'get', | |||
| params, | |||
| data: {}, | |||
| }); | |||
| }; | |||
| // 上传文件3: 获取分片上传地址 | |||
| // params: { uuid, uploadID, size, chunkNumber, type, file_name, scene=model } | |||
| // return: url | |||
| export const getMultipartUrl = (params) => { | |||
| return service({ | |||
| url: `/attachments/get_multipart_url`, | |||
| method: 'get', | |||
| params, | |||
| data: {}, | |||
| }); | |||
| }; | |||
| // 上传文件4: 完成上传后 | |||
| // data: { uuid, uploadID, size, type, file_name, dataset_id, description, scene=model, modeluuid=xxxx } | |||
| export const setCompleteMultipart = (data) => { | |||
| return service({ | |||
| url: `/attachments/complete_multipart`, | |||
| method: 'post', | |||
| params: {}, | |||
| data, | |||
| }); | |||
| }; | |||
| @@ -0,0 +1,23 @@ | |||
| import service from "../service"; | |||
| import Qs from 'qs'; | |||
| // 保存本地模型 | |||
| export const saveLocalModel = (data) => { | |||
| return service({ | |||
| url: `${data.repo}/modelmanage/create_local_model`, | |||
| method: 'post', | |||
| headers: { 'Content-type': 'application/x-www-form-urlencoded' }, | |||
| params: {}, | |||
| data: Qs.stringify(data), | |||
| }); | |||
| }; | |||
| // 求模型信息 | |||
| export const getModelInfoByName = (params) => { | |||
| return service({ | |||
| url: `${params.repo}/modelmanage/show_model_info_api`, | |||
| method: 'get', | |||
| params, | |||
| data: {}, | |||
| }); | |||
| }; | |||
| @@ -0,0 +1,425 @@ | |||
| <template> | |||
| <div> | |||
| <div class="header"> | |||
| <span class="title">{{ type == '1' ? '修改模型信息' : '导入本地模型' }}</span> | |||
| </div> | |||
| <div class="content"> | |||
| <div class="guide-c" v-if="type != '1'"> | |||
| <div class="step focused"> | |||
| <div class="num">1</div> | |||
| <div class="txt">创建模型</div> | |||
| </div> | |||
| <div class="line"></div> | |||
| <div class="step"> | |||
| <div class="num">2</div> | |||
| <div class="txt">上传模型文件</div> | |||
| </div> | |||
| </div> | |||
| <div class="row-c" v-loading="loading"> | |||
| <div class="row"> | |||
| <div class="r-title"><label class="required">可用集群</label></div> | |||
| <div class="r-content" style="display:flex;"> | |||
| <div class="cluster-type-btn" v-if="type != 1 || type == 1 && state.type === 0" | |||
| :class="state.type === 0 ? 'focused' : ''" | |||
| :style="type != 1 ? 'border-top-right-radius:unset;border-bottom-right-radius:unset;' : ''" | |||
| @click="state.type = 0"> | |||
| <svg xmlns="http://www.w3.org/2000/svg" | |||
| class="icon styles__StyledSVGIconPathComponent-sc-16fsqc8-0 cSaWDI svg-icon-path-icon fill" | |||
| viewBox="0 0 32 32" width="16" height="16"> | |||
| <defs data-reactroot=""></defs> | |||
| <g> | |||
| <path | |||
| d="M4 3.989c0-0.731 0.593-1.323 1.324-1.323h21.352c0.729 0.005 1.318 0.594 1.324 1.322v24.022c-0.001 0.731-0.593 1.323-1.324 1.323h-21.352c-0.729-0.005-1.318-0.594-1.324-1.322v-24.022zM25.333 14.667v-9.333h-18.667v9.333h18.667zM25.333 17.333h-18.667v9.333h18.667v-9.333zM12 8h8v2.667h-8v-2.667zM12 20h8v2.667h-8v-2.667z"> | |||
| </path> | |||
| </g> | |||
| </svg> | |||
| <span class="txt">CPU / GPU</span> | |||
| </div> | |||
| <div class="cluster-type-btn" v-if="type != 1 || type == 1 && state.type === 1" | |||
| :class="state.type === 1 ? 'focused' : ''" | |||
| :style="type != 1 ? 'border-top-left-radius:unset;border-bottom-left-radius:unset;' : ''" | |||
| @click="state.type = 1"> | |||
| <svg xmlns="http://www.w3.org/2000/svg" | |||
| class="icon styles__StyledSVGIconPathComponent-sc-16fsqc8-0 iKfgJk svg-icon-path-icon fill" | |||
| viewBox="0 0 32 32" width="16" height="16"> | |||
| <defs data-reactroot=""></defs> | |||
| <g> | |||
| <path | |||
| d="M4 3.989c0-0.731 0.593-1.323 1.324-1.323h21.352c0.729 0.005 1.318 0.594 1.324 1.322v24.022c-0.001 0.731-0.593 1.323-1.324 1.323h-21.352c-0.729-0.005-1.318-0.594-1.324-1.322v-24.022zM25.333 14.667v-9.333h-18.667v9.333h18.667zM25.333 17.333h-18.667v9.333h18.667v-9.333zM12 8h8v2.667h-8v-2.667zM12 20h8v2.667h-8v-2.667z"> | |||
| </path> | |||
| </g> | |||
| </svg> | |||
| <span class="txt">Ascend NPU</span> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <div class="r-title"><label class="required">模型名称</label></div> | |||
| <div class="r-content"> | |||
| <el-input size="medium" :maxLength="25" v-model="state.name" placeholder="请输入模型名称"></el-input> | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <div class="r-title"><label class="required">版本</label></div> | |||
| <div class="r-content"> | |||
| <el-input class="input-disabled" style="width:288px;" size="medium" v-model="state.version" readonly> | |||
| </el-input> | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <div class="r-title"><label class="required">模型框架</label></div> | |||
| <div class="r-content"> | |||
| <el-select style="width:288px;" size="medium" v-model="state.engine" placeholder="请选择"> | |||
| <el-option v-for="item in engineList" :key="item.k" :label="item.v" :value="item.k"> | |||
| </el-option> | |||
| </el-select> | |||
| </div> | |||
| </div> | |||
| <div class="row"> | |||
| <div class="r-title"><label>模型标签</label></div> | |||
| <div class="r-content"> | |||
| <el-input size="medium" :maxLength="255" v-model="state.label" placeholder="输入标签,多个标签用空格区分"></el-input> | |||
| </div> | |||
| </div> | |||
| <div class="row" style="align-items:flex-start;"> | |||
| <div class="r-title"><label>模型描述</label></div> | |||
| <div class="r-content"> | |||
| <el-input type="textarea" :maxLength="255" size="medium" v-model="state.description" :rows="3" | |||
| placeholder="描述字数不超过255个字符"> | |||
| </el-input> | |||
| </div> | |||
| </div> | |||
| <div class="row" style="margin-top:20px"> | |||
| <div class="r-title"><label></label></div> | |||
| <div class="r-content"> | |||
| <el-button size="medium" class="green" @click="submit">保存模型</el-button> | |||
| <el-button size="medium" @click="cancel">取消</el-button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { saveLocalModel, getModelInfoByName } from '~/apis/modules/modelmanage'; | |||
| // import { CLUSTERS, COMPUTER_RESOURCES, ACC_CARD_TYPE } from '~/const'; | |||
| import { getUrlSearchParams } from '~/utils'; | |||
| // import { formatDate } from 'element-ui/lib/utils/date-util'; | |||
| const REPO_NAME = location.pathname.split('/')[2]; | |||
| export default { | |||
| data() { | |||
| return { | |||
| type: '0', // 1-修改,其它-新增 | |||
| loading: false, | |||
| state: { | |||
| type: 0, | |||
| name: REPO_NAME + '_model_' + Math.random().toString(36).substr(2, 4), | |||
| version: '0.0.1', | |||
| engine: '0', | |||
| label: '', | |||
| description: '', | |||
| }, | |||
| engineList: [{ k: '0', v: 'PyTorch' }, { k: '1', v: 'TensorFlow' }, { k: '2', v: 'MindSpore' }, { k: '4', v: 'PaddlePaddle' }, { k: '5', v: 'OneFlow' }, { k: '6', v: 'MXNet' }, { k: '3', v: 'Other' }], | |||
| }; | |||
| }, | |||
| components: { /*QueueDialog*/ }, | |||
| methods: { | |||
| submit() { | |||
| console.log(this.state); | |||
| if (!this.state.name) { | |||
| this.$message({ | |||
| type: 'info', | |||
| message: '请输入模型名称', | |||
| }); | |||
| return; | |||
| } | |||
| saveLocalModel({ | |||
| repo: location.pathname.split('/').slice(0, 3).join('/'), | |||
| ...this.state, | |||
| }).then(res => { | |||
| res = res.data; | |||
| if (res && res.code == '0') { | |||
| if (this.type == '1') { | |||
| this.cancel(); | |||
| return; | |||
| } | |||
| const list = window.location.href.split('/'); | |||
| list.pop(); | |||
| list.push('create_local_model_tmpl2'); | |||
| window.location.href = list.join('/') + '?type=0name=' + this.state.name + '&id=' + res.id; | |||
| } else { | |||
| this.$message({ | |||
| type: 'error', | |||
| message: '模型保存失败', | |||
| }); | |||
| } | |||
| }).catch(err => { | |||
| console.log(err); | |||
| }); | |||
| }, | |||
| cancel() { | |||
| const list = window.location.href.split('/'); | |||
| list.pop(); | |||
| list.push('show_model'); | |||
| window.location.href = list.join('/'); | |||
| }, | |||
| }, | |||
| mounted() { | |||
| const urlParams = getUrlSearchParams(); | |||
| if (urlParams.type == '1' && urlParams.name && urlParams.id) { // update | |||
| this.type = urlParams.type; | |||
| this.state.name = urlParams.name; | |||
| this.state.id = urlParams.id; | |||
| this.loading = true; | |||
| getModelInfoByName({ | |||
| repo: location.pathname.split('/').slice(0, 3).join('/'), | |||
| name: urlParams.name, | |||
| id: urlParams.id | |||
| }).then(res => { | |||
| this.loading = false; | |||
| const list = res.data; | |||
| if (list && list.length) { | |||
| const data = list[0]; | |||
| this.state.type = data.type; | |||
| this.state.id = data.id; | |||
| this.state.name = data.name; | |||
| this.state.version = data.version; | |||
| this.state.engine = data.engine.toString(); | |||
| this.state.label = data.label; | |||
| this.state.description = data.description; | |||
| } | |||
| }).catch(err => { | |||
| this.loading = false; | |||
| console.log(err); | |||
| this.cancel(); | |||
| }); | |||
| } | |||
| }, | |||
| beforeDestroy() { | |||
| }, | |||
| }; | |||
| </script> | |||
| <style scoped lang="less"> | |||
| .header { | |||
| height: 45px; | |||
| border-color: rgb(212, 212, 213); | |||
| border-width: 1px; | |||
| border-style: solid; | |||
| border-radius: 5px 5px 0px 0px; | |||
| font-size: 14px; | |||
| background: rgb(240, 240, 240); | |||
| display: flex; | |||
| align-items: center; | |||
| .title { | |||
| font-weight: 600; | |||
| font-size: 16px; | |||
| color: rgb(16, 16, 16); | |||
| margin-left: 10px; | |||
| } | |||
| } | |||
| .content { | |||
| margin-top: -1px; | |||
| border-color: rgb(212, 212, 213); | |||
| border-width: 1px; | |||
| border-style: solid; | |||
| padding: 30px 0; | |||
| border-top: none; | |||
| .guide-c { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| margin-bottom: 20px; | |||
| .step { | |||
| display: flex; | |||
| align-items: center; | |||
| color: rgb(136, 136, 136); | |||
| .num { | |||
| border: 1px solid rgb(136, 136, 136); | |||
| width: 34px; | |||
| height: 34px; | |||
| font-size: 24px; | |||
| text-align: center; | |||
| line-height: 34px; | |||
| border-radius: 17px; | |||
| margin-right: 8px; | |||
| } | |||
| .txt { | |||
| font-weight: 600; | |||
| } | |||
| &.focused { | |||
| color: rgb(22, 132, 252); | |||
| .num { | |||
| border-color: rgb(22, 132, 252); | |||
| } | |||
| } | |||
| } | |||
| .line { | |||
| height: 2px; | |||
| width: 50px; | |||
| background-color: rgb(187, 187, 187); | |||
| margin: 0 10px; | |||
| } | |||
| } | |||
| .row-c { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| flex-direction: column; | |||
| margin: 0 auto; | |||
| width: 80%; | |||
| .row { | |||
| width: 100%; | |||
| display: flex; | |||
| align-items: center; | |||
| margin: 8px 0; | |||
| margin-left: -190px; | |||
| .r-title { | |||
| text-align: right; | |||
| font-size: .92857143em; | |||
| font-weight: 700; | |||
| color: rgba(0, 0, 0, .87); | |||
| width: 200px; | |||
| margin-right: 28px; | |||
| position: relative; | |||
| .required { | |||
| &::after { | |||
| position: absolute; | |||
| margin: -0.2em 0 0 0.2em; | |||
| content: '*'; | |||
| color: #db2828; | |||
| } | |||
| } | |||
| } | |||
| .r-content { | |||
| flex: 1; | |||
| .cluster-type-btn { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| border: 1px solid #DCDFE6; | |||
| height: 36px; | |||
| padding: 10px; | |||
| cursor: pointer; | |||
| border-radius: 4px; | |||
| .icon { | |||
| margin-right: 5px; | |||
| } | |||
| .txt {} | |||
| &.focused { | |||
| border-color: rgb(50, 145, 248); | |||
| color: rgb(50, 145, 248); | |||
| cursor: default; | |||
| .icon { | |||
| :not([stroke]) { | |||
| fill: rgb(50, 145, 248); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .input-disabled { | |||
| /deep/ .el-input__inner { | |||
| background-color: #f5f5f6 !important; | |||
| color: #888888 !important; | |||
| } | |||
| } | |||
| .el-select-dropdown__item.selected { | |||
| color: rgba(0, 0, 0, .95); | |||
| } | |||
| /deep/ .el-button { | |||
| background-color: #e0e1e2; | |||
| color: rgba(0, 0, 0, .6); | |||
| border-color: transparent; | |||
| transition: opacity .1s ease, background-color .1s ease, color .1s ease, box-shadow .1s ease, background .1s ease, -webkit-box-shadow .1s ease; | |||
| will-change: auto; | |||
| -webkit-tap-highlight-color: transparent; | |||
| &:hover { | |||
| border-color: transparent; | |||
| background-color: #cacbcd; | |||
| color: rgba(0, 0, 0, .8); | |||
| } | |||
| &:focus { | |||
| background-color: #cacbcd; | |||
| color: rgba(0, 0, 0, .8); | |||
| border-color: transparent; | |||
| } | |||
| &:active { | |||
| background-color: #babbbc; | |||
| color: rgba(0, 0, 0, .9); | |||
| border-color: transparent; | |||
| } | |||
| &.green { | |||
| background-color: #5bb973; | |||
| color: #fff; | |||
| &:hover { | |||
| background-color: #16ab39; | |||
| border-color: transparent; | |||
| } | |||
| &:focus { | |||
| background-color: #0ea432; | |||
| border-color: transparent; | |||
| } | |||
| &:active { | |||
| background-color: #198f35; | |||
| border-color: transparent; | |||
| } | |||
| } | |||
| } | |||
| /deep/ .el-select { | |||
| .is-focus { | |||
| .el-input__inner { | |||
| border-color: #85b7d9; | |||
| } | |||
| } | |||
| } | |||
| /deep/ .el-input__inner { | |||
| &:focus { | |||
| border-color: #85b7d9; | |||
| } | |||
| } | |||
| /deep/ .el-textarea__inner { | |||
| &:focus { | |||
| border-color: #85b7d9; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,677 @@ | |||
| <template> | |||
| <div> | |||
| <div class="header"> | |||
| <span class="title">{{ type == '1' ? '增加模型文件' : '上传模型文件' }}</span> | |||
| </div> | |||
| <div class="content"> | |||
| <div class="guide-c" v-if="type != '1'"> | |||
| <div class="step"> | |||
| <div class="num">1</div> | |||
| <div class="txt">创建模型</div> | |||
| </div> | |||
| <div class="line"></div> | |||
| <div class="step focused"> | |||
| <div class="num">2</div> | |||
| <div class="txt">上传模型文件</div> | |||
| </div> | |||
| </div> | |||
| <div class="row-c"> | |||
| <div class="row"> | |||
| <div class="r-title"><label class="required">模型名称</label></div> | |||
| <div class="r-content"> | |||
| <el-input size="medium" class="input-disabled" v-model="state.name" placeholder="请输入模型名称" readonly> | |||
| </el-input> | |||
| </div> | |||
| </div> | |||
| <div class="row" style="align-items:flex-start;"> | |||
| <div class="r-title"><label class="required">文件上传</label></div> | |||
| <div class="r-content"> | |||
| <div> | |||
| <form class="dropzone" ref="dropzoneRef"></form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| <div class="row" style="margin-top:10px"> | |||
| <div class="r-title"><label></label></div> | |||
| <div class="r-content"> | |||
| <el-button size="medium" class="green" @click="submit">上传</el-button> | |||
| <el-button size="medium" @click="cancel">取消</el-button> | |||
| </div> | |||
| </div> | |||
| <div class="row" style="align-items:flex-start;"> | |||
| <div class="r-title"><label>上传状态:</label></div> | |||
| <div class="r-content"> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </template> | |||
| <script> | |||
| import { getModelInfoByName } from '~/apis/modules/modelmanage'; | |||
| import { getChunks, getNewMultipart, getMultipartUrl, setCompleteMultipart } from '~/apis/modules/fileupload'; | |||
| // import { CLUSTERS, COMPUTER_RESOURCES, ACC_CARD_TYPE } from '~/const'; | |||
| import { getUrlSearchParams } from '~/utils'; | |||
| // import { formatDate } from 'element-ui/lib/utils/date-util'; | |||
| import 'dropzone/dist/dropzone.css'; | |||
| import Dropzone from 'dropzone'; | |||
| import SparkMD5 from "spark-md5"; | |||
| Dropzone.autoDiscover = false; | |||
| const uploadChunkSize = 1024 * 1024 * 64; | |||
| const md5ChunkSize = 1024 * 1024 * 64; | |||
| export default { | |||
| data() { | |||
| return { | |||
| dropzoneHandler: null, | |||
| type: '0', // 1-修改,其它-新增 | |||
| state: { | |||
| type: '', | |||
| id: '', | |||
| name: '', | |||
| version: '', | |||
| engine: '', | |||
| label: '', | |||
| description: '', | |||
| size: '', | |||
| } | |||
| }; | |||
| }, | |||
| components: { /*QueueDialog*/ }, | |||
| methods: { | |||
| initDropZone() { | |||
| this.dropzoneHandler = new Dropzone(this.$refs.dropzoneRef, { | |||
| url: '/', | |||
| parallelUploads: 20, | |||
| uploadMultiple: true, | |||
| maxFilesize: 2048, | |||
| timeout: 0, | |||
| addRemoveLinks: true, | |||
| autoProcessQueue: false, | |||
| dictDefaultMessage: '点击添加文件或直接拖拽文件到此处', | |||
| dictInvalidFileType: 'dictInvalidFileType', | |||
| dictFileTooBig: 'dictFileTooBig', | |||
| dictRemoveFile: 'dictRemoveFile', | |||
| }); | |||
| this.dropzoneHandler.on("addedfile", file => { | |||
| console.log(`File added: ${file.name}`); | |||
| console.log(file); | |||
| // this.calcFileMd5(file); | |||
| }); | |||
| this.dropzoneHandler.on("removedfile", file => { | |||
| console.log(`File removedfile: ${file.name}`); | |||
| }); | |||
| this.dropzoneHandler.on("maxfilesexceeded", file => { | |||
| console.log(`File removedfile: ${file.name}`); | |||
| }); | |||
| }, | |||
| calcFileMd5(file) { | |||
| const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; | |||
| const chunkSize = md5ChunkSize; | |||
| const chunks = Math.ceil(file.size / chunkSize); | |||
| const spark = new SparkMD5.ArrayBuffer(); | |||
| const fileReader = new FileReader(); | |||
| file.totalChunkCounts = chunks; | |||
| if (file.size == 0) { | |||
| file.totalChunkCounts = 1; | |||
| } | |||
| let currentChunk = 0; | |||
| return new Promise((resolve, reject) => { | |||
| fileReader.onload = function (e) { | |||
| console.log(file.name + ': read chunk nr', currentChunk + 1, 'of', chunks); | |||
| spark.append(e.target.result); | |||
| currentChunk++; | |||
| if (currentChunk < chunks) { | |||
| loadNext(); | |||
| } else { | |||
| console.log(file.name + ': finished loading'); | |||
| console.info(file.name + ': computed hash', spark.end()); | |||
| const md5 = spark.end(); | |||
| spark.destroy(); | |||
| file.uniqueIdentifier = md5; | |||
| resolve(md5); | |||
| } | |||
| }; | |||
| fileReader.onerror = function (e) { | |||
| console.warn(file.name + ': oops, calcFileMd5 went wrong.'); | |||
| reject(e); | |||
| }; | |||
| function loadNext() { | |||
| const start = currentChunk * chunkSize; | |||
| const end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize; | |||
| fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); | |||
| } | |||
| loadNext(); | |||
| }); | |||
| }, | |||
| async _getChunksInfo(file) { | |||
| const params = { | |||
| params: { | |||
| md5: file.uniqueIdentifier, | |||
| type: this.state.type, | |||
| file_name: file.name, | |||
| _csrf: csrf, | |||
| }, | |||
| }; | |||
| try { | |||
| // const response = axios.get("/attachments/get_chunks", params); | |||
| const response = await getChunks(params); | |||
| file.uploadID = response.data.uploadID; | |||
| file.uuid = response.data.uuid; | |||
| file.uploaded = response.data.uploaded; | |||
| file.chunks = response.data.chunks; | |||
| file.attachID = response.data.attachID; | |||
| file.datasetID = response.data.datasetID; | |||
| file.datasetName = response.data.datasetName; | |||
| file.realName = response.data.fileName; | |||
| return file; | |||
| } catch (error) { | |||
| this.emitDropzoneFailed(file); | |||
| console.log("getChunksInfo catch: ", error); | |||
| return null; | |||
| } | |||
| }, | |||
| getChunksInfo(file) { | |||
| return getChunks({ | |||
| md5: file.uniqueIdentifier, | |||
| type: this.state.type, | |||
| file_name: file.name, | |||
| scene: 'model', | |||
| }).then(res => { | |||
| const data = res.data; | |||
| file.uploadID = data.uploadID; | |||
| file.uuid = data.uuid; | |||
| file.uploaded = data.uploaded; | |||
| file.chunks = data.chunks; | |||
| file.attachID = data.attachID; | |||
| // file.datasetID = data.datasetID; | |||
| // file.datasetName = data.datasetName; | |||
| file.modelUuid = data.modeluuid; | |||
| file.modelName = data.modelName; | |||
| file.realName = data.fileName; | |||
| return file; | |||
| }).catch(err => { | |||
| console.info('getChunksInfo', err); | |||
| return err; | |||
| }); | |||
| }, | |||
| async _newUpload(file) { | |||
| const res = await axios.get("/attachments/new_multipart", { | |||
| params: { | |||
| totalChunkCounts: file.totalChunkCounts, | |||
| md5: file.uniqueIdentifier, | |||
| size: file.size, | |||
| fileType: file.type, | |||
| type: this.uploadtype, | |||
| file_name: file.name, | |||
| _csrf: csrf, | |||
| }, | |||
| }); | |||
| file.uploadID = res.data.uploadID; | |||
| file.uuid = res.data.uuid; | |||
| }, | |||
| newUpload(file) { | |||
| return getNewMultipart({ | |||
| totalChunkCounts: file.totalChunkCounts, | |||
| md5: file.uniqueIdentifier, | |||
| size: file.size, | |||
| fileType: file.type, | |||
| type: this.state.type, | |||
| file_name: file.name, | |||
| scene: 'model', | |||
| modeluuid: file.modelUuid, | |||
| }).then(res => { | |||
| const data = res.data; | |||
| file.uploadID = data.uploadID; | |||
| file.uuid = data.uuid; | |||
| if (file.uploadID && file.uuid) { | |||
| file.chunks = ''; | |||
| this.breakpointUpload(file); | |||
| } | |||
| return file; | |||
| }).catch(err => { | |||
| console.log('getNewMultipart', err); | |||
| return err; | |||
| }); | |||
| }, | |||
| breakpointUpload(file) { | |||
| const blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice; | |||
| const chunks = Math.ceil(file.size / chunkSize); | |||
| const fileReader = new FileReader(); | |||
| const time = new Date().getTime(); | |||
| const chunkSize = uploadChunkSize; | |||
| const _this = this; | |||
| let currentChunk = 0; | |||
| const successChunks = []; | |||
| const successParts = file.chunks.split(","); | |||
| for (let i = 0; i < successParts.length; i++) { | |||
| successChunks[i] = successParts[i].split("-")[0]; | |||
| } | |||
| const urls = []; | |||
| const etags = []; | |||
| function checkSuccessChunks() { | |||
| const index = successChunks.indexOf((currentChunk + 1).toString()); | |||
| if (index == -1) { | |||
| return false; | |||
| } | |||
| return true; | |||
| } | |||
| async function getUploadChunkUrl(currentChunk, partSize) { | |||
| const res = await getMultipartUrl({ | |||
| uuid: file.uuid, | |||
| uploadID: file.uploadID, | |||
| size: partSize, | |||
| chunkNumber: currentChunk + 1, | |||
| type: _this.state.type, | |||
| file_name: file.name, | |||
| scene: 'model', | |||
| }); | |||
| urls[currentChunk] = res.data.url; | |||
| } | |||
| async function uploadMinioNewMethod(url, e) { | |||
| const xhr = new XMLHttpRequest(); | |||
| xhr.open("PUT", url, false); | |||
| if (_this.state.type == 0) { | |||
| xhr.setRequestHeader("Content-Type", "text/plain"); | |||
| xhr.send(e.target.result); | |||
| const etagValue = xhr.getResponseHeader("etag"); | |||
| etags[currentChunk] = etagValue; | |||
| } else if (_this.state.type == 1) { | |||
| xhr.setRequestHeader("Content-Type", ""); | |||
| xhr.send(e.target.result); | |||
| const etagValue = xhr.getResponseHeader("ETag"); | |||
| etags[currentChunk] = etagValue; | |||
| } | |||
| } | |||
| async function uploadChunk(e) { | |||
| try { | |||
| if (!checkSuccessChunks()) { | |||
| const start = currentChunk * chunkSize; | |||
| const partSize = start + chunkSize >= file.size ? file.size - start : chunkSize; | |||
| // 获取分片上传url | |||
| await getUploadChunkUrl(currentChunk, partSize); | |||
| if (urls[currentChunk] != '') { | |||
| await uploadMinioNewMethod(urls[currentChunk], e); | |||
| if (etags[currentChunk] != '') { | |||
| } else { | |||
| console.log("上传到minio uploadChunk etags[currentChunk] == ''"); // TODO | |||
| } | |||
| } else { | |||
| console.log("uploadChunk urls[currentChunk] != ''"); // TODO | |||
| } | |||
| } | |||
| } catch (error) { | |||
| console.log(error); | |||
| } | |||
| } | |||
| async function completeUpload() { | |||
| return await setCompleteMultipart({ | |||
| uuid: file.uuid, | |||
| uploadID: file.uploadID, | |||
| file_name: file.name, | |||
| size: file.size, | |||
| type: _this.state.type, | |||
| scene: 'model', | |||
| modeluuid: file.modelUuid, | |||
| }); | |||
| } | |||
| function loadNext() { | |||
| const start = currentChunk * chunkSize; | |||
| const end = start + chunkSize >= file.size ? file.size : start + chunkSize; | |||
| fileReader.readAsArrayBuffer(blobSlice.call(file, start, end)); | |||
| } | |||
| fileReader.onload = async (e) => { | |||
| try { | |||
| await uploadChunk(e); | |||
| fileReader.abort(); | |||
| currentChunk++; | |||
| if (currentChunk < chunks) { | |||
| console.log(`第${currentChunk}个分片上传完成, 开始第${currentChunk + 1}/${chunks}个分片上传`); | |||
| // this.progress = Math.ceil((currentChunk / chunks) * 100); | |||
| // this.updateProgress(file, this.dropzoneParams.data("uploading"), Number(((currentChunk / chunks) * 100).toFixed(2)), 3); | |||
| // this.status = `${this.dropzoneParams.data("uploading")} ${((currentChunk / chunks) * 100).toFixed(2)}%`; | |||
| loadNext(); | |||
| } else { | |||
| try { | |||
| await completeUpload(); | |||
| console.log(`文件上传完成:${file.name} \n分片:${chunks} 大小:${file.size} 用时:${(new Date().getTime() - time) / 1000} s`); | |||
| // this.uploadLength++; | |||
| // this.allUploadLength++; | |||
| // this.updateProgress(file, "上传成功", 100, 0); | |||
| // this.finishUpload(file); | |||
| } catch (err) { | |||
| const info = "上传失败"; | |||
| console.log(info, file) | |||
| // this.allUploadLength++; | |||
| // this.uploadError(file, info); | |||
| // this.updateProgress(file, info, Number(((currentChunk / chunks) * 100).toFixed(2)) - 1, 2); | |||
| } | |||
| } | |||
| } catch (err) { | |||
| console.log(err); | |||
| } | |||
| }; | |||
| console.log("上传分片..."); | |||
| loadNext(); | |||
| }, | |||
| submit() { | |||
| console.log(this.dropzoneHandler); | |||
| const fileList = this.dropzoneHandler.getAcceptedFiles(); | |||
| if (!fileList.length) return; | |||
| for (let i = 0, iLen = fileList.length; i < iLen; i++) { | |||
| const file = fileList[i]; | |||
| this.calcFileMd5(file).then(res => { // 计算MD5 | |||
| this.getChunksInfo(file).then(res => { // 获取Chunk信息 | |||
| if (file.uploadID == '' || file.uuid == '') { // 未上传过 | |||
| this.newUpload(file); | |||
| } else if (file.uploaded == '1') { // 已上传成功 秒传 | |||
| if (file.attachID == '0') { // 删除数据集记录,未删除文件 | |||
| // await addAttachment(file); | |||
| console.log(`file.attachID == '0'`); | |||
| } | |||
| // 同一模型上传同一个文件 | |||
| if (file.modelUuid != '') { | |||
| if (file.modelName != '' && file.realName != '') { | |||
| let info = `该文件已上传在模型: ${file.modelName}`; | |||
| console.log(info); | |||
| // this.uploadError(file, info); | |||
| // this.allUploadLength++; | |||
| // this.updateProgress(file, "上传失败", 0, 1, info); | |||
| } | |||
| } | |||
| console.log('文件上传处理完成'); | |||
| } else { // 断点续传 | |||
| this.breakpointUpload(file); | |||
| } | |||
| }).catch(err => { | |||
| console.info('getChunksInfo', err); | |||
| }); | |||
| }).catch(err => { | |||
| console.info('calcFileMd5', err); | |||
| }); | |||
| } | |||
| }, | |||
| cancel() { | |||
| const list = window.location.href.split('/'); | |||
| list.pop(); | |||
| list.push('show_model'); | |||
| window.location.href = list.join('/'); | |||
| }, | |||
| }, | |||
| mounted() { | |||
| const urlParams = getUrlSearchParams(); | |||
| if (urlParams.name && urlParams.id) { | |||
| this.type = urlParams.type || '0'; | |||
| this.state.name = urlParams.name; | |||
| this.state.id = urlParams.id; | |||
| this.loading = true; | |||
| getModelInfoByName({ | |||
| repo: location.pathname.split('/').slice(0, 3).join('/'), | |||
| name: urlParams.name, | |||
| id: urlParams.id | |||
| }).then(res => { | |||
| this.loading = false; | |||
| const list = res.data; | |||
| if (list && list.length) { | |||
| const data = list[0]; | |||
| this.state.type = data.type; | |||
| this.state.id = data.id; | |||
| this.state.name = data.name; | |||
| this.state.version = data.version; | |||
| this.state.engine = data.engine.toString(); | |||
| this.state.label = data.label; | |||
| this.state.description = data.description; | |||
| this.state.size = data.size; | |||
| } | |||
| }).catch(err => { | |||
| this.loading = false; | |||
| console.log(err); | |||
| this.cancel(); | |||
| }); | |||
| } else { | |||
| this.cancel(); | |||
| return; | |||
| } | |||
| this.initDropZone(); | |||
| }, | |||
| beforeDestroy() { | |||
| }, | |||
| }; | |||
| </script> | |||
| <style scoped lang="less"> | |||
| .header { | |||
| height: 45px; | |||
| border-color: rgb(212, 212, 213); | |||
| border-width: 1px; | |||
| border-style: solid; | |||
| border-radius: 5px 5px 0px 0px; | |||
| font-size: 14px; | |||
| background: rgb(240, 240, 240); | |||
| display: flex; | |||
| align-items: center; | |||
| .title { | |||
| font-weight: 600; | |||
| font-size: 16px; | |||
| color: rgb(16, 16, 16); | |||
| margin-left: 10px; | |||
| } | |||
| } | |||
| .content { | |||
| margin-top: -1px; | |||
| border-color: rgb(212, 212, 213); | |||
| border-width: 1px; | |||
| border-style: solid; | |||
| padding: 30px 0; | |||
| border-top: none; | |||
| .guide-c { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| margin-bottom: 20px; | |||
| .step { | |||
| display: flex; | |||
| align-items: center; | |||
| color: rgb(136, 136, 136); | |||
| .num { | |||
| border: 1px solid rgb(136, 136, 136); | |||
| width: 34px; | |||
| height: 34px; | |||
| font-size: 24px; | |||
| text-align: center; | |||
| line-height: 34px; | |||
| border-radius: 17px; | |||
| margin-right: 8px; | |||
| } | |||
| .txt { | |||
| font-weight: 600; | |||
| } | |||
| &.focused { | |||
| color: rgb(22, 132, 252); | |||
| .num { | |||
| border-color: rgb(22, 132, 252); | |||
| } | |||
| } | |||
| } | |||
| .line { | |||
| height: 2px; | |||
| width: 50px; | |||
| background-color: rgb(187, 187, 187); | |||
| margin: 0 10px; | |||
| } | |||
| } | |||
| .row-c { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| flex-direction: column; | |||
| margin: 0 auto; | |||
| width: 80%; | |||
| .row { | |||
| width: 100%; | |||
| display: flex; | |||
| align-items: center; | |||
| margin: 8px 0; | |||
| margin-left: -190px; | |||
| .r-title { | |||
| text-align: right; | |||
| font-size: .92857143em; | |||
| font-weight: 700; | |||
| color: rgba(0, 0, 0, .87); | |||
| width: 200px; | |||
| margin-right: 28px; | |||
| position: relative; | |||
| .required { | |||
| &::after { | |||
| position: absolute; | |||
| margin: -0.2em 0 0 0.2em; | |||
| content: '*'; | |||
| color: #db2828; | |||
| } | |||
| } | |||
| } | |||
| .r-content { | |||
| flex: 1; | |||
| .cluster-type-btn { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| border: 1px solid #DCDFE6; | |||
| height: 36px; | |||
| padding: 10px; | |||
| cursor: pointer; | |||
| border-radius: 4px; | |||
| .icon { | |||
| margin-right: 5px; | |||
| } | |||
| .txt {} | |||
| &.focused { | |||
| border-color: rgb(50, 145, 248); | |||
| color: rgb(50, 145, 248); | |||
| cursor: default; | |||
| .icon { | |||
| :not([stroke]) { | |||
| fill: rgb(50, 145, 248); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .input-disabled { | |||
| /deep/ .el-input__inner { | |||
| background-color: #f5f5f6 !important; | |||
| color: #888888 !important; | |||
| } | |||
| } | |||
| /deep/ .dz-progress { | |||
| display: none; | |||
| } | |||
| .el-select-dropdown__item.selected { | |||
| color: #85b7d9; | |||
| } | |||
| /deep/ .el-button { | |||
| background-color: #e0e1e2; | |||
| color: rgba(0, 0, 0, .6); | |||
| border-color: transparent; | |||
| transition: opacity .1s ease, background-color .1s ease, color .1s ease, box-shadow .1s ease, background .1s ease, -webkit-box-shadow .1s ease; | |||
| will-change: auto; | |||
| -webkit-tap-highlight-color: transparent; | |||
| &:hover { | |||
| border-color: transparent; | |||
| background-color: #cacbcd; | |||
| color: rgba(0, 0, 0, .8); | |||
| } | |||
| &:focus { | |||
| background-color: #cacbcd; | |||
| color: rgba(0, 0, 0, .8); | |||
| border-color: transparent; | |||
| } | |||
| &:active { | |||
| background-color: #babbbc; | |||
| color: rgba(0, 0, 0, .9); | |||
| border-color: transparent; | |||
| } | |||
| &.green { | |||
| background-color: #5bb973; | |||
| color: #fff; | |||
| &:hover { | |||
| background-color: #16ab39; | |||
| border-color: transparent; | |||
| } | |||
| &:focus { | |||
| background-color: #0ea432; | |||
| border-color: transparent; | |||
| } | |||
| &:active { | |||
| background-color: #198f35; | |||
| border-color: transparent; | |||
| } | |||
| } | |||
| } | |||
| /deep/ .el-select { | |||
| .is-focus { | |||
| .el-input__inner { | |||
| border-color: #85b7d9; | |||
| } | |||
| } | |||
| } | |||
| /deep/ .el-input__inner { | |||
| &:focus { | |||
| border-color: #85b7d9; | |||
| } | |||
| } | |||
| /deep/ .el-textarea__inner { | |||
| &:focus { | |||
| border-color: #85b7d9; | |||
| } | |||
| } | |||
| </style> | |||
| @@ -0,0 +1,17 @@ | |||
| import Vue from 'vue'; | |||
| import ElementUI from 'element-ui'; | |||
| import 'element-ui/lib/theme-chalk/index.css'; | |||
| import localeEn from 'element-ui/lib/locale/lang/en'; | |||
| import localeZh from 'element-ui/lib/locale/lang/zh-CN'; | |||
| import { i18n, lang } from '~/langs'; | |||
| import App from './modelmanage-local-create-1.vue'; | |||
| Vue.use(ElementUI, { | |||
| locale: lang === 'zh-CN' ? localeZh : localeEn, | |||
| size: 'small', | |||
| }); | |||
| new Vue({ | |||
| i18n, | |||
| render: (h) => h(App), | |||
| }).$mount('#__vue-root'); | |||
| @@ -0,0 +1,17 @@ | |||
| import Vue from 'vue'; | |||
| import ElementUI from 'element-ui'; | |||
| import 'element-ui/lib/theme-chalk/index.css'; | |||
| import localeEn from 'element-ui/lib/locale/lang/en'; | |||
| import localeZh from 'element-ui/lib/locale/lang/zh-CN'; | |||
| import { i18n, lang } from '~/langs'; | |||
| import App from './modelmanage-local-create-2.vue'; | |||
| Vue.use(ElementUI, { | |||
| locale: lang === 'zh-CN' ? localeZh : localeEn, | |||
| size: 'small', | |||
| }); | |||
| new Vue({ | |||
| i18n, | |||
| render: (h) => h(App), | |||
| }).$mount('#__vue-root'); | |||
| @@ -5,3 +5,12 @@ export const getListValueWithKey = (list, key, k = 'k', v = 'v') => { | |||
| } | |||
| return ''; | |||
| }; | |||
| export const getUrlSearchParams = () => { | |||
| const params = new URLSearchParams(location.search); | |||
| const obj = {}; | |||
| params.forEach((value, key) => { | |||
| obj[key] = value; | |||
| }); | |||
| return obj; | |||
| }; | |||