vue3-element-admin实现同一个菜单多标签

原框架代码: 赵志江/huzhushan-vue3-element-admin

目录

TagsBar实现

实现同一个菜单多标签

device/detail/:id,不同参数时页面缓存删不掉的问题


TagsBar实现

在src/layout/components/下新建目录Tagsbar,新建index.vue

<template>
  <div class="tags-container" :class="{ hide: !isTagsbarShow }">
    <el-scrollbar
      ref="scrollContainer"
      :vertical="false"
      class="scroll-container"
      @wheel.prevent="onScroll"
    >
      <router-link
        v-for="(tag, i) in tagList"
        :key="tag.fullPath"
        :to="tag"
        :ref="el => setItemRef(i, el)"
        custom
        v-slot="{ navigate, isExactActive }"
      >
        <div
          class="tags-item"
          :class="isExactActive? 'active' : ''"
          @click="navigate"
          @click.middle="closeTag(tag)"
          @contextmenu.prevent="openMenu(tag, $event)"
        >
          <span class="title">{{ $t(tag.title) }}</span>

          <el-icon
            v-if="!isAffix(tag)"
            class="el-icon-close"
            @click.prevent.stop="closeTag(tag)"
          >
            <Close />
          </el-icon>
        </div>
      </router-link>
    </el-scrollbar>
  </div>
  <ul
    v-show="visible"
    :style="{ left: left + 'px', top: top + 'px' }"
    class="contextmenu"
  >
    <!-- <li @click="refreshSelectedTag(selectedTag)">{{ $t('tags.refresh') }}</li> -->
    <li v-if="!isAffix(selectedTag)" @click="closeTag(selectedTag)">
      {{ $t('tags.close') }}
    </li>
    <li @click="closeOtherTags">{{ $t('tags.other') }}</li>
    <li @click="closeLeftTags">{{ $t('tags.left') }}</li>
    <li @click="closeRightTags">{{ $t('tags.right') }}</li>
    <li @click="closeAllTags">{{ $t('tags.all') }}</li>
  </ul>
</template>

<script>
import { defineComponent, computed, getCurrentInstance } from 'vue'
import { useTags } from './hooks/useTags'
import { useContextMenu } from './hooks/useContextMenu'
import { useLayoutsettings } from '@/pinia/modules/layoutSettings'

export default defineComponent({
  name: 'Tagsbar',
  mounted() {
    
  },
  setup() {
	const instance = getCurrentInstance()
	instance.appContext.config.globalProperties.$tagsbar = this
	
    const defaultSettings = useLayoutsettings()
    const isTagsbarShow = computed(() => defaultSettings.tagsbar.isShow)

    const tags = useTags()
    const contextMenu = useContextMenu(tags.tagList)

    const onScroll = e => {
      tags.handleScroll(e)
      contextMenu.closeMenu.value()
    }

    return {
      isTagsbarShow,
      onScroll,
      ...tags,
      ...contextMenu
    }
  },
})
</script>

<style lang="scss" scoped>
.tags-container {
  height: 32px;
  width: 100%;
  background: #fff;
  border-bottom: 1px solid #e0e4ef;
  &.hide {
    display: none;
  }
  .scroll-container {
    white-space: nowrap;
    overflow: hidden;
    ::v-deep(.el-scrollbar__bar) {
      bottom: 0px;
    }
  }

  .tags-item {
    display: inline-block;
    height: 32px;
    line-height: 32px;
    box-sizing: border-box;
    border-left: 1px solid #e6e6e6;
    border-right: 1px solid #e6e6e6;
    color: #5c5c5c;
    background: #fff;
    padding: 0 8px;
    font-size: 12px;
    margin-left: -1px;
    vertical-align: bottom;
    cursor: pointer;
    &:first-of-type {
      margin-left: 15px;
    }
    &:last-of-type {
      margin-right: 15px;
    }
    &.active {
      color: #303133;
      background: #f5f5f5;
    }
    .title {
      display: inline-block;
      vertical-align: top;
      max-width: 200px;
      overflow: hidden;
      white-space: nowrap;
      text-overflow: ellipsis;
    }
    .el-icon-close {
      color: #5c5c5c;
      margin-left: 8px;
      width: 16px;
      height: 16px;
      vertical-align: -2px;
      border-radius: 50%;
      text-align: center;
      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
      transform-origin: 100% 50%;
      &:before {
        transform: scale(0.8);
        display: inline-block;
        vertical-align: -2px;
      }
      &:hover {
        background-color: #333;
        color: #fff;
      }
    }
  }
}
.contextmenu {
  margin: 0;
  background: #fff;
  z-index: 3000;
  position: fixed;
  list-style-type: none;
  padding: 5px 0;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 400;
  color: #333;
  box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
  white-space: nowrap;
  li {
    margin: 0;
    padding: 8px 16px;
    cursor: pointer;
    &:hover {
      background: #eee;
    }
  }
}
</style>

