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>

 

posted @ 2025-02-10 14:00  潇可爱❤  阅读(20)  评论(0)    收藏  举报