整合vite4.js+vue3+pinia开发轻量级后台管理系统Vite-Admin

使用最新前端技术栈 vue3+vite4+vue-router+pinia2+vue-i18n+ve-plus 等技术构建漂亮的后台前端管理系统模板ViteVueAdmin。包含了图表、表格、表单、路由鉴权、错误处理、多种主题模板样式等功能。

在这里插入图片描述
在这里插入图片描述

技术栈

  • 编码工具:Cursor+Sublime
  • 框架技术:vite4+vue3+pinia2+vue-router
  • 组件库:ve-plus (基于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
  • 本地存储:pinia-plugin-persistedstate

在这里插入图片描述
在这里插入图片描述
vite-admin 支持动态换肤、4种模板主题、自定义侧边路由菜单
在这里插入图片描述
vite-admin 全面接入自研vue3桌面PC端UI组件库ve-plus
https://blog.csdn.net/yanxinyun1990/article/details/129312570
在这里插入图片描述

项目结构目录

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

VEPlus组件库

基于vue3.js开发的一款全新轻量级高定制化UI组件库。
在这里插入图片描述
基于vue3.js自定义pc端组件库VePlus

npm install ve-plus

ViteAdmin主题模板

项目提供了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" :style="{'--themeSkin': store.config.skin}">
        <component :is="LayoutConfig[config.layout]" />
    </div>
</template>
<template>
	<div class="veadmin__layout flexbox flex-col ve__filter">
		<!-- //顶部导航 -->
		<div class="ve__layout-header flexbox flex-alignc">
			<div class="veadmin__logo text-gradient">
				<router-link class="flexbox flex-alignc" to="/">
					<img src="/vite.svg" alt="" /> VITE-ADMIN
				</router-link>
			</div>
			<Toolbar style="flex: 1; color: rgba(235,235,235,.7);" />
		</div>
		
		<div class="ve__layout-body flex1 flexbox">
			<!-- //侧边栏 -->
			<div class="ve__layout-sidebar">
				<SideMenu />
			</div>

			<!-- //中间栏 -->
			<div class="ve__layout-menus" :class="{'hidden': store.config.collapse}" style="position: relative; z-index: 999;">
				<Scrollbar autohide>
					<RouteMenu :rootRouteEnable="false" style="margin-top: 5px; padding: 5px;" />
				</Scrollbar>
			</div>

			<!-- //右边栏 -->
			<div class="ve__layout-main flex1 flexbox flex-col">
				<!-- 面包屑导航 -->
				<Breadcrumb showMask />
				<!-- 标签栏 -->
				<TabsView />

				<!-- 主内容区 -->
				<Main />
			</div>
		</div>
	</div>
</template>

vite-admin 支持本地动态路由缓存功能。

<!-- 主缓存模板 -->
<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组件实现多级路由菜单(横向/竖向)。
在这里插入图片描述
在这里插入图片描述

调用方式

<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)"
/>
<!-- 路由菜单 -->
<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>

vue3国际化解决方案vue-i18n

vite4-admin 支持中英文/繁体三种语言模式,使用 vue-i18n: ^9.2.2 组件。
在这里插入图片描述

/**
 * 国际化配置
 * @author YXY
 */

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>

vue3封装图表Hooks

在这里插入图片描述

/**
 * 动态图表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()
    })
    // 自适应图表
    useResizeObserver(node, resizeHandle)

    return chartRef
}

vue3状态管理pinia

在vue3中使用Pinia替代Vuex进行状态管理,使用 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']
        }
    }
)

在这里插入图片描述
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)

  // 滚动到当前路由
  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>

在这里插入图片描述
https://github.com/prazdevs/pinia-plugin-persistedstate

/**
 * 状态管理 Pinia
 */

import { createPinia } from 'pinia'
// 引入pinia本地持久化存储
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export default pinia

pinia非常轻量级,使用很方便。建议大家开发vue3项目可以搭载试试。

好了,基于vite4+vue3+pinia开发后台管理就分享到这里,希望大家喜欢哈。

最后附上两个实例项目

Svelte-Ui-Admin基于svelte-ui中后台管理系统

vue3-tauri-chat:基于tauri聊天实例|tauri仿微信客户端

在这里插入图片描述

Logo

前往低代码交流专区

更多推荐