前言
隨著 vite.js
快速迭代升級,越來越多的開發者傾向於使用vite.js來搭建vue3專案。今天給大家分享 vite4+pinia+ve-plus
開發輕量級後臺管理系統專案ViteAdmin。
Vite4-Vue3-Admin 使用前端最新技術vite4+pinia+vue-router@4+
搭建中後臺管理模板。
技術框架
- 編碼工具:Vscode
- 框架技術:vite4.x+vue3+pinia+vue-router
- UI元件庫:vePlus (基於vue3自定義元件庫)
- 樣式處理:sass^1.58.3
- 圖表元件:echarts^5.4.2
- 國際化方案:vue-i18n^9.2.2
- 富文字編輯器元件:wangeditor^4.7.15
- markdown編輯器:md-editor-v3^2.11.0
功能特性
- 支援中文+英文+繁體多語言模式切換。
- 支援表格單選/多選、邊框/隔行換色、橫向/縱向虛擬捲軸等功能。
- 搭配高顏值vue3元件庫VEPlus,風格更加統一。
- 內建多個模板佈局樣式
- 支援動態路由許可權控制
- 支援keepalive路由快取
- ...
專案結構
vue3元件庫ve-plus
ve-plus:一款基於vue3開發的輕量級高定製化UI元件庫,包含超過40+常用功能元件。
至於如何安裝使用,大家可以去看看之前的這篇分享文章。
https://blog.csdn.net/yanxinyun1990/article/details/129312570
佈局模板
vite-admin後臺管理提供了4種常用的佈局模板。
<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">
<component :is="LayoutConfig[config.layout]" />
</div>
</template>
<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">
<!-- 路由鑑權 -->
<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
根據ve-plus元件庫提供的Menu元件,結合路由JSON配置,生成路由選單。
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' },
// 是否開啟一級路由選單
rootRouteEnable: { type: Boolean, default: true },
// 是否要收縮
collapsed: { type: Boolean, default: false },
// 選單背景色
background: String,
// 滑過背景色
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
})
console.log('根路由地址::>>', rootRoute.value)
console.log('過濾後路由地址::>>', menuFilterOptions.value)
watch(() => route.path, () => {
nextTick(() => {
activeKey.value = getActiveRoute(route)
})
})
// 批次渲染圖示
const batchRenderIcon = (option) => {
return h(Icon, {name: option?.meta?.icon})
}
// 批次渲染標題
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>
呼叫方式
<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)"
/>
vue3國際化多語言vue-i18n
vite-admin支援中英文+繁體切換語言。透過pinia-plugin-persistedstate儲存功能。
import { createI18n } from 'vue-i18n'
import { appStore } from '@/store/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)
}
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: "簡體中文"},
{key: "zh-TW", label: "繁體字"},
{key: "en", label: "英文"},
])
const changeLang = () => {
// 設定locale語言
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>
keepAlive路由快取
專案支援開啟keep-alive動態路由頁面快取功能。
專案中使用pinia2狀態管理,pinia-plugin-persistedstate進行本地儲存。
/**
* 標籤欄快取狀態管理
* 在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([]) // 標籤欄列表
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)
}
updateCacheViews()
}
// 移除左側標籤
const removeLeftTabs = (route) => {
const index = tabIndex(route)
if(index > -1) {
tabViews.value = tabViews.value.filter((item, i) => item?.meta?.isAffix || i >= index)
}
updateCacheViews()
}
// 移除右側標籤
const removeRightTabs = (route) => {
const index = tabIndex(route)
if(index > -1) {
tabViews.value = tabViews.value.filter((item, i) => item?.meta?.isAffix || i <= index)
}
updateCacheViews()
}
// 移除其它標籤
const removeOtherTabs = (route) => {
tabViews.value = tabViews.value.filter(item => item?.meta?.isAffix || item?.path === route?.path)
updateCacheViews()
}
// 移除所有標籤
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
}
},
// 本地持久化儲存(預設儲存localStorage)
{
// persist: true
persist: {
storage: localStorage,
paths: ['tabViews', 'cacheViews']
}
}
)
<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)
// 滾動到當前路由
const scrollToActiveRoute = () => {
nextTick(() => {
const activeRef = scrollbarRef.value.scrollbarWrap.querySelector('.actived').offsetLeft
scrollbarRef.value.scrollTo({left: activeRef, top: 0, behavior: 'smooth'})
})
}
// 監聽路由(增加標籤/快取)
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)
}
// 是否第一個標籤
const isFirstTab = () => {
return selectedTab.value.path === store.tabViews[0].path || selectedTab.value.path === '/home/index'
}
// 是否最後一個標籤
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()
// 設定座標
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()
}
// 跳轉更新路由
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)
}
// 關閉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>
OK,今天就先分享這裡,希望大家能喜歡喲~~