layout-side.vue 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <script setup lang="ts">
  2. import type { CSSProperties } from 'vue';
  3. // import { onClickOutside } from '@vueuse/core';
  4. import { computed, ref, shallowRef, useSlots, watchEffect } from 'vue';
  5. import { ScrollArea } from '@vben-core/shadcn-ui';
  6. import { useNamespace } from '@vben-core/toolkit';
  7. import { SideCollapseButton, SidePinButton } from './widgets';
  8. interface Props {
  9. /**
  10. * 背景颜色
  11. */
  12. backgroundColor: string;
  13. /**
  14. * 折叠区域高度
  15. * @default 32
  16. */
  17. collapseHeight?: number;
  18. /**
  19. * 折叠宽度
  20. * @default 48
  21. */
  22. collapseWidth?: number;
  23. /**
  24. * 隐藏的dom是否可见
  25. * @default true
  26. */
  27. domVisible?: boolean;
  28. /**
  29. * 扩展区域背景颜色
  30. */
  31. extraBackgroundColor: string;
  32. /**
  33. * 扩展区域宽度
  34. * @default 180
  35. */
  36. extraWidth?: number;
  37. /**
  38. * 固定扩展区域
  39. * @default false
  40. */
  41. fixedExtra?: boolean;
  42. /**
  43. * 头部高度
  44. */
  45. headerHeight: number;
  46. /**
  47. * 是否侧边混合模式
  48. * @default false
  49. */
  50. isSideMixed?: boolean;
  51. /**
  52. * 混合菜单宽度
  53. * @default 80
  54. */
  55. mixedWidth?: number;
  56. /**
  57. * 顶部padding
  58. * @default 60
  59. */
  60. paddingTop?: number;
  61. /**
  62. * 是否显示
  63. * @default true
  64. */
  65. show?: boolean;
  66. /**
  67. * 显示折叠按钮
  68. * @default false
  69. */
  70. showCollapseButton?: boolean;
  71. /**
  72. * 主题
  73. */
  74. theme?: string;
  75. /**
  76. * 宽度
  77. * @default 180
  78. */
  79. width?: number;
  80. /**
  81. * zIndex
  82. * @default 0
  83. */
  84. zIndex?: number;
  85. }
  86. defineOptions({ name: 'LayoutSide' });
  87. const props = withDefaults(defineProps<Props>(), {
  88. collapseHeight: 42,
  89. collapseWidth: 48,
  90. domVisible: true,
  91. extraWidth: 180,
  92. fixedExtra: false,
  93. isSideMixed: false,
  94. mixedWidth: 80,
  95. paddingTop: 60,
  96. show: true,
  97. showCollapseButton: true,
  98. theme: 'dark',
  99. width: 180,
  100. zIndex: 0,
  101. });
  102. const emit = defineEmits<{ leave: [] }>();
  103. const collapse = defineModel<boolean>('collapse');
  104. const extraCollapse = defineModel<boolean>('extraCollapse');
  105. const expandOnHovering = defineModel<boolean>('expandOnHovering');
  106. const expandOnHover = defineModel<boolean>('expandOnHover');
  107. const extraVisible = defineModel<boolean>('extraVisible');
  108. const { b, e, is } = useNamespace('side');
  109. const slots = useSlots();
  110. const asideRef = shallowRef<HTMLDivElement | null>();
  111. const scrolled = ref(false);
  112. const hiddenSideStyle = computed((): CSSProperties => {
  113. return calcMenuWidthStyle(true);
  114. });
  115. const style = computed((): CSSProperties => {
  116. const { isSideMixed, paddingTop, zIndex } = props;
  117. return {
  118. ...calcMenuWidthStyle(false),
  119. paddingTop: `${paddingTop}px`,
  120. zIndex,
  121. ...(isSideMixed && extraVisible.value ? { transition: 'none' } : {}),
  122. };
  123. });
  124. const extraStyle = computed((): CSSProperties => {
  125. const { extraBackgroundColor, extraWidth, show, width, zIndex } = props;
  126. return {
  127. backgroundColor: extraBackgroundColor,
  128. left: `${width}px`,
  129. width: extraVisible.value && show ? `${extraWidth}px` : 0,
  130. zIndex,
  131. };
  132. });
  133. const extraTitleStyle = computed((): CSSProperties => {
  134. const { headerHeight } = props;
  135. return {
  136. height: `${headerHeight - 1}px`,
  137. };
  138. });
  139. const contentWidthStyle = computed((): CSSProperties => {
  140. const { collapseWidth, fixedExtra, isSideMixed, mixedWidth } = props;
  141. if (isSideMixed && fixedExtra) {
  142. return { width: `${collapse.value ? collapseWidth : mixedWidth}px` };
  143. }
  144. return {};
  145. });
  146. const contentStyle = computed((): CSSProperties => {
  147. const { collapseHeight, headerHeight } = props;
  148. return {
  149. height: `calc(100% - ${headerHeight + collapseHeight}px)`,
  150. paddingTop: '8px',
  151. ...contentWidthStyle.value,
  152. };
  153. });
  154. const headerStyle = computed((): CSSProperties => {
  155. const { headerHeight, isSideMixed } = props;
  156. return {
  157. ...(isSideMixed ? { display: 'flex', justifyContent: 'center' } : {}),
  158. height: `${headerHeight}px`,
  159. ...contentWidthStyle.value,
  160. };
  161. });
  162. const extraContentStyle = computed((): CSSProperties => {
  163. const { collapseHeight, headerHeight } = props;
  164. return {
  165. color: 'red',
  166. height: `calc(100% - ${headerHeight + collapseHeight}px)`,
  167. };
  168. });
  169. const collapseStyle = computed((): CSSProperties => {
  170. const { collapseHeight } = props;
  171. return {
  172. height: `${collapseHeight}px`,
  173. };
  174. });
  175. watchEffect(() => {
  176. extraVisible.value = props.fixedExtra ? true : extraVisible.value;
  177. });
  178. function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
  179. const { backgroundColor, extraWidth, fixedExtra, isSideMixed, show, width } =
  180. props;
  181. let widthValue = `${width + (isSideMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
  182. const { collapseWidth } = props;
  183. if (isHiddenDom && expandOnHovering.value && !expandOnHover.value) {
  184. widthValue = `${collapseWidth}px`;
  185. }
  186. return {
  187. ...(widthValue === '0px' ? { overflow: 'hidden' } : {}),
  188. backgroundColor,
  189. flex: `0 0 ${widthValue}`,
  190. marginLeft: show ? 0 : `-${widthValue}`,
  191. maxWidth: widthValue,
  192. minWidth: widthValue,
  193. width: widthValue,
  194. };
  195. }
  196. function handleMouseenter() {
  197. // 未开启和未折叠状态不生效
  198. if (expandOnHover.value) {
  199. return;
  200. }
  201. if (!expandOnHovering.value) {
  202. collapse.value = false;
  203. }
  204. expandOnHovering.value = true;
  205. }
  206. function handleMouseleave() {
  207. emit('leave');
  208. if (expandOnHover.value) {
  209. return;
  210. }
  211. expandOnHovering.value = false;
  212. collapse.value = true;
  213. extraVisible.value = false;
  214. }
  215. function handleScroll(event: Event) {
  216. const target = event.target as HTMLElement;
  217. scrolled.value = (target?.scrollTop ?? 0) > 0;
  218. }
  219. </script>
  220. <template>
  221. <div v-if="domVisible" :class="e('hide')" :style="hiddenSideStyle"></div>
  222. <aside
  223. :class="[b(), is(theme, true)]"
  224. :style="style"
  225. @mouseenter="handleMouseenter"
  226. @mouseleave="handleMouseleave"
  227. >
  228. <SidePinButton
  229. v-if="!collapse && !isSideMixed"
  230. v-model:expand-on-hover="expandOnHover"
  231. :theme="theme"
  232. />
  233. <div v-if="slots.logo" :style="headerStyle">
  234. <slot name="logo"></slot>
  235. </div>
  236. <ScrollArea :style="contentStyle" :on-scroll="handleScroll">
  237. <div :class="[e('shadow'), { scrolled }]"></div>
  238. <slot></slot>
  239. </ScrollArea>
  240. <div :style="collapseStyle"></div>
  241. <SideCollapseButton
  242. v-if="showCollapseButton && !isSideMixed"
  243. v-model:collapse="collapse"
  244. :theme="theme"
  245. />
  246. <div
  247. v-if="isSideMixed"
  248. ref="asideRef"
  249. :class="e('extra')"
  250. class="transition-[width] duration-200"
  251. :style="extraStyle"
  252. >
  253. <SideCollapseButton
  254. v-if="isSideMixed && expandOnHover"
  255. v-model:collapse="extraCollapse"
  256. :theme="theme"
  257. />
  258. <SidePinButton
  259. v-if="!extraCollapse"
  260. v-model:expand-on-hover="expandOnHover"
  261. :theme="theme"
  262. />
  263. <div v-if="!extraCollapse" :style="extraTitleStyle">
  264. <slot name="extra-title"></slot>
  265. </div>
  266. <ScrollArea
  267. :style="extraContentStyle"
  268. :class="e('extra-content')"
  269. :on-scroll="handleScroll"
  270. >
  271. <div :class="[e('shadow'), { scrolled }]"></div>
  272. <slot name="extra"></slot>
  273. </ScrollArea>
  274. </div>
  275. </aside>
  276. </template>
  277. <style scoped lang="scss">
  278. @import '@vben-core/design/global';
  279. @include b('side') {
  280. --color-surface: var(--color-menu);
  281. position: fixed;
  282. top: 0;
  283. left: 0;
  284. height: 100%;
  285. transition: all 0.2s ease 0s;
  286. @include is('dark') {
  287. --color-surface: var(--color-menu-dark);
  288. }
  289. @include e('shadow') {
  290. position: absolute;
  291. top: 0;
  292. z-index: 1;
  293. inline-size: 100%;
  294. block-size: 40px;
  295. height: 50px;
  296. pointer-events: none;
  297. background: linear-gradient(
  298. to bottom,
  299. hsl(var(--color-surface)),
  300. transparent
  301. );
  302. opacity: 0;
  303. transition: opacity 0.15s ease-in-out;
  304. will-change: opacity;
  305. &.scrolled {
  306. opacity: 1;
  307. }
  308. }
  309. @include is('dark') {
  310. .#{$namespace}-side__extra {
  311. &-content {
  312. border-color: hsl(var(--color-dark-border)) !important;
  313. }
  314. }
  315. }
  316. @include e('hide') {
  317. height: 100%;
  318. transition: all 0.2s ease 0s;
  319. }
  320. @include e('extra') {
  321. position: fixed;
  322. top: 0;
  323. height: 100%;
  324. overflow: hidden;
  325. transition: all 0.2s ease 0s;
  326. &-content {
  327. padding: 4px 0;
  328. }
  329. }
  330. }
  331. </style>