mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-08-05 12:07:22 -04:00
179 lines
5.9 KiB
Vue
179 lines
5.9 KiB
Vue
<script setup lang="ts">
|
||
import { useDebounce } from '@vueuse/core';
|
||
|
||
const props = withDefaults(defineProps<{
|
||
endpoint: '/api/admin/users';
|
||
query?: Record<string, unknown>;
|
||
columns: number;
|
||
perPage?: number;
|
||
marked?: (el: unknown) => boolean;
|
||
fixed?: boolean;
|
||
count?: boolean;
|
||
}>(), {
|
||
query: () => ({}),
|
||
perPage: 100,
|
||
});
|
||
|
||
const debouncedQuery = useDebounce(toRef(props, 'query'));
|
||
|
||
const page = ref(0);
|
||
|
||
const tableData = useFetch(props.endpoint, {
|
||
params: computed(() => ({
|
||
...debouncedQuery.value,
|
||
limit: props.perPage,
|
||
offset: page.value * props.perPage,
|
||
})),
|
||
});
|
||
|
||
const isLoading = computed(() => {
|
||
return JSON.stringify(props.query) !== JSON.stringify(debouncedQuery.value) || tableData.status.value === 'pending';
|
||
});
|
||
|
||
const dataPage = computed(() => tableData.data.value?.data);
|
||
const rowsCount = computed(() => tableData.data.value?.count);
|
||
|
||
const pages = computed(() => {
|
||
if (rowsCount.value === undefined) {
|
||
return 0;
|
||
}
|
||
return Math.ceil(rowsCount.value / props.perPage);
|
||
});
|
||
|
||
type VPage = {
|
||
page: number;
|
||
text: string | number;
|
||
enabled: boolean;
|
||
} | {
|
||
page?: never;
|
||
text: string;
|
||
enabled: false;
|
||
};
|
||
|
||
const pagesRange = computed(() => {
|
||
const vPages: VPage[] = [];
|
||
vPages.push({ page: 0, text: '«', enabled: page.value > 0 });
|
||
vPages.push({ page: page.value - 1, text: '‹', enabled: page.value > 0 });
|
||
for (let i = 0; i < pages.value; i++) {
|
||
if (i <= 4 || (page.value - 3 <= i && i <= page.value + 3) || i >= pages.value - 3) {
|
||
vPages.push({ page: i, text: i + 1, enabled: true });
|
||
} else if (vPages[vPages.length - 1].text !== '…') {
|
||
vPages.push({ text: '…', enabled: false });
|
||
}
|
||
}
|
||
vPages.push({ page: page.value + 1, text: '›', enabled: page.value < pages.value - 1 });
|
||
vPages.push({ page: pages.value - 1, text: '»', enabled: page.value < pages.value - 1 });
|
||
return vPages;
|
||
});
|
||
|
||
watch(() => props.endpoint, () => {
|
||
page.value = 0;
|
||
});
|
||
watch(() => props.query, (after, before) => {
|
||
if (JSON.stringify(after) === JSON.stringify(before)) {
|
||
return;
|
||
}
|
||
page.value = 0;
|
||
});
|
||
|
||
const reset = () => {
|
||
page.value = 0;
|
||
};
|
||
|
||
const thead = useTemplateRef('thead');
|
||
const focus = () => {
|
||
setTimeout((_) => {
|
||
thead.value?.scrollIntoView();
|
||
}, 300);
|
||
};
|
||
|
||
defineExpose({ reset, focus });
|
||
</script>
|
||
|
||
<template>
|
||
<section class="table-responsive">
|
||
<div class="input-group ">
|
||
<span class="input-group-text">
|
||
<Spinner v-if="isLoading" size="1.25em" />
|
||
<Icon v-else v="filter" />
|
||
</span>
|
||
<slot name="filter"></slot>
|
||
</div>
|
||
|
||
<table :class="['table table-striped table-hover', fixed ? `table-fixed-${columns}` : '']">
|
||
<thead ref="thead">
|
||
<tr>
|
||
<td :colspan="columns">
|
||
<nav v-if="pages > 1">
|
||
<ul class="pagination pagination-sm justify-content-center mb-0">
|
||
<li v-for="p in pagesRange" :class="['page-item', p.page === page ? 'active' : '', p.enabled ? '' : 'disabled']">
|
||
<a v-if="p.enabled" class="page-link" href="#" @click.prevent="page = p.page">
|
||
{{ p.text }}
|
||
</a>
|
||
<span v-else class="page-link">
|
||
{{ p.text }}
|
||
</span>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</td>
|
||
</tr>
|
||
<tr v-if="count && rowsCount">
|
||
<td :colspan="columns">
|
||
<T>table.count</T><T>quotation.colon</T>
|
||
<strong>{{ rowsCount }}</strong>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<slot name="header"></slot>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<template v-if="dataPage !== undefined">
|
||
<template v-if="rowsCount">
|
||
<tr v-for="el in dataPage" :key="el.id" :class="{ marked: marked ? marked(el) : false }">
|
||
<slot name="row" :el="el"></slot>
|
||
</tr>
|
||
</template>
|
||
<template v-else>
|
||
<tr>
|
||
<td :colspan="columns" class="text-center">
|
||
<slot name="empty">
|
||
<Icon v="search" />
|
||
<T>table.empty</T>
|
||
</slot>
|
||
</td>
|
||
</tr>
|
||
</template>
|
||
</template>
|
||
</tbody>
|
||
<tfoot>
|
||
<tr>
|
||
<td :colspan="columns + 1">
|
||
<nav v-if="pages > 1">
|
||
<ul class="pagination pagination-sm justify-content-center">
|
||
<li v-for="p in pagesRange" :class="['page-item', p.page === page ? 'active' : '', p.enabled ? '' : 'disabled']">
|
||
<a v-if="p.enabled" class="page-link" href="#" @click.prevent="page = p.page">
|
||
{{ p.text }}
|
||
</a>
|
||
<span v-else class="page-link">
|
||
{{ p.text }}
|
||
</span>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
</td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</section>
|
||
</template>
|
||
|
||
<style lang="scss">
|
||
@import "assets/variables";
|
||
|
||
.marked {
|
||
border-inline-start: 3px solid $primary;
|
||
}
|
||
</style>
|