dhtmlxGantt 与 Salesforce LWC 集成

本教程将介绍如何将 dhtmlxGantt 集成到 Salesforce Lightning Web Component 中。

如果你使用的是其他技术,可以参考以下集成方案:

集成过程涉及使用 Salesforce CLI 创建 Lightning Web Component 并将其部署到 Salesforce 组织。为了获得更流畅的开发体验,建议在 Visual Studio Code 中安装 Salesforce Extension Pack

完整源码已托管在 GitHub

此外,还提供了视频教程,演示如何使用 Salesforce LWC 构建 Gantt 图表。

前置条件

请确保已经安装了 Salesforce CLI。如未安装,可参考本指南进行安装。

步骤 1. 创建项目

如果还没有开发者账号,可以注册一个免费的开发者账号。具体操作可参考本指南

在 Salesforce 中,使用左侧搜索栏找到并选择 Dev Hub

在打开的设置页面中,启用 Dev Hub 功能:

接下来,为 Salesforce DX 项目创建一个目录:

$ mkdir ~/salesforce

使用 CLI 生成 Salesforce DX 项目:

$ cd ~/salesforce
$ sfdx project generate -n gantt-salesforce-app  
    target dir = C:\Users\User\salesforce
        create gantt-salesforce-app\config\project-scratch-def.json
        create gantt-salesforce-app\README.md
        create gantt-salesforce-app\sfdx-project.json
        create gantt-salesforce-app\.husky\pre-commit
        create gantt-salesforce-app\.vscode\extensions.json
        create gantt-salesforce-app\.vscode\launch.json
        create gantt-salesforce-app\.vscode\settings.json
        create gantt-salesforce-app\force-app\main\default\lwc\.eslintrc.json
        create gantt-salesforce-app\force-app\main\default\aura\.eslintrc.json
        create gantt-salesforce-app\scripts\soql\account.soql
        create gantt-salesforce-app\scripts\apex\hello.apex
        create gantt-salesforce-app\.eslintignore
        create gantt-salesforce-app\.forceignore
        create gantt-salesforce-app\.gitignore
        create gantt-salesforce-app\.prettierignore
        create gantt-salesforce-app\.prettierrc
        create gantt-salesforce-app\jest.config.js
        create gantt-salesforce-app\package.json

进入新创建的项目文件夹:

$ cd gantt-salesforce-app

步骤 2. 授权

使用 Web Server Flow 授权 Org

$ sfdx org login web -d
 
Successfully authorized ... with org ID ...

然后,在项目的配置文件(sfdx-project.json)中,将 "sfdcLoginUrl" 参数设置为组织的 "My Domain URL"。该 URL 可在 "My Domain" 设置页面找到。例如:

gantt-salesforce-app/sfdx-project.json

"sfdcLoginUrl" : "https://xbs2-dev-ed.my.salesforce.com"

使用以下命令创建 Scratch Org:

$ sfdx org create scratch -f config/project-scratch-def.json -d
 
