Files
web/app/components/datatable/DataView.vue
2026-05-21 16:54:07 +07:00

460 lines
12 KiB
Vue

<template>
<TimeOption
v-bind="{
pagename: vpagename,
api: api,
timeopt: timeopt,
filter: optfilter,
importdata: props.importdata,
newDataAvailable: newDataAvailable,
params: vparams,
}"
ref="timeopt"
@option="timeOption"
@excel="exportExcel"
@add="insert"
@manual-refresh="manualRefresh"
@refresh-data="refreshData"
@import="openImportModal"
class="mb-3"
v-if="timeopt"
></TimeOption>
<DataTable
v-bind="{ pagename: vpagename }"
@edit="edit"
@insert="insert"
@dataevent="dataEvent"
v-if="pagedata"
/>
<Modal
@close="showmodal = undefined"
v-bind="showmodal"
v-if="showmodal"
/>
</template>
<script setup>
import TimeOption from "~/components/datatable/TimeOption";
import { useStore } from "~/stores/index";
// [FIX] Thêm onActivated, onDeactivated để xử lý KeepAlive
import { ref, watch, onBeforeUnmount, onActivated, onDeactivated } from "vue";
const emit = defineEmits(["modalevent", "dataevent", "dataUpdated"]);
const store = useStore();
const props = defineProps({
pagename: String,
api: String,
setting: String,
filter: Object,
params: Object,
data: Object,
modal: Object,
timeopt: Object,
realtime: Object,
importdata: Object,
});
const { $copy, $find, $findapi, $getapi, $setpage, $clone, $stripHtml, $snackbar, $dayjs } = useNuxtApp();
const showmodal = ref();
const pagedata = ref();
const newDataAvailable = ref(false);
const pendingNewData = ref(null);
const lastDataHash = ref(null);
const pollingInterval = ref(null);
let vpagename = props.pagename;
let vfilter = props.filter ? $copy(props.filter) : undefined;
let vparams = props.params ? $copy(props.params) : undefined;
let connection = undefined;
let optfilter = props.filter || (props.params ? props.params.filter : undefined);
const realtimeConfig = ref({ time: 0, update: "true" });
if (props.realtime) {
realtimeConfig.value = {
time: props.realtime.time || 0,
update: props.realtime.update,
};
}
if (vparams?.filter) {
for (const [key, value] of Object.entries(vparams.filter)) {
if (value.toString().indexOf("$") >= 0) {
vparams.filter[key] = store[value.replace("$", "")].id;
}
}
}
const generateDataHash = (data) => {
if (!data) return null;
try {
const replacer = (key, value) =>
value && typeof value === "object" && !Array.isArray(value)
? Object.keys(value)
.sort()
.reduce((sorted, key) => {
sorted[key] = value[key];
return sorted;
}, {})
: value;
const stringToHash = JSON.stringify(data, replacer);
return stringToHash.split("").reduce((a, b) => {
a = (a << 5) - a + b.charCodeAt(0);
return a & a;
}, 0);
} catch (e) {
console.error("Error generating data hash:", e);
return null;
}
};
// [FIX] Tách hàm dừng polling ra riêng để tái sử dụng
const stopAutoCheck = () => {
if (pollingInterval.value) {
clearInterval(pollingInterval.value);
pollingInterval.value = null;
}
};
const startAutoCheck = () => {
// [FIX] Dừng interval cũ trước khi tạo mới, tránh tạo nhiều interval chồng nhau
stopAutoCheck();
if (realtimeConfig.value.time && realtimeConfig.value.time > 0) {
pollingInterval.value = setInterval(() => checkDataChanges(), realtimeConfig.value.time * 1000);
}
};
const checkDataChanges = async () => {
try {
const connlist = [];
const conn1 = $findapi(props.api);
if (vfilter) {
const filter = $copy(conn1.params.filter) || {};
for (const [key, value] of Object.entries(vfilter)) {
filter[key] = value;
}
for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace("$", "")].id;
}
}
conn1.params.filter = filter;
}
if (vparams) conn1.params = $copy(vparams);
delete conn1.params.sort;
delete conn1.params.values;
// conn1.params.summary = "aggregate";
// conn1.params.distinct_values = JSON.stringify({
// total_count: { type: "Count", field: "id" },
// last_updated: { type: "Max", field: "update_time" },
// last_created: { type: "Max", field: "create_time" },
// });
connlist.push(conn1);
const rs = await $getapi(connlist);
const obj = $find(rs, { name: props.api });
const newMetadata = obj ? obj.data.rows : {};
const newHash = generateDataHash(newMetadata);
if (lastDataHash.value === null) {
lastDataHash.value = newHash;
return;
}
if (newHash !== lastDataHash.value) {
lastDataHash.value = newHash;
if (realtimeConfig.value.update === "true") {
await loadFullDataAsync();
emit("dataUpdated", {
newData: store[vpagename].data,
autoUpdate: true,
hasChanges: true,
});
} else {
newDataAvailable.value = true;
emit("dataUpdated", {
newData: null,
autoUpdate: false,
hasChanges: true,
});
}
}
} catch (error) {
console.error("Error checking data:", error);
}
};
const loadFullDataAsync = async () => {
try {
const connlist = [];
const conn1 = $findapi(props.api);
if (vfilter) {
const filter = $copy(conn1.params.filter) || {};
for (const [key, value] of Object.entries(vfilter)) {
filter[key] = value;
}
for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace("$", "")].id;
}
}
conn1.params.filter = filter;
}
if (vparams) conn1.params = $copy(vparams);
delete conn1.params.summary;
delete conn1.params.distinct_values;
connlist.push(conn1);
const rs = await $getapi(connlist);
const obj = $find(rs, { name: props.api });
const newData = obj ? $copy(obj.data.rows) : [];
updateDataDisplay(newData);
} catch (error) {
console.error("Error loading full data:", error);
}
};
const openImportModal = () => {
const copy = $copy(props.importdata);
showmodal.value = copy;
};
const updateDataDisplay = (newData) => {
const copy = $clone(store[vpagename]);
copy.data = newData;
copy.update = { data: newData };
store.commit(vpagename, copy);
newDataAvailable.value = false;
pendingNewData.value = null;
};
const manualRefresh = () => {
if (pendingNewData.value) {
updateDataDisplay(pendingNewData.value);
}
};
const refreshData = async () => {
stopAutoCheck();
newDataAvailable.value = false;
pendingNewData.value = null;
await getApi();
lastDataHash.value = null;
await checkDataChanges();
newDataAvailable.value = false;
startAutoCheck();
};
provide("refreshData", refreshData);
watch(
() => props.realtime,
(newVal) => {
if (newVal) {
realtimeConfig.value.time = newVal.time || 0;
realtimeConfig.value.update = newVal.update === true;
startAutoCheck();
}
},
{ deep: true },
);
onDeactivated(() => {
stopAutoCheck();
});
onActivated(() => {
startAutoCheck();
});
onBeforeUnmount(() => {
stopAutoCheck();
});
const timeOption = (v) => {
if (!v) return getApi();
if (v.filter_or) {
if (vfilter) vfilter = undefined;
if (vparams) {
vparams.filter_or = v.filter_or;
} else {
const found = $copy($findapi(props.api));
found.params.filter_or = v.filter_or;
if (props.filter) {
const filter = $copy(props.filter);
for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace("$", "")].id;
}
}
found.params.filter = filter;
}
vparams = found.params;
}
return getApi();
}
let filter = vfilter ? vfilter : props.params ? props.params.filter || {} : {};
for (const [key, value] of Object.entries(v.filter)) {
filter[key] = value;
}
for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace("$", "")].id;
}
}
if (vfilter) {
vfilter = filter;
vparams = undefined;
} else if (vparams) {
vparams.filter = filter;
vparams.filter_or = undefined;
}
if (!vfilter && !vparams) vfilter = filter;
getApi();
};
const edit = (v) => {
const copy = props.modal ? $copy(props.modal) : {};
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api, row: v };
f.pagename = vpagename;
f.row = v;
copy.vbind = f;
showmodal.value = copy;
};
const insert = () => {
const copy = props.modal ? $copy(props.modal) : {};
const f = copy.vbind ? copy.vbind : { pagename: vpagename, api: props.api };
f.pagename = vpagename;
copy.vbind = f;
showmodal.value = copy;
};
const getApi = async () => {
const connlist = [];
let row = props.setting?.id ? $copy(props.setting) : undefined;
if (!row) {
const found = $find(
store.settings.filter((v) => v),
props.setting > 0 ? { id: props.setting } : { name: props.setting },
);
if (found) row = $copy(found);
}
if (!row) {
const conn = $findapi("usersetting");
conn.params.filter = props.setting > 0 ? { id: props.setting } : { name: props.setting };
connlist.push(conn);
}
let data = props.data ? $copy(props.data) : undefined;
if (!data) {
const conn1 = $findapi(props.api);
if (vfilter) {
const filter = conn1.params.filter || {};
for (const [key, value] of Object.entries(vfilter)) {
filter[key] = value;
}
for (const [key, value] of Object.entries(filter)) {
if (value.toString().indexOf("$") >= 0) {
filter[key] = store[value.replace("$", "")].id;
}
}
conn1.params.filter = filter;
}
if (vparams) conn1.params = vparams;
connection = conn1;
connlist.push(conn1);
}
let obj = undefined;
if (connlist.length > 0) {
const rs = await $getapi(connlist);
const ele = $find(rs, { name: "usersetting" });
if (ele) {
row = $find(ele.data.rows, { name: props.setting.name || props.setting });
const copy = $copy(store.settings);
copy.push(row);
store.commit("settings", copy);
}
obj = $find(rs, { name: props.api });
if (obj) data = $copy(obj.data.rows);
}
pagedata.value = $setpage(vpagename, row, obj);
const copy = $clone(pagedata.value);
copy.data = data;
copy.update = { data: data };
store.commit(vpagename, copy);
};
const dataEvent = (v, field, data) => {
if (data?.modal) {
const copy = $copy(data.modal);
const f = copy.vbind ?? {};
if (!f.api) f.api = props.api;
f.pagename = vpagename;
if (!f.row) f.row = v;
copy.vbind = f;
copy.field = field;
showmodal.value = copy;
}
emit("modalevent", { name: "dataevent", data: { row: v, field } });
emit("dataevent", v, field);
};
const exportExcel = async () => {
if (!props.api) return;
const found = $findapi("exportcsv");
found.params = connection.params;
const fields = pagedata.value.fields
.filter((v) => (v.show && v.export !== "no") || v.export === "yes")
.map((x) => ({ name: x.name, label: $stripHtml(x.label) }));
found.params.fields = JSON.stringify(fields);
found.url = connection.url.replace("data/", "exportcsv/");
const rs = await $getapi([found]);
if (rs === "error") {
$snackbar("Đã xảy ra lỗi. Vui lòng thử lại.");
return;
}
const url = window.URL.createObjectURL(new Blob([rs[0].data]));
const link = document.createElement("a");
const fileName = `${$dayjs(new Date()).format("YYYYMMDDhhmmss")}-data.csv`;
link.href = url;
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
link.remove();
};
if (!props.timeopt) await getApi();
startAutoCheck();
</script>