项目初始化

This commit is contained in:
2026-03-19 10:57:24 +08:00
commit ee94d420ad
3822 changed files with 582614 additions and 0 deletions

View File

@@ -0,0 +1,171 @@
<template>
<Menu
v-bind="getBindValues"
:activeName="activeName"
:openNames="getOpenKeys"
:class="prefixCls"
:activeSubMenuNames="activeSubMenuNames"
@select="handleSelect"
>
<slot name="menuBefore"></slot>
<template v-for="item in items" :key="item.path">
<SimpleSubMenu :item="item" :parent="true" :collapsedShowTitle="collapsedShowTitle" :collapse="collapse" />
</template>
<slot name="menuAfter"></slot>
</Menu>
</template>
<script lang="ts">
import type { MenuState } from './types';
import type { Menu as MenuType } from '@jeesite/core/router/types';
import type { RouteLocationNormalizedLoaded } from 'vue-router';
import { defineComponent, computed, ref, Ref, unref, reactive, toRefs, watch } from 'vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import Menu from './components/Menu.vue';
import SimpleSubMenu from './SimpleSubMenu.vue';
import { listenerRouteChange } from '@jeesite/core/logics/mitt/routeChange';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { REDIRECT_NAME } from '@jeesite/core/router/constant';
import { useRouter } from 'vue-router';
import { isFunction, isUrl } from '@jeesite/core/utils/is';
import { openWindow } from '@jeesite/core/utils';
import { useOpenKeys } from './useOpenKeys';
const 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,
};
export default defineComponent({
name: 'SimpleMenu',
components: {
Menu,
SimpleSubMenu,
},
inheritAttrs: false,
props,
emits: ['menuClick'],
setup(props, { attrs, emit }) {
// const currentActiveMenu = ref('');
const isClickGo = ref(false);
const menuState = reactive<MenuState>({
activeName: '',
openNames: [],
activeSubMenuNames: [],
});
const { currentRoute } = useRouter();
const { prefixCls } = useDesign('simple-menu');
const { items, accordion, mixSider, collapse } = toRefs(props) as {
items: Ref<MenuType[]>;
accordion: Ref<boolean>;
mixSider: Ref<boolean>;
collapse: Ref<boolean>;
};
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);
handleMenuChange();
}
},
{ immediate: true },
);
watch(
() => props.items,
() => {
if (!props.isSplitMenu) {
return;
}
// setOpenKeys(currentRoute.value.path);
handleMenuChange();
},
{ flush: 'post' },
);
listenerRouteChange((route) => {
if (route.name === REDIRECT_NAME) return;
// currentActiveMenu.value = route.meta?.currentActiveMenu as string;
// if (unref(currentActiveMenu)) {
// menuState.activeName = unref(currentActiveMenu);
// setOpenKeys(unref(currentActiveMenu));
// }
handleMenuChange(route);
});
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
if (unref(isClickGo)) {
isClickGo.value = false;
return;
}
// const path = (route || unref(currentRoute)).path;
const currRoute = route || unref(currentRoute);
const path = (currRoute.meta?.currentActiveMenu as string) || currRoute.path;
await setOpenKeys(path);
if (menuState.openNames.length > 0) {
menuState.activeName = menuState.openNames[menuState.openNames.length - 1];
} else {
menuState.activeName = path;
}
}
async function handleSelect(key: string, item: any) {
if (isUrl(key)) {
openWindow(key);
return;
}
const { beforeClickFn } = props;
if (beforeClickFn && isFunction(beforeClickFn)) {
const flag = await beforeClickFn(key);
if (!flag) return;
}
emit('menuClick', key, item);
isClickGo.value = true;
await setOpenKeys(key);
if (menuState.openNames.length > 0) {
menuState.activeName = menuState.openNames[menuState.openNames.length - 1];
} else {
menuState.activeName = key;
}
// console.log('SidebarMenuClick', menuState.activeName, menuState.openNames);
}
return {
prefixCls,
getBindValues,
handleSelect,
getOpenKeys,
...toRefs(menuState),
};
},
});
</script>
<style lang="less">
@import './index.less';
</style>

View File

@@ -0,0 +1,70 @@
<template>
<span :class="getTagClass" v-if="getShowTag">{{ getContent }}</span>
</template>
<script lang="ts">
import type { Menu } from '@jeesite/core/router/types';
import { defineComponent, computed } from 'vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { propTypes } from '@jeesite/core/utils/propTypes';
const props = {
item: {
type: Object as PropType<Menu>,
default: () => ({}),
},
dot: propTypes.bool,
collapseParent: propTypes.bool,
};
export default defineComponent({
name: 'SimpleMenuTag',
props,
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,127 @@
<template>
<MenuItem
:name="item.path"
v-if="!menuHasChildren(item) && getShowMenu"
v-bind="$props"
:class="getLevelClass"
:style="`color: ${getColor}`"
:title="getI18nName"
:item="getMenuItem"
>
<Icon v-if="getIcon" :icon="getIcon" :size="16" />
<div v-if="collapsedShowTitle && getIsCollapseParent" class="collapse-title mt-1">
{{ getI18nName }}
</div>
<template #title>
<span :class="['ml-2', `${prefixCls}-sub-title`]">
{{ getI18nName }}
</span>
<SimpleMenuTag :item="item" :collapseParent="getIsCollapseParent" />
</template>
</MenuItem>
<SubMenu
:name="item.path"
v-if="menuHasChildren(item) && getShowMenu"
:class="[getLevelClass, theme]"
:collapsedShowTitle="collapsedShowTitle"
:style="`color: ${getColor}`"
:title="getI18nName"
>
<template #title>
<Icon v-if="getIcon" :icon="getIcon" :size="16" />
<div v-if="collapsedShowTitle && getIsCollapseParent" class="collapse-title mt-2">
{{ 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" :item="childrenItem" :parent="false" />
</template>
</SubMenu>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import type { Menu } from '@jeesite/core/router/types';
import { defineComponent, computed } from 'vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import Icon from '@jeesite/core/components/Icon';
import MenuItem from './components/MenuItem.vue';
import SubMenu from './components/SubMenuItem.vue';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { useI18n } from '@jeesite/core/hooks/web/useI18n';
import SimpleMenuTag from './SimpleMenuTag.vue';
import { omit } from 'lodash-es';
const props = {
item: {
type: Object as PropType<Menu>,
default: () => ({}),
},
parent: propTypes.bool,
collapsedShowTitle: propTypes.bool,
collapse: propTypes.bool,
theme: propTypes.oneOf(['dark', 'light']),
};
export default defineComponent({
name: 'SimpleSubMenu',
components: {
SubMenu,
MenuItem,
SimpleMenuTag,
Icon,
},
props,
setup(props) {
const { t } = useI18n();
const { prefixCls } = useDesign('simple-menu');
const getShowMenu = computed(() => !props.item?.meta?.hideMenu);
const getIcon = computed(() => props.item?.icon);
const getColor = computed(() => props.item?.color);
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,
},
];
});
const getMenuItem = computed(() => {
return omit(props.item, 'children', 'icon', 'title', 'color', 'extend');
});
function menuHasChildren(menuTreeItem: Menu): boolean {
return (
!menuTreeItem.meta?.hideChildrenInMenu &&
Reflect.has(menuTreeItem, 'children') &&
!!menuTreeItem.children &&
menuTreeItem.children.length > 0
);
}
return {
prefixCls,
menuHasChildren,
getShowMenu,
getIcon,
getColor,
getI18nName,
getShowSubTitle,
getLevelClass,
getIsCollapseParent,
getMenuItem,
};
},
});
</script>

View File

@@ -0,0 +1,161 @@
<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 '@jeesite/core/hooks/web/useDesign';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { createSimpleRootMenuContext } from './useSimpleMenuContext';
import { mitt } from '@jeesite/core/utils/mitt';
const 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: () => [],
},
};
export default defineComponent({
name: 'Menu',
props,
emits: ['select', 'open-change'],
setup(props, { emit }) {
const rootMenuEmitter = mitt<any>();
const instance = getCurrentInstance();
const currentActiveName = ref<string | number>('');
const openedNames = ref<(string | number)[]>([]);
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 | number) {
if (openedNames.value.includes(name)) return;
openedNames.value.push(name);
updateOpened();
}
function removeSubMenu(name: string | number) {
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, item }) => {
currentActiveName.value = name;
nextTick(() => {
props.collapse && removeAll();
});
emit('select', name, item);
});
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 '@jeesite/core/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,110 @@
<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>
<div style="position: absolute; top: 9px; right: 6px" v-if="getDemoMode && item.name == '角色管理'">
<Badge count="hot" />
</div>
</li>
</template>
<script lang="ts" setup name="MenuItem">
import { PropType, useSlots } from 'vue';
import { defineComponent, ref, computed, unref, getCurrentInstance, watch } from 'vue';
import { useDesign } from '@jeesite/core/hooks/web/useDesign';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { useMenuItem } from './useMenu';
import { Badge, Tooltip } from 'ant-design-vue';
import { useSimpleRootMenuContext } from './useSimpleMenuContext';
import { useUserStore } from '@jeesite/core/store/modules/user';
const props = defineProps({
name: {
type: [String, Number] as PropType<string | number>,
required: true,
},
item: {
type: Object,
default: {},
},
disabled: propTypes.bool,
});
const slots = useSlots();
const userStore = useUserStore();
const instance = getCurrentInstance();
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', { name: props.name, item: props.item });
if (unref(getCollapse)) {
return;
}
const { uidList } = getParentList();
rootMenuEmitter.emit('on-update-opened', {
opend: false,
parent: instance?.parent,
uidList: uidList,
});
}
watch(
() => activeName.value,
(name: string | number) => {
if (name === props.name) {
const { list, uidList } = getParentList();
active.value = true;
list.forEach((item) => {
if (item.proxy) {
(item.proxy as any).active = true;
}
});
rootMenuEmitter.emit('on-update-active-name:submenu', uidList);
} else {
active.value = false;
}
},
{ immediate: true },
);
const getDemoMode = computed(() => {
return userStore.getPageCacheByKey('demoMode');
});
</script>

View File

@@ -0,0 +1,329 @@
<template>
<li :class="getClass">
<template v-if="!getCollapse">
<div :class="`${prefixCls}-submenu-title`" @click.stop="handleClick" :style="getItemStyle">
<slot name="title"></slot>
<Icon icon="i-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"
@open-change="handleOpenChange"
: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="i-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`]">
<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 '@jeesite/core/hooks/web/useDesign';
import { propTypes } from '@jeesite/core/utils/propTypes';
import { useMenuItem } from './useMenu';
import { useSimpleRootMenuContext } from './useSimpleMenuContext';
import { CollapseTransition } from '@jeesite/core/components/Transition';
import { Icon } from '@jeesite/core/components/Icon';
import { Popover } from 'ant-design-vue';
import { isBoolean, isObject } from '@jeesite/core/utils/is';
import { mitt } from '@jeesite/core/utils/mitt';
const DELAY = 200;
const props = {
name: {
type: [String, Number] as PropType<string | number>,
required: true,
},
disabled: propTypes.bool,
collapsedShowTitle: propTypes.bool,
};
export default defineComponent({
name: 'SubMenu',
components: {
Icon,
CollapseTransition,
Popover,
},
props,
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 => {
return {
minWidth: '200px',
};
});
const getIsOpend = computed(() => {
const name = props.name;
if (unref(getCollapse)) {
return parentGetOpenNames().includes(name as string);
}
return state.opened;
});
const getSubClass = computed(() => {
const isActive = rootProps.activeSubMenuNames.includes(props.name as string);
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 as string);
clearTimeout(data.timeout!);
data.timeout = setTimeout(() => {
parentAddSubmenu(props.name as string);
}, 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 as string);
}
}, 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 handleOpenChange(open: boolean) {
state.opened = open;
}
// 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,
handleOpenChange,
getParentSubMenu,
getOverlayStyle,
getTheme,
getIsOpend,
getEvents,
getSubClass,
...toRefs(state),
...toRefs(data),
};
},
});
</script>

View File

