Electron-Vue3-Vadmin后台系统|vite2+electron桌面端权限管理系统
基于vite2.x+electron12桌面端后台管理系统Vite2ElectronVAdmin。
继上一次分享vite2整合electron搭建后台框架,这次带来的是最新开发的跨桌面中后台权限管理系统。使用最新的前端技术栈,内置 i18n 国际化解决方案,动态权限路由,权限验证,整合了典型的表格/表单等业务模块功能。
一、技术栈
- 编码器:vscode
- vue3技术:vite2.1.5+vue3.0+vuex4+vue-router@4
- 跨端框架:electron^12.0.4
- 打包工具:vue-cli-plugin-electron-builder
- UI组件库:element-plus^1.0.2 (饿了么vue3组件库)
- 表格拖拽:sortablejs^1.13.0
- 图表组件:echarts^5.1.1
- 国际化方案:vue-i18n^9.1.6
- 数据模拟:mockjs^1.1.0
二、主要特性
- 前端技术栈Vite2、Vue3、Electron12、Element Plus、Vue-i18n、Echarts5.x、Sortable、Mockjs。
- 权限认证支持组件式+指令式两种方式。
- 支持中文/英文/繁体国际化解决方案。
- 支持表格拖拽排序、缩放、树形表格等功能。
- 支持加载动态权限菜单,多方式轻松权限控制。
- 高效率开发,整个框架已经搭建完毕,只需新增相应模块即可。
三、项目结构图
整个项目使用最新vue3语法编码,采用标准的分层目录结构形式,数据均是使用Mock.js进行模拟。
◆ electron支持多开新窗口
项目支持打开多个窗口,如主题换肤、关于等窗口。只需通过如下的方式调用即可。
import { winCfg, createWin } from '@/windows/actions' // 换肤窗口 const handleOpenTheme = () => { createWin({ title: '个性装扮', route: '/skin', width: 750, height: 480, modal: true, parent: winCfg.window.id, resize: false, }) }
大家如果对electron创建多窗口模式感兴趣的话,可以去看看下面这篇文章。
https://www.cnblogs.com/xiaoyan2017/p/14403820.html
◆ electron实现无边框Mac导航栏效果
如上图:顶部导航栏默认是Mac风格,也支持自定义标题、背景/文字颜色、是否沉浸式透明背景等功能。
设置 -webkit-app-region: drag 实现导航条可拖拽,标题及按钮 -webkit-app-region:no-drag 可响应点击事件。
<!-- //顶部导航 --> <template> <WinBar zIndex="1000"> <template #wbtn> <MsgMenu /> <Lang /> <a class="wbtn" title="换肤" @click="handleSkinWin"><i class="iconfont icon-huanfu"></i></a> <Setting /> <a class="wbtn" title="刷新" @click="handleRefresh"><i class="iconfont el-icon-refresh"></i></a> <a class="wbtn" :class="{'on': isAlwaysOnTop}" :title="isAlwaysOnTop ? '取消置顶' : '置顶'" @click="handleAlwaysTop"><i class="iconfont icon-ding"></i></a> <Avatar @logout="handleLogout" /> </template> </WinBar> </template>
对于自定义导航条的实现方式,由于之前有过相关分享文章,这里就不详细介绍了。
https://www.cnblogs.com/xiaoyan2017/p/14449570.html
◆ Vite2|electron项目布局模板
为了使得项目分层结构更加清晰,布局分为 Auth 和 Main 两大模块。
<!-- //Auth主模块模板 --> <template> <div class="vadmin__wrapper"> <router-view class="vadmin__layouts-auth"></router-view> </div> </template> <script> import { useRoute } from "vue-router" import useTitle from '@/hooks/useTitle' export default { components: {}, setup() { const route = useRoute() // 设置标题 useTitle(route) } } </script>
<!-- //Main主模块模板 --> <template> <div class="vadmin__wrapper" :style="{'--themeSkin': store.state.skin}"> <div v-if="!route.meta.isNewin" class="vadmin__layouts-main flexbox flex-col"> <!-- //顶部导航 --> <div class="layout__topbar"> <TopNav /> </div> <div class="layout__workpanel flex1 flexbox"> <!-- //侧边栏 --> <div v-show="rootRouteEnable" class="panel__leftlayer"> <SideMenu :routes="mainRoutes" :rootRoute="rootRoute" /> </div> <!-- //中间栏 --> <div class="panel__middlelayer" :class="{'collapsed': collapsed}"> <RouteMenu :routes="getAllRoutes" :rootRoute="rootRoute" :defaultActive="defaultActive" :rootRouteEnable="rootRouteEnable" /> </div> <!-- //右边栏 --> <div class="panel__rightlayer flex1 flexbox flex-col"> <!-- 面包屑导航 --> <BreadCrumb /> <!-- 主内容区 --> <v3-scroll autohide> <div class="lay__container"> <!-- //路由权限控制 --> <permission :roles="route.meta.roles"> <template #tooltips> <Forbidden /> </template> <router-view></router-view> </permission> </div> </v3-scroll> </div> </div> </div> <router-view v-else class="vadmin__layouts-main flexbox flex-col"></router-view> </div> </template>
◆ Vue-Router路由配置
/** * 路由配置 Router util * @author XiaoYan */ import { createRouter, createWebHashHistory } from "vue-router" import { ElLoading } from "element-plus" import { loginWin } from "@/windows/actions" import store from '@/store' // 导入公共模板/路由配置 import mainLayout from "@/layouts/main" import authLayout from "@/layouts/auth" import mainRoutes from "@/layouts/main/routes.js" import authRoutes from "@/layouts/auth/routes.js" const RoutesLs = [ // 主页面模块 { path: '/', redirect: '/home/index', component: mainLayout, children: mainRoutes, }, // 验证模块 { path: '/auth', redirect: '/auth/login', component: authLayout, children: authRoutes, }, // 错误模块 { path: '/:pathMatch(.*)*', component: () => import('@/views/error/404.vue'), meta: { title: 'app__global-page-notfound', } } ] const router = createRouter({ history: createWebHashHistory(), routes: RoutesLs, }) // 全局钩子拦截验证状态 let loadingIns router.beforeEach((to, from, next) => { // 开启加载提示 loadingIns = ElLoading.service({ lock: true, text: 'Loading...', spinner: 'el-icon-loading', background: 'rgba(19, 209, 122, .1)' }) // 判断当前路由是否需要验证状态 const isLogined = store.state.isLogin if(to.meta.auth) { if(isLogined) { next() }else { loginWin() loadingIns.close() } }else { next() } }) router.afterEach(() => { // 关闭加载提示 loadingIns.close() })
◆ Vue-I18n国际化解决方案
项目中路由采用了 vue-i18n 国际化,支持中文|繁体|英文三种语言。
目前vue-i18n插件支持vue3项目了,大家需安装最新版本即可。
npm i vue-i18n@next -D
如上图:新建locale目录用来处理相应模块语言配置。
import { createI18n } from "vue-i18n" import Storage from "@/utils/storage" // 默认值 export const langKey = 'lang' export const langVal = 'zh-CN' /* elementPlus国际化配置 */ import enUS from "element-plus/lib/locale/lang/en" import zhCN from "element-plus/lib/locale/lang/zh-cn" import zhTW from "element-plus/lib/locale/lang/zh-tw" export const elPlusLang = { 'en-US': enUS, 'zh-CN': zhCN, 'zh-TW': zhTW, } /* 初始化多语言 */ export const $messages = importAllLang() export const $lang = getLang() const i18n = createI18n({ legacy: false, locale: $lang, messages: $messages })
◆ 动态化图表Hooks
项目中图表是使用最新的Echarts组件。为了避免每次都使用echarts.init调用图表接口。于是就封装了调用图表hook函数。
一开始是使用监听window.resize来自适应图表尺寸,这里有一个bug,只有窗口大小改变才会触发,而DOM改变则不会触发了,于是改用 element-resize-detector 来监听,完美解决问题。
/** * 动态化图表Hook * @author XiaoYan */ import { onMounted, onBeforeUnmount, ref } from "vue" import * as echarts from "echarts" import elementResizeDetectorMaker from "element-resize-detector" import utils from "@/utils" export default function useChart(refs, options) { let chartInst let chartRef = ref(null) let erd = elementResizeDetectorMaker() const handleResize = utils.debounce(() => { chartInst.resize() }, 100) onMounted(() => { if(refs.value) { chartInst = echarts.init(refs.value) chartInst.setOption(options) chartRef.value = chartInst } // window.addEventListener('resize', handleResize) erd.listenTo(refs.value, handleResize) }) onBeforeUnmount(() => { chartInst.dispose() // window.removeEventListener('resize', handleResize) erd.removeListener(refs.value, handleResize) }) return chartRef }
◆ 路由权限管理
项目中的权限尺寸组件式和指令式两种模式。
<!-- //权限验证模板 --> <template> <slot v-if="isPermission"/> <slot v-else name="tooltips"> <el-alert title="对不起,您没有权限操作此页面!" type="error" show-icon></el-alert> </slot> </template> <script> import { computed } from "vue"; import { useStore } from "vuex"; import { getPermissionRoute } from "@/utils/routes"; export default { props: { roles: { type: [String, Array] } }, components: {}, setup(props) { const store = useStore(); // 判断是否有权限 const isPermission = computed(() => getPermissionRoute(JSON.stringify(store.state.roles), props.roles) ); return { isPermission }; } }; </script>
import store from "@/store" import { getPermissionRoute } from "@/utils/routes" const Permission = (el, binding) => { const { value } = binding if(value) { const userRoles = JSON.stringify(store.state.roles) if(!getPermissionRoute(userRoles, value)) { el.parentNode && el.parentNode.removeChild(el) } }else { console.error(`Set Roles! Like v-permission="['admin', 'dev']" or v-permission="'test'"`) } } export default Permission
组件调用
<Permission roles="test"> <template #tooltips> <h2 style="color:red;">此模块只有test角色才能操作!</h2> </template> <el-button type="primary" icon="el-icon-search">查询</el-button> </Permission> <el-divider /> <Permission roles="dev"> <template #tooltips> <h2 style="color:red;">你无权操作Dev模块!</h2> </template> <el-button type="primary" icon="el-icon-edit">编辑</el-button> <el-button type="warning" icon="el-icon-delete">删除</el-button> </Permission>
指令调用
<el-button v-permission="'test'" type="primary" icon="el-icon-search">查询</el-button> <el-button v-permission="'dev'" type="success" icon="el-icon-plus">新增</el-button> <el-button v-permission="['test', 'dev']" type="warning" icon="el-icon-edit">编辑</el-button> <el-button v-permission="['admin']" type="danger" icon="el-icon-delete">删除</el-button>
◆ electron-builder打包配置
{ "productName": "electron-vadmin", "appId": "cc.xiaoyan.electron-vadmin", "copyright": "Copyright © 2021-present XiaoYan", "compression": "maximum", "asar": false, "extraResources": [{ "from": "./resource","to": "resource" }], "nsis": { "oneClick": false, "allowToChangeInstallationDirectory": true, "perMachine": true, "deleteAppDataOnUninstall": true, "createDesktopShortcut": true, "createStartMenuShortcut": true, "shortcutName": "ElectronVAdmin" }, "win": { "icon": "./resource/shortcut.ico", "artifactName": "${productName}-v${version}-${platform}-${arch}-setup.${ext}", "target": [{ "target": "nsis","arch": ["ia32"] }] }, "mac": { "icon": "./resource/shortcut.icns","artifactName": "${productName}-v${version}-${platform}-${arch}-setup.${ext}" }, "linux": { "icon": "./resource","artifactName": "${productName}-v${version}-${platform}-${arch}-setup.${ext}" } }
最后还需注意
1、项目路径命名不能包含中文,否则打包会报错!
2、尽量不要使用 getCurrentInstance 函数来使用router或store,打包也会报错!
3、打包后运行出现白屏情况,可配置 history: createWebHashHistory()
4、提示fs.existsSync错误,设置nodeIntegration: true开启Node支持;
好了,基于vite2+electron开发后台管理系统就分享到这里,希望对大家有些帮助~
最后附上一个最新原创研发electron+vue3桌面端后台管理解决方案
Electron31-Vue3Admin管理系统|vite5+electron+pinia桌面端后台Exe