基于vite4.x+vue3+pinia前端后臺(tái)管理系統(tǒng)解決方案ViteAdmin。
前段時(shí)間分享了一篇vue3自研pc端UI組件庫(kù)VEPlus。這次帶來(lái)最新開(kāi)發(fā)的基于vite4+vue3+pinia技術(shù)棧搭配ve-plus組件庫(kù)構(gòu)建的中后臺(tái)權(quán)限管理系統(tǒng)框架。支持vue-i18n國(guó)際化多語(yǔ)言、動(dòng)態(tài)路由鑒權(quán)、4種布局模板及tab頁(yè)面緩存等功能。
技術(shù)棧
- 編碼工具:Cursor+Sublime
- 框架技術(shù):vite4+vue3+pinia+vue-router
- UI組件庫(kù):ve-plus (基于vue3自研ui組件庫(kù))
- 樣式處理:sass^1.58.3
- 圖表組件:echarts^5.4.2
- 國(guó)際化方案:vue-i18n^9.2.2
- 富文本編輯器組件:wangeditor^4.7.15
- markdown編輯器:md-editor-v3^2.11.0
- 數(shù)據(jù)模擬:mockjs^1.1.0
功能點(diǎn)
- 最新前端技術(shù)棧vite4、vue3、pinia、vue-router、vue-i18n、ve-plus。
- 支持中文/英文/繁體多語(yǔ)言模式切換。
- 支持表格單選/多選、邊框/隔行換色、橫向/縱向虛擬滾動(dòng)條等功能。
- 搭配高顏值的vue3-plus組件庫(kù),風(fēng)格更加統(tǒng)一。
- 內(nèi)置多個(gè)模板布局樣式
- 支持動(dòng)態(tài)路由權(quán)限控制
- 支持tabs動(dòng)態(tài)路由緩存
- 高效率開(kāi)發(fā),整個(gè)框架已經(jīng)搭建完畢,只需定制化相應(yīng)模塊即可。
項(xiàng)目頁(yè)面結(jié)構(gòu)
整體采用vue3 setup語(yǔ)法糖模式開(kāi)發(fā),搭配ve-plus輕量級(jí)組件庫(kù),使得界面清新且運(yùn)行極速。
效果圖
Vue3 UI? VEPlus組件
ve-plus:基于vue3開(kāi)發(fā)的pc端組件庫(kù),包含了40+常用的功能組件,易于上手。
veplus整合了vue3.js開(kāi)發(fā)的兩個(gè)獨(dú)立插件vue3-layer彈窗、vue3-scrollbar虛擬滾動(dòng)條組件。
快速安裝
npm install ve-plus -S cnpm install ve-plus -S yarn add ve-plus
具體的使用方法,大家可以去看看這篇分享文章。
https://www.cnblogs.com/xiaoyan2017/p/17170454.html
Vite-Admin布局模塊
公共布局文件在layouts目錄,提供了4種經(jīng)典的布局模板。
<script setup> import { computed } from 'vue' import { appStore } from '@/store/modules/app' // 引入布局模板 import Classic from './layout/classic/index.vue' import Columns from './layout/columns/index.vue' import Vertical from './layout/vertical/index.vue' import Transverse from './layout/transverse/index.vue' const store = appStore() const config = computed(() => store.config) const LayoutConfig = { classic: Classic, columns: Columns, vertical: Vertical, transverse: Transverse } </script> <template> <div class="veadmin__container" :style="{'--themeSkin': store.config.skin}"> <component :is="LayoutConfig[config.layout]" /> </div> </template>
主模板main.vue提供了Permission權(quán)限控制及KeepAlive路由緩存。
<!-- 主緩存模板 --> <script setup> import { ref } from 'vue' import { useRoutes } from '@/hooks/useRoutes' import { tabsStore } from '@/store/modules/tabs' import Permission from '@/components/Permission.vue' import Forbidden from '@/views/error/forbidden.vue' const { route } = useRoutes() const store = tabsStore() </script> <template> <Scrollbar autohide gap="2"> <div class="ve__layout-main__wrapper"> <!-- 路由鑒權(quán) --> <Permission :roles="route?.meta?.roles"> <template #tips> <Forbidden /> </template> <!-- 路由緩存 --> <router-view v-slot="{ Component }"> <transition name="ve-slide-right" mode="out-in" appear> <KeepAlive :include="store.cacheViews"> <component v-if="store.reload" :is="Component" :key="route.path" /> </KeepAlive> </transition> </router-view> </Permission> </div> </Scrollbar> </template>
自定義路由菜單RouteMenu
?
?
如上圖:路由菜單組件只需傳入配置參數(shù),即可切換不同的模式。
<RouteMenu :rootRouteEnable="false" /> <RouteMenu rootRouteEnable collapsed background="#292d3e" backgroundHover="#353b54" color="rgba(235,235,235,.7)" /> <RouteMenu mode="horizontal" background="#292d3e" backgroundHover="#353b54" color="rgba(235,235,235,.7)" />
RouteMenu.vue模板
<!-- 路由菜單 --> <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 '@/store/modules/app' // 引入路由集合 import mainRoutes from '@/router/modules/main.js' const props = defineProps({ // 菜單模式(vertical|horizontal) mode: { type: String, default: 'vertical' }, // 是否開(kāi)啟一級(jí)路由菜單 rootRouteEnable: { type: Boolean, default: true }, // 是否要收縮 collapsed: { type: Boolean, default: false }, // 菜單背景色 background: String, // 滑過(guò)背景色 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 } // 過(guò)濾掉一級(jí)菜單 return menuOptions.value.find(item => item.path == rootRoute.value && item.children)?.children }) console.log('根路由地址::>>', rootRoute.value) console.log('過(guò)濾后路由地址::>>', menuFilterOptions.value) watch(() => route.path, () => { nextTick(() => { activeKey.value = getActiveRoute(route) }) }) // 批量渲染圖標(biāo) const batchRenderIcon = (option) => { return h(Icon, {name: option?.meta?.icon}) } // 批量渲染標(biāo)題 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>
vue-i18n國(guó)際化解決方案
vite-admin支持中英文/繁體三種語(yǔ)言模式,使用?"vue-i18n": "^9.2.2"?組件。
?
/** * 國(guó)際化配置 * @author YXY */ import { createI18n } from 'vue-i18n' import { appStore } from '@/store/modules/app' // 引入語(yǔ)言配置 import enUS from './en-US' import zhCN from './zh-CN' import zhTW from './zh-TW' // 默認(rèn)語(yǔ)言 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) }
Lang.vue模板
<script setup> import { ref } from 'vue' import { useI18n } from 'vue-i18n' import { appStore } from '@/store/modules/app' const { locale } = useI18n() const store = appStore() const langVal = ref(locale.value) const langOptions = ref([ {key: "zh-CN", label: "簡(jiǎn)體中文"}, {key: "zh-TW", label: "繁體字"}, {key: "en", label: "英文"}, ]) const changeLang = () => { // 設(shè)置locale語(yǔ)言 locale.value = langVal.value store.lang = locale.value // store.setLang(locale.value) } </script> <template> <Dropdown v-model="langVal" :options="langOptions" placement="bottom" @change="changeLang"> <div class="toolbar__item"><Icon name="ve-icon-lang" size="20" cursor /></div> <template #label="{item}"> <div> {{item.label}} <span style="color: #999; font-size: 12px;">{{item.key}}</span> </div> </template> </Dropdown> </template>
Vue3動(dòng)態(tài)圖表Hooks
vite-admin支持動(dòng)態(tài)圖表,使用?"echarts": "^5.4.2"?組件。
/** * 動(dòng)態(tài)圖表Hooks * @author YXY */ import { onMounted, onBeforeUnmount, ref } from 'vue' import * as echarts from 'echarts' import { useResizeObserver } from 've-plus' export function useEcharts(node, options) { let chartNode let chartRef = ref(null) const resizeHandle = () => { chartNode && chartNode.resize() } onMounted(() => { if(node.value) { chartNode = echarts.init(node.value) chartNode.setOption(options) chartRef.value = chartNode } }) onBeforeUnmount(() => { chartNode.dispose() }) // 自適應(yīng)圖表 useResizeObserver(node, resizeHandle) return chartRef }
通過(guò)useResizeObserver函數(shù),支持圖表自適應(yīng)大小。
網(wǎng)站動(dòng)態(tài)標(biāo)題title
通過(guò)監(jiān)聽(tīng)路由route更改,動(dòng)態(tài)設(shè)置網(wǎng)站標(biāo)題。
/** * 設(shè)置網(wǎng)站標(biāo)題 * @author YXY */ import { watch, unref } from 'vue' import { useRouter } from 'vue-router' import { useI18n } from 'vue-i18n' export function useTitle() { const { VITE_APP_TITLE } = import.meta.env const { currentRoute } = useRouter() const { t, locale } = useI18n() watch( () => [currentRoute.value.path, locale.value], () => { console.log('開(kāi)始監(jiān)聽(tīng)標(biāo)題變化........') const route = unref(currentRoute) const title = route?.meta?.title ? `${t(route?.meta?.title)} - ${VITE_APP_TITLE}` : VITE_APP_TITLE console.log('監(jiān)聽(tīng)標(biāo)題', title) document.title = title }, {immediate: true} ) }
動(dòng)態(tài)路由緩存
ve-admin支持keepalive路由緩存功能。使用?pinia?替代?vuex?狀態(tài)管理,使用?pinia-plugin-persistedstate?持久化存儲(chǔ)。
https://prazdevs.github.io/pinia-plugin-persistedstate/zh/
TabsView.vue模板
<script setup> import { ref, computed, watch, nextTick, h } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { appStore } from '@/store/modules/app' import { tabsStore } from '@/store/modules/tabs' const { t } = useI18n() const router = useRouter() const route = useRoute() const app = appStore() const store = tabsStore() const tabKey = ref(route.path) const tabOptions = computed(() => store.tabViews) // 滾動(dòng)到當(dāng)前路由 const scrollToActiveRoute = () => { nextTick(() => { const activeRef = scrollbarRef.value.scrollbarWrap.querySelector('.actived').offsetLeft scrollbarRef.value.scrollTo({left: activeRef, top: 0, behavior: 'smooth'}) }) } // 監(jiān)聽(tīng)路由(增加標(biāo)簽/緩存) watch(() => route.path, () => { tabKey.value = route.path const params = { path: route.path, name: route.name, meta: { ...route.meta } } store.addTabs(params) scrollToActiveRoute() }, { immediate: true }) // 右鍵菜單 const scrollbarRef = ref() const selectedTab = ref({}) const contextmenuRef = ref() const contextmenuOptions = ref([ { key: 'refresh', icon: 've-icon-reload', label: 'tabview__contextmenu-refresh' }, { key: 'close', icon: 've-icon-close', label: 'tabview__contextmenu-close' }, { key: 'closeLeft', icon: 've-icon-logout', label: 'tabview__contextmenu-closeleft' }, { key: 'closeRight', icon: 've-icon-logout1', label: 'tabview__contextmenu-closeright' }, { key: 'closeOther', icon: 've-icon-retweet', label: 'tabview__contextmenu-closeother' }, { key: 'closeAll', icon: 've-icon-close-square', label: 'tabview__contextmenu-closeall' }, ]) const handleRenderLabel = (option) => { return t(option?.label) } // 是否第一個(gè)標(biāo)簽 const isFirstTab = () => { return selectedTab.value.path === store.tabViews[0].path || selectedTab.value.path === '/home/index' } // 是否最后一個(gè)標(biāo)簽 const isLastTab = () => { return selectedTab.value.path === store.tabViews[store.tabViews.length - 1].path } const openContextMenu = (tab, e) => { selectedTab.value = tab contextmenuOptions.value[1].disabled = tab.meta?.isAffix contextmenuOptions.value[2].disabled = isFirstTab() contextmenuOptions.value[3].disabled = isLastTab() // 設(shè)置坐標(biāo) contextmenuRef.value.setPos(e.clientX, e.clientY) contextmenuRef.value.show() } const changeContextMenu = (v) => { if(v.key == 'refresh') { if(tabKey.value !== selectedTab.value.path) { router.push(selectedTab.value.path) } store.reloadTabs() return }else if(v.key == 'close') { store.removeTabs(selectedTab.value) }else if(v.key == 'closeLeft') { store.removeLeftTabs(selectedTab.value) }else if(v.key == 'closeRight') { store.removeRightTabs(selectedTab.value) }else if(v.key == 'closeOther') { store.removeOtherTabs(selectedTab.value) }else if(v.key == 'closeAll') { store.clearTabs() } updateTabRoute() } // 跳轉(zhuǎn)更新路由 const updateTabRoute = () => { const lastTab = store.tabViews.slice(-1)[0] if(lastTab && lastTab.path) { router.push(lastTab.path) }else { router.push('/') } } // 切換tab const changeTab = (tab) => { router.push(tab.path) } // 關(guān)閉tab const closeTab = (tab) => { store.removeTabs(tab) updateTabRoute() } </script> <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" @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>
/** * 狀態(tài)管理 Pinia */ import { createPinia } from 'pinia' // 引入pinia本地持久化存儲(chǔ) import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) export default pinia
/** * 標(biāo)簽欄緩存狀態(tài)管理 * 在setup store中 * ref() 就是 state 屬性 * computed() 就是 getters * function() 就是 actions * @author YXY * Q:282310962 WX:xy190310 */ import { ref, nextTick } from 'vue' import { useRoute } from 'vue-router' import { defineStore } from 'pinia' import { appStore } from '@/store/modules/app' export const tabsStore = defineStore('tabs', () => { const currentRoute = useRoute() const store = appStore() /*state*/ const tabViews = ref([]) // 標(biāo)簽欄列表 const cacheViews = ref([]) // 緩存列表 const reload = ref(true) // 刷新標(biāo)識(shí) // 判斷tabViews某個(gè)路由是否存在 const tabIndex = (route) => { return tabViews.value.findIndex(item => item?.path === route?.path) } /*actions*/ // 新增標(biāo)簽 const addTabs = (route) => { const index = tabIndex(route) if(index > -1) { tabViews.value.map(item => { if(item.path == route.path) { // 當(dāng)前路由緩存 return Object.assign(item, route) } }) }else { tabViews.value.push(route) } // 更新keep-alive緩存 updateCacheViews() } // 移除標(biāo)簽 const removeTabs = (route) => { const index = tabIndex(route) if(index > -1) { tabViews.value.splice(index, 1) } updateCacheViews() } // 移除左側(cè)標(biāo)簽 const removeLeftTabs = (route) => { const index = tabIndex(route) if(index > -1) { tabViews.value = tabViews.value.filter((item, i) => item?.meta?.isAffix || i >= index) } updateCacheViews() } // 移除右側(cè)標(biāo)簽 const removeRightTabs = (route) => { const index = tabIndex(route) if(index > -1) { tabViews.value = tabViews.value.filter((item, i) => item?.meta?.isAffix || i <= index) } updateCacheViews() } // 移除其它標(biāo)簽 const removeOtherTabs = (route) => { tabViews.value = tabViews.value.filter(item => item?.meta?.isAffix || item?.path === route?.path) updateCacheViews() } // 移除所有標(biāo)簽 const clearTabs = () => { tabViews.value = tabViews.value.filter(item => item?.meta?.isAffix) 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 } }, // 本地持久化存儲(chǔ)(默認(rèn)存儲(chǔ)localStorage) { // persist: true persist: { storage: localStorage, paths: ['tabViews', 'cacheViews'] } } )
vite.config.js配置文件
import { defineConfig, loadEnv } from 'vite' import vue from '@vitejs/plugin-vue' import { resolve } from 'path' import { wrapEnv } from './src/utils/env' // https://vitejs.dev/config/ export default defineConfig(({ mode }) => { const viteEnv = loadEnv(mode, process.cwd()) const env = wrapEnv(viteEnv) return { plugins: [vue()], // base: '/', // mode: 'development', // development|production /*構(gòu)建選項(xiàng)*/ build: { // minify: 'esbuild', // 打包方式 esbuild(打包快)|terser // chunkSizeWarningLimit: 2000, // 打包大小警告 // rollupOptions: { // output: { // chunkFileNames: 'assets/js/[name]-[hash].js', // entryFileNames: 'assets/js/[name]-[hash].js', // assetFileNames: 'assets/[ext]/[name]-[hash].[ext]', // } // } }, esbuild: { // 打包去除 console.log 和 debugger drop: env.VITE_DROP_CONSOLE ? ['console', 'debugger'] : [] }, /*開(kāi)發(fā)服務(wù)器選項(xiàng)*/ server: { // 端口 port: env.VITE_PORT, // 是否瀏覽器自動(dòng)打開(kāi) open: env.VITE_OPEN, // 開(kāi)啟https https: env.VITE_HTTPS, // 代理配置 proxy: { // ... } }, resolve: { // 設(shè)置別名 alias: { '@': resolve(__dirname, 'src'), '@assets': resolve(__dirname, 'src/assets'), '@components': resolve(__dirname, 'src/components'), '@views': resolve(__dirname, 'src/views'), // 解決vue-i18n警告提示:You are running the esm-bundler build of vue-i18n. 'vue-i18n': 'vue-i18n/dist/vue-i18n.cjs.js' } } } })
Okra,基于 vite4.x+pinia+vePlus 開(kāi)發(fā)后臺(tái)管理系統(tǒng)模板就分享到這里,希望對(duì)大家有些幫助哈~~ ??
最后附上兩個(gè)最近實(shí)例項(xiàng)目
https://www.cnblogs.com/xiaoyan2017/p/16830689.html
https://www.cnblogs.com/xiaoyan2017/p/16701624.html
文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-412559.html
?文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-412559.html
到了這里,關(guān)于Vite-Admin后臺(tái)管理系統(tǒng)|vite4+vue3+pinia前端后臺(tái)框架實(shí)例的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!