data-grid

Data grid with expandable rows

Data grid with expandable rows

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

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

<script setup lang="ts">
/**
 * Порт c-data-grid-8.tsx (data-grid с раскрываемыми строками, детали заказа
 * в expandedContent).
 *
 * `Button` из оригинала (`registry/bases/radix/ui/button.tsx`, базовый,
 * не reui) не поддерживает проп `mode` — в апстриме `{...{ mode: "icon",
 * variant: "ghost" }}` спредится на нативный `<button>` как мёртвый
 * DOM-атрибут (правило 4a: перенесено буква в букву, `mode` не передаётся
 * во Vue тоже, поскольку `Button` не знает такого пропа).
 *
 * Сетевые аватары (`images.unsplash.com`) и флаги (`flagcdn.com`) заменены
 * общей 1x1 `data:` заглушкой (см. docs/PORTING.md §13/§9 — тот же приём,
 * что и в `badge`/`avatar`/`card`).
 */
import { h, ref } from "vue"
import { Badge } from "@/components/reui/badge"
import { DataGrid, DataGridContainer, DataGridPagination, DataGridScrollArea, DataGridTable } from "@/components/reui/data-grid"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  getCoreRowModel,
  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
  details: string
}

const demoData: IData[] = [
  { id: "1", name: "Alex Johnson", availability: "online", avatar: PIXEL, status: "active", flag: "us", email: "alex@apple.com", company: "Apple", role: "CEO", joined: "Jan, 2024", location: "United States", balance: 5143.03, details: "Alex is a visionary leader at Apple, focusing on innovation and team growth." },
  { id: "2", name: "Sarah Chen", availability: "away", avatar: PIXEL, status: "inactive", flag: "gb", email: "sarah@openai.com", company: "OpenAI", role: "CTO", joined: "Mar, 2023", location: "United Kingdom", balance: 4321.87, details: "Sarah is a technology pioneer specializing in artificial intelligence and machine learning." },
  { id: "3", name: "Michael Rodriguez", availability: "busy", avatar: PIXEL, status: "active", flag: "ca", email: "michael@meta.com", company: "Meta", role: "Designer", joined: "Jun, 2022", location: "Canada", balance: 7654.98, details: "Michael is a creative designer passionate about building user-centric experiences." },
  { id: "4", name: "Emma Wilson", availability: "offline", avatar: PIXEL, status: "inactive", flag: "au", email: "emma@tesla.com", company: "Tesla", role: "Developer", joined: "Sep, 2024", location: "Australia", balance: 3456.45, details: "Emma is a talented developer focused on innovative solutions in automotive technology." },
  { id: "5", name: "David Kim", availability: "online", avatar: PIXEL, status: "active", flag: "de", email: "david@sap.com", company: "SAP", role: "Lawyer", joined: "Nov, 2023", location: "Germany", balance: 9876.54, details: "David is a corporate lawyer specializing in technology and software agreements." },
  { id: "6", name: "Aron Thompson", availability: "away", avatar: PIXEL, status: "active", flag: "my", email: "aron@keenthemes.com", company: "Keenthemes", role: "Director", joined: "Feb, 2022", location: "Malaysia", balance: 6214.22, details: "Aron oversees product development and team leadership at Keenthemes." },
  { id: "7", name: "James Brown", availability: "busy", avatar: PIXEL, status: "inactive", flag: "es", email: "james@bbva.es", company: "BBVA", role: "Product Manager", joined: "Aug, 2024", location: "Spain", balance: 5321.77, details: "James manages product development and strategy for BBVA's digital platforms." },
  { id: "8", name: "Maria Garcia", availability: "offline", avatar: PIXEL, status: "active", flag: "jp", email: "maria@sony.jp", company: "Sony", role: "Marketing Lead", joined: "Dec, 2023", location: "Japan", balance: 8452.39, details: "Maria leads innovative marketing campaigns for Sony's flagship products." },
  { id: "9", name: "Nick Johnson", availability: "online", avatar: PIXEL, status: "inactive", flag: "fr", email: "nick@lvmh.fr", company: "LVMH", role: "Data Scientist", joined: "Apr, 2022", location: "France", balance: 7345.1, details: "Nick is a data scientist optimizing sales and marketing analytics at LVMH." },
  { id: "10", name: "Liam Thompson", availability: "away", avatar: PIXEL, status: "inactive", flag: "it", email: "liam@eni.it", company: "ENI", role: "Engineer", joined: "Jul, 2024", location: "Italy", balance: 5214.88, details: "Liam is a lead engineer developing sustainable energy solutions at ENI." },
  { id: "11", name: "Alex Johnson", availability: "busy", avatar: PIXEL, status: "inactive", flag: "br", email: "alex@vale.br", company: "Vale", role: "Software Engineer", joined: "May, 2023", location: "Brazil", balance: 9421.5, details: "Alex develops cutting-edge software to optimize mining operations at Vale." },
  { id: "12", name: "Sarah Chen", availability: "offline", avatar: PIXEL, status: "active", flag: "in", email: "sarah@tata.in", company: "Tata", role: "Sales Manager", joined: "Oct, 2024", location: "India", balance: 4521.67, details: "Sarah manages international sales for Tata's industrial and automotive products." },
]

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

