177 lines
4.7 KiB
Vue
177 lines
4.7 KiB
Vue
<template>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full border-collapse">
|
|
<!-- Table Header -->
|
|
<thead class="bg-gray-50 border-b border-gray-200">
|
|
<tr>
|
|
<th
|
|
v-for="column in columns"
|
|
:key="column.key"
|
|
:class="[
|
|
'px-6 py-3 text-left text-xs font-semibold text-gray-700 uppercase tracking-wider',
|
|
column.align === 'center' && 'text-center',
|
|
column.align === 'right' && 'text-right',
|
|
column.headerClass
|
|
]"
|
|
:style="{ width: column.width }"
|
|
>
|
|
{{ column.label }}
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<!-- Table Body -->
|
|
<tbody class="bg-white divide-y divide-gray-200">
|
|
<tr
|
|
v-for="(row, index) in data"
|
|
:key="getRowKey(row, index)"
|
|
:class="[
|
|
'hover:bg-gray-50 transition',
|
|
rowClickable && 'cursor-pointer'
|
|
]"
|
|
@click="handleRowClick(row)"
|
|
>
|
|
<td
|
|
v-for="column in columns"
|
|
:key="column.key"
|
|
:class="[
|
|
'px-6 py-4 text-sm text-gray-900',
|
|
column.align === 'center' && 'text-center',
|
|
column.align === 'right' && 'text-right',
|
|
column.cellClass
|
|
]"
|
|
>
|
|
<!-- Custom Cell Slot -->
|
|
<slot
|
|
v-if="$slots[`cell-${column.key}`]"
|
|
:name="`cell-${column.key}`"
|
|
:row="row"
|
|
:value="row[column.key]"
|
|
></slot>
|
|
|
|
<!-- Default Cell Content -->
|
|
<span v-else>{{ row[column.key] }}</span>
|
|
</td>
|
|
</tr>
|
|
|
|
<!-- Empty State -->
|
|
<tr v-if="data.length === 0">
|
|
<td :colspan="columns.length" class="px-6 py-12 text-center text-gray-500">
|
|
<slot name="empty">
|
|
<div class="text-gray-400">
|
|
<p class="text-lg mb-2">📭</p>
|
|
<p>{{ emptyMessage }}</p>
|
|
</div>
|
|
</slot>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- Pagination -->
|
|
<div v-if="showPagination && totalPages > 1" class="flex justify-between items-center px-6 py-4 border-t border-gray-200">
|
|
<div class="text-sm text-gray-600">
|
|
แสดง {{ startItem }}-{{ endItem }} จาก {{ totalItems }} รายการ
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<q-btn
|
|
flat
|
|
dense
|
|
icon="chevron_left"
|
|
:disable="currentPage === 1"
|
|
@click="changePage(currentPage - 1)"
|
|
/>
|
|
<q-btn
|
|
v-for="page in visiblePages"
|
|
:key="page"
|
|
flat
|
|
dense
|
|
:label="String(page)"
|
|
:color="page === currentPage ? 'primary' : 'grey'"
|
|
@click="changePage(page)"
|
|
/>
|
|
<q-btn
|
|
flat
|
|
dense
|
|
icon="chevron_right"
|
|
:disable="currentPage === totalPages"
|
|
@click="changePage(currentPage + 1)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
interface Column {
|
|
key: string;
|
|
label: string;
|
|
width?: string;
|
|
align?: 'left' | 'center' | 'right';
|
|
headerClass?: string;
|
|
cellClass?: string;
|
|
}
|
|
|
|
interface Props {
|
|
columns: Column[];
|
|
data: any[];
|
|
rowKey?: string;
|
|
rowClickable?: boolean;
|
|
emptyMessage?: string;
|
|
showPagination?: boolean;
|
|
currentPage?: number;
|
|
pageSize?: number;
|
|
totalItems?: number;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
rowKey: 'id',
|
|
rowClickable: false,
|
|
emptyMessage: 'ไม่พบข้อมูล',
|
|
showPagination: false,
|
|
currentPage: 1,
|
|
pageSize: 10,
|
|
totalItems: 0
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
'row-click': [row: any];
|
|
'page-change': [page: number];
|
|
}>();
|
|
|
|
const getRowKey = (row: any, index: number) => {
|
|
return row[props.rowKey] || index;
|
|
};
|
|
|
|
const handleRowClick = (row: any) => {
|
|
if (props.rowClickable) {
|
|
emit('row-click', row);
|
|
}
|
|
};
|
|
|
|
const changePage = (page: number) => {
|
|
emit('page-change', page);
|
|
};
|
|
|
|
// Pagination calculations
|
|
const totalPages = computed(() => Math.ceil(props.totalItems / props.pageSize));
|
|
const startItem = computed(() => (props.currentPage - 1) * props.pageSize + 1);
|
|
const endItem = computed(() => Math.min(props.currentPage * props.pageSize, props.totalItems));
|
|
|
|
const visiblePages = computed(() => {
|
|
const pages = [];
|
|
const maxVisible = 5;
|
|
let start = Math.max(1, props.currentPage - Math.floor(maxVisible / 2));
|
|
let end = Math.min(totalPages.value, start + maxVisible - 1);
|
|
|
|
if (end - start < maxVisible - 1) {
|
|
start = Math.max(1, end - maxVisible + 1);
|
|
}
|
|
|
|
for (let i = start; i <= end; i++) {
|
|
pages.push(i);
|
|
}
|
|
|
|
return pages;
|
|
});
|
|
</script>
|