Build a lightweight management background template ViteAdmin based on Vite4.x+Vue3+VEPlus+Pinia

Integrate vite4.js+vue3+pinia to develop lightweight backend management system Vite-Admin

Use the latest front-end technology stack vue3+vite4+vue-router+pinia2+vue-i18n+ve-plus and other technologies to build a beautiful back-end front-end management system template ViteVueAdmin. Contains functions such as charts, tables, forms, routing authentication, error handling, multiple theme template styles, etc.

Insert image description here
Insert image description here

technology stack

  • Coding tool: Cursor+Sublime
  • Framework technology: vite4+vue3+pinia2+vue-router
  • Component library: ve-plus (custom component library based on vue3)
  • Style processing: sass^1.58.3
  • Chart component: echarts^5.4.2
  • Internationalization solution: vue-i18n^9.2.2
  • Rich text editor component: wangeditor^4.7.15
  • markdown editor: md-editor-v3^2.11.0
  • Local storage: pinia-plugin-persistedstate

Insert image description here
Insert image description here
vite-admin supports dynamic skin changing, 4 template themes , and customized side routing menu .
Insert image description here
vite-admin has full access to the self-developed vue3 desktop PC UI component library ve-plus .
https://blog.csdn.net/yanxinyun1990/article/details/129312570
Insert image description here

Project structure directory

Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here
Insert image description here

VEPlus component library

A new lightweight and highly customized UI component library developed based on vue3.js.
Insert image description here
Customized PC component library VePlus based on vue3.js

npm install ve-plus

ViteAdmin theme template

The project provides 4 layout templates : classic + column + horizontal + vertical , there is always one that suits you.
Insert image description here

<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 supports local dynamic route caching function.

<!-- 主缓存模板 -->
<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>

RouteMenuRouteMenu

Supports customizing side routing menus, using the Menu component of the ve-plus component library to implement multi-level routing menus (horizontal/vertical).
Insert image description here
Insert image description here

Calling method

<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 internationalization solution vue-i18n

vite4-admin supports three language modes: Chinese, English/Traditional , and uses the vue-i18n: ^9.2.2 component.
Insert image description here

/**
 * 国际化配置
 * @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)
}

Insert image description here
Lang.vue template page

<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 package chart Hooks

Insert image description here

/**
 * 动态图表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 state management pinia

PiniaUse substitution Vuexfor state management in vue3 and use pinia-plugin-persistedstatepersistent storage.

Insert image description here

/**
 * 标签栏缓存状态管理
 * 在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']
        }
    }
)

Insert image description here
tabsview.vue template page

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

Insert image description here
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 is very lightweight and easy to use. It is recommended that you develop vue3 projects and try it out.

Okay, I will share the backend management developed based on vite4+vue3+pinia here. I hope you all like it.

Finally, two example projects are attached

Svelte-Ui-Admin is based on the svelte-ui middle and backend management system

vue3-tauri-chat: Based on tauri chat instance | tauri imitation WeChat client

Insert image description here

Guess you like

Origin blog.csdn.net/yanxinyun1990/article/details/130144212