mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-27 14:49:43 +08:00
wip(menu): perf menu
This commit is contained in:
@@ -20,11 +20,10 @@
|
||||
import { useTransitionSetting } from '/@/hooks/setting/useTransitionSetting';
|
||||
import PageLayout from '/@/layouts/page/index';
|
||||
import { Loading } from '/@/components/Loading';
|
||||
import Transition from '/@/views/demo/comp/lazy/Transition.vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutContent',
|
||||
components: { PageLayout, Loading, Transition },
|
||||
components: { PageLayout, Loading },
|
||||
setup() {
|
||||
const { prefixCls } = useDesign('layout-content');
|
||||
const { getOpenPageLoading } = useTransitionSetting();
|
||||
|
@@ -3,38 +3,19 @@ import './index.less';
|
||||
import type { FunctionalComponent } from 'vue';
|
||||
import type { Component } from '/@/components/types';
|
||||
|
||||
import {
|
||||
defineComponent,
|
||||
unref,
|
||||
computed,
|
||||
ref,
|
||||
nextTick,
|
||||
watchEffect,
|
||||
// nextTick
|
||||
} from 'vue';
|
||||
import { defineComponent, unref, computed } from 'vue';
|
||||
|
||||
import { Layout, Tooltip, Badge } from 'ant-design-vue';
|
||||
import { AppLogo } from '/@/components/Application';
|
||||
import UserDropdown from './UserDropdown';
|
||||
import LayoutMenu from '../menu';
|
||||
import LayoutBreadcrumb from './LayoutBreadcrumb.vue';
|
||||
import LockAction from './actions/LockAction';
|
||||
import LayoutTrigger from '../trigger/index.vue';
|
||||
import NoticeAction from './notice/NoticeActionItem.vue';
|
||||
import {
|
||||
RedoOutlined,
|
||||
FullscreenExitOutlined,
|
||||
FullscreenOutlined,
|
||||
LockOutlined,
|
||||
BugOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { LockOutlined, BugOutlined } from '@ant-design/icons-vue';
|
||||
|
||||
import { AppSearch } from '/@/components/Application';
|
||||
import { useModal } from '/@/components/Modal';
|
||||
|
||||
import { useFullscreen } from '/@/hooks/web/useFullScreen';
|
||||
import { useTabs } from '/@/hooks/web/useTabs';
|
||||
import { useWindowSizeFn } from '/@/hooks/event/useWindowSizeFn';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
@@ -49,8 +30,10 @@ import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
|
||||
import { AppLocalePicker } from '/@/components/Application';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { useLayoutContext } from '../useLayoutContext';
|
||||
|
||||
import { UserDropDown, LayoutBreadcrumb, FullScreen } from './components';
|
||||
import { useAppInject } from '/@/hooks/web/useAppInject';
|
||||
import { useDesign } from '../../../hooks/web/useDesign';
|
||||
interface TooltipItemProps {
|
||||
title: string;
|
||||
}
|
||||
@@ -72,24 +55,14 @@ export default defineComponent({
|
||||
fixed: propTypes.bool,
|
||||
},
|
||||
setup(props) {
|
||||
let logoEl: Element | null | undefined;
|
||||
|
||||
const logoWidthRef = ref(200);
|
||||
const logoRef = ref<ComponentRef>(null);
|
||||
|
||||
const injectValue = useLayoutContext();
|
||||
|
||||
const { refreshPage } = useTabs();
|
||||
const { t } = useI18n();
|
||||
|
||||
const { getShowTopMenu, getShowHeaderTrigger, getSplit, getIsHorizontal } = useMenuSetting();
|
||||
|
||||
const { prefixCls } = useDesign('layout-header');
|
||||
const { getShowTopMenu, getShowHeaderTrigger, getSplit } = useMenuSetting();
|
||||
const { getShowLocale } = useLocaleSetting();
|
||||
const { getUseErrorHandle, getShowBreadCrumbIcon } = useRootSetting();
|
||||
const { getUseErrorHandle } = useRootSetting();
|
||||
|
||||
const {
|
||||
getHeaderTheme,
|
||||
getShowRedo,
|
||||
getUseLockPage,
|
||||
getShowFullScreen,
|
||||
getShowNotice,
|
||||
@@ -100,23 +73,11 @@ export default defineComponent({
|
||||
|
||||
const { push } = useRouter();
|
||||
const [register, { openModal }] = useModal();
|
||||
const { toggleFullscreen, isFullscreenRef } = useFullscreen();
|
||||
|
||||
useWindowSizeFn(
|
||||
() => {
|
||||
calcTopMenuWidth();
|
||||
},
|
||||
100,
|
||||
{ immediate: false }
|
||||
);
|
||||
const { getIsMobile } = useAppInject();
|
||||
|
||||
const headerClass = computed(() => {
|
||||
const theme = unref(getHeaderTheme);
|
||||
return theme ? `layout-header__header--${theme}` : '';
|
||||
});
|
||||
|
||||
const isPc = computed(() => {
|
||||
return !unref(injectValue.isMobile);
|
||||
return theme ? `${prefixCls}__header--${theme}` : '';
|
||||
});
|
||||
|
||||
const getSplitType = computed(() => {
|
||||
@@ -127,25 +88,6 @@ export default defineComponent({
|
||||
return unref(getSplit) ? MenuModeEnum.HORIZONTAL : null;
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (unref(getIsHorizontal)) {
|
||||
calcTopMenuWidth();
|
||||
}
|
||||
});
|
||||
|
||||
function calcTopMenuWidth() {
|
||||
nextTick(() => {
|
||||
if (!unref(getShowTopMenu)) return;
|
||||
let width = 0;
|
||||
if (!logoEl) {
|
||||
logoEl = unref(logoRef)?.$el;
|
||||
}
|
||||
if (!logoEl) return;
|
||||
width += logoEl.clientWidth;
|
||||
logoWidthRef.value = width + 80;
|
||||
});
|
||||
}
|
||||
|
||||
function handleToErrorList() {
|
||||
push(PageEnum.ERROR_LOG_PAGE).then(() => {
|
||||
errorStore.commitErrorListCountState(0);
|
||||
@@ -156,27 +98,28 @@ export default defineComponent({
|
||||
openModal(true);
|
||||
}
|
||||
|
||||
function renderHeaderContent() {
|
||||
const width = unref(logoWidthRef);
|
||||
function renderHeaderLeft() {
|
||||
return (
|
||||
<div class="layout-header__content ">
|
||||
{unref(getShowHeaderLogo) && (
|
||||
<AppLogo class={`layout-header__logo`} ref={logoRef} theme={unref(getHeaderTheme)} />
|
||||
)}
|
||||
|
||||
<>
|
||||
{unref(getShowContent) && (
|
||||
<div class="layout-header__left">
|
||||
<div class={`${prefixCls}__left`}>
|
||||
{unref(getShowHeaderTrigger) && (
|
||||
<LayoutTrigger theme={unref(getHeaderTheme)} sider={false} />
|
||||
)}
|
||||
{unref(getShowBread) && unref(isPc) && (
|
||||
<LayoutBreadcrumb showIcon={unref(getShowBreadCrumbIcon)} />
|
||||
{unref(getShowBread) && !unref(getIsMobile) && (
|
||||
<LayoutBreadcrumb theme={unref(getHeaderTheme)} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
{unref(getShowTopMenu) && unref(isPc) && (
|
||||
<div class={[`layout-header__menu `]} style={{ width: `calc(100% - ${width}px)` }}>
|
||||
function renderHeaderContent() {
|
||||
return (
|
||||
<div class={`${prefixCls}__content`}>
|
||||
{unref(getShowTopMenu) && !unref(getIsMobile) && (
|
||||
<div class={[`${prefixCls}__menu `]}>
|
||||
{/* <div class={[`layout-header__menu `]}> */}
|
||||
<LayoutMenu
|
||||
isHorizontal={true}
|
||||
@@ -193,18 +136,18 @@ export default defineComponent({
|
||||
|
||||
function renderActionDefault(Comp: Component | any, event: Fn) {
|
||||
return (
|
||||
<div class="layout-header__action-item" onClick={event}>
|
||||
<Comp class="layout-header__action-icon" />
|
||||
<div class={`${prefixCls}__action-item`} onClick={event}>
|
||||
<Comp class={`${prefixCls}__action-icon`} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAction() {
|
||||
return (
|
||||
<div class={`layout-header__action`}>
|
||||
{unref(isPc) && <AppSearch class="layout-header__action-item" />}
|
||||
<div class={`${prefixCls}__action`}>
|
||||
{!unref(getIsMobile) && <AppSearch class={`${prefixCls}__action-item`} />}
|
||||
|
||||
{unref(getUseErrorHandle) && unref(isPc) && (
|
||||
{unref(getUseErrorHandle) && !unref(getIsMobile) && (
|
||||
<TooltipItem title={t('layout.header.tooltipErrorLog')}>
|
||||
{() => (
|
||||
<Badge
|
||||
@@ -219,48 +162,27 @@ export default defineComponent({
|
||||
</TooltipItem>
|
||||
)}
|
||||
|
||||
{unref(getUseLockPage) && unref(isPc) && (
|
||||
{unref(getUseLockPage) && !unref(getIsMobile) && (
|
||||
<TooltipItem title={t('layout.header.tooltipLock')}>
|
||||
{() => renderActionDefault(LockOutlined, handleLockPage)}
|
||||
</TooltipItem>
|
||||
)}
|
||||
|
||||
{unref(getShowNotice) && unref(isPc) && (
|
||||
{unref(getShowNotice) && !unref(getIsMobile) && (
|
||||
<TooltipItem title={t('layout.header.tooltipNotify')}>
|
||||
{() => <NoticeAction />}
|
||||
</TooltipItem>
|
||||
)}
|
||||
|
||||
{unref(getShowRedo) && unref(isPc) && (
|
||||
<TooltipItem title={t('layout.header.tooltipRedo')}>
|
||||
{() => renderActionDefault(RedoOutlined, refreshPage)}
|
||||
</TooltipItem>
|
||||
)}
|
||||
{unref(getShowFullScreen) && !unref(getIsMobile) && <FullScreen />}
|
||||
|
||||
<UserDropDown theme={unref(getHeaderTheme)} />
|
||||
|
||||
{unref(getShowFullScreen) && unref(isPc) && (
|
||||
<TooltipItem
|
||||
title={
|
||||
unref(isFullscreenRef)
|
||||
? t('layout.header.tooltipExitFull')
|
||||
: t('layout.header.tooltipEntryFull')
|
||||
}
|
||||
>
|
||||
{() => {
|
||||
const Icon = !unref(isFullscreenRef) ? (
|
||||
<FullscreenOutlined />
|
||||
) : (
|
||||
<FullscreenExitOutlined />
|
||||
);
|
||||
return renderActionDefault(Icon, toggleFullscreen);
|
||||
}}
|
||||
</TooltipItem>
|
||||
)}
|
||||
<UserDropdown class="layout-header__user-dropdown" />
|
||||
{unref(getShowLocale) && (
|
||||
<AppLocalePicker
|
||||
reload={true}
|
||||
showText={false}
|
||||
class="layout-header__action-item locale"
|
||||
class={`${prefixCls}__action-item locale`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -270,6 +192,10 @@ export default defineComponent({
|
||||
function renderHeaderDefault() {
|
||||
return (
|
||||
<>
|
||||
{unref(getShowHeaderLogo) && (
|
||||
<AppLogo class={`${prefixCls}__logo`} theme={unref(getHeaderTheme)} />
|
||||
)}
|
||||
{renderHeaderLeft()}
|
||||
{renderHeaderContent()}
|
||||
{renderAction()}
|
||||
<LockAction onRegister={register} />
|
||||
@@ -279,9 +205,7 @@ export default defineComponent({
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<Layout.Header
|
||||
class={['layout-header', 'flex p-0 px-4 ', unref(headerClass), { fixed: props.fixed }]}
|
||||
>
|
||||
<Layout.Header class={[prefixCls, unref(headerClass), { fixed: props.fixed }]}>
|
||||
{() => renderHeaderDefault()}
|
||||
</Layout.Header>
|
||||
);
|
||||
|
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
},
|
||||
});
|
@@ -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>
|
47
src/layouts/default/header/components/ErrorAction.vue
Normal file
47
src/layouts/default/header/components/ErrorAction.vue
Normal 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>
|
36
src/layouts/default/header/components/FullScreen.vue
Normal file
36
src/layouts/default/header/components/FullScreen.vue
Normal 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>
|
15
src/layouts/default/header/components/index.ts
Normal file
15
src/layouts/default/header/components/index.ts
Normal 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'));
|
116
src/layouts/default/header/components/lock/LockModal.vue
Normal file
116
src/layouts/default/header/components/lock/LockModal.vue
Normal 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>
|
38
src/layouts/default/header/components/lock/index.vue
Normal file
38
src/layouts/default/header/components/lock/index.vue
Normal 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>
|
@@ -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;
|
||||
}
|
@@ -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;
|
||||
}
|
@@ -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>
|
156
src/layouts/default/header/components/user-dropdown/index.vue
Normal file
156
src/layouts/default/header/components/user-dropdown/index.vue
Normal 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>
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
161
src/layouts/default/header/index.vue
Normal file
161
src/layouts/default/header/index.vue
Normal 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>
|
@@ -18,7 +18,7 @@
|
||||
import { Layout } from 'ant-design-vue';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
|
||||
import LayoutHeader from './header/LayoutHeader';
|
||||
import LayoutHeader from './header/index.vue';
|
||||
import LayoutContent from './content/index.vue';
|
||||
import LayoutSideBar from './sider';
|
||||
import LayoutMultipleHeader from './header/LayoutMultipleHeader';
|
||||
@@ -29,8 +29,6 @@
|
||||
import { createLayoutContext } from './useLayoutContext';
|
||||
|
||||
import { registerGlobComp } from '/@/components/registerGlobComp';
|
||||
import { createBreakpointListen } from '/@/hooks/event/useBreakpoint';
|
||||
import { isMobile } from '/@/utils/is';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'DefaultLayout',
|
||||
@@ -44,22 +42,17 @@
|
||||
Layout,
|
||||
},
|
||||
setup() {
|
||||
const headerRef = ref<ComponentRef>(null);
|
||||
const isMobileRef = ref(false);
|
||||
|
||||
const { prefixCls } = useDesign('default-layout');
|
||||
|
||||
createLayoutContext({ fullHeader: headerRef, isMobile: isMobileRef });
|
||||
|
||||
createBreakpointListen(() => {
|
||||
isMobileRef.value = isMobile();
|
||||
});
|
||||
|
||||
// ! Only register global components here
|
||||
// ! Can reduce the size of the first screen code
|
||||
// default layout It is loaded after login. So it won’t be packaged to the first screen
|
||||
registerGlobComp();
|
||||
|
||||
const headerRef = ref<ComponentRef>(null);
|
||||
|
||||
const { prefixCls } = useDesign('default-layout');
|
||||
|
||||
createLayoutContext({ fullHeader: headerRef });
|
||||
|
||||
const { getShowFullHeaderRef } = useHeaderSetting();
|
||||
|
||||
const { getShowSidebar } = useMenuSetting();
|
||||
|
@@ -9,6 +9,7 @@ import { AppLogo } from '/@/components/Application';
|
||||
import { MenuModeEnum, MenuSplitTyeEnum } from '/@/enums/menuEnum';
|
||||
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { ScrollContainer } from '/@/components/Container';
|
||||
|
||||
import { useGo } from '/@/hooks/web/usePage';
|
||||
import { useSplitMenu } from './useLayoutMenu';
|
||||
@@ -16,6 +17,7 @@ import { openWindow } from '/@/utils';
|
||||
import { propTypes } from '/@/utils/propTypes';
|
||||
import { isUrl } from '/@/utils/is';
|
||||
import { useRootSetting } from '/@/hooks/setting/useRootSetting';
|
||||
import { CSSProperties } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'LayoutMenu',
|
||||
@@ -53,12 +55,25 @@ export default defineComponent({
|
||||
const getComputedMenuMode = computed(() => props.menuMode || unref(getMenuMode));
|
||||
|
||||
const getComputedMenuTheme = computed(() => props.theme || unref(getMenuTheme));
|
||||
const showLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType));
|
||||
const appendClass = computed(() => props.splitType === MenuSplitTyeEnum.TOP);
|
||||
|
||||
const getIsShowLogo = computed(() => unref(getShowLogo) && unref(getIsSidebarType));
|
||||
|
||||
const getUseScroll = computed(() => {
|
||||
return unref(getIsSidebarType) || props.splitType === MenuSplitTyeEnum.LEFT;
|
||||
});
|
||||
|
||||
const getWrapperStyle = computed(
|
||||
(): CSSProperties => {
|
||||
return {
|
||||
height: `calc(100% - ${unref(getIsShowLogo) ? '48px' : '0px'})`,
|
||||
};
|
||||
}
|
||||
);
|
||||
/**
|
||||
* click menu
|
||||
* @param menu
|
||||
*/
|
||||
|
||||
function handleMenuClick(path: string) {
|
||||
go(path);
|
||||
}
|
||||
@@ -76,7 +91,7 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
function renderHeader() {
|
||||
if (!unref(showLogo)) return null;
|
||||
if (!unref(getIsShowLogo)) return null;
|
||||
|
||||
return (
|
||||
<AppLogo
|
||||
@@ -87,7 +102,7 @@ export default defineComponent({
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
function renderMenu() {
|
||||
return (
|
||||
<BasicMenu
|
||||
beforeClickFn={beforeMenuClickFn}
|
||||
@@ -99,13 +114,22 @@ export default defineComponent({
|
||||
items={unref(menusRef)}
|
||||
accordion={unref(getAccordion)}
|
||||
onMenuClick={handleMenuClick}
|
||||
appendClass={unref(appendClass)}
|
||||
showLogo={unref(showLogo)}
|
||||
>
|
||||
{{
|
||||
header: () => renderHeader(),
|
||||
}}
|
||||
</BasicMenu>
|
||||
showLogo={unref(getIsShowLogo)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return () => {
|
||||
return (
|
||||
<>
|
||||
{renderHeader()}
|
||||
{unref(getUseScroll) ? (
|
||||
<ScrollContainer style={unref(getWrapperStyle)}>{() => renderMenu()}</ScrollContainer>
|
||||
) : (
|
||||
renderMenu()
|
||||
)}
|
||||
;
|
||||
</>
|
||||
);
|
||||
};
|
||||
},
|
||||
|
@@ -14,9 +14,7 @@ import { permissionStore } from '/@/store/modules/permission';
|
||||
export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
|
||||
// Menu array
|
||||
const menusRef = ref<Menu[]>([]);
|
||||
|
||||
const { currentRoute } = useRouter();
|
||||
|
||||
const { setMenuSetting, getIsHorizontal, getSplit } = useMenuSetting();
|
||||
|
||||
const [throttleHandleSplitLeftMenu] = useThrottle(handleSplitLeftMenu, 50);
|
||||
@@ -25,9 +23,11 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
|
||||
() => unref(splitType) !== MenuSplitTyeEnum.LEFT && !unref(getIsHorizontal)
|
||||
);
|
||||
|
||||
const splitLeft = computed(() => !unref(getSplit) || unref(splitType) !== MenuSplitTyeEnum.LEFT);
|
||||
const getSplitLeft = computed(
|
||||
() => !unref(getSplit) || unref(splitType) !== MenuSplitTyeEnum.LEFT
|
||||
);
|
||||
|
||||
const spiltTop = computed(() => unref(splitType) === MenuSplitTyeEnum.TOP);
|
||||
const getSpiltTop = computed(() => unref(splitType) === MenuSplitTyeEnum.TOP);
|
||||
|
||||
const normalType = computed(() => {
|
||||
return unref(splitType) === MenuSplitTyeEnum.NONE || !unref(getSplit);
|
||||
@@ -65,7 +65,7 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
|
||||
|
||||
// Handle left menu split
|
||||
async function handleSplitLeftMenu(parentPath: string) {
|
||||
if (unref(splitLeft)) return;
|
||||
if (unref(getSplitLeft)) return;
|
||||
|
||||
// spilt mode left
|
||||
const children = await getChildrenMenus(parentPath);
|
||||
@@ -88,7 +88,7 @@ export function useSplitMenu(splitType: Ref<MenuSplitTyeEnum>) {
|
||||
}
|
||||
|
||||
// split-top
|
||||
if (unref(spiltTop)) {
|
||||
if (unref(getSpiltTop)) {
|
||||
const shallowMenus = await getShallowMenus();
|
||||
|
||||
menusRef.value = shallowMenus;
|
||||
|
@@ -80,7 +80,7 @@ export default defineComponent({
|
||||
getShowSearch,
|
||||
} = useHeaderSetting();
|
||||
|
||||
const { getShowMultipleTab, getShowQuick } = useMultipleTabSetting();
|
||||
const { getShowMultipleTab, getShowQuick, getShowRedo } = useMultipleTabSetting();
|
||||
|
||||
const getShowMenuRef = computed(() => {
|
||||
return unref(getShowMenu) && !unref(getIsHorizontal);
|
||||
@@ -246,6 +246,13 @@ export default defineComponent({
|
||||
def={unref(getShowMultipleTab)}
|
||||
/>
|
||||
|
||||
<SwitchItem
|
||||
title={t('layout.setting.tabsRedoBtn')}
|
||||
event={HandlerEnum.TABS_SHOW_REDO}
|
||||
def={unref(getShowRedo)}
|
||||
disabled={!unref(getShowMultipleTab)}
|
||||
/>
|
||||
|
||||
<SwitchItem
|
||||
title={t('layout.setting.tabsQuickBtn')}
|
||||
event={HandlerEnum.TABS_SHOW_QUICK}
|
||||
|
@@ -31,6 +31,7 @@ export enum HandlerEnum {
|
||||
HEADER_SEARCH,
|
||||
|
||||
TABS_SHOW_QUICK,
|
||||
TABS_SHOW_REDO,
|
||||
TABS_SHOW,
|
||||
|
||||
LOCK_TIME,
|
||||
|
@@ -113,6 +113,8 @@ export function handler(event: HandlerEnum, value: any): DeepPartial<ProjectConf
|
||||
|
||||
case HandlerEnum.TABS_SHOW:
|
||||
return { multiTabsSetting: { show: value } };
|
||||
case HandlerEnum.TABS_SHOW_REDO:
|
||||
return { multiTabsSetting: { showRedo: value } };
|
||||
|
||||
// ============header==================
|
||||
case HandlerEnum.HEADER_THEME:
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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)}
|
||||
|
@@ -16,7 +16,7 @@ export function useSiderEvent() {
|
||||
const brokenRef = ref(false);
|
||||
const collapseRef = ref(true);
|
||||
|
||||
const { setMenuSetting, getCollapsed, getMiniWidthNumber, getShowMenu } = useMenuSetting();
|
||||
const { setMenuSetting, getCollapsed, getMiniWidthNumber } = useMenuSetting();
|
||||
|
||||
const getCollapsedWidth = computed(() => {
|
||||
return unref(brokenRef) ? 0 : unref(getMiniWidthNumber);
|
||||
@@ -36,12 +36,7 @@ export function useSiderEvent() {
|
||||
brokenRef.value = broken;
|
||||
}
|
||||
|
||||
function onSiderClick(e: ChangeEvent) {
|
||||
if (!e || !e.target || e.target.className !== 'basic-menu__content') return;
|
||||
if (!unref(getCollapsed) || !unref(getShowMenu)) return;
|
||||
setMenuSetting({ collapsed: false });
|
||||
}
|
||||
return { getCollapsedWidth, onCollapseChange, onBreakpointChange, onSiderClick };
|
||||
return { getCollapsedWidth, onCollapseChange, onBreakpointChange };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,7 +4,7 @@
|
||||
<span class="ml-1">{{ getTitle }}</span>
|
||||
</div>
|
||||
|
||||
<span :class="`${prefixCls}__extra`" v-else>
|
||||
<span :class="`${prefixCls}__extra-quick`" v-else @click="handleContext">
|
||||
<RightOutlined />
|
||||
</span>
|
||||
</Dropdown>
|
||||
|
37
src/layouts/default/tabs/components/TabRedo.vue
Normal file
37
src/layouts/default/tabs/components/TabRedo.vue
Normal 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>
|
@@ -34,30 +34,30 @@
|
||||
border: 1px solid darken(@border-color-light, 6%);
|
||||
transition: none;
|
||||
|
||||
&:not(.ant-tabs-tab-active)::after {
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
background-color: @primary-color;
|
||||
content: '';
|
||||
opacity: 0;
|
||||
transform: translate(-50%, 0) scaleX(0);
|
||||
transform-origin: center;
|
||||
transition: none;
|
||||
}
|
||||
// &:not(.ant-tabs-tab-active)::before {
|
||||
// position: absolute;
|
||||
// top: -1px;
|
||||
// left: 50%;
|
||||
// width: 100%;
|
||||
// height: 2px;
|
||||
// background-color: @primary-color;
|
||||
// content: '';
|
||||
// opacity: 0;
|
||||
// transform: translate(-50%, 0) scaleX(0);
|
||||
// transform-origin: center;
|
||||
// transition: none;
|
||||
// }
|
||||
|
||||
&:hover {
|
||||
.ant-tabs-close-x {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:not(.ant-tabs-tab-active)::after {
|
||||
opacity: 1;
|
||||
transform: translate(-50%, 0) scaleX(1);
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
// &:not(.ant-tabs-tab-active)::before {
|
||||
// opacity: 1;
|
||||
// transform: translate(-50%, 0) scaleX(1);
|
||||
// transition: all 0.3s ease-in-out;
|
||||
// }
|
||||
}
|
||||
|
||||
.ant-tabs-close-x {
|
||||
@@ -152,12 +152,13 @@
|
||||
}
|
||||
|
||||
&-content {
|
||||
&__extra {
|
||||
&__extra-quick,
|
||||
&__extra-redo {
|
||||
display: inline-block;
|
||||
width: @multiple-height;
|
||||
width: 36px;
|
||||
height: @multiple-height;
|
||||
line-height: @multiple-height;
|
||||
color: #999;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border-left: 1px solid #eee;
|
||||
@@ -171,6 +172,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__extra-redo {
|
||||
span[role='img'] {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
|
@@ -17,14 +17,16 @@
|
||||
</template>
|
||||
</TabPane>
|
||||
</template>
|
||||
<template #tabBarExtraContent>
|
||||
<QuickButton />
|
||||
|
||||
<template #tabBarExtraContent v-if="getShowRedo || getShowQuick">
|
||||
<TabRedo v-if="getShowRedo" />
|
||||
<QuickButton v-if="getShowQuick" />
|
||||
</template>
|
||||
</Tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent, watch, computed, unref, ref } from 'vue';
|
||||
import { defineComponent, computed, unref, ref } from 'vue';
|
||||
|
||||
import { Tabs } from 'ant-design-vue';
|
||||
import TabContent from './components/TabContent.vue';
|
||||
@@ -38,61 +40,52 @@
|
||||
import { REDIRECT_NAME } from '/@/router/constant';
|
||||
import { useDesign } from '/@/hooks/web/useDesign';
|
||||
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
|
||||
import { listenerLastChangeTab } from '/@/logics/mitt/tabChange';
|
||||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'MultipleTabs',
|
||||
components: {
|
||||
QuickButton: createAsyncComponent(() => import('./components/QuickButton.vue')),
|
||||
TabRedo: createAsyncComponent(() => import('./components/TabRedo.vue')),
|
||||
Tabs,
|
||||
TabPane: Tabs.TabPane,
|
||||
TabContent,
|
||||
},
|
||||
setup() {
|
||||
const affixTextList = initAffixTabs();
|
||||
|
||||
const activeKeyRef = ref('');
|
||||
|
||||
useTabsDrag(affixTextList);
|
||||
const { prefixCls } = useDesign('multiple-tabs');
|
||||
const go = useGo();
|
||||
const { getShowQuick, getShowRedo } = useMultipleTabSetting();
|
||||
|
||||
const getTabsState = computed(() => tabStore.getTabsState);
|
||||
|
||||
const unClose = computed(() => {
|
||||
return getTabsState.value.length === 1;
|
||||
});
|
||||
const unClose = computed(() => unref(getTabsState).length === 1);
|
||||
|
||||
const getWrapClass = computed(() => {
|
||||
return [
|
||||
prefixCls,
|
||||
{
|
||||
[`${prefixCls}--hide-close`]: unClose,
|
||||
[`${prefixCls}--hide-close`]: unref(unClose),
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
watch(
|
||||
() => tabStore.getLastChangeRouteState?.path,
|
||||
() => {
|
||||
if (tabStore.getLastChangeRouteState?.name === REDIRECT_NAME) {
|
||||
return;
|
||||
}
|
||||
const lastChangeRoute = unref(tabStore.getLastChangeRouteState);
|
||||
if (!lastChangeRoute || !userStore.getTokenState) return;
|
||||
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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@@ -10,7 +10,6 @@ import { useTabs } from '/@/hooks/web/useTabs';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { useHeaderSetting } from '/@/hooks/setting/useHeaderSetting';
|
||||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
|
||||
import { useMultipleTabSetting } from '/@/hooks/setting/useMultipleTabSetting';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -24,11 +23,8 @@ export function useTabDropdown(tabContentProps: TabContentProps) {
|
||||
|
||||
const { getShowMenu, setMenuSetting } = useMenuSetting();
|
||||
const { getShowHeader, setHeaderSetting } = useHeaderSetting();
|
||||
const { getShowQuick } = useMultipleTabSetting();
|
||||
|
||||
const isTabs = computed(() =>
|
||||
!unref(getShowQuick) ? true : tabContentProps.type === TabContentEnum.TAB_TYPE
|
||||
);
|
||||
const isTabs = computed(() => tabContentProps.type === TabContentEnum.TAB_TYPE);
|
||||
|
||||
const getCurrentTab = computed(
|
||||
(): RouteLocationNormalized => {
|
||||
|
@@ -3,7 +3,6 @@ import { createContext, useContext } from '/@/hooks/core/useContext';
|
||||
|
||||
export interface LayoutContextProps {
|
||||
fullHeader: Ref<ComponentRef>;
|
||||
isMobile: Ref<boolean>;
|
||||
}
|
||||
|
||||
const key: InjectionKey<LayoutContextProps> = Symbol();
|
||||
|
Reference in New Issue
Block a user