提交 a65ad9ed 编写于 作者: V vben

wip(menu): perf menu

上级 ec7efcf0
......@@ -7,11 +7,13 @@
### ⚡ Performance Improvements
- 异步引入组件
- 优化整体结构
### 🎫 Chores
- 返回顶部样式调整,避免遮住其他元素
- 升级`ant-design-vue``2.0.0-rc.4`
- 升级`ant-design-vue``2.0.0-rc.5`
- 刷新按钮布局调整
### 🐛 Bug Fixes
......@@ -23,6 +25,8 @@
- 修复按钮样式问题
- 修复菜单分割模式问题
- 修复 `Modal``Drawer`组件在使用 emits 数据传递失效问题
- 修复菜单已知问题
- 修复上传组件 api 失效问题
## 2.0.0-rc.13 (2020-12-10)
......
......@@ -2,9 +2,7 @@ import { withInstall } from '../util';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import AppLogo from './src/AppLogo.vue';
export const AppLocalePicker = createAsyncComponent(() => import('./src/AppLocalePicker.vue'), {
loading: true,
});
export const AppLocalePicker = createAsyncComponent(() => import('./src/AppLocalePicker.vue'));
export const AppProvider = createAsyncComponent(() => import('./src/AppProvider.vue'));
export const AppSearch = createAsyncComponent(() => import('./src/search/AppSearch.vue'), {
loading: true,
......
......@@ -11,8 +11,8 @@
:overlayClassName="`${prefixCls}-overlay`"
>
<span :class="prefixCls">
<GlobalOutlined :class="`${prefixCls}__icon`" />
<span v-if="showText">{{ getLangText }}</span>
<Icon icon="cil:language" />
<span v-if="showText" :class="`${prefixCls}__text`">{{ getLangText }}</span>
</span>
</Dropdown>
</template>
......@@ -30,9 +30,10 @@
import { propTypes } from '/@/utils/propTypes';
import { useDesign } from '/@/hooks/web/useDesign';
import Icon from '/@/components/Icon';
export default defineComponent({
name: 'AppLocalPicker',
components: { GlobalOutlined, Dropdown },
components: { GlobalOutlined, Dropdown, Icon },
props: {
// Whether to display text
showText: propTypes.bool.def(true),
......@@ -88,8 +89,8 @@
align-items: center;
cursor: pointer;
&__icon {
margin-right: 4px;
&__text {
margin-left: 6px;
}
}
</style>
......@@ -87,7 +87,7 @@
}
&__title {
font-size: 18px;
font-size: 16px;
font-weight: 700;
opacity: 0;
transition: all 0.5s;
......
......@@ -3,11 +3,13 @@
</template>
<script lang="ts">
import type { PropType } from 'vue';
import { defineComponent, toRefs } from 'vue';
import { defineComponent, toRefs, ref } from 'vue';
import { createAppProviderContext } from './useAppContext';
import designSetting from '/@/settings/designSetting';
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
export default defineComponent({
name: 'AppProvider',
inheritAttrs: false,
......@@ -18,8 +20,17 @@
},
},
setup(props) {
const isMobileRef = ref(false);
createBreakpointListen(({ screenMap, sizeEnum, width }) => {
const lgWidth = screenMap.get(sizeEnum.LG);
if (lgWidth) {
isMobileRef.value = width.value - 1 < lgWidth;
}
});
const { prefixCls } = toRefs(props);
createAppProviderContext({ prefixCls });
createAppProviderContext({ prefixCls, isMobile: isMobileRef });
return {};
},
});
......
......@@ -50,6 +50,6 @@
@prefix-cls: ~'@{namespace}-app-search';
.@{prefix-cls} {
padding: 0 10px;
padding: 2px;
}
</style>
......@@ -3,6 +3,8 @@ import { createContext, useContext } from '/@/hooks/core/useContext';
export interface AppProviderContextProps {
prefixCls: Ref<string>;
isMobile: Ref<boolean>;
}
const key: InjectionKey<AppProviderContextProps> = Symbol();
......
......@@ -77,7 +77,7 @@ export default defineComponent({
onMounted(update);
return () => (
<div ref={elRef} class={[attrs.class, 'app-iconify anticon']} style={unref(wrapStyleRef)} />
<span ref={elRef} class={[attrs.class, 'app-iconify anticon']} style={unref(wrapStyleRef)} />
);
},
});
<template>
<slot name="header" v-if="!getIsHorizontal" />
<ScrollContainer :class="`${prefixCls}-wrapper`" :style="getWrapperStyle">
<Menu
:selectedKeys="selectedKeys"
:defaultSelectedKeys="defaultSelectedKeys"
:mode="mode"
:openKeys="getOpenKeys"
:inlineIndent="inlineIndent"
:theme="theme"
@openChange="handleOpenChange"
:class="getMenuClass"
@click="handleMenuClick"
:subMenuOpenDelay="0.2"
v-bind="getInlineCollapseOptions"
>
<template v-for="item in items" :key="item.path">
<BasicSubMenuItem
:item="item"
:theme="theme"
:level="1"
:appendClass="appendClass"
:parentPath="currentParentPath"
:showTitle="showTitle"
:isHorizontal="isHorizontal"
/>
</template>
</Menu>
</ScrollContainer>
<Menu
:selectedKeys="selectedKeys"
:defaultSelectedKeys="defaultSelectedKeys"
:mode="mode"
:openKeys="getOpenKeys"
:inlineIndent="inlineIndent"
:theme="theme"
@openChange="handleOpenChange"
:class="getMenuClass"
@click="handleMenuClick"
:subMenuOpenDelay="0.2"
v-bind="getInlineCollapseOptions"
>
<template v-for="item in items" :key="item.path">
<BasicSubMenuItem
:item="item"
:theme="theme"
:level="1"
:showTitle="showTitle"
:isHorizontal="isHorizontal"
/>
</template>
</Menu>
</template>
<script lang="ts">
import type { MenuState } from './types';
import {
computed,
defineComponent,
unref,
reactive,
watch,
toRefs,
ref,
CSSProperties,
} from 'vue';
import { computed, defineComponent, unref, reactive, watch, toRefs, ref } from 'vue';
import { Menu } from 'ant-design-vue';
import BasicSubMenuItem from './components/BasicSubMenuItem.vue';
import { ScrollContainer } from '/@/components/Container';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import { appStore } from '/@/store/modules/app';
import { useOpenKeys } from './useOpenKeys';
import { useRouter } from 'vue-router';
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router';
import { isFunction } from '/@/utils/is';
import { getCurrentParentPath } from '/@/router/menus';
import { basicProps } from './props';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { REDIRECT_NAME } from '/@/router/constant';
import { tabStore } from '/@/store/modules/tab';
import { useDesign } from '/@/hooks/web/useDesign';
import { getCurrentParentPath } from '/@/router/menus';
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
export default defineComponent({
name: 'BasicMenu',
components: {
Menu,
ScrollContainer,
BasicSubMenuItem,
// BasicSubMenuItem: createAsyncComponent(() => import('./components/BasicSubMenuItem.vue')),
},
props: basicProps,
emits: ['menuClick'],
setup(props, { emit }) {
const currentParentPath = ref('');
const isClickGo = ref(false);
const menuState = reactive<MenuState>({
......@@ -97,18 +80,24 @@
accordion
);
const getMenuClass = computed(() => {
const getIsTopMenu = computed(() => {
const { type, mode } = props;
return (
(type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL) ||
(props.isHorizontal && unref(getSplit))
);
});
const getMenuClass = computed(() => {
return [
prefixCls,
`justify-${unref(getTopMenuAlign)}`,
{
[`${prefixCls}--hide-title`]: !unref(showTitle),
[`${prefixCls}--collapsed-show-title`]: props.collapsedShowTitle,
[`${prefixCls}__second`]:
!props.isHorizontal && appStore.getProjectConfig.menuSetting.split,
[`${prefixCls}__sidebar-hor`]:
type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL,
[`${prefixCls}__second`]: !props.isHorizontal && unref(getSplit),
[`${prefixCls}__sidebar-hor`]: unref(getIsTopMenu),
},
];
});
......@@ -125,23 +114,10 @@
return inlineCollapseOptions;
});
const getWrapperStyle = computed(
(): CSSProperties => {
return {
height: `calc(100% - ${props.showLogo ? '48px' : '0px'})`,
overflowY: 'hidden',
};
}
);
watch(
() => tabStore.getCurrentTab,
() => {
if (unref(currentRoute).name === REDIRECT_NAME) return;
handleMenuChange();
unref(getSplit) && getParentPath();
}
);
listenerLastChangeTab((route) => {
if (route.name === REDIRECT_NAME) return;
handleMenuChange(route);
}, false);
watch(
() => props.items,
......@@ -153,16 +129,6 @@
}
);
getParentPath();
async function getParentPath() {
const { appendClass } = props;
if (!appendClass) return '';
const parentPath = await getCurrentParentPath(unref(currentRoute).path);
currentParentPath.value = parentPath;
}
async function handleMenuClick({ key, keyPath }: { key: string; keyPath: string[] }) {
const { beforeClickFn } = props;
if (beforeClickFn && isFunction(beforeClickFn)) {
......@@ -176,28 +142,31 @@
menuState.selectedKeys = [key];
}
function handleMenuChange() {
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
if (unref(isClickGo)) {
isClickGo.value = false;
return;
}
const path = unref(currentRoute).path;
const path = (route || unref(currentRoute)).path;
if (props.mode !== MenuModeEnum.HORIZONTAL) {
setOpenKeys(path);
}
menuState.selectedKeys = [path];
if (unref(getIsTopMenu)) {
const parentPath = await getCurrentParentPath(path);
menuState.selectedKeys = [parentPath];
} else {
menuState.selectedKeys = [path];
}
}
return {
prefixCls,
getIsHorizontal,
getWrapperStyle,
handleMenuClick,
getInlineCollapseOptions,
getMenuClass,
handleOpenChange,
getOpenKeys,
currentParentPath,
showTitle,
...toRefs(menuState),
};
......
import type { Menu as MenuType } from '/@/router/types';
import type { PropType } from 'vue';
import { computed, unref } from 'vue';
import { defineComponent } from 'vue';
import Icon from '/@/components/Icon/index';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
const { t } = useI18n();
export default defineComponent({
name: 'MenuContent',
props: {
item: {
type: Object as PropType<MenuType>,
default: null,
},
showTitle: {
type: Boolean as PropType<boolean>,
default: true,
},
level: {
type: Number as PropType<number>,
default: 0,
},
isHorizontal: {
type: Boolean as PropType<boolean>,
default: true,
},
},
setup(props) {
const { prefixCls } = useDesign('basic-menu');
const getI18nName = computed(() => t(props.item?.name));
const getTagClass = computed(() => {
const { item } = props;
const { tag = {} } = item || {};
const { dot, type = 'error' } = tag;
return [
`${prefixCls}__tag`,
type,
{
dot,
},
];
});
const getNameClass = computed(() => {
const { showTitle } = props;
return { [`${prefixCls}--show-title`]: showTitle, [`${prefixCls}__name`]: !showTitle };
});
/**
* @description: 渲染图标
*/
function renderIcon(icon?: string) {
return icon ? <Icon icon={icon} size={18} class="menu-item-icon" /> : null;
}
function renderTag() {
const { item, showTitle, isHorizontal } = props;
if (!item || showTitle || isHorizontal) return null;
const { tag } = item;
if (!tag) return null;
const { dot, content } = tag;
if (!dot && !content) return null;
return <span class={unref(getTagClass)}>{dot ? '' : content}</span>;
}
return () => {
const { item } = props;
if (!item) {
return null;
}
const { icon } = item;
const name = unref(getI18nName);
return (
<span class={`${prefixCls}__content-wrapper`}>
{renderIcon(icon)}
{
<span class={unref(getNameClass)}>
{name}
{renderTag()}
</span>
}
</span>
);
};
},
});
<template>
<MenuItem :class="getLevelClass">
<MenuContent v-bind="$props" :item="item" />
<MenuItemContent v-bind="$props" :item="item" />
</MenuItem>
</template>
<script lang="ts">
......@@ -9,25 +9,18 @@
import { useDesign } from '/@/hooks/web/useDesign';
import { itemProps } from '../props';
import MenuContent from '../MenuContent';
import MenuItemContent from './MenuItemContent.vue';
export default defineComponent({
name: 'BasicMenuItem',
components: { MenuItem: Menu.Item, MenuContent },
components: { MenuItem: Menu.Item, MenuItemContent },
props: itemProps,
setup(props) {
const { prefixCls } = useDesign('basic-menu-item');
const getLevelClass = computed(() => {
const { appendClass, level, item, parentPath, theme } = props;
const isAppendActiveCls = appendClass && level === 1 && item.path === parentPath;
const { level, theme } = props;
const levelCls = [
`${prefixCls}__level${level}`,
theme,
{
'top-active-menu': isAppendActiveCls,
},
];
const levelCls = [`${prefixCls}__level${level}`, theme];
return levelCls;
});
return {
......
......@@ -2,7 +2,7 @@
<BasicMenuItem v-if="!menuHasChildren(item)" v-bind="$props" />
<SubMenu v-else :class="[`${prefixCls}__level${level}`, theme]">
<template #title>
<MenuContent v-bind="$props" :item="item" />
<MenuItemContent v-bind="$props" :item="item" />
</template>
<!-- <template #expandIcon="{ key }">
<ExpandIcon :key="key" />
......@@ -21,17 +21,17 @@
import { useDesign } from '/@/hooks/web/useDesign';
import { itemProps } from '../props';
import BasicMenuItem from './BasicMenuItem.vue';
import MenuContent from '../MenuContent';
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import MenuItemContent from './MenuItemContent.vue';
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export default defineComponent({
name: 'BasicSubMenuItem',
isSubMenu: true,
components: {
BasicMenuItem,
SubMenu: Menu.SubMenu,
MenuItem: Menu.Item,
MenuContent,
MenuItemContent,
// ExpandIcon: createAsyncComponent(() => import('./ExpandIcon.vue')),
},
props: itemProps,
......
<template>
<span :class="`${prefixCls}-wrapper`">
<Icon v-if="getIcon" :icon="getIcon" :size="18" :class="`${prefixCls}-wrapper__icon`" />
<span :class="getNameClass">
{{ getI18nName }}
<MenuItemTag v-bind="$props" />
</span>
</span>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import Icon from '/@/components/Icon/index';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
import { contentProps } from '../props';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
const { t } = useI18n();
export default defineComponent({
name: 'MenuItemContent',
components: { Icon, MenuItemTag: createAsyncComponent(() => import('./MenuItemTag.vue')) },
props: contentProps,
setup(props) {
const { prefixCls } = useDesign('basic-menu-item-content');
const getI18nName = computed(() => t(props.item?.name));
const getIcon = computed(() => props.item?.icon);
const getNameClass = computed(() => {
const { showTitle } = props;
return { [`${prefixCls}--show-title`]: showTitle, [`${prefixCls}__name`]: !showTitle };
});
return {
prefixCls,
getNameClass,
getI18nName,
getIcon,
};
},
});
</script>
<template>
<span :class="getTagClass" v-if="getShowTag">{{ getContent }}</span>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { contentProps } from '../props';
export default defineComponent({
name: 'MenuItemTag',
props: contentProps,
setup(props) {
const { prefixCls } = useDesign('basic-menu-item-tag');
const getShowTag = computed(() => {
const { item, showTitle, isHorizontal } = props;
if (!item || showTitle || isHorizontal) 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 } = props;
const { tag } = item;
const { dot, content } = tag!;
return dot ? '' : content;
});
const getTagClass = computed(() => {
const { item } = props;
const { tag = {} } = item || {};
const { dot, type = 'error' } = tag;
return [
prefixCls,
[`${prefixCls}--${type}`],
{
[`${prefixCls}--dot`]: dot,
},
];
});
return {
prefixCls,
getTagClass,
getShowTag,
getContent,
};
},
});
</script>
@import (reference) '../../../design/index.less';
@basic-menu-prefix-cls: ~'@{namespace}-basic-menu';
@basic-menu-content-prefix-cls: ~'@{namespace}-basic-menu-item-content';
@basic-menu-tag-prefix-cls: ~'@{namespace}-basic-menu-item-tag';
.active-style() {
color: @white;
......@@ -41,7 +43,7 @@
// }
// collapsed show title start
&--show-title {
.@{basic-menu-content-prefix-cls}--show-title {
max-width: unset !important;
opacity: 1 !important;
}
......@@ -78,104 +80,34 @@
& > li > .ant-menu-submenu-title {
line-height: 24px;
}
.@{basic-menu-prefix-cls}__content-wrapper {
.@{basic-menu-content-prefix-cls}-wrapper {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
.@{basic-menu-prefix-cls}--show-title {
.@{basic-menu-content-prefix-cls}--show-title {
line-height: 30px;
}
}
}
.ant-menu-item {
transition: unset;
}
// scrollbar -s tart
// &-wrapper {
/* 滚动槽 */
// &::-webkit-scrollbar {
// width: 5px;
// height: 5px;
// }
// &::-webkit-scrollbar-track {
// background: rgba(0, 0, 0, 0);
// }
// &::-webkit-scrollbar-thumb {
// background: rgba(255, 255, 255, 0.2);
// border-radius: 3px;
// box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
// }
// ::-webkit-scrollbar-thumb:hover {
// background: @border-color-dark;
// }
// }
// scrollbar end
.@{basic-menu-content-prefix-cls}-wrapper {
width: 100%;
&-item__level1.light {
&.top-active-menu {
top: 0 !important;
&__icon {
vertical-align: text-top;
}
}
&.top-active-menu:not(.ant-menu-item-selected) {
color: @primary-color;
border-bottom: 3px solid @primary-color;
}
.ant-menu-item {
transition: unset;
}
&__sidebar-hor {
// overflow: hidden;
&.ant-menu-horizontal {
display: flex;
border: 0;
align-items: center;
.@{basic-menu-prefix-cls}-item__level1 {
margin-right: 2px;
}
&.ant-menu-light {
.ant-menu-item {
&.@{basic-menu-prefix-cls}-item__level1 {
height: @header-height;
line-height: @header-height;
}
}
.ant-menu-submenu:hover,
.ant-menu-item-open,
.ant-menu-submenu-open,
.ant-menu-item-selected,
.ant-menu-submenu-selected,
.ant-menu-item:hover,
.ant-menu-item-active,
.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open,
.ant-menu-submenu-active,
.ant-menu-submenu-title:hover {
color: @primary-color !important;
border-bottom: 3px solid @primary-color;
}
.ant-menu-submenu {
&:hover {
border-bottom: 3px solid @primary-color;
}
&.ant-menu-selected,
&.ant-menu-submenu-selected {
border-bottom: 3px solid @primary-color;
}
}
}
&.ant-menu-dark {
background: transparent;
......@@ -204,12 +136,6 @@
.@{basic-menu-prefix-cls}-item__level1 {
background: transparent;
&.top-active-menu {
color: @white;
background: @top-menu-active-bg-color;
border-radius: 2px 2px 0 0;
}
&.ant-menu-item-selected,
&.ant-menu-submenu-selected {
background: @top-menu-active-bg-color !important;
......@@ -292,7 +218,7 @@
}
&.ant-menu-light:not(.@{basic-menu-prefix-cls}__sidebar-hor) {
overflow: hidden;
// overflow: hidden;
border-right: none;
.ant-menu-item.ant-menu-item-selected.@{basic-menu-prefix-cls}-menu-item__level1,
......@@ -301,26 +227,32 @@
}
}
// 激活的子菜单样式
.ant-menu-item.ant-menu-item-selected {
position: relative;
}
&.@{basic-menu-prefix-cls}__second.ant-menu-inline-collapsed:not(.@{basic-menu-prefix-cls}__sidebar-hor) {
// Reset menu item row height
.ant-menu-item,
.ant-menu-sub.ant-menu-inline > .ant-menu-item,
.ant-menu-sub.ant-menu-inline > .ant-menu-submenu > .ant-menu-submenu-title {
.@{basic-menu-prefix-cls}-item__level1 {
display: flex;
height: @app-menu-item-height * 1.4;
padding: 6px 0 !important;
margin: 0;
font-size: 12px;
line-height: @app-menu-item-height;
align-items: center;
text-align: center;
> div {
padding: 6px 0 !important;
font-size: 12px;
}
.@{basic-menu-content-prefix-cls}__name {
display: inline-block;
width: 50%;
overflow: hidden;
}
}
}
.@{basic-menu-prefix-cls}__tag {
.@{basic-menu-tag-prefix-cls} {
position: absolute;
top: calc(50% - 8px);
right: 30px;
......@@ -332,7 +264,7 @@
color: #fff;
border-radius: 2px;
&.dot {
&--dot {
top: calc(50% - 4px);
width: 8px;
height: 8px;
......@@ -340,19 +272,19 @@
border-radius: 50%;
}
&.primary {
&--primary {
background: @primary-color;
}
&.error {
&--error {
background: @error-color;
}
&.success {
&--success {
background: @success-color;
}
&.warn {
&--warn {
background: @warning-color;
}
}
......@@ -362,10 +294,6 @@
transition: unset;
}
// .ant-menu-submenu-arrow {
// transition: all 0.15s ease 0s;
// }
.ant-menu-inline.ant-menu-sub {
box-shadow: unset !important;
transition: unset;
......@@ -384,10 +312,10 @@
// collapsed show title end
.ant-menu-item,
.ant-menu-submenu-title {
> .@{basic-menu-prefix-cls}__name {
> .@{basic-menu-content-prefix-cls}__name {
width: 100%;
.@{basic-menu-prefix-cls}__tag {
.@{basic-menu-tag-prefix-cls} {
float: right;
margin-top: @app-menu-item-height / 2;
transform: translate(0%, -50%);
......
......@@ -9,7 +9,6 @@ export const basicProps = {
type: Array as PropType<Menu[]>,
default: () => [],
},
appendClass: propTypes.bool,
collapsedShowTitle: propTypes.bool,
......@@ -42,8 +41,16 @@ export const itemProps = {
},
level: propTypes.number,
theme: propTypes.oneOf(['dark', 'light']),
appendClass: propTypes.bool,
parentPath: propTypes.string,
showTitle: propTypes.bool,
isHorizontal: propTypes.bool,
};
export const contentProps = {
item: {
type: Object as PropType<Menu>,
default: null,
},
showTitle: propTypes.bool.def(true),
level: propTypes.number.def(0),
isHorizontal: propTypes.bool.def(true),
};
......@@ -42,6 +42,8 @@ export function useOpenKeys(
if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion)) {
menuState.openKeys = openKeys;
} else {
// const menuList = toRaw(menus.value);
// getAllParentPath(menuList, path);
const rootSubMenuKeys: string[] = [];
for (const { children, path } of unref(menus)) {
if (children && children.length > 0) {
......
......@@ -22,11 +22,13 @@
});
</script>
<style lang="less" scoped>
@import (reference) '../../../design/index.less';
.app-footer {
position: fixed;
right: 0;
bottom: 0;
z-index: 99;
z-index: @page-footer-z-index;
display: flex;
width: 100%;
align-items: center;
......
......@@ -6,7 +6,7 @@
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
z-index: @preview-comp-z-index;
background: rgba(0, 0, 0, 0.5);
user-select: none;
......
......@@ -55,7 +55,6 @@
import { checkFileType, checkImgType, getBase64WithFile } from './helper';
import { buildUUID } from '/@/utils/uuid';
import { createImgPreview } from '/@/components/Preview/index';
import { uploadApi } from '/@/api/sys/upload';
import { isFunction } from '/@/utils/is';
import { warn } from '/@/utils/log';
import FileList from './FileList';
......@@ -176,7 +175,7 @@
}
try {
item.status = UploadResultStatus.UPLOADING;
const { data } = await uploadApi(
const { data } = await props.api?.(
{
...(props.uploadParams || {}),
file: item.file,
......
......@@ -128,7 +128,7 @@
// =================================
// ==============breadcrumb=========
// =================================
@breadcrumb-item-normal-color: #6e90a7;
@breadcrumb-item-normal-color: #999;
// =================================
// ==============button=============
// =================================
......
......@@ -31,6 +31,10 @@ html,
background-color: #fff !important;
}
html {
overflow: hidden;
}
html,
body {
width: 100%;
......
......@@ -11,12 +11,24 @@
@header-height: 48px;
// logo width
@logo-width: 36px;
@logo-width: 32px;
//
@side-drag-z-index: 200;
@page-loading-z-index: 10000;
@lock-page-z-index: 3000;
@layout-header-fixed-z-index: 500;
@multiple-tab-fixed-z-index: 505;
@layout-sider-fixed-z-index: 510;
@preview-comp-z-index: 1000;
@page-footer-z-index: 99;
// left-menu
@app-menu-item-height: 42px;
......@@ -10,6 +10,9 @@ export interface CreateCallbackParams {
screen: ComputedRef<sizeEnum | undefined>;
width: ComputedRef<number>;
realWidth: ComputedRef<number>;
screenEnum: typeof screenEnum;
screenMap: Map<sizeEnum, number>;
sizeEnum: typeof sizeEnum;
}
export function useBreakpoint() {
......@@ -54,8 +57,8 @@ export function createBreakpointListen(fn?: (opt: CreateCallbackParams) => void)
name: 'resize',
listener: () => {
resizeFn();
getWindowWidth();
resizeFn();
},
});
......@@ -65,12 +68,14 @@ export function createBreakpointListen(fn?: (opt: CreateCallbackParams) => void)
globalRealWidthRef = computed((): number => unref(realWidthRef));
function resizeFn() {
fn &&
fn({
screen: globalScreenRef,
width: globalWidthRef,
realWidth: globalRealWidthRef,
});
fn?.({
screen: globalScreenRef,
width: globalWidthRef,
realWidth: globalRealWidthRef,
screenEnum,
screenMap,
sizeEnum,
});
}
resizeFn();
......
......@@ -4,7 +4,6 @@ import { computed, unref } from 'vue';
import { appStore } from '/@/store/modules/app';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useFullContent } from '/@/hooks/web/useFullContent';
......@@ -12,7 +11,6 @@ import { useFullContent } from '/@/hooks/web/useFullContent';
import { MenuModeEnum } from '/@/enums/menuEnum';
const { getFullContent } = useFullContent();
const { getShowMultipleTab } = useMultipleTabSetting();
const {
getMenuMode,
getSplit,
......@@ -53,8 +51,6 @@ const getHeaderBgColor = computed(() => unref(getHeaderSetting).bgColor);
const getShowSearch = computed(() => unref(getHeaderSetting).showSearch);
const getShowRedo = computed(() => unref(getHeaderSetting).showRedo && unref(getShowMultipleTab));
const getUseLockPage = computed(() => unref(getHeaderSetting).useLockPage);
const getShowFullScreen = computed(() => unref(getHeaderSetting).showFullScreen);
......@@ -91,7 +87,6 @@ export function useHeaderSetting() {
getShowDoc,
getShowSearch,
getHeaderTheme,
getShowRedo,
getUseLockPage,
getShowFullScreen,
getShowNotice,
......
......@@ -65,6 +65,10 @@ const getIsHorizontal = computed(() => {
return unref(getMenuMode) === MenuModeEnum.HORIZONTAL;
});
const getIsMixMode = computed(() => {
return unref(getMenuMode) === MenuModeEnum.INLINE && unref(getMenuType) === MenuTypeEnum.MIX;
});
const getRealWidth = computed(() => {
return unref(getCollapsed) ? unref(getMiniWidthNumber) : unref(getMenuWidth);
});
......@@ -130,5 +134,6 @@ export function useMenuSetting() {
getIsTopMenu,
getMenuBgColor,
getShowSidebar,
getIsMixMode,
};
}
......@@ -10,6 +10,8 @@ const getShowMultipleTab = computed(() => unref(getMultipleTabSetting).show);
const getShowQuick = computed(() => unref(getMultipleTabSetting).showQuick);
const getShowRedo = computed(() => unref(getMultipleTabSetting).showRedo);
function setMultipleTabSetting(multiTabsSetting: Partial<MultiTabsSetting>) {
appStore.commitProjectConfigState({ multiTabsSetting });
}
......@@ -21,5 +23,6 @@ export function useMultipleTabSetting() {
getMultipleTabSetting,
getShowMultipleTab,
getShowQuick,
getShowRedo,
};
}
import { useAppProviderContext } from '/@/components/Application';
import { computed, unref } from 'vue';
export function useAppInject() {
const values = useAppProviderContext();
return {
getIsMobile: computed(() => unref(values.isMobile)),
};
}
......@@ -25,7 +25,7 @@ export function useI18n(namespace?: string) {
return {
...methods,
t: (key: string, ...arg: Partial<Parameters<typeof t>>) => {
t: (key: string, ...arg: any) => {
if (!key) return '';
return t(getKey(key), ...(arg as Parameters<typeof t>));
},
......
......@@ -33,11 +33,13 @@ export function useGo() {
export const useRedo = () => {
const { push, currentRoute } = router;
const { query, params } = currentRoute.value;
function redo() {
push({
path: '/redirect' + unref(currentRoute).fullPath,
query,
params,
function redo(): Promise<boolean> {
return new Promise((resolve) => {
push({
path: '/redirect' + unref(currentRoute).fullPath,
query,
params,
}).then(() => resolve(true));
});
}
return redo;
......
......@@ -11,7 +11,11 @@ export function useTabs() {
}
return {
refreshPage: () => canIUseFn() && tabStore.commitRedoPage(),
refreshPage: async () => {
if (canIUseFn()) {
await tabStore.commitRedoPage();
}
},
closeAll: () => canIUseFn() && tabStore.closeAllTabAction(),
closeLeft: () => canIUseFn() && tabStore.closeLeftTabAction(tabStore.getCurrentTab),
closeRight: () => canIUseFn() && tabStore.closeRightTabAction(tabStore.getCurrentTab),
......
......@@ -20,11 +20,10 @@
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
import PageLayout from '/@/layouts/page/index';
import { Loading } from '/@/components/Loading';
import Transition from '/@/views/demo/comp/lazy/Transition.vue';
export default defineComponent({
name: 'LayoutContent',
components: { PageLayout, Loading, Transition },
components: { PageLayout, Loading },
setup() {
const { prefixCls } = useDesign('layout-content');
const { getOpenPageLoading } = useTransitionSetting();
......
......@@ -3,38 +3,19 @@ import './index.less';
import type { FunctionalComponent } from 'vue';
import type { Component } from '/@/components/types';
import {
defineComponent,
unref,
computed,
ref,
nextTick,
watchEffect,
// nextTick
} from 'vue';
import { defineComponent, unref, computed } from 'vue';
import { Layout, Tooltip, Badge } from 'ant-design-vue';
import { AppLogo } from '/@/components/Application';
import UserDropdown from './UserDropdown';
import LayoutMenu from '../menu';
import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
import LockAction from './actions/LockAction';
import LayoutTrigger from '../trigger/index.vue';
import NoticeAction from './notice/NoticeActionItem.vue';
import {
RedoOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
LockOutlined,
BugOutlined,
} from '@ant-design/icons-vue';
import { LockOutlined, BugOutlined } from '@ant-design/icons-vue';
import { AppSearch } from '/@/components/Application';
import { useModal } from '/@/components/Modal';
import { useFullscreen } from '/@/hooks/web/useFullScreen';
import { useTabs } from '/@/hooks/web/useTabs';
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
......@@ -49,8 +30,10 @@ import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { AppLocalePicker } from '/@/components/Application';
import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes';
import { useLayoutContext } from '../useLayoutContext';
import { UserDropDown, LayoutBreadcrumb, FullScreen } from './components';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '../../../hooks/web/useDesign';
interface TooltipItemProps {
title: string;
}
......@@ -72,24 +55,14 @@ export default defineComponent({
fixed: propTypes.bool,
},
setup(props) {
let logoEl: Element | null | undefined;
const logoWidthRef = ref(200);
const logoRef = ref<ComponentRef>(null);
const injectValue = useLayoutContext();
const { refreshPage } = useTabs();
const { t } = useI18n();
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsHorizontal } = useMenuSetting();
const { prefixCls } = useDesign('layout-header');
const { getShowTopMenu, getShowHeaderTrigger, getSplit } = useMenuSetting();
const { getShowLocale } = useLocaleSetting();
const { getUseErrorHandle, getShowBreadCrumbIcon } = useRootSetting();
const { getUseErrorHandle } = useRootSetting();
const {
getHeaderTheme,
getShowRedo,
getUseLockPage,
getShowFullScreen,
getShowNotice,
......@@ -100,23 +73,11 @@ export default defineComponent({
const { push } = useRouter();
const [register, { openModal }] = useModal();
const { toggleFullscreen, isFullscreenRef } = useFullscreen();
useWindowSizeFn(
() => {
calcTopMenuWidth();
},
100,
{ immediate: false }
);
const { getIsMobile } = useAppInject();
const headerClass = computed(() => {
const theme = unref(getHeaderTheme);
return theme ? `layout-header__header--${theme}` : '';
});
const isPc = computed(() => {
return !unref(injectValue.isMobile);
return theme ? `${prefixCls}__header--${theme}` : '';
});
const getSplitType = computed(() => {
......@@ -127,25 +88,6 @@ export default defineComponent({
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
});
watchEffect(() => {
if (unref(getIsHorizontal)) {
calcTopMenuWidth();
}
});
function calcTopMenuWidth() {
nextTick(() => {
if (!unref(getShowTopMenu)) return;
let width = 0;
if (!logoEl) {
logoEl = unref(logoRef)?.$el;
}
if (!logoEl) return;
width += logoEl.clientWidth;
logoWidthRef.value = width + 80;
});
}
function handleToErrorList() {
push(PageEnum.ERROR_LOG_PAGE).then(() => {
errorStore.commitErrorListCountState(0);
......@@ -156,27 +98,28 @@ export default defineComponent({
openModal(true);
}
function renderHeaderContent() {
const width = unref(logoWidthRef);
function renderHeaderLeft() {
return (
<div class="layout-header__content ">
{unref(getShowHeaderLogo) && (
<AppLogo class={`layout-header__logo`} ref={logoRef} theme={unref(getHeaderTheme)} />
)}
<>
{unref(getShowContent) && (
<div class="layout-header__left">
<div class={`${prefixCls}__left`}>
{unref(getShowHeaderTrigger) && (
<LayoutTrigger theme={unref(getHeaderTheme)} sider={false} />
)}
{unref(getShowBread) && unref(isPc) && (
<LayoutBreadcrumb showIcon={unref(getShowBreadCrumbIcon)} />
{unref(getShowBread) && !unref(getIsMobile) && (
<LayoutBreadcrumb theme={unref(getHeaderTheme)} />
)}
</div>
)}
</>
);
}
{unref(getShowTopMenu) && unref(isPc) && (
<div class={[`layout-header__menu `]} style={{ width: `calc(100% - ${width}px)` }}>
function renderHeaderContent() {
return (
<div class={`${prefixCls}__content`}>
{unref(getShowTopMenu) && !unref(getIsMobile) && (
<div class={[`${prefixCls}__menu `]}>
{/* <div class={[`layout-header__menu `]}> */}
<LayoutMenu
isHorizontal={true}
......@@ -193,18 +136,18 @@ export default defineComponent({
function renderActionDefault(Comp: Component | any, event: Fn) {
return (
<div class="layout-header__action-item" onClick={event}>
<Comp class="layout-header__action-icon" />
<div class={`${prefixCls}__action-item`} onClick={event}>
<Comp class={`${prefixCls}__action-icon`} />
</div>
);
}
function renderAction() {
return (
<div class={`layout-header__action`}>
{unref(isPc) && <AppSearch class="layout-header__action-item" />}
<div class={`${prefixCls}__action`}>
{!unref(getIsMobile) && <AppSearch class={`${prefixCls}__action-item`} />}
{unref(getUseErrorHandle) && unref(isPc) && (
{unref(getUseErrorHandle) && !unref(getIsMobile) && (
<TooltipItem title={t('layout.header.tooltipErrorLog')}>
{() => (
<Badge
......@@ -219,48 +162,27 @@ export default defineComponent({
</TooltipItem>
)}
{unref(getUseLockPage) && unref(isPc) && (
{unref(getUseLockPage) && !unref(getIsMobile) && (
<TooltipItem title={t('layout.header.tooltipLock')}>
{() => renderActionDefault(LockOutlined, handleLockPage)}
</TooltipItem>
)}
{unref(getShowNotice) && unref(isPc) && (
{unref(getShowNotice) && !unref(getIsMobile) && (
<TooltipItem title={t('layout.header.tooltipNotify')}>
{() => <NoticeAction />}
</TooltipItem>
)}
{unref(getShowRedo) && unref(isPc) && (
<TooltipItem title={t('layout.header.tooltipRedo')}>
{() => renderActionDefault(RedoOutlined, refreshPage)}
</TooltipItem>
)}
{unref(getShowFullScreen) && !unref(getIsMobile) && <FullScreen />}
<UserDropDown theme={unref(getHeaderTheme)} />
{unref(getShowFullScreen) && unref(isPc) && (
<TooltipItem
title={
unref(isFullscreenRef)
? t('layout.header.tooltipExitFull')
: t('layout.header.tooltipEntryFull')
}
>
{() => {
const Icon = !unref(isFullscreenRef) ? (
<FullscreenOutlined />
) : (
<FullscreenExitOutlined />
);
return renderActionDefault(Icon, toggleFullscreen);
}}
</TooltipItem>
)}
<UserDropdown class="layout-header__user-dropdown" />
{unref(getShowLocale) && (
<AppLocalePicker
reload={true}
showText={false}
class="layout-header__action-item locale"
class={`${prefixCls}__action-item locale`}
/>
)}
</div>
......@@ -270,6 +192,10 @@ export default defineComponent({
function renderHeaderDefault() {
return (
<>
{unref(getShowHeaderLogo) && (
<AppLogo class={`${prefixCls}__logo`} theme={unref(getHeaderTheme)} />
)}
{renderHeaderLeft()}
{renderHeaderContent()}
{renderAction()}
<LockAction onRegister={register} />
......@@ -279,9 +205,7 @@ export default defineComponent({
return () => {
return (
<Layout.Header
class={['layout-header', 'flex p-0 px-4 ', unref(headerClass), { fixed: props.fixed }]}
>
<Layout.Header class={[prefixCls, unref(headerClass), { fixed: props.fixed }]}>
{() => renderHeaderDefault()}
</Layout.Header>
);
......
@import (reference) '../../../design/index.less';
.multiple-tab-header {
margin-left: 1px;
transition: width 0.2s;
......@@ -10,7 +12,7 @@
&.fixed {
position: fixed;
top: 0;
z-index: 100;
z-index: @multiple-tab-fixed-z-index;
width: 100%;
}
}
......@@ -2,7 +2,7 @@ import './LayoutMultipleHeader.less';
import { defineComponent, unref, computed, ref, watch, nextTick, CSSProperties } from 'vue';
import LayoutHeader from './LayoutHeader';
import LayoutHeader from './index.vue';
import MultipleTabs from '../tabs/index.vue';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
......@@ -10,6 +10,7 @@ import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useFullContent } from '/@/hooks/web/useFullContent';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { useLayoutContext } from '../useLayoutContext';
import { useAppInject } from '/@/hooks/web/useAppInject';
export default defineComponent({
name: 'LayoutMultipleHeader',
......@@ -21,8 +22,8 @@ export default defineComponent({
const injectValue = useLayoutContext();
const { getCalcContentWidth } = useMenuSetting();
const { getCalcContentWidth, getSplit } = useMenuSetting();
const { getIsMobile } = useAppInject();
const {
getFixed,
getShowInsetHeaderRef,
......@@ -36,7 +37,7 @@ export default defineComponent({
const { getShowMultipleTab } = useMultipleTabSetting();
const showTabsRef = computed(() => {
const getShowTabs = computed(() => {
return unref(getShowMultipleTab) && !unref(getFullContent);
});
......@@ -56,7 +57,7 @@ export default defineComponent({
(): CSSProperties => {
const style: CSSProperties = {};
if (unref(getFixed)) {
style.width = unref(injectValue.isMobile) ? '100%' : unref(getCalcContentWidth);
style.width = unref(getIsMobile) ? '100%' : unref(getCalcContentWidth);
}
if (unref(getShowFullHeaderRef)) {
style.top = `${unref(fullHeaderHeightRef)}px`;
......@@ -84,7 +85,7 @@ export default defineComponent({
const fullHeaderEl = unref(injectValue.fullHeader)?.$el;
let height = 0;
if (headerEl && !unref(getShowFullHeaderRef)) {
if (headerEl && !unref(getShowFullHeaderRef) && !unref(getSplit)) {
height += headerEl.offsetHeight;
}
......@@ -97,6 +98,7 @@ export default defineComponent({
height += fullHeaderHeight;
fullHeaderHeightRef.value = fullHeaderHeight;
}
placeholderHeightRef.value = height;
});
},
......@@ -114,7 +116,7 @@ export default defineComponent({
class={['multiple-tab-header', unref(getHeaderTheme), { fixed: unref(getIsFixed) }]}
>
{unref(getShowInsetHeaderRef) && <LayoutHeader ref={headerElRef} />}
{unref(showTabsRef) && <MultipleTabs ref={tabElRef} />}
{unref(getShowTabs) && <MultipleTabs ref={tabElRef} />}
</div>
</>
);
......
// components
import { Dropdown, Menu } from 'ant-design-vue';
import { defineComponent, computed, unref } from 'vue';
// res
import headerImg from '/@/assets/images/header.jpg';
import Icon from '/@/components/Icon/index';
import { userStore } from '/@/store/modules/user';
import { DOC_URL } from '/@/settings/siteSetting';
import { openWindow } from '/@/utils';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { FunctionalComponent } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
type MenuEvent = 'loginOut' | 'doc';
interface MenuItemProps {
icon: string;
text: string;
key: MenuEvent;
}
const prefixCls = 'user-dropdown';
const MenuItem: FunctionalComponent<MenuItemProps> = (props) => {
const { key, icon, text } = props;
return (
<Menu.Item key={key}>
{() => (
<span class="flex items-center">
<Icon icon={icon} class="mr-1" />
<span>{text}</span>
</span>
)}
</Menu.Item>
);
};
export default defineComponent({
name: 'UserDropdown',
setup() {
const { t } = useI18n();
const { getShowDoc } = useHeaderSetting();
const getUserInfo = computed(() => {
const { realName = '', desc } = userStore.getUserInfoState || {};
return { realName, desc };
});
// login out
function handleLoginOut() {
userStore.confirmLoginOut();
}
// open doc
function openDoc() {
openWindow(DOC_URL);
}
function handleMenuClick(e: { key: MenuEvent }) {
switch (e.key) {
case 'loginOut':
handleLoginOut();
break;
case 'doc':
openDoc();
break;
}
}
function renderSlotsDefault() {
const { realName } = unref(getUserInfo);
return (
<section class={prefixCls}>
<img class={`${prefixCls}__header`} src={headerImg} />
<section class={`${prefixCls}__info`}>
<section class={`${prefixCls}__name`}>{realName}</section>
</section>
</section>
);
}
function renderSlotOverlay() {
const showDoc = unref(getShowDoc);
return (
<Menu onClick={handleMenuClick}>
{() => (
<>
{showDoc && (
<MenuItem
key="doc"
text={t('layout.header.dropdownItemDoc')}
icon="gg:loadbar-doc"
/>
)}
{/* @ts-ignore */}
{showDoc && <Menu.Divider />}
<MenuItem
key="loginOut"
text={t('layout.header.dropdownItemLoginOut')}
icon="carbon:power"
/>
</>
)}
</Menu>
);
}
return () => {
return (
<Dropdown placement="bottomLeft" overlayClassName="app-layout-header-user-dropdown-overlay">
{{
default: () => renderSlotsDefault(),
overlay: () => renderSlotOverlay(),
}}
</Dropdown>
);
};
},
});
<template>
<div class="layout-breadcrumb">
<div :class="[prefixCls, `${prefixCls}--${theme}`]">
<a-breadcrumb :routes="routes">
<template #itemRender="{ route, routes }">
<Icon :icon="route.meta.icon" v-if="showIcon && route.meta.icon" />
<Icon :icon="route.meta.icon" v-if="getShowBreadCrumbIcon && route.meta.icon" />
<span v-if="routes.indexOf(route) === routes.length - 1">
{{ t(route.meta.title) }}
</span>
......@@ -14,7 +14,6 @@
</div>
</template>
<script lang="ts">
import { PropType } from 'vue';
import { defineComponent, ref, toRaw, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';
......@@ -26,18 +25,23 @@
import { HomeOutlined } from '@ant-design/icons-vue';
import { PageEnum } from '/@/enums/pageEnum';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { propTypes } from '/@/utils/propTypes';
export default defineComponent({
name: 'LayoutBreadcrumb',
components: { HomeOutlined, Icon },
props: {
showIcon: {
type: Boolean as PropType<boolean>,
default: false,
},
theme: propTypes.oneOf(['dark', 'light']),
},
setup() {
const routes = ref<RouteLocationMatched[]>([]);
const { currentRoute } = useRouter();
const { prefixCls } = useDesign('layout-breadcrumb');
const { getShowBreadCrumbIcon } = useRootSetting();
const { t } = useI18n();
watchEffect(() => {
......@@ -63,17 +67,71 @@
);
if (filterBreadcrumbList.length === breadcrumbList.length) {
filterBreadcrumbList.unshift({
filterBreadcrumbList.unshift(({
path: PageEnum.BASE_HOME,
meta: {
title: t('layout.header.home'),
},
});
} as unknown) as RouteLocationMatched);
}
routes.value = filterBreadcrumbList;
routes.value = filterBreadcrumbList.length === 1 ? [] : filterBreadcrumbList;
});
return { routes, t };
return { routes, t, prefixCls, getShowBreadCrumbIcon };
},
});
</script>
<style lang="less">
@import (reference) '../../../../design/index.less';
@prefix-cls: ~'@{namespace}-layout-breadcrumb';
.@{prefix-cls} {
display: flex;
padding: 0 8px;
align-items: center;
.ant-breadcrumb-link {
.anticon {
margin-right: 4px;
margin-bottom: 2px;
}
}
&--light {
.ant-breadcrumb-link {
color: @breadcrumb-item-normal-color;
a {
color: @text-color-base;
&:hover {
color: @primary-color;
}
}
}
.ant-breadcrumb-separator {
color: @breadcrumb-item-normal-color;
}
}
&--dark {
.ant-breadcrumb-link {
color: rgba(255, 255, 255, 0.6);
a {
color: rgba(255, 255, 255, 0.8);
&:hover {
color: @white;
}
}
}
.ant-breadcrumb-separator,
.anticon {
color: rgba(255, 255, 255, 0.8);
}
}
}
</style>
<template>
<Tooltip
:title="t('layout.header.tooltipErrorLog')"
placement="bottom"
:mouseEnterDelay="0.5"
@click="handleToErrorList"
>
<Badge :count="getCount" :offset="[0, 10]" dot :overflowCount="99">
<BugOutlined />
</Badge>
</Tooltip>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { Tooltip, Badge } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { BugOutlined } from '@ant-design/icons-vue';
import { errorStore } from '/@/store/modules/error';
import { PageEnum } from '/@/enums/pageEnum';
import { useRouter } from 'vue-router';
export default defineComponent({
name: 'ErrorAction',
components: { BugOutlined, Tooltip, Badge },
setup() {
const { t } = useI18n();
const { push } = useRouter();
const getCount = computed(() => {
return errorStore.getErrorListCountState;
});
function handleToErrorList() {
push(PageEnum.ERROR_LOG_PAGE).then(() => {
errorStore.commitErrorListCountState(0);
});
}
return {
t,
getCount,
handleToErrorList,
};
},
});
</script>
<template>
<Tooltip :title="getTitle" placement="bottom" :mouseEnterDelay="0.5">
<span @click="toggleFullscreen">
<FullscreenOutlined v-if="!isFullscreen" />
<FullscreenExitOutlined v-else />
</span>
</Tooltip>
</template>
<script lang="ts">
import { defineComponent, computed, unref } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useFullscreen } from '/@/hooks/web/useFullScreen';
import { FullscreenExitOutlined, FullscreenOutlined } from '@ant-design/icons-vue';
export default defineComponent({
name: 'FullScreen',
components: { FullscreenExitOutlined, FullscreenOutlined, Tooltip },
setup() {
const { t } = useI18n();
const { toggleFullscreen, isFullscreenRef } = useFullscreen();
const getTitle = computed(() => {
return unref(isFullscreenRef)
? t('layout.header.tooltipExitFull')
: t('layout.header.tooltipEntryFull');
});
return {
getTitle,
isFullscreen: isFullscreenRef,
toggleFullscreen,
};
},
});
</script>
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export const UserDropDown = createAsyncComponent(() => import('./user-dropdown/index.vue'), {
loading: true,
});
export const LayoutBreadcrumb = createAsyncComponent(() => import('./Breadcrumb.vue'));
export const FullScreen = createAsyncComponent(() => import('./FullScreen.vue'));
export const Notify = createAsyncComponent(() => import('./notify/index.vue'));
export const LockItem = createAsyncComponent(() => import('./lock/index.vue'));
export const ErrorAction = createAsyncComponent(() => import('./ErrorAction.vue'));
<template>
<BasicModal
:footer="null"
:title="t('layout.header.lockScreen')"
v-bind="$attrs"
:class="prefixCls"
@register="register"
>
<div :class="`${prefixCls}__entry`">
<div :class="`${prefixCls}__header`">
<img src="/@/assets/images/header.jpg" :class="`${prefixCls}__header-img`" />
<p :class="`${prefixCls}__header-name`">{{ getRealName }}</p>
</div>
<BasicForm @register="registerForm" layout="vertical" />
<div :class="`${prefixCls}__footer`">
<a-button type="primary" block class="mt-2" @click="handleLock">
{{ t('layout.header.lockScreenBtn') }}
</a-button>
</div>
</div>
</BasicModal>
</template>
<script lang="ts">
import { defineComponent, computed } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
import { BasicModal, useModalInner } from '/@/components/Modal/index';
import { BasicForm, useForm } from '/@/components/Form/index';
import { userStore } from '/@/store/modules/user';
import { lockStore } from '/@/store/modules/lock';
export default defineComponent({
name: 'LockModal',
components: { BasicModal, BasicForm },
setup() {
const { t } = useI18n();
const { prefixCls } = useDesign('header-lock-modal');
const getRealName = computed(() => {
return userStore.getUserInfoState?.realName;
});
const [register, { closeModal }] = useModalInner();
const [registerForm, { validateFields, resetFields }] = useForm({
showActionButtonGroup: false,
schemas: [
{
field: 'password',
label: t('layout.header.lockScreenPassword'),
component: 'InputPassword',
required: true,
},
],
});
async function handleLock() {
const values = (await validateFields()) as any;
const password: string | undefined = values.password;
closeModal();
lockStore.commitLockInfoState({
isLock: true,
pwd: password,
});
await resetFields();
}
return {
t,
prefixCls,
getRealName,
register,
registerForm,
handleLock,
};
},
});
</script>
<style lang="less">
@import (reference) '../../../../../design/index.less';
@prefix-cls: ~'@{namespace}-header-lock-modal';
.@{prefix-cls} {
&__entry {
position: relative;
height: 240px;
padding: 130px 30px 60px 30px;
background: #fff;
border-radius: 10px;
}
&__header {
position: absolute;
top: 0;
left: calc(50% - 45px);
width: auto;
text-align: center;
&-img {
width: 70px;
border-radius: 50%;
}
&-name {
margin-top: 5px;
}
}
&__footer {
text-align: center;
}
}
</style>
<template>
<span @click="handleLock">
<Tooltip :title="t('layout.header.tooltipLock')" placement="bottom" :mouseEnterDelay="0.5">
<LockOutlined />
</Tooltip>
<LockAction @register="register" />
</span>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { Tooltip } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { LockOutlined } from '@ant-design/icons-vue';
import { useModal } from '/@/components/Modal';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export default defineComponent({
name: 'FullScreen',
components: {
LockOutlined,
Tooltip,
LockAction: createAsyncComponent(() => import('./LockModal.vue')),
},
setup() {
const { t } = useI18n();
const [register, { openModal }] = useModal();
function handleLock() {
openModal(true);
}
return {
t,
register,
handleLock,
};
},
});
</script>
<template>
<a-list class="list">
<a-list :class="prefixCls">
<template v-for="item in list" :key="item.id">
<a-list-item class="list-item">
<a-list-item-meta>
......@@ -33,6 +33,7 @@
<script lang="ts">
import { defineComponent, PropType } from 'vue';
import { ListItem } from './data';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
props: {
......@@ -41,10 +42,17 @@
default: () => [],
},
},
setup() {
const { prefixCls } = useDesign('header-notify-list');
return { prefixCls };
},
});
</script>
<style lang="less" scoped>
.list {
@import (reference) '../../../../../design/index.less';
@prefix-cls: ~'@{namespace}-header-notify-list';
.@{prefix-cls} {
&::-webkit-scrollbar {
display: none;
}
......
<template>
<div class="layout-header__action-item notify-action">
<Popover title="" trigger="click" overlayClassName="layout-header__notify-action">
<div :class="prefixCls">
<Popover title="" trigger="click" :overlayClassName="`${prefixCls}__overlay`">
<Badge :count="count" dot :numberStyle="numberStyle">
<BellOutlined class="layout-header__action-icon" />
<BellOutlined />
</Badge>
<template #content>
<Tabs>
......@@ -26,10 +26,13 @@
import { BellOutlined } from '@ant-design/icons-vue';
import { tabListData } from './data';
import NoticeList from './NoticeList.vue';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
components: { Popover, BellOutlined, Tabs, TabPane: Tabs.TabPane, Badge, NoticeList },
setup() {
const { prefixCls } = useDesign('header-notify');
let count = 0;
for (let i = 0; i < tabListData.length; i++) {
......@@ -37,6 +40,7 @@
}
return {
prefixCls,
tabListData,
count,
numberStyle: {},
......@@ -45,13 +49,16 @@
});
</script>
<style lang="less">
.layout-header__notify-action {
max-width: 360px;
}
@import (reference) '../../../../../design/index.less';
@prefix-cls: ~'@{namespace}-header-notify';
.notify-action {
.@{prefix-cls} {
padding-top: 2px;
&__overlay {
max-width: 360px;
}
.ant-tabs-content {
width: 300px;
}
......
<template>
<MenuItem :key="key">
<span class="flex items-center">
<Icon :icon="icon" class="mr-1" />
<span>{{ text }}</span>
</span>
</MenuItem>
</template>
<script lang="ts">
// components
import { Menu } from 'ant-design-vue';
import { defineComponent } from 'vue';
import Icon from '/@/components/Icon/index';
import { propTypes } from '/@/utils/propTypes';
export default defineComponent({
name: 'DropdownMenuItem',
components: { MenuItem: Menu.Item, Icon },
props: {
key: propTypes.string,
text: propTypes.string,
icon: propTypes.string,
},
});
</script>
<template>
<Dropdown placement="bottomLeft" :overlayClassName="`${prefixCls}-dropdown-overlay`">
<span :class="[prefixCls, `${prefixCls}--${theme}`]">
<img :class="`${prefixCls}__header`" src="/@/assets/images/header.jpg" />
<span :class="`${prefixCls}__info`">
<span :class="`${prefixCls}__name anticon`">{{ getUserInfo.realName }}</span>
</span>
</span>
<template #overlay>
<Menu @click="handleMenuClick">
<MenuItem key="doc" :text="t('layout.header.dropdownItemDoc')" icon="gg:loadbar-doc" />
<MenuDivider v-if="getShowDoc" />
<MenuItem
key="loginOut"
:text="t('layout.header.dropdownItemLoginOut')"
icon="carbon:power"
/>
</Menu>
</template>
</Dropdown>
</template>
<script lang="ts">
// components
import { Dropdown, Menu } from 'ant-design-vue';
import { defineComponent, computed } from 'vue';
// res
import Icon from '/@/components/Icon/index';
import { userStore } from '/@/store/modules/user';
import { DOC_URL } from '/@/settings/siteSetting';
import { openWindow } from '/@/utils';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useI18n } from '/@/hooks/web/useI18n';
import { useDesign } from '/@/hooks/web/useDesign';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { propTypes } from '/@/utils/propTypes';
type MenuEvent = 'loginOut' | 'doc';
export default defineComponent({
name: 'UserDropdown',
components: {
Dropdown,
Menu,
MenuItem: createAsyncComponent(() => import('./DropMenuItem.vue')),
MenuDivider: Menu.Divider,
Icon,
},
props: {
theme: propTypes.oneOf(['dark', 'light']),
},
setup() {
const { prefixCls } = useDesign('header-user-dropdown');
const { t } = useI18n();
const { getShowDoc } = useHeaderSetting();
const getUserInfo = computed(() => {
const { realName = '', desc } = userStore.getUserInfoState || {};
return { realName, desc };
});
// login out
function handleLoginOut() {
userStore.confirmLoginOut();
}
// open doc
function openDoc() {
openWindow(DOC_URL);
}
function handleMenuClick(e: { key: MenuEvent }) {
switch (e.key) {
case 'loginOut':
handleLoginOut();
break;
case 'doc':
openDoc();
break;
}
}
return {
prefixCls,
t,
getUserInfo,
handleMenuClick,
getShowDoc,
};
},
});
</script>
<style lang="less">
@import (reference) '../../../../../design/index.less';
@prefix-cls: ~'@{namespace}-header-user-dropdown';
.@{prefix-cls} {
display: flex;
height: @header-height;
min-width: 100px;
padding: 0 0 0 10px;
padding-right: 10px;
overflow: hidden;
font-size: 12px;
cursor: pointer;
align-items: center;
&:hover {
background: @header-light-bg-hover-color;
}
img {
width: 26px;
height: 26px;
margin-right: 12px;
}
&__header {
border-radius: 50%;
}
&__name {
font-size: 14px;
}
&--dark {
&:hover {
background: @header-dark-bg-hover-color;
}
}
&--light {
.@{prefix-cls}__name {
color: @text-color-base;
}
.@{prefix-cls}__desc {
color: @header-light-desc-color;
}
}
&-dropdown-overlay {
.ant-dropdown-menu-item {
min-width: 160px;
}
}
}
</style>
@import (reference) '../../../design/index.less';
@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger';
@header-prefix-cls: ~'@{namespace}-layout-header';
@locale-prefix-cls: ~'@{namespace}-app-locale-picker';
.layout-header {
.@{header-prefix-cls} {
display: flex;
height: @header-height;
padding: 0 20px 0 0;
padding: 0;
margin-left: -1px;
line-height: @header-height;
color: @white;
......@@ -12,15 +14,28 @@
align-items: center;
justify-content: space-between;
&.fixed {
&--fixed {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
z-index: @layout-header-fixed-z-index;
width: 100%;
}
&__left {
&-logo {
height: @header-height;
min-width: 192px;
padding: 0 10px;
font-size: 14px;
img {
width: @logo-width;
height: @logo-width;
margin-right: 2px;
}
}
&-left {
display: flex;
height: 100%;
align-items: center;
......@@ -33,7 +48,7 @@
align-items: center;
.anticon {
font-size: 17px;
font-size: 16px;
}
&.light {
......@@ -52,82 +67,65 @@
}
}
}
.layout-breadcrumb {
display: flex;
padding: 0 8px;
align-items: center;
.ant-breadcrumb-link {
.anticon {
margin-right: 4px;
margin-bottom: 2px;
}
}
}
}
&__content {
display: flex;
&-menu {
height: 100%;
flex-grow: 1;
min-width: 0;
flex: 1;
align-items: center;
}
&__header--light {
background: @white;
border-bottom: 1px solid @header-light-bottom-border-color;
&-action {
display: flex;
min-width: 200px;
padding-right: 12px;
align-items: center;
.layout-header__menu {
height: calc(@header-height - 1px);
&__item {
display: flex;
height: @header-height;
padding: 0 2px;
font-size: 1.2em;
cursor: pointer;
align-items: center;
.ant-menu-submenu {
.ant-badge {
height: @header-height;
line-height: @header-height;
}
}
.layout-breadcrumb {
.ant-breadcrumb-link {
color: @breadcrumb-item-normal-color;
a {
color: @text-color-base;
&:hover {
color: @primary-color;
}
}
.ant-badge-dot {
top: 10px;
right: 2px;
}
}
.ant-breadcrumb-separator {
color: @breadcrumb-item-normal-color;
}
span[role='img'] {
padding: 0 8px;
}
}
.layout-header__logo {
height: @header-height;
color: @text-color-base;
&--light {
background: @white;
border-bottom: 1px solid @header-light-bottom-border-color;
img {
width: @logo-width;
height: @logo-width;
margin-right: 6px;
}
.@{header-prefix-cls}-logo {
color: @text-color-base;
&:hover {
background: @header-light-bg-hover-color;
}
}
.layout-header__action {
&-item {
.@{header-prefix-cls}-action {
&__item {
&:hover {
background: @header-light-bg-hover-color;
}
&.locale {
padding: 0 10px;
.@{locale-prefix-cls} {
padding: 0 6px;
color: rgba(0, 0, 0, 0.65);
}
}
......@@ -137,134 +135,23 @@
color: @text-color-base;
}
}
.layout-header__user-dropdown {
&:hover {
background: @header-light-bg-hover-color;
}
}
.user-dropdown {
&__name {
color: @text-color-base;
}
&__desc {
color: @header-light-desc-color;
}
}
}
&__header--dark {
&--dark {
background: @header-dark-bg-color;
.layout-header__action {
&-item {
&:hover {
background: @header-dark-bg-hover-color;
}
}
}
.layout-header__logo {
height: @header-height;
img {
width: @logo-width;
height: @logo-width;
margin-right: 10px;
}
&:hover {
background: @header-dark-bg-hover-color;
}
}
.layout-header__user-dropdown {
.@{header-prefix-cls}-logo {
&:hover {
background: @header-dark-bg-hover-color;
}
}
.layout-breadcrumb {
.ant-breadcrumb-link {
color: rgba(255, 255, 255, 0.6);
a {
color: rgba(255, 255, 255, 0.8);
&:hover {
color: @white;
}
.@{header-prefix-cls}-action {
&__item {
&:hover {
background: @header-dark-bg-hover-color;
}
}
.ant-breadcrumb-separator,
.anticon {
color: rgba(255, 255, 255, 0.8);
}
}
}
&__logo {
padding: 0 10px;
}
&__bread {
display: none;
flex: 1;
}
&__action {
display: flex;
align-items: center;
&-item {
display: flex;
height: @header-height;
padding: 0 2px;
font-size: 1.2em;
cursor: pointer;
align-items: center;
}
&-icon {
padding: 0 8px;
}
}
&__menu {
margin-left: 4px;
overflow: hidden;
align-items: center;
}
&__user-dropdown {
height: @header-height;
padding: 0 0 0 10px;
}
.user-dropdown {
display: flex;
padding-right: 10px;
font-size: 12px;
cursor: pointer;
align-items: center;
img {
width: 26px;
height: 26px;
margin-right: 12px;
}
&__header {
border-radius: 50%;
}
}
}
.app-layout-header-user-dropdown-overlay {
.ant-dropdown-menu-item {
min-width: 160px;
}
}
<template>
<Header :class="getHeaderClass">
<!-- left start -->
<div :class="`${prefixCls}-left`">
<!-- logo -->
<AppLogo v-if="getShowHeaderLogo" :class="`${prefixCls}-logo`" :theme="getHeaderTheme" />
<LayoutTrigger
v-if="getShowContent && getShowHeaderTrigger"
:theme="getHeaderTheme"
:sider="false"
/>
<LayoutBreadcrumb
v-if="getShowContent && getShowBread && !getIsMobile"
:theme="getHeaderTheme"
/>
</div>
<!-- left end -->
<!-- menu start -->
<div :class="`${prefixCls}-menu`" v-if="getShowTopMenu && !getIsMobile">
<LayoutMenu
:isHorizontal="true"
:theme="getHeaderTheme"
:splitType="getSplitType"
:menuMode="getMenuMode"
/>
</div>
<!-- menu-end -->
<!-- action -->
<div :class="`${prefixCls}-action`">
<AppSearch v-if="!getIsMobile" :class="`${prefixCls}-action__item`" />
<ErrorAction v-if="getUseErrorHandle && !getIsMobile" :class="`${prefixCls}-action__item`" />
<LockItem v-if="getUseLockPage && !getIsMobile" :class="`${prefixCls}-action__item`" />
<Notify v-if="getShowNotice && !getIsMobile" :class="`${prefixCls}-action__item`" />
<FullScreen v-if="getShowFullScreen && !getIsMobile" :class="`${prefixCls}-action__item`" />
<UserDropDown :theme="getHeaderTheme" />
<AppLocalePicker
v-if="getShowLocale"
:reload="true"
:showText="false"
:class="`${prefixCls}-action__item`"
/>
</div>
</Header>
</template>
<script lang="ts">
import { defineComponent, unref, computed } from 'vue';
import { propTypes } from '/@/utils/propTypes';
import { Layout } from 'ant-design-vue';
import { AppLogo } from '/@/components/Application';
import LayoutMenu from '../menu';
import LayoutTrigger from '../trigger/index.vue';
import { AppSearch } from '/@/components/Application';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useLocaleSetting } from '/@/hooks/setting/useLocaleSetting';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { AppLocalePicker } from '/@/components/Application';
import {
UserDropDown,
LayoutBreadcrumb,
FullScreen,
Notify,
LockItem,
ErrorAction,
} from './components';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'LayoutHeader',
components: {
Header: Layout.Header,
AppLogo,
LayoutTrigger,
LayoutBreadcrumb,
LayoutMenu,
UserDropDown,
AppLocalePicker,
FullScreen,
Notify,
LockItem,
AppSearch,
ErrorAction,
},
props: {
fixed: propTypes.bool,
},
setup(props) {
const { prefixCls } = useDesign('layout-header');
const { getShowTopMenu, getShowHeaderTrigger, getSplit } = useMenuSetting();
const { getShowLocale } = useLocaleSetting();
const { getUseErrorHandle } = useRootSetting();
const {
getHeaderTheme,
getUseLockPage,
getShowFullScreen,
getShowNotice,
getShowContent,
getShowBread,
getShowHeaderLogo,
} = useHeaderSetting();
const { getIsMobile } = useAppInject();
const getHeaderClass = computed(() => {
const theme = unref(getHeaderTheme);
return [
prefixCls,
{ [`${prefixCls}--fixed`]: props.fixed, [`${prefixCls}--${theme}`]: theme },
];
});
const getSplitType = computed(() => {
return unref(getSplit) ? MenuSplitTyeEnum.TOP : MenuSplitTyeEnum.NONE;
});
const getMenuMode = computed(() => {
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
});
return {
prefixCls,
getHeaderClass,
getShowHeaderLogo,
getHeaderTheme,
getShowHeaderTrigger,
getIsMobile,
getShowBread,
getShowContent,
getSplitType,
getMenuMode,
getShowTopMenu,
getShowLocale,
getShowFullScreen,
getShowNotice,
getUseLockPage,
getUseErrorHandle,
};
},
});
</script>
<style lang="less">
@import './index.less';
</style>
......@@ -18,7 +18,7 @@
import { Layout } from 'ant-design-vue';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import LayoutHeader from './header/LayoutHeader';
import LayoutHeader from './header/index.vue';
import LayoutContent from './content/index.vue';
import LayoutSideBar from './sider';
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
......@@ -29,8 +29,6 @@
import { createLayoutContext } from './useLayoutContext';
import { registerGlobComp } from '/@/components/registerGlobComp';
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
import { isMobile } from '/@/utils/is';
export default defineComponent({
name: 'DefaultLayout',
......@@ -44,22 +42,17 @@
Layout,
},
setup() {
const headerRef = ref<ComponentRef>(null);
const isMobileRef = ref(false);
const { prefixCls } = useDesign('default-layout');
createLayoutContext({ fullHeader: headerRef, isMobile: isMobileRef });
createBreakpointListen(() => {
isMobileRef.value = isMobile();
});
// ! Only register global components here
// ! Can reduce the size of the first screen code
// default layout It is loaded after login. So it won’t be packaged to the first screen
registerGlobComp();
const headerRef = ref<ComponentRef>(null);
const { prefixCls } = useDesign('default-layout');
createLayoutContext({ fullHeader: headerRef });
const { getShowFullHeaderRef } = useHeaderSetting();
const { getShowSidebar } = useMenuSetting();
......
......@@ -9,6 +9,7 @@ import { AppLogo } from '/@/components/Application';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { ScrollContainer } from '/@/components/Container';
import { useGo } from '/@/hooks/web/usePage';
import { useSplitMenu } from './useLayoutMenu';
......@@ -16,6 +17,7 @@ import { openWindow } from '/@/utils';
import { propTypes } from '/@/utils/propTypes';
import { isUrl } from '/@/utils/is';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { CSSProperties } from 'vue';
export default defineComponent({
name: 'LayoutMenu',
......@@ -53,12 +55,25 @@ export default defineComponent({
const getComputedMenuMode = computed(() => props.menuMode || unref(getMenuMode));
const getComputedMenuTheme = computed(() => props.theme || unref(getMenuTheme));
const showLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType));
const appendClass = computed(() => props.splitType === MenuSplitTyeEnum.TOP);
const getIsShowLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType));
const getUseScroll = computed(() => {
return unref(getIsSidebarType) || props.splitType === MenuSplitTyeEnum.LEFT;
});
const getWrapperStyle = computed(
(): CSSProperties => {
return {
height: `calc(100% - ${unref(getIsShowLogo) ? '48px' : '0px'})`,
};
}
);
/**
* click menu
* @param menu
*/
function handleMenuClick(path: string) {
go(path);
}
......@@ -76,7 +91,7 @@ export default defineComponent({
}
function renderHeader() {
if (!unref(showLogo)) return null;
if (!unref(getIsShowLogo)) return null;
return (
<AppLogo
......@@ -87,7 +102,7 @@ export default defineComponent({
);
}
return () => {
function renderMenu() {
return (
<BasicMenu
beforeClickFn={beforeMenuClickFn}
......@@ -99,13 +114,22 @@ export default defineComponent({
items={unref(menusRef)}
accordion={unref(getAccordion)}
onMenuClick={handleMenuClick}
appendClass={unref(appendClass)}
showLogo={unref(showLogo)}
>
{{
header: () => renderHeader(),
}}
</BasicMenu>
showLogo={unref(getIsShowLogo)}
/>
);
}
return () => {
return (
<>
{renderHeader()}
{unref(getUseScroll) ? (
<ScrollContainer style={unref(getWrapperStyle)}>{() => renderMenu()}</ScrollContainer>
) : (
renderMenu()
)}
;
</>
);
};
},
......
......@@ -14,9 +14,7 @@ import { permissionStore } from '/@/store/modules/permission';
export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
// Menu array
const menusRef = ref<Menu[]>([]);
const { currentRoute } = useRouter();
const { setMenuSetting, getIsHorizontal, getSplit } = useMenuSetting();
const [throttleHandleSplitLeftMenu] = useThrottle(handleSplitLeftMenu, 50);
......@@ -25,9 +23,11 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
() => unref(splitType) !== MenuSplitTyeEnum.LEFT && !unref(getIsHorizontal)
);
const splitLeft = computed(() => !unref(getSplit) || unref(splitType) !== MenuSplitTyeEnum.LEFT);
const getSplitLeft = computed(
() => !unref(getSplit) || unref(splitType) !== MenuSplitTyeEnum.LEFT
);
const spiltTop = computed(() => unref(splitType) === MenuSplitTyeEnum.TOP);
const getSpiltTop = computed(() => unref(splitType) === MenuSplitTyeEnum.TOP);
const normalType = computed(() => {
return unref(splitType) === MenuSplitTyeEnum.NONE || !unref(getSplit);
......@@ -65,7 +65,7 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
// Handle left menu split
async function handleSplitLeftMenu(parentPath: string) {
if (unref(splitLeft)) return;
if (unref(getSplitLeft)) return;
// spilt mode left
const children = await getChildrenMenus(parentPath);
......@@ -88,7 +88,7 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
}
// split-top
if (unref(spiltTop)) {
if (unref(getSpiltTop)) {
const shallowMenus = await getShallowMenus();
menusRef.value = shallowMenus;
......
......@@ -80,7 +80,7 @@ export default defineComponent({
getShowSearch,
} = useHeaderSetting();
const { getShowMultipleTab, getShowQuick } = useMultipleTabSetting();
const { getShowMultipleTab, getShowQuick, getShowRedo } = useMultipleTabSetting();
const getShowMenuRef = computed(() => {
return unref(getShowMenu) && !unref(getIsHorizontal);
......@@ -246,6 +246,13 @@ export default defineComponent({
def={unref(getShowMultipleTab)}
/>
<SwitchItem
title={t('layout.setting.tabsRedoBtn')}
event={HandlerEnum.TABS_SHOW_REDO}
def={unref(getShowRedo)}
disabled={!unref(getShowMultipleTab)}
/>
<SwitchItem
title={t('layout.setting.tabsQuickBtn')}
event={HandlerEnum.TABS_SHOW_QUICK}
......
......@@ -31,6 +31,7 @@ export enum HandlerEnum {
HEADER_SEARCH,
TABS_SHOW_QUICK,
TABS_SHOW_REDO,
TABS_SHOW,
LOCK_TIME,
......
......@@ -113,6 +113,8 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
case HandlerEnum.TABS_SHOW:
return { multiTabsSetting: { show: value } };
case HandlerEnum.TABS_SHOW_REDO:
return { multiTabsSetting: { showRedo: value } };
// ============header==================
case HandlerEnum.HEADER_THEME:
......
@import (reference) '../../../design/index.less';
@prefix-cls: ~'@{namespace}-layout-sideBar';
.layout-sidebar {
// overflow: hidden;
.@{prefix-cls} {
z-index: @layout-sider-fixed-z-index;
&.fixed {
&--fixed {
position: fixed;
top: 0;
left: 0;
height: 100%;
}
&--mix {
top: @header-height;
height: calc(100% - @header-height);
}
&.ant-layout-sider-dark {
background: @sider-dark-bg-color;
......
import './index.less';
import { computed, defineComponent, ref, unref, watch, nextTick, CSSProperties } from 'vue';
import { computed, defineComponent, ref, unref, CSSProperties } from 'vue';
import { Layout } from 'ant-design-vue';
import LayoutMenu from '../menu';
......@@ -8,14 +8,13 @@ import LayoutMenu from '../menu';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useTrigger, useDragLine, useSiderEvent } from './useLayoutSider';
import { useLayoutContext } from '../useLayoutContext';
import { useAppInject } from '/@/hooks/web/useAppInject';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({
name: 'LayoutSideBar',
setup() {
const topRef = ref(0);
const dragBarRef = ref<ElRef>(null);
const sideRef = ref<ElRef>(null);
......@@ -27,22 +26,18 @@ export default defineComponent({
getRealWidth,
getMenuHidden,
getMenuFixed,
getIsMixMode,
} = useMenuSetting();
const { getShowFullHeaderRef, getUnFixedAndFull } = useHeaderSetting();
const injectValue = useLayoutContext();
const { prefixCls } = useDesign('layout-sideBar');
const { getTriggerAttr, getTriggerSlot } = useTrigger();
const { getIsMobile } = useAppInject();
const { renderDragLine } = useDragLine(sideRef, dragBarRef);
const {
getCollapsedWidth,
onBreakpointChange,
onCollapseChange,
onSiderClick,
} = useSiderEvent();
const { getCollapsedWidth, onBreakpointChange, onCollapseChange } = useSiderEvent();
const getMode = computed(() => {
return unref(getSplit) ? MenuModeEnum.INLINE : null;
......@@ -57,40 +52,16 @@ export default defineComponent({
});
const getSiderClass = computed(() => {
return {
'layout-sidebar': true,
fixed: unref(getMenuFixed),
hidden: !unref(showClassSideBarRef),
};
return [
prefixCls,
{
[`${prefixCls}--fixed`]: unref(getMenuFixed),
hidden: !unref(showClassSideBarRef),
[`${prefixCls}--mix`]: unref(getIsMixMode),
},
];
});
const getSiderStyle = computed(() => {
const top = `${unref(topRef)}px`;
if (!unref(getMenuFixed)) {
return { top };
}
return {
top,
height: `calc(100% - ${top})`,
};
});
watch(
() => getShowFullHeaderRef.value,
() => {
topRef.value = 0;
if (unref(getUnFixedAndFull)) return;
nextTick(() => {
const fullHeaderEl = unref(injectValue.fullHeader)?.$el;
if (!fullHeaderEl) return;
topRef.value = fullHeaderEl.offsetHeight;
});
},
{
immediate: true,
}
);
const getHiddenDomStyle = computed(
(): CSSProperties => {
const width = `${unref(getRealWidth)}px`;
......@@ -121,7 +92,7 @@ export default defineComponent({
return () => {
return (
<>
{unref(getMenuFixed) && !unref(injectValue.isMobile) && (
{unref(getMenuFixed) && !unref(getIsMobile) && (
<div style={unref(getHiddenDomStyle)} class={{ hidden: !unref(showClassSideBarRef) }} />
)}
<Layout.Sider
......@@ -129,12 +100,10 @@ export default defineComponent({
breakpoint="lg"
collapsible
class={unref(getSiderClass)}
style={unref(getSiderStyle)}
width={unref(getMenuWidth)}
collapsed={unref(getCollapsed)}
collapsedWidth={unref(getCollapsedWidth)}
theme={unref(getMenuTheme)}
onClick={onSiderClick}
onCollapse={onCollapseChange}
onBreakpoint={onBreakpointChange}
{...unref(getTriggerAttr)}
......
......@@ -16,7 +16,7 @@ export function useSiderEvent() {
const brokenRef = ref(false);
const collapseRef = ref(true);
const { setMenuSetting, getCollapsed, getMiniWidthNumber, getShowMenu } = useMenuSetting();
const { setMenuSetting, getCollapsed, getMiniWidthNumber } = useMenuSetting();
const getCollapsedWidth = computed(() => {
return unref(brokenRef) ? 0 : unref(getMiniWidthNumber);
......@@ -36,12 +36,7 @@ export function useSiderEvent() {
brokenRef.value = broken;
}
function onSiderClick(e: ChangeEvent) {
if (!e || !e.target || e.target.className !== 'basic-menu__content') return;
if (!unref(getCollapsed) || !unref(getShowMenu)) return;
setMenuSetting({ collapsed: false });
}
return { getCollapsedWidth, onCollapseChange, onBreakpointChange, onSiderClick };
return { getCollapsedWidth, onCollapseChange, onBreakpointChange };
}
/**
......
......@@ -4,7 +4,7 @@
<span class="ml-1">{{ getTitle }}</span>
</div>
<span :class="`${prefixCls}__extra`" v-else>
<span :class="`${prefixCls}__extra-quick`" v-else @click="handleContext">
<RightOutlined />
</span>
</Dropdown>
......
<template>
<Tooltip :title="t('layout.multipleTab.tooltipRedo')" placement="bottom" :mouseEnterDelay="0.5">
<span :class="`${prefixCls}__extra-redo`" @click="handleRedo">
<RedoOutlined :spin="loading" />
</span>
</Tooltip>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue';
import { RedoOutlined } from '@ant-design/icons-vue';
import { useDesign } from '/@/hooks/web/useDesign';
import { Tooltip } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
import { useTabs } from '/@/hooks/web/useTabs';
export default defineComponent({
name: 'TabContent',
components: { RedoOutlined, Tooltip },
setup() {
const loading = ref(false);
const { prefixCls } = useDesign('multiple-tabs-content');
const { t } = useI18n();
const { refreshPage } = useTabs();
async function handleRedo() {
loading.value = true;
await refreshPage();
setTimeout(() => {
loading.value = false;
// Animation execution time
}, 1000);
}
return { prefixCls, t, handleRedo, loading };
},
});
</script>
......@@ -34,30 +34,30 @@
border: 1px solid darken(@border-color-light, 6%);
transition: none;
&:not(.ant-tabs-tab-active)::after {
position: absolute;
bottom: -1px;
left: 50%;
width: 100%;
height: 2px;
background-color: @primary-color;
content: '';
opacity: 0;
transform: translate(-50%, 0) scaleX(0);
transform-origin: center;
transition: none;
}
// &:not(.ant-tabs-tab-active)::before {
// position: absolute;
// top: -1px;
// left: 50%;
// width: 100%;
// height: 2px;
// background-color: @primary-color;
// content: '';
// opacity: 0;
// transform: translate(-50%, 0) scaleX(0);
// transform-origin: center;
// transition: none;
// }
&:hover {
.ant-tabs-close-x {
opacity: 1;
}
&:not(.ant-tabs-tab-active)::after {
opacity: 1;
transform: translate(-50%, 0) scaleX(1);
transition: all 0.3s ease-in-out;
}
// &:not(.ant-tabs-tab-active)::before {
// opacity: 1;
// transform: translate(-50%, 0) scaleX(1);
// transition: all 0.3s ease-in-out;
// }
}
.ant-tabs-close-x {
......@@ -152,12 +152,13 @@
}
&-content {
&__extra {
&__extra-quick,
&__extra-redo {
display: inline-block;
width: @multiple-height;
width: 36px;
height: @multiple-height;
line-height: @multiple-height;
color: #999;
color: #666;
text-align: center;
cursor: pointer;
border-left: 1px solid #eee;
......@@ -171,6 +172,12 @@
}
}
&__extra-redo {
span[role='img'] {
transform: rotate(0deg);
}
}
&__info {
display: inline-block;
width: 100%;
......
......@@ -17,14 +17,16 @@
</template>
</TabPane>
</template>
<template #tabBarExtraContent>
<QuickButton />
<template #tabBarExtraContent v-if="getShowRedo || getShowQuick">
<TabRedo v-if="getShowRedo" />
<QuickButton v-if="getShowQuick" />
</template>
</Tabs>
</div>
</template>
<script lang="ts">
import { defineComponent, watch, computed, unref, ref } from 'vue';
import { defineComponent, computed, unref, ref } from 'vue';
import { Tabs } from 'ant-design-vue';
import TabContent from './components/TabContent.vue';
......@@ -38,61 +40,52 @@
import { REDIRECT_NAME } from '/@/router/constant';
import { useDesign } from '/@/hooks/web/useDesign';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
export default defineComponent({
name: 'MultipleTabs',
components: {
QuickButton: createAsyncComponent(() => import('./components/QuickButton.vue')),
TabRedo: createAsyncComponent(() => import('./components/TabRedo.vue')),
Tabs,
TabPane: Tabs.TabPane,
TabContent,
},
setup() {
const affixTextList = initAffixTabs();
const activeKeyRef = ref('');
useTabsDrag(affixTextList);
const { prefixCls } = useDesign('multiple-tabs');
const go = useGo();
const { getShowQuick, getShowRedo } = useMultipleTabSetting();
const getTabsState = computed(() => tabStore.getTabsState);
const unClose = computed(() => {
return getTabsState.value.length === 1;
});
const unClose = computed(() => unref(getTabsState).length === 1);
const getWrapClass = computed(() => {
return [
prefixCls,
{
[`${prefixCls}--hide-close`]: unClose,
[`${prefixCls}--hide-close`]: unref(unClose),
},
];
});
watch(
() => tabStore.getLastChangeRouteState?.path,
() => {
if (tabStore.getLastChangeRouteState?.name === REDIRECT_NAME) {
return;
}
const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
if (!lastChangeRoute || !userStore.getTokenState) return;
const { path, fullPath } = lastChangeRoute;
const p = fullPath || path;
if (activeKeyRef.value !== p) {
activeKeyRef.value = p;
}
tabStore.addTabAction(lastChangeRoute);
},
{
immediate: true,
listenerLastChangeTab((route) => {
const { name } = route;
if (name === REDIRECT_NAME || !route || !userStore.getTokenState) return;
const { path, fullPath } = route;
const p = fullPath || path;
if (activeKeyRef.value !== p) {
activeKeyRef.value = p;
}
);
tabStore.addTabAction(unref(route));
});
function handleChange(activeKey: any) {
activeKeyRef.value = activeKey;
......@@ -114,6 +107,8 @@
handleChange,
activeKeyRef,
getTabsState,
getShowQuick,
getShowRedo,
};
},
});
......
......@@ -10,7 +10,6 @@ import { useTabs } from '/@/hooks/web/useTabs';
import { useI18n } from '/@/hooks/web/useI18n';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
const { t } = useI18n();
......@@ -24,11 +23,8 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
const { getShowMenu, setMenuSetting } = useMenuSetting();
const { getShowHeader, setHeaderSetting } = useHeaderSetting();
const { getShowQuick } = useMultipleTabSetting();
const isTabs = computed(() =>
!unref(getShowQuick) ? true : tabContentProps.type === TabContentEnum.TAB_TYPE
);
const isTabs = computed(() => tabContentProps.type === TabContentEnum.TAB_TYPE);
const getCurrentTab = computed(
(): RouteLocationNormalized => {
......
......@@ -3,7 +3,6 @@ import { createContext, useContext } from '/@/hooks/core/useContext';
export interface LayoutContextProps {
fullHeader: Ref<ComponentRef>;
isMobile: Ref<boolean>;
}
const key: InjectionKey<LayoutContextProps> = Symbol();
......
......@@ -7,7 +7,7 @@ export default {
tooltipErrorLog: 'Error log',
tooltipLock: 'Lock screen',
tooltipNotify: 'Notification',
tooltipRedo: 'Refresh',
tooltipEntryFull: 'Full Screen',
tooltipExitFull: 'Exit Full Screen',
......
......@@ -7,4 +7,5 @@ export default {
closeAll: 'Close All',
putAway: 'PutAway',
unfold: 'Unfold',
tooltipRedo: 'Refresh',
};
......@@ -53,6 +53,7 @@ export default {
breadcrumbIcon: 'Breadcrumbs Icon',
tabs: 'Tabs',
tabsQuickBtn: 'Tabs quick button',
tabsRedoBtn: 'Tabs redo button',
sidebar: 'Sidebar',
header: 'Header',
footer: 'Footer',
......
......@@ -8,7 +8,7 @@ export default {
tooltipErrorLog: '错误日志',
tooltipLock: '锁定屏幕',
tooltipNotify: '消息通知',
tooltipRedo: '刷新',
tooltipEntryFull: '全屏',
tooltipExitFull: '退出全屏',
......
......@@ -7,4 +7,5 @@ export default {
closeAll: '关闭全部',
putAway: '收起',
unfold: '展开',
tooltipRedo: '刷新',
};
......@@ -52,6 +52,7 @@ export default {
breadcrumbIcon: '面包屑图标',
tabs: '标签页',
tabsQuickBtn: '标签页快捷按钮',
tabsRedoBtn: '标签页刷新按钮',
sidebar: '左侧菜单',
header: '顶栏',
footer: '页脚',
......
/**
* Used to monitor routing changes to change the status of menus and tabs. There is no need to monitor the route, because the route status change is affected by the page rendering time, which will be slow
*/
import Mitt from '/@/utils/mitt';
import type { RouteLocationNormalized } from 'vue-router';
import { getRoute } from '/@/router/helper/routeHelper';
const mitt = new Mitt();
const key = Symbol();
let lastChangeTab: RouteLocationNormalized;
export function setLastChangeTab(lastChangeRoute: RouteLocationNormalized) {
mitt.emit(key, getRoute(lastChangeRoute));
lastChangeTab = getRoute(lastChangeRoute);
}
export function listenerLastChangeTab(
callback: (route: RouteLocationNormalized) => void,
immediate = true
) {
mitt.on(key, callback);
if (immediate) {
callback(lastChangeTab);
}
}
......@@ -8,13 +8,12 @@ import { createPageLoadingGuard } from './pageLoadingGuard';
import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
import { getRoute } from '/@/router/helper/routeHelper';
import { setTitle } from '/@/utils/browser';
import { AxiosCanceler } from '/@/utils/http/axios/axiosCancel';
import { tabStore } from '/@/store/modules/tab';
import { useI18n } from '/@/hooks/web/useI18n';
import { REDIRECT_NAME } from '/@/router/constant';
import { setLastChangeTab } from '/@/logics/mitt/tabChange';
const { closeMessageOnSwitch, removeAllHttpPending } = useProjectSetting();
const globSetting = useGlobSetting();
......@@ -35,8 +34,7 @@ export function createGuard(router: Router) {
router.beforeEach(async (to) => {
to.meta.loaded = !!loadedPageMap.get(to.path);
// Notify routing changes
tabStore.commitLastChangeRouteState(getRoute(to));
setLastChangeTab(to);
try {
if (closeMessageOnSwitch) {
Modal.destroyAll();
......
......@@ -68,6 +68,7 @@ export async function getCurrentParentPath(currentPath: string) {
export async function getShallowMenus(): Promise<Menu[]> {
const menus = await getAsyncMenus();
const routes = router.getRoutes();
const shallowMenuList = menus.map((item) => ({ ...item, children: undefined }));
return !isBackMode() ? shallowMenuList.filter(basicFilter(routes)) : shallowMenuList;
}
......
......@@ -62,8 +62,7 @@ const setting: ProjectConfig = {
theme: ThemeEnum.LIGHT,
// Whether to enable the lock screen function
useLockPage: true,
// Whether to show the refresh button
showRedo: true,
// Whether to show the full screen button
showFullScreen: true,
// Whether to show the document button
......@@ -117,6 +116,9 @@ const setting: ProjectConfig = {
canDrag: true,
// Turn on quick actions
showQuick: true,
// Whether to show the refresh button
showRedo: true,
},
// Transition Setting
......
......@@ -5,7 +5,6 @@ import { Action, Module, Mutation, VuexModule, getModule } from 'vuex-module-dec
import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper';
import { PageEnum } from '/@/enums/pageEnum';
import { userStore } from './user';
import store from '/@/store';
import router from '/@/router';
......@@ -13,8 +12,7 @@ import { PAGE_NOT_FOUND_ROUTE, REDIRECT_ROUTE } from '/@/router/constant';
import { RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
import { getRoute } from '/@/router/helper/routeHelper';
import { useGo, useRedo } from '/@/hooks/web/usePage';
// declare namespace TabsStore {
import { cloneDeep } from 'lodash-es';
const NAME = 'tab';
......@@ -34,19 +32,12 @@ class Tab extends VuexModule {
// tab list
tabsState: RouteLocationNormalized[] = [];
// Last route change
lastChangeRouteState: RouteLocationNormalized | null = null;
lastDragEndIndexState = 0;
get getTabsState() {
return this.tabsState;
}
get getLastChangeRouteState() {
return this.lastChangeRouteState;
}
get getCurrentTab(): RouteLocationNormalized {
const route = unref(router.currentRoute);
return this.tabsState.find((item) => item.path === route.path)!;
......@@ -60,12 +51,6 @@ class Tab extends VuexModule {
return this.lastDragEndIndexState;
}
@Mutation
commitLastChangeRouteState(route: RouteLocationNormalized): void {
if (!userStore.getTokenState) return;
this.lastChangeRouteState = route;
}
@Mutation
commitClearCache(): void {
this.cachedMapState = new Map();
......@@ -152,7 +137,7 @@ class Tab extends VuexModule {
this.tabsState.splice(updateIndex, 1, curTab);
return;
}
this.tabsState.push(route);
this.tabsState = cloneDeep([...this.tabsState, route]);
}
/**
......@@ -210,7 +195,7 @@ class Tab extends VuexModule {
}
@Mutation
commitRedoPage() {
async commitRedoPage() {
const route = router.currentRoute.value;
for (const [key, value] of this.cachedMapState) {
const index = value.findIndex((item) => item === (route.name as string));
......@@ -225,7 +210,7 @@ class Tab extends VuexModule {
this.cachedMapState.set(key, value);
}
const redo = useRedo();
redo();
await redo();
}
@Action
......
......@@ -27,6 +27,9 @@ export interface MultiTabsSetting {
// 开启快速操作
showQuick: boolean;
canDrag: boolean;
// 显示刷新按钮
showRedo: boolean;
}
export interface HeaderSetting {
......@@ -34,8 +37,7 @@ export interface HeaderSetting {
fixed: boolean;
show: boolean;
theme: ThemeEnum;
// 显示刷新按钮
showRedo: boolean;
// 显示全屏按钮
showFullScreen: boolean;
// 开启全屏功能
......
......@@ -6,13 +6,13 @@
* @returns {Function} The function's instance
*/
export default class Mitt {
private cache: Map<string, Array<(data: any) => void>>;
private cache: Map<string | Symbol, Array<(...data: any) => void>>;
constructor(all = []) {
// A Map of event names to registered handler functions.
this.cache = new Map(all);
}
once(type: string, handler: Fn) {
once(type: string | Symbol, handler: Fn) {
const decor = (...args: any[]) => {
handler && handler.apply(this, args);
this.off(type, decor);
......@@ -27,7 +27,7 @@ export default class Mitt {
* @param {string|symbol} type Type of event to listen for, or `"*"` for all events
* @param {Function} handler Function to call in response to given event
*/
on(type: string, handler: Fn) {
on(type: string | Symbol, handler: Fn) {
const handlers = this.cache.get(type);
const added = handlers && handlers.push(handler);
if (!added) {
......@@ -41,7 +41,7 @@ export default class Mitt {
* @param {string|symbol} type Type of event to unregister `handler` from, or `"*"`
* @param {Function} handler Handler function to remove
*/
off(type: string, handler: Fn) {
off(type: string | Symbol, handler: Fn) {
const handlers = this.cache.get(type);
if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1);
......@@ -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, 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);
}
......
......@@ -144,7 +144,7 @@
right: 0;
bottom: 0;
left: 0;
z-index: 3000;
z-index: @lock-page-z-index;
display: flex;
width: 100vw;
height: 100vh;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册