846 lines
25 KiB
Vue
846 lines
25 KiB
Vue
<script setup>
|
|
import { useLayoutStore } from "~/stores/layout";
|
|
import { useWindowSize } from "vue-window-size";
|
|
import { useDebounceFn } from "@vueuse/core";
|
|
|
|
const layoutStore = useLayoutStore();
|
|
const mobileWidth = layoutStore.mobileWidth;
|
|
|
|
const { width } = useWindowSize();
|
|
const windowWidth = ref(width);
|
|
|
|
const props = defineProps({
|
|
field: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
data: {
|
|
type: Array,
|
|
default: () => [],
|
|
},
|
|
basic: {
|
|
type: Boolean,
|
|
default: true,
|
|
},
|
|
advanced: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
options: {
|
|
type: Object,
|
|
default: () => ({
|
|
variant: "default",
|
|
striped: false,
|
|
bordered: false,
|
|
borderless: false,
|
|
hover: false,
|
|
}),
|
|
},
|
|
optionsAdvanced: {
|
|
type: Object,
|
|
default: () => ({
|
|
sortable: true,
|
|
filterable: false,
|
|
responsive: false,
|
|
outsideBorder: false,
|
|
}),
|
|
},
|
|
grid: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
pageSize: {
|
|
type: Number,
|
|
default: 5,
|
|
},
|
|
sort: {
|
|
type: Object,
|
|
default: () => ({
|
|
column: "",
|
|
direction: "asc",
|
|
}),
|
|
},
|
|
showRowNumbers: {
|
|
type: Boolean,
|
|
default: false,
|
|
},
|
|
});
|
|
|
|
// Default varaiable
|
|
const dataTable = shallowRef([]);
|
|
const dataTitle = ref([]);
|
|
const dataLength = ref(0);
|
|
|
|
// Advanced Option Variable
|
|
const currentSort = ref(0);
|
|
const currentSortDir = ref("asc");
|
|
const currentPage = ref(1);
|
|
const pageSize = ref(props.pageSize);
|
|
const maxPageShown = ref(3);
|
|
|
|
// Searching Variable
|
|
const keyword = ref("");
|
|
const debouncedKeyword = ref("");
|
|
|
|
// Filtering Variable
|
|
const filter = ref([]);
|
|
const openFilter = ref(false);
|
|
|
|
const hideTable = ref(false);
|
|
|
|
// Other Variable
|
|
const sortColumnFirstTime = ref(false);
|
|
|
|
const isDesktop = computed(() => {
|
|
return windowWidth.value >= mobileWidth ? true : false;
|
|
});
|
|
|
|
if (props.optionsAdvanced.responsive) {
|
|
if (isDesktop.value) {
|
|
hideTable.value = false;
|
|
} else {
|
|
hideTable.value = true;
|
|
}
|
|
}
|
|
|
|
const camelCasetoTitle = (str, exclusions = []) => {
|
|
if (exclusions.includes(str)) {
|
|
return str.replace(/([A-Z])/g, " $1").trim();
|
|
} else if (/\(.*\)/.test(str)) {
|
|
return str; // if the string contains parentheses, return the original string
|
|
} else {
|
|
return str.replace(/([A-Z])/g, " $1").replace(/^./, (str) => {
|
|
return str.toUpperCase();
|
|
});
|
|
}
|
|
};
|
|
|
|
const spacingCharactertoCamelCase = (array) => {
|
|
// Loop array string and convert to camel case
|
|
|
|
let result = [];
|
|
|
|
array.forEach((element) => {
|
|
if (element.charAt(0) == element.charAt(0).toUpperCase()) {
|
|
// Camelcase the string and remove spacing
|
|
// and if there is () in the string, do Uppercase inside the () and dont spacing it
|
|
|
|
let camelCase = element
|
|
.replace(/([A-Z])/g, " $1")
|
|
.replace(/^./, (str) => {
|
|
return str.toUpperCase();
|
|
})
|
|
.replace(/\s/g, "");
|
|
|
|
let resultCamelCase = camelCase.replace(/\(([^)]+)\)/, (str) => {
|
|
return str.toUpperCase();
|
|
});
|
|
|
|
result.push(resultCamelCase);
|
|
} else {
|
|
result.push(element);
|
|
}
|
|
});
|
|
|
|
// console.log(result);
|
|
return result;
|
|
};
|
|
|
|
// watch props.data change and redo all the data
|
|
watch(
|
|
() => [props.data, props.field],
|
|
() => {
|
|
if (props.data && props.data.length > 0) {
|
|
dataTable.value = props.data;
|
|
dataLength.value = props.data.length;
|
|
if (props.field && props.field.length > 0) {
|
|
dataTitle.value = spacingCharactertoCamelCase(props.field);
|
|
} else {
|
|
dataTitle.value = Object.keys(dataTable.value[0]);
|
|
}
|
|
} else {
|
|
dataTable.value = [];
|
|
dataLength.value = 0;
|
|
dataTitle.value = [];
|
|
}
|
|
},
|
|
{ immediate: true }
|
|
);
|
|
|
|
const setColumnTitle = (data) => {
|
|
try {
|
|
if (props.field && props.field.length == 0) {
|
|
dataTitle.value = Object.keys(data);
|
|
} else {
|
|
dataTitle.value = spacingCharactertoCamelCase(props.field);
|
|
}
|
|
} catch (error) {
|
|
console.log(error);
|
|
}
|
|
};
|
|
|
|
const filteredDatabyTitle = (data, title) => {
|
|
let result = "";
|
|
try {
|
|
if (props.field && props.field.length == 0) {
|
|
Object.entries(data).forEach(([key, value]) => {
|
|
if (key === title) {
|
|
result = value;
|
|
return;
|
|
}
|
|
});
|
|
} else {
|
|
// Get index title from columnTitle
|
|
let index = dataTitle.value.indexOf(title);
|
|
|
|
// Convert data json to array
|
|
let arr = Object.values(data);
|
|
|
|
result = arr[index];
|
|
}
|
|
if (result === "" || result === null) result = "-";
|
|
return result;
|
|
} catch (error) {
|
|
console.log(error);
|
|
return "-";
|
|
}
|
|
};
|
|
|
|
onMounted(() => {
|
|
if (dataTable.value.length > 0) {
|
|
setColumnTitle(dataTable.value[0]);
|
|
}
|
|
});
|
|
|
|
// Computed data
|
|
const computedData = computed(() => {
|
|
if (!dataTable.value.length) return [];
|
|
|
|
let result = dataTable.value.slice();
|
|
|
|
// Sort (only for advanced tables)
|
|
if (props.advanced && currentSort.value !== null) {
|
|
result.sort((a, b) => {
|
|
let modifier = 1;
|
|
|
|
dataTitle.value.forEach((title, index) => {
|
|
// console.log(title, props.sort.column);
|
|
// First sort by column title
|
|
if (title === props.sort.column && !sortColumnFirstTime.value) {
|
|
currentSort.value = index;
|
|
currentSortDir.value = props.sort.direction;
|
|
sortColumnFirstTime.value = true;
|
|
}
|
|
});
|
|
|
|
// Check if column title is number or string and convert spacing to camelcase
|
|
let a1 = filteredDatabyTitle(a, dataTitle.value[currentSort.value]);
|
|
let b1 = filteredDatabyTitle(b, dataTitle.value[currentSort.value]);
|
|
|
|
if (typeof a1 === "string") a1 = a1.toLowerCase();
|
|
if (typeof b1 === "string") b1 = b1.toLowerCase();
|
|
|
|
// Convert string to number if possible
|
|
if (isNumeric(a1)) a1 = parseFloat(a1);
|
|
if (isNumeric(b1)) b1 = parseFloat(b1);
|
|
|
|
if (currentSortDir.value === "desc") modifier = -1;
|
|
if (a1 < b1) return -1 * modifier;
|
|
if (a1 > b1) return 1 * modifier;
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
// Filter
|
|
if (debouncedKeyword.value) {
|
|
const lowercaseKeyword = debouncedKeyword.value.toLowerCase();
|
|
result = result.filter((row) =>
|
|
Object.values(row).some((value) =>
|
|
String(value).toLowerCase().includes(lowercaseKeyword)
|
|
)
|
|
);
|
|
}
|
|
|
|
// Add row numbers if showRowNumbers is true
|
|
if (props.showRowNumbers) {
|
|
result = result.map((row, index) => ({
|
|
No: (currentPage.value - 1) * pageSize.value + index + 1,
|
|
...row,
|
|
}));
|
|
}
|
|
|
|
// Pagination (only for advanced tables)
|
|
if (props.advanced) {
|
|
const start = (currentPage.value - 1) * pageSize.value;
|
|
const end = start + pageSize.value;
|
|
dataLength.value = result.length;
|
|
return result.slice(start, end);
|
|
}
|
|
|
|
// For basic tables, return all results without pagination
|
|
dataLength.value = result.length;
|
|
return result;
|
|
});
|
|
|
|
const isNumeric = (n) => {
|
|
return !isNaN(parseFloat(n)) && isFinite(n);
|
|
};
|
|
|
|
const totalEntries = computed(() => {
|
|
return dataLength.value;
|
|
});
|
|
|
|
const sort = (index) => {
|
|
if (index === currentSort.value) {
|
|
currentSortDir.value = currentSortDir.value === "asc" ? "desc" : "asc";
|
|
} else if (index !== currentSort.value && currentSortDir.value == "desc") {
|
|
currentSortDir.value = "asc";
|
|
}
|
|
currentSort.value = index;
|
|
};
|
|
|
|
const pages = computed(() => {
|
|
let totalPG = Math.ceil(dataLength.value / pageSize.value);
|
|
const numShown = Math.min(maxPageShown.value, totalPG);
|
|
let first = currentPage.value - Math.floor(numShown / 2);
|
|
first = Math.max(first, 1);
|
|
first = Math.min(first, totalPG - numShown + 1);
|
|
return [...Array(numShown)].map((k, i) => i + first);
|
|
});
|
|
|
|
const totalPage = computed(() => {
|
|
return Math.ceil(dataLength.value / pageSize.value);
|
|
});
|
|
|
|
const pageChange = (page) => {
|
|
currentPage.value = page;
|
|
};
|
|
|
|
const nextPage = () => {
|
|
if (currentPage.value * pageSize.value < dataLength.value)
|
|
currentPage.value++;
|
|
};
|
|
const prevPage = () => {
|
|
if (currentPage.value > 1) currentPage.value--;
|
|
};
|
|
|
|
const firstPage = () => {
|
|
currentPage.value = 1;
|
|
};
|
|
|
|
const lastPage = () => {
|
|
currentPage.value = totalPage.value;
|
|
};
|
|
|
|
const hideColumn = (key) => {
|
|
if (!getFilter(key)) {
|
|
// insert into filter variable to tell there is a change in filter
|
|
setFilter(key, "hide", true);
|
|
} else {
|
|
// update filter variable to tell there is a change in filter
|
|
setFilter(key, "hide", false);
|
|
}
|
|
};
|
|
|
|
const setFilter = (key, action, condition) => {
|
|
// Check if key exist inside filter
|
|
let index = filter.value.findIndex((item) => item.key === key);
|
|
|
|
if (index == -1) {
|
|
// If key not exist, insert new filter
|
|
filter.value.push({
|
|
key: key,
|
|
action: {
|
|
[action]: condition,
|
|
},
|
|
});
|
|
} else {
|
|
// If key exist, update filter
|
|
filter.value[index].action[action] = condition;
|
|
// Update references to use dataTitle
|
|
let columnIndex = dataTitle.value.indexOf(key);
|
|
if (columnIndex !== -1) {
|
|
// Remove column from dataTitle
|
|
dataTitle.value.splice(columnIndex, 1);
|
|
}
|
|
}
|
|
};
|
|
|
|
const getFilter = (key) => {
|
|
let result = false;
|
|
filter.value.forEach((item) => {
|
|
if (item.key === key) {
|
|
result = item.action.hide;
|
|
}
|
|
});
|
|
return result;
|
|
};
|
|
|
|
// Watch filter.value
|
|
watch(
|
|
() => filter.value,
|
|
() => {
|
|
// console.log(filter.value);
|
|
// Loop json object filter.value
|
|
filter.value.forEach((item) => {
|
|
// Hide Column
|
|
if (item.action.hide) {
|
|
// Get index title from dataTitle
|
|
let index = dataTitle.value.indexOf(item.key);
|
|
|
|
if (index !== -1) {
|
|
// Remove column from dataTitle
|
|
dataTitle.value.splice(index, 1);
|
|
}
|
|
} else if (!item.action.hide) {
|
|
// Get index title from dataTitle
|
|
let indexData = dataTitle.value.indexOf(item.key);
|
|
|
|
if (!dataTitle.value.includes(item.key)) {
|
|
// Add Column back to its original position
|
|
dataTitle.value.splice(indexData, 0, item.key);
|
|
|
|
// Sort the dataTitle like dataTitle
|
|
dataTitle.value.sort((a, b) => {
|
|
let indexA = dataTitle.value.indexOf(a);
|
|
let indexB = dataTitle.value.indexOf(b);
|
|
return indexA - indexB;
|
|
});
|
|
}
|
|
}
|
|
});
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
const filterComputed = computed(() => {
|
|
let result = [];
|
|
let i = 0;
|
|
filter.value.forEach((item) => {
|
|
if (item.action.hide) {
|
|
result.push({
|
|
title: item.key,
|
|
hide: item.action.hide,
|
|
});
|
|
}
|
|
i++;
|
|
});
|
|
// console.log(result);
|
|
return result;
|
|
});
|
|
|
|
// watch pinia getter windowWidth
|
|
watch(
|
|
() => windowWidth.value,
|
|
() => {
|
|
if (props.optionsAdvanced.responsive) {
|
|
if (windowWidth.value <= mobileWidth) {
|
|
hideTable.value = true;
|
|
} else {
|
|
hideTable.value = false;
|
|
}
|
|
}
|
|
},
|
|
{ deep: true }
|
|
);
|
|
|
|
// Debounce the search input
|
|
watch(
|
|
keyword,
|
|
useDebounceFn((val) => {
|
|
debouncedKeyword.value = val;
|
|
}, 300)
|
|
);
|
|
|
|
// Modify the columnTitle computed property
|
|
const computedColumnTitle = computed(() => {
|
|
let titles =
|
|
props.field.length > 0
|
|
? spacingCharactertoCamelCase(props.field)
|
|
: dataTable.value.length > 0
|
|
? Object.keys(dataTable.value[0])
|
|
: [];
|
|
|
|
if (props.showRowNumbers) {
|
|
titles.unshift("No");
|
|
}
|
|
|
|
return titles;
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
v-if="dataTable && dataTable.length > 0"
|
|
class="table-wrapper"
|
|
:class="{
|
|
'!border': advanced && !hideTable && optionsAdvanced.outsideBorder,
|
|
}"
|
|
>
|
|
<div
|
|
class="table-header"
|
|
:class="{
|
|
open: openFilter,
|
|
'!max-h-full': !optionsAdvanced.filterable,
|
|
}"
|
|
v-if="advanced"
|
|
>
|
|
<div
|
|
class="table-header-filter"
|
|
:class="{
|
|
'!items-center !gap-3': !optionsAdvanced.filterable,
|
|
}"
|
|
>
|
|
<div>
|
|
<div class="flex gap-x-2">
|
|
<FormKit
|
|
v-model="keyword"
|
|
type="search"
|
|
placeholder="Search..."
|
|
outer-class="mb-0"
|
|
/>
|
|
<rs-button
|
|
v-if="optionsAdvanced.filterable"
|
|
class="!px-3 sm:!px-6"
|
|
@click="openFilter ? (openFilter = false) : (openFilter = true)"
|
|
>
|
|
<Icon
|
|
name="ic:outline-filter-alt"
|
|
class="mr-0 md:mr-1"
|
|
size="1rem"
|
|
/>
|
|
<span class="hidden sm:block">Filter</span>
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-center items-center gap-x-2">
|
|
<span class="text-[rgb(var(--text-color))]">Result per page:</span>
|
|
<FormKit
|
|
type="select"
|
|
v-model="pageSize"
|
|
:options="[5, 10, 25, 100]"
|
|
outer-class="mb-0"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="flex flex-wrap items-center justify-start gap-x-3"
|
|
v-if="optionsAdvanced.filterable"
|
|
>
|
|
<rs-dropdown
|
|
:title="camelCasetoTitle(val)"
|
|
size="sm"
|
|
class="mt-3"
|
|
v-for="(val, index) in dataTitle"
|
|
:key="index"
|
|
>
|
|
<rs-dropdown-item @click="hideColumn(val)">
|
|
{{ getFilter(val) ? "Show Column" : "Hide Column" }}
|
|
<Icon
|
|
:name="getFilter(val) ? 'mdi:eye-outline' : 'mdi:eye-off-outline'"
|
|
size="1rem"
|
|
class="ml-auto"
|
|
></Icon>
|
|
</rs-dropdown-item>
|
|
</rs-dropdown>
|
|
</div>
|
|
</div>
|
|
<div
|
|
v-if="filterComputed.length > 0"
|
|
class="table-header-filter-list w-full m-4"
|
|
>
|
|
<div class="flex flex-wrap items-center justify-start gap-x-2">
|
|
<div
|
|
class="flex items-center justify-center gap-x-2 border border-primary text-primary rounded-lg py-1 px-2"
|
|
v-for="(val, index) in filterComputed"
|
|
:key="index"
|
|
>
|
|
{{ val ? camelCasetoTitle(val.title) : "" }}
|
|
<Icon
|
|
name="ic:round-close"
|
|
class="mr-0 md:mr-1 hover:text-red-500 cursor-pointer"
|
|
size="1rem"
|
|
@click="hideColumn(val.title)"
|
|
></Icon>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="w-full overflow-x-auto">
|
|
<client-only>
|
|
<table
|
|
v-if="!hideTable"
|
|
class="table-content"
|
|
:class="{
|
|
'!border-y !border-0 border-[rgb(var(--bg-1))]': advanced,
|
|
'table-fixed': options.fixed,
|
|
'table-auto': !options.fixed,
|
|
}"
|
|
>
|
|
<thead
|
|
class="text-left border-[rgb(var(--border-color))]"
|
|
:class="{
|
|
'border-y': !options.borderless,
|
|
'border-[rgb(var(--border-color))] bg-[rgb(var(--bg-2))]':
|
|
options.variant === 'default',
|
|
'border-primary/50 bg-primary text-white':
|
|
options.variant === 'primary',
|
|
'border-secondary/50 bg-secondary text-white':
|
|
options.variant === 'secondary',
|
|
'border-info/50 bg-info text-white ': options.variant === 'info',
|
|
'border-success/50 bg-success text-white':
|
|
options.variant === 'success',
|
|
'border-warning/50 bg-warning text-white':
|
|
options.variant === 'warning',
|
|
'border-danger/50 bg-danger text-white':
|
|
options.variant === 'danger',
|
|
}"
|
|
>
|
|
<tr>
|
|
<th
|
|
class="relative py-3 pl-5 pr-8 whitespace-nowrap"
|
|
:class="{
|
|
'border-r last:border-l last:border-r-0':
|
|
options.bordered && !options.borderless,
|
|
'border-[rgb(var(--border-color))]':
|
|
options.variant === 'default',
|
|
'border-primary/80': options.variant === 'primary',
|
|
'border-secondary/80': options.variant === 'secondary',
|
|
'border-info/80': options.variant === 'info',
|
|
'border-success/80': options.variant === 'success',
|
|
'border-warning/80': options.variant === 'warning',
|
|
'border-danger/80': options.variant === 'danger',
|
|
'w-36': options.fixed,
|
|
'cursor-pointer': optionsAdvanced.sortable && advanced,
|
|
'w-16': val === 'No' && props.showRowNumbers,
|
|
}"
|
|
style="min-width: 100px"
|
|
@click="
|
|
optionsAdvanced.sortable && advanced && val !== 'No'
|
|
? sort(index)
|
|
: null
|
|
"
|
|
v-for="(val, index) in computedColumnTitle"
|
|
:key="index"
|
|
>
|
|
{{ camelCasetoTitle(val) }}
|
|
<div
|
|
v-if="optionsAdvanced.sortable && advanced"
|
|
class="sortable"
|
|
>
|
|
<Icon
|
|
class="absolute top-3 right-2 opacity-20"
|
|
size="1.25rem"
|
|
name="carbon:chevron-sort"
|
|
/>
|
|
<Icon
|
|
v-if="currentSort == index && currentSortDir == 'asc'"
|
|
class="absolute top-3 right-2 opacity-50"
|
|
size="1.25rem"
|
|
name="carbon:chevron-sort-up"
|
|
/>
|
|
<Icon
|
|
v-else-if="currentSort == index && currentSortDir == 'desc'"
|
|
class="absolute top-3 right-2 opacity-50"
|
|
size="1.25rem"
|
|
name="carbon:chevron-sort-down"
|
|
/>
|
|
</div>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr
|
|
:class="{
|
|
'border-y border-[rgb(var(--border-color))]':
|
|
!options.bordered && !options.borderless,
|
|
'border-b': options.bordered && !options.borderless,
|
|
'border-b-0': options.borderless,
|
|
'border-[rgb(var(--border-color))] odd:bg-[rgb(var(--bg-1))] even:bg-[rgb(var(--bg-2))]':
|
|
options.variant === 'default' && options.striped,
|
|
'border-primary/20 odd:bg-white even:bg-primary/5':
|
|
options.variant === 'primary' && options.striped,
|
|
'border-secondary/20 odd:bg-white even:bg-secondary/5':
|
|
options.variant === 'secondary' && options.striped,
|
|
'border-info/20 odd:bg-white even:bg-info/5':
|
|
options.variant === 'info' && options.striped,
|
|
'border-success/20 odd:bg-white even:bg-success/5':
|
|
options.variant === 'success' && options.striped,
|
|
'border-warning/20 odd:bg-white even:bg-warning/5':
|
|
options.variant === 'warning' && options.striped,
|
|
'border-danger/20 odd:bg-white even:bg-danger/5':
|
|
options.variant === 'danger' && options.striped,
|
|
'': options.hover && options.variant === 'default',
|
|
'cursor-pointer hover:bg-primary/5':
|
|
options.hover && options.variant === 'primary',
|
|
'cursor-pointer hover:bg-secondary/5':
|
|
options.hover && options.variant === 'secondary',
|
|
'cursor-pointer hover:bg-info/5':
|
|
options.hover && options.variant === 'info',
|
|
'cursor-pointer hover:bg-success/5':
|
|
options.hover && options.variant === 'success',
|
|
'cursor-pointer hover:bg-warning/5':
|
|
options.hover && options.variant === 'warning',
|
|
'cursor-pointer hover:bg-danger/5':
|
|
options.hover && options.variant === 'danger',
|
|
}"
|
|
v-for="(val1, index1) in computedData"
|
|
:key="index1"
|
|
>
|
|
<td
|
|
class="p-4 pl-5 break-words"
|
|
:class="{
|
|
'border-r last:border-l last:border-r-0':
|
|
options.bordered && !options.borderless,
|
|
'border-[rgb(var(--border-color))]':
|
|
options.variant === 'default',
|
|
'border-primary/20': options.variant === 'primary',
|
|
'border-secondary/20': options.variant === 'secondary',
|
|
'border-info/20': options.variant === 'info',
|
|
'border-success/20': options.variant === 'success',
|
|
'border-warning/20': options.variant === 'warning',
|
|
'border-danger/20': options.variant === 'danger',
|
|
}"
|
|
v-for="(val2, index2) in computedColumnTitle"
|
|
:key="index2"
|
|
>
|
|
<slot
|
|
:name="val2"
|
|
:text="filteredDatabyTitle(val1, val2)"
|
|
:value="val1"
|
|
>
|
|
{{ filteredDatabyTitle(val1, val2) }}
|
|
</slot>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<div v-else>
|
|
<rs-collapse v-if="computedData.length > 0" accordion>
|
|
<rs-collapse-item v-for="(val, index) in computedData" :key="index">
|
|
<template #title>
|
|
<div class="grid grid-cols-2">
|
|
<div class="flex flex-col col-span-1">
|
|
<span class="font-semibold leading-tight">
|
|
{{ Object.values(val)[0] }}
|
|
</span>
|
|
<span class="text-sm"> {{ Object.values(val)[1] }} </span>
|
|
</div>
|
|
<div class="flex justify-end items-center col-span-1">
|
|
<div class="mr-4">
|
|
{{ Object.values(val)[computedData.length] }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<template #default>
|
|
<div
|
|
class="flex justify-between items-center even:bg-inherit odd:bg-[rgb(var(--bg-1))] rounded-lg p-3"
|
|
v-for="(val1, index1) in Object.entries(val).slice(
|
|
2,
|
|
Object.entries(val).length
|
|
)"
|
|
:key="index1"
|
|
>
|
|
<span>
|
|
{{ camelCasetoTitle(val1[0]) }}
|
|
</span>
|
|
<span>
|
|
{{ val1[1] }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
</rs-collapse-item>
|
|
</rs-collapse>
|
|
</div>
|
|
</client-only>
|
|
</div>
|
|
<div v-if="advanced" class="table-footer">
|
|
<div class="flex justify-center items-center gap-x-2">
|
|
<span class="text-sm text-[rgb(var(--text-color))] hidden md:block"
|
|
>Showing {{ pageSize * currentPage - pageSize + 1 }} to
|
|
{{ pageSize * currentPage }} of {{ totalEntries }} entries</span
|
|
>
|
|
</div>
|
|
<div class="table-footer-page">
|
|
<rs-button
|
|
:variant="`${
|
|
options.variant == 'default' ? 'primary' : options.variant
|
|
}-outline`"
|
|
class="!rounded-full !p-1 !w-8 !h-8"
|
|
@click="firstPage"
|
|
:disabled="currentPage == 1"
|
|
>
|
|
<Icon name="ic:round-keyboard-double-arrow-left" size="1rem"></Icon>
|
|
</rs-button>
|
|
<rs-button
|
|
:variant="`${
|
|
options.variant == 'default' ? 'primary' : options.variant
|
|
}-outline`"
|
|
class="!rounded-full !p-1 !w-8 !h-8"
|
|
@click="prevPage"
|
|
:disabled="currentPage == 1"
|
|
>
|
|
<Icon name="ic:round-keyboard-arrow-left" size="1rem"></Icon>
|
|
</rs-button>
|
|
<rs-button
|
|
:variant="`${
|
|
currentPage == val && options.variant != 'default'
|
|
? options.variant
|
|
: currentPage == val && options.variant == 'default'
|
|
? 'primary'
|
|
: options.variant == 'default'
|
|
? 'primary-outline'
|
|
: options.variant + '-outline'
|
|
}`"
|
|
class="!rounded-full !p-1 !w-8 !h-8"
|
|
v-for="(val, index) in pages"
|
|
:key="index"
|
|
@click="pageChange(val)"
|
|
>
|
|
{{ val }}
|
|
</rs-button>
|
|
<rs-button
|
|
:variant="`${
|
|
options.variant == 'default' ? 'primary' : options.variant
|
|
}-outline`"
|
|
class="!rounded-full !p-1 !w-8 !h-8"
|
|
@click="nextPage"
|
|
:disabled="currentPage == totalPage"
|
|
>
|
|
<Icon name="ic:round-keyboard-arrow-right" size="1rem"></Icon>
|
|
</rs-button>
|
|
<rs-button
|
|
:variant="`${
|
|
options.variant == 'default' ? 'primary' : options.variant
|
|
}-outline`"
|
|
class="!rounded-full !p-1 !w-8 !h-8"
|
|
@click="lastPage"
|
|
:disabled="currentPage == totalPage"
|
|
>
|
|
<Icon name="ic:round-keyboard-double-arrow-right" size="1rem"></Icon>
|
|
</rs-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="table-wrapper">
|
|
<div
|
|
class="border border-[rgb(var(--border-color))] rounded-lg overflow-hidden"
|
|
>
|
|
<div
|
|
class="bg-[rgb(var(--bg-2))] p-4 border-b border-[rgb(var(--border-color))]"
|
|
>
|
|
<h3 class="text-lg font-semibold text-[rgb(var(--text-color))]"></h3>
|
|
</div>
|
|
<div class="p-8 text-center">
|
|
<Icon name="mdi:table-off" class="text-gray-300 mb-4" size="48px" />
|
|
<p class="text-[rgb(var(--text-color))] text-lg font-medium">
|
|
No Data Available
|
|
</p>
|
|
<p class="text-gray-500 mt-2">
|
|
There are currently no entries to display in this table.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|