chore(config): 初始化项目配置文件

- 添加 .editorconfig 文件统一代码风格
- 配置 .env.development 环境变量文件
- 创建 .env.example 环境变量示例文件
- 设置 .eslintignore 忽略检查规则
- 配置 .eslintrc.js 代码检查规则
- 添加 .gitignore 文件忽略版本控制
- 设置 .prettierignore 忽略格式化规则
- 新增隐私政策HTML页面文件
- 创建API密钥编辑组件基础结构
This commit is contained in:
2025-12-15 13:29:17 +08:00
commit 1856a611ce
877 changed files with 176918 additions and 0 deletions

View File

@@ -0,0 +1,133 @@
<!-- 顶栏消息通知 -->
<template>
<a-dropdown
v-model:visible="visible"
placement="bottom"
:trigger="['click']"
:overlay-style="{ padding: '0 10px' }"
>
<a-badge :count="unreadNum" dot class="ele-notice-trigger" :offset="[4, 6]">
<bell-outlined style="padding: 8px 0"/>
</a-badge>
</a-dropdown>
</template>
<script lang="ts" setup>
import {computed, ref} from 'vue';
// import {useChatStore} from '@/store/modules/chat';
import {useUserStore} from '@/store/modules/user';
import {pageChatMessage} from '@/api/system/chat';
import {ChatMessage} from '@/api/system/chat/model';
// const chatStore = useChatStore();
const userStore = useUserStore();
// 是否显示
const visible = ref<boolean>(false);
// 通知数据
const notice = ref<ChatMessage[]>([]);
console.log(userStore.info?.userId,'.....userId')
// chatStore.connectSocketIO(userStore.info?.userId || 0);
// 未读数量
const unreadNum = computed(() => {
return notice.value.length;
});
/* 查询数据 */
// const query = () => {
// pageNotices({ status: 0 })
// .then((result) => {
// notice.value = result?.list;
// })
// .catch((e) => {
// message.error(e.message);
// });
//
// pageTodos({ status: 0 })
// .then((result) => {
// todo.value = result?.list;
// })
// .catch((e) => {
// message.error(e.message);
// });
// };
/* 查询未读数量 */
const queryUnReadNum = () => {
const toUserId = Number(userStore.info?.userId || 0);
console.log(toUserId);
const status = 0;
pageChatMessage({toUserId, status, keywords: ''}).then((result) => {
console.log(result);
notice.value = result?.list || [];
});
};
queryUnReadNum();
</script>
<script lang="ts">
import {BellOutlined} from '@ant-design/icons-vue';
export default {
name: 'HeaderNotice',
components: {
BellOutlined
}
};
</script>
<style lang="less">
.ele-notice-trigger.ant-badge {
color: inherit;
}
.ele-notice-pop {
&.ant-dropdown-menu {
padding: 0;
width: 286px;
max-width: 100%;
margin-top: 11px;
}
// 内容
.ant-list-item {
padding-left: 24px;
padding-right: 24px;
transition: background-color 0.3s;
cursor: pointer;
&:hover {
background: hsla(0, 0%, 60%, 0.05);
}
}
.ant-tag {
margin: 0;
}
// 操作按钮
.ele-notice-actions {
border-top: 1px solid hsla(0, 0%, 60%, 0.15);
& > .ele-cell-content {
line-height: 46px;
text-align: center;
transition: background-color 0.3s;
cursor: pointer;
color: inherit;
&:hover {
background: hsla(0, 0%, 60%, 0.05);
}
}
}
}
.ele-cell-content {
padding: 4px;
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,309 @@
<!-- 顶栏右侧区域 -->
<template>
<div class="ele-admin-header-tool">
<!-- 消息通知 -->
<div class="ele-admin-header-tool-item" @click="openUrl(`/user/notice`)">
<header-notice/>
</div>
<!-- 全屏切换 -->
<div
:class="[
'ele-admin-header-tool-item',
{ 'hidden-sm-and-down': styleResponsive }
]"
@click="toggleFullscreen"
>
<fullscreen-exit-outlined v-if="fullscreen"/>
<fullscreen-outlined v-else/>
</div>
<!-- 用户信息 -->
<div class="ele-admin-header-tool-item">
<a-dropdown placement="bottom" :overlay-style="{ minWidth: '280px' }">
<div class="ele-admin-header-avatar">
<a-avatar :src="loginUser?.avatar">
<template v-if="!loginUser?.avatar" #icon>
<user-outlined/>
</template>
</a-avatar>
<span :class="{ 'hidden-sm-and-down': styleResponsive }">
{{ loginUser.nickname }}
</span>
<down-outlined style="margin-left: 6px"/>
</div>
<template #overlay>
<a-menu :selectable="false" @click="onUserDropClick">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<div class="user-profile">
<div class="user-info">
<div class="nickname">
<div class="ele-text-placeholder">
<span class="text-gray-500 text-lg">{{
website?.websiteName
}}</span>
</div>
<div class="text-gray-400">
用户ID<span class="ele-text-secondary" @click="copyText(loginUser.userId)">{{
loginUser.userId
}}</span>
</div>
<div class="text-gray-400">
昵称<span class="ele-text-secondary">{{
loginUser.nickname
}}</span>
</div>
<div class="text-gray-400">
手机<span class="ele-text-secondary">{{
loginUser.mobile
}}</span>
</div>
<div class="text-gray-400">
租户<span class="ele-text-secondary" @click="copyText(loginUser.tenantId)">{{
loginUser.tenantId
}}</span>
</div>
<div class="text-gray-400">
角色
<template
v-for="(item, index) in loginUser.roles"
:key="item.roleId"
>
<a-tag v-if="index === 0">
<div class="role-name">
<span>{{ item.roleName }}</span>
</div>
</a-tag>
</template>
</div>
</div>
</div>
</div>
</a-card>
<a-menu-divider/>
<a-menu-item key="profile">
<div class="ele-cell">
<div class="ele-cell-content"> {{ t('layout.header.profile') }}</div>
</div>
</a-menu-item>
<a-menu-divider/>
<a-menu-item key="password">
<div class="ele-cell">
<div class="ele-cell-content"> {{ t('layout.header.password') }}</div>
</div>
</a-menu-item>
<template v-if="loginUser.username == 'admin' || loginUser.username == 'superAdmin'">
<a-menu-divider/>
<a-menu-item key="accessKey">
<div class="ele-cell">
<div class="ele-cell-content"> {{ t('layout.header.accessKey') }}</div>
</div>
</a-menu-item>
<a-menu-divider/>
<a-menu-item key="system">
<div class="ele-cell">
<div class="ele-cell-content"> {{ t('layout.header.system') }}</div>
</div>
</a-menu-item>
</template>
<a-menu-divider/>
<a-menu-item key="logout">
<div class="ele-cell" align="center">
<div class="ele-cell-content">
{{ t('layout.header.logout') }}
</div>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
<!-- 主题设置 -->
<!-- <div class="ele-admin-header-tool-item" @click="openSetting">-->
<!-- <more-outlined/>-->
<!-- </div>-->
</div>
<!-- 修改密码弹窗 -->
<password-modal v-model:visible="passwordVisible"/>
<!-- 主题设置抽屉 -->
<setting-drawer v-model:visible="settingVisible"/>
<!-- 二维码 -->
<Qrcode v-model:visible="showQrcode" :data="SiteUrl" @done="hideShare"/>
</template>
<script lang="ts" setup>
import {computed, createVNode, ref} from 'vue';
import {useRouter} from 'vue-router';
import {useI18n} from 'vue-i18n';
import {Modal} from 'ant-design-vue/es';
import {
DownOutlined,
ExclamationCircleOutlined,
FullscreenOutlined,
FullscreenExitOutlined
} from '@ant-design/icons-vue';
import {storeToRefs} from 'pinia';
import {copyText, openUrl} from '@/utils/common';
import {useThemeStore} from '@/store/modules/theme';
import HeaderNotice from './header-notice.vue';
import PasswordModal from './password-modal.vue';
import SettingDrawer from './setting-drawer.vue';
import {useUserStore} from '@/store/modules/user';
import {logout} from '@/utils/page-tab-util';
import {listRoles} from '@/api/system/role';
import { useSiteStore } from '@/store/modules/site';
import Qrcode from "@/components/QrCode/index.vue";
import {AppInfo} from "@/api/cms/cmsWebsite/model";
import {getCmsWebsiteFieldByCode} from "@/api/cms/cmsWebsiteField";
// 是否开启响应式布局
const themeStore = useThemeStore();
const {styleResponsive} = storeToRefs(themeStore);
const SiteUrl = localStorage.getItem('SiteUrl');
// 是否显示二维码
const showQrcode = ref(false);
// 使用网站信息 store
const siteStore = useSiteStore();
// const TENANT_ID = localStorage.getItem('TenantId');
// const TENANT_NAME = localStorage.getItem('TenantName');
const emit = defineEmits<{
(e: 'fullscreen'): void;
}>();
defineProps<{
// 是否是全屏
fullscreen: boolean;
}>();
const {push} = useRouter();
const {t} = useI18n();
const userStore = useUserStore();
// 是否显示修改密码弹窗
const passwordVisible = ref(false);
// 是否显示主题设置抽屉
const settingVisible = ref(false);
// 当前用户信息
const loginUser = computed(() => userStore.info ?? {});
const website = ref<AppInfo>();
/* 用户信息下拉点击 */
const onUserDropClick = ({key}) => {
if (key === 'password') {
passwordVisible.value = true;
} else if (key === 'profile') {
push('/user/profile');
} else if (key === 'accessKey') {
push('/system/access-key');
} else if (key === 'taskAdd') {
push('/user/task/add');
} else if (key === 'myTask') {
push('/user/task/index');
} else if (key === 'skin') {
settingVisible.value = true;
} else if (key === 'system') {
push('/system/setting');
} else if (key === 'logout') {
// 退出登录
Modal.confirm({
title: t('layout.logout.title'),
content: t('layout.logout.message'),
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
logout();
}
});
}
};
const hideShare = () => {
showQrcode.value = false;
}
/* 打开主题设置抽屉 */
// const openSetting = () => {
// settingVisible.value = true;
// };
/* 切换全屏 */
const toggleFullscreen = () => {
emit('fullscreen');
};
const reload = () => {
// 查询网站信息
if (!localStorage.getItem('WebsiteId')) {
siteStore.fetchSiteInfo().catch(console.error);
}
// 查询商户角色的roleId
if (!localStorage.getItem('RoleIdByMerchant')) {
listRoles({roleCode: 'merchant'}).then((res) => {
if (res.length > 0) {
const item = res[0];
localStorage.setItem('RoleIdByMerchant', `${item.roleId}`);
localStorage.setItem('RoleNameByMerchant', `${item.roleName}`);
}
});
}
// 检查是否启动自定义接口
if(import.meta.env.PROD){
getCmsWebsiteFieldByCode('ApiUrl').then(res => {
if(res){
localStorage.setItem('ApiUrl', `${res.value}`);
}
})
}
};
reload();
</script>
<script lang="ts">
import * as icons from '@/layout/menu-icons';
export default {
components: icons,
data() {
return {
iconData: [
{
title: '已引入的图标',
icons: Object.keys(icons)
}
]
};
}
};
</script>
<style lang="less" scoped>
.ant-select {
color: #bfbfbf;
}
.user-profile {
display: flex;
.user-info {
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
.nickname {
display: flex;
flex-direction: column;
}
}
}
.ele-admin-header-tool-item {
:deep(.ant-select-tree-switcher) {
background-color: #ff00fe;
}
:deep(.ant-select-tree-indent) {
display: none;
}
}
</style>

