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

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

View File

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

View File

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

View File

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

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>
<div class="layout-breadcrumb">
<div :class="[prefixCls, `${prefixCls}--${theme}`]">
<a-breadcrumb :routes="routes">
<template #itemRender="{ route, routes }">
<Icon :icon="route.meta.icon" v-if="showIcon && route.meta.icon" />
<Icon :icon="route.meta.icon" v-if="getShowBreadCrumbIcon && route.meta.icon" />
<span v-if="routes.indexOf(route) === routes.length - 1">
{{ t(route.meta.title) }}
</span>
@@ -14,7 +14,6 @@
</div>
</template>
<script lang="ts">
import { PropType } from 'vue';
import { defineComponent, ref, toRaw, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n';
@@ -26,18 +25,23 @@
import { HomeOutlined } from '@ant-design/icons-vue';
import { PageEnum } from '/@/enums/pageEnum';
import { useDesign } from '/@/hooks/web/useDesign';
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
import { propTypes } from '/@/utils/propTypes';
export default defineComponent({
name: 'LayoutBreadcrumb',
components: { HomeOutlined, Icon },
props: {
showIcon: {
type: Boolean as PropType<boolean>,
default: false,
},
theme: propTypes.oneOf(['dark', 'light']),
},
setup() {
const routes = ref<RouteLocationMatched[]>([]);
const { currentRoute } = useRouter();
const { prefixCls } = useDesign('layout-breadcrumb');
const { getShowBreadCrumbIcon } = useRootSetting();
const { t } = useI18n();
watchEffect(() => {
@@ -63,17 +67,71 @@
);
if (filterBreadcrumbList.length === breadcrumbList.length) {
filterBreadcrumbList.unshift({
filterBreadcrumbList.unshift(({
path: PageEnum.BASE_HOME,
meta: {
title: t('layout.header.home'),
},
});
} as unknown) as RouteLocationMatched);
}
routes.value = filterBreadcrumbList;
routes.value = filterBreadcrumbList.length === 1 ? [] : filterBreadcrumbList;
});
return { routes, t };
return { routes, t, prefixCls, getShowBreadCrumbIcon };
},
});
</script>
<style lang="less">
@import (reference) '../../../../design/index.less';
@prefix-cls: ~'@{namespace}-layout-breadcrumb';
.@{prefix-cls} {
display: flex;
padding: 0 8px;
align-items: center;
.ant-breadcrumb-link {
.anticon {
margin-right: 4px;
margin-bottom: 2px;
}
}
&--light {
.ant-breadcrumb-link {
color: @breadcrumb-item-normal-color;
a {
color: @text-color-base;
&:hover {
color: @primary-color;
}
}
}
.ant-breadcrumb-separator {
color: @breadcrumb-item-normal-color;
}
}
&--dark {
.ant-breadcrumb-link {
color: rgba(255, 255, 255, 0.6);
a {
color: rgba(255, 255, 255, 0.8);
&:hover {
color: @white;
}
}
}
.ant-breadcrumb-separator,
.anticon {
color: rgba(255, 255, 255, 0.8);
}
}
}
</style>

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

