完整的演示源代码可在 GitHub 上获取: https://github.com/DHTMLX/gantt-migrating-from-frappe.
从 Frappe Gantt 迁移到 DHTMLX Gantt
简介
本指南将带您了解将现有应用从 Frappe Gantt 迁移到 DHTMLX Gantt 的过程。我们将涵盖所有必要步骤,包括数据库架构变更、服务器端 API 修改,以及客户端代码更新。
前提条件
在开始迁移之前,请确保你具备:
- 一个正在使用 Frappe Gantt 的现有应用
- 已安装 Node.js (>= 20.0.0)
- 具有 Frappe Gantt 数据结构的 MySQL 数据库
- 对 Express.js 和 JavaScript 有基本了解
步骤 1:数据库迁移
理解当前架构
如果你按照 Frappe Gantt 演示设置,你应该只有一个表:frappe_tasks。
frappe_tasks 表结构:

创建 DHTMLX 表
DHTMLX Gantt 使用两张独立的表:一张用于任务,一张用于依赖链接。请在同一个数据库中创建它们:
USE frappe_dhtmlx;
CREATE TABLE IF NOT EXISTS gantt_tasks (
id VARCHAR(36) NOT NULL DEFAULT (UUID()),
text VARCHAR(255) NOT NULL,
start_date DATETIME NOT NULL,
end_date DATETIME NOT NULL,
duration INT NOT NULL,
progress FLOAT NOT NULL DEFAULT 0,
parent VARCHAR(36) NOT NULL DEFAULT '0',
PRIMARY KEY (id)
);
CREATE TABLE IF NOT EXISTS gantt_links (
id VARCHAR(36) NOT NULL DEFAULT (UUID()),
source VARCHAR(36) NOT NULL,
target VARCHAR(36) NOT NULL,
type VARCHAR(1) NOT NULL,
PRIMARY KEY (id)
);
迁移现有数据
现在将你现有的 Frappe 数据迁移到新的 DHTMLX 表中。
迁移任务(tasks):
INSERT INTO gantt_tasks (id, text, start_date, end_date, duration, progress, parent)
SELECT
id,
name, -- name → text
start, -- start → start_date
end, -- end → end_date
GREATEST(DATEDIFF(end, start), 1), -- 以天为单位的持续时间(最小 1 天)
progress / 100.0, -- 将百分比(0-100)转换为小数(0-1)
'0' -- Frappe 中没有层级结构,所有任务都是根级
FROM frappe_tasks;
你可以验证结果:
SELECT * FROM gantt_tasks;
迁移链接(依赖关系)
在 Frappe Gantt 的数据结构中,依赖关系存储在 frappe_tasks 表的 dependencies 列中,作为字符 串形式。
在 DHTMLX Gantt 中,任务和链接存储在分别的表中。每个链接是一行,字段为:
id- 链接的唯一标识source- 依赖起始任务的 idtarget- 依赖结束的任务的 idtype- 依赖类型:"0"(FS)、"1"(SS)、"2"(FF)、"3"(SF)
由于所有 Frappe 依赖都是 FS,迁移时始终将 type 设置为 "0"。
创建一个 migrate-frappe-to-dhtmlx.js 文件并粘贴以下代码:
import mysql from 'mysql2/promise';
import 'dotenv/config';
const dbConfig = {
host: process.env.HOST,
user: process.env.MYSQL_USER,
password: process.env.PASSWORD,
database: process.env.DATABASE,
};
async function migrateFrappeToDHtmlX() {
let connection;
try {
connection = await mysql.createConnection(dbConfig);
// 查询所有具有依赖关系的任务
const [tasks] = await connection.execute(
'SELECT id, dependencies FROM frappe_tasks WHERE dependencies IS NOT NULL AND dependencies != ""',
);
console.log(`Found ${tasks.length} tasks with dependencies`);
if (tasks.length === 0) {
console.log('No dependencies to migrate.');
return;
}
const links = [];
for (const task of tasks) {
const targetId = task.id;
const dependencies = task.dependencies;
// 以逗号分隔的依赖 ID 拆分
const depIds = dependencies
.split(',')
.map((dep) => dep.trim())
.filter((dep) => dep);
console.log(`\nTask ${targetId} depends on: ${depIds.join(', ')}`);
// 每个依赖关系转化为 Finish-to-Start 链接(type "0")
for (const sourceId of depIds) {
links.push({
source: sourceId,
target: targetId,
type: '0',
});
}
}
if (links.length > 0) {
console.log(`\nInserting ${links.length} links into gantt_links...`);
await connection.beginTransaction();
try {
await connection.execute('DELETE FROM gantt_links');
console.log('Cleared existing links from gantt_links table');
for (const link of links) {
await connection.execute('INSERT INTO gantt_links (source, target, type) VALUES (?, ?, ?)', [
link.source,
link.target,
link.type,
]);
}
await connection.commit();
console.log('Links inserted successfully!');
} catch (error) {
await connection.rollback();
throw error;
}
}
const [insertedLinks] = await connection.execute('SELECT * FROM gantt_links');
console.log(`Total links in gantt_links: ${insertedLinks.length}`);
console.log('\nMigration completed successfully!');
} catch (error) {
console.error('Migration failed:', error.message);
process.exit(1);
} finally {
if (connection) {
await connection.end();
console.log('\nDatabase connection closed.');
}
}
}
migrateFrappeToDHtmlX();
然后在你的 package.json 中添加一个脚本:
{
"scripts": {
"migrate": "node migrate-frappe-to-dhtmlx.js"
}
}
执行迁移:
npm run migrate
你可以验证链接是否正确迁移:
SELECT * FROM gantt_links;
你应该看到每个依赖关系对应一行,并且 source 与 target 的 ID 正确。
将 Frappe 任务字段映射到 DHTMLX Gantt
| Frappe 字段 | DHTMLX 字段 | 说明 |
|---|---|---|
id | id | 任务 ID |
name | text | 任务名称 |
start | start_date | 任务的开始时间 |
end | end_date | 任务的结束时间 |
| (not stored) | duration | 任务持续时间。在 DHTMLX Gantt 中如果未指定,系统会基于 start_date 和 end_date 自动计算 |
progress | progress | Frappe:整数 0–100;DHTMLX:小数 0.0–1.0 |
| (not supported) | parent | Frappe 没有层级关系。在 DHTMLX Gantt 中你可以指定父任务 |
dependencies | (links table) | Frappe 以字符 串形式存储;DHTMLX 使用单独的 gantt_links 表 |
步骤 2:后端迁移(server.js)
删除 Frappe 特定端点与辅助函数
在 Frappe 服务器中,数据加载和任务的 CRUD 操作通过 /data/tasks 进行。删除或替换以下所有内容:
function formatTaskForClient(dbTask)- Frappe 特定的响应格式化函数app.get('/data/tasks', ...)- 返回一个简单的任务对象数组app.post('/data/tasks', ...)- 创建任务;响应返回完整的任务对象app.put('/data/tasks/:id', ...)- 更新任务;响应返回更新后的任务对象app.delete('/data/tasks/:id', ...)- 删除任务;返回 HTTP 204 无主体
安装 DHTMLX Gantt 包
按照 安装指南 安装 DHTMLX Gantt。
本教程中,我们将使用 DHTMLX Gantt 的试用版本:
npm install @dhx/trial-gantt
添加数据加载端点
DHTMLX 期望在一个单独的 GET /data 响应中返回任务和链接,格式为 { tasks: [], links: [] }。
将 Frappe 的 GET /data/tasks 端点替换为:
import dateFormat from 'date-format-lite';
// GET /data - 加载任务和链接
app.get('/data', async (req, res) => {
try {
const [tasks] = await pool.query('SELECT * FROM gantt_tasks ORDER BY start_date');
const [links] = await pool.query('SELECT * FROM gantt_links');
tasks.forEach((task) => {
if (task.start_date) {
task.start_date = task.start_date.format('YYYY-MM-DD hh:mm:ss');
}
if (task.end_date) {
task.end_date = task.end_date.format('YYYY-MM-DD hh:mm:ss');
}
});
res.json({ tasks, links });
} catch (error) {
console.error('Error loading data:', error);
res.status(500).json({ error: 'Failed to load data' });
}
});