layout.vue 9.1 KB

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