Preview Updated 2026-05-10

TimelineGantt

Horizontal time axis + multi-row task bars — today marker, weekend tint, drag to move / resize, dependency arrows, group sections.

Basic usage

rows is the array of task rows; each row’s bars is the list of time spans on that row. start / end set the visible range, day-width controls the pixel width of one day, and today is marked with a vertical accent line.

背景 视口
src/App.vue
<script setup lang="ts">
import { CfTimelineGantt, type GanttRow } from '@chufix-design/vue';

const today = new Date();
function iso(offset: number): string {
  const d = new Date(today);
  d.setDate(d.getDate() + offset);
  return d.toISOString().slice(0, 10);
}

const rows: GanttRow[] = [
  {
    id: 'design',
    label: '设计稿',
    bars: [{ id: 'd1', label: '高保真', start: iso(-3), end: iso(2), progress: 0.6 }],
  },
  {
    id: 'frontend',
    label: '前端开发',
    bars: [{ id: 'f1', label: '组件实现', start: iso(0), end: iso(8) }],
  },
  {
    id: 'backend',
    label: '后端接口',
    bars: [
      { id: 'b1', label: 'API 联调', start: iso(2), end: iso(7), color: 'oklch(70% 0.13 175)' },
    ],
  },
  {
    id: 'qa',
    label: '测试 / 上线',
    bars: [
      { id: 'q1', label: '回归', start: iso(8), end: iso(11), color: 'oklch(74% 0.16 80)' },
      { id: 'q2', label: '上线', start: iso(12), end: iso(13), color: 'oklch(64% 0.18 30)' },
    ],
  },
];
</script>
<template>
  <CfTimelineGantt
    :rows="rows"
    :start="iso(-7)"
    :end="iso(15)"
    :day-width="28"
  />
</template>
<script setup>
import { CfTimelineGantt } from '@chufix-design/vue';

const today = new Date();
function iso(offset): string {
  const d = new Date(today);
  d.setDate(d.getDate() + offset);
  return d.toISOString().slice(0, 10);
}

const rows= [
  {
    id: 'design',
    label: '设计稿',
    bars: [{ id: 'd1', label: '高保真', start: iso(-3), end: iso(2), progress: 0.6 }],
  },
  {
    id: 'frontend',
    label: '前端开发',
    bars: [{ id: 'f1', label: '组件实现', start: iso(0), end: iso(8) }],
  },
  {
    id: 'backend',
    label: '后端接口',
    bars: [
      { id: 'b1', label: 'API 联调', start: iso(2), end: iso(7), color: 'oklch(70% 0.13 175)' },
    ],
  },
  {
    id: 'qa',
    label: '测试 / 上线',
    bars: [
      { id: 'q1', label: '回归', start: iso(8), end: iso(11), color: 'oklch(74% 0.16 80)' },
      { id: 'q2', label: '上线', start: iso(12), end: iso(13), color: 'oklch(64% 0.18 30)' },
    ],
  },
];
</script>
<template>
  <CfTimelineGantt
    :rows="rows"
    :start="iso(-7)"
    :end="iso(15)"
    :day-width="28"
  />
</template>
import { CfTimelineGantt } from '@chufix-design/react';

export default function Demo() {
  const today = new Date();
  function iso(offset: number): string {
    const d = new Date(today);
    d.setDate(d.getDate() + offset);
    return d.toISOString().slice(0, 10);
  }

  const rows: GanttRow[] = [
    {
      id: 'design',
      label: '设计稿',
      bars: [{ id: 'd1', label: '高保真', start: iso(-3), end: iso(2), progress: 0.6 }],
    },
    {
      id: 'frontend',
      label: '前端开发',
      bars: [{ id: 'f1', label: '组件实现', start: iso(0), end: iso(8) }],
    },
    {
      id: 'backend',
      label: '后端接口',
      bars: [
        { id: 'b1', label: 'API 联调', start: iso(2), end: iso(7), color: 'oklch(70% 0.13 175)' },
      ],
    },
    {
      id: 'qa',
      label: '测试 / 上线',
      bars: [
        { id: 'q1', label: '回归', start: iso(8), end: iso(11), color: 'oklch(74% 0.16 80)' },
        { id: 'q2', label: '上线', start: iso(12), end: iso(13), color: 'oklch(64% 0.18 30)' },
      ],
    },
  ];
  return (
    <>
      <CfTimelineGantt rows={rows} start={iso(-7)} end={iso(15)} dayWidth={28} />
    </>
  );
}
import { CfTimelineGantt } from '@chufix-design/react';

