feat(数据集): 数据导出中心

This commit is contained in:
dataeaseShu 2024-05-20 12:06:30 +08:00
parent b31fcbbff6
commit 787a959c1b
5 changed files with 705 additions and 5 deletions

View File

@ -247,3 +247,39 @@ export const getFunction = async (): Promise<DatasetDetail[]> => {
return res?.data
})
}
export const exportTasks = async (type): Promise<IResponse> => {
return request.post({ url: '/exportCenter/exportTasks/' + type, data: {} }).then(res => {
return res?.data
})
}
export const exportRetry = async (id): Promise<IResponse> => {
return request.post({ url: '/exportCenter/retry/' + id, data: {} }).then(res => {
return res?.data
})
}
export const downloadFile = async (id): Promise<Blob> => {
return request.get({ url: 'exportCenter/download/' + id, responseType: 'blob' }).then(res => {
return res?.data
})
}
export const exportDelete = async (id): Promise<IResponse> => {
return request.get({ url: '/exportCenter/delete/' + id }).then(res => {
return res?.data
})
}
export const exportDeleteAll = async (type, data): Promise<IResponse> => {
return request.post({ url: '/exportCenter/deleteAll/' + type, data }).then(res => {
return res?.data
})
}
export const exportDeletePost = async (data): Promise<IResponse> => {
return request.post({ url: '/exportCenter/delete', data }).then(res => {
return res?.data
})
}

View File

