提交 ff2b12b4 编写于 作者: V vben

refactor(menu): added component. Solve the menu stuck problem

上级 056fc131
## Wip
### ✨ Refactor
- 新增 `SimpleMenu`组件替代左侧菜单组件(顶部菜单没有替换,功能尽量做到简单不卡)。解决菜单卡顿问题。
### 🐛 Bug Fixes
- 修复 `TableAction`图标问题
......
......@@ -39,7 +39,7 @@ export default [
// mock user login
{
url: '/api/login',
timeout: 1000,
timeout: 200,
method: 'post',
response: ({ body }) => {
const { username, password } = body;
......@@ -62,7 +62,6 @@ export default [
},
{
url: '/api/getUserInfoById',
timeout: 200,
method: 'get',
response: ({ query }) => {
const { userId } = query;
......
......@@ -19,7 +19,7 @@
},
"dependencies": {
"@iconify/iconify": "^2.0.0-rc.6",
"@vueuse/core": "^4.0.5",
"@vueuse/core": "^4.0.8",
"ant-design-vue": "^2.0.0-rc.8",
"apexcharts": "^3.23.1",
"axios": "^0.21.1",
......@@ -45,12 +45,12 @@
"devDependencies": {
"@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^11.0.0",
"@iconify/json": "^1.1.286",
"@iconify/json": "^1.1.287",
"@ls-lint/ls-lint": "^1.9.2",
"@purge-icons/generated": "^0.5.1",
"@types/echarts": "^4.9.3",
"@types/fs-extra": "^9.0.6",
"@types/http-proxy": "^1.17.4",
"@types/http-proxy": "^1.17.5",
"@types/koa-static": "^4.0.1",
"@types/lodash-es": "^4.17.4",
"@types/mockjs": "^1.0.3",
......@@ -63,24 +63,24 @@
"@typescript-eslint/eslint-plugin": "^4.13.0",
"@typescript-eslint/parser": "^4.13.0",
"@vitejs/plugin-legacy": "^1.2.1",
"@vitejs/plugin-vue": "^1.0.5",
"@vitejs/plugin-vue": "^1.0.6",
"@vitejs/plugin-vue-jsx": "^1.0.2",
"@vue/compiler-sfc": "^3.0.5",
"@vuedx/typecheck": "^0.5.0",
"@vuedx/typescript-plugin-vue": "^0.5.0",
"autoprefixer": "^10.2.1",
"commitizen": "^4.2.2",
"commitizen": "^4.2.3",
"conventional-changelog-cli": "^2.1.1",
"conventional-changelog-custom-config": "^0.3.1",
"cross-env": "^7.0.3",
"dotenv": "^8.2.0",
"eslint": "^7.17.0",
"eslint": "^7.18.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.4.1",
"esno": "^0.4.0",
"fs-extra": "^9.0.1",
"husky": "^4.3.7",
"husky": "^4.3.8",
"koa-static": "^5.0.0",
"less": "^4.1.0",
"lint-staged": "^10.5.3",
......@@ -96,11 +96,11 @@
"stylelint-order": "^4.1.0",
"ts-node": "^9.1.0",
"typescript": "^4.1.3",
"vite": "^2.0.0-beta.27",
"vite": "^2.0.0-beta.30",
"vite-plugin-html": "^2.0.0-beta.5",
"vite-plugin-mock": "^2.0.0-beta.3",
"vite-plugin-purge-icons": "^0.5.1",
"vite-plugin-pwa": "^0.3.6",
"vite-plugin-pwa": "^0.3.8",
"vue-eslint-parser": "^7.3.0",
"yargs": "^16.2.0"
},
......
......@@ -2,54 +2,52 @@
<Teleport to="body">
<transition name="zoom-fade" mode="out-in">
<div :class="getClass" @click.stop v-if="visible">
<ClickOutSide @clickOutside="handleClose">
<div :class="`${prefixCls}-content`">
<div :class="`${prefixCls}-input__wrapper`">
<a-input
:class="`${prefixCls}-input`"
:placeholder="t('common.searchText')"
allow-clear
@change="handleSearch"
>
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<span :class="`${prefixCls}-cancel`" @click="handleClose">{{
t('common.cancelText')
}}</span>
</div>
<div :class="`${prefixCls}-content`" v-click-outside="handleClose">
<div :class="`${prefixCls}-input__wrapper`">
<a-input
:class="`${prefixCls}-input`"
:placeholder="t('common.searchText')"
allow-clear
@change="handleSearch"
>
<template #prefix>
<SearchOutlined />
</template>
</a-input>
<span :class="`${prefixCls}-cancel`" @click="handleClose">{{
t('common.cancelText')
}}</span>
</div>
<div :class="`${prefixCls}-not-data`" v-show="getIsNotData">
{{ t('component.app.searchNotData') }}
</div>
<ul :class="`${prefixCls}-list`" v-show="!getIsNotData" ref="scrollWrap">
<li
:ref="setRefs(index)"
v-for="(item, index) in searchResult"
:key="item.path"
:data-index="index"
@mouseenter="handleMouseenter"
@click="handleEnter"
:class="[
`${prefixCls}-list__item`,
{
[`${prefixCls}-list__item--active`]: activeIndex === index,
},
]"
>
<div :class="`${prefixCls}-list__item-icon`">
<g-icon :icon="item.icon || 'mdi:form-select'" :size="20" />
</div>
<div :class="`${prefixCls}-list__item-text`">{{ item.name }}</div>
<div :class="`${prefixCls}-list__item-enter`">
<g-icon icon="ant-design:enter-outlined" :size="20" />
</div>
</li>
</ul>
<AppSearchFooter />
<div :class="`${prefixCls}-not-data`" v-show="getIsNotData">
{{ t('component.app.searchNotData') }}
</div>
</ClickOutSide>
<ul :class="`${prefixCls}-list`" v-show="!getIsNotData" ref="scrollWrap">
<li
:ref="setRefs(index)"
v-for="(item, index) in searchResult"
:key="item.path"
:data-index="index"
@mouseenter="handleMouseenter"
@click="handleEnter"
:class="[
`${prefixCls}-list__item`,
{
[`${prefixCls}-list__item--active`]: activeIndex === index,
},
]"
>
<div :class="`${prefixCls}-list__item-icon`">
<g-icon :icon="item.icon || 'mdi:form-select'" :size="20" />
</div>
<div :class="`${prefixCls}-list__item-text`">{{ item.name }}</div>
<div :class="`${prefixCls}-list__item-enter`">
<g-icon icon="ant-design:enter-outlined" :size="20" />
</div>
</li>
</ul>
<AppSearchFooter />
</div>
</div>
</transition>
</Teleport>
......@@ -63,17 +61,20 @@
import { SearchOutlined } from '@ant-design/icons-vue';
import AppSearchFooter from './AppSearchFooter.vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { ClickOutSide } from '/@/components/ClickOutSide';
import { useAppInject } from '/@/hooks/web/useAppInject';
import clickOutside from '/@/directives/clickOutside';
export default defineComponent({
name: 'AppSearchModal',
components: { SearchOutlined, ClickOutSide, AppSearchFooter },
components: { SearchOutlined, AppSearchFooter },
emits: ['close'],
props: {
visible: Boolean,
},
directives: {
clickOutside,
},
setup(_, { emit }) {
const scrollWrap = ref<ElRef>(null);
const { prefixCls } = useDesign('app-search-modal');
......
export { default as Menu } from './src/index.vue';
<template>
<ul :class="getClass" :style="getStyle">
<slot></slot>
</ul>
</template>
<script lang="ts">
import { defineComponent, ref, computed, CSSProperties, unref } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { propTypes } from '/@/utils/propTypes';
export default defineComponent({
props: {
mode: propTypes.oneOf(['horizontal', 'vertical']).def('vertical'),
theme: propTypes.oneOf(['light', 'dark', 'primary']).def('light'),
activeName: propTypes.oneOfType([propTypes.string, propTypes.number]),
openNames: propTypes.array.def([]),
accordion: propTypes.bool,
width: propTypes.string.def('210px'),
},
setup(props) {
const currentActiveName = ref(props.activeName);
const openedNames = ref<string[]>();
const { prefixCls } = useDesign('menu');
const getClass = computed(() => {
const { theme, mode } = props;
let curTheme = theme;
if (mode === 'vertical' && theme === 'primary') {
curTheme = 'light';
}
return [
prefixCls,
`${prefixCls}-${curTheme}`,
{
[`${prefixCls}-${mode}`]: mode,
},
];
});
const getStyle = computed(
(): CSSProperties => {
const { mode, width } = props;
if (mode === 'vertical') {
return {
width: width,
};
}
return {};
}
);
function updateActiveName() {
if (unref(currentActiveName) === undefined) {
currentActiveName.value = -1;
}
}
function updateOpened() {}
return { getClass, getStyle };
},
});
</script>
export { default as SimpleMenu } from './src/SimpleMenu.vue';
<template>
<Menu
v-bind="getBindValues"
@select="handleSelect"
:activeName="activeName"
:openNames="openNames"
:class="prefixCls"
:activeSubMenuNames="activeSubMenuNames"
>
<template v-for="item in items" :key="item.path">
<SimpleSubMenu
:item="item"
:parent="true"
:collapsedShowTitle="collapsedShowTitle"
:collapse="collapse"
/>
</template>
</Menu>
</template>
<script lang="ts">
import type { PropType } from 'vue';
import type { MenuState } from './types';
import type { Menu as MenuType } from '/@/router/types';
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 { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
import { propTypes } from '/@/utils/propTypes';
import { REDIRECT_NAME } from '/@/router/constant';
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router';
import { isFunction } from '/@/utils/is';
import { useOpenKeys } from './useOpenKeys';
export default defineComponent({
name: 'SimpleMenu',
inheritAttrs: false,
components: {
Menu,
SimpleSubMenu,
},
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>>,
},
},
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 } = toRefs(props);
const { setOpenKeys } = useOpenKeys(menuState, items, accordion, mixSider);
const getBindValues = computed(() => ({ ...attrs, ...props }));
watch(
() => props.collapse,
(collapse) => {
if (collapse) {
menuState.openNames = [];
} else {
setOpenKeys(currentRoute.value.path);
}
},
{ immediate: true }
);
listenerLastChangeTab((route) => {
if (route.name === REDIRECT_NAME) return;
currentActiveMenu.value = route.meta?.currentActiveMenu;
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);
// if (unref(currentActiveMenu)) return;
}
async function handleSelect(key: string) {
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;
}
return {
prefixCls,
getBindValues,
handleSelect,
...toRefs(menuState),
};
},
});
</script>
<style lang="less">
@import './index.less';
</style>
<template>
<span :class="getTagClass" v-if="getShowTag">{{ getContent }}</span>
</template>
<script lang="ts">
import type { Menu } from '/@/router/types';
import type { PropType } from 'vue';
import { defineComponent, computed } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'SimpleMenuTag',
props: {
item: {
type: Object as PropType<Menu>,
default: {},
},
collapseParent: {
type: Boolean as PropType<boolean>,
default: false,
},
},
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,
},
];
});
return {
getTagClass,
getShowTag,
getContent,
};
},
});
</script>
<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']">
{{ getI18nName }}
</span>
<SimpleMenuTag :item="item" :collapseParent="getIsCollapseParent" />
</template>
</MenuItem>
<SubMenu
: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" :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 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';
const { t } = useI18n();
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']),
},
setup(props) {
const { prefixCls } = useDesign('simple-menu');
const getShowMenu = computed(() => {
return !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: Menu): boolean {
return (
Reflect.has(menuTreeItem, 'children') &&
!!menuTreeItem.children &&
menuTreeItem.children.length > 0
);
}
return {
prefixCls,
menuHasChildren,
getShowMenu,
getIcon,
getI18nName,
getShowSubTitle,
getLevelClass,
getIsCollapseParent,
};
},
});
</script>
<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';
import { isString } from '/@/utils/is';
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 = new 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,
});
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);
});
});
return { getClass, openedNames };
},
});
</script>
<style lang="less">
@import './menu.less';
</style>
<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: any) {
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: any) {
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: any) {
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
},
beforeLeave(el: any) {
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: any) {
if (el.scrollHeight !== 0) {
addClass(el, 'collapse-transition');
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
},
afterLeave(el: any) {
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>
<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 />
</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';
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 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;
}
});
rootMenuEmitter.emit('on-update-active-name:submenu', uidList);
} else {
active.value = false;
}
},
{ immediate: true }
);
return { getClass, prefixCls, getItemStyle, getCollapse, handleClickItem, showTooptip };
},
});
</script>
<template>
<li :class="getClass">
<template v-if="!getCollapse">
<div :class="`${prefixCls}-submenu-title`" @click.stop="handleClick" :style="getItemStyle">
<slot name="title"></slot>
<Icon
icon="eva:arrow-ios-downward-outline"
:size="14"
:class="`${prefixCls}-submenu-title-icon`"
/>
</div>
<MenuCollapseTransition>
<ul :class="prefixCls" v-show="opened">
<slot></slot>
</ul>
</MenuCollapseTransition>
</template>
<Popover
placement="right"
:overlayClassName="`${prefixCls}-menu-popover`"
v-else
:visible="getIsOpend"
@visibleChange="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>
<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 '/@/hooks/web/useDesign';
import { propTypes } from '/@/utils/propTypes';
import { useMenuItem } from './useMenu';
import { useSimpleRootMenuContext } from './useSimpleMenuContext';
import MenuCollapseTransition from './MenuCollapseTransition.vue';
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,
MenuCollapseTransition,
Popover,
},
props: {
name: {
type: [String, Number] as PropType<string | number>,
required: true,
},
disabled: propTypes.bool,
collapsedShowTitle: propTypes.bool,
},
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 = new 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);
}
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,
});
}
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)) {
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[]) => {
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>
@menu-prefix-cls: ~'@{namespace}-menu';
@menu-popup-prefix-cls: ~'@{namespace}-menu-popup';
@submenu-popup-prefix-cls: ~'@{namespace}-menu-submenu-popup';
// @menu-dark: #191a23;
// @menu-dark-active-bg: #101117;
@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: @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: 12px 20px;
color: @menu-dark-subsidiary-color;
cursor: pointer;
transition: all @transition-time @ease-in-out;
// &:hover {
// color: @primary-color;
// }
&-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: @primary-color !important;
}
}
}
&-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: fade(@primary-color, 8);
.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: #fff;
.@{menu-prefix-cls}-submenu-active {
color: @primary-color !important;
// background: fade(@primary-color, 8);
&-border {
.light-border();
}
}
}
&-dark {
// background: @menu-dark;
.@{menu-prefix-cls}-submenu-active {
color: #fff !important;
}
}
&-item {
position: relative;
z-index: 1;
display: flex;
font-size: @font-size-base;
color: inherit;
list-style: none;
cursor: pointer;
outline: none;
align-items: center;
// transition: all @transition-time @ease-in-out;
&: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: 12px 24px;
cursor: pointer;
// transition: all @transition-time @ease-in-out;
&: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: fade(@primary-color, 8);
.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: fade(@primary-color, 3);
&::after {
display: none;
}
&::before {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background: @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: @primary-color !important;
}
&:hover {
color: #fff;
// background: @menu-dark;
}
// &-active:not(.@{menu-prefix-cls}-submenu) {
// color: @primary-color;
// }
}
&-dark&-vertical&-collapse {
> li.@{menu-prefix-cls}-item-active,
.@{menu-prefix-cls}-submenu-active {
position: relative;
color: #fff !important;
background-color: @sider-dark-darken-bg-color !important;
&::before {
position: absolute;
top: 0;
left: 0;
width: 3px;
height: 100%;
background: @primary-color;
content: '';
}
.@{menu-prefix-cls}-submenu-collapse {
background-color: transparent;
}
}
}
&-dark&-vertical &-submenu &-item {
// &:hover {
// color: #fff;
// background: transparent;
// }
&-active,
&-active:hover {
color: #fff;
border-right: none;
}
}
&-dark&-vertical &-child-item-active > &-submenu-title {
color: #fff;
}
&-dark&-vertical &-opened {
// background: @menu-dark-active-bg;
// .@{menu-prefix-cls}-submenu-title {
// background: @menu-dark;
// }
.@{menu-prefix-cls}-submenu-has-parent-submenu {
.@{menu-prefix-cls}-submenu-title {
background: transparent;
}
}
}
}
}
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;
}
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 = [];
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,
};
}
import type { InjectionKey, Ref } from 'vue';
import { createContext, useContext } from '/@/hooks/core/useContext';
import Mitt from '/@/utils/mitt';
export interface SimpleRootMenuContextProps {
rootMenuEmitter: Mitt;
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);
}
@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-1-bg-color;
> .@{prefix-cls}-submenu-title {
background-color: @sider-dark-lighten-1-bg-color;
}
}
.collapse-title {
font-size: 12px;
}
}
.@{simple-prefix-cls} {
&-tag {
position: absolute;
top: calc(50% - 10px);
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% - 4px);
width: 6px;
height: 6px;
padding: 0;
border-radius: 50%;
}
&--primary {
background: @primary-color;
}
&--error {
background: @error-color;
}
&--success {
background: @success-color;
}
&--warn {
background: @warning-color;
}
}
}
export interface MenuState {
activeName: string;
openNames: string[];
activeSubMenuNames: string[];
}
import type { Menu as MenuType } from '/@/router/types';
import type { MenuState } from './types';
import { Ref, toRaw } from 'vue';
import { unref } from 'vue';
import { es6Unique } from '/@/utils';
import { getAllParentPath } from '/@/router/helper/menuHelper';
import { useTimeoutFn } from '/@/hooks/core/useTimeout';
export function useOpenKeys(
menuState: MenuState,
menus: Ref<MenuType[]>,
accordion: Ref<boolean>,
mixSider: Ref<boolean>
// mode: Ref<MenuModeEnum>,
) {
async function setOpenKeys(path: string) {
// if (mode.value === MenuModeEnum.HORIZONTAL) {
// return;
// }
const native = !mixSider.value;
useTimeoutFn(
() => {
const menuList = toRaw(menus.value);
if (menuList?.length === 0) {
menuState.activeSubMenuNames = [];
menuState.openNames = [];
return;
}
const keys = getAllParentPath(menuList, path);
if (!unref(accordion)) {
menuState.openNames = es6Unique([...menuState.openNames, ...keys]);
} else {
menuState.openNames = keys;
}
menuState.activeSubMenuNames = menuState.openNames;
},
16,
native
);
}
return { setOpenKeys };
}
......@@ -31,7 +31,7 @@
import type { CSSProperties, PropType } from 'vue';
import type { BasicColumn } from '../../types/table';
import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
import { defineComponent, ref, unref, nextTick, computed, watchEffect } from 'vue';
import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue';
import { useDesign } from '/@/hooks/web/useDesign';
......
import type { Ref } from 'vue';
import type { TableActionType } from '../types/table';
import { provide, inject } from 'vue';
const key = Symbol('table');
type Instance = TableActionType & { wrapRef: Ref<Nullable<HTMLElement>> };
export function provideTable(instance: Instance) {
provide(key, instance);
}
export function injectTable(): Instance {
return inject(key) as Instance;
}
......@@ -22,6 +22,12 @@
background: rgba(0, 0, 0, 0.3);
}
.ant-popover {
&-content {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
}
// =================================
// ==============descriptions=======
// =================================
......
......@@ -9,7 +9,6 @@
--sider-dark-darken-bg-color: #273352;
--sider-dark-lighten-1-bg-color: #273352;
--sider-dark-lighten-2-bg-color: #273352;
--sider-dark-lighten-3-bg-color: #273352;
}
@white: #fff;
......@@ -88,7 +87,6 @@
@sider-dark-darken-bg-color: var(--sider-dark-darken-bg-color);
@sider-dark-lighten-1-bg-color: var(--sider-dark-lighten-1-bg-color);
@sider-dark-lighten-2-bg-color: var(--sider-dark-lighten-2-bg-color);
@sider-dark-lighten-3-bg-color: var(--sider-dark-lighten-3-bg-color);
// trigger
@trigger-dark-hover-bg-color: rgba(255, 255, 255, 0.2);
......
......@@ -78,9 +78,12 @@ const getIsMixMode = computed(() => {
});
const getRealWidth = computed(() => {
return unref(getCollapsed) && !unref(getMixSideFixed)
? unref(getMiniWidthNumber)
: unref(getMenuWidth);
if (unref(getIsMixSidebar)) {
return unref(getCollapsed) && !unref(getMixSideFixed)
? unref(getMiniWidthNumber)
: unref(getMenuWidth);
}
return unref(getCollapsed) ? unref(getMiniWidthNumber) : unref(getMenuWidth);
});
const getMiniWidthNumber = computed(() => {
......
......@@ -142,7 +142,7 @@
});
const getLogoWidth = computed(() => {
if (!unref(getIsMixMode)) {
if (!unref(getIsMixMode) || unref(getIsMobile)) {
return {};
}
const width = unref(getMenuWidth) < 180 ? 180 : unref(getMenuWidth);
......
......@@ -4,6 +4,7 @@ import type { PropType, CSSProperties } from 'vue';
import { computed, defineComponent, unref, toRef } from 'vue';
import { BasicMenu } from '/@/components/Menu';
import { SimpleMenu } from '/@/components/SimpleMenu';
import { AppLogo } from '/@/components/Application';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
......@@ -126,7 +127,18 @@ export default defineComponent({
}
function renderMenu() {
return (
const menus = unref(menusRef);
if (!menus || !menus.length) return null;
return !props.isHorizontal ? (
<SimpleMenu
items={menus}
theme={unref(getComputedMenuTheme)}
accordion={unref(getAccordion)}
collapse={unref(getCollapsed)}
collapsedShowTitle={unref(getCollapsedShowTitle)}
onMenuClick={handleMenuClick}
/>
) : (
<BasicMenu
beforeClickFn={beforeMenuClickFn}
isHorizontal={props.isHorizontal}
......@@ -135,7 +147,7 @@ export default defineComponent({
showLogo={unref(getIsShowLogo)}
mode={unref(getComputedMenuMode)}
theme={unref(getComputedMenuTheme)}
items={unref(menusRef)}
items={menus}
accordion={unref(getAccordion)}
onMenuClick={handleMenuClick}
/>
......
......@@ -40,7 +40,12 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
async ([path]: [string, MenuSplitTyeEnum]) => {
if (unref(splitNotLeft) || unref(getIsMobile)) return;
const parentPath = await getCurrentParentPath(path);
const { meta } = unref(currentRoute);
const currentActiveMenu = meta.currentActiveMenu;
let parentPath = await getCurrentParentPath(path);
if (!parentPath) {
parentPath = await getCurrentParentPath(currentActiveMenu);
}
parentPath && throttleHandleSplitLeftMenu(parentPath);
},
{
......@@ -67,11 +72,15 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
// Handle left menu split
async function handleSplitLeftMenu(parentPath: string) {
console.log('======================');
console.log(unref(getSplitLeft));
console.log('======================');
if (unref(getSplitLeft) || unref(getIsMobile)) return;
// spilt mode left
const children = await getChildrenMenus(parentPath);
if (!children) {
if (!children || !children.length) {
setMenuSetting({ hidden: true });
menusRef.value = [];
return;
......
......@@ -61,9 +61,7 @@
/>
</div>
<ScrollContainer :class="`${prefixCls}-menu-list__content`">
<BasicMenu
:isHorizontal="false"
mode="inline"
<SimpleMenu
:items="chilrenMenus"
:theme="getMenuTheme"
mixSider
......@@ -85,7 +83,7 @@
import { defineComponent, onMounted, ref, computed, unref } from 'vue';
import { BasicMenu, MenuTag } from '/@/components/Menu';
import { MenuTag } from '/@/components/Menu';
import { ScrollContainer } from '/@/components/Container';
import Icon from '/@/components/Icon';
import { AppLogo } from '/@/components/Application';
......@@ -103,13 +101,14 @@
import clickOutside from '/@/directives/clickOutside';
import { getShallowMenus, getChildrenMenus, getCurrentParentPath } from '/@/router/menus';
import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
import { SimpleMenu } from '/@/components/SimpleMenu';
export default defineComponent({
name: 'LayoutMixSider',
components: {
ScrollContainer,
AppLogo,
BasicMenu,
SimpleMenu,
MenuTag,
Icon,
Trigger,
......@@ -335,6 +334,7 @@
<style lang="less">
@prefix-cls: ~'@{namespace}-layout-mix-sider';
@tag-prefix-cls: ~'@{namespace}-basic-menu-item-tag';
@menu-prefix-cls: ~'@{namespace}-menu';
@width: 80px;
.@{prefix-cls} {
position: fixed;
......@@ -351,6 +351,10 @@
right: 2px;
}
.@{menu-prefix-cls} {
width: 100% !important;
}
&-dom {
height: 100%;
overflow: hidden;
......@@ -392,6 +396,10 @@
}
}
.@{prefix-cls}-menu-list {
&__content {
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1);
}
&__title {
.pushpin {
color: rgba(0, 0, 0, 0.35);
......@@ -578,10 +586,10 @@
&-drag-bar {
position: absolute;
top: 0;
right: -3px;
width: 3px;
height: 100%;
top: 50px;
right: -1px;
width: 1px;
height: calc(100% - 50px);
cursor: ew-resize;
background: #f8f8f9;
border-top: none;
......
export default {
loadingText: 'Loading...',
cancelText: 'Close',
okText: 'Confirm',
};
export default {
search: 'Menu search',
};
export default {
cancelText: 'Close',
okText: 'Confirm',
};
export default {
loadingText: '加载中...',
cancelText: '关闭',
okText: '确认',
};
export default {
search: '菜单搜索',
};
export default {
cancelText: '关闭',
okText: '确认',
};
......@@ -71,7 +71,7 @@ export function updateSidebarBgColor(color: string) {
setCssVar(SIDER_DARK_BG_COLOR, color);
setCssVar(SIDER_DARK_DARKEN_BG_COLOR, darken(color, 6));
setCssVar(SIDER_LIGHTEN_1_BG_COLOR, lighten(color, 4));
setCssVar(SIDER_LIGHTEN_1_BG_COLOR, lighten(color, 5));
setCssVar(SIDER_LIGHTEN_2_BG_COLOR, lighten(color, 8));
// only #ffffff is light
......
......@@ -8,6 +8,7 @@ import { createMessageGuard } from './messageGuard';
import { createScrollGuard } from './scrollGuard';
import { createHttpGuard } from './httpGuard';
import { createPageGuard } from './pageGuard';
import { createStateGuard } from './stateGuard';
export function createGuard(router: Router) {
createPageGuard(router);
......@@ -18,4 +19,5 @@ export function createGuard(router: Router) {
createTitleGuard(router);
createProgressGuard(router);
createPermissionGuard(router);
createStateGuard(router);
}
......@@ -3,7 +3,7 @@ import { appStore } from '/@/store/modules/app';
import { PageEnum } from '/@/enums/pageEnum';
import { removeTabChangeListener } from '/@/logics/mitt/tabChange';
export function createHttpGuard(router: Router) {
export function createStateGuard(router: Router) {
router.afterEach((to) => {
// Just enter the login page and clear the authentication information
if (to.path === PageEnum.BASE_LOGIN) {
......
......@@ -54,7 +54,9 @@ export const getMenus = async (): Promise<Menu[]> => {
// 获取当前路径的顶级路径
export async function getCurrentParentPath(currentPath: string) {
const menus = await getAsyncMenus();
const allParentPath = await getAllParentPath(menus, currentPath);
return allParentPath?.[0];
}
......
......@@ -28,7 +28,7 @@ export default class Mitt {
* @param {Function} handler Function to call in response to given event
*/
on(type: string | Symbol, handler: Fn) {
const handlers = this.cache.get(type);
const handlers = this.cache?.get(type);
const added = handlers && handlers.push(handler);
if (!added) {
this.cache.set(type, [handler]);
......@@ -57,7 +57,7 @@ export default class Mitt {
* @param {string|symbol} type The event type to invoke
* @param {*} [evt] Any value (object is recommended and powerful), passed to each handler
*/
emit(type: string | Symbol, evt: any) {
emit(type: string | Symbol, evt?: any) {
for (const handler of (this.cache.get(type) || []).slice()) handler(evt);
for (const handler of (this.cache.get('*') || []).slice()) handler(type, evt);
}
......
......@@ -89,6 +89,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
optimizeDeps: {
include: [
'@ant-design/icons-vue',
'echarts/map/js/china',
'ant-design-vue/es/locale/zh_CN',
'moment/dist/locale/zh-cn',
'ant-design-vue/es/locale/en_US',
......
......@@ -1123,10 +1123,10 @@
resolved "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.2.1.tgz#29a5a86bcfaa41555c8483a287294e520cc28cd6"
integrity sha512-WmvsSfVKQx62vLbHXJvdh4PDjSK9YU6VW9ppXTlbjgDKCYtpy2sMWbK4i9OBdxY6RRwMMVctZhWo6Y5jfMRyTg==
"@eslint/eslintrc@^0.2.2":
version "0.2.2"
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.2.2.tgz#d01fc791e2fc33e88a29d6f3dc7e93d0cd784b76"
integrity sha512-EfB5OHNYp1F4px/LI/FEnGylop7nOqkQ1LRzCM0KccA2U8tvV8w01KBv37LbO7nW4H+YhKyo2LcJhRwjjV17QQ==
"@eslint/eslintrc@^0.3.0":
version "0.3.0"
resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.3.0.tgz#d736d6963d7003b6514e6324bec9c602ac340318"
integrity sha512-1JTKgrOKAHVivSvOYw+sJOunkBjUOvjqWk1DPja7ZFhIS2mX/4EgTT8M7eTK9jrKhL/FvXXEbQwIs3pg1xp3dg==
dependencies:
ajv "^6.12.4"
debug "^4.1.1"
......@@ -1135,7 +1135,7 @@
ignore "^4.0.6"
import-fresh "^3.2.1"
js-yaml "^3.13.1"
lodash "^4.17.19"
lodash "^4.17.20"
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
......@@ -1184,10 +1184,10 @@
dependencies:
cross-fetch "^3.0.6"
"@iconify/json@^1.1.285":
version "1.1.285"
resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.285.tgz#2f1665c9f3ce4cd9eb2e0c980c0ac8955ce520fc"
integrity sha512-ABoWg/GibeN3hzTvvzd9oSmSo3V8Hyb3f0LMMUD195xlrd8083nBzFFhA12EfEMnxNsouj6ZtvlgIDnYWEXRow==
"@iconify/json@^1.1.287":
version "1.1.287"
resolved "https://registry.npmjs.org/@iconify/json/-/json-1.1.287.tgz#31fe253ce97fb2bf673a60c2467810a3f48a00c3"
integrity sha512-wvmQDpHqzbYZv2mDsdp1eXUN+ff53FjElT19uVxFRPOkY2kaIhs7dMPS/ZeDD38TE2eH1arTzZ2KhtB+Mxe8VQ==
"@intlify/core-base@9.0.0-beta.16":
version "9.0.0-beta.16"
......@@ -1494,10 +1494,10 @@
resolved "https://registry.npmjs.org/@types/http-errors/-/http-errors-1.8.0.tgz#682477dbbbd07cd032731cb3b0e7eaee3d026b69"
integrity sha512-2aoSC4UUbHDj2uCsCxcG/vRMXey/m17bC7UwitVm5hn22nI8O8Y9iDpA76Orc+DWkQ4zZrOKEshCqR/jSuXAHA==
"@types/http-proxy@^1.17.4":
version "1.17.4"
resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz#e7c92e3dbe3e13aa799440ff42e6d3a17a9d045b"
integrity sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==
"@types/http-proxy@^1.17.5":
version "1.17.5"
resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.5.tgz#c203c5e6e9dc6820d27a40eb1e511c70a220423d"
integrity sha512-GNkDE7bTv6Sf8JbV2GksknKOsk7OznNYHSdrtvPJXO0qJ9odZig6IZKUi5RFGi6d1bf6dgIAe4uXi3DBc7069Q==
dependencies:
"@types/node" "*"
......@@ -1751,10 +1751,10 @@
"@typescript-eslint/types" "4.13.0"
eslint-visitor-keys "^2.0.0"
"@vitejs/plugin-legacy@^1.2.0":
version "1.2.0"
resolved "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.2.0.tgz#e6a2f7802f1a81c712f72656300fcdf7541eeab0"
integrity sha512-eoJi1M7Or16bkRjXFtdG39c8ElvbgxUxlXFo8GO2VmgOGO42r6Ku5MJD4ZkweIM7XGunyFvmEwTYgpUVC4PiPg==
"@vitejs/plugin-legacy@^1.2.1":
version "1.2.1"
resolved "https://registry.npmjs.org/@vitejs/plugin-legacy/-/plugin-legacy-1.2.1.tgz#783a72c56ce987f00caf334acc33195a0bbf6f24"
integrity sha512-bVOYH7WxffDSvfFfCGk/UYCzKw59n18fHGOV3VXRSQmeaBmbxuq0CRdAS3EtPvp74DjgA4GiZ+BsrQ0LyF0/yA==
dependencies:
"@babel/standalone" "^7.12.12"
core-js "^3.8.2"
......@@ -1772,10 +1772,10 @@
"@vue/babel-plugin-jsx" "^1.0.1"
hash-sum "^2.0.0"
"@vitejs/plugin-vue@^1.0.5":
version "1.0.5"
resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.0.5.tgz#2639178e975bebc505e9be1c88d25faf9bc4dd06"
integrity sha512-Fq/Z1rTs7j3QhvmIjeIHqInw2YneXa8Td3z7cYQhyAZXF/WmGMegbapeBqGAoAcGSOfWpOO7Tr0c/T+Qke0O6Q==
"@vitejs/plugin-vue@^1.0.6":
version "1.0.6"
resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-1.0.6.tgz#698afa5a77a6dcd22cf7757801f46a6f01cdbb53"
integrity sha512-cWJewtxnVVpjlhq6DoZ7VP7sF1jTZYVg66ehslZ0tJANWk1uRiCXdqD8yQ4npZ4XewDICQzK+c+9i3Xsubx59w==
"@vue/babel-helper-vue-transform-on@^1.0.0":
version "1.0.0"
......@@ -2023,18 +2023,18 @@
vscode-languageserver-textdocument "^1.0.1"
vscode-uri "^2.1.2"
"@vueuse/core@^4.0.5":
version "4.0.5"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.5.tgz#97bd5f24a28401598436629312eafe66ed0e1bed"
integrity sha512-Kfy5ys9o1XIY6NwX9O7iad4/FbHrcDuP/LtsgIFvl7XDQtbYArHu5ZSOQyBwqE32TdAqnFi5sYd4vjSvVvpD4A==
"@vueuse/core@^4.0.8":
version "4.0.8"
resolved "https://registry.npmjs.org/@vueuse/core/-/core-4.0.8.tgz#d5690154c147ae787bf5d67bf8fe3046dff96d85"
integrity sha512-wD0JJUXpRgRBPCnGsAqcVk9Zz545zOmIjGv/1Mlco3rVmal7LEZ3rJh8SnBelxuyVNvRwifkK1gtbT24jY6V8Q==
dependencies:
"@vueuse/shared" "4.0.5"
"@vueuse/shared" "4.0.8"
vue-demi latest
"@vueuse/shared@4.0.5":
version "4.0.5"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.5.tgz#0610210da9a01843cdb3fa88c177b29b62738efc"
integrity sha512-PUSlwoSaerwHA1PPjBGnerXPIvAcVGoxcpjNdbHW44lPqoWskWl2CxG+l2Iz+Zf2iapCatp3ovXnMd16RRvQ1Q==
"@vueuse/shared@4.0.8":
version "4.0.8"
resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-4.0.8.tgz#ba6c350b5f0ef12e2a603d956cc6d2809ff5be4f"
integrity sha512-euAfdZeFHGAyCBoy7izgufC/kTt+yEjuVjeCmfuDQNAj7QsdzEpRlyblD+EGifHbyGFx8F3Ql6/bQzdTdwRFHA==
dependencies:
vue-demi latest
......@@ -2800,7 +2800,7 @@ commander@~2.17.1:
resolved "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
commitizen@^4.0.3, commitizen@^4.2.2:
commitizen@^4.0.3:
version "4.2.2"
resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.2.2.tgz#1a93dd07208521ea1ebbf832593542dac714cc79"
integrity sha512-uz+E6lGsDBDI2mYA4QfOxFeqdWUYwR1ky11YmLgg2BnEEP3YbeejpT4lxzGjkYqumnXr062qTOGavR9NtX/iwQ==
......@@ -2820,6 +2820,26 @@ commitizen@^4.0.3, commitizen@^4.2.2:
strip-bom "4.0.0"
strip-json-comments "3.0.1"
commitizen@^4.2.3:
version "4.2.3"
resolved "https://registry.npmjs.org/commitizen/-/commitizen-4.2.3.tgz#088d0ef72500240d331b11e02e288223667c1475"
integrity sha512-pYlYEng7XMV2TW4xtjDKBGqeJ0Teq2zyRSx2S3Ml1XAplHSlJZK8vm1KdGclpMEZuGafbS5TeHXIVnHk8RWIzQ==
dependencies:
cachedir "2.2.0"
cz-conventional-changelog "3.2.0"
dedent "0.7.0"
detect-indent "6.0.0"
find-node-modules "2.0.0"
find-root "1.1.0"
fs-extra "8.1.0"
glob "7.1.4"
inquirer "6.5.2"
is-utf8 "^0.2.1"
lodash "^4.17.20"
minimist "1.2.5"
strip-bom "4.0.0"
strip-json-comments "3.0.1"
common-tags@^1.8.0:
version "1.8.0"
resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.0.tgz#8e3153e542d4a39e9b10554434afaaf98956a937"
......@@ -3159,6 +3179,20 @@ currently-unhandled@^0.4.1:
dependencies:
array-find-index "^1.0.1"
cz-conventional-changelog@3.2.0:
version "3.2.0"
resolved "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.2.0.tgz#6aef1f892d64113343d7e455529089ac9f20e477"
integrity sha512-yAYxeGpVi27hqIilG1nh4A9Bnx4J3Ov+eXy4koL3drrR+IO9GaWPsKjik20ht608Asqi8TQPf0mczhEeyAtMzg==
dependencies:
chalk "^2.4.1"
commitizen "^4.0.3"
conventional-commit-types "^3.0.0"
lodash.map "^4.5.1"
longest "^2.0.1"
word-wrap "^1.0.3"
optionalDependencies:
"@commitlint/load" ">6.1.1"
cz-conventional-changelog@3.3.0:
version "3.3.0"
resolved "https://registry.npmjs.org/cz-conventional-changelog/-/cz-conventional-changelog-3.3.0.tgz#9246947c90404149b3fe2cf7ee91acad3b7d22d2"
......@@ -3571,13 +3605,13 @@ eslint-visitor-keys@^2.0.0:
resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz#21fdc8fbcd9c795cc0321f0563702095751511a8"
integrity sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==
eslint@^7.17.0:
version "7.17.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-7.17.0.tgz#4ccda5bf12572ad3bf760e6f195886f50569adb0"
integrity sha512-zJk08MiBgwuGoxes5sSQhOtibZ75pz0J35XTRlZOk9xMffhpA9BTbQZxoXZzOl5zMbleShbGwtw+1kGferfFwQ==
eslint@^7.18.0:
version "7.18.0"
resolved "https://registry.npmjs.org/eslint/-/eslint-7.18.0.tgz#7fdcd2f3715a41fe6295a16234bd69aed2c75e67"
integrity sha512-fbgTiE8BfUJZuBeq2Yi7J3RB3WGUQ9PNuNbmgi6jt9Iv8qrkxfy19Ds3OpL1Pm7zg3BtTVhvcUZbIRQ0wmSjAQ==
dependencies:
"@babel/code-frame" "^7.0.0"
"@eslint/eslintrc" "^0.2.2"
"@eslint/eslintrc" "^0.3.0"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
......@@ -3601,7 +3635,7 @@ eslint@^7.17.0:
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash "^4.17.19"
lodash "^4.17.20"
minimatch "^3.0.4"
natural-compare "^1.4.0"
optionator "^0.9.1"
......@@ -4423,10 +4457,10 @@ human-signals@^1.1.1:
resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==
husky@^4.3.7:
version "4.3.7"
resolved "https://registry.npmjs.org/husky/-/husky-4.3.7.tgz#ca47bbe6213c1aa8b16bbd504530d9600de91e88"
integrity sha512-0fQlcCDq/xypoyYSJvEuzbDPHFf8ZF9IXKJxlrnvxABTSzK1VPT2RKYQKrcgJ+YD39swgoB6sbzywUqFxUiqjw==
husky@^4.3.8:
version "4.3.8"
resolved "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d"
integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow==
dependencies:
chalk "^4.0.0"
ci-info "^2.0.0"
......@@ -7840,20 +7874,20 @@ vite-plugin-purge-icons@^0.5.1:
"@purge-icons/generated" "^0.5.1"
rollup-plugin-purge-icons "^0.5.1"
vite-plugin-pwa@^0.3.6:
version "0.3.6"
resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.3.6.tgz#a522af3fd5461991907f6829975b437f2c847339"
integrity sha512-GDgT8jFGHUz2j11I7Z0W+X5mnkaUoMVitJ/UjN/ezjy9HcXrvxaIVnhzMdESJSv+dxy4DD9ymD91cF9Ei6//cQ==
vite-plugin-pwa@^0.3.8:
version "0.3.8"
resolved "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.3.8.tgz#c98a683ddbbe87bd55db79acb6a5c849f2a29879"
integrity sha512-W5FBJeS3KjaCG1qu7LMTX9+E0u6qNHFk+hk917s4MnAlQ/XnBs30kgRXVBXtVAPhgvn8rqj2ww+2OYed+MKtIg==
dependencies:
debug "^4.3.2"
fast-glob "^3.2.4"
pretty-bytes "^5.5.0"
workbox-build "^6.0.2"
vite@^2.0.0-beta.27:
version "2.0.0-beta.27"
resolved "https://registry.npmjs.org/vite/-/vite-2.0.0-beta.27.tgz#a2e4b3a698e67c89fd963ff51ee5283ec564c65c"
integrity sha512-1fGPjSVE4MmCGVguFy7pPurCLnvHu4fJSzVjejd9GoFqCNie+JKCpe3KGsxIb9B8ot/aDd4ISCB0+fH1/01FUA==
vite@^2.0.0-beta.30:
version "2.0.0-beta.30"
resolved "https://registry.npmjs.org/vite/-/vite-2.0.0-beta.30.tgz#d0c1056d1fb05c489614360f92363eebec41a6b4"
integrity sha512-wOeO64J3k4jGjCOkH/6RUcIyT/HOTaDZSiXE75aWYqV9hI7Q6uEeSXbAFtb9bG82RGLEWdsqtCvx5t7gaeqtsw==
dependencies:
esbuild "^0.8.26"
postcss "^8.2.1"
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册