export default function Demo() {
  const today = new Date();
  function iso(offset): string {
    const d = new Date(today);
    d.setDate(d.getDate() + offset);
    return d.toISOString().slice(0, 10);
  }

  const rows= [
    {
      id: 'design',
      label: '设计稿',
      bars: [{ id: 'd1', label: '高保真', start: iso(-3), end: iso(2), progress: 0.6 }],
    },
    {
      id: 'frontend',
      label: '前端开发',
      bars: [{ id: 'f1', label: '组件实现', start: iso(0), end: iso(8) }],
    },
    {
      id: 'backend',
      label: '后端接口',
      bars: [
        { id: 'b1', label: 'API 联调', start: iso(2), end: iso(7), color: 'oklch(70% 0.13 175)' },
      ],
    },
    {
      id: 'qa',
      label: '测试 / 上线',
      bars: [
        { id: 'q1', label: '回归', start: iso(8), end: iso(11), color: 'oklch(74% 0.16 80)' },
        { id: 'q2', label: '上线', start: iso(12), end: iso(13), color: 'oklch(64% 0.18 30)' },
      ],
    },
  ];
  return (
    <>
      <CfTimelineGantt rows={rows} start={iso(-7)} end={iso(15)} dayWidth={28} />
    </>
  );
}

Editable (drag + resize)

With editable, bars become draggable; left/right handles appear for one-sided resize. Mutations are surfaced via the @bar-change event with { bar, row, next: { start, end }, meta } so the host writes them back into its own model.

背景 视口
src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { CfTimelineGantt, toast, type GanttRow } from '@chufix-design/vue';

function iso(offset: number): string {
  const d = new Date();
  d.setDate(d.getDate() + offset);
  return d.toISOString().slice(0, 10);
}

const rows = ref<GanttRow[]>([
  {
    id: 'r1',
    label: '里程碑 A',
    bars: [{ id: 'a1', label: '设计', start: iso(-2), end: iso(3), progress: 0.4 }],
  },
  {
    id: 'r2',
    label: '里程碑 B',
    bars: [{ id: 'b1', label: '开发', start: iso(2), end: iso(8) }],
  },
  {
    id: 'r3',
    label: '里程碑 C',
    bars: [{ id: 'c1', label: '验收', start: iso(8), end: iso(12), color: 'oklch(74% 0.16 80)' }],
  },
]);

function onChange(payload: {
  bar: { id: string };
  next: { start: Date; end: Date };
}) {
  for (const row of rows.value) {
    const b = row.bars.find((x) => x.id === payload.bar.id);
    if (b) {
      b.start = payload.next.start.toISOString().slice(0, 10);
      b.end = payload.next.end.toISOString().slice(0, 10);
    }
  }
  toast({
    type: 'info',
    message: `${payload.bar.id}: ${payload.next.start.toISOString().slice(5, 10)} → ${payload.next.end.toISOString().slice(5, 10)}`,
  });
}
</script>
<template>
  <p style="margin: 0 0 8px; color: var(--fg-3); font-size: 12px;">
    拖拽条体移动;拖拽左/右边缘缩放(每格 = 1 天)。
  </p>
  <CfTimelineGantt
    :rows="rows"
    :start="iso(-7)"
    :end="iso(15)"
    :day-width="28"
    editable
    @bar-change="onChange"
  />
</template>
<script setup>
import { ref } from 'vue';
import { CfTimelineGantt, toast } from '@chufix-design/vue';

function iso(offset): string {
  const d = new Date();
  d.setDate(d.getDate() + offset);
  return d.toISOString().slice(0, 10);
}

const rows = ref<GanttRow[]>([
  {
    id: 'r1',
    label: '里程碑 A',
    bars: [{ id: 'a1', label: '设计', start: iso(-2), end: iso(3), progress: 0.4 }],
  },
  {
    id: 'r2',
    label: '里程碑 B',
    bars: [{ id: 'b1', label: '开发', start: iso(2), end: iso(8) }],
  },
  {
    id: 'r3',
    label: '里程碑 C',
    bars: [{ id: 'c1', label: '验收', start: iso(8), end: iso(12), color: 'oklch(74% 0.16 80)' }],
  },
]);

function onChange(payload: {
  bar: { id: string };
  next: { start: Date; end: Date };
}) {
  for (const row of rows.value) {
    const b = row.bars.find((x) => x.id === payload.bar.id);
    if (b) {
      b.start = payload.next.start.toISOString().slice(0, 10);
      b.end = payload.next.end.toISOString().slice(0, 10);
    }
  }
  toast({
    type: 'info',
    message: `${payload.bar.id}: ${payload.next.start.toISOString().slice(5, 10)} → ${payload.next.end.toISOString().slice(5, 10)}`,
  });
}
</script>
<template>
  <p style="margin: 0 0 8px; color: var(--fg-3); font-size: 12px;">
    拖拽条体移动;拖拽左/右边缘缩放(每格 = 1 天)。
  </p>
  <CfTimelineGantt
    :rows="rows"
    :start="iso(-7)"
    :end="iso(15)"
    :day-width="28"
    editable
    @bar-change="onChange"
  />
</template>
import { useState } from 'react';
import { CfTimelineGantt } from '@chufix-design/react';

export default function Demo() {
  function iso(offset: number): string {
    const d = new Date();
    d.setDate(d.getDate() + offset);
    return d.toISOString().slice(0, 10);
  }

  const [rows, setRows] = useState<GanttRow[]>([
    {
      id: 'r1',
      label: '里程碑 A',
      bars: [{ id: 'a1', label: '设计', start: iso(-2), end: iso(3), progress: 0.4 }],
    },
    {
      id: 'r2',
      label: '里程碑 B',
      bars: [{ id: 'b1', label: '开发', start: iso(2), end: iso(8) }],
    },
    {
      id: 'r3',
      label: '里程碑 C',
      bars: [{ id: 'c1', label: '验收', start: iso(8), end: iso(12), color: 'oklch(74% 0.16 80)' }],
    },
  ]);

  function onChange(payload: {
    bar: { id: string };
    next: { start: Date; end: Date };
  }) {
    for (const row of rows) {
      const b = row.bars.find((x) => x.id === payload.bar.id);
      if (b) {
        b.start = payload.next.start.toISOString().slice(0, 10);
        b.end = payload.next.end.toISOString().slice(0, 10);
      }
    }
    toast({
      type: 'info',
      message: `${payload.bar.id}: ${payload.next.start.toISOString().slice(5, 10)} → ${payload.next.end.toISOString().slice(5, 10)}`,
    });
  }
  return (
    <>
      <p style={{ margin: "0 0 8px", color: "var(--fg-3)", fontSize: 12 }}>
          拖拽条体移动;拖拽左/右边缘缩放(每格 = 1 天)。
        </p>
        <CfTimelineGantt rows={rows} start={iso(-7)} end={iso(15)} dayWidth={28} editable onBarChange={onChange} />
    </>
  );
}
import { useState } from 'react';
import { CfTimelineGantt } from '@chufix-design/react';

