mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-04-11 10:53:52 +02:00
271 lines
7.8 KiB
Vue
271 lines
7.8 KiB
Vue
<template>
|
|
<div class="data-table-wrapper">
|
|
<el-table
|
|
v-loading="loading"
|
|
:data="paginatedData"
|
|
v-bind="mergedTableProps"
|
|
:default-sort="resolvedDefaultSort"
|
|
lazy
|
|
@sort-change="handleSortChange"
|
|
@selection-change="handleSelectionChange"
|
|
@row-click="handleRowClick">
|
|
<slot></slot>
|
|
</el-table>
|
|
|
|
<div v-if="showPagination" class="pagination-wrapper">
|
|
<el-pagination
|
|
size="small"
|
|
:current-page="internalCurrentPage"
|
|
:page-size="effectivePageSize"
|
|
:total="totalItems"
|
|
v-bind="mergedPaginationProps"
|
|
@size-change="handleSizeChange"
|
|
@current-change="handleCurrentChange" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onBeforeUnmount, ref, toRaw, toRefs, watch } from 'vue';
|
|
|
|
import { useAppearanceSettingsStore, useVrcxStore } from '../stores';
|
|
|
|
const props = defineProps({
|
|
data: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
tableProps: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
paginationProps: {
|
|
type: Object,
|
|
default: () => ({})
|
|
},
|
|
currentPage: {
|
|
type: Number,
|
|
default: 1
|
|
},
|
|
pageSize: {
|
|
type: Number,
|
|
default: 20
|
|
},
|
|
pageSizeLinked: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
filters: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
layout: {
|
|
type: String,
|
|
default: 'table, pagination'
|
|
}
|
|
});
|
|
|
|
const emit = defineEmits([
|
|
'update:currentPage',
|
|
'update:pageSize',
|
|
'size-change',
|
|
'current-change',
|
|
'selection-change',
|
|
'row-click',
|
|
'filtered-data',
|
|
'sort-change'
|
|
]);
|
|
|
|
const appearanceSettingsStore = useAppearanceSettingsStore();
|
|
const vrcxStore = useVrcxStore();
|
|
|
|
const { data, currentPage, pageSize, tableProps, paginationProps, filters } = toRefs(props);
|
|
|
|
const internalCurrentPage = ref(currentPage.value);
|
|
const internalPageSize = ref(pageSize.value);
|
|
|
|
const asRawArray = (value) => (Array.isArray(value) ? toRaw(value) : []);
|
|
const isEmptyFilterValue = (value) => (Array.isArray(value) ? value.length === 0 : !value);
|
|
|
|
const showPagination = computed(() => {
|
|
return props.layout.includes('pagination');
|
|
});
|
|
|
|
const effectivePageSize = computed(() => {
|
|
return props.pageSizeLinked ? appearanceSettingsStore.tablePageSize : internalPageSize.value;
|
|
});
|
|
|
|
const throttledData = ref(asRawArray(data.value));
|
|
const throttledFilters = ref(filters.value);
|
|
|
|
let throttleTimerId = null;
|
|
const syncThrottledInputs = () => {
|
|
throttleTimerId = null;
|
|
throttledData.value = asRawArray(data.value);
|
|
throttledFilters.value = Array.isArray(filters.value) ? filters.value.slice() : filters.value;
|
|
};
|
|
|
|
const scheduleThrottledSync = () => {
|
|
if (throttleTimerId !== null) return;
|
|
throttleTimerId = setTimeout(syncThrottledInputs, 500);
|
|
};
|
|
|
|
watch(data, scheduleThrottledSync);
|
|
watch(() => (Array.isArray(data.value) ? data.value.length : 0), scheduleThrottledSync);
|
|
watch(filters, scheduleThrottledSync, { deep: true });
|
|
watch(effectivePageSize, scheduleThrottledSync);
|
|
|
|
onBeforeUnmount(() => {
|
|
if (throttleTimerId !== null) {
|
|
clearTimeout(throttleTimerId);
|
|
throttleTimerId = null;
|
|
}
|
|
});
|
|
|
|
const resolvedDefaultSort = computed(() => {
|
|
if (props.tableProps?.defaultSort === null) {
|
|
return undefined;
|
|
}
|
|
return (
|
|
props.tableProps?.defaultSort ?? {
|
|
prop: 'created_at',
|
|
order: 'descending'
|
|
}
|
|
);
|
|
});
|
|
|
|
const mergedTableProps = computed(() => {
|
|
const src = tableProps.value || {};
|
|
const rest = { ...src };
|
|
if ('defaultSort' in rest) {
|
|
delete rest.defaultSort;
|
|
}
|
|
return {
|
|
stripe: true,
|
|
...rest
|
|
};
|
|
});
|
|
|
|
const mergedPaginationProps = computed(() => ({
|
|
layout: 'sizes, prev, pager, next, total',
|
|
...paginationProps.value,
|
|
pageSizes: paginationProps.value?.pageSizes ?? appearanceSettingsStore.tablePageSizes
|
|
}));
|
|
|
|
const applyFilter = function (row, filter) {
|
|
if (Array.isArray(filter.prop)) {
|
|
return filter.prop.some((propItem) => applyFilter(row, { prop: propItem, value: filter.value }));
|
|
}
|
|
|
|
const cellValue = row[filter.prop];
|
|
if (cellValue === undefined || cellValue === null) return false;
|
|
|
|
if (Array.isArray(filter.value)) {
|
|
return filter.value.some((val) => String(cellValue).toLowerCase() === String(val).toLowerCase());
|
|
} else {
|
|
return String(cellValue).toLowerCase().includes(String(filter.value).toLowerCase());
|
|
}
|
|
};
|
|
|
|
const filteredData = computed(() => {
|
|
let result = throttledData.value.slice();
|
|
const activeFilters = throttledFilters.value;
|
|
|
|
if (activeFilters && Array.isArray(activeFilters) && activeFilters.length > 0) {
|
|
activeFilters.forEach((filter) => {
|
|
if (isEmptyFilterValue(filter?.value)) {
|
|
return;
|
|
}
|
|
if (filter.filterFn) {
|
|
result = result.filter((row) => filter.filterFn(row, filter));
|
|
} else if (!Array.isArray(filter.value) || filter.value.length > 0) {
|
|
result = result.filter((row) => applyFilter(row, filter));
|
|
}
|
|
});
|
|
}
|
|
|
|
return result;
|
|
});
|
|
|
|
watch(
|
|
filteredData,
|
|
(value) => {
|
|
emit('filtered-data', value);
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
const paginatedData = computed(() => {
|
|
if (!showPagination.value) {
|
|
return filteredData.value;
|
|
}
|
|
|
|
const start = (internalCurrentPage.value - 1) * effectivePageSize.value;
|
|
const end = start + effectivePageSize.value;
|
|
return filteredData.value.slice(start, end);
|
|
});
|
|
|
|
const totalItems = computed(() => {
|
|
const length = filteredData.value.length;
|
|
const max = vrcxStore.maxTableSize;
|
|
return length > max && length < max + 51 ? max : length;
|
|
});
|
|
|
|
const handleSortChange = ({ prop, order }) => {
|
|
emit('sort-change', { prop, order });
|
|
};
|
|
|
|
const handleSelectionChange = (selection) => {
|
|
emit('selection-change', selection);
|
|
};
|
|
|
|
const handleRowClick = (row, column, event) => {
|
|
emit('row-click', row, column, event);
|
|
};
|
|
|
|
const handleSizeChange = (size) => {
|
|
if (props.pageSizeLinked) {
|
|
appearanceSettingsStore.setTablePageSize(size);
|
|
emit('update:pageSize', size);
|
|
emit('size-change', size);
|
|
return;
|
|
}
|
|
internalPageSize.value = size;
|
|
emit('update:pageSize', size);
|
|
emit('size-change', size);
|
|
};
|
|
|
|
const handleCurrentChange = (page) => {
|
|
internalCurrentPage.value = page;
|
|
emit('update:currentPage', page);
|
|
emit('current-change', page);
|
|
};
|
|
|
|
watch(currentPage, (newVal) => {
|
|
internalCurrentPage.value = newVal;
|
|
});
|
|
|
|
watch(pageSize, (newVal) => {
|
|
internalPageSize.value = newVal;
|
|
});
|
|
</script>
|
|
|
|
<style scoped>
|
|
.data-table-wrapper {
|
|
margin: 0 3px;
|
|
font-feature-settings:
|
|
'tnum' 1,
|
|
'lnum' 1;
|
|
}
|
|
|
|
.pagination-wrapper {
|
|
margin-top: 16px;
|
|
display: flex;
|
|
justify-content: space-around;
|
|
}
|
|
</style>
|