@ -4,6 +4,7 @@ import { usePermissionStore } from '@/store/modules/permission'
import { isExternal } from '@/utils/validate'
import { formatRoute } from '@/router/establish'
import HeaderMenuItem from './HeaderMenuItem.vue'
import { useEmitt } from '@/hooks/web/useEmitt'
import { Icon } from '@/components/icon-custom'
import { ElHeader, ElMenu } from 'element-plus-secondary'
import SystemCfg from './SystemCfg.vue'
@ -15,8 +16,8 @@ import { isDesktop } from '@/utils/ModelUtil'
import { XpackComponent } from '@/components/plugin'
import { useAppearanceStoreWithOut } from '@/store/modules/appearance'
import AiComponent from '@/layout/components/AiComponent.vue'
import { useEmitt } from '@/hooks/web/useEmitt'
import { findBaseParams } from '@/api/aiComponent'
import ExportExcel from '@/views/visualized/data/dataset/ExportExcel.vue'
import AiTips from '@/layout/components/AiTips.vue'
const appearanceStore = useAppearanceStoreWithOut()
const { push } = useRouter()
@ -40,7 +41,10 @@ const activeIndex = computed(() => {
return route.path
})
const permissionStore = usePermissionStore()
const ExportExcelRef = ref()
const downloadClick = () => {
ExportExcelRef.value.init()
}
const routers: any[] = formatRoute(permissionStore.getRoutersNotHidden as AppCustomRouteRecordRaw[])
const showSystem = ref(false)
const showToolbox = ref(false)
@ -85,6 +89,10 @@ onMounted(() => {
initShowSystem()
initShowToolbox()
initAiBase()
useEmitt({
name: 'data-export-center',
callback: downloadClick
})
})
</script>
@ -112,6 +120,9 @@ onMounted(() => {
<el-icon style="margin: 0 10px" class="ai-icon" v-if="aiBaseUrl && !showOverlay">
<Icon name="dv-ai" @click="handleAiClick" />
</el-icon>
<el-icon style="margin: 0 10px">
<Icon name="dv-preview-download" @click="downloadClick" />
</el-icon>
<ai-tips @confirm="aiTipsConfirm" v-if="showOverlay" class="ai-icon-tips"></ai-tips>
<ToolboxCfg v-if="showToolbox" />
<TopDoc />
@ -121,6 +132,7 @@ onMounted(() => {
<div v-if="showOverlay" class="overlay"></div>
</div>
</el-header>
<ExportExcel ref="ExportExcelRef"></ExportExcel>
</template>
<style lang="less" scoped>

View File

@ -50,6 +50,37 @@ export default {
filter_condition: '筛选条件',
no_auth_tips: '缺少菜单权限请联系管理员'
},
data_export: {
export_center: '数据导出中心',
export_info: '查看进度进行下载',
exporting: '后台导出中,可前往',
del_all: '全部删除',
export_failed: '导出失败',
export_from: '导出来源',
export_obj: '导出对象',
export_time: '导出时间',
sure_del_all: '确定删除全部导出记录吗',
sure_del: '确定删除该导出记录吗',
no_failed_file: '暂无失败文件',
no_file: '暂无文件',
no_task: '暂无任务',
download_all: '下载全部',
download: '下载'
},
driver: {
driver: '驱动',
please_choose_driver: '请选择驱动',
mgm: '驱动管理',
exit_mgm: '退出驱动管理',
add: '添加驱动',
modify: '修改',
show_info: '驱动信息',
file_name: '文件名',
version: '版本',
please_set_driverClass: '请指定驱动类',
please_set_surpportVersions: '请输入支持的数据库大版本',
surpportVersions: '支持版本'
},
login: {
welcome: '欢迎使用',
btn: '登录',

View File

@ -392,7 +392,7 @@ em {
}
strong {
font-synthesis: style weight!important;
font-synthesis: style weight !important;
}
.ed-date-editor .ed-range__icon {
@ -400,12 +400,94 @@ strong {
}
.ed-picker__popper {
--ed-datepicker-border-color: #DEE0E3 !important;
--ed-datepicker-border-color: #dee0e3 !important;
}
.ed-dialog__headerbtn {
top: 21px !important;
display: flex;
align-items: center;
justify-content: center
justify-content: center;
}
.de-message-export {
min-width: 20px !important;
padding: 16px 20px !important;
display: flex;
align-items: center;
box-shadow: 0px 4px 8px 0px #1f23291a;
& > p {
font-family: AlibabaPuHuiTi;
font-size: 14px;
font-weight: 500;
line-height: 22px;
letter-spacing: 0px;
text-align: left;
color: #1f2329;
display: flex;
align-items: center;
padding-right: 20px;
}
.btn-text {
padding: 2px 4px;
&:hover {
background: var(--primary10, #3370ff1a);
}
}
.ed-message__closeBtn {
margin-left: 28px;
height: 16px;
width: 16px;
position: relative;
margin-right: 0;
top: 0;
right: 0;
transform: translateY(0);
color: #646a73;
}
.ed-message__icon {
height: 16px;
width: 16px;
margin-right: 8px;
}
}
.de-message-loading {
border: 1px solid var(--primary, #3370ff) !important;
background: #f0f4ff !important;
@keyframes circle {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.ed-message__icon {
color: var(--primary, #3370ff);
animation: circle infinite 0.75s linear;
}
}
.de-message-error {
border: 1px solid var(--deDanger, #f54a45) !important;
background: var(--deWhitemsgDeDanger, #fef1f1) !important;
.ed-message__icon {
color: var(--deDanger, #f54a45);
}
}
.de-message-success {
border: 1px solid var(--deSuccess, #34c724) !important;
background: var(--deWhitemsgDeSuccess, #f0fbef) !important;
.ed-message__icon {
color: var(--deSuccess, #34c724);
}
}

View File

@ -0,0 +1,539 @@
<script lang="ts" setup>
import { ref, h, onUnmounted } from 'vue'
import { EmptyBackground } from '@/components/empty-background'
import { ElButton, ElMessage, ElMessageBox } from 'element-plus-secondary'
import { RefreshLeft } from '@element-plus/icons-vue'
import {
exportTasks,
exportRetry,
downloadFile,
exportDelete,
exportDeleteAll,
exportDeletePost
} from '@/api/dataset'
import { useI18n } from '@/hooks/web/useI18n'
import { useEmitt } from '@/hooks/web/useEmitt'
import Icon from '@/components/icon-custom/src/Icon.vue'
const { t } = useI18n()
const tableData = ref([])
const drawerLoading = ref(false)
const drawer = ref(false)
const exportDatasetLoading = ref(false)
const activeName = ref('ALL')
const multipleSelection = ref([])
const description = ref('暂无任务')
const tabList = ref([
{
label: '导出中(0)',
name: 'IN_PROGRESS'
},
{
label: '成功(0)',
name: 'SUCCESS'
},
{
label: '失败(0)',
name: 'FAILED'
},
{
label: '等待中(0)',
name: 'PENDING'
},
{
label: '全部(0)',
name: 'ALL'
}
])
let timer
const handleClose = () => {
drawer.value = false
clearInterval(timer)
}
onUnmounted(() => {
clearInterval(timer)
})
const handleClick = () => {
if (activeName.value === 'ALL') {
description.value = t('data_export.no_file')
} else if (activeName.value === 'FAILED') {
description.value = t('data_export.no_failed_file')
} else {
description.value = t('data_export.no_task')
}
tableData.value = []
drawerLoading.value = true
exportTasks(activeName.value)
.then(res => {
tabList.value.forEach(item => {
if (item.name === 'ALL') {
item.label = '全部' + '(' + res.data.length + ')'
}
if (item.name === 'IN_PROGRESS') {
item.label =
'导出中' +
'(' +
res.data.filter(task => task.exportStatus === 'IN_PROGRESS').length +
')'
}
if (item.name === 'SUCCESS') {
item.label =
'成功' + '(' + res.data.filter(task => task.exportStatus === 'SUCCESS').length + ')'
}
if (item.name === 'FAILED') {
item.label =
'失败' + '(' + res.data.filter(task => task.exportStatus === 'FAILED').length + ')'
}
if (item.name === 'PENDING') {
item.label =
'等待中' + '(' + res.data.filter(task => task.exportStatus === 'PENDING').length + ')'
}
})
if (activeName.value === 'ALL') {
tableData.value = res.data
} else {
tableData.value = res.data.filter(task => task.exportStatus === activeName.value)
}
})
.finally(() => {
drawerLoading.value = false
})
}
const init = () => {
drawer.value = true
handleClick()
timer = setInterval(() => {
if (activeName.value === 'IN_PROGRESS') {
exportTasks(activeName.value).then(res => {
tabList.value.forEach(item => {
if (item.name === 'ALL') {
item.label = '全部' + '(' + res.data.length + ')'
}
if (item.name === 'IN_PROGRESS') {
item.label =
'导出中' +
'(' +
res.data.filter(task => task.exportStatus === 'IN_PROGRESS').length +
')'
}
if (item.name === 'SUCCESS') {
item.label =
'成功' + '(' + res.data.filter(task => task.exportStatus === 'SUCCESS').length + ')'
}
if (item.name === 'FAILED') {
item.label =
'失败' + '(' + res.data.filter(task => task.exportStatus === 'FAILED').length + ')'
}
if (item.name === 'PENDING') {
item.label =
'等待中' + '(' + res.data.filter(task => task.exportStatus === 'PENDING').length + ')'
}
})
if (activeName.value === 'ALL') {
tableData.value = res.data
} else {
tableData.value = res.data.filter(task => task.exportStatus === activeName.value)
}
})
}
}, 5000)
}
const taskExportTopicCall = task => {
if (JSON.parse(task).exportStatus === 'SUCCESS') {
openMessageLoading(
JSON.parse(task).exportFromName +
' ' +
t('excel.export') +
t('dataset.completed') +
t('dataset.goto'),
'success',
callbackExport
)
}
if (JSON.parse(task).exportStatus === 'FAILED') {
openMessageLoading(
JSON.parse(task).exportFromName +
' ' +
t('excel.export') +
t('dataset.error') +
t('dataset.goto'),
'error',
callbackExport
)
}
}
const openMessageLoading = (text, type = 'success', cb) => {
// success error loading
const customClass = `de-message-${type || 'success'} de-message-export`
ElMessage({
message: h('p', null, [
t(text),
h(
ElButton,
{
text: true,
size: 'mini',
class: 'btn-text',
onClick: () => {
cb()
}
},
t('data_export.export_center')
)
]),
icon: type === 'loading' ? h(RefreshLeft) : '',
duration: 0,
type,
showClose: true,
customClass
})
}
const callbackExport = () => {
useEmitt().emitter.emit('data-export-center')
}
const downLoadAll = () => {
if (multipleSelection.value.length === 0) {
tableData.value.forEach(item => {
downloadFile(item.id)
.then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = item.fileName //
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.finally(() => {
exportDatasetLoading.value = false
})
})
return
}
multipleSelection.value.map(ele => {
downloadFile(ele.id)
.then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = ele.fileName //
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.finally(() => {
exportDatasetLoading.value = false
})
})
}
const timestampFormatDate = value => {
if (!value) {
return '-'
}
return new Date(value).toLocaleString()
}
const downloadClick = item => {
downloadFile(item.id)
.then(res => {
const blob = new Blob([res], { type: 'application/vnd.ms-excel' })
const link = document.createElement('a')
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.download = item.fileName //
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.finally(() => {
exportDatasetLoading.value = false
})
}
const retry = item => {
exportRetry(item.id).then(() => {
handleClick()
})
}
const deleteField = item => {
ElMessageBox.confirm(t('data_export.sure_del'), {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
})
.then(() => {
exportDelete(item.id).then(() => {
ElMessage.success(t('commons.delete_success'))
handleClick()
})
})
.catch(() => {
// info(t('commons.delete_cancel'))
})
}
const handleSelectionChange = val => {
multipleSelection.value = val
}
const confirmDelete = () => {
const options = {
title: '确定删除该任务吗?',
type: 'primary',
cb: deleteField
}
// handlerConfirm(options)
}
const delAll = () => {
if (multipleSelection.value.length === 0) {
ElMessageBox.confirm(t('data_export.sure_del_all'), {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
})
.then(() => {
exportDeleteAll(
activeName,
multipleSelection.value.map(ele => ele.id)
).then(() => {
ElMessage.success(t('commons.delete_success'))
handleClick()
})
})
.catch(() => {
// info(t('commons.delete_cancel'))
})
return
}
ElMessageBox.confirm(t('data_export.sure_del'), {
confirmButtonType: 'danger',
type: 'warning',
autofocus: false,
showClose: false
})
.then(() => {
exportDeletePost(multipleSelection.value.map(ele => ele.id)).then(() => {
ElMessage.success(t('commons.delete_success'))
handleClick()
})
})
.catch(() => {
// info(t('commons.delete_cancel'))
})
}
defineExpose({
init
})
</script>
<template>
<el-drawer
v-loading="drawerLoading"
custom-class="de-export-excel"
:title="$t('data_export.export_center')"
v-model="drawer"
direction="rtl"
size="1000px"
append-to-body
:before-close="handleClose"
>
<el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane v-for="tab in tabList" :key="tab.name" :label="tab.label" :name="tab.name" />
</el-tabs>
<el-button
v-show="activeName === 'SUCCESS' && multipleSelection.length === 0"
secondary
@click="downLoadAll"
>
<template #icon>
<Icon name="de-delete"></Icon>
</template>
{{ $t('data_export.download_all') }}
</el-button>
<el-button
v-show="activeName === 'SUCCESS' && multipleSelection.length !== 0"
secondary
@click="downLoadAll"
><template #icon> <Icon name="de-delete"></Icon> </template>{{ $t('data_export.download') }}
</el-button>
<el-button v-show="multipleSelection.length === 0" secondary @click="delAll"
><template #icon> <Icon name="de-delete"></Icon> </template>{{ $t('data_export.del_all') }}
</el-button>
<el-button v-show="multipleSelection.length !== 0" secondary @click="delAll"
><template #icon> <Icon name="de-delete"></Icon> </template>{{ $t('commons.delete') }}
</el-button>
<div class="table-container" :class="!tableData.length && 'hidden-bottom'">
<el-table
ref="multipleTable"
:data="tableData"
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" />
<el-table-column prop="fileName" :label="$t('driver.file_name')" width="332">
<template #default="scope">
<div class="name-excel">
<el-icon>
<Icon name="file-excel_colorful"></Icon>
</el-icon>
<div class="name-content">
<div class="fileName">{{ scope.row.fileName }}</div>
<div v-if="scope.row.exportStatus === 'FAILED'" class="failed">
{{ $t('data_export.export_failed') }}
</div>
<div v-if="scope.row.exportStatus === 'SUCCESS'" class="success">
{{ scope.row.fileSize }}{{ scope.row.fileSizeUnit }}
</div>
</div>
</div>
<div v-if="scope.row.exportStatus === 'FAILED'" class="red-line" />
<el-progress
v-if="scope.row.exportStatus === 'IN_PROGRESS'"
:percentage="+scope.row.exportPogress"
/>
</template>
</el-table-column>
<el-table-column prop="exportFromName" :label="$t('data_export.export_obj')" width="200" />
<el-table-column prop="exportFromType" width="120" :label="$t('data_export.export_from')">
<template #default="scope">
<span v-if="scope.row.exportFromType === 'dataset'">{{ $t('dataset.datalist') }}</span>
<span v-if="scope.row.exportFromType === 'chart'">{{ $t('panel.view') }}</span>
</template>
</el-table-column>
<el-table-column prop="exportTime" width="180" :label="$t('data_export.export_time')">
<template #default="scope">
<span>{{ timestampFormatDate(scope.row.exportTime) }}</span>
</template>
</el-table-column>
<el-table-column fixed="right" prop="operate" width="80" :label="$t('commons.operating')">
<template #default="scope">
<el-button
v-if="scope.row.exportStatus === 'SUCCESS'"
type="text"
@click="downloadClick(scope.row)"
>
<div class="download-export">
<el-icon>
<Icon name="dv-preview-download"></Icon>
</el-icon>
</div>
</el-button>
<el-button
v-if="scope.row.exportStatus === 'FAILED'"
type="text"
@click="retry(scope.row)"
>
<template #icon>
<Icon name="dv-preview-download"></Icon>
</template>
</el-button>
<el-button type="text" @click="deleteField(scope.row)">
<template #icon>
<Icon name="dv-preview-download"></Icon>
</template>
</el-button>
</template>
</el-table-column>
<template #empty>
<empty-background :description="description" img-type="noneWhite" />
</template>
</el-table>
</div>
</el-drawer>
</template>
<style lang="less">
.de-export-excel {
.ed-drawer__header {
border-bottom: none;
}
.ed-tabs {
margin-top: -25px;
.ed-tabs__header {
margin-bottom: 24px;
}
}
.download-export {
font-size: 16px;
}
.table-container {
margin-top: 16px;
.ed-table .cell {
padding-left: 12px;
padding-right: 12px;
}
&.hidden-bottom {
.ed-table::before {
display: none;
}
}
.name-excel {
display: flex;
align-items: center;
.name-content {
max-width: 280px;
margin-left: 4px;
.fileName {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 100%;
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
.failed {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: #f54a45;
}
.success {
font-size: 12px;
font-weight: 400;
line-height: 20px;
color: #8f959e;
}
}
}
.ed-table__header {
border-top: 1px solid #1f232926;
}
th.ed-table__cell.is-leaf {
border-color: #1f232926;
}
.red-line {
width: 100%;
height: 4px;
background: #f54a45;
position: absolute;
left: 0;
bottom: 0;
}
}
}
</style>