layout-sidebar.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <script setup lang="ts">
  2. import type { CSSProperties } from 'vue';
  3. import { computed, shallowRef, useSlots, watchEffect } from 'vue';
  4. import { useResizable } from '@vben-core/composables';
  5. import { VbenScrollbar } from '@vben-core/shadcn-ui';
  6. import { useScrollLock } from '@vueuse/core';
  7. import { SidebarCollapseButton, SidebarFixedButton } from './widgets';
  8. interface Props {
  9. /**
  10. * 折叠区域高度
  11. * @default 42
  12. */
  13. collapseHeight?: number;
  14. /**
  15. * 折叠宽度
  16. * @default 48
  17. */
  18. collapseWidth?: number;
  19. /**
  20. * 隐藏的dom是否可见
  21. * @default true
  22. */
  23. domVisible?: boolean;
  24. /**
  25. * 扩展区域宽度
  26. */
  27. extraWidth: number;
  28. /**
  29. * 固定扩展区域
  30. * @default false
  31. */
  32. fixedExtra?: boolean;
  33. /**
  34. * 头部高度
  35. */
  36. headerHeight: number;
  37. /**
  38. * 是否侧边混合模式
  39. * @default false
  40. */
  41. isSidebarMixed?: boolean;
  42. /**
  43. * 顶部margin
  44. * @default 60
  45. */
  46. marginTop?: number;
  47. /**
  48. * 混合菜单宽度
  49. * @default 80
  50. */
  51. mixedWidth?: number;
  52. /**
  53. * 顶部padding
  54. * @default 60
  55. */
  56. paddingTop?: number;
  57. /**
  58. * 是否显示
  59. * @default true
  60. */
  61. show?: boolean;
  62. /**
  63. * 显示折叠按钮
  64. * @default true
  65. */
  66. showCollapseButton?: boolean;
  67. /**
  68. * 显示固定按钮
  69. * @default true
  70. */
  71. showFixedButton?: boolean;
  72. /**
  73. * 主题
  74. */
  75. theme: string;
  76. /**
  77. * 子主题
  78. */
  79. themeSub: string;
  80. /**
  81. * 宽度
  82. */
  83. width: number;
  84. /**
  85. * zIndex
  86. * @default 0
  87. */
  88. zIndex?: number;
  89. }
  90. const props = withDefaults(defineProps<Props>(), {
  91. collapseHeight: 42,
  92. collapseWidth: 48,
  93. domVisible: true,
  94. fixedExtra: false,
  95. isSidebarMixed: false,
  96. marginTop: 0,
  97. mixedWidth: 70,
  98. paddingTop: 0,
  99. show: true,
  100. showCollapseButton: true,
  101. showFixedButton: true,
  102. zIndex: 0,
  103. });
  104. const emit = defineEmits<{ leave: []; 'update:width': [value: number] }>();
  105. const collapse = defineModel<boolean>('collapse');
  106. const extraCollapse = defineModel<boolean>('extraCollapse');
  107. const expandOnHovering = defineModel<boolean>('expandOnHovering');
  108. const expandOnHover = defineModel<boolean>('expandOnHover');
  109. const extraVisible = defineModel<boolean>('extraVisible');
  110. const isLocked = useScrollLock(document.body);
  111. const slots = useSlots();
  112. // @ts-expect-error unused
  113. const asideRef = shallowRef<HTMLDivElement | null>();
  114. const hiddenSideStyle = computed((): CSSProperties => calcMenuWidthStyle(true));
  115. const style = computed((): CSSProperties => {
  116. const { isSidebarMixed, marginTop, paddingTop, zIndex } = props;
  117. return {
  118. '--scroll-shadow': 'var(--sidebar)',
  119. ...calcMenuWidthStyle(false),
  120. height: `calc(100% - ${marginTop}px)`,
  121. marginTop: `${marginTop}px`,
  122. paddingTop: `${paddingTop}px`,
  123. zIndex,
  124. ...(isSidebarMixed && extraVisible.value ? { transition: 'none' } : {}),
  125. };
  126. });
  127. const extraStyle = computed((): CSSProperties => {
  128. const { extraWidth, show, width, zIndex } = props;
  129. return {
  130. left: `${width}px`,
  131. width: extraVisible.value && show ? `${extraWidth}px` : 0,
  132. zIndex,
  133. };
  134. });
  135. const extraTitleStyle = computed((): CSSProperties => {
  136. const { headerHeight } = props;
  137. return {
  138. height: `${headerHeight - 1}px`,
  139. };
  140. });
  141. const contentWidthStyle = computed((): CSSProperties => {
  142. const { collapseWidth, fixedExtra, isSidebarMixed, mixedWidth } = props;
  143. if (isSidebarMixed && fixedExtra) {
  144. return { width: `${collapse.value ? collapseWidth : mixedWidth}px` };
  145. }
  146. return {};
  147. });
  148. const contentStyle = computed((): CSSProperties => {
  149. const { collapseHeight, headerHeight } = props;
  150. return {
  151. height: `calc(100% - ${headerHeight + collapseHeight}px)`,
  152. paddingTop: '8px',
  153. ...contentWidthStyle.value,
  154. };
  155. });
  156. const headerStyle = computed((): CSSProperties => {
  157. const { headerHeight, isSidebarMixed } = props;
  158. return {
  159. ...(isSidebarMixed ? { display: 'flex', justifyContent: 'center' } : {}),
  160. height: `${headerHeight - 1}px`,
  161. ...contentWidthStyle.value,
  162. };
  163. });
  164. const extraContentStyle = computed((): CSSProperties => {
  165. const { collapseHeight, headerHeight } = props;
  166. return {
  167. height: `calc(100% - ${headerHeight + collapseHeight}px)`,
  168. };
  169. });
  170. const collapseStyle = computed((): CSSProperties => {
  171. return {
  172. height: `${props.collapseHeight}px`,
  173. };
  174. });
  175. watchEffect(() => {
  176. extraVisible.value = props.fixedExtra ? true : extraVisible.value;
  177. });
  178. function calcMenuWidthStyle(isHiddenDom: boolean): CSSProperties {
  179. const { extraWidth, fixedExtra, isSidebarMixed, show, width } = props;
  180. let widthValue =
  181. width === 0
  182. ? '0px'
  183. : `${width + (isSidebarMixed && fixedExtra && extraVisible.value ? extraWidth : 0)}px`;
  184. const { collapseWidth } = props;
  185. if (isHiddenDom && expandOnHovering.value && !expandOnHover.value) {
  186. widthValue = `${collapseWidth}px`;
  187. }
  188. return {
  189. ...(widthValue === '0px' ? { overflow: 'hidden' } : {}),
  190. flex: `0 0 ${widthValue}`,
  191. marginLeft: show ? 0 : `-${widthValue}`,
  192. maxWidth: widthValue,
  193. minWidth: widthValue,
  194. width: widthValue,
  195. };
  196. }
  197. function handleMouseenter(e: MouseEvent) {
  198. if (e?.offsetX < 10) {
  199. return;
  200. }
  201. // 未开启和未折叠状态不生效
  202. if (expandOnHover.value) {
  203. return;
  204. }
  205. if (!expandOnHovering.value) {
  206. collapse.value = false;
  207. }
  208. if (props.isSidebarMixed) {
  209. isLocked.value = true;
  210. }
  211. expandOnHovering.value = true;
  212. }
  213. function handleMouseleave() {
  214. emit('leave');
  215. if (props.isSidebarMixed) {
  216. isLocked.value = false;
  217. }
  218. if (expandOnHover.value) {
  219. return;
  220. }
  221. expandOnHovering.value = false;
  222. collapse.value = true;
  223. extraVisible.value = false;
  224. }
  225. const { isDragging, startDrag } = useResizable({
  226. min: 160,
  227. max: 320,
  228. onChange: (newWidth) => {
  229. emit('update:width', newWidth);
  230. },
  231. });
  232. const handleDragSidebar = (e: MouseEvent) => {
  233. const { width } = props;
  234. startDrag(e, width);
  235. };
  236. </script>
  237. <template>
  238. <div
  239. v-if="domVisible"
  240. :class="theme"
  241. :style="hiddenSideStyle"
  242. class="h-full transition-all duration-150"
  243. ></div>
  244. <aside
  245. :style="style"
  246. class="fixed left-0 top-0 h-full transition-all duration-150"
  247. :class="{ 'transition-none': isDragging }"
  248. @mouseenter="handleMouseenter"
  249. @mouseleave="handleMouseleave"
  250. >
  251. <div
  252. class="h-full"
  253. :class="[
  254. theme,
  255. {
  256. 'bg-sidebar-deep': isSidebarMixed,
  257. 'border-r border-border bg-sidebar': !isSidebarMixed,
  258. },
  259. ]"
  260. :style="{ width: `${width}px` }"
  261. >
  262. <SidebarFixedButton
  263. v-if="!collapse && !isSidebarMixed && showFixedButton"
  264. v-model:expand-on-hover="expandOnHover"
  265. />
  266. <div v-if="slots.logo" :style="headerStyle">
  267. <slot name="logo"></slot>
  268. </div>
  269. <VbenScrollbar :style="contentStyle" shadow shadow-border>
  270. <slot></slot>
  271. </VbenScrollbar>
  272. <div :style="collapseStyle"></div>
  273. <SidebarCollapseButton
  274. v-if="showCollapseButton && !isSidebarMixed"
  275. v-model:collapsed="collapse"
  276. />
  277. <div
  278. class="absolute inset-y-0 -right-0.5 z-1000 w-1 cursor-col-resize"
  279. @mousedown="handleDragSidebar"
  280. ></div>
  281. </div>
  282. <div
  283. v-if="isSidebarMixed"
  284. ref="asideRef"
  285. :class="[
  286. themeSub,
  287. {
  288. 'border-l': extraVisible,
  289. 'transition-none': isDragging,
  290. },
  291. ]"
  292. :style="extraStyle"
  293. class="fixed top-0 h-full overflow-hidden border-r border-border bg-sidebar transition-all duration-200"
  294. >
  295. <SidebarCollapseButton
  296. v-if="isSidebarMixed && expandOnHover"
  297. v-model:collapsed="extraCollapse"
  298. />
  299. <SidebarFixedButton
  300. v-if="!extraCollapse"
  301. v-model:expand-on-hover="expandOnHover"
  302. />
  303. <div v-if="!extraCollapse" :style="extraTitleStyle" class="pl-2">
  304. <slot name="extra-title"></slot>
  305. </div>
  306. <VbenScrollbar
  307. :style="extraContentStyle"
  308. class="border-border py-2"
  309. shadow
  310. shadow-border
  311. >
  312. <slot name="extra"></slot>
  313. </VbenScrollbar>
  314. </div>
  315. </aside>
  316. </template>