Skip to content

最佳实践

本文档汇集了 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()
  })
})

相关资源

基于 MIT 许可证发布