禁止修改全局布局,以及菜单栏变动

This commit is contained in:
2026-08-23 16:08:48 +08:00
parent 4efb84e5d2
commit 657774e1b1
20 changed files with 34 additions and 1289 deletions
+2 -11
View File
@@ -1,20 +1,11 @@
<template>
<template>
<div id="app">
<router-view />
<theme-picker />
</div>
</template>
<script>
import ThemePicker from "@/components/ThemePicker"
export default {
name: "App",
components: { ThemePicker }
name: "App"
}
</script>
<style scoped>
#app .theme-picker {
display: none;
}
</style>
@@ -92,9 +92,6 @@ export default {
}
},
computed: {
theme() {
return this.$store.state.settings.theme
},
routes() {
return this.$store.getters.defaultRoutes
}
@@ -213,7 +210,7 @@ export default {
activeStyle(index) {
if (index !== this.activeIndex) return {}
return {
'background-color': this.theme,
'background-color': '#00875A',
'color': '#fff'
}
},
@@ -1,170 +0,0 @@
<template>
<el-color-picker
v-model="theme"
:predefine="['#00875A', '#006B47', '#007F4F', '#11a983', '#13c2c2', '#6959CD', '#f5222d', '#212121', ]"
class="theme-picker"
popper-class="theme-picker-dropdown"
/>
</template>
<script>
const ORIGINAL_THEME = '#00875A' // default color
export default {
data() {
return {
chalk: '', // content of theme-chalk css
theme: ''
}
},
computed: {
defaultTheme() {
return this.$store.state.settings.theme
}
},
watch: {
defaultTheme: {
handler: function(val, oldVal) {
this.theme = val
},
immediate: true
},
async theme(val) {
await this.setTheme(val)
}
},
created() {
if (this.defaultTheme !== ORIGINAL_THEME) {
this.setTheme(this.defaultTheme)
}
},
methods: {
async setTheme(val) {
const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
if (typeof val !== 'string') return
const themeCluster = this.getThemeCluster(val.replace('#', ''))
const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
const getHandler = (variable, id) => {
return () => {
const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
let styleTag = document.getElementById(id)
if (!styleTag) {
styleTag = document.createElement('style')
styleTag.setAttribute('id', id)
document.head.appendChild(styleTag)
}
styleTag.innerText = newStyle
}
}
if (!this.chalk) {
const url = `/styles/theme-chalk/index.css`
await this.getCSSString(url, 'chalk')
}
const chalkHandler = getHandler('chalk', 'chalk-style')
chalkHandler()
const styles = [].slice.call(document.querySelectorAll('style'))
.filter(style => {
const text = style.innerText
return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
})
styles.forEach(style => {
const { innerText } = style
if (typeof innerText !== 'string') return
style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
})
this.$emit('change', val)
},
updateStyle(style, oldCluster, newCluster) {
let newStyle = style
oldCluster.forEach((color, index) => {
newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
})
return newStyle
},
getCSSString(url, variable) {
return new Promise(resolve => {
const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200) {
this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
resolve()
}
}
xhr.open('GET', url)
xhr.send()
})
},
getThemeCluster(theme) {
const tintColor = (color, tint) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
if (tint === 0) { // when primary color is in its rgb space
return [red, green, blue].join(',')
} else {
red += Math.round(tint * (255 - red))
green += Math.round(tint * (255 - green))
blue += Math.round(tint * (255 - blue))
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
}
const shadeColor = (color, shade) => {
let red = parseInt(color.slice(0, 2), 16)
let green = parseInt(color.slice(2, 4), 16)
let blue = parseInt(color.slice(4, 6), 16)
red = Math.round((1 - shade) * red)
green = Math.round((1 - shade) * green)
blue = Math.round((1 - shade) * blue)
red = red.toString(16)
green = green.toString(16)
blue = blue.toString(16)
return `#${red}${green}${blue}`
}
const clusters = [theme]
for (let i = 0; i <= 9; i++) {
clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
}
clusters.push(shadeColor(theme, 0.1))
return clusters
}
}
}
</script>
<style>
.theme-message,
.theme-picker-dropdown {
z-index: 99999 !important;
}
.theme-picker .el-color-picker__trigger {
height: 26px !important;
width: 26px !important;
padding: 2px;
}
.theme-picker-dropdown .el-color-dropdown__link-btn {
display: none;
}
</style>
@@ -1,35 +0,0 @@
<template>
<footer v-if="visible" class="copyright">
<span>{{ content }}</span>
</footer>
</template>
<script>
export default {
computed: {
visible() {
return this.$store.state.settings.footerVisible
},
content() {
return this.$store.state.settings.footerContent
}
}
}
</script>
<style scoped>
.copyright {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 36px;
padding: 10px 20px;
text-align: right;
background-color: #f8f8f8;
color: #666;
font-size: 14px;
border-top: 1px solid #e7e7e7;
z-index: 999;
}
</style>
+2 -21
View File
@@ -1,9 +1,8 @@
<template>
<div class="navbar" :class="['nav' + navType, { 'header-mode': headerMode }]">
<div class="navbar" :class="[{ 'header-mode': headerMode }]">
<hamburger v-if="!headerMode" id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb v-if="!headerMode && navType == 1" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="!headerMode && navType == 2" id="topmenu-container" class="topmenu-container" />
<breadcrumb v-if="!headerMode" id="breadcrumb-container" class="breadcrumb-container" />
<div class="right-menu">
<template v-if="device!=='mobile'">
<el-tooltip content="菜单搜索" effect="dark" placement="bottom">
@@ -34,9 +33,6 @@
<router-link to="/user/profile">
<el-dropdown-item icon="el-icon-user">个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item v-if="setting" icon="el-icon-s-tools" @click.native="setLayout">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item icon="el-icon-lock" @click.native="lockScreen">
<span>锁定屏幕</span>
</el-dropdown-item>
@@ -52,7 +48,6 @@
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import TopNav from './TopNav'
import Hamburger from '@/components/Hamburger'
import Screenfull from '@/components/Screenfull'
import Search from '@/components/HeaderSearch'
@@ -61,7 +56,6 @@ import HeaderNotice from './HeaderNotice'
export default {
components: {
Breadcrumb,
TopNav,
Hamburger,
Screenfull,
Search,
@@ -85,16 +79,6 @@ export default {
'nickName',
'roles'
]),
setting: {
get() {
return this.$store.state.settings.showSettings
}
},
navType: {
get() {
return this.$store.state.settings.navType
}
},
// 用户角色标签:根据 roles 数组智能映射为友好文本
userRoleLabel() {
const roleMap = {
@@ -112,9 +96,6 @@ export default {
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
setLayout(event) {
this.$emit('setLayout')
},
handleCommand(command) {
// 预留:下拉菜单 command 事件入口
},
-219
View File
@@ -1,219 +0,0 @@
<template>
<el-drawer
:visible.sync="visible"
direction="rtl"
size="280px"
:title="'布局设置'"
class="drawer-setting"
:with-header="false"
:modal-append-to-body="false"
>
<div class="setting-drawer-index">
<div class="setting-drawer-index__title">主题色</div>
<div class="setting-drawer-index__main-color theme-color">
<ul class="ul-color">
<li
v-for="item in colorList"
:key="item.color"
class="li-theme"
:class="{ active: item.color === theme }"
:style="{ background: item.color }"
@click="changeTheme(item.color)"
>
<i v-show="item.color === theme" class="icon-check el-icon-check" />
</li>
</ul>
</div>
<div class="setting-drawer-index__title">界面功能</div>
<div class="setting-item">
<span class="setting-item__label">侧边栏颜色</span>
<el-dropdown trigger="click" @command="handleSideTheme">
<span class="setting-item__value">
{{ sideThemeLabel }}
<i class="el-icon-arrow-down" />
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="theme-dark">深色主题</el-dropdown-item>
<el-dropdown-item command="theme-light">浅色主题</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="setting-item">
<span class="setting-item__label">导航栏模式</span>
<el-dropdown trigger="click" @command="handleNavType">
<span class="setting-item__value">
{{ navTypeLabel }}
<i class="el-icon-arrow-down" />
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="1">侧边栏模式</el-dropdown-item>
<el-dropdown-item command="2">顶部导航模式</el-dropdown-item>
<el-dropdown-item command="3">侧栏+顶栏</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
<div class="setting-item">
<span class="setting-item__label"> TagsView 标签栏</span>
<el-switch
:value="tagsView"
@change="changeSetting({ key: 'tagsView', value: !tagsView })"
/>
</div>
<div class="setting-item">
<span class="setting-item__label"> 侧边栏 Logo</span>
<el-switch
:value="sidebarLogo"
@change="changeSetting({ key: 'sidebarLogo', value: !sidebarLogo })"
/>
</div>
<div class="setting-item">
<span class="setting-item__label"> 固定 Header</span>
<el-switch
:value="fixedHeader"
@change="changeSetting({ key: 'fixedHeader', value: !fixedHeader })"
/>
</div>
</div>
</el-drawer>
</template>
<script>
import { mapState } from 'vuex'
export default {
name: 'Settings',
data() {
return {
visible: false,
/**
* 主题色列表
*/
colorList: [
{ color: '#00875A', title: '教育绿' },
{ color: '#304156', title: '深蓝' },
{ color: '#409EFF', title: '亮蓝' },
{ color: '#626AEF', title: '亮紫' },
{ color: '#EB2F96', title: '粉红' },
{ color: '#F8721F', title: '橙色' },
{ color: '#CD201F', title: '红色' }
]
}
},
computed: {
...mapState({
theme: state => state.settings.theme,
sideTheme: state => state.settings.sideTheme,
navType: state => state.settings.navType,
tagsView: state => state.settings.tagsView,
sidebarLogo: state => state.settings.sidebarLogo,
fixedHeader: state => state.settings.fixedHeader
}),
sideThemeLabel() {
return this.sideTheme === 'theme-dark' ? '深色主题' : '浅色主题'
},
navTypeLabel() {
const map = { 1: '侧边栏模式', 2: '顶部导航模式', 3: '侧栏+顶栏' }
return map[this.navType] || '侧边栏模式'
}
},
methods: {
openSetting() {
this.visible = true
},
closeSetting() {
this.visible = false
},
// 修改主题色
changeTheme(color) {
this.$store.dispatch('settings/changeSetting', { key: 'theme', value: color })
},
handleSideTheme(command) {
this.changeSetting({ key: 'sideTheme', value: command })
},
handleNavType(command) {
this.changeSetting({ key: 'navType', value: Number(command) })
},
// 修改布局设置,并持久化到本地存储
changeSetting(data) {
this.$store.dispatch('settings/changeSetting', data)
const setting = JSON.parse(localStorage.getItem('layout-setting') || '{}')
setting[data.key] = data.value
localStorage.setItem('layout-setting', JSON.stringify(setting))
}
}
}
</script>
<style lang="scss" scoped>
.setting-drawer-index {
padding: 0 20px;
box-sizing: border-box;
&__title {
margin-top: 26px;
margin-bottom: 12px;
font-weight: 700;
color: #333;
}
&__main-color {
border-radius: 4px;
}
.theme-color {
.ul-color {
margin: 0;
padding: 0;
.li-theme {
list-style: none;
display: inline-block;
width: 30px;
height: 30px;
margin-right: 6px;
border-radius: 50%;
cursor: pointer;
text-align: center;
vertical-align: middle;
&.active {
box-shadow: 0 0 0 2px #fff, 0 0 0 4px currentColor;
}
.icon-check {
color: #fff;
font-size: 14px;
line-height: 30px;
}
}
}
}
.setting-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 0;
font-size: 14px;
color: #606266;
&__value {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 0 12px;
height: 30px;
line-height: 30px;
border-radius: 4px;
border: 1px solid #dcdfe6;
cursor: pointer;
font-size: 13px;
}
}
}
</style>
@@ -1,430 +0,0 @@
<template>
<el-drawer size="280px" :visible="showSettings" :with-header="false" :append-to-body="true" :before-close="closeSetting" :lock-scroll="false">
<div class="drawer-container">
<div>
<div class="setting-drawer-content">
<div class="setting-drawer-title">
<h3 class="drawer-title">菜单导航设置</h3>
</div>
<div class="nav-wrap">
<el-tooltip content="左侧菜单" placement="bottom">
<div class="item left" @click="handleNavType(1)" :style="{'--theme': theme}" :class="{ activeItem: navType == 1 }">
<b></b><b></b>
</div>
</el-tooltip>
<el-tooltip content="混合菜单" placement="bottom">
<div class="item mix" @click="handleNavType(2)" :style="{'--theme': theme}" :class="{ activeItem: navType == 2 }">
<b></b><b></b>
</div>
</el-tooltip>
<el-tooltip content="顶部菜单" placement="bottom">
<div class="item top" @click="handleNavType(3)" :style="{'--theme': theme}" :class="{ activeItem: navType == 3 }">
<b></b><b></b>
</div>
</el-tooltip>
</div>
<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>
<theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
</div>
</div>
<el-divider/>
<h3 class="drawer-title">系统布局配置</h3>
<div class="drawer-item">
<span>开启页签</span>
<el-switch v-model="tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>持久化标签页</span>
<el-switch v-model="tagsViewPersist" :disabled="!tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>显示页签图标</span>
<el-switch v-model="tagsIcon" :disabled="!tagsView" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>标签页样式</span>
<el-radio-group v-model="tagsViewStyle" :disabled="!tagsView" size="mini" class="drawer-switch">
<el-radio-button label="card">卡片</el-radio-button>
<el-radio-button label="chrome">谷歌</el-radio-button>
</el-radio-group>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<el-switch v-model="fixedHeader" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>显示 Logo</span>
<el-switch v-model="sidebarLogo" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>动态标题</span>
<el-switch v-model="dynamicTitle" class="drawer-switch" />
</div>
<div class="drawer-item">
<span>底部版权</span>
<el-switch v-model="footerVisible" class="drawer-switch" />
</div>
<el-divider/>
<el-button size="small" type="primary" plain icon="el-icon-document-add" @click="saveSetting">保存配置</el-button>
<el-button size="small" plain icon="el-icon-refresh" @click="resetSetting">重置配置</el-button>
</div>
</div>
</el-drawer>
</template>
<script>
import ThemePicker from '@/components/ThemePicker'
export default {
components: { ThemePicker },
expose: ['openSetting'],
data() {
return {
theme: this.$store.state.settings.theme,
sideTheme: this.$store.state.settings.sideTheme,
navType: this.$store.state.settings.navType,
showSettings: false
}
},
computed: {
fixedHeader: {
get() {
return this.$store.state.settings.fixedHeader
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'fixedHeader',
value: val
})
}
},
tagsViewPersist: {
get() {
return this.$store.state.settings.tagsViewPersist
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsViewPersist',
value: val
})
}
},
tagsView: {
get() {
return this.$store.state.settings.tagsView
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsView',
value: val
})
}
},
tagsIcon: {
get() {
return this.$store.state.settings.tagsIcon
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsIcon',
value: val
})
}
},
tagsViewStyle: {
get() {
return this.$store.state.settings.tagsViewStyle
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'tagsViewStyle',
value: val
})
}
},
sidebarLogo: {
get() {
return this.$store.state.settings.sidebarLogo
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'sidebarLogo',
value: val
})
}
},
dynamicTitle: {
get() {
return this.$store.state.settings.dynamicTitle
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'dynamicTitle',
value: val
})
this.$store.dispatch('settings/setTitle', this.$store.state.settings.title)
}
},
footerVisible: {
get() {
return this.$store.state.settings.footerVisible
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'footerVisible',
value: val
})
}
}
},
watch: {
navType: {
handler(val) {
if (val == 1) {
this.$store.dispatch("app/toggleSideBarHide", false)
}
if (val == 2) {
}
if (val == 3) {
this.$store.dispatch("app/toggleSideBarHide", true)
}
if ([1, 3].includes(val)) {
this.$store.commit("SET_SIDEBAR_ROUTERS",this.$store.state.permission.defaultRoutes)
}
},
immediate: true,
deep: true
}
},
methods: {
themeChange(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'theme',
value: val
})
this.theme = val
},
handleTheme(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'sideTheme',
value: val
})
this.sideTheme = val
},
handleNavType(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'navType',
value: val
})
this.navType = val
},
openSetting() {
this.showSettings = true
},
closeSetting(){
this.showSettings = false
},
saveSetting() {
this.$modal.loading("正在保存到本地,请稍候...")
if (!this.tagsViewPersist) {
this.$cache.local.remove('tags-view-visited')
}
this.$cache.local.set(
"layout-setting",
`{
"navType":${this.navType},
"tagsView":${this.tagsView},
"tagsIcon":${this.tagsIcon},
"tagsViewStyle":"${this.tagsViewStyle}",
"tagsViewPersist":${this.tagsViewPersist},
"fixedHeader":${this.fixedHeader},
"sidebarLogo":${this.sidebarLogo},
"dynamicTitle":${this.dynamicTitle},
"footerVisible":${this.footerVisible},
"sideTheme":"${this.sideTheme}",
"theme":"${this.theme}"
}`
)
setTimeout(this.$modal.closeLoading(), 1000)
},
resetSetting() {
this.$modal.loading("正在清除设置缓存并刷新,请稍候...")
this.$cache.local.remove('tags-view-visited')
this.$cache.local.remove("layout-setting")
setTimeout("window.location.reload()", 1000)
}
}
}
</script>
<style lang="scss" scoped>
.setting-drawer-content {
.setting-drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
font-weight: bold;
}
.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;
}
.setting-drawer-block-checbox-selectIcon {
position: absolute;
top: 0;
right: 0;
width: 100%;
height: 100%;
padding-top: 15px;
padding-left: 24px;
color: #00875A;
font-weight: 700;
font-size: 14px;
}
}
}
}
.drawer-container {
padding: 20px;
font-size: 14px;
line-height: 1.5;
word-wrap: break-word;
.drawer-title {
margin-bottom: 12px;
color: rgba(0, 0, 0, .85);
font-size: 14px;
line-height: 22px;
}
.drawer-item {
color: rgba(0, 0, 0, .65);
font-size: 14px;
padding: 12px 0;
}
.drawer-switch {
float: right
}
}
// 导航模式
.nav-wrap {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 10px;
margin-bottom: 20px;
.activeItem {
border: 2px solid #{'var(--theme)'} !important;
}
.item {
position: relative;
margin-right: 16px;
cursor: pointer;
width: 56px;
height: 48px;
border-radius: 4px;
background: #f0f2f5;
border: 2px solid transparent;
}
.left {
b:first-child {
display: block;
height: 30%;
background: #fff;
}
b:last-child {
width: 30%;
background: #1b2a47;
position: absolute;
height: 100%;
top: 0;
border-radius: 4px 0 0 4px;
}
}
.mix {
b:first-child {
border-radius: 4px 4px 0 0;
display: block;
height: 30%;
background: #1b2a47;
}
b:last-child {
width: 30%;
background: #1b2a47;
position: absolute;
height: 70%;
border-radius: 0 0 0 4px;
}
}
.top {
b:first-child {
display: block;
height: 30%;
background: #1b2a47;
border-radius: 4px 4px 0 0;
}
}
}
</style>
@@ -1,13 +1,13 @@
<template>
<div :class="['sidebar-theme-wrapper', settings.sideTheme]" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
<el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
<template>
<div :class="['sidebar-theme-wrapper', 'theme-dark']" :style="{ backgroundColor: variables.menuBackground }">
<el-scrollbar class="theme-dark" wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
:text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
:background-color="variables.menuBackground"
:text-color="variables.menuColor"
:unique-opened="true"
:active-text-color="settings.sideTheme === 'theme-dark' ? '#ffffff' : settings.theme"
:active-text-color="'#ffffff'"
:collapse-transition="false"
mode="vertical"
>
@@ -1,5 +1,5 @@
<template>
<div id="tags-view-container" class="tags-view-container" :class="{ 'tags-view-container--chrome': tagsViewStyle === 'chrome' }" :style="chromeVars">
<template>
<div id="tags-view-container" class="tags-view-container">
<!-- 左切换箭头 -->
<span class="tags-nav-btn tags-nav-btn--left" :class="{ disabled: !canScrollLeft }" @click="scrollLeft">
<i class="el-icon-arrow-left" />
@@ -11,7 +11,7 @@
v-for="tag in visitedViews"
ref="tag"
:key="tag.path"
:class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
:class="{ 'active': isActive(tag) }"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
@@ -19,7 +19,6 @@
@click.middle.native="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent.native="openMenu(tag, $event)"
>
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" style="margin-right: 3px;" />
{{ tag.title }}
<span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
@@ -91,26 +90,8 @@ export default {
routes() {
return this.$store.state.permission.routes
},
theme() {
return this.$store.state.settings.theme
},
tagsIcon() {
return this.$store.state.settings.tagsIcon
},
tagsViewStyle() {
return this.$store.state.settings.tagsViewStyle
},
selectedDropdownTag() {
return this.visitedViews.find(v => this.isActive(v)) || {}
},
chromeVars() {
if (this.tagsViewStyle !== 'chrome') return {}
const primary = this.theme || '#00875A'
return {
'--chrome-tab-active-bg': this.mixHexWithWhite(primary, 0.15),
'--chrome-tab-text-active': primary,
'--chrome-wing-r': '10px'
}
}
},
watch: {
@@ -148,24 +129,14 @@ export default {
this.toggleFullscreen()
}
},
mixHexWithWhite(hex, ratio) {
const clean = hex.replace('#', '')
const r = parseInt(clean.substring(0, 2), 16)
const g = parseInt(clean.substring(2, 4), 16)
const b = parseInt(clean.substring(4, 6), 16)
const mr = Math.round(r * ratio + 255 * (1 - ratio))
const mg = Math.round(g * ratio + 255 * (1 - ratio))
const mb = Math.round(b * ratio + 255 * (1 - ratio))
return `rgb(${mr}, ${mg}, ${mb})`
},
isActive(route) {
return route.path === this.$route.path
},
tagActiveStyle(tag) {
if (!this.isActive(tag) || this.tagsViewStyle !== 'card') return {}
if (!this.isActive(tag)) return {}
return {
"background-color": this.theme,
"border-color": this.theme
"background-color": "#00875A",
"border-color": "#00875A"
}
},
isAffix(tag) {
@@ -209,9 +180,6 @@ export default {
return tags
},
initTags() {
if (this.$store.state.settings.tagsViewPersist) {
this.$store.dispatch('tagsView/loadPersistedViews')
}
const affixTags = this.affixTags = this.filterAffixTags(this.routes)
for (const tag of affixTags) {
if (tag.name) {
@@ -1,193 +0,0 @@
<template>
<el-menu
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
>
<template v-for="(item, index) in topMenus">
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-submenu :style="{'--theme': theme}" index="more" :key="visibleNumber" v-if="topMenus.length > visibleNumber">
<template slot="title">更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item
:index="item.path"
:key="index"
v-if="index >= visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
</el-submenu>
</el-menu>
</template>
<script>
import { constantRoutes } from "@/router"
import { isHttp } from "@/utils/validate"
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile']
export default {
data() {
return {
// 顶部栏初始数
visibleNumber: 5,
// 当前激活菜单的 index
currentIndex: undefined
}
},
computed: {
theme() {
return this.$store.state.settings.theme
},
// 顶部显示菜单
topMenus() {
let topMenus = []
this.routers.map((menu) => {
if (menu.hidden !== true) {
// 兼容顶部栏一级菜单内部跳转
if (menu.path === '/' && menu.children) {
topMenus.push(menu.children[0])
} else {
topMenus.push(menu)
}
}
})
return topMenus
},
// 所有的路由信息
routers() {
return this.$store.state.permission.topbarRouters
},
// 设置子路由
childrenMenus() {
var childrenMenus = []
this.routers.map((router) => {
for (var item in router.children) {
if (router.children[item].parentPath === undefined) {
if (router.path === "/") {
router.children[item].path = "/" + router.children[item].path
} else {
if (!isHttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path
}
}
router.children[item].parentPath = router.path
}
childrenMenus.push(router.children[item])
}
})
return constantRoutes.concat(childrenMenus)
},
// 默认激活的菜单
activeMenu() {
const path = this.$route.path
let activePath = path
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length)
if (!this.$route.meta.link) {
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"))
this.$store.dispatch('app/toggleSideBarHide', false)
}
} else if (!this.$route.children) {
activePath = path
this.$store.dispatch('app/toggleSideBarHide', true)
}
this.activeRoutes(activePath)
return activePath
},
},
beforeMount() {
window.addEventListener('resize', this.setVisibleNumber)
},
beforeDestroy() {
window.removeEventListener('resize', this.setVisibleNumber)
},
mounted() {
this.setVisibleNumber()
},
methods: {
// 根据宽度计算设置显示栏数
setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3
this.visibleNumber = parseInt(width / 85)
},
// 菜单选择事件
handleSelect(key, keyPath) {
this.currentIndex = key
const route = this.routers.find(item => item.path === key)
if (isHttp(key)) {
// http(s):// 路径新窗口打开
window.open(key, "_blank")
} else if (!route || !route.children) {
// 没有子路由路径内部打开
const routeMenu = this.childrenMenus.find(item => item.path === key)
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query)
this.$router.push({ path: key, query: query })
} else {
this.$router.push({ path: key })
}
this.$store.dispatch('app/toggleSideBarHide', true)
} else {
// 显示左侧联动菜单
this.activeRoutes(key)
this.$store.dispatch('app/toggleSideBarHide', false)
}
},
// 当前激活的路由
activeRoutes(key) {
var routes = []
if (this.childrenMenus && this.childrenMenus.length > 0) {
this.childrenMenus.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item)
}
})
}
if (routes.length > 0) {
this.$store.commit("SET_SIDEBAR_ROUTERS", routes)
} else {
this.$store.dispatch('app/toggleSideBarHide', true)
}
}
}
}
</script>
<style lang="scss">
.topmenu-container.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #303133 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-submenu.is-active .el-submenu__title {
border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133;
}
/* submenu item */
.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #303133 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
</style>
+1 -2
View File
@@ -1,5 +1,4 @@
export { default as AppMain } from './AppMain'
export { default as AppMain } from './AppMain'
export { default as Navbar } from './Navbar'
export { default as Settings } from './Settings'
export { default as Sidebar } from './Sidebar/index.vue'
export { default as TagsView } from './TagsView/index.vue'
+7 -15
View File
@@ -1,5 +1,5 @@
<template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': theme, '--current-color-light': theme + '1a', '--current-color-dark-bg': theme + '33'}">
<template>
<div :class="classObj" class="app-wrapper" :style="{'--current-color': '#00875A', '--current-color-light': '#00875A1a', '--current-color-dark-bg': '#00875A33'}">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<!-- 顶部全宽 Header -->
@@ -15,23 +15,22 @@
<span class="sys-title">教学管理信息系统</span>
</div>
<semester-bar class="top-header__center" />
<navbar class="top-header__right" @setLayout="setLayout" :header-mode="true" />
<navbar class="top-header__right" :header-mode="true" />
</div>
<!-- 下方:侧边栏 + 主内容 -->
<div class="lower-container" :class="{sidebarHide:sidebar.hide}">
<sidebar v-if="!sidebar.hide" class="sidebar-container"/>
<div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
<tags-view v-if="needTagsView"/>
<div :class="{hasTagsView:true,sidebarHide:sidebar.hide}" class="main-container">
<tags-view/>
<app-main/>
<settings ref="settingRef"/>
</div>
</div>
</div>
</template>
<script>
import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
import { AppMain, Navbar, Sidebar, TagsView } from './components'
import SemesterBar from './components/SemesterBar'
import Hamburger from '@/components/Hamburger'
import ResizeMixin from './mixin/ResizeHandler'
@@ -43,7 +42,6 @@ export default {
components: {
AppMain,
Navbar,
Settings,
Sidebar,
TagsView,
SemesterBar,
@@ -52,11 +50,8 @@ export default {
mixins: [ResizeMixin],
computed: {
...mapState({
theme: state => state.settings.theme,
sideTheme: state => state.settings.sideTheme,
sidebar: state => state.app.sidebar,
device: state => state.app.device,
needTagsView: state => state.settings.tagsView
device: state => state.app.device
}),
classObj() {
return {
@@ -74,9 +69,6 @@ export default {
},
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
setLayout() {
this.$refs.settingRef.openSetting()
}
}
}
+1 -2
View File
@@ -1,4 +1,4 @@
import router from './router'
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress'
@@ -18,7 +18,6 @@ const isWhiteList = (path) => {
router.beforeEach((to, from, next) => {
NProgress.start()
if (getToken()) {
to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
const isLock = store.getters.isLock
/* has token*/
if (to.path === '/login') {
-66
View File
@@ -1,66 +0,0 @@
module.exports = {
/**
* 网页标题
*/
title: process.env.VUE_APP_TITLE,
/**
* 侧边栏主题 深色主题theme-dark,浅色主题theme-light
*/
sideTheme: 'theme-dark',
/**
* 系统布局配置
*/
showSettings: true,
/**
* 菜单导航模式 1、纯左侧 2、混合(左侧+顶部) 3、纯顶部
*/
navType: 1,
/**
* 是否显示 tagsView
*/
tagsView: true,
/**
* 持久化标签页
*/
tagsViewPersist: false,
/**
* 显示页签图标
*/
tagsIcon: false,
/**
* 标签页样式:card 卡片(默认)、chrome 谷歌浏览器风格
*/
tagsViewStyle: 'card',
/**
* 是否固定头部
*/
fixedHeader: true,
/**
* 是否显示logo
*/
sidebarLogo: true,
/**
* 是否显示动态标题
*/
dynamicTitle: false,
/**
* 是否显示底部版权
*/
footerVisible: false,
/**
* 底部版权文本内容
*/
footerContent: 'Copyright © 2018-2026 roomroot. All Rights Reserved.'
}
+2 -4
View File
@@ -1,4 +1,4 @@
import Vue from 'vue'
import Vue from 'vue'
import Vuex from 'vuex'
import app from './modules/app'
import lock from './modules/lock'
@@ -6,7 +6,6 @@ import dict from './modules/dict'
import user from './modules/user'
import tagsView from './modules/tagsView'
import permission from './modules/permission'
import settings from './modules/settings'
import getters from './getters'
Vue.use(Vuex)
@@ -18,8 +17,7 @@ const store = new Vuex.Store({
dict,
user,
tagsView,
permission,
settings
permission
},
getters
})
-52
View File
@@ -1,52 +0,0 @@
import defaultSettings from '@/settings'
import { useDynamicTitle } from '@/utils/dynamicTitle'
const { sideTheme, showSettings, navType, tagsView, tagsViewPersist, tagsIcon, tagsViewStyle, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
const state = {
title: '',
theme: storageSetting.theme || '#00875A',
sideTheme: storageSetting.sideTheme || sideTheme,
showSettings: showSettings,
navType: storageSetting.navType === undefined ? navType : storageSetting.navType,
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
tagsViewPersist: storageSetting.tagsViewPersist === undefined ? tagsViewPersist : storageSetting.tagsViewPersist,
tagsIcon: storageSetting.tagsIcon === undefined ? tagsIcon : storageSetting.tagsIcon,
tagsViewStyle: storageSetting.tagsViewStyle === undefined ? tagsViewStyle : storageSetting.tagsViewStyle,
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
footerVisible: storageSetting.footerVisible === undefined ? footerVisible : storageSetting.footerVisible,
footerContent: footerContent
}
const mutations = {
CHANGE_SETTING: (state, { key, value }) => {
if (state.hasOwnProperty(key)) {
state[key] = value
}
},
SET_TITLE: (state, title) => {
state.title = title
}
}
const actions = {
// 修改布局设置
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
},
// 设置网页标题
setTitle({ commit }, title) {
commit('SET_TITLE', title)
useDynamicTitle()
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
+2 -2
View File
@@ -1,10 +1,10 @@
import store from '@/store'
import cache from '@/plugins/cache'
const PERSIST_KEY = 'tags-view-visited'
// 布局已固定,不启用标签页持久化
function isPersistEnabled() {
return store.state.settings.tagsViewPersist
return false
}
function saveVisitedViews(views) {
-13
View File
@@ -1,13 +0,0 @@
import store from '@/store'
import defaultSettings from '@/settings'
/**
* 动态修改标题
*/
export function useDynamicTitle() {
if (store.state.settings.dynamicTitle) {
document.title = store.state.settings.title + ' - ' + defaultSettings.title
} else {
document.title = defaultSettings.title
}
}
+2 -3
View File
@@ -1,4 +1,4 @@
<template>
<template>
<div class="login">
<el-form ref="loginForm" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">{{title}}</h3>
@@ -50,14 +50,13 @@
<script>
import Cookies from "js-cookie"
import { encrypt, decrypt } from '@/utils/jsencrypt'
import defaultSettings from '@/settings'
export default {
name: "Login",
data() {
return {
title: process.env.VUE_APP_TITLE,
footerContent: defaultSettings.footerContent,
footerContent: "Copyright © 2018-2026 roomroot. All Rights Reserved.",
loginForm: {
username: "admin",
password: "admin123",
+2 -3
View File
@@ -1,4 +1,4 @@
<template>
<template>
<div class="register">
<el-form ref="registerForm" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">{{title}}</h3>
@@ -55,14 +55,13 @@
<script>
import { register } from "@/api/login"
import passwordRule from "@/utils/passwordRule"
import defaultSettings from '@/settings'
export default {
mixins: [passwordRule],
data() {
return {
title: process.env.VUE_APP_TITLE,
footerContent: defaultSettings.footerContent,
footerContent: "Copyright © 2018-2026 roomroot. All Rights Reserved.",
registerForm: {
username: "",
password: "",