data-grid

Data grid with summary stats footer

Data grid with summary stats footer

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

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

<!--
  Порт `c-data-grid-25.tsx` ("Summary stats footer").

  Сетевые `unsplash`-аватары и `flagcdn.com` флаги заменены статичным 1x1
  `data:` URI (`PIXEL`), тот же приём, что в остальных `block-data-grid`
  (см. c-data-grid-1.vue/13.vue). `IconPlaceholder` заменена инлайновым
  `<svg>` (lucide-react v0.545.0: "chart-no-axes-column-increasing" — путь
  по умолчанию для алиаса "BarChartIcon" в оригинале, "refresh-cw").
  Демо-данные урезаны с 10 до 6 строк (сохранены все 4 статуса).
-->
<script setup lang="ts">
import { computed, h, ref } from "vue"
import { Badge } from "@/components/reui/badge"
import { DataGrid, DataGridColumnHeader, DataGridPagination, DataGridScrollArea, DataGridTable, DataGridTableFootRow, DataGridTableFootRowCell } from "@/components/reui/data-grid"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
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
  avatar: string
  location: string
  flag: string
  status: "Active" | "Inactive" | "Pending" | "Blocked"
  balance: number
}

const demoData: IData[] = [
  { id: "1", name: "Alex Johnson", avatar: PIXEL, location: "United States", flag: "us", status: "Active", balance: 5143.03 },
  { id: "2", name: "Sarah Chen", avatar: PIXEL, location: "United Kingdom", flag: "gb", status: "Inactive", balance: 4321.87 },
  { id: "3", name: "Michael Rodriguez", avatar: PIXEL, location: "Canada", flag: "ca", status: "Blocked", balance: 7654.98 },
  { id: "4", name: "Emma Wilson", avatar: PIXEL, location: "Australia", flag: "au", status: "Inactive", balance: 3456.45 },
  { id: "5", name: "David Kim", avatar: PIXEL, location: "Germany", flag: "de", status: "Active", balance: 9876.54 },
  { id: "6", name: "Aron Thompson", avatar: PIXEL, location: "Malaysia", flag: "my", status: "Pending", balance: 6214.22 },
]

const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const sorting = ref<SortingState>([{ id: "name", desc: false }])

const stats = computed(() => {
  const count = demoData.length
  const activeCount = demoData.filter((r) => r.status === "Active").length
  const balances = demoData.map((r) => r.balance)
  const minBalance = Math.min(...balances)
  const maxBalance = Math.max(...balances)
  const avgBalance = balances.reduce((a, b) => a + b, 0) / count
  return { count, activeCount, minBalance, maxBalance, avgBalance }
})

function fmt(n: number) {
  return "$" + n.toLocaleString("en-US", { minimumFractionDigits: 2 })
}

const columns: ColumnDef<IData>[] = [
  {
    accessorKey: "name",
    id: "name",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "User", visibility: true }),
    cell: (info) =>
      h("div", { class: "flex items-center gap-3" }, [
        h(Avatar, { class: "size-8" }, () => [
          h(AvatarImage, { src: info.row.original.avatar, alt: info.row.original.name }),
          h(AvatarFallback, () => info.row.original.name.split(" ").map((n) => n[0]).join("")),
        ]),
        h("div", { class: "text-foreground font-medium" }, info.row.original.name),
      ]),
    size: 220,
    meta: {
      autoSize: true,
    },
    enableSorting: true,
    enableHiding: false,
    enableResizing: true,
  },
  {
    accessorKey: "location",
    id: "location",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Location", visibility: true }),
    cell: (info) =>
      h("div", { class: "flex items-center gap-1.5" }, [
        h("img", { src: PIXEL, alt: info.row.original.flag, class: "size-4 rounded-full object-cover" }),
        h("div", { class: "text-foreground font-medium" }, info.row.original.location),
      ]),
    size: 150,
    enableSorting: true,
    enableHiding: true,
    enableResizing: true,
  },
  {
    accessorKey: "status",
    id: "status",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Status", visibility: true }),
    cell: (info) => {
      const s = info.row.original.status
      if (s === "Active") return h(Badge, { variant: "success-outline" }, () => "Active")
      if (s === "Blocked") return h(Badge, { variant: "destructive-outline" }, () => "Blocked")
      if (s === "Inactive") return h(Badge, { variant: "info-outline" }, () => "Inactive")
      return h(Badge, { variant: "warning-outline" }, () => "Pending")
    },
    size: 110,
    enableSorting: true,
    enableHiding: true,
    enableResizing: true,
  },
  {
    accessorKey: "balance",
    id: "balance",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Balance", visibility: true }),
    cell: (info) => h("div", { class: "text-foreground font-medium tabular-nums" }, fmt(info.row.original.balance)),
    size: 130,
    enableSorting: true,
    enableHiding: true,
    enableResizing: false,
  },
]

