前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题

This commit is contained in:
JEECG
2024-06-23 10:39:52 +08:00
parent bb918b742e
commit 0325e34dcb
1439 changed files with 171106 additions and 0 deletions

View File

@ -0,0 +1,2 @@
export { default as SimpleMenu } from './src/SimpleMenu.vue';
export { default as SimpleMenuTag } from './src/SimpleMenuTag.vue';

View File

@ -0,0 +1,195 @@
<template>
<Menu
v-bind="getBindValues"
:activeName="activeName"
:openNames="getOpenKeys"
:class="`${prefixCls} ${isThemeBright ? 'bright' : ''}`"
:activeSubMenuNames="activeSubMenuNames"
@select="handleSelect"
>
<template v-for="item in items" :key="item.path">
<SimpleSubMenu :isThemeBright="isThemeBright" :item="item" :parent="true" :collapsedShowTitle="collapsedShowTitle" :collapse="collapse" />
</template>
</Menu>
</template>
<script lang="ts">
import type { MenuState } from './types';
import type { Menu as MenuType } from '/@/router/types';
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import { defineComponent, computed, ref, unref, reactive, toRefs, watch } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import Menu from './components/Menu.vue';
import SimpleSubMenu from './SimpleSubMenu.vue';
import { listenerRouteChange } from '/@/logics/mitt/routeChange';
import { propTypes } from '/@/utils/propTypes';
import { REDIRECT_NAME } from '/@/router/constant';
import { useRouter } from 'vue-router';
import { isFunction, isUrl } from '/@/utils/is';
import { openWindow } from '/@/utils';
import { useOpenKeys } from './useOpenKeys';
import { URL_HASH_TAB } from '/@/utils';
import { useAppStore } from '/@/store/modules/app';
export default defineComponent({
name: 'SimpleMenu',
components: {
Menu,
SimpleSubMenu,
},
inheritAttrs: false,
props: {
items: {
type: Array as PropType<MenuType[]>,
default: () => [],
},
collapse: propTypes.bool,
mixSider: propTypes.bool,
theme: propTypes.string,
accordion: propTypes.bool.def(true),
collapsedShowTitle: propTypes.bool,
beforeClickFn: {
type: Function as PropType<(key: string) => Promise<boolean>>,
},
isSplitMenu: propTypes.bool,
},
emits: ['menuClick'],
setup(props, { attrs, emit }) {
const currentActiveMenu = ref('');
const isClickGo = ref(false);
const appStore = useAppStore();
const isThemeBright = ref(false);
const menuState = reactive<MenuState>({
activeName: '',
openNames: [],
activeSubMenuNames: [],
});
const { currentRoute } = useRouter();
const { prefixCls } = useDesign('simple-menu');
const { items, accordion, mixSider, collapse } = toRefs(props);
const { setOpenKeys, getOpenKeys } = useOpenKeys(menuState, items, accordion, mixSider, collapse);
const getBindValues = computed(() => ({ ...attrs, ...props }));
watch(
() => props.collapse,
(collapse) => {
if (collapse) {
menuState.openNames = [];
} else {
setOpenKeys(currentRoute.value.path);
}
},
{ immediate: true }
);
watch(
() => props.items,
() => {
if (!props.isSplitMenu) {
return;
}
setOpenKeys(currentRoute.value.path);
},
{ flush: 'post' }
);
// update-begin--author:liaozhiyang---date:20240408---for【QQYUN-8922】左侧导航栏文字颜色调整区分彩色和暗黑
watch(
() => appStore.getProjectConfig.menuSetting,
(menuSetting) => {
isThemeBright.value = !!menuSetting?.isThemeBright;
},
{ immediate: true, deep: true }
);
// update-end--author:liaozhiyang---date:20240408---for【QQYUN-8922】左侧导航栏文字颜色调整区分彩色和暗黑
listenerRouteChange((route) => {
if (route.name === REDIRECT_NAME) return;
currentActiveMenu.value = route.meta?.currentActiveMenu as string;
handleMenuChange(route);
if (unref(currentActiveMenu)) {
menuState.activeName = unref(currentActiveMenu);
setOpenKeys(unref(currentActiveMenu));
}
});
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
if (unref(isClickGo)) {
isClickGo.value = false;
return;
}
const path = (route || unref(currentRoute)).path;
menuState.activeName = path;
setOpenKeys(path);
}
async function handleSelect(key: string) {
if (isUrl(key)) {
// update-begin--author:sunjianlei---date:20220408---for: 【VUEN-656】配置外部网址打不开原因是带了#号,需要替换一下
let url = key.replace(URL_HASH_TAB, '#');
window.open(url)
//openWindow(url);
// update-begin--author:sunjianlei---date:20220408---for: 【VUEN-656】配置外部网址打不开原因是带了#号,需要替换一下
return;
}
// update-begin--author:liaozhiyang---date:20240227---for【QQYUN-6366】内部路由也可以支持采用新浏览器tab打开
const findItem = getMatchingMenu(props.items, key);
if (findItem?.internalOrExternal == true) {
window.open(location.origin + key);
return;
}
// update-end--author:liaozhiyang---date:20240227---for【QQYUN-6366】内部路由也可以支持采用新浏览器tab打开
const { beforeClickFn } = props;
if (beforeClickFn && isFunction(beforeClickFn)) {
const flag = await beforeClickFn(key);
if (!flag) return;
}
emit('menuClick', key);
isClickGo.value = true;
setOpenKeys(key);
menuState.activeName = key;
}
/**
* 2024-02-27
* liaozhiyang
* 获取菜单中匹配的path所在的项
*/
const getMatchingMenu = (menus, path) => {
for (let i = 0, len = menus.length; i < len; i++) {
const item = menus[i];
if (item.path === path && !item.redirect && !item.paramPath) {
return item;
} else if (item.children?.length) {
const result = getMatchingMenu(item.children, path);
if (result) {
return result;
}
}
}
return '';
}
return {
prefixCls,
getBindValues,
handleSelect,
getOpenKeys,
...toRefs(menuState),
isThemeBright,
};
},
});
</script>
<style lang="less">
@import './index.less';
</style>

