本示例的完整演示源码可在 GitHub 获取: https://github.com/DHTMLX/gantt-migrating-from-bryntum.
从 Bryntum Gantt 迁移到 DHTMLX Gantt
介绍
Bryntum Gantt 是一个用于项目管理工具的 JavaScript 甘特图组件。
本指南将引导您完成将现有应用从 Bryntum Gantt 迁移到 DHTMLX Gantt 的全过程。我们将覆盖所有必要步骤,包括数据库模式(架构)变更、服务器端 API 修改,以及客户端代码更新。
前提条件
在开始迁移之前,请确保您具备:
- 已经在使用 Bryntum Gantt 的现有可运行应用
- 已安装 Node.js(≥ 20.0.0)
- 具备 Bryntum 数据结构的 MySQL 数据库
- 基本了解 Express.js 和 JavaScript
第一步:数据库迁移
理解当前架构
如果您按照 Bryntum 演示设置进行操作,应该有两张表:tasks 和 dependencies。
tasks 表结构:

dependencies 表结构:

创建 DHTMLX 表
DHTMLX Gantt 使用更简单的数据库结构。创建两张与 DHTMLX Gantt 兼容的新表:
CREATE TABLE `gantt_tasks`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`text` varchar(255) NOT NULL,
`start_date` datetime NOT NULL,
`end_date` datetime NOT NULL,
`duration` int(11) NOT NULL,
`progress` float NOT NULL,
`parent` int(11) NOT NULL,
`constraint_type` varchar(20) DEFAULT 'asap',
`constraint_date` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `gantt_links`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`source` int(11) NOT NULL,
`target` int(11) NOT NULL,
`type` varchar(1) NOT NULL,
PRIMARY KEY (`id`)
);
注:
- 我们添加了
constraint_type和constraint_date字段,以支持带约束的自动调度 — 这是 Bryntum 与 DHTMLX Gantt 都支持的特性。 - 我们添加了
end_date字段,以保留甘特图计算得到的确切结束日期。若不存储end_date,将会基于start_date与duration重新计算,且结果可能因启用的特性(自动调度、工作时间、约束)而不同。存储end_date可以避免丢失这个基本信息。
迁移现有数据
现在将现有的 Bryntum 数据迁移到新的 DHTMLX 表中。
迁移任务(tasks):
INSERT INTO gantt_tasks (
`id`,
`text`,
`start_date`,
`end_date`,
`duration`,
`progress`,
`parent`,
`constraint_type`,
`constraint_date`
)
SELECT
`id`,
`name`, -- 'name' */} 'text'
`startDate`,
DATE_ADD(`startDate`, INTERVAL `duration` DAY), -- 根据 start_date + duration 计算 end_date
`duration`,
`percentDone`, -- 'percentDone' */} 'progress'
IFNULL(`parentId`, 0), -- 'parentId' */} 'parent' (0 表示根任务)
CASE `constraintType` -- 映射约束类型
WHEN 'assoonaspossible' THEN 'asap'
WHEN 'aslateaspossible' THEN 'alap'
WHEN 'startnoearlierthan' THEN 'snet'
WHEN 'startnolaterthan' THEN 'snlt'
WHEN 'finishnoearlierthan' THEN 'fnet'
WHEN 'finishnolaterthan' THEN 'fnlt'
WHEN 'muststarton' THEN 'mso'
WHEN 'mustfinishon' THEN 'mfo'
ELSE 'asap'
END,
`constraintDate`
FROM tasks;
迁移链接(dependencies):
INSERT INTO gantt_links (`id`, `source`, `target`, `type`)
SELECT
`id`,
`fromEvent`, -- 'fromEvent' */} 'source'
`toEvent`, -- 'toEvent' */} 'target'
CASE `type` -- 将 Bryntum 链接类型转换为 DHTMLX 格式
WHEN 0 THEN '1' -- Start-to-Start
WHEN 1 THEN '3' -- Start-to-Finish
WHEN 2 THEN '0' -- Finish-to-Start (最常见)
WHEN 3 THEN '2' -- Finish-to-Finish
ELSE '0' -- 默认为 Finish-to-Start
END
FROM dependencies;
您可以通过执行以下命令来验证数据是否正确迁移:
SELECT * FROM gantt_tasks;
SELECT * FROM gantt_links;
您应该能看到所有任务和链接已正确传输,并且字段映射正确。
Bryntum 任务字段到 DHTMLX Gantt 的映射
Bryntum Gantt 的 TaskModel 包含许多字段,这些字段在 DHTMLX Gantt 中实现方式不同,或在迁移时需要特殊处理。下表解释了将最常用的 Bryntum 任务字段映射到 DHTMLX Gantt 的方式:
| Bryntum Field | Description | Recommended Approach |
|---|---|---|
effort / effortUnit | 完成任务所需的工作量(如 16h),用于基于工作量和资源的调度 | DHTMLX Gantt 不在任务级别支持基于工作量的调度。可改用 Resource Management 模块来分配资源并可视化工作量。资源分配可以表示工作量(如每日小时数),但任务持续时间不会自动重新计算,需手动或通过自定义逻辑管理。 |
durationUnit | 解释任务持续时间的单位(小时、天、周等) | DHTMLX Gantt 使用通过 gantt.config.duration_unit 全局配置的单位。 在迁移时,建议将所有持续时间规范化为单一单位。如需对不同任务使用不同单位(如某些任务为小时,某些为天),可以使用 formatter module。 |
schedulingMode | 定义任务调度行为(Normal、FixedDuration、FixedEffort 等) | 无直接等价。DHTMLX Gantt 不支持按任务的调度模式。可以将该值存储为自定义字段,如有需要,使用 Gantt 事件(如 onBeforeTaskUpdate)强制自定义行为。 |
note | 附加到任务的自由文本备注或描述 | 可直接迁移为自定义文本字段(如 note 或 description),并在 lightbox、tooltip 或自定义网格列中显示。 |
manuallyScheduled | 指示任务是否被排除在自动调度之外 | 可使用任务的 task.auto_scheduling 属性,允许单个任务从全局自动调度中排除,同时全局仍启用。请参阅 禁用特定任务的自动调度。 |
calendar | 分配给任务的日历 | DHTMLX Gantt 支持多日历。可以通过 calendar_id 属性(或由 gantt.config.calendar_property 定义的自定义属性)将日历分配给任务。参阅 为任务分配日历。 |
deadline | 任务不应超过的目标日期 | 通过 task.deadline 属性完全支持。当指定时,DHTMLX Gantt 会在时间线显示可视的截止日期指示。该值使用与 start_date 相同的日期格式。 |
第二步:后端迁移(server.js)
删除 Bryntum 专用代码
首先,从 server.js 中移除 Bryntum 包的服务中间件:
// 删除这行:
app.use(express.static(path.join(__dirname, '/node_modules/@bryntum/gantt')));
重要: 在本示例中将使用的 Vite 不再需要直接提供 node_modules。删除任何暴露整个 node_modules 目录的中间件:
// 如果存在,请删除:
app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));
删除 Bryntum 端点
删除以下 Bryntum 专用端点和帮助函数:
app.get('/load', ...)- Bryntum 数据加载端点app.post('/sync', ...)- Bryntum 同步端点applyTableChanges()函数createOperation()函数updateOperation()函数deleteOperation()函数
安装 DHTMLX Gantt 包和 Vite
移除 Bryntum 依赖项。如果您通过 npm 使用 Bryntum,请卸载它:
npm uninstall @bryntum/gantt
按照 安装指南 安装 DHTMLX Gantt。
在本教程中,我们将使用 DHTMLX Gantt 的试用版:
npm install @dhx/trial-gantt
另外安装 Vite 作为构建工具:
npm install --save-dev vite
添加数据加载端点
添加 GET 端点以加载 DHTMLX 格式的数据:
import dateFormat from 'date-format-lite';
// GET /data - 加载任务和链接
app.get('/data', async (req, res) => {
try {
const [[tasks], [links]] = await Promise.all([
db.query('SELECT * FROM gantt_tasks'),
db.query('SELECT * FROM gantt_links'),
]);
// 为 DHTMLX Gantt 格式格式化日期
for (let i = 0; i < tasks.length; i++) {
tasks[i].start_date = tasks[i].start_date.format("YYYY-MM-DD hh:mm:ss");
tasks[i].end_date = tasks[i].end_date.format("YYYY-MM-DD hh:mm:ss");
// 如果存在 constraint_date,则格式化
if (tasks[i].constraint_date) {
tasks[i].constraint_date = tasks[i].constraint_date.format("YYYY-MM-DD hh:mm:ss");
}
}
res.json({
tasks,
links
});
} catch (error) {
res.status(500).json({
success: false,
message: error.message,
});
}
});
注: 该响应格式与 Bryntum 不同。DHTMLX 期望 { tasks: [], links: [] },而不是 Bryntum 的嵌套结构。
为 Tasks 与 Links 添加 CRUD 端点
DHTMLX Gantt DataProcessor 使用 RESTful 端点。为任务操作添加处理程序:
// 创建新任务
app.post("/data/task", async (req, res) => {
const task = getTask(req.body);
const { text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date } = task;
try {
const [result] = await db.query(
"INSERT INTO gantt_tasks(text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date) VALUES (?,?,?,?,?,?,?,?)",
[text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date]
);
sendResponse(res, "inserted", result.insertId);
} catch (error) {
sendResponse(res, "error", null, error);
}
});
// 更新现有任务
app.put("/data/task/:id", async (req, res) => {
const sid = req.params.id;
const task = getTask(req.body);
const { text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date } = task;
try {
await db.query(
"UPDATE gantt_tasks SET text = ?, start_date = ?, end_date = ?, duration = ?, progress = ?, parent = ?, constraint_type = ?, constraint_date = ? WHERE id = ?",
[text, start_date, end_date, duration, progress, parent, constraint_type, constraint_date, sid]
);
sendResponse(res, "updated");
} catch (error) {
sendResponse(res, "error", null, error);
}
});
// 删除任务
app.delete("/data/task/:id", async (req, res) => {
const sid = req.params.id;
try {
await db.query("DELETE FROM gantt_tasks WHERE id = ?", [sid]);
sendResponse(res, "deleted");
} catch (error) {
sendResponse(res, "error", null, error);
}
});
为链接(依赖)添加处理程序:
// 创建新链接
app.post("/data/link", async (req, res) => {
const link = getLink(req.body);
const { source, target, type } = link;
try {
const [result] = await db.query(
"INSERT INTO gantt_links(source, target, type) VALUES (?,?,?)",
[source, target, type]
);
sendResponse(res, "inserted", result.insertId);
} catch (error) {
sendResponse(res, "error", null, error);
}
});
// 更新现有链接
app.put("/data/link/:id", async (req, res) => {
const sid = req.params.id;
const link = getLink(req.body);
const { source, target, type } = link;
try {
await db.query(
"UPDATE gantt_links SET source = ?, target = ?, type = ? WHERE id = ?",
[source, target, type, sid]
);
sendResponse(res, "updated");
} catch (error) {
sendResponse(res, "error", null, error);
}
});
// 删除链接
app.delete("/data/link/:id", async (req, res) => {
const sid = req.params.id;
try {
await db.query("DELETE FROM gantt_links WHERE id = ?", [sid]);
sendResponse(res, "deleted");
} catch (error) {
sendResponse(res, "error", null, error);
}
});