const columnOrder = ref<string[]>(columns.map((c) => c.id as string))

const table = useVueTable({
  get data() {
    return demoData
  },
  columns,
  get pageCount() {
    return Math.ceil(demoData.length / pagination.value.pageSize)
  },
  getRowId: (row: IData) => row.id,
  state: {
    get pagination() {
      return pagination.value
    },
    get sorting() {
      return sorting.value
    },
    get columnOrder() {
      return columnOrder.value
    },
  },
  onColumnOrderChange: (updater) => {
    columnOrder.value = typeof updater === "function" ? updater(columnOrder.value) : updater
  },
  onPaginationChange: (updater) => {
    pagination.value = typeof updater === "function" ? updater(pagination.value) : updater
  },
  onSortingChange: (updater) => {
    sorting.value = typeof updater === "function" ? updater(sorting.value) : updater
  },
  getCoreRowModel: getCoreRowModel(),
  getFilteredRowModel: getFilteredRowModel(),
  getPaginationRowModel: getPaginationRowModel(),
  getSortedRowModel: getSortedRowModel(),
})

const visibleCount = computed(() => table.getVisibleLeafColumns().length)
</script>

<template>
  <DataGrid
    :table="table"
    :record-count="demoData.length"
    :table-layout="{ columnsPinnable: true, columnsResizable: true, columnsVisibility: true }"
  >
    <Card class="w-full gap-0 p-0">
      <CardHeader class="flex items-center justify-between px-3.5 py-2">
        <div class="flex items-center gap-2">
          <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="text-muted-foreground size-4">
            <path d="M5 21v-6" />
            <path d="M12 21V9" />
            <path d="M19 21V3" />
          </svg>
          <span class="text-foreground text-sm font-medium">Team Summary</span>
        </div>
        <CardAction>
          <Button variant="outline" size="sm">
            <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">
              <path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8" />
              <path d="M21 3v5h-5" />
              <path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16" />
              <path d="M8 16H3v5" />
            </svg>
            Refresh
          </Button>
        </CardAction>
      </CardHeader>
      <CardContent class="border-y px-0">
        <DataGridScrollArea>
          <DataGridTable>
            <template #footer>
              <DataGridTableFootRow>
                <DataGridTableFootRowCell :col-span="visibleCount - 2" />
                <DataGridTableFootRowCell>
                  <div class="flex flex-col gap-0.5">
                    <span class="text-muted-foreground text-xs">Min</span>
                    <span class="tabular-nums">{{ fmt(stats.minBalance) }}</span>
                  </div>
                </DataGridTableFootRowCell>
                <DataGridTableFootRowCell>
                  <div class="flex flex-col gap-0.5">
                    <span class="text-muted-foreground text-xs">Max</span>
                    <span class="tabular-nums">{{ fmt(stats.maxBalance) }}</span>
                  </div>
                </DataGridTableFootRowCell>
              </DataGridTableFootRow>
              <DataGridTableFootRow>
                <DataGridTableFootRowCell :col-span="visibleCount - 2">
                  <div class="flex items-center gap-1.5">
                    <span class="text-muted-foreground">Avg balance</span>
                    <span class="tabular-nums">{{ fmt(stats.avgBalance) }}</span>
                  </div>
                </DataGridTableFootRowCell>
                <DataGridTableFootRowCell :col-span="2">
                  <div class="flex items-center gap-1.5">
                    <span class="text-muted-foreground">Active</span>
                    <Badge variant="success" size="sm">{{ stats.activeCount }}</Badge>
                  </div>
                </DataGridTableFootRowCell>
              </DataGridTableFootRow>
            </template>
          </DataGridTable>
        </DataGridScrollArea>
      </CardContent>
      <CardFooter class="border-none bg-transparent! px-2.5 py-2">
        <DataGridPagination />
      </CardFooter>
    </Card>
  </DataGrid>
</template>

Установка

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

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

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

  • @tanstack/vue-table

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