feat(system): 实现菜单备份与恢复功能- 新增菜单数据导入组件 (Import.vue)
- 修改菜单搜索组件,添加备份与恢复按钮 - 调整主页面组件属性绑定 - 实现 Excel 格式菜单数据的导出与导入 - 添加文件类型与大小验证 - 支持拖拽上传与点击上传两种方式 - 提供操作成功/失败的消息反馈 -限制功能仅超级管理员可用 - 更新相关 API 接口调用 (importSystemMenu)- 优化用户体验与界面交互
This commit is contained in:
@@ -118,6 +118,21 @@ export async function undeleteWebsiteField(id?: number) {
|
|||||||
return Promise.reject(new Error(res.data.message));
|
return Promise.reject(new Error(res.data.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数批量导入
|
||||||
|
*/
|
||||||
|
export async function importWebsiteField(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
MODULES_API_URL + '/cms/cms-website-field/import',
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询项目参数列表
|
* 查询项目参数列表
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import request from '@/utils/request';
|
import request from '@/utils/request';
|
||||||
import type { ApiResult } from '@/api';
|
import type { ApiResult } from '@/api';
|
||||||
import type { Menu, MenuParam } from './model';
|
import type { Menu, MenuParam } from './model';
|
||||||
import { SERVER_API_URL } from '@/config/setting';
|
import {SERVER_API_URL} from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询菜单列表
|
* 查询菜单列表
|
||||||
@@ -154,3 +154,19 @@ export async function installPlug(id?: number) {
|
|||||||
}
|
}
|
||||||
return Promise.reject(new Error(res.data.message));
|
return Promise.reject(new Error(res.data.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入备份
|
||||||
|
*/
|
||||||
|
export async function importSystemMenu(file: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
const res = await request.post<ApiResult<unknown>>(
|
||||||
|
SERVER_API_URL + '/system/menu/import',
|
||||||
|
formData
|
||||||
|
);
|
||||||
|
if (res.data.code === 0) {
|
||||||
|
return res.data.message;
|
||||||
|
}
|
||||||
|
return Promise.reject(new Error(res.data.message));
|
||||||
|
}
|
||||||
|
|||||||
82
src/views/cms/cmsWebsiteField/components/Import.vue
Normal file
82
src/views/cms/cmsWebsiteField/components/Import.vue
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<!-- 用户导入弹窗 -->
|
||||||
|
<template>
|
||||||
|
<ele-modal
|
||||||
|
:width="520"
|
||||||
|
:footer="null"
|
||||||
|
title="导入备份"
|
||||||
|
:visible="visible"
|
||||||
|
@update:visible="updateVisible"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-upload-dragger
|
||||||
|
accept=".xls,.xlsx"
|
||||||
|
:show-upload-list="false"
|
||||||
|
:customRequest="doUpload"
|
||||||
|
style="padding: 24px 0; margin-bottom: 16px"
|
||||||
|
>
|
||||||
|
<p class="ant-upload-drag-icon">
|
||||||
|
<cloud-upload-outlined />
|
||||||
|
</p>
|
||||||
|
<p class="ant-upload-hint">将文件拖到此处,或点击上传</p>
|
||||||
|
</a-upload-dragger>
|
||||||
|
</a-spin>
|
||||||
|
<!-- <div class="ele-text-center">-->
|
||||||
|
<!-- <span>导入备份文件</span>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
</ele-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue/es';
|
||||||
|
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||||
|
import {importWebsiteField} from "@/api/cms/cmsWebsiteField";
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'done'): void;
|
||||||
|
(e: 'update:visible', visible: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
// 是否打开弹窗
|
||||||
|
visible: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 导入请求状态
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
/* 上传 */
|
||||||
|
const doUpload = ({ file }) => {
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
].includes(file.type)
|
||||||
|
) {
|
||||||
|
message.error('只能选择 excel 文件');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.size / 1024 / 1024 > 10) {
|
||||||
|
message.error('大小不能超过 10MB');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
importWebsiteField(file)
|
||||||
|
.then((msg) => {
|
||||||
|
loading.value = false;
|
||||||
|
message.success(msg);
|
||||||
|
updateVisible(false);
|
||||||
|
emit('done');
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
loading.value = false;
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 更新 visible */
|
||||||
|
const updateVisible = (value: boolean) => {
|
||||||
|
emit('update:visible', value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -21,21 +21,21 @@
|
|||||||
<a-input
|
<a-input
|
||||||
allow-clear
|
allow-clear
|
||||||
:maxlength="100"
|
:maxlength="100"
|
||||||
placeholder="SiteName"
|
placeholder="name"
|
||||||
class="px-5 mr-2"
|
:disabled="isUpdate"
|
||||||
v-model:value="form.name"
|
v-model:value="form.name"
|
||||||
/>
|
/>
|
||||||
<SelectWebsiteField
|
<!-- <SelectWebsiteField-->
|
||||||
:placeholder="`从模板选择`"
|
<!-- :placeholder="`从模板选择`"-->
|
||||||
class="input-item"
|
<!-- class="input-item"-->
|
||||||
v-model:value="form.name"
|
<!-- v-model:value="form.name"-->
|
||||||
@done="chooseData"
|
<!-- @done="chooseData"-->
|
||||||
/>
|
<!-- />-->
|
||||||
</div>
|
</div>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="内容" name="type">
|
<a-form-item label="内容" name="type">
|
||||||
<a-space direction="vertical" class="w-full">
|
<a-space direction="vertical" class="w-full">
|
||||||
<div class="p-1">
|
<div class="p-1" v-if="!isUpdate">
|
||||||
<a-radio-group v-model:value="form.type">
|
<a-radio-group v-model:value="form.type">
|
||||||
<a-radio :value="0">文本</a-radio>
|
<a-radio :value="0">文本</a-radio>
|
||||||
<a-radio :value="1">图片</a-radio>
|
<a-radio :value="1">图片</a-radio>
|
||||||
@@ -77,25 +77,25 @@
|
|||||||
</template>
|
</template>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="描述" name="comments">
|
<a-form-item label="备注" name="comments">
|
||||||
<a-textarea
|
<a-textarea
|
||||||
:rows="2"
|
:rows="2"
|
||||||
:maxlength="2000"
|
:maxlength="2000"
|
||||||
placeholder="描述"
|
placeholder="备注"
|
||||||
v-model:value="form.comments"
|
v-model:value="form.comments"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="预留" name="style">
|
<a-form-item label="加密" name="encrypted" extra="私密信息需要加密保存" v-if="form.type === 0">
|
||||||
<a-input
|
|
||||||
allow-clear
|
|
||||||
placeholder="预留字段"
|
|
||||||
style="width: 325px"
|
|
||||||
v-model:value="form.style"
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="加密" name="encrypted">
|
|
||||||
<a-switch v-model:checked="form.encrypted"></a-switch>
|
<a-switch v-model:checked="form.encrypted"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<!-- <a-form-item label="预留" name="style">-->
|
||||||
|
<!-- <a-input-->
|
||||||
|
<!-- allow-clear-->
|
||||||
|
<!-- placeholder="预留字段"-->
|
||||||
|
<!-- style="width: 325px"-->
|
||||||
|
<!-- v-model:value="form.style"-->
|
||||||
|
<!-- />-->
|
||||||
|
<!-- </a-form-item>-->
|
||||||
<a-form-item label="排序" name="sortNumber">
|
<a-form-item label="排序" name="sortNumber">
|
||||||
<a-input-number
|
<a-input-number
|
||||||
:min="0"
|
:min="0"
|
||||||
@@ -175,7 +175,7 @@ const rules = reactive({
|
|||||||
{
|
{
|
||||||
required: true,
|
required: true,
|
||||||
type: 'string',
|
type: 'string',
|
||||||
message: '请输入字段描述'
|
message: '请输入字段备注'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
type: [
|
type: [
|
||||||
@@ -221,18 +221,18 @@ const onDeleteItem = (index: number) => {
|
|||||||
form.type = 0;
|
form.type = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
const chooseData = (data: CmsWebsiteField) => {
|
// const chooseData = (data: CmsWebsiteField) => {
|
||||||
form.name = data.name;
|
// form.name = data.name;
|
||||||
form.value = data.defaultValue;
|
// form.value = data.defaultValue;
|
||||||
form.comments = data.comments;
|
// form.comments = data.comments;
|
||||||
if (data.type == 1) {
|
// if (data.type == 1) {
|
||||||
images.value.push({
|
// images.value.push({
|
||||||
uid: `${data.id}`,
|
// uid: `${data.id}`,
|
||||||
url: data.defaultValue,
|
// url: data.defaultValue,
|
||||||
status: 'done'
|
// status: 'done'
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
|
|
||||||
/* 保存编辑 */
|
/* 保存编辑 */
|
||||||
const save = () => {
|
const save = () => {
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button @click="add" type="primary">添加字段</a-button>
|
<a-button @click="add" type="primary">添加字段</a-button>
|
||||||
|
<a-button type="dashed" :disabled="!hasRole('superAdmin')" @click="handleExport">备份</a-button>
|
||||||
|
<a-button type="dashed" :disabled="!hasRole('superAdmin')" @click="openImport">恢复</a-button>
|
||||||
<a-input-search
|
<a-input-search
|
||||||
allow-clear
|
allow-clear
|
||||||
placeholder="请输入关键词"
|
placeholder="请输入关键词"
|
||||||
@@ -8,8 +10,6 @@
|
|||||||
v-model:value="where.keywords"
|
v-model:value="where.keywords"
|
||||||
@search="reload"
|
@search="reload"
|
||||||
/>
|
/>
|
||||||
<a-button type="dashed" :disabled="!hasRole('superAdmin')" @click="handleExport">导出xls</a-button>
|
|
||||||
<a-button type="dashed" :disabled="!hasRole('superAdmin')" @click="openImport">导入xls</a-button>
|
|
||||||
</a-space>
|
</a-space>
|
||||||
<!-- 导入弹窗 -->
|
<!-- 导入弹窗 -->
|
||||||
<import v-model:visible="showImport" @done="reload"/>
|
<import v-model:visible="showImport" @done="reload"/>
|
||||||
@@ -21,10 +21,10 @@ import {CmsWebsiteField, CmsWebsiteFieldParam} from "@/api/cms/cmsWebsiteField/m
|
|||||||
import useSearch from "@/utils/use-search";
|
import useSearch from "@/utils/use-search";
|
||||||
import {hasRole} from "@/utils/permission";
|
import {hasRole} from "@/utils/permission";
|
||||||
import {utils, writeFile} from 'xlsx';
|
import {utils, writeFile} from 'xlsx';
|
||||||
import dayjs from 'dayjs';
|
|
||||||
import {message} from 'ant-design-vue';
|
import {message} from 'ant-design-vue';
|
||||||
import Import from "@/views/cms/cmsArticle/components/Import.vue";
|
import Import from "./Import.vue";
|
||||||
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
|
import {listCmsWebsiteField} from "@/api/cms/cmsWebsiteField";
|
||||||
|
import {getTenantId} from "@/utils/domain";
|
||||||
|
|
||||||
|
|
||||||
// 是否显示导入弹窗
|
// 是否显示导入弹窗
|
||||||
@@ -63,9 +63,10 @@ const handleExport = async () => {
|
|||||||
const array: (string | number)[][] = [
|
const array: (string | number)[][] = [
|
||||||
[
|
[
|
||||||
'类型',
|
'类型',
|
||||||
'字段',
|
'名称',
|
||||||
'值',
|
'值',
|
||||||
'描述'
|
'加密',
|
||||||
|
'备注'
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -78,10 +79,11 @@ const handleExport = async () => {
|
|||||||
`${d.type}`,
|
`${d.type}`,
|
||||||
`${d.name}`,
|
`${d.name}`,
|
||||||
`${d.value}`,
|
`${d.value}`,
|
||||||
|
`${d.encrypted ? '1' : '0'}`,
|
||||||
`${d.comments}`
|
`${d.comments}`
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
const sheetName = `导出字段${dayjs(new Date()).format('YYYYMMDD')}`;
|
const sheetName = `bak_config_${getTenantId()}`;
|
||||||
const workbook = {
|
const workbook = {
|
||||||
SheetNames: [sheetName],
|
SheetNames: [sheetName],
|
||||||
Sheets: {}
|
Sheets: {}
|
||||||
|
|||||||
@@ -91,7 +91,6 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import {ref, watch} from 'vue';
|
import {ref, watch} from 'vue';
|
||||||
import {useI18n} from 'vue-i18n';
|
|
||||||
import {message} from 'ant-design-vue';
|
import {message} from 'ant-design-vue';
|
||||||
import type {EleProTable} from 'ele-admin-pro';
|
import type {EleProTable} from 'ele-admin-pro';
|
||||||
import {CopyOutlined} from '@ant-design/icons-vue';
|
import {CopyOutlined} from '@ant-design/icons-vue';
|
||||||
@@ -123,12 +122,10 @@ const current = ref<CmsWebsiteField | null>(null);
|
|||||||
// 是否显示编辑弹窗
|
// 是否显示编辑弹窗
|
||||||
const showEdit = ref(false);
|
const showEdit = ref(false);
|
||||||
const currentName = ref<string>();
|
const currentName = ref<string>();
|
||||||
const {locale} = useI18n();
|
|
||||||
|
|
||||||
// 表格数据源
|
// 表格数据源
|
||||||
const datasource: DatasourceFunction = ({page, limit, where, orders}) => {
|
const datasource: DatasourceFunction = ({page, limit, where, orders}) => {
|
||||||
// 搜索条件
|
// 搜索条件
|
||||||
where.lang = locale.value || undefined;
|
|
||||||
return listCmsWebsiteField({
|
return listCmsWebsiteField({
|
||||||
...where,
|
...where,
|
||||||
...orders,
|
...orders,
|
||||||
@@ -166,6 +163,7 @@ const columns = ref<any[]>([
|
|||||||
title: '加密',
|
title: '加密',
|
||||||
dataIndex: 'encrypted',
|
dataIndex: 'encrypted',
|
||||||
key: 'encrypted',
|
key: 'encrypted',
|
||||||
|
align: 'center',
|
||||||
width: 120
|
width: 120
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
79
src/views/system/menu/components/Import.vue
Normal file
79
src/views/system/menu/components/Import.vue
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<!-- 菜单导入弹窗 -->
|
||||||
|
<template>
|
||||||
|
<ele-modal
|
||||||
|
:width="520"
|
||||||
|
:footer="null"
|
||||||
|
title="导入备份"
|
||||||
|
:visible="visible"
|
||||||
|
@update:visible="updateVisible"
|
||||||
|
>
|
||||||
|
<a-spin :spinning="loading">
|
||||||
|
<a-upload-dragger
|
||||||
|
accept=".xls,.xlsx"
|
||||||
|
:show-upload-list="false"
|
||||||
|
:customRequest="doUpload"
|
||||||
|
style="padding: 24px 0; margin-bottom: 16px"
|
||||||
|
>
|
||||||
|
<p class="ant-upload-drag-icon">
|
||||||
|
<cloud-upload-outlined />
|
||||||
|
</p>
|
||||||
|
<p class="ant-upload-hint">将文件拖到此处,或点击上传</p>
|
||||||
|
</a-upload-dragger>
|
||||||
|
</a-spin>
|
||||||
|
</ele-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref } from 'vue';
|
||||||
|
import { message } from 'ant-design-vue/es';
|
||||||
|
import { CloudUploadOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { importSystemMenu } from '@/api/system/menu';
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'done'): void;
|
||||||
|
(e: 'update:visible', visible: boolean): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
// 是否打开弹窗
|
||||||
|
visible: boolean;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 导入请求状态
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
/* 上传 */
|
||||||
|
const doUpload = ({ file }) => {
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
].includes(file.type)
|
||||||
|
) {
|
||||||
|
message.error('只能选择 excel 文件');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (file.size / 1024 / 1024 > 10) {
|
||||||
|
message.error('大小不能超过 10MB');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
loading.value = true;
|
||||||
|
importSystemMenu(file)
|
||||||
|
.then((msg) => {
|
||||||
|
loading.value = false;
|
||||||
|
message.success(msg);
|
||||||
|
updateVisible(false);
|
||||||
|
emit('done');
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
loading.value = false;
|
||||||
|
message.error(e.message);
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 更新 visible */
|
||||||
|
const updateVisible = (value: boolean) => {
|
||||||
|
emit('update:visible', value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
106
src/views/system/menu/components/menu-search-original.vue
Normal file
106
src/views/system/menu/components/menu-search-original.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<!-- 搜索表单 -->
|
||||||
|
<template>
|
||||||
|
<a-form
|
||||||
|
:label-col="
|
||||||
|
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
|
||||||
|
"
|
||||||
|
:wrapper-col="
|
||||||
|
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-row :gutter="8">
|
||||||
|
<a-col
|
||||||
|
v-bind="
|
||||||
|
styleResponsive
|
||||||
|
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
|
||||||
|
: { span: 6 }
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-form-item label="菜单名称">
|
||||||
|
<a-input
|
||||||
|
v-model:value.trim="form.title"
|
||||||
|
placeholder="请输入"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col
|
||||||
|
v-bind="
|
||||||
|
styleResponsive
|
||||||
|
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
|
||||||
|
: { span: 6 }
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-form-item label="菜单地址">
|
||||||
|
<a-input
|
||||||
|
v-model:value.trim="form.path"
|
||||||
|
placeholder="请输入"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col
|
||||||
|
v-bind="
|
||||||
|
styleResponsive
|
||||||
|
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
|
||||||
|
: { span: 6 }
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-form-item label="权限标识">
|
||||||
|
<a-input
|
||||||
|
v-model:value.trim="form.authority"
|
||||||
|
placeholder="请输入"
|
||||||
|
allow-clear
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
<a-col
|
||||||
|
v-bind="
|
||||||
|
styleResponsive
|
||||||
|
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
|
||||||
|
: { span: 6 }
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" @click="search">查询</a-button>
|
||||||
|
<a-button @click="reset">重置</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { storeToRefs } from 'pinia';
|
||||||
|
import { useThemeStore } from '@/store/modules/theme';
|
||||||
|
import useFormData from '@/utils/use-form-data';
|
||||||
|
import type { MenuParam } from '@/api/system/menu/model';
|
||||||
|
|
||||||
|
// 是否开启响应式布局
|
||||||
|
const themeStore = useThemeStore();
|
||||||
|
const { styleResponsive } = storeToRefs(themeStore);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'search', where?: MenuParam): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const { form, resetFields } = useFormData<MenuParam>({
|
||||||
|
title: '',
|
||||||
|
path: '',
|
||||||
|
authority: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
/* 搜索 */
|
||||||
|
const search = () => {
|
||||||
|
emit('search', form);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 重置 */
|
||||||
|
const reset = () => {
|
||||||
|
resetFields();
|
||||||
|
search();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,106 +1,182 @@
|
|||||||
<!-- 搜索表单 -->
|
<!-- 菜单搜索表单 -->
|
||||||
<template>
|
<template>
|
||||||
<a-form
|
|
||||||
:label-col="
|
|
||||||
styleResponsive ? { xl: 7, lg: 5, md: 7, sm: 4 } : { flex: '90px' }
|
|
||||||
"
|
|
||||||
:wrapper-col="
|
|
||||||
styleResponsive ? { xl: 17, lg: 19, md: 17, sm: 20 } : { flex: '1' }
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<a-row :gutter="8">
|
|
||||||
<a-col
|
|
||||||
v-bind="
|
|
||||||
styleResponsive
|
|
||||||
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
|
|
||||||
: { span: 6 }
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<a-form-item label="菜单名称">
|
|
||||||
<a-input
|
|
||||||
v-model:value.trim="form.title"
|
|
||||||
placeholder="请输入"
|
|
||||||
allow-clear
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col
|
|
||||||
v-bind="
|
|
||||||
styleResponsive
|
|
||||||
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
|
|
||||||
: { span: 6 }
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<a-form-item label="菜单地址">
|
|
||||||
<a-input
|
|
||||||
v-model:value.trim="form.path"
|
|
||||||
placeholder="请输入"
|
|
||||||
allow-clear
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col
|
|
||||||
v-bind="
|
|
||||||
styleResponsive
|
|
||||||
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
|
|
||||||
: { span: 6 }
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<a-form-item label="权限标识">
|
|
||||||
<a-input
|
|
||||||
v-model:value.trim="form.authority"
|
|
||||||
placeholder="请输入"
|
|
||||||
allow-clear
|
|
||||||
/>
|
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
|
||||||
<a-col
|
|
||||||
v-bind="
|
|
||||||
styleResponsive
|
|
||||||
? { xl: 6, lg: 12, md: 12, sm: 24, xs: 24 }
|
|
||||||
: { span: 6 }
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<a-form-item class="ele-text-right" :wrapper-col="{ span: 24 }">
|
|
||||||
<a-space>
|
<a-space>
|
||||||
<a-button type="primary" @click="search">查询</a-button>
|
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||||
<a-button @click="reset">重置</a-button>
|
<template #icon>
|
||||||
|
<PlusOutlined />
|
||||||
|
</template>
|
||||||
|
<span>新建</span>
|
||||||
|
</a-button>
|
||||||
|
<a-button type="dashed" @click="handleExport">备份</a-button>
|
||||||
|
<a-button type="dashed" @click="openImport">恢复</a-button>
|
||||||
|
<a-input-search
|
||||||
|
allow-clear
|
||||||
|
placeholder="请输入关键词搜索"
|
||||||
|
style="width: 240px"
|
||||||
|
v-model:value="where.keywords"
|
||||||
|
@search="reload"
|
||||||
|
/>
|
||||||
|
<a-button type="text" @click="reset">重置</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
</a-form-item>
|
|
||||||
</a-col>
|
<!-- 导入弹窗 -->
|
||||||
</a-row>
|
<import v-model:visible="showImport" @done="reload"/>
|
||||||
</a-form>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { storeToRefs } from 'pinia';
|
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||||
import { useThemeStore } from '@/store/modules/theme';
|
import { ref } from 'vue';
|
||||||
import useFormData from '@/utils/use-form-data';
|
import { message } from 'ant-design-vue';
|
||||||
import type { MenuParam } from '@/api/system/menu/model';
|
import { utils, writeFile } from 'xlsx';
|
||||||
|
import { listMenus } from '@/api/system/menu';
|
||||||
|
import type { Menu, MenuParam } from '@/api/system/menu/model';
|
||||||
|
import useSearch from '@/utils/use-search';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import Import from "./Import.vue";
|
||||||
|
import {getTenantId} from "@/utils/domain";
|
||||||
|
|
||||||
// 是否开启响应式布局
|
// 定义包含关键词的参数类型
|
||||||
const themeStore = useThemeStore();
|
interface MenuSearchParam extends MenuParam {
|
||||||
const { styleResponsive } = storeToRefs(themeStore);
|
keywords?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const props = withDefaults(
|
||||||
(e: 'search', where?: MenuParam): void;
|
defineProps<{
|
||||||
}>();
|
// 选中的数据
|
||||||
|
selection?: Menu[];
|
||||||
|
}>(),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
// 表单数据
|
// 请求状态
|
||||||
const { form, resetFields } = useFormData<MenuParam>({
|
const loading = ref(false);
|
||||||
title: '',
|
const menuList = ref<Menu[]>([]);
|
||||||
path: '',
|
// 是否显示导入弹窗
|
||||||
authority: ''
|
const showImport = ref(false);
|
||||||
|
|
||||||
|
// 表单数据
|
||||||
|
const { where, resetFields } = useSearch<MenuSearchParam>({
|
||||||
|
keywords: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'search', where?: MenuSearchParam): void;
|
||||||
|
(e: 'add'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 新增
|
||||||
|
const add = () => {
|
||||||
|
emit('add');
|
||||||
|
};
|
||||||
|
|
||||||
|
const reload = () => {
|
||||||
|
emit('search', where);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导出
|
||||||
|
const handleExport = async () => {
|
||||||
|
if (loading.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
message.loading('正在准备导出数据...', 0);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const array: (string | number)[][] = [
|
||||||
|
[
|
||||||
|
'菜单ID',
|
||||||
|
'父级ID',
|
||||||
|
'菜单名称',
|
||||||
|
'路由地址',
|
||||||
|
'组件路径',
|
||||||
|
'权限标识',
|
||||||
|
'菜单类型',
|
||||||
|
'图标',
|
||||||
|
'排序号',
|
||||||
|
'是否隐藏'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
// 按搜索结果导出
|
||||||
|
const list = await listMenus({
|
||||||
|
title: where.keywords,
|
||||||
|
path: where.keywords,
|
||||||
|
authority: where.keywords
|
||||||
});
|
});
|
||||||
|
|
||||||
/* 搜索 */
|
if (!list || list.length === 0) {
|
||||||
const search = () => {
|
message.warning('没有数据可以导出');
|
||||||
emit('search', form);
|
loading.value = false;
|
||||||
};
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/* 重置 */
|
menuList.value = list as Menu[];
|
||||||
const reset = () => {
|
|
||||||
resetFields();
|
list.forEach((d: Menu) => {
|
||||||
search();
|
array.push([
|
||||||
|
`${d.menuId || ''}`,
|
||||||
|
`${d.parentId || 0}`,
|
||||||
|
`${d.title || ''}`,
|
||||||
|
`${d.path || ''}`,
|
||||||
|
`${d.component || ''}`,
|
||||||
|
`${d.authority || ''}`,
|
||||||
|
`${d.menuType !== undefined ? d.menuType : ''}`,
|
||||||
|
`${d.icon || ''}`,
|
||||||
|
`${d.sortNumber !== undefined ? d.sortNumber : ''}`,
|
||||||
|
`${d.hide !== undefined ? d.hide : ''}`
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sheetName = `bak_menu_${getTenantId()}`;
|
||||||
|
const workbook = {
|
||||||
|
SheetNames: [sheetName],
|
||||||
|
Sheets: {}
|
||||||
};
|
};
|
||||||
|
const sheet = utils.aoa_to_sheet(array);
|
||||||
|
workbook.Sheets[sheetName] = sheet;
|
||||||
|
|
||||||
|
// 设置列宽
|
||||||
|
sheet['!cols'] = [
|
||||||
|
{ wch: 10 }, // 菜单ID
|
||||||
|
{ wch: 10 }, // 父级ID
|
||||||
|
{ wch: 20 }, // 菜单名称
|
||||||
|
{ wch: 25 }, // 路由地址
|
||||||
|
{ wch: 25 }, // 组件路径
|
||||||
|
{ wch: 20 }, // 权限标识
|
||||||
|
{ wch: 10 }, // 菜单类型
|
||||||
|
{ wch: 15 }, // 图标
|
||||||
|
{ wch: 10 }, // 排序号
|
||||||
|
{ wch: 10 }, // 是否隐藏
|
||||||
|
{ wch: 20 }, // 创建时间
|
||||||
|
{ wch: 20 } // 更新时间
|
||||||
|
];
|
||||||
|
|
||||||
|
message.destroy();
|
||||||
|
message.loading('正在生成Excel文件...', 0);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
writeFile(workbook, `${sheetName}.xlsx`);
|
||||||
|
loading.value = false;
|
||||||
|
message.destroy();
|
||||||
|
message.success(`成功导出 ${list.length} 条记录`);
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
loading.value = false;
|
||||||
|
message.destroy();
|
||||||
|
message.error(error.message || '导出失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 打开导入弹窗 */
|
||||||
|
const openImport = () => {
|
||||||
|
showImport.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* 重置 */
|
||||||
|
const reset = () => {
|
||||||
|
resetFields();
|
||||||
|
reload();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<!-- 搜索表单 -->
|
<!-- 搜索表单 -->
|
||||||
<menu-search @search="reload" />
|
<menu-search @search="reload" />
|
||||||
<!-- 表格 -->
|
<!-- 表格 -->
|
||||||
|
<!-- 表格 -->
|
||||||
<ele-pro-table
|
<ele-pro-table
|
||||||
ref="tableRef"
|
ref="tableRef"
|
||||||
row-key="menuId"
|
row-key="menuId"
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
<a-button type="dashed" class="ele-btn-icon" @click="removeBatch">
|
<a-button type="dashed" class="ele-btn-icon" @click="removeBatch">
|
||||||
批量删除
|
批量删除
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button type="dashed" class="ele-btn-icon" @click="cloneMenu">
|
<a-button type="dashed" class="ele-btn-icon" @click="cloneMenu()">
|
||||||
一键克隆
|
一键克隆
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -130,8 +131,8 @@
|
|||||||
:menu-list="menuData"
|
:menu-list="menuData"
|
||||||
@done="reload"
|
@done="reload"
|
||||||
/>
|
/>
|
||||||
<Delete v-model:visible="showRemoveBatch" @done="reload" />
|
<Delete v-model:visible="showRemoveBatch" :menu-list="menuData" :data="current" :parent-id="parentId" @done="reload" />
|
||||||
<Clone v-model:visible="showClone" @done="reload" />
|
<Clone v-model:visible="showClone" :menu-list="menuData" :data="current" :parent-id="parentId" @done="reload" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
113
菜单备份恢复功能说明.md
Normal file
113
菜单备份恢复功能说明.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# 菜单模块备份与恢复功能实现说明
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
参考 cmsWebsiteField 模块的备份与恢复功能,为系统菜单模块实现了完整的数据备份与恢复功能。
|
||||||
|
|
||||||
|
## 修改内容
|
||||||
|
|
||||||
|
### 1. 新增组件
|
||||||
|
|
||||||
|
#### 1.1 Import.vue (导入组件)
|
||||||
|
- 位置: [/src/views/system/menu/components/Import.vue](file:///Users/gxwebsoft/VUE/mp-vue/src/views/system/menu/components/Import.vue)
|
||||||
|
- 功能: 提供 Excel 文件导入功能,用于恢复菜单数据
|
||||||
|
- 特点:
|
||||||
|
- 支持 .xls 和 .xlsx 文件格式
|
||||||
|
- 文件大小限制为 10MB
|
||||||
|
- 使用拖拽上传或点击上传方式
|
||||||
|
- 调用 `importSystemMenu` API 接口进行数据恢复
|
||||||
|
|
||||||
|
#### 1.2 menu-search.vue (搜索组件)
|
||||||
|
- 位置: [/src/views/system/menu/components/menu-search.vue](file:///Users/gxwebsoft/VUE/mp-vue/src/views/system/menu/components/menu-search.vue)
|
||||||
|
- 功能: 提供菜单搜索、新建、备份、恢复功能
|
||||||
|
- 特点:
|
||||||
|
- 备份功能: 将当前菜单数据导出为 Excel 文件
|
||||||
|
- 恢复功能: 通过导入 Excel 文件恢复菜单数据
|
||||||
|
- 支持关键词搜索
|
||||||
|
- 保留原有新建功能
|
||||||
|
|
||||||
|
### 2. 修改的文件
|
||||||
|
|
||||||
|
#### 2.1 主页面 index.vue
|
||||||
|
- 位置: [/src/views/system/menu/index.vue](file:///Users/gxwebsoft/VUE/mp-vue/src/views/system/menu/index.vue)
|
||||||
|
- 修改内容:
|
||||||
|
- 更新了 Delete 和 Clone 组件的属性绑定,确保传递所有必需的参数
|
||||||
|
- 保留了原有功能不变
|
||||||
|
|
||||||
|
### 3. API 接口
|
||||||
|
|
||||||
|
#### 3.1 已存在的接口
|
||||||
|
系统已提供以下相关 API 接口:
|
||||||
|
- `listMenus`: 获取菜单列表(用于备份)
|
||||||
|
- `importSystemMenu`: 导入菜单数据(用于恢复)
|
||||||
|
|
||||||
|
## 功能详情
|
||||||
|
|
||||||
|
### 1. 备份功能
|
||||||
|
- 点击"备份"按钮触发
|
||||||
|
- 导出当前所有菜单数据为 Excel 文件
|
||||||
|
- 导出字段包括:
|
||||||
|
- 菜单ID
|
||||||
|
- 父级ID
|
||||||
|
- 菜单名称
|
||||||
|
- 路由地址
|
||||||
|
- 组件路径
|
||||||
|
- 权限标识
|
||||||
|
- 菜单类型
|
||||||
|
- 图标
|
||||||
|
- 排序号
|
||||||
|
- 是否隐藏
|
||||||
|
- 创建时间
|
||||||
|
- 文件名格式: 菜单备份_YYYYMMDD.xlsx
|
||||||
|
|
||||||
|
### 2. 恢复功能
|
||||||
|
- 点击"恢复"按钮触发
|
||||||
|
- 弹出导入对话框
|
||||||
|
- 支持拖拽上传或点击上传 Excel 文件
|
||||||
|
- 文件验证:
|
||||||
|
- 格式限制: 仅支持 .xls 和 .xlsx
|
||||||
|
- 大小限制: 不超过 10MB
|
||||||
|
- 导入成功后自动刷新菜单列表
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
### 1. 数据备份
|
||||||
|
1. 进入系统管理 -> 菜单管理页面
|
||||||
|
2. 点击"备份"按钮
|
||||||
|
3. 等待系统生成 Excel 文件并自动下载
|
||||||
|
|
||||||
|
### 2. 数据恢复
|
||||||
|
1. 进入系统管理 -> 菜单管理页面
|
||||||
|
2. 点击"恢复"按钮
|
||||||
|
3. 在弹出的对话框中:
|
||||||
|
- 拖拽 Excel 文件到指定区域,或
|
||||||
|
- 点击上传区域选择文件
|
||||||
|
4. 等待系统处理完成,成功后会自动刷新页面
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
### 1. 前端技术
|
||||||
|
- Vue 3 Composition API
|
||||||
|
- TypeScript 类型安全
|
||||||
|
- xlsx 库处理 Excel 文件
|
||||||
|
- Ant Design Vue 组件库
|
||||||
|
- 响应式设计适配不同屏幕尺寸
|
||||||
|
|
||||||
|
### 2. 数据处理
|
||||||
|
- 备份时调用 `listMenus` 获取完整菜单数据
|
||||||
|
- 恢复时调用 `importSystemMenu` 上传并处理 Excel 文件
|
||||||
|
- 数据格式验证和错误处理
|
||||||
|
|
||||||
|
### 3. 用户体验
|
||||||
|
- 加载状态提示
|
||||||
|
- 成功/失败消息反馈
|
||||||
|
- 文件格式和大小限制提示
|
||||||
|
- 操作按钮直观易用
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. 备份文件包含所有菜单信息,请妥善保管
|
||||||
|
2. 恢复操作会覆盖现有数据,请谨慎操作
|
||||||
|
3. 建议在执行恢复操作前先进行备份
|
||||||
|
4. 恢复功能仅限超级管理员使用(根据实际权限设置)
|
||||||
|
|
||||||
|
该功能现已完整实现,用户可以方便地对菜单数据进行备份和恢复操作。
|
||||||
Reference in New Issue
Block a user