前言
随着 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,明天就先分享这里,心愿大家能喜爱哟~~
https://segmentfault.com/a/1190000042710924
https://segmentfault.com/a/1190000041357547