Vite4.x+Vue3+VEPlus+Pinia に基づいた軽量管理バックグラウンド テンプレート ViteAdmin を構築する

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
  • マークダウンエディタ: 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 つのレイアウト テンプレートが用意されており、最適なものが必ず 1 つあります。
ここに画像の説明を挿入します

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

ルートメニュールートメニュー

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 は、中国語、英語/繁体字の3 つの言語モードをサポートし、 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
 * @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 状態管理ピニア

vue3 の状態管理にはPinia置換を使用し、永続ストレージを使用します。Vuexpinia-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 に基づいて開発されたバックエンド管理を共有します。

最後に、2 つのサンプル プロジェクトを添付します。

Svelte-Ui-Admin は、svelte-ui ミドルおよびバックエンド管理システムに基づいています。

vue3-tauri-chat: tauri チャット インスタンスに基づく | tauri 模倣 WeChat クライアント

ここに画像の説明を挿入します

おすすめ

転載: blog.csdn.net/yanxinyun1990/article/details/130144212