80 changed files with 1335 additions and 969 deletions
@ -1,96 +0,0 @@ |
|||||
import type { Menu as MenuType } from '/@/router/types'; |
|
||||
import type { PropType } from 'vue'; |
|
||||
import { computed, unref } from 'vue'; |
|
||||
|
|
||||
import { defineComponent } from 'vue'; |
|
||||
import Icon from '/@/components/Icon/index'; |
|
||||
import { useI18n } from '/@/hooks/web/useI18n'; |
|
||||
import { useDesign } from '/@/hooks/web/useDesign'; |
|
||||
|
|
||||
const { t } = useI18n(); |
|
||||
|
|
||||
export default defineComponent({ |
|
||||
name: 'MenuContent', |
|
||||
props: { |
|
||||
item: { |
|
||||
type: Object as PropType<MenuType>, |
|
||||
default: null, |
|
||||
}, |
|
||||
showTitle: { |
|
||||
type: Boolean as PropType<boolean>, |
|
||||
default: true, |
|
||||
}, |
|
||||
level: { |
|
||||
type: Number as PropType<number>, |
|
||||
default: 0, |
|
||||
}, |
|
||||
isHorizontal: { |
|
||||
type: Boolean as PropType<boolean>, |
|
||||
default: true, |
|
||||
}, |
|
||||
}, |
|
||||
setup(props) { |
|
||||
const { prefixCls } = useDesign('basic-menu'); |
|
||||
|
|
||||
const getI18nName = computed(() => t(props.item?.name)); |
|
||||
|
|
||||
const getTagClass = computed(() => { |
|
||||
const { item } = props; |
|
||||
const { tag = {} } = item || {}; |
|
||||
const { dot, type = 'error' } = tag; |
|
||||
return [ |
|
||||
`${prefixCls}__tag`, |
|
||||
type, |
|
||||
{ |
|
||||
dot, |
|
||||
}, |
|
||||
]; |
|
||||
}); |
|
||||
|
|
||||
const getNameClass = computed(() => { |
|
||||
const { showTitle } = props; |
|
||||
return { [`${prefixCls}--show-title`]: showTitle, [`${prefixCls}__name`]: !showTitle }; |
|
||||
}); |
|
||||
|
|
||||
/** |
|
||||
* @description: 渲染图标 |
|
||||
*/ |
|
||||
function renderIcon(icon?: string) { |
|
||||
return icon ? <Icon icon={icon} size={18} class="menu-item-icon" /> : null; |
|
||||
} |
|
||||
|
|
||||
function renderTag() { |
|
||||
const { item, showTitle, isHorizontal } = props; |
|
||||
if (!item || showTitle || isHorizontal) return null; |
|
||||
|
|
||||
const { tag } = item; |
|
||||
if (!tag) return null; |
|
||||
|
|
||||
const { dot, content } = tag; |
|
||||
if (!dot && !content) return null; |
|
||||
|
|
||||
return <span class={unref(getTagClass)}>{dot ? '' : content}</span>; |
|
||||
} |
|
||||
|
|
||||
return () => { |
|
||||
const { item } = props; |
|
||||
if (!item) { |
|
||||
return null; |
|
||||
} |
|
||||
const { icon } = item; |
|
||||
const name = unref(getI18nName); |
|
||||
|
|
||||
return ( |
|
||||
<span class={`${prefixCls}__content-wrapper`}> |
|
||||
{renderIcon(icon)} |
|
||||
{ |
|
||||
<span class={unref(getNameClass)}> |
|
||||
{name} |
|
||||
{renderTag()} |
|
||||
</span> |
|
||||
} |
|
||||
</span> |
|
||||
); |
|
||||
}; |
|
||||
}, |
|
||||
}); |
|
||||
@ -0,0 +1,41 @@ |
|||||
|
<template> |
||||
|
<span :class="`${prefixCls}-wrapper`"> |
||||
|
<Icon v-if="getIcon" :icon="getIcon" :size="18" :class="`${prefixCls}-wrapper__icon`" /> |
||||
|
<span :class="getNameClass"> |
||||
|
{{ getI18nName }} |
||||
|
<MenuItemTag v-bind="$props" /> |
||||
|
</span> |
||||
|
</span> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import { computed, defineComponent } from 'vue'; |
||||
|
|
||||
|
import Icon from '/@/components/Icon/index'; |
||||
|
import { useI18n } from '/@/hooks/web/useI18n'; |
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
import { contentProps } from '../props'; |
||||
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
||||
|
const { t } = useI18n(); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: 'MenuItemContent', |
||||
|
components: { Icon, MenuItemTag: createAsyncComponent(() => import('./MenuItemTag.vue')) }, |
||||
|
props: contentProps, |
||||
|
setup(props) { |
||||
|
const { prefixCls } = useDesign('basic-menu-item-content'); |
||||
|
const getI18nName = computed(() => t(props.item?.name)); |
||||
|
const getIcon = computed(() => props.item?.icon); |
||||
|
|
||||
|
const getNameClass = computed(() => { |
||||
|
const { showTitle } = props; |
||||
|
return { [`${prefixCls}--show-title`]: showTitle, [`${prefixCls}__name`]: !showTitle }; |
||||
|
}); |
||||
|
return { |
||||
|
prefixCls, |
||||
|
getNameClass, |
||||
|
getI18nName, |
||||
|
getIcon, |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -0,0 +1,56 @@ |
|||||
|
<template> |
||||
|
<span :class="getTagClass" v-if="getShowTag">{{ getContent }}</span> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import { defineComponent, computed } from 'vue'; |
||||
|
|
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
import { contentProps } from '../props'; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: 'MenuItemTag', |
||||
|
props: contentProps, |
||||
|
setup(props) { |
||||
|
const { prefixCls } = useDesign('basic-menu-item-tag'); |
||||
|
|
||||
|
const getShowTag = computed(() => { |
||||
|
const { item, showTitle, isHorizontal } = props; |
||||
|
if (!item || showTitle || isHorizontal) return false; |
||||
|
|
||||
|
const { tag } = item; |
||||
|
if (!tag) return false; |
||||
|
|
||||
|
const { dot, content } = tag; |
||||
|
if (!dot && !content) return false; |
||||
|
return true; |
||||
|
}); |
||||
|
|
||||
|
const getContent = computed(() => { |
||||
|
if (!getShowTag.value) return ''; |
||||
|
const { item } = props; |
||||
|
const { tag } = item; |
||||
|
const { dot, content } = tag!; |
||||
|
return dot ? '' : content; |
||||
|
}); |
||||
|
|
||||
|
const getTagClass = computed(() => { |
||||
|
const { item } = props; |
||||
|
const { tag = {} } = item || {}; |
||||
|
const { dot, type = 'error' } = tag; |
||||
|
return [ |
||||
|
prefixCls, |
||||
|
[`${prefixCls}--${type}`], |
||||
|
{ |
||||
|
[`${prefixCls}--dot`]: dot, |
||||
|
}, |
||||
|
]; |
||||
|
}); |
||||
|
return { |
||||
|
prefixCls, |
||||
|
getTagClass, |
||||
|
getShowTag, |
||||
|
getContent, |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -0,0 +1,10 @@ |
|||||
|
import { useAppProviderContext } from '/@/components/Application'; |
||||
|
import { computed, unref } from 'vue'; |
||||
|
|
||||
|
export function useAppInject() { |
||||
|
const values = useAppProviderContext(); |
||||
|
|
||||
|
return { |
||||
|
getIsMobile: computed(() => unref(values.isMobile)), |
||||
|
}; |
||||
|
} |
||||
@ -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> |
|
||||
); |
|
||||
}; |
|
||||
}, |
|
||||
}); |
|
||||
@ -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> |
||||
@ -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> |
||||
@ -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')); |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -0,0 +1,28 @@ |
|||||
|
/** |
||||
|
* Used to monitor routing changes to change the status of menus and tabs. There is no need to monitor the route, because the route status change is affected by the page rendering time, which will be slow |
||||
|
*/ |
||||
|
|
||||
|
import Mitt from '/@/utils/mitt'; |
||||
|
import type { RouteLocationNormalized } from 'vue-router'; |
||||
|
import { getRoute } from '/@/router/helper/routeHelper'; |
||||
|
|
||||
|
const mitt = new Mitt(); |
||||
|
|
||||
|
const key = Symbol(); |
||||
|
|
||||
|
let lastChangeTab: RouteLocationNormalized; |
||||
|
|
||||
|
export function setLastChangeTab(lastChangeRoute: RouteLocationNormalized) { |
||||
|
mitt.emit(key, getRoute(lastChangeRoute)); |
||||
|
lastChangeTab = getRoute(lastChangeRoute); |
||||
|
} |
||||
|
|
||||
|
export function listenerLastChangeTab( |
||||
|
callback: (route: RouteLocationNormalized) => void, |
||||
|
immediate = true |
||||
|
) { |
||||
|
mitt.on(key, callback); |
||||
|
if (immediate) { |
||||
|
callback(lastChangeTab); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue