446 lines
13 KiB
Vue
446 lines
13 KiB
Vue
<template>
|
|
<div
|
|
v-if="timeRanges || !enableTime"
|
|
class="has-text-primary fixed-grid has-12-cols"
|
|
style="border-bottom: 2px solid var(--bulma-grey-80)"
|
|
>
|
|
<div class="grid mb-3">
|
|
<div
|
|
v-if="enableTime"
|
|
class="cell is-col-span-7 is-flex is-align-items-center is-flex-wrap-wrap"
|
|
style="gap: 0.5rem 1rem"
|
|
>
|
|
<Caption
|
|
:title="lang === 'vi' ? 'Thời gian' : 'Time'"
|
|
type="has-text-orange"
|
|
/>
|
|
<div
|
|
v-for="v in timeRanges"
|
|
:key="v.code"
|
|
:class="['is-flex is-align-items-center', v.code !== current && 'is-clickable']"
|
|
style="gap: 0.35rem"
|
|
@click="v.code !== current && changeOption(v)"
|
|
>
|
|
<span
|
|
:class="v.code === current ? 'font-bold has-text-orange' : 'font-medium has-text-grey-50'"
|
|
style="text-wrap: nowrap"
|
|
>
|
|
{{ v.name }}
|
|
</span>
|
|
<span
|
|
:class="[
|
|
'tag rounded-md w-5 h-6 fs-13',
|
|
v.code === current
|
|
? 'font-bold has-text-orange has-background-orange-90'
|
|
: 'font-medium has-text-grey-40 has-background-grey-90',
|
|
]"
|
|
>{{ v.count }}</span
|
|
>
|
|
</div>
|
|
<span
|
|
v-if="newDataAvailable"
|
|
class="has-text-danger-50 px-3 py-1.5 has-background-danger-95 rounded-md is-italic fs-14"
|
|
>Có dữ liệu mới, vui lòng làm mới.</span
|
|
>
|
|
</div>
|
|
<div
|
|
class="cell is-col-span-5 is-flex is-align-items-center is-flex-wrap-wrap"
|
|
style="gap: 0.25rem 0.5rem"
|
|
>
|
|
<Caption
|
|
:title="lang === 'vi' ? `Tìm ${this.store.viewport > 2 ? 'kiếm' : ''}` : 'Search'"
|
|
type="has-text-orange"
|
|
/>
|
|
<input
|
|
id="input"
|
|
type="text"
|
|
v-model="text"
|
|
@keyup="startSearch"
|
|
:placeholder="lang === 'vi' ? 'Nhập từ khóa...' : 'Enter keyword...'"
|
|
class="input is-orange fs-12"
|
|
:style="{
|
|
maxWidth: '150px',
|
|
width: this.store.viewport === 1 ? '150px' : 'auto',
|
|
}"
|
|
/>
|
|
<div class="field has-addons is-flex-wrap-wrap is-align-items-center">
|
|
<p
|
|
v-if="importdata && $getEditRights()"
|
|
class="control"
|
|
>
|
|
<button
|
|
class="button is-ghost has-text-orange fs-14"
|
|
@click="openImport()"
|
|
>
|
|
<span class="icon">
|
|
<Icon
|
|
name="material-symbols:upload-rounded"
|
|
:size="22"
|
|
/>
|
|
</span>
|
|
</button>
|
|
<span
|
|
class="tooltiptext has-background-orange-soft has-text-orange-bold"
|
|
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
|
>
|
|
Nhập dữ liệu
|
|
</span>
|
|
</p>
|
|
<p
|
|
v-if="enableAdd && $getEditRights()"
|
|
class="control"
|
|
>
|
|
<button
|
|
class="button is-ghost has-text-orange fs-14"
|
|
@click="$emit('add')"
|
|
>
|
|
<span class="icon">
|
|
<Icon
|
|
name="material-symbols:add-rounded"
|
|
:size="22"
|
|
/>
|
|
</span>
|
|
</button>
|
|
<span
|
|
class="tooltiptext has-background-orange-soft has-text-orange-bold"
|
|
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
|
>
|
|
Thêm mới
|
|
</span>
|
|
</p>
|
|
<p class="control">
|
|
<button
|
|
class="button is-ghost has-text-orange fs-14"
|
|
@click="$emit('excel')"
|
|
>
|
|
<span class="icon">
|
|
<Icon
|
|
name="mdi:microsoft-excel"
|
|
:size="22"
|
|
/>
|
|
</span>
|
|
</button>
|
|
<span
|
|
class="tooltiptext has-background-orange-soft has-text-orange-bold"
|
|
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
|
>
|
|
Xuất Excel
|
|
</span>
|
|
</p>
|
|
<p class="control">
|
|
<button
|
|
class="button is-ghost has-text-orange fs-14"
|
|
@click="$emit('refresh-data')"
|
|
>
|
|
<span class="icon">
|
|
<Icon
|
|
name="material-symbols:refresh-rounded"
|
|
:size="22"
|
|
/>
|
|
</span>
|
|
</button>
|
|
<span
|
|
class="tooltiptext has-background-orange-soft has-text-orange-bold"
|
|
style="top: 110%; bottom: unset; min-width: max-content; left: -45px"
|
|
>
|
|
Làm mới
|
|
</span>
|
|
</p>
|
|
<Icon
|
|
v-if="loading"
|
|
name="svg-spinners:90-ring"
|
|
:size="22"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Modal
|
|
v-if="showmodal"
|
|
v-bind="showmodal"
|
|
@close="showmodal = undefined"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { useStore } from "@/stores/index";
|
|
|
|
export default {
|
|
setup() {
|
|
const store = useStore();
|
|
return { store };
|
|
},
|
|
|
|
props: ["pagename", "api", "timeopt", "filter", "realtime", "newDataAvailable", "params", "importdata"],
|
|
|
|
data() {
|
|
return {
|
|
options: [
|
|
{ code: 0, name: this.store.lang === "vi" ? "Hôm nay" : "Today" },
|
|
{ code: 1, name: "1D" },
|
|
{ code: 7, name: "7D" },
|
|
{ code: 30, name: "1M" },
|
|
{ code: 90, name: "3M" },
|
|
{ code: 180, name: "6M" },
|
|
{ code: 360, name: "1Y" },
|
|
{ code: 36000, name: this.store.lang === "vi" ? "Tất cả" : "All" },
|
|
],
|
|
showmodal: undefined,
|
|
current: 7,
|
|
search: undefined,
|
|
timer: undefined,
|
|
text: undefined,
|
|
status: [{ code: 100, name: "Chọn" }],
|
|
timeRanges: undefined,
|
|
enableAdd: true,
|
|
enableTime: true,
|
|
choices: [], // Sẽ được cập nhật động từ pagedata
|
|
pagedata: undefined,
|
|
loading: false,
|
|
pollingInterval: null,
|
|
searchableFields: [], // Lưu thông tin các field có thể tìm kiếm
|
|
};
|
|
},
|
|
|
|
watch: {
|
|
pagename() {
|
|
this.updateSearchableFields();
|
|
},
|
|
},
|
|
|
|
async created() {
|
|
// Cập nhật searchable fields ngay từ đầu
|
|
this.updateSearchableFields();
|
|
|
|
if (this.store.viewport < 5) {
|
|
this.options = [
|
|
{ code: 0, name: "Hôm nay" },
|
|
{ code: 1, name: "1D" },
|
|
{ code: 7, name: "7D" },
|
|
{ code: 30, name: "1M" },
|
|
{ code: 90, name: "3M" },
|
|
{ code: 36000, name: "Tất cả" },
|
|
];
|
|
}
|
|
|
|
this.checkTimeopt();
|
|
if (!this.enableTime) return this.$emit("option");
|
|
|
|
let found = this.$findapi(this.api);
|
|
found.commit = undefined;
|
|
|
|
let filter = this.$copy(this.filter);
|
|
if (filter) {
|
|
// dynamic parameter
|
|
for (const [key, value] of Object.entries(filter)) {
|
|
if (value.toString().indexOf("$") >= 0) {
|
|
filter[key] = this.store[value.replace("$", "")].id;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (found.params.filter) {
|
|
if (!filter) filter = {};
|
|
for (const [key, value] of Object.entries(found.params.filter)) {
|
|
filter[key] = value;
|
|
}
|
|
}
|
|
|
|
this.options.map((v) => {
|
|
let f = filter ? this.$copy(filter) : {};
|
|
f["create_time__date__gte"] = this.$dayjs().subtract(v.code, "day").format("YYYY-MM-DD");
|
|
v.filter = f;
|
|
});
|
|
|
|
this.$emit("option", this.$find(this.options, { code: this.current }));
|
|
|
|
let f = {};
|
|
this.options.map((v) => {
|
|
f[v.code] = {
|
|
type: "Count",
|
|
field: "create_time__date",
|
|
filter: v.filter,
|
|
};
|
|
});
|
|
|
|
let params = { summary: "aggregate", distinct_values: f };
|
|
found.params = params;
|
|
|
|
try {
|
|
let rs = await this.$getapi([found]);
|
|
for (const [key, value] of Object.entries(rs[0].data.rows)) {
|
|
const found = this.$find(this.options, { code: Number(key) });
|
|
if (found) {
|
|
found.count = value;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error("Error fetching data:", error);
|
|
}
|
|
|
|
this.timeRanges = this.$copy(this.options);
|
|
},
|
|
|
|
mounted() {
|
|
if (this.realtime) {
|
|
const interval = typeof this.realtime === "number" ? this.realtime * 1000 : 5000;
|
|
this.pollingInterval = setInterval(this.refresh, interval);
|
|
}
|
|
},
|
|
|
|
beforeUnmount() {
|
|
if (this.pollingInterval) {
|
|
clearInterval(this.pollingInterval);
|
|
}
|
|
},
|
|
|
|
computed: {
|
|
lang: function () {
|
|
return this.store.lang;
|
|
},
|
|
},
|
|
|
|
methods: {
|
|
// Lấy các field có thể tìm kiếm từ API params.values
|
|
updateSearchableFields() {
|
|
try {
|
|
// Lấy API config
|
|
const found = this.$findapi(this.api);
|
|
|
|
if (!found) {
|
|
console.warn("Không tìm thấy API config");
|
|
this.choices = [];
|
|
this.searchableFields = [];
|
|
return;
|
|
}
|
|
|
|
// Ưu tiên lấy values từ props.params, nếu không có thì lấy từ API config
|
|
let valuesString = "";
|
|
|
|
if (this.params && this.params.values) {
|
|
// Lấy từ props.params (ưu tiên cao nhất)
|
|
valuesString = this.params.values;
|
|
} else if (found.params && found.params.values) {
|
|
// Lấy từ API config mặc định
|
|
valuesString = found.params.values;
|
|
} else {
|
|
console.warn("Không tìm thấy API values trong props hoặc config");
|
|
this.choices = [];
|
|
this.searchableFields = [];
|
|
return;
|
|
}
|
|
|
|
// Parse values string từ API
|
|
let fieldNames = valuesString.split(",").map((v) => v.trim());
|
|
// Lấy pagedata để lấy label
|
|
this.pagedata = this.store[this.pagename];
|
|
|
|
// Lấy tất cả các field để search (không lọc format)
|
|
const searchable = fieldNames.filter((fieldName) => {
|
|
// Loại bỏ các field kỹ thuật
|
|
if (
|
|
fieldName === "id" ||
|
|
fieldName === "create_time" ||
|
|
fieldName === "update_time" ||
|
|
fieldName === "created_at" ||
|
|
fieldName === "updated_at"
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
});
|
|
|
|
// Lấy tên và label các field
|
|
this.choices = searchable;
|
|
this.searchableFields = searchable.map((fieldName) => {
|
|
// Lấy field base name (trước dấu __)
|
|
const baseFieldName = fieldName.split("__")[0];
|
|
const fieldInfo =
|
|
this.pagedata && this.pagedata.fields ? this.pagedata.fields.find((f) => f.name === baseFieldName) : null;
|
|
|
|
return {
|
|
name: fieldName,
|
|
label: fieldInfo ? fieldInfo.label : fieldName,
|
|
};
|
|
});
|
|
} catch (error) {
|
|
console.error("Error updating searchable fields:", error);
|
|
this.choices = [];
|
|
this.searchableFields = [];
|
|
}
|
|
},
|
|
|
|
refresh() {
|
|
let found = this.$find(this.options, { code: this.current });
|
|
this.changeOption(found);
|
|
},
|
|
|
|
changeOption(v) {
|
|
this.current = v.code;
|
|
if (this.search) {
|
|
this.text = undefined;
|
|
this.search = undefined;
|
|
}
|
|
this.$emit("option", this.$find(this.timeRanges, { code: this.current }));
|
|
},
|
|
|
|
doSearch() {
|
|
// Cập nhật choices trước khi search
|
|
this.updateSearchableFields();
|
|
|
|
this.pagedata = this.store[this.pagename];
|
|
|
|
if (!this.pagedata || !this.pagedata.fields) {
|
|
console.warn("Không có pagedata hoặc fields");
|
|
return;
|
|
}
|
|
|
|
let fields = this.pagedata.fields.filter((v) => this.choices.findIndex((x) => x === v.name) >= 0);
|
|
|
|
if (fields.length === 0) {
|
|
console.warn("Không tìm thấy field để tìm kiếm");
|
|
return;
|
|
}
|
|
|
|
let f = {};
|
|
fields.map((v) => {
|
|
f[`${v.name}__icontains`] = this.search;
|
|
});
|
|
|
|
this.$emit("option", { filter_or: f });
|
|
},
|
|
|
|
openImport() {
|
|
if (!this.importdata) return;
|
|
// Emit event lên parent (DataView)
|
|
this.$emit("import", this.importdata);
|
|
},
|
|
|
|
startSearch(val) {
|
|
this.search = this.$empty(val.target.value) ? "" : val.target.value.trim();
|
|
|
|
if (this.timer) clearTimeout(this.timer);
|
|
this.timer = setTimeout(() => this.doSearch(), 300);
|
|
},
|
|
|
|
checkTimeopt() {
|
|
if (this.timeopt > 0) {
|
|
let obj = this.$find(this.options, {
|
|
code: this.$formatNumber(this.timeopt),
|
|
});
|
|
if (obj) this.current = obj.code;
|
|
}
|
|
if (this.timeopt ? this.$empty(this.timeopt.disable) : true) return;
|
|
if (this.timeopt.disable.indexOf("add") >= 0) this.enableAdd = false;
|
|
if (this.timeopt.disable.indexOf("time") >= 0) this.enableTime = false;
|
|
if (this.timeopt.time) {
|
|
let obj = this.$find(this.options, {
|
|
code: this.$formatNumber(this.timeopt.time),
|
|
});
|
|
if (obj) this.current = obj.code;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|