export default function Demo() {
  function iso(offset): string {
    const d = new Date();
    d.setDate(d.getDate() + offset);
    return d.toISOString().slice(0, 10);
  }

  const [rows, setRows] = useState<GanttRow[]>([
    {
      id: 'r1',
      label: '里程碑 A',
      bars: [{ id: 'a1', label: '设计', start: iso(-2), end: iso(3), progress: 0.4 }],
    },
    {
      id: 'r2',
      label: '里程碑 B',
      bars: [{ id: 'b1', label: '开发', start: iso(2), end: iso(8) }],
    },
    {
      id: 'r3',
      label: '里程碑 C',
      bars: [{ id: 'c1', label: '验收', start: iso(8), end: iso(12), color: 'oklch(74% 0.16 80)' }],
    },
  ]);

  function onChange(payload: {
    bar: { id: string };
    next: { start: Date; end: Date };
  }) {
    for (const row of rows) {
      const b = row.bars.find((x) => x.id === payload.bar.id);
      if (b) {
        b.start = payload.next.start.toISOString().slice(0, 10);
        b.end = payload.next.end.toISOString().slice(0, 10);
      }
    }
    toast({
      type: 'info',
      message: `${payload.bar.id}: ${payload.next.start.toISOString().slice(5, 10)} → ${payload.next.end.toISOString().slice(5, 10)}`,
    });
  }
  return (
    <>
      <p style={{ margin: "0 0 8px", color: "var(--fg-3)", fontSize: 12 }}>
          拖拽条体移动;拖拽左/右边缘缩放(每格 = 1 天)。
        </p>
        <CfTimelineGantt rows={rows} start={iso(-7)} end={iso(15)} dayWidth={28} editable onBarChange={onChange} />
    </>
  );
}

Groups + dependencies

A row may declare group: string — the component injects a section header in first-encounter order (synced between sidebar and body). dependencies is an array of bar-id pairs; the component draws an arrowed elbow line between the source’s right edge and the target’s left edge.

背景 视口
src/App.vue
<script setup lang="ts">
import { CfTimelineGantt, type GanttRow } from '@chufix-design/vue';

function iso(offset: number): string {
  const d = new Date();
  d.setDate(d.getDate() + offset);
  return d.toISOString().slice(0, 10);
}

const rows: GanttRow[] = [
  { id: 'p1-design', group: 'Phase 1', label: '设计', bars: [{ id: 'p1d', label: 'wireframe', start: iso(-2), end: iso(2) }] },
  { id: 'p1-dev', group: 'Phase 1', label: '开发', bars: [{ id: 'p1v', label: 'auth + db', start: iso(2), end: iso(7) }] },
  { id: 'p2-design', group: 'Phase 2', label: '设计', bars: [{ id: 'p2d', label: 'feature flow', start: iso(7), end: iso(11), color: 'oklch(70% 0.13 175)' }] },
  { id: 'p2-dev', group: 'Phase 2', label: '开发', bars: [{ id: 'p2v', label: 'shipping', start: iso(11), end: iso(16), color: 'oklch(70% 0.13 175)' }] },
];

const dependencies = [
  { from: 'p1d', to: 'p1v' },
  { from: 'p1v', to: 'p2d' },
  { from: 'p2d', to: 'p2v' },
];
</script>
<template>
  <CfTimelineGantt
    :rows="rows"
    :dependencies="dependencies"
    :start="iso(-5)"
    :end="iso(20)"
    :day-width="26"
  />
</template>
<script setup>
import { CfTimelineGantt } from '@chufix-design/vue';

function iso(offset): string {
  const d = new Date();
  d.setDate(d.getDate() + offset);
  return d.toISOString().slice(0, 10);
}

const rows= [
  { id: 'p1-design', group: 'Phase 1', label: '设计', bars: [{ id: 'p1d', label: 'wireframe', start: iso(-2), end: iso(2) }] },
  { id: 'p1-dev', group: 'Phase 1', label: '开发', bars: [{ id: 'p1v', label: 'auth + db', start: iso(2), end: iso(7) }] },
  { id: 'p2-design', group: 'Phase 2', label: '设计', bars: [{ id: 'p2d', label: 'feature flow', start: iso(7), end: iso(11), color: 'oklch(70% 0.13 175)' }] },
  { id: 'p2-dev', group: 'Phase 2', label: '开发', bars: [{ id: 'p2v', label: 'shipping', start: iso(11), end: iso(16), color: 'oklch(70% 0.13 175)' }] },
];

const dependencies = [
  { from: 'p1d', to: 'p1v' },
  { from: 'p1v', to: 'p2d' },
  { from: 'p2d', to: 'p2v' },
];
</script>
<template>
  <CfTimelineGantt
    :rows="rows"
    :dependencies="dependencies"
    :start="iso(-5)"
    :end="iso(20)"
    :day-width="26"
  />
</template>
import { CfTimelineGantt } from '@chufix-design/react';

