股权穿透图

该文章已生成可运行项目,
<template>
  <!-- 内容框 -->
  <div class="stock-wrap">
    <div class="header" @click="handleLinkOther">
      <div class="logo" :style="{ backgroundColor: getRandomColor() }">
        {{ baseInfo.companyName ? baseInfo.companyName.charAt(0) : "" }}
      </div>
      <div class="company-wrap">
        <div class="com-name">
          {{ baseInfo.companyName || "-" }}
        </div>
        <div class="status-wrap">
          <div
            v-for="(item, index) in baseInfo.tagsList"
            :key="index"
            class="status"
            :class="getStatusClass(item, index)"
          >
            {{ item }}
          </div>
        </div>
      </div>
    </div>
    <div id="appc" class="echarts" v-loading="loading"></div>
    <div class="filter-box" @click="drawer = true">
      <el-icon><Filter /></el-icon>
      <div>筛选</div>
    </div>
    <el-drawer
      v-model="drawer"
      size="45%"
      direction="rtl"
      :before-close="handleClose"
      header-class="echarts-chose-drawer-header"
    >
      <template #header>
        <h4 class="fiter-title">筛选</h4>
      </template>
      <div class="slider-wrap" style="margin-top: -10px">
        <div class="slider-item">
          <div class="slider-title">股东持股比例</div>
          <div class="slider-demo-block">
            <div class="slider-w">
              <el-slider
                range
                v-model="control.parent"
                :show-tooltip="false"
                @change="handleChange"
              />
              <div class="show-tip">
                <span>{{ control.parent[0] }}%</span>
                <span>{{ control.parent[1] }}%</span>
              </div>
            </div>
            <span class="btn" @click="handleSlier('parent')">不限</span>
          </div>
        </div>
        <div class="slider-item">
          <div class="slider-title">对外投资比例</div>

          <div class="slider-demo-block">
            <div class="slider-w">
              <el-slider
                range
                v-model="control.child"
                :show-tooltip="false"
                @change="handleChange"
              />
              <div class="show-tip">
                <span>{{ control.child[0] }}%</span>
                <span>{{ control.child[1] }}%</span>
              </div>
            </div>
            <span class="btn" @click="handleSlier('child')">不限</span>
          </div>
        </div>
      </div>
    </el-drawer>
  </div>
</template>
<script>
import * as $d3 from "d3";
import { dataTemp } from "./data";
import { reactive, toRaw } from "vue";
import {
  listEnterpriseInforListAll,
  listPartnerInfoAll,
  listInvestInfoAll,
  equityPenetrationChartQuery,
} from "@/api/userManage";
// ---------------------------- 换了写法 start-------------------------
// 筛选过滤 右下角 股东持股比例
function filterTree1(node, control) {
  if (node.parents && node.parents.length > 0) {
    node.parents = node.parents.filter((item) => {
      const percent = parseFloat(item.percent);
      return percent >= control.parent[0] && percent <= control.parent[1];
    });
    if (node.parents && node.parents.length > 0) {
      node.parents.map((item) => {
        filterTree1(item);
      });
    }
  }
}
// 筛选过滤 右下角 对外投资比例
function filterTree2(node, control) {
  if (node.children && node.children.length > 0) {
    node.children = node.children.filter((item) => {
      const percent = parseFloat(item.percent);
      return percent >= control.child[0] && percent <= control.child[1];
    });
    if (node.children && node.children.length > 0) {
      node.children.map((item) => {
        filterTree1(item);
      });
    }
  }
}

function keep2Dot(percentStr) {
  const percentNum = parseFloat(percentStr.replace("%", ""));

  // 保留两位小数并重新添加百分号
  return percentNum.toFixed(2) + "%";
}
// 获取股东
async function parentsTree(node, options = {}) {
  const {
    currentDepth = 1,
    maxDepth = 3,
    concurrency = 5,
    onProgress,
  } = options;

  if (currentDepth >= maxDepth) return;

  // 如果没有子节点数据,尝试获取
  if (!node.parents || node.parents.length === 0) {
    try {
      const obj = await listPartnerInfoAll({ companyId: node.id });
      if (obj.data) {
        node.parents = obj.data.map((itemP, index) => {
          return {
            id: itemP.id,
            name: itemP.investedCompanyName || "",
            percent: itemP.stockName,
            parents: null,
            status: itemP.status,
            econKind: itemP.econKind,
          };
        });
      }
      if (onProgress) onProgress(node, currentDepth);
    } catch (error) {
      console.error(`Failed to fetch parents for ${node.id}:`, error);
      node.parents = null; // 或者标记为错误状态
      return;
    }
  }

  // 使用Promise.all控制并发
  const batches = [];
  for (let i = 0; i < node.parents.length; i += concurrency) {
    const batch = node.parents.slice(i, i + concurrency);
    batches.push(batch);
  }

  for (const batch of batches) {
    await Promise.all(
      batch.map((child) =>
        parentsTree(child, {
          currentDepth: currentDepth + 1,
          maxDepth,
          concurrency,
          onProgress,
        })
      )
    );
  }
}

