feat: 全局顶部导航菜单

This commit is contained in:
baiqi 2023-06-25 18:17:23 +08:00 committed by 刘瑞斌
parent 13087a37fe
commit 6a1fb79faf
17 changed files with 164 additions and 74 deletions

View File

@ -41,11 +41,6 @@
if (key === 'colorWeak') {
document.body.style.filter = value ? 'invert(80%)' : 'none';
}
if (key === 'topMenu') {
appStore.updateSettings({
menuCollapse: false,
});
}
appStore.updateSettings({ [key]: value });
};
</script>

View File

@ -45,11 +45,6 @@
key: 'menu',
defaultVal: appStore.menu,
},
{
name: 'settings.topMenu',
key: 'topMenu',
defaultVal: appStore.topMenu,
},
{ name: 'settings.footer', key: 'footer', defaultVal: appStore.footer },
{ name: 'settings.tabBar', key: 'tabBar', defaultVal: appStore.tabBar },
{

View File

@ -31,7 +31,6 @@
},
});
const topMenu = computed(() => appStore.topMenu);
const openKeys = ref<string[]>([]);
const selectedKey = ref<string[]>([]);
@ -67,11 +66,14 @@
const result: string[] = [];
let isFind = false;
const backtrack = (item: RouteRecordRaw | null, keys: string[]) => {
if (item?.name === target) {
isFind = true;
if (target.includes(item?.name as string)) {
result.push(...keys);
if (result.length >= 2) {
//
isFind = true;
return;
}
}
if (item?.children?.length) {
item.children.forEach((el) => {
backtrack(el, [...keys, el.name as string]);
@ -139,7 +141,7 @@
unmount-on-close={false}
popup-offset={4}
position="right"
class="arco-trigger-menu absolute"
class={['arco-trigger-menu absolute', personalMenusVisble.value ? 'block' : 'hidden']}
v-slots={{
content: () => (
<div class="arco-trigger-menu-inner">
@ -173,9 +175,12 @@
),
}}
>
<a-menu-item key="personalInfo">
<a-icon type="user" />
<a-menu-item class="flex items-center justify-between" key="personalInfo">
<div class="hover:!bg-transparent">
<icon-face-smile-fill />
{userStore.name}
</div>
<icon-caret-down class="!m-0" />
</a-menu-item>
</a-trigger>
);
@ -213,7 +218,7 @@
return () => (
<a-menu
mode={topMenu.value ? 'horizontal' : 'vertical'}
mode={'vertical'}
v-model:collapsed={collapsed.value}
v-model:open-keys={openKeys.value}
show-collapse-button={appStore.device !== 'mobile'}
@ -311,9 +316,10 @@
&:not(.arco-menu-inline-header) {
background-color: rgb(var(--primary-9)) !important;
}
.arco-menu-icon {
.arco-icon {
color: rgb(var(--primary-5)) !important;
&:hover {
background-color: var(--color-bg-6) !important;
}
}
.arco-menu-title {

View File

@ -0,0 +1,52 @@
<template>
<a-menu
v-if="appStore.topMenus.length > 0"
class="bg-transparent"
mode="horizontal"
:default-selected-keys="[appStore.topMenus[0].name]"
>
<a-menu-item v-for="menu of appStore.topMenus" :key="(menu.name as string)" @click="jumpPath(menu.name)">
{{ t(menu.meta?.locale || '') }}
</a-menu-item>
</a-menu>
</template>
<script setup lang="ts">
import { useRouter, RouteRecordRaw, RouteRecordNormalized, RouteRecordName } from 'vue-router';
import { cloneDeep } from 'lodash-es';
import { useAppStore } from '@/store';
import { listenerRouteChange } from '@/utils/route-listener';
import usePermission from '@/hooks/usePermission';
import appClientMenus from '@/router/app-menus';
import { useI18n } from '@/hooks/useI18n';
const copyRouter = cloneDeep(appClientMenus) as RouteRecordNormalized[];
const permission = usePermission();
const appStore = useAppStore();
const router = useRouter();
const { t } = useI18n();
/**
* 监听路由变化存储打开的三级子路由
*/
listenerRouteChange((newRoute) => {
const { name } = newRoute;
copyRouter.forEach((el: RouteRecordRaw) => {
//
if (permission.accessRouter(el)) {
if (name && (name as string).includes((el?.name as string) || '')) {
const currentParent = el?.children?.find(
(item) => name && (name as string).includes((item?.name as string) || '')
);
appStore.setTopMenus(currentParent?.children?.filter((item) => item.meta?.isTopMenu));
}
}
});
}, true);
function jumpPath(route: RouteRecordName | undefined) {
router.push({ name: route });
}
</script>
<style lang="less" scoped></style>

View File

@ -6,7 +6,7 @@
</a-space>
</div>
<div class="center-side">
<Menu v-if="topMenu"></Menu>
<TopMenu />
</div>
<ul class="right-side">
<li>
@ -119,14 +119,13 @@
import { computed, ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import { useFullscreen } from '@vueuse/core';
import { useAppStore, useUserStore } from '@/store';
import { useUserStore } from '@/store';
import { LOCALE_OPTIONS } from '@/locale';
import useLocale from '@/locale/useLocale';
import useUser from '@/hooks/useUser';
import Menu from '@/components/pure/menu/index.vue';
import TopMenu from '@/components/pure/ms-top-menu/index.vue';
import MessageBox from '../message-box/index.vue';
const appStore = useAppStore();
const userStore = useUserStore();
const { logout } = useUser();
const { changeLocale, currentLocale } = useLocale();
@ -135,7 +134,6 @@
const avatar = computed(() => {
return userStore.avatar;
});
const topMenu = computed(() => appStore.topMenu && appStore.menu);
// const setVisible = () => {
// appStore.updateSettings({ globalSettings: true });
// };
@ -176,6 +174,7 @@
@apply flex items-center;
padding-left: 24px;
width: 185px;
}
.center-side {
@apply flex-1;

View File

@ -3,7 +3,6 @@
"colorWeak": false,
"navbar": true,
"menu": true,
"topMenu": false,
"hideMenu": false,
"menuCollapse": false,
"footer": false,

View File

@ -68,7 +68,7 @@
useResponsive(true);
const navbarHeight = `56px`;
const navbar = computed(() => appStore.navbar);
const renderMenu = computed(() => appStore.menu && !appStore.topMenu);
const renderMenu = computed(() => appStore.menu);
const hideMenu = computed(() => appStore.hideMenu);
const footer = computed(() => appStore.footer);
const collapsedWidth = 86;
@ -157,9 +157,9 @@
}
}
.layout-content {
@apply overflow-y-hidden;
@apply box-content overflow-y-hidden;
min-height: 100vh;
height: calc(100vh - 56px);
background-color: var(--color-bg-3);
transition: padding 0.2s cubic-bezier(0.34, 0.69, 0.1, 1);
.arco-layout-content {

View File

@ -13,9 +13,11 @@ export default {
message: {
'menu.apiTest': 'Api Test',
'menu.settings': 'System Settings',
'menu.settings.system': 'System',
'menu.settings.organization': 'Organization',
'menu.settings.usergroup': 'User Group',
'menu.settings.user': 'User',
'menu.settings.organization': 'Organization',
'menu.settings.organizationAndProject': 'Org & Project',
'navbar.action.locale': 'Switch to English',
...sys,
...localeSettings,

View File

@ -10,7 +10,6 @@ export default {
'settings.navbar.screen.toExit': 'Click to exit the full screen mode',
'settings.navbar.alerts': 'alerts',
'settings.menu': 'Menu',
'settings.topMenu': 'Top Menu',
'settings.tabBar': 'Tab Bar',
'settings.footer': 'Footer',
'settings.otherSettings': 'Other Settings',

View File

@ -1,5 +1,6 @@
import { unref, ref } from 'vue';
import dayjs from 'dayjs';
import { Message } from '@arco-design/web-vue';
import { i18n } from '@/locale';
import { setHtmlPageLang, loadLocalePool } from '@/locale/helper';
@ -33,6 +34,7 @@ function setI18nLanguage(locale: LocaleType) {
async function changeLocale(locale: LocaleType) {
const globalI18n = i18n.global;
const currentLocale = unref(globalI18n.locale);
Message.loading(currentLocale === 'zh-CN' ? '语言切换中...' : 'Language switching...');
if (currentLocale === locale) {
return locale;
}

View File

@ -13,9 +13,11 @@ export default {
message: {
'menu.apiTest': '接口测试',
'menu.settings': '系统设置',
'menu.settings.system': '系统',
'menu.settings.organization': '组织',
'menu.settings.user': '用户',
'menu.settings.usergroup': '用户组',
'menu.settings.organization': '组织',
'menu.settings.organizationAndProject': '组织与项目',
'menu.user': '个人中心',
'navbar.action.locale': '切换为中文',
...sys,

View File

@ -10,7 +10,6 @@ export default {
'settings.navbar.screen.toExit': '点击退出全屏模式',
'settings.navbar.alerts': '消息通知',
'settings.menu': '菜单栏',
'settings.topMenu': '顶部菜单栏',
'settings.tabBar': '多页签',
'settings.footer': '底部',
'settings.otherSettings': '其他设置',

View File

@ -1,7 +1,7 @@
import Mock from 'mockjs';
import setupMock, { successResponseWrap, failResponseWrap } from '@/utils/setup-mock';
import { GetMenuListUrl, LogoutUrl, GetUserInfoUrl } from '@/api/requrls/user';
import { GetMenuListUrl, LogoutUrl, GetUserInfoUrl, LoginUrl } from '@/api/requrls/user';
import { isLogin } from '@/utils/auth';
setupMock({
@ -32,6 +32,11 @@ setupMock({
return failResponseWrap(null, '未登录', 50008);
});
// 登出
Mock.mock(new RegExp(LoginUrl), () => {
return successResponseWrap({});
});
// 登出
Mock.mock(new RegExp(LogoutUrl), () => {
return successResponseWrap(null);
@ -61,35 +66,46 @@ setupMock({
],
},
{
path: '/system',
name: 'system',
path: '/setting',
name: 'setting',
meta: {
locale: 'menu.settings',
icon: 'icon-dashboard',
order: 0,
},
children: [
{
path: 'system',
name: 'settingSystem',
redirect: '/setting/system/user',
meta: {
locale: 'menu.settings.system',
roles: ['*'],
hideChildrenInMenu: true,
},
children: [
{
path: 'user',
name: 'user',
name: 'settingSystemUser',
meta: {
locale: 'menu.settings.user',
roles: ['*'],
icon: 'icon-computer',
isTopMenu: true,
},
},
{
path: 'usergroup',
name: 'usergroup',
component: () => import('@/views/system/usergroup/index.vue'),
name: 'settingSystemUsergroup',
meta: {
locale: 'menu.settings.usergroup',
roles: ['*'],
icon: 'icon-computer',
isTopMenu: true,
},
},
],
},
],
},
{
path: '/personal',
name: 'personal',

View File

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

View File

@ -1,35 +1,50 @@
import { DEFAULT_LAYOUT } from '../base';
import { AppRouteRecordRaw } from '../types';
const ApiTest: AppRouteRecordRaw = {
path: '/system',
name: 'system',
const System: AppRouteRecordRaw = {
path: '/setting',
name: 'setting',
component: DEFAULT_LAYOUT,
meta: {
locale: 'menu.settings',
icon: 'icon-dashboard',
order: 0,
},
children: [
{
path: 'system',
name: 'settingSystem',
redirect: '/setting/system/user',
component: null,
meta: {
locale: 'menu.settings.system',
roles: ['*'],
hideChildrenInMenu: true,
},
children: [
{
path: 'user',
name: 'user',
name: 'settingSystemUser',
component: () => import('@/views/system/user/index.vue'),
meta: {
locale: 'menu.settings.user',
roles: ['*'],
isTopMenu: true,
},
},
{
path: 'usergroup',
name: 'usergroup',
name: 'settingSystemUsergroup',
component: () => import('@/views/system/usergroup/index.vue'),
meta: {
locale: 'menu.settings.usergroup',
roles: ['*'],
isTopMenu: true,
},
},
],
},
],
};
export default ApiTest;
export default System;

View File

@ -6,10 +6,10 @@ import { useI18n } from '@/hooks/useI18n';
import type { AppState } from './types';
import type { NotificationReturn } from '@arco-design/web-vue/es/notification/interface';
import type { RouteRecordNormalized } from 'vue-router';
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
const useAppStore = defineStore('app', {
state: (): AppState => ({ ...defaultSettings, loading: false, loadingTip: '加载中...' }),
state: (): AppState => ({ ...defaultSettings, loading: false, loadingTip: '', topMenus: [] as RouteRecordRaw[] }),
getters: {
appCurrentSetting(state: AppState): AppState {
@ -27,6 +27,9 @@ const useAppStore = defineStore('app', {
getLoadingStatus(state: AppState): boolean {
return state.loading;
},
getTopMenus(state: AppState): RouteRecordRaw[] {
return state.topMenus;
},
},
actions: {
@ -102,6 +105,12 @@ const useAppStore = defineStore('app', {
this.loading = false;
this.loadingTip = t('message.loadingDefaultTip');
},
/**
*
*/
setTopMenus(menus: RouteRecordRaw[] | undefined) {
this.topMenus = menus ? [...menus] : [];
},
},
});

View File

@ -1,11 +1,10 @@
import type { RouteRecordNormalized } from 'vue-router';
import type { RouteRecordNormalized, RouteRecordRaw } from 'vue-router';
export interface AppState {
theme: string;
colorWeak: boolean;
navbar: boolean;
menu: boolean;
topMenu: boolean;
hideMenu: boolean;
menuCollapse: boolean;
footer: boolean;
@ -17,6 +16,7 @@ export interface AppState {
serverMenu: RouteRecordNormalized[];
loading: boolean;
loadingTip: string;
topMenus: RouteRecordRaw[];
[key: string]: unknown;
}