还在用千篇一律的甘特图样式?这个开源库的自定义能力让我惊呆了!
作为前端开发者,最近在做一个内部项目管理工具,UI同学非要让甘特图和产品设计风格完美融合。调研了一圈,发现mzgantt的自定义渲染能力简直强到离谱!
今天就来手把手教你如何用mzgantt打造独一无二的甘特图样式,保你5分钟就能上手!
一、 为什么要自定义渲染?
先看几个真实场景:
- 产品经理:"这个甘特图能不能和我们品牌色一致?"
- UI设计师:"任务状态能不能用不同的视觉表现?"
- 用户体验师:"交互反馈可以更丰富些吗?"
- 技术主管:"需要支持多主题切换功能"
mzgantt一招全搞定!来看看效果对比:
Before:
javascript
arduino
// 默认样式
const gantt = new Gantt('#ganttContainer');
After:
javascript
javascript
// 自定义后
const gantt = new Gantt('#ganttContainer', {
style: {
colors: {
primary: '#722ed1', // 品牌紫色
success: '#13c2c2', // 成功色
warning: '#fa8c16', // 警告色
error: '#eb2f96' // 错误色
}
},
onTaskRender: (task, element) => {
// 各种酷炫的自定义逻辑
}
});
二、 5分钟快速上手
2.1 基础样式定制
最简单的方式是通过CSS变量快速调整:
css
css
/* 在全局样式表中添加 */
:root {
--gantt-primary-color: #722ed1; /* 主色 */
--gantt-success-color: #13c2c2; /* 成功色 */
--gantt-warning-color: #fa8c16; /* 警告色 */
--gantt-error-color: #eb2f96; /* 错误色 */
--gantt-row-height: 28px; /* 行高 */
--gantt-bar-height: 22px; /* 任务条高度 */
--gantt-border-radius: 6px; /* 圆角 */
}
/* 暗色主题 */
[data-theme="dark"] {
--gantt-primary-color: #7c3aed;
--gantt-background-color: #1a1a1a;
--gantt-text-color: #ffffff;
}
2.2 配置项定制
也可以通过JavaScript配置实现:
javascript
go
const gantt = new Gantt('#ganttContainer', {
style: {
layout: {
rowHeight: 28, // 行高
barHeight: 22, // 任务条高度
padding: 4, // 内边距
cornerRadius: 6 // 圆角半径
},
colors: {
primary: '#722ed1', // 品牌主色
success: '#13c2c2',
warning: '#fa8c16',
error: '#eb2f96'
},
animation: {
duration: 300, // 动画时长
easing: 'ease-out' // 缓动函数
}
}
});
三、 高级自定义实战
3.1 自定义任务渲染
来点真正酷炫的!完全控制任务条的渲染:
javascript
ini
gantt.on('beforeTaskRender', (task, element) => {
// 1. 根据状态添加样式
if (task.progress === 100) {
element.classList.add('task-completed');
element.style.opacity = '0.7';
} else if (new Date(task.end) < new Date()) {
element.classList.add('task-overdue');
element.style.border = '2px dashed var(--gantt-error-color)';
}
// 2. 根据优先级设置颜色
const priorityColors = {
critical: '#ff4d4f', // 紧急-红色
high: '#fa8c16', // 高优先级-橙色
medium: '#faad14', // 中优先级-黄色
low: '#52c41a' // 低优先级-绿色
};
if (task.priority && priorityColors[task.priority]) {
element.style.backgroundColor = priorityColors[task.priority];
}
// 3. 添加自定义内容
if (task.important) {
const icon = document.createElement('div');
icon.className = 'task-icon';
icon.innerHTML = '⭐';
icon.style.cssText = `
position: absolute;
top: -8px;
right: -8px;
font-size: 14px;
`;
element.appendChild(icon);
}
// 4. 添加悬停效果
element.style.transition = 'all 0.2s ease';
element.addEventListener('mouseenter', () => {
element.style.transform = 'translateY(-1px)';
element.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
});
element.addEventListener('mouseleave', () => {
element.style.transform = 'translateY(0)';
element.style.boxShadow = 'none';
});
return element;
});
3.2 自定义时间轴
时间轴也可以玩出花样:
javascript
ini
gantt.on('timeAxisRender', (date, element, viewMode) => {
// 周末特殊样式
const dayOfWeek = date.getDay();
if (dayOfWeek === 0 || dayOfWeek === 6) {
element.style.background = 'linear-gradient(45deg, #fff2e8, #ffefe6)';
}
// 今天特殊标记
const today = new Date();
if (date.toDateString() === today.toDateString()) {
element.style.fontWeight = 'bold';
element.style.color = 'var(--gantt-primary-color)';
const marker = document.createElement('div');
marker.style.cssText = `
width: 6px;
height: 6px;
background: var(--gantt-primary-color);
border-radius: 50%;
position: absolute;
bottom: 2px;
left: 50%;
transform: translateX(-50%);
`;
element.appendChild(marker);
}
return element;
});
四、 打造主题系统
4.1 简单主题切换器
html
xml
<!-- 主题切换器 -->
<div class="theme-picker">
<button data-theme="light">🌞 浅色</button>
<button data-theme="dark">🌙 暗色</button>
<button data-theme="corporate">💼 企业</button>
</div>
<style>
.theme-picker {
position: fixed;
top: 20px;
right: 20px;
z-index: 1000;
}
.theme-picker button {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 6px;
cursor: pointer;
margin: 0 4px;
}
.theme-picker button:hover {
background: #f5f5f5;
}
</style>
<script>
// 主题配置
const themes = {
light: {
primary: '#1890ff',
background: '#ffffff',
text: '#262626'
},
dark: {
primary: '#177ddc',
background: '#141414',
text: '#f0f0f0'
},
corporate: {
primary: '#722ed1',
background: '#f9f0ff',
text: '#262626'
}
};
// 主题切换
document.querySelectorAll('.theme-picker button').forEach(btn => {
btn.addEventListener('click', () => {
const theme = btn.dataset.theme;
applyTheme(themes[theme]);
localStorage.setItem('gantt-theme', theme);
});
});
function applyTheme(theme) {
const root = document.documentElement;
Object.entries(theme).forEach(([key, value]) => {
root.style.setProperty(`--gantt-${key}-color`, value);
});
}
// 加载保存的主题
const savedTheme = localStorage.getItem('gantt-theme') || 'light';
applyTheme(themes[savedTheme]);
</script>
4.2 高级主题管理器
javascript
kotlin
class ThemeManager {
constructor(gantt) {
this.gantt = gantt;
this.themes = new Map();
this.currentTheme = 'light';
this.init();
}
init() {
this.registerThemes();
this.setupThemeSwitcher();
this.loadSavedTheme();
}
registerThemes() {
// 注册主题
this.themes.set('light', {
name: '浅色主题',
colors: {
primary: '#1890ff',
background: '#ffffff',
text: '#262626',
border: '#f0f0f0'
}
});
this.themes.set('dark', {
name: '暗色主题',
colors: {
primary: '#177ddc',
background: '#1f1f1f',
text: '#f0f0f0',
border: '#434343'
}
});
}
switchTheme(themeName) {
const theme = this.themes.get(themeName);
if (!theme) return;
this.currentTheme = themeName;
this.applyTheme(theme);
this.saveTheme(themeName);
// 触发主题切换事件
this.gantt.emit('themeChange', { theme: themeName, config: theme });
}
applyTheme(theme) {
const root = document.documentElement;
// 应用CSS变量
Object.entries(theme.colors).forEach(([key, value]) => {
root.style.setProperty(`--gantt-${key}-color`, value);
});
// 更新甘特图配置
this.gantt.updateConfig({
style: {
colors: theme.colors
}
});
}
setupThemeSwitcher() {
// 创建主题切换UI
const switcher = document.createElement('div');
switcher.className = 'theme-switcher';
switcher.innerHTML = `
<select>
${Array.from(this.themes.entries()).map(([key, theme]) =>
`<option value="${key}">${theme.name}</option>`
).join('')}
</select>
`;
document.body.appendChild(switcher);
switcher.querySelector('select').addEventListener('change', (e) => {
this.switchTheme(e.target.value);
});
}
saveTheme(themeName) {
localStorage.setItem('gantt-theme', themeName);
}
loadSavedTheme() {
const savedTheme = localStorage.getItem('gantt-theme');
if (savedTheme && this.themes.has(savedTheme)) {
this.switchTheme(savedTheme);
}
}
}
// 使用
const gantt = new Gantt('#ganttContainer');
new ThemeManager(gantt);
五、 性能优化技巧
5.1 渲染性能优化
javascript
ini
// 防抖渲染
function debounceRender(gantt, delay = 16) {
let timeoutId;
const originalRender = gantt.render.bind(gantt);
gantt.render = function() {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
originalRender();
}, delay);
};
}
// 使用
debounceRender(gantt);
// 虚拟滚动(大数据量时)
class VirtualScroll {
constructor(gantt) {
this.gantt = gantt;
this.visibleRange = { start: 0, end: 50 };
this.setupVirtualScroll();
}
setupVirtualScroll() {
this.gantt.container.addEventListener('scroll', () => {
this.updateVisibleRange();
});
}
updateVisibleRange() {
const scrollTop = this.gantt.container.scrollTop;
const containerHeight = this.gantt.container.clientHeight;
const rowHeight = parseInt(getComputedStyle(document.documentElement)
.getPropertyValue('--gantt-row-height') || '28');
const start = Math.floor(scrollTop / rowHeight);
const end = start + Math.ceil(containerHeight / rowHeight) + 10; // 缓冲10行
if (start !== this.visibleRange.start || end !== this.visibleRange.end) {
this.visibleRange = { start, end };
this.renderVisibleTasks();
}
}
}
5.2 内存优化
javascript
javascript
// 缓存渲染结果
const renderCache = new Map();
gantt.on('beforeTaskRender', (task, element) => {
const cacheKey = `${task.id}-${task.version || '0'}`;
// 检查缓存
if (renderCache.has(cacheKey)) {
return renderCache.get(cacheKey).cloneNode(true);
}
// ...正常渲染逻辑
// 缓存结果
renderCache.set(cacheKey, element.cloneNode(true));
return element;
});
// 定期清理缓存
setInterval(() => {
const now = Date.now();
for (const [key, value] of renderCache.entries()) {
if (now - value.timestamp > 60000) { // 1分钟过期
renderCache.delete(key);
}
}
}, 30000);
六、 实战案例分享
6.1 项目状态可视化
javascript
less
// 根据项目状态渲染不同样式
gantt.on('beforeTaskRender', (task, element) => {
// 清除现有状态类
element.className = element.className.replace(/\bstatus-\w+/g, '');
// 添加状态类
const status = this.getTaskStatus(task);
element.classList.add(`status-${status}`);
// 添加状态指示器
if (!element.querySelector('.status-indicator')) {
const indicator = document.createElement('div');
indicator.className = 'status-indicator';
element.prepend(indicator);
}
return element;
});
// CSS样式
.status-delayed {
background: linear-gradient(45deg, #ff4d4f, #ff7875);
color: white;
}
.status-completed {
background: linear-gradient(45deg, #52c41a, #73d13d);
color: white;
}
.status-progress {
background: linear-gradient(45deg, #1890ff, #69c0ff);
color: white;
}
6.2 高级动画效果
javascript
javascript
// 添加任务动画
class TaskAnimator {
constructor(gantt) {
this.gantt = gantt;
this.setupAnimations();
}
setupAnimations() {
// 任务出现动画
gantt.on('afterTaskRender', (task, element) => {
this.animateTaskAppear(element);
});
// 任务更新动画
gantt.on('taskUpdate', (task, oldData) => {
this.animateTaskUpdate(task, oldData);
});
}
animateTaskAppear(element) {
element.animate([
{ opacity: 0, transform: 'translateY(20px)' },
{ opacity: 1, transform: 'translateY(0)' }
], {
duration: 300,
easing: 'ease-out'
});
}
animateTaskUpdate(task, oldData) {
const element = document.querySelector(`[data-task-id="${task.id}"]`);
if (element) {
element.animate([
{ backgroundColor: 'var(--gantt-warning-color)' },
{ backgroundColor: getComputedStyle(element).backgroundColor }
], {
duration: 500,
easing: 'ease-out'
});
}
}
}
七、 总结与建议
mzgantt自定义渲染的优势:
- ✅ 极致灵活:从颜色到布局完全可控
- ✅ 性能优秀:智能渲染优化,大数据量也不卡顿
- ✅ 易于使用:简单的API,快速上手
- ✅ 扩展性强:支持插件和主题系统
实战建议:
- 先规划后实现:提前设计好主题和样式规范
- 性能优先:大数据量一定要用虚拟滚动
- 保持一致性:确保自定义样式符合产品设计语言
- 渐进增强:先从基础定制开始,逐步添加高级功能
避坑指南:
- 🚫 不要过度定制影响性能
- 🚫 避免使用太多重绘操作
- 🚫 注意浏览器兼容性
- 🚫 记得处理移动端适配
下一步探索:
- 尝试创建自己的主题插件
- 探索更多交互动画效果
- 学习如何优化渲染性能
- 参与开源社区贡献
讨论话题:
你在项目中有哪些酷炫的自定义效果?遇到了哪些挑战?欢迎在评论区分享你的经验和作品!