Creating Scratch Org...
RequestId: 2SR5j0000006JhCGAU 
(https://xbsoftware2-dev-ed.my.salesforce.com/2SR5j0000006JhCGAU)
OrgId: 00DH40000000s0D
Username: test-tc0telfqhudt@example.com
✓ Prepare Request
✓ Send Request
✓ Wait For Org
✓ Available
✓ Authenticate
✓ Deploy Settings
Done
 
Your scratch org is ready.

步骤 3. 向 Salesforce 添加 Gantt

要使用该库,需要将其作为静态资源上传到 Salesforce。打开你的 scratch org:

$ sfdx org open

进入 "Static Resources" 标签页,点击 "New" 按钮:

填写清晰的名称(如 "dhtmlxgantt7111"),上传包含库文件(dhtmlxgantt.jsdhtmlxgantt.css)的 ZIP 压缩包,并将 Cache Control 设置为 "Public" 以提升性能。然后保存更改。

现在,dhtmlxGantt 库已经可以在 Salesforce 内部使用。

步骤 4. 创建数据模型

dhtmlxGantt 的主要组件是 Tasks 和 Links。一个实用的处理方式是将它们的属性以 JSON 格式存储在 Salesforce 中。首先为 Tasks 和 Links 创建自定义对象。在 Object Manager 中,选择 "Create",然后选择 "Custom Object":

任务对象(Task object)

为任务对象命名,例如 GanttTaskGanttTasks

请确保记录名称与对象名称一致,例如:

Object Name: GanttTask => Record Name: GanttTask Name

保存新对象。

随后,打开 "Fields & Relationships" 标签页,点击 "New" 添加字段:

  • Duration

选择 "Number" 作为数据类型,然后继续。

将字段命名为 "Duration"。该字段用于存储 JSON 序列化的 Task 属性。持续点击 "Next" 直到出现 "Save & New" 按钮。

接受默认设置,点击 "Next" 直到可以保存或添加新字段。

  • Parent

创建 "Parent" 字段,数据类型选择 "Text"。

继续点击 "Next" 直到可用 "Save & New" 按钮。

  • Progress

添加 "Progress" 字段,数据类型选择 "Number"。

继续点击 "Next" 直到可以保存或添加新字段。

  • Start date

创建 "Start Date" 字段,数据类型选择 "Date/Time"。

点击默认选项,直到出现 "Save" 按钮。

最终,你的对象字段应如下所示:

Link 对象

首先,打开对象管理器,选择“创建”,然后选择“自定义对象”:

将该链接对象命名为 GanttLink/GanttLinks

请确保记录名称与对象名称相对应,例如:

对象名称:GanttLink => 记录名称:GanttLink Name

继续创建所需的字段。

  • Source

添加一个名为“Source”的字段,并选择“文本”作为数据类型。

点击“下一步”(保持默认设置),直到出现“保存并新建”按钮。

  • Target

添加一个名为“Target”的字段,数据类型同样选择“文本”。

点击“下一步”(接受默认设置),直到可以看到“保存并新建”按钮。

  • Type

添加一个名为“Type”的字段,数据类型同样选择“文本”。

点击“下一步”(接受默认设置),直到出现“保存”按钮。

最后,页面应如下所示:

步骤 5. 创建 Lightning Web 组件

要生成 Lightning Web 组件,运行以下命令:

$ sfdx lightning generate component --type lwc -n gantt -d force-app/main/default/lwc
 
target dir = 
C:\Users\User\source\salesforce\gantt-salesforce-app\force-app\main\default\lwc
   create force-app\main\default\lwc\gantt\gantt.js
   create force-app\main\default\lwc\gantt\gantt.html
   create force-app\main\default\lwc\gantt\gantt.js-meta.xml

gantt.js-meta.xml 中更新组件定义,以便在 Lightning App Builder 中暴露该组件:

force-app/main/default/lwc/gantt/gantt.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>54.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
    </targets>
    <targetConfigs>
        <targetConfig targets="lightning__AppPage">
            <property name="height" label="Height" type="Integer" default="800" />
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

打开 gantt.html,插入以下代码:

force-app/main/default/lwc/gantt/gantt.html

<template>
    <div class="thegantt" lwc:dom="manual" style='width: 100%;'></div>
</template>

gantt.js 中,添加如下代码:

force-app/main/default/lwc/gantt/gantt.js

/* eslint-disable guard-for-in */
/* eslint-disable no-undef */
import { LightningElement, api } from "lwc";
import { ShowToastEvent } from "lightning/platformShowToastEvent";
import { loadStyle, loadScript } from "lightning/platformResourceLoader";
import { createRecord, updateRecord, deleteRecord } from "lightning/uiRecordApi";
 
// Static resources
import GanttFiles from "@salesforce/resourceUrl/dhtmlxgantt7111";
 
// Controllers
import getTasks from "@salesforce/apex/GanttData.getTasks";
 
function unwrap(fromSF) {
    const data = fromSF.tasks.map((a) => ({
        id: a.Id,
        text: a.Name,
        start_date: a.Start_Date__c,
        duration: a.Duration__c,
        parent: a.Parent__c,
        progress: a.Progress__c,
        type: a.Task_Type__c,
    }));
    const links = fromSF.links.map((a) => ({
        id: a.Id,
        source: a.Source__c,
        target: a.Target__c,
        type: a.Type__c
    }));
    return { data, links};
}
 
export default class GanttView extends LightningElement {
    static delegatesFocus = true;
 
    @api height;
    ganttInitialized = false;
 
    renderedCallback() {
        if (this.ganttInitialized) {
            return;
        }
        this.ganttInitialized = true;
 
        Promise.all([
            loadScript(this, GanttFiles + "/dhtmlxgantt.js"),
            loadStyle(this, GanttFiles + "/dhtmlxgantt.css")
        ])
            .then(() => {
                this.initializeUI();
            })
            .catch((error) => {
                this.dispatchEvent(
                    new ShowToastEvent({
                        title: "Error loading Gantt",
                        message: error.message,
                        variant: "error"
                    })
                );
            });
    }
 
    initializeUI() {
        const root = this.template.querySelector(".thegantt");
        root.style.height = this.height + "px";
 
        //uncomment the following line if you use the Enterprise or Ultimate version
        //const gantt = window.Gantt.getGanttInstance();
        gantt.templates.parse_date = (date) => new Date(date);
        gantt.templates.format_date = (date) => date.toISOString();
 
        gantt.init(root);
        getTasks().then((d) => {
            const chartData = unwrap(d);
            gantt.parse({
                tasks: chartData.data,
                links: chartData.links
            });
        });
 
        ///↓↓↓ saving changes back to SF backend ↓↓↓
        gantt.createDataProcessor({
            task: {
                create: (data) => {
                    console.log("createTask",data);
                    const insert = {
                        apiName: "GanttTask__c",
                        fields: {
                            Name: data.text,
                            Start_Date__c: data.start_date,
                            Duration__c: data.duration,
                            Parent__c: String(data.parent),
                            Progress__c: data.progress
                        }
                    };
                    gantt.config.readonly = true; // suppress changes
                                                  // until saving is complete  
                    return createRecord(insert).then((res) => {
                        gantt.config.readonly = false;
                        return { tid: res.id, ...res };
                    });
                },
                update: (data, id) => {
                    console.log("updateTask",data);
                    const update = {
                        fields: {
                            Id: id,
                            Name: data.text,
                            Start_Date__c: data.start_date,
                            Duration__c: data.duration,
                            Parent__c: String(data.parent),
                            Progress__c: data.progress
                        }
                    };
                    return updateRecord(update).then(() => ({}));
                },
                delete: (id) => {
                    return deleteRecord(id).then(() => ({}));
                }
            },
            link: {
                create: (data) => {
                    const insert = {
                        apiName: "GanttLink__c",
                        fields: {
                            Source__c: data.source,
                            Target__c: data.target,
                            Type__c: data.type
                        }
                    };
                    return createRecord(insert).then((res) => {
                        return { tid: res.id };
                    });
                },
                update: (data, id) => {
                    const update = {
                        apiName: "GanttLink__c",
                        fields: {
                            Id: id,
                            Source__c: data.source,
                            Target__c: data.target,
                            Type__c: data.type
                        }
                    };
                    return updateRecord(update).then(() => ({}));
                },
                delete: (id) => {
                    return deleteRecord(id).then(() => ({}));
                }
            }
        });
    }
}

步骤 6. 创建 Apex 类

接下来,创建一个类,用于处理 Lightning 组件与数据模型之间的通信。

$ sfdx apex generate class -n GanttData -d force-app/main/default/classes
 
target dir = 
C:\Users\User\salesforce\gantt-salesforce-app\force-app\main\default\classes
   create force-app\main\default\classes\GanttData.cls
   create force-app\main\default\classes\GanttData.cls-meta.xml

创建类后,打开 GanttData.cls 并添加以下代码:

force-app/main/default/classes/GanttData.cls

public with sharing class GanttData {
 
    @RemoteAction
    @AuraEnabled(cacheable=true)
    public static Map<String, Object> getTasks() {
 
        // fetching the Records via SOQL
        List<GanttTask__c> Tasks = new List<GanttTask__c>();
        Tasks = [SELECT Id, Name, Start_Date__c, Duration__c, 
                    Parent__c FROM GanttTask__c];
 
        List<GanttLink__c> Links = new List<GanttLink__c>();
        Links = [SELECT Id, Type__c, Source__c, Target__c FROM GanttLink__c];
 
        Map<String, Object> result = new Map<String, Object>{
            'tasks' => Tasks, 'links' => Links };
        return result;
   }
}

将源代码从 Scratch Org 拉取到你的项目中:

$ sfdx project retrieve start

然后将源代码重新部署到 Scratch Org:

$ sfdx project deploy start

步骤 7. 创建 Lightning 页面

启动“Lightning App Builder”并创建一个新的 Lightning 页面。

选择“App Page”,并填写页面名称和布局。

Gantt 自定义组件现在应可用于新页面。将其添加到任意分区并保存。

激活该页面。

保存你的更改。

打开应用页面。你可以通过在应用启动器中输入 Gantt 来访问它。

如果一切配置正确,Lightning 页面上将显示一个简单的甘特图演示。

应用安全性

Gantt 本身不提供针对 SQL 注入、XSS 或 CSRF 攻击等威胁的内置防护。确保应用安全是开发者的责任。更多详情请参阅相关文档。Salesforce 提供了强大的安全功能来保护你的数据和应用。你也可以根据自己组织的结构和需求定制安全策略。如需进一步指导,请查阅 Salesforce Security Guide。关于 Lightning 组件安全的更多信息,请参阅此处

故障排查

如果你已完成所有步骤,但页面上的甘特图未显示任务和链接,请参阅 Troubleshooting Backend Integration Issues 文章。该文档提供了诊断和解决常见问题的方法。

后续步骤

完成甘特图配置后,你可以在 GitHub 上查看完整代码,支持克隆或下载,用于你的项目。

此外,你还可以查阅涵盖各种甘特功能的指南与其他后端框架集成甘特的教程

Back to top