TimelineGantt 甘特图
横向时间轴 + 多行任务条 —— today 标线、周末高亮、拖拽移动 / 缩放、依赖箭头、按 group 分组。
基础用法
rows 是任务行数组;每行的 bars 是该行上的若干时间段。start / end 决定可视范围,day-width 控制一天的像素宽度,今天会用一条蓝色竖线标出。
背景
<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> 可编辑(拖拽 / 缩放)
editable 打开后,条体可拖拽整体移动;左右边缘出现把柄,可以单边缩放。变更通过 @bar-change 事件抛出 { bar, row, next: { start, end }, meta },宿主自己回写到 model。
背景
<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> 分组 + 依赖
行可以带 group: string —— 渲染时自动按首次出现顺序插入分组标题(侧栏 + 主体一起对齐)。dependencies 数组以 bar id 配对,组件在主体上画一条带箭头的折线,表示前置/后置关系。
背景
<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> API
Props
| Prop | 类型 | 默认 | 说明 |
|---|---|---|---|
rows | GanttRow[] | — | 任务行数组 |
start / end | DateLike | — | 可视范围(包含 end 当天) |
unit | 'day' | 'week' | 'month' | 'day' | 轴粒度(v1 仅影响显示文案) |
dayWidth | number | 32 | 每天的像素宽度 |
rowHeight | number | 40 | 每行高度 |
labelWidth | number | 200 | 左侧名称栏宽度 |
showToday | boolean | true | 渲染 today 竖线 |
dependencies | GanttDependency[] | — | 依赖关系 { from, to } 数组(bar id 配对) |
editable | boolean | false | 允许拖拽移动 / 边缘缩放 |
weekStartsOn | 0 | 1 | 1 | 周日 / 周一为起点(影响周末判断) |
caption | string | — | 顶部说明文字 |
size | 'sm' | 'md' | 'lg' | 'md' | 字号档位 |
类型
interface GanttRow {
id: string;
label: string;
group?: string;
bars: GanttBar[];
}
interface GanttBar {
id: string;
start: DateLike;
end: DateLike;
label?: string;
color?: string; // 自定义色,覆盖默认 accent
progress?: number; // 0–1 完成度
disabled?: boolean;
}
interface GanttDependency {
from: string; // 源 bar id
to: string; // 目标 bar id
}
事件
- Vue:
@bar-click(bar, row)/@row-click(row)/@bar-change({ bar, row, next, meta }) - React:
onBarClick/onRowClick/onBarChange
meta.action 取值:'move' | 'resize-start' | 'resize-end'。meta.prev 是拖拽前的原始 { start, end }。
反馈与讨论
TimelineGantt 甘特图 的讨论