index.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. <template>
  2. <div
  3. class="layout-header"
  4. :class="{
  5. 'layout-header-horizontal': navMode === 'horizontal',
  6. }"
  7. >
  8. <!--顶部菜单-->
  9. <div
  10. v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)"
  11. class="layout-header-left"
  12. >
  13. <div v-if="navMode === 'horizontal'" class="logo">
  14. <img alt="" src="~@/assets/images/logo.png" />
  15. <h2 v-show="!collapsed" class="title">NaiveAdmin</h2>
  16. </div>
  17. </div>
  18. <!--左侧菜单-->
  19. <div v-else class="layout-header-left">
  20. <!-- 菜单收起 -->
  21. <div
  22. id="collapsed-trigger"
  23. class="ml-1 layout-header-trigger layout-header-trigger-min collapsed-trigger"
  24. @click="() => $emit('update:collapsed')"
  25. >
  26. <el-icon class="el-input__icon" v-if="collapsed" :size="18">
  27. <MenuUnfoldOutlined />
  28. </el-icon>
  29. <el-icon class="el-input__icon" v-else :size="18">
  30. <MenuFoldOutlined />
  31. </el-icon>
  32. </div>
  33. <!-- 刷新 -->
  34. <div
  35. v-if="headerSetting.isReload"
  36. class="mr-1 layout-header-trigger layout-header-trigger-min"
  37. @click="reloadPage"
  38. >
  39. <el-icon class="el-input__icon" :size="18">
  40. <ReloadOutlined />
  41. </el-icon>
  42. </div>
  43. <!-- 面包屑 -->
  44. <el-breadcrumb v-if="crumbsSetting.show" class="hidden-sm-only">
  45. <template v-for="routeItem in breadcrumbList" :key="routeItem.name">
  46. <el-breadcrumb-item v-if="routeItem.meta.breadcrumbView != false">
  47. <el-dropdown v-if="routeItem.children.length" :options="routeItem.children">
  48. <span class="link-text">
  49. <el-icon
  50. class="el-input__icon"
  51. v-if="crumbsSetting.showIcon && routeItem.meta.icon"
  52. >
  53. <component :is="routeItem.meta.icon" />
  54. </el-icon>
  55. <Render
  56. :ref="`renderDom_${routeItem.name}`"
  57. :value="getRender(routeItem.meta.title)"
  58. />
  59. </span>
  60. <template #dropdown>
  61. <el-dropdown-menu>
  62. <el-dropdown-item v-for="item in routeItem.children" :key="item.name">
  63. <el-icon class="el-input__icon" v-if="crumbsSetting.showIcon && item.meta.icon">
  64. <component :is="item.meta.icon" />
  65. </el-icon>
  66. <Render
  67. :ref="`renderDom_${routeItem.name}`"
  68. :value="getRender(routeItem.meta.title)"
  69. />
  70. </el-dropdown-item>
  71. </el-dropdown-menu>
  72. </template>
  73. </el-dropdown>
  74. <span v-else class="link-text">
  75. <el-icon class="el-input__icon" v-if="crumbsSetting.showIcon && routeItem.meta.icon">
  76. <component :is="routeItem.meta.icon" />
  77. </el-icon>
  78. <Render
  79. :ref="`renderDom_${routeItem.name}`"
  80. :value="getRender(routeItem.meta.title)"
  81. />
  82. </span>
  83. </el-breadcrumb-item>
  84. </template>
  85. </el-breadcrumb>
  86. </div>
  87. <div class="header-horizontal-menu">
  88. <Sider
  89. v-if="navMode === 'horizontal' || (navMode === 'horizontal-mix' && mixMenu)"
  90. v-model:location="getMenuLocation"
  91. v-bind="siderOption"
  92. :inverted="getInverted"
  93. mode="horizontal"
  94. />
  95. </div>
  96. <div class="layout-header-right">
  97. <div
  98. v-for="item in iconList"
  99. :key="item.icon.name"
  100. v-on="item.eventObject || {}"
  101. class="layout-header-trigger layout-header-trigger-min"
  102. >
  103. <el-tooltip placement="bottom" :content="item.tips">
  104. <el-icon class="el-input__icon" :size="18">
  105. <component :is="item.icon" />
  106. </el-icon>
  107. </el-tooltip>
  108. </div>
  109. <!--切换全屏-->
  110. <div class="layout-header-trigger layout-header-trigger-min">
  111. <el-tooltip placement="bottom" :content="isFullscreen ? '还原' : '全屏'">
  112. <el-icon class="el-input__icon" :size="18" v-if="isFullscreen" @click="toggleFullScreen">
  113. <FullscreenExitOutlined />
  114. </el-icon>
  115. <el-icon class="el-input__icon" :size="18" v-else @click="toggleFullScreen">
  116. <FullscreenOutlined />
  117. </el-icon>
  118. </el-tooltip>
  119. </div>
  120. <!--消息-->
  121. <div class="layout-header-trigger layout-header-trigger-min notifier-plus">
  122. <NotifierProPlus />
  123. </div>
  124. <!-- 个人中心 -->
  125. <div class="layout-header-trigger layout-header-trigger-min">
  126. <el-dropdown trigger="hover" @command="avatarSelect">
  127. <div class="flex items-center">
  128. <h4 class="username">{{ getUsername }}</h4>
  129. <el-divider direction="vertical" />
  130. <h4 class="mr-1 username">{{ getTenantName }}</h4>
  131. <div class="avatar">
  132. <el-avatar round :src="schoolboy" />
  133. </div>
  134. </div>
  135. <template #dropdown>
  136. <el-dropdown-menu>
  137. <el-dropdown-item command="1"
  138. ><el-icon class="el-input__icon" :size="18"><UserSwitchOutlined /></el-icon
  139. >个人设置</el-dropdown-item
  140. >
  141. <el-dropdown-item command="3"
  142. ><el-icon class="el-input__icon" :size="18"><LockClosedOutline /></el-icon
  143. >修改密码</el-dropdown-item
  144. >
  145. <el-dropdown-item command="2"
  146. ><el-icon class="el-input__icon" :size="18"><LogoutOutlined /></el-icon
  147. >退出登录</el-dropdown-item
  148. >
  149. </el-dropdown-menu>
  150. </template>
  151. </el-dropdown>
  152. </div>
  153. <!--设置-->
  154. <div
  155. id="setting-trigger"
  156. class="layout-header-trigger layout-header-trigger-min setting-trigger"
  157. @click="openSetting"
  158. >
  159. <el-tooltip placement="bottom-end" content="项目配置">
  160. <el-icon class="el-input__icon" :size="18" style="font-weight: bold">
  161. <SettingOutlined />
  162. </el-icon>
  163. </el-tooltip>
  164. </div>
  165. </div>
  166. </div>
  167. <!--项目配置-->
  168. <ProjectSetting ref="drawerSetting" />
  169. <!-- 搜索 -->
  170. <AppSearch ref="appSearchRef" />
  171. <!--修改密码-->
  172. <AmendPwd ref="amendPwdRef" />
  173. </template>
  174. <script lang="ts" setup>
  175. import { computed, ref, unref, watch, inject } from 'vue';
  176. import { useRoute, useRouter } from 'vue-router';
  177. import { TABS_ROUTES } from '@/store/mutation-types';
  178. import { useUserStore } from '@/store/modules/user';
  179. import { useLockscreenStore } from '@/store/modules/lockscreen';
  180. import { useProjectSetting } from '@/hooks/setting/useProjectSetting';
  181. import { AppSearch } from '@/components/Application/index';
  182. import ProjectSetting from './ProjectSetting.vue';
  183. import NotifierProPlus from './NotifierProPlus.vue';
  184. import Sider from '../Sider/Sider.vue';
  185. import AmendPwd from './AmendPwd.vue';
  186. import { useGo, useRedo } from '@/hooks/web/usePage';
  187. import {
  188. FullscreenExitOutlined,
  189. FullscreenOutlined,
  190. GithubOutlined,
  191. LockOutlined,
  192. LogoutOutlined,
  193. MenuFoldOutlined,
  194. MenuUnfoldOutlined,
  195. ReloadOutlined,
  196. SearchOutlined,
  197. SettingOutlined,
  198. UserSwitchOutlined,
  199. } from '@vicons/antd';
  200. import { LockClosedOutline } from '@vicons/ionicons5';
  201. import { PageEnum } from '@/enums/pageEnum';
  202. import schoolboy from '@/assets/images/schoolboy.png';
  203. import { useFullscreen } from '@vueuse/core';
  204. import { useAsyncRouteStore } from '@/store/modules/asyncRoute';
  205. import { ElMessageBox, ElMessage } from 'element-plus';
  206. import { useDesignSetting } from '@/hooks/setting/useDesignSetting';
  207. import { Render, getRender } from '@/components/Render';
  208. defineEmits(['update:collapsed']);
  209. const userStore = useUserStore();
  210. const useLockscreen = useLockscreenStore();
  211. const appSearchRef = ref();
  212. const isRefresh = ref(false);
  213. const { getDarkTheme } = useDesignSetting();
  214. const { getNavMode, getNavTheme, getHeaderSetting, getMenuSetting, getCrumbsSetting } =
  215. useProjectSetting();
  216. const props = defineProps({
  217. inverted: {
  218. type: Boolean,
  219. },
  220. });
  221. const go = useGo();
  222. const BASE_LOGIN_NAME = PageEnum.BASE_LOGIN_NAME;
  223. const drawerSetting = ref();
  224. const amendPwdRef = ref();
  225. // const username = userStore?.info ? ref(userStore?.info.username) : '';
  226. const collapsed = inject('collapsed');
  227. const navMode = getNavMode;
  228. const headerSetting = getHeaderSetting;
  229. const crumbsSetting = getCrumbsSetting;
  230. const getUsername = computed(() => {
  231. return userStore.getUserInfo.username;
  232. });
  233. const getTenantName = computed(() => {
  234. return userStore.getUserInfo.tenantName;
  235. });
  236. const getInverted = computed(() => {
  237. const navTheme = unref(getNavTheme);
  238. return ['light', 'header-dark'].includes(navTheme) ? props.inverted : !props.inverted;
  239. });
  240. const siderOption = computed(() => {
  241. const navTheme = unref(getNavTheme);
  242. let backgroundColor = '#fff';
  243. let textColor = '#333';
  244. if (unref(getDarkTheme)) {
  245. backgroundColor = '#18181c';
  246. textColor = '#fff';
  247. } else if (['header-dark'].includes(navTheme)) {
  248. backgroundColor = '#001428';
  249. textColor = '#bbb';
  250. }
  251. return {
  252. backgroundColor,
  253. textColor,
  254. };
  255. });
  256. const mixMenu = computed(() => {
  257. return unref(getMenuSetting).mixMenu;
  258. });
  259. const getMenuLocation = computed(() => {
  260. return 'header';
  261. });
  262. const router = useRouter();
  263. const route = useRoute();
  264. const { isFullscreen, toggle } = useFullscreen();
  265. const asyncRouteStore = useAsyncRouteStore();
  266. const generator: any = (routerMap) => {
  267. return routerMap
  268. .filter((item) => {
  269. return !item.meta?.hidden;
  270. })
  271. .map((item) => {
  272. const currentMenu = {
  273. ...item,
  274. label: item.meta.title,
  275. key: item.name,
  276. disabled: item.path === '/',
  277. props: {
  278. onClick: () => {
  279. go(item, false);
  280. },
  281. },
  282. };
  283. // 是否有子菜单,并递归处理
  284. if (item.children && item.children.length > 0) {
  285. // Recursion
  286. currentMenu.children = generator(item.children, currentMenu);
  287. }
  288. return currentMenu;
  289. });
  290. };
  291. watch(
  292. () => route.fullPath,
  293. (to) => {
  294. isRefresh.value = to.indexOf('/redirect/') != -1;
  295. },
  296. { immediate: true },
  297. );
  298. // eslint-disable-next-line vue/return-in-computed-property
  299. const breadcrumbList = computed(() => {
  300. if (!isRefresh.value) return generator(route.matched);
  301. });
  302. // 刷新页面
  303. async function reloadPage() {
  304. const redo = useRedo(router);
  305. await redo();
  306. }
  307. // 退出登录
  308. const doLogout = () => {
  309. ElMessageBox.confirm('您确定要退出登录吗', '提示', {
  310. confirmButtonText: '确定',
  311. cancelButtonText: '取消',
  312. type: 'warning',
  313. }).then(() => {
  314. userStore.logout().then(() => {
  315. ElMessage({
  316. type: 'success',
  317. message: '成功退出登录',
  318. });
  319. // 移除标签页
  320. localStorage.removeItem(TABS_ROUTES);
  321. asyncRouteStore.setDynamicAddedRoute(false);
  322. router.replace({
  323. name: BASE_LOGIN_NAME,
  324. query: {
  325. redirect: route.fullPath,
  326. },
  327. });
  328. });
  329. });
  330. };
  331. // 全屏切换
  332. const toggleFullScreen = () => {
  333. toggle();
  334. };
  335. // 图标列表
  336. const iconList = [
  337. {
  338. icon: SearchOutlined,
  339. tips: '搜索',
  340. eventObject: {
  341. click: () => openAppSearch(),
  342. },
  343. },
  344. {
  345. icon: GithubOutlined,
  346. tips: 'github',
  347. eventObject: {
  348. click: () => window.open('https://github.com/jekip/naive-ui-admin'),
  349. },
  350. },
  351. {
  352. icon: LockOutlined,
  353. tips: '锁屏',
  354. eventObject: {
  355. click: () => useLockscreen.setLock(true),
  356. },
  357. },
  358. ];
  359. //头像下拉菜单
  360. const avatarSelect = (command) => {
  361. switch (command) {
  362. case '1':
  363. router.push({ name: 'Setting' });
  364. break;
  365. case '2':
  366. doLogout();
  367. break;
  368. case '3':
  369. amendPwdRef.value.showModal();
  370. break;
  371. }
  372. };
  373. function openSetting() {
  374. const { openDrawer } = drawerSetting.value;
  375. openDrawer();
  376. }
  377. function openAppSearch() {
  378. appSearchRef.value && appSearchRef.value.show();
  379. }
  380. </script>
  381. <style lang="scss" scoped>
  382. .layout-header {
  383. display: flex;
  384. align-items: center;
  385. padding: 0;
  386. height: $header-height;
  387. box-shadow: 0 1px 4px rgb(0 21 41 / 8%);
  388. transition: all 0.2s ease-in-out;
  389. flex: 1;
  390. z-index: 11;
  391. :deep(.n-menu--horizontal) {
  392. width: calc(100%);
  393. overflow-x: auto;
  394. }
  395. &-left {
  396. display: flex;
  397. align-items: center;
  398. .logo {
  399. display: flex;
  400. align-items: center;
  401. justify-content: center;
  402. height: 64px;
  403. line-height: 64px;
  404. overflow: hidden;
  405. white-space: nowrap;
  406. padding-left: 10px;
  407. min-width: 130px;
  408. img {
  409. width: auto;
  410. height: 32px;
  411. margin-right: 10px;
  412. }
  413. .title {
  414. margin-bottom: 0;
  415. }
  416. }
  417. :deep(.ant-breadcrumb span:last-child .link-text) {
  418. color: #515a6e;
  419. }
  420. .n-breadcrumb {
  421. display: inline-block;
  422. }
  423. &-menu {
  424. color: var(--n-text-color);
  425. }
  426. }
  427. &-right {
  428. display: flex;
  429. align-items: center;
  430. padding-right: 20px;
  431. .avatar {
  432. display: flex;
  433. align-items: center;
  434. height: 64px;
  435. }
  436. > * {
  437. cursor: pointer;
  438. }
  439. }
  440. &-trigger {
  441. display: inline-block;
  442. width: 64px;
  443. height: 64px;
  444. text-align: center;
  445. cursor: pointer;
  446. transition: all 0.2s ease-in-out;
  447. .n-icon {
  448. display: flex;
  449. align-items: center;
  450. height: 64px;
  451. line-height: 64px;
  452. }
  453. &:hover {
  454. background: hsla(0, 0%, 100%, 0.08);
  455. }
  456. .anticon {
  457. font-size: 16px;
  458. color: #515a6e;
  459. }
  460. }
  461. &-trigger-min {
  462. width: auto;
  463. padding: 0 12px;
  464. display: flex;
  465. align-items: center;
  466. }
  467. .header-horizontal-menu {
  468. flex: 1;
  469. overflow: hidden;
  470. }
  471. }
  472. .layout-header-horizontal {
  473. :deep(.n-menu--horizontal) {
  474. width: calc(100% - 130px);
  475. overflow-x: auto;
  476. }
  477. }
  478. .layout-header-light {
  479. background: #fff;
  480. color: #515a6e;
  481. .n-icon {
  482. color: #515a6e;
  483. }
  484. .layout-header-left {
  485. :deep(.n-breadcrumb .n-breadcrumb-item:last-child .n-breadcrumb-item__link) {
  486. color: #515a6e;
  487. }
  488. }
  489. .layout-header-trigger {
  490. &:hover {
  491. background: #f8f8f9;
  492. }
  493. }
  494. }
  495. .layout-header-fix {
  496. position: fixed;
  497. top: 0;
  498. right: 0;
  499. left: 200px;
  500. z-index: 11;
  501. }
  502. .notifier-plus {
  503. display: flex;
  504. align-items: center;
  505. }
  506. </style>