Vue 3从零绘制类甘特图:生产排产视图实战与性能优化
先说结论如果你在项目里需要的是一个看着像甘特图、但又不完全是标准甘特图的排产/调度视图直接在 Vue 3 里手绘一个类甘特图组件往往比接入现成甘特图库更省心。这篇文章我就拿最近做的生产调度排产视图当例子把从零绘制类甘特图的思路、关键代码、踩坑点完整捋一遍包括这类图表最常见的左侧表格 右侧时间轴结构的对齐方案、任务条拖拽与缩放的联动逻辑、以及大数据量下的渲染性能问题。项目里没有现成的 gantt 库可用或者觉得 dhtmlx、frappe-gantt 这些库太重、定制成本太高的人这篇应该能帮你少走不少弯路。1. 为什么放着现成甘特图库不用非要自己画1.1 三方甘特图库的局限在哪我一开始也考虑过直接用成熟库。dhtmlxGantt 功能确实强是正经甘特图库里的老大哥但问题在于它是商业授权而且它那一套事件体系、数据格式、样式结构都是人家定死的。你想改一个任务条底下的颜色分段或者左侧表格树的展开收起动画得先跟它的源码搏斗半天。frappe-gantt 是开源的轻量但它的定位是给项目管理用的标准甘特图默认只有任务条、依赖线、进度不支持你自定义左侧树形表格也不支持多行任务合并显示。生产调度的场景里一个工单可能横跨多台设备、多个时间段每个时间段的状态颜色还不一样这种类甘特图需求明显不是 frappe-gantt 这种标准甘特图能直接套的。还有个现实问题你要接的是 Vue 3 项目而大多数甘特图库的 Vue 封装要么停在 Vue 2要么封装得很浅你依然要手动处理数据的响应式联动。与其绕来绕去不如直接基于 Vue 3 的组合式 API 自己做一个——要什么形状自己画要什么交互自己绑反而干净利落。1.2 类甘特图和标准甘特图的本质差异标准甘特图比如项目管理里的横道图记录的是任务的开始时间、结束时间、完成百分比结构是任务一行进度一条进度条任务之间可能会有依赖箭头。类甘特图在制造业排产、产线调度、容量规划里非常常见它看起来还是时间轴上的横条但有几个额外特点一条任务工单可以被拆成多段同一个工单在 8:00-10:00 用设备 A14:00-16:00 用设备 B表现出来就是同一个任务行里有横跨不同时间的多个色块。标准甘特图库往往不支持这种一段任务多个时间段的模型。每个时间段可能有状态属性正常、等待、换型、检修不同状态用不同颜色于是任务行变成了分段彩条。左侧表格信息很重除了任务名通常还得显示工单号、产品、数量、负责人、当前状态等信息右侧是时间轴。左侧表格和右侧图表的行高必须严格一致滚动必须同步否则先前的对齐设计就崩了。自己做类甘特图本质上不是在画甘特图而是在搭建一个时间轴渲染引擎 自定义表格 拖拽交互的集成组件。搞清楚这个本质之后实现路径反而清晰了。2. 整体布局架构左侧表格与右侧时间轴的对齐方案2.1 双栏布局怎么选型类甘特图典型的布局是左侧一张固定宽度的表格右侧一条带时间刻度的滚动时间轴。传统做法是左侧用table右侧用绝对定位的 div然后监听两个容器的滚动事件做同步。一开始我也做了双容器的滚动同步方案但实际操作中发现两个问题一是滚动同步总有一方会慢半拍尤其在 Windows 触摸板惯性滚动、或者内容高度达到几千像素时肉眼能看出左侧表格和右侧时间轴的错位二是维护成本高滚动监听、锁定标志、手动校准逻辑零零散散地堆着代码很容易乱。后来我把结构改成单容器方案┌─────────────────────────────────────┐ │ header-left │ header-right │ ├────────────────├────────────────────┤ │ body-left │ body-right │ │ (fixed) │ (overflow-auto) │ └────────────────┴────────────────────┘核心思路是整块组件用一个虚拟滚动容器body-right滚动左侧表格不自己滚动而是通过一个 translateY 的订阅值随时同步过来。右侧滚动时把 scrollTop 写进响应式变量左侧表格区域根据这个变量做 transform 平移。这样做的好处是滚动只发生在右侧一个容器上不存在左右同时滚动导致的时序差。左侧表格只是视觉上跟着动了但实际没有产生任何原生滚动行为所以绝不会错位。2.2 头部滚动同步与时间轴冻结头部也有一个细节时间轴表头日期、星期、刻度线需要横向跟随滚动但纵向要冻结不跟随。所以在布局上我拆成四个区域header-left左侧表格的列标题固定不动header-right时间轴刻度头部横向滚动跟随纵向固定body-left左侧表格的行的内容横向固定纵向跟随滚动body-right时间轴主体横向纵向都滚动实现时 header-right 里面其实是一个跟 body-right 一样宽的 divbody-right 滚动时监听scroll事件把scrollLeft同步给 header-right 里的那个 div 的transform: translateX(-...)。关键点是不要用 scrollLeft 直接给 header 的 scrollLeft 赋值否则会出现两个原生滚动容器互相抢滚动的抖动。用 transform 来同步头部视觉上是跟着走了实际上头部完全没有原生滚动状态所以不会产生冲突。function handleScroll(e: Event) { const target e.target as HTMLElement state.scrollTop target.scrollTop state.scrollLeft target.scrollLeft // 左侧表格行容器transform: translateY(-scrollTop) // 头部时间轴容器transform: translateX(-scrollLeft) }2.3 时间轴刻度的生成规则时间轴刻度是类甘特图最基础的外观骨架。它需要根据当前缩放级别按小时、按天、按周、按月动态生成刻度。我的实现是interface TimeScaleUnit { key: hour | day | week | month majorUnitLabel: (date: Date) string // 大刻度显示 minorStepMs: number // 小刻度间隔毫秒 majorStepMs: number // 大刻度间隔毫秒 }比如按天显示的缩放级别小刻度是 1 天大刻度是 1 周大刻度下面的标签显示2025年第23周按小时显示的缩放级别小刻度 2 小时大刻度 1 天标签显示6月5日 周三。刻度不是一把一把渲染全部日期的而是按可视区域动态生成。body-right 可视宽度大概是 1200px比如按天显示时一天占 80px那么可视区域大约能看到 15 天。只需要计算当前 scrollLeft 对应的起始日期然后往右多生成 2 个往左留 1 个缓冲再根据数组循环渲染刻度线即可。function buildTicks() { const startDate getStartDateFromScrollLeft(state.scrollLeft) const count Math.ceil(viewWidth / scaleWidth) 4 // 多两个缓冲 const ticks [] for (let i -1; i count; i) { const d addDays(startDate, i) ticks.push({ date: d, label: formatLabel(d, scaleUnit), x: dateToX(d) }) } return ticks }滚动过程中每次scroll都要重新计算刻度的话开销会很大其实可以在每次 scroll 超过一个阈值比如超过一个刻度宽时才重新生成。配合requestAnimationFrame做节流性能上完全没问题。3. 核心交互实现任务条拖拽与时间缩放联动3.1 数据模型设计先想清楚再写代码类甘特图的交互归根结底是两个坐标系之间的映射像素坐标和时间坐标。所以数据模型必须先明确interface Task { id: string name: string rowIndex: number // 左侧表格中的行索引 segments: TaskSegment[] // 一段任务的多个时间段 } interface TaskSegment { id: string taskId: string resourceId: string // 设备/资源ID决定显示在哪个行 startTime: number // 毫秒时间戳 endTime: number status: running | waiting | setup | maintenance }我把任务拆分成Task和TaskSegment两层。一个任务工单可以跨多个资源多段时间所以它不是一个简单的一行一个时间条。真正决定显示在哪一行的是resourceId不是taskId。这意味着渲染时不可能按 task 来循环必须先把所有 segment 按照resourceId分组然后映射到对应行上。3.2 像素与时间的双向映射这是整个组件能够成立的基础。我先定一个规则hourWidth代表一小时的像素宽度它是全局唯一的缩放变量chartStartTime代表时间轴的起始时间通常取所有 segment 的最小时间往前对齐到整点那么任意时间t对应的x坐标就是x (t - chartStartTime) / 3600000 * hourWidth反之任意像素x对应的时间是t chartStartTime x / hourWidth * 3600000看似简单的两个公式但要注意几个边界一是日期跨天时需要处理时区二是在按天显示和按小时显示切换时hourWidth 变了所有已渲染的任务条都要重新计算位置。我的做法是所有视图内数据永远保存时间戳不保存像素坐标像素坐标只在 render 时计算。这样切缩放也好、拖动也好数据模型都干净不会出现座标值被旧缩放污染的脏状态。3.3 任务条拖动的完整流程拖动是类甘特图最常用的交互用户把一个任务段拖到另一个时间段。传统做法是给每个 segment 绑定 mousedown 事件然后监听全局 mousemove 和 mouseup。但我发现直接给每个 segment 绑事件在大数据量下性能不理想而且事件对象在移动过程中不断创建容易引发 GC 压力。我采用的是拖动代理方案鼠标按下 segment 时不直接移动这个 segment而是记录起点startClientX和原始startTime。在mousemove中计算deltaX currentClientX - startClientX再换算成时间偏移deltaTime deltaX / hourWidth * 3600000。生成一个新的拖动预览对象包含临时的时间和坐标渲染在 overlay 层。等mouseup时才真正把新时间写回 store更新这个 segment 的数据。如果存在相邻的同任务段拖动后还要检查是否合并、是否和另一段重叠。为什么用 overlay 而不是直接改真实数据核心原因拖动过程中可能每秒触发 60 次数据更新如果这些更新都进入响应式 store左侧表格、右侧捕捉区域、其他联动组件全部跟着重渲染开销太大。先做视觉预览最后落笔一次这个方案在性能上好了非常多。按住Shift时可禁用吸附移动更加顺滑。默认情况下拖动结束时会吸附到最近的半小时刻度避免出现奇怪的 13:47 这种时间点。3.4 缩放手柄与时长修改任务条左右两端各有一个 6px 宽的 hotzone鼠标移入会变成col-resize光标。按住左端拖动时修改startTime按住右端修改endTime。这里有一个容易踩的坑不能一边拖动一边实时写 store否则每次 render 时会把 handle 的鼠标事件区域也弄乱出现拖到一半事件丢失的现象。所以缩放同样走 overlay 预览只有当 mouseup 时才提交新值。提交时要做合法性检查具体包括新时长不能小于最小粒度比如 30 分钟不能与同一行的其他 segment 重叠如果有依赖关系比如某任务的前置任务结束时间不能晚于后置任务的开始时间这些校验如果放在 UI 层会很冗长我是抽了一个纯函数validateSegmentTime(segments, changedSegment)只做时间逻辑判断不碰任何 DOM。测试起来方便以后接排产算法也不用大量改代码。开始时间2025-06-02 08:00 结束时间2025-06-02 14:30 时长6.5小时 状态生产任务条内部显示这几个字段按状态着色基本就能满足排产视图的信息需求。如果还要显示已生产数量/计划数量可以再加一行小字样式上不复杂有数据源就能接上。4. 数据响应式与状态管理Vue 3 特有的坑4.1 reactive 不是万能的浅层响应式更实用在 Vue 3 里用reactive包裹一个多层嵌套的数组确实方便但代价是深度代理的开销。当 segment 数量到达上千个时reactive对每个属性访问都会走 Proxy 的 get/set 拦截拖拽过程中频繁更新某个 segment 的时间字段会牵连整个响应式系统做大量依赖收集和派发。我的实际经验是甘特图这类高频交互的可视化组件不适合把每一条 segment 都用深层响应式包起来。我是这样处理的用一个shallowRef保存整个数据结构在初始化时把 segments 数组转为一个普通数组对象在拖拽过程中如果要更新某个 segment 的时间不需要让这个 segment 本身是响应式的只需要让外层容器重新渲染即可为了触发外层容器更新用了一个version计数器const store shallowRef({ tasks: [], segments: [], version: 0 }) function updateSegment(seg: TaskSegment) { // 直接改普通对象上的属性 seg.startTime newStart seg.endTime newEnd // 手动递增版本号触发UI更新 store.value.version }视图层里凡是显示 segment 位置的地方都依赖store.value.version版本号一变那段渲染函数就重新执行。这样既保住了数据仍然是响应式驱动外层版本号响应式又避开了深层 Proxy 的性能损耗。如果你觉得这个方案不够标准也可以直接用reactive包住最外层然后把segments数组替换成新的数组引用。注意是替换引用不是原地修改数组元素。Vue 3 的响应式对数组整体替换是很友好的因为触发的粒度比较粗但足够用了。4.2 拖拽过程中的更新节流就算用了浅层响应式mouseup 之前每帧都去触发 render 也没有必要。我的实际做法是mousemove 过程中只更新 overlay 预览层的坐标这段不走 store只操作一个普通的 refmouseup 时才把最终值写进 store触发一次渲染如果 segment 数量不大比如 500 条以内也可以做实时预览每 2~3 帧同步一次这里的关键思想是交互过程中的高频变更走局部状态交互结束后的落库变更走全局状态。这是甘特图这类组件性能调优的最核心原则。4.3 Composition API 封装把数据操作和渲染解耦我用一个useGanttStore的组合函数管理所有数据操作包括getSegmentsByResource(resourceId)moveSegment(segmentId, newStartTime)resizeSegment(segmentId, newStartTime, newEndTime)getVisibleSegments(range)validateSegmentChange(segment, newStart, newEnd)组件内部只依赖这个组合函数暴露的方法渲染层完全不直接操作数据。这样后续如果要把真实接口数据接过来、或者要把时间数据从本地假数据切换为后端返回的排产结果只需要改useGanttStore内部的数据源即可组件和视图层完全不需要动。5. 实测性能瓶颈与渲染优化5.1 渲染方式选 DOM 还是 Canvas画这种类甘特图到底应该用 DOM 还是 Canvas我见过一些方案用 Canvas 画整个时间轴。Canvas 在数据量大但交互简单时性能确实好因为不需要维护无数个 DOM 节点。但一旦面临类似每个任务条要hover显示浮层、点击选中、拖拽移动、不同状态不同颜色、行背景交替这类交互需求时Canvas 的手写事件命中、命中区域判断非常繁琐。DOM 方案不是不行只是不能无脑渲染。我的经验是在可视化区域内最多同时存在的 segment 条数在 1000 个以内普通 DOM 完全扛得住超过这个数才需要考虑 Canvas 或 WebGL 方案。排产调度视图一般来说一个屏幕内能看到的任务条也就是几十到几百条DOM 是性价比最高的。5.2 虚拟滚动只渲染看得见的行时间轴内容如果总共有几千行任务就不可能一次性把所有行都渲染出来否则首屏速度会非常难看。虚拟滚动的核心逻辑不复杂根据scrollTop计算当前可见区域的行区间只渲染这个区间内的任务行行高固定为 48px 的话startIndex Math.floor(scrollTop / 48)endIndex startIndex Math.ceil(viewportHeight / 48) 3上下各留3行缓冲实现虚拟滚动时配合position: relative和transform: translateY(startIndex * 48px)来定位而不是给每一行加top因为 transform 不触发重排性能更好。div classgantt-body scrollhandleScroll div classgantt-track :style{ height: totalHeight px } div classgantt-rows :style{ transform: translateY(${startIndex * rowHeight}px) } div v-forrow in visibleRows :keyrow.id classgantt-row !-- 任务条们 -- /div /div /div /div外层gantt-track撑出真实高度用于模拟滚动条内部只渲染可见行用 transform 定位。这样即使有 5000 行数据DOM 里同时存在的任务行也只有 20 个左右。5.3 右侧 body 滚动时左侧表格如何配合左侧表格的滚动配合本质上也是一个虚拟滚动监听右侧 body-right 的 scrollTop用同一个startIndex计算左侧表格该显示哪个区间的行左侧表格行的数据直接用统一的visibleRows数组因为左右本来就是对同一批数据的不同列展示所以我左侧表格和右侧时间轴共用同一个visibleRows。这样结构上天然不会错位一行数据的左侧表格和右侧任务条永远在同一行。5.4 时间轴的横向虚拟化除了纵向行数多横向的时间刻度也是一样的问题。如果用真实 DOM 把所有时间刻度都渲染出来比如按小时显示渲染 3 个月的时间刻度那就是 2000 多个刻度标签首屏会非常卡。横向同样做虚拟化根据 scrollLeft 计算当前可见的时间区间只生成区间内的刻度节点。刻度节点的间距由缩放级别决定比如按小时缩放小刻度 2 小时一个大刻度 1 天一个按天缩放小刻度 6 小时一个大刻度 1 周一个按周缩放小刻度 1 天一个大刻度 1 个月一个这种多级刻度的好处是用户可以从看清楚每天的时间段切换到看一个月的大概排布这在排产场景里特别实用。横向刻度用 canvas 画可能效率更高但 DOM 也够用因为可见的刻度数量通常在 20~50 个以内浏览器处理这点节点毫无压力。5.5 首屏渲染的另一个大坑图片和字体加载类甘特图项目往往伴随丰富的信息展示任务条里会显示产品图片、状态图标、用户头像等。一旦任务条多起来图片请求会非常多首屏会一直出现闪烁和空白。我的处理策略是任务条缩略图先在列表层面用统一的占位色块等图片加载完成后再替换而不是一开始就塞几十个 img 标签字体图标全部用 SVG 写成内联组件避免额外的字体文件请求大图懒加载只在可视区域内加载图片配合 IntersectionObserver 做懒加载这类优化看似不是甘特图的核心逻辑但真的会直接影响用户体验特别是有大量任务条的调度看板场景。6. 时间轴缩放与滚动的方案落地6.1 缩放级别切换的数据一致性时间轴缩放的实现不能是简单改一个 hourWidth 就完事。切换缩放时有一个关键问题当前视口中心的时间点应该保持不变。举个例子用户现在正盯着 6 月 5 日下午 3 点这个位置从按天视图切到按小时视图那么这个时间点最好还在屏幕中心附近视角不能丢。实现上是在切换前记录centerTime chartStartTime scrollLeft / hourWidth * 3600000切换 hourWidth 后再反算新的 scrollLeftscrollLeft (centerTime - chartStartTime) / 3600000 * newHourWidth - viewportWidth / 2这样视觉上不会唰一下跳到别的地方用户的阅读焦点能稳住。我在实现过程中踩过一个小坑开始做的时候忘了保存中心时间直接重置 scrollLeft 为 0结果每次切换缩放画面就跳到最左边用起来非常反直觉。后来补上中心时间保持逻辑才顺滑。6.2 如何选择缩放粒度的切换阈值所谓缩放不是无限放大要有一个合理的粒度范围和切换策略。我这边提供的档位有按小时一小时宽度 100px适合看某个设备一天的排程细节按4小时一小时宽度 40px比较适查看连续几天按天一小时宽度 15px适合看 2~4 周的整体排布按周一小时宽度不足 5px这时任务条几乎是纯色块只用来做宏观调度同时也支持滚轮 Ctrl/⌘ 滚轮来缩放。滚轮缩放时以鼠标所在位置为锚点做缩放这样用户想放大哪个区域只需要把鼠标放上去滚一下就行体验比点按钮好很多。不过要注意不是所有用户都习惯这个交互不需要的时候别打开免得干扰表格本身的滚动操作。我的做法是鼠标在 body-right 区域内滚轮缩放但如果左键拖拽选中了一个区域就优先执行框选逻辑。7. 实测中的一些小坑与技巧7.1 引用的图表和自定义样式的冲突类甘特图往往会和系统里已存在的 UI 框架样式冲突尤其是 Vue 3 项目里通常都引入了 Element Plus 或 Ant Design Vue。框架自带的全局样式可能会影响甘特图的表格、按钮、滚动条等。我碰到的典型问题是Element Plus 的表格会把 td 的 border 默认设置为1px solid导致我在甘特图左侧表格上自定的边框颜色全部失效。解决很简单给甘特图组件最外层加一个自定义gantt-container的 scoped 样式并提高选择器优先级覆盖掉框架的默认样式。需要注意的一点是不要用 !important 满天飞很容易写坏后续的维护。更好的做法是给甘特图容器加一个独立的 class 命名空间在内部重设样式这样既有隔离性又不会污染全局。7.2 vxe-table 和甘特图的配合热词里提到了 vxe-table我确实尝试过用 vxe-table 做左侧表格因为它的虚拟滚动和行列操作比较强。但实测下来vxe-table 和右侧甘特图的滚动同步偶尔会出现滚动闭环的情况——两个滚动容器互相触发滚动事件导致页面轻微抖动。解决方法是把 vxe-table 的滚动条禁用让表格外层只做静态展示真正滚动只由右侧甘特图区域控制。但这样等于放弃了 vxe-table 自带的一些表格功能最后还是回到自己写表格行的方案。如果你的项目里已经有 vxe-table也不一定非要替换。可以不用 vxe-table 的滚动只拿它当普通表格渲染然后用我们上面对齐方案强制同步滚动逻辑也是可以的。7.3 拖拽中如何优雅地做到不点击时不做操作甘特图区域除了拖拽点击任务条还要弹出详情弹窗所以点击和拖拽的判断很容易冲突。最朴素的方案是mousedown 时记录坐标mouseup 时计算移动距离如果小于 3px 就算点击否则算拖拽。我用了一个 token 方案mousedown 时生成唯一 token并把它存到当前拖拽状态里。mouseup 时检查 token 是否存在且等于当前值才判断是有效的点击。这样能够避免移动过程中 mouseup 事件被其他子组件误消费。这个细节不复杂但直接关系到交互手感建议在做交互时先处理掉这个边界。7.4 依赖关系的绘制类甘特图如果涉及任务依赖一般会在任务条之间画一条带箭头的折线。我这边画依赖线时没有用 SVG而是在任务条底部再加一个 overlay 图层专门用于绘制依赖连线。为什么不用 SVG 覆盖整个甘特图因为时间轴的横向滚动如果靠 transform 移动SVG 里的坐标也需要跟着动容易出误差。overlay 图层和任务条共用同一个坐标系只是多渲染一层绝对定位的 div 或 SVG 元素思路更简单。目前我给依赖线用 SVG 实现因为折线带箭头路径计算方便。横向滚动时整个 SVG 跟着 transform 平移纵向虚拟滚动时只绘制当前可见行的依赖线。实测没什么问题。8. 如果要做成可复用的组件建议预留哪些接口做了这个类甘特图组件后我总结出几个值得预留给外部的接口。一开始我没想清楚后面接数据时改了很多次踩过一遍就觉得这些接口越早设计越好。interface GanttOptions { rowHeight: number startDate: Date endDate: Date hourWidth: number scaleUnit: hour | day | week resources: Resource[] tasks: Task[] onSegmentMove: (segmentId: string, newStart: number, newEnd: number) void onSegmentClick: (segmentId: string, event: MouseEvent) void onCanvasClick: (event: MouseEvent) void }外部只要传入任务和资源数据再传入回调函数组件内部自己维护视图状态不需要外部知道甘特图内部是 DOM 还是 Canvas。这样接后端数据时只需要把接口的返回映射成任务数组不需要修改组件本身。组件划分上我分了两层GanttChart.vue负责外层布局、滚动容器、滚动同步、虚拟渲染GanttTrack.vue负责单行的任务条渲染、拖拽、缩放、选中态这样也可以用defineExpose暴露一些方法给外部比如scrollToTime(time)、zoomIn()、zoomOut()、getSelectedSegment()方便通过 ref 直接调用组件的能力。有些场景比如点击甘特图某个空白时间区域自动创建一个任务需要在 canvas 区域的 mousedown 里判断点击目标是否为空白区域然后把时间坐标换算成时间值触发外部的创建回调。这个交互我一直保留因为排产人员经常直接在甘特图上拖出一个时间窗来新增排程。9. 排产调度场景中的特殊需求9.1 三角模糊加工时间怎么展示在甘特图上如果涉及生产调度里的模糊加工时间甘特图上通常不只是显示一个确定的时间条而是显示一个区间范围。我的做法是一个任务段有 optimistic、most likely、pessimistic 三条时间值。视图上画三个层次外圈浅色半透明条表示 pessimistic 时间范围中间深色条表示 most likely 时间范围内部亮点标记表示 optimistic 时间点这样排产工程师一眼就能看出这个任务的加工时间余量。实际实现时如果让endTime字段直接保存悲观值会导致任务条看起来过长误导后续排产判断。所以我在 segment 上扩展出optimisticTime、likelyTime、pessimisticTime三个字段渲染时按显示模式切换数据层不动。9.2 设备资源的行展示规则设备资源行的展示有个细节如果某台设备当天没有排产任务也要显示一行空行。用户点击空行区域时是在该设备的时间轴上新增任务。如果某台设备有多段任务行内可以不只放 segment还可以扩展出一个小箭头用来展开下一层子任务。实际排产里往往一个设备下面还会再分子工序、子工步但不一定每个都需要画出来。我是通过expandable标志位控制是否显示展开箭头。9.3 生产排程的禁止拖拽场景不是所有任务都允许随意拖动排程。所以每组 segment 上我加了一个lockReason字段如果有值拖拽时显示一个 tooltip 提示为什么锁住不允许移动。这样可以避免已锁定的任务被误操作挪动影响整个排产计划。这个在日常生活中可能用不上但在生产调度里是个大坑直接改时间会导致后面所有任务跟着乱掉。10. 总结与个人看法从筛库、对比到最终决定自己实现整个过程最深的体会是甘特图库不等于类甘特图解决方案。项目管理和生产排产的底层数据结构完全不同前者一行一个任务后者一任务多时间段多资源多状态直接用现成库往往要强行改造数据模型改到最后发现还不如自己画。如果完全从零开发优先考虑以下这些核心点单容器滚动而不是双容器同步数据层永远保存时间戳而不是像素坐标拖拽预览走 overlay 方案mouseup 才落库虚拟滚动同时作用于左侧表格和右侧时间轴缩放时保持中心时间不变按这个顺序一步步做不说能一次完美但主干架构是稳的后续无论是接排产算法还是增加其他交互都不会碰到推倒重来的情况。我个人在实际操作中的偏好是始终把数据操作和视觉渲染解耦得足够干净这样项目后期接真实接口、接后端推送、接排产优化算法时几乎不需要改组件代码只扩展数据层方法就行。类甘特图这种组件前端可能的坑我都踩得差不多了剩下的坑就看你的业务场景能造出什么新需求了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →