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>

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-gantt-5.json

Зависимости реестра

npm-зависимости

  • date-fns

Источник: порт из ReUI (Keenthemes, MIT)