vue2 d3實(shí)現(xiàn)企查查股權(quán)穿透圖股權(quán)結(jié)構(gòu)圖效果詳解
前言
vue3 框架中使用vue2代碼結(jié)合d3完成股權(quán)穿透圖和股權(quán)結(jié)構(gòu)圖(h5)
(沒(méi)錯(cuò)聽(tīng)上去很違規(guī),但我懶得把代碼從vue2改成vue3了,所以是在vue3框架里用vue2寫法完成的)
最終效果:
版本信息:
"d3": "4.13.0",
"vant": "^3.1.5",
"vue": "^3.0.0",
股權(quán)穿透圖基礎(chǔ)功能:
1、默認(rèn)上下游信息展示,如果沒(méi)有上下游信息只展示自己
2、點(diǎn)擊請(qǐng)求子節(jié)點(diǎn)信息展示,收起子節(jié)點(diǎn)
3、全屏功能
4、放大器放大縮?。╮eact項(xiàng)目中不知道為啥使用d3.zoom方法不好使,可能跟網(wǎng)頁(yè)中滾動(dòng)事件沖突有關(guān),最后選擇單獨(dú)防止放大器進(jìn)行放大縮小功能)
5、移動(dòng)功能
股權(quán)結(jié)構(gòu)圖基礎(chǔ)功能:
1、tab切換展示上游或下游信息
2、默認(rèn)展示一層
3、點(diǎn)擊請(qǐng)求子節(jié)點(diǎn)信息展示,收起子節(jié)點(diǎn)
股權(quán)穿透圖代碼:
<template> <div class="father-box"> <div id="rightPenetrationpage" :style="{ 'transition': 'transform .5s ease', '-ms-transition': 'transform .5s ease', '-moz-transition': 'transform .5s ease','-webkit-transition': 'transform .5s ease','-o-transition': 'transform .5s ease'}"> <custom-nav-bar :title="title" left-arrow @on-clickleft="onClickLeft"> </custom-nav-bar> <!-- <div class="full" @click.stop="showFullScreen"> <div class="full-icon"></div> <span>{{isFull ? '退出全屏' :'全屏'}}</span> </div> --> <div id="penetrateChart" :style="{width:'100%',display:'block',margin:'auto'}" > </div> </div> </div> </template> <script lang="ts"> import { defineComponent} from 'vue' import { useStore } from 'vuex' import CustomNavBar from '@/components/common/CustomNavbar.vue' import { fetchCompanySearchDetail, fetchEquityUpperInfo, fetchEquityBelowInfo } from '@/api/companySearch' import { Notify, Toast } from 'vant' import { formatMoney, getBLen } from '@/utils/tool' import { sm2Decrypted } from '@/enrich/crypto-gm' import { GlobalMutation } from '@/store/types/mutation-types' import * as $d3 from 'd3' // 過(guò)渡時(shí)間 const DURATION = 0 // 加減符號(hào)半徑 const SYMBOLA_S_R = 9 // 公司 const COMPANY = '0' // 人 const PERSON = '1' //x,y距離 // let x0 = 0, y0 = 0, dx = 0, dy = 0 export default defineComponent({ props: {}, components: { CustomNavBar }, data () { return { layoutTree: {} as any, diamonds: {} as any, d3: $d3, // hasChildNodeArr: [], originDiamonds: {} as any, diagonalUp: '', diagonalDown: '', tree: {} as any, rootUp: {} as any, rootDown: {} as any, svg: {} as any, svgW: document.documentElement.clientWidth, svgH: document.documentElement.clientHeight - 44, title: '股權(quán)穿透圖', isFull: false, name: '', id: '', token: '', regCapi: '', userid: '', parents: [] as any[], // 下游信息 children: [] as any[], // 上游信息 } }, // beforeCreate () { // document.body.style.overflow = 'hidden' // }, // beforeDestroy () { // document.body.style.overflow = 'auto' // }, // created () { // // window.addEventListener('orientationchange', this.changeOrient) // }, mounted () { const store = useStore() const data = this.$route.query.data ? JSON.parse(sm2Decrypted(this.$route.query.data)) : {} const id = data.id const token = data.token ? data.token : store.state.global.token const userid = data.userid this.id = id this.token = token this.userid = userid store.commit(`global/${GlobalMutation.SET_TOKEN}`, token) Toast.loading({ message: '加載中', forbidClick: true, duration: 0, }); this.getInit() }, beforeUnmount() { this.d3.select('#treesvg').remove() console.log('頁(yè)面關(guān)閉') }, methods: { // changeOrient () { // const box = document.getElementById('penetrateChart').children[0] // const g = document.getElementById('penetrateChart').children[0].children[0] // let navbar = document.querySelector('.navbar') // let flag = false // flag = isOrient() // setTimeout(()=>{ // if(flag){ // navbar?.classList.add('smallBar') // }else{ // navbar?.classList.remove('smallBar') // } // console.log(document.documentElement.clientWidth, document.documentElement.clientHeight) // box.setAttribute('width', document.documentElement.clientWidth) // box.setAttribute('height', document.documentElement.clientHeight) // g.setAttribute('transform', 'translate(' + (document.documentElement.clientWidth / 2) + ',' + (document.documentElement.clientHeight / 2) + ')') // }, 100) // }, async getDetailInfo(){ await fetchCompanySearchDetail({ token: this.token, instId: this.id, userId: this.userid }).then((response)=>{ const {code =0, records = [] } = response if (code > 0 && records != null) { this.regCapi = records[0].reg_capi this.name = records[0].chn_full_nm } }) }, async getUpper(){ await fetchEquityUpperInfo({ token: this.token, instId: this.id, regCapi: this.regCapi, currentPage: 0, pageSize: 200, }).then((response) => { const {code =0, records = [] } = response if (code > 0 && records != null) { const dataSource = [] as any[]; records.forEach(element =>{ // let children = [] // // 設(shè)置children節(jié)點(diǎn) // if(element.list){ // element.list.forEach(child =>{ // children.push({ // money: child.amount ? formatMoney((child.amount / 10000).toFixed(2)) :'--', // scale: child.hold_rati || '--%', // name: child.chn_full_nm || '--', // id: child.inst_cust_id || '--', // type: '0' // }) // }) // } dataSource.push({ // children: children, isHaveChildren: element.dataType === '1' ? true : false, money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) : '--', scale: element.hold_rati || '--%', name: element.chn_full_nm || '--', id: element.inst_cust_id || '--', type: '0', regCapi: element.reg_capi }) }) this.parents = dataSource } }) }, async getBelow(){ await fetchEquityBelowInfo({ token: this.token, instId: this.id, currentPage: 0, pageSize: 200, }).then((response) => { const {code =0, records = []} = response if (code > 0 && records != null) { const dataSource = [] as any[]; records.forEach(element =>{ // let children = [] // // 設(shè)置children節(jié)點(diǎn) // if(element.list){ // element.list.forEach(child =>{ // children.push({ // money: child.amount ? formatMoney((child.amount / 10000).toFixed(2)) :'--', // scale: child.hold_rati || '--%', // name: child.chn_full_nm || '--', // id: child.inst_cust_id || '--', // type: '0' // }) // }) // } dataSource.push({ // children: children, isHaveChildren: element.dataType === '1' ? true : false, money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--', scale: element.hold_rati || '--%', name: element.chn_full_nm || '--', id: element.inst_cust_id || '--', type: '0', }) }) this.children = dataSource } }) }, // 獲取樹狀數(shù)據(jù) getTreeData(){ console.log( this.children, this.parents, '111111111') let obj = { id: this.id, name: this.name, tap: '節(jié)點(diǎn)', children: this.children, parents: this.parents, } this.tree = {...obj} Toast.clear() }, async getInit(){ await this.getDetailInfo() // await this.getUpper() // await this.getBelow() Promise.all([this.getUpper(), this.getBelow()]).finally(()=>{ this.getTreeData() this.init() }) }, init () { let d3 = this.d3 let svgW = this.svgW let svgH = this.svgH // x0 = svgW / 2, // y0= svgH / 2 // 方塊形狀 this.diamonds = { w: 162, h: 66, intervalW: 182, intervalH: 150 } // 源頭對(duì)象 this.originDiamonds = { w: 208, h: 41 } this.layoutTree = d3.tree().nodeSize([this.diamonds.intervalW, this.diamonds.intervalH]).separation(() => 1); // 主圖 this.svg = d3.select('#penetrateChart').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvg') .call(d3.zoom().scaleExtent([0.3, 2]).on('zoom', () => { // 設(shè)置縮放位置以及平移初始位 // if(isiOS && this.isFull){ // // 修改ios手機(jī)上才有的移動(dòng)bug,安卓手機(jī),pc端沒(méi)有 // let x = d3.event.transform.x // d3.event.transform.x = d3.event.transform.y // d3.event.transform.y = -x // console.log('222', '出現(xiàn)移動(dòng)bug', d3.event.transform) // console.log(isiOS, d3.event.transform.x, d3.event.transform.y) // // dx = d3.event.transform.x - x0 // // dy = d3.event.transform.y - y0 // // x0 = d3.event.transform.x // // y0 = d3.event.transform.y // // d3.event.transform.x = d3.event.transform.x + dy // // d3.event.transform.y = d3.event.transform.y + dx // // this.svg.attr('transform', 'translate(' + (svgW / 2) + ',' + (svgH / 2) + ') rotate(90)') // } this.svg.attr('transform', d3.event.transform.translate(svgW / 2, svgH / 2)); })) .on('dblclick.zoom', null) .attr('style', 'position: relative;z-index: 2') //background-image:url(${setWatermark({name: this.$store.state.global.user.userName, loginName: this.$store.state.global.user.userId}).toDataURL()}) .append('g').attr('id', 'g').attr('transform', `translate(${svgW / 2},${svgH / 2})`) let upTree = {} as any let downTree = {} as any // 拷貝樹的數(shù)據(jù) Object.keys(this.tree).map(item => { if (item === 'parents') { upTree = JSON.parse(JSON.stringify(this.tree)) upTree.children = this.tree[item] upTree.parents = null } else if (item === 'children') { downTree = JSON.parse(JSON.stringify(this.tree)) downTree.children = this.tree[item] downTree.parents = null } }) // hierarchy 返回新的結(jié)構(gòu) x0,y0初始化起點(diǎn)坐標(biāo) this.rootUp = d3.hierarchy(upTree, d => d.children); this.rootUp.x0 = 0 this.rootUp.y0 = 0 this.rootDown = d3.hierarchy(downTree, d => d.children); this.rootDown.x0 = 0 this.rootDown.y0 = 0; // 上 和 下 結(jié)構(gòu) let treeArr = [ { data: this.rootUp, type: 'up' }, { data: this.rootDown, type: 'down' } ] if(!this.tree['children'].length && !this.tree['parents'].length){ this.updataSelf() }else{ treeArr.map(item => { if (item.data.children) { // item.data.children.forEach(this.collapse); this.update(item.data, item.type, item.data) } }) } }, updataSelf(){ let nodes = this.rootUp.descendants() let node = this.svg.selectAll('g.node') .data(nodes, d => d.data.id || ''); let nodeEnter = node.enter().append('g') .attr('class', d => 'node node_' + d.depth) //d => showtype === 'up' && !d.depth ? 'hide-node' : // .attr('transform', 'translate(' + (svgW / 2) + ',' + (svgH / 2) + ')') .attr('opacity', 1); // 擁有下部分則隱藏初始?jí)K d => showtype === 'up' && !d.depth ? (this.rootDown.data.children. length ? 0 : 1) : 1 // 創(chuàng)建矩形 nodeEnter.append('rect') .attr('type', d => d.data.id + '_' + d.depth) .attr('width', d => d.depth ? this.diamonds.w : (getBLen(d.data.name)/2 * 20 + 20)) .attr('height', d => d.depth ? (d.data.type === COMPANY ? this.diamonds.h : this.diamonds.h - 10) : this.originDiamonds.h) .attr('x', d => d.depth ? -this.diamonds.w / 2 : -(getBLen(d.data.name)/2 * 20 + 20) / 2) .attr('y', d => d.depth ? 0 : -15) .attr('stroke', '#DE4A3C') .attr('stroke-width', 1) .attr('rx', 10) .attr('ry', 10) .style('fill', d => { if (d.data.type === COMPANY || !d.depth) { return d.depth ? '#fff' : '#DE4A3C' } else if (d.data.type === PERSON) { return '#fff' } }); // 文字 nodeEnter.append('text') .attr('x', 0) .attr('y', 0) .attr('dy', `${this.originDiamonds.h/2 - 10}px`) .attr('text-anchor', 'middle') .attr('fill', d => d.depth ? '#DE4A3C' : '#fff') .text(d => d.data.name) .style('font-size', d => d.depth ? '16px' : '20px') .style('font-family', 'PingFangSC-Medium') .style('font-weight', '500') }, /* *[update 函數(shù)描述], [click 函數(shù)描述] * @param {[Object]} source 第一次是初始源對(duì)象,后面是點(diǎn)擊的對(duì)象 * @param {[String]} showtype up表示向上 down表示向下 * @param {[Object]} sourceTree 初始源對(duì)象 */ update (source, showtype, sourceTree) { // eslint-disable-next-line let _this = this if (source.parents === null) { source.isOpen = !source.isOpen } let nodes if (showtype === 'up') { nodes = this.layoutTree(this.rootUp).descendants() } else { nodes = this.layoutTree(this.rootDown).descendants() } let links = nodes.slice(1); nodes.forEach(d => { d.y = d.depth *(d.depth == 1 ? 120 : this.diamonds.intervalH); }); let node = this.svg.selectAll('g.node' + showtype) .data(nodes, d => d.data.id || ''); let nodeEnter = node.enter().append('g') .attr('class', d => showtype === 'up' && !d.depth ? 'hide-node' : 'node' + showtype) .attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + d.y + ')') .attr('opacity', d => showtype === 'up' && !d.depth ? (this.rootDown.data.children.length ? 0 : 1) : 1); // 擁有下部分則隱藏初始?jí)K // 創(chuàng)建矩形 nodeEnter.append('rect') .attr('type', d => d.data.id) .attr('width', d => d.depth ? this.diamonds.w : (getBLen(d.data.name)/2 * 20 + 20)) .attr('height', d => d.depth ? (d.data.type === COMPANY ? this.diamonds.h : this.diamonds.h - 10) : this.originDiamonds.h) .attr('x', d => d.depth ? -this.diamonds.w / 2 : -(getBLen(d.data.name)/2 * 20 + 20) / 2) .attr('y', d => d.depth ? showtype === 'up' ? -this.diamonds.h / 2 : 0 : -15) .attr('stroke', d => d.data.type === COMPANY || !d.depth ? '#DE4A3C' : '#7A9EFF') .attr('stroke-width', 1) .attr('rx', 10) .attr('ry', 10) .style('fill', d => { if (d.data.type === COMPANY || !d.depth) { return d.depth ? '#fff' : '#DE4A3C' } else if (d.data.type === PERSON) { return '#fff' } }); // 創(chuàng)建圓 加減 let circle = nodeEnter.append('g') .attr('class', 'circle') .on('click', function (d) { _this.click(d, showtype, sourceTree) }); circle.append('circle') .attr('type', d => d.data.id || '') .attr('r', (d) => d.depth ? (d.data.isHaveChildren ? SYMBOLA_S_R : 0) : 0) .attr('cy', d => d.depth ? showtype === 'up' ? -(SYMBOLA_S_R + this.diamonds.h / 2) : (this.diamonds.h + SYMBOLA_S_R) : 0) .attr('cx', 0) .attr('fill', '#F9DDD9') .attr('stroke', '#FCEDEB') .style('stroke-width', 1) circle.append('text') .attr('x', 0) .attr('dy', d => d.depth ? (showtype === 'up' ? -(SYMBOLA_S_R / 2 + this.diamonds.h / 2) : this.diamonds.h + SYMBOLA_S_R + 4) : 0) .attr('text-anchor', 'middle') .attr('class', 'fa') .style('fill', '#DE4A3C') .text(function(d) { if(d.depth){ if (d.children) { return '-'; } else if (d._children || d.data.isHaveChildren) { return '+'; } else { return ''; } }else { return ''; } }) .style('font-size', '16px'); node.select('.fa') .text(function (d) { if (d.children) { return '-'; } else if (d._children || d.data.isHaveChildren) { return '+'; } else { return ''; } }) // 持股比例 nodeEnter.append('g') .attr('transform', () => 'translate(0,0)') .append('text') .attr('x', 35) .attr('y', showtype === 'up' ? this.diamonds.h -10 : -10) .attr('text-anchor', 'middle') .attr('fill', d => d.data.type === COMPANY ? '#DE4A3C' : '#7A9EFF') .attr('opacity', d => !d.depth ? 0 : 1) .text(d => d.data.scale) .style('font-size', '14px') .style('font-family', 'PingFangSC-Regular') .style('font-weight', '400'); // 公司名稱 // y軸 否表源頭的字體距離 nodeEnter.append('text') .attr('x', 0) .attr('y', d => { // 如果是上半部分 if (showtype === 'up') { // 如果是1層以上 if (d.depth) { return -this.diamonds.h / 2 } else { return 0 } } else { if (d.depth) { return 0 } else { // if (d.data.name.length > 10) { // return -5 // } return 0 } } }) .attr('dy', d => d.depth ? (d.data.name.length > 10 ? '1.3em' : '1.8em') : `${this.originDiamonds.h/2 - 10}px`) .attr('text-anchor', 'middle') .attr('fill', d => d.depth ? '#DE4A3C' : '#fff') .text(d =>d.depth ? (d.data.name.length > 10) ? d.data.name.substr(0, 10) : d.data.name : d.data.name) .style('font-size', d => d.depth ? '14px' : '18px') .style('font-family', 'PingFangSC-Medium') .style('font-weight', '500') .on('click', (d) => { if(d.data.id && d.depth){ // 跳轉(zhuǎn)操作之類的 } }); // 名稱過(guò)長(zhǎng) 第二段 nodeEnter.append('text') .attr('x', 0) .attr('y', d => { // ? (d.depth ? -this.diamonds.h / 2 : 0) : 0 if (showtype === 'up') { if (d.depth) { return -this.diamonds.h / 2 } return 8 } else { if (!d.depth) { return 8 } return 0 } }) .attr('dy', d => d.depth ? '2.5em' : '.3em') .attr('text-anchor', 'middle') .attr('fill', d => d.depth ? '#DE4A3C' : '#fff') .text(d => { // 索引從第19個(gè)開(kāi)始截取有表示超出 if(d.depth){ if (d.data.name.substr(19, 1)) { return d.data.name.substr(10, 9) + '...' } return d.data.name.substr(10, 9) }else{ return null } }) .style('font-size', '14px') .style('font-family', 'PingFangSC-Medium') .style('font-weight', '500'); // 認(rèn)繳金額 nodeEnter.append('text') .attr('x', 0) .attr('y', showtype === 'up' ? -this.diamonds.h / 2 : 0) .attr('dy', d => d.data.name.substr(10, d.data.name.length).length ? '4.5em' : '4.1em') .attr('text-anchor', 'middle') .attr('fill', d => d.depth ? '#445166' : '#fff') .text(d => d.data.money ? d.data.money.length > 12 ? `認(rèn)繳金額:${d.data.money.substr(0, 12)}…` : `認(rèn)繳金額:${d.data.money}萬(wàn)元` : '') .style('font-size', '12px') .style('font-family', 'PingFangSC-Regular') .style('font-weight', '400') .style('color', '#666666'); /* * 繪制箭頭 * @param {string} markerUnits [設(shè)置為strokeWidth箭頭會(huì)隨著線的粗細(xì)發(fā)生變化] * @param {string} viewBox 坐標(biāo)系的區(qū)域 * @param {number} markerWidth,markerHeight 標(biāo)識(shí)的大小 * @param {string} orient 繪制方向,可設(shè)定為:auto(自動(dòng)確認(rèn)方向)和 角度值 * @param {number} stroke-width 箭頭寬度 * @param {string} d 箭頭的路徑 * @param {string} fill 箭頭顏色 * @param {string} id resolved0表示公司 resolved1表示個(gè)人 * 直接用一個(gè)marker達(dá)不到兩種顏色都展示的效果 */ nodeEnter.append('marker') .attr('id', showtype + 'resolved0') .attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse') .attr('viewBox', '0 -5 10 10') .attr('markerWidth', 12) .attr('markerHeight', 12) .attr('orient', '90') .attr('refX', () => showtype === 'up' ? '-50' : '10') .attr('stroke-width', 2) .attr('fill', '#DE4A3C') .append('path') .attr('d', 'M0,-5L10,0L0,5') .attr('fill', '#DE4A3C'); nodeEnter.append('marker') .attr('id', showtype + 'resolved1') .attr('markerUnits', 'strokeWidth') .attr('markerUnits', 'userSpaceOnUse') .attr('viewBox', '0 -5 10 10') .attr('markerWidth', 12) .attr('markerHeight', 12) .attr('orient', '90') .attr('refX', () => showtype === 'up' ? '-50' : '10') .attr('stroke-width', 2) .attr('fill', '#DE4A3C') .append('path') .attr('d', 'M0,-5L10,0L0,5') .attr('fill', '#7A9EFF'); // 將節(jié)點(diǎn)轉(zhuǎn)換到它們的新位置。 let nodeUpdate = node // .transition() // .duration(DURATION) .attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + (d.y) + ')'); // 將退出節(jié)點(diǎn)轉(zhuǎn)換到父節(jié)點(diǎn)的新位置. let nodeExit = node.exit() // .transition() // .duration(DURATION) .attr('transform', () => showtype === 'up' ? 'translate(' + source.x + ',' + -(source.y) + ')' : 'translate(' + source.x + ',' + (parseInt(source.y)) + ')') .remove(); nodeExit.select('rect') .attr('width', this.diamonds.w) .attr('height', this.diamonds.h) .attr('stroke', 'black') .attr('stroke-width', 1); // 修改線條 let link = this.svg.selectAll('path.link' + showtype) .data(links, d => d.data.id); // 在父級(jí)前的位置畫線。 let linkEnter = link.enter().insert('path', 'g') .attr('class', 'link' + showtype) .attr('marker-start', d => `url(#${showtype}resolved${d.data.type})`)// 根據(jù)箭頭標(biāo)記的id號(hào)標(biāo)記箭頭 .attr('stroke', d => d.data.type === COMPANY ? '#DE4A3C' : '#7A9EFF') .style('fill-opacity', 1) .attr('fill', 'none') .attr('stroke-width', '1px') .attr('d', () => { let o = {x: source.x0, y: source.y0}; return _this.diagonal(o, o, showtype) }); let linkUpdate = linkEnter.merge(link); // 過(guò)渡更新位置. linkUpdate // .transition() // .duration(DURATION) .attr('d', d => _this.diagonal(d, d.parent, showtype)); // 將退出節(jié)點(diǎn)轉(zhuǎn)換到父節(jié)點(diǎn)的新位置 link.exit() // .transition() // .duration(DURATION) .attr('d', () => { let o = { x: source.x, y: source.y }; return _this.diagonal(o, o, showtype) }).remove(); // 隱藏舊位置方面過(guò)渡. nodes.forEach(d => { d.x0 = d.x; d.y0 = d.y }); }, // 拷貝到_children 隱藏1排以后的樹 // collapse (source) { // if (source.children) { // source._children = source.children; // source._children.forEach(this.collapse); // source.children = null; // this.hasChildNodeArr.push(source); // } // }, // 獲取點(diǎn)擊上游的上游 async fetchUpper (id, regCapi){ Toast.loading({ message: '加載中', forbidClick: true, duration: 0, }); const dataSource = []; try{ const response = await fetchEquityUpperInfo({ token: this.token, instId: id, currentPage: 0, pageSize: 200, regCapi: regCapi, }) const {code =0, records = []} = response if (code > 0 && records != null) { const dataSource = [] as any[]; records.forEach(element =>{ dataSource.push({ isHaveChildren: null, money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--', scale: element.hold_rati || '--%', name: element.chn_full_nm || '--', id: element.inst_cust_id || '--', type: '0' }) }) Toast.clear() return dataSource }else{ Toast.clear() return dataSource } }catch(error){ Toast.clear() return dataSource } }, // 獲取點(diǎn)擊下游的下游 async fetchBelow (id){ Toast.loading({ message: '加載中', forbidClick: true, duration: 0, }); const dataSource = []; try{ const response = await fetchEquityBelowInfo({ token: this.token, instId: id, currentPage: 0, pageSize: 200, }) const {code =0, records = []} = response if (code > 0 && records != null) { const dataSource = [] as any[]; records.forEach(element =>{ dataSource.push({ isHaveChildren: null, money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--', scale: element.hold_rati || '--%', name: element.chn_full_nm || '--', id: element.inst_cust_id || '--', type: '0' }) }) Toast.clear() return dataSource }else{ Toast.clear() return dataSource } }catch(error){ Toast.clear() return dataSource } }, async click (source, showType, sourceTree) { // 不是起點(diǎn)才能點(diǎn) // if (source.depth) { // if (source.children) { // source._children = source.children; // source.children = null; // } else { // source.children = source._children; // source._children = null; // } // } if(source.children){ // 點(diǎn)擊減號(hào) source._children = source.children; source.children = null; }else { // 點(diǎn)擊加號(hào) if(!source._children){ let res = [] as any[] if(showType === 'up'){ res = await this.fetchUpper(source.data.id, source.data.regCapi) }else { res = await this.fetchBelow(source.data.id) } if(!res.length){ Notify({ message: '上游或下游企業(yè)信息為空!', type: 'warning', duration: 1500 }) return } res.forEach(item =>{ let newNode = this.d3.hierarchy(item) newNode.depth = source.depth + 1; newNode.height = source.height - 1; newNode.parent = source; if(!source.children){ source.children = []; source.data.children = []; } source.children.push(newNode); source.data.children.push(newNode.data); }) }else{ source.children = source._children; source._children = null; } } this.update(source, showType, sourceTree) }, diagonal (s, d, showtype) { // 折線 let endMoveNum = 0; let moveDistance = 0; if (d) { if (showtype == 'down') { let downMoveNum = d.depth ? this.diamonds.h/2 : this.originDiamonds.h/2 -10 ; // var downMoveNum = 30; let tmpNum = s.y + (d.y - s.y) / 2; endMoveNum = downMoveNum; moveDistance = tmpNum + endMoveNum; } else { let upMoveNum = d.depth ? 0 : -this.originDiamonds.h/2 + 5 ; let tmpNum = d.y + (s.y - d.y) / 2; endMoveNum = upMoveNum; moveDistance = tmpNum + endMoveNum; } } if (showtype === 'up') { return ( 'M' + s.x + ',' + -s.y + 'L' + s.x + ',' + -moveDistance + 'L' + d.x + ',' + -moveDistance + 'L' + d.x + ',' + -d.y ); }else { return ( 'M' + s.x + ',' + s.y + 'L' + s.x + ',' + moveDistance + 'L' + d.x + ',' + moveDistance + 'L' + d.x + ',' + d.y ); } }, resetSvg () { this.d3.select('#treesvg').remove() this.init() }, // 點(diǎn)擊全屏 showFullScreen(){ let width = document.documentElement.clientWidth, height = document.documentElement.clientHeight, wrapper = document.getElementById('rightPenetrationpage') as HTMLElement, style = ''; const navbar = document.querySelector('.navbar') as HTMLElement const fullScreen = document.querySelector('.full') as HTMLElement // const box = document.getElementById('penetrateChart').children[0] // const g = document.getElementById('penetrateChart').children[0].children[0] // setTimeout(()=>{ // // box.setAttribute('width', width) // // box.setAttribute('height', height - 44) // g.setAttribute('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')') // }, 200) if (this.isFull) { // 豎屏 console.log('豎過(guò)來(lái)') this.isFull = false this.svgH = height - 44 this.svgW = width // 設(shè)置按鈕和頂部樣式 fullScreen.classList.remove('fullRight') navbar.classList.remove('smallBar') style += 'width:100%'; style += 'height:100%;'; style += '-webkit-transform: translateX(0) translateZ(0px) rotate(0); -ms-transform: translateX(0) translateZ(0px) rotate(0); -moz-transform:translateX(0) translateZ(0px) rotate(0); -o-transform: translateX(0) translateZ(0px) rotateY(0); transform: translateX(0) translateZ(0px) rotate(0);'; style += '-webkit-transform-origin: 0 0;'; style += '-ms-transform-origin: 0 0;'; style += '-moz-transform-origin: 0 0;'; style += '-o-transform-origin: 0 0;'; style += 'transform-origin: 0 0;'; } else { // 橫屏 console.log('橫過(guò)來(lái)') this.isFull = true this.svgH = width - 44 this.svgW = height // 設(shè)置按鈕和頂部樣式 fullScreen.classList.add('fullRight') navbar.classList.add('smallBar') style += 'width:' + height + 'px;';// 注意旋轉(zhuǎn)后的寬高切換 style += 'height:' + width + 'px;'; style += '-webkit-transform: translateX(0) translateZ(0px) rotate(90deg); -ms-transform: translateX(0) translateZ(0px) rotate(90deg); -moz-transform: translateX(0) translateZ(0px) rotate(90deg); -o-transform: translateX(0) translateZ(0px) rotate(90deg); transform:translateX(0) translateZ(0px) rotate(90deg);'; // 注意旋轉(zhuǎn)中點(diǎn)的處理 style += 'transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;'; style += '-webkit-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;'; style += '-ms-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;'; style += '-moz-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;'; style += '-o-transform-origin: ' + width / 2 + 'px ' + width / 2 + 'px;'; } wrapper.style.cssText = style; // 重新渲染圖片 this.resetSvg() }, // 點(diǎn)擊返回 onClickLeft(){ // jsBridge.callHandler('navigationToSkip', {type: '0'}, ()=> { // console.log('11111111111') // }) history.back() } } }) </script> <style lang="scss" scoped> .father-box{ transform: perspective(1000px); -ms-transform: perspective(1000px); -moz-transform: perspective(1000px); -webkit-transform: perspective(1000px); -o-transform: perspective(1000px); } .info-icon { width: 16px; height: 16px; background-image: url('../../assets/icon/icon_info.png'); background-repeat: no-repeat; background-size: cover; } .full { position: absolute; top: 12px; right:20px; font-size: 14px; color: #DE4A3C; display: flex; z-index: 9999; line-height: 24px; .full-icon { width: 24px; height: 24px; background-image: url('../../assets/icon/icon_fullscreen.png'); background-repeat: no-repeat; background-size: cover; margin-right: 7px; } } .fullRight{ top: 12px !important; right:35px; } .smallBar { :deep(.van-nav-bar__left){ display: none; } :deep(.van-nav-bar__content){ background-color: transparent; } :deep(.van-nav-bar__title){ // font-size: 4.8vh; margin-left:30px; } } </style>
股權(quán)結(jié)構(gòu)圖代碼:
<template> <div id="structureChartIn" :style="{width:'100%',display:'block',margin:'auto'}" > </div> </template> <script lang="ts"> // import { setWatermark } from '@/utils/tool.js' import { defineComponent} from 'vue' import { formatMoney, getBLen } from '@/utils/tool' import { fetchEquityUpperInfo } from '@/api/companySearch' import { Notify, Toast } from 'vant' import * as $d3 from 'd3' // 過(guò)渡時(shí)間 const DURATION = 400 // 加減符號(hào)半徑 const SYMBOLA_S_R = 9 // // 公司 // const COMPANY = '0' // // 人 // const PERSON = '1' export default defineComponent({ props: { tree: { type: Object, default: () => { return {} } }, token: { type: String, default: '' } }, components: { }, data () { return { diamonds: {} as any, originDiamonds: {} as any, d3: $d3, // hasChildNodeArr: [], root: {} as any, svg: {} as any, svgW: document.documentElement.clientWidth, svgH: document.documentElement.clientHeight - 88, title: '股權(quán)結(jié)構(gòu)圖', lastClickD: null, } }, watch: { tree(newVal){ if(newVal.name){ this.init() } } }, mounted () { // window.addEventListener('orientationchange', this.changeOrient) }, // beforeUnmount(){ // window.removeEventListener('orientationchange', this.changeOrient) // }, methods: { // changeOrient () { // const box = document.getElementById('structureChartIn').children[0] // const g = document.getElementById('structureChartIn').children[0].children[0] // let navbar = document.querySelector('.navbar') // let flag = false // flag = isOrient() // setTimeout(()=>{ // if(flag){ // navbar?.classList.add('smallBar') // }else{ // navbar?.classList.remove('smallBar') // } // console.log(document.documentElement.clientWidth, document.documentElement.clientHeight) // box.setAttribute('width', document.documentElement.clientWidth) // box.setAttribute('height', document.documentElement.clientHeight) // g.setAttribute('transform', 'translate(' + (document.documentElement.clientWidth / 2) + ',' + (document.documentElement.clientHeight / 2) + ')') // }, 100) // }, init () { let d3 = this.d3 let svgW = this.svgW let svgH = this.svgH let margin = {top: 20, right: 20, bottom: 30, left: 10} // 方塊形狀 this.diamonds = { w: 320, h: 60, } // 源頭對(duì)象 this.originDiamonds = { w: 208, h: 36 } // 主圖 this.svg = d3.select('#structureChartIn').append('svg').attr('width', svgW).attr('height', svgH).attr('id', 'treesvgIn') .call(d3.zoom().scaleExtent([0.3, 2]).on('zoom', () => { const transform = d3.event.transform this.svg.attr('transform', transform.translate(margin.left, margin.top)); })) .on('dblclick.zoom', null) .attr('style', 'position: relative;z-index: 2') // background-image:url(${setWatermark({name: this.$store.state.global.user.userName, loginName: this.$store.state.global.user.userId}).toDataURL()}) .append('g').attr('id', 'gIn') .attr('transform', `translate(${margin.left},${margin.top})`) // 拷貝樹的數(shù)據(jù) let downTree = {} as any Object.keys(this.tree).map(item => { if (item === 'children') { downTree = JSON.parse(JSON.stringify(this.tree)) downTree.children = this.tree[item] } }) // hierarchy 返回新的結(jié)構(gòu) x0,y0初始化起點(diǎn)坐標(biāo) this.root = d3.hierarchy(downTree); this.root.x0 = 0 this.root.y0 = 0 if(!this.root.children){ this.update(this.root) }else { // this.root.children.forEach(this.collapse) this.update(this.root) } }, /* *[update 函數(shù)描述], [click 函數(shù)描述] * @param {[Object]} source 第一次是初始源對(duì)象,后面是點(diǎn)擊的對(duì)象 */ update (source) { // eslint-disable-next-line let _this = this let nodes= this.root.descendants() let index = -1, count = 0; this.root.eachBefore(function(n) { count+=20; n.style = 'node_' + n.depth; n.x = ++index * _this.diamonds.h + count; n.y = n.depth * 25; // 設(shè)置下一層水平位置向后移25px }); let node = this.svg.selectAll('g.node') .data(nodes, d => { return d.data.id || '' } ); let nodeEnter = node.enter().append('g') .attr('class', d => 'node node_' + d.depth) .attr('transform', 'translate(' + source.y0 + ',' + source.x0 + ')') .attr('opacity', 0); // 創(chuàng)建矩形 nodeEnter.append('rect') .attr('type', d => d.data.id) .attr('width', d => d.depth ? this.diamonds.w : (d.data.children.length ? (getBLen(d.data.name)/2 * 18 + 62) : (getBLen(d.data.name)/2 * 18 + 20))) .attr('height', d => d.depth ? this.diamonds.h : this.originDiamonds.h) .attr('y', -this.diamonds.h / 2) .style('stroke', '#DE4A3C') .attr('stroke-width', 1) .attr('rx', 6) .attr('ry', 6) .style('fill', d => { return d.data.tap ? '#DE4A3C' : '#fff' }); nodeEnter.append('rect') .attr('y', -this.diamonds.h / 2) .attr('height', d => d.depth ? this.diamonds.h : this.originDiamonds.h) .attr('width', 6) .attr('rx', 6) .attr('ry', 6) .style('fill', '#DE4A3C') // 文字 nodeEnter.append('text') .attr('dy', d=> d.depth ? -7 : -5) .attr('dx', d=> d.depth ? 36 : (d.data.children.length ? 36 : 10)) .style('font-size', d=> d.depth ? '14px' : '18px') .style('font-weight', '500') .attr('fill', d => d.depth ? '#333333' : '#fff') .text(function(d) { // 名字長(zhǎng)度超過(guò)進(jìn)行截取 if(d.depth){ if(d.data.name.length>20){ return d.data.name.substring(0, 19) + '...'; } } return d.data.name; }) .on('click', (d) => { if(d.data.id && d.depth){ // 跳轉(zhuǎn)操作之類的 } }); // 持股比例 nodeEnter.append('text') .attr('dy', 17) .attr('dx', 36) .style('font-size', '12px') .style('fill', '#666666') .text(function(d) { if(!d.data.tap){ return ('持股比例 ' +':') } }); nodeEnter.append('text') .attr('dy', 17) .attr('dx', 95) .style('font-size', '12px') .style('fill', '#DE4A3C') .text(function(d) { if(!d.data.tap){ return (d.data.scale) } }); // 認(rèn)繳金額 nodeEnter.append('text') .attr('dy', 17) .attr('dx', 150) .style('font-size', '12px') .style('fill', '#666666') .text(function(d) { if(!d.data.tap){ return ('認(rèn)繳金額 ' +':') } }); nodeEnter.append('text') .attr('dy', 17) .attr('dx', 210) .style('font-size', '12px') .style('fill', '#DE4A3C') .text(function(d) { if(!d.data.tap){ if(d.data.money.length > 14){ return d.data.money.substr(0, 14) + '...' }else{ return (d.data.money + '萬(wàn)元') } } }); // 箭頭 // nodeEnter.append('text') // .attr('dy', 5.5) // .attr('dx', 200 ) // .style('font-size', '20px') // .style('fill', '#000') // .text(function(d) { // if(!d.data.tap){ // return '>' // } // }); // 創(chuàng)造圓 加減 let circle = nodeEnter.append('g') .attr('class', 'circle') .on('click', _this.click); circle.append('circle') .style('fill', '#F9DDD9') .style('stroke', '#FCEDEB') .style('stroke-width', 1) .attr('r', function (d) { if (d.children || d.data.isHaveChildren) { return 9; } else { return 0; } }) .attr('cy', d => d.depth ? 0 : (- SYMBOLA_S_R -3)) .attr('cx', 20) circle.append('text') .attr('dy', d => d.depth ? 4.5 : -7) .attr('dx', 20) .attr('text-anchor', 'middle') .attr('class', 'fa') .style('fill', '#DE4A3C') .text(function(d) { if (d.children) { return '-'; } else if (d._children || d.data.isHaveChildren) { return '+'; } else { return ''; } }) .style('font-size', '16px'); node.select('.fa') .text(function (d) { if (d.children) { return '-'; } else if (d._children || d.data.isHaveChildren) { return '+'; } else { return ''; } }) /* * 繪制箭頭 * @param {string} markerUnits [設(shè)置為strokeWidth箭頭會(huì)隨著線的粗細(xì)發(fā)生變化] * @param {string} viewBox 坐標(biāo)系的區(qū)域 * @param {number} markerWidth,markerHeight 標(biāo)識(shí)的大小 * @param {string} orient 繪制方向,可設(shè)定為:auto(自動(dòng)確認(rèn)方向)和 角度值 * @param {number} stroke-width 箭頭寬度 * @param {string} d 箭頭的路徑 * @param {string} fill 箭頭顏色 */ // nodeEnter.append('marker') // .attr('id', 'resolvedIn') // .attr('markerUnits', 'strokeWidth') // .attr('markerUnits', 'userSpaceOnUse') // .attr('viewBox', '0 -5 10 10') // .attr('markerWidth', 8) // .attr('markerHeight', 8) // .attr('orient', '0') // .attr('refX', '10') // // .attr('refY', '10') // .attr('stroke-width', 2) // .attr('fill', '#DE4A3C') // .append('path') // .attr('d', 'M0,-5L10,0L0,5') // .attr('fill', '#DE4A3C'); // 將節(jié)點(diǎn)轉(zhuǎn)換到它們的新位置。 nodeEnter // .transition() // .duration(DURATION) .attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; }) .style('opacity', 1); node // .transition() // .duration(DURATION) .attr('transform', function(d) { return 'translate(' + d.y + ',' + d.x + ')'; }) .style('opacity', 1) .select('rect'); // 將退出節(jié)點(diǎn)轉(zhuǎn)換到父節(jié)點(diǎn)的新位置. let nodeExit = node.exit() // .transition() // .duration(DURATION) .attr('transform', () => 'translate(' + source.y + ',' + (parseInt(source.x)) + ')') .style('opacity', 0) .remove(); // nodeExit.select('rect') // .attr('width', this.diamonds.w) // .attr('height', this.diamonds.h) // .attr('stroke', 'black') // .attr('stroke-width', 1); // 修改線條 let link = this.svg.selectAll('path.link') .data(this.root.links(), d => d.target.id); // 在父級(jí)前的位置畫線。 let linkEnter = link.enter().insert('path', 'g') .attr('class', d =>{ return 'link link_' + d.target.depth } ) // .attr('marker-end', 'url(#resolvedIn)')// 根據(jù)箭頭標(biāo)記的id號(hào)標(biāo)記箭頭 .attr('stroke', '#DE4A3C') .style('fill-opacity', 1) .attr('fill', 'none') .attr('stroke-width', '1px') .attr('d', () => { let o = {x: source.x0, y: source.y0}; return _this.diagonal({source: o, target: o}) }) // .transition() // .duration(DURATION) .attr('d', _this.diagonal); // 過(guò)渡更新位置. link // .transition() // .duration(DURATION) .attr('d', _this.diagonal); // 將退出節(jié)點(diǎn)轉(zhuǎn)換到父節(jié)點(diǎn)的新位置 link.exit() // .transition() // .duration(DURATION) .attr('d', () => { let o = { x: source.x, y: source.y }; return _this.diagonal({source: o, target: o}) }).remove(); // 隱藏舊位置方面過(guò)渡. this.root.each(d => { d.x0 = d.x; d.y0 = d.y }); }, // 獲取點(diǎn)擊上游的上游 async fetchUpper (id, regCapi){ Toast.loading({ message: '加載中', forbidClick: true, duration: 0, }); const dataSource = []; try{ const response = await fetchEquityUpperInfo({ token: this.token, instId: id, currentPage: 0, pageSize: 200, regCapi: regCapi, }) const {code =0, records = []} = response if (code > 0 && records != null) { console.log(records) const dataSource = [] as any[]; records.forEach(element =>{ dataSource.push({ isHaveChildren: null, money: element.amount ? formatMoney((element.amount / 10000).toFixed(2)) :'--', scale: element.hold_rati || '--%', name: element.chn_full_nm || '--', id: element.inst_cust_id || '--', type: '0' }) }) Toast.clear() return dataSource }else{ Toast.clear() return dataSource } }catch(error){ Toast.clear() return dataSource } }, async click (source) { // if (d.children) { // d._children = d.children; // d.children = null; // } else { // d.children = d._children; // d._children = null; // } // if (this.lastClickD){ // this.lastClickD._isSelected = false; // } // d._isSelected = true; // this.lastClickD = d; if(source.children){ // 點(diǎn)擊減號(hào) source._children = source.children; source.children = null; }else { // 點(diǎn)擊加號(hào) if(!source._children){ let res = [] as any[] res = await this.fetchUpper(source.data.id, source.data.regCapi) if(!res.length){ Notify({ message: '上游或下游企業(yè)信息為空!', type: 'warning', duration: 1500 }) return } res.forEach(item =>{ let newNode = this.d3.hierarchy(item) newNode.depth = source.depth + 1; newNode.height = source.height - 1; newNode.parent = source; if(!source.children){ source.children = []; source.data.children = []; } source.children.push(newNode); source.data.children.push(newNode.data); }) }else{ source.children = source._children; source._children = null; } } this.update(source); }, // 拷貝到_children 隱藏1排以后的樹 // collapse (source) { // if (source.children) { // source._children = source.children; // source._children.forEach(this.collapse); // source.children = null; // this.hasChildNodeArr.push(source); // } // }, diagonal (d) { return `M ${d.source.y} ${d.source.x} H ${(d.source.y + (d.target.y-d.source.y)/2)} V ${d.target.x} H ${d.target.y}`; }, } }) </script>
總結(jié):
前端小白一枚,在之前只使用過(guò)echarts進(jìn)行可視化,在開(kāi)發(fā)這個(gè)功能時(shí)候發(fā)現(xiàn)d3版本中文網(wǎng)站內(nèi)容較少,基本出現(xiàn)問(wèn)題討論也是在外文網(wǎng)站,踩過(guò)一堆版本的坑,最終選擇穩(wěn)定且例子比較多的v4版本。還有基本都是默認(rèn)信息展示,很少有點(diǎn)擊請(qǐng)求的功能,進(jìn)行一個(gè)最終功能的整合。
以上就是vue2 d3實(shí)現(xiàn)企查查股權(quán)穿透圖股權(quán)結(jié)構(gòu)圖效果詳解的詳細(xì)內(nèi)容,更多關(guān)于vue2 d3股權(quán)穿透圖結(jié)構(gòu)圖的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vue router/Element重復(fù)點(diǎn)擊導(dǎo)航路由報(bào)錯(cuò)問(wèn)題及解決
這篇文章主要介紹了Vue router/Element重復(fù)點(diǎn)擊導(dǎo)航路由報(bào)錯(cuò)問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07Vue Element前端應(yīng)用開(kāi)發(fā)之獲取后端數(shù)據(jù)
這篇文章主要介紹了Vue Element前端應(yīng)用開(kāi)發(fā)之獲取后端數(shù)據(jù),對(duì)vue感興趣的同學(xué),可以參考下2021-05-05axios上傳文件錯(cuò)誤:Current?request?is?not?a?multipart?request
最近工作中使用vue上傳文件的時(shí)候遇到了個(gè)問(wèn)題,下面這篇文章主要給大家介紹了關(guān)于axios上傳文件錯(cuò)誤:Current?request?is?not?a?multipart?request解決的相關(guān)資料,需要的朋友可以參考下2023-01-01vue項(xiàng)目中如何實(shí)現(xiàn)element-ui組件按需引入
這篇文章主要介紹了vue項(xiàng)目中如何實(shí)現(xiàn)element-ui組件按需引入,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-05-05el-table表頭根據(jù)內(nèi)容自適應(yīng)完美解決表頭錯(cuò)位和固定列錯(cuò)位
這篇文章主要介紹了el-table表頭根據(jù)內(nèi)容自適應(yīng)完美解決表頭錯(cuò)位和固定列錯(cuò)位,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01vue報(bào)錯(cuò)Not?allowed?to?load?local?resource的解決辦法
這篇文章主要給大家介紹了關(guān)于vue報(bào)錯(cuò)Not?allowed?to?load?local?resource的解決辦法,這個(gè)錯(cuò)誤是因?yàn)閂ue不能加載本地資源的原因,需要的朋友可以參考下2023-07-07Vuex數(shù)據(jù)持久化實(shí)現(xiàn)的思路與代碼
Vuex數(shù)據(jù)持久化可以很好的解決全局狀態(tài)管理,當(dāng)刷新后數(shù)據(jù)會(huì)消失,這是我們不愿意看到的。這篇文章主要給大家介紹了關(guān)于Vuex數(shù)據(jù)持久化實(shí)現(xiàn)的思路與代碼,需要的朋友可以參考下2021-05-05