layout.vue 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. <script lang="ts" setup>
  2. import type { MenuRecordRaw } from '@vben/types';
  3. import { computed, useSlots, watch } from 'vue';
  4. import { useWatermark } from '@vben/hooks';
  5. import { $t } from '@vben/locales';
  6. import {
  7. preferences,
  8. updatePreferences,
  9. usePreferences,
  10. } from '@vben/preferences';
  11. import { useLockStore, useUserStore } from '@vben/stores';
  12. import { mapTree } from '@vben/utils';
  13. import { VbenAdminLayout } from '@vben-core/layout-ui';
  14. import { Toaster, VbenBackTop, VbenLogo } from '@vben-core/shadcn-ui';
  15. import { Breadcrumb, CheckUpdates, Preferences } from '../widgets';
  16. import { LayoutContent } from './content';
  17. import { Copyright } from './copyright';
  18. import { LayoutFooter } from './footer';
  19. import { LayoutHeader } from './header';
  20. import {
  21. LayoutExtraMenu,
  22. LayoutMenu,
  23. LayoutMixedMenu,
  24. useExtraMenu,
  25. useMixedMenu,
  26. } from './menu';
  27. import { LayoutTabbar } from './tabbar';
  28. defineOptions({ name: 'BasicLayout' });
  29. const emit = defineEmits<{ clearPreferencesAndLogout: [] }>();
  30. const {
  31. isDark,
  32. isHeaderNav,
  33. isMixedNav,
  34. isMobile,
  35. isSideMixedNav,
  36. layout,
  37. sidebarCollapsed,
  38. theme,
  39. } = usePreferences();
  40. const userStore = useUserStore();
  41. const { updateWatermark } = useWatermark();
  42. const lockStore = useLockStore();
  43. const sidebarTheme = computed(() => {
  44. const dark = isDark.value || preferences.theme.semiDarkMenu;
  45. return dark ? 'dark' : 'light';
  46. });
  47. const headerTheme = computed(() => {
  48. const dark = isDark.value || preferences.theme.semiDarkHeader;
  49. return dark ? 'dark' : 'light';
  50. });
  51. const logoClass = computed(() => {
  52. const { collapsedShowTitle } = preferences.sidebar;
  53. const classes: string[] = [];
  54. if (collapsedShowTitle && sidebarCollapsed.value && !isMixedNav.value) {
  55. classes.push('mx-auto');
  56. }
  57. if (isSideMixedNav.value) {
  58. classes.push('flex-center');
  59. }
  60. return classes.join(' ');
  61. });
  62. const isMenuRounded = computed(() => {
  63. return preferences.navigation.styleType === 'rounded';
  64. });
  65. const logoCollapsed = computed(() => {
  66. const shouldCollapse = isHeaderNav.value || isMixedNav.value;
  67. if (shouldCollapse) {
  68. return false;
  69. }
  70. const shouldExpandOnMobile = !sidebarCollapsed.value && isMobile.value;
  71. if (shouldExpandOnMobile) {
  72. return false;
  73. }
  74. return sidebarCollapsed.value || isSideMixedNav.value;
  75. });
  76. const showHeaderNav = computed(() => {
  77. return isHeaderNav.value || isMixedNav.value;
  78. });
  79. // 侧边多列菜单
  80. const {
  81. extraActiveMenu,
  82. extraMenus,
  83. handleDefaultSelect,
  84. handleMenuMouseEnter,
  85. handleMixedMenuSelect,
  86. handleSideMouseLeave,
  87. sidebarExtraVisible,
  88. } = useExtraMenu();
  89. const {
  90. handleMenuSelect,
  91. headerActive,
  92. headerMenus,
  93. sidebarActive,
  94. sidebarMenus,
  95. sidebarVisible,
  96. } = useMixedMenu();
  97. function wrapperMenus(menus: MenuRecordRaw[]) {
  98. return mapTree(menus, (item) => {
  99. return { ...item, name: $t(item.name) };
  100. });
  101. }
  102. function toggleSidebar() {
  103. updatePreferences({
  104. sidebar: {
  105. hidden: !preferences.sidebar.hidden,
  106. },
  107. });
  108. }
  109. function clearPreferencesAndLogout() {
  110. emit('clearPreferencesAndLogout');
  111. }
  112. watch(
  113. () => preferences.app.watermark,
  114. async (val) => {
  115. if (val) {
  116. await updateWatermark({
  117. content: `${preferences.app.name} 用户名: ${userStore.userInfo?.username}`,
  118. });
  119. }
  120. },
  121. {
  122. immediate: true,
  123. },
  124. );
  125. const slots = useSlots();
  126. const headerSlots = computed(() => {
  127. return Object.keys(slots).filter((key) => key.startsWith('header-'));
  128. });
  129. </script>
  130. <template>
  131. <VbenAdminLayout
  132. v-model:sidebar-extra-visible="sidebarExtraVisible"
  133. :content-compact="preferences.app.contentCompact"
  134. :footer-enable="preferences.footer.enable"
  135. :footer-fixed="preferences.footer.fixed"
  136. :header-hidden="preferences.header.hidden"
  137. :header-mode="preferences.header.mode"
  138. :header-theme="headerTheme"
  139. :header-toggle-sidebar-button="preferences.widget.sidebarToggle"
  140. :header-visible="preferences.header.enable"
  141. :is-mobile="preferences.app.isMobile"
  142. :layout="layout"
  143. :sidebar-collapse="preferences.sidebar.collapsed"
  144. :sidebar-collapse-show-title="preferences.sidebar.collapsedShowTitle"
  145. :sidebar-enable="sidebarVisible"
  146. :sidebar-expand-on-hover="preferences.sidebar.expandOnHover"
  147. :sidebar-extra-collapse="preferences.sidebar.extraCollapse"
  148. :sidebar-hidden="preferences.sidebar.hidden"
  149. :sidebar-theme="sidebarTheme"
  150. :sidebar-width="preferences.sidebar.width"
  151. :tabbar-enable="preferences.tabbar.enable"
  152. :tabbar-height="preferences.tabbar.height"
  153. @side-mouse-leave="handleSideMouseLeave"
  154. @toggle-sidebar="toggleSidebar"
  155. @update:sidebar-collapse="
  156. (value: boolean) => updatePreferences({ sidebar: { collapsed: value } })
  157. "
  158. @update:sidebar-enable="
  159. (value: boolean) => updatePreferences({ sidebar: { enable: value } })
  160. "
  161. @update:sidebar-expand-on-hover="
  162. (value: boolean) =>
  163. updatePreferences({ sidebar: { expandOnHover: value } })
  164. "
  165. @update:sidebar-extra-collapse="
  166. (value: boolean) =>
  167. updatePreferences({ sidebar: { extraCollapse: value } })
  168. "
  169. >
  170. <!-- logo -->
  171. <template #logo>
  172. <VbenLogo
  173. v-if="preferences.logo.enable"
  174. :class="logoClass"
  175. :collapsed="logoCollapsed"
  176. :src="preferences.logo.source"
  177. :text="preferences.app.name"
  178. :theme="showHeaderNav ? headerTheme : theme"
  179. />
  180. </template>
  181. <!-- 头部区域 -->
  182. <template #header>
  183. <LayoutHeader :theme="theme">
  184. <template
  185. v-if="!showHeaderNav && preferences.breadcrumb.enable"
  186. #breadcrumb
  187. >
  188. <Breadcrumb
  189. :hide-when-only-one="preferences.breadcrumb.hideOnlyOne"
  190. :show-home="preferences.breadcrumb.showHome"
  191. :show-icon="preferences.breadcrumb.showIcon"
  192. :type="preferences.breadcrumb.styleType"
  193. />
  194. </template>
  195. <template v-if="showHeaderNav" #menu>
  196. <LayoutMenu
  197. :default-active="headerActive"
  198. :menus="wrapperMenus(headerMenus)"
  199. :rounded="isMenuRounded"
  200. :theme="headerTheme"
  201. class="w-full"
  202. mode="horizontal"
  203. @select="handleMenuSelect"
  204. />
  205. </template>
  206. <template #user-dropdown>
  207. <slot name="user-dropdown"></slot>
  208. </template>
  209. <template #notification>
  210. <slot name="notification"></slot>
  211. </template>
  212. <template v-for="item in headerSlots" #[item]>
  213. <slot :name="item"></slot>
  214. </template>
  215. </LayoutHeader>
  216. </template>
  217. <!-- 侧边菜单区域 -->
  218. <template #menu>
  219. <LayoutMenu
  220. :accordion="preferences.navigation.accordion"
  221. :collapse="preferences.sidebar.collapsed"
  222. :collapse-show-title="preferences.sidebar.collapsedShowTitle"
  223. :default-active="sidebarActive"
  224. :menus="wrapperMenus(sidebarMenus)"
  225. :rounded="isMenuRounded"
  226. :theme="sidebarTheme"
  227. mode="vertical"
  228. @select="handleMenuSelect"
  229. />
  230. </template>
  231. <template #mixed-menu>
  232. <!-- :collapse="!preferences.sidebar.collapsedShowTitle" -->
  233. <LayoutMixedMenu
  234. :active-path="extraActiveMenu"
  235. :menus="wrapperMenus(headerMenus)"
  236. :rounded="isMenuRounded"
  237. :theme="sidebarTheme"
  238. @default-select="handleDefaultSelect"
  239. @enter="handleMenuMouseEnter"
  240. @select="handleMixedMenuSelect"
  241. />
  242. </template>
  243. <!-- 侧边额外区域 -->
  244. <template #side-extra>
  245. <LayoutExtraMenu
  246. :accordion="preferences.navigation.accordion"
  247. :collapse="preferences.sidebar.extraCollapse"
  248. :menus="wrapperMenus(extraMenus)"
  249. :rounded="isMenuRounded"
  250. :theme="sidebarTheme"
  251. />
  252. </template>
  253. <template #side-extra-title>
  254. <VbenLogo
  255. v-if="preferences.logo.enable"
  256. :text="preferences.app.name"
  257. :theme="theme"
  258. />
  259. </template>
  260. <template #tabbar>
  261. <LayoutTabbar
  262. v-if="preferences.tabbar.enable"
  263. :show-icon="preferences.tabbar.showIcon"
  264. :theme="theme"
  265. />
  266. </template>
  267. <!-- 主体内容 -->
  268. <template #content>
  269. <LayoutContent />
  270. </template>
  271. <!-- 页脚 -->
  272. <template v-if="preferences.footer.enable" #footer>
  273. <LayoutFooter>
  274. <Copyright
  275. v-if="preferences.copyright.enable"
  276. v-bind="preferences.copyright"
  277. />
  278. </LayoutFooter>
  279. </template>
  280. <template #extra>
  281. <slot name="extra"></slot>
  282. <Toaster />
  283. <CheckUpdates
  284. v-if="preferences.app.enableCheckUpdates"
  285. :check-updates-interval="preferences.app.checkUpdatesInterval"
  286. />
  287. <Transition v-if="preferences.widget.lockScreen" name="slide-up">
  288. <slot v-if="lockStore.isLockScreen" name="lock-screen"></slot>
  289. </Transition>
  290. <template v-if="preferences.app.enablePreferences">
  291. <Preferences
  292. @clear-preferences-and-logout="clearPreferencesAndLogout"
  293. />
  294. </template>
  295. <VbenBackTop />
  296. </template>
  297. </VbenAdminLayout>
  298. </template>