全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2025-01-30_canvas库 konva 实现腾讯文档 [甘特图视图]

您的位置:首页 >> 新闻 >> 行业资讯

canvas库 konva 实现腾讯文档 [甘特图视图] 点击关注公众号,“技术干货”及时达! 简要说明写文章时贴的代码还处于脱敏阶段,有些业务功能不参与其中。我会在后续完全脱敏后完善优化并开源代码。上一篇文章实现了日历视图,布局和功能相对简单一点,所以核心功能我用了一个 class 去实现。甘特图功能和布局相对复杂一点,且为了后续扩展,这块要将功能细化。可能开源的代码并不能够直接参与到你们的实际项目中,主要是为了让大家体会一个功能的实现的过程。 我先贴一下简易的效果图和布局和文件目录结构。 结构分析双图层模式 静态图层: 年月季周单独一个 Group列日期标题单独一个 Group,主要是为了处理纵向滚动条滚动正文部分 不要让 title 也被 offset 掉。渲染正文 Group,滚动时控制 offseX 和 offseY,这样就不用去设置 layer 涂层的位置,最小单位更新。动态图层 横向|纵向滚动条单独一个 Rectmaker 里程碑 | task 每个独有一个 Group通过结构分配 然后初始化各个模块 这样初始化后 布局大致上有了纹路,接下来要做的就是在各个功能区填充对应的小功能。 渲染 年月 tab 区域如果是 dom 实现的话很简单 用 konva 的话 我们需要定义好结构 灰色打底的 Rect 满整个 tab 宽度白色选中背景的 Rect 占用某一个选项宽度选项文字渲染点击事件,修改白色选中背景的 Rect 的位置并添加动画export type Iunit = 'WEEK' | 'MONTH' | 'QUARTER' | 'YEAR'; export class DateRange { readonly container: Konva.Group; // 周|月|季|年 unit: Iunit = 'MONTH'; unitMap = new Map([ ['WEEK', { name: '周', x: 28, index: 0 }], ['MONTH', { name: '月', x: 88, index: 1 }], ['QUARTER', { name: '季', x: 148, index: 2 }], ['YEAR', { name: '年', x: 208, index: 3 }], ]); constructor(private readonly core: Core) { this.unit = 'MONTH'; this.container = new Konva.Group({ x: this.core.config.containerWidth - 260, }); const bgcolor = new Konva.Rect({ width: 245, height: 30, fill: 'rgba(235,236,237,1)', opacity: 1, cornerRadius: 2 }) const activeBgColor = new Konva.Rect({ x: 63, y: 3, width: 60, height: 24, // 白色 fill: 'rgba(255,255,255,1)', opacity: 1, cornerRadius: 2 }); this.container.add(bgcolor, activeBgColor); const values = this.unitMap.values(); while (true) { const iterator = values.next(); if (iterator.done) { break; } const rect = new Konva.Rect({ x: iterator.value.x - 20, y: 3, width: 50, height: 23, fill: 'transparent', // fill: 'red', cornerRadius: 3 }); const text = new Konva.Text({ x: iterator.value.x, y: 8, width: 50, height: 20, fill: 'black', text: iterator.value.name, fontSize: 14 }) const itemGroup = new Konva.Group(); itemGroup.on('click', () = { activeBgColor.to({ x: iterator.value.index * 60 + 3, easing: Konva.Easings.StrongEaseOut, duration: 0.2 }) }) itemGroup.add(rect, text); this.container.add(itemGroup); } // 鼠标进入 this.container.on('mouseenter', () = this.core.cursor('pointer')); // 鼠标离开 this.container.on('mouseleave', () = this.core.cursor('default')); } } 渲染列标题日期和正文 (以月为标准进行渲染)通过效果图来看。我们只需要渲染可视区域的几个 Rect 节点即可完成,但是如果仅仅如此的话,后面我们判断 task 的 y 坐标和 hover 添加 task 不太好确定。于是我使用 konva devtool 谷歌插件调试腾讯文档的 konva 节点 ,得到的结果是其实是渲染的一个区域的多个 rect,只不过隐藏了边的颜色,外加渲染了几条竖线日期线。我们也依葫芦画瓢来准备参数。 所以我们要渲染一个类似 table 出来,准备好所需参数 export class Config { // 列数量 columnCount = 0; // container的 offsetX offsetX = 0; offsetY = 0; // 行高 rowHeight = 33; // 列宽 columnWidth = 60; // 行数量 rowCount = 40; // 画布的宽度 containerWidth = 1080; // 画布的高度 containerHeight = 600; // 开始时间 startDate = "2024-08-22"; // 结束时间 endDate = "2025-09-25"; // 挂载节点 container = '.container'; // 模式 mode: 'edit' | 'read' = 'edit' constructor(config?: PartialConfig) { Object.assign(this, config); this.columnCount = DatePostion.calculateColumnCount(this.startDate, this.endDate); } update(config: PartialConfig) { Object.assign(this, config); } } 绘制一个表格出来很简单,但是我们要结合滚动条滚动的位置来渲染并且只渲染可视区域的节点,需要自己准备一些辅助函数实现。render 这个 class 专门处理渲染相关的逻辑最关键就是 render 中这个 getDrawConfig 函数,这个函数可以在 存在 offsetX 和 offsetY | containerHeight | containerWidth 的限制情况下得到 可视区域中应该存在哪些单元格。 getDrawConfig函数,({ initRowCount }: Partial{ initRowCount?: number }) { const { offsetX, offsetY, containerHeight, containerWidth: width, startDate, endDate, columnCount } = this.config; const containerWidth = width - 20; const rowHeight = () = this.config.rowHeight; const columnWidth = () = this.config.columnWidth; const rowCount = initRowCount || this.config.rowCount; const rowStartIndex = getRowStartIndexForOffset({ itemType: "row", rowHeight, columnWidth, rowCount, columnCount, instanceProps: this.instanceProps, offset: offsetY, }); const rowStopIndex = getRowStopIndexForStartIndex({ startIndex: rowStartIndex, rowCount, rowHeight, columnWidth, scrollTop: offsetY, containerHeight, instanceProps: this.instanceProps, }); const columnStartIndex = getColumnStartIndexForOffset({ itemType: "column", rowHeight, columnWidth, rowCount, columnCount, instanceProps: this.instanceProps, offset: offsetX, }); const columnStopIndex = getColumnStopIndexForStartIndex({ startIndex: columnStartIndex, columnCount, rowHeight, columnWidth, scrollLeft: offsetX, containerWidth, instanceProps: this.instanceProps, }); const items = []; if (columnCount 0 && rowCount) { for (let rowIndex = rowStartIndex; rowIndex = rowStopIndex; rowIndex++) { for ( let columnIndex = columnStartIndex; columnIndex = columnStopIndex; columnIndex++ ) { const width = getColumnWidth(columnIndex, this.instanceProps); const x = getColumnOffset({ index: columnIndex, rowHeight, columnWidth, instanceProps: this.instanceProps, }); const height = getRowHeight(rowIndex, this.instanceProps); const y = getRowOffset({ index: rowIndex, rowHeight, columnWidth, instanceProps: this.instanceProps, }); const date = this.getDateFromX(x); items.push( { x, y, width, height, rowIndex, columnIndex, date: date.date, title: date.title, name: `container_cell ${date.date}`, isWeak: this.isWeekend(new Date(date.date)), key: itemKey({ rowIndex, columnIndex }), } ); } } } // this.config.update({ columnCount }); this.columnStartIndex = columnStartIndex; this.columnStopIndex = columnStopIndex; this.itemCells = items; } 调用这个函数。得到所有单元格数据。拿到数据后我们通过专门的渲染函数进行渲染列标题和单元格。 draw() { const { containerGroup, headerTextGroup } = this.staticLayer; containerGroup.removeChildren(); headerTextGroup.removeChildren(); this.getDrawConfig({}); const items = this.itemCells; const columnStartIndex = this.columnStartIndex; const columnStopIndex = this.columnStopIndex; for (let index = 0; index = (columnStopIndex - columnStartIndex); index++) { const colindex = items[index]; const text = new Konva.Text({ x: colindex.x + colindex.width / 2, y: -14, text: colindex.date.slice(-4), fontSize: 12, fill: 'rabg(0,0,0,1)', name: 'text', key: colindex.key, }) text.setAttr('offsetX', text.width() / 2) // 设置 offsetX 为文本宽度的一半,确保文字居中 headerTextGroup.add(text) } items.forEach(({ x, y, width, height, rowIndex, columnIndex, name, key, isWeak, date }) = { containerGroup.add(new RectBorderNode({ x, y, date, height, width, hitStrokeWidth: 1, strokeWidth: 0.2, fill: isWeak ? 'rgba(243,245,247,1)' : '#FFF', name, key, listening: false, strokeBottomColor: '#C0C4C9', strokeTopColor: '#C0C4C9', strokeRightColor: 'rgba(201,192,196 , 0.3)', strokeLeftColor: 'rgba(201,192,196 , 0.3)', })) }) } 渲染图如下: 但是我们知道腾讯文档显示出来的就是竖线,没有呈现表格,那我们要做的就是设置 rect 为白色,然后单独渲染几条竖线出来 让视觉看起来正常就行。代码中会有呈现。然后效果图。 完成了基础的渲染。接下来就要完成动态的部分了,横线滚动条滚动变更区域单元格。先看下横向滚动条的实现,确定滚动条的宽度,图中有实现。 // dragmove更新表格 并重新绘制 verticalBarRect.on('dragmove', (event) = { const scrollbarX = event.target.x(); // 根据滚动条位置计算内容的滚动位置 const scrollRatio = scrollbarX / maxScrollbarX; const tempScrollLeft = scrollRatio * maxScroll; // 更新内容的 x 位置 this.core.moveOffsetX(tempScrollLeft); }); verticalBarRect.on("dragend", () = { this.render.draw(); }) //core.ts moveOffsetX(offsetX: number) { // 更新内容的 x 位置 this.config.update({ offsetX }) this.staticLayer.containerGroup.x(-offsetX); this.staticLayer.headerTextGroup.x(-offsetX); this.render.scrollX(); } //. render.ts scrollX() { this. draw()函数重新渲染。 我们再来看下(); this.makerManager.update(); this.taskManager.moveX(); } 当我们滚动然后调用更新。this.core.moveOffsetX(tempScrollLeft);会触发 draw() 函数重新渲染。我们再来看下 draw 函数的实现。其逻辑是 在重新渲染之前先销毁所有的 cell 单元格 然后再 add 所有的 cell Rect 节点。 我们模拟一个动画。持续滚动 = 持续更新渲染。看看表现 setTimeout(() = { this.request(); }, 200); } private x = 0; request() { requestAnimationFrame(() = { if (this.config.offsetX 800) { return; } this.x += 2; batchDrawManager.batchDraw(() = this.moveOffsetX(this.x)) this.request(); }) } 通过录制火焰图,我发现会存在 Partially Presented Frame 的情况。cpu 占比也高了点。因为在一帧中所做的事情太多了,这块我们要去优化。 优化滚动渲染性能「1. 第一点我首先想到的是看 konva 官网有没有提到优化手段,刚好存在 listening : false ,于是实践。」 思路:dragmove 第一次执行的时候将 layer 层和 Group listening = false 因为在滚动的过程中 我们也不需要参与到节点的用户交互。在 dragend 滚动结束的时候再将 listening = true设置回来。 「2. 性能问题肯定跟节点数量有关,那我们能不能从节点数量出发优化呢?当然是可以的。既然在滚动过程 甘特图其实是不参与用户交互的。那么在这中间怎么渲染都行 只要视觉保持一致就行。」 滚动开始:重新实现一个函数,不要生成所有单元格,只需要生成一行的单元格,然后我利用一行的单元格,来生成一行的 Rect 只需要把 Rect 的两边的高度跟 Group 的高度把持一致即可,这样我们保持视觉统一的情况下,又大大的减少了节点的绘制。拖动结束:调用原来的绘制函数,生成所有的 cell。保持视觉和交互一致。只是新增了 animationDraw 函数,this.getDrawConfig({ initRowCount: 1 });只生成一行数据。 verticalBarRect.on('dragmove', (event) = { const scrollbarX = event.target.x(); // 根据滚动条位置计算内容的滚动位置 const scrollRatio = scrollbarX / maxScrollbarX; const tempScrollLeft = scrollRatio * maxScroll; // 更新内容的 x 位置 this.core.moveOffsetX(tempScrollLeft); }); verticalBarRect.on("dragend", () = { this.render.draw(); }) //。core.ts moveOffsetX(offsetX: number) { // 更新内容的 x 位置 this.config.update({ offsetX }) this.staticLayer.containerGroup.x(-offsetX); this.staticLayer.headerTextGroup.x(-offsetX); this.render.scrollX(); } //。render.ts scrollX() { this.animationDraw(); this.makerManager.update(); this.taskManager.moveX(); } animationDraw() { const { staticLayer: { containerGroup, headerTextGroup }, columnStartIndex, columnStopIndex, itemCells: items, } = this; containerGroup.destroyChildren(); headerTextGroup.destroyChildren(); this.getDrawConfig({ initRowCount: 1 }); 优化后。再执行一下动画,看看性能分析,明显好多了,符合自己的预期了。 maker 和 task 渲染这块儿用几个 class 来管理。滚动时只需要调用 manager 中的 update 方法 会执行所有 maker 中的 update 更新 x 坐标。 task 也是一样。需要一个 manager 来管理。但是与 maker 不同的是。task 的时间跨度需要支持可以拖动更新的。作为一个 sdk,我们需要设计权限配置,当 mode = 'edit'才可以被更改。 没有权限时 实例化 : 存在权限时 实例化:ResizeTask 实现在 task 基础上 扩展 resize 功能。 纵向滚动条这块的功能不复杂。这个滚动条不需要过多的讲解。滚动更新 offsetY 的值,然后调用 taskManager 中 moveY 函数,更新每一个 task 的 y 坐标即可。但是我们知道 y 坐标变化。cells 也应该重新渲染 但是我们为什么不去重新渲染呢?还是一样的优化思路。我们只在滚动结束后更新渲染。视觉上就像没有更新过 cells 一样。性能不用担心。 发布订阅一个合格的 sdk 应该向调用方暴露一些接口,方便调用者知道 sdk 的进度和接收事件。这里我举个例子。 调用方使用 : const gantt = new Gantt() gantt.API.on("tapTask", (params) = { console.log('params', params); }) gantt.API.on("rightMenuTask", (params) = { console.log('params', params); }) 补充其实要充分实现这个功能,还有很多需要补充和优化的点。比如最上面提到的 Config 配置类,应该由调用者传入。还有 父子节点关联, 都需要扩展。 const gantt = new Gantt({ mode : 'edit', startDate : '2024-10-30' }) gantt.API.setData({ makers:[{ startDate : '2024-10-30' }], tasks:[ {...} ] }) gantt.API.on("tapTask", (params) = { console.log('params', params); }) gantt.API.on("rightMenuTask", (params) = { console.log('params', params); }) 结束本次文章我主要想讲解一下功能分析和性能优化,写文章不是我的强项,有不懂的可以留言。源码过几天整理后更新在文章后面,可以自己再去追加功能。 点击关注公众号,“技术干货”及时达! 阅读原文

上一篇:2025-07-27_鳍源x索尼水下机器人拍摄解决方案,引领影视产业水下拍摄发展 下一篇:2021-08-17_48部「特别」即将在此碰撞!丨「海浪新力量电影计划」NEW ERA主竞赛单元入围公布

TAG标签:

18
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价