feat: 全局顶部导航菜单
This commit is contained in:
parent
13087a37fe
commit
6a1fb79faf
|
@ -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>
|
||||
|
|
|
@ -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 },
|
||||
{
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
},
|
||||
});
|
||||
|
||||
const topMenu = computed(() => appStore.topMenu);
|
||||
const openKeys = ref<string[]>([]);
|
||||
const selectedKey = ref<string[]>([]);
|
||||
|
||||
|
@ -67,10 +66,13 @@
|
|||
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);
|
||||
return;
|
||||
if (result.length >= 2) {
|
||||
// 由于目前存在三级子路由,所以至少会匹配到三层才算结束
|
||||
isFind = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (item?.children?.length) {
|
||||
item.children.forEach((el) => {
|
||||
|
@ -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" />
|
||||
{userStore.name}
|
||||
<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;
|
||||
.arco-icon {
|
||||
color: rgb(var(--primary-5)) !important;
|
||||
&:hover {
|
||||
background-color: var(--color-bg-6) !important;
|
||||
}
|
||||
}
|
||||
.arco-menu-title {
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
"colorWeak": false,
|
||||
"navbar": true,
|
||||
"menu": true,
|
||||
"topMenu": false,
|
||||
"hideMenu": false,
|
||||
"menuCollapse": false,
|
||||
"footer": false,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -10,7 +10,6 @@ export default {
|
|||
'settings.navbar.screen.toExit': '点击退出全屏模式',
|
||||
'settings.navbar.alerts': '消息通知',
|
||||
'settings.menu': '菜单栏',
|
||||
'settings.topMenu': '顶部菜单栏',
|
||||
'settings.tabBar': '多页签',
|
||||
'settings.footer': '底部',
|
||||
'settings.otherSettings': '其他设置',
|
||||
|
|
|
@ -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,8 +66,8 @@ setupMock({
|
|||
],
|
||||
},
|
||||
{
|
||||
path: '/system',
|
||||
name: 'system',
|
||||
path: '/setting',
|
||||
name: 'setting',
|
||||
meta: {
|
||||
locale: 'menu.settings',
|
||||
icon: 'icon-dashboard',
|
||||
|
@ -70,23 +75,34 @@ setupMock({
|
|||
},
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'user',
|
||||
path: 'system',
|
||||
name: 'settingSystem',
|
||||
redirect: '/setting/system/user',
|
||||
meta: {
|
||||
locale: 'menu.settings.user',
|
||||
locale: 'menu.settings.system',
|
||||
roles: ['*'],
|
||||
icon: 'icon-computer',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'usergroup',
|
||||
name: 'usergroup',
|
||||
component: () => import('@/views/system/usergroup/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.settings.usergroup',
|
||||
roles: ['*'],
|
||||
icon: 'icon-computer',
|
||||
hideChildrenInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'settingSystemUser',
|
||||
meta: {
|
||||
locale: 'menu.settings.user',
|
||||
roles: ['*'],
|
||||
isTopMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'usergroup',
|
||||
name: 'settingSystemUsergroup',
|
||||
meta: {
|
||||
locale: 'menu.settings.usergroup',
|
||||
roles: ['*'],
|
||||
isTopMenu: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
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',
|
||||
|
@ -12,24 +12,39 @@ const ApiTest: AppRouteRecordRaw = {
|
|||
},
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'user',
|
||||
component: () => import('@/views/system/user/index.vue'),
|
||||
path: 'system',
|
||||
name: 'settingSystem',
|
||||
redirect: '/setting/system/user',
|
||||
component: null,
|
||||
meta: {
|
||||
locale: 'menu.settings.user',
|
||||
roles: ['*'],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'usergroup',
|
||||
name: 'usergroup',
|
||||
component: () => import('@/views/system/usergroup/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.settings.usergroup',
|
||||
locale: 'menu.settings.system',
|
||||
roles: ['*'],
|
||||
hideChildrenInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'user',
|
||||
name: 'settingSystemUser',
|
||||
component: () => import('@/views/system/user/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.settings.user',
|
||||
roles: ['*'],
|
||||
isTopMenu: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'usergroup',
|
||||
name: 'settingSystemUsergroup',
|
||||
component: () => import('@/views/system/usergroup/index.vue'),
|
||||
meta: {
|
||||
locale: 'menu.settings.usergroup',
|
||||
roles: ['*'],
|
||||
isTopMenu: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default ApiTest;
|
||||
export default System;
|
||||
|
|
|
@ -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] : [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue