在开发过程中,我们往往会使用到权限映射这样的架构图,也就是我们常说的组织架构,废话不多说,直接上代码…
演示效果
Vue实现流程图,借鉴vue-tree-color
引入依赖
npm install vue-tree-color
同时查看项目中是否已安装less和less-loader,因为该组件使用到less
npm install --save-dev less less-loader
如果这里启动项目报错,有可能是less和less-loader的版本过高,可以降低版本,或者指定版本号
npm i less@3.9.0 less-loader@4.1.0 -D
添加全局
import Vue2OrgTree from 'vue-tree-color'
Vue.use(Vue2OrgTree)
组件二次封装
创建components文件夹,在components下加入下面两个文件
- vueOrgTree.vue文件
<template>
<div ref='container' class='container'>
<div v-if='isShow' class='spin'>
<a-spin tip='数据加载中...' />
</div>
<vueOrgTree
v-if='!isShow'
class='vueOrgTree'
:data='data'
:horizontal='true'
:collapsable='true'
@on-expand='onExpand'
@on-node-click='NodeClick'
@on-node-mouseover='onMouseover'
@on-node-mouseout='onMouseout'
:renderContent='renderContent'
/>
</div>
</template>
<script>
import vueOrgTree from './Components/vueOrgTree.vue'
import throttle from 'lodash/throttle'
//导入数据
import { data, img, className } from './index'
export default {
name: 'permissionMap',
components: {
vueOrgTree
},
data() {
return {
win: '',
container: '',
handler: () => {
},
isShow: true,
data: {}
}
},
created() {
},
mounted() {
this.win = window
this.container = this.$refs.container
this.container.style.minHeight = `${window.innerHeight - (52 + 65 + 12)}px`
// 节流监听窗口大小
this.handler = throttle(this.resize, 800)
this.win.addEventListener('resize', this.handler)
//当设置完container宽高后获取数据
this.isShow=true
setTimeout(()=>{
this.data = data
if (this.data){
this.isShow = false
this.toggleExpand(this.data, true)
}
},3000)
//默认展开所有节点
},
methods: {
// 监听自定义事件
resize() {
this.container.style.minHeight = `${window.innerHeight - (52 + 65 + 24)}px`
if (typeof this.win !== 'undefined') {
if (!this.container || !this.width || !this.height) return
}
},
//渲染节点
renderContent(h, data) {
// 通过data中的className属性来对div元素进行注入class
// 每个节点渲染必然会走这个函数
//这里对应的不同的className 需要在上面的tree.less中写入样式
const result = this.changeRender(data)
return result
},
//判断相对等级渲染的dom
changeRender(data) {
if ([0, 1, '0', '1'].includes(data.level)) {
data.img = img
data.className = className
return (
<div style='position:relative;display:flex;align-items: center;'>
<div
style='position: absolute;display: flex;width: 72px;height: 72px;border-radius: 50%;justify-content: center;align-items: center;'>
<img
src={(data.level == 0 && data.type == 0) ? data.img.userUrl : (data.level == 1 && data.type == 1) ? data.img.teacherUrl : (data.level == 1 && data.type == 2) ? data.img.adminUrl : (data.level == 1 && data.type == 3) ? data.img.directorUrl : data.img.postUrl} />
</div>
<div
class={(data.level == 0 && data.type == 0) ? data.className.user : (data.level == 1 && data.type == 1) ? data.className.teacher : (data.level == 1 && data.type == 2) ? data.className.admin : (data.level == 1 && data.type == 3) ? data.className.director : data.className.post}
>
<span>
{data.label}
</span>
<span style='font-size: 11px;'>{data.jobNum}</span>
</div>
</div>
)
}
if ([2, '2'].includes(data.level)) {
data.img = img
data.className = className
return (
<div style='position:relative;display:flex;align-items: center;'>
<div
class={data.type == 1 ? data.className.teacherChild : data.type == 2 ? data.className.adminChild : data.type == 3 ? data.className.directorChild : data.className.postChild}
>{data.label}
</div>
</div>
)
}
if ([3, '3'].includes(data.level)) {
data.className = className
return (
<div style='position:relative;display:flex;align-items: center;'>
<div
class={data.type == 1 ? data.className.tecListChild : data.type == 2 ? data.className.adminListChild : data.type == 3 ? data.className.directorListChild : data.className.postListChild}
>
<div
style='width: 24px;height: 24px;border-radius: 50%;color: #fff;display: block;justify-content: normal;align-items: normal;padding: 0;min-width: 0;font-weight: normal;'
class={data.type == 1 ? data.className.teacherChild : data.type == 2 ? data.className.adminChild : data.type == 3 ? data.className.directorChild : data.className.postChild}>
<i
class={data.mark == 'app' ? 'action-icon actionzhuomianyingyongshezhi' : data.mark == 'server' ? 'action-icon actionfuwu1' : 'action-icon actioncaidan'}
style=''></i>
</div>
<div style='display: flex;flex: 1;justify-content: center;height: 24px;'>{data.label}</div>
</div>
</div>
)
} else {
data.className = className
return (
<div style='position:relative;display:flex;align-items: center;'>
<div
style='display: flex;justify-content: center;align-items: center;font-weight: 400;min-width: 90px;font-size: 13px;'
class={data.type == 1 ? data.className.tecListChild : data.type == 2 ? data.className.adminListChild : data.type == 3 ? data.className.directorListChild : data.className.postListChild}>
{data.label}
</div>
</div>
)
}
},
//鼠标移出
onMouseout(e, data) {
console.log('onMouseout', data)
},
//鼠标移入
onMouseover(e, data) {
console.log('onMouseover', data)
},
//点击节点
NodeClick(e, data) {
console.log(e, data)
},
//默认展开
toggleExpand(data, val) {
if (Array.isArray(data)) {
data.forEach(item => {
this.$set(item, 'expand', val)
if (item.children) {
this.toggleExpand(item.children, val)
}
})
} else {
this.$set(data, 'expand', val)
if (data.children) {
this.toggleExpand(data.children, val)
}
}
},
collapse(list) {
list.forEach(child => {
if (child.expand) {
child.expand = false
}
child.children && this.collapse(child.children)
})
},
//展开
onExpand(e, data) {
if ('expand' in data) {
data.expand = !data.expand
if (!data.expand && data.children) {
this.collapse(data.children)
}
} else {
this.$set(data, 'expand', true)
}
}
},
beforeDestroy() {
this.win.removeEventListener('resize', this.handler)
}
}
</script>
<style scoped lang='less'>
@import "./index";
</style>
- node.js文件
// 判断是否叶子节点
const isLeaf = (data, prop) => {
return !(Array.isArray(data[prop]) && data[prop].length > 0)
}
// console.info('Thank you for using vue-tree-color \nIf you have any questions about this plug-in, please contact me in the following ways \nWeChat: yanjiahui12345 \nWeChat official number: Web_Miao')
// 创建 node 节点
export const renderNode = (h, data, context) => {
const { props } = context
const cls = ['org-tree-node']
const childNodes = []
const children = data[props.props.children]
if (isLeaf(data, props.props.children)) {
cls.push('is-leaf')
} else if (props.collapsable && !data[props.props.expand]) {
cls.push('collapsed')
}
childNodes.push(renderLabel(h, data, context))
if (!props.collapsable || data[props.props.expand]) {
childNodes.push(renderChildren(h, children, context))
}
return h('div', {
domProps: {
className: cls.join(' ')
}
}, childNodes)
}
// 创建展开折叠按钮
export const renderBtn = (h, data, { props, listeners }) => {
const expandHandler = listeners['on-expand']
let cls = ['org-tree-node-btn']
if (data[props.props.expand]) {
cls.push('expanded')
}
return h('span', {
domProps: {
className: cls.join(' ')
},
on: {
click: e => expandHandler && expandHandler(e,data)
}
})
}
// 创建 label 节点
export const renderLabel = (h, data, context) => {
const { props, listeners } = context
const label = data[props.props.label]
const renderContent = props.renderContent
// event handlers
const clickHandler = listeners['on-node-click']
const mouseOverHandler = listeners['on-node-mouseover']
const mouseOutHandler = listeners['on-node-mouseout']
const childNodes = []
if (typeof renderContent === 'function') {
let vnode = renderContent(h, data)
vnode && childNodes.push(vnode)
} else {
childNodes.push(label)
}
if (props.collapsable && !isLeaf(data, props.props.children)) {
childNodes.push(renderBtn(h, data, context))
}
const cls = ['org-tree-node-label-inner']
let { labelWidth, labelClassName, selectedClassName, selectedKey ,judge,NodeClass} = props
if (typeof labelWidth === 'number') {
labelWidth += 'px'
}
if (typeof labelClassName === 'function') {
labelClassName = labelClassName(data)
}
labelClassName && cls.push(labelClassName)
// add selected class and key from props
if (typeof selectedClassName === 'function') {
selectedClassName = selectedClassName(data)
}
selectedClassName && selectedKey && data[selectedKey] && cls.push(selectedClassName)
return h('div', {
domProps: {
className: 'org-tree-node-label'
}
}, [h('div', {
domProps: {
className:ChangeTheColor(data,judge,NodeClass) + " org-tree-node-label-inner"
},
style: { width: labelWidth },
on: {
'click': e => clickHandler && clickHandler(e, data),
'mouseover': e => mouseOverHandler && mouseOverHandler(e, data),
'mouseout': e => mouseOutHandler && mouseOutHandler(e, data)
}
}, childNodes)])
}
function ChangeTheColor(e,judge,NodeClass){
if(judge !== "" && judge !== undefined && judge !== null && judge.swtich !== false){
for(var k in judge) {
var a = (eval("e."+k))
if(NodeClass){
for(let c =0 ;c<NodeClass.length;c++){
if( a === NodeClass[c])
return NodeClass[c]
else if(NodeClass.length-1==c)
return ""
}
}else{
return ""
}
}
}else{
return ""
}
}
// 创建 node 子节点
export const renderChildren = (h, list, context) => {
if (Array.isArray(list) && list.length) {
const children = list.map(item => {
return renderNode(h, item, context)
})
return h('div', {
domProps: {
className: 'org-tree-node-children'
}
}, children)
}
return ''
}
export const render = (h, context) => {
const {props} = context
return renderNode(h, props.data, context)
}
export default render
在页面中使用
- 创建新页面,在页面中引入需要用到的数据(自己提出公共数据,根据项目需求创建),引入样式(根据项目需求创建)
创建permissionMap.vue
<template>
<div ref='container' class='container'>
<div v-if='isShow' class='spin'>
<a-spin tip='数据加载中...' />
</div>
<vueOrgTree
v-if='!isShow'
class='vueOrgTree'
:data='data'
:horizontal='true'
:collapsable='true'
@on-expand='onExpand'
@on-node-click='NodeClick'
@on-node-mouseover='onMouseover'
@on-node-mouseout='onMouseout'
:renderContent='renderContent'
/>
</div>
</template>
<script>
import vueOrgTree from './Components/vueOrgTree.vue'
import throttle from 'lodash/throttle'
//导入数据
import { data, img, className } from './index'
export default {
name: 'permissionMap',
components: {
vueOrgTree
},
data() {
return {
win: '',
container: '',
handler: () => {
},
isShow: true,
data: {}
}
},
created() {
},
mounted() {
this.win = window
this.container = this.$refs.container
this.container.style.minHeight = `${window.innerHeight - (52 + 65 + 12)}px`
// 节流监听窗口大小
this.handler = throttle(this.resize, 800)
this.win.addEventListener('resize', this.handler)
//当设置完container宽高后获取数据
this.isShow=true
setTimeout(()=>{
this.data = data
if (this.data){
this.isShow = false
this.toggleExpand(this.data, true)
}
},3000)
//默认展开所有节点
},
methods: {
// 监听自定义事件
resize() {
this.container.style.minHeight = `${window.innerHeight - (52 + 65 + 24)}px`
if (typeof this.win !== 'undefined') {
if (!this.container || !this.width || !this.height) return
}
},
//渲染节点
renderContent(h, data) {
// 通过data中的className属性来对div元素进行注入class
// 每个节点渲染必然会走这个函数
//这里对应的不同的className 需要在上面的tree.less中写入样式
const result = this.changeRender(data)
return result
},
//判断相对等级渲染的dom
changeRender(data) {
if ([0, 1, '0', '1'].includes(data.level)) {
data.img = img
data.className = className
return (
<div style='position:relative;display:flex;align-items: center;'>
<div
style='position: absolute;display: flex;width: 72px;height: 72px;border-radius: 50%;justify-content: center;align-items: center;'>
<img
src={(data.level == 0 && data.type == 0) ? data.img.userUrl : (data.level == 1 && data.type == 1) ? data.img.teacherUrl : (data.level == 1 && data.type == 2) ? data.img.adminUrl : (data.level == 1 && data.type == 3) ? data.img.directorUrl : data.img.postUrl} />
</div>
<div
class={(data.level == 0 && data.type == 0) ? data.className.user : (data.level == 1 && data.type == 1) ? data.className.teacher : (data.level == 1 && data.type == 2) ? data.className.admin : (data.level == 1 && data.type == 3) ? data.className.director : data.className.post}
>
<span>
{data.label}
</span>
<span style='font-size: 11px;'>{data.jobNum}</span>
</div>
</div>
)
}
if ([2, '2'].includes(data.level)) {
data.img = img
data.className = className
return (
<div style='position:relative;display:flex;align-items: center;'>
<div
class={data.type == 1 ? data.className.teacherChild : data.type == 2 ? data.className.adminChild : data.type == 3 ? data.className.directorChild : data.className.postChild}
>{data.label}
</div>
</div>
)
}
if ([3, '3'].includes(data.level)) {
data.className = className
return (
<div style='position:relative;display:flex;align-items: center;'>
<div
class={data.type == 1 ? data.className.tecListChild : data.type == 2 ? data.className.adminListChild : data.type == 3 ? data.className.directorListChild : data.className.postListChild}
>
<div
style='width: 24px;height: 24px;border-radius: 50%;color: #fff;display: block;justify-content: normal;align-items: normal;padding: 0;min-width: 0;font-weight: normal;'
class={data.type == 1 ? data.className.teacherChild : data.type == 2 ? data.className.adminChild : data.type == 3 ? data.className.directorChild : data.className.postChild}>
<i
class={data.mark == 'app' ? 'action-icon actionzhuomianyingyongshezhi' : data.mark == 'server' ? 'action-icon actionfuwu1' : 'action-icon actioncaidan'}
style=''></i>
</div>
<div style='display: flex;flex: 1;justify-content: center;height: 24px;'>{data.label}</div>
</div>
</div>
)
} else {
data.className = className
return (
<div style='position:relative;display:flex;align-items: center;'>
<div
style='display: flex;justify-content: center;align-items: center;font-weight: 400;min-width: 90px;font-size: 13px;'
class={data.type == 1 ? data.className.tecListChild : data.type == 2 ? data.className.adminListChild : data.type == 3 ? data.className.directorListChild : data.className.postListChild}>
{data.label}
</div>
</div>
)
}
},
//鼠标移出
onMouseout(e, data) {
console.log('onMouseout', data)
},
//鼠标移入
onMouseover(e, data) {
console.log('onMouseover', data)
},
//点击节点
NodeClick(e, data) {
console.log(e, data)
},
//默认展开
toggleExpand(data, val) {
if (Array.isArray(data)) {
data.forEach(item => {
this.$set(item, 'expand', val)
if (item.children) {
this.toggleExpand(item.children, val)
}
})
} else {
this.$set(data, 'expand', val)
if (data.children) {
this.toggleExpand(data.children, val)
}
}
},
collapse(list) {
list.forEach(child => {
if (child.expand) {
child.expand = false
}
child.children && this.collapse(child.children)
})
},
//展开
onExpand(e, data) {
if ('expand' in data) {
data.expand = !data.expand
if (!data.expand && data.children) {
this.collapse(data.children)
}
} else {
this.$set(data, 'expand', true)
}
}
},
beforeDestroy() {
this.win.removeEventListener('resize', this.handler)
}
}
</script>
<style scoped lang='less'>
@import "./index";
</style>
index.js公共数据(根据项目需求看是否提取)
//数据
export const data = {
id: 0,
label: '用户角色',
level: 0,//层级
type: 0,//类型(教师以及下面的子集为同一类型)
jobNum: '123456',
children: [
{
id: 1,
label: '教师',
level: 1,
type: 1,
jobNum: '角色',
children: [
{
id: 55,
level: 2,
type: 1,
label: '办事大厅',
children: [
{
id: 61,
level: 3,
type: 1,
label: '应用',
mark: 'app',//应用层标识
children: [
{
id: 63,
level: 4,
type: 1,
label: '信息站群',
children: [
{
id: 63,
level: 5,
type: 1,
label: '信息站群1'
},
{
id: 63,
level: 5,
type: 1,
label: '信息站群2'
}
]
},
{
id: 63,
level: 4,
type: 1,
label: 'AI助教'
}
]
},
{
id: 62,
level: 3,
type: 1,
label: '服务',
mark: 'server'//服务层标识
},
{
id: 63,
level: 3,
type: 1,
label: '菜单',
mark: 'menu'//菜单层标识
}
]
},
{
id: 56,
level: 2,
type: 1,
label: '离校系统',
children: [
{
id: 61,
level: 3,
type: 1,
label: '应用'
},
{
id: 62,
level: 3,
type: 1,
label: '服务'
},
{
id: 63,
level: 3,
type: 1,
label: '菜单'
}
]
},
{
id: 57,
level: 2,
type: 1,
label: '用户中心',
children: [
{
id: 61,
level: 3,
type: 1,
label: '应用'
},
{
id: 62,
level: 3,
type: 1,
label: '服务'
},
{
id: 63,
level: 3,
type: 1,
label: '菜单'
}
]
}
]
},
{
id: 2,
label: '管理员',
level: 1,
type: 2,
jobNum: '角色',
children: [
{
id: 55,
level: 2,
type: 2,
label: '办事大厅',
children: [
{
id: 61,
level: 3,
type: 2,
label: '应用',
children: [
{
id: 63,
level: 4,
type: 2,
label: '信息站群',
children: [
{
id: 63,
level: 5,
type: 2,
label: '信息站群1'
},
{
id: 63,
level: 5,
type: 2,
label: '信息站群2'
}
]
},
{
id: 63,
level: 4,
type: 2,
label: 'AI助教'
}
]
},
{
id: 62,
level: 3,
type: 2,
label: '服务'
},
{
id: 63,
level: 3,
type: 2,
label: '菜单'
}
]
},
{
id: 56,
level: 2,
type: 2,
label: '离校系统'
},
{
id: 57,
level: 2,
type: 2,
label: '用户中心'
}
]
},
{
id: 3,
label: '学院主任',
level: 1,
type: 3,
jobNum: '角色',
children: [
{
id: 58,
level: 2,
type: 3,
label: '办事大厅',
children: [
{
id: 61,
level: 3,
type: 3,
label: '应用',
children: [
{
id: 63,
level: 4,
type: 3,
label: '信息站群',
children: [
{
id: 63,
level: 5,
type: 3,
label: '信息站群1'
},
{
id: 63,
level: 5,
type: 3,
label: '信息站群2'
}
]
},
{
id: 63,
level: 4,
type: 3,
label: 'AI助教'
}
]
},
{
id: 62,
level: 3,
type: 3,
label: '服务'
},
{
id: 63,
level: 3,
type: 3,
label: '菜单'
}
]
},
{
id: 59,
level: 2,
type: 3,
label: '离校系统'
},
{
id: 60,
level: 2,
type: 3,
label: '用户中心'
}
]
},
{
id: 4,
label: '岗位',
level: 1,
type: 4,
jobNum: '角色',
children: [
{
id: 61,
level: 2,
type: 4,
label: '办事大厅',
children: [
{
id: 61,
level: 3,
type: 4,
label: '应用',
children: [
{
id: 63,
level: 4,
type: 4,
label: '信息站群'
,
children: [
{
id: 63,
level: 5,
type: 4,
label: '信息站群1'
},
{
id: 63,
level: 5,
type: 4,
label: '信息站群2'
}
]
},
{
id: 63,
level: 4,
type: 4,
label: 'AI助教'
}
]
},
{
id: 62,
level: 3,
type: 4,
label: '服务'
},
{
id: 63,
level: 3,
type: 4,
label: '菜单'
}
]
},
{
id: 62,
level: 2,
type: 4,
label: '离校系统'
},
{
id: 63,
level: 2,
type: 4,
label: '用户中心'
}
]
}
]
}
//前两级渲染时需要的图片
export const img = {
userUrl: require('@assets/permissionMap/user.png'),//用户角色
teacherUrl: require('@assets/permissionMap/teacher.png'),//教师
adminUrl: require('@assets/permissionMap/administrators.png'),//管理员
directorUrl: require('@assets/permissionMap/director.png'),//学院主任
postUrl: require('@assets/permissionMap/post.png')//岗位
}
//渲染中所需要的样式
export const className = {
user: 'userBgc',//用户角色层样式
teacher: 'teacherBgc',//教师样式
admin: 'adminBgc',//管理员层样式
director: 'directorBgc',//学院主任样式
post: 'postBgc',//岗位样式
sys: 'sysStyle',//系统层样式
teacherChild: 'teaC',//教师子集样式
adminChild: 'adminC',//管理员子集样式
directorChild: 'directC',//学院主任样式子集
postChild: 'postC',//岗位样式子集
tecListChild: 'tecListC',//教师应用,服务,菜单样式
adminListChild: 'adminListC',//管理员应用,服务,菜单样式
directorListChild: 'directorListC',//学院主任应用,服务,菜单样式
postListChild: 'postListC'//岗位应用,服务,菜单样式
}
index.less样式
.container {
display: flex;
.vueOrgTree {
display: flex;
flex: 1;
align-items: center;
box-sizing: border-box;
overflow: auto;
}
.spin {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
background: #fff;
}
}
.userBgc, .teacherBgc, .adminBgc, .directorBgc, .postBgc, .sysStyle {
display: flex;
//justify-content: center;
align-items: center;
flex-direction: column;
padding: 0px 15px 0px 74px;
min-width: 157px;
height: 50px;
box-shadow: 0px 0px 13px 0px rgba(0, 0, 0, 0.09);
border-radius: 31px !important;
border: 4px solid #FFFFFF;
font-size: 16px;
font-weight: bold;
color: #FFFFFF;
}
.teaC, .adminC, .directC, .postC {
display: flex;
justify-content: center;
align-items: center;
padding: 10px 20px;
min-width: 80px;
height: 30px;
border-radius: 16px;
font-size: 13px;
font-weight: bold;
color: #FFFFFF;
}
.userBgc {
background: #88A3C7;
}
.teacherBgc, .teaC {
background: #FFA66F;
}
.adminBgc, .adminC {
background: #7BA1FF;
}
.directorBgc, .directC {
background: #A382EF;
}
.postBgc, .postC {
background: #60CFD9;
}
.tecListC, .adminListC, .directorListC, .postListC {
display: flex;
justify-content: space-between;
align-items: center;
min-width: 80px;
height: 30px;
border-radius: 21px;
border: 1px solid #FFA66F;
padding: 3px;
font-size: 14px;
font-weight: 500;
color: #333333;
}
.tecListC {
border: 1px solid #FFA66F;
}
.adminListC {
border: 1px solid #7BA1FF;
}
.directorListC {
border: 1px solid #A382EF;
}
.postListC {
border: 1px solid #60CFD9;
}