View File

@@ -0,0 +1,33 @@
<!-- 顶栏消息通知 -->
<template>
<a-popover>
<template #content>
<p class="ele-text-placeholder" style="text-align: center">
请使用微信扫描二维码
</p>
<a-image
:preview="false"
:width="200"
:height="200"
src="https://file.wsdns.cn/20230518/6b3c88423e0e437f81b3adbf63d30cba.png"
/>
<p class="ele-text-placeholder" style="text-align: center">
客服电话0771-5386339
</p>
</template>
<div class="ele-admin-header-tool-item">
<wechat-outlined style="padding: 8px 0" />
</div>
</a-popover>
</template>
<script lang="ts" setup>
import { WechatOutlined } from '@ant-design/icons-vue';
</script>
<style lang="less">
.wechat-box {
.title {
line-height: 40px;
}
}
</style>

View File

@@ -0,0 +1,52 @@
<!-- 国际化语言切换组件 -->
<template>
<a-dropdown
:placement="placement"
:overlay-style="{ minWidth: '120px', paddingTop: '17px' }"
>
<slot>
<global-outlined :style="style" />
</slot>
<template #overlay>
<a-menu :selected-keys="language" @click="changeLanguage">
<a-menu-item key="en">English</a-menu-item>
<a-menu-item key="zh_CN">简体中文</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
<script lang="ts" setup>
import type { CSSProperties } from 'vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { GlobalOutlined } from '@ant-design/icons-vue';
import { I18N_CACHE_NAME } from '@/config/setting';
withDefaults(
defineProps<{
// dropdown placement
placement?: any;
// 自定义样式
style?: CSSProperties;
}>(),
{
placement: 'bottom',
style: () => {
return { transform: 'scale(1.08)' };
}
}
);
const { locale } = useI18n();
// 当前显示语言
const language = computed(() => [locale.value]);
/* 切换语言 */
const changeLanguage = ({ key }) => {
locale.value = key;
localStorage.setItem(I18N_CACHE_NAME, key);
window.location.reload();
};
</script>

