equity-penetration-chart.vue 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863
  1. <template>
  2. <div style="height: 100%">
  3. <a-select
  4. show-search
  5. label-in-value
  6. :value="value"
  7. placeholder="选择公司"
  8. style="width: 400px"
  9. :filter-option="false"
  10. :not-found-content="fetching ? undefined : null"
  11. @search="fetchQuotedCompany"
  12. @change="handleChange"
  13. @focus="fetchQuotedCompany"
  14. >
  15. <a-spin v-if="fetching" slot="notFoundContent" size="small" />
  16. <a-select-option v-for="d in quotedCompanys" :key="d.id">
  17. {{ d.name }}
  18. </a-select-option>
  19. </a-select>
  20. <label v-show="lastUpdated.latestUpdatedDate"
  21. >&nbsp;&nbsp; 数据更新时间: {{ lastUpdated.latestUpdatedDate }}</label
  22. >
  23. <div id="appc" style="height: 98%"></div>
  24. </div>
  25. </template>
  26. <script>
  27. import * as $d3 from 'd3'
  28. import axios from '@/common/services/axios-instance'
  29. import debounce from 'lodash/debounce'
  30. export default {
  31. name: 'Legalperson',
  32. data() {
  33. this.fetchQuotedCompany = debounce(this.fetchQuotedCompany, 800)
  34. return {
  35. d3: $d3,
  36. cachedTreeData: {},
  37. treeData: {},
  38. allQuotedCompanys: [],
  39. quotedCompanys: [],
  40. value: [],
  41. fetching: false,
  42. lastUpdated: {},
  43. }
  44. },
  45. mounted() {
  46. axios.get('api/xcoa-mobile/v1/equityPenetration').then((res) => {
  47. if (res.data) {
  48. this.treeData = res.data
  49. this.cachedTreeData[2353689640] = this.treeData
  50. this.constructor()
  51. }
  52. })
  53. axios.get('api/xcoa-mobile/v1/quotedCompanys').then((res) => {
  54. if (res.data) {
  55. this.allQuotedCompanys = res.data
  56. }
  57. })
  58. axios.get('api/xcoa-mobile/v1/lastUpdatedDate').then((res) => {
  59. if (res.data) {
  60. this.lastUpdated = res.data
  61. }
  62. })
  63. },
  64. methods: {
  65. fetchQuotedCompany(value) {
  66. console.log('fetching user', value)
  67. this.quotedCompanys = []
  68. this.fetching = true
  69. if (value) {
  70. const filtedC = this.allQuotedCompanys.filter((c) => c.name.includes(value))
  71. if (filtedC.length > 200) {
  72. const t = filtedC.slice(0, 200)
  73. t.push({ id: -1, name: '200+ ......' })
  74. this.quotedCompanys = t
  75. } else {
  76. this.quotedCompanys = filtedC
  77. }
  78. } else {
  79. this.quotedCompanys = this.allQuotedCompanys.slice(0, 200)
  80. this.quotedCompanys.push({ id: -1, name: '200+ ......' })
  81. }
  82. this.fetching = false
  83. },
  84. handleChange(value) {
  85. console.log(value)
  86. if (value.key === -1) {
  87. return false
  88. }
  89. Object.assign(this, {
  90. value,
  91. quotedCompanys: [],
  92. fetching: false,
  93. })
  94. const companyId = value.key
  95. if (this.cachedTreeData[companyId]) {
  96. console.log('loadFromCache')
  97. this.treeData = this.cachedTreeData[companyId]
  98. document.getElementById('appc').innerHTML = ''
  99. this.constructor()
  100. } else {
  101. axios
  102. .get('api/xcoa-mobile/v1/equityPenetration?quotedCompanyId=' + companyId)
  103. .then((res) => {
  104. if (res.data) {
  105. console.log('loadFromNetwork')
  106. this.treeData = res.data
  107. this.cachedTreeData[companyId] = this.treeData
  108. document.getElementById('appc').innerHTML = ''
  109. this.constructor()
  110. }
  111. })
  112. }
  113. console.log(value)
  114. },
  115. // 股权树
  116. constructor(options) {
  117. // 树的源数据
  118. this.originTreeData = this.treeData
  119. // 宿主元素选择器
  120. this.el = document.getElementById('appc')
  121. // 一些配置项
  122. this.config = {
  123. // 节点的横向距离
  124. dx: 200,
  125. // 节点的纵向距离
  126. dy: 170,
  127. // svg的viewBox的宽度
  128. width: 0,
  129. // svg的viewBox的高度
  130. height: 500,
  131. // 节点的矩形框宽度
  132. rectWidth: 170,
  133. // 节点的矩形框高度
  134. rectHeight: 70,
  135. }
  136. this.svg = null
  137. this.gAll = null
  138. this.gLinks = null
  139. this.gNodes = null
  140. // 给树加坐标点的方法
  141. this.tree = null
  142. // 投资公司树的根节点
  143. this.rootOfDown = null
  144. // 股东树的根节点
  145. this.rootOfUp = null
  146. this.drawChart({
  147. type: 'fold',
  148. })
  149. },
  150. // 初始化树结构数据
  151. drawChart(options) {
  152. // 宿主元素的d3选择器对象
  153. const host = this.d3.select(this.el)
  154. // 宿主元素的DOM,通过node()获取到其DOM元素对象
  155. const dom = host.node()
  156. // 宿主元素的DOMRect
  157. const domRect = dom.getBoundingClientRect()
  158. // svg的宽度和高度
  159. this.config.width = domRect.width
  160. this.config.height = domRect.height
  161. const oldSvg = this.d3.select('svg')
  162. // 如果宿主元素中包含svg标签了,那么则删除这个标签,再重新生成一个
  163. if (!oldSvg.empty()) {
  164. oldSvg.remove()
  165. }
  166. const svg = this.d3
  167. .create('svg')
  168. .attr('viewBox', () => {
  169. const parentsLength = this.originTreeData.parents ? this.originTreeData.parents.length : 0
  170. return [
  171. -this.config.width / 2,
  172. // 如果有父节点,则根节点居中,否则根节点上浮一段距离
  173. parentsLength > 0 ? -this.config.height / 2 : -this.config.height / 3,
  174. this.config.width,
  175. this.config.height,
  176. ]
  177. })
  178. .style('user-select', 'none')
  179. .style('cursor', 'move')
  180. // 包括连接线和节点的总集合
  181. const gAll = svg.append('g').attr('id', 'all')
  182. svg
  183. .call(
  184. this.d3
  185. .zoom()
  186. .scaleExtent([0.2, 5])
  187. .on('zoom', (e) => {
  188. gAll.attr('transform', () => {
  189. return `translate(${e.transform.x},${e.transform.y}) scale(${e.transform.k})`
  190. })
  191. })
  192. )
  193. .on('dblclick.zoom', null) // 取消默认的双击放大事件
  194. this.gAll = gAll
  195. // 连接线集合
  196. this.gLinks = gAll.append('g').attr('id', 'linkGroup')
  197. // 节点集合
  198. this.gNodes = gAll.append('g').attr('id', 'nodeGroup')
  199. // 设置好节点之间距离的tree方法
  200. this.tree = this.d3.tree().nodeSize([this.config.dx, this.config.dy])
  201. this.rootOfDown = this.d3.hierarchy(this.originTreeData, (d) => d.children)
  202. this.rootOfUp = this.d3.hierarchy(this.originTreeData, (d) => d.parents)
  203. this.tree(this.rootOfDown)
  204. ;[this.rootOfDown.descendants(), this.rootOfUp.descendants()].forEach((nodes) => {
  205. nodes.forEach((node) => {
  206. node._children = node.children || null
  207. if (options.type === 'all') {
  208. // 如果是all的话,则表示全部都展开
  209. node.children = node._children
  210. } else if (options.type === 'fold') {
  211. // 如果是fold则表示除了父节点全都折叠
  212. // 将非根节点的节点都隐藏掉(其实对于这个组件来说加不加都一样)
  213. if (node.depth) {
  214. node.children = null
  215. }
  216. }
  217. })
  218. })
  219. // 箭头(下半部分)
  220. svg
  221. .append('marker')
  222. .attr('id', 'markerOfDown')
  223. .attr('markerUnits', 'userSpaceOnUse')
  224. .attr('viewBox', '0 -5 10 10') // 坐标系的区域
  225. .attr('refX', 55) // 箭头坐标
  226. .attr('refY', 0)
  227. .attr('markerWidth', 10) // 标识的大小
  228. .attr('markerHeight', 10)
  229. .attr('orient', '90') // 绘制方向,可设定为:auto(自动确认方向)和 角度值
  230. .attr('stroke-width', 2) // 箭头宽度
  231. .append('path')
  232. .attr('d', 'M0,-5L10,0L0,5') // 箭头的路径
  233. .attr('fill', '#215af3') // 箭头颜色
  234. // 箭头(上半部分)
  235. svg
  236. .append('marker')
  237. .attr('id', 'markerOfUp')
  238. .attr('markerUnits', 'userSpaceOnUse')
  239. .attr('viewBox', '0 -5 10 10') // 坐标系的区域
  240. .attr('refX', -50) // 箭头坐标
  241. .attr('refY', 0)
  242. .attr('markerWidth', 10) // 标识的大小
  243. .attr('markerHeight', 10)
  244. .attr('orient', '90') // 绘制方向,可设定为:auto(自动确认方向)和 角度值
  245. .attr('stroke-width', 2) // 箭头宽度
  246. .append('path')
  247. .attr('d', 'M0,-5L10,0L0,5') // 箭头的路径
  248. .attr('fill', '#215af3') // 箭头颜色
  249. this.svg = svg
  250. this.update()
  251. // 将svg置入宿主元素中
  252. host.append(function () {
  253. return svg.node()
  254. })
  255. },
  256. // 更新数据
  257. update(source) {
  258. if (!source) {
  259. source = {
  260. x0: 0,
  261. y0: 0,
  262. }
  263. // 设置根节点所在的位置(原点)
  264. this.rootOfDown.x0 = 0
  265. this.rootOfDown.y0 = 0
  266. this.rootOfUp.x0 = 0
  267. this.rootOfUp.y0 = 0
  268. }
  269. const nodesOfDown = this.rootOfDown.descendants().reverse()
  270. const linksOfDown = this.rootOfDown.links()
  271. const nodesOfUp = this.rootOfUp.descendants().reverse()
  272. const linksOfUp = this.rootOfUp.links()
  273. this.tree(this.rootOfDown)
  274. this.tree(this.rootOfUp)
  275. const myTransition = this.svg.transition().duration(500)
  276. /** * 绘制子公司树 ***/
  277. const node1 = this.gNodes.selectAll('g.nodeOfDownItemGroup').data(nodesOfDown, (d) => {
  278. return d.data.id
  279. })
  280. const node1Enter = node1
  281. .enter()
  282. .append('g')
  283. .attr('class', 'nodeOfDownItemGroup')
  284. .attr('transform', (d) => {
  285. return `translate(${source.x0},${source.y0})`
  286. })
  287. .attr('fill-opacity', 0)
  288. .attr('stroke-opacity', 0)
  289. .style('cursor', 'pointer')
  290. // 外层的矩形框
  291. node1Enter
  292. .append('rect')
  293. .attr('width', (d) => {
  294. if (d.depth === 0) {
  295. return (d.data.name.length + 2) * 16
  296. }
  297. return this.config.rectWidth
  298. })
  299. .attr('height', (d) => {
  300. if (d.depth === 0) {
  301. return 30
  302. }
  303. return this.config.rectHeight
  304. })
  305. .attr('x', (d) => {
  306. if (d.depth === 0) {
  307. return (-(d.data.name.length + 2) * 16) / 2
  308. }
  309. return -this.config.rectWidth / 2
  310. })
  311. .attr('y', (d) => {
  312. if (d.depth === 0) {
  313. return -15
  314. }
  315. return -this.config.rectHeight / 2
  316. })
  317. .attr('rx', 5)
  318. .attr('stroke-width', 1)
  319. .attr('stroke', (d) => {
  320. if (d.depth === 0) {
  321. return '#5682ec'
  322. }
  323. return '#7A9EFF'
  324. })
  325. .attr('fill', (d) => {
  326. if (d.depth === 0) {
  327. return '#7A9EFF'
  328. }
  329. return '#FFFFFF'
  330. })
  331. .on('click', (e, d) => {
  332. this.nodeClickEvent(e, d)
  333. })
  334. .on('dblclick', (e, d) => {})
  335. // 文本主标题
  336. node1Enter
  337. .append('text')
  338. .attr('class', 'main-title')
  339. .attr('x', (d) => {
  340. return 0
  341. })
  342. .attr('y', (d) => {
  343. if (d.depth === 0) {
  344. return 5
  345. }
  346. return -14
  347. })
  348. .attr('text-anchor', (d) => {
  349. return 'middle'
  350. })
  351. .text((d) => {
  352. if (d.depth === 0) {
  353. const levelStr = d.data.level ? '(' + d.data.level + ')' : ''
  354. if (levelStr && d.data.name.indexOf(levelStr) === -1) {
  355. d.data.name = d.data.name + levelStr
  356. }
  357. return d.data.name
  358. } else {
  359. const levelStr = d.data.level ? '(' + d.data.level + ')' : ''
  360. if (levelStr && d.data.name.indexOf(levelStr) === -1) {
  361. d.data.name = d.data.name + levelStr
  362. }
  363. return d.data.name.length > 11 ? d.data.name.substring(0, 11) : d.data.name
  364. }
  365. })
  366. .attr('fill', (d) => {
  367. if (d.depth === 0) {
  368. return '#FFFFFF'
  369. }
  370. return '#000000'
  371. })
  372. .style('font-size', (d) => (d.depth === 0 ? 16 : 14))
  373. .style(
  374. 'font-family',
  375. "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
  376. )
  377. .on('click', (e, d) => {
  378. this.nodeClickEvent(e, d)
  379. })
  380. .on('dblclick', (e, d) => {})
  381. // .style('font-weight', 'bold')
  382. // 副标题
  383. node1Enter
  384. .append('text')
  385. .attr('class', 'sub-title')
  386. .attr('x', (d) => {
  387. return 0
  388. })
  389. .attr('y', (d) => {
  390. return 5
  391. })
  392. .attr('text-anchor', (d) => {
  393. return 'middle'
  394. })
  395. .text((d) => {
  396. if (d.depth !== 0) {
  397. const subTitle = d.data.name.substring(11)
  398. if (subTitle.length > 10) {
  399. return subTitle.substring(0, 10) + '...'
  400. }
  401. return subTitle
  402. }
  403. })
  404. .style('font-size', (d) => 14)
  405. .style(
  406. 'font-family',
  407. "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
  408. )
  409. .on('click', (e, d) => {
  410. this.nodeClickEvent(e, d)
  411. })
  412. .on('dblclick', (e, d) => {})
  413. // .style('font-weight', 'bold')
  414. // 控股比例
  415. node1Enter
  416. .append('text')
  417. .attr('class', 'percent')
  418. .attr('x', (d) => {
  419. return 12
  420. })
  421. .attr('y', (d) => {
  422. return -45
  423. })
  424. .text((d) => {
  425. if (d.depth !== 0) {
  426. return d.data.percent
  427. }
  428. })
  429. .attr('fill', '#000000')
  430. .style(
  431. 'font-family',
  432. "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
  433. )
  434. .style('font-size', (d) => 14)
  435. // 增加展开按钮
  436. const expandBtnG = node1Enter
  437. .append('g')
  438. .attr('class', 'expandBtn')
  439. .attr('transform', (d) => {
  440. return `translate(${0},${this.config.rectHeight / 2})`
  441. })
  442. .style('display', (d) => {
  443. // 如果是根节点,不显示
  444. if (d.depth === 0) {
  445. return 'none'
  446. }
  447. // 如果没有子节点,则不显示
  448. if (!d._children) {
  449. return 'none'
  450. }
  451. })
  452. .on('click', (e, d) => {
  453. if (d.children) {
  454. d._children = d.children
  455. d.children = null
  456. } else {
  457. d.children = d._children
  458. }
  459. this.update(d)
  460. })
  461. expandBtnG.append('circle').attr('r', 10).attr('fill', '#7A9EFF').attr('cy', 10)
  462. expandBtnG
  463. .append('text')
  464. .attr('text-anchor', 'middle')
  465. .attr('fill', '#ffffff')
  466. .attr('y', 14)
  467. .style('font-size', 16)
  468. .style('font-family', '微软雅黑')
  469. .text((d) => {
  470. return d.children ? '-' : '+'
  471. })
  472. const link1 = this.gLinks
  473. .selectAll('path.linkOfDownItem')
  474. .data(linksOfDown, (d) => d.target.data.id)
  475. const link1Enter = link1
  476. .enter()
  477. .append('path')
  478. .attr('class', 'linkOfDownItem')
  479. .attr('d', (d) => {
  480. const o = {
  481. source: {
  482. x: source.x0,
  483. y: source.y0,
  484. },
  485. target: {
  486. x: source.x0,
  487. y: source.y0,
  488. },
  489. }
  490. return this.drawLink(o)
  491. })
  492. .attr('fill', 'none')
  493. .attr('stroke', '#7A9EFF')
  494. .attr('stroke-width', 1)
  495. .attr('marker-end', 'url(#markerOfDown)')
  496. // 有元素update更新和元素新增enter的时候
  497. node1
  498. .merge(node1Enter)
  499. .transition(myTransition)
  500. .attr('transform', (d) => {
  501. return `translate(${d.x},${d.y})`
  502. })
  503. .attr('fill-opacity', 1)
  504. .attr('stroke-opacity', 1)
  505. // 有元素消失时
  506. node1
  507. .exit()
  508. .transition(myTransition)
  509. .remove()
  510. .attr('transform', (d) => {
  511. return `translate(${source.x0},${source.y0})`
  512. })
  513. .attr('fill-opacity', 0)
  514. .attr('stroke-opacity', 0)
  515. link1.merge(link1Enter).transition(myTransition).attr('d', this.drawLink)
  516. link1
  517. .exit()
  518. .transition(myTransition)
  519. .remove()
  520. .attr('d', (d) => {
  521. const o = {
  522. source: {
  523. x: source.x,
  524. y: source.y,
  525. },
  526. target: {
  527. x: source.x,
  528. y: source.y,
  529. },
  530. }
  531. return this.drawLink(o)
  532. })
  533. /** * 绘制股东树 ***/
  534. nodesOfUp.forEach((node) => {
  535. node.y = -node.y
  536. })
  537. const node2 = this.gNodes.selectAll('g.nodeOfUpItemGroup').data(nodesOfUp, (d) => {
  538. return d.data.id
  539. })
  540. const node2Enter = node2
  541. .enter()
  542. .append('g')
  543. .attr('class', 'nodeOfUpItemGroup')
  544. .attr('transform', (d) => {
  545. return `translate(${source.x0},${source.y0})`
  546. })
  547. .attr('fill-opacity', 0)
  548. .attr('stroke-opacity', 0)
  549. .style('cursor', 'pointer')
  550. // 外层的矩形框
  551. node2Enter
  552. .append('rect')
  553. .attr('width', (d) => {
  554. if (d.depth === 0) {
  555. return (d.data.name.length + 2) * 16
  556. }
  557. return this.config.rectWidth
  558. })
  559. .attr('height', (d) => {
  560. if (d.depth === 0) {
  561. return 30
  562. }
  563. return this.config.rectHeight
  564. })
  565. .attr('x', (d) => {
  566. if (d.depth === 0) {
  567. return (-(d.data.name.length + 2) * 16) / 2
  568. }
  569. return -this.config.rectWidth / 2
  570. })
  571. .attr('y', (d) => {
  572. if (d.depth === 0) {
  573. return -15
  574. }
  575. return -this.config.rectHeight / 2
  576. })
  577. .attr('rx', 5)
  578. .attr('stroke-width', 1)
  579. .attr('stroke', (d) => {
  580. if (d.depth === 0) {
  581. return '#5682ec'
  582. }
  583. return '#7A9EFF'
  584. })
  585. .attr('fill', (d) => {
  586. if (d.depth === 0) {
  587. return '#7A9EFF'
  588. }
  589. return '#FFFFFF'
  590. })
  591. .on('click', (e, d) => {
  592. this.nodeClickEvent(e, d)
  593. })
  594. // 文本主标题
  595. node2Enter
  596. .append('text')
  597. .attr('class', 'main-title')
  598. .attr('x', (d) => {
  599. return 0
  600. })
  601. .attr('y', (d) => {
  602. if (d.depth === 0) {
  603. return 5
  604. }
  605. return -14
  606. })
  607. .attr('text-anchor', (d) => {
  608. return 'middle'
  609. })
  610. .text((d) => {
  611. if (d.depth === 0) {
  612. return d.data.name
  613. } else {
  614. const levelStr = d.data.level ? '(' + d.data.level + ')' : ''
  615. if (levelStr && d.data.name.indexOf(levelStr) === -1) {
  616. d.data.name = d.data.name + levelStr
  617. }
  618. return d.data.name.length > 11 ? d.data.name.substring(0, 11) : d.data.name
  619. }
  620. })
  621. .attr('fill', (d) => {
  622. if (d.depth === 0) {
  623. return '#FFFFFF'
  624. }
  625. return '#000000'
  626. })
  627. .style('font-size', (d) => (d.depth === 0 ? 16 : 14))
  628. .style(
  629. 'font-family',
  630. "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
  631. )
  632. .on('click', (e, d) => {
  633. this.nodeClickEvent(e, d)
  634. })
  635. // .style('font-weight', 'bold')
  636. // 副标题
  637. node2Enter
  638. .append('text')
  639. .attr('class', 'sub-title')
  640. .attr('x', (d) => {
  641. return 0
  642. })
  643. .attr('y', (d) => {
  644. return 5
  645. })
  646. .attr('text-anchor', (d) => {
  647. return 'middle'
  648. })
  649. .text((d) => {
  650. if (d.depth !== 0) {
  651. const subTitle = d.data.name.substring(11)
  652. if (subTitle.length > 10) {
  653. return subTitle.substring(0, 10) + '...'
  654. }
  655. return subTitle
  656. }
  657. })
  658. .style('font-size', (d) => 14)
  659. .style(
  660. 'font-family',
  661. "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
  662. )
  663. // .style('font-weight', 'bold')
  664. // 控股比例
  665. node2Enter
  666. .append('text')
  667. .attr('class', 'percent')
  668. .attr('x', (d) => {
  669. return 12
  670. })
  671. .attr('y', (d) => {
  672. return 55
  673. })
  674. .text((d) => {
  675. if (d.depth !== 0) {
  676. return d.data.percent
  677. }
  678. })
  679. .attr('fill', '#000000')
  680. .style(
  681. 'font-family',
  682. "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"
  683. )
  684. .style('font-size', (d) => 14)
  685. // 增加展开按钮
  686. const expandBtnG2 = node2Enter
  687. .append('g')
  688. .attr('class', 'expandBtn')
  689. .attr('transform', (d) => {
  690. return `translate(${0},${-this.config.rectHeight / 2})`
  691. })
  692. .style('display', (d) => {
  693. // 如果是根节点,不显示
  694. if (d.depth === 0) {
  695. return 'none'
  696. }
  697. // 如果没有子节点,则不显示
  698. if (!d._children) {
  699. return 'none'
  700. }
  701. })
  702. .on('click', (e, d) => {
  703. if (d.children) {
  704. d._children = d.children
  705. d.children = null
  706. } else {
  707. d.children = d._children
  708. }
  709. this.update(d)
  710. })
  711. expandBtnG2.append('circle').attr('r', 10).attr('fill', '#7A9EFF').attr('cy', -10)
  712. expandBtnG2
  713. .append('text')
  714. .attr('text-anchor', 'middle')
  715. .attr('fill', '#ffffff')
  716. .attr('y', -7)
  717. .style('font-size', 16)
  718. .style('font-family', '微软雅黑')
  719. .text((d) => {
  720. return d.children ? '-' : '+'
  721. })
  722. const link2 = this.gLinks
  723. .selectAll('path.linkOfUpItem')
  724. .data(linksOfUp, (d) => d.target.data.id)
  725. const link2Enter = link2
  726. .enter()
  727. .append('path')
  728. .attr('class', 'linkOfUpItem')
  729. .attr('d', (d) => {
  730. const o = {
  731. source: {
  732. x: source.x0,
  733. y: source.y0,
  734. },
  735. target: {
  736. x: source.x0,
  737. y: source.y0,
  738. },
  739. }
  740. return this.drawLink(o)
  741. })
  742. .attr('fill', 'none')
  743. .attr('stroke', '#7A9EFF')
  744. .attr('stroke-width', 1)
  745. .attr('marker-end', 'url(#markerOfUp)')
  746. // 有元素update更新和元素新增enter的时候
  747. node2
  748. .merge(node2Enter)
  749. .transition(myTransition)
  750. .attr('transform', (d) => {
  751. return `translate(${d.x},${d.y})`
  752. })
  753. .attr('fill-opacity', 1)
  754. .attr('stroke-opacity', 1)
  755. // 有元素消失时
  756. node2
  757. .exit()
  758. .transition(myTransition)
  759. .remove()
  760. .attr('transform', (d) => {
  761. return `translate(${source.x0},${source.y0})`
  762. })
  763. .attr('fill-opacity', 0)
  764. .attr('stroke-opacity', 0)
  765. link2.merge(link2Enter).transition(myTransition).attr('d', this.drawLink)
  766. link2
  767. .exit()
  768. .transition(myTransition)
  769. .remove()
  770. .attr('d', (d) => {
  771. const o = {
  772. source: {
  773. x: source.x,
  774. y: source.y,
  775. },
  776. target: {
  777. x: source.x,
  778. y: source.y,
  779. },
  780. }
  781. return this.drawLink(o)
  782. })
  783. // node数据改变的时候更改一下加减号
  784. const expandButtonsSelection = this.d3.selectAll('g.expandBtn')
  785. expandButtonsSelection
  786. .select('text')
  787. .transition()
  788. .text((d) => {
  789. return d.children ? '-' : '+'
  790. })
  791. this.rootOfDown.eachBefore((d) => {
  792. d.x0 = d.x
  793. d.y0 = d.y
  794. })
  795. this.rootOfUp.eachBefore((d) => {
  796. d.x0 = d.x
  797. d.y0 = d.y
  798. })
  799. },
  800. // 直角连接线 by wushengyuan
  801. drawLink({ source, target }) {
  802. const halfDistance = (target.y - source.y) / 2
  803. const halfY = source.y + halfDistance
  804. return `M${source.x},${source.y} L${source.x},${halfY} ${target.x},${halfY} ${target.x},${target.y}`
  805. },
  806. // 展开所有的节点
  807. expandAllNodes() {
  808. this.drawChart({
  809. type: 'all',
  810. })
  811. },
  812. // 将所有节点都折叠
  813. foldAllNodes() {
  814. this.drawChart({
  815. type: 'fold',
  816. })
  817. },
  818. // 点击节点获取节点数据
  819. nodeClickEvent(e, d) {
  820. console.log('当前节点的数据:', d)
  821. if (d.data.level) {
  822. this.handleChange({ key: d.data.logicId, label: d.data.name })
  823. }
  824. },
  825. },
  826. }
  827. </script>
  828. <style lang="scss" scoped></style>