前言

vue3 框架中使用vue2代码结合d3完成股权穿透图和股权结构图(h5)

(没错听上去很违规,但我懒得把代码从vue2改成vue3了,所以是在vue3框架里用vue2写法完成的)

最终效果:

版本信息:

"d3": "4.13.0",

"vant": "^3.1.5",

"vue": "^3.0.0",

股权穿透图基础功能:

1、默认上下游信息展示,如果没有上下游信息只展示自己

2、点击请求子节点信息展示,收起子节点

3、全屏功能

4、放大器放大缩小(react项目中不知道为啥使用d3.zoom方法不好使,可能跟网页中滚动事件冲突有关,最后选择单独防止放大器进行放大缩小功能)

5、移动功能

股权结构图基础功能:

1、tab切换展示上游或下游信息

2、默认展示一层

3、点击请求子节点信息展示,收起子节点

代码链接:

https://github.com/QiuDaShua/vue2-d3.git

股权穿透图代码:

<template><div class="father-box"><divid="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><!-- <divclass="full"@click.stop="showFullScreen"><div class="full-icon"></div><span>{{isFull ? '退出全屏' :'全屏'}}</span></div> --><divid="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'// 过渡时间const DURATION = 0// 加减符号半径const SYMBOLA_S_R = 9// 公司const COMPANY = '0'// 人const PERSON = '1'//x,y距离// let x0 = 0, y0 = 0, dx = 0, dy = 0export default defineComponent({props: {},components: {CustomNavBar},data () {return {layoutTree: {} as any,diamonds: {} as any,d3: $d3,// hasChildNodeArr: [],originDiamonds: {} as any,diagonalUp: '',diagonalDown: '',tree: {'name': '中信期货', 'id': '1','children': [{'children': [], 'money': 3000,'scale': 30,'name': '中信期货黔西南分公司', 'id': '1-1','type': '0'}, {'children': [], 'money': 3000,'scale': 30,'name': '中信期货六盘水分公司', 'id': '1-2','type': '0'}, {'children': [], 'money': 3000,'scale': 30,'name': '中信期货贵阳分公司', 'id': '1-3','type': '0'}, {'children': [], 'money': 3000,'scale': 30,'name': '中信期货安顺分公司', 'id': '1-4','type': '0'}, {'children': [], 'money': 3000,'scale': 30,'name': '中信期货毕节分公司', 'id': '1-5','type': '0'}, {'children': [],'money': 3000,'scale': 30,'name': '中信期货遵义分公司', 'id': '1-6','type': '0'}, {'children': [], 'money': 3000,'scale': 30,'name': '中信期货黔东南分公司', 'id': '1-7','type': '0'}, {'children': [{'controlPerson': false, 'children': [], 'old': false, 'name': '中信期货黔南分公司下属公司1', 'id': '1-8-1', 'money': 200, 'scale': 20, 'type': '0'},{'controlPerson': false, 'children': [], 'old': false, 'name': '中信期货黔南分公司下属公司2', 'id': '1-8-2',  'money': 200, 'scale': 20, 'type': '0'},], 'money': 3000,'scale': 30,'name': '中信期货铜仁分公司', 'id': '1-8', 'type': '0'}, {'children': [{'controlPerson': false, 'children': [], 'old': false, 'name': '中信期货黔南分公司下属公司1', 'id': '1-9-1',  'money': 200, 'scale': 20, 'type': '0'},{'controlPerson': false, 'children': [], 'old': false, 'name': '中信期货黔南分公司下属公司2', 'id': '1-9-2',  'money': 200, 'scale': 20, 'type': '0'},], 'name': '中信期货黔南分公司','id': '1-9', 'money': 3000, 'scale': 30,'type': '0'}], 'parents': [{'controlPerson': true, 'money': '3000', 'children': [{'controlPerson': true, 'money': '3000', 'children': [], 'parentMoney': 3000, 'old': true, 'id': '1-01-1', 'name': '发展公司父级公司1', 'scale': 30, 'type': '0', 'oldUrlName': ''},{'controlPerson': true, 'money': '3000', 'children': [], 'parentMoney': 3000, 'old': true, 'id': '2-01-1', 'name': '发展公司父级公司2', 'scale': 70, 'type': '0', 'oldUrlName': ''},], 'name': '发展公司', 'id': '01-1', 'scale': 90, 'type': '0', 'oldUrlName': ''}]},rootUp: {} as any,rootDown: {} as any,svg: {} as any,svgW: document.documentElement.clientWidth,svgH: document.documentElement.clientHeight - 44,title: '股权穿透图',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.idconst token = data.token ? data.token : store.state.global.tokenconst userid = data.useridthis.id = idthis.token = tokenthis.userid = useridstore.commit(`global/${GlobalMutation.SET_TOKEN}`, token)Toast.loading({message: '加载中',forbidClick: true,duration: 0,});this.getInit()},beforeUnmount() {this.d3.select('#treesvg').remove()console.log('页面关闭')},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 = [] } = responseif (code > 0 && records != null) {this.regCapi = records[0].reg_capithis.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 = [] } = responseif (code > 0 && records != null) {const dataSource = [] as any[];records.forEach(element =>{// let children = []// // 设置children节点// 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 = []} = responseif (code > 0 && records != null) {const dataSource = [] as any[];records.forEach(element =>{// let children = []// // 设置children节点// 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}})},// 获取树状数据getTreeData(){console.log( this.children, this.parents, '111111111')let obj = {id: this.id,name: this.name,tap: '节点',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.d3let svgW = this.svgWlet svgH = this.svgH// x0 = svgW / 2,// y0=  svgH / 2// 方块形状this.diamonds = {w: 162,h: 66,intervalW: 182,intervalH: 150}// 源头对象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', () => {// 设置缩放位置以及平移初始位// if(isiOS && this.isFull){//   // 修改ios手机上才有的移动bug,安卓手机,pc端没有//   let x = d3.event.transform.x//   d3.event.transform.x = d3.event.transform.y//   d3.event.transform.y = -x//   console.log('222', '出现移动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 anylet downTree = {} as any// 拷贝树的数据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 返回新的结构 x0,y0初始化起点坐标this.rootUp = d3.hierarchy(upTree, d => d.children);this.rootUp.x0 = 0this.rootUp.y0 = 0this.rootDown = d3.hierarchy(downTree, d => d.children);this.rootDown.x0 = 0this.rootDown.y0 = 0;// 上 和 下 结构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); // 拥有下部分则隐藏初始块  d => showtype     === 'up' && !d.depth ? (this.rootDown.data.children.   length ? 0 : 1) : 1// 创建矩形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 函数描述], [click 函数描述]*  @param  {[Object]} source 第一次是初始源对象,后面是点击的对象*  @param  {[String]} showtype up表示向上 down表示向下*  @param  {[Object]} sourceTree 初始源对象*/update (source, showtype, sourceTree) {// eslint-disable-next-linelet _this = thisif (source.parents === null) {source.isOpen = !source.isOpen}let nodesif (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); // 拥有下部分则隐藏初始块// 创建矩形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'}});// 创建圆 加减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){// 跳转操作之类的}});// 名称过长 第二段nodeEnter.append('text').attr('x', 0).attr('y', d => {// ? (d.depth ? -this.diamonds.h / 2 : 0) : 0if (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个开始截取有表示超出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');// 认缴金额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 ? `认缴金额:${d.data.money.substr(0, 12)}…` : `认缴金额:${d.data.money}万元` : '').style('font-size', '12px').style('font-family', 'PingFangSC-Regular').style('font-weight', '400').style('color', '#666666');/** 绘制箭头* @param  {string} markerUnits [设置为strokeWidth箭头会随着线的粗细发生变化]* @param {string} viewBox 坐标系的区域* @param {number} markerWidth,markerHeight 标识的大小* @param {string} orient 绘制方向,可设定为:auto(自动确认方向)和 角度值* @param {number} stroke-width 箭头宽度* @param {string} d 箭头的路径* @param {string} fill 箭头颜色* @param {string} id resolved0表示公司 resolved1表示个人* 直接用一个marker达不到两种颜色都展示的效果*/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');// 将节点转换到它们的新位置。let nodeUpdate = node// .transition()// .duration(DURATION).attr('transform', d => showtype === 'up' ? 'translate(' + d.x + ',' + -(d.y) + ')' : 'translate(' + d.x + ',' + (d.y) + ')');// 将退出节点转换到父节点的新位置.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);// 在父级前的位置画线。let linkEnter = link.enter().insert('path', 'g').attr('class', 'link' + showtype).attr('marker-start', d => `url(#${showtype}resolved${d.data.type})`)// 根据箭头标记的id号标记箭头.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);// 过渡更新位置.linkUpdate// .transition()// .duration(DURATION).attr('d', d => _this.diagonal(d, d.parent, showtype));// 将退出节点转换到父节点的新位置link.exit()// .transition()// .duration(DURATION).attr('d', () => {let o = {x: source.x,y: source.y};return _this.diagonal(o, o, showtype)}).remove();// 隐藏旧位置方面过渡.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);//   }// },// 获取点击上游的上游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 = []} = responseif (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 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 = []} = responseif (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) {// 不是起点才能点// if (source.depth) {//   if (source.children) {//     source._children = source.children;//     source.children = null;//   } else {//     source.children = source._children;//     source._children = null;//   }// }if(source.children){// 点击减号source._children = source.children;source.children = null;}else {// 点击加号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: '上游或下游企业信息为空!',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()},// 点击全屏showFullScreen(){let width = document.documentElement.clientWidth,height = document.documentElement.clientHeight,wrapper = document.getElementById('rightPenetrationpage') as HTMLElement,style = '';const navbar = document.querySelector('.navbar') as HTMLElementconst 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('竖过来')this.isFull = falsethis.svgH = height - 44this.svgW = width// 设置按钮和顶部样式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('横过来')this.isFull = truethis.svgH = width - 44this.svgW = height// 设置按钮和顶部样式fullScreen.classList.add('fullRight')navbar.classList.add('smallBar')style += 'width:' + height + 'px;';// 注意旋转后的宽高切换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);';// 注意旋转中点的处理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()},// 点击返回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>