View File

@@ -0,0 +1,17 @@
<template>
<span>{{ item.meta.title }}</span>
<div v-if="item.meta && item.meta.badge" class="ele-menu-badge">
<a-badge
:count="item.meta.badge"
:number-style="{ background: item.meta.badgeColor as string }"
/>
</div>
</template>
<script lang="ts" setup>
import type { MenuItemType } from 'ele-admin-pro/es';
defineProps<{
item: MenuItemType;
}>();
</script>

View File

@@ -0,0 +1,26 @@
<!-- 全局页脚 -->
<template>
<div v-if="config.setting?.showAdminCopyright" class="ele-text-center" style="padding: 16px 0">
<div class="ele-text-secondary" style="margin-top: 8px">
{{ t('layout.footer.copyright') }}
</div>
</div>
</template>
<script lang="ts" setup>
import {useI18n} from 'vue-i18n';
import {useWebsiteSettingStore} from "@/store/modules/setting";
import {getSettingByKey} from "@/api/system/setting";
const {t} = useI18n();
const config = useWebsiteSettingStore();
const reload = async () => {
if (!config.setting) {
const info = await getSettingByKey('privacy');
config.setSetting(info)
}
}
reload();
</script>

View File

@@ -0,0 +1,148 @@
<!-- 修改密码弹窗 -->
<template>
<ele-modal
:width="420"
:title="t('layout.header.password')"
:visible="visible"
:confirm-loading="loading"
:body-style="{ paddingBottom: '16px' }"
@update:visible="updateVisible"
@cancel="onCancel"
@ok="onOk"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { sm: 6 } : { flex: '90px' }"
:wrapper-col="styleResponsive ? { sm: 18 } : { flex: '1' }"
>
<a-form-item :label="t('login.oldPassword')" name="oldPassword">
<a-input-password
v-model:value="form.oldPassword"
placeholder="请输入旧密码"
/>
</a-form-item>
<a-form-item :label="t('login.newPassword')" name="password">
<a-input-password
v-model:value="form.password"
placeholder="请输入新密码"
/>
</a-form-item>
<a-form-item :label="t('login.confirm')" name="password2">
<a-input-password
v-model:value="form.password2"
placeholder="请再次输入新密码"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { updatePassword } from '@/api/layout';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const { t } = useI18n();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
defineProps<{
visible: boolean;
}>();
//
const formRef = ref<FormInstance | null>(null);
// 提交loading
const loading = ref<boolean>(false);
// 表单数据
const { form, resetFields } = useFormData({
oldPassword: '',
password: '',
password2: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
oldPassword: [
{
required: true,
type: 'string',
message: '请输入旧密码',
trigger: 'blur'
}
],
password: [
{
required: true,
type: 'string',
message: '请输入新密码',
trigger: 'blur'
}
],
password2: [
{
required: true,
type: 'string',
validator: async (_rule: Rule, value: string) => {
if (!value) {
return Promise.reject('请再次输入新密码');
}
if (value !== form.password) {
return Promise.reject('两次输入密码不一致');
}
return Promise.resolve();
},
trigger: 'blur'
}
]
});
/* 修改visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
/* 保存修改 */
const onOk = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
updatePassword(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 关闭回调 */
const onCancel = () => {
resetFields();
formRef.value?.clearValidate();
loading.value = false;
};
</script>

View File

@@ -0,0 +1,747 @@
<!-- 主题设置抽屉 -->
<template>
<a-drawer
:width="280"
:visible="visible"
:body-style="{ padding: 0 }"
:header-style="{
position: 'absolute',
top: '16px',
right: 0,
padding: 0,
background: 'none'
}"
:z-index="1001"
@update:visible="updateVisible"
>
<div :class="['ele-setting-wrapper', { 'ele-setting-dark': darkMode }]">
<div class="ele-setting-title">{{ t('layout.setting.title') }}</div>
<!-- 侧栏风格 -->
<div
v-if="layoutStyle !== 'top'"
class="ele-setting-theme ele-text-primary"
>
<a-tooltip :title="t('layout.setting.sideStyles.dark')">
<div
class="ele-bg-base ele-side-dark"
@click="updateSideStyle('dark')"
>
<check-outlined v-if="sideStyle === 'dark'"/>
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.sideStyles.light')">
<div class="ele-bg-base" @click="updateSideStyle('light')">
<check-outlined v-if="sideStyle === 'light'"/>
</div>
</a-tooltip>
</div>
<!-- 顶栏风格 -->
<div class="ele-setting-theme ele-text-primary">
<a-tooltip :title="t('layout.setting.headStyles.light')">
<div
class="ele-bg-base ele-head-light"
@click="updateHeadStyle('light')"
>
<check-outlined v-if="headStyle === 'light'"/>
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.headStyles.dark')">
<div
class="ele-bg-base ele-head-dark"
@click="updateHeadStyle('dark')"
>
<check-outlined v-if="headStyle === 'dark'"/>
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.headStyles.primary')">
<div
class="ele-bg-base ele-head-primary"
@click="updateHeadStyle('primary')"
>
<div class="ele-bg-primary"></div>
<check-outlined v-if="headStyle === 'primary'"/>
</div>
</a-tooltip>
</div>
<!-- 主题色 -->
<div class="ele-setting-colors">
<div
v-for="item in themes"
:key="item.name"
:style="{ 'background-color': item.color || item.value }"
class="ele-setting-color-item"
@click="updateColor(item.value)"
>
<check-outlined v-if="item.value ? item.value === color : !color"/>
<a-tooltip :title="t('layout.setting.colors.' + item.name)">
<div class="ele-setting-color-tooltip"></div>
</a-tooltip>
</div>
<!-- 颜色选择器 -->
<ele-color-picker
v-model:value="colorValue"
:predefine="predefineColors"
custom-class="ele-setting-color-picker"
@change="updateColor"
/>
</div>
<!-- 暗黑模式 -->
<div class="ele-setting-item">
<div class="setting-item-title">{{ t('layout.setting.darkMode') }}</div>
<div class="setting-item-control">
<a-switch size="small" :checked="darkMode" @change="updateDarkMode"/>
</div>
</div>
<a-divider/>
<!-- 导航布局 -->
<div
:class="[
'ele-setting-title ele-text-secondary',
{ 'hidden-xs-only': styleResponsive }
]"
>
{{ t('layout.setting.layoutStyle') }}
</div>
<div
:class="[
'ele-setting-theme ele-text-primary',
{ 'hidden-xs-only': styleResponsive }
]"
>
<a-tooltip :title="t('layout.setting.layoutStyles.side')">
<div
class="ele-bg-base ele-side-dark"
@click="updateLayoutStyle('side')"
>
<check-outlined v-if="layoutStyle === 'side'"/>
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.layoutStyles.top')">
<div
class="ele-bg-base ele-head-dark ele-layout-top"
@click="updateLayoutStyle('top')"
>
<check-outlined v-if="layoutStyle === 'top'"/>
</div>
</a-tooltip>
<a-tooltip :title="t('layout.setting.layoutStyles.mix')">
<div
class="ele-bg-base ele-layout-mix"
@click="updateLayoutStyle('mix')"
>
<check-outlined v-if="layoutStyle === 'mix'"/>
</div>
</a-tooltip>
</div>
<!-- 侧栏菜单布局 -->
<div
v-if="layoutStyle !== 'top'"
:class="['ele-setting-item', { 'hidden-xs-only': styleResponsive }]"
>
<div class="setting-item-title">
{{ t('layout.setting.sideMenuStyle') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="sideMenuStyle === 'mix'"
@change="updateSideMenuStyle"
/>
</div>
</div>
<!-- 内容区域定宽 -->
<div :class="['ele-setting-item', { 'hidden-xs-only': styleResponsive }]">
<div class="setting-item-title">
{{ t('layout.setting.bodyFull') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="!bodyFull"
@change="updateBodyFull"
/>
</div>
</div>
<a-divider :class="{ 'hidden-xs-only': styleResponsive }"/>
<div class="ele-setting-title ele-text-secondary">
{{ t('layout.setting.other') }}
</div>
<!-- 固定主体 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.fixedBody') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="fixedBody"
@change="updateFixedBody"
/>
</div>
</div>
<!-- 固定顶栏 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.fixedHeader') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:disabled="fixedBody"
:checked="fixedHeader"
@change="updateFixedHeader"
/>
</div>
</div>
<!-- 固定侧栏 -->
<div
v-if="layoutStyle !== 'top'"
:class="['ele-setting-item', { 'hidden-xs-only': styleResponsive }]"
>
<div class="setting-item-title">
{{ t('layout.setting.fixedSidebar') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:disabled="fixedBody"
:checked="fixedSidebar"
@change="updateFixedSidebar"
/>
</div>
</div>
<!-- logo 置于顶栏 -->
<div
v-if="layoutStyle !== 'top'"
:class="['ele-setting-item', { 'hidden-xs-only': styleResponsive }]"
>
<div class="setting-item-title">
{{ t('layout.setting.logoAutoSize') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="logoAutoSize"
@change="updateLogoAutoSize"
/>
</div>
</div>
<!-- 移动端响应式 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.styleResponsive') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="styleResponsive"
@change="updateStyleResponsive"
/>
</div>
</div>
<!-- 侧栏彩色图标 -->
<div v-if="layoutStyle !== 'top'" class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.colorfulIcon') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="colorfulIcon"
@change="updateColorfulIcon"
/>
</div>
</div>
<!-- 侧栏排他展开 -->
<div v-if="layoutStyle !== 'top'" class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.sideUniqueOpen') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="sideUniqueOpen"
@change="updateSideUniqueOpen"
/>
</div>
</div>
<!-- 全局页脚 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.showFooter') }}
</div>
<div class="setting-item-control">
<a-switch
size="small"
:checked="showFooter"
@change="updateShowFooter"
/>
</div>
</div>
<!-- 色弱模式 -->
<div class="ele-setting-item">
<div class="setting-item-title">{{ t('layout.setting.weakMode') }}</div>
<div class="setting-item-control">
<a-switch size="small" :checked="weakMode" @change="updateWeakMode"/>
</div>
</div>
<!-- 页签 -->
<div class="ele-setting-item">
<div class="setting-item-title">{{ t('layout.setting.showTabs') }}</div>
<div class="setting-item-control">
<a-switch size="small" :checked="showTabs" @change="updateShowTabs"/>
</div>
</div>
<!-- 页签风格 -->
<div v-if="showTabs" class="ele-setting-item">
<div class="setting-item-title">{{ t('layout.setting.tabStyle') }}</div>
<div class="setting-item-control">
<a-select
size="small"
:value="tabStyle"
style="width: 80px"
@change="updateTabStyle"
>
<a-select-option value="default">
{{ t('layout.setting.tabStyles.default') }}
</a-select-option>
<a-select-option value="dot">
{{ t('layout.setting.tabStyles.dot') }}
</a-select-option>
<a-select-option value="card">
{{ t('layout.setting.tabStyles.card') }}
</a-select-option>
</a-select>
</div>
</div>
<!-- 切换动画 -->
<div class="ele-setting-item">
<div class="setting-item-title">
{{ t('layout.setting.transitionName') }}
</div>
<div class="setting-item-control">
<a-select
size="small"
:value="transitionName"
style="width: 100px"
@change="updateTransitionName"
>
<a-select-option value="slide-right">
{{ t('layout.setting.transitions.slideRight') }}
</a-select-option>
<a-select-option value="slide-bottom">
{{ t('layout.setting.transitions.slideBottom') }}
</a-select-option>
<a-select-option value="zoom-in">
{{ t('layout.setting.transitions.zoomIn') }}
</a-select-option>
<a-select-option value="zoom-out">
{{ t('layout.setting.transitions.zoomOut') }}
</a-select-option>
<a-select-option value="fade">
{{ t('layout.setting.transitions.fade') }}
</a-select-option>
</a-select>
</div>
</div>
<!-- 提示 -->
<a-divider/>
<a-alert show-icon type="warning" :message="t('layout.setting.tips')">
<template #icon>
<sound-outlined/>
</template>
</a-alert>
<!-- 重置 -->
<a-button block type="dashed" @click="resetSetting">
{{ t('layout.setting.reset') }}
</a-button>
</div>
</a-drawer>
</template>
<script lang="ts" setup>
import {ref} from 'vue';
import {useI18n} from 'vue-i18n';
import {storeToRefs} from 'pinia';
import {message} from 'ant-design-vue/es';
import {CheckOutlined, SoundOutlined} from '@ant-design/icons-vue';
import {messageLoading} from 'ele-admin-pro/es';
import type {
ThemeItem,
HeadStyleType,
SideStyleType,
LayoutStyleType,
TabStyleType
} from 'ele-admin-pro/es';
import {useThemeStore} from '@/store/modules/theme';
defineProps<{
// drawer 是否显示, v-model
visible: boolean;
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
}>();
const {t} = useI18n();
const themeStore = useThemeStore();
const {
showTabs,
showFooter,
headStyle,
sideStyle,
layoutStyle,
sideMenuStyle,
tabStyle,
transitionName,
fixedHeader,
fixedSidebar,
fixedBody,
bodyFull,
logoAutoSize,
colorfulIcon,
sideUniqueOpen,
styleResponsive,
weakMode,
darkMode,
color
} = storeToRefs(themeStore);
// 主题列表
const themes = ref<ThemeItem[]>([
{
name: 'default',
color: '#1890ff'
},
{
name: 'dust',
value: '#5f80c7'
},
{
name: 'sunset',
value: '#faad14'
},
{
name: 'volcano',
value: '#f5686f'
},
{
name: 'purple',
value: '#9266f9'
},
{
name: 'green',
value: '#33cc99'
},
{
name: 'geekblue',
value: '#32a2d4'
}
]);
// 颜色选择器预设颜色
const predefineColors = ref<string[]>([
'#f5222d',
'#fa541c',
'#fa8c16',
'#faad14',
'#a0d911',
'#52c41a',
'#13c2c2',
'#2f54eb',
'#722ed1',
'#eb2f96'
]);
// 颜色选择器选中颜色
const colorValue = ref<string | undefined>(void 0);
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const updateShowTabs = (value: boolean) => {
themeStore.setShowTabs(value);
};
const updateShowFooter = (value: boolean) => {
themeStore.setShowFooter(value);
};
const updateHeadStyle = (value: HeadStyleType) => {
themeStore.setHeadStyle(value);
};
const updateSideStyle = (value: SideStyleType) => {
themeStore.setSideStyle(value);
};
const updateLayoutStyle = (value: LayoutStyleType) => {
themeStore.setLayoutStyle(value);
};
const updateSideMenuStyle = (value: boolean) => {
themeStore.setSideMenuStyle(value ? 'mix' : 'default');
};
const updateTabStyle = (value: TabStyleType) => {
themeStore.setTabStyle(value);
};
const updateTransitionName = (value: string) => {
themeStore.setTransitionName(value);
};
const updateFixedHeader = (value: boolean) => {
themeStore.setFixedHeader(value);
};
const updateFixedSidebar = (value: boolean) => {
themeStore.setFixedSidebar(value);
};
const updateFixedBody = (value: boolean) => {
themeStore.setFixedBody(value);
};
const updateBodyFull = (value: boolean) => {
themeStore.setBodyFull(!value);
};
const updateLogoAutoSize = (value: boolean) => {
themeStore.setLogoAutoSize(value);
};
const updateStyleResponsive = (value: boolean) => {
themeStore.setStyleResponsive(value);
updateVisible(false);
};
const updateColorfulIcon = (value: boolean) => {
themeStore.setColorfulIcon(value);
};
const updateSideUniqueOpen = (value: boolean) => {
themeStore.setSideUniqueOpen(value);
};
const updateWeakMode = (value: boolean) => {
themeStore.setWeakMode(value);
};
const updateDarkMode = (value: boolean) => {
doWithLoading(() => themeStore.setDarkMode(value));
};
const updateColor = (value?: string) => {
doWithLoading(() => themeStore.setColor(value));
};
const resetSetting = () => {
doWithLoading(() => themeStore.resetSetting());
};
const doWithLoading = (fun: () => Promise<void>) => {
const hide = messageLoading('正在加载主题..', 0);
setTimeout(() => {
fun()
.then(() => {
hide();
initColorValue();
})
.catch((e) => {
hide();
console.error(e);
message.error('主题加载失败');
});
}, 0);
};
const initColorValue = () => {
if (color?.value && !themes.value.some((t) => t.value === color.value)) {
colorValue.value = color.value;
} else {
colorValue.value = void 0;
}
};
initColorValue();
</script>
<style lang="less">
.ele-setting-wrapper {
padding: 20px 18px;
.ele-setting-title {
font-size: 13px;
margin-bottom: 15px;
}
/* 主题风格 */
.ele-setting-theme > div {
width: 52px;
height: 36px;
line-height: 1;
border-radius: 3px;
margin: 0 20px 30px 0;
padding: 16px 0 0 26px;
box-sizing: border-box;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
display: inline-block;
vertical-align: top;
position: relative;
overflow: hidden;
cursor: pointer;
transition: background-color 0.2s;
&:before,
&:after,
& > .ele-bg-primary {
content: '';
width: 100%;
height: 10px;
background: #fff;
position: absolute;
left: 0;
top: 0;
transition: background-color 0.2s;
}
&:after {
width: 14px;
height: 100%;
}
&.ele-side-dark:after,
&.ele-head-dark:before,
&.ele-layout-mix:before,
&.ele-layout-mix:after {
background: #001529;
}
&.ele-head-light:before,
&.ele-head-dark:before,
& > .ele-bg-primary {
z-index: 1;
}
&.ele-layout-top {
padding-left: 19px;
&:after {
display: none;
}
}
}
/* 主题色选择 */
.ele-setting-colors {
color: #fff;
margin-bottom: 20px;
}
.ele-setting-color-item {
width: 20px;
height: 20px;
line-height: 20px;
border-radius: 2px;
margin: 8px 8px 0 0;
display: inline-block;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
vertical-align: top;
position: relative;
text-align: center;
cursor: pointer;
.ele-setting-color-tooltip {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
/* 主题配置项 */
.ele-setting-item {
display: flex;
align-items: center;
margin-bottom: 20px;
.setting-item-title {
flex: 1;
line-height: 28px;
}
.setting-item-control {
line-height: 1;
}
}
.ant-divider {
margin-bottom: 20px;
}
.ant-alert + .ant-btn {
margin-top: 12px;
}
/* 暗黑模式 */
&.ele-setting-dark .ele-setting-theme > div {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.55);
&:before,
&:after,
& > .ele-bg-primary {
background: #1f1f1f;
}
&.ele-side-dark:after,
&.ele-head-dark:before,
&.ele-layout-mix:before,
&.ele-layout-mix:after {
background: #262626;
}
}
}
/* 颜色选择器 */
.ele-setting-color-picker.ele-color-picker-trigger {
padding: 0;
width: 20px;
height: 20px;
margin-top: 8px;
border: none !important;
background: none !important;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
& > .ele-color-picker-trigger-inner {
background: none;
&.is-empty {
background: conic-gradient(from 90deg at 50% 50%,
rgb(255, 0, 0) -19.41deg,
rgb(255, 0, 0) 18.76deg,
rgb(255, 138, 0) 59.32deg,
rgb(255, 230, 0) 99.87deg,
rgb(20, 255, 0) 141.65deg,
rgb(0, 163, 255) 177.72deg,
rgb(5, 0, 255) 220.23deg,
rgb(173, 0, 255) 260.13deg,
rgb(255, 0, 199) 300.69deg,
rgb(255, 0, 0) 340.59deg,
rgb(255, 0, 0) 378.76deg);
& + .ele-color-picker-trigger-arrow {
display: none;
}
}
}
}
</style>

372
src/layout/index.vue Normal file
View File

@@ -0,0 +1,372 @@
<template>
<ele-pro-layout
:menus="menus"
:tabs="tabs"
:collapse="collapse"
:side-nav-collapse="sideNavCollapse"
:body-fullscreen="bodyFullscreen"
:show-tabs="showTabs"
:show-footer="showFooter"
:head-style="headStyle"
:side-style="sideStyle"
:layout-style="layoutStyle"
:side-menu-style="sideMenuStyle"
:tab-style="tabStyle"
:fixed-header="fixedHeader"
:fixed-sidebar="fixedSidebar"
:fixed-body="fixedBody"
:body-full="bodyFull"
:logo-auto-size="logoAutoSize"
:colorful-icon="colorfulIcon"
:side-unique-open="sideUniqueOpen"
:style-responsive="styleResponsive"
:hide-footers="HIDE_FOOTERS"
:hide-sidebars="HIDE_SIDEBARS"
:repeatable-tabs="REPEATABLE_TABS"
:home-title="HOME_TITLE || t('layout.home')"
:home-path="HOME_PATH"
:layout-path="LAYOUT_PATH"
:redirect-path="REDIRECT_PATH"
:locale="locale"
:i18n="i18n"
@update:collapse="updateCollapse"
@update:side-nav-collapse="updateSideNavCollapse"
@update:body-fullscreen="updateBodyFullscreen"
@tab-add="addPageTab"
@tab-remove="removePageTab"
@tab-remove-all="removeAllPageTab"
@tab-remove-left="removeLeftPageTab"
@tab-remove-right="removeRightPageTab"
@tab-remove-other="removeOtherPageTab"
@reload-page="reloadPageTab"
@logo-click="onLogoClick"
@screen-size-change="screenSizeChange"
@set-home-components="setHomeComponents"
@tab-context-menu="onTabContextMenu"
>
<!-- 路由出口 -->
<router-layout />
<!-- logo 图标 -->
<template #logo>
<a-space class="sys-logo flex items-center justify-center">
<AAvatar
shape="square"
:preview="false"
:size="28"
v-if="logoPath"
:src="logoPath"
alt="logo"
/>
<template v-if="!collapse">
{{ projectName }}
</template>
</a-space>
</template>
<!-- 顶栏右侧区域 -->
<template #right>
<header-tools :fullscreen="fullscreen" @fullscreen="onFullscreen" />
</template>
<!-- 全局页脚 -->
<template #footer>
<page-footer />
</template>
<!-- 菜单图标 -->
<template #icon="{ icon }">
<component :is="icon" class="ant-menu-item-icon" />
</template>
<!-- 自定义菜单标题增加徽章小红点 -->
<template #title="{ item }">
<menu-title :item="item" />
</template>
<template #top-title="{ item }">
<menu-title :item="item" />
</template>
<template #nav-title="{ item }">
<menu-title :item="item" />
</template>
</ele-pro-layout>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { useRouter } from 'vue-router';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { message } from 'ant-design-vue/es';
import { toggleFullscreen, isFullscreen } from 'ele-admin-pro/es';
import { useUserStore } from '@/store/modules/user';
import { useThemeStore } from '@/store/modules/theme';
import RouterLayout from '@/components/RouterLayout/index.vue';
import HeaderTools from './components/header-tools.vue';
import PageFooter from './components/page-footer.vue';
import MenuTitle from './components/menu-title.vue';
import {
HIDE_SIDEBARS,
HIDE_FOOTERS,
REPEATABLE_TABS,
HOME_TITLE,
HOME_PATH,
LAYOUT_PATH,
REDIRECT_PATH,
I18N_ENABLE
} from '@/config/setting';
import {
addPageTab,
removePageTab,
removeAllPageTab,
removeLeftPageTab,
removeRightPageTab,
removeOtherPageTab,
reloadPageTab,
setHomeComponents
} from '@/utils/page-tab-util';
import type { TabCtxMenuOption } from 'ele-admin-pro/es/ele-pro-layout/types';
import { hasPermission } from '@/utils/permission';
const { push } = useRouter();
const { t, locale } = useI18n();
const userStore = useUserStore();
// 是否刷新页面
if(localStorage.getItem('Reload')){
window.location.reload();
localStorage.removeItem('Reload')
}
const themeStore = useThemeStore();
// 网站名称
const projectName = t('layout.system');
// 网站LOGO
const logoPath = '/logo.png';
// 是否全屏
const fullscreen = ref(false);
// 菜单数据
const { menus } = storeToRefs(userStore);
// 布局风格
const {
tabs,
collapse,
sideNavCollapse,
bodyFullscreen,
showTabs,
showFooter,
headStyle,
sideStyle,
layoutStyle,
sideMenuStyle,
tabStyle,
fixedHeader,
fixedSidebar,
fixedBody,
bodyFull,
logoAutoSize,
colorfulIcon,
sideUniqueOpen,
styleResponsive
} = storeToRefs(themeStore);
/* 侧栏折叠切换 */
const updateCollapse = (value: boolean) => {
themeStore.setCollapse(value);
};
/* 双侧栏一级折叠切换 */
const updateSideNavCollapse = (value: boolean) => {
themeStore.setSideNavCollapse(value);
};
/* 内容区域全屏切换 */
const updateBodyFullscreen = (value: boolean) => {
themeStore.setBodyFullscreen(value);
};
/* logo 点击事件 */
const onLogoClick = (isHome: boolean) => {
if (hasPermission('sys:company:profile')) {
push(`/`);
return false;
}
isHome || push(LAYOUT_PATH);
};
/* 监听屏幕尺寸改变 */
const screenSizeChange = () => {
themeStore.updateScreenSize();
fullscreen.value = isFullscreen();
};
/* 全屏切换 */
const onFullscreen = () => {
try {
fullscreen.value = toggleFullscreen();
} catch (e) {
message.error('您的浏览器不支持全屏模式');
}
};
/* 页签右键菜单点击事件 */
const onTabContextMenu = ({
key,
tabKey,
item,
active
}: TabCtxMenuOption) => {
switch (key) {
case 'reload': // 刷新
reloadPageTab({
isHome: !item,
fullPath: item?.fullPath ?? tabKey
});
break;
case 'close': // 关闭当前
removePageTab({
key: item?.fullPath ?? tabKey,
active
});
break;
case 'left': // 关闭左侧
removeLeftPageTab({
key: tabKey,
active
});
break;
case 'right': // 关闭右侧
removeRightPageTab({
key: tabKey,
active
});
break;
case 'other': // 关闭其他
removeOtherPageTab({
key: tabKey,
active
});
break;
}
};
/* 菜单标题国际化 */
const i18n = (_path: string, key?: string) => {
if (!I18N_ENABLE || !key) {
return;
}
const k = 'route.' + key + '._name';
const title = t(k);
if (title !== k) {
return title;
}
};
</script>
<script lang="ts">
import * as MenuIcons from './menu-icons';
export default {
name: 'EleLayout',
components: MenuIcons
};
</script>
<style lang="less">
// 侧栏菜单徽章样式,定位在右侧垂直居中并调小尺寸
.ele-menu-badge {
position: absolute;
top: 50%;
right: 14px;
line-height: 1;
margin-top: -9px;
font-size: 0;
.ant-badge-count {
height: 18px;
line-height: 18px;
border-radius: 9px;
box-shadow: none;
min-width: 18px;
padding: 0 4px;
}
.ant-scroll-number-only {
height: 18px;
& > p.ant-scroll-number-only-unit {
height: 18px;
}
}
}
// 父级菜单标题中右侧多定位一点,避免与箭头重合
.ant-menu-submenu-title > .ant-menu-title-content .ele-menu-badge {
right: 36px;
}
// 折叠悬浮中样式调整
.ant-menu-submenu-popup {
.ant-menu-submenu-title > .ant-menu-title-content .ele-menu-badge {
right: 30px;
}
}
// 顶栏菜单标题中样式调整
.ele-admin-header-nav{
display: flex;
justify-content: center;
}
.ele-admin-header-nav > .ant-menu {
& > .ant-menu-item,
& > .ant-menu-submenu > .ant-menu-submenu-title {
& > .ant-menu-title-content .ele-menu-badge {
position: static;
right: auto;
top: auto;
display: inline-block;
vertical-align: 5px;
margin: 0 0 0 4px;
}
}
}
// 双侧栏时一级侧栏菜单中样式调整,定位在右上角
.ele-admin-sidebar-nav-menu > .ant-menu {
& > .ant-menu-item,
& > .ant-menu-submenu > .ant-menu-submenu-title {
& > .ant-menu-title-content .ele-menu-badge {
top: 0;
right: 0;
margin: 0;
}
}
}
// 双侧栏时一级侧栏菜单折叠后样式调整
.ele-admin-nav-collapse .ele-admin-sidebar-nav-menu > .ant-menu {
& > .ant-menu-item,
& > .ant-menu-submenu > .ant-menu-submenu-title {
& > .ant-menu-title-content .ele-menu-badge {
top: 0;
right: 0;
}
}
}
// 菜单折叠后在 tooltip 中不显示徽章
.ant-tooltip-inner .ele-menu-badge {
display: none;
}
// logo
.ele-admin-logo {
margin-right: 12px;
img {
border-radius: 4px !important;
}
}
svg.md-editor-icon{
width: 27px !important;
height: 27px !important;
}
</style>

153
src/layout/menu-icons.ts Normal file
View File

@@ -0,0 +1,153 @@
/** 菜单用到的图标 */
export {
HomeOutlined,
SettingOutlined,
TeamOutlined,
DesktopOutlined,
FileTextOutlined,
TableOutlined,
AppstoreOutlined,
CheckCircleOutlined,
ExclamationCircleOutlined,
UserOutlined,
TagOutlined,
IdcardOutlined,
BarChartOutlined,
AuditOutlined,
PicLeftOutlined,
BellOutlined,
CloseCircleOutlined,
QuestionCircleOutlined,
SoundOutlined,
ApartmentOutlined,
DashboardOutlined,
OneToOneOutlined,
DragOutlined,
InteractionOutlined,
BankOutlined,
BlockOutlined,
CheckSquareOutlined,
ProfileOutlined,
WarningOutlined,
FolderOutlined,
YoutubeOutlined,
ControlOutlined,
EllipsisOutlined,
CalendarOutlined,
AppstoreAddOutlined,
FileSearchOutlined,
EnvironmentOutlined,
CompassOutlined,
FontSizeOutlined,
SketchOutlined,
BgColorsOutlined,
PrinterOutlined,
QrcodeOutlined,
BarcodeOutlined,
PictureOutlined,
LinkOutlined,
AlertOutlined,
HistoryOutlined,
ChromeOutlined,
CodeOutlined,
AntDesignOutlined,
ReadOutlined,
CrownOutlined,
LaptopOutlined,
ShoppingCartOutlined,
SkinOutlined,
ShopOutlined,
ShoppingOutlined,
AliyunOutlined,
GiftOutlined,
InsuranceOutlined,
FileZipOutlined,
SearchOutlined,
RedEnvelopeOutlined,
MoneyCollectOutlined,
TagsOutlined,
PayCircleOutlined,
RocketOutlined,
BarsOutlined,
WalletOutlined,
UngroupOutlined,
ToolOutlined,
HourglassOutlined,
FormatPainterOutlined,
CarOutlined,
DownloadOutlined,
UploadOutlined,
MailOutlined,
CloudOutlined,
ClearOutlined,
CoffeeOutlined,
CopyrightOutlined,
CompressOutlined,
CalculatorOutlined,
AimOutlined,
AudioOutlined,
BugOutlined,
CloudSyncOutlined,
CommentOutlined,
DisconnectOutlined,
DingtalkOutlined,
ExpandOutlined,
DollarOutlined,
FolderOpenOutlined,
ExperimentOutlined,
ForkOutlined,
FolderAddOutlined,
FunnelPlotOutlined,
GlobalOutlined,
LikeOutlined,
LayoutOutlined,
MergeCellsOutlined,
MehOutlined,
MobileOutlined,
NodeExpandOutlined,
PaperClipOutlined,
ScanOutlined,
ShakeOutlined,
ThunderboltOutlined,
TrademarkOutlined,
UnlockOutlined,
UsbOutlined,
WhatsAppOutlined,
WomanOutlined,
WifiOutlined,
VerifiedOutlined,
UserSwitchOutlined,
UsergroupAddOutlined,
UsergroupDeleteOutlined,
UserAddOutlined,
UserDeleteOutlined,
TranslationOutlined,
TransactionOutlined,
TrophyOutlined,
StarOutlined,
SplitCellsOutlined,
ShareAltOutlined,
RestOutlined,
PartitionOutlined,
MedicineBoxOutlined,
HeartOutlined,
GatewayOutlined,
FlagOutlined,
FireOutlined,
FieldNumberOutlined,
FieldStringOutlined,
AppleOutlined,
AndroidOutlined,
GithubOutlined,
Html5Outlined,
QqOutlined,
AlipayOutlined,
RedditOutlined,
InstagramOutlined,
WechatOutlined,
BorderOutlined,
PieChartOutlined,
AreaChartOutlined,
KeyOutlined,
LockOutlined
} from '@ant-design/icons-vue';