// 获取子公司
async function childrenTree(node, options = {}) {
  const {
    currentDepth = 1,
    maxDepth = 3,
    concurrency = 5,
    onProgress,
  } = options;

  if (currentDepth >= maxDepth) return;

  // 如果没有子节点数据,尝试获取
  if (!node.children || node.children.length === 0) {
    try {
      const obj = await listInvestInfoAll({ companyId: node.id });
      if (obj.data) {
        node.children = obj.data.map((itemP, index) => {
          return {
            id: itemP.investedCompanyId || itemP.companyId,
            name: itemP.investedCompanyName || "",
            percent: itemP.stockPercent,
            children: null,
            status: itemP.status,
            econKind: itemP.econKind,
          };
        });
      }
      if (onProgress) onProgress(node, currentDepth);
    } catch (error) {
      console.error(`Failed to fetch children for ${node.id}:`, error);
      node.children = null; // 或者标记为错误状态
      return;
    }
  }

  // 使用Promise.all控制并发
  const batches = [];
  for (let i = 0; i < node.children.length; i += concurrency) {
    const batch = node.children.slice(i, i + concurrency);
    batches.push(batch);
  }

  for (const batch of batches) {
    await Promise.all(
      batch.map((child) =>
        childrenTree(child, {
          currentDepth: currentDepth + 1,
          maxDepth,
          concurrency,
          onProgress,
        })
      )
    );
  }
}
// ---------------------------- 换了写法 end-------------------------