新建hooks目录,新建useTags.js

import { storeToRefs } from 'pinia'
import { useTags as useTagsbar } from '@/pinia/modules/tags'
import { useScrollbar } from './useScrollbar'
import { watch, computed, ref, nextTick, onBeforeMount } from 'vue'
import { useRouter } from 'vue-router'

export const isAffix = tag => {
  return !!tag.meta && !!tag.meta.affix
}

export const useTags = () => {
  const tagStore = useTagsbar()
  const { tagList } = storeToRefs(tagStore)
  const { addTag, delTag, saveActivePosition, updateTagList } = tagStore
  const router = useRouter()
  const route = router.currentRoute
  const routes = computed(() => router.getRoutes())

  const tagsItem = ref([])

  const setItemRef = (i, el) => {
    tagsItem.value[i] = el
  }

  const scrollbar = useScrollbar(tagsItem)

  watch(
    () => tagList.value.length,
    () => {
      tagsItem.value = []
    }
  )

  const filterAffixTags = routes => {
    return routes.filter(route => isAffix(route))
  }

  const initTags = () => {
    const affixTags = filterAffixTags(routes.value)

    for (const tag of affixTags) {
      if (tag.name) {
        addTag(tag)
      }
    }
    // 不在路由中的所有标签,需要删除
    const noUseTags = tagList.value.filter(tag =>
      routes.value.every(route => route.name !== tag.name)
    )
    noUseTags.forEach(tag => {
      delTag(tag)
    })
  }

  const addTagList = () => {
    const tag = route.value
    if (!!tag.name && tag.matched[0].components.default.name === 'layout') {
      addTag(tag)
    }
  }

  const saveTagPosition = tag => {
    const index = tagList.value.findIndex(
      item => item.fullPath === tag.fullPath
    )

    saveActivePosition(Math.max(0, index))
  }

  const moveToCurrentTag = () => {
    nextTick(() => {
      for (const tag of tagsItem.value) {
        if (!!tag && tag.to.path === route.value.path) {
          scrollbar.moveToTarget(tag)

          if (tag.to.fullPath !== route.value.fullPath) {
            updateTagList(route.value)
          }
          break
        }
      }
    })
  }

  onBeforeMount(() => {
    initTags()
    addTagList()
    moveToCurrentTag()
  })

  watch(route, (newRoute, oldRoute) => {
    saveTagPosition(oldRoute) // 保存标签的位置
    addTagList()
    moveToCurrentTag()
  })

  return {
    tagList,
    setItemRef,
    isAffix,
    ...scrollbar,
  }
}

useScrollbar.js

import { ref } from 'vue'

export const useScrollbar = tagsItem => {
  const scrollContainer = ref(null)
  const scrollLeft = ref(0)

  const doScroll = val => {
    scrollLeft.value = val
    scrollContainer.value.setScrollLeft(scrollLeft.value)
  }

  const handleScroll = e => {
    const $wrap = scrollContainer.value.wrapRef
    if ($wrap.offsetWidth + scrollLeft.value > $wrap.children[0].scrollWidth) {
      doScroll($wrap.children[0].scrollWidth - $wrap.offsetWidth)
      return
    } else if (scrollLeft.value < 0) {
      doScroll(0)
      return
    }
    const eventDelta = e.wheelDelta || -e.deltaY
    doScroll(scrollLeft.value - eventDelta / 4)
  }

  const moveToTarget = currentTag => {
    const $wrap = scrollContainer.value.wrapRef
    const tagList = tagsItem.value

    let firstTag = null
    let lastTag = null

    if (tagList.length > 0) {
      firstTag = tagList[0]
      lastTag = tagList[tagList.length - 1]
    }
    if (firstTag === currentTag) {
      doScroll(0)
    } else if (lastTag === currentTag) {
      doScroll($wrap.children[0].scrollWidth - $wrap.offsetWidth)
    } else {
      const el = currentTag.$el.nextElementSibling

      el.offsetLeft + el.offsetWidth > $wrap.offsetWidth
        ? doScroll(el.offsetLeft - el.offsetWidth)
        : doScroll(0)
    }
  }

  return {
    scrollContainer,
    handleScroll,
    moveToTarget,
  }
}

