大家好,欢迎来到IT知识分享网。
不会抓妖的小道士的故事
有个从来抓不到鬼的小道士,他终于还俗了,在他婚礼的那一天,他的师兄弟没有一个到场,
可是全城的鬼怪都聚集在了婚礼上空,想为这个总是心肠很软、碎碎念、很唠叨,
最后娶了个小狐狸的这个家伙当一道劫雷,可是那天,晴空万里,什么都没有发生;
言归正传 首先看效果
该效果使用的是D3js+vue2进行
使用时 注意事项
-
D3js版本不宜过高 否则会报错 出现无法移动现象 使用”^5.16.0″版本
-
由于我使用了load-scss 如果自己安装无法启动可以使用我的版本
-
部分依赖 都放在 devDependencies模块下
“devDependencies”: { “d3”: “^5.16.0”, “sass-loader”: “^7.1.0”, “node-sass”: “^4.12.0”, }
具体步骤就不在这进行一一讲述了,太多了,你们看来也烦,就直接上代码,运行即可,代码文件如下 代码中需要注意的事项都在TODO里面写着,请认直阅读该注释,如有疑问可留言,感谢各位大佬的阅读
<template>
<div class="task-container">
<div :id="id" class="tree-container">
<svg class="d3-tree ">
<!-- 任务树 -->
<g class="container"></g>
<!-- 关联任务树 -->
<g class="relationContainer"></g>
</svg>
</div>
</div>
</template>
<script>
import * as d3 from 'd3'
export default {
name: 'Home',
data() {
return {
isTaskBegin: true,//任务是否开始 是计划还是还是开始执行任务
id:'',//卡片唯一ID
taskDetail: {},//任务数据
width: null,
height: null,
zoom: null,//鼠标点击移动卡片速度
treeData: {},
popper_arrow: 'right',//鼠标练级移动的方向
duration: 750,
root: null,
rootRelation: null,//卡片之间的关系
popoverData: {},//移除卡片时接受的数据
stopDialog: false,//
clientX: 0,
clientY: 0,
x: 0,
y: 0,
scale: 1,
refresh: 0,//页面刷新卡片刷新
timer: null,
transformXY: null,
}
},
computed: {
treemap() {
return d3.tree().size([this.height, this.width]).nodeSize([400, 300]);
}
},
created() {
this.id = this.uuid()
},
mounted() {
this.initSvg();
},
methods: {
/**
* 初始化画布与获取任务数据
*/
initSvg() {
//获取任务数据
this.findTaskTrack()
//创建svg画布
this.width = document.getElementById(this.id).clientWidth
this.height = document.getElementById(this.id).clientHeight
const svg = d3.select(this.$el).select('svg.d3-tree');
const container = d3.select(this.$el).select('g.container');
const relationContainer = d3.select(this.$el).select('g.relationContainer');
const transform = d3.zoomIdentity.translate(this.width / 2, 220).scale(0.8);
// init zoom behavior, which is both an object and function
this.zoom = d3.zoom().scaleExtent([1 / 12, 2]).on('zoom', (e) => {
this.transformXY = d3.event.transform;
clearTimeout(this.timer);
this.timer = setTimeout(() => {
container.attr('transform', this.transformXY)
relationContainer.attr('transform', this.transformXY)
}, this.refresh)
})
svg.transition().duration(0).call(this.zoom.transform, transform)
svg.call(this.zoom)
},
/**
* 每个任务不同的ID
*/
uuid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1)
}
return (
s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4()
)
},
/**
* @description 获取构造根节点
*/
getRoot(subTaskList) {
let root = d3.hierarchy(this.treeData, d => {
return d[subTaskList]
})
root.x0 = this.width / 2
root.y0 = 220
return root
},
/**
* 点击大卡片节点事件 这里点击卡片的圆圈处会触发另一个事件 clickCircle
* @param d 节点数据 包含任务数据
* @param d{children该任务下的子任务数据,data:该任务的数据,parent:父节点的数据 }
*/
clickCard(d) {
// console.log(d);
},
/**
* 点击圆圈进行操作
* @param d 节点数据
* @param d{children该任务下的子任务数据,data:该任务的数据,parent:父节点的数据 }
*/
clickCircle(d) {
console.log(d);
},
/**
* 点击分解按钮
* @param d 节点数据
* @param d{children该任务下的子任务数据,data:该任务的数据,parent:父节点的数据 }
*/
decompose(d) {
var e = d3.event;
if (e.stopPropagation) {
e.stopPropagation();
} else {
window.event.returnValue == false;
}
// 这里做一些对分解任务进行处理,进行弹窗跳转页面什么的
},
/**
* 鼠标移上卡片时触发的事件
*/
addVisible(d) {
if (this.stopDialog) return this.stopDialog = false;
this.popoverData = d.data;
if (this.x + (d.x + 150) * this.scale > this.width - 320) {
this.popper_arrow = 'left';
this.clientX = this.x + (d.x - 150) * this.scale - 336;
} else {
this.popper_arrow = 'right';
this.clientX = 16 + this.x + (d.x + 150) * this.scale;
}
this.clientY = this.y + (d.y - 190) * this.scale;
},
/**
* 鼠标移开卡片的事件
*/
delVisible(d) {
},
/**
*
*/
diagonal(s, d, g_className) {
return g_className === 'container' ? `M ${s.x} ${s.y}C ${s.x} ${d.y},${s.x} ${d.y},${d.x} ${d.y}` : `M ${s.x} ${-s.y - 210}C ${s.x} ${-d.y - 210},${s.x} ${-d.y - 210},${d.x} ${-d.y - 210}`
},
/**
* @description 数据与Dom进行绑定
*/
update(source, g_className) {
let dTreeData = null;
dTreeData=g_className === 'container'?this.treemap(this.root):this.treemap(this.rootRelation);
let nodes = dTreeData.descendants()
let links = dTreeData.descendants().slice(1)
nodes.forEach(d => {
d.y = d.depth * 280
})
const svg = d3.select(this.$el).select('svg.d3-tree')
const container = svg.select(`g.${g_className}`)
let node = container.selectAll('g.node').data(nodes, d => {
return d.id
})
let nodeEnter = node.enter().append('g').attr('class', 'node').attr('transform', d => {
return 'translate(' + source.x0 + ',' + source.y0 + ')'
})
// 节点画图
this.canvasNode(nodeEnter, g_className)
// Transition nodes to their new position.
let nodeUpdate = nodeEnter.merge(node).transition().duration(this.duration).attr("transform", function (d) {
return g_className === 'container'?"translate(" + d.x + "," + d.y + ")":"translate(" + d.x + "," + -d.y + ")";
});
// 节点更新
nodeUpdate.select("circle").style("fill", function (d) {
if (d.children) return "url(#image_jian)";
if (d._children) return "url(#image_jia)";
return "#fff";
});
nodeUpdate.select("text")
.style("fill-opacity", 1)
let nodeExit = node.exit().transition().duration(this.duration).attr("transform", function (d) {
return g_className === 'container'?"translate(" + source.x + "," + source.y + ")":"translate(" + source.x + "," + -source.y + ")";
}).remove();
let link = container.selectAll('path.link').data(links, d => {
return d.id
})
//TODO taskId 为数据Id 如果不同可自行修改
let linkEnter = link.enter().insert("path", "g").attr("class", "link").attr("id", (d) => {
return d.data.taskId
}).attr("d", d => {
let o = {x: source.x0, y: source.y0};
return this.diagonal(o, o, g_className)
}).attr("fill", 'none').attr("stroke-width", 1).attr('stroke', '#ccc');
let textpath = container.selectAll('text.textpath').data(links, d => {
return d.id
})
let textLink = textpath.enter().insert("text", "path").attr("class", "textpath").attr("x", (d) => {
if (Math.abs(d.parent.x - d.x) > 220) return 320
return 220 //Math.abs(d.x/2)
}).attr("y", (d) => {
return g_className === 'container'?d.y:-d.y
}).attr("text-anchor", 'top');
//TODO taskId 为数据Id 如果不同可自行修改
textLink.append('textPath').attr("fill", "#2986cf").attr("xlink:href", (d) => {
return '#' + d.data.taskId
}).attr('start-offset', "50%").style('font-size', 12).text(function (d) {
//TODO 这里显示卡片与卡片之间的关系
return g_className === 'container'?'分解':'关联'
})
let linkUpdate = linkEnter.merge(link)
linkUpdate.transition().duration(this.duration).attr('d', d => {
return this.diagonal(d, d.parent, g_className)
})
link.exit().transition().duration(this.duration).attr("d", d => {
let o = {x: source.x, y: source.y};
return this.diagonal(o, o, g_className)
}).remove();
let textUpdate = textLink.merge(textpath)
textUpdate.transition().duration(this.duration).attr('d', d => {
return this.diagonal(d, d.parent, g_className)
})
// Transition exiting nodes to the parent's new position.
textpath.exit().transition().duration(this.duration).attr("d", d => {
let o = {x: source.x, y: source.y};
return this.diagonal(o, o, g_className)
}).remove();
// Stash the old positions for transition.
nodes.forEach(d => {
d.x0 = d.x
d.y0 = d.y
})
// 渲染结束后, 转圈圈消失 TODO 这里可以设置数据加载的过程
// this.loading = false;
},
// 画节点模块
canvasNode(nodeEnter, g_className) {
nodeEnter.append("circle").attr("r", 10).attr("stroke", "#fff").attr("stroke-width", 1)
.on('click', (d) => {
this.stopDialog = true;
if (!d._children && !d.children)
return
if (d.children) {
this.$set(d, '_children', d.children)
d.children = null
} else {
this.$set(d, 'children', d._children)
d._children = null
}
this.$nextTick(() => {
this.update(d, g_className)
}
)
}).style("fill", function (d) {
if (d.children) return "url(#image_jian)";
if (d._children) return "url(#image_jia)";
return "#fff";
}).attr("transform", function (d) {
return g_className === 'container'?"translate(" + 0 + "," + 10 + ")":"translate(" + 0 + "," + -220 + ")"
}).style("display", (d) => {
return d.children || d._children ? 'block' : 'none'
})
// TODO 创建一个 卡片
let rectText = nodeEnter.append("g").on('click', this.clickCard).on('mouseenter', this.addVisible).on('mouseleave', this.delVisible)
.style("fill-opacity", 1).style("cursor", (d) => {
return this.isTaskBegin?'default':'pointer'
}).attr("transform", function (d) {
return "translate(" + 0 + "," + 0 + ")";
});
// 节点背景图 TODO 卡片样式
rectText.append("rect")
.attr("x", -150)
.attr("y", -190)
.attr("rx", 6)
.attr("ry", 6)
.attr("width", 300)
.attr("height", 210)
.attr("fill", "white")
.attr("stroke", (d) => {
// TODO 这里进行区分卡片等级颜色
return d.data.taskLevel === 1?"#33a39c":"#999";
}).style("fill-opacity", 1);
//TODO 标题区域
let titleText = rectText.append("g").attr("transform", function (d) {
return "translate(" + -150 + "," + -210 + ")";
});
titleText.append("rect")
.attr("width", 300)
.attr("height", 40)
.attr("rx", 6)
.attr("ry", 6)
.attr("fill", (d) => {
if (d.data.taskLevel === 1) {
return '#27B148'
}
return g_className === 'container'?"#33a39c":"#2986cf"
}).attr("stroke", (d) => {
if (d.data.taskLevel === 1) {
return '#27B148'
}
return g_className === 'container'?"#33a39c":"#2986cf"
}).style("fill-opacity", 1);
//TODO 任务名字 对于位置 x y
let titleName = titleText.append("svg")
.attr("x", 0)
.attr("y", 0)
.attr("width", 300)
.attr("height", 30)
.style("fill-opacity", 1);
titleName.append("title")
.attr("dy", 20)
.text(function (d) {
return `${d.data.taskLevel === 1 ? '计划' : '任务'}名称:${d.data.taskName}`
}).style("fill-opacity", 1);
titleName.append("text").attr("x", (d) => {
return d.data.taskName.length >= 9 && g_className === 'container' ? 130 : 150
}).attr("y", 0).attr("dy", 26).attr("fill", "#fff").attr("text-anchor", 'middle').text(function (d) {
if (d.data.taskName && d.data.taskName.length >= 9) {
return `${d.data.taskLevel === 1 ? '计划' : '任务'}名称:${d.data.taskName.substring(0, 8)}...`
} else {
return `${d.data.taskLevel === 1 ? '计划' : '任务'}名称:${d.data.taskName}`
}
}).style("fill-opacity", 1);
//TODO 分解按钮 任务分解按钮显示的位置 x表示在卡片x左边的位置 y >0 表示向下移动的位置
titleText.append("text")
.on('click', this.decompose)
.attr("x", 250)
.attr("y", 0)
.attr("dy", 26)
.attr("fill", "#0000dd")
.attr("text-anchor", 'right')
.text('(分解)')
.style("display", (d) => {
// TODO 进行显示隐藏分解按钮 如果数据中有一个字段,可以通过 d.data.XX属性进行判断是否显示
return 1 ? 'block' : 'none'
}).style("text-decoration", "underline").style("fill-opacity", 1);
//TODO 审核人、创建人 这里值得注意 除了标题以外 下面添加的属性 x基本固定为-140 y轴为负值,从该卡片右下角为卡片坐标系原点 所以为负数
let superAuditorName = rectText.append("svg")
.attr("x", -140)
.attr("y", -150)
.attr("width", 200)
.attr("height", 30)
.style("fill-opacity", 1);
superAuditorName.append("text").attr("dy", 20).attr("text-anchor", 'left').text((d) => {
if (d.data.taskLevel === 1) {
if (d.data.executorUserName && d.data.executorUserName.length >= 10) {
return '创建人:' + d.data.executorUserName.substring(0, 8) + '...'
} else {
return '创建人:' + (d.data.executorUserName ? d.data.executorUserName : '无')
}
} else {
if (d.data.auditorName && d.data.auditorName.length >= 10) {
return '审核人:' + d.data.auditorName.substring(0, 8) + '...'
} else {
return '审核人:' + (d.data.auditorName ? d.data.auditorName : '无')
}
}
}).style("fill-opacity", 1);
// TODO 处理人 这里值得注意 如果有些字段你某些任务没有 你得对y坐标进行处理 虽然你可以使用 style("display","none") 进行隐藏 但最终这个位置就是一个空缺 不美观
let executorUserName = rectText.append("svg")
.attr("x", -140)
.attr("y", -120)
.attr("width", 200)
.attr("height", 30)
.style("fill-opacity", 1);
executorUserName.append("text")
.attr("dy", 20)
.attr("text-anchor", 'left')
.text(function (d) {
// TODO 如果没有这个地段 不应显示就给它text设置 或者你可以为空 后面也可以不需要进行对Y 坐标处理 看需求怎么搞
if (d.data.taskLevel !== 1) {
if (d.data.executorUserName && d.data.executorUserName.length >= 10) {
return '处理人:' + d.data.executorUserName.substring(0, 8) + '...'
} else {
return '处理人:' + (d.data.executorUserName ? d.data.executorUserName : '无')
}
}
}).style("fill-opacity", 1);
// TODO 所属组织、执行组织 我这里因为一级任务没有处理人 所以根节点卡片不应该出现该字段 我没使用display属性 而是对y 坐标进行了处理
rectText.append("text")
.attr("x", -140)
.attr("y", (d) => {return d.data.taskLevel === 1 ? -120 : -90})
.attr("dy", 20)
.attr("text-anchor", 'left')
.text((d) => {
return d.data.taskLevel === 1?' 所属组织:' + (d.data.executorOrgName||'无'):'执行组织 :' + (d.data.dutyOrgName||'无')})
.style("fill-opacity", 1);
// 任务开始时间
rectText.append("text").attr("x", -140).attr("y", (d) => {
return d.data.taskLevel === 1 ? -90 : -60
}).attr("dy", 20).attr("text-anchor", 'left').text((d) => {
return !d.data.taskStartTime?`最近任务开始日期:无`:`最近任务开始日期:${d.data.taskStartTime}`
}).style("fill-opacity", 1);
// 任务结束时间
rectText.append("text")
.attr("x", -140)
.attr("y", (d) => {return d.data.taskLevel === 1 ? -60 : -30})
.attr("dy", 20)
.attr("text-anchor", 'left')
.text((d) => {return !d.data.taskEndTime?`最近任务结束日期:无`:`最近任务结束日期:${d.data.taskEndTime}`})
.style("fill-opacity", 1);
// TODO 进度条 当任务开始时 没有分配任务按钮 变成一个任务的百分比进度
let circleText = rectText.append("g").on('click', this.clickCircle).style("cursor", (d) => {
if (this.isTaskBegin) {
return d.parent && d.data.executorUserId === d.parent.data.executorUserId ? 'no-drop' : 'pointer';
} else {
return 'default'
}
}).attr("transform", function (d) {
return "translate(" + 110 + "," + -130 + ")";
});
circleText.append("circle").attr("r", (d) => {
//圆圈半径大小
return this.isTaskBegin?26:20;
}).style("fill", (d) => {
if (this.isTaskBegin) {
return d.parent && d.data.executorUserId === d.parent.data.executorUserId ? '#edf2fc' : '#0FA3A5';
}
//圈的颜色
const colorArr={
1:"#f7c692",
2:"#FA8C15",
3:"#64D16D",
4:"#1592E6",
5:"#666565",
null:"#E62412"
}
return colorArr[d.data.taskStatus]||colorArr['null'];
});
// 进度值
circleText.append("text").attr("y", (d) => {
if (this.isTaskBegin) {
return -2
} else {
return 40
}
}).attr("text-anchor", 'middle').attr("fill", (d) => {
if (this.isTaskBegin) return '#fff';
// 对应 圈下面的百分比值颜色
const colorArr={
1:"#f7c692",
2:"#FA8C15",
3:"#64D16D",
4:"#1592E6",
5:"#666565",
null:"#E62412"
}
return colorArr[d.data.taskStatus]||colorArr['null'];
})
.text((d) => {
if (this.isTaskBegin) return '分配'
return d.data.feedbackProgress ? Number(d.data.feedbackProgress) * 1000 / 10 + '%' : '0%'
}
).style("fill-opacity", 1);
circleText.append("text")
.attr("text-anchor", 'middle')
.attr("fill", '#fff')
.attr("y", 13)
.text('任务')
.style("display", (d) => {
return this.isTaskBegin ? 'block' : 'none'
});
},
findTaskTrack() {
// 调用接口获取数据
const {code, result, message} = this.simulationData();
if (code === 200) {
this.treeData = result;
//如果有子任务进行递归处理
this.recursionDispose(result, "subTaskList", (node, level, index) => {
if (node.userIdList && node.userIdList.length > 100) {
this.refresh = parseInt(node.userIdList.length / 25);
}
});
this.root = this.getRoot('subTaskList')
this.update(this.root, 'container')
}
},
/**
* 递归处理任务
* @param data 任务数据
* @param childrenKey 对应子任务属性值
* @param dispose 回调函数处理
* @param level 级别
* @param index 循环索引
* @param parent 父级数据
*/
recursionDispose(data, childrenKey, dispose, level, index, parent = null) {
let l = typeof level === 'number' ? level : 1;
let i = typeof index === 'number' ? index : 0;
let children = data && data[childrenKey];
if (children instanceof Array) {
for (let j = 0, len = children.length; j < len; j++) {
this.recursionDispose(children[j], childrenKey, dispose, l + 1, i + 1, data);
}
}
dispose(data, l, i, parent);
},
/**
* 模拟数据
*/
simulationData(){
return {
"code": 200,
"message": "操作成功",
"result": {
"id": null,
"taskId": "62869132028440166471755569060000",
"taskName": "井底的蜗牛之爬出井底计划",
"parentId": "0",
"superId": "62869132028440166471755569060000",
"taskLevel": 1,
"taskStatus": null,
"taskStartTime": null,
"taskEndTime": null,
"userIdList": null,
"executorUserId": "6170876529206517766172707211783",
"executorUserName": "井底的蜗牛",
"executorOrgId": "61708228580701388861705474010582",
"executorOrgName": "井蜗集团-效率部门",
"superAuditorName": null,
"auditorName": null,
"feedbackProgress": 20,
"organizationId": null,
"dutyOrgName": null,
"subTaskList": [
{
"id": null,
"taskId": "62968505366453862401144414070000",
"taskName": "井底的蜗牛之爬出井底计划PlaneA",
"parentId": "62869132028440166471755569060000",
"superId": "62869132028440166471755569060000",
"taskLevel": 2,
"taskStatus": null,
"taskStartTime": null,
"taskEndTime": null,
"userIdList": null,
"executorUserId": "61815438753855078491605563732019",
"executorUserName": "井底的蜗牛2号",
"executorOrgId": "6170867344908369926172328501773",
"executorOrgName": "如何提升自身速度",
"superAuditorName": null,
"auditorName": "井底的蜗牛",
"feedbackProgress": 10,
"organizationId": "61708228580701388861705474010582",
"dutyOrgName": "井蜗集团-发展规划部",
"subTaskList": [],
},
{
"id": null,
"taskId": "62972444949060403201421141040000",
"taskName": "井底的蜗牛之爬出井底计划PlaneB",
"parentId": "62869132028440166471755569060000",
"superId": "62869132028440166471755569060000",
"taskLevel": 2,
"taskStatus": null,
"taskStartTime": null,
"taskEndTime": null,
"userIdList": null,
"executorUserId": "61815461448101888091606504802026",
"executorUserName": "井底的蜗牛3号",
"executorOrgId": "6170867344908369926172328501773",
"executorOrgName": "探索井外世界",
"superAuditorName": null,
"auditorName": "井底的蜗牛",
"feedbackProgress": 10,
"organizationId": "61708228580701388861705474010582",
"dutyOrgName": "井蜗集团-研究探索部",
"subTaskList": [],
}
],
}
}
}
}
}
</script>
<style scoped lang="scss">
.task-container {
width: 100%;
height: 100vh;
position: relative;
}
.tree-container {
width: 100%;
height: calc(100% - 46px);
position: relative;
overflow: hidden;
.d3-tree {
width: 100%;
height: 100%;
// .container {
// transform: translate(50%, 140px);
// }
}
.tip_content {
background: #fff;
border: 2px solid #33a39c;
position: absolute;
z-index: 5;
width: 319px;
// height: 195px;
border-radius: 6px;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.2);
padding: 10px 20px;
div {
padding: 6px 0;
font-size: 14px;
flex: 2;
}
.arrow_left {
position: absolute;
right: -12px;
top: 40px;
display: block;
padding: 0;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 12px;
border-left-color: #33a39c;
border-right-width: 0;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, .03));
&::after {
content: '';
position: absolute;
right: 2px;
top: -12px;
display: block;
padding: 0;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 12px;
border-left-color: #fff;
border-right-width: 0;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, .03));
}
}
.arrow_right {
position: absolute;
left: -12px;
top: 40px;
display: block;
padding: 0;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 12px;
border-right-color: #33a39c;
border-left-width: 0;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, .03));
&::after {
content: '';
position: absolute;
left: 2px;
top: -12px;
display: block;
padding: 0;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
border-width: 12px;
border-right-color: #fff;
border-left-width: 0;
filter: drop-shadow(0 2px 12px rgba(0, 0, 0, .03));
}
}
}
}
.d3-tree {
.node {
cursor: pointer;
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 1.5px;
}
.node text {
font: 18px sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 1.5px;
}
}
</style>
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/14560.html