main
			
			
		
		
							parent
							
								
									064cba6373
								
							
						
					
					
						commit
						c5180c0dfe
					
				| 
						 | 
					@ -115,10 +115,14 @@
 | 
				
			||||||
    .sidebar-container {
 | 
					    .sidebar-container {
 | 
				
			||||||
      width: 54px !important;
 | 
					      width: 54px !important;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .main-container {
 | 
					    .main-container {
 | 
				
			||||||
      margin-left: 54px;
 | 
					      margin-left: 54px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    .layMain-container {
 | 
				
			||||||
 | 
					      margin-left: 0 !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .sub-menu-title-noDropdown {
 | 
					    .sub-menu-title-noDropdown {
 | 
				
			||||||
      padding: 0 !important;
 | 
					      padding: 0 !important;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,7 +36,12 @@ const appStore = useAppStore()
 | 
				
			||||||
const settingsStore = useSettingsStore()
 | 
					const settingsStore = useSettingsStore()
 | 
				
			||||||
const permissionStore = usePermissionStore()
 | 
					const permissionStore = usePermissionStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const sidebarRouters =  computed(() => permissionStore.sidebarRouters);
 | 
					const sidebarRouters =  computed(() => permissionStore.sidebarRouters.filter((item) => {
 | 
				
			||||||
 | 
					  return item.path === localStorage.getItem('CURRENT_MENU');
 | 
				
			||||||
 | 
					}));
 | 
				
			||||||
 | 
					// const sidebarRouters =  computed(() => permissionStore.sidebarRouters.filter((item) => {
 | 
				
			||||||
 | 
					//   return item.path === localStorage.getItem('CURRENT_MENU')
 | 
				
			||||||
 | 
					// }));
 | 
				
			||||||
const showLogo = computed(() => settingsStore.sidebarLogo);
 | 
					const showLogo = computed(() => settingsStore.sidebarLogo);
 | 
				
			||||||
const sideTheme = computed(() => settingsStore.sideTheme);
 | 
					const sideTheme = computed(() => settingsStore.sideTheme);
 | 
				
			||||||
const theme = computed(() => settingsStore.theme);
 | 
					const theme = computed(() => settingsStore.theme);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,68 @@
 | 
				
			||||||
 | 
					<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: 6px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::-webkit-scrollbar {
 | 
				
			||||||
 | 
					  width: 6px;
 | 
				
			||||||
 | 
					  height: 6px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::-webkit-scrollbar-track {
 | 
				
			||||||
 | 
					  background-color: #f1f1f1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					::-webkit-scrollbar-thumb {
 | 
				
			||||||
 | 
					  background-color: #c0c0c0;
 | 
				
			||||||
 | 
					  border-radius: 3px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,27 @@
 | 
				
			||||||
 | 
					<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="iframeUrl(item.meta.link, item.query)"
 | 
				
			||||||
 | 
					    ></inner-link>
 | 
				
			||||||
 | 
					  </transition-group>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import InnerLink from "../InnerLink/index";
 | 
				
			||||||
 | 
					import useTagsViewStore from "@/store/modules/tagsView";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const route = useRoute();
 | 
				
			||||||
 | 
					const tagsViewStore = useTagsViewStore();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function iframeUrl(url, query) {
 | 
				
			||||||
 | 
					  if (Object.keys(query).length > 0) {
 | 
				
			||||||
 | 
					    let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&");
 | 
				
			||||||
 | 
					    return url + "?" + params;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return url;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,188 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="navbar">
 | 
				
			||||||
 | 
					    <top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
 | 
				
			||||||
 | 
					    <div class="right-menu">
 | 
				
			||||||
 | 
					      <template v-if="appStore.device !== 'mobile'">
 | 
				
			||||||
 | 
					        <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">
 | 
				
			||||||
 | 
					                <el-dropdown-item>个人中心</el-dropdown-item>
 | 
				
			||||||
 | 
					              </router-link>
 | 
				
			||||||
 | 
					              <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
 | 
				
			||||||
 | 
					                <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'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const appStore = useAppStore()
 | 
				
			||||||
 | 
					const userStore = useUserStore()
 | 
				
			||||||
 | 
					const settingsStore = useSettingsStore()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 = '/index';
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }).catch(() => { });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const emits = defineEmits(['setLayout'])
 | 
				
			||||||
 | 
					function setLayout() {
 | 
				
			||||||
 | 
					  emits('setLayout');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang='scss' scoped>
 | 
				
			||||||
 | 
					.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>
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					@ -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 = import.meta.env.VITE_APP_TITLE;
 | 
				
			||||||
 | 
					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>
 | 
				
			||||||
| 
						 | 
					@ -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, index) in item.children"
 | 
				
			||||||
 | 
					        :key="child.path + index"
 | 
				
			||||||
 | 
					        :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>
 | 
				
			||||||
| 
						 | 
					@ -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>
 | 
				
			||||||
| 
						 | 
					@ -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: 39px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -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: 5px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  .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>
 | 
				
			||||||
| 
						 | 
					@ -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'
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,108 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
 | 
				
			||||||
 | 
					    <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container layMain-container">
 | 
				
			||||||
 | 
					      <div :class="{ 'fixed-header': fixedHeader }">
 | 
				
			||||||
 | 
					        <navbar @setLayout="setLayout" />
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <app-main />
 | 
				
			||||||
 | 
					      <settings ref="settingRef" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup>
 | 
				
			||||||
 | 
					import { useWindowSize } from '@vueuse/core'
 | 
				
			||||||
 | 
					import Sidebar from './components/Sidebar/index.vue'
 | 
				
			||||||
 | 
					import { AppMain, Navbar, Settings, TagsView } from './components'
 | 
				
			||||||
 | 
					import defaultSettings from '@/settings'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import useAppStore from '@/store/modules/app'
 | 
				
			||||||
 | 
					import useSettingsStore from '@/store/modules/settings'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const settingsStore = useSettingsStore()
 | 
				
			||||||
 | 
					const theme = computed(() => settingsStore.theme);
 | 
				
			||||||
 | 
					const sideTheme = computed(() => settingsStore.sideTheme);
 | 
				
			||||||
 | 
					const sidebar = computed(() => useAppStore().sidebar);
 | 
				
			||||||
 | 
					const device = computed(() => useAppStore().device);
 | 
				
			||||||
 | 
					const needTagsView = computed(() => settingsStore.tagsView);
 | 
				
			||||||
 | 
					const fixedHeader = computed(() => settingsStore.fixedHeader);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const classObj = computed(() => ({
 | 
				
			||||||
 | 
					  hideSidebar: true,
 | 
				
			||||||
 | 
					  openSidebar: sidebar.value.opened,
 | 
				
			||||||
 | 
					  withoutAnimation: sidebar.value.withoutAnimation,
 | 
				
			||||||
 | 
					  mobile: device.value === 'mobile'
 | 
				
			||||||
 | 
					}))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const { width, height } = useWindowSize();
 | 
				
			||||||
 | 
					const WIDTH = 992; // refer to Bootstrap's responsive design
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					watchEffect(() => {
 | 
				
			||||||
 | 
					  if (device.value === 'mobile' && sidebar.value.opened) {
 | 
				
			||||||
 | 
					    useAppStore().closeSideBar({ withoutAnimation: false })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (width.value - 1 < WIDTH) {
 | 
				
			||||||
 | 
					    useAppStore().toggleDevice('mobile')
 | 
				
			||||||
 | 
					    useAppStore().closeSideBar({ withoutAnimation: true })
 | 
				
			||||||
 | 
					  } else {
 | 
				
			||||||
 | 
					    useAppStore().toggleDevice('desktop')
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function handleClickOutside() {
 | 
				
			||||||
 | 
					  useAppStore().closeSideBar({ withoutAnimation: false })
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const settingRef = ref(null);
 | 
				
			||||||
 | 
					function setLayout() {
 | 
				
			||||||
 | 
					  settingRef.value.openSetting();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="scss" scoped>
 | 
				
			||||||
 | 
					  @import "@/assets/styles/mixin.scss";
 | 
				
			||||||
 | 
					  @import "@/assets/styles/variables.module.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% - #{$base-sidebar-width});
 | 
				
			||||||
 | 
					  transition: width 0.28s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.hideSidebar .fixed-header {
 | 
				
			||||||
 | 
					  width: calc(100% - 54px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.sidebarHide .fixed-header {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mobile .fixed-header {
 | 
				
			||||||
 | 
					  width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -4,6 +4,7 @@ import {
 | 
				
			||||||
} from 'vue-router'
 | 
					} from 'vue-router'
 | 
				
			||||||
/* Layout */
 | 
					/* Layout */
 | 
				
			||||||
import Layout from '@/layout'
 | 
					import Layout from '@/layout'
 | 
				
			||||||
 | 
					import LayoutMain from '@/layoutMain'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Note: 路由配置项
 | 
					 * Note: 路由配置项
 | 
				
			||||||
| 
						 | 
					@ -59,7 +60,7 @@ export const constantRoutes = [{
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    path: '',
 | 
					    path: '',
 | 
				
			||||||
    component: Layout,
 | 
					    component: LayoutMain,
 | 
				
			||||||
    redirect: '/index',
 | 
					    redirect: '/index',
 | 
				
			||||||
    children: [{
 | 
					    children: [{
 | 
				
			||||||
      path: '/index',
 | 
					      path: '/index',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1037
									
								
								src/views/index.vue
								
								
								
								
							
							
						
						
									
										1037
									
								
								src/views/index.vue
								
								
								
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Loading…
	
		Reference in New Issue