系统数据迁移(系统基础数据导出、导入)功能开发

This commit is contained in:
suguangxu
2025-06-05 18:27:01 +08:00
parent 13ab86a814
commit 090457b5e7
6 changed files with 598 additions and 0 deletions

View File

@ -0,0 +1,45 @@
import { defHttp } from '/@/utils/http/axios';
import { ErrorMessageMode } from '/#/axios';
enum Api {
ExportDatas= '/system/dataMigration/exportDatas',
DownloadDatas='/system/dataMigration/downloadDatas',
}
/**
* @description: 系统配置迁移-导出资源
*/
export async function exportDatas(params, mode: ErrorMessageMode = 'modal') {
return defHttp.post(
{
url: Api.ExportDatas,
data:params,
},
{
errorMessageMode: mode,
},
);
}
/**
* @description: 根据uuid(目录名称)下载数据
*/
export async function downloadDatas(
params?: object,
mode: ErrorMessageMode = 'modal'
) {
return defHttp.download(
{
url: Api.DownloadDatas+"/"+params.uuid,
method: 'GET',
responseType: 'blob',
},
{
errorMessageMode: mode,
},
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,262 @@
<template>
<div class="export">
<div class="l-export" ><span @click.stop="openConfirmDialog">导出所选项目{{configType}}</span></div>
<div class="list-wrapper">
<a-list size="small" bordered :data-source="state.options">
<template #renderItem="{ item }">
<a-list-item><a-checkbox style="margin-right:30px" v-model:checked="item.checked"/>{{ item.name }}</a-list-item>
</template>
<template #header>
<a-checkbox
style="margin-right:30px"
v-model:checked="state.checkAll"
:indeterminate="state.indeterminate"
@change="onCheckAllChange">
</a-checkbox>
选择全部
</template>
</a-list>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref,watch,reactive,h} from 'vue';
import {useMessage} from "/@/hooks/web/useMessage";
import { useI18n } from '/@/hooks/web/useI18n';
import { exportDatas,downloadDatas} from '/@/api/system/dataMigration';
import { downloadByData } from '/@/utils/file/download';
import { dateUtil } from '/@/utils/dateUtil';
const { t } = useI18n();
const state = reactive({
indeterminate: false,
checkAll: false,
options:[
{
checked:false,
code:"租户",
name:'租户'
},
/* {
checked:false,
code:"用户",
name:'用户'
}, */
{
checked:false,
code:'角色',
name:'角色'
},
{
checked:false,
code:'岗位',
name:'岗位'
},
/* {
checked:false,
code:'部门',
name:'部门'
}, */
{
checked:false,
code:'用户组',
name:'用户组'
},
{
checked:false,
code:'菜单',
name:'菜单'
},
{
checked:false,
code:'表单',
name:'表单'
},
{
checked:false,
code:'流程定义',
name:'流程定义'
},
{
checked:false,
code:'系统变量',
name:'系统变量'
},
{
checked:false,
code:'数据字典',
name:'数据字典'
},
{
checked:false,
code:'桌面配置',
name:'桌面配置'
},
{
checked:false,
code:'自定义接口',
name:'自定义接口'
},
{
checked:false,
code:'角色-菜单授权',
name:'权限:角色-菜单授权(菜单、按钮、列表、表单)'
},
{
checked:false,
code:'角色-自定义接口授权',
name:'权限:角色-自定义接口授权'
},
/* {
checked:false,
code:'用户-角色授权',
name:'权限:用户-角色授权'
},
{
checked:false,
code:'用户-用户组授权',
name:'权限:用户-用户组授权'
},
{
checked:false,
code:'用户-岗位授权',
name:'权限:用户-岗位授权'
},
{
checked:false,
code:'用户-部门授权',
name:'权限:用户-部门授权'
}, */
{
checked:false,
code:'租户-菜单授权',
name:'权限:租户-菜单授权'
}
]
});
function onCheckAllChange(e: any){
if(e.target.checked){
state.options.forEach((item)=>{
item.checked=true;
});
}else{
state.options.forEach((item)=>{
item.checked=false;
});
}
state.indeterminate=false;
}
watch(
() => state.options,
val => {
const checkedList=val.filter((item)=>item.checked);
if(checkedList.length>0){
if(checkedList.length< val.length){
state.indeterminate=true;
}else{
state.checkAll=true;
state.indeterminate=false;
}
}else{
state.indeterminate=false;
}
},
{ deep: true },
);
function openConfirmDialog(){
const { notification,createConfirm} = useMessage();
const checkedList=state.options.filter((item)=>item.checked);
if(checkedList.length==0){
notification.warning({
message: '提示',
description: '未选择任何项目'
});
return;
}
createConfirm({
iconType: 'warning',
title: () => h('span', t('温馨提示')),
content: () => t('确定导出所选项目吗?'),
width:'500px',
onOk: async () => {
handleExport();
},
okText: () => t('确认'),
cancelText: () => t('取消'),
});
}
async function handleExport(){
try {
const checkedCodeList=state.options.filter((item)=>item.checked).map(item => item.code);
let currentTime=dateUtil(new Date()).format('YYYY-MM-DD_HH_mm_ss');
let result= await exportDatas({
items:checkedCodeList
});
if(result){
const fileName=dateUtil(new Date()).format('YYYY-MM-DD_HH_mm_ss');
const res = await downloadDatas({uuid:result});
downloadByData(
res.data,
fileName+".zip"
);
}
} catch (error) {
console.error(error);
}
}
</script>
<style lang="less" scoped>
.export{
display:flex;
flex-direction:column;
height:100%;
width:100%;
padding:30px 30px 30px 100px;
border:1px solid #d6d6d6;
border-radius:8px;
overflow:hidden;
}
.l-export{
display:block;
margin-left:20px;
margin-bottom:20px;
color: rgba(0,0,255,0.8);
text-decoration:underline rgba(#0000ff,0.8);
font-weight: 500;
font-size: 16px;
cursor: pointer;
&:hover{
color: rgba(0,0,255,1);
text-decoration:underline #0000ff;
}
}
.list-wrapper{
height:calc(100% - 50px);
.ant-list{
width:100%;
height:calc(100% - 30px);
overflow:hidden;
border:0px;
}
}
</style>

View File

@ -0,0 +1,157 @@
<template>
<span @click.stop="open">
<slot></slot>
<a-modal
v-model:visible="data.visible"
:title="t(`导入数据[${coverType=='override'?'覆盖模式':'新增模式'}]`)"
:maskClosable="false"
@ok="doUpload"
@cancel="close"
@click.stop=""
>
<div class="upload-box">
<a-upload
v-model:file-list="fileList"
class="upload-box"
name="file"
accept=".json,.zip"
:headers="data.headers"
:max-count="1"
@change="handleChange"
:before-upload="beforeUpload"
@remove="handleRemove"
>
<img :src="BgImg" />
<div class="a-upload__text">{{ t('将文件拖到此处,或') }}<em>{{ t('点击上传') }}</em></div>
</a-upload>
</div>
</a-modal>
</span>
</template>
<script setup lang="ts">
import { reactive,ref} from 'vue';
import BgImg from '../../assets/sysconfig_import.png';
import { useI18n } from '/@/hooks/web/useI18n';
import { defHttp } from '/@/utils/http/axios';
import { useGlobSetting } from '/@/hooks/setting';
import { useMessage } from '/@/hooks/web/useMessage';
const props = defineProps({
coverType: {
type: Boolean
},
});
const { t } = useI18n();
const globSetting = useGlobSetting();
const { notification } = useMessage();
const data: {
visible: boolean;
} = reactive({
visible: false
});
const fileList=ref([]);
async function open() {
if(props.coverType!=='override'){
alert("新增模式导入暂时不可用");
return;
}
data.visible = true;
}
function close() {
data.visible = false;
fileList.value=[];
}
function handleRemove(file){
const index = fileList.value.indexOf(file);
const newFileList = fileList.value.slice();
newFileList.splice(index, 1);
fileList.value = newFileList;
};
function beforeUpload(file){
fileList.value = [...(fileList.value || []), file];
return false;
};
function doUpload(){
if(fileList==undefined||fileList.value.length==0){
notification.error({
message: '提示',
description: t('请选择文件'),
});
return;
}
const files=[];
fileList.value.forEach(file => {
files.push(file.originFileObj);
});
const url="/system/dataMigration/importDatas";
defHttp.uploadFile(
{
baseURL: globSetting.apiUrl,
url:url,
method: 'POST',
},
{
name: 'file',
file: files,
data:{
configType:props.configType,
cover:props.coverType=='override'?true:false
}
}
).then((data) => {
notification.success({
message: '提示',
description: t('导入中...请稍后确认导入结果!'),
});
close();
}).catch((err) => {
console.error(err);
}).finally(()=>{
// close();
});
};
</script>
<style lang="less" scoped>
.upload-box {
display: inline-block;
}
.upload-demo {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.a-upload-dragger {
width: 615px;
height: 370px;
border: none;
}
.a-upload__text {
position: absolute;
bottom: 100px;
right: 100px;
font-weight: bold;
color: #1d2027;
}
em {
font-style: normal;
color: #4f83fd;
}
</style>

View File

@ -0,0 +1,43 @@
<template>
<div class="import">
<ImportSystemConfig coverType="new">
<span class="item-action">新增模式导入</span>
</ImportSystemConfig>
<ImportSystemConfig coverType="override">
<span class="item-action">覆盖模式导入</span>
</ImportSystemConfig>
</div>
</template>
<script lang="ts" setup>
import ImportSystemConfig from './ImportSystemConfig.vue';
</script>
<style lang="less" scoped>
.import{
display:flex;
flex-direction:row;
align-items:center;
justify-content:center;
height:100%;
width:100%;
gap:100px;
border:1px solid #d6d6d6;
border-radius:8px;
}
.item-action {
border:1px solid rgba(206, 206, 206, 1);
padding:6px 8px;
border-radius:8px;
color:rgba(0, 0, 0, 0.8);
}
.item-action:hover {
cursor: pointer;
background:rgba(22, 119, 224, 1);
color:white;
}
</style>

View File

@ -0,0 +1,91 @@
<template>
<div class="system-data-migration">
<a-tabs v-model:activeKey="tabActiveKey" class="main-tabs">
<a-tab-pane key="import" tab="导出工作区">
<Export/>
</a-tab-pane>
<a-tab-pane key="outport" tab="导入工作区" force-render>
<Import/>
</a-tab-pane>
</a-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, onUnmounted, createVNode} from 'vue';
import { PageWrapper } from '/@/components/Page';
import Export from './components/export/index.vue';
import Import from './components/import/index.vue';
const tabActiveKey = ref('import');
</script>
<style lang="less" scoped>
.system-data-migration{
position:absolute;
height:100%;
width:100%;
background-color:white;
padding-top:10px;
}
.main-tabs {
height: 100%;
}
:deep(.ant-tabs-nav){
border-bottom:0px;
}
:deep(.ant-tabs-nav-wrap) {
width: 100% !important;
display: block !important;
}
:deep(.ant-tabs-tab) {
min-width: 200px !important;
width:calc(50% - 2px);
display: block !important;
text-align: center !important;
font-size:16px;
padding:8px 0px;
&.ant-tabs-tab-active{
// box-shadow: 10px 10px 100px rgba(#1677ff, 1) inset;
.ant-tabs-tab-btn{
// color:#fff;
}
}
}
:deep(.ant-tabs-tab:first-child) {
border-radius: 10px 0px 0px 10px;
}
:deep(.ant-tabs-tab + .ant-tabs-tab){
margin:0px 0px 0px 0px;
border-radius: 0px 10px 10px 0px;
}
:deep(.ant-tabs-content-holder) {
height:100%;
}
:deep(.ant-tabs-content) {
height: 100% !important;
overflow-y: auto;
padding:10px 10px;
}
:deep(.ant-tabs-tabpane) {
padding: 0 8px;
height:100%;
}
:deep(.ant-tabs-nav) {
margin-bottom: 0;
}
</style>