gantt
Gantt chart with task table and progress
Gantt chart with task table and progress
Загрузка превью…
src/gantt/c-gantt-5.vue
<!--
ПОРТ c-gantt-5 ("Gantt chart with task table and progress").
`columns` — реальный tree-panel `GanttColumn[]` (Owner avatar+имя, Status
badge); `defaultInteractions={drag:false,resize:false,selectSlot:false}`
держит таймлайн нередактируемым, как в оригинале. `AddTaskButton` —
тот же локальный-компонент-внутри-`<Gantt>` приём, что и в c-gantt-3/4.vue
(получает `useGantt()` через provide/inject вместо отсутствующего
`apiRef`). Дата фиксирована явно в UTC (никаких `new Date()`), тот же
якорь, что и в `ui-gantt`.
-->
<script setup lang="ts">
import { defineComponent, h, reactive, ref } from "vue"
import { addDays, startOfDay, startOfWeek } from "date-fns"
import { Gantt, GanttNav, GanttToolbar, GanttView, useGantt } from "@/components/reui/gantt"
import type { GanttColumn, GanttEvent, GanttResource } from "@/components/reui/gantt"
import { Badge, type BadgeVariants } from "@/components/reui/badge"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
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))
// Pinned to the exact start of the visible month range (not "anchor"/"now"):
// centering on a fraction inside the range left a few px of scrollLeft
// rounding drift between the two runtimes at this pane's narrow width
// (200px timeline pane, 124rem track) - fraction 0 collapses both sides to
// the same scrollLeft=0 deterministically.
const RANGE_ORIGIN = new Date(Date.UTC(2024, 0, 1))
const PIXEL = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
type TaskMeta = { owner: string; status: string }
const OWNER_AVATARS: Record<string, string> = {
"Ada Lovelace": PIXEL,
"Grace Hopper": PIXEL,
"Alan Turing": PIXEL,
"Linus Torvalds": PIXEL,
"Katherine Johnson": PIXEL,
"Margaret Hamilton": PIXEL,
}
function ownerInitials(name: string) {
const parts = name.trim().split(/\s+/).filter(Boolean)
if (parts.length === 0) return "?"
if (parts.length === 1) return parts[0]!.slice(0, 2).toUpperCase()
return (parts[0]![0]! + parts[parts.length - 1]![0]!).toUpperCase()
}
const STATUS_VARIANT: Record<string, BadgeVariants["variant"]> = {
Done: "success-light",
"In progress": "warning-light",
"Not started": "secondary",
}
const INITIAL_RESOURCES: GanttResource[] = [
{
id: "planning",
title: "Planning",
children: [
{ id: "requirements", title: "Requirements" },
{ id: "design-phase", title: "Design" },
],
},
{
id: "build",
title: "Build",
children: [
{ id: "frontend", title: "Frontend" },
{ id: "backend", title: "Backend" },
],
},
{
id: "launch",
title: "Launch",
children: [
{ id: "qa", title: "QA & Testing" },
{ id: "rollout", title: "Rollout" },
],
},
]
const INITIAL_META: Record<string, TaskMeta> = {
requirements: { owner: "Ada Lovelace", status: "Done" },
"design-phase": { owner: "Grace Hopper", status: "Done" },
frontend: { owner: "Alan Turing", status: "In progress" },
backend: { owner: "Linus Torvalds", status: "In progress" },
qa: { owner: "Katherine Johnson", status: "Not started" },
rollout: { owner: "Margaret Hamilton", status: "Not started" },
}
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("requirements", "Requirements", -10, 8, "var(--color-blue-500)", 100),
bar("design-phase", "Design", -8, 8, "var(--color-sky-500)", 100),
bar("frontend", "Frontend", -2, 10, "var(--color-violet-500)", 60),
bar("backend", "Backend", 0, 10, "var(--color-purple-500)", 45),
bar("qa", "QA & Testing", 8, 8, "var(--color-amber-500)", 0),
bar("rollout", "Rollout", 14, 5, "var(--color-emerald-500)", 0),
]
}
const bars = buildBars(ANCHOR)
const resources = ref<GanttResource[]>(INITIAL_RESOURCES)
const meta = reactive<Record<string, TaskMeta>>({ ...INITIAL_META })
const addedCount = ref(0)
const columns: GanttColumn[] = [
{
id: "owner",
title: "Owner",
width: 130,
align: "start",
render: ({ resource, isGroup }) => {
if (isGroup) return null
const owner = meta[resource.id]?.owner
if (!owner) return null
return h("span", { class: "flex min-w-0 items-center gap-2" }, [
h(Avatar, { class: "size-5 shrink-0" }, () => [
h(AvatarImage, { src: OWNER_AVATARS[owner] ?? PIXEL, alt: owner }),
h(AvatarFallback, { class: "text-[10px]" }, () => ownerInitials(owner)),
]),
h("span", { class: "truncate" }, owner),
])
},
},
{
id: "status",
title: "Status",
width: 100,
align: "start",
render: ({ resource, isGroup }) => {
if (isGroup) return null
const status = meta[resource.id]?.status
if (!status) return null
return h(Badge, { variant: STATUS_VARIANT[status] ?? "secondary", size: "sm" }, () => status)
},
},
]
const AddTaskButton = defineComponent({
setup() {
const instance = useGantt<unknown>()
const addTask = () => {
const n = ++addedCount.value
const id = `task-${n}`
resources.value = resources.value.map((group) =>
group.id === "launch" ? { ...group, children: [...(group.children ?? []), { id, title: `New task ${n}` }] } : group
)
meta[id] = { owner: "Unassigned", status: "Not started" }
const week = startOfWeek(startOfDay(ANCHOR), { weekStartsOn: 0 })
const start = addDays(week, (n % 6) - 2)
instance.api.addEvent({
id: `bar-${id}`,
title: `New task ${n}`,
start,
end: addDays(start, 5),
allDay: true,
color: "var(--color-slate-400)",
resourceId: id,
progress: 0,
})
}
return () =>
h(
Button,
{ variant: "outline", size: "sm", onClick: addTask },
() => [
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 task",
]
)
},
})
</script>
<template>
<div class="w-full p-4">
<Card class="w-full py-0">
<CardContent class="p-0">
<Gantt
:date="ANCHOR"
:initial-center="RANGE_ORIGIN"
:default-events="bars"
:resources="resources"
default-scale="month"
:infinite-scroll="false"
:default-interactions="{ drag: false, resize: false, selectSlot: false }"
:columns="columns"
:tree-panel="{ width: 400, nameColumnWidth: 150 }"
class="h-[500px] 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>
<AddTaskButton />
</GanttToolbar>
</div>
<GanttView />
</Gantt>
</CardContent>
</Card>
</div>
</template>