feat(system): 新增系统配置页面和登录页切换,快捷菜单抽屉功能

- 新增系统配置页面,可设置系统名称、备案号、登录页类型等
- 实现登录页类型切换,支持多种登录页面样式- 添加主题类和登录页类型的选择功能
- 优化导航栏,增加抽屉菜单按钮
- 调整布局样式,支持新的主题类,登录页选择
This commit is contained in:
fhysy 2025-07-03 15:43:04 +08:00
parent 6d3e42d9c1
commit ab709310c4
9 changed files with 445 additions and 17 deletions

View File

@ -0,0 +1,143 @@
<template>
<el-drawer
:visible.sync="visible"
direction="btt"
size="95%"
custom-class="drawer-menu"
:with-header="false"
@close="$emit('update:visible', false)"
>
<div class="drawer-menu-content">
<div class="drawer-menu-title">快捷菜单</div>
<div v-for="group in filteredMenuGroups" :key="group.path" class="drawer-menu-group">
<div class="drawer-menu-group-title">{{ group.meta && group.meta.title }}</div>
<div class="drawer-menu-grid">
<div
v-for="item in group.children"
:key="item.path"
class="drawer-menu-item"
@click="handleMenuClick(item, group.path)"
>
<div class="drawer-menu-icon">
<svg-icon :icon-class="item.meta && item.meta.icon"/>
<!-- <i v-if="item.meta && item.meta.icon" :class="['iconfont', item.meta.icon]" /> -->
</div>
<div class="drawer-menu-label">{{ item.meta && item.meta.title }}</div>
</div>
</div>
</div>
</div>
</el-drawer>
</template>
<script>
import { mapGetters } from 'vuex';
function isExternal(path) {
return /^(https?:|mailto:|tel:)/.test(path)
}
export default {
name: 'DrawerMenu',
props: {
visible: {
type: Boolean,
default: false
}
},
computed: {
...mapGetters(['sidebarRouters']),
filteredMenuGroups() {
// hidden
function filterMenu(list) {
return (list || []).filter(item => !item.hidden).map(item => {
const newItem = { ...item };
if (item.children) {
newItem.children = filterMenu(item.children);
}
return newItem;
});
}
//
return filterMenu(this.sidebarRouters).filter(group => group.children && group.children.length > 0);
}
},
methods: {
handleMenuClick(item, parentPath = '') {
// path
let fullPath = '';
if (item.path) {
if (item.path.startsWith('http') || item.path.startsWith('https') || item.path.startsWith('mailto') || item.path.startsWith('tel')) {
fullPath = item.path;
} else {
if (item.path.startsWith('/')) {
fullPath = item.path;
} else {
fullPath = parentPath ? (parentPath.endsWith('/') ? parentPath + item.path : parentPath + '/' + item.path) : item.path;
}
}
}
if (!fullPath) return;
if (isExternal(fullPath)) {
window.open(fullPath, '_blank');
} else {
this.$router.push(fullPath);
}
this.$emit('update:visible', false);
}
}
}
</script>
<style scoped>
.drawer-menu {
border-radius: 16px 16px 0 0;
background: #19294a;
min-height: 300px;
}
.drawer-menu-content {
padding: 24px 16px 32px 16px;
}
.drawer-menu-title {
color: #000;
font-size: 22px;
line-height: 26px;
font-weight: bold;
margin-bottom: 10px;
}
.drawer-menu-group-title {
color: #000;
font-size: 18px;
line-height: 26px;
font-weight: bold;
margin-bottom: 10px;
}
.drawer-menu-grid {
display: flex;
flex-wrap: wrap;
gap: 10px 10px;
}
.drawer-menu-item {
width: 100px;
text-align: center;
cursor: pointer;
margin-bottom: 10px;
}
.drawer-menu-icon {
width: 56px;
height: 56px;
margin: 0 auto 5px auto;
border-radius: 10px;
background: #00A9AB;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: #fff;
}
.drawer-menu-label {
color: #000;
font-size: 14px;
line-height: 18px;
}
</style>

View File