股权结构图代码:

<template><divid="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'// 过渡时间const DURATION = 400// 加减符号半径const SYMBOLA_S_R = 9// // 公司// const COMPANY = '0'// // 人// const PERSON = '1'export default defineComponent({props: {tree: {type: Object,default: () => {return     {
'name': '马云','tap': '节点','id': '1','children': [{'name': '中国平安人寿保险股份有限公司自有资金马云的公司厉害得很','scale': '2.27','id': '1-1','money': '3000','children': [{'name': '中国证券金融股份有限公司','scale': '2.27','id': '1-1-1','money': '3000','children': [{'name': '中国证券金融股份有限公司','scale': '2.27','id': '1-1-1-1','money': '3000',}]},{'name': '中央汇金资产管理有限责任公司','scale': '2.27','id': '1-1-2','money': '3000',}]}]}}},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: '股权结构图',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.d3let svgW = this.svgWlet svgH = this.svgHlet margin = {top: 20, right: 20, bottom: 30, left: 10}// 方块形状this.diamonds = {w: 320,h: 60,}// 源头对象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.transformthis.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})`)       // 拷贝树的数据let downTree = {} as anyObject.keys(this.tree).map(item => {if (item === 'children') {downTree = JSON.parse(JSON.stringify(this.tree))downTree.children = this.tree[item]}})// hierarchy 返回新的结构 x0,y0初始化起点坐标this.root = d3.hierarchy(downTree);this.root.x0 = 0this.root.y0 = 0if(!this.root.children){this.update(this.root)}else {// this.root.children.forEach(this.collapse)this.update(this.root)}},/**[update 函数描述], [click 函数描述]*  @param  {[Object]} source 第一次是初始源对象,后面是点击的对象*/update (source) {// eslint-disable-next-linelet _this = thislet 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; // 设置下一层水平位置向后移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);// 创建矩形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) {// 名字长度超过进行截取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){// 跳转操作之类的}});// 持股比例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)} });// 认缴金额nodeEnter.append('text').attr('dy', 17).attr('dx', 150).style('font-size', '12px').style('fill', '#666666').text(function(d) {if(!d.data.tap){return ('认缴金额 ' +':')} });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 + '万元')}} });// 箭头// 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 '>'//         }//     });// 创造圆 加减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 [设置为strokeWidth箭头会随着线的粗细发生变化]* @param {string} viewBox 坐标系的区域* @param {number} markerWidth,markerHeight 标识的大小* @param {string} orient 绘制方向,可设定为:auto(自动确认方向)和 角度值* @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');// 将节点转换到它们的新位置。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');// 将退出节点转换到父节点的新位置.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);// 在父级前的位置画线。let linkEnter = link.enter().insert('path', 'g').attr('class', d =>{return 'link link_' + d.target.depth} )// .attr('marker-end', 'url(#resolvedIn)')// 根据箭头标记的id号标记箭头.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);// 过渡更新位置.link// .transition()// .duration(DURATION).attr('d', _this.diagonal);// 将退出节点转换到父节点的新位置link.exit()// .transition()// .duration(DURATION).attr('d', () => {let o = {x: source.x,y: source.y};return _this.diagonal({source: o, target: o})}).remove();// 隐藏旧位置方面过渡.this.root.each(d => {d.x0 = d.x;d.y0 = d.y});},// 获取点击上游的上游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 = []} = responseif (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){// 点击减号source._children = source.children;source.children = null;}else {// 点击加号if(!source._children){let res = [] as any[]res = await this.fetchUpper(source.data.id, source.data.regCapi)if(!res.length){Notify({message: '上游或下游企业信息为空!',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>

