data-grid
Data grid with local infinite scroll
Data grid with local infinite scroll
Загрузка превью…
src/data-grid/c-data-grid-27.vue
<!--
Порт `c-data-grid-27.tsx` ("Local infinite scroll" / virtualized directory).
`DataGridTableVirtual` виртуализирует строки через `@tanstack/vue-virtual`
внутри `DataGridScrollArea` (см. docs/PORTING.md задание и комментарий в
DataGridTableVirtual.vue) — на статичном скриншоте видно ровно то
подмножество строк, которое попадает в фиксированный `h-[480px]`
viewport при монтировании, без эмуляции скролла.
Сетевые `unsplash`-аватары заменены статичным 1x1 `data:` URI (`PIXEL`).
`Math.random()` в генераторе баланса заменён детерминированной функцией
индекса (см. `balanceFor`). `IconPlaceholder` заменена инлайновым `<svg>`
(lucide-react v0.545.0: "refresh-cw"). 200 строк демо-данных сгенерированы
программно (`generateData`), как и в оригинале — это разрешено заданием
явно.
-->
<script setup lang="ts">
import { h, ref } from "vue"
import { Badge } from "@/components/reui/badge"
import { DataGrid, DataGridColumnHeader, DataGridScrollArea, DataGridTableVirtual } from "@/components/reui/data-grid"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { getCoreRowModel, getSortedRowModel, useVueTable } from "@tanstack/vue-table"
import type { ColumnDef, SortingState } from "@tanstack/vue-table"
const PIXEL =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
interface IData {
id: string
name: string
avatar: string
department: string
status: "Active" | "Inactive" | "Pending"
balance: number
}
const names = [
"Alex Johnson",
"Sarah Chen",
"Michael Rodriguez",
"Emma Wilson",
"David Kim",
"Aron Thompson",
"James Brown",
"Maria Garcia",
"Nick Johnson",
"Liam Thompson",
]
const departments = [
"Engineering",
"Marketing",
"Design",
"Sales",
"Finance",
"Operations",
"Legal",
"Support",
]
const statuses: IData["status"][] = ["Active", "Inactive", "Pending"]
// ponytail: Math.random() would make the generated dataset non-deterministic
// across runs, breaking a static screenshot compare — a simple index-based
// formula (spread via a large odd multiplier, wrapped into the same
// 1000..10000 range as the original) is enough for demo data.
function balanceFor(index: number): number {
return Math.round((((index * 137) % 9000) + 1000) * 100) / 100
}
function generateData(count: number): IData[] {
return Array.from({ length: count }, (_, i) => ({
id: String(i + 1),
name: names[i % names.length]!,
avatar: PIXEL,
department: departments[i % departments.length]!,
status: statuses[i % statuses.length]!,
balance: balanceFor(i),
}))
}
const allData = generateData(200)
const sorting = ref<SortingState>([])
const columns: ColumnDef<IData>[] = [
{
accessorKey: "id",
id: "id",
header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "#" }),
cell: (info) => h("span", { class: "text-muted-foreground tabular-nums" }, info.row.original.id),
size: 70,
enableSorting: false,
},
{
accessorKey: "name",
id: "name",
header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "User" }),
cell: (info) =>
h("div", { class: "flex items-center gap-3" }, [
h(Avatar, { class: "size-7" }, () => [
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("span", { class: "text-foreground font-medium" }, info.row.original.name),
]),
minSize: 150,
meta: {
autoSize: true,
},
enableSorting: true,
},
{
accessorKey: "department",
id: "department",
header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Department" }),
cell: (info) => info.row.original.department,
size: 150,
enableSorting: true,
},
{
accessorKey: "status",
id: "status",
header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Status" }),
cell: (info) => {
const s = info.row.original.status
if (s === "Active") return h(Badge, { variant: "success-outline" }, () => "Active")
if (s === "Inactive") return h(Badge, { variant: "info-outline" }, () => "Inactive")
return h(Badge, { variant: "warning-outline" }, () => "Pending")
},
size: 120,
enableSorting: true,
},
{
accessorKey: "balance",
id: "balance",
header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Balance" }),
cell: (info) =>
h("span", { class: "tabular-nums" }, `$${info.row.original.balance.toLocaleString("en-US", { minimumFractionDigits: 2 })}`),
size: 140,
enableSorting: true,
enableResizing: true,
},
]
const table = useVueTable({
get data() {
return allData
},
columns,
getRowId: (row: IData) => row.id,
state: {
get sorting() {
return sorting.value
},
},
onSortingChange: (updater) => {
sorting.value = typeof updater === "function" ? updater(sorting.value) : updater
},
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
})
</script>
<template>
<DataGrid
:table="table"
:record-count="allData.length"
:table-layout="{ columnsResizable: true, headerSticky: true }"
:table-class-names="{ headerSticky: 'sticky top-0 z-10 bg-muted/90 backdrop-blur-xs' }"
>
<Card class="w-full gap-0 p-0">
<CardHeader class="flex items-center justify-between gap-3 px-4 py-2">
<CardTitle class="text-sm font-medium">Virtualized Directory</CardTitle>
<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-t p-0">
<DataGridScrollArea class="h-[480px]">
<DataGridTableVirtual :estimate-size="49" />
</DataGridScrollArea>
</CardContent>
</Card>
</DataGrid>
</template>