View File

@ -0,0 +1,68 @@
<template>
<span :class="getTagClass" v-if="getShowTag">{{ getContent }}</span>
</template>
<script lang="ts">
import type { Menu } from '/@/router/types';
import { defineComponent, computed } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { propTypes } from '/@/utils/propTypes';
export default defineComponent({
name: 'SimpleMenuTag',
props: {
item: {
type: Object as PropType<Menu>,
default: () => ({}),
},
dot: propTypes.bool,
collapseParent: propTypes.bool,
},
setup(props) {
const { prefixCls } = useDesign('simple-menu');
const getShowTag = computed(() => {
const { item } = props;
if (!item) return false;
const { tag } = item;
if (!tag) return false;
const { dot, content } = tag;
if (!dot && !content) return false;
return true;
});
const getContent = computed(() => {
if (!getShowTag.value) return '';
const { item, collapseParent } = props;
const { tag } = item;
const { dot, content } = tag!;
return dot || collapseParent ? '' : content;
});
const getTagClass = computed(() => {
const { item, collapseParent } = props;
const { tag = {} } = item || {};
const { dot, type = 'error' } = tag;
const tagCls = `${prefixCls}-tag`;
return [
tagCls,
[`${tagCls}--${type}`],
{
[`${tagCls}--collapse`]: collapseParent,
[`${tagCls}--dot`]: dot || props.dot,
},
];
});
return {
getTagClass,
getShowTag,
getContent,
};
},
});
</script>

View File

@ -0,0 +1,117 @@
<template>
<MenuItem :name="item.path" v-if="!menuHasChildren(item) && getShowMenu" v-bind="$props" :class="getLevelClass">
<Icon v-if="getIcon" :icon="getIcon" :size="16" />
<div v-if="collapsedShowTitle && getIsCollapseParent" class="mt-1 collapse-title">
{{ getI18nName }}
</div>
<template #title>
<span :class="['ml-2', `${prefixCls}-sub-title`]">
{{ getI18nName }}
</span>
<SimpleMenuTag :item="item" :collapseParent="getIsCollapseParent" />
</template>
</MenuItem>
<SubMenu
:isThemeBright="isThemeBright"
:name="item.path"
v-if="menuHasChildren(item) && getShowMenu"
:class="[getLevelClass, theme]"
:collapsedShowTitle="collapsedShowTitle"
>
<template #title>
<Icon v-if="getIcon" :icon="getIcon" :size="16" />
<div v-if="collapsedShowTitle && getIsCollapseParent" class="mt-2 collapse-title">
{{ getI18nName }}
</div>
<span v-show="getShowSubTitle" :class="['ml-2', `${prefixCls}-sub-title`]">
{{ getI18nName }}
</span>
<SimpleMenuTag :item="item" :collapseParent="!!collapse && !!parent" />
</template>
<template v-for="childrenItem in item.children || []" :key="childrenItem.path">
<SimpleSubMenu v-bind="$props" :isThemeBright="isThemeBright" :item="childrenItem" :parent="false" />
</template>
</SubMenu>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import type { Menu } from '/@/router/types';
import { defineComponent, computed } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import Icon from '/@/components/Icon/index';
import { checkChildrenHidden } from '/@/utils/common/compUtils';
import MenuItem from './components/MenuItem.vue';
import SubMenu from './components/SubMenuItem.vue';
import { propTypes } from '/@/utils/propTypes';
import { useI18n } from '/@/hooks/web/useI18n';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export default defineComponent({
name: 'SimpleSubMenu',
components: {
SubMenu,
MenuItem,
SimpleMenuTag: createAsyncComponent(() => import('./SimpleMenuTag.vue')),
Icon,
},
props: {
item: {
type: Object as PropType<Menu>,
default: () => ({}),
},
parent: propTypes.bool,
collapsedShowTitle: propTypes.bool,
collapse: propTypes.bool,
theme: propTypes.oneOf(['dark', 'light']),
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8927】侧边栏导航二级菜单彩色模式文字颜色调整
isThemeBright: {
type: Boolean,
default: false,
},
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8927】侧边栏导航二级菜单彩色模式文字颜色调整
},
setup(props) {
const { t } = useI18n();
const { prefixCls } = useDesign('simple-menu');
const getShowMenu = computed(() => !props.item?.meta?.hideMenu);
const getIcon = computed(() => props.item?.icon);
const getI18nName = computed(() => t(props.item?.name));
const getShowSubTitle = computed(() => !props.collapse || !props.parent);
const getIsCollapseParent = computed(() => !!props.collapse && !!props.parent);
const getLevelClass = computed(() => {
return [
{
[`${prefixCls}__parent`]: props.parent,
[`${prefixCls}__children`]: !props.parent,
},
];
});
function menuHasChildren(menuTreeItem): boolean {
return (
!menuTreeItem.meta?.hideChildrenInMenu &&
Reflect.has(menuTreeItem, 'children') &&
!!menuTreeItem.children &&
menuTreeItem.children.length > 0
&&checkChildrenHidden(menuTreeItem)
);
}
return {
prefixCls,
menuHasChildren,
checkChildrenHidden,
getShowMenu,
getIcon,
getI18nName,
getShowSubTitle,
getLevelClass,
getIsCollapseParent,
};
},
});
</script>

