mirror of
https://github.com/jeecgboot/JeecgBoot.git
synced 2025-12-23 22:36:39 +08:00
前端和后端源码,合并到一个git仓库中,方便用户下载,避免前后端不匹配的问题
This commit is contained in:
2
jeecgboot-vue3/src/components/SimpleMenu/index.ts
Normal file
2
jeecgboot-vue3/src/components/SimpleMenu/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { default as SimpleMenu } from './src/SimpleMenu.vue';
|
||||
export { default as SimpleMenuTag } from './src/SimpleMenuTag.vue';
|
||||
195
jeecgboot-vue3/src/components/SimpleMenu/src/SimpleMenu.vue
Normal file
195
jeecgboot-vue3/src/components/SimpleMenu/src/SimpleMenu.vue
Normal 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>
|
||||
@ -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>
|
||||
117
jeecgboot-vue3/src/components/SimpleMenu/src/SimpleSubMenu.vue
Normal file
117
jeecgboot-vue3/src/components/SimpleMenu/src/SimpleSubMenu.vue
Normal 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>
|
||||
148
jeecgboot-vue3/src/components/SimpleMenu/src/components/Menu.vue
Normal file
148
jeecgboot-vue3/src/components/SimpleMenu/src/components/Menu.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
77
jeecgboot-vue3/src/components/SimpleMenu/src/index.less
Normal file
77
jeecgboot-vue3/src/components/SimpleMenu/src/index.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
5
jeecgboot-vue3/src/components/SimpleMenu/src/types.ts
Normal file
5
jeecgboot-vue3/src/components/SimpleMenu/src/types.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface MenuState {
|
||||
activeName: string;
|
||||
openNames: string[];
|
||||
activeSubMenuNames: string[];
|
||||
}
|
||||
44
jeecgboot-vue3/src/components/SimpleMenu/src/useOpenKeys.ts
Normal file
44
jeecgboot-vue3/src/components/SimpleMenu/src/useOpenKeys.ts
Normal 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user