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>