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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 参数批量导入
|
||||
*/
|
||||
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 type { ApiResult } from '@/api';
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入备份
|
||||
*/
|
||||
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
|
||||
allow-clear
|
||||
:maxlength="100"
|
||||
placeholder="SiteName"
|
||||
class="px-5 mr-2"
|
||||
placeholder="name"
|
||||
:disabled="isUpdate"
|
||||
v-model:value="form.name"
|
||||
/>
|
||||
<SelectWebsiteField
|
||||
:placeholder="`从模板选择`"
|
||||
class="input-item"
|
||||
v-model:value="form.name"
|
||||
@done="chooseData"
|
||||
/>
|
||||
<!-- <SelectWebsiteField-->
|
||||
<!-- :placeholder="`从模板选择`"-->
|
||||
<!-- class="input-item"-->
|
||||
<!-- v-model:value="form.name"-->
|
||||
<!-- @done="chooseData"-->
|
||||
<!-- />-->
|
||||
</div>
|
||||
</a-form-item>
|
||||
<a-form-item label="内容" name="type">
|
||||
<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 :value="0">文本</a-radio>
|
||||
<a-radio :value="1">图片</a-radio>
|
||||
@@ -77,25 +77,25 @@
|
||||
</template>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<a-form-item label="描述" name="comments">
|
||||
<a-form-item label="备注" name="comments">
|
||||
<a-textarea
|
||||
:rows="2"
|
||||
:maxlength="2000"
|
||||
placeholder="描述"
|
||||
placeholder="备注"
|
||||
v-model:value="form.comments"
|
||||
/>
|
||||
</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="encrypted">
|
||||
<a-form-item label="加密" name="encrypted" extra="私密信息需要加密保存" v-if="form.type === 0">
|
||||
<a-switch v-model:checked="form.encrypted"></a-switch>
|
||||
</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-input-number
|
||||
:min="0"
|
||||
@@ -175,7 +175,7 @@ const rules = reactive({
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入字段描述'
|
||||
message: '请输入字段备注'
|
||||
}
|
||||
],
|
||||
type: [
|
||||
@@ -221,18 +221,18 @@ const onDeleteItem = (index: number) => {
|
||||
form.type = 0;
|
||||
};
|
||||
|
||||
const chooseData = (data: CmsWebsiteField) => {
|
||||
form.name = data.name;
|
||||
form.value = data.defaultValue;
|
||||
form.comments = data.comments;
|
||||
if (data.type == 1) {
|
||||
images.value.push({
|
||||
uid: `${data.id}`,
|
||||
url: data.defaultValue,
|
||||
status: 'done'
|
||||
});
|
||||
}
|
||||
};
|
||||
// const chooseData = (data: CmsWebsiteField) => {
|
||||
// form.name = data.name;
|
||||
// form.value = data.defaultValue;
|
||||
// form.comments = data.comments;
|
||||
// if (data.type == 1) {
|
||||
// images.value.push({
|
||||
// uid: `${data.id}`,
|
||||
// url: data.defaultValue,
|
||||
// status: 'done'
|
||||
// });
|
||||
// }
|
||||
// };
|
||||
|
||||
/* 保存编辑 */
|
||||
const save = () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<a-space>
|
||||
<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
|
||||
allow-clear
|
||||
placeholder="请输入关键词"
|
||||
@@ -8,8 +10,6 @@
|
||||
v-model:value="where.keywords"
|
||||
@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>
|
||||
<!-- 导入弹窗 -->
|
||||
<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 {hasRole} from "@/utils/permission";
|
||||
import {utils, writeFile} from 'xlsx';
|
||||
import dayjs from 'dayjs';
|
||||
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 {getTenantId} from "@/utils/domain";
|
||||
|
||||
|
||||
// 是否显示导入弹窗
|
||||
@@ -63,9 +63,10 @@ const handleExport = async () => {
|
||||
const array: (string | number)[][] = [
|
||||
[
|
||||
'类型',
|
||||
'字段',
|
||||
'名称',
|
||||
'值',
|
||||
'描述'
|
||||
'加密',
|
||||
'备注'
|
||||
]
|
||||
];
|
||||
|
||||
@@ -78,10 +79,11 @@ const handleExport = async () => {
|
||||
`${d.type}`,
|
||||
`${d.name}`,
|
||||
`${d.value}`,
|
||||
`${d.encrypted ? '1' : '0'}`,
|
||||
`${d.comments}`
|
||||
]);
|
||||
});
|
||||
const sheetName = `导出字段${dayjs(new Date()).format('YYYYMMDD')}`;
|
||||
const sheetName = `bak_config_${getTenantId()}`;
|
||||
const workbook = {
|
||||
SheetNames: [sheetName],
|
||||
Sheets: {}
|
||||
|
||||
@@ -91,7 +91,6 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, watch} from 'vue';
|
||||
import {useI18n} from 'vue-i18n';
|
||||
import {message} from 'ant-design-vue';
|
||||
import type {EleProTable} from 'ele-admin-pro';
|
||||
import {CopyOutlined} from '@ant-design/icons-vue';
|
||||
@@ -123,12 +122,10 @@ const current = ref<CmsWebsiteField | null>(null);
|
||||
// 是否显示编辑弹窗
|
||||
const showEdit = ref(false);
|
||||
const currentName = ref<string>();
|
||||
const {locale} = useI18n();
|
||||
|
||||
// 表格数据源
|
||||
const datasource: DatasourceFunction = ({page, limit, where, orders}) => {
|
||||
// 搜索条件
|
||||
where.lang = locale.value || undefined;
|
||||
return listCmsWebsiteField({
|
||||
...where,
|
||||
...orders,
|
||||
@@ -166,6 +163,7 @@ const columns = ref<any[]>([
|
||||
title: '加密',
|
||||
dataIndex: 'encrypted',
|
||||
key: 'encrypted',
|
||||
align: 'center',
|
||||
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>
|
||||
<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>
|
||||
<a-space>
|
||||
<a-button type="primary" class="ele-btn-icon" @click="add">
|
||||
<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>
|
||||
|
||||
<!-- 导入弹窗 -->
|
||||
<import v-model:visible="showImport" @done="reload"/>
|
||||
</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';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import { ref } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
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();
|
||||
const { styleResponsive } = storeToRefs(themeStore);
|
||||
// 定义包含关键词的参数类型
|
||||
interface MenuSearchParam extends MenuParam {
|
||||
keywords?: string;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'search', where?: MenuParam): void;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
// 选中的数据
|
||||
selection?: Menu[];
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
// 表单数据
|
||||
const { form, resetFields } = useFormData<MenuParam>({
|
||||
title: '',
|
||||
path: '',
|
||||
authority: ''
|
||||
});
|
||||
// 请求状态
|
||||
const loading = ref(false);
|
||||
const menuList = ref<Menu[]>([]);
|
||||
// 是否显示导入弹窗
|
||||
const showImport = ref(false);
|
||||
|
||||
/* 搜索 */
|
||||
const search = () => {
|
||||
emit('search', form);
|
||||
};
|
||||
// 表单数据
|
||||
const { where, resetFields } = useSearch<MenuSearchParam>({
|
||||
keywords: ''
|
||||
});
|
||||
|
||||
/* 重置 */
|
||||
const reset = () => {
|
||||
resetFields();
|
||||
search();
|
||||
};
|
||||
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) {
|
||||
message.warning('没有数据可以导出');
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
menuList.value = list as Menu[];
|
||||
|
||||
list.forEach((d: Menu) => {
|
||||
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>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<!-- 搜索表单 -->
|
||||
<menu-search @search="reload" />
|
||||
<!-- 表格 -->
|
||||
<!-- 表格 -->
|
||||
<ele-pro-table
|
||||
ref="tableRef"
|
||||
row-key="menuId"
|
||||
@@ -36,7 +37,7 @@
|
||||
<a-button type="dashed" class="ele-btn-icon" @click="removeBatch">
|
||||
批量删除
|
||||
</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-space>
|
||||
@@ -130,8 +131,8 @@
|
||||
:menu-list="menuData"
|
||||
@done="reload"
|
||||
/>
|
||||
<Delete v-model:visible="showRemoveBatch" @done="reload" />
|
||||
<Clone v-model:visible="showClone" @done="reload" />
|
||||
<Delete v-model:visible="showRemoveBatch" :menu-list="menuData" :data="current" :parent-id="parentId" @done="reload" />
|
||||
<Clone v-model:visible="showClone" :menu-list="menuData" :data="current" :parent-id="parentId" @done="reload" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user