377 lines
10 KiB
Vue
377 lines
10 KiB
Vue
<template>
|
|
<div>
|
|
<div
|
|
class="field has-addons"
|
|
:id="docid"
|
|
>
|
|
<div class="control has-icons-left has-icons-right is-expanded">
|
|
<div
|
|
:class="['dropdown', pos, { 'is-active': focused }]"
|
|
style="width: 100%"
|
|
>
|
|
<div
|
|
class="dropdown-trigger"
|
|
style="width: 100%"
|
|
>
|
|
<input
|
|
:class="[
|
|
'input',
|
|
{
|
|
'is-danger': error,
|
|
'has-text-dark': disabled,
|
|
},
|
|
]"
|
|
:disabled="disabled"
|
|
type="text"
|
|
@focus="setFocus"
|
|
@blur="lostFocus"
|
|
@keyup.enter="pressEnter"
|
|
@keyup.esc="lostFocus"
|
|
@keyup="beginSearch"
|
|
v-model="value"
|
|
:placeholder="placeholder"
|
|
/>
|
|
<span class="icon is-left">
|
|
<Icon
|
|
name="material-symbols:search"
|
|
:size="22"
|
|
/>
|
|
</span>
|
|
<span class="icon is-right">
|
|
<Icon
|
|
name="material-symbols:keyboard-arrow-down-rounded"
|
|
:size="22"
|
|
/>
|
|
</span>
|
|
</div>
|
|
<div
|
|
class="dropdown-menu"
|
|
style="min-width: 100%"
|
|
role="menu"
|
|
@click="doClick()"
|
|
>
|
|
<div
|
|
class="dropdown-content px-3"
|
|
style="min-width: 100%"
|
|
>
|
|
<p
|
|
v-if="data.length === 0"
|
|
class="has-text-grey px-2 py-1"
|
|
>
|
|
{{ isVietnamese ? "Không có giá trị thỏa mãn" : "No matching values" }}
|
|
</p>
|
|
<ScrollBox
|
|
v-else
|
|
v-bind="{
|
|
data: data,
|
|
name: field,
|
|
fontsize: 14,
|
|
maxheight: '200px',
|
|
notick: true,
|
|
}"
|
|
@selected="choose"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div
|
|
class="control"
|
|
v-if="clearable && value"
|
|
>
|
|
<button
|
|
class="button is-primary px-2"
|
|
@click="clearValue"
|
|
style="height: 100%"
|
|
type="button"
|
|
>
|
|
<SvgIcon v-bind="{ name: 'close.svg', type: 'white', size: 24 }"></SvgIcon>
|
|
</button>
|
|
</div>
|
|
<div
|
|
class="control"
|
|
v-if="viewaddon"
|
|
>
|
|
<button
|
|
class="button is-dark px-2"
|
|
@click="viewInfo()"
|
|
style="height: 100%"
|
|
type="button"
|
|
>
|
|
<SvgIcon v-bind="{ name: 'view.svg', type: 'white', size: 24 }"></SvgIcon>
|
|
</button>
|
|
</div>
|
|
<div
|
|
class="control"
|
|
v-if="addon"
|
|
>
|
|
<button
|
|
class="button is-primary"
|
|
@click="addNew()"
|
|
style="height: 100%"
|
|
type="button"
|
|
>
|
|
<span class="icon">
|
|
<Icon
|
|
name="material-symbols:add-2-rounded"
|
|
:size="22"
|
|
/>
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<Modal
|
|
@dataevent="dataevent"
|
|
@close="showmodal = undefined"
|
|
v-bind="showmodal"
|
|
v-if="showmodal"
|
|
></Modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { useStore } from "@/stores/index";
|
|
|
|
export default {
|
|
props: [
|
|
"api",
|
|
"field",
|
|
"column",
|
|
"first",
|
|
"optionid",
|
|
"filter",
|
|
"addon",
|
|
"viewaddon",
|
|
"position",
|
|
"disabled",
|
|
"vdata",
|
|
"clearable",
|
|
"placeholder",
|
|
"searchfield",
|
|
],
|
|
setup() {
|
|
const store = useStore();
|
|
return { store };
|
|
},
|
|
data() {
|
|
return {
|
|
search: undefined,
|
|
data: [],
|
|
timer: undefined,
|
|
value: undefined,
|
|
selected: undefined,
|
|
showmodal: undefined,
|
|
params: this.api ? this.$findapi(this.api)["params"] : undefined,
|
|
orgdata: undefined,
|
|
error: false,
|
|
focused: false,
|
|
count1: 0,
|
|
count2: 0,
|
|
docid: this.$id(),
|
|
pos: undefined,
|
|
};
|
|
},
|
|
computed: {
|
|
isVietnamese() {
|
|
return this.store.lang === "vi";
|
|
},
|
|
},
|
|
async created() {
|
|
this.getPos();
|
|
if (this.vdata) {
|
|
this.orgdata = this.$copy(this.vdata);
|
|
this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
|
|
}
|
|
if (this.first) {
|
|
this.data = this.orgdata ? this.$copy(this.orgdata) : await this.getData();
|
|
if (this.optionid) {
|
|
let f = {};
|
|
f[this.field] = this.optionid;
|
|
if (this.optionid > 0) f = { id: this.optionid };
|
|
this.selected = this.$find(this.data, f);
|
|
if (this.selected && this.vdata) {
|
|
return (this.value = this.selected[this.field]);
|
|
}
|
|
}
|
|
} else if (this.optionid) {
|
|
this.selected = await this.$getdata(this.api, { id: this.optionid }, undefined, true);
|
|
}
|
|
if (this.selected) this.doSelect(this.selected);
|
|
},
|
|
watch: {
|
|
optionid: function (newVal) {
|
|
if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
|
|
if (this.selected) this.doSelect(this.selected);
|
|
else this.value = undefined;
|
|
},
|
|
filter: async function (newVal) {
|
|
this.data = await this.getData();
|
|
},
|
|
vdata: function (newval) {
|
|
if (newval) {
|
|
this.orgdata = this.$copy(this.vdata);
|
|
this.orgdata.map((v) => (v.search = this.$nonAccent(v[this.field])));
|
|
this.data = this.$copy(this.orgdata);
|
|
this.selected = undefined;
|
|
this.value = undefined;
|
|
if (this.optionid) this.selected = this.$find(this.data, { id: this.optionid });
|
|
if (this.selected) this.doSelect(this.selected);
|
|
}
|
|
},
|
|
},
|
|
methods: {
|
|
choose(v) {
|
|
this.focused = false;
|
|
this.count1 = 0;
|
|
this.count2 = 0;
|
|
this.doSelect(v);
|
|
},
|
|
setFocus() {
|
|
this.focused = true;
|
|
this.count1 = 0;
|
|
this.count2 = 0;
|
|
},
|
|
lostFocus() {
|
|
let self = this;
|
|
setTimeout(() => {
|
|
if (self.focused && self.count1 === 0) self.focused = false;
|
|
}, 200);
|
|
},
|
|
pressEnter() {
|
|
if (this.data.length === 0) return;
|
|
this.choose(this.data[0]);
|
|
},
|
|
doClick() {
|
|
this.count1 += 1;
|
|
},
|
|
doSelect(option) {
|
|
if (this.$empty(option)) return;
|
|
this.$emit("option", option);
|
|
this.$emit("modalevent", { name: "option", data: option });
|
|
this.selected = option;
|
|
this.value = this.selected[this.field];
|
|
},
|
|
clearValue() {
|
|
this.value = undefined;
|
|
this.selected = undefined;
|
|
this.$emit("option", null);
|
|
this.$emit("modalevent", { name: "option", data: null });
|
|
},
|
|
findObject(val) {
|
|
let rows = this.$copy(this.orgdata);
|
|
if (this.$empty(val)) this.data = rows;
|
|
else {
|
|
let text = this.$nonAccent(val);
|
|
this.data = rows.filter((v) => v.search.toLowerCase().indexOf(text.toLowerCase()) >= 0);
|
|
}
|
|
},
|
|
async getData() {
|
|
this.params.filter = this.filter;
|
|
let data = await this.$getdata(this.api, undefined, this.params);
|
|
return data;
|
|
},
|
|
async getApi(val) {
|
|
if (this.vdata) return this.findObject(val);
|
|
let text = val ? val.toLowerCase() : "";
|
|
let f = {};
|
|
|
|
// Sử dụng searchfield nếu có, nếu không thì dùng column
|
|
const fieldsToSearch = this.searchfield || this.column;
|
|
|
|
fieldsToSearch.map((v) => {
|
|
f[`${v}__icontains`] = text;
|
|
});
|
|
this.params.filter_or = f;
|
|
if (this.filter) this.params.filter = this.$copy(this.filter);
|
|
let arr = await this.$getdata(this.api, undefined, this.params);
|
|
this.data = this.$copy(arr);
|
|
},
|
|
beginSearch(e) {
|
|
let val = e.target.value;
|
|
this.search = val;
|
|
if (this.timer) clearTimeout(this.timer);
|
|
this.timer = setTimeout(() => this.getApi(val), 150);
|
|
},
|
|
addNew() {
|
|
this.showmodal = this.$copy(this.addon);
|
|
},
|
|
dataevent(v) {
|
|
console.log("SearchBox received dataevent:", v); // Debug log
|
|
|
|
if (!v || !v.id) {
|
|
console.error("Invalid data received in SearchBox:", v);
|
|
return;
|
|
}
|
|
|
|
// Tìm và cập nhật trong danh sách
|
|
let idx = this.$findIndex(this.data, { id: v.id });
|
|
if (idx < 0) {
|
|
// Nếu chưa có trong danh sách, thêm vào đầu
|
|
this.data.unshift(v);
|
|
console.log("Added new item to data:", v);
|
|
} else {
|
|
// Nếu đã có, cập nhật
|
|
this.data[idx] = v;
|
|
console.log("Updated existing item in data:", v);
|
|
}
|
|
|
|
// Cập nhật orgdata nếu có
|
|
if (this.orgdata) {
|
|
let orgIdx = this.$findIndex(this.orgdata, { id: v.id });
|
|
if (orgIdx < 0) {
|
|
this.orgdata.unshift(v);
|
|
// Thêm search field cho orgdata
|
|
if (this.field && v[this.field]) {
|
|
v.search = this.$nonAccent(v[this.field]);
|
|
}
|
|
} else {
|
|
this.orgdata[orgIdx] = v;
|
|
if (this.field && v[this.field]) {
|
|
this.orgdata[orgIdx].search = this.$nonAccent(v[this.field]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// **Tự động select item vừa tạo/cập nhật**
|
|
this.doSelect(v);
|
|
|
|
// Đóng modal
|
|
this.showmodal = undefined;
|
|
|
|
console.log("SearchBox data after update:", this.data);
|
|
},
|
|
viewInfo() {
|
|
if (!this.selected)
|
|
return this.$dialog(
|
|
this.isVietnamese ? "Vui lòng lựa chọn trước khi xem thông tin." : "Please select before viewing",
|
|
this.isVietnamese ? "Thông báo" : "Notice",
|
|
);
|
|
let copy = this.$copy(this.viewaddon);
|
|
copy.vbind = { row: this.selected };
|
|
this.showmodal = copy;
|
|
},
|
|
getPos() {
|
|
switch (this.position) {
|
|
case "is-top-left":
|
|
this.pos = "is-up is-left";
|
|
break;
|
|
case "is-top-right":
|
|
this.pos = "is-up is-right";
|
|
break;
|
|
case "is-bottom-left":
|
|
this.pos = "is-right";
|
|
break;
|
|
case "is-bottom-right":
|
|
this.pos = "is-right";
|
|
break;
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
<style scoped>
|
|
.field:not(:last-child) {
|
|
margin-bottom: 0;
|
|
}
|
|
</style>
|