去年三月份高中同学拉我做的微信小程序,尽管可能除了高一的时候做网页写过些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半天的家伙,如果找不到这个画布模块的基础我估计就要凉了……

【微信小程序】可拖拽操作的“树状图”模块的制作和小程序经验的总结相关推荐

  1. 实现树状图_举个栗子!Tableau 技巧(132):用参数操作实现数据下钻

    之前,我们分享过 用集操作实现树状图的数据下钻 .其实,对于 2019.2 及以上版本的 Tableau 用户,利用其新功能参数操作 也能便捷的实现图表中数据的下钻. Tips:集和参数都可以实现下钻 ...

  2. 微信小程序~触摸相关事件(拖拽操作、手势识别、多点触控)

    touchstart 手指触摸动作开始 touchmove 手指触摸后移动 touchcancel 手指触摸动作被打断,如来电提醒,弹窗 touchend 手指触摸动作结束 拖拽操作案例1:(注意按钮 ...

  3. 小程序的拖拽、缩放和旋转手势

    在开发中,有时会遇到像App中的手势那样的效果,下面就仿照App实现了一下. wxml部分: <view class="touch-container"><vie ...

  4. [模拟拖拽] 模拟将一个文件拖拽到一个软件窗口上,实现拖拽操作(微信语音播放器)...

    "金蛇语音播放器" 是我随便写的一个假名.要实现的功能是: 我在网上下载了一个播放器,在自己公司的软件中使用,用来播放微信的语音. 因为版权问题,我不想让别人知道我用的是金蛇播放器 ...

  5. WPF 的拖拽操作(DragDrop)

    在WPF中似乎没有对拖拽操作进行改变,和以前的方式一样.如果曾近在 Windows 窗体应用程序中使用过鼠标拖放,就会发现在 WPF 中的编程接口实际上没有发生变化.重要的区别是用于拖放操作的方法和事 ...

  6. UE4 二维地图的缩放与拖拽操作

    这里写自定义目录标题 UE4 二维地图的缩放与拖拽操作 拖拽和缩放 基础搭建 添加小图标 地图缩放 地图拖拽 实现部分 效果展示 小图标的重合显示 效果展示 UE4 二维地图的缩放与拖拽操作 纯蓝图实 ...

  7. 使用jq-ui实现选中多元素进行拖拽操作

    使用jq-ui实现选中多元素进行拖拽操作 开发中为了方便用户想引入拖拽操作,但发现jq只支持单个节点拖拽,google百度了半天只有几个实现了多选拖拽了例子,但感觉不是很好用,所以干脆利用晚上时间自己 ...

  8. highchart的draggable-points.js依赖实现图表的动态拖拽操作

    highchart的draggable-points.js依赖实现图表的动态拖拽操作 需求 实现 总结 需求 实现一个曲线图,能够通过鼠标去拖拽点,来进行修改图表 实现 例子基于vue来实现,如果是j ...

  9. 微信小程序简单树状图的实现

    由于微信没有树状图这个组件,自己做了个简简单单的树状图,有需要的可以参考下我这个. 先上图 实现了三级,如果有更多级别的需求,可以参考我的规律进行添加. 下面是wxml的代码 <view> ...

最新文章

  1. for of 的用法区别_语法全解介词to和for的用法 如何简单区别使用
  2. 【springboot中使用拦截器】
  3. 现代交换技术学习笔记001
  4. 如果再这么玩下去,中国的科研就没戏了
  5. JavaEE基础(05):过滤器、监听器、拦截器,应用详解
  6. [从零开始]HelloWorld——第一个应用程序
  7. Java 算法 学做菜
  8. Kaggle电影数据集:movies_metadata.csv
  9. python实现邮件发送图表_python基于SMTP实现可视化邮件发送
  10. Informatic 9.0 client和server 安装配置
  11. 5个促进 OKR 成功的文化准则
  12. html自我介绍怎么弄,用html设计一个自我介绍的静态网页
  13. 远端服务器无响应 请联系网络供应商腾达,移动宽带连接腾达路由器显示 远端服务器无响应。请联系您的网络运营...
  14. 系统入门(1):安卓系统bootloader模式是什么?如何进入bootloader
  15. linux ctrl r 搜索,linux下用ctrl+r快速搜索history命令
  16. SAP外协采购单和销售单需求关闭预留未清处理方法
  17. Struts2+Datagrid表格显示(可显示多表内容)
  18. 禁止计算机自动弹出广告,如何禁止电脑弹出广告
  19. python cursor游标_精通 Oracle+Python,第 1 部分:查询最佳应践
  20. Cocos2d-JS v3.0 RC2发布说明

热门文章

  1. 【数电实验7】Verilog—外星萤火虫
  2. floyd, 潜意识等
  3. FL Studio水果编曲软件V20.0.3.542密钥序列号版
  4. python人体动作识别_hand-keras-yolo3-recognize
  5. H5跳转App、跳转小程序
  6. sipXecs技术交流QQ群
  7. 大学计算机--计算思维的视角
  8. 【滤波】基于最近邻算法实现多目标航迹关联附matlab代码
  9. Paxos Made Practical
  10. Java中数组怎么初始化?数组初始化方法