117 lines
3.2 KiB
Vue
117 lines
3.2 KiB
Vue
<template>
|
|
<div id="tableContainer">
|
|
<table :id="tableId" class="display">
|
|
<thead>
|
|
<tr>
|
|
<th v-for="(header, i) in computedHeaders" :key="i">{{ header }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
v-for="(row, rowIdx) in filteredData"
|
|
:key="rowIdx"
|
|
@click="$emit('row-click', row, rowIdx)"
|
|
class="searchable-row"
|
|
>
|
|
<td v-for="(header, colIdx) in computedHeaders" :key="colIdx">
|
|
{{ isObjectData ? (row[header] ?? '') : (row[colIdx] ?? '') }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
|
|
<!-- Simple pagination -->
|
|
<div v-if="totalPages > 1" class="table-pagination mt-3 d-flex justify-content-between align-items-center">
|
|
<button class="btn btn-sm btn-default" :disabled="currentPage <= 1" @click="currentPage--">
|
|
Previous
|
|
</button>
|
|
<span>Page {{ currentPage }} of {{ totalPages }}</span>
|
|
<button class="btn btn-sm btn-default" :disabled="currentPage >= totalPages" @click="currentPage++">
|
|
Next
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed } from 'vue'
|
|
|
|
const props = defineProps({
|
|
/**
|
|
* Array of objects or array of arrays
|
|
*/
|
|
data: { type: Array, required: true },
|
|
headers: { type: Array, default: () => [] },
|
|
tableId: { type: String, default: 'dynamicTable' },
|
|
pageLength: { type: Number, default: 10 },
|
|
defaultSort: { type: Object, default: () => ({ column: 0, direction: 'asc' }) },
|
|
defaultSearch: { type: String, default: '' },
|
|
})
|
|
|
|
defineEmits(['row-click'])
|
|
|
|
const currentPage = ref(1)
|
|
const searchTerm = ref(props.defaultSearch)
|
|
|
|
const isObjectData = computed(() =>
|
|
props.data.length > 0 &&
|
|
props.data[0] !== null &&
|
|
typeof props.data[0] === 'object' &&
|
|
!Array.isArray(props.data[0])
|
|
)
|
|
|
|
const computedHeaders = computed(() => {
|
|
if (props.headers.length > 0) return props.headers
|
|
if (isObjectData.value) return Object.keys(props.data[0])
|
|
return []
|
|
})
|
|
|
|
const sortedData = computed(() => {
|
|
const dataCopy = [...props.data]
|
|
const { column, direction } = props.defaultSort
|
|
|
|
dataCopy.sort((a, b) => {
|
|
const valA = isObjectData.value ? a[computedHeaders.value[column]] : a[column]
|
|
const valB = isObjectData.value ? b[computedHeaders.value[column]] : b[column]
|
|
|
|
if (valA < valB) return direction === 'asc' ? -1 : 1
|
|
if (valA > valB) return direction === 'asc' ? 1 : -1
|
|
return 0
|
|
})
|
|
|
|
return dataCopy
|
|
})
|
|
|
|
const filteredData = computed(() => {
|
|
let result = sortedData.value
|
|
|
|
if (searchTerm.value) {
|
|
const q = searchTerm.value.toLowerCase()
|
|
result = result.filter(row => {
|
|
const text = isObjectData.value
|
|
? Object.values(row).join(' ')
|
|
: row.join(' ')
|
|
return text.toLowerCase().includes(q)
|
|
})
|
|
}
|
|
|
|
// Pagination
|
|
const start = (currentPage.value - 1) * props.pageLength
|
|
return result.slice(start, start + props.pageLength)
|
|
})
|
|
|
|
const totalPages = computed(() => {
|
|
const total = sortedData.value.length
|
|
return Math.max(1, Math.ceil(total / props.pageLength))
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.searchable-row {
|
|
cursor: pointer;
|
|
}
|
|
.searchable-row:hover {
|
|
background-color: rgba(0, 0, 0, 0.05);
|
|
}
|
|
</style>
|