父组件:
情况一:
接口获取树结构扁平化后剔除重复值再转为正确的树结构
情况二:
接口获取扁平化数据不需要二次转换,直接对比重复值进行剔除再转为正确的树结构
结构树使用了纯CSS 多层树结构:https://blog.csdn.net/juanzhang91008/article/details/141335075
vue项目实现图片缩放与拖拽功能:https://blog.csdn.net/qq_63310300/article/details/128872535
<template>
<div class="home" ref="back_box">
<div class="box" draggable="true" @dragstart="dragStart($event)" @dragend="dragEnd($event)" @wheel="handlewheel" :style="`left:${elLeft}px;top:${elTop}px;width:${elWidth}px;height:${elHeight}px;`">
<div style="display: flex; justify-content: left; align-items: center" class="text" :style="`left:${(0 * this.elWidth) / 100}px;top:${(25 * this.elHeight) / 100}px; transform:scale(${meter_zoom})`">
<div class="tree">
<span class="title">{{ treeData[0].name }}</span>
</div>
<div class="flex-column">
<tree-node v-for="node in treeData[0].children" :key="node.id" :node="node"></tree-node>
</div>
</div>
</div>
</div>
</template>
<script>
import TreeNode from './components/TreeNode.vue';
export default {
name: 'HomeView',
components: {
TreeNode
},
data() {
return {
initWidth: 0, // 父元素的宽
initHeight: 0, // 父元素的高
startclientX: 0,
startclientY: 0,
elLeft: 100,
elTop: 0,
zoom: 1, //缩放比例
elWidth: 0, // 元素宽
elHeight: 0, // 元素高
meter_zoom: 0, // 子元素的缩放比例
list: [
{ id: 1, parentId: 0, name: 'Node 1', isOpen: true },
{ id: 2, parentId: 1, name: 'Node 1.1', isOpen: true },
{ id: 4, parentId: 2, name: 'Node 1.1.1', isOpen: true },
{ id: 5, parentId: 2, name: 'Node 1.1.2', isOpen: true },
{ id: 3, parentId: 1, name: 'Node 1.2', isOpen: true },
{ id: 6, parentId: 3, name: 'Node 1.2.1', isOpen: true },
{ id: 9, parentId: 6, name: 'Node 1.2.1', isOpen: true },
{ id: 10, parentId: 6, name: 'Node 1.2.2', isOpen: true },
{ id: 11, parentId: 6, name: 'Node 1.2.3', isOpen: true },
{ id: 7, parentId: 3, name: 'Node 1.2.1', isOpen: true },
{ id: 7, parentId: 3, name: 'Node 1.2.1', isOpen: true }
], //扁平结构的数据(从接口获取)
noRepeatList: [], //存放剔除重复值之后的数据
treeData: [] //将扁平化数据转换为树结构
};
},
created() {
// this.list = this.treeToFlat(this.treeData, 0); // 打平数据
this.noRepeatList = this.noRepeatList.concat(this.comparativeData()); //对比子级数据与现有数据是否存在重复值
let tree = this.toTree(this.noRepeatList); //将扁平化数据转换为树结构
this.treeData = tree;
// console.log(JSON.stringify(tree));
},
mounted() {
this.initBodySize();
},
methods: {
toTree(items) {
const tree = []; //存放最终的树状结构
const itemMap = {}; //存放每个节点数据
for (const item of items) {
const { id } = item;
itemMap[id] = { ...item, children: [] }; //每个节点增加一个children属性,用来存放子节点
}
// 遍历所有节点,将每个节点放到其父节点的children数组中
for (const item of items) {
const { id, parentId } = item;
// 如果是根节点,则直接放入结果数组中
if (parentId == null || parentId == 0) {
tree.push(itemMap[id]);
} else {
// 如果不是根节点,则将当前节点放入其父节点的children数组中
// 子元素的parentId 等于 父节点的id itemMap[parentId] 父节点 //itemMap[id] 当前节点
if (itemMap[parentId]) itemMap[parentId].children.push(itemMap[id]);
}
}
return tree;
},
//对比是否存在重复数据
comparativeData() {
let arr = [];
let result = this.list.map((i) => {
let one = this.noRepeatList.findIndex((k) => k.id == i.id);
if (one < 0) {
arr.push(i);
}
});
return arr;
},
// 打平函数
treeToFlat(data, parentId, res = []) {
data.forEach((v) => {
res.push({
id: v.id,
parentId: parentId,
name: v.name,
isOpen: true
});
if (v.children && v.children.length) {
this.treeToFlat(v.children, v.id, res);
}
});
return res;
},
// 拖拽开始时间
dragStart(e) {
console.log(e);
// 记录元素拖拽初始位置
this.startclientX = e.clientX;
this.startclientY = e.clientY;
},
// 拖拽完成事件
dragEnd(e) {
console.log(e);
// 计算偏移量
this.elLeft += e.clientX - this.startclientX;
this.elTop += e.clientY - this.startclientY;
},
// 页面初始化
initBodySize() {
this.initWidth = this.$refs.back_box.clientWidth;
this.initHeight = this.initWidth * (1080 / 1920);
this.elWidth = this.initWidth * (100 / 1400); // 元素宽
this.elHeight = this.initHeight * (100 / 50); // 元素高
this.meter_zoom = this.elWidth / 100; // 子元素的缩放比例
},
// 鼠标滚轮事件
handlewheel(e) {
if (e.wheelDelta < 0) {
this.zoom -= 0.05;
} else {
this.zoom += 0.05;
}
// 如果放大超过3 就限制 不能一直放大
if (this.zoom >= 1.5) {
this.zoom = 1.5;
alert('已放至最大');
return;
}
// 同理 缩小也是
if (this.zoom <= 0.5) {
this.zoom = 0.5;
alert('已放至最小');
return;
}
this.elWidth = this.initWidth * (100 / (1920 / 2)) * this.zoom;
this.elHeight = this.initHeight * (100 / (1080 / 2)) * this.zoom;
this.meter_zoom = this.elWidth / 100;
}
}
};
</script>
<style lang="scss" scoped>
* {
margin: 0;
padding: 0;
}
.home {
//
width: 1200px;
height: 662px;
position: relative;
.box {
width: 100px;
height: 100px;
user-select: none;
// background: blue;
position: absolute;
z-index: 2;
.text {
width: 100px;
height: 100px;
transform-origin: 0 0;
font-size: 16px;
position: absolute;
}
}
}
.flex-column {
display: flex;
flex-direction: column;
align-items: flex-start;
margin-left: 20px;
}
.tree .title {
display: block;
padding: 10px 20px;
border-radius: 10px;
background: #0092ee;
color: white;
width: 150px;
text-align: center;
margin: 5px 0;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
margin-right: 60px;
position: relative;
font-size: 14px;
font-weight: 600;
margin-left: 20px;
margin-bottom: 25%;
}
.tree .title:after {
content: '';
width: 50px;
height: 1px;
background: #53a1ef;
position: absolute;
right: -50px;
top: 50%;
}
</style>
子组件:
<template><!-- <div @click="node.isOpen = !node.isOpen">{{ node.name }}</div> --><div class="item"><span class="title" @click="node.isOpen = !node.isOpen">{{ node.name }}<span class="img"> <i class="z-icon-circle-plus"></i></span></span><!-- 递归调用自身组件 --><div class="flex-column" v-if="node.isOpen && node.children && node.children.length"><TreeNode v-for="childrenNode in node.children" :key="childrenNode.id" :node="childrenNode"></TreeNode></div></div> </template> <script> import TreeNode from './TreeNode.vue';export default {name: 'TreeNode',components: {TreeNode},props: {node: {type: Object,default() {return {};}}},data() {return {};},watch: {node: {handler(val) {console.log(val);},deep: true,immediate: true}},methods: {} }; </script> <style lang="scss" scoped> .flex-column {display: flex;flex-direction: column;align-items: flex-start;margin-left: 20px; }.item {position: relative;display: flex;align-items: center; } .img {position: absolute;right: 2px; } .item:before {content: '';position: absolute;left: -30px;width: 1px;/*这里的高度要等于自身高度 + 节点间margin高度 */height: calc(100% + 20px);top: 0;transform: translateY(-10px);background: #53a1ef; }.item .title:before {content: '';position: absolute;left: -30px;top: 50%;width: 30px;height: 1px;background: #53a1ef; }.item .title {display: block;padding: 10px 20px;border-radius: 10px;background: #0092ee;color: rgb(248, 248, 248);width: 150px;text-align: center;margin: 5px 0;box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);margin-right: 60px;position: relative;font-size: 14px;font-weight: 600; }.item .title:after {content: '';width: 50px;height: 1px;background: #53a1ef;position: absolute;right: -50px;top: 50%; }.item .title a {text-decoration: none;color: white;font-size: 14px;font-weight: 600; }.item .title a:hover {cursor: pointer; }.item:first-child:before {height: calc(50% + 10px);transform: unset;top: 50%; }.item:last-child:before {height: calc(50% + 10px);transform: unset;top: -10px; }.item .title:not(:has(+ .flex-column)):after {display: none; }/* 下级只有一层时,隐藏竖线 */ .item:only-of-type:before {content: '';position: absolute;left: -30px;width: 1px;/*这里的高度要等于自身高度 + 节点间margin高度 */height: calc(100% + 20px);top: 0;transform: translateY(-10px);background: white; }// .title { // display: block; // padding: 10px 20px; // border-radius: 10px; // background: #0092ee; // color: white; // width: 150px; // text-align: center; // margin: 5px 0; // box-shadow: 0 0 10px rgba(0, 0, 0, 0.05); // margin-right: 60px; // position: relative; // font-size: 14px; // font-weight: 600; // margin-left: 20px; // }// .title:after { // content: ''; // width: 50px; // height: 1px; // background: #53a1ef; // position: absolute; // right: -50px; // top: 50%; // } </style>