总结:

前端小白一枚,在之前只使用过echarts进行可视化,在开发这个功能时候发现d3版本中文网站内容较少,基本出现问题讨论也是在外文网站,踩过一堆版本的坑,最终选择稳定且例子比较多的v4版本。还有基本都是默认信息展示,很少有点击请求的功能,进行一个最终功能的整合,如果有问题欢迎大家积极提出讨论,共同进步~

【vue d3 v4】vue2结合d3实现类似企查查的股权穿透图、股权结构图相关推荐

  1. D3 企业关系图谱 企业构成图谱 股权穿透图 股权结构图 关联方认定图

    前言:之前说做了项目,这个项目黄了,公司跑路了,代码就拿出来分享,主要就是实现各种类似企查查的各种图谱,欢迎交流.目前已大致实现了: D3 企业关系图谱 企业构成图谱 股权穿透图 股权结构图 关联方认 ...

  2. 使用d3实现类似脑图,股权穿透图,关系图谱

    前言 最近工作需要完成一个股权穿透图,找了好多文档发现都不满足需求,最终选择d3.js来实现,包含子集的收缩展开,交互以及其他功能.之前由于没做过类似关系图以及不了解d3,踩了很多坑,我会尽可能将代码 ...

  3. d3.js实现股权穿透图(vue+d3.js)

    d3.js实现股权穿透图 业务需求 1.实现的效果图 2.安装依赖 3.全部代码(复制粘贴即可实现) 参考git地址 业务需求 前段时间写过一篇使用relation-graph插件实现的股权穿透图效果 ...

  4. d3 企业图谱 仿天眼查 企查查

    最近接到一个需求,终端要加入企业图谱的功能.能无线穿透下去,之前写过一个类似树形图但是节点长度没有自适应(如下图),样式也不够好看,产品提出做一个类似企查查那种的企业图谱,能更直观的展示企业信息,无奈 ...

  5. 仿企查查、天眼查 d3关联关系图 力项导图

    研究完股权穿透图跟企业图谱 最近又写了一个企业.人物的力项导图 力导向图可以直观看出各个元素之间的关系 具体的代码就不贴了,官网很多 demo 都可以直接拿来用 先看效果 d3灵活的地方就在于它可以 ...

  6. D3 天眼查 股权穿透 股权结构

    效果图如上 大致效果就是仿照天眼查股权穿透图 曲线会出现节点位置不对(曲线的算法是在不会 技术菜解决不了)   最后刀放到产品脖子他同意用折线代替 在下面已经补充 JSON数据  把请求换成请求本地j ...

  7. d3 v4版本画基本图

    感谢 http://www.ourd3js.com/wordpress/ 提供的技术参考,其中地图的文件china.geojson 下载地址http://www.ourd3js.com/wordpre ...

  8. d3 v4 api interpolate

    方法 说明 d3.interpolate interpolate arbitrary values. d3.interpolateArray interpolate arrays of arbitra ...

  9. 仿企查查、天眼查股权穿透d3

    企业图谱做出来了,接下来仿企查查写个股权穿透的图谱 企查查股权穿透 自己的 首先使用的方法以及生成图的方法 跟企业图谱类似 也是用的d3官方demo给出的生成双向树的方法,不过版本是d3.v3 相比企 ...

