RuoYi-Vue 1.0
This commit is contained in:
		
							
								
								
									
										57
									
								
								ruoyi-ui/src/layout/components/AppMain.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								ruoyi-ui/src/layout/components/AppMain.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | ||||
| <template> | ||||
|   <section class="app-main"> | ||||
|     <transition name="fade-transform" mode="out-in"> | ||||
|       <keep-alive :include="cachedViews"> | ||||
|         <router-view :key="key" /> | ||||
|       </keep-alive> | ||||
|     </transition> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| export default { | ||||
|   name: 'AppMain', | ||||
|   computed: { | ||||
|     cachedViews() { | ||||
|       return this.$store.state.tagsView.cachedViews | ||||
|     }, | ||||
|     key() { | ||||
|       return this.$route.path | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .app-main { | ||||
|   /* 50= navbar  50  */ | ||||
|   min-height: calc(100vh - 50px); | ||||
|   width: 100%; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
| } | ||||
|  | ||||
| .fixed-header+.app-main { | ||||
|   padding-top: 50px; | ||||
| } | ||||
|  | ||||
| .hasTagsView { | ||||
|   .app-main { | ||||
|     /* 84 = navbar + tags-view = 50 + 34 */ | ||||
|     min-height: calc(100vh - 84px); | ||||
|   } | ||||
|  | ||||
|   .fixed-header+.app-main { | ||||
|     padding-top: 84px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <style lang="scss"> | ||||
| // fix css style bug in open el-dialog | ||||
| .el-popup-parent--hidden { | ||||
|   .fixed-header { | ||||
|     padding-right: 15px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										181
									
								
								ruoyi-ui/src/layout/components/Navbar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								ruoyi-ui/src/layout/components/Navbar.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,181 @@ | ||||
| <template> | ||||
|   <div class="navbar"> | ||||
|     <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> | ||||
|  | ||||
|     <breadcrumb id="breadcrumb-container" class="breadcrumb-container" /> | ||||
|  | ||||
|     <div class="right-menu"> | ||||
|       <template v-if="device!=='mobile'"> | ||||
|         <search id="header-search" class="right-menu-item" /> | ||||
|          | ||||
|         <el-tooltip content="下载源码" effect="dark" placement="bottom"> | ||||
|           <ruo-yi id="ruoyi" class="right-menu-item hover-effect" /> | ||||
|         </el-tooltip> | ||||
|  | ||||
|         <screenfull id="screenfull" class="right-menu-item hover-effect" /> | ||||
|  | ||||
|         <el-tooltip content="布局大小" effect="dark" placement="bottom"> | ||||
|           <size-select id="size-select" class="right-menu-item hover-effect" /> | ||||
|         </el-tooltip> | ||||
|  | ||||
|       </template> | ||||
|  | ||||
|       <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click"> | ||||
|         <div class="avatar-wrapper"> | ||||
|           <img :src="avatar" class="user-avatar"> | ||||
|           <i class="el-icon-caret-bottom" /> | ||||
|         </div> | ||||
|         <el-dropdown-menu slot="dropdown"> | ||||
|           <router-link to="/user/profile"> | ||||
|             <el-dropdown-item>个人中心</el-dropdown-item> | ||||
|           </router-link> | ||||
|           <el-dropdown-item> | ||||
|             <span @click="setting = true">布局设置</span> | ||||
|           </el-dropdown-item> | ||||
|           <el-dropdown-item divided> | ||||
|             <span @click="logout">退出登录</span> | ||||
|           </el-dropdown-item> | ||||
|         </el-dropdown-menu> | ||||
|       </el-dropdown> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex' | ||||
| import Breadcrumb from '@/components/Breadcrumb' | ||||
| import Hamburger from '@/components/Hamburger' | ||||
| import Screenfull from '@/components/Screenfull' | ||||
| import SizeSelect from '@/components/SizeSelect' | ||||
| import Search from '@/components/HeaderSearch' | ||||
| import RuoYi from '@/components/RuoYi' | ||||
|  | ||||
| export default { | ||||
|   components: { | ||||
|     Breadcrumb, | ||||
|     Hamburger, | ||||
|     Screenfull, | ||||
|     SizeSelect, | ||||
|     Search, | ||||
|     RuoYi | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapGetters([ | ||||
|       'sidebar', | ||||
|       'avatar', | ||||
|       'device' | ||||
|     ]), | ||||
|     setting: { | ||||
|       get() { | ||||
|         return this.$store.state.settings.showSettings | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$store.dispatch('settings/changeSetting', { | ||||
|           key: 'showSettings', | ||||
|           value: val | ||||
|         }) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     toggleSideBar() { | ||||
|       this.$store.dispatch('app/toggleSideBar') | ||||
|     }, | ||||
|     async logout() { | ||||
|       this.$confirm('确定注销并退出系统吗?', '提示', { | ||||
|         confirmButtonText: '确定', | ||||
|         cancelButtonText: '取消', | ||||
|         type: 'warning' | ||||
|       }).then(() => { | ||||
|         this.$store.dispatch('LogOut').then(() => { | ||||
|           location.reload() | ||||
|         }) | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .navbar { | ||||
|   height: 50px; | ||||
|   overflow: hidden; | ||||
|   position: relative; | ||||
|   background: #fff; | ||||
|   box-shadow: 0 1px 4px rgba(0,21,41,.08); | ||||
|  | ||||
|   .hamburger-container { | ||||
|     line-height: 46px; | ||||
|     height: 100%; | ||||
|     float: left; | ||||
|     cursor: pointer; | ||||
|     transition: background .3s; | ||||
|     -webkit-tap-highlight-color:transparent; | ||||
|  | ||||
|     &:hover { | ||||
|       background: rgba(0, 0, 0, .025) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .breadcrumb-container { | ||||
|     float: left; | ||||
|   } | ||||
|  | ||||
|   .errLog-container { | ||||
|     display: inline-block; | ||||
|     vertical-align: top; | ||||
|   } | ||||
|  | ||||
|   .right-menu { | ||||
|     float: right; | ||||
|     height: 100%; | ||||
|     line-height: 50px; | ||||
|  | ||||
|     &:focus { | ||||
|       outline: none; | ||||
|     } | ||||
|  | ||||
|     .right-menu-item { | ||||
|       display: inline-block; | ||||
|       padding: 0 8px; | ||||
|       height: 100%; | ||||
|       font-size: 18px; | ||||
|       color: #5a5e66; | ||||
|       vertical-align: text-bottom; | ||||
|  | ||||
|       &.hover-effect { | ||||
|         cursor: pointer; | ||||
|         transition: background .3s; | ||||
|  | ||||
|         &:hover { | ||||
|           background: rgba(0, 0, 0, .025) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .avatar-container { | ||||
|       margin-right: 30px; | ||||
|  | ||||
|       .avatar-wrapper { | ||||
|         margin-top: 5px; | ||||
|         position: relative; | ||||
|  | ||||
|         .user-avatar { | ||||
|           cursor: pointer; | ||||
|           width: 40px; | ||||
|           height: 40px; | ||||
|           border-radius: 10px; | ||||
|         } | ||||
|  | ||||
|         .el-icon-caret-bottom { | ||||
|           cursor: pointer; | ||||
|           position: absolute; | ||||
|           right: -20px; | ||||
|           top: 25px; | ||||
|           font-size: 12px; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										108
									
								
								ruoyi-ui/src/layout/components/Settings/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								ruoyi-ui/src/layout/components/Settings/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| <template> | ||||
|   <div class="drawer-container"> | ||||
|     <div> | ||||
|       <h3 class="drawer-title">系统布局配置</h3> | ||||
|  | ||||
|       <div class="drawer-item"> | ||||
|         <span>主题颜色</span> | ||||
|         <theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" /> | ||||
|       </div> | ||||
|  | ||||
|       <div class="drawer-item"> | ||||
|         <span>开启 Tags-Views</span> | ||||
|         <el-switch v-model="tagsView" class="drawer-switch" /> | ||||
|       </div> | ||||
|  | ||||
|       <div class="drawer-item"> | ||||
|         <span>固定 Header</span> | ||||
|         <el-switch v-model="fixedHeader" class="drawer-switch" /> | ||||
|       </div> | ||||
|  | ||||
|       <div class="drawer-item"> | ||||
|         <span>显示 Logo</span> | ||||
|         <el-switch v-model="sidebarLogo" class="drawer-switch" /> | ||||
|       </div> | ||||
|  | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ThemePicker from '@/components/ThemePicker' | ||||
|  | ||||
| export default { | ||||
|   components: { ThemePicker }, | ||||
|   data() { | ||||
|     return {} | ||||
|   }, | ||||
|   computed: { | ||||
|     fixedHeader: { | ||||
|       get() { | ||||
|         return this.$store.state.settings.fixedHeader | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$store.dispatch('settings/changeSetting', { | ||||
|           key: 'fixedHeader', | ||||
|           value: val | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     tagsView: { | ||||
|       get() { | ||||
|         return this.$store.state.settings.tagsView | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$store.dispatch('settings/changeSetting', { | ||||
|           key: 'tagsView', | ||||
|           value: val | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|     sidebarLogo: { | ||||
|       get() { | ||||
|         return this.$store.state.settings.sidebarLogo | ||||
|       }, | ||||
|       set(val) { | ||||
|         this.$store.dispatch('settings/changeSetting', { | ||||
|           key: 'sidebarLogo', | ||||
|           value: val | ||||
|         }) | ||||
|       } | ||||
|     }, | ||||
|   }, | ||||
|   methods: { | ||||
|     themeChange(val) { | ||||
|       this.$store.dispatch('settings/changeSetting', { | ||||
|         key: 'theme', | ||||
|         value: val | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .drawer-container { | ||||
|   padding: 24px; | ||||
|   font-size: 14px; | ||||
|   line-height: 1.5; | ||||
|   word-wrap: break-word; | ||||
|  | ||||
|   .drawer-title { | ||||
|     margin-bottom: 12px; | ||||
|     color: rgba(0, 0, 0, .85); | ||||
|     font-size: 14px; | ||||
|     line-height: 22px; | ||||
|   } | ||||
|  | ||||
|   .drawer-item { | ||||
|     color: rgba(0, 0, 0, .65); | ||||
|     font-size: 14px; | ||||
|     padding: 12px 0; | ||||
|   } | ||||
|  | ||||
|   .drawer-switch { | ||||
|     float: right | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										25
									
								
								ruoyi-ui/src/layout/components/Sidebar/FixiOSBug.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								ruoyi-ui/src/layout/components/Sidebar/FixiOSBug.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| export default { | ||||
|   computed: { | ||||
|     device() { | ||||
|       return this.$store.state.app.device | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     // In order to fix the click on menu on the ios device will trigger the mouseleave bug | ||||
|     this.fixBugIniOS() | ||||
|   }, | ||||
|   methods: { | ||||
|     fixBugIniOS() { | ||||
|       const $subMenu = this.$refs.subMenu | ||||
|       if ($subMenu) { | ||||
|         const handleMouseleave = $subMenu.handleMouseleave | ||||
|         $subMenu.handleMouseleave = (e) => { | ||||
|           if (this.device === 'mobile') { | ||||
|             return | ||||
|           } | ||||
|           handleMouseleave(e) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										29
									
								
								ruoyi-ui/src/layout/components/Sidebar/Item.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ruoyi-ui/src/layout/components/Sidebar/Item.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| <script> | ||||
| export default { | ||||
|   name: 'MenuItem', | ||||
|   functional: true, | ||||
|   props: { | ||||
|     icon: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     }, | ||||
|     title: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     } | ||||
|   }, | ||||
|   render(h, context) { | ||||
|     const { icon, title } = context.props | ||||
|     const vnodes = [] | ||||
|  | ||||
|     if (icon) { | ||||
|       vnodes.push(<svg-icon icon-class={icon}/>) | ||||
|     } | ||||
|  | ||||
|     if (title) { | ||||
|       vnodes.push(<span slot='title'>{(title)}</span>) | ||||
|     } | ||||
|     return vnodes | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										36
									
								
								ruoyi-ui/src/layout/components/Sidebar/Link.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								ruoyi-ui/src/layout/components/Sidebar/Link.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
|  | ||||
| <template> | ||||
|   <!-- eslint-disable vue/require-component-is --> | ||||
|   <component v-bind="linkProps(to)"> | ||||
|     <slot /> | ||||
|   </component> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { isExternal } from '@/utils/validate' | ||||
|  | ||||
| export default { | ||||
|   props: { | ||||
|     to: { | ||||
|       type: String, | ||||
|       required: true | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     linkProps(url) { | ||||
|       if (isExternal(url)) { | ||||
|         return { | ||||
|           is: 'a', | ||||
|           href: url, | ||||
|           target: '_blank', | ||||
|           rel: 'noopener' | ||||
|         } | ||||
|       } | ||||
|       return { | ||||
|         is: 'router-link', | ||||
|         to: url | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										84
									
								
								ruoyi-ui/src/layout/components/Sidebar/Logo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								ruoyi-ui/src/layout/components/Sidebar/Logo.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| <template> | ||||
|   <div class="sidebar-logo-container" :class="{'collapse':collapse}"> | ||||
|     <transition name="sidebarLogoFade"> | ||||
|       <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/"> | ||||
|         <img v-if="logo" :src="logo" class="sidebar-logo"> | ||||
|         <h1 v-else class="sidebar-title">{{ title }} </h1> | ||||
|       </router-link> | ||||
|       <router-link v-else key="expand" class="sidebar-logo-link" to="/"> | ||||
|         <img v-if="logo" :src="logo" class="sidebar-logo"> | ||||
|         <h1 class="sidebar-title">{{ title }} </h1> | ||||
|       </router-link> | ||||
|     </transition> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import logoImg from '@/assets/logo/logo.png' | ||||
|  | ||||
| export default { | ||||
|   name: 'SidebarLogo', | ||||
|   props: { | ||||
|     collapse: { | ||||
|       type: Boolean, | ||||
|       required: true | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     return { | ||||
|       title: '若依管理系统', | ||||
|       logo: logoImg | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .sidebarLogoFade-enter-active { | ||||
|   transition: opacity 1.5s; | ||||
| } | ||||
|  | ||||
| .sidebarLogoFade-enter, | ||||
| .sidebarLogoFade-leave-to { | ||||
|   opacity: 0; | ||||
| } | ||||
|  | ||||
| .sidebar-logo-container { | ||||
|   position: relative; | ||||
|   width: 100%; | ||||
|   height: 50px; | ||||
|   line-height: 50px; | ||||
|   background: #2b2f3a; | ||||
|   text-align: center; | ||||
|   overflow: hidden; | ||||
|  | ||||
|   & .sidebar-logo-link { | ||||
|     height: 100%; | ||||
|     width: 100%; | ||||
|  | ||||
|     & .sidebar-logo { | ||||
|       width: 32px; | ||||
|       height: 32px; | ||||
|       vertical-align: middle; | ||||
|       margin-right: 12px; | ||||
|     } | ||||
|  | ||||
|     & .sidebar-title { | ||||
|       display: inline-block; | ||||
|       margin: 0; | ||||
|       color: #fff; | ||||
|       font-weight: 600; | ||||
|       line-height: 50px; | ||||
|       font-size: 14px; | ||||
|       font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; | ||||
|       vertical-align: middle; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   &.collapse { | ||||
|     .sidebar-logo { | ||||
|       margin-right: 0px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										93
									
								
								ruoyi-ui/src/layout/components/Sidebar/SidebarItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								ruoyi-ui/src/layout/components/Sidebar/SidebarItem.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| <template> | ||||
|   <div v-if="!item.hidden" class="menu-wrapper"> | ||||
|     <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"> | ||||
|       <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)"> | ||||
|         <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}"> | ||||
|           <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" /> | ||||
|         </el-menu-item> | ||||
|       </app-link> | ||||
|     </template> | ||||
|  | ||||
|     <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> | ||||
|       <template slot="title"> | ||||
|         <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" /> | ||||
|       </template> | ||||
|       <sidebar-item | ||||
|         v-for="child in item.children" | ||||
|         :key="child.path" | ||||
|         :is-nest="true" | ||||
|         :item="child" | ||||
|         :base-path="resolvePath(child.path)" | ||||
|         class="nest-menu" | ||||
|       /> | ||||
|     </el-submenu> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import path from 'path' | ||||
| import { isExternal } from '@/utils/validate' | ||||
| import Item from './Item' | ||||
| import AppLink from './Link' | ||||
| import FixiOSBug from './FixiOSBug' | ||||
|  | ||||
| export default { | ||||
|   name: 'SidebarItem', | ||||
|   components: { Item, AppLink }, | ||||
|   mixins: [FixiOSBug], | ||||
|   props: { | ||||
|     // route object | ||||
|     item: { | ||||
|       type: Object, | ||||
|       required: true | ||||
|     }, | ||||
|     isNest: { | ||||
|       type: Boolean, | ||||
|       default: false | ||||
|     }, | ||||
|     basePath: { | ||||
|       type: String, | ||||
|       default: '' | ||||
|     } | ||||
|   }, | ||||
|   data() { | ||||
|     this.onlyOneChild = null | ||||
|     return {} | ||||
|   }, | ||||
|   methods: { | ||||
|     hasOneShowingChild(children = [], parent) { | ||||
|       const showingChildren = children.filter(item => { | ||||
|         if (item.hidden) { | ||||
|           return false | ||||
|         } else { | ||||
|           // Temp set(will be used if only has one showing child) | ||||
|           this.onlyOneChild = item | ||||
|           return true | ||||
|         } | ||||
|       }) | ||||
|  | ||||
|       // When there is only one child router, the child router is displayed by default | ||||
|       if (showingChildren.length === 1) { | ||||
|         return true | ||||
|       } | ||||
|  | ||||
|       // Show parent if there are no child router to display | ||||
|       if (showingChildren.length === 0) { | ||||
|         this.onlyOneChild = { ... parent, path: '', noShowingChildren: true } | ||||
|         return true | ||||
|       } | ||||
|  | ||||
|       return false | ||||
|     }, | ||||
|     resolvePath(routePath) { | ||||
|       if (isExternal(routePath)) { | ||||
|         return routePath | ||||
|       } | ||||
|       if (isExternal(this.basePath)) { | ||||
|         return this.basePath | ||||
|       } | ||||
|       return path.resolve(this.basePath, routePath) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										54
									
								
								ruoyi-ui/src/layout/components/Sidebar/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								ruoyi-ui/src/layout/components/Sidebar/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| <template> | ||||
|   <div :class="{'has-logo':showLogo}"> | ||||
|     <logo v-if="showLogo" :collapse="isCollapse" /> | ||||
|     <el-scrollbar wrap-class="scrollbar-wrapper"> | ||||
|       <el-menu | ||||
|         :default-active="activeMenu" | ||||
|         :collapse="isCollapse" | ||||
|         :background-color="variables.menuBg" | ||||
|         :text-color="variables.menuText" | ||||
|         :unique-opened="true" | ||||
|         :active-text-color="variables.menuActiveText" | ||||
|         :collapse-transition="false" | ||||
|         mode="vertical" | ||||
|       > | ||||
|         <sidebar-item v-for="route in permission_routes" :key="route.path" :item="route" :base-path="route.path" /> | ||||
|       </el-menu> | ||||
|     </el-scrollbar> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import { mapGetters } from 'vuex' | ||||
| import Logo from './Logo' | ||||
| import SidebarItem from './SidebarItem' | ||||
| import variables from '@/assets/styles/variables.scss' | ||||
|  | ||||
| export default { | ||||
|   components: { SidebarItem, Logo }, | ||||
|   computed: { | ||||
|     ...mapGetters([ | ||||
|       'permission_routes', | ||||
|       'sidebar' | ||||
|     ]), | ||||
|     activeMenu() { | ||||
|       const route = this.$route | ||||
|       const { meta, path } = route | ||||
|       // if set path, the sidebar will highlight the path you set | ||||
|       if (meta.activeMenu) { | ||||
|         return meta.activeMenu | ||||
|       } | ||||
|       return path | ||||
|     }, | ||||
|     showLogo() { | ||||
|       return this.$store.state.settings.sidebarLogo | ||||
|     }, | ||||
|     variables() { | ||||
|       return variables | ||||
|     }, | ||||
|     isCollapse() { | ||||
|       return !this.sidebar.opened | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										85
									
								
								ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								ruoyi-ui/src/layout/components/TagsView/ScrollPane.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | ||||
| <template> | ||||
|   <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll"> | ||||
|     <slot /> | ||||
|   </el-scrollbar> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| const tagAndTagSpacing = 4 // tagAndTagSpacing | ||||
|  | ||||
| export default { | ||||
|   name: 'ScrollPane', | ||||
|   data() { | ||||
|     return { | ||||
|       left: 0 | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     scrollWrapper() { | ||||
|       return this.$refs.scrollContainer.$refs.wrap | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     handleScroll(e) { | ||||
|       const eventDelta = e.wheelDelta || -e.deltaY * 40 | ||||
|       const $scrollWrapper = this.scrollWrapper | ||||
|       $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4 | ||||
|     }, | ||||
|     moveToTarget(currentTag) { | ||||
|       const $container = this.$refs.scrollContainer.$el | ||||
|       const $containerWidth = $container.offsetWidth | ||||
|       const $scrollWrapper = this.scrollWrapper | ||||
|       const tagList = this.$parent.$refs.tag | ||||
|  | ||||
|       let firstTag = null | ||||
|       let lastTag = null | ||||
|  | ||||
|       // find first tag and last tag | ||||
|       if (tagList.length > 0) { | ||||
|         firstTag = tagList[0] | ||||
|         lastTag = tagList[tagList.length - 1] | ||||
|       } | ||||
|  | ||||
|       if (firstTag === currentTag) { | ||||
|         $scrollWrapper.scrollLeft = 0 | ||||
|       } else if (lastTag === currentTag) { | ||||
|         $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth | ||||
|       } else { | ||||
|         // find preTag and nextTag | ||||
|         const currentIndex = tagList.findIndex(item => item === currentTag) | ||||
|         const prevTag = tagList[currentIndex - 1] | ||||
|         const nextTag = tagList[currentIndex + 1] | ||||
|  | ||||
|         // the tag's offsetLeft after of nextTag | ||||
|         const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing | ||||
|  | ||||
|         // the tag's offsetLeft before of prevTag | ||||
|         const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing | ||||
|  | ||||
|         if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) { | ||||
|           $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth | ||||
|         } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) { | ||||
|           $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .scroll-container { | ||||
|   white-space: nowrap; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
|   width: 100%; | ||||
|   /deep/ { | ||||
|     .el-scrollbar__bar { | ||||
|       bottom: 0px; | ||||
|     } | ||||
|     .el-scrollbar__wrap { | ||||
|       height: 49px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										286
									
								
								ruoyi-ui/src/layout/components/TagsView/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								ruoyi-ui/src/layout/components/TagsView/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,286 @@ | ||||
| <template> | ||||
|   <div id="tags-view-container" class="tags-view-container"> | ||||
|     <scroll-pane ref="scrollPane" class="tags-view-wrapper"> | ||||
|       <router-link | ||||
|         v-for="tag in visitedViews" | ||||
|         ref="tag" | ||||
|         :key="tag.path" | ||||
|         :class="isActive(tag)?'active':''" | ||||
|         :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" | ||||
|         tag="span" | ||||
|         class="tags-view-item" | ||||
|         @click.middle.native="closeSelectedTag(tag)" | ||||
|         @contextmenu.prevent.native="openMenu(tag,$event)" | ||||
|       > | ||||
|         {{ tag.title }} | ||||
|         <span v-if="!tag.meta.affix" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" /> | ||||
|       </router-link> | ||||
|     </scroll-pane> | ||||
|     <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu"> | ||||
|       <li @click="refreshSelectedTag(selectedTag)">刷新页面</li> | ||||
|       <li v-if="!(selectedTag.meta&&selectedTag.meta.affix)" @click="closeSelectedTag(selectedTag)">关闭当前</li> | ||||
|       <li @click="closeOthersTags">关闭其他</li> | ||||
|       <li @click="closeAllTags(selectedTag)">关闭所有</li> | ||||
|     </ul> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import ScrollPane from './ScrollPane' | ||||
| import path from 'path' | ||||
|  | ||||
| export default { | ||||
|   components: { ScrollPane }, | ||||
|   data() { | ||||
|     return { | ||||
|       visible: false, | ||||
|       top: 0, | ||||
|       left: 0, | ||||
|       selectedTag: {}, | ||||
|       affixTags: [] | ||||
|     } | ||||
|   }, | ||||
|   computed: { | ||||
|     visitedViews() { | ||||
|       return this.$store.state.tagsView.visitedViews | ||||
|     }, | ||||
|     routes() { | ||||
|       return this.$store.state.permission.routes | ||||
|     } | ||||
|   }, | ||||
|   watch: { | ||||
|     $route() { | ||||
|       this.addTags() | ||||
|       this.moveToCurrentTag() | ||||
|     }, | ||||
|     visible(value) { | ||||
|       if (value) { | ||||
|         document.body.addEventListener('click', this.closeMenu) | ||||
|       } else { | ||||
|         document.body.removeEventListener('click', this.closeMenu) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   mounted() { | ||||
|     this.initTags() | ||||
|     this.addTags() | ||||
|   }, | ||||
|   methods: { | ||||
|     isActive(route) { | ||||
|       return route.path === this.$route.path | ||||
|     }, | ||||
|     filterAffixTags(routes, basePath = '/') { | ||||
|       let tags = [] | ||||
|       routes.forEach(route => { | ||||
|         if (route.meta && route.meta.affix) { | ||||
|           const tagPath = path.resolve(basePath, route.path) | ||||
|           tags.push({ | ||||
|             fullPath: tagPath, | ||||
|             path: tagPath, | ||||
|             name: route.name, | ||||
|             meta: { ...route.meta } | ||||
|           }) | ||||
|         } | ||||
|         if (route.children) { | ||||
|           const tempTags = this.filterAffixTags(route.children, route.path) | ||||
|           if (tempTags.length >= 1) { | ||||
|             tags = [...tags, ...tempTags] | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|       return tags | ||||
|     }, | ||||
|     initTags() { | ||||
|       const affixTags = this.affixTags = this.filterAffixTags(this.routes) | ||||
|       for (const tag of affixTags) { | ||||
|         // Must have tag name | ||||
|         if (tag.name) { | ||||
|           this.$store.dispatch('tagsView/addVisitedView', tag) | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     addTags() { | ||||
|       const { name } = this.$route | ||||
|       if (name) { | ||||
|         this.$store.dispatch('tagsView/addView', this.$route) | ||||
|       } | ||||
|       return false | ||||
|     }, | ||||
|     moveToCurrentTag() { | ||||
|       const tags = this.$refs.tag | ||||
|       this.$nextTick(() => { | ||||
|         for (const tag of tags) { | ||||
|           if (tag.to.path === this.$route.path) { | ||||
|             this.$refs.scrollPane.moveToTarget(tag) | ||||
|             // when query is different then update | ||||
|             if (tag.to.fullPath !== this.$route.fullPath) { | ||||
|               this.$store.dispatch('tagsView/updateVisitedView', this.$route) | ||||
|             } | ||||
|             break | ||||
|           } | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     refreshSelectedTag(view) { | ||||
|       this.$store.dispatch('tagsView/delCachedView', view).then(() => { | ||||
|         const { fullPath } = view | ||||
|         this.$nextTick(() => { | ||||
|           this.$router.replace({ | ||||
|             path: '/redirect' + fullPath | ||||
|           }) | ||||
|         }) | ||||
|       }) | ||||
|     }, | ||||
|     closeSelectedTag(view) { | ||||
|       this.$store.dispatch('tagsView/delView', view).then(({ visitedViews }) => { | ||||
|         if (this.isActive(view)) { | ||||
|           this.toLastView(visitedViews, view) | ||||
|         } | ||||
|       }) | ||||
|     }, | ||||
|     closeOthersTags() { | ||||
|       this.$router.push(this.selectedTag) | ||||
|       this.$store.dispatch('tagsView/delOthersViews', this.selectedTag).then(() => { | ||||
|         this.moveToCurrentTag() | ||||
|       }) | ||||
|     }, | ||||
|     closeAllTags(view) { | ||||
|       this.$store.dispatch('tagsView/delAllViews').then(({ visitedViews }) => { | ||||
|         if (this.affixTags.some(tag => tag.path === view.path)) { | ||||
|           return | ||||
|         } | ||||
|         this.toLastView(visitedViews, view) | ||||
|       }) | ||||
|     }, | ||||
|     toLastView(visitedViews, view) { | ||||
|       const latestView = visitedViews.slice(-1)[0] | ||||
|       if (latestView) { | ||||
|         this.$router.push(latestView) | ||||
|       } else { | ||||
|         // now the default is to redirect to the home page if there is no tags-view, | ||||
|         // you can adjust it according to your needs. | ||||
|         if (view.name === 'Dashboard') { | ||||
|           // to reload home page | ||||
|           this.$router.replace({ path: '/redirect' + view.fullPath }) | ||||
|         } else { | ||||
|           this.$router.push('/') | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     openMenu(tag, e) { | ||||
|       const menuMinWidth = 105 | ||||
|       const offsetLeft = this.$el.getBoundingClientRect().left // container margin left | ||||
|       const offsetWidth = this.$el.offsetWidth // container width | ||||
|       const maxLeft = offsetWidth - menuMinWidth // left boundary | ||||
|       const left = e.clientX - offsetLeft + 15 // 15: margin right | ||||
|  | ||||
|       if (left > maxLeft) { | ||||
|         this.left = maxLeft | ||||
|       } else { | ||||
|         this.left = left | ||||
|       } | ||||
|  | ||||
|       this.top = e.clientY | ||||
|       this.visible = true | ||||
|       this.selectedTag = tag | ||||
|     }, | ||||
|     closeMenu() { | ||||
|       this.visible = false | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
| .tags-view-container { | ||||
|   height: 34px; | ||||
|   width: 100%; | ||||
|   background: #fff; | ||||
|   border-bottom: 1px solid #d8dce5; | ||||
|   box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04); | ||||
|   .tags-view-wrapper { | ||||
|     .tags-view-item { | ||||
|       display: inline-block; | ||||
|       position: relative; | ||||
|       cursor: pointer; | ||||
|       height: 26px; | ||||
|       line-height: 26px; | ||||
|       border: 1px solid #d8dce5; | ||||
|       color: #495060; | ||||
|       background: #fff; | ||||
|       padding: 0 8px; | ||||
|       font-size: 12px; | ||||
|       margin-left: 5px; | ||||
|       margin-top: 4px; | ||||
|       &:first-of-type { | ||||
|         margin-left: 15px; | ||||
|       } | ||||
|       &:last-of-type { | ||||
|         margin-right: 15px; | ||||
|       } | ||||
|       &.active { | ||||
|         background-color: #42b983; | ||||
|         color: #fff; | ||||
|         border-color: #42b983; | ||||
|         &::before { | ||||
|           content: ''; | ||||
|           background: #fff; | ||||
|           display: inline-block; | ||||
|           width: 8px; | ||||
|           height: 8px; | ||||
|           border-radius: 50%; | ||||
|           position: relative; | ||||
|           margin-right: 2px; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   .contextmenu { | ||||
|     margin: 0; | ||||
|     background: #fff; | ||||
|     z-index: 3000; | ||||
|     position: absolute; | ||||
|     list-style-type: none; | ||||
|     padding: 5px 0; | ||||
|     border-radius: 4px; | ||||
|     font-size: 12px; | ||||
|     font-weight: 400; | ||||
|     color: #333; | ||||
|     box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); | ||||
|     li { | ||||
|       margin: 0; | ||||
|       padding: 7px 16px; | ||||
|       cursor: pointer; | ||||
|       &:hover { | ||||
|         background: #eee; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
|  | ||||
| <style lang="scss"> | ||||
| //reset element css of el-icon-close | ||||
| .tags-view-wrapper { | ||||
|   .tags-view-item { | ||||
|     .el-icon-close { | ||||
|       width: 16px; | ||||
|       height: 16px; | ||||
|       vertical-align: 2px; | ||||
|       border-radius: 50%; | ||||
|       text-align: center; | ||||
|       transition: all .3s cubic-bezier(.645, .045, .355, 1); | ||||
|       transform-origin: 100% 50%; | ||||
|       &:before { | ||||
|         transform: scale(.6); | ||||
|         display: inline-block; | ||||
|         vertical-align: -3px; | ||||
|       } | ||||
|       &:hover { | ||||
|         background-color: #b4bccc; | ||||
|         color: #fff; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										5
									
								
								ruoyi-ui/src/layout/components/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								ruoyi-ui/src/layout/components/index.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| export { default as AppMain } from './AppMain' | ||||
| export { default as Navbar } from './Navbar' | ||||
| export { default as Settings } from './Settings' | ||||
| export { default as Sidebar } from './Sidebar/index.vue' | ||||
| export { default as TagsView } from './TagsView/index.vue' | ||||
							
								
								
									
										102
									
								
								ruoyi-ui/src/layout/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								ruoyi-ui/src/layout/index.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,102 @@ | ||||
| <template> | ||||
|   <div :class="classObj" class="app-wrapper"> | ||||
|     <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" /> | ||||
|     <sidebar class="sidebar-container" /> | ||||
|     <div :class="{hasTagsView:needTagsView}" class="main-container"> | ||||
|       <div :class="{'fixed-header':fixedHeader}"> | ||||
|         <navbar /> | ||||
|         <tags-view v-if="needTagsView" /> | ||||
|       </div> | ||||
|       <app-main /> | ||||
|       <right-panel v-if="showSettings"> | ||||
|         <settings /> | ||||
|       </right-panel> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script> | ||||
| import RightPanel from '@/components/RightPanel' | ||||
| import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components' | ||||
| import ResizeMixin from './mixin/ResizeHandler' | ||||
| import { mapState } from 'vuex' | ||||
|  | ||||
| export default { | ||||
|   name: 'Layout', | ||||
|   components: { | ||||
|     AppMain, | ||||
|     Navbar, | ||||
|     RightPanel, | ||||
|     Settings, | ||||
|     Sidebar, | ||||
|     TagsView | ||||
|   }, | ||||
|   mixins: [ResizeMixin], | ||||
|   computed: { | ||||
|     ...mapState({ | ||||
|       sidebar: state => state.app.sidebar, | ||||
|       device: state => state.app.device, | ||||
|       showSettings: state => state.settings.showSettings, | ||||
|       needTagsView: state => state.settings.tagsView, | ||||
|       fixedHeader: state => state.settings.fixedHeader | ||||
|     }), | ||||
|     classObj() { | ||||
|       return { | ||||
|         hideSidebar: !this.sidebar.opened, | ||||
|         openSidebar: this.sidebar.opened, | ||||
|         withoutAnimation: this.sidebar.withoutAnimation, | ||||
|         mobile: this.device === 'mobile' | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     handleClickOutside() { | ||||
|       this.$store.dispatch('app/closeSideBar', { withoutAnimation: false }) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang="scss" scoped> | ||||
|   @import "~@/assets/styles/mixin.scss"; | ||||
|   @import "~@/assets/styles/variables.scss"; | ||||
|  | ||||
|   .app-wrapper { | ||||
|     @include clearfix; | ||||
|     position: relative; | ||||
|     height: 100%; | ||||
|     width: 100%; | ||||
|  | ||||
|     &.mobile.openSidebar { | ||||
|       position: fixed; | ||||
|       top: 0; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .drawer-bg { | ||||
|     background: #000; | ||||
|     opacity: 0.3; | ||||
|     width: 100%; | ||||
|     top: 0; | ||||
|     height: 100%; | ||||
|     position: absolute; | ||||
|     z-index: 999; | ||||
|   } | ||||
|  | ||||
|   .fixed-header { | ||||
|     position: fixed; | ||||
|     top: 0; | ||||
|     right: 0; | ||||
|     z-index: 9; | ||||
|     width: calc(100% - #{$sideBarWidth}); | ||||
|     transition: width 0.28s; | ||||
|   } | ||||
|  | ||||
|   .hideSidebar .fixed-header { | ||||
|     width: calc(100% - 54px) | ||||
|   } | ||||
|  | ||||
|   .mobile .fixed-header { | ||||
|     width: 100%; | ||||
|   } | ||||
| </style> | ||||
							
								
								
									
										45
									
								
								ruoyi-ui/src/layout/mixin/ResizeHandler.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								ruoyi-ui/src/layout/mixin/ResizeHandler.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| import store from '@/store' | ||||
|  | ||||
| const { body } = document | ||||
| const WIDTH = 992 // refer to Bootstrap's responsive design | ||||
|  | ||||
| export default { | ||||
|   watch: { | ||||
|     $route(route) { | ||||
|       if (this.device === 'mobile' && this.sidebar.opened) { | ||||
|         store.dispatch('app/closeSideBar', { withoutAnimation: false }) | ||||
|       } | ||||
|     } | ||||
|   }, | ||||
|   beforeMount() { | ||||
|     window.addEventListener('resize', this.$_resizeHandler) | ||||
|   }, | ||||
|   beforeDestroy() { | ||||
|     window.removeEventListener('resize', this.$_resizeHandler) | ||||
|   }, | ||||
|   mounted() { | ||||
|     const isMobile = this.$_isMobile() | ||||
|     if (isMobile) { | ||||
|       store.dispatch('app/toggleDevice', 'mobile') | ||||
|       store.dispatch('app/closeSideBar', { withoutAnimation: true }) | ||||
|     } | ||||
|   }, | ||||
|   methods: { | ||||
|     // use $_ for mixins properties | ||||
|     // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential | ||||
|     $_isMobile() { | ||||
|       const rect = body.getBoundingClientRect() | ||||
|       return rect.width - 1 < WIDTH | ||||
|     }, | ||||
|     $_resizeHandler() { | ||||
|       if (!document.hidden) { | ||||
|         const isMobile = this.$_isMobile() | ||||
|         store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop') | ||||
|  | ||||
|         if (isMobile) { | ||||
|           store.dispatch('app/closeSideBar', { withoutAnimation: true }) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user