function transformCompanyStructure(originalData) {
  // 处理母公司信息
  const result = {
    id: "",
    name: "",
    children: [],
    parents: [],
  };

  // 处理股东信息(parents)
  if (originalData.partnerInfoList && originalData.partnerInfoList.length > 0) {
    result.parents = processPartnerList(originalData.partnerInfoList);
  }

  // 处理对外投资信息(children)
  if (
    originalData.foreignInvestmentList &&
    originalData.foreignInvestmentList.length > 0
  ) {
    result.children = processInvestmentList(originalData.foreignInvestmentList);
  }

  return result;

  // 递归股东列表
  function processPartnerList(investments) {
    return investments.map((investment) => {
      const item = {
        id: investment.pkeyNo || investment.id,
        name: investment.stockName,
        percent: investment.stockPercent
          ? keep2Dot(investment.stockPercent)
          : "",
        stockType: investment.stockType,
      };
      // 如果有,递归处理
      if (investment.children && investment.children.length > 0) {
        item.parents = processPartnerList(investment.children);
      }

      return item;
    });
  }

  // 递归处理投资列表
  function processInvestmentList(investments) {
    return investments.map((investment) => {
      const item = {
        id: investment.investedCompanyId,
        name: investment.investedCompanyName,
        percent: investment.stockPercent
          ? keep2Dot(investment.stockPercent)
          : "",
        stockType: investment.stockType || "企业股东",
      };

      // 如果有子投资,递归处理
      if (investment.children && investment.children.length > 0) {
        item.children = processInvestmentList(investment.children);
      }

      return item;
    });
  }
}
export default {
  name: "StockEcharts",
  data() {
    return {
      drawer: false,
      d3: $d3,
      data: {},
      dataInit: {}, // 初始化第一次获取用户的层级
      companyId: "", //  小米 0d180abd7096e3f6a501d27082b61a60 成都 b92b4d531bcbe17f678bfe5cb5f25962,
      companyName: "",
      companyIdSelf: "", // 用户点击后的公司id
      baseInfo: {},
      loading: false,
      control: {
        parent: [0, 100],
        child: [0, 100],
      },
    };
  },
  mounted() {
    // 小米通讯技术 863e13b3db1654fadcb5a1789f4b5f18
    // 小米科技 0d180abd7096e3f6a501d27082b61a60
    this.constructor();
    document.title = "股权穿透图";
    const query = this.$route.query;
    const companyId = query.companyId;
    const companyName = query.companyName;
    if (companyId) {
      this.companyId = decodeURIComponent(companyId);
      this.companyName = decodeURIComponent(companyName);
    } else {
      this.companyId = "863e13b3db1654fadcb5a1789f4b5f18"; //  ""; c02a9a5543164b7d4de98dcf11d1c73a 'dc29b8a74e00fe3d41047a72f4a2c3ab'
      this.companyName = "小米";
    }
    const tagsList = [];
    if (query.status) {
      tagsList.push(decodeURIComponent(query.status.split("(")[0]));
    }
    if (query.econKind) {
      tagsList.push(decodeURIComponent(query.econKind.split("(")[0]));
    }
    this.baseInfo = {
      companyName: this.companyName,
      id: this.companyId,
      tagsList,
    };
    this.getData();
  },

  methods: {
    // 过滤筛选
    handleChange() {
      const MyData = JSON.parse(JSON.stringify(this.dataInit));
      filterTree1(MyData, this.control);
      filterTree2(MyData, this.control);
      this.data = MyData; // MyData;
      // 重新渲染
      this.drawChart({
        type: "fold",
      });
    },
    // 触发筛选
    handleSlier(type) {
      this.control[type] = [0, 100];
      this.handleChange();
    },
    getData() {
      this.loading = true;
      equityPenetrationChartQuery(this.companyId)
        .then((res) => {
          const temp = transformCompanyStructure(res.data || {});
          temp.id = this.companyId;
          temp.name = this.companyName;

          this.data = temp; // MyData;

          this.dataInit = JSON.parse(JSON.stringify(temp));

          this.drawChart({
            type: "fold",
          });
        })
        .finally(() => {
          this.loading = false;
        });
    },
    // 股权树
    constructor(options) {
      // 宿主元素选择器
      this.el = document.getElementById("appc");
      // 一些配置项
      this.config = {
        // 节点的横向距离
        dx: 190,
        // 节点的纵向距离
        dy: 80,
        // svg的viewBox的宽度 用d3的接口赋值
        width: 0,
        // svg的viewBox的高度
        height: 0,
        // 节点的矩形框宽度
        rectWidth: 170,
        // 节点的矩形框高度
        rectHeight: 40,
      };
      this.svg = null;
      this.gAll = null;
      this.gLinks = null;
      this.gNodes = null;
      // 给树加坐标点的方法
      this.tree = null;
      // 子公司树的根节点
      this.rootOfDown = null;
      // 股东树的根节点
      this.rootOfUp = null;
      this.scaleFactor = 1;
    },

    // 初始化树结构数据
    drawChart(options) {
      // 树的源数据

      this.originTreeData = this.data;

      // 宿主元素的d3选择器对象
      let host = this.d3.select(this.el);
      // 宿主元素的DOM,通过node()获取到其DOM元素对象
      let dom = host.node();
      // 宿主元素的DOMRect
      let domRect = dom.getBoundingClientRect();
      // 设置svg的宽度和高度
      this.config.width = domRect.width;
      this.config.height = domRect.height;
      let oldSvg = this.d3.select("svg");
      // 如果宿主元素中包含svg标签了,那么则删除这个标签,再重新生成一个
      if (!oldSvg.empty()) {
        oldSvg.remove();
      }

      // 确定元素的位置 用户坐标系​(逻辑坐标系)到 ​视口坐标系​(实际显示)的映射关系
      // "viewBox" 第二个参数是个数组(4个值) [min-x,min-y,width,height]
      // 如果第一第二个参数设置为0,则根节点是在左上角,需要根据可是范围进行偏移,偏移的时候,考虑是否有父节点
      const svg = this.d3
        .create("svg")
        .attr("viewBox", () => {
          // 获取根节点顶部的数据
          let parentsLength = this.originTreeData.parents
            ? this.originTreeData.parents.length
            : 0;

          return [
            -this.config.width / 2,
            // 如果有父节点,则根节点居中,否则根节点上浮一段距离
            parentsLength > 0
              ? -this.config.height / 2
              : -this.config.height / 3,
            this.config.width,
            this.config.height,
          ];
        })
        .style("user-select", "none") // 用于禁用文本选择, 阻止用户在交互时意外选中 SVG 中的文本元素
        .style("cursor", "move") // 用于修改鼠标指针样式,当用户悬停在元素上时显示移动光标(通常为十字箭头或抓手图标)
        .attr("transform", `scale(${this.scaleFactor})`);
      // 包括连接线和节点的总集合 在 SVG 画布中创建一个带有 ID 的分组元素
      const gAll = svg.append("g").attr("id", "all");

      // call 允许你将当前选择集传递给一个函数,从而实现可重用代码和链式调用 调用了zoom, scaleExtent, on等
      // zoom 用于限制缩放行为的范围 translateExtent 设置平移范围
      svg
        .call(
          this.d3
            .zoom()
            .scaleExtent([0.2, 5])
            .on("zoom", (e) => {
              gAll.attr("transform", () => {
                return `translate(${e.transform.x},${e.transform.y}) scale(${e.transform.k})`;
              });
            })
        )
        .on("dblclick.zoom", null); // 取消默认的双击放大事件
      this.gAll = gAll;
      // 连接线集合
      this.gLinks = gAll.append("g").attr("id", "linkGroup");
      // 节点集合
      this.gNodes = gAll.append("g").attr("id", "nodeGroup");

      // 设置好节点之间距离的tree方法 控制节点的大小和间距 [节点水平间距,节点垂直间距]
      // tree() 是一个层级布局算法,专门用于将 d3.hierarchy() 生成的层次结构数据转换为适合树形图可视化的节点坐标
      // 而且设置了树形图的布局
      this.tree = this.d3.tree().nodeSize([this.config.dx, this.config.dy]);

      // hierarchy() 获取初始化数据 父级数据 指定parents作为层级
      // 创建层次结构 子级数据
      // 返回值一个由 D3 封装的特殊数据结构,包含了原始数据及其层次关系信息
      this.rootOfDown = this.d3.hierarchy(
        this.originTreeData,
        (d) => d.children
      );

      this.rootOfUp = this.d3.hierarchy(this.originTreeData, (d) => d.parents);

      // 用于获取当前节点及其所有子孙节点的扁平化数组
      [this.rootOfDown.descendants(), this.rootOfUp.descendants()].forEach(
        (nodes) => {
          nodes.forEach((node) => {
            node._children = node.children || null;
            if (options.type === "all") {
              //如果是all的话,则表示全部都展开
              node.children = node._children;
            } else if (options.type === "fold") {
              //如果是fold则表示除了父节点全都折叠
              // 将非根节点的节点都隐藏掉(其实对于这个组件来说加不加都一样)
              if (node.depth) {
                node.children = null;
              }
            }
          });
        }
      );

      //箭头(下半部分)
      svg
        .append("marker")
        .attr("id", "markerOfDown")
        .attr("markerUnits", "userSpaceOnUse")
        .attr("viewBox", "0 -5 10 10") //坐标系的区域
        .attr("refX", 55) //箭头坐标
        .attr("refY", 0)
        .attr("markerWidth", 6) //标识的大小
        .attr("markerHeight", 6)
        .attr("orient", "90") //绘制方向,可设定为:auto(自动确认方向)和 角度值
        .attr("stroke-width", 2) //箭头宽度
        .append("path")
        .attr("d", "M0,-5L10,0L0,5") //箭头的路径
        .attr("fill", "#C80F06"); //箭头颜色
      //箭头(上半部分)
      svg
        .append("marker")
        .attr("id", "markerOfUp")
        .attr("markerUnits", "userSpaceOnUse")
        .attr("viewBox", "0 -5 10 10") //坐标系的区域
        .attr("refX", -45) //箭头坐标
        .attr("refY", 0)
        .attr("markerWidth", 6) //标识的大小
        .attr("markerHeight", 6)
        .attr("orient", "90") //绘制方向,可设定为:auto(自动确认方向)和 角度值
        .attr("stroke-width", 2) //箭头宽度
        .append("path")
        .attr("d", "M0,-5L10,0L0,5") //箭头的路径
        .attr("fill", "#C80F06"); //箭头颜色
      this.svg = svg;
      this.update();
      // 将svg置入宿主元素中
      host.append(function () {
        return svg.node();
      });
    },
    updateNewData(e, d) {
      listInvestInfoAll({
        companyId: this.companyId, // d.id,
      }).then((res) => {
        res.data;
        if (res.data) {
          d.children = res.data.map((itemP, index) => {
            return {
              id: itemP.companyId ? itemP.companyId + index : "tt" + index,
              name: itemP.investedCompanyName || "",
              percent: itemP.stockPercent,
              children: [{}],
            };
          });

          this.update(d);
        }
      });
    },
    // 更新数据
    update(source) {
      if (!source) {
        source = {
          x0: 0,
          y0: 0,
        };
        // 设置根节点所在的位置(原点)
        this.rootOfDown.x0 = 0;
        this.rootOfDown.y0 = 0;
        this.rootOfUp.x0 = 0;
        this.rootOfUp.y0 = 0;
      }
      let nodesOfDown = this.rootOfDown.descendants().reverse();
      let linksOfDown = this.rootOfDown.links();

      let nodesOfUp = this.rootOfUp.descendants().reverse();
      let linksOfUp = this.rootOfUp.links();

      //  计算节点坐标,把创建层次结构放入布局里面
      this.tree(this.rootOfDown);
      this.tree(this.rootOfUp);

      // transition() 是用于创建平滑动画过渡的核心
      const myTransition = this.svg.transition().duration(500);

      /***  绘制子公司树  ***/
      // 节点的集合
      const node1 = this.gNodes
        .selectAll("g.nodeOfDownItemGroup")
        .data(nodesOfDown, (d) => {
          return d.data.id;
        });
      const node1Enter = node1
        .enter()
        .append("g")
        .attr("class", "nodeOfDownItemGroup")
        .attr("transform", (d) => {
          return `translate(${source.x0},${source.y0})`;
        })
        .attr("fill-opacity", 0)
        .attr("stroke-opacity", 0)
        .style("cursor", "pointer");
      // 外层的矩形框
      node1Enter
        .append("rect")
        .attr("width", (d) => {
          if (d.depth === 0) {
            return (d.data.name.length + 2) * 16;
          }
          return this.config.rectWidth;
        })
        .attr("height", (d) => {
          if (d.depth === 0) {
            return 30;
          }
          return this.config.rectHeight;
        })
        .attr("x", (d) => {
          if (d.depth === 0) {
            return (-(d.data.name.length + 2) * 16) / 2;
          }
          return -this.config.rectWidth / 2;
        })
        .attr("y", (d) => {
          if (d.depth === 0) {
            return -15;
          }
          return -this.config.rectHeight / 2;
        })
        .attr("rx", 5)
        .attr("stroke-width", 1)
        .attr("stroke", (d) => {
          if (d.depth === 0) {
            return "#C80F06";
          }
          return "#FEF1E7";
        })
        .attr("fill", (d) => {
          if (d.depth === 0) {
            return "#C80F06";
          }
          return "#FEF1E7";
        })
        .on("click", (e, d) => {
          // 绘制子公司树
          this.nodeClickEvent(e, d);
        });
      // 文本主标题
      node1Enter
        .append("text")
        .attr("class", "main-title")
        .attr("x", (d) => {
          return 0;
        })
        .attr("y", (d) => {
          if (d.depth === 0) {
            return 5;
          }
          return -14;
        })
        .attr("text-anchor", (d) => {
          return "middle";
        })

        .attr("dy", function (d) {
          // 根据节点类型调整
          return "0.8em";
        })
        .text((d) => {
          if (d.depth === 0) {
            return d.data.name;
          } else {
            return d.data.name && d.data.name.length > 12
              ? d.data.name.substring(0, 12)
              : d.data.name;
          }
        })
        .attr("fill", (d) => {
          if (d.depth === 0) {
            return "#FFFFFF";
          }
          return "#000000";
        })
        .on("click", (e, d) => {
          // 绘制子公司树
          this.nodeClickEvent(e, d);
        });

      // .style("font-size", (d) => (d.depth === 0 ? 16 : 14))
      // .style("font-family", "黑体")
      // .style("font-weight", "bold");
      // 副标题
      node1Enter
        .append("text")
        .attr("class", "sub-title")
        .attr("x", (d) => {
          return 0;
        })
        .attr("y", (d) => {
          return 5;
        })
        .attr("text-anchor", (d) => {
          return "middle";
        })
        .attr("dy", function (d) {
          // 根据节点类型调整
          return "0.7em";
        })
        .text((d) => {
          if (d.depth !== 0) {
            let subTitle = d.data.name ? d.data.name.substring(12) : "";
            if (subTitle.length > 10) {
              return subTitle.substring(0, 10) + "...";
            }
            return subTitle;
          }
        });
      // .style("font-size", (d) => 14)
      // .style("font-family", "黑体")
      // .style("font-weight", "bold");

      // 控股公司
      node1Enter
        .append("text")
        .attr("class", "percent")
        .attr("x", (d) => {
          return 12;
        })
        .attr("y", (d) => {
          return -27;
        })
        .text((d) => {
          if (d.depth !== 0) {
            return d.data.percent;
          }
        })
        .on("click", (e, d) => {
          // 绘制子公司树
          this.nodeClickEvent(e, d);
        });

      // .attr("fill", "#000000")
      // .style("font-family", "黑体")
      // .style("font-size", (d) => 14);

      // 增加展开按钮
      const expandBtnG = node1Enter
        .append("g")
        .attr("class", "expandBtn")
        .attr("transform", (d) => {
          return `translate(${0},${this.config.rectHeight / 2})`;
        })
        .style("display", (d) => {
          // 如果是根节点,不显示
          if (d.depth === 0) {
            return "none";
          }
          // 如果没有子节点,则不显示
          if (!d._children) {
            return "none";
          }
        })
        .on("click", (e, d) => {
          if (d.children) {
            d._children = d.children;
            d.children = null;
          } else {
            d.children = d._children;
          }
          this.update(d);
          // if (d.children) {
          //   d._children = d.children;
          //   d.children = null;
          // } else {
          //   d.children = d._children;
          // }

          // this.updateNewData(e, d);
        });

      expandBtnG
        .append("circle")
        .attr("r", 8)
        .attr("fill", "#C80F06")
        .attr("cy", 8);

      expandBtnG
        .append("text")
        .attr("text-anchor", "middle")
        .attr("fill", "#ffffff")
        .attr("y", 12)
        .style("font-size", 16)
        .style("font-family", "微软雅黑")
        .text((d) => {
          return d.children ? "-" : "+";
        });

      const link1 = this.gLinks
        .selectAll("path.linkOfDownItem")
        .data(linksOfDown, (d) => d.target.data.id);

      const link1Enter = link1
        .enter()
        .append("path")
        .attr("class", "linkOfDownItem")
        .attr("d", (d) => {
          let o = {
            source: {
              x: source.x0,
              y: source.y0,
            },
            target: {
              x: source.x0,
              y: source.y0,
            },
          };
          return this.drawLink(o);
        })
        .attr("fill", "none")
        .attr("stroke", "#B8B8B8")
        .attr("stroke-width", 1)
        .attr("marker-end", "url(#markerOfDown)");

      // 有元素update更新和元素新增enter的时候
      node1
        .merge(node1Enter)
        .transition(myTransition)
        .attr("transform", (d) => {
          return `translate(${d.x},${d.y})`;
        })
        .attr("fill-opacity", 1)
        .attr("stroke-opacity", 1);

      // 有元素消失时
      node1
        .exit()
        .transition(myTransition)
        .remove()
        .attr("transform", (d) => {
          return `translate(${source.x0},${source.y0})`;
        })
        .attr("fill-opacity", 0)
        .attr("stroke-opacity", 0);

      link1.merge(link1Enter).transition(myTransition).attr("d", this.drawLink);

      link1
        .exit()
        .transition(myTransition)
        .remove()
        .attr("d", (d) => {
          let o = {
            source: {
              x: source.x,
              y: source.y,
            },
            target: {
              x: source.x,
              y: source.y,
            },
          };
          return this.drawLink(o);
        });

      /***  绘制股东树  ***/

      nodesOfUp.forEach((node) => {
        node.y = -node.y;
      });

      const node2 = this.gNodes
        .selectAll("g.nodeOfUpItemGroup")
        .data(nodesOfUp, (d) => {
          return d.data.id;
        });

      const node2Enter = node2
        .enter()
        .append("g")
        .attr("class", (d) => {
          if (d.depth === 0) {
            return "rootGroup";
          }
          return "nodeOfUpItemGroup";
        })
        .attr("transform", (d) => {
          return `translate(${source.x0},${source.y0})`;
        })
        .attr("fill-opacity", 0)
        .attr("stroke-opacity", 0)
        .style("cursor", "pointer");

      // 外层的矩形框
      node2Enter
        .append("rect")
        .attr("width", (d) => {
          if (d.depth === 0) {
            return (d.data.name.length + 2) * 16;
          }
          return this.config.rectWidth;
        })
        .attr("height", (d) => {
          if (d.depth === 0) {
            return 30;
          }
          return this.config.rectHeight;
        })
        .attr("x", (d) => {
          if (d.depth === 0) {
            return (-(d.data.name.length + 2) * 16) / 2;
          }
          return -this.config.rectWidth / 2;
        })
        .attr("y", (d) => {
          if (d.depth === 0) {
            return -15;
          }
          return -this.config.rectHeight / 2;
        })
        .attr("rx", 5)

        .attr("stroke-width", 1)
        .attr("stroke", (d) => {
          if (d.depth === 0) {
            return "#C80F06";
          }
          return "#FEF1E7";
        })
        .attr("fill", (d) => {
          if (d.depth === 0) {
            return "#C80F06";
          }
          return "#FEF1E7";
        })

        .on("click", (e, d) => {
          // 绘制股东树
          this.nodeClickEvent(e, d);
        });
      // 文本主标题
      node2Enter
        .append("text")
        .attr("class", "main-title")
        .attr("x", (d) => {
          return 0;
        })
        .attr("y", (d) => {
          if (d.depth === 0) {
            return 5;
          }
          return -14;
        })
        .attr("text-anchor", (d) => {
          return "middle";
        })
        .attr("dy", function (d) {
          // 根据节点类型调整
          if (d.depth !== 0) {
            return "1.0em";
          } else {
            // return "";
          }
        })
        .text((d) => {
          if (d.depth === 0) {
            return d.data.name;
          } else {
            return d.data.name && d.data.name.length > 12
              ? d.data.name.substring(0, 12)
              : d.data.name;
          }
        })
        .attr("fill", (d) => {
          if (d.depth === 0) {
            return "#FFFFFF";
          }
          return "#000000";
        })
        .on("click", (e, d) => {
          // 绘制子公司树
          this.nodeClickEvent(e, d);
        });
      // .style("font-size", (d) => (d.depth === 0 ? 14 : 12));
      // .style("font-family", "黑体")
      // .style("font-weight", "bold");
      // 副标题
      node2Enter
        .append("text")
        .attr("class", "sub-title")
        .attr("x", (d) => {
          return 0;
        })
        .attr("y", (d) => {
          return 5;
        })
        .attr("text-anchor", (d) => {
          return "middle";
        })
        .attr("dy", function (d) {
          // 根据节点类型调整
          if (d.depth !== 0) {
            return "0.7em";
          }
        })
        .text((d) => {
          if (d.depth !== 0) {
            let subTitle = d.data.name ? d.data.name.substring(12) : "";
            if (subTitle.length > 10) {
              return subTitle.substring(0, 10) + "...";
            }
            return subTitle;
          }
        })
        .on("click", (e, d) => {
          // 绘制子公司树
          this.nodeClickEvent(e, d);
        });
      // .style("font-size", (d) => 14)
      // .style("font-family", "黑体")
      // .style("font-weight", "bold");

      // 股东
      node2Enter
        .append("text")
        .attr("class", "percent")
        .attr("x", (d) => {
          return 12;
        })
        .attr("y", (d) => {
          return 35;
        })
        .text((d) => {
          if (d.depth !== 0) {
            return d.data.percent;
          }
        });
      // .attr("fill", "#000000")
      // .style("font-family", "黑体")
      // .style("font-size", (d) => 14);

      // 增加展开按钮
      const expandBtnG2 = node2Enter
        .append("g")
        .attr("class", "expandBtn")
        .attr("transform", (d) => {
          return `translate(${0},${-this.config.rectHeight / 2})`;
        })
        .style("display", (d) => {
          // 如果是根节点,不显示
          if (d.depth === 0) {
            return "none";
          }
          // 如果没有子节点,则不显示
          if (!d._children) {
            return "none";
          }
        })
        .on("click", (e, d) => {
          if (d.children) {
            d._children = d.children;
            d.children = null;
          } else {
            d.children = d._children;
          }
          this.update(d);
        });

      expandBtnG2
        .append("circle")
        .attr("r", 8)
        .attr("fill", "#C80F06")
        .attr("cy", -8);

      expandBtnG2
        .append("text")
        .attr("text-anchor", "middle")
        .attr("fill", "#ffffff")
        .attr("y", -3)
        .style("font-size", 16)
        // .style("font-family", "微软雅黑")
        .text((d) => {
          return d.children ? "-" : "+";
        });

      const link2 = this.gLinks
        .selectAll("path.linkOfUpItem")
        .data(linksOfUp, (d) => d.target.data.id);

      const link2Enter = link2
        .enter()
        .append("path")
        .attr("class", "linkOfUpItem")
        .attr("d", (d) => {
          let o = {
            source: {
              x: source.x0,
              y: source.y0,
            },
            target: {
              x: source.x0,
              y: source.y0,
            },
          };
          return this.drawLink(o);
        })
        .attr("fill", "none")
        .attr("stroke", "#B8B8B8")
        .attr("stroke-width", 1)
        .attr("marker-end", "url(#markerOfUp)");

      // 有元素update更新和元素新增enter的时候
      node2
        .merge(node2Enter)
        .transition(myTransition)
        .attr("transform", (d) => {
          return `translate(${d.x},${d.y})`;
        })
        .attr("fill-opacity", 1)
        .attr("stroke-opacity", 1);
      // 有元素消失时
      node2
        .exit()
        .transition(myTransition)
        .remove()
        .attr("transform", (d) => {
          return `translate(${source.x0},${source.y0})`;
        })
        .attr("fill-opacity", 0)
        .attr("stroke-opacity", 0);
      link2.merge(link2Enter).transition(myTransition).attr("d", this.drawLink);
      link2
        .exit()
        .transition(myTransition)
        .remove()
        .attr("d", (d) => {
          let o = {
            source: {
              x: source.x,
              y: source.y,
            },
            target: {
              x: source.x,
              y: source.y,
            },
          };
          return this.drawLink(o);
        });
      // node数据改变的时候更改一下加减号
      const expandButtonsSelection = this.d3.selectAll("g.expandBtn");
      expandButtonsSelection
        .select("text")
        .transition()
        .text((d) => {
          return d.children ? "-" : "+";
        });

      this.rootOfDown.eachBefore((d) => {
        d.x0 = d.x;
        d.y0 = d.y;
      });
      this.rootOfUp.eachBefore((d) => {
        d.x0 = d.x;
        d.y0 = d.y;
      });
    },
    // 直角连接线 by wushengyuan
    drawLink({ source, target }) {
      const halfDistance = (target.y - source.y) / 2;
      const halfY = source.y + halfDistance;
      return `M${source.x},${source.y} L${source.x},${halfY} ${target.x},${halfY} ${target.x},${target.y}`;
    },
    // 展开所有的节点
    expandAllNodes() {
      this.drawChart({
        type: "all",
      });
    },
    // 将所有节点都折叠
    foldAllNodes() {
      this.drawChart({
        type: "fold",
      });
    },
    //点击节点获取节点数据,请求详情接口获取 公司信息,如果股东是个人或者无法查出公司信息,则不渲染
    // 优化: 在获取所有股东和子公司的时候吧econKind 也获取,然后就不用再次获取listEnterpriseInforListAll接口了
    // 优化还是要考虑listEnterpriseInforListAll 有没有返回值的问题

    nodeClickEvent(e, d) {
      this.loading = true;
      // d.data.stockType 有值代表企业股东
      console.log("----------", d);
      if (d.data && d.data.id && d.data.stockType === "企业股东") {
        listEnterpriseInforListAll({ companyId: d.data.id }).then(
          async (res) => {
            this.companyIdSelf = d.data.id;
            const data = res.data && res.data.length > 0 ? res.data[0] : "";

            if (data) {
              this.companyId = data.companyId;
              this.companyName = data.companyName;
              data.tagsList = [];
              if (data.status) {
                data.tagsList.push(data.status.split("(")[0]);
              }
              if (data.econKind) {
                data.tagsList.push(data.econKind.split("(")[0]);
              }
              this.baseInfo = data;

              this.getData();
            } else {
              // this.$message.warning("查询不到该公司信息");
              ElMessage.warning("查询不到该公司信息");
              this.loading = false;
            }
          }
        );
      } else {
        ElMessage.warning("不能查询自然人股东信息");
        this.loading = false;
      }
    },
    // 右上角状态
    getStatusClass(item) {
      const status = {
        存续: "status-active",
        注销: "status-cancelled",
        吊销: "status-revoked",
        股份有限公司: "type-tag",
        有限责任公司: "type-tag",
      };
      return status[item] || "";
    },

    getRandomColor() {
      const colors = [
        "#53C89C",
        "#4EBCDE",
        "#2E8AF0",
        "#588BEA",
        "#6875E4",
        "#9370DB",
        "#FC9682",
        "#F5A882",
        "#F8BF7F",
        "#F5D678",
      ];
      return colors[Math.floor(Math.random() * colors.length)];
    },
    // 右上角点击跳转
    handleLinkOther() {
      wx.miniProgram.postMessage({
        data: {
          action: "customEvent",
          payload: { key: "value" },
        },
      });
      // wx.miniProgram.navigateBack();

      wx.miniProgram.redirectTo({
        url:
          "/pages-sub/CompanyDetails/CompanyDetails?companyId=" +
          this.companyIdSelf,
      });
    },
  },
};
</script>
<style lang="scss" scoped>
.stock-wrap {
  overflow: hidden;
  position: relative;
  width: 100%;
  height: 100%;
  .header {
    padding: 15px;
    border-bottom: 1px solid #ececec;
    display: flex;

    .logo {
      width: 43px;
      height: 43px;
      background: #63d5d2;
      border-radius: 4px;
      line-height: 40px;
      font-size: 18px;
      text-align: center;
      margin-right: 6px;
      color: #fff;
    }
    .com-name {
      font-weight: bold;
      font-size: 15px;
      color: #333333;
      margin-bottom: 6px;
    }
    .status-wrap {
      display: flex;
      .status {
        height: 18px;
        line-height: 18px;
        font-size: 12px;
        padding: 0 6px;
        background: #ebf7f2;
        border-radius: 2px;
        color: #3bb27f;
        margin-right: 5px;
        &.status-active {
          background: #ebf7f2;
          color: #3bb27f;
        }

        &.status-revoked {
          background: #fff1e4;
          color: #ff830d;
        }

        &.status-cancelled {
          background: #fff1e4;
          color: #ff830d;
        }

        &.type-tag {
          background: #e1ecff;
          color: #0d86ff;
        }
      }
    }
  }
  .echarts {
    height: calc(100% - 80px);
  }
  .filter-box {
    position: fixed;
    bottom: 30px;
    right: 20px;
    width: 49px;
    height: 49px;
    background: #ffffff;
    border-radius: 4px;
    border: 1px solid #e5e5e5;
    color: #333333;
    text-align: center;
    padding-top: 4px;

    div {
      text-align: center;
      color: #333333;
      font-size: 13px;
    }
  }
}
::v-deep .nodeOfDownItemGroup {
  .main-title,
  .sub-title {
    font-size: 12px;
    font-weight: 500;
    fill: #ff8327;
  }
  .percent {
    font-size: 12px;
    font-weight: 500;
    fill: #666666;
  }
}
::v-deep .nodeOfUpItemGroup {
  .main-title,
  .sub-title {
    font-size: 12px;
    font-weight: 500;
    fill: #ff8327;
  }
  .percent {
    font-size: 12px;
    font-weight: 500;
    fill: #666666;
  }
}
::v-deep .rootGroup {
  .main-title {
    font-size: 12px;
    font-weight: 500;
    fill: #fff;
  }
}
.fiter-title {
  text-align: center;
  font-size: 15px;
}
</style>
<style lang="scss" scoped>
::v-deep .el-drawer {
  &.rtl {
    border-radius: 8px 0 0 8px;
  }
}
::v-deep .echarts-chose-drawer-header {
  &.el-drawer__header {
    margin-bottom: 0px;
    padding: 0px 20px;
    border-bottom: 1px solid #ececec;
    .fiter-title {
      margin: 10px;
      color: #333;
      font-weight: 500;
    }
    .el-icon {
      color: #333;
    }
  }
}
::v-deep .slider-item {
  &:last-of-type {
    margin-top: 15px;
  }
  .slider-title {
    font-size: 14px;
    font-weight: 500;
  }
  .slider-demo-block {
    display: flex;
    .slider-w {
      flex: 1;
      .show-tip {
        display: flex;
        justify-content: space-between;
        font-size: 12px;
        color: #999999;
      }
    }
  }
  .btn {
    width: 68px;
    height: 30px;
    background: #f6f6f6;
    border-radius: 4px;
    font-size: 12px;
    line-height: 30px;
    text-align: center;
    display: block;
    margin-left: 25px;
  }
  .el-slider__button {
    border: 2px solid #fff;
    position: relative;
    box-shadow: 0px 0px 10px 0px rgba(152, 102, 93, 0.3);
    &::after {
      content: "";
      position: absolute;
      width: 8px;
      height: 8px;
      background: #c80f06;
      top: 50%;
      left: 50%;
      border-radius: 50%;
      transform: translateX(-50%) translateY(-50%);
    }
  }
  .el-slider__bar {
    background-color: #c80f06;
  }
}
</style>

