mirror of
				https://gitee.com/hhyykk/ipms-sjy-ui.git
				synced 2025-10-31 10:18:43 +08:00 
			
		
		
		
	初始化项目,自 v1.7.1 版本开始
This commit is contained in:
		
							
								
								
									
										78
									
								
								src/layout/Layout.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								src/layout/Layout.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| <script lang="tsx"> | ||||
| import { computed, defineComponent, unref } from 'vue' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { Backtop } from '@/components/Backtop' | ||||
| import { Setting } from '@/layout/components/Setting' | ||||
| import { useRenderLayout } from './components/useRenderLayout' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('layout') | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| // 是否是移动端 | ||||
| const mobile = computed(() => appStore.getMobile) | ||||
|  | ||||
| // 菜单折叠 | ||||
| const collapse = computed(() => appStore.getCollapse) | ||||
|  | ||||
| const layout = computed(() => appStore.getLayout) | ||||
|  | ||||
| const handleClickOutside = () => { | ||||
|   appStore.setCollapse(true) | ||||
| } | ||||
|  | ||||
| const renderLayout = () => { | ||||
|   switch (unref(layout)) { | ||||
|     case 'classic': | ||||
|       const { renderClassic } = useRenderLayout() | ||||
|       return renderClassic() | ||||
|     case 'topLeft': | ||||
|       const { renderTopLeft } = useRenderLayout() | ||||
|       return renderTopLeft() | ||||
|     case 'top': | ||||
|       const { renderTop } = useRenderLayout() | ||||
|       return renderTop() | ||||
|     case 'cutMenu': | ||||
|       const { renderCutMenu } = useRenderLayout() | ||||
|       return renderCutMenu() | ||||
|     default: | ||||
|       break | ||||
|   } | ||||
| } | ||||
|  | ||||
| export default defineComponent({ | ||||
|   name: 'Layout', | ||||
|   setup() { | ||||
|     return () => ( | ||||
|       <section class={[prefixCls, `${prefixCls}__${layout.value}`, 'w-[100%] h-[100%] relative']}> | ||||
|         {mobile.value && !collapse.value ? ( | ||||
|           <div | ||||
|             class="absolute top-0 left-0 w-full h-full opacity-30 z-99 bg-[var(--el-color-black)]" | ||||
|             onClick={handleClickOutside} | ||||
|           ></div> | ||||
|         ) : undefined} | ||||
|  | ||||
|         {renderLayout()} | ||||
|  | ||||
|         <Backtop></Backtop> | ||||
|  | ||||
|         <Setting></Setting> | ||||
|       </section> | ||||
|     ) | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| $prefix-cls: #{$namespace}-layout; | ||||
|  | ||||
| .#{$prefix-cls} { | ||||
|   background-color: var(--app-content-bg-color); | ||||
|   :deep(.#{$elNamespace}-scrollbar__view) { | ||||
|     height: 100% !important; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										51
									
								
								src/layout/components/AppView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/layout/components/AppView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useTagsViewStore } from '@/store/modules/tagsView' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { Footer } from '@/layout/components/Footer' | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const layout = computed(() => appStore.getLayout) | ||||
|  | ||||
| const fixedHeader = computed(() => appStore.getFixedHeader) | ||||
|  | ||||
| const footer = computed(() => appStore.getFooter) | ||||
|  | ||||
| const tagsViewStore = useTagsViewStore() | ||||
|  | ||||
| const getCaches = computed((): string[] => { | ||||
|   return tagsViewStore.getCachedViews | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <section | ||||
|     :class="[ | ||||
|       'p-[var(--app-content-padding)] w-[100%] bg-[var(--app-contnet-bg-color)] dark:bg-[var(--el-bg-color)]', | ||||
|       { | ||||
|         '!min-h-[calc(100%-var(--app-footer-height))]': | ||||
|           fixedHeader && (layout === 'classic' || layout === 'topLeft') && footer, | ||||
|  | ||||
|         '!min-h-[calc(100%-var(--tags-view-height)-var(--top-tool-height)-var(--app-footer-height))]': | ||||
|           ((!fixedHeader && layout === 'classic') || layout === 'top') && footer, | ||||
|  | ||||
|         '!min-h-[calc(100%-var(--tags-view-height)-var(--app-footer-height))]': | ||||
|           !fixedHeader && layout === 'topLeft' && footer, | ||||
|  | ||||
|         '!min-h-[calc(100%-var(--top-tool-height))]': fixedHeader && layout === 'cutMenu' && footer, | ||||
|  | ||||
|         '!min-h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))]': | ||||
|           !fixedHeader && layout === 'cutMenu' && footer | ||||
|       } | ||||
|     ]" | ||||
|   > | ||||
|     <router-view> | ||||
|       <template #default="{ Component, route }"> | ||||
|         <keep-alive :include="getCaches"> | ||||
|           <component :is="Component" :key="route.fullPath" /> | ||||
|         </keep-alive> | ||||
|       </template> | ||||
|     </router-view> | ||||
|   </section> | ||||
|   <Footer v-if="footer" /> | ||||
| </template> | ||||
							
								
								
									
										3
									
								
								src/layout/components/Breadcrumb/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/Breadcrumb/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import Breadcrumb from './src/Breadcrumb.vue' | ||||
|  | ||||
| export { Breadcrumb } | ||||
							
								
								
									
										128
									
								
								src/layout/components/Breadcrumb/src/Breadcrumb.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/layout/components/Breadcrumb/src/Breadcrumb.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,128 @@ | ||||
| <script lang="tsx"> | ||||
| import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus' | ||||
| import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue' | ||||
| import { useRouter } from 'vue-router' | ||||
| import { usePermissionStore } from '@/store/modules/permission' | ||||
| import { filterBreadcrumb } from './helper' | ||||
| import { filter, treeToList } from '@/utils/tree' | ||||
| import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router' | ||||
|  | ||||
| import { Icon } from '@/components/Icon' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('breadcrumb') | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| // 面包屑图标 | ||||
| const breadcrumbIcon = computed(() => appStore.getBreadcrumbIcon) | ||||
|  | ||||
| export default defineComponent({ | ||||
|   name: 'Breadcrumb', | ||||
|   setup() { | ||||
|     const { currentRoute } = useRouter() | ||||
|  | ||||
|     const { t } = useI18n() | ||||
|  | ||||
|     const levelList = ref<AppRouteRecordRaw[]>([]) | ||||
|  | ||||
|     const permissionStore = usePermissionStore() | ||||
|  | ||||
|     const menuRouters = computed(() => { | ||||
|       const routers = permissionStore.getRouters | ||||
|       return filterBreadcrumb(routers) | ||||
|     }) | ||||
|  | ||||
|     const getBreadcrumb = () => { | ||||
|       const currentPath = currentRoute.value.path | ||||
|  | ||||
|       levelList.value = filter<AppRouteRecordRaw>(unref(menuRouters), (node: AppRouteRecordRaw) => { | ||||
|         return node.path === currentPath | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     const renderBreadcrumb = () => { | ||||
|       const breadcrumbList = treeToList<AppRouteRecordRaw[]>(unref(levelList)) | ||||
|       return breadcrumbList.map((v) => { | ||||
|         const disabled = v.redirect === 'noredirect' | ||||
|         const meta = v.meta as RouteMeta | ||||
|         return ( | ||||
|           <ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}> | ||||
|             {meta?.icon && breadcrumbIcon.value ? ( | ||||
|               <> | ||||
|                 <Icon icon={meta.icon} class="mr-[2px]" svgClass="inline-block"></Icon> | ||||
|                 {t(v?.meta?.title)} | ||||
|               </> | ||||
|             ) : ( | ||||
|               t(v?.meta?.title) | ||||
|             )} | ||||
|           </ElBreadcrumbItem> | ||||
|         ) | ||||
|       }) | ||||
|     } | ||||
|  | ||||
|     watch( | ||||
|       () => currentRoute.value, | ||||
|       (route: RouteLocationNormalizedLoaded) => { | ||||
|         if (route.path.startsWith('/redirect/')) { | ||||
|           return | ||||
|         } | ||||
|         getBreadcrumb() | ||||
|       }, | ||||
|       { | ||||
|         immediate: true | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     return () => ( | ||||
|       <ElBreadcrumb separator="/" class={`${prefixCls} flex items-center h-full ml-[10px]`}> | ||||
|         <TransitionGroup appear enter-active-class="animate__animated animate__fadeInRight"> | ||||
|           {renderBreadcrumb()} | ||||
|         </TransitionGroup> | ||||
|       </ElBreadcrumb> | ||||
|     ) | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| $prefix-cls: #{$elNamespace}-breadcrumb; | ||||
|  | ||||
| .#{$prefix-cls} { | ||||
|   :deep(&__item) { | ||||
|     display: flex; | ||||
|     .#{$prefix-cls}__inner { | ||||
|       display: flex; | ||||
|       align-items: center; | ||||
|       color: var(--top-header-text-color); | ||||
|  | ||||
|       &:hover { | ||||
|         color: var(--el-color-primary); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   :deep(&__item):not(:last-child) { | ||||
|     .#{$prefix-cls}__inner { | ||||
|       color: var(--top-header-text-color); | ||||
|  | ||||
|       &:hover { | ||||
|         color: var(--el-color-primary); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   :deep(&__item):last-child { | ||||
|     .#{$prefix-cls}__inner { | ||||
|       color: var(--el-text-color-placeholder); | ||||
|  | ||||
|       &:hover { | ||||
|         color: var(--el-text-color-placeholder); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										31
									
								
								src/layout/components/Breadcrumb/src/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/layout/components/Breadcrumb/src/helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| import { pathResolve } from '@/utils/routerHelper' | ||||
| import type { RouteMeta } from 'vue-router' | ||||
|  | ||||
| export const filterBreadcrumb = ( | ||||
|   routes: AppRouteRecordRaw[], | ||||
|   parentPath = '' | ||||
| ): AppRouteRecordRaw[] => { | ||||
|   const res: AppRouteRecordRaw[] = [] | ||||
|  | ||||
|   for (const route of routes) { | ||||
|     const meta = route?.meta as RouteMeta | ||||
|     if (meta.hidden && !meta.canTo) { | ||||
|       continue | ||||
|     } | ||||
|  | ||||
|     const data: AppRouteRecordRaw = | ||||
|       !meta.alwaysShow && route.children?.length === 1 | ||||
|         ? { ...route.children[0], path: pathResolve(route.path, route.children[0].path) } | ||||
|         : { ...route } | ||||
|  | ||||
|     data.path = pathResolve(parentPath, data.path) | ||||
|  | ||||
|     if (data.children) { | ||||
|       data.children = filterBreadcrumb(data.children, data.path) | ||||
|     } | ||||
|     if (data) { | ||||
|       res.push(data) | ||||
|     } | ||||
|   } | ||||
|   return res | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/layout/components/Collapse/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/Collapse/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import Collapse from './src/Collapse.vue' | ||||
|  | ||||
| export { Collapse } | ||||
							
								
								
									
										34
									
								
								src/layout/components/Collapse/src/Collapse.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/layout/components/Collapse/src/Collapse.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { propTypes } from '@/utils/propTypes' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('collapse') | ||||
|  | ||||
| defineProps({ | ||||
|   color: propTypes.string.def('') | ||||
| }) | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const collapse = computed(() => appStore.getCollapse) | ||||
|  | ||||
| const toggleCollapse = () => { | ||||
|   const collapsed = unref(collapse) | ||||
|   appStore.setCollapse(!collapsed) | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div :class="prefixCls"> | ||||
|     <Icon | ||||
|       :size="18" | ||||
|       :icon="collapse ? 'ep:expand' : 'ep:fold'" | ||||
|       :color="color" | ||||
|       class="cursor-pointer" | ||||
|       @click="toggleCollapse" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
							
								
								
									
										10
									
								
								src/layout/components/ContextMenu/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/layout/components/ContextMenu/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | ||||
| import ContextMenu from './src/ContextMenu.vue' | ||||
| import { ElDropdown } from 'element-plus' | ||||
| import type { RouteLocationNormalizedLoaded } from 'vue-router' | ||||
|  | ||||
| export interface ContextMenuExpose { | ||||
|   elDropdownMenuRef: ComponentRef<typeof ElDropdown> | ||||
|   tagItem: RouteLocationNormalizedLoaded | ||||
| } | ||||
|  | ||||
| export { ContextMenu } | ||||
							
								
								
									
										73
									
								
								src/layout/components/ContextMenu/src/ContextMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/layout/components/ContextMenu/src/ContextMenu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| <script setup lang="ts"> | ||||
| import { PropType } from 'vue' | ||||
|  | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import type { RouteLocationNormalizedLoaded } from 'vue-router' | ||||
| import { contextMenuSchema } from '@/types/contextMenu' | ||||
| import type { ElDropdown } from 'element-plus' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('context-menu') | ||||
|  | ||||
| const { t } = useI18n() | ||||
|  | ||||
| const emit = defineEmits(['visibleChange']) | ||||
|  | ||||
| const props = defineProps({ | ||||
|   schema: { | ||||
|     type: Array as PropType<contextMenuSchema[]>, | ||||
|     default: () => [] | ||||
|   }, | ||||
|   trigger: { | ||||
|     type: String as PropType<'click' | 'hover' | 'focus' | 'contextmenu'>, | ||||
|     default: 'contextmenu' | ||||
|   }, | ||||
|   tagItem: { | ||||
|     type: Object as PropType<RouteLocationNormalizedLoaded>, | ||||
|     default: () => ({}) | ||||
|   } | ||||
| }) | ||||
|  | ||||
| const command = (item: contextMenuSchema) => { | ||||
|   item.command && item.command(item) | ||||
| } | ||||
|  | ||||
| const visibleChange = (visible: boolean) => { | ||||
|   emit('visibleChange', visible, props.tagItem) | ||||
| } | ||||
|  | ||||
| const elDropdownMenuRef = ref<ComponentRef<typeof ElDropdown>>() | ||||
|  | ||||
| defineExpose({ | ||||
|   elDropdownMenuRef, | ||||
|   tagItem: props.tagItem | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <ElDropdown | ||||
|     ref="elDropdownMenuRef" | ||||
|     :class="prefixCls" | ||||
|     :trigger="trigger" | ||||
|     placement="bottom-start" | ||||
|     @command="command" | ||||
|     @visible-change="visibleChange" | ||||
|     popper-class="v-context-menu-popper" | ||||
|   > | ||||
|     <slot></slot> | ||||
|     <template #dropdown> | ||||
|       <ElDropdownMenu> | ||||
|         <ElDropdownItem | ||||
|           v-for="(item, index) in schema" | ||||
|           :key="`dropdown${index}`" | ||||
|           :divided="item.divided" | ||||
|           :disabled="item.disabled" | ||||
|           :command="item" | ||||
|         > | ||||
|           <Icon :icon="item.icon" /> {{ t(item.label) }} | ||||
|         </ElDropdownItem> | ||||
|       </ElDropdownMenu> | ||||
|     </template> | ||||
|   </ElDropdown> | ||||
| </template> | ||||
							
								
								
									
										3
									
								
								src/layout/components/Footer/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/Footer/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import Footer from './src/Footer.vue' | ||||
|  | ||||
| export { Footer } | ||||
							
								
								
									
										21
									
								
								src/layout/components/Footer/src/Footer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/layout/components/Footer/src/Footer.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('footer') | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const title = computed(() => appStore.getTitle) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     :class="prefixCls" | ||||
|     class="text-center text-[var(--el-text-color-placeholder)] bg-[var(--app-contnet-bg-color)] h-[var(--app-footer-height)] leading-[var(--app-footer-height)] dark:bg-[var(--el-bg-color)]" | ||||
|   > | ||||
|     <p style="font-size: 14px">Copyright ©2022-{{ title }}</p> | ||||
|   </div> | ||||
| </template> | ||||
							
								
								
									
										3
									
								
								src/layout/components/LocaleDropdown/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/LocaleDropdown/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import LocaleDropdown from './src/LocaleDropdown.vue' | ||||
|  | ||||
| export { LocaleDropdown } | ||||
							
								
								
									
										50
									
								
								src/layout/components/LocaleDropdown/src/LocaleDropdown.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/layout/components/LocaleDropdown/src/LocaleDropdown.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useLocaleStore } from '@/store/modules/locale' | ||||
| import { useLocale } from '@/hooks/web/useLocale' | ||||
| import { propTypes } from '@/utils/propTypes' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('locale-dropdown') | ||||
|  | ||||
| defineProps({ | ||||
|   color: propTypes.string.def('') | ||||
| }) | ||||
|  | ||||
| const localeStore = useLocaleStore() | ||||
|  | ||||
| const langMap = computed(() => localeStore.getLocaleMap) | ||||
|  | ||||
| const currentLang = computed(() => localeStore.getCurrentLocale) | ||||
|  | ||||
| const setLang = (lang: LocaleType) => { | ||||
|   if (lang === unref(currentLang).lang) return | ||||
|   // 需要重新加载页面让整个语言多初始化 | ||||
|   window.location.reload() | ||||
|   localeStore.setCurrentLocale({ | ||||
|     lang | ||||
|   }) | ||||
|   const { changeLocale } = useLocale() | ||||
|   changeLocale(lang) | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <ElDropdown :class="prefixCls" trigger="click" @command="setLang"> | ||||
|     <Icon | ||||
|       :size="18" | ||||
|       icon="ion:language-sharp" | ||||
|       class="cursor-pointer" | ||||
|       :class="$attrs.class" | ||||
|       :color="color" | ||||
|     /> | ||||
|     <template #dropdown> | ||||
|       <ElDropdownMenu> | ||||
|         <ElDropdownItem v-for="item in langMap" :key="item.lang" :command="item.lang"> | ||||
|           {{ item.name }} | ||||
|         </ElDropdownItem> | ||||
|       </ElDropdownMenu> | ||||
|     </template> | ||||
|   </ElDropdown> | ||||
| </template> | ||||
							
								
								
									
										3
									
								
								src/layout/components/Logo/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/Logo/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import Logo from './src/Logo.vue' | ||||
|  | ||||
| export { Logo } | ||||
							
								
								
									
										87
									
								
								src/layout/components/Logo/src/Logo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/layout/components/Logo/src/Logo.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,87 @@ | ||||
| <script setup lang="ts"> | ||||
| import { ref, watch, computed, onMounted, unref } from 'vue' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('logo') | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const show = ref(true) | ||||
|  | ||||
| const title = computed(() => appStore.getTitle) | ||||
|  | ||||
| const layout = computed(() => appStore.getLayout) | ||||
|  | ||||
| const collapse = computed(() => appStore.getCollapse) | ||||
|  | ||||
| onMounted(() => { | ||||
|   if (unref(collapse)) show.value = false | ||||
| }) | ||||
|  | ||||
| watch( | ||||
|   () => collapse.value, | ||||
|   (collapse: boolean) => { | ||||
|     if (unref(layout) === 'topLeft' || unref(layout) === 'cutMenu') { | ||||
|       show.value = true | ||||
|       return | ||||
|     } | ||||
|     if (!collapse) { | ||||
|       setTimeout(() => { | ||||
|         show.value = !collapse | ||||
|       }, 400) | ||||
|     } else { | ||||
|       show.value = !collapse | ||||
|     } | ||||
|   } | ||||
| ) | ||||
|  | ||||
| watch( | ||||
|   () => layout.value, | ||||
|   (layout) => { | ||||
|     if (layout === 'top' || layout === 'cutMenu') { | ||||
|       show.value = true | ||||
|     } else { | ||||
|       if (unref(collapse)) { | ||||
|         show.value = false | ||||
|       } else { | ||||
|         show.value = true | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| ) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div> | ||||
|     <router-link | ||||
|       :class="[ | ||||
|         prefixCls, | ||||
|         layout !== 'classic' ? `${prefixCls}__Top` : '', | ||||
|         'flex !h-[var(--logo-height)] items-center cursor-pointer justify-center relative', | ||||
|         'dark:bg-[var(--el-bg-color)]' | ||||
|       ]" | ||||
|       to="/" | ||||
|     > | ||||
|       <img | ||||
|         src="@/assets/imgs/logo.png" | ||||
|         class="w-[calc(var(--logo-height)-10px)] h-[calc(var(--logo-height)-10px)]" | ||||
|       /> | ||||
|       <div | ||||
|         v-if="show" | ||||
|         :class="[ | ||||
|           'ml-10px text-16px font-700', | ||||
|           { | ||||
|             'text-[var(--logo-title-text-color)]': layout === 'classic', | ||||
|             'text-[var(--top-header-text-color)]': | ||||
|               layout === 'topLeft' || layout === 'top' || layout === 'cutMenu' | ||||
|           } | ||||
|         ]" | ||||
|       > | ||||
|         {{ title }} | ||||
|       </div> | ||||
|     </router-link> | ||||
|   </div> | ||||
| </template> | ||||
							
								
								
									
										3
									
								
								src/layout/components/Menu/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/Menu/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import Menu from './src/Menu.vue' | ||||
|  | ||||
| export { Menu } | ||||
							
								
								
									
										298
									
								
								src/layout/components/Menu/src/Menu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								src/layout/components/Menu/src/Menu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,298 @@ | ||||
| <script lang="tsx"> | ||||
| import { PropType } from 'vue' | ||||
| import { ElMenu, ElScrollbar } from 'element-plus' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { usePermissionStore } from '@/store/modules/permission' | ||||
| import { useRenderMenuItem } from './components/useRenderMenuItem' | ||||
| import { isUrl } from '@/utils/is' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import { LayoutType } from '@/types/layout' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('menu') | ||||
|  | ||||
| export default defineComponent({ | ||||
|   name: 'Menu', | ||||
|   props: { | ||||
|     menuSelect: { | ||||
|       type: Function as PropType<(index: string) => void>, | ||||
|       default: undefined | ||||
|     } | ||||
|   }, | ||||
|   setup(props) { | ||||
|     const appStore = useAppStore() | ||||
|  | ||||
|     const layout = computed(() => appStore.getLayout) | ||||
|  | ||||
|     const { push, currentRoute } = useRouter() | ||||
|  | ||||
|     const permissionStore = usePermissionStore() | ||||
|  | ||||
|     const menuMode = computed((): 'vertical' | 'horizontal' => { | ||||
|       // 竖 | ||||
|       const vertical: LayoutType[] = ['classic', 'topLeft', 'cutMenu'] | ||||
|  | ||||
|       if (vertical.includes(unref(layout))) { | ||||
|         return 'vertical' | ||||
|       } else { | ||||
|         return 'horizontal' | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     const routers = computed(() => | ||||
|       unref(layout) === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters | ||||
|     ) | ||||
|  | ||||
|     const collapse = computed(() => appStore.getCollapse) | ||||
|  | ||||
|     const uniqueOpened = computed(() => appStore.getUniqueOpened) | ||||
|  | ||||
|     const activeMenu = computed(() => { | ||||
|       const { meta, path } = unref(currentRoute) | ||||
|       // if set path, the sidebar will highlight the path you set | ||||
|       if (meta.activeMenu) { | ||||
|         return meta.activeMenu as string | ||||
|       } | ||||
|       return path | ||||
|     }) | ||||
|  | ||||
|     const menuSelect = (index: string) => { | ||||
|       if (props.menuSelect) { | ||||
|         props.menuSelect(index) | ||||
|       } | ||||
|       // 自定义事件 | ||||
|       if (isUrl(index)) { | ||||
|         window.open(index) | ||||
|       } else { | ||||
|         push(index) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const renderMenuWrap = () => { | ||||
|       if (unref(layout) === 'top') { | ||||
|         return renderMenu() | ||||
|       } else { | ||||
|         return <ElScrollbar>{renderMenu()}</ElScrollbar> | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     const renderMenu = () => { | ||||
|       return ( | ||||
|         <ElMenu | ||||
|           defaultActive={unref(activeMenu)} | ||||
|           mode={unref(menuMode)} | ||||
|           collapse={ | ||||
|             unref(layout) === 'top' || unref(layout) === 'cutMenu' ? false : unref(collapse) | ||||
|           } | ||||
|           uniqueOpened={unref(layout) === 'top' ? false : unref(uniqueOpened)} | ||||
|           backgroundColor="var(--left-menu-bg-color)" | ||||
|           textColor="var(--left-menu-text-color)" | ||||
|           activeTextColor="var(--left-menu-text-active-color)" | ||||
|           onSelect={menuSelect} | ||||
|         > | ||||
|           {{ | ||||
|             default: () => { | ||||
|               const { renderMenuItem } = useRenderMenuItem(unref(menuMode)) | ||||
|               return renderMenuItem(unref(routers)) | ||||
|             } | ||||
|           }} | ||||
|         </ElMenu> | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     return () => ( | ||||
|       <div | ||||
|         id={prefixCls} | ||||
|         class={[ | ||||
|           `${prefixCls} ${prefixCls}__${unref(menuMode)}`, | ||||
|           'h-[100%] overflow-hidden flex-col bg-[var(--left-menu-bg-color)]', | ||||
|           { | ||||
|             'w-[var(--left-menu-min-width)]': unref(collapse) && unref(layout) !== 'cutMenu', | ||||
|             'w-[var(--left-menu-max-width)]': !unref(collapse) && unref(layout) !== 'cutMenu' | ||||
|           } | ||||
|         ]} | ||||
|       > | ||||
|         {renderMenuWrap()} | ||||
|       </div> | ||||
|     ) | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| $prefix-cls: #{$namespace}-menu; | ||||
|  | ||||
| .is-active--after { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   width: 4px; | ||||
|   height: 100%; | ||||
|   background-color: var(--el-color-primary); | ||||
|   content: ''; | ||||
| } | ||||
|  | ||||
| .#{$prefix-cls} { | ||||
|   position: relative; | ||||
|   transition: width var(--transition-time-02); | ||||
|  | ||||
|   &:after { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     height: 100%; | ||||
|     border-left: 1px solid var(--left-menu-border-color); | ||||
|     content: ''; | ||||
|   } | ||||
|  | ||||
|   :deep(.#{$elNamespace}-menu) { | ||||
|     width: 100% !important; | ||||
|     border-right: none; | ||||
|  | ||||
|     // 设置选中时子标题的颜色 | ||||
|     .is-active { | ||||
|       & > .#{$elNamespace}-sub-menu__title { | ||||
|         color: var(--left-menu-text-active-color) !important; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 设置子菜单悬停的高亮和背景色 | ||||
|     .#{$elNamespace}-sub-menu__title, | ||||
|     .#{$elNamespace}-menu-item { | ||||
|       &:hover { | ||||
|         color: var(--left-menu-text-active-color) !important; | ||||
|         background-color: var(--left-menu-bg-color) !important; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 设置选中时的高亮背景和高亮颜色 | ||||
|     .#{$elNamespace}-sub-menu.is-active, | ||||
|     .#{$elNamespace}-menu-item.is-active { | ||||
|       color: var(--left-menu-text-active-color) !important; | ||||
|       background-color: var(--left-menu-bg-active-color) !important; | ||||
|  | ||||
|       &:hover { | ||||
|         background-color: var(--left-menu-bg-active-color) !important; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .#{$elNamespace}-menu-item.is-active { | ||||
|       position: relative; | ||||
|  | ||||
|       &:after { | ||||
|         @extend .is-active--after; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 设置子菜单的背景颜色 | ||||
|     .#{$elNamespace}-menu { | ||||
|       .#{$elNamespace}-sub-menu__title, | ||||
|       .#{$elNamespace}-menu-item:not(.is-active) { | ||||
|         background-color: var(--left-menu-bg-light-color) !important; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 折叠时的最小宽度 | ||||
|   :deep(.#{$elNamespace}-menu--collapse) { | ||||
|     width: var(--left-menu-min-width); | ||||
|  | ||||
|     & > .is-active, | ||||
|     & > .is-active > .#{$elNamespace}-sub-menu__title { | ||||
|       position: relative; | ||||
|       background-color: var(--left-menu-collapse-bg-active-color) !important; | ||||
|  | ||||
|       &:after { | ||||
|         @extend .is-active--after; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 折叠动画的时候,就需要把文字给隐藏掉 | ||||
|   :deep(.horizontal-collapse-transition) { | ||||
|     // transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out !important; | ||||
|     .#{$prefix-cls}__title { | ||||
|       display: none; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 水平菜单 | ||||
|   &__horizontal { | ||||
|     height: calc(var(--top-tool-height)) !important; | ||||
|  | ||||
|     :deep(.#{$elNamespace}-menu--horizontal) { | ||||
|       height: calc(var(--top-tool-height)); | ||||
|       border-bottom: none; | ||||
|       // 重新设置底部高亮颜色 | ||||
|       & > .#{$elNamespace}-sub-menu.is-active { | ||||
|         .#{$elNamespace}-sub-menu__title { | ||||
|           border-bottom-color: var(--el-color-primary) !important; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       .#{$elNamespace}-menu-item.is-active { | ||||
|         position: relative; | ||||
|  | ||||
|         &:after { | ||||
|           display: none !important; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       .#{$prefix-cls}__title { | ||||
|         /* stylelint-disable-next-line */ | ||||
|         max-height: calc(var(--top-tool-height) - 2px) !important; | ||||
|         /* stylelint-disable-next-line */ | ||||
|         line-height: calc(var(--top-tool-height) - 2px); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <style lang="scss"> | ||||
| $prefix-cls: #{$namespace}-menu-popper; | ||||
|  | ||||
| .is-active--after { | ||||
|   position: absolute; | ||||
|   top: 0; | ||||
|   right: 0; | ||||
|   width: 4px; | ||||
|   height: 100%; | ||||
|   background-color: var(--el-color-primary); | ||||
|   content: ''; | ||||
| } | ||||
|  | ||||
| .#{$prefix-cls}--vertical, | ||||
| .#{$prefix-cls}--horizontal { | ||||
|   // 设置选中时子标题的颜色 | ||||
|   .is-active { | ||||
|     & > .el-sub-menu__title { | ||||
|       color: var(--left-menu-text-active-color) !important; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 设置子菜单悬停的高亮和背景色 | ||||
|   .el-sub-menu__title, | ||||
|   .el-menu-item { | ||||
|     &:hover { | ||||
|       color: var(--left-menu-text-active-color) !important; | ||||
|       background-color: var(--left-menu-bg-color) !important; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // 设置选中时的高亮背景 | ||||
|   .el-menu-item.is-active { | ||||
|     position: relative; | ||||
|     background-color: var(--left-menu-bg-active-color) !important; | ||||
|  | ||||
|     &:hover { | ||||
|       background-color: var(--left-menu-bg-active-color) !important; | ||||
|     } | ||||
|  | ||||
|     &:after { | ||||
|       @extend .is-active--after; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @@ -0,0 +1,59 @@ | ||||
| import { ElSubMenu, ElMenuItem } from 'element-plus' | ||||
| import type { RouteMeta } from 'vue-router' | ||||
| import { hasOneShowingChild } from '../helper' | ||||
| import { isUrl } from '@/utils/is' | ||||
| import { useRenderMenuTitle } from './useRenderMenuTitle' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import { pathResolve } from '@/utils/routerHelper' | ||||
|  | ||||
| export const useRenderMenuItem = ( | ||||
|   // allRouters: AppRouteRecordRaw[] = [], | ||||
|   menuMode: 'vertical' | 'horizontal' | ||||
| ) => { | ||||
|   const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => { | ||||
|     return routers.map((v) => { | ||||
|       const meta = (v.meta ?? {}) as RouteMeta | ||||
|       if (!meta.hidden) { | ||||
|         const { oneShowingChild, onlyOneChild } = hasOneShowingChild(v.children, v) | ||||
|         const fullPath = isUrl(v.path) ? v.path : pathResolve(parentPath, v.path) // getAllParentPath<AppRouteRecordRaw>(allRouters, v.path).join('/') | ||||
|  | ||||
|         const { renderMenuTitle } = useRenderMenuTitle() | ||||
|  | ||||
|         if ( | ||||
|           oneShowingChild && | ||||
|           (!onlyOneChild?.children || onlyOneChild?.noShowingChildren) && | ||||
|           !meta?.alwaysShow | ||||
|         ) { | ||||
|           return ( | ||||
|             <ElMenuItem index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}> | ||||
|               {{ | ||||
|                 default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta) | ||||
|               }} | ||||
|             </ElMenuItem> | ||||
|           ) | ||||
|         } else { | ||||
|           const { getPrefixCls } = useDesign() | ||||
|  | ||||
|           const preFixCls = getPrefixCls('menu-popper') | ||||
|           return ( | ||||
|             <ElSubMenu | ||||
|               index={fullPath} | ||||
|               popperClass={ | ||||
|                 menuMode === 'vertical' ? `${preFixCls}--vertical` : `${preFixCls}--horizontal` | ||||
|               } | ||||
|             > | ||||
|               {{ | ||||
|                 title: () => renderMenuTitle(meta), | ||||
|                 default: () => renderMenuItem(v.children!, fullPath) | ||||
|               }} | ||||
|             </ElSubMenu> | ||||
|           ) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     renderMenuItem | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,22 @@ | ||||
| import type { RouteMeta } from 'vue-router' | ||||
| import { Icon } from '@/components/Icon' | ||||
|  | ||||
| export const useRenderMenuTitle = () => { | ||||
|   const renderMenuTitle = (meta: RouteMeta) => { | ||||
|     const { t } = useI18n() | ||||
|     const { title = 'Please set title', icon } = meta | ||||
|  | ||||
|     return icon ? ( | ||||
|       <> | ||||
|         <Icon icon={meta.icon}></Icon> | ||||
|         <span class="v-menu__title">{t(title as string)}</span> | ||||
|       </> | ||||
|     ) : ( | ||||
|       <span class="v-menu__title">{t(title as string)}</span> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     renderMenuTitle | ||||
|   } | ||||
| } | ||||
							
								
								
									
										54
									
								
								src/layout/components/Menu/src/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/layout/components/Menu/src/helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| import type { RouteMeta } from 'vue-router' | ||||
| import { findPath } from '@/utils/tree' | ||||
|  | ||||
| type OnlyOneChildType = AppRouteRecordRaw & { noShowingChildren?: boolean } | ||||
|  | ||||
| interface HasOneShowingChild { | ||||
|   oneShowingChild?: boolean | ||||
|   onlyOneChild?: OnlyOneChildType | ||||
| } | ||||
|  | ||||
| export const getAllParentPath = <T = Recordable>(treeData: T[], path: string) => { | ||||
|   const menuList = findPath(treeData, (n) => n.path === path) as AppRouteRecordRaw[] | ||||
|   return (menuList || []).map((item) => item.path) | ||||
| } | ||||
|  | ||||
| export const hasOneShowingChild = ( | ||||
|   children: AppRouteRecordRaw[] = [], | ||||
|   parent: AppRouteRecordRaw | ||||
| ): HasOneShowingChild => { | ||||
|   const onlyOneChild = ref<OnlyOneChildType>() | ||||
|  | ||||
|   const showingChildren = children.filter((v) => { | ||||
|     const meta = (v.meta ?? {}) as RouteMeta | ||||
|     if (meta.hidden) { | ||||
|       return false | ||||
|     } else { | ||||
|       // Temp set(will be used if only has one showing child) | ||||
|       onlyOneChild.value = v | ||||
|       return true | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   // When there is only one child router, the child router is displayed by default | ||||
|   if (showingChildren.length === 1) { | ||||
|     return { | ||||
|       oneShowingChild: true, | ||||
|       onlyOneChild: unref(onlyOneChild) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Show parent if there are no child router to display | ||||
|   if (!showingChildren.length) { | ||||
|     onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } | ||||
|     return { | ||||
|       oneShowingChild: true, | ||||
|       onlyOneChild: unref(onlyOneChild) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     oneShowingChild: false, | ||||
|     onlyOneChild: unref(onlyOneChild) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/layout/components/Message/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/Message/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import Message from './src/Message.vue' | ||||
|  | ||||
| export { Message } | ||||
							
								
								
									
										113
									
								
								src/layout/components/Message/src/Message.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								src/layout/components/Message/src/Message.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| <script setup lang="ts"> | ||||
| import dayjs from 'dayjs' | ||||
| import * as NotifyMessageApi from '@/api/system/notify/message' | ||||
|  | ||||
| const { push } = useRouter() | ||||
| const activeName = ref('notice') | ||||
| const unreadCount = ref(0) // 未读消息数量 | ||||
| const list = ref<any[]>([]) // 消息列表 | ||||
|  | ||||
| // 获得消息列表 | ||||
| const getList = async () => { | ||||
|   list.value = await NotifyMessageApi.getUnreadNotifyMessageListApi() | ||||
|   // 强制设置 unreadCount 为 0,避免小红点因为轮询太慢,不消除 | ||||
|   unreadCount.value = 0 | ||||
| } | ||||
|  | ||||
| // 获得未读消息数 | ||||
| const getUnreadCount = async () => { | ||||
|   NotifyMessageApi.getUnreadNotifyMessageCountApi().then((data) => { | ||||
|     unreadCount.value = data | ||||
|   }) | ||||
| } | ||||
|  | ||||
| // 跳转我的站内信 | ||||
| const goMyList = () => { | ||||
|   push({ | ||||
|     name: 'MyNotifyMessage' | ||||
|   }) | ||||
| } | ||||
|  | ||||
| // ========== 初始化 ========= | ||||
| onMounted(() => { | ||||
|   // 首次加载小红点 | ||||
|   getUnreadCount() | ||||
|   // 轮询刷新小红点 | ||||
|   setInterval(() => { | ||||
|     getUnreadCount() | ||||
|   }, 1000 * 60 * 2) | ||||
| }) | ||||
| </script> | ||||
| <template> | ||||
|   <div class="message"> | ||||
|     <ElPopover placement="bottom" :width="400" trigger="click"> | ||||
|       <template #reference> | ||||
|         <ElBadge :is-dot="unreadCount > 0" class="item"> | ||||
|           <Icon icon="ep:bell" :size="18" class="cursor-pointer" @click="getList" /> | ||||
|         </ElBadge> | ||||
|       </template> | ||||
|       <ElTabs v-model="activeName"> | ||||
|         <ElTabPane label="我的站内信" name="notice"> | ||||
|           <div class="message-list"> | ||||
|             <template v-for="item in list" :key="item.id"> | ||||
|               <div class="message-item"> | ||||
|                 <img src="@/assets/imgs/avatar.gif" alt="" class="message-icon" /> | ||||
|                 <div class="message-content"> | ||||
|                   <span class="message-title"> | ||||
|                     {{ item.templateNickname }}:{{ item.templateContent }} | ||||
|                   </span> | ||||
|                   <span class="message-date"> | ||||
|                     {{ dayjs(item.createTime).format('YYYY-MM-DD HH:mm:ss') }} | ||||
|                   </span> | ||||
|                 </div> | ||||
|               </div> | ||||
|             </template> | ||||
|           </div> | ||||
|         </ElTabPane> | ||||
|       </ElTabs> | ||||
|       <!-- 更多 --> | ||||
|       <div style="text-align: right; margin-top: 10px"> | ||||
|         <XButton type="primary" preIcon="ep:view" title="查看全部" @click="goMyList" /> | ||||
|       </div> | ||||
|     </ElPopover> | ||||
|   </div> | ||||
| </template> | ||||
| <style scoped lang="scss"> | ||||
| .message-empty { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   align-items: center; | ||||
|   justify-content: center; | ||||
|   height: 260px; | ||||
|   line-height: 45px; | ||||
| } | ||||
| .message-list { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   .message-item { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     padding: 20px 0; | ||||
|     border-bottom: 1px solid var(--el-border-color-light); | ||||
|     &:last-child { | ||||
|       border: none; | ||||
|     } | ||||
|     .message-icon { | ||||
|       width: 40px; | ||||
|       height: 40px; | ||||
|       margin: 0 20px 0 5px; | ||||
|     } | ||||
|     .message-content { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       .message-title { | ||||
|         margin-bottom: 5px; | ||||
|       } | ||||
|       .message-date { | ||||
|         font-size: 12px; | ||||
|         color: var(--el-text-color-secondary); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										3
									
								
								src/layout/components/Screenfull/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/Screenfull/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import Screenfull from './src/Screenfull.vue' | ||||
|  | ||||
| export { Screenfull } | ||||
							
								
								
									
										30
									
								
								src/layout/components/Screenfull/src/Screenfull.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/layout/components/Screenfull/src/Screenfull.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| <script setup lang="ts"> | ||||
| import { Icon } from '@/components/Icon' | ||||
| import { useFullscreen } from '@vueuse/core' | ||||
| import { propTypes } from '@/utils/propTypes' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('screenfull') | ||||
|  | ||||
| defineProps({ | ||||
|   color: propTypes.string.def('') | ||||
| }) | ||||
|  | ||||
| const { toggle, isFullscreen } = useFullscreen() | ||||
|  | ||||
| const toggleFullscreen = () => { | ||||
|   toggle() | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div :class="prefixCls" @click="toggleFullscreen"> | ||||
|     <Icon | ||||
|       :size="18" | ||||
|       :icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'" | ||||
|       :color="color" | ||||
|     /> | ||||
|   </div> | ||||
| </template> | ||||
							
								
								
									
										3
									
								
								src/layout/components/Setting/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/Setting/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import Setting from './src/Setting.vue' | ||||
|  | ||||
| export { Setting } | ||||
							
								
								
									
										298
									
								
								src/layout/components/Setting/src/Setting.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										298
									
								
								src/layout/components/Setting/src/Setting.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,298 @@ | ||||
| <script setup lang="ts"> | ||||
| import { ElMessage } from 'element-plus' | ||||
| import { useCssVar, useClipboard } from '@vueuse/core' | ||||
|  | ||||
| import { CACHE_KEY, useCache } from '@/hooks/web/useCache' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| import { trim, setCssVar } from '@/utils' | ||||
| import { colorIsDark, lighten, hexToRGB } from '@/utils/color' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { ThemeSwitch } from '@/layout/components/ThemeSwitch' | ||||
| import ColorRadioPicker from './components/ColorRadioPicker.vue' | ||||
| import InterfaceDisplay from './components/InterfaceDisplay.vue' | ||||
| import LayoutRadioPicker from './components/LayoutRadioPicker.vue' | ||||
|  | ||||
| const { t } = useI18n() | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
| const prefixCls = getPrefixCls('setting') | ||||
| const layout = computed(() => appStore.getLayout) | ||||
| const drawer = ref(false) | ||||
|  | ||||
| // 主题色相关 | ||||
| const systemTheme = ref(appStore.getTheme.elColorPrimary) | ||||
|  | ||||
| const setSystemTheme = (color: string) => { | ||||
|   setCssVar('--el-color-primary', color) | ||||
|   appStore.setTheme({ elColorPrimary: color }) | ||||
|   const leftMenuBgColor = useCssVar('--left-menu-bg-color', document.documentElement) | ||||
|   setMenuTheme(trim(unref(leftMenuBgColor))) | ||||
| } | ||||
|  | ||||
| // 头部主题相关 | ||||
| const headerTheme = ref(appStore.getTheme.topHeaderBgColor || '') | ||||
|  | ||||
| const setHeaderTheme = (color: string) => { | ||||
|   const isDarkColor = colorIsDark(color) | ||||
|   const textColor = isDarkColor ? '#fff' : 'inherit' | ||||
|   const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6' | ||||
|   const topToolBorderColor = isDarkColor ? color : '#eee' | ||||
|   setCssVar('--top-header-bg-color', color) | ||||
|   setCssVar('--top-header-text-color', textColor) | ||||
|   setCssVar('--top-header-hover-color', textHoverColor) | ||||
|   setCssVar('--top-tool-border-color', topToolBorderColor) | ||||
|   appStore.setTheme({ | ||||
|     topHeaderBgColor: color, | ||||
|     topHeaderTextColor: textColor, | ||||
|     topHeaderHoverColor: textHoverColor, | ||||
|     topToolBorderColor | ||||
|   }) | ||||
|   if (unref(layout) === 'top') { | ||||
|     setMenuTheme(color) | ||||
|   } | ||||
| } | ||||
|  | ||||
| // 菜单主题相关 | ||||
| const menuTheme = ref(appStore.getTheme.leftMenuBgColor || '') | ||||
|  | ||||
| const setMenuTheme = (color: string) => { | ||||
|   const primaryColor = useCssVar('--el-color-primary', document.documentElement) | ||||
|   const isDarkColor = colorIsDark(color) | ||||
|   const theme: Recordable = { | ||||
|     // 左侧菜单边框颜色 | ||||
|     leftMenuBorderColor: isDarkColor ? 'inherit' : '#eee', | ||||
|     // 左侧菜单背景颜色 | ||||
|     leftMenuBgColor: color, | ||||
|     // 左侧菜单浅色背景颜色 | ||||
|     leftMenuBgLightColor: isDarkColor ? lighten(color!, 6) : color, | ||||
|     // 左侧菜单选中背景颜色 | ||||
|     leftMenuBgActiveColor: isDarkColor | ||||
|       ? 'var(--el-color-primary)' | ||||
|       : hexToRGB(unref(primaryColor), 0.1), | ||||
|     // 左侧菜单收起选中背景颜色 | ||||
|     leftMenuCollapseBgActiveColor: isDarkColor | ||||
|       ? 'var(--el-color-primary)' | ||||
|       : hexToRGB(unref(primaryColor), 0.1), | ||||
|     // 左侧菜单字体颜色 | ||||
|     leftMenuTextColor: isDarkColor ? '#bfcbd9' : '#333', | ||||
|     // 左侧菜单选中字体颜色 | ||||
|     leftMenuTextActiveColor: isDarkColor ? '#fff' : 'var(--el-color-primary)', | ||||
|     // logo字体颜色 | ||||
|     logoTitleTextColor: isDarkColor ? '#fff' : 'inherit', | ||||
|     // logo边框颜色 | ||||
|     logoBorderColor: isDarkColor ? color : '#eee' | ||||
|   } | ||||
|   appStore.setTheme(theme) | ||||
|   appStore.setCssVarTheme() | ||||
| } | ||||
| if (layout.value === 'top' && !appStore.getIsDark) { | ||||
|   headerTheme.value = '#fff' | ||||
|   setHeaderTheme('#fff') | ||||
| } | ||||
|  | ||||
| // 监听layout变化,重置一些主题色 | ||||
| watch( | ||||
|   () => layout.value, | ||||
|   (n) => { | ||||
|     if (n === 'top' && !appStore.getIsDark) { | ||||
|       headerTheme.value = '#fff' | ||||
|       setHeaderTheme('#fff') | ||||
|     } else { | ||||
|       setMenuTheme(unref(menuTheme)) | ||||
|     } | ||||
|   } | ||||
| ) | ||||
|  | ||||
| // 拷贝 | ||||
| const copyConfig = async () => { | ||||
|   const { copy, copied, isSupported } = useClipboard({ | ||||
|     source: ` | ||||
|       // 面包屑 | ||||
|       breadcrumb: ${appStore.getBreadcrumb}, | ||||
|       // 面包屑图标 | ||||
|       breadcrumbIcon: ${appStore.getBreadcrumbIcon}, | ||||
|       // 折叠图标 | ||||
|       hamburger: ${appStore.getHamburger}, | ||||
|       // 全屏图标 | ||||
|       screenfull: ${appStore.getScreenfull}, | ||||
|       // 尺寸图标 | ||||
|       size: ${appStore.getSize}, | ||||
|       // 多语言图标 | ||||
|       locale: ${appStore.getLocale}, | ||||
|       // 消息图标 | ||||
|       message: ${appStore.getMessage}, | ||||
|       // 标签页 | ||||
|       tagsView: ${appStore.getTagsView}, | ||||
|       // 标签页图标 | ||||
|       getTagsViewIcon: ${appStore.getTagsViewIcon}, | ||||
|       // logo | ||||
|       logo: ${appStore.getLogo}, | ||||
|       // 菜单手风琴 | ||||
|       uniqueOpened: ${appStore.getUniqueOpened}, | ||||
|       // 固定header | ||||
|       fixedHeader: ${appStore.getFixedHeader}, | ||||
|       // 页脚 | ||||
|       footer: ${appStore.getFooter}, | ||||
|       // 灰色模式 | ||||
|       greyMode: ${appStore.getGreyMode}, | ||||
|       // layout布局 | ||||
|       layout: '${appStore.getLayout}', | ||||
|       // 暗黑模式 | ||||
|       isDark: ${appStore.getIsDark}, | ||||
|       // 组件尺寸 | ||||
|       currentSize: '${appStore.getCurrentSize}', | ||||
|       // 主题相关 | ||||
|       theme: { | ||||
|         // 主题色 | ||||
|         elColorPrimary: '${appStore.getTheme.elColorPrimary}', | ||||
|         // 左侧菜单边框颜色 | ||||
|         leftMenuBorderColor: '${appStore.getTheme.leftMenuBorderColor}', | ||||
|         // 左侧菜单背景颜色 | ||||
|         leftMenuBgColor: '${appStore.getTheme.leftMenuBgColor}', | ||||
|         // 左侧菜单浅色背景颜色 | ||||
|         leftMenuBgLightColor: '${appStore.getTheme.leftMenuBgLightColor}', | ||||
|         // 左侧菜单选中背景颜色 | ||||
|         leftMenuBgActiveColor: '${appStore.getTheme.leftMenuBgActiveColor}', | ||||
|         // 左侧菜单收起选中背景颜色 | ||||
|         leftMenuCollapseBgActiveColor: '${appStore.getTheme.leftMenuCollapseBgActiveColor}', | ||||
|         // 左侧菜单字体颜色 | ||||
|         leftMenuTextColor: '${appStore.getTheme.leftMenuTextColor}', | ||||
|         // 左侧菜单选中字体颜色 | ||||
|         leftMenuTextActiveColor: '${appStore.getTheme.leftMenuTextActiveColor}', | ||||
|         // logo字体颜色 | ||||
|         logoTitleTextColor: '${appStore.getTheme.logoTitleTextColor}', | ||||
|         // logo边框颜色 | ||||
|         logoBorderColor: '${appStore.getTheme.logoBorderColor}', | ||||
|         // 头部背景颜色 | ||||
|         topHeaderBgColor: '${appStore.getTheme.topHeaderBgColor}', | ||||
|         // 头部字体颜色 | ||||
|         topHeaderTextColor: '${appStore.getTheme.topHeaderTextColor}', | ||||
|         // 头部悬停颜色 | ||||
|         topHeaderHoverColor: '${appStore.getTheme.topHeaderHoverColor}', | ||||
|         // 头部边框颜色 | ||||
|         topToolBorderColor: '${appStore.getTheme.topToolBorderColor}' | ||||
|       } | ||||
|     ` | ||||
|   }) | ||||
|   if (!isSupported) { | ||||
|     ElMessage.error(t('setting.copyFailed')) | ||||
|   } else { | ||||
|     await copy() | ||||
|     if (unref(copied)) { | ||||
|       ElMessage.success(t('setting.copySuccess')) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // 清空缓存 | ||||
| const clear = () => { | ||||
|   const { wsCache } = useCache() | ||||
|   wsCache.delete(CACHE_KEY.LAYOUT) | ||||
|   wsCache.delete(CACHE_KEY.THEME) | ||||
|   wsCache.delete(CACHE_KEY.IS_DARK) | ||||
|   window.location.reload() | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     :class="prefixCls" | ||||
|     class="fixed top-[45%] right-0 w-40px h-40px text-center leading-40px bg-[var(--el-color-primary)] cursor-pointer" | ||||
|     @click="drawer = true" | ||||
|   > | ||||
|     <Icon icon="ep:setting" color="#fff" /> | ||||
|   </div> | ||||
|  | ||||
|   <ElDrawer v-model="drawer" direction="rtl" size="350px" :z-index="4000"> | ||||
|     <template #header> | ||||
|       <span class="text-16px font-700">{{ t('setting.projectSetting') }}</span> | ||||
|     </template> | ||||
|  | ||||
|     <div class="text-center"> | ||||
|       <!-- 主题 --> | ||||
|       <ElDivider>{{ t('setting.theme') }}</ElDivider> | ||||
|       <ThemeSwitch /> | ||||
|  | ||||
|       <!-- 布局 --> | ||||
|       <ElDivider>{{ t('setting.layout') }}</ElDivider> | ||||
|       <LayoutRadioPicker /> | ||||
|  | ||||
|       <!-- 系统主题 --> | ||||
|       <ElDivider>{{ t('setting.systemTheme') }}</ElDivider> | ||||
|       <ColorRadioPicker | ||||
|         v-model="systemTheme" | ||||
|         :schema="[ | ||||
|           '#409eff', | ||||
|           '#009688', | ||||
|           '#536dfe', | ||||
|           '#ff5c93', | ||||
|           '#ee4f12', | ||||
|           '#0096c7', | ||||
|           '#9c27b0', | ||||
|           '#ff9800' | ||||
|         ]" | ||||
|         @change="setSystemTheme" | ||||
|       /> | ||||
|  | ||||
|       <!-- 头部主题 --> | ||||
|       <ElDivider>{{ t('setting.headerTheme') }}</ElDivider> | ||||
|       <ColorRadioPicker | ||||
|         v-model="headerTheme" | ||||
|         :schema="[ | ||||
|           '#fff', | ||||
|           '#151515', | ||||
|           '#5172dc', | ||||
|           '#e74c3c', | ||||
|           '#24292e', | ||||
|           '#394664', | ||||
|           '#009688', | ||||
|           '#383f45' | ||||
|         ]" | ||||
|         @change="setHeaderTheme" | ||||
|       /> | ||||
|  | ||||
|       <!-- 菜单主题 --> | ||||
|       <template v-if="layout !== 'top'"> | ||||
|         <ElDivider>{{ t('setting.menuTheme') }}</ElDivider> | ||||
|         <ColorRadioPicker | ||||
|           v-model="menuTheme" | ||||
|           :schema="[ | ||||
|             '#fff', | ||||
|             '#001529', | ||||
|             '#212121', | ||||
|             '#273352', | ||||
|             '#191b24', | ||||
|             '#383f45', | ||||
|             '#001628', | ||||
|             '#344058' | ||||
|           ]" | ||||
|           @change="setMenuTheme" | ||||
|         /> | ||||
|       </template> | ||||
|     </div> | ||||
|  | ||||
|     <!-- 界面显示 --> | ||||
|     <ElDivider>{{ t('setting.interfaceDisplay') }}</ElDivider> | ||||
|     <InterfaceDisplay /> | ||||
|  | ||||
|     <ElDivider /> | ||||
|     <div> | ||||
|       <ElButton type="primary" class="w-full" @click="copyConfig">{{ t('setting.copy') }}</ElButton> | ||||
|     </div> | ||||
|     <div class="mt-5px"> | ||||
|       <ElButton type="danger" class="w-full" @click="clear"> | ||||
|         {{ t('setting.clearAndReset') }} | ||||
|       </ElButton> | ||||
|     </div> | ||||
|   </ElDrawer> | ||||
| </template> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| $prefix-cls: #{$namespace}-setting; | ||||
|  | ||||
| .#{$prefix-cls} { | ||||
|   border-radius: 6px 0 0 6px; | ||||
| } | ||||
| </style> | ||||
| @@ -0,0 +1,65 @@ | ||||
| <script setup lang="ts"> | ||||
| import { PropType } from 'vue' | ||||
| import { propTypes } from '@/utils/propTypes' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('color-radio-picker') | ||||
|  | ||||
| const props = defineProps({ | ||||
|   schema: { | ||||
|     type: Array as PropType<string[]>, | ||||
|     default: () => [] | ||||
|   }, | ||||
|   modelValue: propTypes.string.def('') | ||||
| }) | ||||
|  | ||||
| const emit = defineEmits(['update:modelValue', 'change']) | ||||
|  | ||||
| const colorVal = ref(props.modelValue) | ||||
|  | ||||
| watch( | ||||
|   () => props.modelValue, | ||||
|   (val: string) => { | ||||
|     if (val === unref(colorVal)) return | ||||
|     colorVal.value = val | ||||
|   } | ||||
| ) | ||||
|  | ||||
| // 监听 | ||||
| watch( | ||||
|   () => colorVal.value, | ||||
|   (val: string) => { | ||||
|     emit('update:modelValue', val) | ||||
|     emit('change', val) | ||||
|   } | ||||
| ) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div :class="prefixCls" class="flex flex-wrap space-x-14px"> | ||||
|     <span | ||||
|       v-for="(item, i) in schema" | ||||
|       :key="`radio-${i}`" | ||||
|       class="w-20px h-20px cursor-pointer rounded-2px border-solid border-gray-300 border-2px text-center leading-20px mb-5px" | ||||
|       :class="{ 'is-active': colorVal === item }" | ||||
|       :style="{ | ||||
|         background: item | ||||
|       }" | ||||
|       @click="colorVal = item" | ||||
|     > | ||||
|       <Icon v-if="colorVal === item" color="#fff" icon="ep:check" :size="16" /> | ||||
|     </span> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| $prefix-cls: #{$namespace}-color-radio-picker; | ||||
|  | ||||
| .#{$prefix-cls} { | ||||
|   .is-active { | ||||
|     border-color: var(--el-color-primary); | ||||
|   } | ||||
| } | ||||
| </style> | ||||
| @@ -0,0 +1,222 @@ | ||||
| <script setup lang="ts"> | ||||
| import { setCssVar } from '@/utils' | ||||
|  | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import { useWatermark } from '@/hooks/web/useWatermark' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
|  | ||||
| const { t } = useI18n() | ||||
| const { getPrefixCls } = useDesign() | ||||
| const { setWatermark } = useWatermark() | ||||
| const prefixCls = getPrefixCls('interface-display') | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const water = ref() | ||||
|  | ||||
| // 面包屑 | ||||
| const breadcrumb = ref(appStore.getBreadcrumb) | ||||
|  | ||||
| const breadcrumbChange = (show: boolean) => { | ||||
|   appStore.setBreadcrumb(show) | ||||
| } | ||||
|  | ||||
| // 面包屑图标 | ||||
| const breadcrumbIcon = ref(appStore.getBreadcrumbIcon) | ||||
|  | ||||
| const breadcrumbIconChange = (show: boolean) => { | ||||
|   appStore.setBreadcrumbIcon(show) | ||||
| } | ||||
|  | ||||
| // 折叠图标 | ||||
| const hamburger = ref(appStore.getHamburger) | ||||
|  | ||||
| const hamburgerChange = (show: boolean) => { | ||||
|   appStore.setHamburger(show) | ||||
| } | ||||
|  | ||||
| // 全屏图标 | ||||
| const screenfull = ref(appStore.getScreenfull) | ||||
|  | ||||
| const screenfullChange = (show: boolean) => { | ||||
|   appStore.setScreenfull(show) | ||||
| } | ||||
|  | ||||
| // 尺寸图标 | ||||
| const size = ref(appStore.getSize) | ||||
|  | ||||
| const sizeChange = (show: boolean) => { | ||||
|   appStore.setSize(show) | ||||
| } | ||||
|  | ||||
| // 多语言图标 | ||||
| const locale = ref(appStore.getLocale) | ||||
|  | ||||
| const localeChange = (show: boolean) => { | ||||
|   appStore.setLocale(show) | ||||
| } | ||||
|  | ||||
| // 消息图标 | ||||
| const message = ref(appStore.getMessage) | ||||
|  | ||||
| const messageChange = (show: boolean) => { | ||||
|   appStore.setMessage(show) | ||||
| } | ||||
|  | ||||
| // 标签页 | ||||
| const tagsView = ref(appStore.getTagsView) | ||||
|  | ||||
| const tagsViewChange = (show: boolean) => { | ||||
|   // 切换标签栏显示时,同步切换标签栏的高度 | ||||
|   setCssVar('--tags-view-height', show ? '35px' : '0px') | ||||
|   appStore.setTagsView(show) | ||||
| } | ||||
|  | ||||
| // 标签页图标 | ||||
| const tagsViewIcon = ref(appStore.getTagsViewIcon) | ||||
|  | ||||
| const tagsViewIconChange = (show: boolean) => { | ||||
|   appStore.setTagsViewIcon(show) | ||||
| } | ||||
|  | ||||
| // logo | ||||
| const logo = ref(appStore.getLogo) | ||||
|  | ||||
| const logoChange = (show: boolean) => { | ||||
|   appStore.setLogo(show) | ||||
| } | ||||
|  | ||||
| // 菜单手风琴 | ||||
| const uniqueOpened = ref(appStore.getUniqueOpened) | ||||
|  | ||||
| const uniqueOpenedChange = (uniqueOpened: boolean) => { | ||||
|   appStore.setUniqueOpened(uniqueOpened) | ||||
| } | ||||
|  | ||||
| // 固定头部 | ||||
| const fixedHeader = ref(appStore.getFixedHeader) | ||||
|  | ||||
| const fixedHeaderChange = (show: boolean) => { | ||||
|   appStore.setFixedHeader(show) | ||||
| } | ||||
|  | ||||
| // 页脚 | ||||
| const footer = ref(appStore.getFooter) | ||||
|  | ||||
| const footerChange = (show: boolean) => { | ||||
|   appStore.setFooter(show) | ||||
| } | ||||
|  | ||||
| // 灰色模式 | ||||
| const greyMode = ref(appStore.getGreyMode) | ||||
|  | ||||
| const greyModeChange = (show: boolean) => { | ||||
|   appStore.setGreyMode(show) | ||||
| } | ||||
|  | ||||
| // 固定菜单 | ||||
| const fixedMenu = ref(appStore.getFixedMenu) | ||||
|  | ||||
| const fixedMenuChange = (show: boolean) => { | ||||
|   appStore.setFixedMenu(show) | ||||
| } | ||||
|  | ||||
| // 设置水印 | ||||
| const setWater = () => { | ||||
|   setWatermark(water.value) | ||||
| } | ||||
|  | ||||
| const layout = computed(() => appStore.getLayout) | ||||
|  | ||||
| watch( | ||||
|   () => layout.value, | ||||
|   (n) => { | ||||
|     if (n === 'top') { | ||||
|       appStore.setCollapse(false) | ||||
|     } | ||||
|   } | ||||
| ) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div :class="prefixCls"> | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.breadcrumb') }}</span> | ||||
|       <ElSwitch v-model="breadcrumb" @change="breadcrumbChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.breadcrumbIcon') }}</span> | ||||
|       <ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.hamburgerIcon') }}</span> | ||||
|       <ElSwitch v-model="hamburger" @change="hamburgerChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.screenfullIcon') }}</span> | ||||
|       <ElSwitch v-model="screenfull" @change="screenfullChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.sizeIcon') }}</span> | ||||
|       <ElSwitch v-model="size" @change="sizeChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.localeIcon') }}</span> | ||||
|       <ElSwitch v-model="locale" @change="localeChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.messageIcon') }}</span> | ||||
|       <ElSwitch v-model="message" @change="messageChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.tagsView') }}</span> | ||||
|       <ElSwitch v-model="tagsView" @change="tagsViewChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.tagsViewIcon') }}</span> | ||||
|       <ElSwitch v-model="tagsViewIcon" @change="tagsViewIconChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.logo') }}</span> | ||||
|       <ElSwitch v-model="logo" @change="logoChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.uniqueOpened') }}</span> | ||||
|       <ElSwitch v-model="uniqueOpened" @change="uniqueOpenedChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.fixedHeader') }}</span> | ||||
|       <ElSwitch v-model="fixedHeader" @change="fixedHeaderChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.footer') }}</span> | ||||
|       <ElSwitch v-model="footer" @change="footerChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.greyMode') }}</span> | ||||
|       <ElSwitch v-model="greyMode" @change="greyModeChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('setting.fixedMenu') }}</span> | ||||
|       <ElSwitch v-model="fixedMenu" @change="fixedMenuChange" /> | ||||
|     </div> | ||||
|  | ||||
|     <div class="flex justify-between items-center"> | ||||
|       <span class="text-14px">{{ t('watermark.watermark') }}</span> | ||||
|       <ElInput v-model="water" class="w-20 right-1" @change="setWater()" /> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| @@ -0,0 +1,170 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('layout-radio-picker') | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const layout = computed(() => appStore.getLayout) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div :class="prefixCls" class="flex flex-wrap space-x-14px"> | ||||
|     <div | ||||
|       :class="[ | ||||
|         `${prefixCls}__classic`, | ||||
|         'relative w-56px h-48px cursor-pointer bg-gray-300', | ||||
|         { | ||||
|           'is-acitve': layout === 'classic' | ||||
|         } | ||||
|       ]" | ||||
|       @click="appStore.setLayout('classic')" | ||||
|     ></div> | ||||
|     <div | ||||
|       :class="[ | ||||
|         `${prefixCls}__top-left`, | ||||
|         'relative w-56px h-48px cursor-pointer bg-gray-300', | ||||
|         { | ||||
|           'is-acitve': layout === 'topLeft' | ||||
|         } | ||||
|       ]" | ||||
|       @click="appStore.setLayout('topLeft')" | ||||
|     ></div> | ||||
|     <div | ||||
|       :class="[ | ||||
|         `${prefixCls}__top`, | ||||
|         'relative w-56px h-48px cursor-pointer bg-gray-300', | ||||
|         { | ||||
|           'is-acitve': layout === 'top' | ||||
|         } | ||||
|       ]" | ||||
|       @click="appStore.setLayout('top')" | ||||
|     ></div> | ||||
|     <div | ||||
|       :class="[ | ||||
|         `${prefixCls}__cut-menu`, | ||||
|         'relative w-56px h-48px cursor-pointer bg-gray-300', | ||||
|         { | ||||
|           'is-acitve': layout === 'cutMenu' | ||||
|         } | ||||
|       ]" | ||||
|       @click="appStore.setLayout('cutMenu')" | ||||
|     > | ||||
|       <div class="absolute h-full w-[33%] top-0 left-[10%] bg-gray-200"></div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| $prefix-cls: #{$namespace}-layout-radio-picker; | ||||
|  | ||||
| .#{$prefix-cls} { | ||||
|   &__classic { | ||||
|     border: 2px solid #e5e7eb; | ||||
|     border-radius: 4px; | ||||
|  | ||||
|     &:before { | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       left: 0; | ||||
|       z-index: 1; | ||||
|       width: 33%; | ||||
|       height: 100%; | ||||
|       background-color: #273352; | ||||
|       border-radius: 4px 0 0 4px; | ||||
|       content: ''; | ||||
|     } | ||||
|  | ||||
|     &:after { | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       left: 0; | ||||
|       width: 100%; | ||||
|       height: 25%; | ||||
|       background-color: #fff; | ||||
|       border-radius: 4px 4px 0 4px; | ||||
|       content: ''; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &__top-left { | ||||
|     border: 2px solid #e5e7eb; | ||||
|     border-radius: 4px; | ||||
|  | ||||
|     &:before { | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       left: 0; | ||||
|       z-index: 1; | ||||
|       width: 100%; | ||||
|       height: 33%; | ||||
|       background-color: #273352; | ||||
|       border-radius: 4px 4px 0 0; | ||||
|       content: ''; | ||||
|     } | ||||
|  | ||||
|     &:after { | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       left: 0; | ||||
|       width: 33%; | ||||
|       height: 100%; | ||||
|       background-color: #fff; | ||||
|       border-radius: 4px 0 0 4px; | ||||
|       content: ''; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &__top { | ||||
|     border: 2px solid #e5e7eb; | ||||
|     border-radius: 4px; | ||||
|  | ||||
|     &:before { | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       left: 0; | ||||
|       z-index: 1; | ||||
|       width: 100%; | ||||
|       height: 33%; | ||||
|       background-color: #273352; | ||||
|       border-radius: 4px 4px 0 0; | ||||
|       content: ''; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &__cut-menu { | ||||
|     border: 2px solid #e5e7eb; | ||||
|     border-radius: 4px; | ||||
|  | ||||
|     &:before { | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       left: 0; | ||||
|       z-index: 1; | ||||
|       width: 100%; | ||||
|       height: 33%; | ||||
|       background-color: #273352; | ||||
|       border-radius: 4px 4px 0 0; | ||||
|       content: ''; | ||||
|     } | ||||
|  | ||||
|     &:after { | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       left: 0; | ||||
|       width: 10%; | ||||
|       height: 100%; | ||||
|       background-color: #fff; | ||||
|       border-radius: 4px 0 0 4px; | ||||
|       content: ''; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .is-acitve { | ||||
|     border-color: var(--el-color-primary); | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										3
									
								
								src/layout/components/SizeDropdown/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/SizeDropdown/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import SizeDropdown from './src/SizeDropdown.vue' | ||||
|  | ||||
| export { SizeDropdown } | ||||
							
								
								
									
										38
									
								
								src/layout/components/SizeDropdown/src/SizeDropdown.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/layout/components/SizeDropdown/src/SizeDropdown.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
|  | ||||
| import { propTypes } from '@/utils/propTypes' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import { ElementPlusSize } from '@/types/elementPlus' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('size-dropdown') | ||||
|  | ||||
| defineProps({ | ||||
|   color: propTypes.string.def('') | ||||
| }) | ||||
|  | ||||
| const { t } = useI18n() | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const sizeMap = computed(() => appStore.sizeMap) | ||||
|  | ||||
| const setCurrentSize = (size: ElementPlusSize) => { | ||||
|   appStore.setCurrentSize(size) | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <ElDropdown :class="prefixCls" trigger="click" @command="setCurrentSize"> | ||||
|     <Icon :size="18" icon="mdi:format-size" :color="color" class="cursor-pointer" /> | ||||
|     <template #dropdown> | ||||
|       <ElDropdownMenu> | ||||
|         <ElDropdownItem v-for="item in sizeMap" :key="item" :command="item"> | ||||
|           {{ t(`size.${item}`) }} | ||||
|         </ElDropdownItem> | ||||
|       </ElDropdownMenu> | ||||
|     </template> | ||||
|   </ElDropdown> | ||||
| </template> | ||||
							
								
								
									
										3
									
								
								src/layout/components/TabMenu/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/TabMenu/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import TabMenu from './src/TabMenu.vue' | ||||
|  | ||||
| export { TabMenu } | ||||
							
								
								
									
										251
									
								
								src/layout/components/TabMenu/src/TabMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										251
									
								
								src/layout/components/TabMenu/src/TabMenu.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,251 @@ | ||||
| <script lang="tsx"> | ||||
| import { usePermissionStore } from '@/store/modules/permission' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
|  | ||||
| import { ElScrollbar } from 'element-plus' | ||||
| import { Icon } from '@/components/Icon' | ||||
| import { Menu } from '@/layout/components/Menu' | ||||
| import { pathResolve } from '@/utils/routerHelper' | ||||
| import { cloneDeep } from 'lodash-es' | ||||
| import { filterMenusPath, initTabMap, tabPathMap } from './helper' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import { isUrl } from '@/utils/is' | ||||
|  | ||||
| const { getPrefixCls, variables } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('tab-menu') | ||||
|  | ||||
| export default defineComponent({ | ||||
|   name: 'TabMenu', | ||||
|   setup() { | ||||
|     const { push, currentRoute } = useRouter() | ||||
|  | ||||
|     const { t } = useI18n() | ||||
|  | ||||
|     const appStore = useAppStore() | ||||
|  | ||||
|     const collapse = computed(() => appStore.getCollapse) | ||||
|  | ||||
|     const fixedMenu = computed(() => appStore.getFixedMenu) | ||||
|  | ||||
|     const permissionStore = usePermissionStore() | ||||
|  | ||||
|     const routers = computed(() => permissionStore.getRouters) | ||||
|  | ||||
|     const tabRouters = computed(() => unref(routers).filter((v) => !v?.meta?.hidden)) | ||||
|  | ||||
|     const setCollapse = () => { | ||||
|       appStore.setCollapse(!unref(collapse)) | ||||
|     } | ||||
|  | ||||
|     onMounted(() => { | ||||
|       if (unref(fixedMenu)) { | ||||
|         const path = `/${unref(currentRoute).path.split('/')[1]}` | ||||
|         const children = unref(tabRouters).find( | ||||
|           (v) => | ||||
|             (v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)) && | ||||
|             v.path === path | ||||
|         )?.children | ||||
|  | ||||
|         tabActive.value = path | ||||
|         if (children) { | ||||
|           permissionStore.setMenuTabRouters( | ||||
|             cloneDeep(children).map((v) => { | ||||
|               v.path = pathResolve(unref(tabActive), v.path) | ||||
|               return v | ||||
|             }) | ||||
|           ) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|  | ||||
|     watch( | ||||
|       () => routers.value, | ||||
|       (routers: AppRouteRecordRaw[]) => { | ||||
|         initTabMap(routers) | ||||
|         filterMenusPath(routers, routers) | ||||
|       }, | ||||
|       { | ||||
|         immediate: true, | ||||
|         deep: true | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     const showTitle = ref(true) | ||||
|  | ||||
|     watch( | ||||
|       () => collapse.value, | ||||
|       (collapse: boolean) => { | ||||
|         if (!collapse) { | ||||
|           setTimeout(() => { | ||||
|             showTitle.value = !collapse | ||||
|           }, 200) | ||||
|         } else { | ||||
|           showTitle.value = !collapse | ||||
|         } | ||||
|       } | ||||
|     ) | ||||
|  | ||||
|     // 是否显示菜单 | ||||
|     const showMenu = ref(unref(fixedMenu) ? true : false) | ||||
|  | ||||
|     // tab高亮 | ||||
|     const tabActive = ref('') | ||||
|  | ||||
|     // tab点击事件 | ||||
|     const tabClick = (item: AppRouteRecordRaw) => { | ||||
|       if (isUrl(item.path)) { | ||||
|         window.open(item.path) | ||||
|         return | ||||
|       } | ||||
|       const newPath = item.children ? item.path : item.path.split('/')[0] | ||||
|       const oldPath = unref(tabActive) | ||||
|       tabActive.value = item.children ? item.path : item.path.split('/')[0] | ||||
|       if (item.children) { | ||||
|         if (newPath === oldPath || !unref(showMenu)) { | ||||
|           showMenu.value = unref(fixedMenu) ? true : !unref(showMenu) | ||||
|         } | ||||
|         if (unref(showMenu)) { | ||||
|           permissionStore.setMenuTabRouters( | ||||
|             cloneDeep(item.children).map((v) => { | ||||
|               v.path = pathResolve(unref(tabActive), v.path) | ||||
|               return v | ||||
|             }) | ||||
|           ) | ||||
|         } | ||||
|       } else { | ||||
|         push(item.path) | ||||
|         permissionStore.setMenuTabRouters([]) | ||||
|         showMenu.value = false | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // 设置高亮 | ||||
|     const isActive = (currentPath: string) => { | ||||
|       const { path } = unref(currentRoute) | ||||
|       if (tabPathMap[currentPath].includes(path)) { | ||||
|         return true | ||||
|       } | ||||
|       return false | ||||
|     } | ||||
|  | ||||
|     const mouseleave = () => { | ||||
|       if (!unref(showMenu) || unref(fixedMenu)) return | ||||
|       showMenu.value = false | ||||
|     } | ||||
|  | ||||
|     return () => ( | ||||
|       <div | ||||
|         id={`${variables.namespace}-menu`} | ||||
|         class={[ | ||||
|           prefixCls, | ||||
|           'relative bg-[var(--left-menu-bg-color)] top-1px z-3000', | ||||
|           { | ||||
|             'w-[var(--tab-menu-max-width)]': !unref(collapse), | ||||
|             'w-[var(--tab-menu-min-width)]': unref(collapse) | ||||
|           } | ||||
|         ]} | ||||
|         onMouseleave={mouseleave} | ||||
|       > | ||||
|         <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height)-1px)]"> | ||||
|           <div> | ||||
|             {() => { | ||||
|               return unref(tabRouters).map((v) => { | ||||
|                 const item = ( | ||||
|                   v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1) | ||||
|                     ? v | ||||
|                     : { | ||||
|                         ...(v?.children && v?.children[0]), | ||||
|                         path: pathResolve(v.path, (v?.children && v?.children[0])?.path as string) | ||||
|                       } | ||||
|                 ) as AppRouteRecordRaw | ||||
|                 return ( | ||||
|                   <div | ||||
|                     class={[ | ||||
|                       `${prefixCls}__item`, | ||||
|                       'text-center text-12px relative py-12px cursor-pointer', | ||||
|                       { | ||||
|                         'is-active': isActive(v.path) | ||||
|                       } | ||||
|                     ]} | ||||
|                     onClick={() => { | ||||
|                       tabClick(item) | ||||
|                     }} | ||||
|                   > | ||||
|                     <div> | ||||
|                       <Icon icon={item?.meta?.icon}></Icon> | ||||
|                     </div> | ||||
|                     {!unref(showTitle) ? undefined : ( | ||||
|                       <p class="break-words mt-5px px-2px">{t(item.meta?.title)}</p> | ||||
|                     )} | ||||
|                   </div> | ||||
|                 ) | ||||
|               }) | ||||
|             }} | ||||
|           </div> | ||||
|         </ElScrollbar> | ||||
|         <div | ||||
|           class={[ | ||||
|             `${prefixCls}--collapse`, | ||||
|             'text-center h-[var(--tab-menu-collapse-height)] leading-[var(--tab-menu-collapse-height)] cursor-pointer' | ||||
|           ]} | ||||
|           onClick={setCollapse} | ||||
|         > | ||||
|           <Icon icon={unref(collapse) ? 'ep:d-arrow-right' : 'ep:d-arrow-left'}></Icon> | ||||
|         </div> | ||||
|         <Menu | ||||
|           class={[ | ||||
|             '!absolute top-0 border-left-1 border-solid border-[var(--left-menu-bg-light-color)]', | ||||
|             { | ||||
|               '!left-[var(--tab-menu-min-width)]': unref(collapse), | ||||
|               '!left-[var(--tab-menu-max-width)]': !unref(collapse), | ||||
|               '!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu) || unref(fixedMenu), | ||||
|               '!w-0': !unref(showMenu) && !unref(fixedMenu) | ||||
|             } | ||||
|           ]} | ||||
|           style="transition: width var(--transition-time-02), left var(--transition-time-02);" | ||||
|         ></Menu> | ||||
|       </div> | ||||
|     ) | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| $prefix-cls: #{$namespace}-tab-menu; | ||||
|  | ||||
| .#{$prefix-cls} { | ||||
|   transition: all var(--transition-time-02); | ||||
|  | ||||
|   &:after { | ||||
|     position: absolute; | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     width: 1px; | ||||
|     height: 100%; | ||||
|     border-left: 1px solid var(--left-menu-border-color); | ||||
|     content: ''; | ||||
|   } | ||||
|  | ||||
|   &__item { | ||||
|     color: var(--left-menu-text-color); | ||||
|     transition: all var(--transition-time-02); | ||||
|  | ||||
|     &:hover { | ||||
|       color: var(--left-menu-text-active-color); | ||||
|       // background-color: var(--left-menu-bg-active-color); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &--collapse { | ||||
|     color: var(--left-menu-text-color); | ||||
|     background-color: var(--left-menu-bg-light-color); | ||||
|     border-top: 1px solid var(--left-menu-border-color); | ||||
|   } | ||||
|  | ||||
|   .is-active { | ||||
|     color: var(--left-menu-text-active-color); | ||||
|     background-color: var(--left-menu-bg-active-color); | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										51
									
								
								src/layout/components/TabMenu/src/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/layout/components/TabMenu/src/helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import { getAllParentPath } from '@/layout/components/Menu/src/helper' | ||||
| import type { RouteMeta } from 'vue-router' | ||||
| import { isUrl } from '@/utils/is' | ||||
| import { cloneDeep } from 'lodash-es' | ||||
|  | ||||
| export type TabMapTypes = { | ||||
|   [key: string]: string[] | ||||
| } | ||||
|  | ||||
| export const tabPathMap = reactive<TabMapTypes>({}) | ||||
|  | ||||
| export const initTabMap = (routes: AppRouteRecordRaw[]) => { | ||||
|   for (const v of routes) { | ||||
|     const meta = (v.meta ?? {}) as RouteMeta | ||||
|     if (!meta?.hidden) { | ||||
|       tabPathMap[v.path] = [] | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const filterMenusPath = ( | ||||
|   routes: AppRouteRecordRaw[], | ||||
|   allRoutes: AppRouteRecordRaw[] | ||||
| ): AppRouteRecordRaw[] => { | ||||
|   const res: AppRouteRecordRaw[] = [] | ||||
|   for (const v of routes) { | ||||
|     let data: Nullable<AppRouteRecordRaw> = null | ||||
|     const meta = (v.meta ?? {}) as RouteMeta | ||||
|     if (!meta.hidden || meta.canTo) { | ||||
|       const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path) | ||||
|  | ||||
|       const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/') | ||||
|  | ||||
|       data = cloneDeep(v) | ||||
|       data.path = fullPath | ||||
|       if (v.children && data) { | ||||
|         data.children = filterMenusPath(v.children, allRoutes) | ||||
|       } | ||||
|  | ||||
|       if (data) { | ||||
|         res.push(data) | ||||
|       } | ||||
|  | ||||
|       if (allParentPath.length && Reflect.has(tabPathMap, allParentPath[0])) { | ||||
|         tabPathMap[allParentPath[0]].push(fullPath) | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return res | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/layout/components/TagsView/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/TagsView/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import TagsView from './src/TagsView.vue' | ||||
|  | ||||
| export { TagsView } | ||||
							
								
								
									
										581
									
								
								src/layout/components/TagsView/src/TagsView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										581
									
								
								src/layout/components/TagsView/src/TagsView.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,581 @@ | ||||
| <script setup lang="ts"> | ||||
| import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router' | ||||
| import { usePermissionStore } from '@/store/modules/permission' | ||||
| import { useTagsViewStore } from '@/store/modules/tagsView' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
|  | ||||
| import { filterAffixTags } from './helper' | ||||
| import { ContextMenu, ContextMenuExpose } from '@/layout/components/ContextMenu' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import { ElScrollbar } from 'element-plus' | ||||
| import { useScrollTo } from '@/hooks/event/useScrollTo' | ||||
| import { useTemplateRefsList } from '@vueuse/core' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('tags-view') | ||||
|  | ||||
| const { t } = useI18n() | ||||
|  | ||||
| const { currentRoute, push, replace } = useRouter() | ||||
|  | ||||
| const permissionStore = usePermissionStore() | ||||
|  | ||||
| const routers = computed(() => permissionStore.getRouters) | ||||
|  | ||||
| const tagsViewStore = useTagsViewStore() | ||||
|  | ||||
| const visitedViews = computed(() => tagsViewStore.getVisitedViews) | ||||
|  | ||||
| const affixTagArr = ref<RouteLocationNormalizedLoaded[]>([]) | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const tagsViewIcon = computed(() => appStore.getTagsViewIcon) | ||||
|  | ||||
| // 初始化tag | ||||
| const initTags = () => { | ||||
|   affixTagArr.value = filterAffixTags(unref(routers)) | ||||
|   for (const tag of unref(affixTagArr)) { | ||||
|     // Must have tag name | ||||
|     if (tag.name) { | ||||
|       tagsViewStore.addVisitedView(tag) | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| const selectedTag = ref<RouteLocationNormalizedLoaded>() | ||||
|  | ||||
| // 新增tag | ||||
| const addTags = () => { | ||||
|   const { name } = unref(currentRoute) | ||||
|   if (name) { | ||||
|     selectedTag.value = unref(currentRoute) | ||||
|     tagsViewStore.addView(unref(currentRoute)) | ||||
|   } | ||||
|   return false | ||||
| } | ||||
|  | ||||
| // 关闭选中的tag | ||||
| const closeSelectedTag = (view: RouteLocationNormalizedLoaded) => { | ||||
|   if (view?.meta?.affix) return | ||||
|   tagsViewStore.delView(view) | ||||
|   if (isActive(view)) { | ||||
|     toLastView() | ||||
|   } | ||||
| } | ||||
|  | ||||
| // 关闭全部 | ||||
| const closeAllTags = () => { | ||||
|   tagsViewStore.delAllViews() | ||||
|   toLastView() | ||||
| } | ||||
|  | ||||
| // 关闭其他 | ||||
| const closeOthersTags = () => { | ||||
|   tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded) | ||||
| } | ||||
|  | ||||
| // 重新加载 | ||||
| const refreshSelectedTag = async (view?: RouteLocationNormalizedLoaded) => { | ||||
|   if (!view) return | ||||
|   tagsViewStore.delCachedView() | ||||
|   const { path, query } = view | ||||
|   await nextTick() | ||||
|   replace({ | ||||
|     path: '/redirect' + path, | ||||
|     query: query | ||||
|   }) | ||||
| } | ||||
|  | ||||
| // 关闭左侧 | ||||
| const closeLeftTags = () => { | ||||
|   tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded) | ||||
| } | ||||
|  | ||||
| // 关闭右侧 | ||||
| const closeRightTags = () => { | ||||
|   tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded) | ||||
| } | ||||
|  | ||||
| // 跳转到最后一个 | ||||
| const toLastView = () => { | ||||
|   const visitedViews = tagsViewStore.getVisitedViews | ||||
|   const latestView = visitedViews.slice(-1)[0] | ||||
|   if (latestView) { | ||||
|     push(latestView) | ||||
|   } else { | ||||
|     if ( | ||||
|       unref(currentRoute).path === permissionStore.getAddRouters[0].path || | ||||
|       unref(currentRoute).path === permissionStore.getAddRouters[0].redirect | ||||
|     ) { | ||||
|       addTags() | ||||
|       return | ||||
|     } | ||||
|     // TODO: You can set another route | ||||
|     push('/') | ||||
|   } | ||||
| } | ||||
|  | ||||
| // 滚动到选中的tag | ||||
| const moveToCurrentTag = async () => { | ||||
|   await nextTick() | ||||
|   for (const v of unref(visitedViews)) { | ||||
|     if (v.fullPath === unref(currentRoute).path) { | ||||
|       moveToTarget(v) | ||||
|       if (v.fullPath !== unref(currentRoute).fullPath) { | ||||
|         tagsViewStore.updateVisitedView(unref(currentRoute)) | ||||
|       } | ||||
|       break | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| const tagLinksRefs = useTemplateRefsList<RouterLinkProps>() | ||||
|  | ||||
| const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => { | ||||
|   const wrap$ = unref(scrollbarRef)?.wrapRef | ||||
|   let firstTag: Nullable<RouterLinkProps> = null | ||||
|   let lastTag: Nullable<RouterLinkProps> = null | ||||
|  | ||||
|   const tagList = unref(tagLinksRefs) | ||||
|   // find first tag and last tag | ||||
|   if (tagList.length > 0) { | ||||
|     firstTag = tagList[0] | ||||
|     lastTag = tagList[tagList.length - 1] | ||||
|   } | ||||
|   if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) { | ||||
|     // 直接滚动到0的位置 | ||||
|     const { start } = useScrollTo({ | ||||
|       el: wrap$!, | ||||
|       position: 'scrollLeft', | ||||
|       to: 0, | ||||
|       duration: 500 | ||||
|     }) | ||||
|     start() | ||||
|   } else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) { | ||||
|     // 滚动到最后的位置 | ||||
|     const { start } = useScrollTo({ | ||||
|       el: wrap$!, | ||||
|       position: 'scrollLeft', | ||||
|       to: wrap$!.scrollWidth - wrap$!.offsetWidth, | ||||
|       duration: 500 | ||||
|     }) | ||||
|     start() | ||||
|   } else { | ||||
|     // find preTag and nextTag | ||||
|     const currentIndex: number = tagList.findIndex( | ||||
|       (item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath | ||||
|     ) | ||||
|     const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`) | ||||
|  | ||||
|     const prevTag = tgsRefs[currentIndex - 1] as HTMLElement | ||||
|     const nextTag = tgsRefs[currentIndex + 1] as HTMLElement | ||||
|  | ||||
|     // the tag's offsetLeft after of nextTag | ||||
|     const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4 | ||||
|  | ||||
|     // the tag's offsetLeft before of prevTag | ||||
|     const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4 | ||||
|  | ||||
|     if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) { | ||||
|       const { start } = useScrollTo({ | ||||
|         el: wrap$!, | ||||
|         position: 'scrollLeft', | ||||
|         to: afterNextTagOffsetLeft - wrap$!.offsetWidth, | ||||
|         duration: 500 | ||||
|       }) | ||||
|       start() | ||||
|     } else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) { | ||||
|       const { start } = useScrollTo({ | ||||
|         el: wrap$!, | ||||
|         position: 'scrollLeft', | ||||
|         to: beforePrevTagOffsetLeft, | ||||
|         duration: 500 | ||||
|       }) | ||||
|       start() | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // 是否是当前tag | ||||
| const isActive = (route: RouteLocationNormalizedLoaded): boolean => { | ||||
|   return route.path === unref(currentRoute).path | ||||
| } | ||||
|  | ||||
| // 所有右键菜单组件的元素 | ||||
| const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>() | ||||
|  | ||||
| // 右键菜单装填改变的时候 | ||||
| const visibleChange = (visible: boolean, tagItem: RouteLocationNormalizedLoaded) => { | ||||
|   if (visible) { | ||||
|     for (const v of unref(itemRefs)) { | ||||
|       const elDropdownMenuRef = v.elDropdownMenuRef | ||||
|       if (tagItem.fullPath !== v.tagItem.fullPath) { | ||||
|         elDropdownMenuRef?.handleClose() | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| // elscroll 实例 | ||||
| const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>() | ||||
|  | ||||
| // 保存滚动位置 | ||||
| const scrollLeftNumber = ref(0) | ||||
|  | ||||
| const scroll = ({ scrollLeft }) => { | ||||
|   scrollLeftNumber.value = scrollLeft as number | ||||
| } | ||||
|  | ||||
| // 移动到某个位置 | ||||
| const move = (to: number) => { | ||||
|   const wrap$ = unref(scrollbarRef)?.wrapRef | ||||
|   const { start } = useScrollTo({ | ||||
|     el: wrap$!, | ||||
|     position: 'scrollLeft', | ||||
|     to: unref(scrollLeftNumber) + to, | ||||
|     duration: 500 | ||||
|   }) | ||||
|   start() | ||||
| } | ||||
|  | ||||
| onMounted(() => { | ||||
|   initTags() | ||||
|   addTags() | ||||
| }) | ||||
|  | ||||
| watch( | ||||
|   () => currentRoute.value, | ||||
|   () => { | ||||
|     addTags() | ||||
|     moveToCurrentTag() | ||||
|   } | ||||
| ) | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <div | ||||
|     :id="prefixCls" | ||||
|     :class="prefixCls" | ||||
|     class="flex w-full relative bg-[#fff] dark:bg-[var(--el-bg-color)]" | ||||
|   > | ||||
|     <span | ||||
|       :class="`${prefixCls}__tool`" | ||||
|       class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] cursor-pointer" | ||||
|       @click="move(-200)" | ||||
|     > | ||||
|       <Icon | ||||
|         icon="ep:d-arrow-left" | ||||
|         :color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'" | ||||
|       /> | ||||
|     </span> | ||||
|     <div class="overflow-hidden flex-1"> | ||||
|       <ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll"> | ||||
|         <div class="flex h-full"> | ||||
|           <ContextMenu | ||||
|             :ref="itemRefs.set" | ||||
|             :schema="[ | ||||
|               { | ||||
|                 icon: 'ep:refresh', | ||||
|                 label: t('common.reload'), | ||||
|                 disabled: selectedTag?.fullPath !== item.fullPath, | ||||
|                 command: () => { | ||||
|                   refreshSelectedTag(item) | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 icon: 'ep:close', | ||||
|                 label: t('common.closeTab'), | ||||
|                 disabled: !!visitedViews?.length && selectedTag?.meta.affix, | ||||
|                 command: () => { | ||||
|                   closeSelectedTag(item) | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 divided: true, | ||||
|                 icon: 'ep:d-arrow-left', | ||||
|                 label: t('common.closeTheLeftTab'), | ||||
|                 disabled: | ||||
|                   !!visitedViews?.length && | ||||
|                   (item.fullPath === visitedViews[0].fullPath || | ||||
|                     selectedTag?.fullPath !== item.fullPath), | ||||
|                 command: () => { | ||||
|                   closeLeftTags() | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 icon: 'ep:d-arrow-right', | ||||
|                 label: t('common.closeTheRightTab'), | ||||
|                 disabled: | ||||
|                   !!visitedViews?.length && | ||||
|                   (item.fullPath === visitedViews[visitedViews.length - 1].fullPath || | ||||
|                     selectedTag?.fullPath !== item.fullPath), | ||||
|                 command: () => { | ||||
|                   closeRightTags() | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 divided: true, | ||||
|                 icon: 'ep:discount', | ||||
|                 label: t('common.closeOther'), | ||||
|                 disabled: selectedTag?.fullPath !== item.fullPath, | ||||
|                 command: () => { | ||||
|                   closeOthersTags() | ||||
|                 } | ||||
|               }, | ||||
|               { | ||||
|                 icon: 'ep:minus', | ||||
|                 label: t('common.closeAll'), | ||||
|                 command: () => { | ||||
|                   closeAllTags() | ||||
|                 } | ||||
|               } | ||||
|             ]" | ||||
|             v-for="item in visitedViews" | ||||
|             :key="item.fullPath" | ||||
|             :tag-item="item" | ||||
|             :class="[ | ||||
|               `${prefixCls}__item`, | ||||
|               item?.meta?.affix ? `${prefixCls}__item--affix` : '', | ||||
|               { | ||||
|                 'is-active': isActive(item) | ||||
|               } | ||||
|             ]" | ||||
|             @visible-change="visibleChange" | ||||
|           > | ||||
|             <div> | ||||
|               <router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }"> | ||||
|                 <div | ||||
|                   @click="navigate" | ||||
|                   class="h-full flex justify-center items-center whitespace-nowrap pl-15px" | ||||
|                 > | ||||
|                   <Icon | ||||
|                     v-if=" | ||||
|                       item?.matched && | ||||
|                       item?.matched[1] && | ||||
|                       item?.matched[1]?.meta?.icon && | ||||
|                       tagsViewIcon | ||||
|                     " | ||||
|                     :icon="item?.matched[1]?.meta?.icon" | ||||
|                     :size="12" | ||||
|                     class="mr-5px" | ||||
|                   /> | ||||
|                   {{ t(item?.meta?.title as string) }} | ||||
|                   <Icon | ||||
|                     :class="`${prefixCls}__item--close`" | ||||
|                     color="#333" | ||||
|                     icon="ep:close" | ||||
|                     :size="12" | ||||
|                     @click.prevent.stop="closeSelectedTag(item)" | ||||
|                   /> | ||||
|                 </div> | ||||
|               </router-link> | ||||
|             </div> | ||||
|           </ContextMenu> | ||||
|         </div> | ||||
|       </ElScrollbar> | ||||
|     </div> | ||||
|     <span | ||||
|       :class="`${prefixCls}__tool`" | ||||
|       class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer" | ||||
|       @click="move(200)" | ||||
|     > | ||||
|       <Icon | ||||
|         icon="ep:d-arrow-right" | ||||
|         :color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'" | ||||
|       /> | ||||
|     </span> | ||||
|     <span | ||||
|       :class="`${prefixCls}__tool`" | ||||
|       class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer" | ||||
|       @click="refreshSelectedTag(selectedTag)" | ||||
|     > | ||||
|       <Icon | ||||
|         icon="ep:refresh-right" | ||||
|         :color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'" | ||||
|       /> | ||||
|     </span> | ||||
|     <ContextMenu | ||||
|       trigger="click" | ||||
|       :schema="[ | ||||
|         { | ||||
|           icon: 'ep:refresh', | ||||
|           label: t('common.reload'), | ||||
|           command: () => { | ||||
|             refreshSelectedTag(selectedTag) | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           icon: 'ep:close', | ||||
|           label: t('common.closeTab'), | ||||
|           disabled: !!visitedViews?.length && selectedTag?.meta.affix | ||||
|         }, | ||||
|         { | ||||
|           divided: true, | ||||
|           icon: 'ep:d-arrow-left', | ||||
|           label: t('common.closeTheLeftTab'), | ||||
|           disabled: !!visitedViews?.length && selectedTag?.fullPath === visitedViews[0].fullPath, | ||||
|           command: () => { | ||||
|             closeLeftTags() | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           icon: 'ep:d-arrow-right', | ||||
|           label: t('common.closeTheRightTab'), | ||||
|           disabled: | ||||
|             !!visitedViews?.length && | ||||
|             selectedTag?.fullPath === visitedViews[visitedViews.length - 1].fullPath, | ||||
|           command: () => { | ||||
|             closeRightTags() | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           divided: true, | ||||
|           icon: 'ep:discount', | ||||
|           label: t('common.closeOther'), | ||||
|           command: () => { | ||||
|             closeOthersTags() | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           icon: 'ep:minus', | ||||
|           label: t('common.closeAll'), | ||||
|           command: () => { | ||||
|             closeAllTags() | ||||
|           } | ||||
|         } | ||||
|       ]" | ||||
|     > | ||||
|       <span | ||||
|         :class="`${prefixCls}__tool`" | ||||
|         class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer block" | ||||
|       > | ||||
|         <Icon | ||||
|           icon="ep:menu" | ||||
|           :color="appStore.getIsDark ? 'var(--el-text-color-regular)' : '#333'" | ||||
|         /> | ||||
|       </span> | ||||
|     </ContextMenu> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| $prefix-cls: #{$namespace}-tags-view; | ||||
|  | ||||
| .#{$prefix-cls} { | ||||
|   :deep(.#{$elNamespace}-scrollbar__view) { | ||||
|     height: 100%; | ||||
|   } | ||||
|  | ||||
|   &__tool { | ||||
|     position: relative; | ||||
|     display: inline-flex; | ||||
|     align-items: center; | ||||
|     justify-content: center; | ||||
|  | ||||
|     &:hover { | ||||
|       :deep(span) { | ||||
|         color: var(--el-color-black) !important; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &:after { | ||||
|       position: absolute; | ||||
|       top: 1px; | ||||
|       left: 0; | ||||
|       width: 100%; | ||||
|       height: calc(100% - 1px); | ||||
|       border-right: 1px solid var(--tags-view-border-color); | ||||
|       border-left: 1px solid var(--tags-view-border-color); | ||||
|       content: ''; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &__item { | ||||
|     position: relative; | ||||
|     top: 2px; | ||||
|     height: calc(100% - 4px); | ||||
|     padding-right: 16px; | ||||
|     margin-left: 4px; | ||||
|     font-size: 12px; | ||||
|     border-radius: 3px 3px 3px 3px; | ||||
|     cursor: pointer; | ||||
|     border: 1px solid #d9d9d9; | ||||
|  | ||||
|     &--close { | ||||
|       position: absolute; | ||||
|       top: 50%; | ||||
|       right: 3px; | ||||
|       display: none; | ||||
|       transform: translate(0, -50%); | ||||
|     } | ||||
|     &:not(.#{$prefix-cls}__item--affix):hover { | ||||
|       .#{$prefix-cls}__item--close { | ||||
|         display: block; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &__item:not(.is-active) { | ||||
|     &:hover { | ||||
|       color: var(--el-color-primary); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &__item.is-active { | ||||
|     color: var(--el-color-white); | ||||
|     background-color: var(--el-color-primary); | ||||
|     border: 1px solid var(--el-color-primary); | ||||
|     .#{$prefix-cls}__item--close { | ||||
|       :deep(span) { | ||||
|         color: var(--el-color-white) !important; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .dark { | ||||
|   .#{$prefix-cls} { | ||||
|     &__tool { | ||||
|       &:hover { | ||||
|         :deep(span) { | ||||
|           color: #fff !important; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       &:after { | ||||
|         border-right: 1px solid var(--el-border-color); | ||||
|         border-left: 1px solid var(--el-border-color); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &__item { | ||||
|       position: relative; | ||||
|       top: 2px; | ||||
|       height: calc(100% - 4px); | ||||
|       padding-right: 16px; | ||||
|       font-size: 12px; | ||||
|       border-radius: 3px 3px 3px 3px; | ||||
|       cursor: pointer; | ||||
|       border: 1px solid var(--el-border-color); | ||||
|     } | ||||
|  | ||||
|     &__item:not(.is-active) { | ||||
|       &:hover { | ||||
|         color: var(--el-color-primary); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     &__item.is-active { | ||||
|       color: var(--el-color-white); | ||||
|       background-color: var(--el-color-primary); | ||||
|       .#{$prefix-cls}__item--close { | ||||
|         :deep(span) { | ||||
|           color: var(--el-color-white) !important; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										21
									
								
								src/layout/components/TagsView/src/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/layout/components/TagsView/src/helper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| import type { RouteMeta, RouteLocationNormalizedLoaded } from 'vue-router' | ||||
| import { pathResolve } from '@/utils/routerHelper' | ||||
|  | ||||
| export const filterAffixTags = (routes: AppRouteRecordRaw[], parentPath = '') => { | ||||
|   let tags: RouteLocationNormalizedLoaded[] = [] | ||||
|   routes.forEach((route) => { | ||||
|     const meta = route.meta as RouteMeta | ||||
|     const tagPath = pathResolve(parentPath, route.path) | ||||
|     if (meta?.affix) { | ||||
|       tags.push({ ...route, path: tagPath, fullPath: tagPath } as RouteLocationNormalizedLoaded) | ||||
|     } | ||||
|     if (route.children) { | ||||
|       const tempTags: RouteLocationNormalizedLoaded[] = filterAffixTags(route.children, tagPath) | ||||
|       if (tempTags.length >= 1) { | ||||
|         tags = [...tags, ...tempTags] | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|  | ||||
|   return tags | ||||
| } | ||||
							
								
								
									
										3
									
								
								src/layout/components/ThemeSwitch/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/ThemeSwitch/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import ThemeSwitch from './src/ThemeSwitch.vue' | ||||
|  | ||||
| export { ThemeSwitch } | ||||
							
								
								
									
										39
									
								
								src/layout/components/ThemeSwitch/src/ThemeSwitch.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/layout/components/ThemeSwitch/src/ThemeSwitch.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { useIcon } from '@/hooks/web/useIcon' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('theme-switch') | ||||
|  | ||||
| const Sun = useIcon({ icon: 'emojione-monotone:sun', color: '#fde047' }) | ||||
|  | ||||
| const CrescentMoon = useIcon({ icon: 'emojione-monotone:crescent-moon', color: '#fde047' }) | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| // 初始化获取是否是暗黑主题 | ||||
| const isDark = ref(appStore.getIsDark) | ||||
|  | ||||
| // 设置switch的背景颜色 | ||||
| const blackColor = 'var(--el-color-black)' | ||||
|  | ||||
| const themeChange = (val: boolean) => { | ||||
|   appStore.setIsDark(val) | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <ElSwitch | ||||
|     :class="prefixCls" | ||||
|     v-model="isDark" | ||||
|     inline-prompt | ||||
|     :border-color="blackColor" | ||||
|     :inactive-color="blackColor" | ||||
|     :active-color="blackColor" | ||||
|     :active-icon="Sun" | ||||
|     :inactive-icon="CrescentMoon" | ||||
|     @change="themeChange" | ||||
|   /> | ||||
| </template> | ||||
							
								
								
									
										90
									
								
								src/layout/components/ToolHeader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/layout/components/ToolHeader.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| <script lang="tsx"> | ||||
| import { defineComponent, computed } from 'vue' | ||||
| import { Message } from '@/layout/components//Message' | ||||
| import { Collapse } from '@/layout/components/Collapse' | ||||
| import { UserInfo } from '@/layout/components/UserInfo' | ||||
| import { Screenfull } from '@/layout/components/Screenfull' | ||||
| import { Breadcrumb } from '@/layout/components/Breadcrumb' | ||||
| import { SizeDropdown } from '@/layout/components/SizeDropdown' | ||||
| import { LocaleDropdown } from '@/layout/components/LocaleDropdown' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls, variables } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('tool-header') | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| // 面包屑 | ||||
| const breadcrumb = computed(() => appStore.getBreadcrumb) | ||||
|  | ||||
| // 折叠图标 | ||||
| const hamburger = computed(() => appStore.getHamburger) | ||||
|  | ||||
| // 全屏图标 | ||||
| const screenfull = computed(() => appStore.getScreenfull) | ||||
|  | ||||
| // 尺寸图标 | ||||
| const size = computed(() => appStore.getSize) | ||||
|  | ||||
| // 布局 | ||||
| const layout = computed(() => appStore.getLayout) | ||||
|  | ||||
| // 多语言图标 | ||||
| const locale = computed(() => appStore.getLocale) | ||||
|  | ||||
| // 消息图标 | ||||
| const message = computed(() => appStore.getMessage) | ||||
|  | ||||
| export default defineComponent({ | ||||
|   name: 'ToolHeader', | ||||
|   setup() { | ||||
|     return () => ( | ||||
|       <div | ||||
|         id={`${variables.namespace}-tool-header`} | ||||
|         class={[ | ||||
|           prefixCls, | ||||
|           'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between', | ||||
|           'dark:bg-[var(--el-bg-color)]' | ||||
|         ]} | ||||
|       > | ||||
|         {layout.value !== 'top' ? ( | ||||
|           <div class="h-full flex items-center"> | ||||
|             {hamburger.value && layout.value !== 'cutMenu' ? ( | ||||
|               <Collapse class="hover-trigger" color="var(--top-header-text-color)"></Collapse> | ||||
|             ) : undefined} | ||||
|             {breadcrumb.value ? <Breadcrumb class="<md:hidden"></Breadcrumb> : undefined} | ||||
|           </div> | ||||
|         ) : undefined} | ||||
|         <div class="h-full flex items-center"> | ||||
|           {screenfull.value ? ( | ||||
|             <Screenfull class="hover-trigger" color="var(--top-header-text-color)"></Screenfull> | ||||
|           ) : undefined} | ||||
|           {size.value ? ( | ||||
|             <SizeDropdown class="hover-trigger" color="var(--top-header-text-color)"></SizeDropdown> | ||||
|           ) : undefined} | ||||
|           {locale.value ? ( | ||||
|             <LocaleDropdown | ||||
|               class="hover-trigger" | ||||
|               color="var(--top-header-text-color)" | ||||
|             ></LocaleDropdown> | ||||
|           ) : undefined} | ||||
|           {message.value ? ( | ||||
|             <Message class="hover-trigger" color="var(--top-header-text-color)"></Message> | ||||
|           ) : undefined} | ||||
|           <UserInfo class="hover-trigger"></UserInfo> | ||||
|         </div> | ||||
|       </div> | ||||
|     ) | ||||
|   } | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| $prefix-cls: #{$namespace}-tool-header; | ||||
|  | ||||
| .#{$prefix-cls} { | ||||
|   transition: left var(--transition-time-02); | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										3
									
								
								src/layout/components/UserInfo/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/layout/components/UserInfo/index.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| import UserInfo from './src/UserInfo.vue' | ||||
|  | ||||
| export { UserInfo } | ||||
							
								
								
									
										76
									
								
								src/layout/components/UserInfo/src/UserInfo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/layout/components/UserInfo/src/UserInfo.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| <script setup lang="ts"> | ||||
| import { ElMessageBox } from 'element-plus' | ||||
|  | ||||
| import { CACHE_KEY, useCache } from '@/hooks/web/useCache' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
| import avatarImg from '@/assets/imgs/avatar.gif' | ||||
| import { useUserStore } from '@/store/modules/user' | ||||
| import { useTagsViewStore } from '@/store/modules/tagsView' | ||||
|  | ||||
| const { t } = useI18n() | ||||
|  | ||||
| const { wsCache } = useCache() | ||||
|  | ||||
| const { push, replace } = useRouter() | ||||
|  | ||||
| const userStore = useUserStore() | ||||
|  | ||||
| const tagsViewStore = useTagsViewStore() | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('user-info') | ||||
|  | ||||
| const user = wsCache.get(CACHE_KEY.USER) | ||||
|  | ||||
| const avatar = user.user.avatar ? user.user.avatar : avatarImg | ||||
|  | ||||
| const userName = user.user.nickname ? user.user.nickname : 'Admin' | ||||
|  | ||||
| const loginOut = () => { | ||||
|   ElMessageBox.confirm(t('common.loginOutMessage'), t('common.reminder'), { | ||||
|     confirmButtonText: t('common.ok'), | ||||
|     cancelButtonText: t('common.cancel'), | ||||
|     type: 'warning' | ||||
|   }) | ||||
|     .then(async () => { | ||||
|       await userStore.loginOut() | ||||
|       tagsViewStore.delAllViews() | ||||
|       replace('/login?redirect=/index') | ||||
|     }) | ||||
|     .catch(() => {}) | ||||
| } | ||||
| const toProfile = async () => { | ||||
|   push('/user/profile') | ||||
| } | ||||
| const toDocument = () => { | ||||
|   window.open('https://doc.iocoder.cn/') | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <template> | ||||
|   <ElDropdown :class="prefixCls" trigger="click"> | ||||
|     <div class="flex items-center"> | ||||
|       <img :src="avatar" alt="" class="w-[calc(var(--logo-height)-25px)] rounded-[50%]" /> | ||||
|       <span class="<lg:hidden text-14px pl-[5px] text-[var(--top-header-text-color)]"> | ||||
|         {{ userName }} | ||||
|       </span> | ||||
|     </div> | ||||
|     <template #dropdown> | ||||
|       <ElDropdownMenu> | ||||
|         <ElDropdownItem> | ||||
|           <Icon icon="ep:tools" /> | ||||
|           <div @click="toProfile">{{ t('common.profile') }}</div> | ||||
|         </ElDropdownItem> | ||||
|         <ElDropdownItem> | ||||
|           <Icon icon="ep:menu" /> | ||||
|           <div @click="toDocument">{{ t('common.document') }}</div> | ||||
|         </ElDropdownItem> | ||||
|         <ElDropdownItem divided> | ||||
|           <Icon icon="ep:switch-button" /> | ||||
|           <div @click="loginOut">{{ t('common.loginOut') }}</div> | ||||
|         </ElDropdownItem> | ||||
|       </ElDropdownMenu> | ||||
|     </template> | ||||
|   </ElDropdown> | ||||
| </template> | ||||
							
								
								
									
										276
									
								
								src/layout/components/useRenderLayout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										276
									
								
								src/layout/components/useRenderLayout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,276 @@ | ||||
| import { computed } from 'vue' | ||||
| import { useAppStore } from '@/store/modules/app' | ||||
| import { Menu } from '@/layout/components/Menu' | ||||
| import { TabMenu } from '@/layout/components/TabMenu' | ||||
| import { TagsView } from '@/layout/components/TagsView' | ||||
| import { Logo } from '@/layout/components/Logo' | ||||
| import AppView from './AppView.vue' | ||||
| import ToolHeader from './ToolHeader.vue' | ||||
| import { ElScrollbar } from 'element-plus' | ||||
| import { useDesign } from '@/hooks/web/useDesign' | ||||
|  | ||||
| const { getPrefixCls } = useDesign() | ||||
|  | ||||
| const prefixCls = getPrefixCls('layout') | ||||
|  | ||||
| const appStore = useAppStore() | ||||
|  | ||||
| const pageLoading = computed(() => appStore.getPageLoading) | ||||
|  | ||||
| // 标签页 | ||||
| const tagsView = computed(() => appStore.getTagsView) | ||||
|  | ||||
| // 菜单折叠 | ||||
| const collapse = computed(() => appStore.getCollapse) | ||||
|  | ||||
| // logo | ||||
| const logo = computed(() => appStore.logo) | ||||
|  | ||||
| // 固定头部 | ||||
| const fixedHeader = computed(() => appStore.getFixedHeader) | ||||
|  | ||||
| // 是否是移动端 | ||||
| const mobile = computed(() => appStore.getMobile) | ||||
|  | ||||
| // 固定菜单 | ||||
| const fixedMenu = computed(() => appStore.getFixedMenu) | ||||
|  | ||||
| export const useRenderLayout = () => { | ||||
|   const renderClassic = () => { | ||||
|     return ( | ||||
|       <> | ||||
|         <div class={['absolute top-0 left-0 h-full', { '!fixed z-3000': mobile.value }]}> | ||||
|           {logo.value ? ( | ||||
|             <Logo | ||||
|               class={[ | ||||
|                 'bg-[var(--left-menu-bg-color)] border-bottom-1 border-solid border-[var(--logo-border-color)] dark:border-[var(--el-border-color)]', | ||||
|                 { | ||||
|                   '!pl-0': mobile.value && collapse.value, | ||||
|                   'w-[var(--left-menu-min-width)]': appStore.getCollapse, | ||||
|                   'w-[var(--left-menu-max-width)]': !appStore.getCollapse | ||||
|                 } | ||||
|               ]} | ||||
|               style="transition: all var(--transition-time-02);" | ||||
|             ></Logo> | ||||
|           ) : undefined} | ||||
|           <Menu class={[{ '!h-[calc(100%-var(--logo-height))]': logo.value }]}></Menu> | ||||
|         </div> | ||||
|         <div | ||||
|           class={[ | ||||
|             `${prefixCls}-content`, | ||||
|             'absolute top-0 h-[100%]', | ||||
|             { | ||||
|               'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]': | ||||
|                 collapse.value && !mobile.value && !mobile.value, | ||||
|               'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]': | ||||
|                 !collapse.value && !mobile.value && !mobile.value, | ||||
|               'fixed !w-full !left-0': mobile.value | ||||
|             } | ||||
|           ]} | ||||
|           style="transition: all var(--transition-time-02);" | ||||
|         > | ||||
|           <ElScrollbar | ||||
|             v-loading={pageLoading.value} | ||||
|             class={[ | ||||
|               `${prefixCls}-content-scrollbar`, | ||||
|               { | ||||
|                 '!h-[calc(100%-var(--top-tool-height)-var(--tags-view-height))] mt-[calc(var(--top-tool-height)+var(--tags-view-height))]': | ||||
|                   fixedHeader.value | ||||
|               } | ||||
|             ]} | ||||
|           > | ||||
|             <div | ||||
|               class={[ | ||||
|                 { | ||||
|                   'fixed top-0 left-0 z-10': fixedHeader.value, | ||||
|                   'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]': | ||||
|                     collapse.value && fixedHeader.value && !mobile.value, | ||||
|                   'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]': | ||||
|                     !collapse.value && fixedHeader.value && !mobile.value, | ||||
|                   '!w-full !left-0': mobile.value | ||||
|                 } | ||||
|               ]} | ||||
|               style="transition: all var(--transition-time-02);" | ||||
|             > | ||||
|               <ToolHeader class="border-bottom-1 border-solid border-[var(--top-tool-border-color)] bg-[var(--top-header-bg-color)] dark:border-[var(--el-border-color)]"></ToolHeader> | ||||
|  | ||||
|               {tagsView.value ? ( | ||||
|                 <TagsView class="border-bottom-1 border-top-1 border-solid border-[var(--tags-view-border-color)] dark:border-[var(--el-border-color)]"></TagsView> | ||||
|               ) : undefined} | ||||
|             </div> | ||||
|  | ||||
|             <AppView></AppView> | ||||
|           </ElScrollbar> | ||||
|         </div> | ||||
|       </> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   const renderTopLeft = () => { | ||||
|     return ( | ||||
|       <> | ||||
|         <div class="flex items-center bg-[var(--top-header-bg-color)] border-bottom-1 border-solid border-[var(--top-tool-border-color)] dark:border-[var(--el-border-color)]"> | ||||
|           {logo.value ? <Logo class="hover-trigger !pr-15px"></Logo> : undefined} | ||||
|  | ||||
|           <ToolHeader class="flex-1"></ToolHeader> | ||||
|         </div> | ||||
|         <div class="absolute top-[var(--logo-height)+1px] left-0 w-full h-[calc(100%-1px-var(--logo-height))] flex"> | ||||
|           <Menu class="!h-full"></Menu> | ||||
|           <div | ||||
|             class={[ | ||||
|               `${prefixCls}-content`, | ||||
|               'h-[100%]', | ||||
|               { | ||||
|                 'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)]': | ||||
|                   collapse.value, | ||||
|                 'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)]': | ||||
|                   !collapse.value | ||||
|               } | ||||
|             ]} | ||||
|             style="transition: all var(--transition-time-02);" | ||||
|           > | ||||
|             <ElScrollbar | ||||
|               v-loading={pageLoading.value} | ||||
|               class={[ | ||||
|                 `${prefixCls}-content-scrollbar`, | ||||
|                 { | ||||
|                   '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]': | ||||
|                     fixedHeader.value && tagsView.value | ||||
|                 } | ||||
|               ]} | ||||
|             > | ||||
|               {tagsView.value ? ( | ||||
|                 <TagsView | ||||
|                   class={[ | ||||
|                     'border-bottom-1 border-top-1 border-solid border-[var(--tags-view-border-color)] dark:border-[var(--el-border-color)]', | ||||
|                     { | ||||
|                       '!fixed top-0 left-0 z-10': fixedHeader.value, | ||||
|                       'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)] mt-[var(--logo-height)]': | ||||
|                         collapse.value && fixedHeader.value, | ||||
|                       'w-[calc(100%-var(--left-menu-max-width))] left-[var(--left-menu-max-width)] mt-[var(--logo-height)]': | ||||
|                         !collapse.value && fixedHeader.value | ||||
|                     } | ||||
|                   ]} | ||||
|                   style="transition: width var(--transition-time-02), left var(--transition-time-02);" | ||||
|                 ></TagsView> | ||||
|               ) : undefined} | ||||
|  | ||||
|               <AppView></AppView> | ||||
|             </ElScrollbar> | ||||
|           </div> | ||||
|         </div> | ||||
|       </> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   const renderTop = () => { | ||||
|     return ( | ||||
|       <> | ||||
|         <div class="flex items-center justify-between bg-[var(--top-header-bg-color)] border-bottom-1 border-solid border-[var(--top-tool-border-color)] dark:border-[var(--el-border-color)]"> | ||||
|           {logo.value ? <Logo class="hover-trigger"></Logo> : undefined} | ||||
|           <Menu class="flex-1 px-10px h-[var(--top-tool-height)]"></Menu> | ||||
|           <ToolHeader></ToolHeader> | ||||
|         </div> | ||||
|         <div class={[`${prefixCls}-content`, 'h-full w-full']}> | ||||
|           <ElScrollbar | ||||
|             v-loading={pageLoading.value} | ||||
|             class={[ | ||||
|               `${prefixCls}-content-scrollbar`, | ||||
|               { | ||||
|                 'mt-[var(--tags-view-height)]': fixedHeader.value | ||||
|               } | ||||
|             ]} | ||||
|           > | ||||
|             {tagsView.value ? ( | ||||
|               <TagsView | ||||
|                 class={[ | ||||
|                   'border-bottom-1 border-top-1 border-solid border-[var(--tags-view-border-color)] dark:border-[var(--el-border-color)]', | ||||
|                   { | ||||
|                     '!fixed w-full top-[var(--top-tool-height)] left-0': fixedHeader.value | ||||
|                   } | ||||
|                 ]} | ||||
|                 style="transition: width var(--transition-time-02), left var(--transition-time-02);" | ||||
|               ></TagsView> | ||||
|             ) : undefined} | ||||
|  | ||||
|             <AppView></AppView> | ||||
|           </ElScrollbar> | ||||
|         </div> | ||||
|       </> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   const renderCutMenu = () => { | ||||
|     return ( | ||||
|       <> | ||||
|         <div class="flex items-center bg-[var(--top-header-bg-color)] border-bottom-1 border-solid border-[var(--top-tool-border-color)] dark:border-[var(--el-border-color)]"> | ||||
|           {logo.value ? <Logo class="hover-trigger !pr-15px"></Logo> : undefined} | ||||
|  | ||||
|           <ToolHeader class="flex-1"></ToolHeader> | ||||
|         </div> | ||||
|         <div class="absolute top-[var(--logo-height)] left-0 w-full h-[calc(100%-var(--logo-height))] flex"> | ||||
|           <TabMenu></TabMenu> | ||||
|           <div | ||||
|             class={[ | ||||
|               `${prefixCls}-content`, | ||||
|               'h-[100%]', | ||||
|               { | ||||
|                 'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)]': | ||||
|                   collapse.value && !fixedMenu.value, | ||||
|                 'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)]': | ||||
|                   !collapse.value && !fixedMenu.value, | ||||
|                 'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]': | ||||
|                   collapse.value && fixedMenu.value, | ||||
|                 'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] ml-[var(--left-menu-max-width)]': | ||||
|                   !collapse.value && fixedMenu.value | ||||
|               } | ||||
|             ]} | ||||
|             style="transition: all var(--transition-time-02);" | ||||
|           > | ||||
|             <ElScrollbar | ||||
|               v-loading={pageLoading.value} | ||||
|               class={[ | ||||
|                 `${prefixCls}-content-scrollbar`, | ||||
|                 { | ||||
|                   '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]': | ||||
|                     fixedHeader.value && tagsView.value | ||||
|                 } | ||||
|               ]} | ||||
|             > | ||||
|               {tagsView.value ? ( | ||||
|                 <TagsView | ||||
|                   class={[ | ||||
|                     'border-bottom-1 border-top-1 border-solid border-[var(--tags-view-border-color)] dark:border-[var(--el-border-color)]', | ||||
|                     { | ||||
|                       '!fixed top-0 left-0 z-10': fixedHeader.value, | ||||
|                       'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]': | ||||
|                         collapse.value && fixedHeader.value, | ||||
|                       'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]': | ||||
|                         !collapse.value && fixedHeader.value, | ||||
|                       '!fixed top-0 left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] z-10': | ||||
|                         fixedHeader.value && fixedMenu.value, | ||||
|                       'w-[calc(100%-var(--tab-menu-min-width)-var(--left-menu-max-width))] left-[var(--tab-menu-min-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]': | ||||
|                         collapse.value && fixedHeader.value && fixedMenu.value, | ||||
|                       'w-[calc(100%-var(--tab-menu-max-width)-var(--left-menu-max-width))] left-[var(--tab-menu-max-width)+var(--left-menu-max-width)] mt-[var(--logo-height)]': | ||||
|                         !collapse.value && fixedHeader.value && fixedMenu.value | ||||
|                     } | ||||
|                   ]} | ||||
|                   style="transition: width var(--transition-time-02), left var(--transition-time-02);" | ||||
|                 ></TagsView> | ||||
|               ) : undefined} | ||||
|  | ||||
|               <AppView></AppView> | ||||
|             </ElScrollbar> | ||||
|           </div> | ||||
|         </div> | ||||
|       </> | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   return { | ||||
|     renderClassic, | ||||
|     renderTopLeft, | ||||
|     renderTop, | ||||
|     renderCutMenu | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 YunaiV
					YunaiV