这次带来的是最新开发的跨桌面中后台权限管理系统。使用最新的前端技术栈,内置 i18n 国际化解决方案,动态权限路由,权限验证,整合了典型的表格/表单等业务模块功能。通过Electron打包成跨平台的应用程序。

1289798-20210517100504897-1863225727.png

技术栈

  • 编码器: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

1289798-20210517101246176-1057605766.gif

主要特性

  1. 前端技术栈Vite2、Vue3、Electron12、Element Plus、Vue-i18n、Echarts5.x、Sortable、Mockjs。
  2. 权限认证支持组件式+指令式两种方式。
  3. 支持中文/英文/繁体国际化解决方案。
  4. 支持表格拖拽排序、缩放、树形表格等功能。
  5. 支持加载动态权限菜单,多方式轻松权限控制。
  6. 高效率开发,整个框架已经搭建完毕,只需新增相应模块即可。

项目结构图

1289798-20210517102145438-451952591.png

整个项目使用最新vue3语法编码,采用标准的分层目录结构形式,数据均是使用Mock.js进行模拟。

1289798-20210517102834588-200800909.png

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实现无边框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>

Vite2|electron项目布局模板

为了使得项目分层结构更加清晰,布局分为 Auth 和 Main 两大模块。
1289798-20210517111703810-1157807999.png

<!-- //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 国际化,支持中文|繁体|英文三种语言。

1289798-20210517112934999-710092814.png

目前vue-i18n插件支持vue3项目了,大家需安装最新版本即可。

npm i vue-i18n@next -D

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支持;