css 树结构转为扁平化数据,剔除重复值再转换为树结构
父组件:
情况一:
接口获取树结构扁平化后剔除重复值再转为正确的树结构
情况二:
接口获取扁平化数据不需要二次转换,直接对比重复值进行剔除再转为正确的树结构
结构树使用了纯CSS 多层树结构:https://blog.csdn.net/juanzhang91008/article/details/141335075
vue项目实现图片缩放与拖拽功能:https://blog.csdn.net/qq_63310300/article/details/128872535
<template>
<z-dialog title="新增/编辑科目" :visible.sync="isDialog" :fullscreen="true" @close="closeDialog()" top="30px" class="croup-type-dialog" :close-on-click-modal="false">
<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})`"
v-if="treeData && treeData.length > 0"
>
<div class="tree">
<div class="tree_title">
<z-tooltip placement="top" popper-class="z-tooltip-demo__more-content" effect="light">
<div slot="content" class="tree_detail">
<ul>
<li>科目名称:{{ treeData[0].indexName }}</li>
<li>科目编码:{{ treeData[0].indexCode }}</li>
<li>模块名称:{{ treeData[0].indexGroupName }}</li>
<li>模块编码:{{ treeData[0].indexGroupCode }}</li>
<li>指标:{{ treeData[0].indexCategory }}</li>
</ul>
</div>
<div>
<p>{{ treeData[0].indexCode || '' }}</p>
<p>
{{ treeData[0].indexName || '' }}{{ treeData[0].isOpen }}
<i class="z-icon-copy-document icon" @click="onCopy(treeData[0].indexCode)"></i>
</p>
</div>
</z-tooltip>
<span class="img" v-if="treeData[0].isChild">
<img v-show="!treeData[0].isOpen" @click="node1Click(treeData[0])" width="20px" src="@/assets/image/icon/add.png" />
<img v-show="treeData[0].isOpen" @click="node1Click(treeData[0])" width="22px" src="@/assets/image/icon/minus.png" />
</span>
</div>
</div>
<div v-if="treeData[0].children && treeData[0].children.length > 0 && treeData[0].isOpen">
<div class="flex-column">
<tree-node v-for="(node, index) in treeData[0].children" :key="node.indexCode" :node="node" @addTree="addTree" @minusTree="minusTree"></tree-node>
</div>
</div>
</div>
</div>
</div>
</z-dialog>
</template>
<script>
import TreeNode from './components/TreeNode.vue';
import api from '@/api';
export default {
name: 'SubjectDependenceTree',
components: {
TreeNode
},
props: {
isTreeDialog: Boolean,
treeRow: {
type: Object,
default: () => {
return {};
}
}
},
data() {
return {
isDialog: false,
initWidth: 0, // 父元素的宽
initHeight: 0, // 父元素的高
startclientX: 0,
startclientY: 0,
elLeft: 100,
elTop: 0,
zoom: 1, //缩放比例
elWidth: 0, // 元素宽
elHeight: 0, // 元素高
meter_zoom: 0, // 子元素的缩放比例
list: [], //扁平结构的数据(从接口获取)
noRepeatList: [], //存放剔除重复值之后的数据
duplicateValueList: [], //存放重复值数据
treeData: [], //将扁平化数据转换为树结构,
childrenNode: {}
};
},
watch: {
isTreeDialog(val) {
this.isDialog = val;
this.initWidth = 0; // 父元素的宽
this.initHeight = 0; // 父元素的高
this.startclientX = 0;
this.startclientY = 0;
this.elLeft = 100;
this.elTop = 0;
this.zoom = 1; //缩放比例
this.elWidth = 0; // 元素宽
this.elHeight = 0; // 元素高
this.meter_zoom = 0; // 子元素的缩放比例
if (val) {
let params = {
indexCode: this.treeRow.indexCode, //科目编码
reportTheme: this.treeRow.reportTheme, //主题
isFirst: true //初始化三层依赖树true,后面展开第四层或往后 false}
};
this.searchQueryIndexTree(params);
this.$nextTick(() => {
setTimeout(() => {
this.initBodySize();
}, 300);
});
}
}
},
methods: {
// 查询结构树初始化前三层
async searchQueryIndexTree(params) {
this.list = [];
const res = await api.queryIndexTree(params);
if (res.success) {
this.list = res.data;
// this.list = this.treeToFlat(this.treeData, 0); // 打平数据
this.noRepeatList = this.noRepeatList.concat(this.comparativeData()); //剔除重复数据
this.treeData = this.toTree(this.noRepeatList); //将扁平化数据转换为树结构
if (this.treeData[0].children.length < 1) {
document.styleSheets[0].addRule('.tree .tree_title::after', 'display: none;');
}
} else {
this.$message.error(`${res.message}`);
}
},
//对比是否存在重复数据,同层级存在重复数据后点击的节点isChild=false,isOpen=false
comparativeData() {
let arr = [];
let hasUsefulNode = false;
let result = this.list.map((i) => {
let one = this.noRepeatList.findIndex((k) => k.indexCode == i.indexCode);
if (one < 0) {
hasUsefulNode = true;
arr.push(i);
}
});
if (!hasUsefulNode) {
let one = this.noRepeatList.find((k) => k.indexCode == this.childrenNode);
if (one) {
one.isChild = false;
one.isOpen = false;
}
}
return arr;
},
//打平数据转换为树结构
toTree(items) {
const tree = []; //存放最终的树状结构
const itemMap = {}; //存放每个节点数据
for (const item of items) {
const { indexCode } = item;
itemMap[indexCode] = { ...item, children: [] }; //每个节点增加一个children属性,用来存放子节点
}
// 遍历所有节点,将每个节点放到其父节点的children数组中
for (const item of items) {
const { indexCode, parentId } = item;
// 如果是根节点,则直接放入结果数组中
if (indexCode == parentId) {
//if (parentId == null || parentId == 0) {
tree.push(itemMap[indexCode]);
} else {
// 如果不是根节点,则将当前节点放入其父节点的children数组中
// 子元素的parentId 等于 父节点的id itemMap[parentId] 父节点
//itemMap[indexCode] 当前节点
if (itemMap[parentId]) itemMap[parentId].children.push(itemMap[indexCode]);
}
}
return tree;
},
closeDialog() {
this.list = []; //扁平结构的数据(从接口获取)
this.noRepeatList = []; //存放剔除重复值之后的数据
this.treeData = []; //将扁平化数据转换为树结构,
this.isDialog = false;
this.$emit('closeSubjectDependenceTree');
},
node1Click(node) {
this.treeData[0].isOpen = !this.treeData[0].isOpen;
if (!this.treeData[0].isOpen) {
document.styleSheets[0].addRule('.tree .tree_title::after', 'display: none;');
} else {
document.styleSheets[0].addRule('.tree .tree_title::after', 'display: block;');
}
},
minusTree(dataNode, event) {
this.treeStyle(dataNode, event);
},
// 展开子结构
addTree(dataNode, event) {
this.childrenNode = dataNode.indexCode;
let node = this.noRepeatList.find((item) => item.indexCode === dataNode.indexCode);
if (node) {
node.isOpen = true;
}
let params = {
indexCode: dataNode.indexCode, //科目编码
reportTheme: this.treeRow.reportTheme, //主题
isFirst: false //初始化三层依赖树true,后面展开第四层或往后 false
};
this.searchQueryIndexTree(params);
this.treeStyle(dataNode, event);
},
treeStyle() {
this.$nextTick(() => {
// 滚动到指定位置
const container = document.querySelector('.home');
let w = (container.scrollWidth - event.clientX * 2) / 2;
let h = (container.scrollHeight - event.clientY) / 2;
if (w < -200) {
this.elWidth = container.scrollWidth; // 元素宽
this.elLeft = w;
} else {
this.initWidth = this.$refs.back_box.clientWidth;
this.elWidth = this.initWidth * (100 / 1400); // 元素宽
this.elLeft = 100;
}
if (this.list && this.list.length > 20) {
this.initWidth = this.$refs.back_box.clientWidth;
this.initHeight = this.initWidth * (1080 / 1920);
this.elTop = this.initHeight * (100 / 40);
} else {
this.elTop = 0;
}
console.log(this.initHeight, this.elTop);
setTimeout(() => {
container.scrollLeft = container.scrollWidth; // 滚动到最右端
}, 300);
// this.elTop = h;
});
},
//复制
onCopy(text) {
if (text) {
navigator.clipboard.writeText(text).then(() => {
this.$message.success('复制成功');
});
} else {
this.$message.error('复制内容不能为空');
}
},
// 打平函数
treeToFlat(data, parentId, res = []) {
data.forEach((v) => {
res.push({
indexCode: v.indexCode,
parentId: parentId,
name: v.name,
isOpen: v.isOpen, //控制是否展开
isChild: v.isChild // 控制是否有子级
});
if (v.children && v.children.length) {
this.treeToFlat(v.children, v.indexCode, res);
}
});
return res;
},
// 拖拽开始时间
dragStart(e) {
// console.log('开始', e.clientX, e.clientY);
// 记录元素拖拽初始位置
this.startclientX = e.clientX;
this.startclientY = e.clientY;
},
// 拖拽完成事件
dragEnd(e) {
// console.log('结束', e.clientX, e.clientY);
// 计算偏移量
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 / 70); // 元素高
this.meter_zoom = 0.6; // 子元素的缩放比例
console.log(this.list && this.list.length);
if (this.list && this.list.length > 20) {
this.elTop = this.initHeight * (100 / 40);
console.log(this.elTop);
}
},
// 鼠标滚轮事件
handlewheel(e) {
let box = this.$refs.back_box;
// 判断是否按下Ctrl键
if (e.ctrlKey) {
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 / 150;
}
}
}
};
</script>
<style lang="scss" scoped>
.croup-type-dialog {
/deep/.z-dialog__body {
height: calc(100vh - 70px);
}
/deep/ .z-dialog {
border-radius: 0px;
}
}
* {
margin: 0;
padding: 0;
}
.home {
/* */
width: 100%;
height: 100%;
position: relative;
overflow: auto;
.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 .tree_title {
display: block;
padding: 10px 10px;
border-radius: 10px;
background: #0092ee;
color: white;
width: 300px;
text-align: left;
margin: 5px 0;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
margin-right: 60px;
position: relative;
font-size: 16px;
font-weight: 600;
margin-left: 20px;
/* // margin-bottom: 25%; */
.img {
position: absolute;
top: 35%;
right: -19px;
z-index: 9999999;
}
.icon {
float: right;
color: rgb(255, 255, 255);
font-size: 20px;
margin-top: 5px;
}
}
.tree_detail li {
margin: 8px 0;
font-size: 12px;
}
.tree .tree_title:after {
content: '';
width: 50px;
height: 1px;
background: #53a1ef;
position: absolute;
right: -50px;
top: 50%;
.img {
position: absolute;
right: -19px;
width: 22px;
height: 22px;
top: 18px;
/* */
z-index: 9999999;
}
}
</style>
子组件:
<template>
<!-- <div @click="node.isOpen = !node.isOpen">{{ node.name }}</div> -->
<div class="item">
<span class="title">
<z-tooltip placement="top" popper-class="z-tooltip-demo__more-content" effect="light">
<div slot="content" class="tree_detail">
<ul>
<li>科目名称:{{ node.indexName }}</li>
<li>科目编码:{{ node.indexCode }}</li>
<li>模块名称:{{ node.indexGroupName }}</li>
<li>模块编码:{{ node.indexGroupCode }}</li>
<li>指标:{{ node.indexCategory }}</li>
</ul>
</div>
<div>
<div>
<p>{{ getIndexCode(node.indexCode) || '' }}</p>
<p>
{{ node.indexName || '' }}{{ node.isOpen }}
<i class="z-icon-copy-document icon" @click="onCopy(node.indexCode)"></i>
</p>
</div>
</div>
</z-tooltip>
<span class="img" v-if="node.isChild">
<img v-show="!node.isOpen" @click="addTree(node, $event)" width="20px" src="@/assets/image/icon/add.png" />
<img v-show="node.isOpen" @click="minusTree(node, $event)" width="22px" src="@/assets/image/icon/minus.png" />
</span>
</span>
<!-- 递归调用自身组件 -->
<div class="flex-column" v-if="node.children && node.children.length > 0 && node.isOpen">
<TreeNode v-for="childrenNode in node.children" :key="childrenNode.indexCode" :node="childrenNode" @addTree="addTree" @minusTree="minusTree"></TreeNode>
</div>
</div>
</template>
<script>
import TreeNode from './TreeNode.vue';
import api from '@/api';
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: {
getIndexCode(indexCode) {
if (indexCode) {
if (indexCode.length > 28) {
let result = indexCode.slice(0, 28) + '...';
return result;
} else {
return indexCode;
}
}
return '';
},
// 点击节点查询子级,如果展开又收起,再次点击展开children又数据不再次查询
addTree(dataNode, event) {
if (dataNode.children.length == 0) {
this.$emit('addTree', dataNode, event);
} else if (this.node.indexCode == dataNode.indexCode) {
this.node.isOpen = !this.node.isOpen;
}
},
minusTree(dataNode, event) {
if (this.node.indexCode == dataNode.indexCode) {
this.node.isOpen = !this.node.isOpen;
}
this.$emit('minusTree', dataNode, event);
},
//复制
onCopy(text) {
if (text) {
navigator.clipboard.writeText(text).then(() => {
this.$message.success('复制成功');
});
} else {
this.$message.error('复制内容不能为空');
}
}
}
};
</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;
}
.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: -50px;
top: 50%;
width: 50px;
height: 1px;
background: #53a1ef;
}
.tree_detail li {
font-size: 12px;
margin: 8px 0;
}
.img {
position: absolute;
right: -19px;
top: 35%;
z-index: 9999999;
}
.item .title {
display: block;
padding: 10px 10px;
border-radius: 10px;
background: #0092ee;
color: white;
width: 300px;
text-align: left;
margin: 5px 0;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.05);
margin-right: 60px;
position: relative;
font-size: 16px;
font-weight: 600;
margin-left: 20px;
.icon {
float: right;
color: rgb(255, 255, 255);
font-size: 20px;
margin-top: 5px;
}
}
.item .title:after {
content: '';
width: 50px;
height: 1px;
background: #53a1ef;
position: absolute;
right: -50px;
top: 50%;
}
.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;
}
</style>
浙公网安备 33010602011771号