View File

@ -0,0 +1,148 @@
<template>
<ul :class="getClass">
<slot></slot>
</ul>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import type { SubMenuProvider } from './types';
import { defineComponent, ref, computed, onMounted, watchEffect, watch, nextTick, getCurrentInstance, provide } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { propTypes } from '/@/utils/propTypes';
import { createSimpleRootMenuContext } from './useSimpleMenuContext';
import mitt from '/@/utils/mitt';
export default defineComponent({
name: 'Menu',
props: {
theme: propTypes.oneOf(['light', 'dark']).def('light'),
activeName: propTypes.oneOfType([propTypes.string, propTypes.number]),
openNames: {
type: Array as PropType<string[]>,
default: () => [],
},
accordion: propTypes.bool.def(true),
width: propTypes.string.def('100%'),
collapsedWidth: propTypes.string.def('48px'),
indentSize: propTypes.number.def(16),
collapse: propTypes.bool.def(true),
activeSubMenuNames: {
type: Array as PropType<(string | number)[]>,
default: () => [],
},
},
emits: ['select', 'open-change'],
setup(props, { emit }) {
const rootMenuEmitter = mitt();
const instance = getCurrentInstance();
const currentActiveName = ref<string | number>('');
const openedNames = ref<string[]>([]);
const { prefixCls } = useDesign('menu');
const isRemoveAllPopup = ref(false);
createSimpleRootMenuContext({
rootMenuEmitter: rootMenuEmitter,
activeName: currentActiveName,
});
const getClass = computed(() => {
const { theme } = props;
return [
prefixCls,
`${prefixCls}-${theme}`,
`${prefixCls}-vertical`,
{
[`${prefixCls}-collapse`]: props.collapse,
},
];
});
watchEffect(() => {
openedNames.value = props.openNames;
});
watchEffect(() => {
if (props.activeName) {
currentActiveName.value = props.activeName;
}
});
watch(
() => props.openNames,
() => {
nextTick(() => {
updateOpened();
});
}
);
function updateOpened() {
rootMenuEmitter.emit('on-update-opened', openedNames.value);
}
function addSubMenu(name: string) {
if (openedNames.value.includes(name)) return;
openedNames.value.push(name);
updateOpened();
}
function removeSubMenu(name: string) {
openedNames.value = openedNames.value.filter((item) => item !== name);
updateOpened();
}
function removeAll() {
openedNames.value = [];
updateOpened();
}
function sliceIndex(index: number) {
if (index === -1) return;
openedNames.value = openedNames.value.slice(0, index + 1);
updateOpened();
}
provide<SubMenuProvider>(`subMenu:${instance?.uid}`, {
addSubMenu,
removeSubMenu,
getOpenNames: () => openedNames.value,
removeAll,
isRemoveAllPopup,
sliceIndex,
level: 0,
props: props as any,
});
onMounted(() => {
openedNames.value = !props.collapse ? [...props.openNames] : [];
updateOpened();
rootMenuEmitter.on('on-menu-item-select', (name: string) => {
currentActiveName.value = name;
nextTick(() => {
props.collapse && removeAll();
});
emit('select', name);
});
rootMenuEmitter.on('open-name-change', ({ name, opened }) => {
if (opened && !openedNames.value.includes(name)) {
openedNames.value.push(name);
} else if (!opened) {
const index = openedNames.value.findIndex((item) => item === name);
index !== -1 && openedNames.value.splice(index, 1);
}
});
});
return { getClass, openedNames };
},
});
</script>
<style lang="less">
@import './menu.less';
</style>

View File