@@ -0,0 +1,345 @@
@menu-prefix-cls: ~'jeesite-menu';
@menu-popup-prefix-cls: ~'jeesite-menu-popup';
@submenu-popup-prefix-cls: ~'jeesite-menu-submenu-popup';
@transition-time: 0.2s;
@menu-dark-subsidiary-color: rgba(255, 255, 255, 0.7);
.light-border {
border-radius: 10px; // 2
// &::after { // 1
// position: absolute;
// top: 0;
// right: 0;
// bottom: 0;
// display: block;
// width: 2px;
// background-color: @primary-color;
// content: '';
// }
}
.ant-popover.@{menu-prefix-cls}-menu-popover {
.ant-popover-arrow {
display: none;
}
.ant-popover-inner,
.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: 12px 20px; // 1
padding: 11px 20px; // 2
color: @menu-dark-subsidiary-color;
cursor: pointer;
transition: all @transition-time @ease-in-out;
&-icon {
position: absolute;
top: 50%;
// right: 18px; // 1
right: 10px; // 2
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: @sider-dark-bg-color;
&:hover {
color: #fff;
background-color: fade(@primary-color, 80);
}
&-selected {
color: #fff;
background-color: fade(@primary-color, 85);
}
}
}
&-light {
.@{menu-prefix-cls}-item,
.@{menu-prefix-cls}-submenu-title {
color: @text-color-base;
&:hover {
color: @primary-color;
background-color: fade(@primary-color, 5);
}
&-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;
color: fade(@text-color-base, 75);
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;
padding: 5px; // 2
.@{menu-prefix-cls}-submenu-active {
color: @primary-color !important;
&-border {
.light-border();
}
}
}
&-dark {
.@{menu-prefix-cls}-submenu-active {
color: #fff !important;
}
}
&-submenu-title {
font-weight: bold;
}
&-item {
position: relative;
z-index: 1;
display: flex;
color: inherit;
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;
}
.anticon {
width: 16px;
}
// vertical
&-vertical &-item,
&-vertical &-submenu-title {
position: relative;
z-index: 1;
// padding: 12px 20px; // 1
padding: 11px 20px; // 2
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; // 1
padding: 11px 0; // 2
text-align: center;
}
.@{menu-prefix-cls}-submenu-popup {
// padding: 11px 0; // 1
padding: 12px 0; // 2
}
}
&-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;
}
&-vertical &-submenu-title-icon {
position: absolute;
top: 50%;
// right: 18px; // 1
right: 10px; // 2
transform: translateY(-50%) rotate(90deg);
opacity: 0.7;
}
&-submenu-title-icon {
transition: transform @transition-time @ease-in-out;
}
&-vertical &-opened > * > &-submenu-title-icon {
transform: translateY(-50%) rotate(0deg);
}
&-vertical &-submenu {
&-nested {
padding-left: 20px;
}
.@{menu-prefix-cls}-item {
padding-left: 43px;
}
}
&-light&-vertical {
padding: 5px 8px 5px 7px; // 2
}
&-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: #555;
// }
}
&-light&-vertical&-collapse {
padding: 5px; // 2
> li.@{menu-prefix-cls}-item-active,
.@{menu-prefix-cls}-submenu-active {
position: relative;
background-color: fade(@primary-color, 5);
&::after {
display: none;
}
// &::before { // 1
// position: absolute;
// top: 0;
// left: 0;
// width: 3px;
// height: 100%;
// background-color: @primary-color;
// content: '';
// }
}
}
&-dark&-vertical {
padding: 5px 8px 5px 7px; // 2
}
&-dark&-vertical &-item,
&-dark&-vertical &-submenu-title {
color: @menu-dark-subsidiary-color;
border-radius: 10px; // 2
&-active:not(.@{menu-prefix-cls}-submenu) {
color: #fff !important;
// background-color: @primary-color !important; // 1
background-color: fade(@primary-color, 85) !important; // 2
}
&:hover {
color: #fff;
}
}
&-dark&-vertical&-collapse {
padding: 5px; // 2
> li.@{menu-prefix-cls}-item-active,
.@{menu-prefix-cls}-submenu-active {
position: relative;
color: #fff !important;
// background-color: @sider-dark-darken-bg-color !important; // 1
background-color: fade(@primary-color, 50) !important; // 2
border-radius: 10px; // 2
// &::before { // 1
// 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: #eee;
}
&-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 '@jeesite/core/utils/mitt';
import { createContext, useContext } from '@jeesite/core/hooks/core/useContext';
export interface SimpleRootMenuContextProps {
rootMenuEmitter: Emitter<any>;
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: ~'jeesite-simple-menu';
@prefix-cls: ~'jeesite-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,66 @@
import type { Menu as MenuType } from '@jeesite/core/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 '@jeesite/core/router/helper/menuHelper';
import { useTimeoutFn } from '@jeesite/core/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 = true; //!mixSider.value;
useTimeoutFn(
() => {
const menuList = toRaw(menus.value);
// console.log('SidebarMenu.menuList', menuList);
if (menuList?.length === 0) {
menuState.activeSubMenuNames = [];
menuState.openNames = [];
return;
}
let keys: string[] = getAllParentPath(menuList, path);
// console.log('SidebarMenu.getAllParentPath', path, keys, menuList);
// if (keys.length === 0) {
// const currentPaths = sessionStorage.getItem('temp-sidebar-paths');
// if (currentPaths) {
// keys = currentPaths.split(',');
// }
// } else {
// sessionStorage.setItem('temp-sidebar-paths', keys.join(','));
// }
if (keys.length === 0) {
return;
}
if (!unref(collapse)) {
if (!unref(accordion)) {
menuState.openNames = uniq([...menuState.openNames, ...keys]);
} else {
menuState.openNames = keys;
}
}
menuState.activeSubMenuNames = menuState.openNames;
// console.log('SidebarMenu.setOpenKeys', path, menuState.openNames);
},
30,
native,
);
}
const getOpenKeys = computed(() => {
return menuState.openNames;
});
return { setOpenKeys: debounceSetOpenKeys, getOpenKeys };
}