MixSider.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
  1. <template>
  2. <div :class="`${prefixCls}-dom`" :style="getDomStyle"></div>
  3. <div
  4. v-click-outside="handleClickOutside"
  5. :style="getWrapStyle"
  6. :class="[
  7. prefixCls,
  8. getMenuTheme,
  9. {
  10. open: openMenu,
  11. mini: getCollapsed,
  12. },
  13. ]"
  14. v-bind="getMenuEvents"
  15. >
  16. <!-- <AppLogo :showTitle="false" :class="`${prefixCls}-logo`" /> -->
  17. <LayoutTrigger :class="`${prefixCls}-trigger`" />
  18. <ScrollContainer>
  19. <ul :class="`${prefixCls}-module`">
  20. <li
  21. :class="[
  22. `${prefixCls}-module__item `,
  23. {
  24. [`${prefixCls}-module__item--active`]: item.path === activePath,
  25. },
  26. ]"
  27. v-bind="getItemEvents(item)"
  28. v-for="item in menuModules"
  29. :key="item.path"
  30. >
  31. <SimpleMenuTag :item="item" collapseParent dot />
  32. <Icon :class="`${prefixCls}-module__icon`" :size="getCollapsed ? 16 : 20" :icon="item.icon || (item.meta && item.meta.icon)" />
  33. <p :class="`${prefixCls}-module__name`">
  34. {{ t(item.name) }}
  35. </p>
  36. </li>
  37. </ul>
  38. </ScrollContainer>
  39. <div :class="`${prefixCls}-menu-list`" ref="sideRef" :style="getMenuStyle">
  40. <div
  41. v-show="openMenu"
  42. :class="[
  43. `${prefixCls}-menu-list__title`,
  44. {
  45. show: openMenu,
  46. },
  47. ]"
  48. >
  49. <span class="text"> {{ title }}</span>
  50. <Icon :size="16" :icon="getMixSideFixed ? 'ri:pushpin-2-fill' : 'ri:pushpin-2-line'" class="pushpin" @click="handleFixedMenu" />
  51. </div>
  52. <ScrollContainer :class="`${prefixCls}-menu-list__content`">
  53. <SimpleMenu :items="childrenMenus" :theme="getMenuTheme" mixSider @menuClick="handleMenuClick" />
  54. </ScrollContainer>
  55. <div v-show="getShowDragBar && openMenu" :class="`${prefixCls}-drag-bar`" ref="dragBarRef"></div>
  56. </div>
  57. </div>
  58. </template>
  59. <script lang="ts">
  60. import type { Menu } from '/@/router/types';
  61. import type { CSSProperties } from 'vue';
  62. import { computed, defineComponent, onMounted, ref, unref } from 'vue';
  63. import type { RouteLocationNormalized } from 'vue-router';
  64. import { ScrollContainer } from '/@/components/Container';
  65. import { SimpleMenu, SimpleMenuTag } from '/@/components/SimpleMenu';
  66. import { Icon } from '/@/components/Icon';
  67. import { AppLogo } from '/@/components/Application';
  68. import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
  69. import { useDragLine } from './useLayoutSider';
  70. import { useGlobSetting } from '/@/hooks/setting';
  71. import { useDesign } from '/@/hooks/web/useDesign';
  72. import { useI18n } from '/@/hooks/web/useI18n';
  73. import { useGo } from '/@/hooks/web/usePage';
  74. import { SIDE_BAR_MINI_WIDTH, SIDE_BAR_SHOW_TIT_MINI_WIDTH } from '/@/enums/appEnum';
  75. import clickOutside from '/@/directives/clickOutside';
  76. import { getChildrenMenus, getCurrentParentPath, getShallowMenus } from '/@/router/menus';
  77. import { listenerRouteChange } from '/@/logics/mitt/routeChange';
  78. import LayoutTrigger from '../trigger/index.vue';
  79. export default defineComponent({
  80. name: 'LayoutMixSider',
  81. components: {
  82. ScrollContainer,
  83. AppLogo,
  84. SimpleMenu,
  85. Icon,
  86. LayoutTrigger,
  87. SimpleMenuTag,
  88. },
  89. directives: {
  90. clickOutside,
  91. },
  92. setup() {
  93. let menuModules = ref<Menu[]>([]);
  94. const activePath = ref('');
  95. const childrenMenus = ref<Menu[]>([]);
  96. const openMenu = ref(false);
  97. const dragBarRef = ref<ElRef>(null);
  98. const sideRef = ref<ElRef>(null);
  99. const currentRoute = ref<Nullable<RouteLocationNormalized>>(null);
  100. const { prefixCls } = useDesign('layout-mix-sider');
  101. const go = useGo();
  102. const { t } = useI18n();
  103. const {
  104. getMenuWidth,
  105. getCanDrag,
  106. getCloseMixSidebarOnChange,
  107. getMenuTheme,
  108. getMixSideTrigger,
  109. getRealWidth,
  110. getMixSideFixed,
  111. mixSideHasChildren,
  112. setMenuSetting,
  113. getIsMixSidebar,
  114. getCollapsed,
  115. } = useMenuSetting();
  116. const { title } = useGlobSetting();
  117. useDragLine(sideRef, dragBarRef, true);
  118. const getMenuStyle = computed((): CSSProperties => {
  119. return {
  120. width: unref(openMenu) ? `${unref(getMenuWidth)}px` : 0,
  121. left: `${unref(getMixSideWidth)}px`,
  122. };
  123. });
  124. const getIsFixed = computed(() => {
  125. /* eslint-disable-next-line */
  126. mixSideHasChildren.value = unref(childrenMenus).length > 0;
  127. const isFixed = unref(getMixSideFixed) && unref(mixSideHasChildren);
  128. if (isFixed) {
  129. /* eslint-disable-next-line */
  130. openMenu.value = true;
  131. }
  132. return isFixed;
  133. });
  134. const getMixSideWidth = computed(() => {
  135. return unref(getCollapsed) ? SIDE_BAR_MINI_WIDTH : SIDE_BAR_SHOW_TIT_MINI_WIDTH;
  136. });
  137. const getDomStyle = computed((): CSSProperties => {
  138. const fixedWidth = unref(getIsFixed) ? unref(getRealWidth) : 0;
  139. const width = `${unref(getMixSideWidth) + fixedWidth}px`;
  140. return getWrapCommonStyle(width);
  141. });
  142. const getWrapStyle = computed((): CSSProperties => {
  143. const width = `${unref(getMixSideWidth)}px`;
  144. return getWrapCommonStyle(width);
  145. });
  146. const getMenuEvents = computed(() => {
  147. return !unref(getMixSideFixed)
  148. ? {
  149. onMouseleave: () => {
  150. setActive(true);
  151. closeMenu();
  152. },
  153. }
  154. : {};
  155. });
  156. const getShowDragBar = computed(() => unref(getCanDrag));
  157. onMounted(async () => {
  158. menuModules.value = await getShallowMenus();
  159. });
  160. listenerRouteChange((route) => {
  161. currentRoute.value = route;
  162. setActive(true);
  163. if (unref(getCloseMixSidebarOnChange)) {
  164. closeMenu();
  165. }
  166. });
  167. function getWrapCommonStyle(width: string): CSSProperties {
  168. return {
  169. width,
  170. maxWidth: width,
  171. minWidth: width,
  172. flex: `0 0 ${width}`,
  173. };
  174. }
  175. // Process module menu click
  176. async function handleModuleClick(path: string, hover = false) {
  177. const children = await getChildrenMenus(path);
  178. if (unref(activePath) === path) {
  179. if (!hover) {
  180. if (!unref(openMenu)) {
  181. openMenu.value = true;
  182. } else {
  183. closeMenu();
  184. }
  185. } else {
  186. if (!unref(openMenu)) {
  187. openMenu.value = true;
  188. }
  189. }
  190. if (!unref(openMenu)) {
  191. setActive();
  192. }
  193. } else {
  194. openMenu.value = true;
  195. activePath.value = path;
  196. }
  197. if (!children || children.length === 0) {
  198. if (!hover) go(path);
  199. childrenMenus.value = [];
  200. closeMenu();
  201. return;
  202. }
  203. childrenMenus.value = children;
  204. }
  205. // Set the currently active menu and submenu
  206. async function setActive(setChildren = false) {
  207. const path = currentRoute.value?.path;
  208. if (!path) return;
  209. activePath.value = await getCurrentParentPath(path);
  210. // hanldeModuleClick(parentPath);
  211. if (unref(getIsMixSidebar)) {
  212. const activeMenu = unref(menuModules).find((item) => item.path === unref(activePath));
  213. const p = activeMenu?.path;
  214. if (p) {
  215. const children = await getChildrenMenus(p);
  216. if (setChildren) {
  217. childrenMenus.value = children;
  218. if (unref(getMixSideFixed)) {
  219. openMenu.value = children.length > 0;
  220. }
  221. }
  222. if (children.length === 0) {
  223. childrenMenus.value = [];
  224. }
  225. }
  226. }
  227. }
  228. function handleMenuClick(path: string) {
  229. go(path);
  230. }
  231. function handleClickOutside() {
  232. setActive(true);
  233. closeMenu();
  234. }
  235. function getItemEvents(item: Menu) {
  236. if (unref(getMixSideTrigger) === 'hover') {
  237. return {
  238. onMouseenter: () => handleModuleClick(item.path, true),
  239. onClick: async () => {
  240. const children = await getChildrenMenus(item.path);
  241. if (item.path && (!children || children.length === 0)) go(item.path);
  242. },
  243. };
  244. }
  245. return {
  246. onClick: () => handleModuleClick(item.path),
  247. };
  248. }
  249. function handleFixedMenu() {
  250. setMenuSetting({
  251. mixSideFixed: !unref(getIsFixed),
  252. });
  253. }
  254. // Close menu
  255. function closeMenu() {
  256. if (!unref(getIsFixed)) {
  257. openMenu.value = false;
  258. }
  259. }
  260. return {
  261. t,
  262. prefixCls,
  263. menuModules,
  264. handleModuleClick: handleModuleClick,
  265. activePath,
  266. childrenMenus: childrenMenus,
  267. getShowDragBar,
  268. handleMenuClick,
  269. getMenuStyle,
  270. handleClickOutside,
  271. sideRef,
  272. dragBarRef,
  273. title,
  274. openMenu,
  275. getMenuTheme,
  276. getItemEvents,
  277. getMenuEvents,
  278. getDomStyle,
  279. handleFixedMenu,
  280. getMixSideFixed,
  281. getWrapStyle,
  282. getCollapsed,
  283. };
  284. },
  285. });
  286. </script>
  287. <style lang="less">
  288. @prefix-cls: ~'@{namespace}-layout-mix-sider';
  289. @width: 80px;
  290. .@{prefix-cls} {
  291. position: fixed;
  292. top: 0;
  293. left: 0;
  294. z-index: @layout-mix-sider-fixed-z-index;
  295. height: 100%;
  296. overflow: hidden;
  297. background-color: @sider-dark-bg-color;
  298. transition: all 0.2s ease 0s;
  299. &-dom {
  300. height: 100%;
  301. overflow: hidden;
  302. transition: all 0.2s ease 0s;
  303. }
  304. &-logo {
  305. display: flex;
  306. height: @header-height;
  307. padding-left: 0 !important;
  308. justify-content: center;
  309. img {
  310. width: @logo-width;
  311. height: @logo-width;
  312. }
  313. }
  314. &.light {
  315. .@{prefix-cls}-logo {
  316. border-bottom: 1px solid rgb(238, 238, 238);
  317. }
  318. &.open {
  319. > .scrollbar {
  320. border-right: 1px solid rgb(238, 238, 238);
  321. }
  322. }
  323. .@{prefix-cls}-module {
  324. &__item {
  325. font-weight: normal;
  326. color: rgba(0, 0, 0, 0.65);
  327. &--active {
  328. color: @primary-color;
  329. background-color: unset;
  330. }
  331. }
  332. }
  333. .@{prefix-cls}-menu-list {
  334. &__content {
  335. box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.1);
  336. }
  337. &__title {
  338. .pushpin {
  339. color: rgba(0, 0, 0, 0.35);
  340. &:hover {
  341. color: rgba(0, 0, 0, 0.85);
  342. }
  343. }
  344. }
  345. }
  346. }
  347. @border-color: @sider-dark-lighten-bg-color;
  348. &.dark {
  349. &.open {
  350. .@{prefix-cls}-logo {
  351. // border-bottom: 1px solid @border-color;
  352. }
  353. > .scrollbar {
  354. border-right: 1px solid @border-color;
  355. }
  356. }
  357. .@{prefix-cls}-menu-list {
  358. background-color: @sider-dark-bg-color;
  359. &__title {
  360. color: @white;
  361. border-bottom: none;
  362. border-bottom: 1px solid @border-color;
  363. }
  364. }
  365. }
  366. > .scrollbar {
  367. height: calc(100% - @header-height - 38px);
  368. }
  369. &.mini &-module {
  370. &__name {
  371. display: none;
  372. }
  373. &__icon {
  374. margin-bottom: 0;
  375. }
  376. }
  377. &-module {
  378. position: relative;
  379. padding-top: 1px;
  380. &__item {
  381. position: relative;
  382. padding: 12px 0;
  383. color: rgba(255, 255, 255, 0.65);
  384. text-align: center;
  385. cursor: pointer;
  386. transition: all 0.3s ease;
  387. &:hover {
  388. color: @white;
  389. }
  390. // &:hover,
  391. &--active {
  392. font-weight: 700;
  393. color: @white;
  394. background-color: @sider-dark-darken-bg-color;
  395. &::before {
  396. position: absolute;
  397. top: 0;
  398. left: 0;
  399. width: 3px;
  400. height: 100%;
  401. background-color: @primary-color;
  402. content: '';
  403. }
  404. }
  405. }
  406. &__icon {
  407. margin-bottom: 8px;
  408. font-size: 24px;
  409. transition: all 0.2s;
  410. }
  411. &__name {
  412. margin-bottom: 0;
  413. font-size: 12px;
  414. transition: all 0.2s;
  415. }
  416. }
  417. &-trigger {
  418. position: absolute;
  419. bottom: 0;
  420. left: 0;
  421. width: 100%;
  422. font-size: 14px;
  423. color: rgba(255, 255, 255, 0.65);
  424. text-align: center;
  425. cursor: pointer;
  426. background-color: @trigger-dark-bg-color;
  427. height: 36px;
  428. line-height: 36px;
  429. }
  430. &.light &-trigger {
  431. color: rgba(0, 0, 0, 0.65);
  432. background-color: #fff;
  433. border-top: 1px solid #eee;
  434. }
  435. &-menu-list {
  436. position: fixed;
  437. top: 0;
  438. width: 200px;
  439. height: calc(100%);
  440. background-color: #fff;
  441. transition: all 0.2s;
  442. &__title {
  443. display: flex;
  444. height: @header-height;
  445. // margin-left: -6px;
  446. font-size: 18px;
  447. color: @primary-color;
  448. border-bottom: 1px solid rgb(238, 238, 238);
  449. opacity: 0;
  450. transition: unset;
  451. align-items: center;
  452. justify-content: space-between;
  453. &.show {
  454. min-width: 130px;
  455. opacity: 1;
  456. transition: all 0.5s ease;
  457. }
  458. .pushpin {
  459. margin-right: 6px;
  460. color: rgba(255, 255, 255, 0.65);
  461. cursor: pointer;
  462. &:hover {
  463. color: #fff;
  464. }
  465. }
  466. }
  467. &__content {
  468. height: calc(100% - @header-height) !important;
  469. .scrollbar__wrap {
  470. height: 100%;
  471. overflow-x: hidden;
  472. }
  473. .scrollbar__bar.is-horizontal {
  474. display: none;
  475. }
  476. .ant-menu {
  477. height: 100%;
  478. }
  479. .ant-menu-inline,
  480. .ant-menu-vertical,
  481. .ant-menu-vertical-left {
  482. border-right: 1px solid transparent;
  483. }
  484. }
  485. }
  486. &-drag-bar {
  487. position: absolute;
  488. top: 50px;
  489. right: -1px;
  490. width: 1px;
  491. height: calc(100% - 50px);
  492. cursor: ew-resize;
  493. background-color: #f8f8f9;
  494. border-top: none;
  495. border-bottom: none;
  496. box-shadow: 0 0 4px 0 rgba(28, 36, 56, 0.15);
  497. }
  498. }
  499. </style>