View File

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

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';
@header-trigger-prefix-cls: ~'@{namespace}-layout-header-trigger';
@header-prefix-cls: ~'@{namespace}-layout-header';
@locale-prefix-cls: ~'@{namespace}-app-locale-picker';
.layout-header {
.@{header-prefix-cls} {
display: flex;
height: @header-height;
padding: 0 20px 0 0;
padding: 0;
margin-left: -1px;
line-height: @header-height;
color: @white;
@@ -12,15 +14,28 @@
align-items: center;
justify-content: space-between;
&.fixed {
&--fixed {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
z-index: @layout-header-fixed-z-index;
width: 100%;
}
&__left {
&-logo {
height: @header-height;
min-width: 192px;
padding: 0 10px;
font-size: 14px;
img {
width: @logo-width;
height: @logo-width;
margin-right: 2px;
}
}
&-left {
display: flex;
height: 100%;
align-items: center;
@@ -33,7 +48,7 @@
align-items: center;
.anticon {
font-size: 17px;
font-size: 16px;
}
&.light {
@@ -52,82 +67,65 @@
}
}
}
.layout-breadcrumb {
display: flex;
padding: 0 8px;
align-items: center;
.ant-breadcrumb-link {
.anticon {
margin-right: 4px;
margin-bottom: 2px;
}
}
}
}
&__content {
display: flex;
&-menu {
height: 100%;
flex-grow: 1;
min-width: 0;
flex: 1;
align-items: center;
}
&__header--light {
background: @white;
border-bottom: 1px solid @header-light-bottom-border-color;
&-action {
display: flex;
min-width: 200px;
padding-right: 12px;
align-items: center;
.layout-header__menu {
height: calc(@header-height - 1px);
&__item {
display: flex;
height: @header-height;
padding: 0 2px;
font-size: 1.2em;
cursor: pointer;
align-items: center;
.ant-menu-submenu {
.ant-badge {
height: @header-height;
line-height: @header-height;
}
}
.layout-breadcrumb {
.ant-breadcrumb-link {
color: @breadcrumb-item-normal-color;
a {
color: @text-color-base;
&:hover {
color: @primary-color;
}
}
}
.ant-breadcrumb-separator {
color: @breadcrumb-item-normal-color;
.ant-badge-dot {
top: 10px;
right: 2px;
}
}
.layout-header__logo {
height: @header-height;
span[role='img'] {
padding: 0 8px;
}
}
&--light {
background: @white;
border-bottom: 1px solid @header-light-bottom-border-color;
.@{header-prefix-cls}-logo {
color: @text-color-base;
img {
width: @logo-width;
height: @logo-width;
margin-right: 6px;
}
&:hover {
background: @header-light-bg-hover-color;
}
}
.layout-header__action {
&-item {
.@{header-prefix-cls}-action {
&__item {
&:hover {
background: @header-light-bg-hover-color;
}
&.locale {
padding: 0 10px;
.@{locale-prefix-cls} {
padding: 0 6px;
color: rgba(0, 0, 0, 0.65);
}
}
@@ -137,134 +135,23 @@
color: @text-color-base;
}
}
.layout-header__user-dropdown {
&:hover {
background: @header-light-bg-hover-color;
}
}
.user-dropdown {
&__name {
color: @text-color-base;
}
&__desc {
color: @header-light-desc-color;
}
}
}
&__header--dark {
&--dark {
background: @header-dark-bg-color;
.layout-header__action {
&-item {
.@{header-prefix-cls}-logo {
&:hover {
background: @header-dark-bg-hover-color;
}
}
.@{header-prefix-cls}-action {
&__item {
&:hover {
background: @header-dark-bg-hover-color;
}
}
}
.layout-header__logo {
height: @header-height;
img {
width: @logo-width;
height: @logo-width;
margin-right: 10px;
}
&:hover {
background: @header-dark-bg-hover-color;
}
}
.layout-header__user-dropdown {
&: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 { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import LayoutHeader from './header/LayoutHeader';
import LayoutHeader from './header/index.vue';
import LayoutContent from './content/index.vue';
import LayoutSideBar from './sider';
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
@@ -29,8 +29,6 @@
import { createLayoutContext } from './useLayoutContext';
import { registerGlobComp } from '/@/components/registerGlobComp';
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
import { isMobile } from '/@/utils/is';
export default defineComponent({
name: 'DefaultLayout',
@@ -44,22 +42,17 @@
Layout,
},
setup() {
const headerRef = ref<ComponentRef>(null);
const isMobileRef = ref(false);
const { prefixCls } = useDesign('default-layout');
createLayoutContext({ fullHeader: headerRef, isMobile: isMobileRef });
createBreakpointListen(() => {
isMobileRef.value = isMobile();
});
// ! Only register global components here
// ! Can reduce the size of the first screen code
// default layout It is loaded after login. So it wont be packaged to the first screen
registerGlobComp();
const headerRef = ref<ComponentRef>(null);
const { prefixCls } = useDesign('default-layout');
createLayoutContext({ fullHeader: headerRef });
const { getShowFullHeaderRef } = useHeaderSetting();
const { getShowSidebar } = useMenuSetting();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ export function useSiderEvent() {
const brokenRef = ref(false);
const collapseRef = ref(true);
const { setMenuSetting, getCollapsed, getMiniWidthNumber, getShowMenu } = useMenuSetting();
const { setMenuSetting, getCollapsed, getMiniWidthNumber } = useMenuSetting();
const getCollapsedWidth = computed(() => {
return unref(brokenRef) ? 0 : unref(getMiniWidthNumber);
@@ -36,12 +36,7 @@ export function useSiderEvent() {
brokenRef.value = broken;
}
function onSiderClick(e: ChangeEvent) {
if (!e || !e.target || e.target.className !== 'basic-menu__content') return;
if (!unref(getCollapsed) || !unref(getShowMenu)) return;
setMenuSetting({ collapsed: false });
}
return { getCollapsedWidth, onCollapseChange, onBreakpointChange, onSiderClick };
return { getCollapsedWidth, onCollapseChange, onBreakpointChange };
}
/**

View File

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

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

View File

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

View File

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

View File

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