useContextMenu.js

import { useTags } from '@/pinia/modules/tags'
import { onMounted, onBeforeUnmount, reactive, toRefs, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { isAffix } from './useTags'

export const useContextMenu = tagList => {
  const router = useRouter()
  const route = useRoute()

  const tagsStore = useTags()

  const state = reactive({
    visible: false,
    top: 0,
    left: 0,
    selectedTag: {},
    openMenu(tag, e) {
      state.visible = true
      state.left = e.clientX
      state.top = e.clientY
      state.selectedTag = tag
    },
    closeMenu() {
      state.visible = false
    },
    refreshSelectedTag(tag) {
      tagsStore.deCacheList(tag)
      const { fullPath } = tag
      nextTick(() => {
        router.replace({
          path: '/redirect' + fullPath,
        })
      })
    },
    closeTag(tag) {
      if (isAffix(tag)) return

      const closedTagIndex = tagList.value.findIndex(
        item => {
			return item.path === tag.path
		}
      )
	  console.log(closedTagIndex)
      tagsStore.delTag(tag)
      if (isActive(tag)) {
        toLastTag(closedTagIndex - 1)
      }
    },
    closeOtherTags() {
      tagsStore.delOtherTags(state.selectedTag)
      router.push(state.selectedTag)
    },
    closeLeftTags() {
      state.closeSomeTags('left')
    },
    closeRightTags() {
      state.closeSomeTags('right')
    },
    closeSomeTags(direction) {
      const index = tagList.value.findIndex(
        item => item.fullPath === state.selectedTag.fullPath
      )

      if (
        (direction === 'left' && index <= 0) ||
        (direction === 'right' && index >= tagList.value.length - 1)
      ) {
        return
      }

      const needToClose =
        direction === 'left'
          ? tagList.value.slice(0, index)
          : tagList.value.slice(index + 1)
      tagsStore.delSomeTags(needToClose)
      router.push(state.selectedTag)
    },
    closeAllTags() {
      tagsStore.delAllTags()
      router.push('/')
    },
  })

  const isActive = tag => {
    return tag.fullPath === route.fullPath
  }

  const toLastTag = lastTagIndex => {
    const lastTag = tagList.value[lastTagIndex]
    if (lastTag) {
      router.push(lastTag.fullPath)
    } else {
      router.push('/')
    }
  }

  onMounted(() => {
    document.addEventListener('click', state.closeMenu)
  })

  onBeforeUnmount(() => {
    document.removeEventListener('click', state.closeMenu)
  })

  return toRefs(state)
}

在src/pinia/modules下新建tags.js

import { defineStore } from 'pinia'
import { getItem, setItem, removeItem } from '@/utils/storage' //getItem和setItem是封装的操作localStorage的方法
const TAGLIST = 'VEA-TAGLIST'

export const useTags = defineStore('tags', {
  state: () => ({
    tagList: getItem(TAGLIST) || [],
    cacheList: [],
    activePosition: -1,
  }),
  actions: {
    saveActivePosition(index) {
      this.activePosition = index
    },
    addTag({ path, fullPath, name, meta, params, query }) {
      if (this.tagList.some(v => v.path === path)) return false
      // 添加tagList
      const target = Object.assign(
        {},
        { path, fullPath, name, meta, params, query },
        {
          title: meta.title || '未命名',
          fullPath: fullPath || path,
        }
      )
      if (this.activePosition === -1) {
        if (name === 'home') {
          this.tagList.unshift(target)
        } else {
          this.tagList.push(target)
        }
      } else {
        this.tagList.splice(this.activePosition + 1, 0, target)
      }
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      // 添加cacheList
      if (this.cacheList.includes(name)) return
      if (!meta.noCache) {
        this.cacheList.push(name)
      }
    },
    deTagList(tag) {
      // 删除tagList
      this.tagList = this.tagList.filter(v => v.path !== tag.path)
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)
    },
    deCacheList(tag) {
      // 删除cacheList
      this.cacheList = this.cacheList.filter(v => v !== tag.name)
    },
    delTag(tag) {
      // 删除tagList
      this.deTagList(tag)

      // 删除cacheList
      this.deCacheList(tag)
    },
    delOtherTags(tag) {
      this.tagList = this.tagList.filter(
        v => !!v.meta.affix || v.path === tag.path
      )
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      this.cacheList = this.cacheList.filter(v => v === tag.name)
    },
    delSomeTags(tags) {
      this.tagList = this.tagList.filter(
        v => !!v.meta.affix || tags.every(tag => tag.path !== v.path)
      )
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      this.cacheList = this.cacheList.filter(v =>
        tags.every(tag => tag.name !== v)
      )
    },
    delAllTags() {
      this.tagList = this.tagList.filter(v => !!v.meta.affix)
      // 保存到localStorage
      removeItem(TAGLIST)
      this.cacheList = []
    },
    updateTagList(tag) {
      const index = this.tagList.findIndex(v => v.path === tag.path)
      if (index > -1) {
        this.tagList[index] = Object.assign({}, this.tagList[index], tag)
        // 保存到localStorage
        setItem(TAGLIST, this.tagList)
      }
    },
    clearAllTags() {
      this.cacheList = []
      this.tagList = []
      // 保存到localStorage
      removeItem(TAGLIST)
    },
  },
})

