vue3+ts+d3+jquery实现鱼骨图和本地导出

514 阅读2分钟
1、fishTool.vue
<template>
  <div>
    <el-button
      type="primary"
      @click="handleExport"
    >
      导出
    </el-button>
    <div
      id="target"
      class="fish-bone-div"
    />
  </div>
</template>
<script lang="ts" setup>
import { ref, watchEffect } from 'vue'
import html2canvas from 'html2canvas'
import { ElMessage } from 'element-plus'

const props = defineProps({
  dataList: {
    type: Array,
    default: () => []
  }
})
const dataList = ref<any[]>([])
const fishBoneQuery = ref('')
const fishbone = ref<any>()
const feedback = ref<any[]>([])
const srcDatas = ref<any[]>([])

// 鱼骨图

window.onload = () => {
  initEchart()
}

// 扁平化树
const buildTable = (nodes:any[], parentId = '-1', path = '.') => {
  const tableItems = []
  for (const node of nodes) {
    if (node.parentId === parentId) {
      const currentPath = `${path}/${node.causalDetailId}`
      const tableItem = { path: currentPath, name: node.name, k: node.causalDetailId }
      tableItems.push(tableItem)
      // 递归处理子节点
      if (node.child) {
        const childTableItems:any[] = buildTable(node.child, node.causalDetailId, currentPath)
        tableItems.push(...childTableItems)
      }
    }
  }
  tableItems.forEach((item:any) => {
    const index = item.path.lastIndexOf('/')
    const result = item.path.substring(index + 1)
    if (item.k === result) {
      item.path = item.path.slice(0, index)
    }
  })
  return tableItems
}

const initEchart = () => {
  if (dataList.value && dataList.value.length) {
    srcDatas.value = buildTable(dataList.value)
    fishBoneQuery.value = ''
    drawInContext()
  }
}
defineExpose({ initEchart })
const drawInContext = function () {
  const srcData = JSON.parse(JSON.stringify(srcDatas.value))
  // const k = fishBoneQuery || srcData[0].k
  // const rec = srcData.filter(function (row) {
  //   return row.k === k
  // })
  // if (rec.length === 0) {
  //   return []
  // }
  // const path = rec[0].path + '/' + rec[0].k
  // const subX = srcData.filter(function (row) {
  //   return row.path.indexOf(path) === 0 && row.path.replace(path, '').split('/').length < 3
  // })
  // srcData = rec.concat(subX)
  drawFishbone(srcData)
}
const drawFishbone = function (data:any) {
  // eslint-disable-next-line no-undef
  fishbone.value = d3.fishbone(
    size(),
    function () {
      return data
    },
    function () {
      return feedback.value
    },
    onDblClickBranch,
    function () { }
  )
  // eslint-disable-next-line no-undef
  const target = d3.select('#target')
  target.select('svg').remove()
  // svg = target
  target
    .append('svg')
    .attr(size())
    .call(fishbone.value.defaultArrow)
    .call(fishbone.value.defaultFishStart)
    .call(fishbone.value.defaultFishEnd)
    .call(fishbone.value)
  forceLayout()
}
// 双击事件
const onDblClickBranch = function (row:any) {
  fishBoneQuery.value = row.k
  drawInContext()
}
const forceLayout = function () {
  const s = size()
  const force = fishbone.value.force()
  force.size([s.width, s.height])
  //  获取或设置节点的引力强度
  force.gravity(0)
  // 获取或设置节点的电荷数.(电荷数决定结点是互相排斥还是吸引)
  force.charge(0)
  // 获取或设置节点间的连接线距离
  force.linkDistance(150)
  // 获取或设置节点间的连接强度
  force.linkStrength(1.2)
  // 获取或设置布局的冷却系数.(冷却系数为0时,节点间不再互相影响)
  force.alpha(0)
  // 取得或者设置最大电荷距离
  force.chargeDistance(120)
  // 取得或者设置摩擦系数
  force.friction(0.9)
  force.start()
  times(250).forEach(force.tick.bind(force))
  force.stop()
  force.start()
}
const size = function () {
  // const g = window.document.documentElement
  return {
    // eslint-disable-next-line no-undef
    width: $('#target').width(),
    // eslint-disable-next-line no-undef
    height: $('#target').height()
  }
}
const times = function (num:number) {
  const r = []
  for (let i = 0; i < num; i++) r.push(i)
  return r
}

// svg转为本地图片导出
const handleExport = () => {
  if (srcDatas.value && srcDatas.value.length) {
    const element:any = document.getElementById('target')
    html2canvas(element).then((canvas:any) => {
    // canvas 是包含截图内容的 HTMLCanvasElement 对象
    // 你可以将其转换为图片并下载,或者将其添加到 DOM 中
      const imgData = canvas.toDataURL('image/png')
      const link = document.createElement('a')
      link.href = imgData
      link.download = '因果分析.png'
      link.click()
      ElMessage.success('导出成功')
    })
  } else {
    ElMessage.warning('请先预览')
  }
}
watchEffect(() => {
  dataList.value = props.dataList
})

</script>

<style lang="scss">
  .echart-div {
    width: 100%;
    text-align: center;
  }

  .fish-bone-div {
    height: 600px;
    width: 100%;
    padding: 20px;
  }

  .note {
    color: gray;
    font-size: 0.8em;
  }

  .label-0 {
    font-size: 1.5em;
  }

  .label-1 {
    font-size: 1.2em;
    fill: #111;
  }

  .label-2 {
    font-size: 1em;
    fill: #444;
  }

  .label-3 {
    font-size: 0.8em;
    fill: #888;
  }

  .label-4 {
    font-size: 0.7em;
    fill: #aaa;
  }

  .label-5 {
    font-size: 0.6em;
    fill: #aaa;
  }

  .label-6 {
    font-size: 0.8em;
    fill: #aaa;
  }

  .link-0 {
    stroke: #337ecc;
    stroke-width: 6px;
  }

  .link-1 {
    stroke: #333;
    stroke-width: 1px;
  }

  .link-2,
  .link-3,
  .link-4,
  .link-5,
  .link-6 {
    stroke: #666;
    stroke-width: 0.5px;
  }

  .link-positive {
    stroke: #409eff;
  }

  .link-negative {
    stroke: #337ecc;
  }

  .positive {
    fill: green;
    stroke: green;
  }

  .negative {
    fill: red;
    stroke: red;
  }
</style>
2、在index.html中引入,三个文件放在public文件夹下再引入
  <script src="/jq1.11.3.min.js"></script>
  <script src="/d3.min.js" charset="utf-8"></script>
  <script src="/d3.fishbone.js" charset="utf-8"></script>
3、npm i html2canvas实现本地导出图片
4、在global.d.ts中解决ts变量未声明问题
// eslint-disable-next-line no-unused-vars
declare let d3:any
// eslint-disable-next-line no-unused-vars
declare let $:any
5、defineExpose暴露方法在父组件onMounted或者watchEffect中调用,因为fishToo.vue是一个子组件
6、效果图
![1714456107068.jpg](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d9fec5a8e30e47f2866ec6bfbabc8968~tplv-k3u1fbpfcp-jj-mark:0:0:0:0:q75.image#?w=1627&h=837&s=32678&e=png&b=ffffff)