@ -1,8 +1,15 @@
<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<!-- <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" /> -->
<!-- <i class="el-icon-menu" @click="drawerMenuVisible = true">
<i class="el-icon-menu" />
</i> -->
<el-tooltip class="item" effect="dark" content="快捷菜单" placement="bottom">
<i class="el-icon-menu drawer-menu-btn" @click="drawerMenuVisible = true"></i>
</el-tooltip>
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
<div class="right-menu">
<template v-if="device!=='mobile'">
@ -41,6 +48,7 @@
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<drawer-menu :visible.sync="drawerMenuVisible" />
</div>
</div>
</template>
@ -54,9 +62,15 @@ import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import RuoYiGit from '@/components/SmartPower/Git'
import RuoYiDoc from '@/components/SmartPower/Doc'
import DrawerMenu from '@/components/DrawerMenu'
import { getIotFileUrl } from "@/utils/hciot"
export default {
data() {
return {
drawerMenuVisible: false
}
},
components: {
Breadcrumb,
Hamburger,
@ -64,7 +78,8 @@ export default {
SizeSelect,
Search,
RuoYiGit,
RuoYiDoc
RuoYiDoc,
DrawerMenu
},
computed: {
...mapGetters([
@ -127,6 +142,20 @@ export default {
background: rgba(0, 0, 0, .025)
}
}
.drawer-menu-btn{
padding: 0 10px;
font-size: 20px;
line-height: 50px;
height: 100%;
float: left;
cursor: pointer;
transition: background .3s;
-webkit-tap-highlight-color:transparent;
&:hover {
background: rgba(0, 0, 0, .025)
}
}
.breadcrumb-container {
float: left;

View File

@ -5,7 +5,7 @@
<div class="setting-drawer-title">
<h3 class="drawer-title">主题风格设置</h3>
</div>
<div class="setting-drawer-block-checbox">
<!-- <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;">
@ -18,7 +18,7 @@
</i>
</div>
</div>
<!-- <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
<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">
@ -29,7 +29,7 @@
</svg>
</i>
</div>
</div> -->
</div>
<div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-N1')">
<img src="@/assets/images/light.svg" alt="N1">
@ -43,12 +43,36 @@
</i>
</div>
</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 class="drawer-item">
<span>主题类</span>
<el-select v-model="themeClass" size="mini" placeholder="请选择主题类" style="float: right; width: 160px" popper-class="settings-select-popper" @change="handleThemeClassChange">
<el-option
v-for="item in themeClassOptions"
:key="item.dictCode"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</div>
<div class="drawer-item">
<span>登录页</span>
<el-select v-model="loginPageType" size="mini" placeholder="请选择登录页" style="float: right; width: 160px" popper-class="settings-select-popper" @change="handleLoginPageTypeChange">
<el-option
v-for="item in loginPageOptions"
:key="item.dictCode"
:label="item.dictLabel"
:value="item.dictValue"
/>
</el-select>
</div>
</div>
<el-divider/>
@ -76,11 +100,16 @@
<script>
import ThemePicker from '@/components/ThemePicker'
import { getDicts } from "@/api/system/dict/data";
export default {
components: { ThemePicker },
data() {
return {}
return {
themeClassOptions: [],
loginPageOptions: [],
loginPageType: ''
}
},
computed: {
theme() {
@ -122,6 +151,30 @@ export default {
})
}
},
themeClass: {
get() {
return this.$store.state.settings.themeClass
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'themeClass',
value: val
})
}
},
},
created() {
//
getDicts('sys_skin_name').then((response) => {
this.themeClassOptions = response.data || [];
});
//
getDicts('sys_login_page').then((response) => {
this.loginPageOptions = response.data || [];
// loginPageType
const cache = localStorage.getItem('loginPageType');
this.loginPageType = cache || (process.env.VUE_APP_LOGIN_PAGE || '');
});
},
methods: {
themeChange(val) {
@ -135,6 +188,15 @@ export default {
key: 'sideTheme',
value: val
})
},
handleThemeClassChange(val) {
// localStorage
localStorage.setItem('themeClass', val);
// v-modelVuex
},
handleLoginPageTypeChange(val) {
// localStorage
localStorage.setItem('loginPageType', val);
}
}
}
@ -198,6 +260,9 @@ export default {
}
.drawer-item {
display: flex;
align-items: center;
justify-content: space-between;
color: rgba(0, 0, 0, .65);
font-size: 14px;
padding: 12px 0;
@ -208,3 +273,8 @@ export default {
}
}
</style>
<style>
.settings-select-popper {
z-index: 41000 !important;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div :class="[classObj,themeClass1]" :style="{'--current-color': theme}" class="app-wrapper">
<div :class="[classObj,themeClass]" :style="{'--current-color': theme}" class="app-wrapper">
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
<sidebar class="sidebar-container" />
<div :class="{hasTagsView:needTagsView}" class="main-container">
@ -36,6 +36,7 @@ export default {
computed: {
...mapState({
theme: state => state.settings.theme,
themeClass: state => state.settings.themeClass,
sideTheme: state => state.settings.sideTheme,
sidebar: state => state.app.sidebar,
device: state => state.app.device,

View File

@ -5,8 +5,8 @@ import Cookies from 'js-cookie'
import Element from 'element-ui'
import './assets/styles/element-variables.scss'
import '@/assets/styles/index.scss'
import '@/assets/styles/ruoyi.scss'
import '@/assets/styles/index.scss'
import App from './App'
import store from './store'
import router from './router'

View File

@ -32,11 +32,11 @@ import ParentView from '@/components/ParentView';
}
*/
// 根据环境变量动态导入组件
function dynamicImportLoginPage(resolve) {
const loginPagePath = process.env.VUE_APP_LOGIN_PAGE;
require([`@/views/${loginPagePath}`], resolve);
}
// // 根据环境变量动态导入组件
// function dynamicImportLoginPage(resolve) {
// const loginPagePath = process.env.VUE_APP_LOGIN_PAGE;
// require([`@/views/${loginPagePath}`], resolve);
// }
// 公共路由
export const constantRoutes = [
@ -53,7 +53,8 @@ export const constantRoutes = [
},
{
path: '/login',
component: (resolve) => dynamicImportLoginPage(resolve),
// component: (resolve) => dynamicImportLoginPage(resolve),
component: (resolve) => require(['@/views/LoginWrapper'], resolve),
hidden: true
},
{
@ -76,6 +77,12 @@ export const constantRoutes = [
component: (resolve) => require(['@/views/error/401'], resolve),
hidden: true
},
{
path: '/bigScreen',
component: () => import('@/views/bigScreenIframe/index'),
hidden: true,
meta: { bigScreen: true }
},
{
path: '',
component: Layout,
@ -92,6 +99,17 @@ export const constantRoutes = [
showNav: false
}
},
// {
// path: '/index',
// component: (resolve) => require(['@/views/index'], resolve),
// name: 'BigScreen',
// meta: {
// title: '监控大屏', icon: 'dashboard', noCache: true, affix: true
// },
// params: {
// showNav: false
// }
// },
]
},
{

View File

@ -3,10 +3,12 @@ import defaultSettings from '@/settings'
const { theme, themeClass, sideTheme, showSettings, tagsView, fixedHeader, sidebarLogo } = defaultSettings
const storageThemeClass = localStorage.getItem('themeClass') || ''
const state = {
theme: theme,
sideTheme: sideTheme,
themeClass: themeClass,
themeClass: storageThemeClass || themeClass,
showSettings: showSettings,
tagsView: tagsView,
fixedHeader: fixedHeader,

View File

@ -0,0 +1,37 @@
<template>
<component v-if="currentLoginComponent" :is="currentLoginComponent" />
</template>
<script>
export default {
name: 'LoginWrapper',
data() {
return {
currentLoginComponent: null
}
},
async created() {
//
let type = localStorage.getItem('loginPageType')
if (!type) {
//
type = process.env.VUE_APP_LOGIN_PAGE || 'login-green'
}
// import
if (type === 'login-blue-black') {
this.currentLoginComponent = (await import('@/views/login-blue-black')).default
} else if (type === 'login-blue-white') {
this.currentLoginComponent = (await import('@/views/login-blue-white')).default
} else if (type === 'login-green') {
this.currentLoginComponent = (await import('@/views/login-green')).default
} else {
// import
try {
this.currentLoginComponent = (await import(`@/views/${type}`)).default
} catch (e) {
this.currentLoginComponent = (await import('@/views/login-green')).default
}
}
}
}
</script>

View File

@ -0,0 +1,128 @@
<template>
<div class="app-container">
<div class="main-content-card">
<el-form :model="form" :rules="rules" ref="form" label-width="160px" label-position="top">
<el-form-item label="系统名称" prop="systemName">
<el-input v-model="form.systemName" placeholder="请输入系统名称" />
</el-form-item>
<el-form-item label="备案号" prop="icp">
<el-input v-model="form.icp" placeholder="请输入备案号" />
</el-form-item>
<el-form-item label="登录页" prop="loginPageType">
<el-select v-model="form.loginPageType" placeholder="请选择登录页">
<el-option v-for="item in loginPageOptions" :key="item.dictCode" :label="item.dictLabel" :value="item.dictValue" />
</el-select>
</el-form-item>
<el-form-item label="主题配色" prop="themeClass">
<el-select v-model="form.themeClass" placeholder="请选择主题配色">
<el-option v-for="item in themeClassOptions" :key="item.dictCode" :label="item.dictLabel" :value="item.dictValue" />
</el-select>
</el-form-item>
<el-form-item label="大屏地址" prop="bigScreenUrl">
<el-input v-model="form.bigScreenUrl" placeholder="请输入大屏地址" />
</el-form-item>
<el-form-item label="登录页logo(200x200px)" prop="loginLogo">
<ImageUpload v-model="form.loginLogo" :limit="1" tip="建议尺寸200x200px" />
</el-form-item>
<el-form-item label="登录页背景图(1920x1080)" prop="loginBg">
<ImageUpload v-model="form.loginBg" :limit="1" tip="建议尺寸1920x1080px" />
</el-form-item>
<el-form-item label="ico图标(16x16px)" prop="ico">
<ImageUpload v-model="form.ico" :limit="1" tip="建议尺寸16x16px格式.ico/.png" />
</el-form-item>
<el-form-item label="菜单页logo(168x168px)" prop="menuLogo">
<ImageUpload v-model="form.menuLogo" :limit="1" tip="建议尺寸168x168px" />
</el-form-item>
<el-form-item label="公众号二维码(258x258px)" prop="wechatQrcode">
<ImageUpload v-model="form.wechatQrcode" :limit="1" tip="建议尺寸258x258px" />
</el-form-item>
<el-form-item label="小程序二维码(258x258px)" prop="miniQrcode">
<ImageUpload v-model="form.miniQrcode" :limit="1" tip="建议尺寸258x258px" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">保存</el-button>
<el-button @click="onReset">重置</el-button>
</el-form-item>
</el-form>
</div>
</div>
</template>
<script>
import { getDicts } from "@/api/system/dict/data";
import ImageUpload from '@/components/ImageUpload'
// API getConfigInfo, saveConfigInfo
import { getConfigInfo, saveConfigInfo } from '@/api/system/config'
export default {
name: 'ConfigPage',
components: { ImageUpload },
data() {
return {
form: {
systemName: '',
icp: '',
loginPageType: '',
themeClass: '',
bigScreenUrl: '',
loginLogo: '',
loginBg: '',
ico: '',
menuLogo: '',
wechatQrcode: '',
miniQrcode: ''
},
rules: {
systemName: [{ required: true, message: '请输入系统名称', trigger: 'blur' }],
loginPageType: [{ required: true, message: '请选择登录页', trigger: 'change' }],
themeClass: [{ required: true, message: '请选择主题配色', trigger: 'change' }]
},
loginPageOptions: [],
themeClassOptions: []
}
},
created() {
//
getDicts('sys_login_page').then(res => {
this.loginPageOptions = res.data || [];
});
getDicts('sys_skin_name').then(res => {
this.themeClassOptions = res.data || [];
});
//
getConfigInfo().then(res => {
if (res.code === 200 && res.data) {
this.form = Object.assign(this.form, res.data)
}
})
},
methods: {
onSubmit() {
this.$refs.form.validate(valid => {
if (!valid) return;
saveConfigInfo(this.form).then(res => {
if (res.code === 200) {
this.$message.success('保存成功')
} else {
this.$message.error(res.msg || '保存失败')
}
})
})
},
onReset() {
this.$refs.form.resetFields();
//
getConfigInfo().then(res => {
if (res.code === 200 && res.data) {
this.form = Object.assign(this.form, res.data)
}
})
}
}
}
</script>
<style scoped>
.system-config-page {
}
</style>