前言

随着 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