export default function Demo() {
  function iso(offset: number): string {
    const d = new Date();
    d.setDate(d.getDate() + offset);
    return d.toISOString().slice(0, 10);
  }

  const rows: GanttRow[] = [
    { id: 'p1-design', group: 'Phase 1', label: '设计', bars: [{ id: 'p1d', label: 'wireframe', start: iso(-2), end: iso(2) }] },
    { id: 'p1-dev', group: 'Phase 1', label: '开发', bars: [{ id: 'p1v', label: 'auth + db', start: iso(2), end: iso(7) }] },
    { id: 'p2-design', group: 'Phase 2', label: '设计', bars: [{ id: 'p2d', label: 'feature flow', start: iso(7), end: iso(11), color: 'oklch(70% 0.13 175)' }] },
    { id: 'p2-dev', group: 'Phase 2', label: '开发', bars: [{ id: 'p2v', label: 'shipping', start: iso(11), end: iso(16), color: 'oklch(70% 0.13 175)' }] },
  ];

  const dependencies = [
    { from: 'p1d', to: 'p1v' },
    { from: 'p1v', to: 'p2d' },
    { from: 'p2d', to: 'p2v' },
  ];
  return (
    <>
      <CfTimelineGantt rows={rows} dependencies={dependencies} start={iso(-5)} end={iso(20)} dayWidth={26} />
    </>
  );
}
import { CfTimelineGantt } from '@chufix-design/react';

export default function Demo() {
  function iso(offset): string {
    const d = new Date();
    d.setDate(d.getDate() + offset);
    return d.toISOString().slice(0, 10);
  }

  const rows= [
    { id: 'p1-design', group: 'Phase 1', label: '设计', bars: [{ id: 'p1d', label: 'wireframe', start: iso(-2), end: iso(2) }] },
    { id: 'p1-dev', group: 'Phase 1', label: '开发', bars: [{ id: 'p1v', label: 'auth + db', start: iso(2), end: iso(7) }] },
    { id: 'p2-design', group: 'Phase 2', label: '设计', bars: [{ id: 'p2d', label: 'feature flow', start: iso(7), end: iso(11), color: 'oklch(70% 0.13 175)' }] },
    { id: 'p2-dev', group: 'Phase 2', label: '开发', bars: [{ id: 'p2v', label: 'shipping', start: iso(11), end: iso(16), color: 'oklch(70% 0.13 175)' }] },
  ];

  const dependencies = [
    { from: 'p1d', to: 'p1v' },
    { from: 'p1v', to: 'p2d' },
    { from: 'p2d', to: 'p2v' },
  ];
  return (
    <>
      <CfTimelineGantt rows={rows} dependencies={dependencies} start={iso(-5)} end={iso(20)} dayWidth={26} />
    </>
  );
}

API

Props

PropTypeDefaultDescription
rowsGanttRow[]Task row array
start / endDateLikeVisible range (end-day inclusive)
unit'day' | 'week' | 'month''day'Axis granularity (v1: affects label text only)
dayWidthnumber32Pixel width of one day
rowHeightnumber40Per-row height
labelWidthnumber200Left sidebar width
showTodaybooleantrueRender today vertical line
dependenciesGanttDependency[]{ from, to } bar-id pairs
editablebooleanfalseDrag-to-move and edge-resize
weekStartsOn0 | 11Sunday / Monday start (affects weekend detection)
captionstringOptional caption above the chart
size'sm' | 'md' | 'lg''md'Font size scale

Types

interface GanttRow {
  id: string;
  label: string;
  group?: string;
  bars: GanttBar[];
}

interface GanttBar {
  id: string;
  start: DateLike;
  end: DateLike;
  label?: string;
  color?: string;     // overrides default accent
  progress?: number;  // 0–1 completion overlay
  disabled?: boolean;
}

interface GanttDependency {
  from: string;
  to: string;
}

Events

  • Vue: @bar-click(bar, row) / @row-click(row) / @bar-change({ bar, row, next, meta })
  • React: onBarClick / onRowClick / onBarChange

meta.action is one of 'move' | 'resize-start' | 'resize-end'. meta.prev is the original { start, end } before the drag began.

反馈与讨论

TimelineGantt · Discussion

0
0 / 600
正在加载评论...