合并 vue与cloud vue3 前端项目
This commit is contained in:
		
							
								
								
									
										53
									
								
								src/layout/components/AppMain.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/layout/components/AppMain.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| <template> | ||||
|   <section class="app-main"> | ||||
|     <router-view v-slot="{ Component, route }"> | ||||
|       <transition name="fade-transform" mode="out-in"> | ||||
|         <keep-alive :include="tagsViewStore.cachedViews"> | ||||
|           <component v-if="!route.meta.link" :is="Component" :key="route.path"/> | ||||
|         </keep-alive> | ||||
|       </transition> | ||||
|     </router-view> | ||||
|     <iframe-toggle /> | ||||
|   </section> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import iframeToggle from "./IframeToggle/index" | ||||
| import useTagsViewStore from '@/store/modules/tagsView' | ||||
|  | ||||
| const tagsViewStore = useTagsViewStore() | ||||
| </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: 17px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										19
									
								
								src/layout/components/IframeToggle/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/layout/components/IframeToggle/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| <template> | ||||
|   <transition-group name="fade-transform" mode="out-in"> | ||||
|     <inner-link | ||||
|       v-for="(item, index) in tagsViewStore.iframeViews" | ||||
|       :key="item.path" | ||||
|       :iframeId="'iframe' + index" | ||||
|       v-show="route.path === item.path" | ||||
|       :src="item.meta.link" | ||||
|     ></inner-link> | ||||
|   </transition-group> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import InnerLink from "../InnerLink/index" | ||||
| import useTagsViewStore from '@/store/modules/tagsView' | ||||
|  | ||||
| const route = useRoute(); | ||||
| const tagsViewStore = useTagsViewStore() | ||||
| </script> | ||||
							
								
								
									
										24
									
								
								src/layout/components/InnerLink/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								src/layout/components/InnerLink/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| <template> | ||||
|   <div :style="'height:' + height"> | ||||
|     <iframe | ||||
|       :id="iframeId" | ||||
|       style="width: 100%; height: 100%" | ||||
|       :src="src" | ||||
|       frameborder="no" | ||||
|     ></iframe> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| const props = defineProps({ | ||||
|   src: { | ||||
|     type: String, | ||||
|     default: "/" | ||||
|   }, | ||||
|   iframeId: { | ||||
|     type: String | ||||
|   } | ||||
| }); | ||||
|  | ||||
| const height = ref(document.documentElement.clientHeight - 94.5 + "px"); | ||||
| </script> | ||||
							
								
								
									
										261
									
								
								src/layout/components/Navbar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										261
									
								
								src/layout/components/Navbar.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,261 @@ | ||||
