data-grid

Data grid with loading skeleton

Data grid with loading skeleton

Загрузка превью…

src/data-grid/c-data-grid-21.vue

<script setup lang="ts">
/**
 * Порт `c-data-grid-21.tsx` ("Data grid with loading skeleton").
 *
 * `isLoading` в оригинале управляется кнопкой "Enable/Disable Loading"
 * (`useState(true)`, стартует включённым). Паттерн демонстрирует именно
 * skeleton-состояние, поэтому здесь `isLoading` зафиксирован константой
 * `true` — статичный скриншот стенда снимает состояние покоя, а не
 * интерактив; кнопка оставлена в разметке (текст меняется по значению),
 * но обработчик клика не переключает состояние (нет `ref`).
 *
 * Аватарки (`images.unsplash.com`) — реальные сетевые запросы,
 * недетерминированы в headless-стенде; заменены локальным 1x1 data-URI
 * (см. docs/PORTING.md §5 "picsum.photos").
 */
import { h } from "vue"
import { Badge } from "@/components/reui/badge"
import { DataGrid, DataGridColumnHeader, DataGridContainer, DataGridPagination, DataGridScrollArea, DataGridTable } from "@/components/reui/data-grid"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import {
  getCoreRowModel,
  getFilteredRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useVueTable,
} from "@tanstack/vue-table"
import type { ColumnDef, PaginationState, SortingState } from "@tanstack/vue-table"

const PIXEL =
  "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="

interface IData {
  id: string
  name: string
  availability: "online" | "away" | "busy" | "offline"
  avatar: string
  status: "active" | "inactive"
  flag: string
  email: string
  company: string
  role: string
  joined: string
  location: string
  balance: number
}

const demoData: IData[] = [
  { id: "1", name: "Alex Johnson", availability: "online", avatar: PIXEL, status: "active", flag: "us", email: "alex@example.com", company: "Apple", role: "CEO", joined: "Jan, 2024", location: "United States", balance: 5143.03 },
  { id: "2", name: "Sarah Chen", availability: "away", avatar: PIXEL, status: "inactive", flag: "gb", email: "sarah@example.com", company: "OpenAI", role: "CTO", joined: "Mar, 2023", location: "United Kingdom", balance: 4321.87 },
  { id: "3", name: "Michael Rodriguez", availability: "busy", avatar: PIXEL, status: "active", flag: "ca", email: "michael@example.com", company: "Meta", role: "Designer", joined: "Jun, 2022", location: "Canada", balance: 7654.98 },
  { id: "4", name: "Emma Wilson", availability: "offline", avatar: PIXEL, status: "inactive", flag: "au", email: "emma@example.com", company: "Tesla", role: "Developer", joined: "Sep, 2024", location: "Australia", balance: 3456.45 },
  { id: "5", name: "David Kim", availability: "online", avatar: PIXEL, status: "active", flag: "de", email: "david@example.com", company: "SAP", role: "Lawyer", joined: "Nov, 2023", location: "Germany", balance: 9876.54 },
  { id: "6", name: "Aron Thompson", availability: "away", avatar: PIXEL, status: "active", flag: "my", email: "lisa@example.com", company: "Keenthemes", role: "Director", joined: "Feb, 2022", location: "Malaysia", balance: 6214.22 },
  { id: "7", name: "James Brown", availability: "busy", avatar: PIXEL, status: "inactive", flag: "es", email: "james@example.com", company: "BBVA", role: "Product Manager", joined: "Aug, 2024", location: "Spain", balance: 5321.77 },
]

const isLoading = true

const pagination: PaginationState = { pageIndex: 0, pageSize: 5 }
const sorting: SortingState = [{ id: "name", desc: true }]

