【微信小程序】可拖拽操作的“树状图”模块的制作和小程序经验的总结
去年三月份高中同学拉我做的微信小程序,尽管可能除了高一的时候做网页写过些js以后就再也没碰过(甚至高一时因为觉得“以后肯定不会做程序员”而拒绝学习代码),但得到了“你就写写前端、做做页面和好看的图片就行了”的答复后便开开心心地开始写了。
——谁也不会想到在Deadline前两天,我一个卑微的前端,大晚上地在用putty跟高中同学调试数据库,并且一晚上遇到奇诡的问题就立刻删库……互相吐槽自己写的代码是shit,想想也是有趣的体验啊哈哈。
5、6月份就写完了(虽然依然有大量的bug),一直想写个博客记录一下开发过程中遇到的问题,以及难得完成了一个算是网上找不到类似案例的“树状图”模块。差不多一年后的今天,补完一些js基础后,回头看看当年写的代码,应该也会有有趣的体验吧。(大概)
在wxa-comp-canvas-drag的基础上进行修改,使用了该模块封装的画布的拖拽、移动函数,诸如缩放、旋转的功能没有用到。
成果预览
有点儿类似思维导图。点击结点可以编辑节点信息(这儿有个index-z的问题),删除节点后会标记该节点的属性,点击“执行删除”后才真正的删除。
主要思路
数据格式
首先复习一下Canvas的主要方法。
save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。
restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。
传入组件的后端数据格式形如:
"Tree": [{"Task": {"TaskID": "testopenidtaskid1","Title": "欢迎使用咕咕Teamwork","Pusher": "咕老板","Content": "利用树状图进行团队任务的管理和可视化","Status": false,"PushDate": "2019-04-01 00:00:00","DeadLine": "2100-01-01 00:00:00","Urgency": 3},"Self": 0,"Child": [1,2,3],"TeamMates": ["咕老板"]},{"Task": {"TaskID": "testopenidtaskid2","Title": "建立总任务树","Pusher": "咕组长","Content": "根据实际情况建立相应的任务树","Status": 1,"PushDate": "2019-04-01 00:00:00","DeadLine": "2100-01-01 00:00:00","Urgency": 3},"Self": 1,"Child": [4,5],"TeamMates": ["咕组长"]},{"Task": {"TaskID": "testopenidtaskid3","Title": "提交和审批","Pusher": "咕秘书","Content": "每个子任务的成员均需要分别提交并进行审批","Status": -1,"PushDate": "2019-04-01 00:00:00","DeadLine": "2100-01-01 00:00:00","Urgency": 3},"Self": 2,"Child": [0],"TeamMates": null},{"Task": {"TaskID": "testopenidtaskid4","Title": "数据分析","Pusher": "咕技术","Content": "通过图表直观地观察自己和团队的完成情况","Status": -1,"PushDate": "2019-04-01 00:00:00","DeadLine": "2100-01-01 00:00:00","Urgency": 3},"Self": 3,"Child": [0],"TeamMates": ["咕技术"]},{"Task": {"TaskID": "testopenidtaskid5","Title": "前期工作","Pusher": "咕组长","Content": "任务的前期工作","Status": 1,"PushDate": "2019-04-01 00:00:00","DeadLine": "2100-01-01 00:00:00","Urgency": 3},"Self": 4,"Child": [0],"TeamMates": ["咕员工A", "咕员工B", "咕员工C"]},{"Task": {"TaskID": "testopenidtaskid5","Title": "后期工作","Pusher": "咕老板","Content": "任务的后期工作","Status": -1,"PushDate": "2019-04-01 00:00:00","DeadLine": "2100-01-01 00:00:00","Urgency": 3},"Self": 5,"Child": [0],"TeamMates": ["咕员工D","咕员工E"]},],"TreeId": "testtasktree","TreeName": "testproject"},
这时候想起来,一开始后端同学传的数据直接是一棵树。后来改成数组了。
模块类
dragGraph类,就是画布上的一个个可以拖拽的框框。传入的比较关键的参数有xy坐标,画布环境,以及一个Task对象taskattr,用来描述这一个结点。
const dragGraph = function({x = 30,y = 30,w,h,type,text,fontSize = DEFAULT_FONT_SIZE,color = 'black',url = null,rotate = 0,sourceId = null,selected = true
}, canvas, factor, taskattrs = {}) {...
};
dragGraph类的方法,因为太长了就摘录一下方法声明,描述一下有哪些功能。
dragGraph.prototype = {// 绘制该节点,如果selected===true,则显示蓝色的描边,如果isDeled===true,则显示// 如果是主任务,则绘制小房子的icon// 根据传入的taskattrZ,绘制“任务完成”icon和人数icon等。paint() {...}// 在画布上绘制一个圆角矩形_roundRect(x, y, w, h, r) {....}// 在画布上根据该节点的x、y绘制矩形边框_drawBorder() {...}// 画一条线_draw_line(ctx, a, b) {...}// 判断触摸点是否在该结点区域内isInGraph(x, y) {...}// 判断触摸点是否在一个多边形区域内,参数为触摸点坐标和insidePolygon(points, testPoint) {...} transform(px, py, x, y, currentGraph) {}
}
edgeGraph为连接结点之间的线条类,传入的形参主要为线条宽度、颜色、两端的结点dragGraph对象。
const edgeGraph = function({width = 2,color = 'gray'
}, canvas, fromGraph, toGraph) {this.ctx = canvas;this.fromGraph = fromGraph;this.toGraph = toGraph;this.width = width;this.color = color;
};
edgeGraph类的方法就比较简单了。一开始设计线条的端点会根据两端结点的相对位置变化,比如变化到一个节点的左上角、右下角之类的。后来发现效果并不好,就直接连接两个端点的center位置。
edgeGraph.prototype = {paint() {// this.ctx.save();// 根据graph相对位置从不同端点绘制线条this.ctx.setLineWidth(this.width);this.ctx.setStrokeStyle(this.color);if (this.fromGraph && this.toGraph) {let fromX = this.fromGraph.centerX;let fromY = this.fromGraph.centerY;let toX = this.toGraph.centerX;let toY = this.toGraph.centerY;this.ctx.moveTo(fromX, fromY);this.ctx.lineTo(toX, toY);this.ctx.stroke();this.ctx.restore();} else {return;}},/*** 画一条线* @param ctx* @param a* @param b* @private*/_draw_line(ctx, a, b) {ctx.moveTo(a[0], a[1]);ctx.lineTo(b[0], b[1]);ctx.stroke();}
};
wxml中写好Canvas布局,绑定三个跟触摸有关的事件。
<canvas canvas-id='canvas-label'
disable-scroll="true"
bindtouchstart="start"
bindtouchmove="move"
bindtouchend="end"style='width: {{width}}rpx; height: {{height}}rpx;'></canvas>
随后就是Component部分。
Component({/*** 组件的属性列表*/properties: {width: {type: Number,value: 750,},height: {type: Number,value: 750,},},/*** 组件的初始数据*/data: {},// 初始化drawArr[],存放dragGraph;edgeArr[],存放edgeGraph对象;// treeRawArr[],存放上文中提到的后端传来的数据格式,并根据它设置drawArr[]和edgeArr[]。attached() {const sysInfo = wx.getSystemInfoSync();const screenWidth = sysInfo.screenWidth;this.factor = screenWidth / 750;//initX = this.toPx(screenWidth/2);//不知道为啥获取不到屏幕正中的绘图位置,就先这样吧initX = 155;console.log(screenWidth + ',' + initX);if (typeof this.drawArr === 'undefined') {this.drawArr = [];}if (typeof this.edgeArr === 'undefined') {this.edgeArr = [];}if (typeof this.treeRawArr == 'undefined') {this.treeRawArr = [];}// 创建画布this.ctx = wx.createCanvasContext('canvas-label', this);this.draw();},/*** 组件的方法列表*/methods: {toPx(rpx) {return rpx * this.factor;},// 设置画布的元素内容initByArr(newArr) {this.drawArr = [];this.edgeArr = [];// 循环插入 drawArrnewArr.forEach((item, index) => {switch (item.type) {case 'bgColor':this.data.bgImage = '';this.data.bgSourceId = '';this.data.bgColor = item.color;break;case 'bgImage':this.data.bgColor = '';this.data.bgImage = item.url;if (item.sourceId) {this.data.bgSourceId = item.sourceId;}break;case 'image':case 'text':if (index === newArr.length - 1) {item.selected = true;} else {item.selected = false;}this.drawArr.push(new dragGraph(item, this.ctx, this.factor));break;}});this.draw();},//getSelectedNode() {if (this.tempGraphArr.length == 0) {return {};} else {return this.tempGraphArr[0];}},// 画布的绘制,每次点击、移动、更改节点后就等于是重新刷新画布draw() {// 绘制edgethis.edgeArr.forEach((item) => {item.paint();});// 绘制graphthis.drawArr.forEach((item) => {item.paint();});return new Promise((resolve) => {this.ctx.draw(false, () => {resolve();});});},// 触摸的事件,主要是获取点击的dragGraphstart(e) {const { x, y } = e.touches[0];....//传值this.triggerEvent('onSelectedChange', JSON.stringify(this.getSelectedNode().taskattrs == undefined ? {} : this.getSelectedNode().taskattrs));this.draw();},// 绑定的移动事件,移动dragGraph的位置并刷新绘图move(e) {...},end(e) {...},// 用微信小程序的api将Canvas导出成一张图片,用于解决在触发编辑框时小程序本身的层级问题。 export () {return new Promise((resolve, reject) => {this.drawArr = this.drawArr.map((item) => {item.selected = false;return item;});this.draw().then(() => {wx.canvasToTempFilePath({canvasId: 'canvas-label',success: (res) => {resolve(res.tempFilePath);},fail: (e) => {reject(e);},}, this);});})},,//这是一些测试本地绘图的东西addNewNode(newNodeAttr) {newNodeAttr = newNodeAttr || {Task: {TaskID: "tt",Title: "新任务",Pusher: "tt",Content: "这是一个新任务",Status: false,PushDate: "2019-05-10 00:00:00",DeadLine: "2050-05-30 00:00:00",Urgency: 3},Self: this.treeRawArr.length,Child: [0],Parent: -1,TeamMates: []}var x_offset = 20,y_offset = 20;var fromNode = this.tempGraphArr[0];var index = fromNode.taskattrs[SELF];newNodeAttr['Parent'] = fromNode.taskattrs[TASK][TASK_ID];newNodeAttr['Self'] = this.treeRawArr.length;this.treeRawArr.push(newNodeAttr);if (this.treeRawArr[index][CHILD][0] == 0) {this.treeRawArr[index][CHILD] = [];}this.treeRawArr[index][CHILD].push(this.treeRawArr.length - 1);//直接push一个新的dragGraph对象var newTaskGraph = new dragGraph({x: fromNode.x + x_offset,y: fromNode.y + y_offset,text: newNodeAttr[TASK][TITLE],type: "text"}, this.ctx, this.factor, newNodeAttr);var newedge = new edgeGraph({width: 2,color: 'gray'}, this.ctx, fromNode, newTaskGraph);this.drawArr.push(newTaskGraph);this.edgeArr.push(newedge);fromNode.selected = false;this.tempGraphArr[0] = newTaskGraph;this.triggerEvent('onSelectedChange', JSON.stringify(this.getSelectedNode().taskattrs == undefined ? {} : this.getSelectedNode().taskattrs));this.draw();},//将结点标记为删除或标记为删除的结点的恢复delNode() {var selectedTaskInfo = this.tempGraphArr[0].taskattrs;this.tempGraphArr[0].isDeled = !this.tempGraphArr[0].isDeled;//this.triggerEvent('onRefresh');this.draw();},// 包括"Tree":{} "self":n "child":[a,b,c]// 传值传的都是dragGraph对象_insertTreeNode(fromTaskGraph) {this.drawArr.push(fromTaskGraph)var d = 100;var pos_x_offset = 0;var pos_y_offset = 80;var childs = fromTaskGraph.taskattrs[CHILD];var totalLen = 0;this.ctx.setFontSize(DEFAULT_FONT_SIZE);this.ctx.setTextBaseline('middle');this.ctx.setTextAlign('center');for (var i = 0; i < childs.length; i++) {//防止出现不应该存在的childif (childs[i] >= this.treeRawArr.length)continue;totalLen += this.ctx.measureText(this.treeRawArr[childs[i]][TASK][TITLE]).width;}//pos_x_offset = -(childs.length - 1) * d / 2;pos_x_offset = -(totalLen + (childs.length - 1) * 15) / 4;//console.log(totalLen);//非叶子节点if (childs[0] != 0) {for (var i = 0; i < childs.length; i++) {var index = childs[i];//防止出现不应该存在的childif (index >= this.treeRawArr.length)continue;var nextTaskNodeAtrr = this.treeRawArr[index];var newTaskGraph = new dragGraph({x: fromTaskGraph.x + pos_x_offset,y: fromTaskGraph.y + pos_y_offset,text: nextTaskNodeAtrr[TASK][TITLE],type: "text"}, this.ctx, this.factor, nextTaskNodeAtrr);var newedge = new edgeGraph({width: 2,color: 'gray'}, this.ctx, fromTaskGraph, newTaskGraph);this.edgeArr.push(newedge);this._insertTreeNode(newTaskGraph);pos_x_offset = pos_x_offset + this.ctx.measureText(nextTaskNodeAtrr[TASK][TITLE]).width + 15;}}},// 传入后端的数据,初始化treeRawArr[],因为原始数据只有chidren属性,所以要手动赋parent值initByTreeArr(treeArr) {// self遍历一遍散列进treeRawArrfor (var i = 0; i < treeArr.length; i++) {// 这是个dictvar thisTask = treeArr[i];// 这是个intvar arrIndex = thisTask[SELF];this.treeRawArr[arrIndex] = thisTask;}//初始化Parentthis.treeRawArr[0][PARENT] = "";for (var i = 0; i < this.treeRawArr.length; i++) {if (this.treeRawArr[i] == null)continue;var childs = this.treeRawArr[i][CHILD];for (var j = 0; j < childs.length; j++) {//Parent字段改成TaskIDs//this.treeRawArr[childs[j]][PARENT]=i;//防止出现了不应该存在的childif (childs[j] < this.treeRawArr.length)this.treeRawArr[childs[j]][PARENT] = this.treeRawArr[i][TASK][TASK_ID];}}this.treeRawArr[0][PARENT] = "";console.log("treeRawArr");console.log(this.treeRawArr);var rootTaskNode = this.treeRawArr[0];var newgraph = new dragGraph({x: initX,y: initY,text: rootTaskNode[TASK][TITLE],type: "text"}, this.ctx, this.factor, rootTaskNode);this._insertTreeNode(newgraph);this.draw();},//执行删除所有标记为isDeled的结点//这里的删除是仅删除父节点的childsonDoDel() {for (var i = 0; i < this.drawArr.length; i++) {if (this.drawArr[i].isDeled) {var index = this.drawArr[i].taskattrs[SELF];for (var j = 0; j < this.treeRawArr.length; j++) {var theindex = -1;//小程序没有indexOf???for (var k = 0; k < this.treeRawArr[j][CHILD].length; k++) {if (this.treeRawArr[j][CHILD][k] == index) {theindex = k;break;}}if (theindex > -1) {//修改CHILDthis.treeRawArr[j][CHILD].splice(theindex, 1);break;}}}}this.triggerEvent("onRefresh");},// 更改结点即数组的信息changeNodeInfo(newInfo, targetNode = this.tempGraphArr[0]) {//console.log(newInfo.Title);targetNode.taskattrs[TASK][TITLE] = newInfo.Title;targetNode.taskattrs[TASK][CONTENT] = newInfo.Content;targetNode.taskattrs[TASK][DEADLINE] = newInfo.DeadLine;this.draw();},// 根据treeRawArr生成drawArr[]和edgeArr[]setByTree() {const initX = 100,initY = 100;var rootTaskNode = this.treeRawArr[0];var newgraph = new dragGraph({x: initX,y: initY,text: rootTaskNode[TASK][TITLE],type: "text"}, this.ctx, this.factor, rootTaskNode);this._insertTreeNode(newgraph);this.draw();},//根据数据格式中的self属性获得Task对象getTaskByIndex(self) {for (var i = 0; i < this.treeRawArr.length; i++) {var thistask = this.treeRawArr[i];if (self == thistask[SELF])return thistask;}return undefined;},clearCanvas() {this.ctx.clearRect(0, 0, this.toPx(this.data.width), this.toPx(this.data.height));this.ctx.draw();this.drawArr = [];this.edgeArr = [];this.data.bgColor = '';this.data.bgSourceId = '';this.data.bgImage = '';}}
});
温习了一遍以后,总算勉强想起来自己当初是怎么设计的了……贴上来的代码做了一些精简,(但我寻思应该也没人会认认真真看完吧,主要还是给自己复习一下)。
总之是传入一个规定格式的数组,通过initByArr(arr)初始化treeRawArr[]数组,调用setByTree()根据该数组初始化drawArr[]和edgeArr[]。对于drawArr[]通过children属性dfs遍历完整棵树push入dragGraph对象。在任何更改画布信息的操作后调用draw()刷新重绘画布。
导出模块:
const defaultOptions = {selector: '#canvas-drag'
};function CanvasDrag(options = {}) {options = {...defaultOptions,...options,};const pages = getCurrentPages();const ctx = pages[pages.length - 1];const canvasDrag = ctx.selectComponent(options.selector);delete options.selector;return canvasDrag;
}
CanvasDrag.initByTreeArr = (arr) => {const canvasDrag = CanvasDrag();if (!canvasDrag) {console.error('请设置组件的id="canvas-drag"!!!');} else {return CanvasDrag().initByTreeArr(arr);}
};...export default CanvasDrag;
最终在父页面导入模块。
tree.js
import CanvasDrag from '../../components/canvas-drag/canvas-drag';
tree.json
{"backgroundTextStyle": "dark","usingComponents": {"canvas-drag": "/components/canvas-drag/index"}
}
遇到的问题
有些问题查了一下,在微信小程序一年间的更新中已经解决了,所以可能没啥时效性。
Canvas层次太高
Canvas作为原生组件,在微信小程序里的层次是最高的,也就是想实现“编辑结点信息”的功能,必然要弹出一个输入框,但他会因为层级的关系被压在Canvas下面。
这个问题微信小程序目前已经解决了,同层渲染
不过当时还是用个差不多的方法解决了。就是点击按钮后,hidden掉Canvas,并将Canvas导出成一张图片放在原先的位置,这时候弹出输入框就不会被层次掩盖了。
<view class="st-radius-view" style='margin-top:40rpx'><view hidden="{{isEdit}}"><canvas-drag id="canvas-drag" bind:onSelectedChange='onSelectedChange' bind:onRefresh='onInitByTree' graph="{{graph}}" width="700" height="750"></canvas-drag></view><image hidden="{{!isEdit}}" src="{{canvasImg}}" style="width:700rpx;height:750rpx;"></image>
</view>
布局,设置hidden。
onEditNode:function(e){this.saveCanvas();this.setData({isEdit:true});CanvasDrag.getTaskByIndex(this.data.selected_node[SELF])[TASK][TASK_ID]='tt_fix';},
点击按钮后保存Canvas为一张图像。
子组件向父组件传递信息
这个树状图有一个点击结点,在父页面显示该节点信息的功能,就需要子组件向父组件传递信息。
wxml界面。
<canvas-drag id="canvas-drag" bind:onSelectedChange='onSelectedChange' bind:onRefresh='onInitByTree' graph="{{graph}}" width="700" height="750"></canvas-drag>
父页面的接受方法,但当时非常迷惑,不知道为什么要把对象转换成字符串再转换成对象……
onSelectedChange: function(e) {this.setData({text_selected_node: e.detail});if (this.data.text_selected_node == JSON.stringify({})) {this.setData({isSelected: false,selected_node:{}});} else {var obj = JSON.parse(this.data.text_selected_node);this.setData({isSelected: true,edit_info:{Title:obj[TASK][TITLE],Content:obj[TASK][CONTENT],DeadLine:obj[TASK][DEADLINE]},selected_node:obj});console.log('ThisTaskID:'+this.data.selected_node[TASK][TASK_ID]);}},
感想
还是非常感谢wxa-comp-canvas-drag的作者,在开始写这个程序的时候我还是连js的this指向问题都能debug半天的家伙,如果找不到这个画布模块的基础我估计就要凉了……
【微信小程序】可拖拽操作的“树状图”模块的制作和小程序经验的总结相关推荐
- 实现树状图_举个栗子!Tableau 技巧(132):用参数操作实现数据下钻
之前,我们分享过 用集操作实现树状图的数据下钻 .其实,对于 2019.2 及以上版本的 Tableau 用户,利用其新功能参数操作 也能便捷的实现图表中数据的下钻. Tips:集和参数都可以实现下钻 ...
- 微信小程序~触摸相关事件(拖拽操作、手势识别、多点触控)
touchstart 手指触摸动作开始 touchmove 手指触摸后移动 touchcancel 手指触摸动作被打断,如来电提醒,弹窗 touchend 手指触摸动作结束 拖拽操作案例1:(注意按钮 ...
- 小程序的拖拽、缩放和旋转手势
在开发中,有时会遇到像App中的手势那样的效果,下面就仿照App实现了一下. wxml部分: <view class="touch-container"><vie ...
- [模拟拖拽] 模拟将一个文件拖拽到一个软件窗口上,实现拖拽操作(微信语音播放器)...
"金蛇语音播放器" 是我随便写的一个假名.要实现的功能是: 我在网上下载了一个播放器,在自己公司的软件中使用,用来播放微信的语音. 因为版权问题,我不想让别人知道我用的是金蛇播放器 ...
- WPF 的拖拽操作(DragDrop)
在WPF中似乎没有对拖拽操作进行改变,和以前的方式一样.如果曾近在 Windows 窗体应用程序中使用过鼠标拖放,就会发现在 WPF 中的编程接口实际上没有发生变化.重要的区别是用于拖放操作的方法和事 ...
- UE4 二维地图的缩放与拖拽操作
这里写自定义目录标题 UE4 二维地图的缩放与拖拽操作 拖拽和缩放 基础搭建 添加小图标 地图缩放 地图拖拽 实现部分 效果展示 小图标的重合显示 效果展示 UE4 二维地图的缩放与拖拽操作 纯蓝图实 ...
- 使用jq-ui实现选中多元素进行拖拽操作
使用jq-ui实现选中多元素进行拖拽操作 开发中为了方便用户想引入拖拽操作,但发现jq只支持单个节点拖拽,google百度了半天只有几个实现了多选拖拽了例子,但感觉不是很好用,所以干脆利用晚上时间自己 ...
- highchart的draggable-points.js依赖实现图表的动态拖拽操作
highchart的draggable-points.js依赖实现图表的动态拖拽操作 需求 实现 总结 需求 实现一个曲线图,能够通过鼠标去拖拽点,来进行修改图表 实现 例子基于vue来实现,如果是j ...
- 微信小程序简单树状图的实现
由于微信没有树状图这个组件,自己做了个简简单单的树状图,有需要的可以参考下我这个. 先上图 实现了三级,如果有更多级别的需求,可以参考我的规律进行添加. 下面是wxml的代码 <view> ...
最新文章
- for of 的用法区别_语法全解介词to和for的用法 如何简单区别使用
- 【springboot中使用拦截器】
- 现代交换技术学习笔记001
- 如果再这么玩下去,中国的科研就没戏了
- JavaEE基础(05):过滤器、监听器、拦截器,应用详解
- [从零开始]HelloWorld——第一个应用程序
- Java 算法 学做菜
- Kaggle电影数据集:movies_metadata.csv
- python实现邮件发送图表_python基于SMTP实现可视化邮件发送
- Informatic 9.0 client和server 安装配置
- 5个促进 OKR 成功的文化准则
- html自我介绍怎么弄,用html设计一个自我介绍的静态网页
- 远端服务器无响应 请联系网络供应商腾达,移动宽带连接腾达路由器显示 远端服务器无响应。请联系您的网络运营...
- 系统入门(1):安卓系统bootloader模式是什么?如何进入bootloader
- linux ctrl r 搜索,linux下用ctrl+r快速搜索history命令
- SAP外协采购单和销售单需求关闭预留未清处理方法
- Struts2+Datagrid表格显示(可显示多表内容)
- 禁止计算机自动弹出广告,如何禁止电脑弹出广告
- python cursor游标_精通 Oracle+Python,第 1 部分:查询最佳应践
- Cocos2d-JS v3.0 RC2发布说明