Electron31-Vue3Admin管理系统|vite5+electron+pinia桌面端后台Exe
自研electron31+vite5桌面端高颜值后台管理解决方案ElectronViteAdmin。
vite-electron31-admin原创基于electron31+vite5+vue3 setup+pinia2+element-plus+echarts搭建客户端轻量级后台管理系统。内置4种通用布局模板,支持i18n国际化、动态路由权限,整合了表格、表单、图表、列表、编辑器等业务场景。
技术栈
- 编辑器:VScode
- 框架技术:vite^5.3.4+vue^3.4.31+vue-router^4.4.0
- 跨端框架:electron^31.3.0
- UI组件库:element-plus^2.7.8
- 状态管理:pinia^2.2.0
- 国际化方案:vue-i18n@9
- 图表组件:echarts^5.5.1
- markdown编辑器:md-editor-v3^4.18.0
- 模拟数据:mockjs^1.1.0
- 打包工具:electron-builder^24.13.3
- electron+vite桥接插件:vite-plugin-electron^0.28.7
项目目录结构
vite-electron-admin桌面端后台系统使用 electron31+vite5 搭建项目模板,采用 vue3 setup 语法开发。
功能特性
- 最新前端技术栈Vite5.x、Vue3、Electron31、ElementPlus、Vue-I18n、Echarts
- 支持中英文/繁体国际化解决方案
- 支持动态权限路由、多页签缓存路由
- 封装多窗口管理器
- 内置4种通用布局模板、自由切换风格
- 整合通用的表格、表单、列表、图表、编辑器、错误处理等模块
- 高颜值UI界面、轻量级模块化、高定制性
目前 Electron31-Vue3Admin 通用后台系统已经发布到我的原创作品集。
Element Plus组件库
electron-viteadmin后台系统采用饿了么前端团队推出的vue3组件库。
Electron主线程配置
/** * electron主线程配置 * @author andy */ import { app, BrowserWindow } from 'electron' import { WindowManager } from '../src/windows/index.js' // 忽略安全警告提示 Electron Security Warning (Insecure Content-Security-Policy) process.env['ELECTRON_DISABLE_SECURITY_WARNINGS'] = true const createWindow = () => { let win = new WindowManager() win.create({isMajor: true}) // 系统托盘管理 win.trayManager() // 监听ipcMain事件 win.ipcManager() } app.whenReady().then(() => { createWindow() app.on('activate', () => { if(BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', () => { if(process.platform !== 'darwin') app.quit() })
main.js入口文件
import { createApp } from 'vue' import './style.scss' import App from './App.vue' import { launchApp } from '@/windows/actions' // 引入路由和状态配置 import Router from './router' import Pinia from './pinia' // 引入插件配置 import Plugins from './plugins' launchApp().then(config => { if(config) { console.log('窗口参数:', config) console.log('窗口id:', config?.id) // 全局存储窗口配置 window.config = config } // 初始化app应用实例 createApp(App) .use(Router) .use(Pinia) .use(Plugins) .mount('#app') })
Electron封装多窗口管理|自定义系统导航栏
如上图:登录窗口切换到主窗口。
<script setup> import { ref, markRaw } from 'vue' import { ElMessageBox } from 'element-plus' import { QuestionFilled, SwitchButton } from '@element-plus/icons-vue' import { isTrue } from '@/utils' import { authState } from '@/pinia/modules/auth' import { winSet } from '@/windows/actions' const authstate = authState() const props = defineProps({ color: String, // 窗口是否可最小化 minimizable: {type: [Boolean, String], default: true}, // 窗口是否可最大化 maximizable: {type: [Boolean, String], default: true}, // 窗口是否可关闭 closable: {type: [Boolean, String], default: true}, // 层级 zIndex: {type: [Number, String], default: 2024}, }) const hasMaximized = ref(false) // 初始监听窗口是否最大化 window.electron.invoke('win-isMaximized').then(res => { hasMaximized.value = res }) // 实时监听窗口是否最大化 window.electron.on('win-maximized', (e, data) => { hasMaximized.value = data }) // 最小化 const handleWinMin = () => { // winSet('minimize', window.config.id) window.electron.invoke('win-min') } // 最大化/还原 const handleWinToggle = () => { // winSet('max2min', window.config.id) window.electron.invoke('win-toggle').then(res => { hasMaximized.value = res }) } // 关闭 const handleWinClose = () => { if(window.config.isMajor) { ElMessageBox.confirm('是否最小化到系统托盘,不退出应用程序?', '', { type: 'warning', icon: markRaw(QuestionFilled), confirmButtonText: '退出应用', cancelButtonText: '最小化到托盘', customStyle: {'borderRadius': '8px'}, roundButton: true, distinguishCancelAndClose: true, }).then(() => { authstate.logout() winSet('close') }).catch((action) => { if(action === 'cancel') { setTimeout(() => { winSet('hide', window.config.id) }, 250) } }) }else { winSet('close', window.config.id) } } </script> <template> <div class="ev__winbtns vu__drag" :style="{'z-index': zIndex}"> <div class="ev__winbtns-actions vu__undrag" :style="{'color': color}"> <a v-if="isTrue(minimizable)" class="wbtn min" title="最小化" @click="handleWinMin"><i class="wicon iconfont elec-icon-min"></i></a> <a v-if="isTrue(maximizable)" class="wbtn toggle" :title="hasMaximized ? '向下还原' : '最大化'" @click="handleWinToggle"> <i class="wicon iconfont" :class="hasMaximized ? 'elec-icon-restore' : 'elec-icon-max'"></i> </a> <a v-if="isTrue(closable)" class="wbtn close" title="关闭" @click="handleWinClose"><i class="wicon iconfont elec-icon-quit"></i></a> </div> </div> </template>
Electron-Vue3Admin布局模板
如上图:内置了4种常用的通用布局模板。也可以根据需要定制化模板。
/** * 通用布局模板 * @author Andy Q:282310962 */ <script setup> import { appState } from '@/pinia/modules/app' // 引入布局模板 import Classic from './template/classic/index.vue' import Columns from './template/columns/index.vue' import Vertical from './template/vertical/index.vue' import Horizontal from './template/horizontal/index.vue' const appstate = appState() const LayoutMap = { 'classic': Classic, 'columns': Columns, 'vertical': Vertical, 'horizontal': Horizontal } </script> <template> <div class="vuadmin__container" :style="{'--themeSkin': appstate.config.skin}"> <component :is="LayoutMap[appstate.config.layout]" /> </div> </template>
electron+vue3-i18n国际化配置
electron-viteadmin系统采用 vue-i18n 国际化解决方案,支持中英文/繁体语言。
/** * 国际化配置 * @author YXY */ import { createI18n } from 'vue-i18n' import { appState } from '@/pinia/modules/app' // 引入语言配置 import enUS from './en-US' import zhCN from './zh-CN' import zhTW from './zh-TW' // 默认语言 export const langVal = 'zh-CN' export default async (app) => { const appstate = appState() const lang = appstate.lang || langVal appstate.setLang(lang) const i18n = createI18n({ legacy: false, locale: lang, messages: { 'en': enUS, 'zh-CN': zhCN, 'zh-TW': zhTW } }) app.use(i18n) }
Vue3动态化图表Echarts
vue3封装图表hooks,用于多个图表符合调用。采用element-resize-detector组件动态监听窗口DOM尺寸变化更新图表。
/** * 动态图表Hook */ import { onMounted, onBeforeUnmount, ref } from 'vue' import * as echarts from 'echarts' import elementResizeDetectorMaker from 'element-resize-detector' export function useEcharts(el, options) { let chartEl let chartRef = ref(null) let erd = elementResizeDetectorMaker() const resizeHandle = () => { chartEl && chartEl.resize() } onMounted(() => { if(el?.value) { chartEl = echarts.init(el.value) chartEl.setOption(options) chartRef.value = chartEl } erd.listenTo(el.value, resizeHandle) }) onBeforeUnmount(() => { chartEl.dispose() erd.removeListener(el.value, resizeHandle) }) return chartRef }
import { useEcharts } from '@/hooks/useEcharts' const userActionChartRef = ref(null) useEcharts(userActionChartRef, { // ... })
Electron+vue3自定义路由菜单
<Menus :rootRouteEnable="false" /> <Menus rootRouteEnable :dark="true" /> <Menus mode="horizontal" :dark="true" />
<script setup> import { ref, computed } from 'vue' import { isObject, isArray, isImg } from '@/utils' import { appState } from '@/pinia/modules/app' import { useRoutes } from '@/hooks/useRoutes' const props = defineProps({ // 菜单模式(vertical|horizontal) mode: { type: String, default: 'vertical' }, // 是否开启一级路由菜单 rootRouteEnable: { type: Boolean, default: true }, // 是否暗黑模式 dark: { type: Boolean } }) import Submenu from './submenu.vue' // 引入主路由表 import routes from '@/router/modules/main.js' const appstate = appState() const { route, getActiveRoute, getCurrentRootRoute, getTreeRoutes } = useRoutes() const activeRoute = computed(() => getActiveRoute(route)) const rootRoute = computed(() => getCurrentRootRoute(route)) const treeRoutes = computed(() => getTreeRoutes(routes)) const filterRoutes = computed(() => { if(props.rootRouteEnable) { return treeRoutes.value } // 过滤一级路由菜单 return treeRoutes.value.find(item => item.path === rootRoute.value && item.children)?.children }) </script> <template> <div class="vu__menubar" :class="{'is-dark': dark, 'is-collapsed': mode == 'vertical' && appstate.config.collapsed}"> <el-menu class="vu__menus" :default-active="activeRoute" :mode="mode" :collapse="appstate.config.collapsed"> <Submenu v-for="route in filterRoutes" :key="route.path" :item="route" :rootRoute="rootRoute" :rootRouteEnable="rootRouteEnable" /> </el-menu> </div> </template>
electron+vue3多页签tabview
element-plus组件库el-dropdown组件控制每次只显示一个右键下拉菜单。
<template> <div class="vu__tabview"> <el-tabs v-model="activeTab" class="vu__tabview-tabs" @tab-change="changeTabs" @tab-remove="removeTab" > <el-tab-pane v-for="(item, index) in tabList" :key="index" :name="item.path" :closable="!item?.meta?.isAffix" > <template #label> <el-dropdown ref="dropdownRef" trigger="contextmenu" :id="item.path" @visible-change="handleDropdownChange($event, item.path)" @command="handleDropdownCommand($event, item)"> <span class="vu__tabview-tabs__label"> <span>{{$t(item?.meta?.title)}}</span> </span> <template #dropdown> <el-dropdown-menu> <el-dropdown-item command="refresh" :icon="Refresh">{{$t('tabview__contextmenu-refresh')}}</el-dropdown-item> <el-dropdown-item command="close" :icon="Close" :disabled="item.meta.isAffix">{{$t('tabview__contextmenu-close')}}</el-dropdown-item> <el-dropdown-item command="closeOther" :icon="Switch">{{$t('tabview__contextmenu-closeother')}}</el-dropdown-item> <el-dropdown-item command="closeLeft" :icon="DArrowLeft">{{$t('tabview__contextmenu-closeleft')}}</el-dropdown-item> <el-dropdown-item command="closeRight" :icon="DArrowRight">{{$t('tabview__contextmenu-closeright')}}</el-dropdown-item> <el-dropdown-item command="closeAll" :icon="CircleCloseFilled">{{$t('tabview__contextmenu-closeall')}}</el-dropdown-item> </el-dropdown-menu> </template> </el-dropdown> </template> </el-tab-pane> </el-tabs> </div> </template>
<script setup> import { onMounted, ref, computed, watch, nextTick } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { Refresh, Close, Switch, DArrowLeft, DArrowRight, CircleCloseFilled } from '@element-plus/icons-vue' import { isObject, isImg } from '@/utils' import { useLink } from '@/hooks/useLink' import { appState } from '@/pinia/modules/app' const router = useRouter() const route = useRoute() const { jump } = useLink() const { locale } = useI18n() let { config: { keepAlive, tabRoutes, cacheRoutes }, updateConfig } = appState() const dropdownRef = ref() const activeTab = ref(route.path) const tabList = ref(tabRoutes) // 新增选项卡 const addTab = () => { const index = tabList.value.findIndex(item => item?.path === activeTab.value) if(index == -1) { tabList.value.push({ path: route?.path, name: route?.name, meta: { ...route?.meta, } }) } updateConfig('tabRoutes', tabList.value) updateCacheRoutes() } // 删除选项卡 const removeTab = (path) => { const index = tabList.value.findIndex(item => item?.path === path) if(index > -1) { tabList.value.splice(index, 1) updateTabs(tabList.value) } } // 删除左侧选项卡 const removeLeftTab = (path) => { const index = tabList.value.findIndex(item => item?.path === path) if(index > -1) { tabList.value = tabList.value.filter((item, i) => item?.meta?.isAffix || i >= index) updateTabs(tabList.value) } } // 删除右侧选项卡 const removeRightTab = (path) => { const index = tabList.value.findIndex(item => item?.path === path) if(index > -1) { tabList.value = tabList.value.filter((item, i) => item?.meta?.isAffix || i <= index) updateTabs(tabList.value) } } // 删除其它选项卡 const removeOtherTab = (path) => { tabList.value = tabList.value.filter(item => item?.meta?.isAffix || item?.path === path) updateTabs(tabList.value) } // 删除全部 const removeAllTab = (path) => { tabList.value = tabList.value.filter(item => item?.meta?.isAffix) updateTabs(tabList.value) } // 更新选项卡 const updateTabs = (tabs) => { updateConfig('tabRoutes', tabs) updateCacheRoutes() const nextTab = tabs[tabs.length + 1] || tabs[tabs.length - 1] if(!nextTab) return jump(nextTab?.path) } // 更新keep-alive缓存 const updateCacheRoutes = () => { let caches = tabList.value.filter(item => keepAlive || item?.meta?.isKeepAlive).map(item => item.name) updateConfig('cacheRoutes', caches) } // 清空keep-alive缓存 const clearCacheRoutes = () => { updateConfig('cacheRoutes', []) } // 点击选项卡 const changeTabs = (path) => { jump(path) } // 右键菜单更新 const handleDropdownChange = (visible, name) => { // 控制每次只显示一个右键菜单 if(!visible) return dropdownRef.value.forEach(item => { if(item.id === name) return item.handleClose() }) } // 右键菜单命令 const handleDropdownCommand = (cmd, item) => { const path = item?.path switch(cmd) { case 'refresh': router.go(0) break case 'close': removeTab(path) break case 'closeLeft': removeLeftTab(path) break case 'closeRight': removeRightTab(path) break case 'closeOther': removeOtherTab(path) break case 'closeAll': removeAllTab() break } } watch(() => route.path, () => { activeTab.value = route.path addTab() }, { immediate: true }) </script>
综上就是electron31+vue3+element-plus实战桌面端轻量级后台系统的一些知识分享,希望对大家有所帮助!
最后附上两个最新原创项目实例
https://www.cnblogs.com/xiaoyan2017/p/18290962
https://www.cnblogs.com/xiaoyan2017/p/18323930
本文为博主原创文章,未经博主允许不得转载,欢迎大家一起交流 QQ(282310962) wx(xy190310)