mirror of
https://github.com/bufanyun/hotgo.git
synced 2025-08-27 21:21:42 +08:00
690 lines
18 KiB
Vue
690 lines
18 KiB
Vue
<template>
|
|
<div class="layout-header">
|
|
<!--顶部菜单-->
|
|
<div
|
|
class="layout-header-left"
|
|
v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)"
|
|
>
|
|
<div class="logo" v-if="navMode === 'horizontal'">
|
|
<img src="~@/assets/images/logo.png" alt="" />
|
|
<h2 v-show="!collapsed" class="title">HotGo</h2>
|
|
</div>
|
|
<AsideMenu
|
|
@update:collapsed="updateMenu"
|
|
v-model:location="getMenuLocation"
|
|
:inverted="getInverted"
|
|
mode="horizontal"
|
|
/>
|
|
</div>
|
|
<!--左侧菜单-->
|
|
<div class="layout-header-left" v-else>
|
|
<!-- 菜单收起 -->
|
|
<div
|
|
class="ml-1 layout-header-trigger layout-header-trigger-min"
|
|
@click="() => $emit('update:collapsed', !collapsed)"
|
|
>
|
|
<n-icon size="18" v-if="collapsed">
|
|
<MenuUnfoldOutlined />
|
|
</n-icon>
|
|
<n-icon size="18" v-else>
|
|
<MenuFoldOutlined />
|
|
</n-icon>
|
|
</div>
|
|
<!-- 刷新 -->
|
|
<div
|
|
class="mr-1 layout-header-trigger layout-header-trigger-min"
|
|
v-if="headerSetting.isReload"
|
|
@click="reloadPage"
|
|
>
|
|
<n-icon size="18">
|
|
<ReloadOutlined />
|
|
</n-icon>
|
|
</div>
|
|
<!-- 面包屑 -->
|
|
<n-breadcrumb v-if="crumbsSetting.show">
|
|
<template v-for="routeItem in breadcrumbList" :key="routeItem.name">
|
|
<n-breadcrumb-item>
|
|
<n-dropdown
|
|
v-if="routeItem.children.length"
|
|
:options="routeItem.children"
|
|
@select="dropdownSelect"
|
|
>
|
|
<span class="link-text">
|
|
<component
|
|
v-if="crumbsSetting.showIcon && routeItem.meta.icon"
|
|
:is="routeItem.meta.icon"
|
|
/>
|
|
{{ routeItem.meta.title }}
|
|
</span>
|
|
</n-dropdown>
|
|
<span class="link-text" v-else>
|
|
<component
|
|
v-if="crumbsSetting.showIcon && routeItem.meta.icon"
|
|
:is="routeItem.meta.icon"
|
|
/>
|
|
{{ routeItem.meta.title }}
|
|
</span>
|
|
</n-breadcrumb-item>
|
|
</template>
|
|
</n-breadcrumb>
|
|
</div>
|
|
<div class="layout-header-right">
|
|
<!-- <div-->
|
|
<!-- class="layout-header-trigger layout-header-trigger-min"-->
|
|
<!-- v-for="item in iconList"-->
|
|
<!-- :key="item.icon.name"-->
|
|
<!-- >-->
|
|
<!-- <n-tooltip placement="bottom">-->
|
|
<!-- <template #trigger>-->
|
|
<!-- <n-icon size="18">-->
|
|
<!-- <component :is="item.icon" v-on="item.eventObject || {}" />-->
|
|
<!-- </n-icon>-->
|
|
<!-- </template>-->
|
|
<!-- <span>{{ item.tips }}</span>-->
|
|
<!-- </n-tooltip>-->
|
|
<!-- </div>-->
|
|
|
|
<div
|
|
class="layout-header-trigger layout-header-trigger-min"
|
|
v-for="item in iconList"
|
|
:key="item.icon.name"
|
|
>
|
|
<n-popover
|
|
placement="bottom"
|
|
v-if="item.icon === 'BellOutlined'"
|
|
trigger="click"
|
|
:width="getIsMobile ? 276 : 420"
|
|
>
|
|
<template #trigger>
|
|
<n-tooltip placement="bottom">
|
|
<template #trigger>
|
|
<n-badge :value="notificationStore.getUnreadCount()" :max="99" processing>
|
|
<n-icon size="18">
|
|
<BellOutlined />
|
|
</n-icon>
|
|
</n-badge>
|
|
</template>
|
|
<span>{{ item.tips }}</span>
|
|
</n-tooltip>
|
|
</template>
|
|
|
|
<SystemMessage />
|
|
</n-popover>
|
|
|
|
<div v-else>
|
|
<n-tooltip placement="bottom">
|
|
<template #trigger>
|
|
<n-icon size="18">
|
|
<component :is="item.icon" v-on="item.eventObject || {}" />
|
|
</n-icon>
|
|
</template>
|
|
<span>{{ item.tips }}</span>
|
|
</n-tooltip>
|
|
</div>
|
|
</div>
|
|
<!--切换全屏-->
|
|
<div class="layout-header-trigger layout-header-trigger-min">
|
|
<n-tooltip placement="bottom">
|
|
<template #trigger>
|
|
<n-icon size="18">
|
|
<component :is="fullscreenIcon" @click="toggleFullScreen" />
|
|
</n-icon>
|
|
</template>
|
|
<span>全屏</span>
|
|
</n-tooltip>
|
|
</div>
|
|
<!-- 个人中心 -->
|
|
<div class="layout-header-trigger layout-header-trigger-min">
|
|
<n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
|
|
<div class="avatar">
|
|
<n-avatar v-if="userStore.avatar" round :size="30" :src="userStore.avatar" />
|
|
<n-avatar v-else round :size="30">{{ userStore.realName }}</n-avatar>
|
|
</div>
|
|
</n-dropdown>
|
|
</div>
|
|
<!--设置-->
|
|
<div class="layout-header-trigger layout-header-trigger-min" @click="openSetting">
|
|
<n-tooltip placement="bottom-end">
|
|
<template #trigger>
|
|
<n-icon size="18" style="font-weight: bold">
|
|
<SettingOutlined />
|
|
</n-icon>
|
|
</template>
|
|
<span>项目配置</span>
|
|
</n-tooltip>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!--项目配置-->
|
|
<ProjectSetting ref="drawerSetting" />
|
|
|
|
<template>
|
|
<n-notification-provider :max="3" />
|
|
</template>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import {
|
|
defineComponent,
|
|
reactive,
|
|
toRefs,
|
|
ref,
|
|
computed,
|
|
unref,
|
|
watch,
|
|
h,
|
|
onMounted,
|
|
} from 'vue';
|
|
import { useRouter, useRoute } from 'vue-router';
|
|
import components from './components';
|
|
import {
|
|
NDialogProvider,
|
|
useDialog,
|
|
useMessage,
|
|
NAvatar,
|
|
NTag,
|
|
NIcon,
|
|
useNotification,
|
|
NotificationReactive,
|
|
NButton,
|
|
} from 'naive-ui';
|
|
import { TABS_ROUTES } from '@/store/mutation-types';
|
|
import { useUserStore } from '@/store/modules/user';
|
|
import { useLockscreenStore } from '@/store/modules/lockscreen';
|
|
import ProjectSetting from './ProjectSetting.vue';
|
|
import { AsideMenu } from '@/layout/components/Menu';
|
|
import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
|
|
import { NotificationsOutline as NotificationsIcon } from '@vicons/ionicons5';
|
|
import SystemMessage from './SystemMessage.vue';
|
|
import { notificationStoreWidthOut } from '@/store/modules/notification';
|
|
import { getIcon } from '@/enums/systemMessageEnum';
|
|
|
|
export default defineComponent({
|
|
name: 'PageHeader',
|
|
components: {
|
|
...components,
|
|
NDialogProvider,
|
|
ProjectSetting,
|
|
AsideMenu,
|
|
SystemMessage,
|
|
},
|
|
props: {
|
|
collapsed: {
|
|
type: Boolean,
|
|
},
|
|
inverted: {
|
|
type: Boolean,
|
|
},
|
|
},
|
|
setup(props, { emit }) {
|
|
const userStore = useUserStore();
|
|
const notificationStore = notificationStoreWidthOut();
|
|
const useLockscreen = useLockscreenStore();
|
|
const message = useMessage();
|
|
const dialog = useDialog();
|
|
const {
|
|
getNavMode,
|
|
getNavTheme,
|
|
getHeaderSetting,
|
|
getMenuSetting,
|
|
getCrumbsSetting,
|
|
getIsMobile,
|
|
} = useProjectSetting();
|
|
|
|
// const { username, avatar } = userStore?.info || {};
|
|
const drawerSetting = ref();
|
|
|
|
const state = reactive({
|
|
// username: username || '',
|
|
// avatar: avatar || '',
|
|
fullscreenIcon: 'FullscreenOutlined',
|
|
navMode: getNavMode,
|
|
navTheme: getNavTheme,
|
|
headerSetting: getHeaderSetting,
|
|
crumbsSetting: getCrumbsSetting,
|
|
});
|
|
|
|
const getInverted = computed(() => {
|
|
const navTheme = unref(getNavTheme);
|
|
return ['light', 'header-dark'].includes(navTheme) ? props.inverted : !props.inverted;
|
|
});
|
|
|
|
const mixMenu = computed(() => {
|
|
return unref(getMenuSetting).mixMenu;
|
|
});
|
|
|
|
const getChangeStyle = computed(() => {
|
|
const { collapsed } = props;
|
|
const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
|
|
return {
|
|
left: collapsed ? `${minMenuWidth}px` : `${menuWidth}px`,
|
|
width: `calc(100% - ${collapsed ? `${minMenuWidth}px` : `${menuWidth}px`})`,
|
|
};
|
|
});
|
|
|
|
const getMenuLocation = computed(() => {
|
|
return 'header';
|
|
});
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
|
|
const generator: any = (routerMap) => {
|
|
return routerMap.map((item) => {
|
|
const currentMenu = {
|
|
...item,
|
|
label: item.meta.title,
|
|
key: item.name,
|
|
disabled: item.path === '/',
|
|
};
|
|
// 是否有子菜单,并递归处理
|
|
if (item.children && item.children.length > 0) {
|
|
// Recursion
|
|
currentMenu.children = generator(item.children, currentMenu);
|
|
}
|
|
return currentMenu;
|
|
});
|
|
};
|
|
|
|
const breadcrumbList = computed(() => {
|
|
return generator(route.matched);
|
|
});
|
|
|
|
const dropdownSelect = (key) => {
|
|
router.push({ name: key });
|
|
};
|
|
|
|
// 刷新页面
|
|
const reloadPage = () => {
|
|
const full = unref(route);
|
|
router.push({
|
|
path: '/redirect' + full.path,
|
|
query: full.query,
|
|
});
|
|
};
|
|
|
|
// 注销登录
|
|
const doLogout = () => {
|
|
dialog.info({
|
|
title: '提示',
|
|
content: '您确定要注销登录吗',
|
|
positiveText: '确定',
|
|
negativeText: '取消',
|
|
onPositiveClick: () => {
|
|
userStore.logout().then(() => {
|
|
message.success('成功注销登录');
|
|
// 移除标签页
|
|
localStorage.removeItem(TABS_ROUTES);
|
|
router
|
|
.replace({
|
|
name: 'Login',
|
|
query: {
|
|
redirect: route.fullPath,
|
|
},
|
|
})
|
|
.finally(() => location.reload());
|
|
});
|
|
},
|
|
onNegativeClick: () => {},
|
|
});
|
|
};
|
|
|
|
// 切换全屏图标
|
|
const toggleFullscreenIcon = () =>
|
|
(state.fullscreenIcon =
|
|
document.fullscreenElement !== null ? 'FullscreenExitOutlined' : 'FullscreenOutlined');
|
|
|
|
// 监听全屏切换事件
|
|
document.addEventListener('fullscreenchange', toggleFullscreenIcon);
|
|
|
|
// 全屏切换
|
|
const toggleFullScreen = () => {
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen();
|
|
} else {
|
|
if (document.exitFullscreen) {
|
|
document.exitFullscreen();
|
|
}
|
|
}
|
|
};
|
|
|
|
// 图标列表
|
|
const iconList = [
|
|
// {
|
|
// icon: 'SearchOutlined',
|
|
// tips: '搜索',
|
|
// },
|
|
{
|
|
icon: 'GithubOutlined',
|
|
tips: 'github',
|
|
eventObject: {
|
|
click: () => window.open('https://github.com/bufanyun/hotgo'),
|
|
},
|
|
},
|
|
{
|
|
icon: 'BellOutlined',
|
|
tips: '我的消息',
|
|
},
|
|
{
|
|
icon: 'LockOutlined',
|
|
tips: '锁屏',
|
|
eventObject: {
|
|
click: () => useLockscreen.setLock(true),
|
|
},
|
|
},
|
|
];
|
|
const avatarOptions = [
|
|
{
|
|
label: '个人设置',
|
|
key: 1,
|
|
},
|
|
{
|
|
label: '注销登录',
|
|
key: 2,
|
|
},
|
|
];
|
|
|
|
//头像下拉菜单
|
|
const avatarSelect = (key) => {
|
|
switch (key) {
|
|
case 1:
|
|
router.push({ name: 'home_account' });
|
|
break;
|
|
case 2:
|
|
doLogout();
|
|
break;
|
|
}
|
|
};
|
|
|
|
function openSetting() {
|
|
const { openDrawer } = drawerSetting.value;
|
|
openDrawer();
|
|
}
|
|
|
|
const notification = useNotification();
|
|
const getMessages = computed(() => {
|
|
return notificationStore.newMessage;
|
|
});
|
|
const nRef = ref<NotificationReactive | null>(null);
|
|
// 监听新消息,推送通知
|
|
watch(
|
|
getMessages,
|
|
(newVal, _oldVal) => {
|
|
if (newVal === null || newVal === undefined) {
|
|
return;
|
|
}
|
|
|
|
nRef.value = notification.create({
|
|
title: newVal.title,
|
|
description:
|
|
newVal.tagTitle === '' || newVal.tagTitle === undefined
|
|
? undefined
|
|
: () =>
|
|
h(
|
|
NTag,
|
|
{
|
|
style: {
|
|
marginRight: '6px',
|
|
},
|
|
type: newVal.tagProps?.type,
|
|
bordered: false,
|
|
},
|
|
{
|
|
default: () => newVal.tagTitle,
|
|
}
|
|
),
|
|
|
|
content: () =>
|
|
newVal.content === '' || newVal.content === undefined
|
|
? undefined
|
|
: h('div', { innerHTML: '<div>' + newVal.content + '</div>' }),
|
|
meta: newVal.createdAt,
|
|
avatar: () =>
|
|
newVal.senderAvatar !== '' || newVal.senderAvatar === undefined
|
|
? h(NAvatar, {
|
|
size: 'small',
|
|
round: true,
|
|
src: newVal.senderAvatar,
|
|
})
|
|
: h(NIcon, null, { default: () => h(getIcon(newVal)) }),
|
|
action: () =>
|
|
h(
|
|
NButton,
|
|
{
|
|
text: true,
|
|
type: 'info',
|
|
onClick: () => {
|
|
(nRef.value as NotificationReactive).destroy();
|
|
router.push({
|
|
name: 'home_message',
|
|
query: {
|
|
type: newVal.type,
|
|
},
|
|
});
|
|
},
|
|
},
|
|
{
|
|
default: () => '查看详情',
|
|
}
|
|
),
|
|
onClose: () => {
|
|
nRef.value = null;
|
|
},
|
|
});
|
|
},
|
|
{ immediate: true, deep: true }
|
|
);
|
|
|
|
const updateMenu = () => {
|
|
emit('update:collapsed', !props.collapsed);
|
|
};
|
|
|
|
onMounted(() => {
|
|
if (notificationStore.getUnreadCount() === 0) {
|
|
notificationStore.pullMessages();
|
|
}
|
|
});
|
|
return {
|
|
...toRefs(state),
|
|
iconList,
|
|
toggleFullScreen,
|
|
doLogout,
|
|
route,
|
|
dropdownSelect,
|
|
avatarOptions,
|
|
getChangeStyle,
|
|
avatarSelect,
|
|
breadcrumbList,
|
|
reloadPage,
|
|
drawerSetting,
|
|
openSetting,
|
|
getInverted,
|
|
getMenuLocation,
|
|
mixMenu,
|
|
NotificationsIcon,
|
|
SystemMessage,
|
|
notificationStore,
|
|
getIsMobile,
|
|
userStore,
|
|
updateMenu,
|
|
};
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.layout-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0;
|
|
height: @header-height;
|
|
box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
|
|
transition: all 0.2s ease-in-out;
|
|
width: 100%;
|
|
z-index: 11;
|
|
|
|
&-left {
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
.logo {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
height: 64px;
|
|
line-height: 64px;
|
|
overflow: hidden;
|
|
white-space: nowrap;
|
|
padding-left: 10px;
|
|
|
|
img {
|
|
width: auto;
|
|
height: 32px;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.title {
|
|
margin-bottom: 0;
|
|
}
|
|
}
|
|
|
|
::v-deep(.ant-breadcrumb span:last-child .link-text) {
|
|
color: #515a6e;
|
|
}
|
|
|
|
.n-breadcrumb {
|
|
display: inline-block;
|
|
}
|
|
|
|
&-menu {
|
|
color: var(--text-color);
|
|
}
|
|
}
|
|
|
|
&-right {
|
|
display: flex;
|
|
align-items: center;
|
|
margin-right: 20px;
|
|
|
|
.avatar {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 64px;
|
|
}
|
|
|
|
> * {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
&-trigger {
|
|
display: inline-block;
|
|
width: 64px;
|
|
height: 64px;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease-in-out;
|
|
|
|
.n-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 64px;
|
|
line-height: 64px;
|
|
}
|
|
|
|
&:hover {
|
|
background: hsla(0, 0%, 100%, 0.08);
|
|
}
|
|
|
|
.anticon {
|
|
font-size: 16px;
|
|
color: #515a6e;
|
|
}
|
|
}
|
|
|
|
&-trigger-min {
|
|
width: auto;
|
|
padding: 0 12px;
|
|
}
|
|
}
|
|
|
|
.layout-header-light {
|
|
background: #fff;
|
|
color: #515a6e;
|
|
|
|
.n-icon {
|
|
color: #515a6e;
|
|
}
|
|
|
|
.layout-header-left {
|
|
::v-deep(.n-breadcrumb .n-breadcrumb-item:last-child .n-breadcrumb-item__link) {
|
|
color: #515a6e;
|
|
}
|
|
}
|
|
|
|
.layout-header-trigger {
|
|
&:hover {
|
|
background: #f8f8f9;
|
|
}
|
|
}
|
|
}
|
|
|
|
.layout-header-fix {
|
|
position: fixed;
|
|
top: 0;
|
|
right: 0;
|
|
left: 200px;
|
|
z-index: 11;
|
|
}
|
|
|
|
::v-deep(.menu-server-link) {
|
|
color: #515a6e;
|
|
|
|
&:hover {
|
|
color: #1890ff;
|
|
}
|
|
}
|
|
|
|
.action-items-wrapper {
|
|
position: relative;
|
|
height: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
z-index: 1;
|
|
|
|
.action-item {
|
|
min-width: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
|
|
&:hover {
|
|
cursor: pointer;
|
|
color: var(--primary-color-hover);
|
|
}
|
|
}
|
|
|
|
.badge-action-item {
|
|
cursor: pointer;
|
|
margin-right: 30px;
|
|
}
|
|
}
|
|
|
|
:deep(.n-input .n-input__border, .n-input .n-input__state-border) {
|
|
border: none;
|
|
border-bottom: 1px solid currentColor;
|
|
}
|
|
|
|
:deep(.el-input__inner) {
|
|
border: none !important;
|
|
height: 35px;
|
|
line-height: 35px;
|
|
color: currentColor !important;
|
|
background-color: transparent !important;
|
|
}
|
|
|
|
:deep(sup) {
|
|
top: 1.3em;
|
|
}
|
|
</style>
|