src/layout/components/Content下新建index.vue,keep-alive组件会根据Component的name来跟include进行匹配,来缓存页面,同样的页面重新进入时不会触发onMounted,只会触发onActivated。

<template>
  <router-view v-slot="{ Component }">
    <keep-alive :include="cacheList.join(',')">
      <component :is="Component" :key="key" />
    </keep-alive>
  </router-view>
</template>
<script>
import { storeToRefs } from 'pinia'
import { computed, defineComponent } from 'vue'
import { useRoute } from 'vue-router'
import { useTags } from '@/pinia/modules/tags'

export default defineComponent({
  setup() {
    const route = useRoute()
    const { cacheList } = storeToRefs(useTags())
    const key = computed(() => route.fullPath)

    return {
      cacheList,
      key,
    }
  },
})
</script>

实现同一个菜单多标签

框架通过vue-router来实现页面跳转和菜单展示,下面介绍对一个菜单,如果实现参数不同,显示多个tag。

按如下定义menu

{
    path: 'detail/:id',
    name: 'device_detail',
    component: () => import('@/views/device/detail.vue'),
    meta: { title: '设备详情', icon: 'el-icon-s-platform' },
    hidden: true,
 }

device/detail.vue中动态修改Component的name:
 

onMounted(() => {
	  ctx.deviceId = parseInt(ctx.$route.params.id)
	  ctx.$options.name = 'device_detail' + ctx.deviceId
})
onActivated(() => {
	ctx.$options.name = 'device_detail' + ctx.deviceId
})

修改src/pinia/modules/tags.js,修改地方:tag.name 改为 this.getFinalName(tag),即根据参数不同name也不同,name放入cacheList,用于唯一标识一个Component。

import { defineStore } from 'pinia'
import { getItem, setItem, removeItem } from '@/utils/storage' //getItem和setItem是封装的操作localStorage的方法
const TAGLIST = 'VEA-TAGLIST'

export const useTags = defineStore('tags', {
  state: () => ({
    tagList: getItem(TAGLIST) || [],
    cacheList: [],
    activePosition: -1,
  }),
  actions: {
    saveActivePosition(index) {
      this.activePosition = index
    },
    addTag({ path, fullPath, name, meta, params, query }) {
      if (this.tagList.some(v => v.path === path)) return false
      var title = meta.title
	  if (name == 'device_detail') {
		title = title + ' ' + query.name
	  }
      // 添加tagList
      const target = Object.assign(
        {},
        { path, fullPath, name, meta, params, query },
        {
          title: title || '未命名',
          fullPath: fullPath || path,
        }
      )
      if (this.activePosition === -1) {
        if (name === 'home') {
          this.tagList.unshift(target)
        } else {
          this.tagList.push(target)
        }
      } else {
        this.tagList.splice(this.activePosition + 1, 0, target)
      }
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      // 添加cacheList
      const finalName = this.getFinalName(target)
      if (this.cacheList.includes(finalName)) return
      if (!meta.noCache) {
        this.cacheList.push(finalName)
      }
    },
    getFinalName(tag) {
		if (tag.name == 'device_detail') {
			return tag.name + tag.params.id
		}
		return tag.name
	},
    deTagList(tag) {
      // 删除tagList
      this.tagList = this.tagList.filter(v => v.path !== tag.path)
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)
    },
    deCacheList(tag) {
      const name = this.getFinalName(tag)
      // 删除cacheList
      this.cacheList = this.cacheList.filter(v => v !== name)
    },
    delTag(tag) {
      // 删除tagList
      this.deTagList(tag)

      // 删除cacheList
      this.deCacheList(tag)
    },
    delOtherTags(tag) {
      this.tagList = this.tagList.filter(
        v => !!v.meta.affix || v.path === tag.path
      )
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      const name = this.getFinalName(tag)
      this.cacheList = this.cacheList.filter(v => v === name)
    },
    delSomeTags(tags) {
      this.tagList = this.tagList.filter(
        v => !!v.meta.affix || tags.every(tag => tag.path !== v.path)
      )
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      this.cacheList = this.cacheList.filter(v =>
        tags.every(tag => tag.name !== v)
      )
    },
    delAllTags() {
      this.tagList = this.tagList.filter(v => !!v.meta.affix)
      // 保存到localStorage
      removeItem(TAGLIST)
      this.cacheList = []
    },
    updateTagList(tag) {
      const index = this.tagList.findIndex(v => v.path === tag.path)
      if (index > -1) {
        this.tagList[index] = Object.assign({}, this.tagList[index], tag)
        // 保存到localStorage
        setItem(TAGLIST, this.tagList)
      }
    },
    clearAllTags() {
      this.cacheList = []
      this.tagList = []
      // 保存到localStorage
      removeItem(TAGLIST)
    },
  },
})

device/detail/:id,不同参数时页面缓存删不掉的问题

现象如下:进入/device/detail/1,再打开/device/detail/2,点击其他标签,删掉/device/detail/2标签,再打开/device/detail/2,此时发现只触发了onActivated方法,没有触发onMounted方法,页面没有重新渲染,keepalive这里的缓存机制不清楚,但是可以知道框架误以为/device/detail/2还在缓存中,直接把缓存中的页面拿过来显示了。

解决方法
对于这种动态菜单的情况,Compnent的key属性增加自增的标识,每次打开标识加1。
修改src/pinia/modules/tags.js,增加detailIndex,在addTag时增加detailIndex的修改
 

import { defineStore } from 'pinia'
import { getItem, setItem, removeItem } from '@/utils/storage' //getItem和setItem是封装的操作localStorage的方法
const TAGLIST = 'VEA-TAGLIST'

