👉 前言
在 Vue + elementUi 开发中,某些后台管理系统是拥有 “ 机构单位 ”、“ 岗位 ” 及 “ 角色 ”
等字段的! 但是有些需求是需要可视化
的对 这些 设计 “ 树状数据
” 实现可视化展示及增删改查操作! 这里就需要用到 elementUi 的 树状组件去封装对应的内容了!
接下来,将展示 机构单位-岗位管理字典
的案例! 案例仅供参考,禁止转载!
👉 一、涉及elementUi 组件
使用到:
① elementUi 的 Tree 树形控件
② Dropdown 下拉菜单
点击了解详细使用规则!
👉 二、实现案例
> HTML模板
<template>
<div class="tree">
<!-- 头部搜索区 -->
<div class="top" ref="top">
<div class="left">
<el-input placeholder="请输入机构名称" v-model="filterText">
<template #append>
<el-button icon="el-icon-search" @click="getData" ></el-button>
</template>
</el-input>
<!-- <div class="search"></div> -->
</div>
</div>
<div class="content" v-if="currentSelected.id">
<el-tree
v-if="data"
ref="treeRef"
v-loading="loading"
element-loading-text="加载中"
class="filter-tree"
node-key="id"
:key="data"
@currentChange="currentChange"
:data="data"
:props="defaultProps"
:default-expanded-keys="['0']"
:current-node-key="currentSelected.id"
:filter-node-method="filterNode"
:highlight-current="true"
@node-click="handleNodeClick"
:load="loadTreeData"
lazy
>
<!-- :expand-on-click-node="false" 是否区分点击和展示事件,即点击文字和点击展开图标都触发加载事件 -->
<template #default="{ node, data }">
<div class="custom-tree-node tree-item">
<!-- 节点名字 -->
<!-- <span>{
{ node.label }}</span> -->
<span v-if="node.label.length <= 5">{
{ node.label }}</span>
<el-tooltip
v-else
class="item"
effect="light"
:content="node.label"
placement="top"
>
<template #default>
<span>{
{node.label.slice(0, 4) + '...'}}</span>
</template>
</el-tooltip>
<!-- 节点下拉菜单 -->
<div @click.stop="(_) => {}">
<el-dropdown size="mini" trigger="click">
<span class="el-dropdown-link">
<i class="el-icon-more"></i>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="action('addOrg', data)">新建子机构</el-dropdown-item>
<div v-if="data.id !== '0'">
<el-dropdown-item @click="action('editOrg', data)">修改机构</el-dropdown-item>
<el-dropdown-item @click="action('addPosition', data)">新建岗位</el-dropdown-item>
<el-dropdown-item @click="action('delete', data)">删除机构</el-dropdown-item>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
</el-tree>
</div>
<el-dialog
v-if="visible"
@close="hideDialog"
v-model="visible"
custom-class="test-dialog"
:title="dialogTitleList[dialogType]"
v-loading="dialogLoading"
>
<!-- <el-radio-group v-model="dialogType" size="small" style="padding: 10px 0;">
<el-radio-button label="addOrg">新建机构</el-radio-button>
<el-radio-button label="editOrg" >修改机构</el-radio-button>
<el-radio-button label="addPosition">新建岗位</el-radio-button>
</el-radio-group> -->
<el-form
ref="ruleForm"
:model="formData"
:key="formData"
:rules="rules[dialogType]"
label-width="100px"
label-position="right">
<!-- 新增机构 -->
<div v-if="dialogType === 'addOrg'">
<el-form-item label="机构名称: " prop="orgName">
<el-input v-model="formData.orgName" size="mini" placeholder="请输入机构名称"></el-input>
</el-form-item>
<el-form-item label="机构编号: " prop="orgCode">
<el-input v-model="formData.orgCode" size="mini" placeholder="请输入机构编号"></el-input>
</el-form-item>
<el-form-item label="父机构: " prop="orgNode">
<treeSeleteOrg
ref="treeSeleteOrg"
:label="formData.pName"
:value="formData.orgNode"
:formData="formData"
placeholder="请选择父机构"
@changeVal="changeFormVal($event, 'formData','orgNode')"
/>
</el-form-item>
</div>
<!-- 修改机构 -->
<div v-if="dialogType === 'editOrg'">
<el-form-item label="机构名称: " prop="orgName">
<el-input v-model="formData.orgName" size="mini" placeholder="请输入机构名称"></el-input>
</el-form-item>
<el-form-item label="机构编号: " prop="orgCode">
<el-input v-model="formData.orgCode" size="mini" placeholder="请输入机构编号"></el-input>
</el-form-item>
</div>
<!-- 新增岗位 -->
<div v-if="dialogType === 'addPosition' || dialogType === 'editPosition'">
<el-form-item label="岗位名称: " prop="positionName">
<el-input v-model="formData.positionName" size="mini" placeholder="请输入岗位名称"></el-input>
</el-form-item>
<el-form-item label="岗位描述: " prop="positionDesc">
<el-input type="textarea" v-model="formData.positionDesc" size="mini" placeholder="请输入岗位描述"></el-input>
</el-form-item>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button size="mini" type="primary" @click="submit(formData, dialogType, currentAction)">提交</el-button>
<el-button size="mini" @click="hideDialog">取消</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script>
import {
onMounted, reactive, ref, watch, getCurrentInstance, toRefs, Vue } from "vue";
import {
ElMessage } from "element-plus";
import treeSeleteOrg from '@/components/treeSelete-org.vue'
import qs from 'qs'
/**
* @param parent // 区分父子集字段,可根据接口调整
* @param children // 二级目录字段名
*/
const data = [];
const deepClone = (obj) => JSON.parse(JSON.stringify(obj));
export default {
name: "Tree",
components: {
treeSeleteOrg },
props: {
},
data() {
return {
}
},
setup(_, {
emit }) {
let treeRef = ref(null);
const state = reactive({
loading: false,
isExtend: true,
filterText: "", // 搜索文本
data: [
{
id: '0',
text: '根目录',
parent: true,
children: []
}
], // 源数据
tagList: [],
defaultProps: {
// 定义字段名
children: "children",
label: "text",
},
currentSelected: {
}, // 当前选中节点
currentAction: {
}, // 当前操作项
dialogLoading: false,
visible: false, // 编辑框显隐
resolve: [],
node: [],
formData: {
},
dialogType: 'addOrg',
dialogTitleList: {
addOrg: '新建机构',
editOrg: '修改机构',
addPosition: '新建岗位',
delete: '删除机构',
editPosition: '修改岗位信息'
},
rules: {
'addOrg': {
orgName: [
{
required: true, message: '请输入机构名称', trigger: 'blur' }
],
// orgCode: [
// { required: true, message: '请输入机构编号', trigger: 'blur' }
// ],
orgNode: [
{
required: true, message: '请选择父机构', trigger: 'change' }
],
},
'editOrg': {
orgName: [
{
required: true, message: '请输入机构名称', trigger: 'blur' }
]
},
'addPosition': {
positionName: [
{
required: true, message: '请输入岗位名称', trigger: 'blur' }
],
// positionDesc: [
// { required: true, message: '请输入岗位描述', trigger: 'blur' }
// ]
},
'editPosition': {
positionName: [
{
required: true, message: '请输入岗位名称', trigger: 'blur' }
],
// positionDesc: [
// { required: true, message: '请输入岗位描述', trigger: 'blur' }
// ]
},
},
});
const hideDialog = () => {
state.formData = {
};
state.visible = false;
state.dialogLoading = false;
};
// 表单赋值
const changeFormVal = (val, formName, formItemName) => {
state[formName][formItemName] = val;
};
const {
proxy, ctx } = getCurrentInstance()
/* 函数定义 */
//请求数据
const getData = async(_) => {
// 设置加载遮罩
state.loading = true;
//获取一级标签目录
let {
data } = await proxy.$axios({
method: "GET",
url: "center/org/listOrgTree",
params: {
keyword: state.filterText,
t: new Date().getTime()
}
});
if(state.filterText) {
state.data = JSON.parse(JSON.stringify(TreeData(data.data)));
} else {
// window.console.log(111)
state.data = [{
id: '0',
text: '根目录',
value: '0',
children: JSON.parse(JSON.stringify(TreeData(data.data)))
}]
}
// state.data[0].children = JSON.parse(JSON.stringify(TreeData(data.data)));
// state.data[0].children = TreeData(data.data);
// window.console.log(state.data[0])
/* 请求完需要进行以下操作 */
setTimeout(() => {
state.loading = false;
state.currentSelected = state.data[0]; // 初始化当前选项
}, 500);
};
// /center/org/deleteOrg
//目录树点击事件,点击请求二级目录
const handleNodeClick = async(data,obj,node) => {
emit('checkVal', data.id);
// window.console.log(data,obj,node)
};
// 加载树状数据
const loadTreeData = async(node, resolve) => {
// window.console.log(node, resolve)
if(node.level === 0 || (node.level === 0 && state.filterText)) {
// node.childNodes = [];
return resolve(node.data);
}
// window.console.log(node, resolve)
let {
data } = await proxy.$axios({
method: "get",
url: "/center/org/listOrgTree",
params: {
type: 'org',
id: node.data.id,
isRootNode: false,
t: new Date().getTime()
},
});
node.childNodes = [];
// window.console.log(data.data, '1213124')
resolve(data.data.length === 1 && data.data[0].entryId === node.data.id ? [] : TreeData(data.data));
};
// 格式化树状数据
const TreeData = (data) => {
let dataArr = [];
data.forEach(res => {
dataArr.push({
...res,
id: res.entryId,
text: res.text,
children: res.children && res.children.length !== 0 ? TreeData(res.children) : []
})
})
return dataArr;
};
const filterNode = (value, data) => {
if (!value) return true;
return data.text.indexOf(value) !== -1;
};
const currentChange = data=>{
state.currentSelected = data
}
/* 监听 */
watch(
(_) => state.visible,
(n) => {
if (!n) state.currentAction = {
};
}
);
watch(
(_) => state.filterText,
(val) => {
if(val == '' || val == null || val == undefined) {
getData();
}
}
);
/* 生命周期 */
onMounted((_) => {
state.currentSelected = state.data[0]
getData();
});
return {
getData, // 模拟请求
handleNodeClick,
loadTreeData, // 懒加载子目录数据
filterNode, // 搜索
currentChange , // 获取选中节点
treeRef, // 树节点
changeFormVal,
hideDialog, // 隐藏弹窗
...toRefs(state)
};
},
methods: {
// showAll() {
// this.$parent.showAllCustomers()
// this.$parent.getAllCustomer()
// },
// 树状控件下拉框点击事件
action(type, obj) {
this.currentAction = deepClone(obj);
// this.currentChange(data)
if(type === 'delete') {
this.$confirm(`此操作将删除
<span style="color: #409EFF; font-weight: bold;">
${
obj.text}
</span> 机构 , 是否继续?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true
}).then(async () => {
// window.console.log(data)
let {
data } = await this.$axios({
method: "POST",
url: "/center/org/deleteOrg",
data: qs.stringify({
'orgIds[]': obj.id, strong: 0 }),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
if(data.success) {
this.$message({
type: 'success',
message: '删除成功!'
});
this.$emit('checkVal', '0');
this.getData();
} else {
this.$message({
type: 'danger',
message: data.message
});
}
return ;
}).catch(() => {
return;
});
} else {
this.dialogType = type;
this.visible = true;
if(type === 'addOrg') {
window.console.log(obj)
this.formData.pName = obj.text;
this.formData.orgNode = obj.entryId || obj.id;
} else if(type === 'editOrg') {
window.console.log(obj)
this.formData = {
orgName: obj.text,
orgCode: obj.orgCode
};
} else if(type === 'editPosition') {
this.formData = {
positionName: obj.positionName,
positionDesc: obj.positionDesc
};
}
}
},
// 弹窗提交
submit(v, type, obj) {
let data = '';
this.$refs['ruleForm'].validate( async (valid) => {
if (valid) {
this.dialogLoading = true;
if(type === 'addOrg' || type === 'editOrg') {
let params = {
pOrgId: type === 'addOrg' ? v.orgNode : obj.pid,
orgId: type === 'addOrg' ? v.id : obj.id,
orgName: v.orgName,
orgCode: v.orgCode,
};
if(type === 'editOrg') {
data = (await this.$axios({
method: "POST",
url: "/center/org/checkOrgNameUnique",
data: qs.stringify(params),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})).data;
if(!data.success) {
this.$message({
type: 'danger',
message: data.message
});
return ;
}
}
data = (await this.$axios({
method: "POST",
url: "/center/org/saveOrganization",
data: qs.stringify(params),
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})).data;
} else if(type === 'addPosition' || type === 'editPosition') {
window.console.log(obj)
let params = {
pOrgId: obj.pOrgId,
positionName: v.positionName,
positionDesc: v.positionDesc,
positionId: type === 'editPosition' ? obj.positionId : '',
positionStatus: '1',
};
data = (await this.$axios({
method: "POST",
url: "/center/org/saveOrgPosition",
data: params,
})).data;
}
// window.console.log(data)
if(data.success) {
this.$message({
type: 'success',
message: this.dialogTitleList[type] + '成功!'
});
} else {
this.$message({
type: 'danger',
message: data.message
});
}
this.hideDialog();
this.$emit('checkVal', '0');
this.getData();
} else {
return false;
}
});
// window.console.log(this.formData, this.dialogType)
}
}
};
</script>
<style lang="less">
.test-dialog {
min-width: 600px;
max-width: 60%;
.el-dialog__header {
text-align: left;
border-bottom: 1px solid #ddd;
}
.el-dialog__footer {
border-top: 1px solid #ddd;
padding: 10px 20px;
}
}
</style>
<style lang="less" scoped>
// 修改组件样式
/deep/ .el-dropdown-menu__item {
&:hover {
font-weight: bold;
color: #55aaff!important;
background-color: rgba(85, 170, 255, 0.1)!important;
}
}
.tree {
box-sizing: border-box;
width: 100%;
.top {
display: flex;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
width: 100%;
height: 38px;
padding: 5px;
.left {
position: relative;
width: 100%;
height: 100%;
.el-input {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 28px;
/deep/.el-input__inner {
padding: 0 4px;
width: 100%;
height: 28px;
}
/deep/.el-input__inner::placeholder {
font-size: 12px;
}
/deep/ .el-input-group__append {
display: flex;
align-items: center;
width: 20px;
height: 26px;
}
}
.search {
position: absolute;
top: 50%;
right: 6px;
transform: translate(0,-50%);
width: 16px;
height: 16px;
background: url(../assets/images/icon/search.svg) no-repeat center center/cover;
&:hover {
cursor: pointer;
}
}
}
}
.allcustomer {
box-sizing: border-box;
width: 100%;
height: 30px;
line-height: 30px;
color: #333;
font-size: 12px;
text-align: left;
padding-left: 30px;
margin: 5px 0 5px;
background: #F2F8FF url(../assets/images/icon/customer.svg) no-repeat 10px center/16px;
&:hover {
cursor: pointer;
color: #4298F3;
}
}
.content {
.el-tree /deep/ .el-tree-node__expand-icon.expanded {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
.el-tree /deep/ .el-icon-caret-right:before {
background: url("../assets/images/icon/openfile.svg") no-repeat 0 0;
content: '';
display: block;
width: 16px;
height: 16px;
font-size: 16px;
background-size: 16px;
}
.el-tree /deep/ .el-tree-node__expand-icon.expanded.el-icon-caret-right:before {
background: url("../assets/images/icon/openfile.svg") no-repeat 0 0;
content: '';
display: block;
width: 16px;
height: 16px;
font-size: 16px;
background-size: 16px;
}
.el-tree /deep/.el-tree-node__expand-icon.is-leaf::before {
background: url("../assets/images/icon/openfile.svg") no-repeat 0 0;
content: '';
display: block;
width: 16px;
height: 16px;
font-size: 16px;
background-size: 16px;
}
.el-tree /deep/.custom-tree-node.tree-item>span {
font-size: 12px;
}
.tree-item {
padding: 4px 10px;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
}
}
</style>
👉 三、效果演示
💬 小温有话说
案例仅供参考,其中
Vue2
与Vue3
语法混合使用,请勿模仿! Vue2 和 Vue3 的内容不建议混用,避免出错,除非你知道会有什么效果!(小温主要是因为偷懒,加之,因为封装结构问题,不方便写在setup()
中,所以该案例仅供参考,可能会有bug,如需讨论,请评论或私聊告知! )
最后的最后,如果觉得不错的话,不妨给个三连吧! 🥰