386 lines
11 KiB
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>
|