function initials(name: string) {
  return name.split(" ").map((n) => n[0]).join("")
}

const columns: ColumnDef<IData>[] = [
  {
    id: "id",
    header: () => null,
    cell: (info) => {
      const row = info.row
      return row.getCanExpand()
        ? h(
            Button,
            {
              class: "size-6 text-muted-foreground hover:bg-transparent",
              onClick: row.getToggleExpandedHandler(),
              variant: "ghost",
            },
            () =>
              row.getIsExpanded()
                ? 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", "aria-hidden": "true" },
                    [h("path", { d: "m18 15-6-6-6 6" })]
                  )
                : 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", "aria-hidden": "true" },
                    [h("path", { d: "m6 9 6 6 6-6" })]
                  )
          )
        : null
    },
    size: 25,
    meta: {
      expandedContent: (row: IData) =>
        h("div", { class: "text-muted-foreground ms-12 py-3 text-sm" }, row.details),
    },
  },
  {
    accessorKey: "name",
    id: "name",
    header: "Name",
    cell: (info) => {
      const row = info.row.original
      return h("div", { class: "flex items-center gap-2" }, [
        h(Avatar, { class: "size-6" }, () => [
          h(AvatarImage, { src: row.avatar, alt: row.name }),
          h(AvatarFallback, () => initials(row.name)),
        ]),
        h("a", { href: "#", class: "text-foreground hover:text-primary font-medium" }, row.name),
      ])
    },
    size: 150,
    enableSorting: true,
    enableHiding: false,
  },
  {
    accessorKey: "email",
    header: "Email",
    cell: (info) =>
      h("a", { href: `mailto:${info.getValue()}`, class: "hover:text-primary hover:underline" }, info.getValue() as string),
    size: 150,
  },
  {
    accessorKey: "location",
    header: "Location",
    cell: (info) => {
      const row = info.row.original
      return h("div", { class: "flex items-center gap-1.5" }, [
        h("img", { src: PIXEL, alt: row.flag, class: "size-4 rounded-full object-cover" }),
        h("div", { class: "text-foreground font-medium" }, row.location),
      ])
    },
    size: 175,
    meta: {
      headerClassName: "",
      cellClassName: "text-start",
    },
  },
  {
    accessorKey: "status",
    id: "status",
    header: "Status",
    cell: (info) => {
      const status = info.row.original.status
      return status === "active"
        ? h(Badge, { variant: "success-outline" }, () => "Approved")
        : h(Badge, { variant: "warning-outline" }, () => "Pending")
    },
    size: 100,
  },
]

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

<template>
  <DataGrid :table="table" :record-count="demoData.length" :table-layout="{ headerBackground: false }">
    <div class="w-full space-y-2.5">
      <DataGridContainer>
        <DataGridScrollArea>
          <DataGridTable />
        </DataGridScrollArea>
      </DataGridContainer>
      <DataGridPagination />
    </div>
  </DataGrid>
</template>

Установка

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

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

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

  • @tanstack/vue-table

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