index.vue 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. <template>
  2. <div class="layout-header">
  3. <!--顶部菜单-->
  4. <div
  5. class="layout-header-left"
  6. v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)"
  7. >
  8. <div class="logo" v-if="navMode === 'horizontal'">
  9. <img src="~@/assets/images/logo.png" alt="" />
  10. <h2 v-show="!collapsed" class="title">NaiveUiAdmin</h2>
  11. </div>
  12. <AsideMenu
  13. v-model:collapsed="collapsed"
  14. v-model:location="getMenuLocation"
  15. :inverted="getInverted"
  16. mode="horizontal"
  17. />
  18. </div>
  19. <!--左侧菜单-->
  20. <div class="layout-header-left" v-else>
  21. <!-- 菜单收起 -->
  22. <div
  23. class="ml-1 layout-header-trigger layout-header-trigger-min"
  24. @click="() => $emit('update:collapsed', !collapsed)"
  25. >
  26. <n-icon size="18" v-if="collapsed">
  27. <MenuUnfoldOutlined />
  28. </n-icon>
  29. <n-icon size="18" v-else>
  30. <MenuFoldOutlined />
  31. </n-icon>
  32. </div>
  33. <!-- 刷新 -->
  34. <div
  35. class="mr-1 layout-header-trigger layout-header-trigger-min"
  36. v-if="headerSetting.isReload"
  37. @click="reloadPage"
  38. >
  39. <n-icon size="18">
  40. <ReloadOutlined />
  41. </n-icon>
  42. </div>
  43. <!-- 面包屑 -->
  44. <n-breadcrumb v-if="crumbsSetting.show">
  45. <template v-for="routeItem in breadcrumbList" :key="routeItem.name">
  46. <n-breadcrumb-item>
  47. <n-dropdown
  48. v-if="routeItem.children.length"
  49. :options="routeItem.children"
  50. @select="dropdownSelect"
  51. >
  52. <span class="link-text">
  53. <component
  54. v-if="crumbsSetting.showIcon && routeItem.meta.icon"
  55. :is="routeItem.meta.icon"
  56. />
  57. {{ routeItem.meta.title }}
  58. </span>
  59. </n-dropdown>
  60. <span class="link-text" v-else>
  61. <component
  62. v-if="crumbsSetting.showIcon && routeItem.meta.icon"
  63. :is="routeItem.meta.icon"
  64. />
  65. {{ routeItem.meta.title }}
  66. </span>
  67. </n-breadcrumb-item>
  68. </template>
  69. </n-breadcrumb>
  70. </div>
  71. <div class="layout-header-right">
  72. <div
  73. class="layout-header-trigger layout-header-trigger-min"
  74. v-for="item in iconList"
  75. :key="item.icon.name"
  76. >
  77. <n-popover
  78. placement="bottom"
  79. v-if="item.icon === 'BellOutlined'"
  80. trigger="click"
  81. :width="300"
  82. >
  83. <template #trigger>
  84. <n-badge :value="notificationStore.messages.length" :max="99" processing>
  85. <n-icon size="18">
  86. <BellOutlined />
  87. </n-icon>
  88. </n-badge>
  89. </template>
  90. <PopoverMessage />
  91. </n-popover>
  92. <div v-else>
  93. <n-tooltip placement="bottom">
  94. <template #trigger>
  95. <n-icon size="18">
  96. <component :is="item.icon" v-on="item.eventObject || {}" />
  97. </n-icon>
  98. </template>
  99. <span>{{ item.tips }}</span>
  100. </n-tooltip>
  101. </div>
  102. </div>
  103. <!--切换全屏-->
  104. <div class="layout-header-trigger layout-header-trigger-min">
  105. <n-tooltip placement="bottom">
  106. <template #trigger>
  107. <n-icon size="18">
  108. <component :is="fullscreenIcon" @click="toggleFullScreen" />
  109. </n-icon>
  110. </template>
  111. <span>全屏</span>
  112. </n-tooltip>
  113. </div>
  114. <!-- 系统切换 -->
  115. <div class="layout-header-trigger layout-header-trigger-min" @click="openModal">
  116. <n-tooltip placement="bottom">
  117. <template #trigger>
  118. <n-icon size="18">
  119. <SwapHorizontalOutline />
  120. </n-icon>
  121. </template>
  122. <span>系统切换</span>
  123. </n-tooltip>
  124. </div>
  125. <!-- 个人中心 -->
  126. <div class="layout-header-trigger layout-header-trigger-min">
  127. <n-dropdown trigger="hover" @select="avatarSelect" :options="avatarOptions">
  128. <div class="avatar" v-if="user_avatar">
  129. <n-avatar round :src="user_avatar">
  130. <template #icon>
  131. <UserOutlined />
  132. </template>
  133. </n-avatar>
  134. </div>
  135. <div class="avatar" v-else>
  136. <n-avatar round>
  137. {{ user_name }}
  138. <template #icon>
  139. <UserOutlined />
  140. </template>
  141. </n-avatar>
  142. </div>
  143. </n-dropdown>
  144. </div>
  145. <!--设置-->
  146. <div class="layout-header-trigger layout-header-trigger-min" @click="openSetting">
  147. <n-tooltip placement="bottom-end">
  148. <template #trigger>
  149. <n-icon size="18" style="font-weight: bold">
  150. <SettingOutlined />
  151. </n-icon>
  152. </template>
  153. <span>项目配置</span>
  154. </n-tooltip>
  155. </div>
  156. </div>
  157. </div>
  158. <!--项目配置-->
  159. <ProjectSetting ref="drawerSetting" />
  160. <ServicesModal ref="serviceModalRef" />
  161. </template>
  162. <script lang="ts">
  163. import { defineComponent, reactive, toRefs, ref, computed, unref, watch } from 'vue';
  164. import { useRouter, useRoute } from 'vue-router';
  165. import components from './components';
  166. import { NDialogProvider, useDialog, useMessage } from 'naive-ui';
  167. import { ACCESS_TOKEN, TABS_ROUTES } from '@/store/mutation-types';
  168. import { useUserStore } from '@/store/modules/user';
  169. import { useLockscreenStore } from '@/store/modules/lockscreen';
  170. import ProjectSetting from './ProjectSetting.vue';
  171. import { AsideMenu } from '@/layout/components/Menu';
  172. import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
  173. import { NotificationsOutline as NotificationsIcon } from '@vicons/ionicons5';
  174. import PopoverMessage from './PopoverMessage.vue';
  175. import { notificationStoreWidthOut } from '@/store/modules/notification';
  176. import { useNotification } from 'naive-ui';
  177. import { storage } from '@/utils/Storage';
  178. import ServicesModal from '@/views/serverSelect/modal.vue';
  179. import { SwapHorizontalOutline } from '@vicons/ionicons5';
  180. import { getBaseLoginUrl } from '@/utils/env';
  181. export default defineComponent({
  182. name: 'PageHeader',
  183. components: {
  184. ServicesModal,
  185. ...components,
  186. NDialogProvider,
  187. ProjectSetting,
  188. AsideMenu,
  189. PopoverMessage,
  190. SwapHorizontalOutline,
  191. },
  192. props: {
  193. collapsed: {
  194. type: Boolean,
  195. },
  196. inverted: {
  197. type: Boolean,
  198. },
  199. },
  200. setup(props) {
  201. const userStore = useUserStore();
  202. const notificationStore = notificationStoreWidthOut();
  203. const useLockscreen = useLockscreenStore();
  204. const message = useMessage();
  205. const dialog = useDialog();
  206. const { getNavMode, getNavTheme, getHeaderSetting, getMenuSetting, getCrumbsSetting } =
  207. useProjectSetting();
  208. const drawerSetting = ref();
  209. const state = reactive({
  210. user_name:
  211. userStore.getUserInfo.user_name == undefined ? 'user' : userStore.getUserInfo.user_name,
  212. user_avatar: userStore.getUserInfo.avatar == undefined ? '' : userStore.getUserInfo.avatar,
  213. fullscreenIcon: 'FullscreenOutlined',
  214. navMode: getNavMode,
  215. navTheme: getNavTheme,
  216. headerSetting: getHeaderSetting,
  217. crumbsSetting: getCrumbsSetting,
  218. });
  219. const getInverted = computed(() => {
  220. const navTheme = unref(getNavTheme);
  221. return ['light', 'header-dark'].includes(navTheme) ? props.inverted : !props.inverted;
  222. });
  223. const mixMenu = computed(() => {
  224. return unref(getMenuSetting).mixMenu;
  225. });
  226. const getChangeStyle = computed(() => {
  227. const { collapsed } = props;
  228. const { minMenuWidth, menuWidth }: any = unref(getMenuSetting);
  229. return {
  230. left: collapsed ? `${minMenuWidth}px` : `${menuWidth}px`,
  231. width: `calc(100% - ${collapsed ? `${minMenuWidth}px` : `${menuWidth}px`})`,
  232. };
  233. });
  234. const getMenuLocation = computed(() => {
  235. return 'header';
  236. });
  237. const router = useRouter();
  238. const route = useRoute();
  239. const generator: any = (routerMap) => {
  240. return routerMap.map((item) => {
  241. const currentMenu = {
  242. ...item,
  243. label: item.meta.title,
  244. key: item.name,
  245. disabled: item.path === '/',
  246. };
  247. // 是否有子菜单,并递归处理
  248. if (item.children && item.children.length > 0) {
  249. // Recursion
  250. currentMenu.children = generator(item.children, currentMenu);
  251. }
  252. return currentMenu;
  253. });
  254. };
  255. const breadcrumbList = computed(() => {
  256. return generator(route.matched);
  257. });
  258. const dropdownSelect = (key) => {
  259. router.push({ name: key });
  260. };
  261. // 刷新页面
  262. const reloadPage = () => {
  263. router.push({
  264. path: '/redirect' + unref(route).fullPath,
  265. });
  266. };
  267. // 退出登录
  268. const doLogout = () => {
  269. dialog.info({
  270. title: '提示',
  271. content: '您确定要退出登录吗',
  272. positiveText: '确定',
  273. negativeText: '取消',
  274. onPositiveClick: () => {
  275. userStore.logout().then(() => {
  276. message.success('成功退出登录');
  277. // 移除标签页
  278. localStorage.removeItem(TABS_ROUTES);
  279. storage.remove(ACCESS_TOKEN);
  280. window.open(getBaseLoginUrl(), '_self');
  281. // router
  282. // .replace({
  283. // name: 'Login',
  284. // query: {
  285. // redirect: route.fullPath,
  286. // },
  287. // })
  288. // .finally(() => location.reload());
  289. });
  290. },
  291. onNegativeClick: () => {},
  292. });
  293. };
  294. // 切换全屏图标
  295. const toggleFullscreenIcon = () =>
  296. (state.fullscreenIcon =
  297. document.fullscreenElement !== null ? 'FullscreenExitOutlined' : 'FullscreenOutlined');
  298. // 监听全屏切换事件
  299. document.addEventListener('fullscreenchange', toggleFullscreenIcon);
  300. // 全屏切换
  301. const toggleFullScreen = () => {
  302. if (!document.fullscreenElement) {
  303. document.documentElement.requestFullscreen();
  304. } else {
  305. if (document.exitFullscreen) {
  306. document.exitFullscreen();
  307. }
  308. }
  309. };
  310. // 图标列表
  311. const iconList = [
  312. // {
  313. // icon: 'SearchOutlined',
  314. // tips: '搜索',
  315. // },
  316. // {
  317. // icon: 'GithubOutlined',
  318. // tips: 'github',
  319. // eventObject: {
  320. // click: () => window.open('https://github.com/jekip/naive-ui-admin'),
  321. // },
  322. // },
  323. {
  324. icon: 'BellOutlined',
  325. tips: '系统消息',
  326. },
  327. // {
  328. // icon: 'LockOutlined',
  329. // tips: '锁屏',
  330. // eventObject: {
  331. // click: () => useLockscreen.setLock(true),
  332. // },
  333. // },
  334. ];
  335. const avatarOptions = [
  336. // {
  337. // label: '个人设置',
  338. // key: 1,
  339. // },
  340. {
  341. label: '退出登录',
  342. key: 2,
  343. },
  344. ];
  345. //头像下拉菜单
  346. const avatarSelect = (key) => {
  347. switch (key) {
  348. case 1:
  349. router.push({ name: 'Setting' });
  350. break;
  351. case 2:
  352. doLogout();
  353. break;
  354. }
  355. };
  356. function openSetting() {
  357. const { openDrawer } = drawerSetting.value;
  358. openDrawer();
  359. }
  360. const notification = useNotification();
  361. const getMessages = computed(() => {
  362. return notificationStore.messages;
  363. });
  364. // 监听新消息,推送通知
  365. watch(
  366. getMessages,
  367. (newVal, _oldVal) => {
  368. if (newVal[0] !== undefined) {
  369. let msg = newVal[0];
  370. notification[msg.type]({
  371. content: msg.desc,
  372. meta: msg.pushAt,
  373. duration: 10000,
  374. keepAliveOnHover: true,
  375. });
  376. // switch (msg.type) {
  377. // case 'success':
  378. // message.success(msg.desc, {
  379. // closable: true,
  380. // duration: 4000,
  381. // });
  382. // break;
  383. // case 'error':
  384. // message.error(msg.desc, {
  385. // closable: true,
  386. // duration: 4000,
  387. // });
  388. // break;
  389. // default:
  390. // message.info(msg.desc, {
  391. // closable: true,
  392. // duration: 4000,
  393. // });
  394. // }
  395. }
  396. },
  397. { immediate: true, deep: true }
  398. );
  399. const serviceModalRef = ref();
  400. function openModal() {
  401. const { openDrawer } = serviceModalRef.value;
  402. openDrawer();
  403. }
  404. return {
  405. ...toRefs(state),
  406. iconList,
  407. toggleFullScreen,
  408. doLogout,
  409. route,
  410. dropdownSelect,
  411. avatarOptions,
  412. getChangeStyle,
  413. avatarSelect,
  414. breadcrumbList,
  415. reloadPage,
  416. drawerSetting,
  417. openSetting,
  418. getInverted,
  419. getMenuLocation,
  420. mixMenu,
  421. NotificationsIcon,
  422. PopoverMessage,
  423. notificationStore,
  424. serviceModalRef,
  425. openModal,
  426. };
  427. },
  428. });
  429. </script>
  430. <style lang="less" scoped>
  431. .layout-header {
  432. display: flex;
  433. justify-content: space-between;
  434. align-items: center;
  435. padding: 0;
  436. height: @header-height;
  437. box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
  438. transition: all 0.2s ease-in-out;
  439. width: 100%;
  440. z-index: 11;
  441. &-left {
  442. display: flex;
  443. align-items: center;
  444. .logo {
  445. display: flex;
  446. align-items: center;
  447. justify-content: center;
  448. height: 64px;
  449. line-height: 64px;
  450. overflow: hidden;
  451. white-space: nowrap;
  452. padding-left: 10px;
  453. img {
  454. width: auto;
  455. height: 32px;
  456. margin-right: 10px;
  457. }
  458. .title {
  459. margin-bottom: 0;
  460. }
  461. }
  462. ::v-deep(.ant-breadcrumb span:last-child .link-text) {
  463. color: #515a6e;
  464. }
  465. .n-breadcrumb {
  466. display: inline-block;
  467. }
  468. &-menu {
  469. color: var(--text-color);
  470. }
  471. }
  472. &-right {
  473. display: flex;
  474. align-items: center;
  475. margin-right: 20px;
  476. .avatar {
  477. display: flex;
  478. align-items: center;
  479. height: 64px;
  480. }
  481. > * {
  482. cursor: pointer;
  483. }
  484. }
  485. &-trigger {
  486. display: inline-block;
  487. width: 64px;
  488. height: 64px;
  489. text-align: center;
  490. cursor: pointer;
  491. transition: all 0.2s ease-in-out;
  492. .n-icon {
  493. display: flex;
  494. align-items: center;
  495. height: 64px;
  496. line-height: 64px;
  497. }
  498. &:hover {
  499. background: hsla(0, 0%, 100%, 0.08);
  500. }
  501. .anticon {
  502. font-size: 16px;
  503. color: #515a6e;
  504. }
  505. }
  506. &-trigger-min {
  507. width: auto;
  508. padding: 0 12px;
  509. }
  510. }
  511. .layout-header-light {
  512. background: #fff;
  513. color: #515a6e;
  514. .n-icon {
  515. color: #515a6e;
  516. }
  517. .layout-header-left {
  518. ::v-deep(.n-breadcrumb .n-breadcrumb-item:last-child .n-breadcrumb-item__link) {
  519. color: #515a6e;
  520. }
  521. }
  522. .layout-header-trigger {
  523. &:hover {
  524. background: #f8f8f9;
  525. }
  526. }
  527. }
  528. .layout-header-fix {
  529. position: fixed;
  530. top: 0;
  531. right: 0;
  532. left: 200px;
  533. z-index: 11;
  534. }
  535. //::v-deep(.menu-server-link) {
  536. // color: #515a6e;
  537. //
  538. // &:hover {
  539. // color: #1890ff;
  540. // }
  541. //}
  542. .action-items-wrapper {
  543. position: relative;
  544. height: 100%;
  545. display: flex;
  546. align-items: center;
  547. z-index: 1;
  548. .action-item {
  549. min-width: 40px;
  550. display: flex;
  551. align-items: center;
  552. &:hover {
  553. cursor: pointer;
  554. color: var(--primary-color-hover);
  555. }
  556. }
  557. .badge-action-item {
  558. cursor: pointer;
  559. margin-right: 30px;
  560. }
  561. }
  562. :deep(.n-input .n-input__border, .n-input .n-input__state-border) {
  563. border: none;
  564. border-bottom: 1px solid currentColor;
  565. }
  566. :deep(.el-input__inner) {
  567. border: none !important;
  568. height: 35px;
  569. line-height: 35px;
  570. color: currentColor !important;
  571. background-color: transparent !important;
  572. }
  573. /deep/sup {
  574. top: 1.3em;
  575. }
  576. </style>