最佳实践
本文档汇集了 GanttFlow 的工程实践建议,帮助你构建高性能、可维护的甘特图应用。
项目结构
推荐的项目结构
src/
├── components/
│ └── gantt/
│ ├── GanttChart.tsx # 主组件
│ ├── GanttToolbar.tsx # 工具栏
│ ├── TaskDialog.tsx # 任务编辑弹窗
│ ├── ExportPanel.tsx # 导出面板
│ └── types.ts # 类型定义
├── hooks/
│ ├── useGanttTasks.ts # 任务数据管理
│ ├── useGanttDependencies.ts # 依赖关系管理
│ └── useGanttExport.ts # 导出逻辑
├── utils/
│ ├── dateUtils.ts # 日期工具函数
│ └── taskUtils.ts # 任务工具函数
└── App.tsx组件封装示例
tsx
// components/gantt/ProjectGantt.tsx
import React, { useState, useCallback, useMemo } from "react"
import { EnhancedGanttChart } from "@agions/gantt-flow"
import "@agions/gantt-flow/style"
import type { Task, Dependency, ViewMode } from "@agions/gantt-flow"
interface ProjectGanttProps {
projectId: string
tasks: Task[]
dependencies: Dependency[]
onTaskUpdate?: (task: Task) => void
onTaskCreate?: (task: Task) => void
}
export function ProjectGantt({
projectId,
tasks: initialTasks,
dependencies: initialDeps,
onTaskUpdate,
onTaskCreate
}: ProjectGanttProps) {
const [tasks, setTasks] = useState(initialTasks)
const [dependencies] = useState(initialDeps)
const [viewMode, setViewMode] = useState<ViewMode>("week")
const [theme, setTheme] = useState<"light" | "dark">("light")
// 任务更新处理
const handleTaskDrag = useCallback((task: Task, e: MouseEvent, newStart: Date, newEnd: Date) => {
const updatedTask = {
...task,
start: newStart,
end: newEnd
}
setTasks(prev => prev.map(t => t.id === task.id ? updatedTask : t))
onTaskUpdate?.(updatedTask)
}, [onTaskUpdate])
// 进度更新处理
const handleProgressChange = useCallback((task: Task, progress: number) => {
const updatedTask = { ...task, progress }
setTasks(prev => prev.map(t => t.id === task.id ? updatedTask : t))
onTaskUpdate?.(updatedTask)
}, [onTaskUpdate])
// 视图切换
const handleViewChange = useCallback((mode: ViewMode) => {
setViewMode(mode)
}, [])
// 计算统计信息
const stats = useMemo(() => ({
total: tasks.length,
completed: tasks.filter(t => t.progress === 100).length,
inProgress: tasks.filter(t => t.progress > 0 && t.progress < 100).length,
overdue: tasks.filter(t => {
if (!t.end) return false
return new Date(t.end) < new Date() && t.progress < 100
}).length
}), [tasks])
return (
<div className="project-gantt">
{/* 统计信息 */}
<div className="gantt-stats">
<span>总任务: {stats.total}</span>
<span>已完成: {stats.completed}</span>
<span>进行中: {stats.inProgress}</span>
<span>逾期: {stats.overdue}</span>
</div>
{/* 工具栏 */}
<div className="gantt-toolbar">
<select
value={viewMode}
onChange={(e) => handleViewChange(e.target.value as ViewMode)}
>
<option value="day">日</option>
<option value="week">周</option>
<option value="month">月</option>
<option value="quarter">季度</option>
<option value="year">年</option>
</select>
<button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
切换主题
</button>
</div>
{/* 甘特图 */}
<EnhancedGanttChart
tasks={tasks}
dependencies={dependencies}
viewMode={viewMode}
options={{ theme }}
onTaskDrag={handleTaskDrag}
onProgressChange={handleProgressChange}
onViewChange={handleViewChange}
/>
</div>
)
}性能优化
大数据量处理
当任务数量超过 100 时,务必启用虚拟滚动:
tsx
<EnhancedGanttChart
tasks={tasks}
virtualScrolling={true}
visibleTaskCount={50} // 根据屏幕大小调整
bufferSize={10} // 缓冲区大小
/>响应式列宽
tsx
import { useWindowSize } from '@vueuse/core'
function useResponsiveColumnWidth() {
const { width } = useWindowSize()
return computed(() => {
if (width.value < 768) return 40 // 手机
if (width.value < 1024) return 50 // 平板
return 60 // 桌面
})
}
// Vue
const columnWidth = useResponsiveColumnWidth()
// React
const columnWidth = useResponsiveColumnWidth()任务数据优化
tsx
// ❌ 不推荐:每次渲染都创建新对象
function BadExample() {
return (
<EnhancedGanttChart
tasks={[
{ id: "1", name: "任务", start: new Date(), end: laterDate }
]}
/>
)
}
// ✅ 推荐:使用 useMemo 缓存任务数据
function GoodExample() {
const tasks = useMemo(() => [
{ id: "1", name: "任务", start: "2024-01-01", end: "2024-01-10" }
], [])
return <EnhancedGanttChart tasks={tasks} />
}使用 React.memo / Vue shallowRef
tsx
// React
const GanttChart = React.memo(EnhancedGanttChart)
// Vue
const ganttChart = shallowRef(ganttChartRef)状态管理
任务状态管理建议
tsx
// 使用 useReducer 管理复杂状态
type GanttAction =
| { type: 'ADD_TASK'; payload: Task }
| { type: 'UPDATE_TASK'; payload: Task }
| { type: 'DELETE_TASK'; payload: string }
| { type: 'MOVE_TASK'; payload: { taskId: string; newStart: Date; newEnd: Date } }
function ganttReducer(state: GanttState, action: GanttAction): GanttState {
switch (action.type) {
case 'ADD_TASK':
return { ...state, tasks: [...state.tasks, action.payload] }
case 'UPDATE_TASK':
return {
...state,
tasks: state.tasks.map(t =>
t.id === action.payload.id ? action.payload : t
)
}
case 'DELETE_TASK':
return {
...state,
tasks: state.tasks.filter(t => t.id !== action.payload)
}
case 'MOVE_TASK':
return {
...state,
tasks: state.tasks.map(t =>
t.id === action.payload.taskId
? { ...t, start: action.payload.newStart, end: action.payload.newEnd }
: t
)
}
default:
return state
}
}国际化
日期格式本地化
tsx
const locales = {
'zh-CN': {
dateFormat: 'YYYY-MM-DD',
weekNames: ['周日', '周一', '周二', '周三', '周四', '周五', '周六'],
monthNames: ['一月', '二月', '三月', '四月', '五月', '六月',
'七月', '八月', '九月', '十月', '十一月', '十二月']
},
'en-US': {
dateFormat: 'MM/DD/YYYY',
weekNames: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
monthNames: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
}
}错误处理
循环依赖检测
tsx
try {
// 添加依赖前验证
const newDependencies = [...dependencies, newDep]
if (hasCycle(newDependencies)) {
throw new Error('检测到循环依赖,无法添加')
}
setDependencies(newDependencies)
} catch (error) {
console.error('添加依赖失败:', error)
// 显示错误提示
}导出错误处理
tsx
const handleExport = async (format: 'png' | 'pdf' | 'excel') => {
try {
let result
switch (format) {
case 'png':
result = await ganttRef.current?.exportAsPNG()
break
case 'pdf':
result = await ganttRef.current?.exportAsPDF()
break
case 'excel':
result = await ganttRef.current?.exportAsExcel()
break
}
// 处理导出结果
} catch (error) {
console.error('导出失败:', error)
// 显示错误提示给用户
}
}可访问性
键盘导航
tsx
<EnhancedGanttChart
tasks={tasks}
// 确保任务可以通过键盘选中
onTaskClick={(task) => {
// 更新焦点状态
setSelectedTask(task)
}}
/>ARIA 标签
tsx
<div
role="application"
aria-label="项目甘特图"
aria-describedby="gantt-instructions"
>
<span id="gantt-instructions" className="sr-only">
使用方向键在任务间导航,按 Enter 编辑任务
</span>
<EnhancedGanttChart tasks={tasks} />
</div>测试
单元测试
tsx
import { describe, it, expect } from 'vitest'
import { render } from '@testing-library/react'
import { EnhancedGanttChart } from '@agions/gantt-flow'
describe('GanttChart', () => {
it('should render tasks correctly', () => {
const tasks = [
{ id: '1', name: 'Test Task', start: '2024-01-01', end: '2024-01-05' }
]
const { getByText } = render(
<EnhancedGanttChart tasks={tasks} />
)
expect(getByText('Test Task')).toBeInTheDocument()
})
})