ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Vue3+dhtmlx-gantt 企业甘特图:任务依赖、拖拽改期、工时回写与 SPA 单例

Vue3+dhtmlx-gantt 企业甘特图:任务依赖、拖拽改期、工时回写与 SPA 单例 Vue3dhtmlx-gantt 企业甘特图任务依赖、拖拽改期、工时回写与 SPA 单例文档地址https://ruoyioffice.com源码1·GitHubhttps://github.com/yuqing2026/ruoyi-office源码2·GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office源码3·Giteehttps://gitee.com/yqzy1688/ruoyi-office微信17156169080备注「RuoYi Office」一句话甘特图如果只是「把任务画成条」和导出一张 PNG 没有区别。企业要的是拖完计划日期写回project_task连线写回project_task_link切走 Tab 再回来图还在。RuoYi Office 用 dhtmlx-gantt 做渲染Vue3 SPA 里用模块级单例明确禁止destructor()。▲ 左侧 WBS、中间条形与 FS 连线、右侧三类事件回写底部三条护栏永不 destructor、隐藏 Tab 重排尺寸、init 失败降级表格引言画得好看拖完没保存等于没做项目管理里看板已经能改状态甘特图要多解决三件看板不管的事痛点不管会怎样计划起止靠手填项目经理改期改到崩溃Excel 和系统各写各的任务之间有先后只改一条的日期下游任务在图上还停在原地页面是 SPA组件卸载时如果销毁 dhtmlx 工厂下一次进 Tab 直接tasksStore读 undefined任务看板那篇写的是vuedraggable跨列改状态。本文不重复看板只写甘特图这一层数据契约、单例生命周期、拖拽改期、依赖连线、双击改工时、隐藏 Tab 与表格降级。技术栈锚点2026Vue3 TypeScript dhtmlx-gantt后端 Spring Boot 3.5接口前缀/admin-api/project/gantt。一、为什么是 dhtmlx-gantt而不是自己画 SVG结论企业甘特图的交互密度树、拖、拉、连线、中文本地化、智能渲染不适合从零画选库之后真正的工程量在「和任务主数据双向同步」以及「不要在 SPA 里拆掉实例」。自己用 ECharts 自定义 series 能画出条但依赖连线、拖拽改工期、树列和「」新建每一项都要重做一套手势。dhtmlx-gantt 把这些手势包好页面只需要parse({ data, links })灌数据在onAfterTaskDrag/onAfterLinkAdd/onAfterLinkDelete里调自己的 API双击不要走它的默认灯箱改开本系统的任务表单。项目台账详情里甘特图是一个 Tab和看板、任务列表共用同一棵project_task▲ 「软件研发202608」台账详情甘特图 Tab阶段下挂任务条上直接写任务名左侧列是任务名称、开始时间、工期天同一棵 WBS 在「项目任务」工作台里还能看执行明细。预估/实际工时不在拖条上改避免和计划窗口抢字段▲ 左侧 WBS 与甘特图同一批任务需求分析 → 系统设计 → 开发实现 → 测试上线工时与执行明细在这棵树上维护甘特图条长仍由计划起止决定阶段taskType1映射 dhtmlx 的project普通任务task里程碑milestone。父节点用parentId根为 0。这和列表页、看板是同一张表不是甘特图自己再存一份进度。二、数据契约datalinks结论前端认 dhtmlx 的字段名text、start_date、duration、progress0~1后端认业务字段taskName、planStartDate、progress0~100。转换发生在ProjectGanttController不发生在 Vue 里手写一遍。GET /project/gantt/data?projectId → { data: GanttTask[], links: GanttLink[] } PUT /project/gantt/task → 更新 planStartDate / planEndDate / progress POST /project/gantt/link → 插入 project_task_link返回新 id DELETE /project/gantt/link?id读的时候工期天由计划起止的Duration.toDays()算出至少为 1。进度除以 100 再给前端因为 dhtmlx 用 0~1。写回时再乘 100。依赖表project_task_link四个关键字段sourceTaskId、targetTaskId、linkType、lag延迟天数。dhtmlx 默认linkType0 完成-开始FS1 开始-开始SS2 完成-完成FF3 开始-完成SF。创建连线时若源任务查不到projectId会空——正常路径都从已加载的任务上拖出来不会走到这条。权限读数据走validateProjectReadable改任务和改连线要project:task:update。甘特图不是「能看见就能改期」的演示控件。三、SPA 单例为什么绝对不能 destructor结论dhtmlx-gantt 的destructor()会把工厂内部的$data一并拆掉。Vue Router 再次挂载组件时getGanttInstance()拿到的是残体init→_reinit→_update_flags读tasksStore会抛 undefined。这是源码注释里写明的坑不是猜想。对策是模块级变量活过组件生命周期letsharedGantt:anynull;letsharedModule:anynull;letsharedEventIds:any[][];asyncfunctionensureGanttInstance(){if(sharedGantt){returnsharedGantt;}if(!sharedModule){sharedModuleawaitimport(dhtmlx-gantt);}constfactorysharedModule.gantt;sharedGantttypeoffactory?.getGanttInstancefunction?factory.getGanttInstance():factory;returnsharedGantt;}组件卸载时只做三件事代数initGeneration作废进行中的 init、detachEvent清掉自定义监听、clearAll()清空任务。ganttInstance置空表示「当前页面没有挂载」工厂单例sharedGantt继续留着。同一时刻产品里只会有一个甘特图面板可见立项详情和台账详情不会叠两个编辑器所以复用单例不会抢容器。若未来要同屏两个图这条策略要改成「每容器一个 instance仍然禁止 destructor 工厂」。动态import(dhtmlx-gantt)把约 200KB 级的库挡在甘特图路由之外工作台首页不必为它付打包成本。四、拖拽改期条动了计划日期必须落库结论onAfterTaskDrag把 dhtmlx 格式化后的起止时间和 duration、progress 交给PUT /project/gantt/task。后端只更新计划起止和进度不在这条链路改预估工时。gantt.attachEvent(onAfterTaskDrag,async(id:any){consttaskgantt.getTask(id);try{awaitupdateGanttTask({id:task.id,start_date:gantt.templates.format_date(task.start_date),end_date:gantt.templates.format_date(task.end_date),duration:task.duration,progress:task.progress,});emit(changed);}catch{message.error(更新失败);}});后端解析yyyy-MM-dd HH:mm:ss写进planStartDate/planEndDate。progress乘 100 写百分比。前端虽然把duration带上去了更新对象目前没有把「工期天」折成estimatedHours——工期在甘特图上是日期差的展示预估/实际工时在任务表单里维护。双击任务onTaskDblClick返回false挡住 dhtmlx 自带灯箱打开本系统TaskForm。工时、负责人、任务类型在表单保存后再loadData()整表刷新。这就是标题里「工时回写」的落点不是拖条改小时数是拖条改计划窗口小时数走表单两套字段不混。拖坏了当前实现是 toast「更新失败」条仍停在拖后的位置没有自动parse回滚。这是明确边界要做乐观回滚需要在 drag 前缓存起止失败再gantt.getTask(id)写回去并refresh。drag_move/drag_resize/drag_links跟readonly走。台账只读态、流程中的立项查看三个开关全关。五、依赖连线先画后提交失败就删掉结论连线是前端先出现箭头再POST /project/gantt/link。成功则changeLinkId换成数据库 id失败则deleteLink图上不能留下幽灵箭头。gantt.attachEvent(onAfterLinkAdd,async(id:any,link:any){try{constresultawaitcreateGanttLink({source:link.source,target:link.target,type:String(link.type),lag:link.lag||0,});gantt.changeLinkId(id,result);}catch{gantt.deleteLink(id);message.error(创建依赖失败);}});删除对称onAfterLinkDelete调DELETE。没有做「删除失败把线加回来」和拖拽一样属于可演进点。新建任务不走 dhtmlx 默认新增onTaskCreated打开创建弹窗并return false避免图上先冒一条没有后端 id 的空任务。和看板的分工看板拖的是状态列甘特图拖的是时间与依赖。两边都emit(changed)详情页会刷新进度汇总不会各算各的百分比。六、Tab 宽度为 0先 init激活再 setSizes结论Ant Design 的 Tab 默认把隐藏面板留在 DOM 里但宽高为 0。dhtmlx 若只在「第一次可见」时 init用户还没点过甘特图就离开页面单例策略会变复杂。这里选择组件onMounted就 init激活 Tab 时再setSizesrender。ProjectGanttPanel接收active。立项页和台账页都把当前 Tab 是否为甘特图传进来。watchactivenextTick 60ms等 Tab 真正铺开已有实例则safeRender()内部setSizesrender并重新拉数没有实例则走initGantt。smart_rendering打开任务多时只画可视区。中文 localegantt.i18n.setLocale(cn)包在 try 里避免个别构建没有 i18n 包时把整图打挂。initGeneration防止快速切项目旧请求返回时发现 generation 变了直接丢弃避免把 A 项目的任务 parse 进 B 项目的容器。七、三次失败就降级成表格结论甘特图库、容器、CSS 任一环节失败不能让详情页白屏。最多重试 3 次然后fallbackMode用普通表格展示任务名、起止、工期、进度。降级表仍走同一个getGanttData。用户可以点「重试」强制initGantt(true)。失败路径同样不调用destructor避免把工厂污染留给下一次成功。只读空项目没有projectId时展示「请先保存立项基本信息后再查看甘特图」不发起 init。这避免了新建草稿页对projectIdundefined打接口。八、和看板、任务列表如何共用主数据结论甘特图不是第三个任务库。project_task是唯一任务实体project_task_link是甘特图专属的依赖看板不读 links。视图读什么写什么任务列表 / 表单全字段含预估/实际工时工时、负责人、类型看板叶子任务 状态状态、列内排序甘特图名称、计划起止、进度、父子、links计划起止、进度、依赖进度字段要注意量纲库里是 0~100甘特图 0~1看板如果按小数展示要自己换算。甘特图刷新后详情页changed会拉项目进度避免条拖到 80%、页头还写 0%。九、技术亮点设计要点实现价值模块级单例sharedGantt跨组件存活避开 destructor 打坏工厂卸载不销毁只 detach clearAll再次进入 Tab 可 init 到新容器拖拽落库onAfterTaskDrag→ PUT计划日期与系统一致连线先画后提交失败deleteLink不留幽灵箭头双击开业务表单onTaskDblClick返回 false工时、负责人走同一套校验隐藏 Tab 重排active时 setSizes宽 0 容器不会画出一条缝失败降级3 次后表格详情页不白屏按需加载动态 import 库和 CSS非项目页不背体积读写量纲进度 ÷100 / ×100和任务主表百分比对齐十、快速体验在线演示https://ruoyioffice.com/web/账号 admin / admin123进入项目 → 项目台账打开一个进行中的研发项目。切到「甘特图」看左侧 WBS 与右侧条是否对上计划日期。拖一条任务的右沿改工期刷新页面日期应保持。从一条任务拖连线到另一条刷新后箭头还在若接口被权限拦住箭头应消失。双击任务改预估工时并保存甘特图刷新条本身长度仍由计划起止决定。切到「看板」改状态再回甘特图任务还在只是进度/状态以主表为准。源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-office GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office Giteehttps://gitee.com/yqzy1688/ruoyi-office常见问题FAQVue3 里 dhtmlx-gantt 为什么切走页面再回来会报 tasksStore undefined调用了destructor()。它会拆掉工厂内部状态。做法是模块级单例卸载只clearAll和解绑事件下次gantt.init(新容器)。拖拽改的是工时还是计划日期拖条、拉右沿改的是计划起止工期天由日期差展示。预估工时、实际工时在双击打开的任务表单里改不走PUT /project/gantt/task。依赖连线失败为什么箭头会消失onAfterLinkAdd里 POST 失败会gantt.deleteLink(id)。这是为了避免前端 id 和数据库对不上。成功则会changeLinkId换成后端主键删除时才能打到正确的行。甘特图和看板会不会各存一份任务不会。都读project_task。看板不管project_task_link甘特图多读一张依赖表。没点过甘特图 Tab库会不会照样加载面板force-render且onMounted就会动态 import。代价是打开详情即加载库好处是切到 Tab 时不必再等下载。首页、列表页不会加载。结语企业甘特图的工程难点不在「把条画漂亮」而在三件事叠在一起和任务主数据双向同步、依赖连线不能脏写、Vue SPA 不能按文档示例去 destructor。RuoYi Office 的取舍很明确——渲染交给 dhtmlx-gantt持久化交给自己的四个接口生命周期交给模块级单例。看板解决「这件事到哪一步了」甘特图解决「这件事从哪天到哪天、谁必须先做完」。两张图共用一棵任务树才谈得上项目计划能改、能回写、能刷新还在。你们现在的甘特图是导出图片给周会用还是项目经理拖完刷新还在欢迎评论里晒一下踩过的 destructor / 宽为 0 的坑。想要体验 RuoYi Office 的强大功能在线演示https://ruoyioffice.com/web/账号 admin / admin123源码仓库GitHubhttps://github.com/yuqing2026/ruoyi-office GitCodehttps://gitcode.com/zhouzhongyan/ruoyi-office Giteehttps://gitee.com/yqzy1688/ruoyi-office技术咨询添加微信17156169080备注「RuoYi Office」⭐如果觉得不错请给个 Star 支持一下
返回列表