Files

400 lines
15 KiB
Vue
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, watch, onMounted } from 'vue';
import Pagination from './Pagination.vue';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import Toolbar from 'primevue/toolbar';
import Dropdown from 'primevue/dropdown';
import MultiSelect from 'primevue/multiselect';
const props = defineProps({
data: {
type: Array,
required: true,
default: () => []
},
columns: {
type: Array,
required: true,
default: () => []
},
selectable: {
type: Boolean,
default: false
},
modelValue: {
type: Array,
default: () => []
},
pagination: {
type: Object,
default: () => null
},
searchable: {
type: Boolean,
default: true
},
searchValue: {
type: String,
default: ''
},
filters: {
type: Object,
default: () => ({})
},
filterLoader: {
type: Boolean,
default: false
},
});
const emit = defineEmits(['update:modelValue', 'selection-change', 'page-change', 'search-change', 'filter-change', 'apply-filters', 'clear-filters']);
const localSelection = ref(props.modelValue);
watch(() => props.modelValue, (newValue) => {
localSelection.value = newValue;
}, { immediate: true });
const handleSelectionChange = (event) => {
localSelection.value = event;
emit('update:modelValue', event);
emit('selection-change', event);
};
const localSearchValue = ref(props.searchValue);
watch(() => props.searchValue, (newValue) => {
localSearchValue.value = newValue;
});
let searchDebounceTimer = null;
const handleSearchInput = () => {
if (searchDebounceTimer) {
clearTimeout(searchDebounceTimer);
}
searchDebounceTimer = setTimeout(() => {
emit('search-change', localSearchValue.value);
}, 1500);
};
const localFilters = ref({ ...props.filters });
const dynamicOptions = ref({});
const loadingOptions = ref({});
const searchDebounceTimers = ref({});
const searchValues = ref({});
onMounted(() => {
props.columns.forEach(column => {
if (column.filter?.dynamic && props.filters[column.key]) {
const filterValue = props.filters[column.key];
searchValues.value[column.key] = filterValue.toString();
loadDynamicOptions(column, true, filterValue.toString(), true);
}
else if (column.filter?.dynamic) {
searchValues.value[column.key] = '';
loadDynamicOptions(column, true, '', false);
}
});
});
watch(() => props.filters, (newValue) => {
Object.keys(newValue).forEach(key => {
if (newValue[key] !== undefined && newValue[key] !== null) {
localFilters.value[key] = newValue[key];
}
});
}, { immediate: true, deep: true });
const handleFilterChange = (columnKey, value) => {
localFilters.value[columnKey] = value;
emit('filter-change', { ...localFilters.value });
};
const clearAllFilters = () => {
localFilters.value = {};
dynamicOptions.value = {};
searchValues.value = {};
emit('clear-filters');
};
const loadDynamicOptions = async (column, forceReload = false, searchTerm = '', shouldSelectValue = false) => {
if (!column.filter?.dynamic || !column.filter?.url) return;
const columnKey = column.key;
loadingOptions.value[columnKey] = true;
try {
let url = column.filter.url;
if (searchTerm) {
url += `?search=${encodeURIComponent(searchTerm)}`;
}
const response = await fetch(url);
const data = await response.json();
if (data.success && data.data) {
dynamicOptions.value[columnKey] = data.data;
if (shouldSelectValue && props.filters[columnKey]) {
const filterValue = props.filters[columnKey];
const foundOption = data.data.find(option => option.value == filterValue);
if (foundOption) {
localFilters.value[columnKey] = foundOption.value;
}
}
} else {
dynamicOptions.value[columnKey] = [];
}
} catch (error) {
console.error('Error:', error);
dynamicOptions.value[columnKey] = [];
} finally {
loadingOptions.value[columnKey] = false;
}
};
const handleFilterSearch = (column, searchTerm) => {
if (!column.filter?.dynamic || !column.filter?.search) return;
const columnKey = column.key;
searchValues.value[columnKey] = searchTerm;
if (searchDebounceTimers.value[columnKey]) {
clearTimeout(searchDebounceTimers.value[columnKey]);
}
searchDebounceTimers.value[columnKey] = setTimeout(() => {
loadDynamicOptions(column, true, searchTerm, false);
}, 1500);
};
const renderFilter = (column) => {
if (!column.filter) return null;
const filterConfig = column.filter;
const currentValue = localFilters.value[column.key] || null;
const columnKey = column.key;
let options = [];
if (filterConfig.dynamic) {
options = dynamicOptions.value[columnKey] || [];
} else if (filterConfig.options) {
options = filterConfig.options;
}
if (filterConfig.type === 'dropdown') {
return {
type: 'dropdown',
options: loadingOptions.value[columnKey] ? [{ label: 'در حال پردازش...', value: null, disabled: true }] : options,
value: currentValue,
placeholder: filterConfig.placeholder || 'انتخاب کنید...',
loading: loadingOptions.value[columnKey] || false,
dynamic: filterConfig.dynamic || false,
search: filterConfig.search || false,
searchValue: searchValues.value[columnKey] || ''
};
} else if (filterConfig.type === 'multiselect') {
return {
type: 'multiselect',
options: loadingOptions.value[columnKey] ? [{ label: 'در حال پردازش...', value: null, disabled: true }] : options,
value: currentValue || [],
placeholder: filterConfig.placeholder || 'انتخاب کنید...',
loading: loadingOptions.value[columnKey] || false,
dynamic: filterConfig.dynamic || false,
search: filterConfig.search || false,
searchValue: searchValues.value[columnKey] || ''
};
}
return null;
};
const renderColumnContent = (column, data) => {
let content = '';
if (column.render && typeof column.render === 'function') {
content = column.render(data);
} else {
content = data[column.key];
}
const defaultStyle = '!font-medium !text-sm';
const customStyle = column.style || defaultStyle;
return `<span class="${customStyle} text-right wrap-break-word text-sm">${content}</span>`;
};
const getColumnStyle = (column) => {
const baseStyle = { minWidth: '120px' };
if (column.width) {
baseStyle.width = column.width + 'px';
baseStyle.minWidth = column.width + 'px';
baseStyle.maxWidth = column.width + 'px';
}
return baseStyle;
};
</script>
<template>
<div class="table-generic">
<div v-if="searchable" class="p-4 pb-2">
<Toolbar class="">
<template #start>
<div class="grid grid-cols-2a max-sm:grid-cols-1 justify-start gap-2">
<div class="grid grid-cols-1 items-center justify-start">
<InputText
size="small"
placeholder="جستجو..."
type="text"
v-model="localSearchValue"
@input="handleSearchInput"
/>
</div>
<div v-if="columns.some(col => col.filter)" class="">
<div class="flex flex-wrap max-sm:!flex-col gap-2 py-2">
<div
v-for="column in columns.filter(col => col.filter)"
:key="`filter-${column.key}`"
class="flex items-center gap-2"
>
<Dropdown
v-if="renderFilter(column)?.type === 'dropdown'"
:modelValue="renderFilter(column).value"
:options="renderFilter(column).options"
:placeholder="renderFilter(column).loading ? 'در حال بارگذاری...' : renderFilter(column).placeholder"
optionLabel="label"
optionValue="value"
size="small"
class="min-w-[130px] max-sm:!w-full max-w-[200px]"
:loading="renderFilter(column).loading"
:disabled="renderFilter(column).loading"
:filter="renderFilter(column).search"
:filterValue="renderFilter(column).searchValue"
:filterPlaceholder="renderFilter(column).search ? 'جستجو...' : ''"
emptyMessage="داده ای یافت نشد"
:filterMatchModeOptions="[]"
:filterFunction="() => true"
@update:modelValue="(value) => handleFilterChange(column.key, value)"
@filter="(event) => handleFilterSearch(column, event.value)"
showClear
/>
<MultiSelect
v-else-if="renderFilter(column)?.type === 'multiselect'"
:modelValue="renderFilter(column).value"
:options="renderFilter(column).options"
:placeholder="renderFilter(column).loading ? 'در حال بارگذاری...' : renderFilter(column).placeholder"
optionLabel="label"
optionValue="value"
size="small"
class="min-w-[150px] max-sm:!w-full max-w-[200px]"
:loading="renderFilter(column).loading"
:disabled="renderFilter(column).loading"
:filter="renderFilter(column).search"
:filterValue="renderFilter(column).searchValue"
:filterPlaceholder="renderFilter(column).search ? 'جستجو...' : ''"
emptyMessage="داده ای یافت نشد"
:filterMatchModeOptions="[]"
:filterFunction="() => true"
@update:modelValue="(value) => handleFilterChange(column.key, value)"
@filter="(event) => handleFilterSearch(column, event.value)"
showClear
/>
</div>
<div class="flex items-center gap-2">
<Button
:label="filterLoader ? 'در حال پردازش...' : 'اعمال فیلتر'"
size="small"
severity="primary"
class="!text-xs h-10"
:loading="filterLoader"
:disabled="filterLoader"
@click="$emit('apply-filters')"
/>
<Button
label="پاک کردن"
size="small"
severity="secondary"
class="!text-xs h-10"
outlined
@click="clearAllFilters"
/>
</div>
</div>
</div>
</div>
<div v-if="localSelection && localSelection.length > 0" class="mr-2 !h-9 grid grid-cols-1">
<slot name="bulk-action" :selection="localSelection"></slot>
</div>
</template>
<template #end>
<slot name="toolbar-actions"></slot>
</template>
</Toolbar>
</div>
<DataTable
:value="data"
dataKey="id"
v-model:selection="localSelection"
:showGridlines="false"
:stripedRows="false"
@selection-change="handleSelectionChange"
>
<template #empty>
<div class="flex justify-center py-4 items-center h-full">
<span class="text-center text-sm">هیچ داده‌ای پیدا نشده!</span>
</div>
</template>
<Column
v-if="selectable"
selectionMode="multiple"
headerStyle="width: 4rem"
/>
<Column
v-for="column in columns"
:key="column.key"
:field="column.key"
:header="column.title"
:sortable="column.hasSort === true"
:style="getColumnStyle(column)"
>
<template #body="{ data }">
<slot v-if="column.slot" :name="`column-${column.key}`" :data="data" :column="column">
<div v-html="renderColumnContent(column, data)"></div>
</slot>
<div v-else v-html="renderColumnContent(column, data)"></div>
</template>
</Column>
</DataTable>
<Pagination
v-if="pagination && pagination.total"
:total="pagination.total"
:current-page="pagination.current_page"
:perPage="pagination.per_page"
:lastPage="pagination.last_page"
:links="pagination.links"
@page-change="(page) => emit('page-change', page)"
/>
</div>
</template>
<style scoped>
.table-generic {
width: 100%;
}
</style>