feat: 左侧菜单样式&个人菜单
This commit is contained in:
parent
8920a12690
commit
6378e1ea30
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"menuCollapse": false,
|
||||
"footer": false,
|
||||
"themeColor": "#5736E9",
|
||||
"menuWidth": 200,
|
||||
"menuWidth": 212,
|
||||
"globalSettings": false,
|
||||
"device": "desktop",
|
||||
"tabBar": false,
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -4,6 +4,8 @@ export const WHITE_LIST = [
|
|||
{ name: 'invite', children: [] },
|
||||
];
|
||||
|
||||
export const BOTTOM_MENU_LIST = ['system'];
|
||||
|
||||
export const NOT_FOUND = {
|
||||
name: 'notFound',
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -14,4 +14,7 @@ export default {
|
|||
'invite.passwordTipTitle': '密码须同时符合,仅支持以下规则',
|
||||
'invite.passwordLengthRule': '长度为8-32位',
|
||||
'invite.passwordWordRule': '必须包含数字和字母,不允许输入中文或空格',
|
||||
'personal.info': '个人信息',
|
||||
'personal.switchOrg': '切换组织',
|
||||
'personal.exit': '退出系统',
|
||||
};
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div> </div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style lang="less" scoped></style>
|
|
@ -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',
|
||||
|
|
|
@ -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': '下载模板',
|
||||
|
|
Loading…
Reference in New Issue