feat: 左侧菜单样式&个人菜单

This commit is contained in:
baiqi 2023-06-25 15:47:31 +08:00 committed by 刘瑞斌
parent 8920a12690
commit 6378e1ea30
15 changed files with 257 additions and 59 deletions

View File

@ -243,22 +243,39 @@
}
/** 下拉菜单 **/
.arco-dropdown {
.arco-dropdown,
.arco-trigger-menu {
border: 0.5px solid var(--color-text-n8);
box-shadow: 0 3px 14px 2px rgb(0 0 0 / 5%), 0 8px 10px 1px rgb(0 0 0 / 6%), 0 5px 5px -3px rgb(0 0 0 / 10%);
.arco-dropdown-list {
.arco-dropdown-list,
.arco-trigger-menu-inner {
@apply relative flex w-full flex-col overflow-hidden;
.arco-dropdown-option {
@apply w-auto;
.arco-dropdown-option,
.arco-trigger-menu-item {
@apply flex w-auto items-center;
margin: 0 6px;
padding: 3px 8px;
height: 30px;
border-radius: var(--border-radius-small);
line-height: normal;
&:hover {
background-color: rgb(var(--primary-1));
}
}
.ms-dropdown-divider {
margin: 6px 0;
}
.arco-trigger-menu-item {
width: 108px;
.arco-icon {
margin-right: 8px;
}
}
.arco-trigger-menu-selected {
color: rgb(var(--primary-7));
background-color: rgb(var(--primary-1));
}
}
}

View File

@ -1,20 +1,25 @@
<script lang="tsx">
import { defineComponent, ref, h, compile, computed } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { useRoute, useRouter, RouteRecordRaw } from 'vue-router';
import { useI18n } from '@/hooks/useI18n';
import useUser from '@/hooks/useUser';
import type { RouteMeta } from 'vue-router';
import { useAppStore } from '@/store';
import { useAppStore, useUserStore } from '@/store';
import { listenerRouteChange } from '@/utils/route-listener';
import { openWindow, regexUrl } from '@/utils';
import useMenuTree from './use-menu-tree';
import { PERSONAL_ROUTE } from '@/router/routes/base';
import { BOTTOM_MENU_LIST } from '@/router/constants';
export default defineComponent({
emit: ['collapse'],
setup() {
const { t } = useI18n();
const appStore = useAppStore();
const userStore = useUserStore();
const router = useRouter();
const route = useRoute();
const { logout } = useUser();
const { menuTree } = useMenuTree();
const collapsed = computed({
get() {
@ -53,6 +58,7 @@
});
}
};
const personalActiveMenus = ref(['']);
/**
* 查找激活的菜单项
* @param target 目标菜单名
@ -73,9 +79,14 @@
}
};
menuTree.value?.forEach((el: RouteRecordRaw | null) => {
if (isFind) return; // Performance optimization
if (isFind) return; //
backtrack(el, [el?.name as string]);
});
personalActiveMenus.value = [''];
if (result.length === 0) {
backtrack(PERSONAL_ROUTE, [PERSONAL_ROUTE.name as string]);
personalActiveMenus.value = [...result];
}
return result;
};
/**
@ -96,11 +107,84 @@
if (appStore.device === 'desktop') appStore.updateSettings({ menuCollapse: val });
};
const personalMenusVisble = ref(false);
const personalMenus = [
{
label: t('personal.info'),
icon: <icon-user />,
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
route: PERSONAL_ROUTE.children![0],
},
{
label: t('personal.switchOrg'),
icon: <icon-swap />,
event: () => {},
},
{
divider: <a-divider class="ms-dropdown-divider" />,
},
{
label: t('personal.exit'),
icon: <icon-export />,
event: logout,
},
];
const personalInfoMenu = () => {
return (
<a-trigger
v-model:popup-visible={personalMenusVisble.value}
trigger="click"
unmount-on-close={false}
popup-offset={4}
position="right"
class="arco-trigger-menu absolute"
v-slots={{
content: () => (
<div class="arco-trigger-menu-inner">
{personalMenus.map((e) => {
if (e.divider) {
return e.divider;
}
return (
<div
class={[
'arco-trigger-menu-item',
personalActiveMenus.value.includes(e.route?.name as string)
? 'arco-trigger-menu-selected'
: '',
]}
onClick={() => {
if (typeof e.event === 'function') {
e.event();
} else if (e.route) {
goto(e.route);
}
personalMenusVisble.value = false;
}}
>
{e.icon}
{e.label}
</div>
);
})}
</div>
),
}}
>
<a-menu-item key="personalInfo">
<a-icon type="user" />
{userStore.name}
</a-menu-item>
</a-trigger>
);
};
const renderSubMenu = () => {
function travel(_route: (RouteRecordRaw | null)[] | null, nodes = []) {
if (_route) {
_route.forEach((element) => {
// This is demo, modify nodes as needed
const icon = element?.meta?.icon ? () => h(compile(`<${element?.meta?.icon}/>`)) : null;
const node =
element?.children && element?.children.length !== 0 ? (
@ -110,6 +194,7 @@
icon,
title: () => h(compile(t(element?.meta?.locale || ''))),
}}
class={BOTTOM_MENU_LIST.includes(element?.name as string) ? 'arco-menu-inline--bottom' : ''}
>
{travel(element?.children)}
</a-sub-menu>
@ -138,24 +223,108 @@
level-indent={34}
style="height: 100%;width:100%;"
onCollapse={setCollapse}
trigger-props={{
'show-arrow': false,
'popup-offset': -4,
}}
v-slots={{
'collapse-icon': () => (appStore.menuCollapse ? <icon-right /> : <icon-left />),
}}
>
{renderSubMenu()}
{personalInfoMenu()}
</a-menu>
);
},
});
</script>
<style lang="less" scoped>
:deep(.arco-menu-inner) {
padding: 16px 16px 0;
.arco-menu-inline-header {
@apply flex items-center;
<style lang="less">
.menu-wrapper {
background-color: var(--color-bg-3);
}
.arco-menu {
&:hover {
.arco-menu-collapse-button {
@apply flex;
}
}
.arco-icon {
&:not(.arco-icon-down) {
font-size: 18px;
.arco-menu-collapse-button {
@apply hidden rounded-full;
top: 22px;
right: 4px;
border: 1px solid #ffffff;
background: linear-gradient(90deg, rgb(var(--primary-9)) 3.36%, #ffffff 100%);
box-shadow: 0 0 7px rgb(15 0 78 / 9%);
.arco-icon {
color: rgb(var(--primary-5));
}
}
}
.arco-menu-inner {
@apply flex flex-col;
padding: 16px 32px 16px 16px !important;
.arco-menu-inline {
&--bottom {
@apply mt-auto;
}
}
.arco-menu-pop-header {
@apply leading-none;
padding: 11px;
}
.arco-menu-inline-header {
@apply flex items-center;
}
.arco-menu-inline-header,
.arco-menu-item {
@apply mb-0 !bg-transparent;
color: var(--color-text-1) !important;
&:hover,
.arco-menu-indent-list:hover,
.arco-icon:hover {
background-color: rgb(var(--primary-1)) !important;
}
.arco-menu-item-inner,
.arco-menu-item-inner:hover {
@apply !bg-transparent;
}
.arco-menu-icon {
.arco-icon {
&:not(.arco-icon-down) {
font-size: 18px;
}
color: var(--color-text-4);
}
}
.arco-menu-title {
color: var(--color-text-1);
}
}
.arco-menu-selected {
color: rgb(var(--primary-5)) !important;
&:not(.arco-menu-inline-header) {
background-color: rgb(var(--primary-9)) !important;
}
.arco-menu-icon {
.arco-icon {
color: rgb(var(--primary-5)) !important;
}
}
.arco-menu-title {
color: rgb(var(--primary-5)) !important;
}
}
.arco-menu-pop {
@apply bg-transparent;
}
}
.arco-menu-collapsed {
width: 86px;
}
</style>

View File

@ -3,7 +3,7 @@
<MsButton><icon-more /></MsButton>
<template #content>
<template v-for="item of props.list">
<a-divider v-if="item.isDivider" :key="`${item.label}-divider`" class="mx-0 my-[6px]" />
<a-divider v-if="item.isDivider" :key="`${item.label}-divider`" class="ms-dropdown-divider" />
<a-doption v-else :key="item.label" :class="item.danger ? 'error-6' : ''">{{ t(item.label || '') }}</a-doption>
</template>
</template>

View File

@ -2,13 +2,7 @@
<div class="navbar">
<div class="left-side">
<a-space>
<svg-icon :width="'43px'" :height="'33px'" :name="'logo'" />
<a-divider direction="vertical" />
<icon-menu-fold
v-if="!topMenu && appStore.device === 'mobile'"
style="font-size: 22px; cursor: pointer"
@click="toggleDrawerMenu"
/>
<svg-icon width="145px" height="32px" name="MS-full-logo" />
</a-space>
</div>
<div class="center-side">
@ -122,7 +116,7 @@
</template>
<script lang="ts" setup>
import { computed, ref, inject } from 'vue';
import { computed, ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { useFullscreen } from '@vueuse/core';
import { useAppStore, useUserStore } from '@/store';
@ -170,7 +164,6 @@
const res = await userStore.switchRoles();
Message.success(res as string);
};
const toggleDrawerMenu = inject('toggleDrawerMenu') as () => void;
</script>
<style scoped lang="less">
@ -182,7 +175,7 @@
.left-side {
@apply flex items-center;
padding-left: 20px;
padding-left: 24px;
}
.center-side {
@apply flex-1;

View File

@ -8,7 +8,7 @@
"menuCollapse": false,
"footer": false,
"themeColor": "#5736E9",
"menuWidth": 200,
"menuWidth": 212,
"globalSettings": false,
"device": "desktop",
"tabBar": false,

View File

@ -1,13 +1,13 @@
import Mock from 'mockjs';
import setupMock, { successResponseWrap, failResponseWrap } from '@/utils/setup-mock';
import { MockParams } from '#/mock';
import { GetMenuListUrl, LogoutUrl, GetUserInfoUrl } from '@/api/requrls/user';
import { isLogin } from '@/utils/auth';
setupMock({
setup() {
// 用户信息
Mock.mock(new RegExp('/api/user/info'), () => {
Mock.mock(new RegExp(GetUserInfoUrl), () => {
if (isLogin()) {
const role = window.localStorage.getItem('userRole') || 'admin';
return successResponseWrap({
@ -32,37 +32,13 @@ setupMock({
return failResponseWrap(null, '未登录', 50008);
});
// 登录
Mock.mock(new RegExp('/api/user/login'), (params: MockParams) => {
const { username, password } = JSON.parse(params.body);
if (!username) {
return failResponseWrap(null, '用户名不能为空', 50000);
}
if (!password) {
return failResponseWrap(null, '密码不能为空', 50000);
}
if (username === 'admin' && password === 'admin') {
window.localStorage.setItem('userRole', 'admin');
return successResponseWrap({
token: '12345',
});
}
if (username === 'user' && password === 'user') {
window.localStorage.setItem('userRole', 'user');
return successResponseWrap({
token: '54321',
});
}
return failResponseWrap(null, '账号或者密码错误', 50000);
});
// 登出
Mock.mock(new RegExp('/api/user/logout'), () => {
Mock.mock(new RegExp(LogoutUrl), () => {
return successResponseWrap(null);
});
// 用户的服务端菜单
Mock.mock(new RegExp('/api/user/menu'), () => {
Mock.mock(new RegExp(GetMenuListUrl), () => {
const menuList = [
{
path: '/api-test',
@ -114,6 +90,18 @@ setupMock({
},
],
},
{
path: '/personal',
name: 'personal',
meta: {},
children: [
{
path: '/personal/info',
name: 'personalInfo',
meta: {},
},
],
},
];
return successResponseWrap(menuList);
});

View File

@ -4,6 +4,8 @@ export const WHITE_LIST = [
{ name: 'invite', children: [] },
];
export const BOTTOM_MENU_LIST = ['system'];
export const NOT_FOUND = {
name: 'notFound',
};

View File

@ -3,7 +3,7 @@ import NProgress from 'nprogress'; // progress bar
import 'nprogress/nprogress.css';
import appRoutes from './routes';
import { REDIRECT_MAIN, NOT_FOUND_ROUTE, INVITE_ROUTE } from './routes/base';
import { REDIRECT_MAIN, NOT_FOUND_ROUTE, INVITE_ROUTE, PERSONAL_ROUTE } from './routes/base';
import createRouteGuard from './guard';
NProgress.configure({ showSpinner: false }); // NProgress Configuration
@ -27,6 +27,7 @@ const router = createRouter({
REDIRECT_MAIN,
NOT_FOUND_ROUTE,
INVITE_ROUTE,
PERSONAL_ROUTE,
],
scrollBehavior() {
return { top: 0 };

View File

@ -37,3 +37,18 @@ export const INVITE_ROUTE: RouteRecordRaw = {
hideInMenu: true,
},
};
export const PERSONAL_ROUTE: RouteRecordRaw = {
path: '/personal',
name: 'personal',
component: DEFAULT_LAYOUT,
meta: {},
children: [
{
path: '/personal/info',
name: 'personalInfo',
component: () => import('@/views/base/personal/index.vue'),
meta: {},
},
],
};

View File

@ -18,7 +18,6 @@ const ApiTest: AppRouteRecordRaw = {
meta: {
locale: 'menu.settings.user',
roles: ['*'],
icon: 'icon-computer',
},
},
{
@ -28,7 +27,6 @@ const ApiTest: AppRouteRecordRaw = {
meta: {
locale: 'menu.settings.usergroup',
roles: ['*'],
icon: 'icon-computer',
},
},
],

View File

@ -14,4 +14,7 @@ export default {
'invite.passwordTipTitle': 'The passwords must match both, only the following rules are supported',
'invite.passwordLengthRule': 'The length is 8-32 digits',
'invite.passwordWordRule': 'Must contain numbers and letters, Chinese or spaces are not allowed',
'personal.info': 'Personal Info',
'personal.switchOrg': 'Switch Org',
'personal.exit': 'Log out',
};

View File

@ -14,4 +14,7 @@ export default {
'invite.passwordTipTitle': '密码须同时符合,仅支持以下规则',
'invite.passwordLengthRule': '长度为8-32位',
'invite.passwordWordRule': '必须包含数字和字母,不允许输入中文或空格',
'personal.info': '个人信息',
'personal.switchOrg': '切换组织',
'personal.exit': '退出系统',
};

View File

@ -0,0 +1,7 @@
<template>
<div> </div>
</template>
<script setup lang="ts"></script>
<style lang="less" scoped></style>

View File

@ -102,6 +102,7 @@ export default {
'system.user.inviteCancel': 'Cancel',
'system.user.inviteEmailPlaceholder': 'Enter multiple email addresses, separated by spaces or carriage returns',
'system.user.inviteSendEmail': 'SendEmail',
'system.user.inviteSuccess': 'Invitation successful',
'system.user.importModalTitle': 'Import user',
'system.user.importDownload': 'Download the template',
'system.user.importModalTip': 'User groups only support adding user groups that exist in the system',

View File

@ -100,6 +100,7 @@ export default {
'system.user.inviteCancel': '取消',
'system.user.inviteEmailPlaceholder': '可输入多个邮箱地址,空格或回车分隔',
'system.user.inviteSendEmail': '发送邮件',
'system.user.inviteSuccess': '邀请成功',
'system.user.importModalTitle': '导入用户',
'system.user.importModalTip': '用户组仅支持添加系统存在的用户组',
'system.user.importDownload': '下载模板',