mirror of
https://github.com/jumpserver/lina.git
synced 2025-08-19 15:28:25 +00:00
perf: 优化 csv/xlsx 导入 (#725)
* perf: 优化导入csv * perf: 优化导入 stash perf: 优化导入 perf: 更新导入 perf: 优化导入 feat: 完成导入优化 perf: 修复bug * perf: 继续优化导入,性能提高三倍 Co-authored-by: ibuler <ibuler@qq.com>
This commit is contained in:
parent
12ffa363c1
commit
eddd27e95d
@ -1,5 +1,8 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
'@vue/app'
|
'@vue/app'
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'"
|
"vue-i18n-report": "vue-i18n-extract report -v './src/**/*.?(js|vue)' -l './src/i18n/langs/**/*.json'"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@babel/plugin-proposal-optional-chaining": "^7.13.12",
|
||||||
"@ztree/ztree_v3": "3.5.44",
|
"@ztree/ztree_v3": "3.5.44",
|
||||||
"axios": "0.21.1",
|
"axios": "0.21.1",
|
||||||
"axios-retry": "^3.1.9",
|
"axios-retry": "^3.1.9",
|
||||||
@ -25,6 +26,7 @@
|
|||||||
"echarts": "^4.7.0",
|
"echarts": "^4.7.0",
|
||||||
"element-ui": "2.13.2",
|
"element-ui": "2.13.2",
|
||||||
"eslint-plugin-html": "^6.0.0",
|
"eslint-plugin-html": "^6.0.0",
|
||||||
|
"install": "^0.13.0",
|
||||||
"jquery": "^3.5.0",
|
"jquery": "^3.5.0",
|
||||||
"js-cookie": "2.2.0",
|
"js-cookie": "2.2.0",
|
||||||
"less": "^3.10.3",
|
"less": "^3.10.3",
|
||||||
@ -43,6 +45,7 @@
|
|||||||
"lodash.values": "^4.3.0",
|
"lodash.values": "^4.3.0",
|
||||||
"moment-parseformat": "^3.0.0",
|
"moment-parseformat": "^3.0.0",
|
||||||
"normalize.css": "7.0.0",
|
"normalize.css": "7.0.0",
|
||||||
|
"npm": "^7.8.0",
|
||||||
"nprogress": "0.2.0",
|
"nprogress": "0.2.0",
|
||||||
"path-to-regexp": "2.4.0",
|
"path-to-regexp": "2.4.0",
|
||||||
"vue": "2.6.10",
|
"vue": "2.6.10",
|
||||||
|
@ -723,6 +723,10 @@ export default {
|
|||||||
default(row, index) {
|
default(row, index) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
totalData: {
|
||||||
|
type: Array,
|
||||||
|
default: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@ -828,6 +832,12 @@ export default {
|
|||||||
* @property {array} rows - 已选中的行数据的数组
|
* @property {array} rows - 已选中的行数据的数组
|
||||||
*/
|
*/
|
||||||
this.$emit('selection-change', val)
|
this.$emit('selection-change', val)
|
||||||
|
},
|
||||||
|
totalData(val) {
|
||||||
|
if (val) {
|
||||||
|
this.total = val.length
|
||||||
|
this.getList()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -877,12 +887,34 @@ export default {
|
|||||||
}
|
}
|
||||||
return query
|
return query
|
||||||
},
|
},
|
||||||
|
getList({ loading = true } = {}) {
|
||||||
|
const { url } = this
|
||||||
|
if (url) {
|
||||||
|
return this.getListFromRemote({ loading: loading })
|
||||||
|
}
|
||||||
|
if (this.totalData) {
|
||||||
|
this.getListFromStaticData()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getListFromStaticData() {
|
||||||
|
if (!this.hasPagination) {
|
||||||
|
this.data = this.totalData
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// page
|
||||||
|
const pageOffset = this.firstPage - defaultFirstPage
|
||||||
|
const page = this.page === 0 ? 1 : this.page
|
||||||
|
const start = (page + pageOffset - 1) * this.size
|
||||||
|
const end = (page + pageOffset) * this.size
|
||||||
|
console.log(`page: ${page}, size: ${this.size}, start: ${start}, end: ${end}`)
|
||||||
|
this.data = this.totalData.slice(start, end)
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 手动刷新列表数据,选项的默认值为: { loading: true }
|
* 手动刷新列表数据,选项的默认值为: { loading: true }
|
||||||
* @public
|
* @public
|
||||||
* @param {object} options 方法选项
|
* @param {object} options 方法选项
|
||||||
*/
|
*/
|
||||||
getList({ loading = true } = {}) {
|
getListFromRemote({ loading = true } = {}) {
|
||||||
const { url } = this
|
const { url } = this
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return
|
return
|
||||||
|
@ -100,6 +100,9 @@ export default {
|
|||||||
iListeners() {
|
iListeners() {
|
||||||
return Object.assign({}, this.$listeners, this.tableConfig.listeners)
|
return Object.assign({}, this.$listeners, this.tableConfig.listeners)
|
||||||
},
|
},
|
||||||
|
dataTable() {
|
||||||
|
return this.$refs.table
|
||||||
|
},
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
'globalTableConfig': 'tableConfig'
|
'globalTableConfig': 'tableConfig'
|
||||||
})
|
})
|
||||||
|
@ -55,7 +55,6 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -74,4 +73,8 @@ export default {
|
|||||||
/*padding-top: 10px;*/
|
/*padding-top: 10px;*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dialog-footer {
|
||||||
|
padding-right: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,64 +1,74 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
:title="$t('common.Import')"
|
:title="importTitle"
|
||||||
:visible.sync="showImportDialog"
|
:visible.sync="showImportDialog"
|
||||||
:destroy-on-close="true"
|
:destroy-on-close="true"
|
||||||
|
:close-on-click-modal="false"
|
||||||
:loading-status="loadStatus"
|
:loading-status="loadStatus"
|
||||||
@confirm="handleImportConfirm"
|
width="80%"
|
||||||
@cancel="handleImportCancel()"
|
class="importDialog"
|
||||||
|
:confirm-title="confirmTitle"
|
||||||
|
:show-cancel="false"
|
||||||
|
:show-confirm="false"
|
||||||
|
@close="handleImportCancel"
|
||||||
>
|
>
|
||||||
<el-form label-position="left" style="padding-left: 50px">
|
<el-form v-if="!showTable" label-position="left" style="padding-left: 50px">
|
||||||
<el-form-item :label="$t('common.fileType' )" :label-width="'100px'">
|
|
||||||
<el-radio-group v-model="importTypeOption">
|
|
||||||
<el-radio v-for="option of importTypeOptions" :key="option.value" class="export-item" :label="option.value" :disabled="!option.can">{{ option.label }}</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item :label="$t('common.Import' )" :label-width="'100px'">
|
<el-form-item :label="$t('common.Import' )" :label-width="'100px'">
|
||||||
<el-radio v-model="importOption" class="export-item" label="1">{{ this.$t('common.Create') }}</el-radio>
|
<el-radio v-model="importOption" class="export-item" label="create">{{ this.$t('common.Create') }}</el-radio>
|
||||||
<el-radio v-model="importOption" class="export-item" label="2">{{ this.$t('common.Update') }}</el-radio>
|
<el-radio v-model="importOption" class="export-item" label="update">{{ this.$t('common.Update') }}</el-radio>
|
||||||
<div style="line-height: 1.5">
|
<div style="line-height: 1.5">
|
||||||
<span v-if="importOption==='1'" class="el-upload__tip">
|
<span class="el-upload__tip">
|
||||||
{{ this.$t('common.imExport.downloadImportTemplateMsg') }}
|
{{ downloadTemplateTitle }}
|
||||||
<el-link type="success" :underline="false" :href="downloadImportTempUrl">{{ this.$t('common.Download') }}</el-link>
|
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('csv')"> CSV </el-link>
|
||||||
</span>
|
<el-link type="success" :underline="false" style="padding-left: 10px" @click="downloadTemplateFile('xlsx')"> XLSX </el-link>
|
||||||
<span v-else class="el-upload__tip">
|
|
||||||
{{ this.$t('common.imExport.downloadUpdateTemplateMsg') }}
|
|
||||||
<el-link type="success" :underline="false" @click="downloadUpdateTempUrl">{{ this.$t('common.Download') }}</el-link>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'">
|
<el-form-item :label="$t('common.Upload' )" :label-width="'100px'" class="file-uploader">
|
||||||
<el-upload
|
<el-upload
|
||||||
ref="upload"
|
ref="upload"
|
||||||
|
drag
|
||||||
action="string"
|
action="string"
|
||||||
list-type="text/csv"
|
list-type="text/csv"
|
||||||
:http-request="handleImport"
|
|
||||||
:limit="1"
|
:limit="1"
|
||||||
:auto-upload="false"
|
:auto-upload="false"
|
||||||
|
:on-change="onFileChange"
|
||||||
:before-upload="beforeUpload"
|
:before-upload="beforeUpload"
|
||||||
|
accept=".csv,.xlsx"
|
||||||
>
|
>
|
||||||
<el-button size="mini" type="default">{{ this.$t('common.SelectFile') }}</el-button>
|
<i class="el-icon-upload" />
|
||||||
<!-- <div slot="tip" :class="uploadHelpTextClass" style="line-height: 1.5">{{ this.$t('common.imExport.onlyCSVFilesTips') }}</div>-->
|
<div class="el-upload__text">{{ $t('common.imExport.dragUploadFileInfo') }}</div>
|
||||||
|
<div slot="tip" class="el-upload__tip">
|
||||||
|
<span :class="{'hasError': hasFileFormatOrSizeError }">{{ $t('common.imExport.uploadCsvLth10MHelpText') }}</span>
|
||||||
|
<div v-if="renderError" class="hasError">{{ renderError }}</div>
|
||||||
|
</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<div v-if="errorMsg" class="error-msg error-results">
|
<div v-else class="importTableZone">
|
||||||
<ul v-if="typeof errorMsg === 'object'">
|
<ImportTable
|
||||||
<li v-for="(item, index) in errorMsg" :key="item + '-' + index"> {{ item }}</li>
|
ref="importTable"
|
||||||
</ul>
|
:json-data="jsonData"
|
||||||
<span v-else>{{ errorMsg }}</span>
|
:import-option="importOption"
|
||||||
|
:url="url"
|
||||||
|
@cancel="cancelUpload"
|
||||||
|
@finish="closeDialog"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Dialog from '@/components/Dialog'
|
import Dialog from '@/components/Dialog'
|
||||||
|
import ImportTable from '@/components/ListTable/TableAction/ImportTable'
|
||||||
|
import { getErrorResponseMsg } from '@/utils/common'
|
||||||
import { createSourceIdCache } from '@/api/common'
|
import { createSourceIdCache } from '@/api/common'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ImportDialog',
|
name: 'ImportDialog',
|
||||||
components: {
|
components: {
|
||||||
Dialog
|
Dialog,
|
||||||
|
ImportTable
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
selectedRows: {
|
selectedRows: {
|
||||||
@ -73,45 +83,49 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
showImportDialog: false,
|
showImportDialog: false,
|
||||||
importOption: '1',
|
importOption: 'create',
|
||||||
isCsv: true,
|
|
||||||
errorMsg: '',
|
errorMsg: '',
|
||||||
loadStatus: false,
|
loadStatus: false,
|
||||||
importTypeOption: 'csv'
|
importTypeOption: 'csv',
|
||||||
|
importTypeIsCsv: true,
|
||||||
|
showTable: false,
|
||||||
|
renderError: '',
|
||||||
|
hasFileFormatOrSizeError: false,
|
||||||
|
jsonData: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
hasSelected() {
|
hasSelected() {
|
||||||
return this.selectedRows.length > 0
|
return this.selectedRows.length > 0
|
||||||
},
|
},
|
||||||
importTypeOptions() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: 'CSV',
|
|
||||||
value: 'csv',
|
|
||||||
can: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Excel',
|
|
||||||
value: 'xlsx',
|
|
||||||
can: true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
upLoadUrl() {
|
|
||||||
return this.url
|
|
||||||
},
|
|
||||||
downloadImportTempUrl() {
|
|
||||||
const format = this.importTypeOption === 'csv' ? 'format=csv&template=import&limit=1' : 'format=xlsx&template=import&limit=1'
|
|
||||||
const url = (this.url.indexOf('?') === -1) ? `${this.url}?${format}` : `${this.url}&${format}`
|
|
||||||
return url
|
|
||||||
},
|
|
||||||
uploadHelpTextClass() {
|
uploadHelpTextClass() {
|
||||||
const cls = ['el-upload__tip']
|
const cls = ['el-upload__tip']
|
||||||
if (!this.isCsv) {
|
if (!this.isCsv) {
|
||||||
cls.push('error-msg')
|
cls.push('error-msg')
|
||||||
}
|
}
|
||||||
return cls
|
return cls
|
||||||
|
},
|
||||||
|
downloadTemplateTitle() {
|
||||||
|
if (this.importOption === 'create') {
|
||||||
|
return this.$t('common.imExport.downloadImportTemplateMsg')
|
||||||
|
} else {
|
||||||
|
return this.$t('common.imExport.downloadUpdateTemplateMsg')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
importTitle() {
|
||||||
|
if (this.importOption === 'create') {
|
||||||
|
return this.$t('common.Import') + this.$t('common.Create')
|
||||||
|
} else {
|
||||||
|
return this.$t('common.Import') + this.$t('common.Update')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmTitle() {
|
||||||
|
return '导入'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
importOption(val) {
|
||||||
|
this.showTable = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@ -120,56 +134,67 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
performUpdate(item) {
|
closeDialog() {
|
||||||
this.$axios.put(
|
this.showImportDialog = false
|
||||||
this.upLoadUrl,
|
},
|
||||||
item.file,
|
cancelUpload() {
|
||||||
{ headers: { 'Content-Type': this.importTypeOption === 'csv' ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
|
this.showTable = false
|
||||||
).then((data) => {
|
this.renderError = ''
|
||||||
const msg = this.$t('common.imExport.updateSuccessMsg', { count: data.length })
|
this.jsonData = {}
|
||||||
this.onSuccess(msg)
|
},
|
||||||
|
onFileChange(file, fileList) {
|
||||||
|
fileList.splice(0, fileList.length)
|
||||||
|
if (file.status !== 'ready') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// const isCsv = file.raw.type = 'text/csv'
|
||||||
|
if (!this.beforeUpload(file)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const isCsv = file.name.indexOf('csv') > -1
|
||||||
|
const renderToJsonUrl = this.url + 'render-to-json/'
|
||||||
|
this.$axios.post(
|
||||||
|
renderToJsonUrl,
|
||||||
|
file.raw,
|
||||||
|
{ headers: { 'Content-Type': isCsv ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
|
||||||
|
).then(data => {
|
||||||
|
this.jsonData = data
|
||||||
|
this.showTable = true
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
this.catchError(error)
|
fileList.splice(0, fileList.length)
|
||||||
|
this.renderError = getErrorResponseMsg(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.loadStatus = false
|
this.loadStatus = false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
performCreate(item) {
|
beforeUpload(file) {
|
||||||
this.$axios.post(
|
const isLt30M = file.size / 1024 / 1024 < 30
|
||||||
this.upLoadUrl,
|
if (!isLt30M) {
|
||||||
item.file,
|
this.hasFileFormatOrSizeError = true
|
||||||
{ headers: { 'Content-Type': this.importTypeOption === 'csv' ? 'text/csv' : 'text/xlsx' }, disableFlashErrorMsg: true }
|
}
|
||||||
).then((data) => {
|
return isLt30M
|
||||||
const msg = this.$t('common.imExport.createSuccessMsg', { count: data.length })
|
},
|
||||||
this.onSuccess(msg)
|
async downloadTemplateFile(tp) {
|
||||||
}).catch(error => {
|
const downloadUrl = await this.getDownloadTemplateUrl(tp)
|
||||||
this.catchError(error)
|
window.open(downloadUrl)
|
||||||
}).finally(() => {
|
},
|
||||||
this.loadStatus = false
|
async getDownloadTemplateUrl(tp) {
|
||||||
})
|
const template = this.importOption === 'create' ? 'import' : 'update'
|
||||||
|
let query = `format=${tp}&template=${template}`
|
||||||
|
if (this.importOption === 'update' && this.selectedRows.length > 0) {
|
||||||
|
const resources = []
|
||||||
|
for (const item of this.selectedRows) {
|
||||||
|
resources.push(item.id)
|
||||||
|
}
|
||||||
|
const resp = await createSourceIdCache(resources)
|
||||||
|
query += `&spm=${resp.spm}`
|
||||||
|
} else {
|
||||||
|
query += '&limit=1'
|
||||||
|
}
|
||||||
|
return this.url.indexOf('?') === -1 ? `${this.url}?${query}` : `${this.url}&${query}`
|
||||||
},
|
},
|
||||||
catchError(error) {
|
catchError(error) {
|
||||||
this.$refs.upload.clearFiles()
|
console.log(error)
|
||||||
if (error.response && error.response.status === 400) {
|
|
||||||
const errorData = error.response.data
|
|
||||||
const totalErrorMsg = []
|
|
||||||
errorData.forEach((value, index) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
totalErrorMsg.push(`line ${index}. ${value}`)
|
|
||||||
} else {
|
|
||||||
const errorMsg = [`line ${index}. `]
|
|
||||||
for (const [k, v] of Object.entries(value)) {
|
|
||||||
if (v) {
|
|
||||||
errorMsg.push(`${k}: ${v}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (errorMsg.length > 1) {
|
|
||||||
totalErrorMsg.push(errorMsg.join(' '))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.errorMsg = totalErrorMsg
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSuccess(msg) {
|
onSuccess(msg) {
|
||||||
this.errorMsg = ''
|
this.errorMsg = ''
|
||||||
@ -181,40 +206,14 @@ export default {
|
|||||||
a.click()
|
a.click()
|
||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
},
|
},
|
||||||
handleImport(item) {
|
|
||||||
this.loadStatus = true
|
|
||||||
if (this.importOption === '1') {
|
|
||||||
this.performCreate(item)
|
|
||||||
} else {
|
|
||||||
this.performUpdate(item)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async downloadUpdateTempUrl() {
|
|
||||||
var resources = []
|
|
||||||
const data = this.selectedRows
|
|
||||||
for (let index = 0; index < data.length; index++) {
|
|
||||||
resources.push(data[index].id)
|
|
||||||
}
|
|
||||||
const spm = await createSourceIdCache(resources)
|
|
||||||
const baseUrl = (process.env.VUE_APP_ENV === 'production') ? (`${this.url}`) : (`${process.env.VUE_APP_BASE_API}${this.url}`)
|
|
||||||
const format = this.importTypeOption === 'csv' ? '?format=csv&template=update&spm=' : '?format=xlsx&template=update&spm='
|
|
||||||
const url = `${baseUrl}${format}` + spm.spm
|
|
||||||
return this.downloadCsv(url)
|
|
||||||
},
|
|
||||||
async handleImportConfirm() {
|
async handleImportConfirm() {
|
||||||
this.$refs.upload.submit()
|
this.$refs['importTable'].performUpload()
|
||||||
},
|
},
|
||||||
handleImportCancel() {
|
handleImportCancel() {
|
||||||
this.showImportDialog = false
|
this.showImportDialog = false
|
||||||
},
|
this.showTable = false
|
||||||
beforeUpload(file) {
|
this.renderError = ''
|
||||||
this.isCsv = this.importTypeOption === 'csv' ? _.endsWith(file.name, 'csv') : _.endsWith(file.name, 'xlsx')
|
this.jsonData = {}
|
||||||
if (!this.isCsv) {
|
|
||||||
this.$message.error(
|
|
||||||
this.$t('common.NeedSpecifiedFile')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return this.isCsv
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,4 +230,49 @@ export default {
|
|||||||
overflow: auto
|
overflow: auto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.importDialog >>> .el-form-item.file-uploader {
|
||||||
|
padding-right: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-uploader >>> .el-upload {
|
||||||
|
width: 100%;
|
||||||
|
//padding-right: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-uploader >>> .el-upload-dragger {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.importTableZone {
|
||||||
|
padding: 0 20px;
|
||||||
|
|
||||||
|
.importTable {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tableFilter {
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.importTable >>> .el-dialog__body {
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-item {
|
||||||
|
margin-left: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.export-item:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hasError {
|
||||||
|
color: $--color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-upload__tip {
|
||||||
|
line-height: 1.5;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
385
src/components/ListTable/TableAction/ImportTable.vue
Normal file
385
src/components/ListTable/TableAction/ImportTable.vue
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="8">
|
||||||
|
<div class="tableFilter">
|
||||||
|
<el-radio-group v-model="importStatusFilter" size="small">
|
||||||
|
<el-radio-button label="all">{{ $t('common.Total') }}</el-radio-button>
|
||||||
|
<el-radio-button label="ok">{{ $t('common.Success') }}</el-radio-button>
|
||||||
|
<el-radio-button label="error">{{ $t('common.Failed') }}</el-radio-button>
|
||||||
|
<el-radio-button label="pending">{{ $t('common.Pending') }}</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8" style="text-align: center">
|
||||||
|
<span class="summary-item summary-total"> {{ $t('common.Total') }}: {{ totalCount }}</span>
|
||||||
|
<span class="summary-item summary-success"> {{ $t('common.Success') }}: {{ successCount }}</span>
|
||||||
|
<span class="summary-item summary-failed"> {{ $t('common.Failed') }}: {{ failedCount }}</span>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<div class="row">
|
||||||
|
<el-progress :percentage="processedPercent" />
|
||||||
|
</div>
|
||||||
|
<DataTable v-if="tableGenDone" id="importTable" :config="tableConfig" class="importTable" />
|
||||||
|
<div class="row" style="padding-top: 20px">
|
||||||
|
<div style="float: right">
|
||||||
|
<el-button size="small" @click="performCancel">{{ $t('common.Cancel') }}</el-button>
|
||||||
|
<el-button size="small" type="primary" @click="performImportAction">{{ importActionTitle }}</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DataTable from '@/components/DataTable'
|
||||||
|
import { sleep } from '@/utils/common'
|
||||||
|
import { EditableInputFormatter, StatusFormatter } from '@/components/ListTable/formatters'
|
||||||
|
export default {
|
||||||
|
name: 'ImportTable',
|
||||||
|
components: {
|
||||||
|
DataTable
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
jsonData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
importOption: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
columns: [],
|
||||||
|
importStatusFilter: 'all',
|
||||||
|
iTotalData: [],
|
||||||
|
tableConfig: {
|
||||||
|
hasSelection: false,
|
||||||
|
hasPagination: false,
|
||||||
|
columns: [],
|
||||||
|
totalData: [],
|
||||||
|
tableAttrs: {
|
||||||
|
stripe: true, // 斑马纹表格
|
||||||
|
border: true, // 表格边框
|
||||||
|
fit: true, // 宽度自适应,
|
||||||
|
tooltipEffect: 'dark',
|
||||||
|
height: '60vh'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tableGenDone: false,
|
||||||
|
importTaskStatus: 'pending', // pending, started, stopped, done
|
||||||
|
importTaskResult: '', // success, hasError
|
||||||
|
hasImport: false,
|
||||||
|
hasContinueButton: false,
|
||||||
|
importActions: {
|
||||||
|
import: this.$t('common.Import'),
|
||||||
|
continue: this.$t('common.Continue'),
|
||||||
|
stop: this.$t('common.Stop'),
|
||||||
|
finished: this.$t('common.Finished')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tableColumnNameMapper() {
|
||||||
|
const mapper = {}
|
||||||
|
for (const column of this.tableConfig.columns) {
|
||||||
|
mapper[column['prop']] = column['label']
|
||||||
|
}
|
||||||
|
return mapper
|
||||||
|
},
|
||||||
|
importAction() {
|
||||||
|
switch (this.importTaskStatus) {
|
||||||
|
case 'pending':
|
||||||
|
return 'import'
|
||||||
|
case 'started':
|
||||||
|
return 'stop'
|
||||||
|
}
|
||||||
|
if (this.totalCount === this.successCount) {
|
||||||
|
return 'finished'
|
||||||
|
} else {
|
||||||
|
return 'continue'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
importActionTitle() {
|
||||||
|
return this.importActions[this.importAction]
|
||||||
|
},
|
||||||
|
successData() {
|
||||||
|
return this.iTotalData.filter((item) => {
|
||||||
|
return item['@status'] === 'ok'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
failedData() {
|
||||||
|
return this.iTotalData.filter((item) => {
|
||||||
|
return typeof item['@status'] === 'object' && item['@status'].name === 'error'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
pendingData() {
|
||||||
|
return this.iTotalData.filter((item) => {
|
||||||
|
return item['@status'] === 'pending'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
totalCount() {
|
||||||
|
return this.iTotalData.length
|
||||||
|
},
|
||||||
|
successCount() {
|
||||||
|
return this.successData.length
|
||||||
|
},
|
||||||
|
failedCount() {
|
||||||
|
return this.failedData.length
|
||||||
|
},
|
||||||
|
pendingCount() {
|
||||||
|
return this.pendingData.length
|
||||||
|
},
|
||||||
|
processedCount() {
|
||||||
|
return this.totalCount - this.pendingCount
|
||||||
|
},
|
||||||
|
processedPercent() {
|
||||||
|
if (this.totalCount === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return Math.round(this.processedCount / this.totalCount * 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
importStatusFilter(val) {
|
||||||
|
if (val === 'all') {
|
||||||
|
this.tableConfig.totalData = this.iTotalData
|
||||||
|
} else if (val === 'error') {
|
||||||
|
this.tableConfig.totalData = this.iTotalData.filter((item) => {
|
||||||
|
return item['@status'].name === 'error'
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
this.tableConfig.totalData = this.iTotalData.filter((item) => {
|
||||||
|
return item['@status'] === val
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.generateTable()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
generateTableColumns(tableTitles, tableData) {
|
||||||
|
const vm = this
|
||||||
|
const columns = [{
|
||||||
|
prop: '@status',
|
||||||
|
label: vm.$t('common.Status'),
|
||||||
|
width: '80px',
|
||||||
|
align: 'center',
|
||||||
|
formatter: StatusFormatter,
|
||||||
|
formatterArgs: {
|
||||||
|
iconChoices: {
|
||||||
|
ok: 'fa-check text-primary',
|
||||||
|
error: 'fa-times text-danger',
|
||||||
|
pending: 'fa-clock-o'
|
||||||
|
},
|
||||||
|
getChoicesKey(val) {
|
||||||
|
if (val === 'ok' || val === 'pending') {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return 'error'
|
||||||
|
},
|
||||||
|
getTip(val, col) {
|
||||||
|
if (val === 'ok') {
|
||||||
|
return vm.$t('common.Success')
|
||||||
|
} else if (val === 'pending') {
|
||||||
|
return vm.$t('common.Pending')
|
||||||
|
} else if (val && val.name === 'error') {
|
||||||
|
return val.error
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
|
hasTips: true
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
for (const item of tableTitles) {
|
||||||
|
const dataItemLens = tableData.map(d => {
|
||||||
|
const prop = item[1]
|
||||||
|
const itemColData = d[prop]
|
||||||
|
if (!d) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (!itemColData || !itemColData.length) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return itemColData.length
|
||||||
|
})
|
||||||
|
let colMaxWidth = Math.max(...dataItemLens) * 10
|
||||||
|
if (colMaxWidth === 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
colMaxWidth = Math.min(180, colMaxWidth)
|
||||||
|
colMaxWidth = Math.max(colMaxWidth, 100)
|
||||||
|
columns.push({
|
||||||
|
prop: item[1],
|
||||||
|
label: item[0],
|
||||||
|
minWidth: colMaxWidth + 'px',
|
||||||
|
showOverflowTooltip: true,
|
||||||
|
formatter: EditableInputFormatter
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return columns
|
||||||
|
},
|
||||||
|
generateTableData(tableTitles, tableData) {
|
||||||
|
const totalData = []
|
||||||
|
tableData.forEach((item, index) => {
|
||||||
|
if (!item.id) {
|
||||||
|
item.id = index
|
||||||
|
}
|
||||||
|
this.$set(item, '@status', 'pending')
|
||||||
|
totalData.push(item)
|
||||||
|
})
|
||||||
|
return totalData
|
||||||
|
},
|
||||||
|
generateTable() {
|
||||||
|
const tableTitles = this.jsonData['title']
|
||||||
|
const tableData = this.jsonData['data']
|
||||||
|
const columns = this.generateTableColumns(tableTitles, tableData)
|
||||||
|
const totalData = this.generateTableData(tableTitles, tableData)
|
||||||
|
this.tableConfig.columns = columns
|
||||||
|
this.tableGenDone = true
|
||||||
|
setTimeout(() => {
|
||||||
|
this.iTotalData = totalData
|
||||||
|
this.tableConfig.totalData = totalData
|
||||||
|
}, 200)
|
||||||
|
},
|
||||||
|
beautifyErrorData(errorData) {
|
||||||
|
if (typeof errorData === 'string') {
|
||||||
|
return errorData
|
||||||
|
} else if (Array.isArray(errorData)) {
|
||||||
|
return errorData
|
||||||
|
} else if (typeof errorData !== 'object') {
|
||||||
|
return errorData
|
||||||
|
}
|
||||||
|
const data = []
|
||||||
|
// eslint-disable-next-line prefer-const
|
||||||
|
for (let [key, value] of Object.entries(errorData)) {
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
value = this.beautifyErrorData(value)
|
||||||
|
}
|
||||||
|
let label = this.tableColumnNameMapper[key]
|
||||||
|
if (!label) {
|
||||||
|
label = key
|
||||||
|
}
|
||||||
|
data.push(`${label}: ${value}`)
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
},
|
||||||
|
performCancel() {
|
||||||
|
this.$emit('cancel')
|
||||||
|
},
|
||||||
|
performFinish() {
|
||||||
|
this.$emit('finish')
|
||||||
|
},
|
||||||
|
performImportAction() {
|
||||||
|
switch (this.importAction) {
|
||||||
|
case 'continue':
|
||||||
|
return this.performContinue()
|
||||||
|
case 'import':
|
||||||
|
return this.performUpload()
|
||||||
|
case 'stop':
|
||||||
|
return this.performStop()
|
||||||
|
case 'finished':
|
||||||
|
return this.performFinish()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
performContinue() {
|
||||||
|
for (const item of this.failedData) {
|
||||||
|
item['@status'] = 'pending'
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
this.performUpload()
|
||||||
|
}, 100)
|
||||||
|
},
|
||||||
|
performStop() {
|
||||||
|
this.importTaskStatus = 'stopped'
|
||||||
|
},
|
||||||
|
async performUpload() {
|
||||||
|
this.importTaskStatus = 'started'
|
||||||
|
for (const item of this.pendingData) {
|
||||||
|
if (this.importTaskStatus === 'stopped') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await this.performUploadObject(item)
|
||||||
|
await sleep(100)
|
||||||
|
}
|
||||||
|
this.importTaskStatus = 'done'
|
||||||
|
if (this.failedCount > 0) {
|
||||||
|
this.$message.error(this.$t('common.imExport.hasImportErrorItemMsg') + '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async performUpdateObject(item) {
|
||||||
|
const updateUrl = `${this.url}${item.id}/`
|
||||||
|
return this.$axios.put(
|
||||||
|
updateUrl,
|
||||||
|
item,
|
||||||
|
{ disableFlashErrorMsg: true }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
async performUploadObject(item) {
|
||||||
|
let handler = this.performCreateObject
|
||||||
|
if (this.importOption === 'update') {
|
||||||
|
handler = this.performUpdateObject
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await handler.bind(this)(item)
|
||||||
|
item['@status'] = 'ok'
|
||||||
|
} catch (error) {
|
||||||
|
const errorData = error?.response?.data
|
||||||
|
const _error = this.beautifyErrorData(errorData)
|
||||||
|
item['@status'] = {
|
||||||
|
name: 'error',
|
||||||
|
error: _error
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
const tableRef = document.getElementById('importTable')
|
||||||
|
const pendingRef = tableRef?.getElementsByClassName('pendingStatus')[0]
|
||||||
|
if (pendingRef) {
|
||||||
|
const parentTdRef = pendingRef.parentElement.parentElement.parentElement.parentElement
|
||||||
|
if (!this.isElementInViewport(parentTdRef)) {
|
||||||
|
parentTdRef.scrollIntoView({ behavior: 'auto', block: 'start', inline: 'start' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async performCreateObject(item) {
|
||||||
|
return this.$axios.post(
|
||||||
|
this.url,
|
||||||
|
item,
|
||||||
|
{ disableFlashErrorMsg: true }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
isElementInViewport(el) {
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
let windowInnerHeight = window.innerHeight || document.documentElement.clientHeight
|
||||||
|
windowInnerHeight = windowInnerHeight * 0.97 - 150
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.left >= 0 &&
|
||||||
|
rect.bottom <= windowInnerHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "~@/styles/element-variables.scss";
|
||||||
|
.summary-item {
|
||||||
|
padding: 0 10px
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-success {
|
||||||
|
color: $--color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-failed {
|
||||||
|
color: $--color-danger;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div @click.stop="editCell">
|
||||||
|
<el-input
|
||||||
|
v-if="inEditMode"
|
||||||
|
v-model="value"
|
||||||
|
size="mini"
|
||||||
|
class="editInput"
|
||||||
|
@keyup.enter.native="onInputEnter"
|
||||||
|
@blur="onInputEnter"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
<span>{{ cellValue }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseFormatter from './base'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'EditableInputFormatter',
|
||||||
|
components: {
|
||||||
|
},
|
||||||
|
extends: BaseFormatter,
|
||||||
|
props: {
|
||||||
|
formatterArgsDefault: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {
|
||||||
|
trigger: 'click',
|
||||||
|
onEnter: ({ row, col, oldValue, newValue }) => {
|
||||||
|
const prop = col.prop
|
||||||
|
row[prop] = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const valueIsString = typeof this.cellValue === 'string'
|
||||||
|
const jsonValue = JSON.stringify(this.cellValue)
|
||||||
|
return {
|
||||||
|
inEditMode: false,
|
||||||
|
value: valueIsString ? this.cellValue : jsonValue,
|
||||||
|
valueIsString: valueIsString,
|
||||||
|
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editCell() {
|
||||||
|
this.inEditMode = true
|
||||||
|
},
|
||||||
|
onInputEnter() {
|
||||||
|
let validValue = ''
|
||||||
|
if (this.valueIsString) {
|
||||||
|
validValue = this.value
|
||||||
|
} else {
|
||||||
|
validValue = JSON.parse(validValue)
|
||||||
|
}
|
||||||
|
this.formatterArgs.onEnter({
|
||||||
|
row: this.row, col: this.col,
|
||||||
|
oldValue: this.cellValue,
|
||||||
|
newValue: validValue
|
||||||
|
})
|
||||||
|
this.inEditMode = false
|
||||||
|
},
|
||||||
|
cancelEdit() {
|
||||||
|
this.inEditMode = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.editInput >>> .el-input__inner {
|
||||||
|
padding: 2px;
|
||||||
|
line-height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editInput {
|
||||||
|
padding: -6px;
|
||||||
|
}
|
||||||
|
</style>
|
68
src/components/ListTable/formatters/StatusFormatter.vue
Normal file
68
src/components/ListTable/formatters/StatusFormatter.vue
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<el-tooltip v-if="formatterArgs.hasTips" placement="bottom" effect="dark">
|
||||||
|
<div slot="content">
|
||||||
|
<template v-if="tipsIsArray">
|
||||||
|
<div v-for="tip of tips" :key="tip">
|
||||||
|
<span>{{ tip }}</span>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<span v-else>
|
||||||
|
{{ tips }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<i :class="'fa ' + iconClass" />
|
||||||
|
</el-tooltip>
|
||||||
|
<i v-else :class="'fa ' + iconClass" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BaseFormatter from './base'
|
||||||
|
export default {
|
||||||
|
name: 'StatusFormatter',
|
||||||
|
extends: BaseFormatter,
|
||||||
|
props: {
|
||||||
|
formatterArgsDefault: {
|
||||||
|
type: Object,
|
||||||
|
default() {
|
||||||
|
return {
|
||||||
|
iconChoices: {
|
||||||
|
true: 'fa-check text-primary',
|
||||||
|
false: 'fa-times text-danger'
|
||||||
|
},
|
||||||
|
getChoicesKey(val) {
|
||||||
|
return !!val
|
||||||
|
},
|
||||||
|
getTip(val, col) {
|
||||||
|
},
|
||||||
|
hasTips: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
formatterArgs: Object.assign(this.formatterArgsDefault, this.col.formatterArgs)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
iconClass() {
|
||||||
|
const key = this.formatterArgs.getChoicesKey(this.cellValue)
|
||||||
|
return this.formatterArgs.iconChoices[key] + ' ' + key + 'Status'
|
||||||
|
},
|
||||||
|
tips() {
|
||||||
|
const vm = this
|
||||||
|
return this.formatterArgs.getTip(this.cellValue, vm)
|
||||||
|
},
|
||||||
|
tipsIsArray() {
|
||||||
|
return Array.isArray(this.tips)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -10,6 +10,8 @@ import SystemUserFormatter from './GrantedSystemUsersShowFormatter'
|
|||||||
import ShowKeyFormatter from '@/components/ListTable/formatters/ShowKeyFormatter'
|
import ShowKeyFormatter from '@/components/ListTable/formatters/ShowKeyFormatter'
|
||||||
import DialogDetailFormatter from './DialogDetailFormatter'
|
import DialogDetailFormatter from './DialogDetailFormatter'
|
||||||
import LoadingActionsFormatter from './LoadingActionsFormatter'
|
import LoadingActionsFormatter from './LoadingActionsFormatter'
|
||||||
|
import EditableInputFormatter from './EditableInputFormatter'
|
||||||
|
import StatusFormatter from './StatusFormatter'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
DetailFormatter,
|
DetailFormatter,
|
||||||
@ -23,7 +25,9 @@ export default {
|
|||||||
ShowKeyFormatter,
|
ShowKeyFormatter,
|
||||||
DialogDetailFormatter,
|
DialogDetailFormatter,
|
||||||
LoadingActionsFormatter,
|
LoadingActionsFormatter,
|
||||||
ArrayFormatter
|
ArrayFormatter,
|
||||||
|
EditableInputFormatter,
|
||||||
|
StatusFormatter
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -38,5 +42,7 @@ export {
|
|||||||
ShowKeyFormatter,
|
ShowKeyFormatter,
|
||||||
DialogDetailFormatter,
|
DialogDetailFormatter,
|
||||||
LoadingActionsFormatter,
|
LoadingActionsFormatter,
|
||||||
ArrayFormatter
|
ArrayFormatter,
|
||||||
|
EditableInputFormatter,
|
||||||
|
StatusFormatter
|
||||||
}
|
}
|
||||||
|
@ -45,17 +45,6 @@ export default {
|
|||||||
dataTable() {
|
dataTable() {
|
||||||
return this.$refs.dataTable.$refs.dataTable
|
return this.$refs.dataTable.$refs.dataTable
|
||||||
},
|
},
|
||||||
// hasCreateAction() {
|
|
||||||
// const hasLeftAction = this.headerActions.hasLeftActions
|
|
||||||
// if (hasLeftAction === false) {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// const hasCreate = this.headerActions.hasCreate
|
|
||||||
// if (hasCreate === false) {
|
|
||||||
// return false
|
|
||||||
// }
|
|
||||||
// return true
|
|
||||||
// },
|
|
||||||
iTableConfig() {
|
iTableConfig() {
|
||||||
const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery })
|
const config = deepmerge(this.tableConfig, { extraQuery: this.extraQuery })
|
||||||
this.$log.debug('Header actions', this.headerActions)
|
this.$log.debug('Header actions', this.headerActions)
|
||||||
|
@ -258,6 +258,10 @@
|
|||||||
"EnterForSearch": "按回车进行搜索",
|
"EnterForSearch": "按回车进行搜索",
|
||||||
"Export": "导出",
|
"Export": "导出",
|
||||||
"Import": "导入",
|
"Import": "导入",
|
||||||
|
"ContinueImport": "继续导入",
|
||||||
|
"Continue": "继续",
|
||||||
|
"Stop": "停止",
|
||||||
|
"Finished": "完成",
|
||||||
"Refresh": "刷新",
|
"Refresh": "刷新",
|
||||||
"Info": "提示",
|
"Info": "提示",
|
||||||
"MFAConfirm": "MFA 认证",
|
"MFAConfirm": "MFA 认证",
|
||||||
@ -320,6 +324,11 @@
|
|||||||
"fieldRequiredError": "这个字段是必填项",
|
"fieldRequiredError": "这个字段是必填项",
|
||||||
"getErrorMsg": "获取失败",
|
"getErrorMsg": "获取失败",
|
||||||
"MFAErrorMsg": "MFA错误,请检查",
|
"MFAErrorMsg": "MFA错误,请检查",
|
||||||
|
"Total": "总共",
|
||||||
|
"Success": "成功",
|
||||||
|
"Failed": "失败",
|
||||||
|
"Pending": "等待",
|
||||||
|
"Status": "状态",
|
||||||
"InputEmailAddress": "请输入正确的邮箱地址",
|
"InputEmailAddress": "请输入正确的邮箱地址",
|
||||||
"imExport": {
|
"imExport": {
|
||||||
"ExportAll": "导出所有",
|
"ExportAll": "导出所有",
|
||||||
@ -327,10 +336,13 @@
|
|||||||
"ExportOnlySelectedItems": "仅导出选择项",
|
"ExportOnlySelectedItems": "仅导出选择项",
|
||||||
"ExportRange": "导出范围",
|
"ExportRange": "导出范围",
|
||||||
"createSuccessMsg": "导入创建成功,总共:{count}",
|
"createSuccessMsg": "导入创建成功,总共:{count}",
|
||||||
"downloadImportTemplateMsg": "下载导入模板",
|
"downloadImportTemplateMsg": "下载创建模板",
|
||||||
"downloadUpdateTemplateMsg": "下载更新模板",
|
"downloadUpdateTemplateMsg": "下载更新模板",
|
||||||
"onlyCSVFilesTips": "仅支持csv文件导入",
|
"onlyCSVFilesTips": "仅支持csv文件导入",
|
||||||
"updateSuccessMsg": "导入更新成功,总共:{count}"
|
"updateSuccessMsg": "导入更新成功,总共:{count}",
|
||||||
|
"uploadCsvLth10MHelpText": "只能上传 csv/xlsx, 且不超过 10m",
|
||||||
|
"dragUploadFileInfo": "将文件拖到此处,或点击上传",
|
||||||
|
"hasImportErrorItemMsg": "存在导入失败项,点击表格编辑后,可以继续导入失败项"
|
||||||
},
|
},
|
||||||
"fileType": "文件类型",
|
"fileType": "文件类型",
|
||||||
"isValid": "有效",
|
"isValid": "有效",
|
||||||
|
@ -257,6 +257,10 @@
|
|||||||
"EnterForSearch": "Press enter to search",
|
"EnterForSearch": "Press enter to search",
|
||||||
"Export": "Export",
|
"Export": "Export",
|
||||||
"Import": "Import",
|
"Import": "Import",
|
||||||
|
"ContinueImport": "ContinueImport",
|
||||||
|
"Continue": "Continue",
|
||||||
|
"Stop": "Stop",
|
||||||
|
"Finished": "Finished",
|
||||||
"Refresh": "Refresh",
|
"Refresh": "Refresh",
|
||||||
"Info": "Info",
|
"Info": "Info",
|
||||||
"MFAConfirm": "MFA Confirm",
|
"MFAConfirm": "MFA Confirm",
|
||||||
@ -319,6 +323,11 @@
|
|||||||
"fieldRequiredError": "This field is required",
|
"fieldRequiredError": "This field is required",
|
||||||
"getErrorMsg": "Get failed",
|
"getErrorMsg": "Get failed",
|
||||||
"fileType": "File type",
|
"fileType": "File type",
|
||||||
|
"Status": "Status",
|
||||||
|
"Total": "Total",
|
||||||
|
"Success": "Success",
|
||||||
|
"Failed": "Failed",
|
||||||
|
"Pending": "Pending",
|
||||||
"imExport": {
|
"imExport": {
|
||||||
"ExportAll": "Export all",
|
"ExportAll": "Export all",
|
||||||
"ExportOnlyFiltered": "Export only filtered",
|
"ExportOnlyFiltered": "Export only filtered",
|
||||||
@ -328,7 +337,10 @@
|
|||||||
"downloadImportTemplateMsg": "Download import template",
|
"downloadImportTemplateMsg": "Download import template",
|
||||||
"downloadUpdateTemplateMsg": "Download update template",
|
"downloadUpdateTemplateMsg": "Download update template",
|
||||||
"onlyCSVFilesTips": "Only csv supported",
|
"onlyCSVFilesTips": "Only csv supported",
|
||||||
"updateSuccessMsg": "Update success, total: {count}"
|
"updateSuccessMsg": "Update success, total: {count}",
|
||||||
|
"dragUploadFileInfo": "Drag file here or click to upload",
|
||||||
|
"uploadCsvLth10MHelpText": "csv/xlsx files with a size less than 10m",
|
||||||
|
"hasImportErrorItemMsg": "Has import failed item, click to edit it and continue upload"
|
||||||
},
|
},
|
||||||
"isValid": "Is valid",
|
"isValid": "Is valid",
|
||||||
"nav": {
|
"nav": {
|
||||||
|
@ -187,6 +187,19 @@ export function getDayFuture(days, now) {
|
|||||||
return new Date(now.getTime() + 3600 * 1000 * 24 * days)
|
return new Date(now.getTime() + 3600 * 1000 * 24 * days)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getErrorResponseMsg(error) {
|
||||||
|
let msg = ''
|
||||||
|
const data = error.response && error.response.data || {}
|
||||||
|
if (data && (data.error || data.msg || data.detail)) {
|
||||||
|
msg = data.error || data.msg || data.detail
|
||||||
|
}
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sleep(time) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, time))
|
||||||
|
}
|
||||||
|
|
||||||
const scheme = document.location.protocol
|
const scheme = document.location.protocol
|
||||||
const port = document.location.port ? ':' + document.location.port : ''
|
const port = document.location.port ? ':' + document.location.port : ''
|
||||||
const BASE_URL = scheme + '//' + document.location.hostname + port
|
const BASE_URL = scheme + '//' + document.location.hostname + port
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import i18n from '@/i18n/i18n'
|
import i18n from '@/i18n/i18n'
|
||||||
import { getTokenFromCookie } from '@/utils/auth'
|
import { getTokenFromCookie } from '@/utils/auth'
|
||||||
|
import { getErrorResponseMsg } from '@/utils/common'
|
||||||
import { refreshSessionIdAge } from '@/api/users'
|
import { refreshSessionIdAge } from '@/api/users'
|
||||||
import { Message, MessageBox } from 'element-ui'
|
import { Message, MessageBox } from 'element-ui'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
@ -84,11 +85,8 @@ function ifBadRequest({ response, error }) {
|
|||||||
|
|
||||||
export function flashErrorMsg({ response, error }) {
|
export function flashErrorMsg({ response, error }) {
|
||||||
if (!response.config.disableFlashErrorMsg) {
|
if (!response.config.disableFlashErrorMsg) {
|
||||||
let msg = error.message
|
const responseErrorMsg = getErrorResponseMsg(error)
|
||||||
const data = response.data
|
const msg = responseErrorMsg || error.message
|
||||||
if (data && (data.error || data.msg || data.detail)) {
|
|
||||||
msg = data.error || data.msg || data.detail
|
|
||||||
}
|
|
||||||
Message({
|
Message({
|
||||||
message: msg,
|
message: msg,
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
Loading…
Reference in New Issue
Block a user