最新文章

  1. python生成epub文件_python在内存中生成Zip文件!
  2. C语言有三个电阻r1r2r3,[VR虚拟现实]ARM硬件试题库及答案(37页)-原创力文档
  3. Eclipse相关问题总结
  4. 【渝粤教育】国家开放大学2018年春季 0554-21T立体构成(一) 参考试题
  5. 图像的灰度级数越多越好_MATLAB-数字图像处理 图像直方图归一化
  6. 源码时代php中级项目,0526PHP班中级项目评比圆满落幕
  7. jbutton添加点击事件_electron-vue自定义边框后点击事件失效问题
  8. socket中使用心跳来检测连接是否断开[ZT]
  9. linux下文件的相关信息
  10. 基础面试题——HTML/CSS
  11. 基于log4net的支持动态文件名、按日期和大小自动分割文件的日志组件
  12. 计算机操作系统(第四版)课后习题答案西电版V2.0校对版
  13. linux中安装rpm命令,linux下,如何安装rpm命令?
  14. 索尼1a dac插电脑用什么驱动。在哪下载,求助
  15. 阿里CEO张勇:打破各企业边界 联手对抗黑灰产
  16. 我的python程序_我试着运行我的python程序,但当我运行它时什么也没有发生
  17. 带你领略Clean架构的魅力,腾讯T3大佬亲自讲解
  18. 笔记本电脑识别不到WiFi、蓝牙消失
  19. 【C++】面向对象之继承篇
  20. 简短加密_神经网络训练中回调的简短实用指南

热门文章

  1. docker安装后启动失败
  2. Tomcat启动报错:因为在清除过期缓存条目后可用空间仍不足 - 请考虑增加缓存的最大空间
  3. Android 面试必备之 JVM 相关口水话,flutterpageview动画
  4. Linux安装mysql并配置外网访问
  5. js的event loop/js内存泄漏
  6. JAVA 实现字符串(String)的模糊查找
  7. 公司人才中介平台的设计与实现
  8. 737到底是什么问题难到了波音公司?
  9. MySQL 数据类型(转)
  10. lyse - Errors and Processes