Files
gameno-dashboard/src/components/common/DataTableWrapper.vue
T
kavehhn 1a34c3b133 Initial commit: admin dashboard for GameNo.
Vue 3 + Vite dashboard with PrimeVue, i18n, and API integration for institution management.
2026-08-09 04:18:09 +02:00

189 lines
4.8 KiB
Vue

<!-- /src/components/common/DataTableWrapper.vue -->
<template>
<div class="data-table-wrapper surface-card p-4 border-round border-1 border-color shadow-sm">
<!-- Toolbar / Search / Filter Header -->
<div class="flex flex-column sm:flex-row align-items-center justify-content-between gap-3 mb-4">
<div class="p-input-icon-right w-full sm:w-20rem">
<IconField iconPosition="right">
<InputIcon class="pi pi-search" />
<InputText
v-model="searchValue"
:placeholder="$t('app.search')"
class="w-full text-sm"
@input="onSearchInput"
/>
</IconField>
</div>
<div class="flex align-items-center gap-2 w-full sm:w-auto justify-content-end">
<slot name="toolbar" />
</div>
</div>
<!-- Skeleton loading -->
<TableSkeleton v-if="loading" :rows="skeletonRowCount" :columns="skeletonColumnCount" />
<!-- Data Table -->
<DataTable
v-else
:value="items"
:lazy="true"
:sortField="sortBy"
:sortOrder="sortOrder === 'asc' ? 1 : -1"
:selection="selection"
dataKey="_id"
responsiveLayout="scroll"
class="p-datatable-sm text-sm"
@sort="onSort"
@update:selection="onSelectionChange"
>
<template #empty>
<EmptyState />
</template>
<Column v-if="selectable" selectionMode="multiple" headerStyle="width: 3rem" />
<slot />
</DataTable>
<!-- Custom Paginator -->
<div
class="flex flex-column sm:flex-row align-items-center justify-content-between gap-3 mt-4 pt-3 border-top-1 border-color"
v-if="totalCount > 0 || loading"
>
<span class="text-muted text-sm">
<template v-if="loading">در حال بارگذاری</template>
<template v-else>
نمایش {{ toPersianDigits(firstItemIndex) }} تا {{ toPersianDigits(lastItemIndex) }} از {{ toPersianDigits(totalCount) }} مورد
</template>
</span>
<Paginator
:rows="limit"
:totalRecords="Math.max(totalCount, 1)"
:first="(page - 1) * limit"
:rowsPerPageOptions="[10, 20, 50, 100]"
:disabled="loading"
@page="onPage"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
import { usePersianDate } from '@/composables/usePersianDate';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import Paginator from 'primevue/paginator';
import InputText from 'primevue/inputtext';
import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon';
import EmptyState from './EmptyState.vue';
import TableSkeleton from './TableSkeleton.vue';
const props = defineProps({
items: {
type: Array,
required: true
},
totalCount: {
type: Number,
default: 0
},
page: {
type: Number,
default: 1
},
limit: {
type: Number,
default: 20
},
sortBy: {
type: String,
default: 'createdAt'
},
sortOrder: {
type: String,
default: 'desc'
},
loading: {
type: Boolean,
default: false
},
selectable: {
type: Boolean,
default: false
},
selection: {
type: Array,
default: () => []
},
skeletonColumns: {
type: Number,
default: 5
}
});
const emit = defineEmits(['page-change', 'sort-change', 'search-change', 'update:selection']);
const { toPersianDigits } = usePersianDate();
const searchValue = ref('');
const skeletonColumnCount = computed(() => Math.max(3, props.skeletonColumns || 5));
const skeletonRowCount = computed(() => Math.min(Math.max(props.limit || 20, 5), 12));
const onSelectionChange = (value) => {
emit('update:selection', value || []);
};
let searchTimer = null;
const onSearchInput = () => {
if (searchTimer) clearTimeout(searchTimer);
searchTimer = setTimeout(() => {
emit('search-change', searchValue.value);
}, 400);
};
const onSort = (event) => {
emit('sort-change', {
sortBy: event.sortField,
sortOrder: event.sortOrder === 1 ? 'asc' : 'desc'
});
};
const onPage = (event) => {
emit('page-change', {
page: event.page + 1,
limit: event.rows
});
};
const firstItemIndex = computed(() => {
if (props.totalCount === 0) return 0;
return (props.page - 1) * props.limit + 1;
});
const lastItemIndex = computed(() => {
return Math.min(props.page * props.limit, props.totalCount);
});
watch(
() => props.items,
() => {
if (props.selectable && props.selection?.length) {
const ids = new Set(props.items.map((item) => String(item._id || item.id)));
const next = props.selection.filter((item) => ids.has(String(item._id || item.id)));
if (next.length !== props.selection.length) {
emit('update:selection', next);
}
}
}
);
</script>
<style scoped lang="scss">
.data-table-wrapper {
background-color: var(--surface-card);
}
</style>