特性?
- 可以自定义主键、配置选项
- 支持预定义节点图标:folder文件夹|normal普通样式
- 多个提示文本可以自定义
- 支持动态接口增删改节点
- 可以自定义根节点id
- 可以设置最多允许添加的层级深度
- 支持拖拽排序,排序过程还可以针对拖拽的节点深度进行自定义限制
- 支持隐藏一级节点(根节点)复选框?
- 支持屏蔽一级节点(根节点)勾选?
sgTree源码?
<template>
<div :class="$options.name">
<div class="tree-header" v-if="!(readonly || readonly === '')">
<div class="sg-left">
<template v-if="uploadData">
<el-tooltip
popper-class="sg-el-tooltip"
:enterable="false"
effect="dark"
:content="`支持拖拽到树上传文件`"
placement="top-start"
>
<el-button
type="text"
icon="el-icon-upload"
size="mini"
@click="(d) => $refs.sgUpload.triggerUploadFile()"
>
批量导入
</el-button>
</el-tooltip>
<el-button type="text" icon="el-icon-download" size="mini" @click="downloadTpl">
下载模板
</el-button>
</template>
</div>
<div class="sg-right">
<el-button type="text" size="mini" @click.stop="addRoot"
>{{ (data.text || {}).addRootButtonText || `添加根节点`
}}<i class="el-icon-circle-plus-outline"></i
></el-button>
</div>
</div>
<div class="tree-container">
<el-tree
:class="
hideRootNodeCheckbox === '' || hideRootNodeCheckbox
? 'hideRootNodeCheckbox'
: ''
"
ref="tree"
:data="treeData"
:node-key="mainKey"
:props="
data.props || {
label: 'label', //指定节点标签为节点对象的某个属性值
children: 'children', //指定子树为节点对象的某个属性值
disabled: 'leaf', //指定节点选择框是否禁用为节点对象的某个属性值
isLeaf: 'leaf', //指定节点是否为叶子节点,仅在指定了 lazy 属性的情况下生效
}
"
:icon-class="`${data.iconType}-tree-node`"
:indent="data.indent || 10"
@current-change="current_change"
@node-click="nodeClick"
highlight-current
@node-drag-start="nodeDragStart"
@node-drag-enter="nodeDragEnter"
@node-drag-leave="nodeDragLeave"
@node-drag-over="nodeDragOver"
@node-drag-end="nodeDragEnd"
@node-drop="nodeDrop"
:draggable="draggable === '' || draggable"
:allow-drop="allowDrop"
:allow-drag="allowDrag"
:show-checkbox="showCheckbox"
:default-checked-keys="defaultCheckedKeys"
@check-change="handleCheckChange"
>
<el-popover
popper-class="tree-el-popover"
placement="right"
trigger="hover"
title=""
content=""
:disabled="readonly || readonly === ''"
slot-scope="{ node, data }"
>
<span class="right">
<el-button
title="添加"
type="text"
size=""
icon="el-icon-circle-plus-outline"
@click.stop="addNode(node, data)"
v-if="showAddButton(node)"
>添加</el-button
>
<el-button
title="删除"
type="text"
size=""
icon="el-icon-remove-outline"
@click.stop="remove(node, data)"
>删除</el-button
>
</span>
<div slot="reference" class="node-label">
<div class="left" :title="node.label">
{{ node.label }}
</div>
</div>
</el-popover>
</el-tree>
<!-- 上传组件 -->
<sgUpload
:disabledWhenShowSels="['.v-modal']"
:drag="uploadData ? dragUpload : false"
ref="sgUpload"
:data="uploadData"
@uploadSuccess="uploadSuccess"
@uploadError="uploadError"
@importError="importError"
@showLoading="showLoading"
@hideLoading="hideLoading"
hideUploadTray
/>
</div>
</div>
</template>
<script>
import sgUpload from "@/vue/components/admin/sgUpload";
export default {
name: "sgTree",
components: {
sgUpload,
},
data() {
return {
// 动态树:增删改_________________________________________________________
rootNode: null, //根节点
rootResolve: null, //根节点
focusNodeId: null, //聚焦高亮新添加ID
mainKey: "id", //默认主键
defaultRootId: "root", //默认根节点ID就是root
maxAddLevel: null, // 最多允许添加的层级
dragUpload: true, //在拖拽节点过程中控制上传组件能否拖拽上传
// _________________________________________________________
};
},
props: [
"treeData",
"data",
"readonly",
"draggable", //是否开启拖拽节点功能
"uploadData",
/* 例子 uploadData: {
accept: '.xls,.xlsx',
actionUrl: `${this.$d.API_ROOT_URL}/core/resource/upload`,
}, */
"allowNodeDrag",
"allowNodeDrop",
"showCheckbox", //节点是否可被选择
"hideRootNodeCheckbox", //隐藏一级节点复选框?
"disabledRootNode", //屏蔽一级节点勾选?
"defaultCheckedKeys", //默认勾选的节点的 key 的数组
],
watch: {
data: {
/*
data.iconType= 节点图标:
folder 文件夹
normal 普通样式
plus 加减符号样式
*/
handler(d) {
d.nodeKey && (this.mainKey = d.nodeKey); //主键
d.rootId && (this.defaultRootId = d.rootId); //根节点ID
d.maxAddLevel && (this.maxAddLevel = d.maxAddLevel); // 最多允许添加的层级
},
deep: true,
immediate: true,
},
},
methods: {
showLoading(file) {
this.$emit(`showLoading`, file);
},
hideLoading(file) {
this.$emit(`hideLoading`, file);
},
// 取消选中
unCheckAll(d) {
this.$refs.tree.setCheckedKeys([]);
this.handleCheckChange([], []);
},
handleCheckChange(data, checked, indeterminate) {
this.$emit(`checkChange`, {
checkedNodes: this.$refs.tree.getCheckedNodes(),
checkedLeafOnlyNodes: this.$refs.tree.getCheckedNodes(true, false), //(leafOnly, includeHalfChecked) 接收两个 boolean 类型的参数,1. 是否只是叶子节点,默认值为 false 2. 是否包含半选节点,默认值为 false【注意:懒加载树形不管用!必须要明确叶子节点展开后面没有子节点了才能识别!】
data,
checked,
indeterminate,
});
},
// 拖拽----------------------------------------
nodeDragStart(node, ev) {
this.dragUpload = false;
this.$emit(`nodeDragStart`, node, ev);
},
nodeDragEnter(draggingNode, dropNode, ev) {
this.$emit(`nodeDragEnter`, draggingNode, dropNode, ev);
},
nodeDragLeave(draggingNode, dropNode, ev) {
this.$emit(`nodeDragLeave`, draggingNode, dropNode, ev);
},
nodeDragOver(draggingNode, dropNode, ev) {
this.$emit(`nodeDragOver`, draggingNode, dropNode, ev);
},
nodeDragEnd(draggingNode, dropNode, dropType, ev) {
// dropType有'before'、'after'、'inner'和'none'4种情况
this.dragUpload = true;
this.$emit(`nodeDragEnd`, draggingNode, dropNode, dropType, ev);
},
nodeDrop(draggingNode, dropNode, dropType, ev) {
// dropType有'before'、'after'、'inner'和'none'4种情况
this.$emit(`nodeDrop`, draggingNode, dropNode, dropType, ev);
},
allowDrop(draggingNode, dropNode, dropType) {
// 拖拽时判定目标节点能否被放置。dropType 参数有三种情况:'prev'、'inner' 和 'next',分别表示放置在目标节点前、插入至目标节点和放置在目标节点后(注意:很奇葩上面node开头的绑定方法dropType有before、after、inner和none4种情况)
return this.allowNodeDrop
? this.allowNodeDrop(draggingNode, dropNode, dropType)
: true;
},
allowDrag(draggingNode) {
return this.allowNodeDrag ? this.allowNodeDrag(draggingNode) : true;
},
// ----------------------------------------
showAddButton(node) {
if (this.maxAddLevel) {
return node.level < this.maxAddLevel; // 最多允许添加的层级
} else return true;
},
downloadTpl(d) {
this.$emit(`downloadTpl`);
},
uploadSuccess(d, f) {
this.$emit(`uploadSuccess`, d, f);
},
uploadError(d, f) {
this.$emit(`uploadError`, d, f);
},
importError(d, f) {
this.$emit(`importError`, d, f);
},
// 聚焦到某一个节点
focusNode(id) {
if (!id) return;
this.$nextTick(() => {
this.$refs.tree.setCurrentKey(id); //高亮显示某个节点
this.$emit(`currentChange`, this.$refs.tree.getCurrentNode());
this.$nextTick(() => {
let dom = document.querySelector(`.el-tree-node.is-current`);
dom &&
dom.scrollIntoView({
behavior: "smooth",
block: "nearest",
inline: "nearest",
}); //缓慢滚动
});
});
},
// 添加根节点
addRoot() {
this.addNode(this.$refs.tree.root, { [this.mainKey]: this.defaultRootId });
},
// 通过id展开指定节点(通常是用于外部调用)
expandNodeById(id) {
let node = this.$refs.tree.getNode(id);
node.expand();
},
//通过id勾选节点
setCheckedKeys(ids) {
this.$refs.tree.setCheckedKeys(ids);
},
// 添加节点
addNode(node, data) {
let resolve = (d) => {
if (data.ID === this.defaultRootId) {
this.treeData.unshift(d);
} else {
data.children || this.$set(data, "children", []);
data.children.push(d);
}
node.expand();
};
let reject = (d) => {
this.rootLoading = false;
node.loading = false;
this.$message.error(d.msg); //添加节点失败
};
this.$emit(`addNode`, { node, data, resolve, reject });
},
// 删除节点
remove(node, data) {
this.$confirm(
(this.data.text || {}).removeConfirmTip ||
`此操作将永久删除该节点及其下面的节点,是否继续?`,
(this.data.text || {}).removeConfirmTitle || `提示`,
{
dangerouslyUseHTMLString: true,
confirmButtonText: `确定`,
cancelButtonText: `取消`,
type: "warning",
}
)
.then(() => {
this.removeNodeData(node, data);
})
.catch(() => {});
},
// 删除节点数据(通过接口向后台删除数据)
removeNodeData(node, data) {
node.loading = true; //出现加载动画
let resolve = (d) => {
node.loading = false;
this.$message.success(`删除成功`);
// 从父节点异步删除子节点
const parent = node.parent;
const children = parent.data.children || parent.data;
const index = children.findIndex((d) => d[this.mainKey] === data[this.mainKey]);
children.splice(index, 1);
// 从显示界面删除节点(有bug,只是删除了树节点的Virtual DOM,实际数据还在)
/* let childNodes = node.parent.childNodes; childNodes.splice( childNodes.findIndex((d) => d.data[this.mainKey] === data[this.mainKey]), 1 ); */
};
let reject = (d) => {
this.rootLoading = false;
node.loading = false;
this.$message.error(d.msg); //删除失败
};
this.$emit(`removeNode`, { node, data, resolve, reject });
},
// 当前选中节点变化时触发的事件
current_change(d) {
this.$emit(`currentChange`, d);
},
//点击节点
nodeClick(d) {
this.focusNodeId = null;
this.$emit(`nodeClick`, d);
},
},
};
</script>
<style lang="scss" scoped>
@import "~@/css/sg";
.sgTree {
$treeHeaderHeight: 30px;
width: 100%;
height: 100%;
display: flex;
flex-wrap: nowrap;
flex-direction: column;
white-space: nowrap;
flex-shrink: 0;
flex-grow: 1;
position: relative;
.tree-header {
display: flex;
justify-content: space-between;
align-items: center;
height: $treeHeaderHeight;
& > .sg-left {
}
& > .sg-right {
}
}
.tree-container {
position: relative;
overflow: auto;
box-sizing: border-box;
height: calc(100% - #{$treeHeaderHeight});
flex-shrink: 0;
flex-grow: 1;
user-select: none;
@include scrollbarHover();
/* >>> .tree-container .el-tree .el-tree-node__content {
cursor: pointer;
} */
>>> .el-tree--highlight-current .el-tree-node.is-current > .el-tree-node__content {
background-color: #409eff22; // 高亮当前选中节点背景
}
>>> .el-tree {
* {
transition: none;
}
.el-tree-node__children {
min-width: max-content; //这样才会出现水平滚动条
}
.normal-tree-node,
.plus-tree-node,
.folder-tree-node {
& + label:not(.el-checkbox) {
/*单行省略号*/
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
flex-shrink: 0;
display: block;
padding: 0 !important;
margin: 0;
width: 20px;
height: 20px;
margin-right: 5px;
background: transparent url("~@/../static/img/fileType/folder/folder.svg")
no-repeat center / contain;
margin-left: 20px;
& ~ span:not(.el-icon-loading) {
width: 100%;
.node-label {
height: 40px;
display: flex;
align-items: center;
}
}
&.expanded {
flex-shrink: 0;
transform: rotate(0deg);
background-image: url("~@/../static/img/fileType/folder/folder-open.svg");
}
&.is-leaf {
background-image: none;
}
}
.normal-tree-node {
margin-left: 10px;
background-image: url("~@/../static/img/fileType/folder/arrow-right.svg");
&.expanded {
transform: rotate(90deg);
background-image: url("~@/../static/img/fileType/folder/arrow-right.svg");
}
&.is-leaf {
background-image: none;
}
}
.plus-tree-node {
margin-left: 10px;
background-image: url("~@/../static/img/fileType/folder/plus.svg");
&.expanded {
background-image: url("~@/../static/img/fileType/folder/minus.svg");
}
&.is-leaf {
background-image: none;
}
}
// 隐藏一级节点的复选框
&.hideRootNodeCheckbox > div > .el-tree-node__content .el-checkbox {
display: none;
}
}
}
}
.tree-el-popover {
.el-button {
padding-top: 0;
padding-bottom: 0;
}
}
</style>
<template>
<div :class="$options.name">
<sgTree
v-loading="loading"
:key="$route.query.BMID + sgTree_fresh"
:treeData="treeData"
:data="treeConfigData"
@currentChange="currentChange"
@addNode="addNode"
@removeNode="removeNode"
:uploadData="{
name: `file`,
accept: '.xls,.xlsx',
actionUrl: `${$d.API_ROOT_URL}/core/column/importColumn`, //批量导入树结构接口
actionData: {
BMID: $global.getBMID(),
PID: `root`,
sgLog: `强哥请求来源:${$options.name}导入栏目xls`,
},
}"
@uploadSuccess="uploadSuccess"
@uploadError="uploadError"
@importError="importError"
@downloadTpl="downloadTpl"
draggable
:allowNodeDrop="allowNodeDrop"
@nodeDragEnd="nodeDragEnd"
/>
</div>
</template>
<script>
import sgTree from "@/vue/components/admin/sgTree";
export default {
name: "sgBody",
components: {
sgTree,
},
data() {
return {
sgTree_fresh: false,
autoId: 0, //自增编号
treeConfigData: {
nodeKey: `ID`, //主键
props: { label: "MC", isLeaf: "leaf" }, //配置选项
iconType: "plus", //节点图标:folder文件夹|normal普通样式|plus加减符号样式
text: {
addRootButtonText: "添加根目录", //添加根节点按钮文本
removeConfirmTitle: "警告!!!", //删除节点提示标题
removeConfirmTip: "此操作将永久删除该节点及其下面的子节点,是否继续?", //删除节点提示内容
},
},
treeData: [],
loading: false,
};
},
created() {
this.initTreeData();
},
methods: {
//初始化数据
initTreeData({ d } = {}) {
this.$global.获取整棵树的数据({
cb: (d) => {
this.treeData = d;
},
});
},
// 拖拽节点相关方法----------------------------------------
allowNodeDrop(draggingNode, dropNode, dropType) {
// 只允许拖拽同级别前后排序
let isPrevOrNext = dropType === "prev" || dropType === "next";
// 同在第一级根节点下
let isSameRootLevel =
draggingNode.level === dropNode.level && draggingNode.level === 1;
// 同在一个节点(非根节点)下
let isSameChildLevel =
draggingNode.parent &&
dropNode.parent &&
draggingNode.parent.data &&
dropNode.parent.data &&
draggingNode.parent.data.ID === dropNode.parent.data.ID;
return isPrevOrNext && (isSameRootLevel || isSameChildLevel);
},
nodeDragEnd(draggingNode, dropNode, dropType, ev) {
// 只允许拖拽同级别前后排序
let isBeforeOrAfter = dropType === "before" || dropType === "after";
if (isBeforeOrAfter) {
/* console.log("被拖拽的节点", draggingNode.data.MC, draggingNode.data.PXZ); console.log("停靠的节点", dropNode.data.MC, dropNode.data.PXZ); */
let theSameLevelDatas = (dropNode.parent.childNodes || []).map((v) => v.data); // 获取同一级节点数据
theSameLevelDatas.forEach((v, i) => (v.PXZ = i)); //重新排序
// console.log("拖拽排序", theSameLevelDatas); //这里需要调用后台接口
let IDS = theSameLevelDatas.map((v) => v.ID); //排序后的ID顺序数组
let data = {
IDS,
sgLog: `强哥请求来源:${this.$options.name}更改同一层级树节点排序值`,
};
this.$d.修改节点排序({
data,
r: {
s: (d) => {
// console.log("【成功】", d);
},
},
});
}
},
// ----------------------------------------
// 获取当前聚焦节点的数据
currentChange(d) {
console.log(``, d);
},
// 添加节点
addNode({ data, resolve }) {
this.$d.新增节点({
data: {
MC: `新增节点名称(${++this.autoId})`,
},
doing: {
l: { show: () => (this.loading = true), close: () => (this.loading = false) },
s: (d) => resolve(d),
f: (d) => reject(d), //删除失败
},
});
},
// 删除节点
removeNode({ node, data, resolve, reject }) {
this.$d.删除节点({
data: { ID: data.ID },
doing: {
s: (d) => resolve(d),
f: (d) => reject(d), //删除失败
},
});
},
updateList(d) {},
uploadSuccess(d, f) {
this.sgTree_fresh = !this.sgTree_fresh;
},
uploadError(d, f) {
this.$message.error(d.msg);
},
// 导入失败
importError(d, f) {},
// 下载导入模板
downloadTpl(d) {},
},
};
</script>