forked from BRETT/BRETT
124 lines
4.2 KiB
Vue
124 lines
4.2 KiB
Vue
<script setup lang="ts" generic="TData">
|
|
import type { ColumnDef, Row } from '@tanstack/vue-table'
|
|
import { FlexRender, useTable } from '@tanstack/vue-table'
|
|
import { dataTableFeatures, type DataTableFeatures } from '@/lib/table'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Input } from '@/components/ui/input'
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableEmpty,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table'
|
|
|
|
const props = withDefaults(defineProps<{
|
|
columns: ColumnDef<DataTableFeatures, TData, unknown>[]
|
|
data: TData[]
|
|
// Suchfeld über den Global-Filter — searchFn entscheidet, was gematcht wird
|
|
searchPlaceholder?: string
|
|
searchFn?: (row: TData, query: string) => boolean
|
|
pageSize?: number
|
|
}>(), {
|
|
pageSize: 10,
|
|
})
|
|
|
|
const searchFn = props.searchFn
|
|
|
|
const table = useTable<DataTableFeatures, TData>({
|
|
features: dataTableFeatures,
|
|
get columns() { return props.columns },
|
|
get data() { return props.data },
|
|
globalFilterFn: searchFn
|
|
? (row: Row<DataTableFeatures, TData>, _columnId: string, filterValue: unknown) =>
|
|
searchFn(row.original, String(filterValue))
|
|
: 'auto',
|
|
initialState: {
|
|
pagination: { pageSize: props.pageSize },
|
|
},
|
|
})
|
|
|
|
const pageIndex = computed(() => table.atoms.pagination.get().pageIndex)
|
|
const pageCount = computed(() => table.getPageCount())
|
|
const globalFilter = computed(() => String(table.atoms.globalFilter.get() ?? ''))
|
|
</script>
|
|
|
|
<template>
|
|
<div class="flex flex-col gap-3">
|
|
<div v-if="searchPlaceholder" class="relative max-w-xs">
|
|
<Icon name="ph:magnifying-glass-fill" class="absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
:model-value="globalFilter"
|
|
:placeholder="searchPlaceholder"
|
|
class="pl-8"
|
|
@update:model-value="table.setGlobalFilter(String($event))"
|
|
/>
|
|
</div>
|
|
|
|
<div class="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow v-for="headerGroup in table.getHeaderGroups()" :key="headerGroup.id">
|
|
<TableHead v-for="header in headerGroup.headers" :key="header.id">
|
|
<slot :name="`header-${header.column.id}`" :header="header" :column="header.column">
|
|
<button
|
|
v-if="header.column.getCanSort()"
|
|
type="button"
|
|
class="flex items-center gap-1.5 font-medium transition-colors hover:text-foreground"
|
|
@click="header.column.toggleSorting()"
|
|
>
|
|
<FlexRender :header="header" />
|
|
<Icon name="ph:arrow-up-fill" v-if="header.column.getIsSorted() === 'asc'" class="size-3.5" />
|
|
<Icon name="ph:arrow-down-fill" v-else-if="header.column.getIsSorted() === 'desc'" class="size-3.5" />
|
|
<Icon name="ph:arrows-down-up-fill" v-else class="size-3.5 opacity-50" />
|
|
</button>
|
|
<FlexRender v-else :header="header" />
|
|
</slot>
|
|
</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
<template v-if="table.getRowModel().rows.length">
|
|
<TableRow v-for="row in table.getRowModel().rows" :key="row.id">
|
|
<TableCell v-for="cell in row.getAllCells()" :key="cell.id">
|
|
<slot :name="`cell-${cell.column.id}`" :row="row" :cell="cell">
|
|
<FlexRender :cell="cell" />
|
|
</slot>
|
|
</TableCell>
|
|
</TableRow>
|
|
</template>
|
|
<TableEmpty v-else :colspan="columns.length">
|
|
Keine Einträge.
|
|
</TableEmpty>
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
|
|
<div v-if="pageCount > 1" class="flex items-center justify-between">
|
|
<span class="text-sm text-muted-foreground">
|
|
Seite {{ pageIndex + 1 }} von {{ pageCount }}
|
|
</span>
|
|
<div class="flex gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!table.getCanPreviousPage()"
|
|
@click="table.previousPage()"
|
|
>
|
|
Zurück
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
:disabled="!table.getCanNextPage()"
|
|
@click="table.nextPage()"
|
|
>
|
|
Weiter
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|