export const useTags = defineStore('tags', {
  state: () => ({
    tagList: getItem(TAGLIST) || [],
    cacheList: [],
    activePosition: -1,
    detailIndex: {}
  }),
  actions: {
    saveActivePosition(index) {
      this.activePosition = index
    },
    addTag({ path, fullPath, name, meta, params, query }) {
      if (this.tagList.some(v => v.path === path)) return false
      var title = meta.title
	  if (name == 'device_detail') {
		title = title + ' ' + query.name
	  }
      // 添加tagList
      const target = Object.assign(
        {},
        { path, fullPath, name, meta, params, query },
        {
          title: title || '未命名',
          fullPath: fullPath || path,
        }
      )
      if (this.activePosition === -1) {
        if (name === 'home') {
          this.tagList.unshift(target)
        } else {
          this.tagList.push(target)
        }
      } else {
        this.tagList.splice(this.activePosition + 1, 0, target)
      }
      // 保存到localStorage
      setItem(TAGLIST, this.tagList)

      // 添加cacheList
      const finalName = this.getFinalName(target)
      if (this.cacheList.includes(finalName)) return
      if (!meta.noCache) {
        if (finalName.startsWith('device_detail')) {
            if (!this.detailIndex[target.path]) {
                this.detailIndex[target.path] = 1
            } else {
                this.detailIndex[target.path]++
            }
        } else {
            this.detailIndex[target.path] = ''
        }
        this.cacheList.push(finalName)
      }
    },

修改src/layout/components/Content/index.vue中的key未route.path + detailIndex.value[route.path]

<template>
  <router-view v-slot="{ Component }">
    <keep-alive :include="cacheList.join(',')">
      <component :is="Component" :key="key" />
    </keep-alive>
  </router-view>
</template>
<script>
import { storeToRefs } from 'pinia'
import { computed, defineComponent } from 'vue'
import { useRoute } from 'vue-router'
import { useTags } from '@/pinia/modules/tags'

export default defineComponent({
  setup() {
    const route = useRoute()
    const { cacheList, detailIndex } = storeToRefs(useTags())
    const key = computed(() => route.path + detailIndex[route.path])

    return {
      cacheList,
      key,
    }
  },
})
</script>

  • 21
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: vue3-element-plus-admin是一个基于Vue3和Element Plus的后台管理系统模板。它提供了丰富的界面组件和功能,可以快速搭建起一个功能完善的后台管理系统。 首先,vue3-element-plus-admin使用了Vue3作为核心框架,Vue3相比于Vue2在性能和开发体验上有很大的提升。Vue3采用了更先进的响应式机制,使得数据更新更高效;新的组合式API让开发者可以更灵活地组织代码和复用逻辑。 其次,vue3-element-plus-admin还使用了Element Plus作为UI组件库。Element Plus是一套基于Element UI的升级版本,提供了更丰富的UI组件和更好的自定义性。通过Element Plus的组件,我们可以轻松实现表格、表单、弹窗、导航等常见的后台管理功能。 此外,vue3-element-plus-admin还提供了一些常用的功能模块,比如权限管理、用户管理、角色管理等。通过这些功能模块,我们可以方便地进行用户身份验证和权限控制。同时,vue3-element-plus-admin还提供了数据可视化的功能,可以将后台数据以图表的形式展示,帮助我们更好地理解和分析数据。 总的来说,vue3-element-plus-admin是一个功能强大、易于使用和可定制化的后台管理系统模板。它使用了Vue3和Element Plus的最新技术,并提供了丰富的界面组件和功能模块,帮助开发者快速搭建起一个现代化的后台管理系统。无论是个人项目还是企业应用,vue3-element-plus-admin都是一个值得选择的工具。 ### 回答2: Vue3-Element Plus-Admin是一个基于Vue3和Element Plus的后台管理系统模板。它提供了一套完整的UI组件库,以及丰富的功能和布局样式,方便开发者快速搭建和定制自己的后台管理系统。 Vue3是Vue.js的最新版本,它在性能和开发体验上做了很多改进。相比于之前的版本,Vue3使用了更加高效的响应式系统,并且在编译和渲染方面也做了优化,提升了应用的性能。同时,Vue3还引入了新的特性和语法糖,让开发更加简洁方便。 Element Plus是一套基于Element UI的UI组件库,它提供了丰富的UI组件,包括按钮、表单、表格、弹窗等,使用起来非常直观和方便。Element Plus的组件也支持按需加载,可以根据项目需求来引入需要的组件,减小项目的体积。 Vue3-Element Plus-Admin结合了Vue3和Element Plus的优势,为开发者提供了一套完整的后台管理系统模板。开发者可以通过这个模板快速搭建后台管理系统,并根据自己的需求来进行自定义。模板提供了常见的功能和布局样式,包括登录、主页、菜单导航、表单、表格等,开发者只需根据具体业务需求进行修改和扩展即可。 总结来说,Vue3-Element Plus-Admin是一个基于Vue3和Element Plus的后台管理系统模板,它提供了一套完整的UI组件库和丰富的功能和布局样式,方便开发者快速搭建和定制自己的后台管理系统。 ### 回答3: vue3-element-plus-admin 是一个基于 Vue3 和 Element Plus 的后台管理系统框架。它使用了最新的 Vue3 版本,具有更高的性能和更好的开发体验。Element Plus 是一套基于 Vue3 的 UI 组件库,提供了丰富的组件来构建前端界面。 vue3-element-plus-admin 提供了一套完整的后台管理系统解决方案。它包括了常见的登录、权限管理、用户管理、角色管理、菜单管理等功能。使用它可以快速搭建一个功能完善的后台管理系统。 该框架采用模块化的开发方式,将各个功能模块拆分成独立的组件,有利于代码的复用和维护。同时它还使用了响应式设计,可以根据屏幕大小自动适应不同的设备,提供更好的用户体验。 在界面设计上,vue3-element-plus-admin 内置了 Element Plus 的样式,提供了整洁美观的界面效果。同时,它还支持自定义主题,可以根据需求进行样式的定制,满足不同项目的需求。 该框架还提供了丰富的插件和工具,如富文本编辑器、数据可视化图表、国际化支持等,方便开发者快速添加各种功能和扩展。 总之,vue3-element-plus-admin 是一个强大的 Vue3 后台管理系统框架,具有丰富的功能和灵活的扩展性,适用于各种中小型项目的快速开发。它提供了优秀的开发体验和用户体验,帮助开发者轻松搭建出优秀的后台管理系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值