feature:代码初始化。
This commit is contained in:
53
src/layout/components/AppMain.vue
Normal file
53
src/layout/components/AppMain.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<section class="app-main">
|
||||
<router-view v-slot="{ Component, route }">
|
||||
<transition name="fade-transform" mode="out-in">
|
||||
<keep-alive :include="tagsViewStore.cachedViews">
|
||||
<component v-if="!route.meta.link" :is="Component" :key="route.path" />
|
||||
</keep-alive>
|
||||
</transition>
|
||||
</router-view>
|
||||
<iframe-toggle />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import iframeToggle from './IframeToggle/index'
|
||||
import useTagsViewStore from '@/store/modules/tagsView'
|
||||
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-main {
|
||||
/* 50= navbar 50 */
|
||||
height: calc(100vh - 0.5rem);
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.hasTagsView {
|
||||
.app-main {
|
||||
/* 84 = navbar + tags-view = 50 + 34 */
|
||||
min-height: calc(100vh - 84px);
|
||||
}
|
||||
|
||||
.fixed-header + .app-main {
|
||||
padding-top: 0.84rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
// fix css style bug in open el-dialog
|
||||
.el-popup-parent--hidden {
|
||||
.fixed-header {
|
||||
padding-right: 17px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
19
src/layout/components/IframeToggle/index.vue
Normal file
19
src/layout/components/IframeToggle/index.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<transition-group name="fade-transform" mode="out-in">
|
||||
<inner-link
|
||||
v-for="(item, index) in tagsViewStore.iframeViews"
|
||||
:key="item.path"
|
||||
:iframeId="'iframe' + index"
|
||||
v-show="route.path === item.path"
|
||||
:src="item.meta.link"
|
||||
></inner-link>
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import InnerLink from "../InnerLink/index"
|
||||
import useTagsViewStore from '@/store/modules/tagsView'
|
||||
|
||||
const route = useRoute();
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
</script>
|
||||
24
src/layout/components/InnerLink/index.vue
Normal file
24
src/layout/components/InnerLink/index.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<div :style="'height:' + height">
|
||||
<iframe
|
||||
:id="iframeId"
|
||||
style="width: 100%; height: 100%"
|
||||
:src="src"
|
||||
frameborder="no"
|
||||
></iframe>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
src: {
|
||||
type: String,
|
||||
default: "/"
|
||||
},
|
||||
iframeId: {
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const height = ref(document.documentElement.clientHeight - 94.5 + "px");
|
||||
</script>
|
||||
224
src/layout/components/Navbar.vue
Normal file
224
src/layout/components/Navbar.vue
Normal file
@@ -0,0 +1,224 @@
|
||||
<template>
|
||||
<!-- 上下结构有style true -->
|
||||
<div class="navbar" :class="defaultLayout === 'topBottom' ? 'navbar-lr' : 'navbar-tb'">
|
||||
<!-- 上下 -->
|
||||
<logo v-if="showLogo && defaultLayout === 'topBottom'" :collapse="isCollapse" />
|
||||
<!-- 左右 && false-->
|
||||
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav && defaultLayout === 'leftRight'" />
|
||||
|
||||
<top-nav id="topmenu-container" :class="defaultLayout === 'topBottom' ? 'topmenu-container' : ''" v-if="settingsStore.topNav" />
|
||||
|
||||
<div class="right-menu">
|
||||
<template v-if="appStore.device !== 'mobile'">
|
||||
<!-- <div class="back-screen" @click="backScreen">返回首页</div> -->
|
||||
<header-search id="header-search" class="right-menu-item" />
|
||||
<screenfull id="screenfull" class="right-menu-item hover-effect" />
|
||||
</template>
|
||||
<div class="avatar-container">
|
||||
<el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
|
||||
<div class="avatar-wrapper">
|
||||
<img src="./../../assets/images/logo.png" alt="" />
|
||||
<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">
|
||||
<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 Logo from './Sidebar/Logo.vue'
|
||||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
import TopNav from '@/components/TopNav'
|
||||
import Screenfull from '@/components/Screenfull'
|
||||
import HeaderSearch from '@/components/HeaderSearch'
|
||||
import useAppStore from '@/store/modules/app'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import { getToken, removeToken } from '@/utils/auth'
|
||||
import { getConfigKey } from '@/api/system/config'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
|
||||
const showLogo = computed(() => settingsStore.sidebarLogo)
|
||||
const defaultLayout = computed(() => settingsStore.defaultLayout)
|
||||
|
||||
const isCollapse = computed(() => !appStore.sidebar.opened)
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
const token = ref(getToken())
|
||||
|
||||
const backScreen = () => {
|
||||
getConfigKey('screenUrl').then((res) => {
|
||||
window.open(`${res.msg}redirectTo?token=${token.value}`, '_self')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.navbar-lr {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.navbar-tb {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.navbar {
|
||||
height: 0.5rem;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
|
||||
.hamburger-container {
|
||||
line-height: 0.46rem;
|
||||
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: 2rem;
|
||||
}
|
||||
|
||||
.errLog-container {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.right-menu {
|
||||
// float: right;
|
||||
// height: 100%;
|
||||
line-height: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.back-screen {
|
||||
width: 0.99rem;
|
||||
height: 0.31rem;
|
||||
line-height: 0.31rem;
|
||||
background: #409eff;
|
||||
border-radius: 0.16rem;
|
||||
font-size: 0.14rem;
|
||||
font-family: PingFang-SC-Medium, PingFang-SC;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
margin-right: 0.23rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.right-menu-item {
|
||||
display: inline-block;
|
||||
padding: 0 0.08rem;
|
||||
height: 100%;
|
||||
font-size: 0.18rem;
|
||||
color: #5a5e66;
|
||||
|
||||
&.hover-effect {
|
||||
cursor: pointer;
|
||||
transition: background 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(0, 0, 0, 0.025);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
margin-right: 0.4rem;
|
||||
|
||||
.avatar-wrapper {
|
||||
margin-top: 5px;
|
||||
position: relative;
|
||||
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
width: 0.4rem;
|
||||
height: 0.4rem;
|
||||
border-radius: 0.1rem;
|
||||
}
|
||||
|
||||
i {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
right: -0.2rem;
|
||||
top: 0.15rem;
|
||||
font-size: 0.12rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
270
src/layout/components/Settings/index.vue
Normal file
270
src/layout/components/Settings/index.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<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="setting-drawer-title">
|
||||
<h3 class="drawer-title">页面布局</h3>
|
||||
</div>
|
||||
<div class="setting-drawer-block-checbox">
|
||||
<el-button :type="defaultLayout === 'leftRight' ? '' : 'primary'" @click="handleUpDown">上下布局</el-button>
|
||||
<el-button :type="defaultLayout === 'leftRight' ? 'primary' : ''" @click="handleLeftRight">左右布局</el-button>
|
||||
</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>
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import originElementPlus from 'element-plus/theme-chalk/index.css'
|
||||
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 defaultLayout = ref(settingsStore.defaultLayout)
|
||||
const storeSettings = computed(() => settingsStore)
|
||||
const predefineColors = ref(['#409EFF', '#ff4500', '#ff8c00', '#ffd700', '#90ee90', '#00ced1', '#1e90ff', '#c71585'])
|
||||
|
||||
const handleUpDown = () => {
|
||||
settingsStore.changeSetting({ key: 'defaultLayout', value: 'topBottom' })
|
||||
}
|
||||
const handleLeftRight = () => {
|
||||
settingsStore.changeSetting({ key: 'defaultLayout', value: 'leftRight' })
|
||||
}
|
||||
|
||||
/** 是否需要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('正在保存到本地,请稍候...')
|
||||
const 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,
|
||||
defaultLayout: storeSettings.value.defaultLayout
|
||||
}
|
||||
localStorage.setItem('layout-setting', JSON.stringify(layoutSetting))
|
||||
setTimeout(proxy.$modal.closeLoading(), 1000)
|
||||
}
|
||||
|
||||
function resetSetting() {
|
||||
proxy.$modal.loading('正在清除设置缓存并刷新,请稍候...')
|
||||
localStorage.removeItem('layout-setting')
|
||||
setTimeout(() => {
|
||||
window.location.reload()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function openSetting() {
|
||||
showSettings.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
openSetting
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.setting-drawer-title {
|
||||
margin-bottom: 12px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
line-height: 22px;
|
||||
font-weight: bold;
|
||||
.drawer-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
.setting-drawer-block-checbox {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.setting-drawer-block-checbox-item {
|
||||
position: relative;
|
||||
margin-right: 16px;
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.custom-img {
|
||||
width: 48px;
|
||||
height: 38px;
|
||||
border-radius: 5px;
|
||||
box-shadow: 1px 1px 2px #898484;
|
||||
}
|
||||
|
||||
.setting-drawer-block-checbox-selectIcon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding-top: 15px;
|
||||
padding-left: 24px;
|
||||
color: #1890ff;
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.drawer-item {
|
||||
color: rgba(0, 0, 0, 0.65);
|
||||
padding: 12px 0;
|
||||
font-size: 14px;
|
||||
|
||||
.comp-style {
|
||||
float: right;
|
||||
margin: -3px 8px 0px 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
40
src/layout/components/Sidebar/Link.vue
Normal file
40
src/layout/components/Sidebar/Link.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<component :is="type" v-bind="linkProps()">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isExternal } from '@/utils/validate'
|
||||
|
||||
const props = defineProps({
|
||||
to: {
|
||||
type: [String, Object],
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const isExt = computed(() => {
|
||||
return isExternal(props.to)
|
||||
})
|
||||
|
||||
const type = computed(() => {
|
||||
if (isExt.value) {
|
||||
return 'a'
|
||||
}
|
||||
return 'router-link'
|
||||
})
|
||||
|
||||
function linkProps() {
|
||||
if (isExt.value) {
|
||||
return {
|
||||
href: props.to,
|
||||
target: '_blank',
|
||||
rel: 'noopener'
|
||||
}
|
||||
}
|
||||
return {
|
||||
to: props.to
|
||||
}
|
||||
}
|
||||
</script>
|
||||
106
src/layout/components/Sidebar/Logo.vue
Normal file
106
src/layout/components/Sidebar/Logo.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<!-- 左右 -->
|
||||
<div
|
||||
v-if="defaultLayout === 'leftRight'"
|
||||
class="sidebar-logo-container"
|
||||
:style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
|
||||
style="width: 100%"
|
||||
:class="{ collapse: collapse }"
|
||||
>
|
||||
<transition name="sidebarLogoFade">
|
||||
<router-link 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>
|
||||
<!-- 上下 -->
|
||||
<div v-else class="sidebar-logo-container" :class="{ collapse: collapse }" style="width: 2rem; background: #fff">
|
||||
<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: #2b2f3a">
|
||||
{{ title }}
|
||||
</h1>
|
||||
</router-link>
|
||||
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
|
||||
<img v-if="logo" :src="logo" class="sidebar-logo" style="margin-right: 0" />
|
||||
<h1 class="sidebar-title" style="color: #2b2f3a">
|
||||
{{ 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'
|
||||
import { getConfigKey } from '@/api/system/config'
|
||||
|
||||
defineProps({
|
||||
collapse: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
getDefaultTitle()
|
||||
|
||||
const title = ref('')
|
||||
function getDefaultTitle() {
|
||||
getConfigKey('defaultTitle').then((res) => {
|
||||
title.value = res.msg
|
||||
})
|
||||
}
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const sideTheme = computed(() => settingsStore.sideTheme)
|
||||
const defaultLayout = computed(() => settingsStore.defaultLayout)
|
||||
</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: 2rem;
|
||||
height: 0.5rem;
|
||||
line-height: 0.5rem;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
|
||||
& .sidebar-logo-link {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& .sidebar-logo {
|
||||
width: 0.32rem;
|
||||
height: 0.32rem;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.12rem;
|
||||
}
|
||||
|
||||
& .sidebar-title {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
color: #2b2f3a;
|
||||
font-weight: 600;
|
||||
line-height: 0.5rem;
|
||||
font-size: 0.14rem;
|
||||
font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
97
src/layout/components/Sidebar/SidebarItem.vue
Normal file
97
src/layout/components/Sidebar/SidebarItem.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<div v-if="!item.hidden">
|
||||
<template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
|
||||
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
|
||||
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
|
||||
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
|
||||
<template #title
|
||||
><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template
|
||||
>
|
||||
</el-menu-item>
|
||||
</app-link>
|
||||
</template>
|
||||
|
||||
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
|
||||
<template v-if="item.meta" #title>
|
||||
<svg-icon :icon-class="item.meta && item.meta.icon" />
|
||||
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
|
||||
</template>
|
||||
|
||||
<sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" />
|
||||
</el-sub-menu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isExternal } from '@/utils/validate'
|
||||
import AppLink from './Link'
|
||||
import { getNormalPath } from '@/utils/ruoyi'
|
||||
|
||||
const props = defineProps({
|
||||
// route object
|
||||
item: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
isNest: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
basePath: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
|
||||
const onlyOneChild = ref({})
|
||||
|
||||
function hasOneShowingChild(children = [], parent) {
|
||||
if (!children) {
|
||||
children = []
|
||||
}
|
||||
const showingChildren = children.filter((item) => {
|
||||
if (item.hidden) {
|
||||
return false
|
||||
} else {
|
||||
// Temp set(will be used if only has one showing child)
|
||||
onlyOneChild.value = item
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
// When there is only one child router, the child router is displayed by default
|
||||
if (showingChildren.length === 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Show parent if there are no child router to display
|
||||
if (showingChildren.length === 0) {
|
||||
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
function resolvePath(routePath, routeQuery) {
|
||||
if (isExternal(routePath)) {
|
||||
return routePath
|
||||
}
|
||||
if (isExternal(props.basePath)) {
|
||||
return props.basePath
|
||||
}
|
||||
if (routeQuery) {
|
||||
const query = JSON.parse(routeQuery)
|
||||
return { path: getNormalPath(props.basePath + '/' + routePath), query }
|
||||
}
|
||||
return getNormalPath(props.basePath + '/' + routePath)
|
||||
}
|
||||
|
||||
function hasTitle(title) {
|
||||
if (title.length > 5) {
|
||||
return title
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
98
src/layout/components/Sidebar/index.vue
Normal file
98
src/layout/components/Sidebar/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<div :class="{ 'has-logo': showLogo }" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
|
||||
<logo v-if="showLogo && defaultLayout === 'leftRight'" :collapse="isCollapse" />
|
||||
<el-scrollbar :class="[sideTheme, defaultLayout === 'topBottom' ? 'topBottom' : 'leftRight']" 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 class="isShow" @click="toggleSideBar">
|
||||
<el-icon :size="20" v-if="isCollapse">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
<el-icon :size="20" v-else>
|
||||
<ArrowLeft />
|
||||
</el-icon>
|
||||
<div class="title" v-if="!isCollapse">收起菜单</div>
|
||||
</div>
|
||||
</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 defaultLayout = computed(() => settingsStore.defaultLayout)
|
||||
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
|
||||
})
|
||||
|
||||
function toggleSideBar() {
|
||||
appStore.toggleSideBar()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.isShow {
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
padding: 0 0.2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: AppleSystemUIFont;
|
||||
color: #8590a5;
|
||||
cursor: pointer;
|
||||
background: rgb(31, 32, 51);
|
||||
.title {
|
||||
font-size: 0.13rem;
|
||||
margin-left: 0.1rem;
|
||||
}
|
||||
}
|
||||
.isShow:hover {
|
||||
color: #ffffff !important;
|
||||
background: linear-gradient(
|
||||
270deg,
|
||||
rgba(95, 224, 255, 0.2) 0%,
|
||||
rgba(34, 130, 240, 0.2) 54%,
|
||||
rgba(143, 51, 255, 0.2) 100%,
|
||||
rgba(9, 239, 226, 0.2) 100%
|
||||
) !important;
|
||||
}
|
||||
.has-logo {
|
||||
.topBottom {
|
||||
height: calc(100% - 50px);
|
||||
}
|
||||
.leftRight {
|
||||
height: calc(100% - 100px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
105
src/layout/components/TagsView/ScrollPane.vue
Normal file
105
src/layout/components/TagsView/ScrollPane.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<el-scrollbar
|
||||
ref="scrollContainer"
|
||||
:vertical="false"
|
||||
class="scroll-container"
|
||||
@wheel.prevent="handleScroll"
|
||||
>
|
||||
<slot />
|
||||
</el-scrollbar>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import useTagsViewStore from '@/store/modules/tagsView'
|
||||
|
||||
const tagAndTagSpacing = ref(4);
|
||||
const { proxy } = getCurrentInstance();
|
||||
|
||||
const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef);
|
||||
|
||||
onMounted(() => {
|
||||
scrollWrapper.value.addEventListener('scroll', emitScroll, true)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
scrollWrapper.value.removeEventListener('scroll', emitScroll)
|
||||
})
|
||||
|
||||
function handleScroll(e) {
|
||||
const eventDelta = e.wheelDelta || -e.deltaY * 40
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
|
||||
}
|
||||
const emits = defineEmits()
|
||||
const emitScroll = () => {
|
||||
emits('scroll')
|
||||
}
|
||||
|
||||
const tagsViewStore = useTagsViewStore()
|
||||
const visitedViews = computed(() => tagsViewStore.visitedViews);
|
||||
|
||||
function moveToTarget(currentTag) {
|
||||
const $container = proxy.$refs.scrollContainer.$el
|
||||
const $containerWidth = $container.offsetWidth
|
||||
const $scrollWrapper = scrollWrapper.value;
|
||||
|
||||
let firstTag = null
|
||||
let lastTag = null
|
||||
|
||||
// find first tag and last tag
|
||||
if (visitedViews.value.length > 0) {
|
||||
firstTag = visitedViews.value[0]
|
||||
lastTag = visitedViews.value[visitedViews.value.length - 1]
|
||||
}
|
||||
|
||||
if (firstTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = 0
|
||||
} else if (lastTag === currentTag) {
|
||||
$scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
|
||||
} else {
|
||||
const tagListDom = document.getElementsByClassName('tags-view-item');
|
||||
const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
|
||||
let prevTag = null
|
||||
let nextTag = null
|
||||
for (const k in tagListDom) {
|
||||
if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
|
||||
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
|
||||
prevTag = tagListDom[k];
|
||||
}
|
||||
if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
|
||||
nextTag = tagListDom[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the tag's offsetLeft after of nextTag
|
||||
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
|
||||
|
||||
// the tag's offsetLeft before of prevTag
|
||||
const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
|
||||
if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
|
||||
$scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
|
||||
} else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
|
||||
$scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
moveToTarget,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.scroll-container {
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
:deep(.el-scrollbar__bar) {
|
||||
bottom: 0px;
|
||||
}
|
||||
:deep(.el-scrollbar__wrap) {
|
||||
height: 49px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
338
src/layout/components/TagsView/index.vue
Normal file
338
src/layout/components/TagsView/index.vue
Normal file
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div id="tags-view-container" class="tags-view-container">
|
||||
<scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
|
||||
<router-link
|
||||
v-for="tag in visitedViews"
|
||||
:key="tag.path"
|
||||
:data-path="tag.path"
|
||||
:class="isActive(tag) ? 'active' : ''"
|
||||
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
|
||||
class="tags-view-item"
|
||||
:style="activeStyle(tag)"
|
||||
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
|
||||
@contextmenu.prevent="openMenu(tag, $event)"
|
||||
>
|
||||
{{ tag.title }}
|
||||
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
|
||||
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
|
||||
</span>
|
||||
</router-link>
|
||||
</scroll-pane>
|
||||
<ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
|
||||
<li @click="refreshSelectedTag(selectedTag)">
|
||||
<refresh-right style="width: 1em; height: 1em;" /> 刷新页面
|
||||
</li>
|
||||
<li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
|
||||
<close style="width: 1em; height: 1em;" /> 关闭当前
|
||||
</li>
|
||||
<li @click="closeOthersTags">
|
||||
<circle-close style="width: 1em; height: 1em;" /> 关闭其他
|
||||
</li>
|
||||
<li v-if="!isFirstView()" @click="closeLeftTags">
|
||||
<back style="width: 1em; height: 1em;" /> 关闭左侧
|
||||
</li>
|
||||
<li v-if="!isLastView()" @click="closeRightTags">
|
||||
<right style="width: 1em; height: 1em;" /> 关闭右侧
|
||||
</li>
|
||||
<li @click="closeAllTags(selectedTag)">
|
||||
<circle-close style="width: 1em; height: 1em;" /> 全部关闭
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ScrollPane from './ScrollPane'
|
||||
import { getNormalPath } from '@/utils/ruoyi'
|
||||
import useTagsViewStore from '@/store/modules/tagsView'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
import usePermissionStore from '@/store/modules/permission'
|
||||
|
||||
const visible = ref(false);
|
||||
const top = ref(0);
|
||||
const left = ref(0);
|
||||
const selectedTag = ref({});
|
||||
const affixTags = ref([]);
|
||||
const scrollPaneRef = ref(null);
|
||||
|
||||
const { proxy } = getCurrentInstance();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const visitedViews = computed(() => useTagsViewStore().visitedViews);
|
||||
const routes = computed(() => usePermissionStore().routes);
|
||||
const theme = computed(() => useSettingsStore().theme);
|
||||
|
||||
watch(route, () => {
|
||||
addTags()
|
||||
moveToCurrentTag()
|
||||
})
|
||||
watch(visible, (value) => {
|
||||
if (value) {
|
||||
document.body.addEventListener('click', closeMenu)
|
||||
} else {
|
||||
document.body.removeEventListener('click', closeMenu)
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
initTags()
|
||||
addTags()
|
||||
})
|
||||
|
||||
function isActive(r) {
|
||||
return r.path === route.path
|
||||
}
|
||||
function activeStyle(tag) {
|
||||
if (!isActive(tag)) return {};
|
||||
return {
|
||||
"background-color": theme.value,
|
||||
"border-color": theme.value
|
||||
};
|
||||
}
|
||||
function isAffix(tag) {
|
||||
return tag.meta && tag.meta.affix
|
||||
}
|
||||
function isFirstView() {
|
||||
try {
|
||||
return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
function isLastView() {
|
||||
try {
|
||||
return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
function filterAffixTags(routes, basePath = '') {
|
||||
let tags = []
|
||||
routes.forEach(route => {
|
||||
if (route.meta && route.meta.affix) {
|
||||
const tagPath = getNormalPath(basePath + '/' + route.path)
|
||||
tags.push({
|
||||
fullPath: tagPath,
|
||||
path: tagPath,
|
||||
name: route.name,
|
||||
meta: { ...route.meta }
|
||||
})
|
||||
}
|
||||
if (route.children) {
|
||||
const tempTags = filterAffixTags(route.children, route.path)
|
||||
if (tempTags.length >= 1) {
|
||||
tags = [...tags, ...tempTags]
|
||||
}
|
||||
}
|
||||
})
|
||||
return tags
|
||||
}
|
||||
function initTags() {
|
||||
const res = filterAffixTags(routes.value);
|
||||
affixTags.value = res;
|
||||
for (const tag of res) {
|
||||
// Must have tag name
|
||||
if (tag.name) {
|
||||
useTagsViewStore().addVisitedView(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
function addTags() {
|
||||
const { name } = route
|
||||
if (name) {
|
||||
useTagsViewStore().addView(route)
|
||||
if (route.meta.link) {
|
||||
useTagsViewStore().addIframeView(route);
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
function moveToCurrentTag() {
|
||||
nextTick(() => {
|
||||
for (const r of visitedViews.value) {
|
||||
if (r.path === route.path) {
|
||||
scrollPaneRef.value.moveToTarget(r);
|
||||
// when query is different then update
|
||||
if (r.fullPath !== route.fullPath) {
|
||||
useTagsViewStore().updateVisitedView(route)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
function refreshSelectedTag(view) {
|
||||
proxy.$tab.refreshPage(view);
|
||||
if (route.meta.link) {
|
||||
useTagsViewStore().delIframeView(route);
|
||||
}
|
||||
}
|
||||
function closeSelectedTag(view) {
|
||||
proxy.$tab.closePage(view).then(({ visitedViews }) => {
|
||||
if (isActive(view)) {
|
||||
toLastView(visitedViews, view)
|
||||
}
|
||||
})
|
||||
}
|
||||
function closeRightTags() {
|
||||
proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
|
||||
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
||||
toLastView(visitedViews)
|
||||
}
|
||||
})
|
||||
}
|
||||
function closeLeftTags() {
|
||||
proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
|
||||
if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
|
||||
toLastView(visitedViews)
|
||||
}
|
||||
})
|
||||
}
|
||||
function closeOthersTags() {
|
||||
router.push(selectedTag.value).catch(() => { });
|
||||
proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
|
||||
moveToCurrentTag()
|
||||
})
|
||||
}
|
||||
function closeAllTags(view) {
|
||||
proxy.$tab.closeAllPage().then(({ visitedViews }) => {
|
||||
if (affixTags.value.some(tag => tag.path === route.path)) {
|
||||
return
|
||||
}
|
||||
toLastView(visitedViews, view)
|
||||
})
|
||||
}
|
||||
function toLastView(visitedViews, view) {
|
||||
const latestView = visitedViews.slice(-1)[0]
|
||||
if (latestView) {
|
||||
router.push(latestView.fullPath)
|
||||
} else {
|
||||
// now the default is to redirect to the home page if there is no tags-view,
|
||||
// you can adjust it according to your needs.
|
||||
if (view.name === 'Dashboard') {
|
||||
// to reload home page
|
||||
router.replace({ path: '/redirect' + view.fullPath })
|
||||
} else {
|
||||
router.push('/')
|
||||
}
|
||||
}
|
||||
}
|
||||
function openMenu(tag, e) {
|
||||
const menuMinWidth = 105
|
||||
const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
|
||||
const offsetWidth = proxy.$el.offsetWidth // container width
|
||||
const maxLeft = offsetWidth - menuMinWidth // left boundary
|
||||
const l = e.clientX - offsetLeft + 15 // 15: margin right
|
||||
|
||||
if (l > maxLeft) {
|
||||
left.value = maxLeft
|
||||
} else {
|
||||
left.value = l
|
||||
}
|
||||
|
||||
top.value = e.clientY
|
||||
visible.value = true
|
||||
selectedTag.value = tag
|
||||
}
|
||||
function closeMenu() {
|
||||
visible.value = false
|
||||
}
|
||||
function handleScroll() {
|
||||
closeMenu()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang='scss' scoped>
|
||||
.tags-view-container {
|
||||
height: 34px;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #d8dce5;
|
||||
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
border: 1px solid #d8dce5;
|
||||
color: #495060;
|
||||
background: #fff;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
margin-left: 5px;
|
||||
margin-top: 4px;
|
||||
&:first-of-type {
|
||||
margin-left: 15px;
|
||||
}
|
||||
&:last-of-type {
|
||||
margin-right: 15px;
|
||||
}
|
||||
&.active {
|
||||
background-color: #42b983;
|
||||
color: #fff;
|
||||
border-color: #42b983;
|
||||
&::before {
|
||||
content: "";
|
||||
background: #fff;
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.contextmenu {
|
||||
margin: 0;
|
||||
background: #fff;
|
||||
z-index: 3000;
|
||||
position: absolute;
|
||||
list-style-type: none;
|
||||
padding: 5px 0;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: #333;
|
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
|
||||
li {
|
||||
margin: 0;
|
||||
padding: 7px 16px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
//reset element css of el-icon-close
|
||||
.tags-view-wrapper {
|
||||
.tags-view-item {
|
||||
.el-icon-close {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: 2px;
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||
transform-origin: 100% 50%;
|
||||
&:before {
|
||||
transform: scale(0.6);
|
||||
display: inline-block;
|
||||
vertical-align: -3px;
|
||||
}
|
||||
&:hover {
|
||||
background-color: #b4bccc;
|
||||
color: #fff;
|
||||
width: 12px !important;
|
||||
height: 12px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
4
src/layout/components/index.js
Normal file
4
src/layout/components/index.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default as AppMain } from './AppMain'
|
||||
export { default as Navbar } from './Navbar'
|
||||
export { default as Settings } from './Settings'
|
||||
export { default as TagsView } from './TagsView/index.vue'
|
||||
131
src/layout/index.vue
Normal file
131
src/layout/index.vue
Normal file
@@ -0,0 +1,131 @@
|
||||
<template>
|
||||
<div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
|
||||
<div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
|
||||
<div v-if="defaultLayout === 'topBottom'" style="height: 100%; overflow: hidden">
|
||||
<navbar @setLayout="setLayout" />
|
||||
<div>
|
||||
<!-- sidebar -->
|
||||
<Sidebar v-if="!sidebar.hide" class="sidebar-container sidebar-height" style="top: 50px" />
|
||||
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
|
||||
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav" />
|
||||
<div>
|
||||
<tags-view v-if="needTagsView" />
|
||||
</div>
|
||||
<app-main />
|
||||
<settings ref="settingRef" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else style="height: 100%; overflow: hidden">
|
||||
<Sidebar v-if="!sidebar.hide" class="sidebar-container" style="height: 100%; top: 0px" />
|
||||
<div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
|
||||
<div :class="{ 'fixed-header': fixedHeader }"></div>
|
||||
<navbar @setLayout="setLayout" />
|
||||
<tags-view v-if="needTagsView" />
|
||||
<app-main />
|
||||
<settings ref="settingRef" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Breadcrumb from '@/components/Breadcrumb'
|
||||
import Sidebar from './components/Sidebar/index.vue'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { AppMain, Navbar, Settings, TagsView } from './components'
|
||||
import useAppStore from '@/store/modules/app'
|
||||
import useSettingsStore from '@/store/modules/settings'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const theme = computed(() => settingsStore.theme)
|
||||
const sidebar = computed(() => useAppStore().sidebar)
|
||||
const device = computed(() => useAppStore().device)
|
||||
const needTagsView = computed(() => settingsStore.tagsView)
|
||||
const fixedHeader = computed(() => settingsStore.fixedHeader)
|
||||
const defaultLayout = computed(() => settingsStore.defaultLayout)
|
||||
|
||||
const classObj = computed(() => ({
|
||||
hideSidebar: !sidebar.value.opened,
|
||||
openSidebar: sidebar.value.opened,
|
||||
withoutAnimation: sidebar.value.withoutAnimation,
|
||||
mobile: device.value === 'mobile'
|
||||
}))
|
||||
|
||||
const { width } = 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% - 0.54rem);
|
||||
}
|
||||
|
||||
.sidebarHide .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile .fixed-header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar-height {
|
||||
height: calc(100% - 0.5rem);
|
||||
top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user