data-grid
Data grid with column totals footer
Data grid with column totals footer
Загрузка превью…
src/data-grid/c-data-grid-24.vue
<!--
Порт `c-data-grid-24.tsx` ("Data grid with a column totals footer row").
Расхождение API: оригинальный `DataGridTable` принимает `footerContent`
пропом (готовый ReactNode). Vue-порт (`DataGridTable.vue`) вместо этого
объявляет именованный слот `footer` (см. `<tfoot v-if="$slots.footer">` —
`packages/ui/src/reui/data-grid/DataGridTable.vue`), тот же приём
render-prop -> именованный слот, что и везде в порте (docs/PORTING.md §2).
Прочие расхождения, как в `c-data-grid-22.vue`/`c-data-grid-23.vue`:
- Аватарки (`images.unsplash.com`) заменены локальным 1x1 data-URI
(PIXEL) — сетевые запросы недетерминированы в headless-стенде.
- `useCopyToClipboard` реализован тривиально в блоке
(`navigator.clipboard.writeText`).
- `toast.success(...)` из `sonner` не перенесён (нет vue-sonner порта,
не влияет на статичный скриншот — тост никогда не монтируется в
состоянии покоя).
- `IconPlaceholder` заменена инлайновым `<svg>` (lucide-react v0.545.0:
"table", "download", "more-horizontal"/"ellipsis").
- Демо-данные урезаны с 10 до 7 строк.
-->
<script setup lang="ts">
import { h } from "vue"
import { Badge } from "@/components/reui/badge"
import { DataGrid, DataGridColumnHeader, DataGridPagination, DataGridScrollArea, DataGridTable, DataGridTableFootRow, DataGridTableFootRowCell, DataGridTableRowSelect, DataGridTableRowSelectAll } 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 } from "@/components/ui/card"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import {
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useVueTable,
} from "@tanstack/vue-table"
import type { ColumnDef, PaginationState, Row, SortingState } from "@tanstack/vue-table"
const PIXEL =
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="
type Status = "Active" | "Inactive" | "Pending" | "Blocked"
interface IData {
id: string
name: string
avatar: string
role: string
status: Status
balance: number
}
const demoData: IData[] = [
{ id: "1", name: "Alex Johnson", avatar: PIXEL, role: "CEO", status: "Active", balance: 5143.03 },
{ id: "2", name: "Sarah Chen", avatar: PIXEL, role: "CTO", status: "Inactive", balance: 4321.87 },
{ id: "3", name: "Michael Rodriguez", avatar: PIXEL, role: "Designer", status: "Blocked", balance: 7654.98 },
{ id: "4", name: "Emma Wilson", avatar: PIXEL, role: "Developer", status: "Inactive", balance: 3456.45 },
{ id: "5", name: "David Kim", avatar: PIXEL, role: "Lawyer", status: "Active", balance: 9876.54 },
{ id: "6", name: "Aron Thompson", avatar: PIXEL, role: "Director", status: "Pending", balance: 6214.22 },
{ id: "7", name: "James Brown", avatar: PIXEL, role: "Product Manager", status: "Inactive", balance: 5321.77 },
]
function copyToClipboard(text: string) {
if (typeof navigator === "undefined" || !navigator.clipboard?.writeText) return
navigator.clipboard.writeText(text)
}
function MoreHorizontalIcon() {
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",
class: "lucide lucide-ellipsis",
},
[
h("circle", { cx: "12", cy: "12", r: "1" }),
h("circle", { cx: "19", cy: "12", r: "1" }),
h("circle", { cx: "5", cy: "12", r: "1" }),
]
)
}
function renderActionsCell(row: Row<IData>) {
return h(DropdownMenu, {}, () => [
h(DropdownMenuTrigger, { asChild: true }, () =>
h(Button, { class: "size-7", size: "icon", variant: "ghost" }, () => h(MoreHorizontalIcon))
),
h(DropdownMenuContent, { side: "bottom", align: "start" }, () => [
h(DropdownMenuItem, { onClick: () => {} }, () => "Edit"),
h(DropdownMenuItem, { onClick: () => copyToClipboard(row.original.id) }, () => "Copy ID"),
h(DropdownMenuSeparator),
h(DropdownMenuItem, { variant: "destructive", onClick: () => {} }, () => "Delete"),
]),
])
}
const pagination: PaginationState = { pageIndex: 0, pageSize: 5 }
const sorting: SortingState = [{ id: "name", desc: false }]
const totalBalance = demoData.reduce((sum, row) => sum + row.balance, 0)
const columns: ColumnDef<IData>[] = [
{
accessorKey: "id",
id: "id",
header: () => h(DataGridTableRowSelectAll),
cell: (ctx) => h(DataGridTableRowSelect, { row: ctx.row }),
enableSorting: false,
size: 35,
enableResizing: false,
},
{
accessorKey: "name",
id: "name",
header: (ctx) => h(DataGridColumnHeader, { title: "User", visibility: true, column: ctx.column as never }),
cell: (ctx) =>
h("div", { class: "flex items-center gap-3" }, [
h(Avatar, { class: "size-8" }, () => [
h(AvatarImage, { src: ctx.row.original.avatar, alt: ctx.row.original.name }),
h(AvatarFallback, () => ctx.row.original.name.split(" ").map((n) => n[0]).join("")),
]),
h("div", { class: "text-foreground font-medium" }, ctx.row.original.name),
]),
minSize: 200,
meta: { autoSize: true },
enableSorting: true,
enableHiding: false,
enableResizing: true,
},
{
accessorKey: "role",
id: "role",
header: (ctx) => h(DataGridColumnHeader, { title: "Role", visibility: true, column: ctx.column as never }),
cell: (ctx) => h("div", { class: "text-foreground font-medium" }, ctx.row.original.role),
size: 150,
enableSorting: true,
enableHiding: true,
enableResizing: true,
},
{
accessorKey: "status",
id: "status",
header: (ctx) => h(DataGridColumnHeader, { title: "Status", visibility: true, column: ctx.column as never }),
cell: (ctx) => {
const s = ctx.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: 110,
enableSorting: true,
enableHiding: true,
enableResizing: true,
},
{
accessorKey: "balance",
id: "balance",
header: (ctx) => h(DataGridColumnHeader, { title: "Balance", visibility: true, column: ctx.column as never }),
cell: (ctx) =>
h(
"div",
{ class: "text-foreground font-medium tabular-nums" },
`$${ctx.row.original.balance.toLocaleString("en-US", { minimumFractionDigits: 2 })}`
),
size: 130,
enableSorting: true,
enableHiding: true,
enableResizing: true,
},
{
id: "actions",
header: "",
cell: (ctx) => renderActionsCell(ctx.row),
size: 60,
enableSorting: false,
enableHiding: false,
enableResizing: false,
},
]
const table = useVueTable({
get data() {
return demoData
},
columns,
get pageCount() {
return Math.ceil(demoData.length / pagination.pageSize)
},
getRowId: (row: IData) => row.id,
state: {
get pagination() {
return pagination
},
get sorting() {
return sorting
},
get columnOrder() {
return columns.map((c) => c.id as string)
},
},
onColumnOrderChange: () => {},
onPaginationChange: () => {},
onSortingChange: () => {},
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getSortedRowModel: getSortedRowModel(),
})
const visibleCount = table.getVisibleLeafColumns().length
</script>
<template>
<DataGrid
:table="table"
:record-count="demoData.length"
:table-layout="{ columnsPinnable: true, columnsResizable: true, columnsVisibility: true }"
>
<Card class="w-full gap-0 py-0">
<CardHeader class="flex items-center justify-between px-3.5 py-2">
<div class="flex items-center gap-2">
<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" class="text-muted-foreground size-4">
<path d="M12 3v18" />
<rect width="18" height="18" x="3" y="3" rx="2" />
<path d="M3 9h18" />
<path d="M3 15h18" />
</svg>
<span class="text-foreground text-sm font-medium">Employee Balances</span>
</div>
<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="visibleCount - 2">
<span class="text-muted-foreground">Total balance</span>
</DataGridTableFootRowCell>
<DataGridTableFootRowCell class="font-bold tabular-nums">
${{ totalBalance.toLocaleString("en-US", { minimumFractionDigits: 2 }) }}
</DataGridTableFootRowCell>
<DataGridTableFootRowCell />
</DataGridTableFootRow>
</template>
</DataGridTable>
</DataGridScrollArea>
</CardContent>
<CardFooter class="border-none bg-transparent! px-3.5 py-2">
<DataGridPagination />
</CardFooter>
</Card>
</DataGrid>
</template>
Установка
npx shadcn-vue@latest add https://revueui.rootapi.dev/r/c-data-grid-24.jsonЗависимости реестра
npm-зависимости
- @tanstack/vue-table
Источник: порт из ReUI (Keenthemes, MIT)