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