mirror of https://github.com/jeecgboot/jeecg-boot
【v3.8.3】优化顶部导航风格菜单的样式,支持外部链接打开及菜单重定向
parent
e825e0f912
commit
44c1079f87
|
@ -124,8 +124,10 @@
|
|||
return;
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20250114---for:【issues/7706】顶部栏导航内部路由也可以支持采用新浏览器tab打开
|
||||
const findItem = getMatchingMenu(props.items, key);
|
||||
if (findItem?.internalOrExternal == true) {
|
||||
const menus = await getMenus();
|
||||
const findItem = getMatchingPath(menus, key);
|
||||
if (findItem?.internalOrExternal == true && !findItem?.children?.length) {
|
||||
// 一级菜单当设置了外部打开,只有没有子菜单时才生效
|
||||
window.open(location.origin + key);
|
||||
return;
|
||||
}
|
||||
|
@ -137,18 +139,32 @@
|
|||
}
|
||||
// update-begin--author:liaozhiyang---date:20240418---for:【QQYUN-8773】顶部混合导航(顶部左侧组合菜单)一级菜单没有配置redirect默认跳子菜单的第一个
|
||||
if (props.type === MenuTypeEnum.MIX) {
|
||||
const menus = await getMenus();
|
||||
const menuItem = getMatchingPath(menus, key);
|
||||
if (menuItem && !menuItem.redirect && menuItem.children?.length) {
|
||||
// 没有重定向且originComponent不是系统默认的就当做是组件,否则就跳子菜单的第一个
|
||||
if (menuItem && !menuItem.redirect && menuItem.originComponent == '/layouts/default/index' && menuItem.children?.length) {
|
||||
const subMenuItem = getSubMenu(menuItem.children);
|
||||
if (subMenuItem?.path) {
|
||||
const path = subMenuItem.redirect ?? subMenuItem.path;
|
||||
let _key = path;
|
||||
if (isUrl(path)) {
|
||||
window.open(path);
|
||||
// window.open(path);
|
||||
// 外部打开emit出去的key不能是url,否则左侧菜单出不来
|
||||
_key = key;
|
||||
}
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20250825---for:【QQYUN-13593】敲敲云首页菜单外部打开
|
||||
// =====================================================================
|
||||
// TODO: 临时代码 - 需要删除!!!
|
||||
// 这是针对敲敲云首页菜单的特殊处理,后续需要重构或删除
|
||||
// =====================================================================
|
||||
// 是外部打开且是白名单内的菜单,则直接打开
|
||||
if (subMenuItem?.internalOrExternal == true && ['/myapps/index'].includes(path)) {
|
||||
window.open(location.origin + path);
|
||||
return;
|
||||
}
|
||||
// =====================================================================
|
||||
// update-end--author:liaozhiyang---date:20250825---for:【QQYUN-13593】敲敲云首页菜单外部打开
|
||||
|
||||
emit('menuClick', _key, { title: subMenuItem.title });
|
||||
} else {
|
||||
emit('menuClick', key, item);
|
||||
|
@ -189,12 +205,12 @@
|
|||
/**
|
||||
* liaozhiyang
|
||||
* 2024-05-18
|
||||
* 获取指定菜单下的第一个菜单
|
||||
* 获取指定菜单下的第一个菜单(忽略隐藏路由)
|
||||
*/
|
||||
function getSubMenu(menus) {
|
||||
for (let i = 0, len = menus.length; i < len; i++) {
|
||||
const item = menus[i];
|
||||
if (item.path && !item.children?.length) {
|
||||
if (item.path && !item.hideMenu && !item.children?.length) {
|
||||
return item;
|
||||
} else if (item.children?.length) {
|
||||
const result = getSubMenu(item.children);
|
||||
|
|
|
@ -2,12 +2,13 @@ import type { Menu } from '/@/router/types';
|
|||
import type { Ref } from 'vue';
|
||||
import { watch, unref, ref, computed } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { MenuSplitTyeEnum } from '/@/enums/menuEnum';
|
||||
import { MenuSplitTyeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { getChildrenMenus, getCurrentParentPath, getMenus, getShallowMenus } from '/@/router/menus';
|
||||
import { usePermissionStore } from '/@/store/modules/permission';
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { PAGE_NOT_FOUND_NAME_404 } from '/@/router/constant';
|
||||
|
||||
export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
|
||||
// Menu array
|
||||
|
@ -15,7 +16,7 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
|
|||
const { currentRoute } = useRouter();
|
||||
const { getIsMobile } = useAppInject();
|
||||
const permissionStore = usePermissionStore();
|
||||
const { setMenuSetting, getIsHorizontal, getSplit } = useMenuSetting();
|
||||
const { setMenuSetting, getIsHorizontal, getSplit, getMenuType } = useMenuSetting();
|
||||
|
||||
const throttleHandleSplitLeftMenu = useThrottleFn(handleSplitLeftMenu, 50);
|
||||
|
||||
|
@ -33,9 +34,22 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
|
|||
[() => unref(currentRoute).path, () => unref(splitType)],
|
||||
async ([path]: [string, MenuSplitTyeEnum]) => {
|
||||
if (unref(splitNotLeft) || unref(getIsMobile)) return;
|
||||
|
||||
const { meta } = unref(currentRoute);
|
||||
const currentActiveMenu = meta.currentActiveMenu as string;
|
||||
// update-begin--author:liaozhiyang---date:20250908---for:【QQYUN-13718】一级菜单默认重定向到子菜单,但子菜单未授权,导致点击一级菜单加载不出子菜单
|
||||
// 顶部混合模式且顶部左侧组合菜单开始时
|
||||
if (unref(getMenuType) === MenuTypeEnum.MIX && unref(getSplit)) {
|
||||
// 404页面时,跳转到重定向的路径
|
||||
if (unref(currentRoute).name === PAGE_NOT_FOUND_NAME_404 && unref(currentRoute)?.redirectedFrom?.path) {
|
||||
const menus = await getMenus();
|
||||
const findItem = menus.find((item:any) => item.redirect === unref(currentRoute).path);
|
||||
if (findItem) {
|
||||
// 说明是从一级菜单重定向过来的
|
||||
path = findItem.path;
|
||||
}
|
||||
}
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20250908---for:【QQYUN-13718】一级菜单默认重定向到子菜单,但子菜单未授权,导致点击一级菜单加载不出子菜单
|
||||
let parentPath = await getCurrentParentPath(path);
|
||||
if (!parentPath) {
|
||||
parentPath = await getCurrentParentPath(currentActiveMenu);
|
||||
|
|
|
@ -36,13 +36,14 @@ export function layoutHandler(event: HandlerEnum, value: any) {
|
|||
baseHandler(HandlerEnum.TABS_THEME, tabsThemeOptions[1].value);
|
||||
} else if (isMixMenu) {
|
||||
baseHandler(event, value);
|
||||
baseHandler(HandlerEnum.HEADER_THEME, HEADER_PRESET_BG_COLOR_LIST[4]);
|
||||
baseHandler(HandlerEnum.HEADER_THEME, HEADER_PRESET_BG_COLOR_LIST[2]);
|
||||
baseHandler(HandlerEnum.MENU_THEME, SIDE_BAR_BG_COLOR_LIST[3]);
|
||||
if (darkMode) {
|
||||
updateHeaderBgColor();
|
||||
updateSidebarBgColor();
|
||||
}
|
||||
baseHandler(HandlerEnum.CHANGE_THEME_COLOR, APP_PRESET_COLOR_LIST[1]);
|
||||
// 顶部混合导航模式主题色改成绿色
|
||||
baseHandler(HandlerEnum.CHANGE_THEME_COLOR, APP_PRESET_COLOR_LIST[2]);
|
||||
baseHandler(HandlerEnum.TABS_THEME, tabsThemeOptions[1].value);
|
||||
} else if (isMixSidebarMenu) {
|
||||
baseHandler(event, value);
|
||||
|
@ -65,6 +66,13 @@ export function layoutHandler(event: HandlerEnum, value: any) {
|
|||
baseHandler(HandlerEnum.CHANGE_THEME_COLOR, APP_PRESET_COLOR_LIST[1]);
|
||||
baseHandler(HandlerEnum.TABS_THEME, tabsThemeOptions[1].value);
|
||||
}
|
||||
// update-begin--author:liaozhiyang---date:20250825---for:【QQYUN-13600】默认顶部混合导航模式且启用顶部左侧导航,切换到其他模式时导航刷新后菜单样式混乱
|
||||
if (isMixMenu) {
|
||||
baseHandler(HandlerEnum.MENU_SPLIT, true);
|
||||
} else {
|
||||
baseHandler(HandlerEnum.MENU_SPLIT, false);
|
||||
}
|
||||
// update-end--author:liaozhiyang---date:20250825---for:【QQYUN-13600】默认顶部混合导航模式且启用顶部左侧导航,切换到其他模式时导航刷新后菜单样式混乱
|
||||
}
|
||||
|
||||
export function baseHandler(event: HandlerEnum, value: any) {
|
||||
|
|
|
@ -14,10 +14,26 @@
|
|||
<!-- mode="out-in"-->
|
||||
<!-- appear-->
|
||||
<!-- >-->
|
||||
<keep-alive v-if="openCache" :include="getCaches">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component v-else :is="Component" :key="route.fullPath" />
|
||||
<template v-if="Component">
|
||||
<keep-alive v-if="openCache" :include="getCaches">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
<component v-else :is="Component" :key="route.fullPath" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<!-- 【QQYUN-13593】空白页美化 -->
|
||||
<div class="animationEffect" :style="effectVars">
|
||||
<div class="effect-layer">
|
||||
<div class="blob blob-a"></div>
|
||||
<div class="blob blob-b"></div>
|
||||
<div class="blob blob-c"></div>
|
||||
</div>
|
||||
<div class="effect-grid"></div>
|
||||
<div class="effect-tip">
|
||||
<p>{{pageTip}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- </transition>-->
|
||||
</template>
|
||||
</RouterView>
|
||||
|
@ -36,6 +52,7 @@
|
|||
import { getTransitionName } from './transition';
|
||||
|
||||
import { useMultipleTabStore } from '/@/store/modules/multipleTab';
|
||||
import { useEmpty } from './useEmpty';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'PageLayout',
|
||||
|
@ -56,7 +73,9 @@
|
|||
}
|
||||
return tabStore.getCachedTabList;
|
||||
});
|
||||
|
||||
// update-begin--author:liaozhiyang---date:20250826---for:【QQYUN-13593】空白页美化
|
||||
const { pageTip, getPageTip, effectVars } = useEmpty();
|
||||
// update-end--author:liaozhiyang---date:20250826---for:【QQYUN-13593】空白页美化
|
||||
return {
|
||||
getTransitionName,
|
||||
openCache,
|
||||
|
@ -64,7 +83,123 @@
|
|||
getBasicTransition,
|
||||
getCaches,
|
||||
getCanEmbedIFramePage,
|
||||
pageTip,
|
||||
getPageTip,
|
||||
effectVars,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
/** update-begin---author:liaozy ---date:2025-08-26 for:空白页美化样式 */
|
||||
.pageTip {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.animationEffect {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
min-height: 420px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(180deg, var(--bg-1) 0%, var(--bg-2) 100%);
|
||||
}
|
||||
|
||||
.effect-layer {
|
||||
position: absolute;
|
||||
top: -20%;
|
||||
left: -20%;
|
||||
right: -20%;
|
||||
bottom: -20%;
|
||||
filter: blur(30px);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.blob {
|
||||
position: absolute;
|
||||
width: 380px;
|
||||
height: 380px;
|
||||
border-radius: 50%;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.blob-a {
|
||||
background: radial-gradient(circle at 30% 30%, var(--blob-a-1) 0%, var(--blob-a-2) 60%, var(--blob-a-2) 100%);
|
||||
left: 5%;
|
||||
top: 10%;
|
||||
animation: float-a 18s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.blob-b {
|
||||
background: radial-gradient(circle at 30% 30%, var(--blob-b-1) 0%, var(--blob-b-2) 60%, var(--blob-b-2) 100%);
|
||||
right: 0;
|
||||
top: 30%;
|
||||
animation: float-b 22s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.blob-c {
|
||||
background: radial-gradient(circle at 30% 30%, var(--blob-c-1) 0%, var(--blob-c-2) 60%, var(--blob-c-2) 100%);
|
||||
left: 35%;
|
||||
bottom: -5%;
|
||||
animation: float-c 26s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes float-a {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(20%, -10%) scale(1.05); }
|
||||
50% { transform: translate(35%, 5%) scale(0.95); }
|
||||
75% { transform: translate(10%, 15%) scale(1.02); }
|
||||
100% { transform: translate(0, 0) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes float-b {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(-15%, 10%) scale(1.08); }
|
||||
50% { transform: translate(-30%, -5%) scale(0.92); }
|
||||
75% { transform: translate(-10%, -15%) scale(1.03); }
|
||||
100% { transform: translate(0, 0) scale(1); }
|
||||
}
|
||||
|
||||
@keyframes float-c {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
25% { transform: translate(-10%, -10%) scale(0.9); }
|
||||
50% { transform: translate(10%, -25%) scale(1.05); }
|
||||
75% { transform: translate(20%, 0%) scale(0.98); }
|
||||
100% { transform: translate(0, 0) scale(1); }
|
||||
}
|
||||
|
||||
.effect-grid {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image: linear-gradient(0deg, var(--grid-color) 1px, rgba(0, 0, 0, 0) 1px),
|
||||
linear-gradient(90deg, var(--grid-color) 1px, rgba(0, 0, 0, 0) 1px);
|
||||
background-size: 36px 36px, 36px 36px;
|
||||
mask-image: radial-gradient(circle at 50% 50%, rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0) 70%);
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.effect-tip {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
text-align: center;
|
||||
pointer-events: none;
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 8px 14px;
|
||||
color: var(--tip-color);
|
||||
font-size: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
}
|
||||
/** update-end---author:liaozy ---date:2025-08-26 for:空白页美化样式 */
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import { computed, unref, ref, watch } from 'vue';
|
||||
import { getMenus } from '/@/router/menus';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { lighten, darken } from '/@/utils/color';
|
||||
export const useEmpty = () => {
|
||||
const { getThemeColor, getDarkMode } = useRootSetting();
|
||||
const route = useRoute();
|
||||
const { getHeaderBgColor } = useHeaderSetting();
|
||||
const { getMenuBgColor } = useMenuSetting();
|
||||
const pageTip = ref('');
|
||||
const effectVars = computed(() => {
|
||||
const primary = unref(getThemeColor) || '#1890ff';
|
||||
const menuBg = unref(getMenuBgColor) || '#ffffff';
|
||||
const headerBg = unref(getHeaderBgColor);
|
||||
const isDark = unref(getDarkMode) === 'dark';
|
||||
// 以主题色为基色,派生三组渐变色
|
||||
const a1 = lighten(primary, 25);
|
||||
const a2 = primary;
|
||||
const b1 = lighten(headerBg, 45);
|
||||
const b2 = lighten(headerBg, 10);
|
||||
const c1 = lighten(menuBg, 35);
|
||||
const c2 = darken(primary, 5);
|
||||
const bg1 = isDark ? '#0f172a' : '#f7f8fa';
|
||||
const bg2 = isDark ? '#111827' : '#f2f5f9';
|
||||
const grid = isDark ? 'rgba(255,255,255,0.04)' : 'rgba(60,70,90,0.06)';
|
||||
const tipColor = isDark ? '#626262' : '#b9b9b9';
|
||||
const tipBg = isDark ? 'rgba(17,24,39,0.6)' : 'rgba(255,255,255,0.6)';
|
||||
const tipBorder = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)';
|
||||
return {
|
||||
'--blob-a-1': a1,
|
||||
'--blob-a-2': a2,
|
||||
'--blob-b-1': b1,
|
||||
'--blob-b-2': b2,
|
||||
'--blob-c-1': c1,
|
||||
'--blob-c-2': c2,
|
||||
'--bg-1': bg1,
|
||||
'--bg-2': bg2,
|
||||
'--grid-color': grid,
|
||||
'--tip-color': tipColor,
|
||||
'--tip-bg': tipBg,
|
||||
'--tip-border': tipBorder,
|
||||
} as Record<string, string>;
|
||||
});
|
||||
|
||||
const getPageTip = async (route) => {
|
||||
const menus = await getMenus();
|
||||
const menu = getMatchingPath(menus, route.path);
|
||||
if (menu) {
|
||||
if (['/layouts/default/index'].includes(menu.originComponent)) {
|
||||
pageTip.value = '点击子菜单跳转到对应外部链接!';
|
||||
} else {
|
||||
pageTip.value = '查看组件引用是否正确';
|
||||
}
|
||||
}
|
||||
};
|
||||
watch(
|
||||
route,
|
||||
() => {
|
||||
getPageTip({ path: window.location.pathname });
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
function getMatchingPath(menus, path) {
|
||||
for (let i = 0, len = menus.length; i < len; i++) {
|
||||
const item = menus[i];
|
||||
if (item.path === path) {
|
||||
return item;
|
||||
} else if (item.children?.length) {
|
||||
const result = getMatchingPath(item.children, path);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
pageTip,
|
||||
getPageTip,
|
||||
effectVars,
|
||||
};
|
||||
};
|
|
@ -98,6 +98,7 @@ export function transformRouteToMenu(routeModList: AppRouteModule[], routerMappi
|
|||
hideMenu,
|
||||
alwaysShow:node.alwaysShow||false,
|
||||
path: node.path,
|
||||
originComponent: node.originComponent,
|
||||
...(node.redirect ? { redirect: node.redirect } : {}),
|
||||
};
|
||||
},
|
||||
|
|
|
@ -134,6 +134,7 @@ export function transformObjToRoute<T = AppRouteModule>(routeList: AppRouteModul
|
|||
routeList.forEach((route) => {
|
||||
const component = route.component as string;
|
||||
if (component) {
|
||||
route.originComponent = component;
|
||||
if (component.toUpperCase() === 'LAYOUT') {
|
||||
route.component = LayoutMap.get(component.toUpperCase());
|
||||
} else {
|
||||
|
|
|
@ -9,6 +9,7 @@ export interface AppRouteRecordRaw extends Omit<RouteRecordRaw, 'meta'> {
|
|||
name: string;
|
||||
meta: RouteMeta;
|
||||
component?: Component | string;
|
||||
originComponent?: string;
|
||||
components?: Component;
|
||||
children?: AppRouteRecordRaw[];
|
||||
props?: Recordable;
|
||||
|
|
Loading…
Reference in New Issue