const columns: ColumnDef<IData>[] = [
  {
    accessorKey: "name",
    id: "name",
    header: (ctx) => h(DataGridColumnHeader, { title: "User", visibility: true, column: ctx.column as never }),
    cell: (ctx) =>
      h("div", { class: "flex items-center gap-3" }, [
        h(Avatar, { class: "size-8" }, () => [
          h(AvatarImage, { src: ctx.row.original.avatar, alt: ctx.row.original.name }),
          h(AvatarFallback, () => ctx.row.original.name.split(" ").map((n) => n[0]).join("")),
        ]),
        h("div", { class: "space-y-px" }, [
          h("div", { class: "text-foreground font-medium" }, ctx.row.original.name),
          h("div", { class: "text-muted-foreground" }, ctx.row.original.email),
        ]),
      ]),
    meta: {
      skeleton: h("div", { class: "flex h-[41px] items-center gap-3" }, [
        h(Skeleton, { class: "size-8 rounded-full" }),
        h("div", { class: "space-y-1" }, [
          h(Skeleton, { class: "h-5 w-24" }),
          h(Skeleton, { class: "h-4 w-16" }),
        ]),
      ]),
    },
    size: 200,
    enableSorting: true,
    enableHiding: false,
    enableResizing: true,
  },
  {
    accessorKey: "email",
    id: "email",
    header: (ctx) => h(DataGridColumnHeader, { title: "Email", visibility: true, column: ctx.column as never }),
    cell: (info) =>
      h("div", { class: "truncate" }, [
        h(
          "a",
          { href: `mailto:${info.getValue()}`, class: "hover:text-primary hover:underline" },
          info.getValue() as string
        ),
      ]),
    size: 150,
    meta: {
      headerClassName: "",
      cellClassName: "text-left",
      skeleton: h(Skeleton, { class: "h-7 w-28" }),
    },
    enableSorting: true,
    enableHiding: true,
    enableResizing: true,
  },
  {
    accessorKey: "status",
    id: "status",
    header: (ctx) => h(DataGridColumnHeader, { title: "Status", visibility: true, column: ctx.column as never }),
    cell: (ctx) =>
      ctx.row.original.status === "active"
        ? h(Badge, { variant: "success-outline" }, () => "Approved")
        : h(Badge, { variant: "warning-outline" }, () => "Pending"),
    meta: {
      skeleton: h(Skeleton, { class: "h-7 w-16" }),
    },
    size: 100,
    enableSorting: true,
    enableHiding: true,
    enableResizing: false,
  },
]

const table = useVueTable({
  get data() {
    return demoData
  },
  columns,
  get pageCount() {
    return Math.ceil((demoData?.length || 0) / pagination.pageSize)
  },
  getRowId: (row: IData) => row.id,
  state: {
    get pagination() {
      return pagination
    },
    get sorting() {
      return sorting
    },
    get columnOrder() {
      return columns.map((column) => column.id as string)
    },
  },
  onPaginationChange: () => {},
  onSortingChange: () => {},
  onColumnOrderChange: () => {},
  getCoreRowModel: getCoreRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  getSortedRowModel: getSortedRowModel(),
})
</script>

<template>
  <DataGrid
    :table="table"
    :record-count="demoData?.length || 0"
    :is-loading="isLoading"
    :table-class-names="{ edgeCell: 'px-5' }"
    :table-layout="{ columnsPinnable: true, columnsMovable: true, columnsVisibility: true }"
  >
    <Card class="w-full gap-3 py-3.5">
      <CardHeader class="flex items-center justify-between px-3.5">
        <CardTitle>Employees</CardTitle>
        <CardAction>
          <Button variant="outline" size="sm">
            {{ isLoading ? "Disable Loading" : "Enable Loading" }}
          </Button>
        </CardAction>
      </CardHeader>
      <div class="w-full px-3.5">
        <Card class="p-0">
          <DataGridContainer>
            <DataGridScrollArea>
              <DataGridTable />
            </DataGridScrollArea>
          </DataGridContainer>
        </Card>
      </div>
      <CardFooter class="border-none bg-transparent! px-3.5 py-0">
        <DataGridPagination />
      </CardFooter>
    </Card>
  </DataGrid>
</template>

Установка

npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-data-grid-21.json

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

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

  • @tanstack/vue-table

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