| <template> | ||||
|   <div class="navbar"> | ||||
|     <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> | ||||
|     <breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav" /> | ||||
|     <top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" /> | ||||
|  | ||||
|     <div class="right-menu flex align-center"> | ||||
|       <template v-if="appStore.device !== 'mobile'"> | ||||
|         <el-select v-model="companyName" | ||||
|                    clearable | ||||
|                    filterable | ||||
|                    reserve-keyword | ||||
|                    placeholder="请选择租户" | ||||
|                    v-if="userId === 1" | ||||
|                    @change="dynamicTenantEvent" | ||||
|                    @clear="dynamicClearEvent"> | ||||
|           <el-option | ||||
|               v-for="item in tenantList" | ||||
|               :key="item.tenantId" | ||||
|               :label="item.companyName" | ||||
|               :value="item.tenantId"> | ||||
|           </el-option> | ||||
|           <template #prefix><svg-icon icon-class="company" class="el-input__icon input-icon" /></template> | ||||
|         </el-select> | ||||
|  | ||||
|         <header-search id="header-search" class="right-menu-item" /> | ||||
|  | ||||
|         <el-tooltip content="源码地址" effect="dark" placement="bottom"> | ||||
|           <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" /> | ||||
|         </el-tooltip> | ||||
|  | ||||
|         <el-tooltip content="文档地址" effect="dark" placement="bottom"> | ||||
|           <ruo-yi-doc id="ruoyi-doc" 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> | ||||
|       <div class="avatar-container"> | ||||
|         <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click"> | ||||
|           <div class="avatar-wrapper"> | ||||
|             <img :src="userStore.avatar" class="user-avatar" /> | ||||
|             <el-icon><caret-bottom /></el-icon> | ||||
|           </div> | ||||
|           <template #dropdown> | ||||
|             <el-dropdown-menu> | ||||
|               <router-link to="/user/profile" v-if="!dynamic"> | ||||
|                 <el-dropdown-item>个人中心</el-dropdown-item> | ||||
|               </router-link> | ||||
|               <el-dropdown-item command="setLayout"> | ||||
|                 <span>布局设置</span> | ||||
|               </el-dropdown-item> | ||||
|               <el-dropdown-item divided command="logout"> | ||||
|                 <span>退出登录</span> | ||||
|               </el-dropdown-item> | ||||
|             </el-dropdown-menu> | ||||
|           </template> | ||||
|         </el-dropdown> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { ElMessageBox } from 'element-plus' | ||||
| import Breadcrumb from '@/components/Breadcrumb' | ||||
| import TopNav from '@/components/TopNav' | ||||
| import Hamburger from '@/components/Hamburger' | ||||
| import Screenfull from '@/components/Screenfull' | ||||
| import SizeSelect from '@/components/SizeSelect' | ||||
| import HeaderSearch from '@/components/HeaderSearch' | ||||
| import RuoYiGit from '@/components/RuoYi/Git' | ||||
| import RuoYiDoc from '@/components/RuoYi/Doc' | ||||
| import useAppStore from '@/store/modules/app' | ||||
| import useUserStore from '@/store/modules/user' | ||||
| import useSettingsStore from '@/store/modules/settings' | ||||
| import { getTenantList } from "@/api/login"; | ||||
| import { dynamicClear, dynamicTenant } from "@/api/system/tenant"; | ||||
|  | ||||
| const appStore = useAppStore() | ||||
| const userStore = useUserStore() | ||||
| const settingsStore = useSettingsStore() | ||||
|  | ||||
| const { proxy } = getCurrentInstance(); | ||||
|  | ||||
| const userId = ref(userStore.userId); | ||||
| const companyName = ref(undefined); | ||||
| const tenantList = ref([]); | ||||
| // 是否切换了租户 | ||||
| const dynamic = ref(false); | ||||
|  | ||||
| // 动态切换 | ||||
| function dynamicTenantEvent(tenantId) { | ||||
|   if (companyName.value != null && companyName.value !== '') { | ||||
|     dynamicTenant(tenantId).then(res => { | ||||
|       dynamic.value = true; | ||||
|       proxy.$tab.closeAllPage() | ||||
|       proxy.$router.push('/') | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| function dynamicClearEvent() { | ||||
|   dynamicClear().then(res => { | ||||
|     dynamic.value = false; | ||||
|     proxy.$tab.closeAllPage() | ||||
|     proxy.$router.push('/') | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // 租户列表 | ||||
| function initTenantList() { | ||||
|   getTenantList().then(res => { | ||||
|     tenantList.value = res.data; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| defineExpose({ | ||||
|   initTenantList, | ||||
| }) | ||||
|  | ||||
| function toggleSideBar() { | ||||
|   appStore.toggleSideBar() | ||||
| } | ||||
|  | ||||
| function handleCommand(command) { | ||||
|   switch (command) { | ||||
|     case "setLayout": | ||||
|       setLayout(); | ||||
|       break; | ||||
|     case "logout": | ||||
|       logout(); | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| function logout() { | ||||
|   ElMessageBox.confirm('确定注销并退出系统吗?', '提示', { | ||||
|     confirmButtonText: '确定', | ||||
|     cancelButtonText: '取消', | ||||
|     type: 'warning' | ||||
|   }).then(() => { | ||||
|     userStore.logOut().then(() => { | ||||
|       location.href = import.meta.env.VITE_APP_CONTEXT_PATH + 'index'; | ||||
|     }) | ||||
|   }).catch(() => { }); | ||||
| } | ||||
|  | ||||
| const emits = defineEmits(['setLayout']) | ||||
| function setLayout() { | ||||
|   emits('setLayout'); | ||||
| } | ||||
| </script> | ||||
|  | ||||
| <style lang='scss' scoped> | ||||
|  | ||||
| :deep .el-select .el-input__wrapper { | ||||
|   height:30px; | ||||
| } | ||||
|  | ||||
| .flex { | ||||
|   display: flex; | ||||
| } | ||||
|  | ||||
| .align-center { | ||||
|   align-items: center; | ||||
| } | ||||
|  | ||||
| .navbar { | ||||
|   height: 50px; | ||||
|   overflow: hidden; | ||||
|   position: relative; | ||||
|   background: #fff; | ||||
|   box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08); | ||||
|  | ||||
|   .hamburger-container { | ||||
|     line-height: 46px; | ||||
|     height: 100%; | ||||
|     float: left; | ||||
|     cursor: pointer; | ||||
|     transition: background 0.3s; | ||||
|     -webkit-tap-highlight-color: transparent; | ||||
|  | ||||
|     &:hover { | ||||
|       background: rgba(0, 0, 0, 0.025); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   .breadcrumb-container { | ||||
|     float: left; | ||||
|   } | ||||
|  | ||||
|   .topmenu-container { | ||||
|     position: absolute; | ||||
|     left: 50px; | ||||
|   } | ||||
|  | ||||
|   .errLog-container { | ||||
|     display: inline-block; | ||||
|     vertical-align: top; | ||||
|   } | ||||
|  | ||||
|   .right-menu { | ||||
|     float: right; | ||||
|     height: 100%; | ||||
|     line-height: 50px; | ||||
|     display: flex; | ||||
|  | ||||
|     &: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 0.3s; | ||||
|  | ||||
|         &:hover { | ||||
|           background: rgba(0, 0, 0, 0.025); | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     .avatar-container { | ||||
|       margin-right: 40px; | ||||
|  | ||||
|       .avatar-wrapper { | ||||
|         margin-top: 5px; | ||||
|         position: relative; | ||||
|  | ||||
|         .user-avatar { | ||||
|           cursor: pointer; | ||||
|           width: 40px; | ||||
|           height: 40px; | ||||
|           border-radius: 10px; | ||||
|         } | ||||
|  | ||||
|         i { | ||||
|           cursor: pointer; | ||||
|           position: absolute; | ||||
|           right: -20px; | ||||
|           top: 25px; | ||||
|           font-size: 12px; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										241
									
								
								src/layout/components/Settings/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										241
									
								
								src/layout/components/Settings/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,241 @@ | ||||
| <template> | ||||
|   <el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px"> | ||||
|     <div class="setting-drawer-title"> | ||||
|       <h3 class="drawer-title">主题风格设置</h3> | ||||
|     </div> | ||||
|     <div class="setting-drawer-block-checbox"> | ||||
|       <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')"> | ||||
|         <img src="@/assets/images/dark.svg" alt="dark" /> | ||||
|         <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;"> | ||||
|           <i aria-label="图标: check" class="anticon anticon-check"> | ||||
|             <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class> | ||||
|               <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" /> | ||||
|             </svg> | ||||
|           </i> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')"> | ||||
|         <img src="@/assets/images/light.svg" alt="light" /> | ||||
|         <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;"> | ||||
|           <i aria-label="图标: check" class="anticon anticon-check"> | ||||
|             <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class> | ||||
|               <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" /> | ||||
|             </svg> | ||||
|           </i> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="drawer-item"> | ||||
|       <span>主题颜色</span> | ||||
|       <span class="comp-style"> | ||||
|         <el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange"/> | ||||
|       </span> | ||||
|     </div> | ||||
|     <el-divider /> | ||||
|  | ||||
|     <h3 class="drawer-title">系统布局配置</h3> | ||||
|  | ||||
|     <div class="drawer-item"> | ||||
|       <span>开启 TopNav</span> | ||||
|       <span class="comp-style"> | ||||
|         <el-switch v-model="topNav" class="drawer-switch" /> | ||||
|       </span> | ||||
|     </div> | ||||
|  | ||||
|     <div class="drawer-item"> | ||||
|       <span>开启 Tags-Views</span> | ||||
|       <span class="comp-style"> | ||||
|         <el-switch v-model="tagsView" class="drawer-switch" /> | ||||
|       </span> | ||||
|     </div> | ||||
|  | ||||
|     <div class="drawer-item"> | ||||
|       <span>固定 Header</span> | ||||
|       <span class="comp-style"> | ||||
|         <el-switch v-model="fixedHeader" class="drawer-switch" /> | ||||
|       </span> | ||||
|     </div> | ||||
|  | ||||
|     <div class="drawer-item"> | ||||
|       <span>显示 Logo</span> | ||||
|       <span class="comp-style"> | ||||
|         <el-switch v-model="sidebarLogo" class="drawer-switch" /> | ||||
|       </span> | ||||
|     </div> | ||||
|  | ||||
|     <div class="drawer-item"> | ||||
|       <span>动态标题</span> | ||||
|       <span class="comp-style"> | ||||
|         <el-switch v-model="dynamicTitle" class="drawer-switch" /> | ||||
|       </span> | ||||
|     </div> | ||||
|  | ||||
|     <el-divider /> | ||||
|  | ||||
|     <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button> | ||||
|     <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button> | ||||
|   </el-drawer> | ||||
|  | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import variables from '@/assets/styles/variables.module.scss' | ||||
| import originElementPlus from 'element-plus/theme-chalk/index.css' | ||||
| import axios from 'axios' | ||||
| import { ElLoading, ElMessage } from 'element-plus' | ||||
| import { useDynamicTitle } from '@/utils/dynamicTitle' | ||||
| import useAppStore from '@/store/modules/app' | ||||
| import useSettingsStore from '@/store/modules/settings' | ||||
| import usePermissionStore from '@/store/modules/permission' | ||||
| import { handleThemeStyle } from '@/utils/theme' | ||||
|  | ||||
| const { proxy } = getCurrentInstance(); | ||||
| const appStore = useAppStore() | ||||
| const settingsStore = useSettingsStore() | ||||
| const permissionStore = usePermissionStore() | ||||
| const showSettings = ref(false); | ||||
| const theme = ref(settingsStore.theme); | ||||
| const sideTheme = ref(settingsStore.sideTheme); | ||||
| const storeSettings = computed(() => settingsStore); | ||||
| const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]); | ||||
|  | ||||
| /** 是否需要topnav */ | ||||
| const topNav = computed({ | ||||
|   get: () => storeSettings.value.topNav, | ||||
|   set: (val) => { | ||||
|     settingsStore.changeSetting({ key: 'topNav', value: val }) | ||||
|     if (!val) { | ||||
|       appStore.toggleSideBarHide(false); | ||||
|       permissionStore.setSidebarRouters(permissionStore.defaultRoutes); | ||||
|     } | ||||
|   } | ||||
| }) | ||||
| /** 是否需要tagview */ | ||||
| const tagsView = computed({ | ||||
|   get: () => storeSettings.value.tagsView, | ||||
|   set: (val) => { | ||||
|     settingsStore.changeSetting({ key: 'tagsView', value: val }) | ||||
|   } | ||||
| }) | ||||
| /**是否需要固定头部 */ | ||||
| const fixedHeader = computed({ | ||||
|   get: () => storeSettings.value.fixedHeader, | ||||
|   set: (val) => { | ||||
|     settingsStore.changeSetting({ key: 'fixedHeader', value: val }) | ||||
|   } | ||||
| }) | ||||
| /**是否需要侧边栏的logo */ | ||||
| const sidebarLogo = computed({ | ||||
|   get: () => storeSettings.value.sidebarLogo, | ||||
|   set: (val) => { | ||||
|     settingsStore.changeSetting({ key: 'sidebarLogo', value: val }) | ||||
|   } | ||||
| }) | ||||
| /**是否需要侧边栏的动态网页的title */ | ||||
| const dynamicTitle = computed({ | ||||
|   get: () => storeSettings.value.dynamicTitle, | ||||
|   set: (val) => { | ||||
|     settingsStore.changeSetting({ key: 'dynamicTitle', value: val }) | ||||
|     // 动态设置网页标题 | ||||
|     useDynamicTitle() | ||||
|   } | ||||
| }) | ||||
|  | ||||
| function themeChange(val) { | ||||
|   settingsStore.changeSetting({ key: 'theme', value: val }) | ||||
|   theme.value = val; | ||||
|   handleThemeStyle(val); | ||||
| } | ||||
| function handleTheme(val) { | ||||
|   settingsStore.changeSetting({ key: 'sideTheme', value: val }) | ||||
|   sideTheme.value = val; | ||||
| } | ||||
| function saveSetting() { | ||||
|   proxy.$modal.loading("正在保存到本地,请稍候..."); | ||||
|   let layoutSetting = { | ||||
|     "topNav": storeSettings.value.topNav, | ||||
|     "tagsView": storeSettings.value.tagsView, | ||||
|     "fixedHeader": storeSettings.value.fixedHeader, | ||||
|     "sidebarLogo": storeSettings.value.sidebarLogo, | ||||
|     "dynamicTitle": storeSettings.value.dynamicTitle, | ||||
|     "sideTheme": storeSettings.value.sideTheme, | ||||
|     "theme": storeSettings.value.theme | ||||
|   }; | ||||
|   localStorage.setItem("layout-setting", JSON.stringify(layoutSetting)); | ||||
|   setTimeout(proxy.$modal.closeLoading(), 1000) | ||||
| } | ||||
| function resetSetting() { | ||||
|   proxy.$modal.loading("正在清除设置缓存并刷新,请稍候..."); | ||||
|   localStorage.removeItem("layout-setting") | ||||
|   setTimeout("window.location.reload()", 1000) | ||||
| } | ||||
| function openSetting() { | ||||
|   showSettings.value = true; | ||||
| } | ||||
|  | ||||
| defineExpose({ | ||||
|   openSetting, | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style lang='scss' scoped> | ||||
| .setting-drawer-title { | ||||
|   margin-bottom: 12px; | ||||
|   color: rgba(0, 0, 0, 0.85); | ||||
|   line-height: 22px; | ||||
|   font-weight: bold; | ||||
|   .drawer-title { | ||||
|     font-size: 14px; | ||||
|   } | ||||
| } | ||||
| .setting-drawer-block-checbox { | ||||
|   display: flex; | ||||
|   justify-content: flex-start; | ||||
|   align-items: center; | ||||
|   margin-top: 10px; | ||||
|   margin-bottom: 20px; | ||||
|  | ||||
|   .setting-drawer-block-checbox-item { | ||||
|     position: relative; | ||||
|     margin-right: 16px; | ||||
|     border-radius: 2px; | ||||
|     cursor: pointer; | ||||
|  | ||||
|     img { | ||||
|       width: 48px; | ||||
|       height: 48px; | ||||
|     } | ||||
|  | ||||
|     .custom-img { | ||||
|       width: 48px; | ||||
|       height: 38px; | ||||
|       border-radius: 5px; | ||||
|       box-shadow: 1px 1px 2px #898484; | ||||
|     } | ||||
|  | ||||
|     .setting-drawer-block-checbox-selectIcon { | ||||
|       position: absolute; | ||||
|       top: 0; | ||||
|       right: 0; | ||||
|       width: 100%; | ||||
|       height: 100%; | ||||
|       padding-top: 15px; | ||||
|       padding-left: 24px; | ||||
|       color: #1890ff; | ||||
|       font-weight: 700; | ||||
|       font-size: 14px; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| .drawer-item { | ||||
|   color: rgba(0, 0, 0, 0.65); | ||||
|   padding: 12px 0; | ||||
|   font-size: 14px; | ||||
|  | ||||
|   .comp-style { | ||||
|     float: right; | ||||
|     margin: -3px 8px 0px 0px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										40
									
								
								src/layout/components/Sidebar/Link.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/layout/components/Sidebar/Link.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,40 @@ | ||||
| <template> | ||||
|   <component :is="type" v-bind="linkProps()"> | ||||
|     <slot /> | ||||
|   </component> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { isExternal } from '@/utils/validate' | ||||
|  | ||||
| const props = defineProps({ | ||||
|   to: { | ||||
|     type: [String, Object], | ||||
|     required: true | ||||
|   } | ||||
| }) | ||||
|  | ||||
| const isExt = computed(() => { | ||||
|   return isExternal(props.to) | ||||
| }) | ||||
|  | ||||
| const type = computed(() => { | ||||
|   if (isExt.value) { | ||||
|     return 'a' | ||||
|   } | ||||
|   return 'router-link' | ||||
| }) | ||||
|  | ||||
| function linkProps() { | ||||
|   if (isExt.value) { | ||||
|     return { | ||||
|       href: props.to, | ||||
|       target: '_blank', | ||||
|       rel: 'noopener' | ||||
|     } | ||||
|   } | ||||
|   return { | ||||
|     to: props.to | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										81
									
								
								src/layout/components/Sidebar/Logo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								src/layout/components/Sidebar/Logo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,81 @@ | ||||
| <template> | ||||
|   <div class="sidebar-logo-container" :class="{ 'collapse': collapse }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"> | ||||
|     <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" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ 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" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }}</h1> | ||||
|       </router-link> | ||||
|     </transition> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import variables from '@/assets/styles/variables.module.scss' | ||||
| import logo from '@/assets/logo/logo.png' | ||||
| import useSettingsStore from '@/store/modules/settings' | ||||
|  | ||||
| defineProps({ | ||||
|   collapse: { | ||||
|     type: Boolean, | ||||
|     required: true | ||||
|   } | ||||
| }) | ||||
|  | ||||
| const title = ref('RuoYi-Vue-Plus'); | ||||
| const settingsStore = useSettingsStore(); | ||||
| const sideTheme = computed(() => settingsStore.sideTheme); | ||||
| </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> | ||||
							
								
								
									
										102
									
								
								src/layout/components/Sidebar/SidebarItem.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/layout/components/Sidebar/SidebarItem.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,102 @@ | ||||
| <template> | ||||
|   <div v-if="!item.hidden"> | ||||
|     <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow"> | ||||
|       <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)"> | ||||
|         <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }"> | ||||
|           <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/> | ||||
|           <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template> | ||||
|         </el-menu-item> | ||||
|       </app-link> | ||||
|     </template> | ||||
|  | ||||
|     <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body> | ||||
|       <template v-if="item.meta" #title> | ||||
|         <svg-icon :icon-class="item.meta && item.meta.icon" /> | ||||
|         <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span> | ||||
|       </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-sub-menu> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import { isExternal } from '@/utils/validate' | ||||
| import AppLink from './Link' | ||||
| import { getNormalPath } from '@/utils/ruoyi' | ||||
|  | ||||
| const props = defineProps({ | ||||
|   // route object | ||||
|   item: { | ||||
|     type: Object, | ||||
|     required: true | ||||
|   }, | ||||
|   isNest: { | ||||
|     type: Boolean, | ||||
|     default: false | ||||
|   }, | ||||
|   basePath: { | ||||
|     type: String, | ||||
|     default: '' | ||||
|   } | ||||
| }) | ||||
|  | ||||
| const onlyOneChild = ref({}); | ||||
|  | ||||
| function hasOneShowingChild(children = [], parent) { | ||||
|   if (!children) { | ||||
|     children = []; | ||||
|   } | ||||
|   const showingChildren = children.filter(item => { | ||||
|     if (item.hidden) { | ||||
|       return false | ||||
|     } else { | ||||
|       // Temp set(will be used if only has one showing child) | ||||
|       onlyOneChild.value = 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) { | ||||
|     onlyOneChild.value = { ...parent, path: '', noShowingChildren: true } | ||||
|     return true | ||||
|   } | ||||
|  | ||||
|   return false | ||||
| }; | ||||
|  | ||||
| function resolvePath(routePath, routeQuery) { | ||||
|   if (isExternal(routePath)) { | ||||
|     return routePath | ||||
|   } | ||||
|   if (isExternal(props.basePath)) { | ||||
|     return props.basePath | ||||
|   } | ||||
|   if (routeQuery) { | ||||
|     let query = JSON.parse(routeQuery); | ||||
|     return { path: getNormalPath(props.basePath + '/' + routePath), query: query } | ||||
|   } | ||||
|   return getNormalPath(props.basePath + '/' + routePath) | ||||
| } | ||||
|  | ||||
| function hasTitle(title){ | ||||
|   if (title.length > 5) { | ||||
|     return title; | ||||
|   } else { | ||||
|     return ""; | ||||
|   } | ||||
| } | ||||
| </script> | ||||
							
								
								
									
										54
									
								
								src/layout/components/Sidebar/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/layout/components/Sidebar/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,54 @@ | ||||
| <template> | ||||
|   <div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"> | ||||
|     <logo v-if="showLogo" :collapse="isCollapse" /> | ||||
|     <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper"> | ||||
|       <el-menu | ||||
|         :default-active="activeMenu" | ||||
|         :collapse="isCollapse" | ||||
|         :background-color="sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground" | ||||
|         :text-color="sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor" | ||||
|         :unique-opened="true" | ||||
|         :active-text-color="theme" | ||||
|         :collapse-transition="false" | ||||
|         mode="vertical" | ||||
|       > | ||||
|         <sidebar-item | ||||
|           v-for="(route, index) in sidebarRouters" | ||||
|           :key="route.path + index" | ||||
|           :item="route" | ||||
|           :base-path="route.path" | ||||
|         /> | ||||
|       </el-menu> | ||||
|     </el-scrollbar> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import Logo from './Logo' | ||||
| import SidebarItem from './SidebarItem' | ||||
| import variables from '@/assets/styles/variables.module.scss' | ||||
| import useAppStore from '@/store/modules/app' | ||||
| import useSettingsStore from '@/store/modules/settings' | ||||
| import usePermissionStore from '@/store/modules/permission' | ||||
|  | ||||
| const route = useRoute(); | ||||
| const appStore = useAppStore() | ||||
| const settingsStore = useSettingsStore() | ||||
| const permissionStore = usePermissionStore() | ||||
|  | ||||
| const sidebarRouters =  computed(() => permissionStore.sidebarRouters); | ||||
| const showLogo = computed(() => settingsStore.sidebarLogo); | ||||
| const sideTheme = computed(() => settingsStore.sideTheme); | ||||
| const theme = computed(() => settingsStore.theme); | ||||
| const isCollapse = computed(() => !appStore.sidebar.opened); | ||||
|  | ||||
| const activeMenu = computed(() => { | ||||
|   const { meta, path } = route; | ||||
|   // if set path, the sidebar will highlight the path you set | ||||
|   if (meta.activeMenu) { | ||||
|     return meta.activeMenu; | ||||
|   } | ||||
|   return path; | ||||
| }) | ||||
|  | ||||
| </script> | ||||
							
								
								
									
										105
									
								
								src/layout/components/TagsView/ScrollPane.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/layout/components/TagsView/ScrollPane.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,105 @@ | ||||
| <template> | ||||
|   <el-scrollbar | ||||
|     ref="scrollContainer" | ||||
|     :vertical="false" | ||||
|     class="scroll-container" | ||||
|     @wheel.prevent="handleScroll" | ||||
|   > | ||||
|     <slot /> | ||||
|   </el-scrollbar> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import useTagsViewStore from '@/store/modules/tagsView' | ||||
|  | ||||
| const tagAndTagSpacing = ref(4); | ||||
| const { proxy } = getCurrentInstance(); | ||||
|  | ||||
| const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef); | ||||
|  | ||||
| onMounted(() => { | ||||
|   scrollWrapper.value.addEventListener('scroll', emitScroll, true) | ||||
| }) | ||||
| onBeforeUnmount(() => { | ||||
|   scrollWrapper.value.removeEventListener('scroll', emitScroll) | ||||
| }) | ||||
|  | ||||
| function handleScroll(e) { | ||||
|   const eventDelta = e.wheelDelta || -e.deltaY * 40 | ||||
|   const $scrollWrapper = scrollWrapper.value; | ||||
|   $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4 | ||||
| } | ||||
| const emits = defineEmits() | ||||
| const emitScroll = () => { | ||||
|   emits('scroll') | ||||
| } | ||||
|  | ||||
| const tagsViewStore = useTagsViewStore() | ||||
| const visitedViews = computed(() => tagsViewStore.visitedViews); | ||||
|  | ||||
| function moveToTarget(currentTag) { | ||||
|   const $container = proxy.$refs.scrollContainer.$el | ||||
|   const $containerWidth = $container.offsetWidth | ||||
|   const $scrollWrapper = scrollWrapper.value; | ||||
|  | ||||
|   let firstTag = null | ||||
|   let lastTag = null | ||||
|  | ||||
|   // find first tag and last tag | ||||
|   if (visitedViews.value.length > 0) { | ||||
|     firstTag = visitedViews.value[0] | ||||
|     lastTag = visitedViews.value[visitedViews.value.length - 1] | ||||
|   } | ||||
|  | ||||
|   if (firstTag === currentTag) { | ||||
|     $scrollWrapper.scrollLeft = 0 | ||||
|   } else if (lastTag === currentTag) { | ||||
|     $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth | ||||
|   } else { | ||||
|     const tagListDom = document.getElementsByClassName('tags-view-item'); | ||||
|     const currentIndex = visitedViews.value.findIndex(item => item === currentTag) | ||||
|     let prevTag = null | ||||
|     let nextTag = null | ||||
|     for (const k in tagListDom) { | ||||
|       if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) { | ||||
|         if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) { | ||||
|           prevTag = tagListDom[k]; | ||||
|         } | ||||
|         if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) { | ||||
|           nextTag = tagListDom[k]; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     // the tag's offsetLeft after of nextTag | ||||
|     const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value | ||||
|  | ||||
|     // the tag's offsetLeft before of prevTag | ||||
|     const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value | ||||
|     if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) { | ||||
|       $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth | ||||
|     } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) { | ||||
|       $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| defineExpose({ | ||||
|   moveToTarget, | ||||
| }) | ||||
| </script> | ||||
|  | ||||
| <style lang='scss' scoped> | ||||
| .scroll-container { | ||||
|   white-space: nowrap; | ||||
|   position: relative; | ||||
|   overflow: hidden; | ||||
|   width: 100%; | ||||
|   :deep(.el-scrollbar__bar) { | ||||
|     bottom: 0px; | ||||
|   } | ||||
|   :deep(.el-scrollbar__wrap) { | ||||
|     height: 49px; | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										338
									
								
								src/layout/components/TagsView/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										338
									
								
								src/layout/components/TagsView/index.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,338 @@ | ||||
| <template> | ||||
|   <div id="tags-view-container" class="tags-view-container"> | ||||
|     <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll"> | ||||
|       <router-link | ||||
|         v-for="tag in visitedViews" | ||||
|         :key="tag.path" | ||||
|         :data-path="tag.path" | ||||
|         :class="isActive(tag) ? 'active' : ''" | ||||
|         :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }" | ||||
|         class="tags-view-item" | ||||
|         :style="activeStyle(tag)" | ||||
|         @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''" | ||||
|         @contextmenu.prevent="openMenu(tag, $event)" | ||||
|       > | ||||
|         {{ tag.title }} | ||||
|         <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)"> | ||||
|           <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" /> | ||||
|         </span> | ||||
|       </router-link> | ||||
|     </scroll-pane> | ||||
|     <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu"> | ||||
|       <li @click="refreshSelectedTag(selectedTag)"> | ||||
|         <refresh-right style="width: 1em; height: 1em;" /> 刷新页面 | ||||
|       </li> | ||||
|       <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"> | ||||
|         <close style="width: 1em; height: 1em;" /> 关闭当前 | ||||
|       </li> | ||||
|       <li @click="closeOthersTags"> | ||||
|         <circle-close style="width: 1em; height: 1em;" /> 关闭其他 | ||||
|       </li> | ||||
|       <li v-if="!isFirstView()" @click="closeLeftTags"> | ||||
|         <back style="width: 1em; height: 1em;" /> 关闭左侧 | ||||
|       </li> | ||||
|       <li v-if="!isLastView()" @click="closeRightTags"> | ||||
|         <right style="width: 1em; height: 1em;" /> 关闭右侧 | ||||
|       </li> | ||||
|       <li @click="closeAllTags(selectedTag)"> | ||||
|         <circle-close style="width: 1em; height: 1em;" /> 全部关闭 | ||||
|       </li> | ||||
|     </ul> | ||||
|   </div> | ||||
| </template> | ||||
|  | ||||
| <script setup> | ||||
| import ScrollPane from './ScrollPane' | ||||
| import { getNormalPath } from '@/utils/ruoyi' | ||||
| import useTagsViewStore from '@/store/modules/tagsView' | ||||
| import useSettingsStore from '@/store/modules/settings' | ||||
| import usePermissionStore from '@/store/modules/permission' | ||||
|  | ||||
| const visible = ref(false); | ||||
| const top = ref(0); | ||||
| const left = ref(0); | ||||
| const selectedTag = ref({}); | ||||
| const affixTags = ref([]); | ||||
| const scrollPaneRef = ref(null); | ||||
|  | ||||
| const { proxy } = getCurrentInstance(); | ||||
| const route = useRoute(); | ||||
| const router = useRouter(); | ||||
|  | ||||
| const visitedViews = computed(() => useTagsViewStore().visitedViews); | ||||
| const routes = computed(() => usePermissionStore().routes); | ||||
| const theme = computed(() => useSettingsStore().theme); | ||||
|  | ||||
| watch(route, () => { | ||||
|   addTags() | ||||
|   moveToCurrentTag() | ||||
| }) | ||||
| watch(visible, (value) => { | ||||
|   if (value) { | ||||
|     document.body.addEventListener('click', closeMenu) | ||||
|   } else { | ||||
|     document.body.removeEventListener('click', closeMenu) | ||||
|   } | ||||
| }) | ||||
| onMounted(() => { | ||||
|   initTags() | ||||
|   addTags() | ||||
| }) | ||||
|  | ||||
| function isActive(r) { | ||||
|   return r.path === route.path | ||||
| } | ||||
| function activeStyle(tag) { | ||||
|   if (!isActive(tag)) return {}; | ||||
|   return { | ||||
|     "background-color": theme.value, | ||||
|     "border-color": theme.value | ||||
|   }; | ||||
| } | ||||
| function isAffix(tag) { | ||||
|   return tag.meta && tag.meta.affix | ||||
| } | ||||
| function isFirstView() { | ||||
|   try { | ||||
|     return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath | ||||
|   } catch (err) { | ||||
|     return false | ||||
|   } | ||||
| } | ||||
| function isLastView() { | ||||
|   try { | ||||
|     return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath | ||||
|   } catch (err) { | ||||
|     return false | ||||
|   } | ||||
| } | ||||
| function filterAffixTags(routes, basePath = '') { | ||||
|   let tags = [] | ||||
|   routes.forEach(route => { | ||||
|     if (route.meta && route.meta.affix) { | ||||
|       const tagPath = getNormalPath(basePath + '/' + route.path) | ||||
|       tags.push({ | ||||
|         fullPath: tagPath, | ||||
|         path: tagPath, | ||||
|         name: route.name, | ||||
|         meta: { ...route.meta } | ||||
|       }) | ||||
|     } | ||||
|     if (route.children) { | ||||
|       const tempTags = filterAffixTags(route.children, route.path) | ||||
|       if (tempTags.length >= 1) { | ||||
|         tags = [...tags, ...tempTags] | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
|   return tags | ||||
| } | ||||
| function initTags() { | ||||
|   const res = filterAffixTags(routes.value); | ||||
|   affixTags.value = res; | ||||
|   for (const tag of res) { | ||||
|     // Must have tag name | ||||
|     if (tag.name) { | ||||
|        useTagsViewStore().addVisitedView(tag) | ||||
|     } | ||||
|   } | ||||
| } | ||||
| function addTags() { | ||||
|   const { name } = route | ||||
|   if (name) { | ||||
|     useTagsViewStore().addView(route) | ||||
|     if (route.meta.link) { | ||||
|       useTagsViewStore().addIframeView(route); | ||||
|     } | ||||
|   } | ||||
|   return false | ||||
| } | ||||
| function moveToCurrentTag() { | ||||
|   nextTick(() => { | ||||
|     for (const r of visitedViews.value) { | ||||
|       if (r.path === route.path) { | ||||
|         scrollPaneRef.value.moveToTarget(r); | ||||
|         // when query is different then update | ||||
|         if (r.fullPath !== route.fullPath) { | ||||
|           useTagsViewStore().updateVisitedView(route) | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| function refreshSelectedTag(view) { | ||||
|   proxy.$tab.refreshPage(view); | ||||
|   if (route.meta.link) { | ||||
|     useTagsViewStore().delIframeView(route); | ||||
|   } | ||||
| } | ||||
| function closeSelectedTag(view) { | ||||
|   proxy.$tab.closePage(view).then(({ visitedViews }) => { | ||||
|     if (isActive(view)) { | ||||
|       toLastView(visitedViews, view) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| function closeRightTags() { | ||||
|   proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => { | ||||
|     if (!visitedViews.find(i => i.fullPath === route.fullPath)) { | ||||
|       toLastView(visitedViews) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| function closeLeftTags() { | ||||
|   proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => { | ||||
|     if (!visitedViews.find(i => i.fullPath === route.fullPath)) { | ||||
|       toLastView(visitedViews) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| function closeOthersTags() { | ||||
|   router.push(selectedTag.value).catch(() => { }); | ||||
|   proxy.$tab.closeOtherPage(selectedTag.value).then(() => { | ||||
|     moveToCurrentTag() | ||||
|   }) | ||||
| } | ||||
| function closeAllTags(view) { | ||||
|   proxy.$tab.closeAllPage().then(({ visitedViews }) => { | ||||
|     if (affixTags.value.some(tag => tag.path === route.path)) { | ||||
|       return | ||||
|     } | ||||
|     toLastView(visitedViews, view) | ||||
|   }) | ||||
| } | ||||
| function toLastView(visitedViews, view) { | ||||
|   const latestView = visitedViews.slice(-1)[0] | ||||
|   if (latestView) { | ||||
|     router.push(latestView.fullPath) | ||||
|   } 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 | ||||
|       router.replace({ path: '/redirect' + view.fullPath }) | ||||
|     } else { | ||||
|       router.push('/') | ||||
|     } | ||||
|   } | ||||
| } | ||||
| function openMenu(tag, e) { | ||||
|   const menuMinWidth = 105 | ||||
|   const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left | ||||
|   const offsetWidth = proxy.$el.offsetWidth // container width | ||||
|   const maxLeft = offsetWidth - menuMinWidth // left boundary | ||||
|   const l = e.clientX - offsetLeft + 15 // 15: margin right | ||||
|  | ||||
|   if (l > maxLeft) { | ||||
|     left.value = maxLeft | ||||
|   } else { | ||||
|     left.value = l | ||||
|   } | ||||
|  | ||||
|   top.value = e.clientY | ||||
|   visible.value = true | ||||
|   selectedTag.value = tag | ||||
| } | ||||
| function closeMenu() { | ||||
|   visible.value = false | ||||
| } | ||||
| function handleScroll() { | ||||
|   closeMenu() | ||||
| } | ||||
| </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, 0.12), 0 0 3px 0 rgba(0, 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, 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 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); | ||||
|       transform-origin: 100% 50%; | ||||
|       &:before { | ||||
|         transform: scale(0.6); | ||||
|         display: inline-block; | ||||
|         vertical-align: -3px; | ||||
|       } | ||||
|       &:hover { | ||||
|         background-color: #b4bccc; | ||||
|         color: #fff; | ||||
|         width: 12px !important; | ||||
|         height: 12px !important; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| </style> | ||||
							
								
								
									
										4
									
								
								src/layout/components/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/layout/components/index.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | ||||
| export { default as AppMain } from './AppMain' | ||||
| export { default as Navbar } from './Navbar' | ||||
| export { default as Settings } from './Settings' | ||||
| export { default as TagsView } from './TagsView/index.vue' | ||||
		Reference in New Issue
	
	Block a user
	 疯狂的狮子li
					疯狂的狮子li