提交 ff2b12b4 作者: vben

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

上级 056fc131
## Wip ## Wip
### ✨ Refactor
- 新增 `SimpleMenu`组件替代左侧菜单组件(顶部菜单没有替换,功能尽量做到简单不卡)。解决菜单卡顿问题。
### 🐛 Bug Fixes ### 🐛 Bug Fixes
- 修复 `TableAction`图标问题 - 修复 `TableAction`图标问题
......
...@@ -39,7 +39,7 @@ export default [ ...@@ -39,7 +39,7 @@ export default [
// mock user login // mock user login
{ {
url: '/api/login', url: '/api/login',
timeout: 1000, timeout: 200,
method: 'post', method: 'post',
response: ({ body }) => { response: ({ body }) => {
const { username, password } = body; const { username, password } = body;
...@@ -62,7 +62,6 @@ export default [ ...@@ -62,7 +62,6 @@ export default [
}, },
{ {
url: '/api/getUserInfoById', url: '/api/getUserInfoById',
timeout: 200,
method: 'get', method: 'get',
response: ({ query }) => { response: ({ query }) => {
const { userId } = query; const { userId } = query;
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
}, },
"dependencies": { "dependencies": {
"@iconify/iconify": "^2.0.0-rc.6", "@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", "ant-design-vue": "^2.0.0-rc.8",
"apexcharts": "^3.23.1", "apexcharts": "^3.23.1",
"axios": "^0.21.1", "axios": "^0.21.1",
...@@ -45,12 +45,12 @@ ...@@ -45,12 +45,12 @@
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^11.0.0", "@commitlint/cli": "^11.0.0",
"@commitlint/config-conventional": "^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", "@ls-lint/ls-lint": "^1.9.2",
"@purge-icons/generated": "^0.5.1", "@purge-icons/generated": "^0.5.1",
"@types/echarts": "^4.9.3", "@types/echarts": "^4.9.3",
"@types/fs-extra": "^9.0.6", "@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/koa-static": "^4.0.1",
"@types/lodash-es": "^4.17.4", "@types/lodash-es": "^4.17.4",
"@types/mockjs": "^1.0.3", "@types/mockjs": "^1.0.3",
...@@ -63,24 +63,24 @@ ...@@ -63,24 +63,24 @@
"@typescript-eslint/eslint-plugin": "^4.13.0", "@typescript-eslint/eslint-plugin": "^4.13.0",
"@typescript-eslint/parser": "^4.13.0", "@typescript-eslint/parser": "^4.13.0",
"@vitejs/plugin-legacy": "^1.2.1", "@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", "@vitejs/plugin-vue-jsx": "^1.0.2",
"@vue/compiler-sfc": "^3.0.5", "@vue/compiler-sfc": "^3.0.5",
"@vuedx/typecheck": "^0.5.0", "@vuedx/typecheck": "^0.5.0",
"@vuedx/typescript-plugin-vue": "^0.5.0", "@vuedx/typescript-plugin-vue": "^0.5.0",
"autoprefixer": "^10.2.1", "autoprefixer": "^10.2.1",
"commitizen": "^4.2.2", "commitizen": "^4.2.3",
"conventional-changelog-cli": "^2.1.1", "conventional-changelog-cli": "^2.1.1",
"conventional-changelog-custom-config": "^0.3.1", "conventional-changelog-custom-config": "^0.3.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"eslint": "^7.17.0", "eslint": "^7.18.0",
"eslint-config-prettier": "^7.1.0", "eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.1", "eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-vue": "^7.4.1", "eslint-plugin-vue": "^7.4.1",
"esno": "^0.4.0", "esno": "^0.4.0",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"husky": "^4.3.7", "husky": "^4.3.8",
"koa-static": "^5.0.0", "koa-static": "^5.0.0",
"less": "^4.1.0", "less": "^4.1.0",
"lint-staged": "^10.5.3", "lint-staged": "^10.5.3",
...@@ -96,11 +96,11 @@ ...@@ -96,11 +96,11 @@
"stylelint-order": "^4.1.0", "stylelint-order": "^4.1.0",
"ts-node": "^9.1.0", "ts-node": "^9.1.0",
"typescript": "^4.1.3", "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-html": "^2.0.0-beta.5",
"vite-plugin-mock": "^2.0.0-beta.3", "vite-plugin-mock": "^2.0.0-beta.3",
"vite-plugin-purge-icons": "^0.5.1", "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", "vue-eslint-parser": "^7.3.0",
"yargs": "^16.2.0" "yargs": "^16.2.0"
}, },
......
...@@ -2,54 +2,52 @@ ...@@ -2,54 +2,52 @@
<Teleport to="body"> <Teleport to="body">
<transition name="zoom-fade" mode="out-in"> <transition name="zoom-fade" mode="out-in">
<div :class="getClass" @click.stop v-if="visible"> <div :class="getClass" @click.stop v-if="visible">
<ClickOutSide @clickOutside="handleClose"> <div :class="`${prefixCls}-content`" v-click-outside="handleClose">
<div :class="`${prefixCls}-content`"> <div :class="`${prefixCls}-input__wrapper`">
<div :class="`${prefixCls}-input__wrapper`"> <a-input
<a-input :class="`${prefixCls}-input`"
:class="`${prefixCls}-input`" :placeholder="t('common.searchText')"
:placeholder="t('common.searchText')" allow-clear
allow-clear @change="handleSearch"
@change="handleSearch" >
> <template #prefix>
<template #prefix> <SearchOutlined />
<SearchOutlined /> </template>
</template> </a-input>
</a-input> <span :class="`${prefixCls}-cancel`" @click="handleClose">{{
<span :class="`${prefixCls}-cancel`" @click="handleClose">{{ t('common.cancelText')
t('common.cancelText') }}</span>
}}</span> </div>
</div>
<div :class="`${prefixCls}-not-data`" v-show="getIsNotData"> <div :class="`${prefixCls}-not-data`" v-show="getIsNotData">
{{ t('component.app.searchNotData') }} {{ 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> </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> </div>
</transition> </transition>
</Teleport> </Teleport>
...@@ -63,17 +61,20 @@ ...@@ -63,17 +61,20 @@
import { SearchOutlined } from '@ant-design/icons-vue'; import { SearchOutlined } from '@ant-design/icons-vue';
import AppSearchFooter from './AppSearchFooter.vue'; import AppSearchFooter from './AppSearchFooter.vue';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
import { ClickOutSide } from '/@/components/ClickOutSide';
import { useAppInject } from '/@/hooks/web/useAppInject'; import { useAppInject } from '/@/hooks/web/useAppInject';
import clickOutside from '/@/directives/clickOutside';
export default defineComponent({ export default defineComponent({
name: 'AppSearchModal', name: 'AppSearchModal',
components: { SearchOutlined, ClickOutSide, AppSearchFooter }, components: { SearchOutlined, AppSearchFooter },
emits: ['close'], emits: ['close'],
props: { props: {
visible: Boolean, visible: Boolean,
}, },
directives: {
clickOutside,
},
setup(_, { emit }) { setup(_, { emit }) {
const scrollWrap = ref<ElRef>(null); const scrollWrap = ref<ElRef>(null);
const { prefixCls } = useDesign('app-search-modal'); 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 @@ ...@@ -31,7 +31,7 @@
import type { CSSProperties, PropType } from 'vue'; import type { CSSProperties, PropType } from 'vue';
import type { BasicColumn } from '../../types/table'; 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 { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue';
import { useDesign } from '/@/hooks/web/useDesign'; 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 @@ ...@@ -22,6 +22,12 @@
background: rgba(0, 0, 0, 0.3); background: rgba(0, 0, 0, 0.3);
} }
.ant-popover {
&-content {
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
}
}
// ================================= // =================================
// ==============descriptions======= // ==============descriptions=======
// ================================= // =================================
......
...@@ -9,7 +9,6 @@ ...@@ -9,7 +9,6 @@
--sider-dark-darken-bg-color: #273352; --sider-dark-darken-bg-color: #273352;
--sider-dark-lighten-1-bg-color: #273352; --sider-dark-lighten-1-bg-color: #273352;
--sider-dark-lighten-2-bg-color: #273352; --sider-dark-lighten-2-bg-color: #273352;
--sider-dark-lighten-3-bg-color: #273352;
} }
@white: #fff; @white: #fff;
...@@ -88,7 +87,6 @@ ...@@ -88,7 +87,6 @@
@sider-dark-darken-bg-color: var(--sider-dark-darken-bg-color); @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-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-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
@trigger-dark-hover-bg-color: rgba(255, 255, 255, 0.2); @trigger-dark-hover-bg-color: rgba(255, 255, 255, 0.2);
......
...@@ -78,9 +78,12 @@ const getIsMixMode = computed(() => { ...@@ -78,9 +78,12 @@ const getIsMixMode = computed(() => {
}); });
const getRealWidth = computed(() => { const getRealWidth = computed(() => {
return unref(getCollapsed) && !unref(getMixSideFixed) if (unref(getIsMixSidebar)) {
? unref(getMiniWidthNumber) return unref(getCollapsed) && !unref(getMixSideFixed)
: unref(getMenuWidth); ? unref(getMiniWidthNumber)
: unref(getMenuWidth);
}
return unref(getCollapsed) ? unref(getMiniWidthNumber) : unref(getMenuWidth);
}); });
const getMiniWidthNumber = computed(() => { const getMiniWidthNumber = computed(() => {
......
...@@ -142,7 +142,7 @@ ...@@ -142,7 +142,7 @@
}); });
const getLogoWidth = computed(() => { const getLogoWidth = computed(() => {
if (!unref(getIsMixMode)) { if (!unref(getIsMixMode) || unref(getIsMobile)) {
return {}; return {};
} }
const width = unref(getMenuWidth) < 180 ? 180 : unref(getMenuWidth); const width = unref(getMenuWidth) < 180 ? 180 : unref(getMenuWidth);
......
...@@ -4,6 +4,7 @@ import type { PropType, CSSProperties } from 'vue'; ...@@ -4,6 +4,7 @@ import type { PropType, CSSProperties } from 'vue';
import { computed, defineComponent, unref, toRef } from 'vue'; import { computed, defineComponent, unref, toRef } from 'vue';
import { BasicMenu } from '/@/components/Menu'; import { BasicMenu } from '/@/components/Menu';
import { SimpleMenu } from '/@/components/SimpleMenu';
import { AppLogo } from '/@/components/Application'; import { AppLogo } from '/@/components/Application';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
...@@ -126,7 +127,18 @@ export default defineComponent({ ...@@ -126,7 +127,18 @@ export default defineComponent({
} }
function renderMenu() { 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 <BasicMenu
beforeClickFn={beforeMenuClickFn} beforeClickFn={beforeMenuClickFn}
isHorizontal={props.isHorizontal} isHorizontal={props.isHorizontal}
...@@ -135,7 +147,7 @@ export default defineComponent({ ...@@ -135,7 +147,7 @@ export default defineComponent({
showLogo={unref(getIsShowLogo)} showLogo={unref(getIsShowLogo)}
mode={unref(getComputedMenuMode)} mode={unref(getComputedMenuMode)}
theme={unref(getComputedMenuTheme)} theme={unref(getComputedMenuTheme)}
items={unref(menusRef)} items={menus}
accordion={unref(getAccordion)} accordion={unref(getAccordion)}
onMenuClick={handleMenuClick} onMenuClick={handleMenuClick}
/> />
......
...@@ -40,7 +40,12 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) { ...@@ -40,7 +40,12 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
async ([path]: [string, MenuSplitTyeEnum]) => { async ([path]: [string, MenuSplitTyeEnum]) => {
if (unref(splitNotLeft) || unref(getIsMobile)) return; 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); parentPath && throttleHandleSplitLeftMenu(parentPath);
}, },
{ {
...@@ -67,11 +72,15 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) { ...@@ -67,11 +72,15 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
// Handle left menu split // Handle left menu split
async function handleSplitLeftMenu(parentPath: string) { async function handleSplitLeftMenu(parentPath: string) {
console.log('======================');
console.log(unref(getSplitLeft));
console.log('======================');
if (unref(getSplitLeft) || unref(getIsMobile)) return; if (unref(getSplitLeft) || unref(getIsMobile)) return;
// spilt mode left // spilt mode left
const children = await getChildrenMenus(parentPath); const children = await getChildrenMenus(parentPath);
if (!children) {
if (!children || !children.length) {
setMenuSetting({ hidden: true }); setMenuSetting({ hidden: true });
menusRef.value = []; menusRef.value = [];
return; return;
......
...@@ -61,9 +61,7 @@ ...@@ -61,9 +61,7 @@
/> />
</div> </div>
<ScrollContainer :class="`${prefixCls}-menu-list__content`"> <ScrollContainer :class="`${prefixCls}-menu-list__content`">
<BasicMenu <SimpleMenu
:isHorizontal="false"
mode="inline"
:items="chilrenMenus" :items="chilrenMenus"
:theme="getMenuTheme" :theme="getMenuTheme"
mixSider mixSider
...@@ -85,7 +83,7 @@ ...@@ -85,7 +83,7 @@
import { defineComponent, onMounted, ref, computed, unref } from 'vue'; 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 { ScrollContainer } from '/@/components/Container';
import Icon from '/@/components/Icon'; import Icon from '/@/components/Icon';
import { AppLogo } from '/@/components/Application'; import { AppLogo } from '/@/components/Application';
...@@ -103,13 +101,14 @@ ...@@ -103,13 +101,14 @@
import clickOutside from '/@/directives/clickOutside'; import clickOutside from '/@/directives/clickOutside';
import { getShallowMenus, getChildrenMenus, getCurrentParentPath } from '/@/router/menus'; import { getShallowMenus, getChildrenMenus, getCurrentParentPath } from '/@/router/menus';
import { listenerLastChangeTab } from '/@/logics/mitt/tabChange'; import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
import { SimpleMenu } from '/@/components/SimpleMenu';
export default defineComponent({ export default defineComponent({
name: 'LayoutMixSider', name: 'LayoutMixSider',
components: { components: {
ScrollContainer, ScrollContainer,
AppLogo, AppLogo,
BasicMenu, SimpleMenu,
MenuTag, MenuTag,
Icon, Icon,
Trigger, Trigger,
...@@ -335,6 +334,7 @@ ...@@ -335,6 +334,7 @@
<style lang="less"> <style lang="less">
@prefix-cls: ~'@{namespace}-layout-mix-sider'; @prefix-cls: ~'@{namespace}-layout-mix-sider';
@tag-prefix-cls: ~'@{namespace}-basic-menu-item-tag'; @tag-prefix-cls: ~'@{namespace}-basic-menu-item-tag';
@menu-prefix-cls: ~'@{namespace}-menu';
@width: 80px; @width: 80px;
.@{prefix-cls} { .@{prefix-cls} {
position: fixed; position: fixed;
...@@ -351,6 +351,10 @@ ...@@ -351,6 +351,10 @@
right: 2px; right: 2px;
} }
.@{menu-prefix-cls} {
width: 100% !important;
}
&-dom { &-dom {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
...@@ -392,6 +396,10 @@ ...@@ -392,6 +396,10 @@
} }
} }
.@{prefix-cls}-menu-list { .@{prefix-cls}-menu-list {
&__content {
box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1);
}
&__title { &__title {
.pushpin { .pushpin {
color: rgba(0, 0, 0, 0.35); color: rgba(0, 0, 0, 0.35);
...@@ -578,10 +586,10 @@ ...@@ -578,10 +586,10 @@
&-drag-bar { &-drag-bar {
position: absolute; position: absolute;
top: 0; top: 50px;
right: -3px; right: -1px;
width: 3px; width: 1px;
height: 100%; height: calc(100% - 50px);
cursor: ew-resize; cursor: ew-resize;
background: #f8f8f9; background: #f8f8f9;
border-top: none; 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) { ...@@ -71,7 +71,7 @@ export function updateSidebarBgColor(color: string) {
setCssVar(SIDER_DARK_BG_COLOR, color); setCssVar(SIDER_DARK_BG_COLOR, color);
setCssVar(SIDER_DARK_DARKEN_BG_COLOR, darken(color, 6)); 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)); setCssVar(SIDER_LIGHTEN_2_BG_COLOR, lighten(color, 8));
// only #ffffff is light // only #ffffff is light
......
...@@ -8,6 +8,7 @@ import { createMessageGuard } from './messageGuard'; ...@@ -8,6 +8,7 @@ import { createMessageGuard } from './messageGuard';
import { createScrollGuard } from './scrollGuard'; import { createScrollGuard } from './scrollGuard';
import { createHttpGuard } from './httpGuard'; import { createHttpGuard } from './httpGuard';
import { createPageGuard } from './pageGuard'; import { createPageGuard } from './pageGuard';
import { createStateGuard } from './stateGuard';
export function createGuard(router: Router) { export function createGuard(router: Router) {
createPageGuard(router); createPageGuard(router);
...@@ -18,4 +19,5 @@ export function createGuard(router: Router) { ...@@ -18,4 +19,5 @@ export function createGuard(router: Router) {
createTitleGuard(router); createTitleGuard(router);
createProgressGuard(router); createProgressGuard(router);
createPermissionGuard(router); createPermissionGuard(router);
createStateGuard(router);
} }
...@@ -3,7 +3,7 @@ import { appStore } from '/@/store/modules/app'; ...@@ -3,7 +3,7 @@ import { appStore } from '/@/store/modules/app';
import { PageEnum } from '/@/enums/pageEnum'; import { PageEnum } from '/@/enums/pageEnum';
import { removeTabChangeListener } from '/@/logics/mitt/tabChange'; import { removeTabChangeListener } from '/@/logics/mitt/tabChange';
export function createHttpGuard(router: Router) { export function createStateGuard(router: Router) {
router.afterEach((to) => { router.afterEach((to) => {
// Just enter the login page and clear the authentication information // Just enter the login page and clear the authentication information
if (to.path === PageEnum.BASE_LOGIN) { if (to.path === PageEnum.BASE_LOGIN) {
......
...@@ -54,7 +54,9 @@ export const getMenus = async (): Promise<Menu[]> => { ...@@ -54,7 +54,9 @@ export const getMenus = async (): Promise<Menu[]> => {
// 获取当前路径的顶级路径 // 获取当前路径的顶级路径
export async function getCurrentParentPath(currentPath: string) { export async function getCurrentParentPath(currentPath: string) {
const menus = await getAsyncMenus(); const menus = await getAsyncMenus();
const allParentPath = await getAllParentPath(menus, currentPath); const allParentPath = await getAllParentPath(menus, currentPath);
return allParentPath?.[0]; return allParentPath?.[0];
} }
......
...@@ -28,7 +28,7 @@ export default class Mitt { ...@@ -28,7 +28,7 @@ export default class Mitt {
* @param {Function} handler Function to call in response to given event * @param {Function} handler Function to call in response to given event
*/ */
on(type: string | Symbol, handler: Fn) { on(type: string | Symbol, handler: Fn) {
const handlers = this.cache.get(type); const handlers = this.cache?.get(type);
const added = handlers && handlers.push(handler); const added = handlers && handlers.push(handler);
if (!added) { if (!added) {
this.cache.set(type, [handler]); this.cache.set(type, [handler]);
...@@ -57,7 +57,7 @@ export default class Mitt { ...@@ -57,7 +57,7 @@ export default class Mitt {
* @param {string|symbol} type The event type to invoke * @param {string|symbol} type The event type to invoke
* @param {*} [evt] Any value (object is recommended and powerful), passed to each handler * @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(type) || []).slice()) handler(evt);
for (const handler of (this.cache.get('*') || []).slice()) handler(type, evt); for (const handler of (this.cache.get('*') || []).slice()) handler(type, evt);
} }
......
...@@ -89,6 +89,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => { ...@@ -89,6 +89,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
optimizeDeps: { optimizeDeps: {
include: [ include: [
'@ant-design/icons-vue', '@ant-design/icons-vue',
'echarts/map/js/china',
'ant-design-vue/es/locale/zh_CN', 'ant-design-vue/es/locale/zh_CN',
'moment/dist/locale/zh-cn', 'moment/dist/locale/zh-cn',
'ant-design-vue/es/locale/en_US', 'ant-design-vue/es/locale/en_US',
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论