@ -0,0 +1,78 @@
<template>
<transition mode="out-in" v-on="on">
<slot></slot>
</transition>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { addClass, removeClass } from '/@/utils/domUtils';
export default defineComponent({
name: 'MenuCollapseTransition',
setup() {
return {
on: {
beforeEnter(el) {
addClass(el, 'collapse-transition');
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.style.height = '0';
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
},
enter(el) {
el.dataset.oldOverflow = el.style.overflow;
if (el.scrollHeight !== 0) {
el.style.height = el.scrollHeight + 'px';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
} else {
el.style.height = '';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
el.style.overflow = 'hidden';
},
afterEnter(el) {
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
},
beforeLeave(el) {
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.dataset.oldOverflow = el.style.overflow;
el.style.height = el.scrollHeight + 'px';
el.style.overflow = 'hidden';
},
leave(el) {
if (el.scrollHeight !== 0) {
addClass(el, 'collapse-transition');
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
},
afterLeave(el) {
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
},
},
};
},
});
</script>

View File

@ -0,0 +1,127 @@
<template>
<li :class="getClass" @click.stop="handleClickItem" :style="getCollapse ? {} : getItemStyle">
<Tooltip placement="right" v-if="showTooptip">
<template #title>
<slot name="title"></slot>
</template>
<div :class="`${prefixCls}-tooltip`">
<slot></slot>
</div>
</Tooltip>
<template v-else>
<slot></slot>
<slot name="title"></slot>
</template>
</li>
</template>
<script lang="ts">
import { PropType } from 'vue';
import { defineComponent, ref, computed, unref, getCurrentInstance, watch } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { propTypes } from '/@/utils/propTypes';
import { useMenuItem } from './useMenu';
import { Tooltip } from 'ant-design-vue';
import { useSimpleRootMenuContext } from './useSimpleMenuContext';
import { useLocaleStore } from '/@/store/modules/locale';
export default defineComponent({
name: 'MenuItem',
components: { Tooltip },
props: {
name: {
type: [String, Number] as PropType<string | number>,
required: true,
},
disabled: propTypes.bool,
},
setup(props, { slots }) {
const instance = getCurrentInstance();
const localeStore = useLocaleStore();
const active = ref(false);
const { getItemStyle, getParentList, getParentMenu, getParentRootMenu } = useMenuItem(instance);
const { prefixCls } = useDesign('menu');
const { rootMenuEmitter, activeName } = useSimpleRootMenuContext();
const getClass = computed(() => {
return [
`${prefixCls}-item`,
{
[`${prefixCls}-item-active`]: unref(active),
[`${prefixCls}-item-selected`]: unref(active),
[`${prefixCls}-item-disabled`]: !!props.disabled,
},
];
});
const getCollapse = computed(() => unref(getParentRootMenu)?.props.collapse);
const showTooptip = computed(() => {
return unref(getParentMenu)?.type.name === 'Menu' && unref(getCollapse) && slots.title;
});
function handleClickItem() {
const { disabled } = props;
if (disabled) {
return;
}
rootMenuEmitter.emit('on-menu-item-select', props.name);
if (unref(getCollapse)) {
return;
}
const { uidList } = getParentList();
rootMenuEmitter.emit('on-update-opened', {
opend: false,
parent: instance?.parent,
uidList: uidList,
});
}
watch(
() => activeName.value,
(name: string) => {
if (name === props.name) {
const { list, uidList } = getParentList();
active.value = true;
list.forEach((item) => {
if (item.proxy) {
(item.proxy as any).active = true;
}
});
//存储路径和标题的关系
storePathTitle(props.name);
rootMenuEmitter.emit('on-update-active-name:submenu', uidList);
} else {
active.value = false;
}
},
{ immediate: true }
);
//update-begin-author:taoyan date:2022-6-1 for: VUEN-1144 online 配置成菜单后,打开菜单,显示名称未展示为菜单名称
function storePathTitle(path) {
console.log('storePathTitle', path);
let title = '';
if (instance!.attrs) {
let item: any = instance!.attrs.item;
if (item) {
title = item.title;
}
}
if (localeStore) {
localeStore.setPathTitle(path, title);
}
}
//update-end-author:taoyan date:2022-6-1 for: VUEN-1144 online 配置成菜单后,打开菜单,显示名称未展示为菜单名称
return { getClass, prefixCls, getItemStyle, getCollapse, handleClickItem, showTooptip };
},
});
</script>

View File

@ -0,0 +1,319 @@
<template>
<li :class="getClass">
<template v-if="!getCollapse">
<div :class="`${prefixCls}-submenu-title`" @click.stop="handleClick" :style="getItemStyle">
<slot name="title" :getIsOpened="getIsOpend"></slot>
<Icon icon="eva:arrow-ios-downward-outline" :size="14" :class="`${prefixCls}-submenu-title-icon`" />
</div>
<CollapseTransition>
<ul :class="prefixCls" v-show="opened">
<slot></slot>
</ul>
</CollapseTransition>
</template>
<Popover
placement="right"
:overlayClassName="`${prefixCls}-menu-popover`"
v-else
:open="getIsOpend"
@openChange="handleVisibleChange"
:overlayStyle="getOverlayStyle"
:align="{ offset: [0, 0] }"
>
<div :class="getSubClass" v-bind="getEvents(false)">
<div
:class="[
{
[`${prefixCls}-submenu-popup`]: !getParentSubMenu,
[`${prefixCls}-submenu-collapsed-show-tit`]: collapsedShowTitle,
},
]"
>
<slot name="title"></slot>
</div>
<Icon v-if="getParentSubMenu" icon="eva:arrow-ios-downward-outline" :size="14" :class="`${prefixCls}-submenu-title-icon`" />
</div>
<!-- eslint-disable-next-line -->
<template #content v-show="opened">
<div v-bind="getEvents(true)">
<ul :class="[prefixCls, `${prefixCls}-${getTheme}`, `${prefixCls}-popup`, `${isThemeBright && 'bright'}`]">
<slot></slot>
</ul>
</div>
</template>
</Popover>
</li>
</template>
<script lang="ts">
import type { CSSProperties, PropType } from 'vue';
import type { SubMenuProvider } from './types';
import { defineComponent, computed, unref, getCurrentInstance, toRefs, reactive, provide, onBeforeMount, inject } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { propTypes } from '/@/utils/propTypes';
import { useMenuItem } from './useMenu';
import { useSimpleRootMenuContext } from './useSimpleMenuContext';
import { CollapseTransition } from '/@/components/Transition';
import Icon from '/@/components/Icon';
import { Popover } from 'ant-design-vue';
import { isBoolean, isObject } from '/@/utils/is';
import mitt from '/@/utils/mitt';
const DELAY = 200;
export default defineComponent({
name: 'SubMenu',
components: {
Icon,
CollapseTransition,
Popover,
},
props: {
name: {
type: [String, Number] as PropType<string | number>,
required: true,
},
disabled: propTypes.bool,
collapsedShowTitle: propTypes.bool,
// update-begin--author:liaozhiyang---date:20240417---for:【QQYUN-8927】侧边栏导航二级菜单彩色模式文字颜色调整
isThemeBright: {
type: Boolean,
default: false,
},
// update-end--author:liaozhiyang---date:20240417---for:【QQYUN-8927】侧边栏导航二级菜单彩色模式文字颜色调整
},
setup(props) {
const instance = getCurrentInstance();
const state = reactive({
active: false,
opened: false,
});
const data = reactive({
timeout: null as TimeoutHandle | null,
mouseInChild: false,
isChild: false,
});
const { getParentSubMenu, getItemStyle, getParentMenu, getParentList } = useMenuItem(instance);
const { prefixCls } = useDesign('menu');
const subMenuEmitter = mitt();
const { rootMenuEmitter } = useSimpleRootMenuContext();
const {
addSubMenu: parentAddSubmenu,
removeSubMenu: parentRemoveSubmenu,
removeAll: parentRemoveAll,
getOpenNames: parentGetOpenNames,
isRemoveAllPopup,
sliceIndex,
level,
props: rootProps,
handleMouseleave: parentHandleMouseleave,
} = inject<SubMenuProvider>(`subMenu:${getParentMenu.value?.uid}`)!;
const getClass = computed(() => {
return [
`${prefixCls}-submenu`,
{
[`${prefixCls}-item-active`]: state.active,
[`${prefixCls}-opened`]: state.opened,
[`${prefixCls}-submenu-disabled`]: props.disabled,
[`${prefixCls}-submenu-has-parent-submenu`]: unref(getParentSubMenu),
[`${prefixCls}-child-item-active`]: state.active,
},
];
});
const getAccordion = computed(() => rootProps.accordion);
const getCollapse = computed(() => rootProps.collapse);
const getTheme = computed(() => rootProps.theme);
const getOverlayStyle = computed((): CSSProperties => {
// update-begin--author:liaozhiyang---date:20240407---for【QQYUN-8774】侧边混合导航菜单宽度调整
return {
minWidth: '150px',
};
// update-end--author:liaozhiyang---date:20240407---for【QQYUN-8774】侧边混合导航菜单宽度调整
});
const getIsOpend = computed(() => {
const name = props.name;
if (unref(getCollapse)) {
return parentGetOpenNames().includes(name);
}
return state.opened;
});
const getSubClass = computed(() => {
const isActive = rootProps.activeSubMenuNames.includes(props.name);
return [
`${prefixCls}-submenu-title`,
{
[`${prefixCls}-submenu-active`]: isActive,
[`${prefixCls}-submenu-active-border`]: isActive && level === 0,
[`${prefixCls}-submenu-collapse`]: unref(getCollapse) && level === 0,
},
];
});
function getEvents(deep: boolean) {
if (!unref(getCollapse)) {
return {};
}
return {
onMouseenter: handleMouseenter,
onMouseleave: () => handleMouseleave(deep),
};
}
function handleClick() {
const { disabled } = props;
if (disabled || unref(getCollapse)) return;
const opened = state.opened;
if (unref(getAccordion)) {
const { uidList } = getParentList();
rootMenuEmitter.emit('on-update-opened', {
opend: false,
parent: instance?.parent,
uidList: uidList,
});
} else {
rootMenuEmitter.emit('open-name-change', {
name: props.name,
opened: !opened,
});
}
state.opened = !opened;
}
function handleMouseenter() {
const disabled = props.disabled;
if (disabled) return;
subMenuEmitter.emit('submenu:mouse-enter-child');
const index = parentGetOpenNames().findIndex((item) => item === props.name);
sliceIndex(index);
const isRoot = level === 0 && parentGetOpenNames().length === 2;
if (isRoot) {
parentRemoveAll();
}
data.isChild = parentGetOpenNames().includes(props.name);
clearTimeout(data.timeout!);
data.timeout = setTimeout(() => {
parentAddSubmenu(props.name);
}, DELAY);
}
function handleMouseleave(deepDispatch = false) {
const parentName = getParentMenu.value?.props.name;
if (!parentName) {
isRemoveAllPopup.value = true;
}
if (parentGetOpenNames().slice(-1)[0] === props.name) {
data.isChild = false;
}
subMenuEmitter.emit('submenu:mouse-leave-child');
if (data.timeout) {
clearTimeout(data.timeout!);
data.timeout = setTimeout(() => {
if (isRemoveAllPopup.value) {
parentRemoveAll();
} else if (!data.mouseInChild) {
parentRemoveSubmenu(props.name);
}
}, DELAY);
}
if (deepDispatch) {
if (getParentSubMenu.value) {
parentHandleMouseleave?.(true);
}
}
}
onBeforeMount(() => {
subMenuEmitter.on('submenu:mouse-enter-child', () => {
data.mouseInChild = true;
isRemoveAllPopup.value = false;
clearTimeout(data.timeout!);
});
subMenuEmitter.on('submenu:mouse-leave-child', () => {
if (data.isChild) return;
data.mouseInChild = false;
clearTimeout(data.timeout!);
});
rootMenuEmitter.on('on-update-opened', (data: boolean | (string | number)[] | Recordable) => {
if (unref(getCollapse)) return;
if (isBoolean(data)) {
state.opened = data;
return;
}
if (isObject(data) && rootProps.accordion) {
const { opend, parent, uidList } = data as Recordable;
if (parent === instance?.parent) {
state.opened = opend;
} else if (!uidList.includes(instance?.uid)) {
state.opened = false;
}
return;
}
if (props.name && Array.isArray(data)) {
state.opened = (data as (string | number)[]).includes(props.name);
}
});
rootMenuEmitter.on('on-update-active-name:submenu', (data: number[]) => {
if (instance?.uid) {
state.active = data.includes(instance?.uid);
}
});
});
function handleVisibleChange(visible: boolean) {
state.opened = visible;
}
// provide
provide<SubMenuProvider>(`subMenu:${instance?.uid}`, {
addSubMenu: parentAddSubmenu,
removeSubMenu: parentRemoveSubmenu,
getOpenNames: parentGetOpenNames,
removeAll: parentRemoveAll,
isRemoveAllPopup,
sliceIndex,
level: level + 1,
handleMouseleave,
props: rootProps,
});
return {
getClass,
prefixCls,
getCollapse,
getItemStyle,
handleClick,
handleVisibleChange,
getParentSubMenu,
getOverlayStyle,
getTheme,
getIsOpend,
getEvents,
getSubClass,
...toRefs(state),
...toRefs(data),
};
},
});
</script>

View File

@ -0,0 +1,340 @@
@menu-prefix-cls: ~'@{namespace}-menu';
@menu-popup-prefix-cls: ~'@{namespace}-menu-popup';
@submenu-popup-prefix-cls: ~'@{namespace}-menu-submenu-popup';
@transition-time: 0.2s;
@menu-dark-subsidiary-color: rgba(255, 255, 255, 0.7);
.light-border {
&::after {
position: absolute;
top: 0;
right: 0;
bottom: 0;
display: block;
width: 2px;
background-color: @primary-color;
content: '';
}
}
.@{menu-prefix-cls}-menu-popover {
.ant-popover-arrow {
display: none;
}
.ant-popover-inner-content {
padding: 0;
}
.@{menu-prefix-cls} {
&-opened > * > &-submenu-title-icon {
transform: translateY(-50%) rotate(90deg) !important;
}
&-item,
&-submenu-title {
position: relative;
z-index: 1;
padding: 10px 14px;
color: @menu-dark-subsidiary-color;
cursor: pointer;
transition: all @transition-time @ease-in-out;
&-icon {
position: absolute;
top: 50%;
right: 18px;
transform: translateY(-50%) rotate(-90deg);
transition: transform @transition-time @ease-in-out;
}
}
&-dark {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu-title {
color: @menu-dark-subsidiary-color;
// background: @menu-dark-active-bg;
&:hover {
color: #fff;
}
&-selected {
color: #fff;
background-color: @primary-color !important;
}
}
// 彩色模式(绿色,橘红等)
&.bright {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu-title {
color: #fff;
&:hover {
color: rgba(255, 255, 255, 0.8);
}
}
}
}
&-light {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu-title {
color: @text-color-base;
&:hover {
color: @primary-color;
}
&-selected {
z-index: 2;
color: @primary-color;
background-color: fade(@primary-color, 10);
.light-border();
}
}
}
}
}
.content();
.content() {
.@{menu-prefix-cls} {
position: relative;
display: block;
width: 100%;
padding: 0;
margin: 0;
font-size: @font-size-base;
color: @text-color-base;
list-style: none;
outline: none;
// .collapse-transition {
// transition: @transition-time height ease-in-out, @transition-time padding-top ease-in-out,
// @transition-time padding-bottom ease-in-out;
// }
&-light {
background-color: #fff;
color: rgba(0, 0, 0, 0.65);
.@{menu-prefix-cls} {
color: rgba(0, 0, 0, 0.65);
}
.@{namespace}-menu-submenu:not(.@{namespace}-menu-item-active) .@{namespace}-menu-submenu-title {
.anticon {
color: rgba(0, 0, 0, 0.9);
}
}
.@{menu-prefix-cls}-submenu-active {
color: @primary-color !important;
&-border {
.light-border();
}
}
}
&-dark {
.@{menu-prefix-cls}-submenu-active {
color: #fff !important;
}
}
&-item {
position: relative;
z-index: 1;
display: flex;
font-size: @font-size-base;
list-style: none;
cursor: pointer;
outline: none;
align-items: center;
&:hover,
&:active {
color: inherit;
}
}
&-item > i {
margin-right: 6px;
}
&-submenu-title > i,
&-submenu-title span > i {
margin-right: 8px;
}
// vertical
&-vertical &-item,
&-vertical &-submenu-title {
position: relative;
z-index: 1;
padding: 14px 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
&:hover {
color: @primary-color;
}
.@{menu-prefix-cls}-tooltip {
width: calc(100% - 0px);
padding: 12px 0;
text-align: center;
}
.@{menu-prefix-cls}-submenu-popup {
padding: 12px 0;
}
}
&-vertical &-submenu-collapse {
.@{submenu-popup-prefix-cls} {
display: flex;
justify-content: center;
align-items: center;
}
.@{menu-prefix-cls}-submenu-collapsed-show-tit {
flex-direction: column;
}
}
&-vertical&-collapse &-item,
&-vertical&-collapse &-submenu-title {
padding: 0 0;
}
&-vertical &-submenu-title-icon {
position: absolute;
top: 50%;
right: 18px;
transform: translateY(-50%);
}
&-submenu-title-icon {
transition: transform @transition-time @ease-in-out;
}
&-vertical &-opened > * > &-submenu-title-icon {
transform: translateY(-50%) rotate(180deg);
}
&-vertical &-submenu {
&-nested {
padding-left: 20px;
}
.@{menu-prefix-cls}-item {
padding-left: 43px;
}
}
&-light&-vertical &-item {
&-active:not(.@{menu-prefix-cls}-submenu) {
z-index: 2;
color: @primary-color;
background-color: fade(@primary-color, 10);
.light-border();
}
&-active.@{menu-prefix-cls}-submenu {
color: @primary-color;
}
}
&-light&-vertical&-collapse {
> li.@{menu-prefix-cls}-item-active,
.@{menu-prefix-cls}-submenu-active {
position: relative;
background-color: fade(@primary-color, 5);
&::after {
display: none;
}
&::before {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background-color: @primary-color;
content: '';
}
}
}
&-dark&-vertical &-item,
&-dark&-vertical &-submenu-title {
color: @menu-dark-subsidiary-color;
&-active:not(.@{menu-prefix-cls}-submenu) {
color: #fff !important;
background-color: @primary-color !important;
}
&:hover {
color: #fff;
}
}
// update-begin--author:liaozhiyang---date:20240408---for【QQYUN-8922】左侧导航栏文字颜色调整区分彩色和暗黑
&-dark&-vertical&.bright &-item,
&-dark&-vertical.bright &-submenu-title {
color: rgba(255, 255, 255, 1);
&-active:not(.@{menu-prefix-cls}-submenu) {
color: #fff !important;
background-color: @primary-color !important;
}
&:hover {
color: rgba(255, 255, 255, 0.8);
}
}
// update-end--author:liaozhiyang---date:20240408---for【QQYUN-8922】左侧导航栏文字颜色调整区分彩色和暗黑
&-dark&-vertical&-collapse {
> li.@{menu-prefix-cls}-item-active,
.@{menu-prefix-cls}-submenu-active {
position: relative;
color: #fff !important;
background-color: @primary-color !important;
&::before {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background-color: @primary-color;
content: '';
}
.@{menu-prefix-cls}-submenu-collapse {
background-color: transparent;
}
}
}
&-dark&-vertical &-submenu &-item {
&-active,
&-active:hover {
color: #fff;
border-right: none;
}
}
&-dark&-vertical &-child-item-active > &-submenu-title {
color: #fff;
}
&-dark&-vertical &-opened {
.@{menu-prefix-cls}-submenu-has-parent-submenu {
.@{menu-prefix-cls}-submenu-title {
background-color: transparent;
}
}
}
}
}

View File

@ -0,0 +1,25 @@
import { Ref } from 'vue';
export interface Props {
theme: string;
activeName?: string | number | undefined;
openNames: string[];
accordion: boolean;
width: string;
collapsedWidth: string;
indentSize: number;
collapse: boolean;
activeSubMenuNames: (string | number)[];
}
export interface SubMenuProvider {
addSubMenu: (name: string | number, update?: boolean) => void;
removeSubMenu: (name: string | number, update?: boolean) => void;
removeAll: () => void;
sliceIndex: (index: number) => void;
isRemoveAllPopup: Ref<boolean>;
getOpenNames: () => (string | number)[];
handleMouseleave?: Fn;
level: number;
props: Props;
}

View File

@ -0,0 +1,84 @@
import { computed, ComponentInternalInstance, unref } from 'vue';
import type { CSSProperties } from 'vue';
export function useMenuItem(instance: ComponentInternalInstance | null) {
const getParentMenu = computed(() => {
return findParentMenu(['Menu', 'SubMenu']);
});
const getParentRootMenu = computed(() => {
return findParentMenu(['Menu']);
});
const getParentSubMenu = computed(() => {
return findParentMenu(['SubMenu']);
});
const getItemStyle = computed((): CSSProperties => {
let parent = instance?.parent;
if (!parent) return {};
const indentSize = (unref(getParentRootMenu)?.props.indentSize as number) ?? 20;
let padding = indentSize;
if (unref(getParentRootMenu)?.props.collapse) {
padding = indentSize;
} else {
while (parent && parent.type.name !== 'Menu') {
if (parent.type.name === 'SubMenu') {
padding += indentSize;
}
parent = parent.parent;
}
}
return { paddingLeft: padding + 'px' };
});
function findParentMenu(name: string[]) {
let parent = instance?.parent;
if (!parent) return null;
while (parent && name.indexOf(parent.type.name!) === -1) {
parent = parent.parent;
}
return parent;
}
function getParentList() {
let parent = instance;
if (!parent)
return {
uidList: [],
list: [],
};
const ret: any[] = [];
while (parent && parent.type.name !== 'Menu') {
if (parent.type.name === 'SubMenu') {
ret.push(parent);
}
parent = parent.parent;
}
return {
uidList: ret.map((item) => item.uid),
list: ret,
};
}
function getParentInstance(instance: ComponentInternalInstance, name = 'SubMenu') {
let parent = instance.parent;
while (parent) {
if (parent.type.name !== name) {
return parent;
}
parent = parent.parent;
}
return parent;
}
return {
getParentMenu,
getParentInstance,
getParentRootMenu,
getParentList,
getParentSubMenu,
getItemStyle,
};
}

View File

@ -0,0 +1,18 @@
import type { InjectionKey, Ref } from 'vue';
import type { Emitter } from '/@/utils/mitt';
import { createContext, useContext } from '/@/hooks/core/useContext';
export interface SimpleRootMenuContextProps {
rootMenuEmitter: Emitter;
activeName: Ref<string | number>;
}
const key: InjectionKey<SimpleRootMenuContextProps> = Symbol();
export function createSimpleRootMenuContext(context: SimpleRootMenuContextProps) {
return createContext<SimpleRootMenuContextProps>(context, key, { readonly: false, native: true });
}
export function useSimpleRootMenuContext() {
return useContext<SimpleRootMenuContextProps>(key);
}

View File

@ -0,0 +1,77 @@
@simple-prefix-cls: ~'@{namespace}-simple-menu';
@prefix-cls: ~'@{namespace}-menu';
.@{prefix-cls} {
&-dark&-vertical .@{simple-prefix-cls}__parent {
background-color: @sider-dark-bg-color;
> .@{prefix-cls}-submenu-title {
background-color: @sider-dark-bg-color;
}
}
&-dark&-vertical .@{simple-prefix-cls}__children,
&-dark&-popup .@{simple-prefix-cls}__children {
background-color: @sider-dark-lighten-bg-color;
> .@{prefix-cls}-submenu-title {
background-color: @sider-dark-lighten-bg-color;
}
}
.collapse-title {
overflow: hidden;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.@{simple-prefix-cls} {
&-sub-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: all 0.3s;
}
&-tag {
position: absolute;
top: calc(50% - 8px);
right: 30px;
display: inline-block;
padding: 2px 3px;
margin-right: 4px;
font-size: 10px;
line-height: 14px;
color: #fff;
border-radius: 2px;
&--collapse {
top: 6px !important;
right: 2px;
}
&--dot {
top: calc(50% - 2px);
width: 6px;
height: 6px;
padding: 0;
border-radius: 50%;
}
&--primary {
background-color: @primary-color;
}
&--error {
background-color: @error-color;
}
&--success {
background-color: @success-color;
}
&--warn {
background-color: @warning-color;
}
}
}

View File

@ -0,0 +1,5 @@
export interface MenuState {
activeName: string;
openNames: string[];
activeSubMenuNames: string[];
}

View File

@ -0,0 +1,44 @@
import type { Menu as MenuType } from '/@/router/types';
import type { MenuState } from './types';
import { computed, Ref, toRaw } from 'vue';
import { unref } from 'vue';
import { uniq } from 'lodash-es';
import { getAllParentPath } from '/@/router/helper/menuHelper';
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
import { useDebounceFn } from '@vueuse/core';
export function useOpenKeys(menuState: MenuState, menus: Ref<MenuType[]>, accordion: Ref<boolean>, mixSider: Ref<boolean>, collapse: Ref<boolean>) {
const debounceSetOpenKeys = useDebounceFn(setOpenKeys, 50);
async function setOpenKeys(path: string) {
const native = !mixSider.value;
const menuList = toRaw(menus.value);
useTimeoutFn(
() => {
if (menuList?.length === 0) {
menuState.activeSubMenuNames = [];
menuState.openNames = [];
return;
}
const keys = getAllParentPath(menuList, path);
if (!unref(accordion)) {
menuState.openNames = uniq([...menuState.openNames, ...keys]);
} else {
menuState.openNames = keys;
}
menuState.activeSubMenuNames = menuState.openNames;
},
30,
native
);
}
const getOpenKeys = computed(() => {
return unref(collapse) ? [] : menuState.openNames;
});
return { setOpenKeys: debounceSetOpenKeys, getOpenKeys };
}