Vue3项目中实现甘特图的完整方案:从需求分析到自研轻量组件
去年给一家做排产系统的公司做前端支撑需求里有一项非常显眼要在新升级的 Vue3 应用里展示一张“类似甘特图”的图表用来把工序、设备和时间轴画在一块让车间调度员一眼看出瓶颈在哪儿。我一开始想得简单找个体积不大的图表库npm 装完就结束了。可真正动手才发现“类甘特图”和“甘特图”之间差着一大截就算把现成库塞进去想跟自家业务字段、交互习惯对齐还是得自己动不少手术。这篇文章不打算写成某个商业库的广告教程我就把自己在 Vue3 项目里折腾甘特图的完整过程拆开从需求界定、第三方库选型到手写一个轻量甘特图的思路再到后面的排障细节。适合正在 Vue3 项目里开发排期视图、任务看板或生产调度图表的同学参考也适合被产品经理一句“搞个甘特图呗”砸到头上的前端朋友直接抄作业。1. 先把“类甘特图”的需求盘清楚再决定方案很多需求方自己也没想清楚要什么开口就是“和 Project 的那个甘特图一样”但这套组件的边界往往非常模糊。如果一开始就盲目选库后面大概率会返工。我建议先花半天时间把需求压缩成几个具体的问题再决定走哪条技术路线。1.1 甘特图的核心构成要素不只是几根横条拆开看一张可用的甘特图至少包含这些要素左侧任务列表任务名称、负责人、进度百分比右侧时间轴按天、周、月或小时刻度以及时间轴上的任务条。任务条的两个端点分别对应开始时间、结束时间条形的宽度就是持续时长。更完整的场景还会包含进度填充、里程碑节点、依赖连线、分组折叠和资源泳道。“类甘特图”的“类”字很关键它意味着可以实现全部也可以只实现一部分。比如生产调度场景下大家最关心的是排产任务在哪些设备上占用了哪几天是否撞车而项目协作场景下大家更关心依赖关系和关键路径。这两类需求画出来的界面虽然都叫甘特图但底层数据模型完全不一样所以必须先拆需求。我整理了一个自我提问清单每次接需求都会先过一遍时间精度是什么按整天、半天还是精确到小时任务条上要展示哪些字段名称、进度、负责人还是自定义标签是否允许拖动任务条直接改工期改完要不要触发后端保存需不需要在任务之间画依赖线依赖线是单向还是双向是否要处理跨天、跨月甚至跨年的长任务有多少任务量几十条、几百条还是超过两千条这些问题直接决定了后续的技术方案。如果只需要展示再大的库都是浪费如果需要拖拽编辑自绘方案至少要额外写几百行交互代码。1.2 需求边界决定技术路线展示、编辑还是排程按使用深度我把甘特图需求分成四个档位第一档是展示型。任务数据和工期从接口一次性拉下来用户只能看不能改。这种场景最轻量你甚至不需要引入任何图表库纯 CSS 加绝对定位就能完成。第二档是交互型。用户能拖动任务条调整时间点击任务弹出编辑面板可能还要支持缩放时间轴。这种场景可以考虑开源组件做二次开发也可以自绘只要做好坐标换算和事件监听。第三档是排程型。涉及依赖关系、资源冲突检测、自动排布、前置任务后置任务联动。这种场景已经超出“图表”范畴更像一个小型算法引擎通常需要在前端维护一套任务调度数据结构。第四档是复杂业务型。在排程基础上还要支持多项目横跨、权限控制、操作历史回放、甘特图与表格联动编辑。这时候我一般都建议直接评估商业库因为自己从零写全功能成本太高。从我的实际经验看很多需求方口中“搞个类似甘特图的图表”落到细节上其实只是第一档或第二档。如果能在一开始就把这些边界聊清楚技术选型会非常从容。1.3 方案选型现成库、开源组件还是自绘做前端的人很容易陷入“什么都要自己造轮子”和“什么都要找现成库”两个极端。甘特图恰好是一个居中场景既有成熟方案可用又有足够空间做简化自绘。下面是当时我做的选型对比方案功能完整度定制自由度维护成本适合场景商业库如 dhtmlx-gantt很高低到中需要授权但省心大型项目管理系统、复杂排程开源组件vue-ganttastic、frappe-gantt中中依赖社区版本迭代要盯快速交付、通用看板自绘方案表格 CSS/SVG低到中很高代码自己维护业务特殊、展示为主我最后实际落地选择了自绘方案主要是因为业务里有个“设备泳道”的概念任务必须按设备分组而且同一设备上的多个任务不能重叠。现有开源组件的资源和泳道概念跟这个模型匹配度不高强行适配反而要写一堆补丁。但这不代表自绘一定最优后面我会专门用一个小节介绍 vue-ganttastic 的接入方法给不同场景留一条快速路线。2. 用现成库快速落地vue-ganttastic 接入实录偶尔项目时间紧或者需求方“就要这种现成效果”我也会直接上开源组件。Vue3 生态里比较顺手的轻量甘特图组件是 vue-ganttastic虽然 stars 不算多但胜在专门为 Vue3 设计体积小依赖也干净。这里把接入过程和一些坑位一起记录下来。2.1 安装和基础接入vue-ganttastic 内部大量使用 dayjs所以项目里最好统一安装同一版本的 dayjs避免 npm 重复安装导致类型或实例不兼容。npm install dayjs vue-ganttastic安装完成后在入口文件里注册import { createApp } from vue import App from ./App.vue import VueGanttastic from vue-ganttastic import vue-ganttastic/dist/style.css createApp(App).use(VueGanttastic).mount(#app)组件名称就叫ganttastic。最小可用模板大概是这样的template ganttastic :taskstasks :optionsoptions dragendonDragEnd timechangeonTimeChange / /template script setup import { ref } from vue const tasks ref([ { id: 1, name: 需求分析, start: 2025-03-01, end: 2025-03-05, progress: 100, type: task }, { id: 2, name: UI设计, start: 2025-03-03, end: 2025-03-10, progress: 60, type: task } ]) const options { start: 2025-03-01, end: 2025-03-31, rowHeight: 44, bar: { height: 28 } } function onDragEnd(task) { // task 是拖拽结束后的任务数据 console.log(task) } /script这样就能跑起来一个最基础的甘特图。组件的样式默认相对简洁时间轴会按月、日自动生成刻度任务条会展示名称和进度。2.2 数据模型和关键配置项用这个库的时候最需要理解的是 task 字段和 options 的关系。task 基本上沿用常见甘特图的数据结构start和end可以是字符串或 Date 对象但建议统一用 dayjs 兼容的字符串格式避免不同浏览器对日期解析的差异。type字段可以区分普通任务、里程碑、摘要任务默认值最好都先给上。options 里比较重要的是时间范围。组件不会自动根据任务值推断时间轴范围必须手动指定start和end。如果第一天有任务第二天也有任务但时间轴范围写窄了任务条就会被截断。所以做查询条件时时间轴起点最好往前多留一天终点往后多留一天给视觉留出呼吸感。rowHeight会影响每个任务行的高度同时也影响整体滚动。bar对象里可以配置任务条的高度、圆角、背景色和进度条颜色。想要让不同状态的设备任务显示不同颜色可以在 task 里加一个color字段然后在 options 里用barStyle回调返回样式。一个常见的坑是官方文档里没有明确说明进度字段必须是 0 到 100 的数值如果漏配或传了undefined组件会直接认为进度是 0任务条看起来就像没有内容。初始化数据时需要统一做一次清洗把缺失字段补上默认值。2.3 接入时踩过的几个坑第一是 dayjs 版本冲突。公司项目是一个 pnpm monorepovue-ganttastic 的 peerDependency 里带了 dayjs但根项目又单独安装了另一个版本。尽管多数情况下功能没问题可在计算任务宽度时偶尔会出现 dayjs 实例不是同一个构造函数导致的异常。解决办法是在根package.json里把 dayjs 统一提升到 1.11.x并打开 pnpm 的shamefully-hoist或配置 public-hoist-pattern。这个坑排查了一下午最后是看运行时错误堆栈才定位到。第二是拖拽事件的数据回写。dragend事件拿到的 task 对象看起来是响应式的但在某些版本里它是组件内部缓存的克隆对象直接 push 回原数组会导致视图不更新。更稳妥的做法是在事件回调里根据task.id找到原始任务再把start、end、progress更新到原对象上。我一般会写一个updateTask(payload)函数统一收口。第三是样式覆盖不稳定。组件内部的类名没有暴露给使用者想微调某些颜色只能靠:deep选择器硬覆盖一旦升级小版本就可能失效。如果只改任务条高度、圆角、字体优先利用 options 里暴露的 CSS 变量不要手写一大段:deep样式。这样至少升级时不会碎。如果你只是想快速搭一个内部工具vue-ganttastic 完全够用。但要注意它本质上是一个轻量开源项目很多高级能力需要自己补比如拖拽创建新任务、依赖线绘制、虚拟滚动。一旦需求超过它的能力边界及时转自绘更舒服。3. 轻量自绘方案一张表加一段绝对定位如果业务排期逻辑特殊或者你只是想做一个不依赖任何图表库的“类甘特图”自绘方案可能比调库更省心。很多朋友听到“自绘”就紧张其实甘特图本质上就是一个二维坐标映射问题任务开始时间决定横坐标任务所在行决定纵坐标时长决定宽度。3.1 布局思路左侧任务表格和右侧时间轴解耦自绘甘特图最常用的布局是左表右图。左侧是一个普通的任务列表展示任务名称、负责人、进度。右侧是时间轴和任务条区域。这两块必须分开因为右侧要横向滚动左侧要保持固定视觉上才能形成“表跟图联动”。我习惯的 DOM 结构是div classgantt-wrapper div classgantt-left div classgantt-left-header任务名称/div div classgantt-left-body div v-fortask in tasks :keytask.id classgantt-task-row {{ task.name }} /div /div /div div classgantt-right refrightRef div classgantt-timeline-header !-- 时间刻度 -- /div div classgantt-content-area div classgantt-content :stylecontentStyle div v-fortask in tasks :keytask.id classgantt-bar-row div classgantt-bar :stylegetBarStyle(task) {{ task.name }} /div /div /div /div /div /div右侧的.gantt-content区域宽度由时间轴总天数决定所有任务条都放在里面做绝对定位。为了实现表头固定和内容滚动.gantt-right再拆成上下两块上面是表头下面是可以横向和纵向滚动的容器。两个滚动容器之间通过scroll事件同步位置防止左右两侧出现行错位。需要注意的一点是左侧表格的行高和右侧任务条行的行高必须完全一致。如果左侧用了border-bottom右侧每行也要加同样的边框并且设置box-sizing: border-box否则积累下来几十行以后左右内容就完全对不齐了。3.2 任务条坐标计算和拖拽更新坐标计算是整个自绘方案的核心。我把时间轴的最小单位设成“天”然后用 dayjs 来计算任务起始日和结束日相差多少天。这里强烈不建议用new Date()相减再做毫秒换算因为会遇到时区和夏令时问题尤其在跨月份、跨年的时候很容易差几个小时。统一用 dayjs 的diff方法按day单位取整最稳妥。核心代码可以封装成一个组合式函数// useGantt.js import { computed } from vue import dayjs from dayjs export function useGantt(tasks, timelineStart, timelineEnd, dayWidth) { const totalDays computed(() { return dayjs(timelineEnd.value).diff(dayjs(timelineStart.value), day) 1 }) const contentWidth computed(() { return totalDays.value * dayWidth.value px }) function getBarStyle(task) { const start dayjs(task.start) const end dayjs(task.end) const left start.diff(dayjs(timelineStart.value), day) * dayWidth.value const width end.diff(start, day) * dayWidth.value dayWidth.value return { left: left px, width: Math.max(width, 20) px, top: task.rowIndex * rowHeight px } } return { contentWidth, getBarStyle } }代码里width多加了一个dayWidth是因为结束日期也占用完整的一格。很多人第一次画的时候都会漏掉这一格导致任务条看起来比实际少了一天。拖拽更新则是在任务条上监听mousedown记录鼠标按下时的横坐标和任务原始开始日期。移动时计算deltaX / dayWidth得到要偏移的天数再更新任务的start和end。这里直接用 dayjs 的add方法精度按天取整即可。一个注意事项是拖拽过程中要调用requestAnimationFrame批量更新防止频繁操作响应式数据导致卡顿。拖拽结束后的边界判断也不能少任务不能拖到时间轴范围之外否则看起来就像消失了一样。还要判断是否与其他任务重叠至少在排产类业务里这个校验是刚需。3.3 时间缩放、依赖连线和扩展思路时间轴缩放是甘特图的高频交互。实现起来也不复杂我把dayWidth做成响应式变量通过鼠标滚轮控制。当用户按住 Ctrl 键滚动时将dayWidth在 24、36、48、72、96 这几个档位之间切换。因为整个布局是用 CSS 计算出来的所以只需要调整这一个变量所有任务条、表头刻度会自动跟着变化。为了避免滚轮缩放时页面也跟着滚动需要在监听事件里调用preventDefault()同时把监听器绑定到图表容器而不是全局节点。这里踩过一个小坑如果绑在window上鼠标在图表区域外滚动也会触发缩放体验非常奇怪。依赖连线可以用 SVG 来做。在甘特图表区域加一个position: absolute且铺满整个内容区的svg把任务条的左右锚点换算成 SVG 坐标系里的坐标然后画折线。折线方向一般是终点在右侧起点在左侧连线从左侧任务条的右边中点出发横向先向右偏移一段再纵向折到右侧任务条的左边中点。这样视觉上像一条“鱼骨线”不会横穿无关任务时显得杂乱。如果要做里程碑只需把任务条换成菱形或圆形节点位置同样基于开始时间和所在行计算。如果要做资源泳道在任务分行的基础上再加一层设备维度每个设备占一个泳道任务条在泳道内按顺序排列。这些扩展都是在坐标体系上做文章并不需要引入复杂依赖这也是我偏爱自绘的原因。4. 甘特图开发中的高频问题和排查技巧自绘甘特图和一个现成库的差距往往不是画出来的效果而是各种极端情况。下面的问题都是我在实际开发中反复遇到的专门拿出来盘一盘方便你少走弯路。4.1 日期边界和时区导致的错位最常见的问题是任务条偏移一天。我排查过不下三次最后发现都是日期解析的锅。用new Date(2025-03-01)解析在某些浏览器会按 UTC 时间处理而本地时间又是东八区结果会变成2025-03-01 08:00导致日期对象在计算时差时少算若干小时。后来我把所有日期处理全部统一到 dayjs并且操作粒度只到“天”。举例来说如果任务是2025-03-01到2025-03-03持续时长应该按两天算还是三天算会直接影响条形宽度。我这边统一默认结束日期当天包含在工作量里所以在计算宽度时end.diff(start, day)之后必须再1否则 3 月 1 日到 3 月 3 日的任务会被画成只有两天宽。4.2 滚动同步和位置漂移左表右图布局最让人头疼的就是左右滚动不同步。如果右侧容器用了overflow: auto左侧表格又没加position: sticky一滚动就会看到左侧内容不受控制地跑掉。我的解决方式是左侧表格的头部固定左侧表格的内容部分根据右侧scrollTop赋值偏移量用transform: translateY(-scrollTop)来实现同步。同时两个容器必须使用同一个滚动条或通过事件联动避免头部和表体错位超过 1px。还有一种更隐蔽的漂移右侧图表的任务条区域外层如果有padding或border在计算坐标时会把宽度挤掉。最好把任务条定位放在一个没有内边距的子容器里所有坐标计算都以内容区左上角为原点。4.3 大量任务渲染性能甘特图任务量超过两千条以后纯 DOM 渲染会明显卡顿尤其每次拖动或缩放时都会触发全量重排。这里有几个优化方向。第一是可视区渲染也就是虚拟滚动只渲染当前滚动窗口内的任务条这个收益最大。实现时需要计算每个任务条所在的纵向位置再根据滚动偏移量过滤渲染列表。第二是尽量用transform代替left和width的频繁修改左侧依然可以使用绝对定位但拖拽过程中的即时位移用translateX来完成能减少重排。第三是数据结构设计甘特图任务数组最好用shallowRef配合不可变更新避免深度响应式遍历带来的开销。如果只是展示几百条任务其实不需要做太复杂的优化。但一旦上了排产调度这类几千条数据的场景不优化很难交得了差。4.4 问题排查速查表这里把上面提到的坑整理成一张速查表遇到问题可以快速对照问题现象常见原因处理建议任务条比实际少一天结束日期未加一天宽度计算时end.diff(start, day) 1任务条偏移一天日期被当成 UTC 解析统一使用 dayjs解析格式为YYYY-MM-DD左右区域滚动错位左侧没有同步 scrollTop监听右侧滚动给左侧加 translateY拖拽后任务时间没变化事件里改的是克隆对象根据 ID 查找原始任务再更新依赖线画在横条后面SVG 图层级不够给 SVG 设置更高的 z-index超过 1000 条任务卡顿每帧全量重绘 DOM做虚拟滚动拖拽时使用 translateX这张表基本覆盖了我自己开发时遇到的大多数问题。遇到新的奇怪现象我一般会先打开 Vue Devtools检查任务数据是否已正确更新再检查样式计算是否准确。甘特图这种可视化组件数据层和渲染层分离得越彻底问题越好定位。最后说点题外话。如果你现在也在评估要不要自己画我的经验是核心业务逻辑越复杂、UI 越有行业特色越值得自绘越接近通用项目管理工具越值得用现成方案或商业库。前端不该被“甘特图”三个字吓住它本质上就是一个受数据驱动的坐标定位问题。真正难的往往是后续那些不见得画在页面上的逻辑比如拖拽后的冲突校验、后端持久化、多人协同时的并发更新。这些内容每一样都可以单独写一篇今天这篇文章能帮你在脑海里搭起一个框架后面再遇到具体场景咱们再逐个击破。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →