Vue Antd Admin 自定义菜单栏图标,支持图片和SVG两种形式!
本教程适用于:Vue Antd Admin,默认脚手架不支持非官方图标库以外的图标(至少我没发现怎么实现),本来想着在网上复制粘贴找一波解决方案,结果还没找到比较合适的(可能我搜索姿势不对吧),没辙,只能自己改造!于是有了这篇博文!
目标
让菜单栏支持非官方图标库以外的图标!最少要支持 iconfont 的 svg 矢量图(懂的都懂)
效果图预览
实现思路
改变官方菜单生成函数即可
如何实现
找到 menus.js 文件,参考路径:src/components/menu/menu.js
修改 renderIcon 函数代码,修改后代码如下:
function (h, icon, key) {
if (this.$scopedSlots.icon && icon && icon !== 'none') {
const vnodes = this.$scopedSlots.icon({icon, key})
vnodes.forEach(vnode => {
vnode.data.class = vnode.data.class ? vnode.data.class : []
vnode.data.class.push('anticon')
})
return vnodes
}
if (!icon || icon === 'none') return null;
// 增加 svg 图标支持
if (icon.indexOf("<svg") === 0) {
// 仅取出svg代码中path部分
let path = icon.match(/<path.+?>/)[0];
// 移除默认填色(否则会导致激活菜单状态下图标颜色不改变问题)
path = path.replace(/\sfill="#[a-z0-9]{3,6}"/, "");
// 创建DOM元素
return h("i", {
class: "anticon",
domProps: {
innerHTML: `<svg viewBox="64 64 896 896" data-icon="" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class="">${path}</svg>`
}
})
} else if (icon.indexOf("/") !== -1) {
// 如果通过 require 方式引入的话(不推荐)
return h('img', {
class: "action",
style: {
width: '1em',
height: '1em',
marginRight: '10px'
},
domProps: {
src: icon
}
})
} else {
return h(Icon, {props: {type: icon}})
}
}
创建一个图标配置文件(当然你不创建也行,随便你!)
扩展你的图标代码,可以从iconfont直接复制svg代码过来(推荐),或者下载图片文件后使用require载入(不推荐)
// 扩展你的图标代码,可以从iconfont直接复制svg代码过来(推荐),或者下载图片文件后使用require载入(不推荐)
export default {
mini: '<svg t="1686793174972" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2594" width="81" height="81"><path d="M402.2 734.6c-71.9 0-130.4-54.1-130.4-121 0-20.8 6-41.2 16.9-59.5 16.4-26.8 42.7-46.6 74.4-56 8.4-2.5 14.9-3.5 20.8-3.5 13.9 0 24.8 10.9 24.8 24.8s-10.9 24.8-24.8 24.8c-1 0-3 0-5.5 1-21.3 6-38.2 18.4-47.6 34.7-6.5 10.4-9.4 21.8-9.4 33.7 0 39.2 36.2 71.4 80.4 71.4 15.4 0 30.3-4 43.7-11.4 23.3-13.4 37.2-35.7 37.2-60V405.7c0-42.2 23.3-80.8 62-102.7 20.8-11.9 44.1-17.9 68-17.9 71.9 0 130.4 54.1 130.4 121 0 20.8-6 41.2-16.9 59.5-16.4 26.8-42.7 46.6-74.4 56-8.9 2.5-14.9 3.5-20.8 3.5-13.9 0-24.8-10.9-24.8-24.8s10.9-24.8 24.8-24.8c1 0 3 0 5.5-1 21.3-6.4 38.2-18.9 47.6-34.7 6.4-10.4 9.4-21.8 9.4-33.7 0-39.2-36.2-71.4-80.8-71.4-15.4 0-30.3 4-43.7 11.4-23.3 13.4-37.2 35.7-37.2 60v207.3c0 42.2-23.3 80.9-62 102.7-20.5 12.5-43.8 18.5-67.6 18.5z m504.4-223.2c0-219.2-177.6-396.8-396.8-396.8S113 292.1 113 511.4s177.6 396.8 396.8 396.8 396.8-177.6 396.8-396.8z m49.6 0c0 246.5-199.9 446.4-446.4 446.4-246.5 0-446.4-199.9-446.4-446.4C63.4 264.9 263.3 65 509.8 65c246.5 0 446.4 199.9 446.4 446.4z m0 0" fill="#333333" p-id="2595"></path></svg>'
}
在路由配置文件中(参考路径:src/router/config.js
),载入配置,然后再对应的位置引用即可!
完整 menus.js 代码分享
/**
* 该插件可根据菜单配置自动生成 ANTD menu组件
* menuOptions示例:
* [
* {
* name: '菜单名称',
* path: '菜单路由',
* meta: {
* icon: '菜单图标',
* invisible: 'boolean, 是否不可见, 默认 false',
* },
* children: [子菜单配置]
* },
* {
* name: '菜单名称',
* path: '菜单路由',
* meta: {
* icon: '菜单图标',
* invisible: 'boolean, 是否不可见, 默认 false',
* },
* children: [子菜单配置]
* }
* ]
*
* i18n: 国际化配置。系统默认会根据 options route配置的 path 和 name 生成英文以及中文的国际化配置,如需自定义或增加其他语言,配置
* 此项即可。如:
* i18n: {
* messages: {
* CN: {dashboard: {name: '监控中心'}}
* HK: {dashboard: {name: '監控中心'}}
* }
* }
**/
import Menu from 'ant-design-vue/es/menu'
import Icon from 'ant-design-vue/es/icon'
import fastEqual from 'fast-deep-equal'
import {getI18nKey} from '@/utils/routerUtil'
const {Item, SubMenu} = Menu
const resolvePath = (path, params = {}) => {
let _path = path
Object.entries(params).forEach(([key, value]) => {
_path = _path.replace(new RegExp(`:${key}`, 'g'), value)
})
return _path
}
const toRoutesMap = (routes) => {
const map = {}
routes.forEach(route => {
map[route.fullPath] = route
if (route.children && route.children.length > 0) {
const childrenMap = toRoutesMap(route.children)
Object.assign(map, childrenMap)
}
})
return map
}
export default {
name: 'IMenu',
props: {
options: {
type: Array,
required: true
},
theme: {
type: String,
required: false,
default: 'dark'
},
mode: {
type: String,
required: false,
default: 'inline'
},
collapsed: {
type: Boolean,
required: false,
default: false
},
i18n: Object,
openKeys: Array
},
data() {
return {
selectedKeys: [],
sOpenKeys: [],
cachedOpenKeys: []
}
},
computed: {
menuTheme() {
return this.theme == 'light' ? this.theme : 'dark'
},
routesMap() {
return toRoutesMap(this.options)
}
},
created() {
this.updateMenu()
if (this.options.length > 0 && !this.options[0].fullPath) {
this.formatOptions(this.options, '')
}
// 自定义国际化配置
if (this.i18n && this.i18n.messages) {
const messages = this.i18n.messages
Object.keys(messages).forEach(key => {
this.$i18n.mergeLocaleMessage(key, messages[key])
})
}
},
watch: {
options(val) {
if (val.length > 0 && !val[0].fullPath) {
this.formatOptions(this.options, '')
}
},
i18n(val) {
if (val && val.messages) {
const messages = this.i18n.messages
Object.keys(messages).forEach(key => {
this.$i18n.mergeLocaleMessage(key, messages[key])
})
}
},
collapsed(val) {
if (val) {
this.cachedOpenKeys = this.sOpenKeys
this.sOpenKeys = []
} else {
this.sOpenKeys = this.cachedOpenKeys
}
},
'$route': function () {
this.updateMenu()
},
sOpenKeys(val) {
this.$emit('openChange', val)
this.$emit('update:openKeys', val)
}
},
methods: {
renderIcon: function (h, icon, key) {
if (this.$scopedSlots.icon && icon && icon !== 'none') {
const vnodes = this.$scopedSlots.icon({icon, key})
vnodes.forEach(vnode => {
vnode.data.class = vnode.data.class ? vnode.data.class : []
vnode.data.class.push('anticon')
})
return vnodes
}
if (!icon || icon === 'none') return null;
// 增加 svg 图标支持
if (icon.indexOf("<svg") === 0) {
// 仅取出svg代码中path部分
let path = icon.match(/<path.+?>/)[0];
// 移除默认填色(否则会导致激活菜单状态下图标颜色不改变问题)
path = path.replace(/\sfill="#[a-z0-9]{3,6}"/, "");
// 创建DOM元素
return h("i", {
class: "anticon",
domProps: {
innerHTML: `<svg viewBox="64 64 896 896" data-icon="" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false" class="">${path}</svg>`
}
})
} else if (icon.indexOf("/") !== -1) {
// 如果通过 require 方式引入的话(不推荐)
return h('img', {
class: "action",
style: {
width: '1em',
height: '1em',
marginRight: '10px'
},
domProps: {
src: icon
}
})
} else {
return h(Icon, {props: {type: icon}})
}
},
renderMenuItem: function (h, menu) {
let tag = 'router-link'
const path = resolvePath(menu.fullPath, menu.meta.params)
let config = {props: {to: {path, query: menu.meta.query},}, attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}}
if (menu.meta && menu.meta.link) {
tag = 'a'
config = {attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;', href: menu.meta.link, target: '_blank'}}
}
return h(
Item, {key: menu.fullPath},
[
h(tag, config,
[
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
this.$t(getI18nKey(menu.fullPath))
]
)
]
)
},
renderSubMenu: function (h, menu) {
let this_ = this
let subItem = [h('span', {slot: 'title', attrs: {style: 'overflow:hidden;white-space:normal;text-overflow:clip;'}},
[
this.renderIcon(h, menu.meta ? menu.meta.icon : 'none', menu.fullPath),
this.$t(getI18nKey(menu.fullPath))
]
)]
let itemArr = []
menu.children.forEach(function (item) {
itemArr.push(this_.renderItem(h, item))
})
return h(SubMenu, {key: menu.fullPath},
subItem.concat(itemArr)
)
},
renderItem: function (h, menu) {
const meta = menu.meta
if (!meta || !meta.invisible) {
let renderChildren = false
const children = menu.children
if (children != undefined) {
for (let i = 0; i < children.length; i++) {
const childMeta = children[i].meta
if (!childMeta || !childMeta.invisible) {
renderChildren = true
break
}
}
}
return (menu.children && renderChildren) ? this.renderSubMenu(h, menu) : this.renderMenuItem(h, menu)
}
},
renderMenu: function (h, menuTree) {
let this_ = this
let menuArr = []
menuTree.forEach(function (menu, i) {
menuArr.push(this_.renderItem(h, menu, '0', i))
})
return menuArr
},
formatOptions(options, parentPath) {
options.forEach(route => {
let isFullPath = route.path.substring(0, 1) == '/'
route.fullPath = isFullPath ? route.path : parentPath + '/' + route.path
if (route.children) {
this.formatOptions(route.children, route.fullPath)
}
})
},
updateMenu() {
this.selectedKeys = this.getSelectedKeys()
let openKeys = this.selectedKeys.filter(item => item !== '')
openKeys = openKeys.slice(0, openKeys.length - 1)
if (!fastEqual(openKeys, this.sOpenKeys)) {
this.collapsed || this.mode === 'horizontal' ? this.cachedOpenKeys = openKeys : this.sOpenKeys = openKeys
}
},
getSelectedKeys() {
let matches = this.$route.matched
const route = matches[matches.length - 1]
let chose = this.routesMap[route.path]
if (chose && chose.meta && chose.meta.highlight) {
chose = this.routesMap[chose.meta.highlight]
const resolve = this.$router.resolve({path: chose.fullPath})
matches = (resolve.resolved && resolve.resolved.matched) || matches
}
return matches.map(item => item.path)
}
},
render(h) {
return h(
Menu,
{
props: {
theme: this.menuTheme,
mode: this.$props.mode,
selectedKeys: this.selectedKeys,
openKeys: this.openKeys ? this.openKeys : this.sOpenKeys
},
on: {
'update:openKeys': (val) => {
this.sOpenKeys = val
},
click: (obj) => {
obj.selectedKeys = [obj.key]
this.$emit('select', obj)
}
}
}, this.renderMenu(h, this.options)
)
}
}