wip(menu): perf menu

This commit is contained in:
vben 2020-12-15 00:13:23 +08:00
parent ec7efcf0f0
commit a65ad9edd5
80 changed files with 1338 additions and 972 deletions

View File

@ -7,11 +7,13 @@
### ⚡ Performance Improvements ### ⚡ Performance Improvements
- 异步引入组件 - 异步引入组件
- 优化整体结构
### 🎫 Chores ### 🎫 Chores
- 返回顶部样式调整,避免遮住其他元素 - 返回顶部样式调整,避免遮住其他元素
- 升级`ant-design-vue`到`2.0.0-rc.4` - 升级`ant-design-vue`到`2.0.0-rc.5`
- 刷新按钮布局调整
### 🐛 Bug Fixes ### 🐛 Bug Fixes
@ -23,6 +25,8 @@
- 修复按钮样式问题 - 修复按钮样式问题
- 修复菜单分割模式问题 - 修复菜单分割模式问题
- 修复 `Modal`与`Drawer`组件在使用 emits 数据传递失效问题 - 修复 `Modal`与`Drawer`组件在使用 emits 数据传递失效问题
- 修复菜单已知问题
- 修复上传组件 api 失效问题
## 2.0.0-rc.13 (2020-12-10) ## 2.0.0-rc.13 (2020-12-10)

View File

@ -2,9 +2,7 @@ import { withInstall } from '../util';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import AppLogo from './src/AppLogo.vue'; import AppLogo from './src/AppLogo.vue';
export const AppLocalePicker = createAsyncComponent(() => import('./src/AppLocalePicker.vue'), { export const AppLocalePicker = createAsyncComponent(() => import('./src/AppLocalePicker.vue'));
loading: true,
});
export const AppProvider = createAsyncComponent(() => import('./src/AppProvider.vue')); export const AppProvider = createAsyncComponent(() => import('./src/AppProvider.vue'));
export const AppSearch = createAsyncComponent(() => import('./src/search/AppSearch.vue'), { export const AppSearch = createAsyncComponent(() => import('./src/search/AppSearch.vue'), {
loading: true, loading: true,

View File

@ -11,8 +11,8 @@
:overlayClassName="`${prefixCls}-overlay`" :overlayClassName="`${prefixCls}-overlay`"
> >
<span :class="prefixCls"> <span :class="prefixCls">
<GlobalOutlined :class="`${prefixCls}__icon`" /> <Icon icon="cil:language" />
<span v-if="showText">{{ getLangText }}</span> <span v-if="showText" :class="`${prefixCls}__text`">{{ getLangText }}</span>
</span> </span>
</Dropdown> </Dropdown>
</template> </template>
@ -30,9 +30,10 @@
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
import Icon from '/@/components/Icon';
export default defineComponent({ export default defineComponent({
name: 'AppLocalPicker', name: 'AppLocalPicker',
components: { GlobalOutlined, Dropdown }, components: { GlobalOutlined, Dropdown, Icon },
props: { props: {
// Whether to display text // Whether to display text
showText: propTypes.bool.def(true), showText: propTypes.bool.def(true),
@ -88,8 +89,8 @@
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
&__icon { &__text {
margin-right: 4px; margin-left: 6px;
} }
} }
</style> </style>

View File

@ -87,7 +87,7 @@
} }
&__title { &__title {
font-size: 18px; font-size: 16px;
font-weight: 700; font-weight: 700;
opacity: 0; opacity: 0;
transition: all 0.5s; transition: all 0.5s;

View File

@ -3,11 +3,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent, toRefs } from 'vue'; import { defineComponent, toRefs, ref } from 'vue';
import { createAppProviderContext } from './useAppContext'; import { createAppProviderContext } from './useAppContext';
import designSetting from '/@/settings/designSetting'; import designSetting from '/@/settings/designSetting';
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
export default defineComponent({ export default defineComponent({
name: 'AppProvider', name: 'AppProvider',
inheritAttrs: false, inheritAttrs: false,
@ -18,8 +20,17 @@
}, },
}, },
setup(props) { 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); const { prefixCls } = toRefs(props);
createAppProviderContext({ prefixCls }); createAppProviderContext({ prefixCls, isMobile: isMobileRef });
return {}; return {};
}, },
}); });

View File

@ -50,6 +50,6 @@
@prefix-cls: ~'@{namespace}-app-search'; @prefix-cls: ~'@{namespace}-app-search';
.@{prefix-cls} { .@{prefix-cls} {
padding: 0 10px; padding: 2px;
} }
</style> </style>

View File

@ -3,6 +3,8 @@ import { createContext, useContext } from '/@/hooks/core/useContext';
export interface AppProviderContextProps { export interface AppProviderContextProps {
prefixCls: Ref<string>; prefixCls: Ref<string>;
isMobile: Ref<boolean>;
} }
const key: InjectionKey<AppProviderContextProps> = Symbol(); const key: InjectionKey<AppProviderContextProps> = Symbol();

View File

@ -77,7 +77,7 @@ export default defineComponent({
onMounted(update); onMounted(update);
return () => ( 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)} />
); );
}, },
}); });

View File

@ -1,79 +1,62 @@
<template> <template>
<slot name="header" v-if="!getIsHorizontal" /> <Menu
<ScrollContainer :class="`${prefixCls}-wrapper`" :style="getWrapperStyle"> :selectedKeys="selectedKeys"
<Menu :defaultSelectedKeys="defaultSelectedKeys"
:selectedKeys="selectedKeys" :mode="mode"
:defaultSelectedKeys="defaultSelectedKeys" :openKeys="getOpenKeys"
:mode="mode" :inlineIndent="inlineIndent"
:openKeys="getOpenKeys" :theme="theme"
:inlineIndent="inlineIndent" @openChange="handleOpenChange"
:theme="theme" :class="getMenuClass"
@openChange="handleOpenChange" @click="handleMenuClick"
:class="getMenuClass" :subMenuOpenDelay="0.2"
@click="handleMenuClick" v-bind="getInlineCollapseOptions"
:subMenuOpenDelay="0.2" >
v-bind="getInlineCollapseOptions" <template v-for="item in items" :key="item.path">
> <BasicSubMenuItem
<template v-for="item in items" :key="item.path"> :item="item"
<BasicSubMenuItem :theme="theme"
:item="item" :level="1"
:theme="theme" :showTitle="showTitle"
:level="1" :isHorizontal="isHorizontal"
:appendClass="appendClass" />
:parentPath="currentParentPath" </template>
:showTitle="showTitle" </Menu>
:isHorizontal="isHorizontal"
/>
</template>
</Menu>
</ScrollContainer>
</template> </template>
<script lang="ts"> <script lang="ts">
import type { MenuState } from './types'; import type { MenuState } from './types';
import { import { computed, defineComponent, unref, reactive, watch, toRefs, ref } from 'vue';
computed,
defineComponent,
unref,
reactive,
watch,
toRefs,
ref,
CSSProperties,
} from 'vue';
import { Menu } from 'ant-design-vue'; import { Menu } from 'ant-design-vue';
import BasicSubMenuItem from './components/BasicSubMenuItem.vue'; import BasicSubMenuItem from './components/BasicSubMenuItem.vue';
import { ScrollContainer } from '/@/components/Container';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import { appStore } from '/@/store/modules/app';
import { useOpenKeys } from './useOpenKeys'; import { useOpenKeys } from './useOpenKeys';
import { useRouter } from 'vue-router'; import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router';
import { isFunction } from '/@/utils/is'; import { isFunction } from '/@/utils/is';
import { getCurrentParentPath } from '/@/router/menus';
import { basicProps } from './props'; import { basicProps } from './props';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { REDIRECT_NAME } from '/@/router/constant'; import { REDIRECT_NAME } from '/@/router/constant';
import { tabStore } from '/@/store/modules/tab';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
import { getCurrentParentPath } from '/@/router/menus';
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; // import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
export default defineComponent({ export default defineComponent({
name: 'BasicMenu', name: 'BasicMenu',
components: { components: {
Menu, Menu,
ScrollContainer,
BasicSubMenuItem, BasicSubMenuItem,
// BasicSubMenuItem: createAsyncComponent(() => import('./components/BasicSubMenuItem.vue')), // BasicSubMenuItem: createAsyncComponent(() => import('./components/BasicSubMenuItem.vue')),
}, },
props: basicProps, props: basicProps,
emits: ['menuClick'], emits: ['menuClick'],
setup(props, { emit }) { setup(props, { emit }) {
const currentParentPath = ref('');
const isClickGo = ref(false); const isClickGo = ref(false);
const menuState = reactive<MenuState>({ const menuState = reactive<MenuState>({
@ -97,18 +80,24 @@
accordion accordion
); );
const getMenuClass = computed(() => { const getIsTopMenu = computed(() => {
const { type, mode } = props; const { type, mode } = props;
return (
(type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL) ||
(props.isHorizontal && unref(getSplit))
);
});
const getMenuClass = computed(() => {
return [ return [
prefixCls, prefixCls,
`justify-${unref(getTopMenuAlign)}`, `justify-${unref(getTopMenuAlign)}`,
{ {
[`${prefixCls}--hide-title`]: !unref(showTitle), [`${prefixCls}--hide-title`]: !unref(showTitle),
[`${prefixCls}--collapsed-show-title`]: props.collapsedShowTitle, [`${prefixCls}--collapsed-show-title`]: props.collapsedShowTitle,
[`${prefixCls}__second`]: [`${prefixCls}__second`]: !props.isHorizontal && unref(getSplit),
!props.isHorizontal && appStore.getProjectConfig.menuSetting.split, [`${prefixCls}__sidebar-hor`]: unref(getIsTopMenu),
[`${prefixCls}__sidebar-hor`]:
type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL,
}, },
]; ];
}); });
@ -125,23 +114,10 @@
return inlineCollapseOptions; return inlineCollapseOptions;
}); });
const getWrapperStyle = computed( listenerLastChangeTab((route) => {
(): CSSProperties => { if (route.name === REDIRECT_NAME) return;
return { handleMenuChange(route);
height: `calc(100% - ${props.showLogo ? '48px' : '0px'})`, }, false);
overflowY: 'hidden',
};
}
);
watch(
() => tabStore.getCurrentTab,
() => {
if (unref(currentRoute).name === REDIRECT_NAME) return;
handleMenuChange();
unref(getSplit) && getParentPath();
}
);
watch( watch(
() => props.items, () => 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[] }) { async function handleMenuClick({ key, keyPath }: { key: string; keyPath: string[] }) {
const { beforeClickFn } = props; const { beforeClickFn } = props;
if (beforeClickFn && isFunction(beforeClickFn)) { if (beforeClickFn && isFunction(beforeClickFn)) {
@ -176,28 +142,31 @@
menuState.selectedKeys = [key]; menuState.selectedKeys = [key];
} }
function handleMenuChange() { async function handleMenuChange(route?: RouteLocationNormalizedLoaded) {
if (unref(isClickGo)) { if (unref(isClickGo)) {
isClickGo.value = false; isClickGo.value = false;
return; return;
} }
const path = unref(currentRoute).path; const path = (route || unref(currentRoute)).path;
if (props.mode !== MenuModeEnum.HORIZONTAL) { if (props.mode !== MenuModeEnum.HORIZONTAL) {
setOpenKeys(path); setOpenKeys(path);
} }
menuState.selectedKeys = [path]; if (unref(getIsTopMenu)) {
const parentPath = await getCurrentParentPath(path);
menuState.selectedKeys = [parentPath];
} else {
menuState.selectedKeys = [path];
}
} }
return { return {
prefixCls, prefixCls,
getIsHorizontal, getIsHorizontal,
getWrapperStyle,
handleMenuClick, handleMenuClick,
getInlineCollapseOptions, getInlineCollapseOptions,
getMenuClass, getMenuClass,
handleOpenChange, handleOpenChange,
getOpenKeys, getOpenKeys,
currentParentPath,
showTitle, showTitle,
...toRefs(menuState), ...toRefs(menuState),
}; };

View File

@ -1,96 +0,0 @@
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>
);
};
},
});

View File

@ -1,6 +1,6 @@
<template> <template>
<MenuItem :class="getLevelClass"> <MenuItem :class="getLevelClass">
<MenuContent v-bind="$props" :item="item" /> <MenuItemContent v-bind="$props" :item="item" />
</MenuItem> </MenuItem>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -9,25 +9,18 @@
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
import { itemProps } from '../props'; import { itemProps } from '../props';
import MenuContent from '../MenuContent'; import MenuItemContent from './MenuItemContent.vue';
export default defineComponent({ export default defineComponent({
name: 'BasicMenuItem', name: 'BasicMenuItem',
components: { MenuItem: Menu.Item, MenuContent }, components: { MenuItem: Menu.Item, MenuItemContent },
props: itemProps, props: itemProps,
setup(props) { setup(props) {
const { prefixCls } = useDesign('basic-menu-item'); const { prefixCls } = useDesign('basic-menu-item');
const getLevelClass = computed(() => { const getLevelClass = computed(() => {
const { appendClass, level, item, parentPath, theme } = props; const { level, theme } = props;
const isAppendActiveCls = appendClass && level === 1 && item.path === parentPath;
const levelCls = [ const levelCls = [`${prefixCls}__level${level}`, theme];
`${prefixCls}__level${level}`,
theme,
{
'top-active-menu': isAppendActiveCls,
},
];
return levelCls; return levelCls;
}); });
return { return {

View File

@ -2,7 +2,7 @@
<BasicMenuItem v-if="!menuHasChildren(item)" v-bind="$props" /> <BasicMenuItem v-if="!menuHasChildren(item)" v-bind="$props" />
<SubMenu v-else :class="[`${prefixCls}__level${level}`, theme]"> <SubMenu v-else :class="[`${prefixCls}__level${level}`, theme]">
<template #title> <template #title>
<MenuContent v-bind="$props" :item="item" /> <MenuItemContent v-bind="$props" :item="item" />
</template> </template>
<!-- <template #expandIcon="{ key }"> <!-- <template #expandIcon="{ key }">
<ExpandIcon :key="key" /> <ExpandIcon :key="key" />
@ -21,17 +21,17 @@
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
import { itemProps } from '../props'; import { itemProps } from '../props';
import BasicMenuItem from './BasicMenuItem.vue'; import BasicMenuItem from './BasicMenuItem.vue';
import MenuContent from '../MenuContent'; import MenuItemContent from './MenuItemContent.vue';
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export default defineComponent({ export default defineComponent({
name: 'BasicSubMenuItem', name: 'BasicSubMenuItem',
isSubMenu: true,
components: { components: {
BasicMenuItem, BasicMenuItem,
SubMenu: Menu.SubMenu, SubMenu: Menu.SubMenu,
MenuItem: Menu.Item, MenuItem: Menu.Item,
MenuContent, MenuItemContent,
// ExpandIcon: createAsyncComponent(() => import('./ExpandIcon.vue')), // ExpandIcon: createAsyncComponent(() => import('./ExpandIcon.vue')),
}, },
props: itemProps, props: itemProps,

View File

@ -0,0 +1,41 @@
<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>

View File

@ -0,0 +1,56 @@
<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>

View File

@ -1,6 +1,8 @@
@import (reference) '../../../design/index.less'; @import (reference) '../../../design/index.less';
@basic-menu-prefix-cls: ~'@{namespace}-basic-menu'; @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() { .active-style() {
color: @white; color: @white;
@ -41,7 +43,7 @@
// } // }
// collapsed show title start // collapsed show title start
&--show-title { .@{basic-menu-content-prefix-cls}--show-title {
max-width: unset !important; max-width: unset !important;
opacity: 1 !important; opacity: 1 !important;
} }
@ -78,104 +80,34 @@
& > li > .ant-menu-submenu-title { & > li > .ant-menu-submenu-title {
line-height: 24px; line-height: 24px;
} }
.@{basic-menu-prefix-cls}__content-wrapper { .@{basic-menu-content-prefix-cls}-wrapper {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-direction: column; flex-direction: column;
.@{basic-menu-prefix-cls}--show-title { .@{basic-menu-content-prefix-cls}--show-title {
line-height: 30px; line-height: 30px;
} }
} }
} }
.@{basic-menu-content-prefix-cls}-wrapper {
width: 100%;
&__icon {
vertical-align: text-top;
}
}
.ant-menu-item { .ant-menu-item {
transition: unset; 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
&-item__level1.light {
&.top-active-menu {
top: 0 !important;
}
&.top-active-menu:not(.ant-menu-item-selected) {
color: @primary-color;
border-bottom: 3px solid @primary-color;
}
}
&__sidebar-hor { &__sidebar-hor {
// overflow: hidden;
&.ant-menu-horizontal { &.ant-menu-horizontal {
display: flex; display: flex;
border: 0;
align-items: center; 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 { &.ant-menu-dark {
background: transparent; background: transparent;
@ -204,12 +136,6 @@
.@{basic-menu-prefix-cls}-item__level1 { .@{basic-menu-prefix-cls}-item__level1 {
background: transparent; 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-item-selected,
&.ant-menu-submenu-selected { &.ant-menu-submenu-selected {
background: @top-menu-active-bg-color !important; background: @top-menu-active-bg-color !important;
@ -292,7 +218,7 @@
} }
&.ant-menu-light:not(.@{basic-menu-prefix-cls}__sidebar-hor) { &.ant-menu-light:not(.@{basic-menu-prefix-cls}__sidebar-hor) {
overflow: hidden; // overflow: hidden;
border-right: none; border-right: none;
.ant-menu-item.ant-menu-item-selected.@{basic-menu-prefix-cls}-menu-item__level1, .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) { &.@{basic-menu-prefix-cls}__second.ant-menu-inline-collapsed:not(.@{basic-menu-prefix-cls}__sidebar-hor) {
// Reset menu item row height // Reset menu item row height
.ant-menu-item, .@{basic-menu-prefix-cls}-item__level1 {
.ant-menu-sub.ant-menu-inline > .ant-menu-item,
.ant-menu-sub.ant-menu-inline > .ant-menu-submenu > .ant-menu-submenu-title {
display: flex; display: flex;
height: @app-menu-item-height * 1.4; height: @app-menu-item-height * 1.4;
padding: 6px 0 !important; padding: 6px 0 !important;
margin: 0; margin: 0;
font-size: 12px;
line-height: @app-menu-item-height; line-height: @app-menu-item-height;
align-items: center; 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; position: absolute;
top: calc(50% - 8px); top: calc(50% - 8px);
right: 30px; right: 30px;
@ -332,7 +264,7 @@
color: #fff; color: #fff;
border-radius: 2px; border-radius: 2px;
&.dot { &--dot {
top: calc(50% - 4px); top: calc(50% - 4px);
width: 8px; width: 8px;
height: 8px; height: 8px;
@ -340,19 +272,19 @@
border-radius: 50%; border-radius: 50%;
} }
&.primary { &--primary {
background: @primary-color; background: @primary-color;
} }
&.error { &--error {
background: @error-color; background: @error-color;
} }
&.success { &--success {
background: @success-color; background: @success-color;
} }
&.warn { &--warn {
background: @warning-color; background: @warning-color;
} }
} }
@ -362,10 +294,6 @@
transition: unset; transition: unset;
} }
// .ant-menu-submenu-arrow {
// transition: all 0.15s ease 0s;
// }
.ant-menu-inline.ant-menu-sub { .ant-menu-inline.ant-menu-sub {
box-shadow: unset !important; box-shadow: unset !important;
transition: unset; transition: unset;
@ -384,10 +312,10 @@
// collapsed show title end // collapsed show title end
.ant-menu-item, .ant-menu-item,
.ant-menu-submenu-title { .ant-menu-submenu-title {
> .@{basic-menu-prefix-cls}__name { > .@{basic-menu-content-prefix-cls}__name {
width: 100%; width: 100%;
.@{basic-menu-prefix-cls}__tag { .@{basic-menu-tag-prefix-cls} {
float: right; float: right;
margin-top: @app-menu-item-height / 2; margin-top: @app-menu-item-height / 2;
transform: translate(0%, -50%); transform: translate(0%, -50%);

View File

@ -9,7 +9,6 @@ export const basicProps = {
type: Array as PropType<Menu[]>, type: Array as PropType<Menu[]>,
default: () => [], default: () => [],
}, },
appendClass: propTypes.bool,
collapsedShowTitle: propTypes.bool, collapsedShowTitle: propTypes.bool,
@ -42,8 +41,16 @@ export const itemProps = {
}, },
level: propTypes.number, level: propTypes.number,
theme: propTypes.oneOf(['dark', 'light']), theme: propTypes.oneOf(['dark', 'light']),
appendClass: propTypes.bool,
parentPath: propTypes.string,
showTitle: propTypes.bool, showTitle: propTypes.bool,
isHorizontal: 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),
};

View File

@ -42,6 +42,8 @@ export function useOpenKeys(
if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion)) { if (unref(mode) === MenuModeEnum.HORIZONTAL || !unref(accordion)) {
menuState.openKeys = openKeys; menuState.openKeys = openKeys;
} else { } else {
// const menuList = toRaw(menus.value);
// getAllParentPath(menuList, path);
const rootSubMenuKeys: string[] = []; const rootSubMenuKeys: string[] = [];
for (const { children, path } of unref(menus)) { for (const { children, path } of unref(menus)) {
if (children && children.length > 0) { if (children && children.length > 0) {

View File

@ -22,11 +22,13 @@
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import (reference) '../../../design/index.less';
.app-footer { .app-footer {
position: fixed; position: fixed;
right: 0; right: 0;
bottom: 0; bottom: 0;
z-index: 99; z-index: @page-footer-z-index;
display: flex; display: flex;
width: 100%; width: 100%;
align-items: center; align-items: center;

View File

@ -6,7 +6,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
z-index: 1000; z-index: @preview-comp-z-index;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
user-select: none; user-select: none;

View File

@ -55,7 +55,6 @@
import { checkFileType, checkImgType, getBase64WithFile } from './helper'; import { checkFileType, checkImgType, getBase64WithFile } from './helper';
import { buildUUID } from '/@/utils/uuid'; import { buildUUID } from '/@/utils/uuid';
import { createImgPreview } from '/@/components/Preview/index'; import { createImgPreview } from '/@/components/Preview/index';
import { uploadApi } from '/@/api/sys/upload';
import { isFunction } from '/@/utils/is'; import { isFunction } from '/@/utils/is';
import { warn } from '/@/utils/log'; import { warn } from '/@/utils/log';
import FileList from './FileList'; import FileList from './FileList';
@ -176,7 +175,7 @@
} }
try { try {
item.status = UploadResultStatus.UPLOADING; item.status = UploadResultStatus.UPLOADING;
const { data } = await uploadApi( const { data } = await props.api?.(
{ {
...(props.uploadParams || {}), ...(props.uploadParams || {}),
file: item.file, file: item.file,

View File

@ -128,7 +128,7 @@
// ================================= // =================================
// ==============breadcrumb========= // ==============breadcrumb=========
// ================================= // =================================
@breadcrumb-item-normal-color: #6e90a7; @breadcrumb-item-normal-color: #999;
// ================================= // =================================
// ==============button============= // ==============button=============
// ================================= // =================================

View File

@ -31,6 +31,10 @@ html,
background-color: #fff !important; background-color: #fff !important;
} }
html {
overflow: hidden;
}
html, html,
body { body {
width: 100%; width: 100%;

View File

@ -11,12 +11,24 @@
@header-height: 48px; @header-height: 48px;
// logo width // logo width
@logo-width: 36px; @logo-width: 32px;
// //
@side-drag-z-index: 200; @side-drag-z-index: 200;
@page-loading-z-index: 10000; @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 // left-menu
@app-menu-item-height: 42px; @app-menu-item-height: 42px;

View File

@ -10,6 +10,9 @@ export interface CreateCallbackParams {
screen: ComputedRef<sizeEnum | undefined>; screen: ComputedRef<sizeEnum | undefined>;
width: ComputedRef<number>; width: ComputedRef<number>;
realWidth: ComputedRef<number>; realWidth: ComputedRef<number>;
screenEnum: typeof screenEnum;
screenMap: Map<sizeEnum, number>;
sizeEnum: typeof sizeEnum;
} }
export function useBreakpoint() { export function useBreakpoint() {
@ -54,8 +57,8 @@ export function createBreakpointListen(fn?: (opt: CreateCallbackParams) => void)
name: 'resize', name: 'resize',
listener: () => { listener: () => {
resizeFn();
getWindowWidth(); getWindowWidth();
resizeFn();
}, },
}); });
@ -65,12 +68,14 @@ export function createBreakpointListen(fn?: (opt: CreateCallbackParams) => void)
globalRealWidthRef = computed((): number => unref(realWidthRef)); globalRealWidthRef = computed((): number => unref(realWidthRef));
function resizeFn() { function resizeFn() {
fn && fn?.({
fn({ screen: globalScreenRef,
screen: globalScreenRef, width: globalWidthRef,
width: globalWidthRef, realWidth: globalRealWidthRef,
realWidth: globalRealWidthRef, screenEnum,
}); screenMap,
sizeEnum,
});
} }
resizeFn(); resizeFn();

View File

@ -4,7 +4,6 @@ import { computed, unref } from 'vue';
import { appStore } from '/@/store/modules/app'; import { appStore } from '/@/store/modules/app';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { useFullContent } from '/@/hooks/web/useFullContent'; import { useFullContent } from '/@/hooks/web/useFullContent';
@ -12,7 +11,6 @@ import { useFullContent } from '/@/hooks/web/useFullContent';
import { MenuModeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum } from '/@/enums/menuEnum';
const { getFullContent } = useFullContent(); const { getFullContent } = useFullContent();
const { getShowMultipleTab } = useMultipleTabSetting();
const { const {
getMenuMode, getMenuMode,
getSplit, getSplit,
@ -53,8 +51,6 @@ const getHeaderBgColor = computed(() => unref(getHeaderSetting).bgColor);
const getShowSearch = computed(() => unref(getHeaderSetting).showSearch); const getShowSearch = computed(() => unref(getHeaderSetting).showSearch);
const getShowRedo = computed(() => unref(getHeaderSetting).showRedo && unref(getShowMultipleTab));
const getUseLockPage = computed(() => unref(getHeaderSetting).useLockPage); const getUseLockPage = computed(() => unref(getHeaderSetting).useLockPage);
const getShowFullScreen = computed(() => unref(getHeaderSetting).showFullScreen); const getShowFullScreen = computed(() => unref(getHeaderSetting).showFullScreen);
@ -91,7 +87,6 @@ export function useHeaderSetting() {
getShowDoc, getShowDoc,
getShowSearch, getShowSearch,
getHeaderTheme, getHeaderTheme,
getShowRedo,
getUseLockPage, getUseLockPage,
getShowFullScreen, getShowFullScreen,
getShowNotice, getShowNotice,

View File

@ -65,6 +65,10 @@ const getIsHorizontal = computed(() => {
return unref(getMenuMode) === MenuModeEnum.HORIZONTAL; return unref(getMenuMode) === MenuModeEnum.HORIZONTAL;
}); });
const getIsMixMode = computed(() => {
return unref(getMenuMode) === MenuModeEnum.INLINE && unref(getMenuType) === MenuTypeEnum.MIX;
});
const getRealWidth = computed(() => { const getRealWidth = computed(() => {
return unref(getCollapsed) ? unref(getMiniWidthNumber) : unref(getMenuWidth); return unref(getCollapsed) ? unref(getMiniWidthNumber) : unref(getMenuWidth);
}); });
@ -130,5 +134,6 @@ export function useMenuSetting() {
getIsTopMenu, getIsTopMenu,
getMenuBgColor, getMenuBgColor,
getShowSidebar, getShowSidebar,
getIsMixMode,
}; };
} }

View File

@ -10,6 +10,8 @@ const getShowMultipleTab = computed(() => unref(getMultipleTabSetting).show);
const getShowQuick = computed(() => unref(getMultipleTabSetting).showQuick); const getShowQuick = computed(() => unref(getMultipleTabSetting).showQuick);
const getShowRedo = computed(() => unref(getMultipleTabSetting).showRedo);
function setMultipleTabSetting(multiTabsSetting: Partial<MultiTabsSetting>) { function setMultipleTabSetting(multiTabsSetting: Partial<MultiTabsSetting>) {
appStore.commitProjectConfigState({ multiTabsSetting }); appStore.commitProjectConfigState({ multiTabsSetting });
} }
@ -21,5 +23,6 @@ export function useMultipleTabSetting() {
getMultipleTabSetting, getMultipleTabSetting,
getShowMultipleTab, getShowMultipleTab,
getShowQuick, getShowQuick,
getShowRedo,
}; };
} }

View File

@ -0,0 +1,10 @@
import { useAppProviderContext } from '/@/components/Application';
import { computed, unref } from 'vue';
export function useAppInject() {
const values = useAppProviderContext();
return {
getIsMobile: computed(() => unref(values.isMobile)),
};
}

View File

@ -25,7 +25,7 @@ export function useI18n(namespace?: string) {
return { return {
...methods, ...methods,
t: (key: string, ...arg: Partial<Parameters<typeof t>>) => { t: (key: string, ...arg: any) => {
if (!key) return ''; if (!key) return '';
return t(getKey(key), ...(arg as Parameters<typeof t>)); return t(getKey(key), ...(arg as Parameters<typeof t>));
}, },

View File

@ -33,11 +33,13 @@ export function useGo() {
export const useRedo = () => { export const useRedo = () => {
const { push, currentRoute } = router; const { push, currentRoute } = router;
const { query, params } = currentRoute.value; const { query, params } = currentRoute.value;
function redo() { function redo(): Promise<boolean> {
push({ return new Promise((resolve) => {
path: '/redirect' + unref(currentRoute).fullPath, push({
query, path: '/redirect' + unref(currentRoute).fullPath,
params, query,
params,
}).then(() => resolve(true));
}); });
} }
return redo; return redo;

View File

@ -11,7 +11,11 @@ export function useTabs() {
} }
return { return {
refreshPage: () => canIUseFn() && tabStore.commitRedoPage(), refreshPage: async () => {
if (canIUseFn()) {
await tabStore.commitRedoPage();
}
},
closeAll: () => canIUseFn() && tabStore.closeAllTabAction(), closeAll: () => canIUseFn() && tabStore.closeAllTabAction(),
closeLeft: () => canIUseFn() && tabStore.closeLeftTabAction(tabStore.getCurrentTab), closeLeft: () => canIUseFn() && tabStore.closeLeftTabAction(tabStore.getCurrentTab),
closeRight: () => canIUseFn() && tabStore.closeRightTabAction(tabStore.getCurrentTab), closeRight: () => canIUseFn() && tabStore.closeRightTabAction(tabStore.getCurrentTab),

View File

@ -20,11 +20,10 @@
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting'; import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
import PageLayout from '/@/layouts/page/index'; import PageLayout from '/@/layouts/page/index';
import { Loading } from '/@/components/Loading'; import { Loading } from '/@/components/Loading';
import Transition from '/@/views/demo/comp/lazy/Transition.vue';
export default defineComponent({ export default defineComponent({
name: 'LayoutContent', name: 'LayoutContent',
components: { PageLayout, Loading, Transition }, components: { PageLayout, Loading },
setup() { setup() {
const { prefixCls } = useDesign('layout-content'); const { prefixCls } = useDesign('layout-content');
const { getOpenPageLoading } = useTransitionSetting(); const { getOpenPageLoading } = useTransitionSetting();

View File

@ -3,38 +3,19 @@ import './index.less';
import type { FunctionalComponent } from 'vue'; import type { FunctionalComponent } from 'vue';
import type { Component } from '/@/components/types'; import type { Component } from '/@/components/types';
import { import { defineComponent, unref, computed } from 'vue';
defineComponent,
unref,
computed,
ref,
nextTick,
watchEffect,
// nextTick
} from 'vue';
import { Layout, Tooltip, Badge } from 'ant-design-vue'; import { Layout, Tooltip, Badge } from 'ant-design-vue';
import { AppLogo } from '/@/components/Application'; import { AppLogo } from '/@/components/Application';
import UserDropdown from './UserDropdown';
import LayoutMenu from '../menu'; import LayoutMenu from '../menu';
import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
import LockAction from './actions/LockAction'; import LockAction from './actions/LockAction';
import LayoutTrigger from '../trigger/index.vue'; import LayoutTrigger from '../trigger/index.vue';
import NoticeAction from './notice/NoticeActionItem.vue'; import NoticeAction from './notice/NoticeActionItem.vue';
import { import { LockOutlined, BugOutlined } from '@ant-design/icons-vue';
RedoOutlined,
FullscreenExitOutlined,
FullscreenOutlined,
LockOutlined,
BugOutlined,
} from '@ant-design/icons-vue';
import { AppSearch } from '/@/components/Application'; import { AppSearch } from '/@/components/Application';
import { useModal } from '/@/components/Modal'; 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 { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '/@/hooks/setting/useRootSetting';
@ -49,8 +30,10 @@ import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { AppLocalePicker } from '/@/components/Application'; import { AppLocalePicker } from '/@/components/Application';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
import { propTypes } from '/@/utils/propTypes'; 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 { interface TooltipItemProps {
title: string; title: string;
} }
@ -72,24 +55,14 @@ export default defineComponent({
fixed: propTypes.bool, fixed: propTypes.bool,
}, },
setup(props) { 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 { t } = useI18n();
const { prefixCls } = useDesign('layout-header');
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsHorizontal } = useMenuSetting(); const { getShowTopMenu, getShowHeaderTrigger, getSplit } = useMenuSetting();
const { getShowLocale } = useLocaleSetting(); const { getShowLocale } = useLocaleSetting();
const { getUseErrorHandle, getShowBreadCrumbIcon } = useRootSetting(); const { getUseErrorHandle } = useRootSetting();
const { const {
getHeaderTheme, getHeaderTheme,
getShowRedo,
getUseLockPage, getUseLockPage,
getShowFullScreen, getShowFullScreen,
getShowNotice, getShowNotice,
@ -100,23 +73,11 @@ export default defineComponent({
const { push } = useRouter(); const { push } = useRouter();
const [register, { openModal }] = useModal(); const [register, { openModal }] = useModal();
const { toggleFullscreen, isFullscreenRef } = useFullscreen(); const { getIsMobile } = useAppInject();
useWindowSizeFn(
() => {
calcTopMenuWidth();
},
100,
{ immediate: false }
);
const headerClass = computed(() => { const headerClass = computed(() => {
const theme = unref(getHeaderTheme); const theme = unref(getHeaderTheme);
return theme ? `layout-header__header--${theme}` : ''; return theme ? `${prefixCls}__header--${theme}` : '';
});
const isPc = computed(() => {
return !unref(injectValue.isMobile);
}); });
const getSplitType = computed(() => { const getSplitType = computed(() => {
@ -127,25 +88,6 @@ export default defineComponent({
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null; 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() { function handleToErrorList() {
push(PageEnum.ERROR_LOG_PAGE).then(() => { push(PageEnum.ERROR_LOG_PAGE).then(() => {
errorStore.commitErrorListCountState(0); errorStore.commitErrorListCountState(0);
@ -156,27 +98,28 @@ export default defineComponent({
openModal(true); openModal(true);
} }
function renderHeaderContent() { function renderHeaderLeft() {
const width = unref(logoWidthRef);
return ( return (
<div class="layout-header__content "> <>
{unref(getShowHeaderLogo) && (
<AppLogo class={`layout-header__logo`} ref={logoRef} theme={unref(getHeaderTheme)} />
)}
{unref(getShowContent) && ( {unref(getShowContent) && (
<div class="layout-header__left"> <div class={`${prefixCls}__left`}>
{unref(getShowHeaderTrigger) && ( {unref(getShowHeaderTrigger) && (
<LayoutTrigger theme={unref(getHeaderTheme)} sider={false} /> <LayoutTrigger theme={unref(getHeaderTheme)} sider={false} />
)} )}
{unref(getShowBread) && unref(isPc) && ( {unref(getShowBread) && !unref(getIsMobile) && (
<LayoutBreadcrumb showIcon={unref(getShowBreadCrumbIcon)} /> <LayoutBreadcrumb theme={unref(getHeaderTheme)} />
)} )}
</div> </div>
)} )}
</>
);
}
{unref(getShowTopMenu) && unref(isPc) && ( function renderHeaderContent() {
<div class={[`layout-header__menu `]} style={{ width: `calc(100% - ${width}px)` }}> return (
<div class={`${prefixCls}__content`}>
{unref(getShowTopMenu) && !unref(getIsMobile) && (
<div class={[`${prefixCls}__menu `]}>
{/* <div class={[`layout-header__menu `]}> */} {/* <div class={[`layout-header__menu `]}> */}
<LayoutMenu <LayoutMenu
isHorizontal={true} isHorizontal={true}
@ -193,18 +136,18 @@ export default defineComponent({
function renderActionDefault(Comp: Component | any, event: Fn) { function renderActionDefault(Comp: Component | any, event: Fn) {
return ( return (
<div class="layout-header__action-item" onClick={event}> <div class={`${prefixCls}__action-item`} onClick={event}>
<Comp class="layout-header__action-icon" /> <Comp class={`${prefixCls}__action-icon`} />
</div> </div>
); );
} }
function renderAction() { function renderAction() {
return ( return (
<div class={`layout-header__action`}> <div class={`${prefixCls}__action`}>
{unref(isPc) && <AppSearch class="layout-header__action-item" />} {!unref(getIsMobile) && <AppSearch class={`${prefixCls}__action-item`} />}
{unref(getUseErrorHandle) && unref(isPc) && ( {unref(getUseErrorHandle) && !unref(getIsMobile) && (
<TooltipItem title={t('layout.header.tooltipErrorLog')}> <TooltipItem title={t('layout.header.tooltipErrorLog')}>
{() => ( {() => (
<Badge <Badge
@ -219,48 +162,27 @@ export default defineComponent({
</TooltipItem> </TooltipItem>
)} )}
{unref(getUseLockPage) && unref(isPc) && ( {unref(getUseLockPage) && !unref(getIsMobile) && (
<TooltipItem title={t('layout.header.tooltipLock')}> <TooltipItem title={t('layout.header.tooltipLock')}>
{() => renderActionDefault(LockOutlined, handleLockPage)} {() => renderActionDefault(LockOutlined, handleLockPage)}
</TooltipItem> </TooltipItem>
)} )}
{unref(getShowNotice) && unref(isPc) && ( {unref(getShowNotice) && !unref(getIsMobile) && (
<TooltipItem title={t('layout.header.tooltipNotify')}> <TooltipItem title={t('layout.header.tooltipNotify')}>
{() => <NoticeAction />} {() => <NoticeAction />}
</TooltipItem> </TooltipItem>
)} )}
{unref(getShowRedo) && unref(isPc) && ( {unref(getShowFullScreen) && !unref(getIsMobile) && <FullScreen />}
<TooltipItem title={t('layout.header.tooltipRedo')}>
{() => renderActionDefault(RedoOutlined, refreshPage)} <UserDropDown theme={unref(getHeaderTheme)} />
</TooltipItem>
)}
{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) && ( {unref(getShowLocale) && (
<AppLocalePicker <AppLocalePicker
reload={true} reload={true}
showText={false} showText={false}
class="layout-header__action-item locale" class={`${prefixCls}__action-item locale`}
/> />
)} )}
</div> </div>
@ -270,6 +192,10 @@ export default defineComponent({
function renderHeaderDefault() { function renderHeaderDefault() {
return ( return (
<> <>
{unref(getShowHeaderLogo) && (
<AppLogo class={`${prefixCls}__logo`} theme={unref(getHeaderTheme)} />
)}
{renderHeaderLeft()}
{renderHeaderContent()} {renderHeaderContent()}
{renderAction()} {renderAction()}
<LockAction onRegister={register} /> <LockAction onRegister={register} />
@ -279,9 +205,7 @@ export default defineComponent({
return () => { return () => {
return ( return (
<Layout.Header <Layout.Header class={[prefixCls, unref(headerClass), { fixed: props.fixed }]}>
class={['layout-header', 'flex p-0 px-4 ', unref(headerClass), { fixed: props.fixed }]}
>
{() => renderHeaderDefault()} {() => renderHeaderDefault()}
</Layout.Header> </Layout.Header>
); );

View File

@ -1,3 +1,5 @@
@import (reference) '../../../design/index.less';
.multiple-tab-header { .multiple-tab-header {
margin-left: 1px; margin-left: 1px;
transition: width 0.2s; transition: width 0.2s;
@ -10,7 +12,7 @@
&.fixed { &.fixed {
position: fixed; position: fixed;
top: 0; top: 0;
z-index: 100; z-index: @multiple-tab-fixed-z-index;
width: 100%; width: 100%;
} }
} }

View File

@ -2,7 +2,7 @@ import './LayoutMultipleHeader.less';
import { defineComponent, unref, computed, ref, watch, nextTick, CSSProperties } from 'vue'; 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 MultipleTabs from '../tabs/index.vue';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
@ -10,6 +10,7 @@ import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useFullContent } from '/@/hooks/web/useFullContent'; import { useFullContent } from '/@/hooks/web/useFullContent';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting'; import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
import { useLayoutContext } from '../useLayoutContext'; import { useLayoutContext } from '../useLayoutContext';
import { useAppInject } from '/@/hooks/web/useAppInject';
export default defineComponent({ export default defineComponent({
name: 'LayoutMultipleHeader', name: 'LayoutMultipleHeader',
@ -21,8 +22,8 @@ export default defineComponent({
const injectValue = useLayoutContext(); const injectValue = useLayoutContext();
const { getCalcContentWidth } = useMenuSetting(); const { getCalcContentWidth, getSplit } = useMenuSetting();
const { getIsMobile } = useAppInject();
const { const {
getFixed, getFixed,
getShowInsetHeaderRef, getShowInsetHeaderRef,
@ -36,7 +37,7 @@ export default defineComponent({
const { getShowMultipleTab } = useMultipleTabSetting(); const { getShowMultipleTab } = useMultipleTabSetting();
const showTabsRef = computed(() => { const getShowTabs = computed(() => {
return unref(getShowMultipleTab) && !unref(getFullContent); return unref(getShowMultipleTab) && !unref(getFullContent);
}); });
@ -56,7 +57,7 @@ export default defineComponent({
(): CSSProperties => { (): CSSProperties => {
const style: CSSProperties = {}; const style: CSSProperties = {};
if (unref(getFixed)) { if (unref(getFixed)) {
style.width = unref(injectValue.isMobile) ? '100%' : unref(getCalcContentWidth); style.width = unref(getIsMobile) ? '100%' : unref(getCalcContentWidth);
} }
if (unref(getShowFullHeaderRef)) { if (unref(getShowFullHeaderRef)) {
style.top = `${unref(fullHeaderHeightRef)}px`; style.top = `${unref(fullHeaderHeightRef)}px`;
@ -84,7 +85,7 @@ export default defineComponent({
const fullHeaderEl = unref(injectValue.fullHeader)?.$el; const fullHeaderEl = unref(injectValue.fullHeader)?.$el;
let height = 0; let height = 0;
if (headerEl && !unref(getShowFullHeaderRef)) { if (headerEl && !unref(getShowFullHeaderRef) && !unref(getSplit)) {
height += headerEl.offsetHeight; height += headerEl.offsetHeight;
} }
@ -97,6 +98,7 @@ export default defineComponent({
height += fullHeaderHeight; height += fullHeaderHeight;
fullHeaderHeightRef.value = fullHeaderHeight; fullHeaderHeightRef.value = fullHeaderHeight;
} }
placeholderHeightRef.value = height; placeholderHeightRef.value = height;
}); });
}, },
@ -114,7 +116,7 @@ export default defineComponent({
class={['multiple-tab-header', unref(getHeaderTheme), { fixed: unref(getIsFixed) }]} class={['multiple-tab-header', unref(getHeaderTheme), { fixed: unref(getIsFixed) }]}
> >
{unref(getShowInsetHeaderRef) && <LayoutHeader ref={headerElRef} />} {unref(getShowInsetHeaderRef) && <LayoutHeader ref={headerElRef} />}
{unref(showTabsRef) && <MultipleTabs ref={tabElRef} />} {unref(getShowTabs) && <MultipleTabs ref={tabElRef} />}
</div> </div>
</> </>
); );

View File

@ -1,125 +0,0 @@
// 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>
);
};
},
});

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="layout-breadcrumb"> <div :class="[prefixCls, `${prefixCls}--${theme}`]">
<a-breadcrumb :routes="routes"> <a-breadcrumb :routes="routes">
<template #itemRender="{ route, 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"> <span v-if="routes.indexOf(route) === routes.length - 1">
{{ t(route.meta.title) }} {{ t(route.meta.title) }}
</span> </span>
@ -14,7 +14,6 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { PropType } from 'vue';
import { defineComponent, ref, toRaw, watchEffect } from 'vue'; import { defineComponent, ref, toRaw, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -26,18 +25,23 @@
import { HomeOutlined } from '@ant-design/icons-vue'; import { HomeOutlined } from '@ant-design/icons-vue';
import { PageEnum } from '/@/enums/pageEnum'; 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({ export default defineComponent({
name: 'LayoutBreadcrumb', name: 'LayoutBreadcrumb',
components: { HomeOutlined, Icon }, components: { HomeOutlined, Icon },
props: { props: {
showIcon: { theme: propTypes.oneOf(['dark', 'light']),
type: Boolean as PropType<boolean>,
default: false,
},
}, },
setup() { setup() {
const routes = ref<RouteLocationMatched[]>([]); const routes = ref<RouteLocationMatched[]>([]);
const { currentRoute } = useRouter(); const { currentRoute } = useRouter();
const { prefixCls } = useDesign('layout-breadcrumb');
const { getShowBreadCrumbIcon } = useRootSetting();
const { t } = useI18n(); const { t } = useI18n();
watchEffect(() => { watchEffect(() => {
@ -63,17 +67,71 @@
); );
if (filterBreadcrumbList.length === breadcrumbList.length) { if (filterBreadcrumbList.length === breadcrumbList.length) {
filterBreadcrumbList.unshift({ filterBreadcrumbList.unshift(({
path: PageEnum.BASE_HOME, path: PageEnum.BASE_HOME,
meta: { meta: {
title: t('layout.header.home'), 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> </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>

View File

@ -0,0 +1,47 @@
<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>

View File

@ -0,0 +1,36 @@
<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>

View File

@ -0,0 +1,15 @@
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'));

View File

@ -0,0 +1,116 @@
<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>

View File

@ -0,0 +1,38 @@
<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>

View File

@ -1,5 +1,5 @@
<template> <template>
<a-list class="list"> <a-list :class="prefixCls">
<template v-for="item in list" :key="item.id"> <template v-for="item in list" :key="item.id">
<a-list-item class="list-item"> <a-list-item class="list-item">
<a-list-item-meta> <a-list-item-meta>
@ -33,6 +33,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from 'vue'; import { defineComponent, PropType } from 'vue';
import { ListItem } from './data'; import { ListItem } from './data';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({ export default defineComponent({
props: { props: {
@ -41,10 +42,17 @@
default: () => [], default: () => [],
}, },
}, },
setup() {
const { prefixCls } = useDesign('header-notify-list');
return { prefixCls };
},
}); });
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.list { @import (reference) '../../../../../design/index.less';
@prefix-cls: ~'@{namespace}-header-notify-list';
.@{prefix-cls} {
&::-webkit-scrollbar { &::-webkit-scrollbar {
display: none; display: none;
} }

View File

@ -1,8 +1,8 @@
<template> <template>
<div class="layout-header__action-item notify-action"> <div :class="prefixCls">
<Popover title="" trigger="click" overlayClassName="layout-header__notify-action"> <Popover title="" trigger="click" :overlayClassName="`${prefixCls}__overlay`">
<Badge :count="count" dot :numberStyle="numberStyle"> <Badge :count="count" dot :numberStyle="numberStyle">
<BellOutlined class="layout-header__action-icon" /> <BellOutlined />
</Badge> </Badge>
<template #content> <template #content>
<Tabs> <Tabs>
@ -26,10 +26,13 @@
import { BellOutlined } from '@ant-design/icons-vue'; import { BellOutlined } from '@ant-design/icons-vue';
import { tabListData } from './data'; import { tabListData } from './data';
import NoticeList from './NoticeList.vue'; import NoticeList from './NoticeList.vue';
import { useDesign } from '/@/hooks/web/useDesign';
export default defineComponent({ export default defineComponent({
components: { Popover, BellOutlined, Tabs, TabPane: Tabs.TabPane, Badge, NoticeList }, components: { Popover, BellOutlined, Tabs, TabPane: Tabs.TabPane, Badge, NoticeList },
setup() { setup() {
const { prefixCls } = useDesign('header-notify');
let count = 0; let count = 0;
for (let i = 0; i < tabListData.length; i++) { for (let i = 0; i < tabListData.length; i++) {
@ -37,6 +40,7 @@
} }
return { return {
prefixCls,
tabListData, tabListData,
count, count,
numberStyle: {}, numberStyle: {},
@ -45,13 +49,16 @@
}); });
</script> </script>
<style lang="less"> <style lang="less">
.layout-header__notify-action { @import (reference) '../../../../../design/index.less';
max-width: 360px; @prefix-cls: ~'@{namespace}-header-notify';
}
.notify-action { .@{prefix-cls} {
padding-top: 2px; padding-top: 2px;
&__overlay {
max-width: 360px;
}
.ant-tabs-content { .ant-tabs-content {
width: 300px; width: 300px;
} }

View File

@ -0,0 +1,27 @@
<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>

View File

@ -0,0 +1,156 @@
<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>

View File

@ -1,10 +1,12 @@
@import (reference) '../../../design/index.less'; @import (reference) '../../../design/index.less';
@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger'; @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; display: flex;
height: @header-height; height: @header-height;
padding: 0 20px 0 0; padding: 0;
margin-left: -1px; margin-left: -1px;
line-height: @header-height; line-height: @header-height;
color: @white; color: @white;
@ -12,15 +14,28 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
&.fixed { &--fixed {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 1000; z-index: @layout-header-fixed-z-index;
width: 100%; 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; display: flex;
height: 100%; height: 100%;
align-items: center; align-items: center;
@ -33,7 +48,7 @@
align-items: center; align-items: center;
.anticon { .anticon {
font-size: 17px; font-size: 16px;
} }
&.light { &.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 { &-menu {
display: flex;
height: 100%; height: 100%;
flex-grow: 1; min-width: 0;
flex: 1;
align-items: center; align-items: center;
} }
&__header--light { &-action {
background: @white; display: flex;
border-bottom: 1px solid @header-light-bottom-border-color; min-width: 200px;
padding-right: 12px;
align-items: center;
.layout-header__menu { &__item {
height: calc(@header-height - 1px); 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; height: @header-height;
line-height: @header-height; line-height: @header-height;
} }
}
.layout-breadcrumb { .ant-badge-dot {
.ant-breadcrumb-link { top: 10px;
color: @breadcrumb-item-normal-color; right: 2px;
a {
color: @text-color-base;
&:hover {
color: @primary-color;
}
}
}
.ant-breadcrumb-separator {
color: @breadcrumb-item-normal-color;
} }
} }
.layout-header__logo { span[role='img'] {
height: @header-height; padding: 0 8px;
}
}
&--light {
background: @white;
border-bottom: 1px solid @header-light-bottom-border-color;
.@{header-prefix-cls}-logo {
color: @text-color-base; color: @text-color-base;
img {
width: @logo-width;
height: @logo-width;
margin-right: 6px;
}
&:hover { &:hover {
background: @header-light-bg-hover-color; background: @header-light-bg-hover-color;
} }
} }
.layout-header__action { .@{header-prefix-cls}-action {
&-item { &__item {
&:hover { &:hover {
background: @header-light-bg-hover-color; background: @header-light-bg-hover-color;
} }
&.locale { .@{locale-prefix-cls} {
padding: 0 10px; padding: 0 6px;
color: rgba(0, 0, 0, 0.65); color: rgba(0, 0, 0, 0.65);
} }
} }
@ -137,134 +135,23 @@
color: @text-color-base; 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; background: @header-dark-bg-color;
.layout-header__action { .@{header-prefix-cls}-logo {
&-item { &:hover {
background: @header-dark-bg-hover-color;
}
}
.@{header-prefix-cls}-action {
&__item {
&:hover { &:hover {
background: @header-dark-bg-hover-color; 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 {
&: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;
}
}
}
.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;
} }
} }

View File

@ -0,0 +1,161 @@
<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>

View File

@ -18,7 +18,7 @@
import { Layout } from 'ant-design-vue'; import { Layout } from 'ant-design-vue';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import LayoutHeader from './header/LayoutHeader'; import LayoutHeader from './header/index.vue';
import LayoutContent from './content/index.vue'; import LayoutContent from './content/index.vue';
import LayoutSideBar from './sider'; import LayoutSideBar from './sider';
import LayoutMultipleHeader from './header/LayoutMultipleHeader'; import LayoutMultipleHeader from './header/LayoutMultipleHeader';
@ -29,8 +29,6 @@
import { createLayoutContext } from './useLayoutContext'; import { createLayoutContext } from './useLayoutContext';
import { registerGlobComp } from '/@/components/registerGlobComp'; import { registerGlobComp } from '/@/components/registerGlobComp';
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
import { isMobile } from '/@/utils/is';
export default defineComponent({ export default defineComponent({
name: 'DefaultLayout', name: 'DefaultLayout',
@ -44,22 +42,17 @@
Layout, Layout,
}, },
setup() { 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 // ! Only register global components here
// ! Can reduce the size of the first screen code // ! Can reduce the size of the first screen code
// default layout It is loaded after login. So it wont be packaged to the first screen // default layout It is loaded after login. So it wont be packaged to the first screen
registerGlobComp(); registerGlobComp();
const headerRef = ref<ComponentRef>(null);
const { prefixCls } = useDesign('default-layout');
createLayoutContext({ fullHeader: headerRef });
const { getShowFullHeaderRef } = useHeaderSetting(); const { getShowFullHeaderRef } = useHeaderSetting();
const { getShowSidebar } = useMenuSetting(); const { getShowSidebar } = useMenuSetting();

View File

@ -9,6 +9,7 @@ import { AppLogo } from '/@/components/Application';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { ScrollContainer } from '/@/components/Container';
import { useGo } from '/@/hooks/web/usePage'; import { useGo } from '/@/hooks/web/usePage';
import { useSplitMenu } from './useLayoutMenu'; import { useSplitMenu } from './useLayoutMenu';
@ -16,6 +17,7 @@ import { openWindow } from '/@/utils';
import { propTypes } from '/@/utils/propTypes'; import { propTypes } from '/@/utils/propTypes';
import { isUrl } from '/@/utils/is'; import { isUrl } from '/@/utils/is';
import { useRootSetting } from '/@/hooks/setting/useRootSetting'; import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { CSSProperties } from 'vue';
export default defineComponent({ export default defineComponent({
name: 'LayoutMenu', name: 'LayoutMenu',
@ -53,12 +55,25 @@ export default defineComponent({
const getComputedMenuMode = computed(() => props.menuMode || unref(getMenuMode)); const getComputedMenuMode = computed(() => props.menuMode || unref(getMenuMode));
const getComputedMenuTheme = computed(() => props.theme || unref(getMenuTheme)); 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 * click menu
* @param menu * @param menu
*/ */
function handleMenuClick(path: string) { function handleMenuClick(path: string) {
go(path); go(path);
} }
@ -76,7 +91,7 @@ export default defineComponent({
} }
function renderHeader() { function renderHeader() {
if (!unref(showLogo)) return null; if (!unref(getIsShowLogo)) return null;
return ( return (
<AppLogo <AppLogo
@ -87,7 +102,7 @@ export default defineComponent({
); );
} }
return () => { function renderMenu() {
return ( return (
<BasicMenu <BasicMenu
beforeClickFn={beforeMenuClickFn} beforeClickFn={beforeMenuClickFn}
@ -99,13 +114,22 @@ export default defineComponent({
items={unref(menusRef)} items={unref(menusRef)}
accordion={unref(getAccordion)} accordion={unref(getAccordion)}
onMenuClick={handleMenuClick} onMenuClick={handleMenuClick}
appendClass={unref(appendClass)} showLogo={unref(getIsShowLogo)}
showLogo={unref(showLogo)} />
> );
{{ }
header: () => renderHeader(),
}} return () => {
</BasicMenu> return (
<>
{renderHeader()}
{unref(getUseScroll) ? (
<ScrollContainer style={unref(getWrapperStyle)}>{() => renderMenu()}</ScrollContainer>
) : (
renderMenu()
)}
;
</>
); );
}; };
}, },

View File

@ -14,9 +14,7 @@ import { permissionStore } from '/@/store/modules/permission';
export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) { export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
// Menu array // Menu array
const menusRef = ref<Menu[]>([]); const menusRef = ref<Menu[]>([]);
const { currentRoute } = useRouter(); const { currentRoute } = useRouter();
const { setMenuSetting, getIsHorizontal, getSplit } = useMenuSetting(); const { setMenuSetting, getIsHorizontal, getSplit } = useMenuSetting();
const [throttleHandleSplitLeftMenu] = useThrottle(handleSplitLeftMenu, 50); const [throttleHandleSplitLeftMenu] = useThrottle(handleSplitLeftMenu, 50);
@ -25,9 +23,11 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
() => unref(splitType) !== MenuSplitTyeEnum.LEFT && !unref(getIsHorizontal) () => 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(() => { const normalType = computed(() => {
return unref(splitType) === MenuSplitTyeEnum.NONE || !unref(getSplit); return unref(splitType) === MenuSplitTyeEnum.NONE || !unref(getSplit);
@ -65,7 +65,7 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
// Handle left menu split // Handle left menu split
async function handleSplitLeftMenu(parentPath: string) { async function handleSplitLeftMenu(parentPath: string) {
if (unref(splitLeft)) return; if (unref(getSplitLeft)) return;
// spilt mode left // spilt mode left
const children = await getChildrenMenus(parentPath); const children = await getChildrenMenus(parentPath);
@ -88,7 +88,7 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
} }
// split-top // split-top
if (unref(spiltTop)) { if (unref(getSpiltTop)) {
const shallowMenus = await getShallowMenus(); const shallowMenus = await getShallowMenus();
menusRef.value = shallowMenus; menusRef.value = shallowMenus;

View File

@ -80,7 +80,7 @@ export default defineComponent({
getShowSearch, getShowSearch,
} = useHeaderSetting(); } = useHeaderSetting();
const { getShowMultipleTab, getShowQuick } = useMultipleTabSetting(); const { getShowMultipleTab, getShowQuick, getShowRedo } = useMultipleTabSetting();
const getShowMenuRef = computed(() => { const getShowMenuRef = computed(() => {
return unref(getShowMenu) && !unref(getIsHorizontal); return unref(getShowMenu) && !unref(getIsHorizontal);
@ -246,6 +246,13 @@ export default defineComponent({
def={unref(getShowMultipleTab)} def={unref(getShowMultipleTab)}
/> />
<SwitchItem
title={t('layout.setting.tabsRedoBtn')}
event={HandlerEnum.TABS_SHOW_REDO}
def={unref(getShowRedo)}
disabled={!unref(getShowMultipleTab)}
/>
<SwitchItem <SwitchItem
title={t('layout.setting.tabsQuickBtn')} title={t('layout.setting.tabsQuickBtn')}
event={HandlerEnum.TABS_SHOW_QUICK} event={HandlerEnum.TABS_SHOW_QUICK}

View File

@ -31,6 +31,7 @@ export enum HandlerEnum {
HEADER_SEARCH, HEADER_SEARCH,
TABS_SHOW_QUICK, TABS_SHOW_QUICK,
TABS_SHOW_REDO,
TABS_SHOW, TABS_SHOW,
LOCK_TIME, LOCK_TIME,

View File

@ -113,6 +113,8 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
case HandlerEnum.TABS_SHOW: case HandlerEnum.TABS_SHOW:
return { multiTabsSetting: { show: value } }; return { multiTabsSetting: { show: value } };
case HandlerEnum.TABS_SHOW_REDO:
return { multiTabsSetting: { showRedo: value } };
// ============header================== // ============header==================
case HandlerEnum.HEADER_THEME: case HandlerEnum.HEADER_THEME:

View File

@ -1,15 +1,21 @@
@import (reference) '../../../design/index.less'; @import (reference) '../../../design/index.less';
@prefix-cls: ~'@{namespace}-layout-sideBar';
.layout-sidebar { .@{prefix-cls} {
// overflow: hidden; z-index: @layout-sider-fixed-z-index;
&.fixed { &--fixed {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
height: 100%; height: 100%;
} }
&--mix {
top: @header-height;
height: calc(100% - @header-height);
}
&.ant-layout-sider-dark { &.ant-layout-sider-dark {
background: @sider-dark-bg-color; background: @sider-dark-bg-color;

View File

@ -1,6 +1,6 @@
import './index.less'; 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 { Layout } from 'ant-design-vue';
import LayoutMenu from '../menu'; import LayoutMenu from '../menu';
@ -8,14 +8,13 @@ import LayoutMenu from '../menu';
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useTrigger, useDragLine, useSiderEvent } from './useLayoutSider'; 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({ export default defineComponent({
name: 'LayoutSideBar', name: 'LayoutSideBar',
setup() { setup() {
const topRef = ref(0);
const dragBarRef = ref<ElRef>(null); const dragBarRef = ref<ElRef>(null);
const sideRef = ref<ElRef>(null); const sideRef = ref<ElRef>(null);
@ -27,22 +26,18 @@ export default defineComponent({
getRealWidth, getRealWidth,
getMenuHidden, getMenuHidden,
getMenuFixed, getMenuFixed,
getIsMixMode,
} = useMenuSetting(); } = useMenuSetting();
const { getShowFullHeaderRef, getUnFixedAndFull } = useHeaderSetting(); const { prefixCls } = useDesign('layout-sideBar');
const injectValue = useLayoutContext();
const { getTriggerAttr, getTriggerSlot } = useTrigger(); const { getTriggerAttr, getTriggerSlot } = useTrigger();
const { getIsMobile } = useAppInject();
const { renderDragLine } = useDragLine(sideRef, dragBarRef); const { renderDragLine } = useDragLine(sideRef, dragBarRef);
const { const { getCollapsedWidth, onBreakpointChange, onCollapseChange } = useSiderEvent();
getCollapsedWidth,
onBreakpointChange,
onCollapseChange,
onSiderClick,
} = useSiderEvent();
const getMode = computed(() => { const getMode = computed(() => {
return unref(getSplit) ? MenuModeEnum.INLINE : null; return unref(getSplit) ? MenuModeEnum.INLINE : null;
@ -57,40 +52,16 @@ export default defineComponent({
}); });
const getSiderClass = computed(() => { const getSiderClass = computed(() => {
return { return [
'layout-sidebar': true, prefixCls,
fixed: unref(getMenuFixed), {
hidden: !unref(showClassSideBarRef), [`${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( const getHiddenDomStyle = computed(
(): CSSProperties => { (): CSSProperties => {
const width = `${unref(getRealWidth)}px`; const width = `${unref(getRealWidth)}px`;
@ -121,7 +92,7 @@ export default defineComponent({
return () => { return () => {
return ( return (
<> <>
{unref(getMenuFixed) && !unref(injectValue.isMobile) && ( {unref(getMenuFixed) && !unref(getIsMobile) && (
<div style={unref(getHiddenDomStyle)} class={{ hidden: !unref(showClassSideBarRef) }} /> <div style={unref(getHiddenDomStyle)} class={{ hidden: !unref(showClassSideBarRef) }} />
)} )}
<Layout.Sider <Layout.Sider
@ -129,12 +100,10 @@ export default defineComponent({
breakpoint="lg" breakpoint="lg"
collapsible collapsible
class={unref(getSiderClass)} class={unref(getSiderClass)}
style={unref(getSiderStyle)}
width={unref(getMenuWidth)} width={unref(getMenuWidth)}
collapsed={unref(getCollapsed)} collapsed={unref(getCollapsed)}
collapsedWidth={unref(getCollapsedWidth)} collapsedWidth={unref(getCollapsedWidth)}
theme={unref(getMenuTheme)} theme={unref(getMenuTheme)}
onClick={onSiderClick}
onCollapse={onCollapseChange} onCollapse={onCollapseChange}
onBreakpoint={onBreakpointChange} onBreakpoint={onBreakpointChange}
{...unref(getTriggerAttr)} {...unref(getTriggerAttr)}

View File

@ -16,7 +16,7 @@ export function useSiderEvent() {
const brokenRef = ref(false); const brokenRef = ref(false);
const collapseRef = ref(true); const collapseRef = ref(true);
const { setMenuSetting, getCollapsed, getMiniWidthNumber, getShowMenu } = useMenuSetting(); const { setMenuSetting, getCollapsed, getMiniWidthNumber } = useMenuSetting();
const getCollapsedWidth = computed(() => { const getCollapsedWidth = computed(() => {
return unref(brokenRef) ? 0 : unref(getMiniWidthNumber); return unref(brokenRef) ? 0 : unref(getMiniWidthNumber);
@ -36,12 +36,7 @@ export function useSiderEvent() {
brokenRef.value = broken; brokenRef.value = broken;
} }
function onSiderClick(e: ChangeEvent) { return { getCollapsedWidth, onCollapseChange, onBreakpointChange };
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 };
} }
/** /**

View File

@ -4,7 +4,7 @@
<span class="ml-1">{{ getTitle }}</span> <span class="ml-1">{{ getTitle }}</span>
</div> </div>
<span :class="`${prefixCls}__extra`" v-else> <span :class="`${prefixCls}__extra-quick`" v-else @click="handleContext">
<RightOutlined /> <RightOutlined />
</span> </span>
</Dropdown> </Dropdown>

View File

@ -0,0 +1,37 @@
<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>

View File

@ -34,30 +34,30 @@
border: 1px solid darken(@border-color-light, 6%); border: 1px solid darken(@border-color-light, 6%);
transition: none; transition: none;
&:not(.ant-tabs-tab-active)::after { // &:not(.ant-tabs-tab-active)::before {
position: absolute; // position: absolute;
bottom: -1px; // top: -1px;
left: 50%; // left: 50%;
width: 100%; // width: 100%;
height: 2px; // height: 2px;
background-color: @primary-color; // background-color: @primary-color;
content: ''; // content: '';
opacity: 0; // opacity: 0;
transform: translate(-50%, 0) scaleX(0); // transform: translate(-50%, 0) scaleX(0);
transform-origin: center; // transform-origin: center;
transition: none; // transition: none;
} // }
&:hover { &:hover {
.ant-tabs-close-x { .ant-tabs-close-x {
opacity: 1; opacity: 1;
} }
&:not(.ant-tabs-tab-active)::after { // &:not(.ant-tabs-tab-active)::before {
opacity: 1; // opacity: 1;
transform: translate(-50%, 0) scaleX(1); // transform: translate(-50%, 0) scaleX(1);
transition: all 0.3s ease-in-out; // transition: all 0.3s ease-in-out;
} // }
} }
.ant-tabs-close-x { .ant-tabs-close-x {
@ -152,12 +152,13 @@
} }
&-content { &-content {
&__extra { &__extra-quick,
&__extra-redo {
display: inline-block; display: inline-block;
width: @multiple-height; width: 36px;
height: @multiple-height; height: @multiple-height;
line-height: @multiple-height; line-height: @multiple-height;
color: #999; color: #666;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
border-left: 1px solid #eee; border-left: 1px solid #eee;
@ -171,6 +172,12 @@
} }
} }
&__extra-redo {
span[role='img'] {
transform: rotate(0deg);
}
}
&__info { &__info {
display: inline-block; display: inline-block;
width: 100%; width: 100%;

View File

@ -17,14 +17,16 @@
</template> </template>
</TabPane> </TabPane>
</template> </template>
<template #tabBarExtraContent>
<QuickButton /> <template #tabBarExtraContent v-if="getShowRedo || getShowQuick">
<TabRedo v-if="getShowRedo" />
<QuickButton v-if="getShowQuick" />
</template> </template>
</Tabs> </Tabs>
</div> </div>
</template> </template>
<script lang="ts"> <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 { Tabs } from 'ant-design-vue';
import TabContent from './components/TabContent.vue'; import TabContent from './components/TabContent.vue';
@ -38,61 +40,52 @@
import { REDIRECT_NAME } from '/@/router/constant'; import { REDIRECT_NAME } from '/@/router/constant';
import { useDesign } from '/@/hooks/web/useDesign'; import { useDesign } from '/@/hooks/web/useDesign';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
export default defineComponent({ export default defineComponent({
name: 'MultipleTabs', name: 'MultipleTabs',
components: { components: {
QuickButton: createAsyncComponent(() => import('./components/QuickButton.vue')), QuickButton: createAsyncComponent(() => import('./components/QuickButton.vue')),
TabRedo: createAsyncComponent(() => import('./components/TabRedo.vue')),
Tabs, Tabs,
TabPane: Tabs.TabPane, TabPane: Tabs.TabPane,
TabContent, TabContent,
}, },
setup() { setup() {
const affixTextList = initAffixTabs(); const affixTextList = initAffixTabs();
const activeKeyRef = ref(''); const activeKeyRef = ref('');
useTabsDrag(affixTextList); useTabsDrag(affixTextList);
const { prefixCls } = useDesign('multiple-tabs'); const { prefixCls } = useDesign('multiple-tabs');
const go = useGo(); const go = useGo();
const { getShowQuick, getShowRedo } = useMultipleTabSetting();
const getTabsState = computed(() => tabStore.getTabsState); const getTabsState = computed(() => tabStore.getTabsState);
const unClose = computed(() => { const unClose = computed(() => unref(getTabsState).length === 1);
return getTabsState.value.length === 1;
});
const getWrapClass = computed(() => { const getWrapClass = computed(() => {
return [ return [
prefixCls, prefixCls,
{ {
[`${prefixCls}--hide-close`]: unClose, [`${prefixCls}--hide-close`]: unref(unClose),
}, },
]; ];
}); });
watch( listenerLastChangeTab((route) => {
() => tabStore.getLastChangeRouteState?.path, const { name } = route;
() => { if (name === REDIRECT_NAME || !route || !userStore.getTokenState) return;
if (tabStore.getLastChangeRouteState?.name === REDIRECT_NAME) {
return;
}
const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
if (!lastChangeRoute || !userStore.getTokenState) return;
const { path, fullPath } = lastChangeRoute; const { path, fullPath } = route;
const p = fullPath || path; const p = fullPath || path;
if (activeKeyRef.value !== p) { if (activeKeyRef.value !== p) {
activeKeyRef.value = p; activeKeyRef.value = p;
}
tabStore.addTabAction(lastChangeRoute);
},
{
immediate: true,
} }
); tabStore.addTabAction(unref(route));
});
function handleChange(activeKey: any) { function handleChange(activeKey: any) {
activeKeyRef.value = activeKey; activeKeyRef.value = activeKey;
@ -114,6 +107,8 @@
handleChange, handleChange,
activeKeyRef, activeKeyRef,
getTabsState, getTabsState,
getShowQuick,
getShowRedo,
}; };
}, },
}); });

View File

@ -10,7 +10,6 @@ import { useTabs } from '/@/hooks/web/useTabs';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting'; import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
const { t } = useI18n(); const { t } = useI18n();
@ -24,11 +23,8 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
const { getShowMenu, setMenuSetting } = useMenuSetting(); const { getShowMenu, setMenuSetting } = useMenuSetting();
const { getShowHeader, setHeaderSetting } = useHeaderSetting(); const { getShowHeader, setHeaderSetting } = useHeaderSetting();
const { getShowQuick } = useMultipleTabSetting();
const isTabs = computed(() => const isTabs = computed(() => tabContentProps.type === TabContentEnum.TAB_TYPE);
!unref(getShowQuick) ? true : tabContentProps.type === TabContentEnum.TAB_TYPE
);
const getCurrentTab = computed( const getCurrentTab = computed(
(): RouteLocationNormalized => { (): RouteLocationNormalized => {

View File

@ -3,7 +3,6 @@ import { createContext, useContext } from '/@/hooks/core/useContext';
export interface LayoutContextProps { export interface LayoutContextProps {
fullHeader: Ref<ComponentRef>; fullHeader: Ref<ComponentRef>;
isMobile: Ref<boolean>;
} }
const key: InjectionKey<LayoutContextProps> = Symbol(); const key: InjectionKey<LayoutContextProps> = Symbol();

View File

@ -7,7 +7,7 @@ export default {
tooltipErrorLog: 'Error log', tooltipErrorLog: 'Error log',
tooltipLock: 'Lock screen', tooltipLock: 'Lock screen',
tooltipNotify: 'Notification', tooltipNotify: 'Notification',
tooltipRedo: 'Refresh',
tooltipEntryFull: 'Full Screen', tooltipEntryFull: 'Full Screen',
tooltipExitFull: 'Exit Full Screen', tooltipExitFull: 'Exit Full Screen',

View File

@ -7,4 +7,5 @@ export default {
closeAll: 'Close All', closeAll: 'Close All',
putAway: 'PutAway', putAway: 'PutAway',
unfold: 'Unfold', unfold: 'Unfold',
tooltipRedo: 'Refresh',
}; };

View File

@ -53,6 +53,7 @@ export default {
breadcrumbIcon: 'Breadcrumbs Icon', breadcrumbIcon: 'Breadcrumbs Icon',
tabs: 'Tabs', tabs: 'Tabs',
tabsQuickBtn: 'Tabs quick button', tabsQuickBtn: 'Tabs quick button',
tabsRedoBtn: 'Tabs redo button',
sidebar: 'Sidebar', sidebar: 'Sidebar',
header: 'Header', header: 'Header',
footer: 'Footer', footer: 'Footer',

View File

@ -8,7 +8,7 @@ export default {
tooltipErrorLog: '错误日志', tooltipErrorLog: '错误日志',
tooltipLock: '锁定屏幕', tooltipLock: '锁定屏幕',
tooltipNotify: '消息通知', tooltipNotify: '消息通知',
tooltipRedo: '刷新',
tooltipEntryFull: '全屏', tooltipEntryFull: '全屏',
tooltipExitFull: '退出全屏', tooltipExitFull: '退出全屏',

View File

@ -7,4 +7,5 @@ export default {
closeAll: '关闭全部', closeAll: '关闭全部',
putAway: '收起', putAway: '收起',
unfold: '展开', unfold: '展开',
tooltipRedo: '刷新',
}; };

View File

@ -52,6 +52,7 @@ export default {
breadcrumbIcon: '面包屑图标', breadcrumbIcon: '面包屑图标',
tabs: '标签页', tabs: '标签页',
tabsQuickBtn: '标签页快捷按钮', tabsQuickBtn: '标签页快捷按钮',
tabsRedoBtn: '标签页刷新按钮',
sidebar: '左侧菜单', sidebar: '左侧菜单',
header: '顶栏', header: '顶栏',
footer: '页脚', footer: '页脚',

View File

@ -0,0 +1,28 @@
/**
* 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);
}
}

View File

@ -8,13 +8,12 @@ import { createPageLoadingGuard } from './pageLoadingGuard';
import { useGlobSetting, useProjectSetting } from '/@/hooks/setting'; import { useGlobSetting, useProjectSetting } from '/@/hooks/setting';
import { getRoute } from '/@/router/helper/routeHelper';
import { setTitle } from '/@/utils/browser'; import { setTitle } from '/@/utils/browser';
import { AxiosCanceler } from '/@/utils/http/axios/axiosCancel'; import { AxiosCanceler } from '/@/utils/http/axios/axiosCancel';
import { tabStore } from '/@/store/modules/tab';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
import { REDIRECT_NAME } from '/@/router/constant'; import { REDIRECT_NAME } from '/@/router/constant';
import { setLastChangeTab } from '/@/logics/mitt/tabChange';
const { closeMessageOnSwitch, removeAllHttpPending } = useProjectSetting(); const { closeMessageOnSwitch, removeAllHttpPending } = useProjectSetting();
const globSetting = useGlobSetting(); const globSetting = useGlobSetting();
@ -35,8 +34,7 @@ export function createGuard(router: Router) {
router.beforeEach(async (to) => { router.beforeEach(async (to) => {
to.meta.loaded = !!loadedPageMap.get(to.path); to.meta.loaded = !!loadedPageMap.get(to.path);
// Notify routing changes // Notify routing changes
tabStore.commitLastChangeRouteState(getRoute(to)); setLastChangeTab(to);
try { try {
if (closeMessageOnSwitch) { if (closeMessageOnSwitch) {
Modal.destroyAll(); Modal.destroyAll();

View File

@ -68,6 +68,7 @@ export async function getCurrentParentPath(currentPath: string) {
export async function getShallowMenus(): Promise<Menu[]> { export async function getShallowMenus(): Promise<Menu[]> {
const menus = await getAsyncMenus(); const menus = await getAsyncMenus();
const routes = router.getRoutes(); const routes = router.getRoutes();
const shallowMenuList = menus.map((item) => ({ ...item, children: undefined })); const shallowMenuList = menus.map((item) => ({ ...item, children: undefined }));
return !isBackMode() ? shallowMenuList.filter(basicFilter(routes)) : shallowMenuList; return !isBackMode() ? shallowMenuList.filter(basicFilter(routes)) : shallowMenuList;
} }

View File

@ -62,8 +62,7 @@ const setting: ProjectConfig = {
theme: ThemeEnum.LIGHT, theme: ThemeEnum.LIGHT,
// Whether to enable the lock screen function // Whether to enable the lock screen function
useLockPage: true, useLockPage: true,
// Whether to show the refresh button
showRedo: true,
// Whether to show the full screen button // Whether to show the full screen button
showFullScreen: true, showFullScreen: true,
// Whether to show the document button // Whether to show the document button
@ -117,6 +116,9 @@ const setting: ProjectConfig = {
canDrag: true, canDrag: true,
// Turn on quick actions // Turn on quick actions
showQuick: true, showQuick: true,
// Whether to show the refresh button
showRedo: true,
}, },
// Transition Setting // Transition Setting

View File

@ -5,7 +5,6 @@ import { Action, Module, Mutation, VuexModule, getModule } from 'vuex-module-dec
import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper'; import { hotModuleUnregisterModule } from '/@/utils/helper/vuexHelper';
import { PageEnum } from '/@/enums/pageEnum'; import { PageEnum } from '/@/enums/pageEnum';
import { userStore } from './user';
import store from '/@/store'; import store from '/@/store';
import router from '/@/router'; 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 { RouteLocationNormalized, RouteLocationRaw } from 'vue-router';
import { getRoute } from '/@/router/helper/routeHelper'; import { getRoute } from '/@/router/helper/routeHelper';
import { useGo, useRedo } from '/@/hooks/web/usePage'; import { useGo, useRedo } from '/@/hooks/web/usePage';
import { cloneDeep } from 'lodash-es';
// declare namespace TabsStore {
const NAME = 'tab'; const NAME = 'tab';
@ -34,19 +32,12 @@ class Tab extends VuexModule {
// tab list // tab list
tabsState: RouteLocationNormalized[] = []; tabsState: RouteLocationNormalized[] = [];
// Last route change
lastChangeRouteState: RouteLocationNormalized | null = null;
lastDragEndIndexState = 0; lastDragEndIndexState = 0;
get getTabsState() { get getTabsState() {
return this.tabsState; return this.tabsState;
} }
get getLastChangeRouteState() {
return this.lastChangeRouteState;
}
get getCurrentTab(): RouteLocationNormalized { get getCurrentTab(): RouteLocationNormalized {
const route = unref(router.currentRoute); const route = unref(router.currentRoute);
return this.tabsState.find((item) => item.path === route.path)!; return this.tabsState.find((item) => item.path === route.path)!;
@ -60,12 +51,6 @@ class Tab extends VuexModule {
return this.lastDragEndIndexState; return this.lastDragEndIndexState;
} }
@Mutation
commitLastChangeRouteState(route: RouteLocationNormalized): void {
if (!userStore.getTokenState) return;
this.lastChangeRouteState = route;
}
@Mutation @Mutation
commitClearCache(): void { commitClearCache(): void {
this.cachedMapState = new Map(); this.cachedMapState = new Map();
@ -152,7 +137,7 @@ class Tab extends VuexModule {
this.tabsState.splice(updateIndex, 1, curTab); this.tabsState.splice(updateIndex, 1, curTab);
return; return;
} }
this.tabsState.push(route); this.tabsState = cloneDeep([...this.tabsState, route]);
} }
/** /**
@ -210,7 +195,7 @@ class Tab extends VuexModule {
} }
@Mutation @Mutation
commitRedoPage() { async commitRedoPage() {
const route = router.currentRoute.value; const route = router.currentRoute.value;
for (const [key, value] of this.cachedMapState) { for (const [key, value] of this.cachedMapState) {
const index = value.findIndex((item) => item === (route.name as string)); const index = value.findIndex((item) => item === (route.name as string));
@ -225,7 +210,7 @@ class Tab extends VuexModule {
this.cachedMapState.set(key, value); this.cachedMapState.set(key, value);
} }
const redo = useRedo(); const redo = useRedo();
redo(); await redo();
} }
@Action @Action

View File

@ -27,6 +27,9 @@ export interface MultiTabsSetting {
// 开启快速操作 // 开启快速操作
showQuick: boolean; showQuick: boolean;
canDrag: boolean; canDrag: boolean;
// 显示刷新按钮
showRedo: boolean;
} }
export interface HeaderSetting { export interface HeaderSetting {
@ -34,8 +37,7 @@ export interface HeaderSetting {
fixed: boolean; fixed: boolean;
show: boolean; show: boolean;
theme: ThemeEnum; theme: ThemeEnum;
// 显示刷新按钮
showRedo: boolean;
// 显示全屏按钮 // 显示全屏按钮
showFullScreen: boolean; showFullScreen: boolean;
// 开启全屏功能 // 开启全屏功能

View File

@ -6,13 +6,13 @@
* @returns {Function} The function's instance * @returns {Function} The function's instance
*/ */
export default class Mitt { export default class Mitt {
private cache: Map<string, Array<(data: any) => void>>; private cache: Map<string | Symbol, Array<(...data: any) => void>>;
constructor(all = []) { constructor(all = []) {
// A Map of event names to registered handler functions. // A Map of event names to registered handler functions.
this.cache = new Map(all); this.cache = new Map(all);
} }
once(type: string, handler: Fn) { once(type: string | Symbol, handler: Fn) {
const decor = (...args: any[]) => { const decor = (...args: any[]) => {
handler && handler.apply(this, args); handler && handler.apply(this, args);
this.off(type, decor); 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 {string|symbol} type Type of event to listen for, or `"*"` for all events
* @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, 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) {
@ -41,7 +41,7 @@ export default class Mitt {
* @param {string|symbol} type Type of event to unregister `handler` from, or `"*"` * @param {string|symbol} type Type of event to unregister `handler` from, or `"*"`
* @param {Function} handler Handler function to remove * @param {Function} handler Handler function to remove
*/ */
off(type: string, handler: Fn) { off(type: string | Symbol, handler: Fn) {
const handlers = this.cache.get(type); const handlers = this.cache.get(type);
if (handlers) { if (handlers) {
handlers.splice(handlers.indexOf(handler) >>> 0, 1); 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 {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, 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);
} }

View File

@ -144,7 +144,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
z-index: 3000; z-index: @lock-page-z-index;
display: flex; display: flex;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;