Tauri-Admin通用后台管理系统|tauri+vue3+pinia桌面端后台EXE
基于tauri+vite4+pinia2跨端后台管理系统应用实例TauriAdmin。
tauri-admin 基于最新跨端技术 Tauri Rust webview2 整合 Vite4 构建桌面端通用后台管理解决方案。搭载轻量级ve-plus组件库、支持多窗口切换管理、vue-i18n多语言包、动态路由权限、常用业务功能模块、3种布局模板及动态路由缓存等功能。
使用技术
- 编码工具:vscode
- 框架技术:tauri+vite^4.2.1+vue^3.2.45+pinia+vue-router
- UI组件库:ve-plus (基于vue3轻量级UI组件库)
- 样式处理:sass^1.63.6
- 图表组件:echarts^5.4.2
- 国际化方案:vue-i18n^9.2.2
- 编辑器组件:wangeditor^4.7.15
- 持久化缓存:pinia-plugin-persistedstate^3.1.0
目前tauri已经迭代到1.4,如果大家对tauri+vue3创建多窗口项目感兴趣,可以去看看之前的这篇分享文章。
https://www.cnblogs.com/xiaoyan2017/p/16812092.html
功能特性
- 使用跨端技术tauri1.4
- 最新前端技术栈vite4、vue3、pinia、vue-router、vue-i18n
- 支持中文/英文/繁体多语言解决方案
- 支持动态路由权限验证
- 支持路由缓存功能/tabs控制切换路由页面
- 内置多个模板布局风格
- 搭配轻量级vue3组件库veplus
- 高效开发,支持增删定制化页面模块
项目结构
使用tauri脚手架搭配vite4构建项目,整体采用vue3 setup语法编码开发。
主入口main.js
import { createApp } from "vue" import "./styles.scss" import App from "./App.vue" // 引入路由及状态管理 import Router from './router' import Pinia from './pinia' // 引入插件配置 import Libs from './libs' const app = createApp(App) app .use(Router) .use(Pinia) .use(Libs) .mount("#app")
Tauri-Admin布局模板
提供了三种常见的布局模板,大家也可以定制喜欢的模板样式。
<script setup> import { computed } from 'vue' import { appStore } from '@/pinia/modules/app' // 引入布局模板 import Columns from './template/columns/index.vue' import Vertical from './template/vertical/index.vue' import Transverse from './template/transverse/index.vue' const store = appStore() const config = computed(() => store.config) const LayoutConfig = { columns: Columns, vertical: Vertical, transverse: Transverse } </script> <template> <div class="veadmin__container" :style="{'--themeSkin': store.config.skin}"> <component :is="LayoutConfig[config.layout]" /> </div> </template>
路由/pinia状态管理
如上图:配置router路由信息。
/** * 路由配置 * @author YXY Q:282310962 */ import { appWindow } from '@tauri-apps/api/window' import { createRouter, createWebHistory } from 'vue-router' import { appStore } from '@/pinia/modules/app' import { hasPermission } from '@/hooks/usePermission' import { loginWin } from '@/multiwins/actions' // 批量导入modules路由 const modules = import.meta.glob('./modules/*.js', { eager: true }) const patchRoutes = Object.keys(modules).map(key => modules[key].default).flat() /** * @description 动态路由参数配置 * @param path ==> 菜单路径 * @param redirect ==> 重定向地址 * @param component ==> 视图文件路径 * 菜单信息(meta) * @param meta.icon ==> 菜单图标 * @param meta.title ==> 菜单标题 * @param meta.activeRoute ==> 路由选中(默认空 route.path) * @param meta.rootRoute ==> 所属根路由选中(默认空) * @param meta.roles ==> 页面权限 ['admin', 'dev', 'test'] * @param meta.breadcrumb ==> 自定义面包屑导航 [{meta:{...}, path: '...'}] * @param meta.isAuth ==> 是否需要验证 * @param meta.isHidden ==> 是否隐藏页面 * @param meta.isFull ==> 是否全屏页面 * @param meta.isKeepAlive ==> 是否缓存页面 * @param meta.isAffix ==> 是否固定标签(tabs标签栏不能关闭) * */ const routes = [ // 首页 { path: '/', redirect: '/home' }, // 错误模块 { path: '/:pathMatch(.*)*', component: () => import('@views/error/404.vue'), meta: { title: 'page__error-notfound' } }, ...patchRoutes ] const router = createRouter({ history: createWebHistory(), routes }) // 全局钩子拦截 router.beforeEach((to, from, next) => { // 开启加载提示 loading({ text: 'Loading...', background: 'rgba(70, 255, 170, .1)', onOpen: () => { console.log('开启loading') }, onClose: () => { console.log('关闭loading') } }) const store = appStore() if(to?.meta?.isAuth && !store.isLogged) { loginWin() loading.close() }else if(!hasPermission(store.roles, to?.meta?.roles)) { // 路由鉴权 appWindow?.show() next('/error/forbidden') loading.close() Notify({ title: '访问限制!', description: `<span style="color: #999;">当前登录角色 ${store.roles} 没有操作权限,请联系管理员授权后再操作。</div>`, type: 'danger', icon: 've-icon-unlock', time: 10 }) }else { appWindow?.show() next() } }) router.afterEach(() => { loading.close() }) router.onError(error => { loading.close() console.warn('Router Error》》', error.message); }) export default router
如上图:vue3项目搭配pinia进行状态管理。
/** * 状态管理 Pinia util * @author YXY */ import { createPinia } from 'pinia' // 引入pinia本地持久化存储 import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) export default pinia
自定义路由菜单
项目中的三种模板提供了不同的路由菜单。均是基于Menu组件封装的RouteMenu菜单。
<script setup> import { ref, computed, h, watch, nextTick } from 'vue' import { useI18n } from 'vue-i18n' import { Icon, useLink } from 've-plus' import { useRoutes } from '@/hooks/useRoutes' import { appStore } from '@/pinia/modules/app' // 引入路由集合 import mainRoutes from '@/router/modules/main.js' const props = defineProps({ // 菜单模式(vertical|horizontal) mode: { type: String, default: 'vertical' }, // 是否开启一级路由菜单 rootRouteEnable: { type: Boolean, default: true }, // 是否要收缩 collapsed: { type: Boolean, default: false }, // 菜单背景色 background: { type: String, default: 'transparent' }, // 滑过背景色 backgroundHover: String, // 菜单文字颜色 color: String, // 菜单激活颜色 activeColor: String }) const { t } = useI18n() const { jumpTo } = useLink() const { route, getActiveRoute, getCurrentRootRoute, getTreeRoutes } = useRoutes() const store = appStore() const rootRoute = computed(() => getCurrentRootRoute(route)) const activeKey = ref(getActiveRoute(route)) const menuOptions = ref(getTreeRoutes(mainRoutes)) const menuFilterOptions = computed(() => { if(props.rootRouteEnable) { return menuOptions.value } // 过滤掉一级菜单 return menuOptions.value.find(item => item.path == rootRoute.value && item.children)?.children }) watch(() => route.path, () => { nextTick(() => { activeKey.value = getActiveRoute(route) }) }) // 批量渲染图标 const batchRenderIcon = (option) => { return h(Icon, {name: option?.meta?.icon ?? 've-icon-verticleleft'}) } // 批量渲染标题 const batchRenderLabel = (option) => { return t(option?.meta?.title) } // 路由菜单更新 const handleUpdate = ({key}) => { jumpTo(key) } </script> <template> <Menu class="veadmin__menus" v-model="activeKey" :options="menuFilterOptions" :mode="mode" :collapsed="collapsed && store.config.collapse" iconSize="18" key-field="path" :renderIcon="batchRenderIcon" :renderLabel="batchRenderLabel" :background="background" :backgroundHover="backgroundHover" :color="color" :activeColor="activeColor" @change="handleUpdate" style="border: 0;" /> </template>
Menu组件支持横向/竖向排列,调用非常简单。
<RouteMenu :rootRouteEnable="false" backgroundHover="#f1f8fb" activeColor="#24c8db" /> <RouteMenu rootRouteEnable collapsed background="#193c47" backgroundHover="#1a5162" color="rgba(235,235,235,.7)" activeColor="#24c8db" :collapsedIconSize="20" /> <RouteMenu mode="horizontal" background="#193c47" backgroundHover="#1a5162" color="rgba(235,235,235,.7)" activeColor="#24c8db" arrowIcon="ve-icon-caretright" />
tauri-admin多语言配置
tauri-vue3-admin项目使用vue-i18n进行多语言处理。
import { createI18n } from 'vue-i18n' import { appStore } 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 store = appStore() const lang = store.lang || langVal const i18n = createI18n({ legacy: false, locale: lang, messages: { 'en': enUS, 'zh-CN': zhCN, 'zh-TW': zhTW } }) app.use(i18n) }
路由缓存功能
项目支持配置路由页面缓存功能。可以在全局pinia/modules/app.js中配置,也可以在router配置项meta中配置isKeepAlive: true。
<template> <div v-if="app.config.tabsview" class="veadmin__tabsview"> <Scrollbar ref="scrollbarRef" mousewheel> <ul class="tabview__wrap"> <li v-for="(tab,index) in tabOptions" :key="index" :class="{'actived': tabKey == tab.path}" @click="changeTab(tab)" @contextmenu.prevent="openContextMenu(tab, $event)" > <Icon class="tab-icon" :name="tab.meta?.icon" /> <span class="tab-title">{{$t(tab.meta?.title)}}</span> <Icon v-if="!tab.meta?.isAffix" class="tab-close" name="ve-icon-close" size="12" @click.prevent.stop="closeTab(tab)" /> </li> </ul> </Scrollbar> </div> <!-- 右键菜单 --> <Dropdown ref="contextmenuRef" trigger="manual" :options="contextmenuOptions" fixed="true" :render-label="handleRenderLabel" @change="changeContextMenu" style="height: 0;" /> </template>
import { ref, nextTick } from 'vue' import { useRoute } from 'vue-router' import { defineStore } from 'pinia' import { appStore } from '@/pinia/modules/app' export const tabsStore = defineStore('tabs', () => { const currentRoute = useRoute() const store = appStore() /*state*/ const tabViews = ref([]) // 标签栏列表 const cacheViews = ref([]) // 缓存列表 const reload = ref(true) // 刷新标识 // 判断tabViews某个路由是否存在 const tabIndex = (route) => { return tabViews.value.findIndex(item => item?.path === route?.path) } /*actions*/ // 新增标签 const addTabs = (route) => { const index = tabIndex(route) if(index > -1) { tabViews.value.map(item => { if(item.path == route.path) { // 当前路由缓存 return Object.assign(item, route) } }) }else { tabViews.value.push(route) } // 更新keep-alive缓存 updateCacheViews() } // 移除标签 const removeTabs = (route) => { const index = tabIndex(route) if(index > -1) { tabViews.value.splice(index, 1) } // 更新keep-alive缓存 updateCacheViews() } // 移除左侧标签 const removeLeftTabs = (route) => { const index = tabIndex(route) if(index > -1) { tabViews.value = tabViews.value.filter((item, i) => item?.meta?.isAffix || i >= index) } // 更新keep-alive缓存 updateCacheViews() } // 移除右侧标签 const removeRightTabs = (route) => { const index = tabIndex(route) if(index > -1) { tabViews.value = tabViews.value.filter((item, i) => item?.meta?.isAffix || i <= index) } // 更新keep-alive缓存 updateCacheViews() } // 移除其它标签 const removeOtherTabs = (route) => { tabViews.value = tabViews.value.filter(item => item?.meta?.isAffix || item?.path === route?.path) // 更新keep-alive缓存 updateCacheViews() } // 移除所有标签 const clearTabs = () => { tabViews.value = tabViews.value.filter(item => item?.meta?.isAffix) // 更新keep-alive缓存 updateCacheViews() } // 更新keep-alive缓存 const updateCacheViews = () => { cacheViews.value = tabViews.value.filter(item => store.config.keepAlive || item?.meta?.isKeepAlive).map(item => item.name) console.log('cacheViews缓存路由>>:', cacheViews.value) } // 移除keep-alive缓存 const removeCacheViews = (route) => { cacheViews.value = cacheViews.value.filter(item => item !== route?.name) } // 刷新路由 const reloadTabs = () => { removeCacheViews(currentRoute) reload.value = false nextTick(() => { updateCacheViews() reload.value = true document.documentElement.scrollTo({ left: 0, top: 0 }) }) } // 清空缓存 const clear = () => { tabViews.value = [] cacheViews.value = [] } return { tabViews, cacheViews, reload, addTabs, removeTabs, removeLeftTabs, removeRightTabs, removeOtherTabs, clearTabs, reloadTabs, clear } }, // 本地持久化存储(默认存储localStorage) { // persist: true persist: { // key: 'tabsState', storage: localStorage, paths: ['tabViews', 'cacheViews'] } } )
tauri.conf.json配置
{ "build": { "beforeDevCommand": "yarn dev", "beforeBuildCommand": "yarn build", "devPath": "http://localhost:1420", "distDir": "../dist", "withGlobalTauri": false }, "package": { "productName": "tauri-admin", "version": "0.0.0" }, "tauri": { "allowlist": { "all": true, "shell": { "all": false, "open": true } }, "bundle": { "active": true, "targets": "all", "identifier": "com.tauri.admin", "icon": [ "icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico" ] }, "security": { "csp": null }, "windows": [ { "fullscreen": false, "resizable": true, "title": "tauri-admin", "width": 1000, "height": 640, "center": true, "decorations": false, "fileDropEnabled": false, "visible": false } ], "systemTray": { "iconPath": "icons/icon.ico", "iconAsTemplate": true, "menuOnLeftClick": false } } }
Cargo.toml配置
[package] name = "tauri-admin" version = "0.0.0" description = "基于tauri+vue3+vite4+pinia轻量级桌面端后台管理Tauri-Admin" authors = "andy <282310962@qq.com>" license = "" repository = "" edition = "2023" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [build-dependencies] tauri-build = { version = "1.4", features = [] } [dependencies] tauri = { version = "1.4", features = ["api-all", "icon-ico", "icon-png", "system-tray"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" [features] # this feature is used for production builds or when `devPath` points to the filesystem # DO NOT REMOVE!! custom-protocol = ["tauri/custom-protocol"]
OK,基于tauri+vue3跨端后台管理系统就分享到这里。希望对大家有所帮助哈~~
最新原创自研tauri2.0+vue3+element-plus客户端后台管理系统
最后附上两个最新开发的Electron和uniapp跨端项目实例
https://www.cnblogs.com/xiaoyan2017/p/17468074.html
https://www.cnblogs.com/xiaoyan2017/p/17507581.html