CviLux_fe/src/components/customUI/Table.vue

386 lines
11 KiB
Vue

<script setup>
import { twMerge } from "tailwind-merge";
import { computed, defineProps, provide, ref, watch } from "vue";
import Pagination from "@/components/customUI/Pagination.vue";
import Checkbox from "@/components/customUI/Checkbox.vue";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
/*
column={
title,key,class, width, filter:Boolean, sort:Boolean
}
*/
const props = defineProps({
columns: Array,
dataSource: Array,
rowKey: String,
withStyle: {
type: Boolean,
default: true,
},
pagination: { type: Boolean, default: true } || {
pageSize: Number,
totalPages: Number,
totalItems: Number,
},
loading: Boolean,
});
const currentDataSource = ref([]);
const dataSourceStorage = ref([]);
watch(
() => props.dataSource,
(newValue) => {
dataSourceStorage.value = newValue;
filterItems.value = Object.fromEntries(
props.columns.map((c, i) => [
c.key,
[...new Set(newValue.map((d) => d[c.key]))].map((name) => ({
name,
selected: false,
})),
])
);
}
);
const updateDataSource = (data) => {
console.log("update", data);
currentDataSource.value = data;
};
provide("current_table_data", {
updateDataSource,
});
const sortRule = ref({});
const filterColumn = ref({});
const filterItems = ref({});
const selectedFilterItem = ref({});
const toggleFilterModal = (key) => {
let newFilter = Object.assign(filterColumn.value);
for (let oKey in newFilter) {
newFilter[oKey] = key === oKey && !newFilter[key];
}
filterColumn.value = newFilter;
};
watch(
() => props.columns,
(newValue) => {
sortRule.value = Object.fromEntries(newValue.map((c) => [c.key, 0]));
filterColumn.value = Object.fromEntries(
newValue.map((c, i) => [c.key, false])
);
selectedFilterItem.value = Object.fromEntries(
newValue.map((c, i) => [c.key, []])
);
},
{
immediate: true,
}
);
/*
0:取消
1:ascending
2:descending
*/
const toggleSortRule = (key) => {
let newSort = Object.assign(sortRule.value);
for (let oKey in newSort) {
newSort[oKey] = key === oKey ? newSort[key] : 0;
}
sortRule.value = newSort;
};
const sort = (column) => {
toggleSortRule(column);
const cantSort = ["object", "boolean"];
console.log(props.dataSource?.[0][column]);
if (cantSort.includes(typeof props.dataSource?.[0][column])) return;
// 小->大
const newArray = Object.assign(props.dataSource, []).sort((a, b) => {
// if (column === "timestamp") {
// return dayjs(a[column]).valueOf() - dayjs(b[column]).valueOf();
// }
if (typeof a[column] === "number") return a[column] - b[column];
else if (typeof a[column] === "string") {
console.log(a[column], b[column], a[column].localeCompare(b[column]));
return a[column].localeCompare(b[column]);
}
// return parseInt(a[column]) - parseInt(b[column]);
});
if (sortRule.value[column] === 0) {
sortRule.value[column] = 1;
dataSourceStorage.value = newArray;
} else if (sortRule.value[column] === 1) {
sortRule.value[column] = 2;
dataSourceStorage.value = newArray.reverse();
} else if (sortRule.value[column] === 2) {
sortRule.value[column] = 0;
dataSourceStorage.value = props.dataSource;
}
};
const form = ref(null);
const onFilter = (key, reset = false) => {
const formData = new FormData(form.value);
reset && formData.delete(key);
for (let [name, value] of formData) {
console.log(name, value);
}
selectedFilterItem.value[key] = formData.getAll(key);
toggleFilterModal(key);
};
watch(
selectedFilterItem,
(newVal) => {
let newData = Object.assign(props.dataSource);
for (let key in newVal) {
if (newVal[key].length > 0) {
newData = newData.filter((d) => newVal[key].includes(d[key]));
}
}
dataSourceStorage.value = newData;
},
{
deep: true,
}
);
</script>
<template>
<div :class="withStyle ? 'content-box' : 'py-5'">
<div class="content-decoration">
<slot name="beforeTable"></slot>
<form ref="form" class="overflow-x-auto">
<table
:class="
twMerge(
withStyle ? 'table' : 'table border',
currentDataSource.length === 0 ? 'h-28' : ''
)
"
>
<!-- head -->
<thead>
<tr>
<th
v-for="column in columns"
:key="column.key"
:class="`${column.class ? column.class : ''}`"
:style="{
width: `${
column.width
? typeof column.width === 'string'
? column.width
: column.width + 'px'
: 'auto'
}`,
}"
>
<span class="flex justify-center">
{{ column.title }}
<div
v-if="column.sort"
class="flex flex-col justify-center w-3 mx-2 relative"
@click="() => sort(column.key)"
>
<font-awesome-icon
:icon="['fas', 'sort-up']"
:class="
twMerge(
'absolute top-0',
sortRule[column.key] === 1 ? 'text-success' : ''
)
"
size="lg"
/>
<font-awesome-icon
:icon="['fas', 'sort-down']"
:class="
twMerge(
'absolute bottom-1',
sortRule[column.key] === 2 ? 'text-success' : ''
)
"
size="lg"
/>
</div>
<div class="ml-2 relative" v-if="column.filter">
<font-awesome-icon
:icon="['fas', 'filter']"
:class="
twMerge(
filterColumn[column.key] ||
selectedFilterItem[column.key].length > 0
? 'text-success'
: ''
)
"
@click="() => toggleFilterModal(column.key)"
/>
<div
class="absolute top-full -left-1/2 z-50"
v-if="filterColumn[column.key]"
>
<div class="card min-w-max bg-body shadow-xl px-10 py-5">
<Checkbox
v-for="item in filterItems[column.key]"
:title="item.name"
:value="item.name"
:key="item.name"
:name="column.key"
:checked="
selectedFilterItem[column.key].includes(item.name)
"
className="justify-start"
/>
<div class="card-actions mt-4 justify-end">
<input
type="reset"
class="btn btn-sm text-white btn-error"
:value="t('button.reset')"
@click="() => onFilter(column.key, true)"
/>
<button
class="btn btn-sm btn-success"
@click.stop.prevent="() => onFilter(column.key)"
>
{{ $t("button.submit") }}
</button>
</div>
</div>
</div>
</div>
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td :colspan="columns.length">
<Loading />
</td>
</tr>
<tr v-else-if="currentDataSource.length == 0">
<td :colspan="columns.length">{{ $t("table.no_data") }}</td>
</tr>
<template v-else :sort="sortRule">
<tr
v-for="(data, index) in currentDataSource"
:key="data.key || data[rowKey]"
>
<template
v-for="column in columns"
:key="`${data.key || data[rowKey]}_${column.key}`"
>
<td
:class="column.class"
:style="{
width: `${
column.width
? typeof column.width === 'string'
? column.width
: column.width + 'px'
: 'auto'
}`,
}"
>
<slot
name="bodyCell"
v-bind="{ record: data, column, index }"
>
{{ data[column.key] }}</slot
>
</td>
</template>
</tr>
</template>
</tbody>
</table>
</form>
<slot name="afterTable"></slot>
<Pagination
:pagination="pagination"
:dataSource="dataSourceStorage"
:sort="sortRule"
/>
</div>
<div class="content-decoration2"></div>
</div>
</template>
<style lang="css" scoped>
/**資料框**/
.content-box {
@apply border border-info p-1 relative mb-4 bg-transparent ;
}
.content-box .table {
@apply rounded-none;
}
.table th {
@apply bg-cyan-600 bg-opacity-30 border-r border-b border-white text-lg font-semibold text-white text-center px-2 py-3;
}
.table td {
@apply border-r border-b border-white text-lg font-semibold text-white text-center px-2 py-3;
}
.table tr td:last-child,
.table tr:first-child th:last-child {
border-right: 0;
}
/* .table tr:last-child td {
border-bottom: v-bind("withStyle ? '0px': '1px'");
} */
/**資料框裝飾**/
.content-box::before {
@apply absolute top-1 left-1 h-5 w-5 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background01.svg')] bg-center;
content: "";
}
.content-box::after {
@apply absolute bottom-1 right-1 h-5 w-5 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background05.svg')] bg-center;
content: "";
}
.content-box .content-decoration {
@apply bg-normal px-8 py-4;
}
/* .content-box .content-decoration::before {
@apply absolute -top-3 -right-[10px] h-8 w-8 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background02.svg')] bg-center;
content: "";
} */
.content-box .content-decoration2::before {
@apply absolute -bottom-1 -left-8 h-14 w-14 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background03.svg')] bg-center;
content: "";
}
/* .content-box .content-decoration2::after {
content: "";
background: url(../../assets/img/table/content-box-background04.svg) center
center;
position: absolute;
right: -27px;
bottom: -7px;
height: 65px;
width: 50px;
background-repeat: no-repeat;
z-index: 2;
} */
</style>