gantt
Gantt chart with quarter roadmap and rollups
Quarter-scale roadmap where workstream groups form swimlanes and parent rows roll children into one summary bar. Toolbar button calls addEvent via apiRef to schedule backlog items. ReUI Gantt, GanttNav, GanttToolbar, GanttView; shadcn Card, Button.
Загрузка превью…
src/gantt/c-gantt-3.vue
<!--
ПОРТ c-gantt-3 ("Gantt chart with quarter roadmap and rollups").
Оригинал берёт императивный доступ через `apiRef`; наш `Gantt.vue` его не
реализует (`defineExpose` не добавлен, см. заголовок Gantt.vue). Тот же
доступ получается напрямую: `AddInitiativeButton` — крошечный локальный
компонент, смонтированный ВНУТРИ `<Gantt>` (в слоте `GanttToolbar`), поэтому
его `useGantt()` находит provide/inject-контекст так же, как `GanttNav`/
`GanttToolbar` уже делают. Родительские workstream-строки не получают
своих баров — `summaryBars` (по умолчанию `true`) сворачивает дочерние
initiative-бары в сводную полосу на родительской строке, как в оригинале.
Дата фиксирована явно в UTC (никаких `new Date()`), тот же якорь, что и в
`ui-gantt`.
-->
<script setup lang="ts">
import { defineComponent, h, ref } from "vue"
import { addDays, startOfDay, startOfWeek } from "date-fns"
import { Gantt, GanttNav, GanttToolbar, GanttView, useGantt } from "@/components/reui/gantt"
import type { GanttEvent, GanttResource } from "@/components/reui/gantt"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
const ANCHOR = new Date(Date.UTC(2024, 0, 8, 0, 0, 0))
const RESOURCES: GanttResource[] = [
{
id: "platform",
title: "Platform",
children: [
{ id: "auth-revamp", title: "Auth Revamp" },
{ id: "api-v2", title: "API v2" },
],
},
{
id: "growth",
title: "Growth",
children: [
{ id: "onboarding", title: "Onboarding Flow" },
{ id: "referrals", title: "Referral Program" },
],
},
{
id: "design-system",
title: "Design System",
children: [
{ id: "tokens", title: "Design Tokens" },
{ id: "components", title: "Component Library" },
],
},
{
id: "backlog",
title: "Backlog",
children: [
{ id: "search", title: "Search Revamp" },
{ id: "billing", title: "Billing v2" },
{ id: "mobile", title: "Mobile App" },
],
},
]
const BACKLOG: Array<{ id: string; title: string; color: string }> = [
{ id: "search", title: "Search Revamp", color: "var(--color-rose-500)" },
{ id: "billing", title: "Billing v2", color: "var(--color-amber-500)" },
{ id: "mobile", title: "Mobile App", color: "var(--color-cyan-500)" },
]
function buildBars(anchor: Date): GanttEvent[] {
const week = startOfWeek(startOfDay(anchor), { weekStartsOn: 0 })
const day = (dayOffset: number) => addDays(week, dayOffset)
const bar = (resourceId: string, title: string, startOffset: number, days: number, color: string, progress?: number): GanttEvent => ({
id: `bar-${resourceId}`,
title,
start: day(startOffset),
end: day(startOffset + days),
allDay: true,
color,
resourceId,
progress,
})
return [
bar("auth-revamp", "Auth Revamp", -30, 60, "var(--color-blue-500)", 100),
bar("api-v2", "API v2", 20, 90, "var(--color-sky-500)", 30),
bar("onboarding", "Onboarding Flow", -20, 60, "var(--color-emerald-500)", 80),
bar("referrals", "Referral Program", 50, 90, "var(--color-teal-500)", 0),
bar("tokens", "Design Tokens", -60, 70, "var(--color-violet-500)", 100),
bar("components", "Component Library", 0, 120, "var(--color-purple-500)", 45),
]
}
const bars = buildBars(ANCHOR)
const scheduled = ref(0)
const AddInitiativeButton = defineComponent({
setup() {
const instance = useGantt<unknown>()
const addInitiative = () => {
const scheduledIds = new Set(instance.api.getEvents().map((event) => event.id))
const index = BACKLOG.findIndex((item) => !scheduledIds.has(`bar-${item.id}`))
if (index === -1) return
const item = BACKLOG[index]!
const week = startOfWeek(startOfDay(ANCHOR), { weekStartsOn: 0 })
const start = addDays(week, 12 + index * 20)
instance.api.addEvent({
id: `bar-${item.id}`,
title: item.title,
start,
end: addDays(start, 24),
allDay: true,
color: item.color,
resourceId: item.id,
})
scheduled.value++
}
return () =>
h(
Button,
{ variant: "outline", size: "sm", disabled: scheduled.value >= BACKLOG.length, onClick: addInitiative },
() => [
h(
"svg",
{
xmlns: "http://www.w3.org/2000/svg",
width: "24",
height: "24",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
class: "size-4",
"aria-hidden": "true",
},
[h("path", { d: "M5 12h14" }), h("path", { d: "M12 5v14" })]
),
"Add to roadmap",
]
)
},
})
</script>
<template>
<div class="w-full p-4">
<Card class="w-full py-0">
<CardContent class="p-0">
<Gantt :date="ANCHOR"
initial-center="anchor" :default-events="bars" :resources="RESOURCES" default-scale="quarter" :infinite-scroll="false" :tree-panel="{ width: 200 }" class="h-[480px] w-full">
<div class="flex flex-wrap items-center gap-2 border-b pe-3">
<GanttNav class="min-w-0 flex-1 border-b-0" />
<GanttToolbar>
<AddInitiativeButton />
</GanttToolbar>
</div>
<GanttView />
</Gantt>
</CardContent>
</Card>
</div>
</template>