data-grid

Data grid with sub table

Data grid with sub table

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

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

<script setup lang="ts">
/**
 * Порт c-data-grid-9.tsx (master-detail: заказы с раскрываемой вложенной
 * таблицей позиций через `meta.expandedContent`).
 *
 * Оригинал (2031 строк) огромен только из-за литерального `demoData`
 * (15 заказов, у каждого до 15 вложенных позиций, ~1650 строк) — логика
 * компонента занимает последние ~250 строк файла. Урезано до 3 заказов
 * (SO-001/003/004, сохранена вариативность `status.variant`: primary-light/
 * success-light/destructive-light) по 2 вложенные позиции каждый (см.
 * задание и отчёт по этой задаче для дословного датасета).
 *
 * Сетевые аватары (`images.unsplash.com`) заменены общей 1x1 `data:`
 * заглушкой (docs/PORTING.md §13/§9).
 *
 * Вложенная таблица позиций (`OrderItemsSubTable` — функция в оригинале)
 * вынесена в приватный `OrderItemsSubTable9.vue` (см. комментарий в файле).
 */
import { h, ref } 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 } from "@/components/ui/card"
import { BadgeVariants } from "@/components/reui/badge"
import {
  getCoreRowModel,
  getPaginationRowModel,
  getSortedRowModel,
  useVueTable,
} from "@tanstack/vue-table"
import type {
  ColumnDef,
  ExpandedState,
  PaginationState,
  SortingState,
} from "@tanstack/vue-table"
import OrderItemsSubTable9 from "./OrderItemsSubTable9.vue"

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

interface OrderItemData {
  id: string
  productName: string
  category: string
  price: string
  quantity: number
}

interface Data {
  id: string
  orderNumber: string
  customer: string
  customerEmail: string
  customerAvatar: string
  total: string
  status: {
    label: string
    variant: BadgeVariants["variant"]
  }
  items: OrderItemData[]
}

const demoData: Data[] = [
  {
    id: "1",
    orderNumber: "SO-001",
    customer: "Alex Johnson",
    customerEmail: "alex@example.com",
    customerAvatar: PIXEL,
    total: "$459.97",
    status: { label: "Shipped", variant: "primary-light" },
    items: [
      { id: "1-1", productName: "Wireless Headphones", category: "Electronics", price: "$199.99", quantity: 1 },
      { id: "1-2", productName: "Phone Case", category: "Accessories", price: "$99.99", quantity: 1 },
    ],
  },
  {
    id: "3",
    orderNumber: "SO-003",
    customer: "Michael Rodriguez",
    customerEmail: "michael@example.com",
    customerAvatar: PIXEL,
    total: "$189.97",
    status: { label: "Delivered", variant: "success-light" },
    items: [
      { id: "3-1", productName: "Coffee Mug", category: "Home", price: "$89.99", quantity: 1 },
      { id: "3-2", productName: "Coffee Beans", category: "Food", price: "$24.99", quantity: 1 },
    ],
  },
  {
    id: "4",
    orderNumber: "SO-004",
    customer: "Emma Wilson",
    customerEmail: "emma@example.com",
    customerAvatar: PIXEL,
    total: "$299.97",
    status: { label: "Cancelled", variant: "destructive-light" },
    items: [
      { id: "4-1", productName: "Laptop Stand", category: "Electronics", price: "$99.99", quantity: 1 },
      { id: "4-2", productName: "Wireless Mouse", category: "Electronics", price: "$49.99", quantity: 1 },
    ],
  },
]

const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const sorting = ref<SortingState>([])
const expandedRows = ref<ExpandedState>({})
const columnOrder = ref<string[]>(["expand", "customer", "items", "total", "status"])

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

function chevronIcon(up: boolean) {
  return 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: up ? "m18 15-6-6-6 6" : "m6 9 6 6 6-6" })]
  )
}

const columns: ColumnDef<Data>[] = [
  {
    id: "expand",
    header: () => null,
    cell: (info) => {
      const row = info.row
      return row.getCanExpand()
        ? h(
            Button,
            {
              onClick: row.getToggleExpandedHandler(),
              size: "icon-sm",
              variant: "ghost",
              class: "opacity-70 hover:bg-transparent hover:opacity-100",
              "aria-label": row.getIsExpanded() ? "Collapse order items" : "Expand order items",
            },
            () => chevronIcon(row.getIsExpanded())
          )
        : null
    },
    size: 25,
    enableResizing: false,
    meta: {
      expandedContent: (row: Data) => h(OrderItemsSubTable9, { items: row.items }),
    },
  },
  {
    accessorKey: "customer",
    id: "customer",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Customer", visibility: true }),
    cell: (info) => {
      const row = info.row.original
      return h("div", { class: "flex items-center gap-3" }, [
        h(Avatar, { class: "size-8" }, () => [
          h(AvatarImage, { src: row.customerAvatar, alt: row.customer }),
          h(AvatarFallback, () => initials(row.customer)),
        ]),
        h("div", { class: "space-y-px" }, [
          h("div", { class: "text-foreground font-medium" }, row.customer),
          h("div", { class: "text-muted-foreground" }, row.customerEmail),
        ]),
      ])
    },
    enableSorting: true,
    enableHiding: true,
    enableResizing: true,
    size: 200,
  },
  {
    accessorKey: "items",
    id: "items",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Items", visibility: true }),
    cell: (info) => {
      const items = info.getValue() as OrderItemData[]
      const itemCount = items.length
      return h(
        "div",
        {
          class: "text-foreground hover:text-primary cursor-pointer text-sm font-medium",
          onClick: () => info.row.getToggleExpandedHandler()(),
        },
        `${itemCount} ${itemCount === 1 ? "item" : "items"}`
      )
    },
    enableSorting: true,
    enableHiding: true,
    enableResizing: true,
    size: 120,
  },
  {
    accessorKey: "total",
    id: "total",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Total", visibility: true }),
    cell: (info) => info.getValue() as string,
    enableSorting: true,
    enableHiding: true,
    enableResizing: true,
    size: 100,
  },
  {
    accessorKey: "status",
    id: "status",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Status", visibility: true }),
    cell: (info) => {
      const status = info.row.original.status
      return h(Badge, { variant: status.variant }, () => status.label)
    },
    enableSorting: true,
    enableHiding: true,
    enableResizing: true,
    size: 120,
  },
]

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

<template>
  <DataGrid
    :table="table"
    :record-count="demoData.length"
    :table-layout="{ columnsPinnable: true, columnsMovable: true, columnsVisibility: true }"
  >
    <div class="w-full space-y-2.5">
      <Card class="p-0">
        <DataGridContainer>
          <DataGridScrollArea>
            <DataGridTable />
          </DataGridScrollArea>
        </DataGridContainer>
      </Card>
      <DataGridPagination />
    </div>
  </DataGrid>
</template>

src/data-grid/OrderItemsSubTable9.vue

<script setup lang="ts">
/**
 * Приватный хелпер `c-data-grid-9.vue` (data-grid-9.tsx: `OrderItemsSubTable`,
 * функция внутри файла оригинала — здесь вынесена в отдельный SFC, тот же
 * приём, что и `CheckboxTreeItem16.vue` в `block-checkbox` (docs/PORTING.md §9):
 * не экспортируется из `index.ts`, деталь реализации одного паттерна.
 *
 * Собственный `useVueTable` — вложенная таблица позиций заказа, рендерится
 * через `meta.expandedContent` родительской таблицы.
 */
import { h, ref } from "vue"
import { DataGrid, DataGridColumnHeader, DataGridContainer, DataGridPagination, DataGridScrollArea, DataGridTable } from "@/components/reui/data-grid"
import { Card } from "@/components/ui/card"
import { getCoreRowModel, getPaginationRowModel, getSortedRowModel, useVueTable } from "@tanstack/vue-table"
import type { ColumnDef, PaginationState, SortingState } from "@tanstack/vue-table"

interface OrderItemData {
  id: string
  productName: string
  category: string
  price: string
  quantity: number
}

const props = defineProps<{ items: OrderItemData[] }>()

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

const columns: ColumnDef<OrderItemData>[] = [
  {
    accessorKey: "productName",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Product" }),
    cell: (info) => info.getValue() as string,
    enableSorting: true,
    size: 200,
  },
  {
    accessorKey: "category",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Category" }),
    cell: (info) => info.getValue() as string,
    enableSorting: true,
    size: 120,
  },
  {
    accessorKey: "price",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Price" }),
    cell: (info) => info.getValue() as string,
    enableSorting: true,
    size: 100,
  },
  {
    accessorKey: "quantity",
    header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Qty" }),
    cell: (info) => info.getValue() as number,
    enableSorting: true,
    size: 80,
  },
]

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

<template>
  <div class="bg-background min-w-0">
    <DataGrid :table="table" :record-count="props.items.length" :table-layout="{ rowBorder: true }">
      <div class="w-full min-w-0 space-y-2.5 p-3 pl-12">
        <Card class="p-0">
          <DataGridContainer class="min-w-0">
            <DataGridScrollArea class="min-w-0">
              <DataGridTable />
            </DataGridScrollArea>
          </DataGridContainer>
        </Card>
        <DataGridPagination class="pb-1.5" />
      </div>
    </DataGrid>
  </div>
</template>

Установка

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

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

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

  • @tanstack/vue-table

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