<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<title>高级甘特图 - 资源管理与日程分析</title>
<script src="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.js"></script>
<link href="https://cdn.dhtmlx.com/gantt/edge/dhtmlxgantt.css" rel="stylesheet">
<!-- 引入Chart.js用于数据显示 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style type="text/css">
html, body {
height: 100%;
padding: 0px;
margin: 0px;
overflow: hidden;
font-family: Arial, sans-serif;
}
.layout {
display: flex;
height: 100vh;
}
.sidebar {
width: 300px;
background: #f5f5f5;
border-right: 1px solid #ddd;
padding: 10px;
overflow-y: auto;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
}
.toolbar {
padding: 10px;
background: #f0f0f0;
border-bottom: 1px solid #ddd;
display: flex;
gap: 10px;
}
.view-tabs {
display: flex;
background: #e0e0e0;
}
.tab {
padding: 10px 20px;
cursor: pointer;
border-right: 1px solid #ccc;
}
.tab.active {
background: #fff;
font-weight: bold;
}
.gantt-container {
flex: 1;
}
.analysis-panel {
padding: 15px;
background: #fff;
border-top: 1px solid #ddd;
max-height: 250px;
overflow-y: auto;
}
.chart-container {
height: 200px;
margin-top: 10px;
}
.resource-item {
padding: 8px;
margin: 5px 0;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
}
.resource-item.selected {
background: #e3f2fd;
border-color: #2196f3;
}
.resource-load {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
margin-top: 5px;
overflow: hidden;
}
.resource-load-bar {
height: 100%;
background: #4caf50;
border-radius: 10px;
}
.overloaded {
background: #f44336;
}
</style>
</head>
<body>
<div class="layout">
<div class="sidebar">
<h3>资源管理</h3>
<div id="resources-list"></div>
<button onclick="showAddResourceForm()">添加资源</button>
<h3>资源负载</h3>
<div id="resource-load-chart" class="chart-container"></div>
<h3>项目进度</h3>
<div id="progress-analysis">
<div>总任务: <span id="total-tasks">0</span></div>
<div>已完成: <span id="completed-tasks">0</span></div>
<div>进行中: <span id="inprogress-tasks">0</span></div>
<div>未开始: <span id="notstarted-tasks">0</span></div>
<div>总体进度: <span id="overall-progress">0%</span></div>
</div>
</div>
<div class="main-content">
<div class="toolbar">
<button onclick="switchView('gantt')">甘特图视图</button>
<button onclick="switchView('resources')">资源视图</button>
<button onclick="loadAnalysis()">刷新分析</button>
<button onclick="showResourceLoad()">资源负载分析</button>
</div>
<div class="view-tabs">
<div class="tab active" data-view="gantt">任务视图</div>
<div class="tab" data-view="resources">资源分配</div>
</div>
<div class="gantt-container">
<div id="gantt_here" style='width:100%; height:100%;'></div>
</div>
<div class="analysis-panel">
<h4>资源负载分析</h4>
<div id="load-analysis-chart" class="chart-container"></div>
</div>
</div>
</div>
<!-- 添加资源的模态框 -->
<div id="resource-modal" style="display:none; position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); background:white; padding:20px; border:1px solid #ccc; z-index:1000;">
<h3>添加资源</h3>
<div>
<label>名称: <input type="text" id="resource-name"></label>
</div>
<div>
<label>邮箱: <input type="email" id="resource-email"></label>
</div>
<div>
<label>角色: <input type="text" id="resource-role"></label>
</div>
<div>
<label>容量(%): <input type="number" id="resource-capacity" min="1" max="100" value="100"></label>
</div>
<div>
<button onclick="addResource()">保存</button>
<button onclick="document.getElementById('resource-modal').style.display='none'">取消</button>
</div>
</div>
<script type="text/javascript">
// 初始化配置
gantt.config.date_format = "%Y-%m-%d %H:%i:%s";
gantt.config.order_branch = true;
gantt.config.order_branch_free = true;
gantt.config.work_time = true;
// 启用资源管理插件
gantt.plugins({
resource: true
});
// 配置资源视图
gantt.config.resource_property = "resources";
gantt.config.resource_store = "resources";
gantt.config.show_resources = true;
// 初始化甘特图
gantt.init("gantt_here");
// 配置灯光框(编辑表单)以支持资源分配
gantt.config.lightbox.sections = [
{name: "description", height: 70, map_to: "text", type: "textarea", focus: true},
{name: "time", height: 72, map_to: "auto", type: "duration"},
{name: "resources", height: 50, map_to: "resources", type: "resources",
options: gantt.serverList("resources")}
];
// 加载数据
gantt.load("/data");
// 初始化数据处理器
const dp = new gantt.dataProcessor("/data");
dp.init(gantt);
dp.setTransactionMode("REST");
// 视图切换
function switchView(viewType) {
if (viewType === "resources") {
gantt.config.show_resources = true;
gantt.render();
} else {
gantt.config.show_resources = false;
gantt.render();
}
}
// 加载资源列表
function loadResources() {
fetch("/data/resources")
.then(response => response.json())
.then(resources => {
const list = document.getElementById("resources-list");
list.innerHTML = "";
resources.forEach(resource => {
const item = document.createElement("div");
item.className = "resource-item";
item.innerHTML = `
<strong>${resource.name}</strong> (${resource.role})
<div>容量: ${resource.capacity}%</div>
<div class="resource-load">
<div class="resource-load-bar" style="width: ${Math.min(resource.capacity, 100)}%"></div>
</div>
`;
item.onclick = () => showResourceDetails(resource.id);
list.appendChild(item);
});
});
}
// 显示添加资源表单
function showAddResourceForm() {
document.getElementById("resource-modal").style.display = "block";
}
// 添加资源
function addResource() {
const name = document.getElementById("resource-name").value;
const email = document.getElementById("resource-email").value;
const role = document.getElementById("resource-role").value;
const capacity = document.getElementById("resource-capacity").value;
fetch("/data/resource", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ name, email, role, capacity })
})
.then(response => response.json())
.then(data => {
if (data.action === "inserted") {
document.getElementById("resource-modal").style.display = "none";
loadResources();
// 刷新资源列表
gantt.updateCollections();
}
});
}
// 加载分析数据
function loadAnalysis() {
// 加载项目进度分析
fetch("/data/progress-analysis")
.then(response => response.json())
.then(analysis => {
document.getElementById("total-tasks").textContent = analysis.total_tasks;
document.getElementById("completed-tasks").textContent = analysis.completed_tasks;
document.getElementById("inprogress-tasks").textContent = analysis.in_progress_tasks;
document.getElementById("notstarted-tasks").textContent = analysis.not_started_tasks;
document.getElementById("overall-progress").textContent = analysis.overall_progress.toFixed(2) + "%";
});
// 加载资源负载分析
showResourceLoad();
}
// 显示资源负载分析
function showResourceLoad() {
const today = new Date();
const startDate = new Date(today.getFullYear(), today.getMonth(), 1);
const endDate = new Date(today.getFullYear(), today.getMonth() + 1, 0);
const formatDate = date => date.toISOString().split('T')[0];
fetch(`/data/resource-load?start_date=${formatDate(startDate)}&end_date=${formatDate(endDate)}`)
.then(response => response.json())
.then(data => {
renderResourceLoadChart(data);
});
}
// 渲染资源负载图表
function renderResourceLoadChart(data) {
// 这里简化处理,实际应该使用Chart.js创建详细图表
const ctx = document.getElementById('load-analysis-chart').getContext('2d');
// 按资源分组数据
const resources = {};
data.forEach(item => {
if (!resources[item.name]) {
resources[item.name] = [];
}
resources[item.name].push({
date: item.date,
load: item.assigned_workload
});
});
// 创建图表
new Chart(ctx, {
type: 'bar',
data: {
labels: Object.keys(resources),
datasets: [{
label: '资源负载',
data: Object.values(resources).map(resource =>
resource.reduce((sum, day) => sum + day.load, 0) / resource.length
),
backgroundColor: Object.values(resources).map(resource => {
const avgLoad = resource.reduce((sum, day) => sum + day.load, 0) / resource.length;
return avgLoad > 100 ? '#f44336' : '#4caf50';
})
}]
},
options: {
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '负载 (%)'
}
}
}
}
});
}
// 初始化页面
document.addEventListener("DOMContentLoaded", function() {
// 加载资源列表
loadResources();
// 加载分析数据
loadAnalysis();
// 设置标签切换
document.querySelectorAll(".tab").forEach(tab => {
tab.addEventListener("click", function() {
document.querySelectorAll(".tab").forEach(t => t.classList.remove("active"));
this.classList.add("active");
switchView(this.getAttribute("data-view"));
});
});
// 设置定期刷新
setInterval(loadAnalysis, 60000); // 每分钟刷新一次分析数据
});
</script>
</body>
</html> const express = require('express');
const bodyParser = require('body-parser');
const path = require('path');
const Promise = require('bluebird');
require("date-format-lite");
const port = 1337;
const app = express();
app.use(express.static(path.join(__dirname, "public")));
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json()); // 添加JSON解析中间件
// 允许跨域
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
// 数据库配置
const mysql = require('promise-mysql');
async function serverСonfig() {
const db = await mysql.createPool({
host: 'localhost',
user: 'root',
password: 'root123',
database: 'gantt_howto_node'
});
// 获取任务数据(包含资源信息)
app.get("/data", async (req, res) => {
try {
const [tasks, links, resources, assignments] = await Promise.all([
db.query("SELECT * FROM gantt_tasks ORDER BY sortorder ASC"),
db.query("SELECT * FROM gantt_links"),
db.query("SELECT * FROM gantt_resources"),
db.query("SELECT * FROM gantt_assignments")
]);
// 处理任务数据
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].open = true;
// 添加资源分配信息
const taskAssignments = assignments.filter(a => a.task_id == tasks[i].id);
tasks[i].resources = taskAssignments.map(a => a.resource_id);
}
res.send({
data: tasks,
collections: {
links: links,
resources: resources
}
});
} catch (error) {
sendResponse(res, "error", null, error);
}
});
// 获取资源负载数据
app.get("/data/resource-load", async (req, res) => {
try {
const { start_date, end_date } = req.query;
const resourceLoad = await db.query(`
SELECT
r.id,
r.name,
r.capacity,
DATE(d.date) as date,
COALESCE(SUM(a.units * t.duration / 8), 0) as assigned_workload
FROM gantt_resources r
CROSS JOIN (
SELECT DATE_ADD(?, INTERVAL seq.seq DAY) as date
FROM (
SELECT (a.N + b.N * 10) seq
FROM (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a
CROSS JOIN (SELECT 0 AS N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b
) seq
WHERE DATE_ADD(?, INTERVAL seq.seq DAY) <= ?
) d
LEFT JOIN gantt_assignments a ON a.resource_id = r.id
LEFT JOIN gantt_tasks t ON t.id = a.task_id AND d.date BETWEEN t.start_date AND DATE_ADD(t.start_date, INTERVAL t.duration DAY)
WHERE d.date BETWEEN ? AND ?
GROUP BY r.id, d.date
ORDER BY r.id, d.date
`, [start_date, start_date, end_date, start_date, end_date]);
res.json(resourceLoad);
} catch (error) {
sendResponse(res, "error", null, error);
}
});
// 获取项目进度分析
app.get("/data/progress-analysis", async (req, res) => {
try {
const analysis = await db.query(`
SELECT
COUNT(*) as total_tasks,
SUM(CASE WHEN progress = 1 THEN 1 ELSE 0 END) as completed_tasks,
SUM(CASE WHEN progress > 0 AND progress < 1 THEN 1 ELSE 0 END) as in_progress_tasks,
SUM(CASE WHEN progress = 0 THEN 1 ELSE 0 END) as not_started_tasks,
AVG(progress) * 100 as overall_progress
FROM gantt_tasks
WHERE parent = 0
`);
res.json(analysis[0]);
} catch (error) {
sendResponse(res, "error", null, error);
}
});
// 资源管理API
app.get("/data/resources", async (req, res) => {
try {
const resources = await db.query("SELECT * FROM gantt_resources");
res.json(resources);
} catch (error) {
sendResponse(res, "error", null, error);
}
});
app.post("/data/resource", async (req, res) => {
try {
const { name, email, role, capacity } = req.body;
const result = await db.query(
"INSERT INTO gantt_resources (name, email, role, capacity) VALUES (?, ?, ?, ?)",
[name, email, role, capacity]
);
sendResponse(res, "inserted", result.insertId);
} catch (error) {
sendResponse(res, "error", null, error);
}
});
app.put("/data/resource/:id", async (req, res) => {
try {
const { name, email, role, capacity } = req.body;
await db.query(
"UPDATE gantt_resources SET name = ?, email = ?, role = ?, capacity = ? WHERE id = ?",
[name, email, role, capacity, req.params.id]
);
sendResponse(res, "updated");
} catch (error) {
sendResponse(res, "error", null, error);
}
});
app.delete("/data/resource/:id", async (req, res) => {
try {
await db.query("DELETE FROM gantt_resources WHERE id = ?", [req.params.id]);
sendResponse(res, "deleted");
} catch (error) {
sendResponse(res, "error", null, error);
}
});
// 资源分配API
app.post("/data/assignment", async (req, res) => {
try {
const { task_id, resource_id, units } = req.body;
const result = await db.query(
"INSERT INTO gantt_assignments (task_id, resource_id, units) VALUES (?, ?, ?)",
[task_id, resource_id, units]
);
sendResponse(res, "inserted", result.insertId);
} catch (error) {
sendResponse(res, "error", null, error);
}
});
app.delete("/data/assignment/:id", async (req, res) => {
try {
await db.query("DELETE FROM gantt_assignments WHERE id = ?", [req.params.id]);
sendResponse(res, "deleted");
} catch (error) {
sendResponse(res, "error", null, error);
}
});
// 保留你原有的任务和链接API...
// [你原有的任务和链接API代码在这里]
function sendResponse(res, action, tid, error) {
if (action == "error") {
console.log(error);
res.status(500).send({ action: "error", message: error.message });
} else {
let result = { action: action };
if (tid !== undefined && tid !== null) result.tid = tid;
res.send(result);
}
}
function getTask(data) {
return {
text: data.text,
start_date: data.start_date.date("YYYY-MM-DD"),
duration: data.duration,
progress: data.progress || 0,
parent: data.parent
};
}
function getLink(data) {
return {
source: data.source,
target: data.target,
type: data.type
};
}
}
serverСonfig();
app.listen(port, () => {
console.log("Server is running on port " + port + "...");
});这些代码有问题http://127.0.0.1:1337/data/resource-load?start_date=2025-07-31&end_date=2025-08-30 404 (Not Found)
showResourceLoad @ (index):306
loadAnalysis @ (index):295
setInterval
(anonymous) @ (index):378
VM1868:1 Uncaught (in promise) SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
VM1869:1 Uncaught (in promise) SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON