equity-penetration-chart.vue 25 KB

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