【v3.8.3】优化顶部导航风格菜单的样式,支持外部链接打开及菜单重定向

pull/8786/merge
JEECG 2025-09-14 11:59:09 +08:00
parent e825e0f912
commit 44c1079f87
8 changed files with 280 additions and 17 deletions

View File

@ -124,8 +124,10 @@
return;
}
// update-begin--author:liaozhiyang---date:20250114---for:issues/7706tab
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);
// emitkeyurl
_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);

View File

@ -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---forQQYUN-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---forQQYUN-13718
let parentPath = await getCurrentParentPath(path);
if (!parentPath) {
parentPath = await getCurrentParentPath(currentActiveMenu);

View File

@ -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) {

View File

@ -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>

View File

@ -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,
};
};

View File

@ -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 } : {}),
};
},

View File

@ -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 {

View File

@ -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;