data-grid
Data grid with per-column aggregate footer
Data grid with per-column aggregate footer
Загрузка превью…
src/data-grid/c-data-grid-26.vue
<!--
Порт `c-data-grid-26.tsx` ("Per-column aggregate footer").
Сетевые `unsplash`-аватары заменены статичным 1x1 `data:` URI (`PIXEL`).
`IconPlaceholder` заменена инлайновым `<svg>` (lucide-react v0.545.0:
"download"). Демо-данные урезаны с 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, CardTitle } 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
status: "Active" | "Inactive" | "Pending" | "Blocked"
balance: number
transactions: number
}
const demoData: IData[] = [
{ id: "1", name: "Alex Johnson", avatar: PIXEL, status: "Active", balance: 5143.03, transactions: 48 },
{ id: "2", name: "Sarah Chen", avatar: PIXEL, status: "Inactive", balance: 4321.87, transactions: 31 },
{ id: "3", name: "Michael Rodriguez", avatar: PIXEL, status: "Blocked", balance: 7654.98, transactions: 67 },
{ id: "4", name: "Emma Wilson", avatar: PIXEL, status: "Inactive", balance: 3456.45, transactions: 22 },
{ id: "5", name: "David Kim", avatar: PIXEL, status: "Active", balance: 9876.54, transactions: 93 },
{ id: "6", name: "Aron Thompson", avatar: PIXEL, status: "Pending", balance: 6214.22, transactions: 55 },
]
const pagination = ref<PaginationState>({ pageIndex: 0, pageSize: 5 })
const sorting = ref<SortingState>([{ id: "name", desc: false }])
const aggregates = computed(() => {
const n = demoData.length
const balances = demoData.map((r) => r.balance)
const txns = demoData.map((r) => r.transactions)
return {
avgBalance: balances.reduce((a, b) => a + b, 0) / n,
minBalance: Math.min(...balances),
maxBalance: Math.max(...balances),
avgTxns: Math.round(txns.reduce((a, b) => a + b, 0) / n),
minTxns: Math.min(...txns),
maxTxns: Math.max(...txns),
}
})
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),
]),
minSize: 200,
meta: {
autoSize: true,
},
enableSorting: true,
enableHiding: false,
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: 150,
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: 150,
enableSorting: true,
enableHiding: true,
enableResizing: true,
},
{
accessorKey: "transactions",
id: "transactions",
header: (ctx) => h(DataGridColumnHeader, { column: ctx.column as never, title: "Transactions", visibility: true }),
cell: (info) => h("div", { class: "text-foreground font-medium tabular-nums" }, info.row.original.transactions),
size: 150,
enableSorting: true,
enableHiding: true,
enableResizing: true,
},
]
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(),
})
</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 gap-3 px-4 py-2">
<CardTitle class="text-sm font-medium">Column Aggregates</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="M12 15V3" />
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<path d="m7 10 5 5 5-5" />
</svg>
Export
</Button>
</CardAction>
</CardHeader>
<CardContent class="border-y px-0">
<DataGridScrollArea>
<DataGridTable>
<template #footer>
<DataGridTableFootRow>
<DataGridTableFootRowCell :col-span="2">
<div class="flex flex-col gap-0.5">
<span class="text-muted-foreground">Summary</span>
<span class="text-foreground font-medium">Across all members</span>
<span class="text-muted-foreground tabular-nums">{{ demoData.length }} members</span>
</div>
</DataGridTableFootRowCell>
<DataGridTableFootRowCell>
<div class="flex flex-col gap-0.5">
<span class="text-muted-foreground">Avg</span>
<span class="tabular-nums">{{ fmt(aggregates.avgBalance) }}</span>
<span class="text-muted-foreground tabular-nums">{{ fmt(aggregates.minBalance) }} - {{ fmt(aggregates.maxBalance) }}</span>
</div>
</DataGridTableFootRowCell>
<DataGridTableFootRowCell>
<div class="flex flex-col gap-0.5">
<span class="text-muted-foreground">Avg</span>
<span class="tabular-nums">{{ aggregates.avgTxns }}</span>
<span class="text-muted-foreground tabular-nums">{{ aggregates.minTxns }} - {{ aggregates.maxTxns }}</span>
</div>
</DataGridTableFootRowCell>
</DataGridTableFootRow>
</template>
</DataGridTable>
</DataGridScrollArea>
</CardContent>
<CardFooter class="border-none bg-transparent! px-3.5 py-2">
<DataGridPagination />
</CardFooter>
</Card>
</DataGrid>
</template>