export const dataTemp = {
  id: "abc1005",
  // 根节点名称
  name: "山东吠舍科技有限责任公司",
  // 子节点列表
  children: [
    {
      id: "abc1006",
      name: "山东第一首陀罗科技服务有限公司",
      percent: "100%",
    },
    {
      id: "abc1007",
      name: "山东第二首陀罗程技术有限公司",
      percent: "100%",
    },
    {
      id: "abc1008",
      name: "山东第三首陀罗光伏材料有限公司",
      percent: "100%",
    },
    {
      id: "abc1009",
      name: "山东第四首陀罗科技发展有限公司",
      percent: "100%",
      children: [
        {
          id: "abc1010",
          name: "山东第一达利特瑞利分析仪器有限公司",
          percent: "100%",
          children: [
            {
              id: "abc1011",
              name: "山东瑞利的子公司一",
              percent: "80%",
              children: [
                {
                  id: "a1",
                  name: "aaaaa",
                  percent: "80%",
                },
                {
                  id: "a2",
                  name: "bbbbb",
                  percent: "80%",
                },
              ],
            },
            {
              id: "abc1012",
              name: "山东瑞利的子公司二",
              percent: "90%",
            },
            {
              id: "abc1013",
              name: "山东瑞利的子公司三",
              percent: "100%",
            },
          ],
        },
      ],
    },
    {
      id: "abc1014",
      name: "山东第五首陀罗电工科技有限公司",
      percent: "100%",
      children: [
        {
          id: "abc1015",
          name: "山东第二达利特低自动化设备有限公司",
          percent: "100%",
          children: [
            {
              id: "abc1016",
              name: "山东敬业的子公司一",
              percent: "100%",
            },
            {
              id: "abc1017",
              name: "山东敬业的子公司二",
              percent: "90%",
            },
          ],
        },
      ],
    },
    {
      id: "abc1020",
      name: "山东第六首陀罗分析仪器(集团)有限责任公司",
      percent: "100%",
      children: [
        {
          id: "abc1021",
          name: "山东第三达利特分气体工业有限公司",
        },
      ],
    },
  ],
  // 父节点列表
  parents: [
    {
      id: "abc2001",
      name: "山东刹帝利集团有限责任公司",
      percent: "60%",
      parents: [
        {
          id: "abc2000",
          name: "山东婆罗门集团有限公司",
          percent: "100%",
        },
      ],
    },
    {
      id: "abc2002",
      name: "吴小远",
      percent: "40%",
      parents: [
        {
          id: "abc1010",
          name: "山东第一达利特瑞利分析仪器有限公司",
          percent: "100%",
          parents: [
            {
              id: "abc1011",
              name: "山东瑞利的子公司一",
              percent: "80%",
            },
            {
              id: "abc1012",
              name: "山东瑞利的子公司二",
              percent: "90%",
            },
            {
              id: "abc1013",
              name: "山东瑞利的子公司三",
              percent: "100%",
            },
          ],
        },
      ],
    },
    {
      id: "abc2003",
      name: "测试数据",
      percent: "40%",
    },
  ],
};

本文章已经生成可运行项目
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值