258 lines
6.1 KiB
Vue
258 lines
6.1 KiB
Vue
<script setup>
|
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useArrayData } from 'composables/useArrayData';
|
|
|
|
const { t } = useI18n();
|
|
|
|
const props = defineProps({
|
|
dataKey: {
|
|
type: String,
|
|
required: true,
|
|
},
|
|
class: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
autoLoad: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
data: {
|
|
type: Array,
|
|
default: null,
|
|
},
|
|
url: {
|
|
type: String,
|
|
default: '',
|
|
},
|
|
filter: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
userFilter: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
where: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
order: {
|
|
type: [String, Array],
|
|
default: '',
|
|
},
|
|
limit: {
|
|
type: Number,
|
|
default: 20,
|
|
},
|
|
userParams: {
|
|
type: Object,
|
|
default: null,
|
|
},
|
|
keepOpts: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
offset: {
|
|
type: Number,
|
|
default: undefined,
|
|
},
|
|
skeleton: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
exprBuilder: {
|
|
type: Function,
|
|
default: null,
|
|
},
|
|
searchUrl: {
|
|
type: String,
|
|
default: null,
|
|
},
|
|
disableInfiniteScroll: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
const emit = defineEmits(['onFetch', 'onPaginate', 'onChange']);
|
|
const isLoading = ref(false);
|
|
const mounted = ref(false);
|
|
const pagination = ref({
|
|
sortBy: props.order,
|
|
rowsPerPage: props.limit,
|
|
page: 1,
|
|
});
|
|
|
|
const arrayData = useArrayData(props.dataKey, {
|
|
url: props.url,
|
|
filter: props.filter,
|
|
userFilter: props.userFilter,
|
|
where: props.where,
|
|
limit: props.limit,
|
|
order: props.order,
|
|
userParams: props.userParams,
|
|
exprBuilder: props.exprBuilder,
|
|
keepOpts: props.keepOpts,
|
|
searchUrl: props.searchUrl,
|
|
});
|
|
const store = arrayData.store;
|
|
|
|
onMounted(async () => {
|
|
if (props.autoLoad && !store.data?.length) await fetch();
|
|
mounted.value = true;
|
|
});
|
|
|
|
onBeforeUnmount(() => arrayData.reset());
|
|
|
|
watch(
|
|
() => props.data,
|
|
() => {
|
|
store.data = props.data;
|
|
}
|
|
);
|
|
|
|
watch(
|
|
() => store.data,
|
|
(data) => {
|
|
if (!mounted.value) return;
|
|
emit('onChange', data);
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
watch(
|
|
() => [props.url, props.filter],
|
|
([url, filter]) => mounted.value && fetch({ url, filter })
|
|
);
|
|
const addFilter = async (filter, params) => {
|
|
await arrayData.addFilter({ filter, params });
|
|
};
|
|
|
|
async function fetch(params) {
|
|
useArrayData(props.dataKey, params);
|
|
arrayData.reset(['filter.skip', 'skip']);
|
|
await arrayData.fetch({ append: false });
|
|
if (!store.hasMoreData) isLoading.value = false;
|
|
|
|
emit('onFetch', store.data);
|
|
return store.data;
|
|
}
|
|
|
|
async function paginate() {
|
|
const { page, rowsPerPage, sortBy, descending } = pagination.value;
|
|
|
|
if (!props.url) return;
|
|
|
|
isLoading.value = true;
|
|
await arrayData.loadMore();
|
|
if (!store.hasMoreData) {
|
|
if (store.userParamsChanged) store.hasMoreData = true;
|
|
store.userParamsChanged = false;
|
|
endPagination();
|
|
return;
|
|
}
|
|
|
|
pagination.value.rowsNumber = store.data.length;
|
|
pagination.value.page = page;
|
|
pagination.value.rowsPerPage = rowsPerPage;
|
|
pagination.value.sortBy = sortBy;
|
|
pagination.value.descending = descending;
|
|
|
|
endPagination();
|
|
}
|
|
|
|
function endPagination() {
|
|
isLoading.value = false;
|
|
emit('onFetch', store.data);
|
|
emit('onPaginate');
|
|
}
|
|
async function onLoad(index, done) {
|
|
if (!store.data || !mounted.value) return done();
|
|
|
|
if (store.data.length === 0 || !props.url) return done(false);
|
|
|
|
pagination.value.page = pagination.value.page + 1;
|
|
|
|
await paginate();
|
|
let isDone = false;
|
|
if (store.userParamsChanged) isDone = !store.hasMoreData;
|
|
done(isDone);
|
|
}
|
|
|
|
defineExpose({ fetch, addFilter, paginate });
|
|
</script>
|
|
|
|
<template>
|
|
<div class="full-width">
|
|
<div
|
|
v-if="!props.autoLoad && !store.data && !isLoading"
|
|
class="info-row q-pa-md text-center"
|
|
>
|
|
<h5>
|
|
{{ t('No data to display') }}
|
|
</h5>
|
|
</div>
|
|
<div
|
|
v-if="props.skeleton && props.autoLoad && !store.data"
|
|
class="card-list q-gutter-y-md"
|
|
>
|
|
<QCard class="card" v-for="$index in props.limit" :key="$index">
|
|
<QItem v-ripple class="q-pa-none items-start cursor-pointer q-hoverable">
|
|
<QItemSection class="q-pa-md">
|
|
<QSkeleton type="rect" class="q-mb-md" square />
|
|
<QSkeleton type="text" square />
|
|
<QSkeleton type="text" class="q-mb-md" square />
|
|
<QSkeleton type="text" square />
|
|
<QSkeleton type="text" square />
|
|
</QItemSection>
|
|
<QSeparator vertical />
|
|
<QCardActions vertical class="justify-between">
|
|
<QSkeleton type="circle" class="q-mb-md" size="40px" />
|
|
<QSkeleton type="circle" class="q-mb-md" size="40px" />
|
|
<QSkeleton type="circle" class="q-mb-md" size="40px" />
|
|
</QCardActions>
|
|
</QItem>
|
|
</QCard>
|
|
</div>
|
|
</div>
|
|
<QInfiniteScroll
|
|
v-if="store.data"
|
|
@load="onLoad"
|
|
:offset="offset"
|
|
:class="['full-width', props.class]"
|
|
:disable="disableInfiniteScroll || !store.hasMoreData"
|
|
v-bind="$attrs"
|
|
>
|
|
<slot name="body" :rows="store.data"></slot>
|
|
<div v-if="isLoading" class="spinner info-row q-pa-md text-center">
|
|
<QSpinner color="primary" size="md" />
|
|
</div>
|
|
</QInfiniteScroll>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.spinner {
|
|
z-index: 1;
|
|
align-content: end;
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
}
|
|
.info-row {
|
|
width: 100%;
|
|
|
|
h5 {
|
|
margin: 0;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<i18n>
|
|
es:
|
|
No data to display: Sin datos que mostrar
|
|
No results found: No se han encontrado resultados
|
|
Load more data: Cargar más resultados
|
|
</i18n>
|