Files
web/app/components/SearchBox.vue
2026-05-19 10:00:15 +07:00

398 lines
9.7 KiB
Vue

<template>
<div>
<div
class="field has-addons"
:id="$id()"
>
<div :class="['control has-icons-left has-icons-right is-expanded', isLoading && 'is-loading']">
<div
:class="['dropdown', pos, { 'is-active': focused }]"
style="width: 100%"
>
<div
class="dropdown-trigger"
style="width: 100%"
>
<input
:class="[
'input',
{
'has-text-dark': disabled,
},
]"
:disabled="disabled"
type="text"
@focus="setFocus"
@blur="lostFocus"
@keyup.enter="pressEnter"
@keyup.esc="lostFocus"
@input="(e) => debouncedGetSuggestions(e.target.value)"
v-model="value"
:placeholder="placeholder"
/>
<span class="icon is-left">
<Icon
name="material-symbols:search"
:size="20"
/>
</span>
<span class="icon is-right">
<Icon
name="material-symbols:keyboard-arrow-down-rounded"
:size="20"
/>
</span>
</div>
<div
class="dropdown-menu"
style="min-width: 100%"
role="menu"
>
<div
class="dropdown-content"
style="min-width: 100%"
>
<ScrollBox
v-if="suggestions.length > 0"
v-bind="{
data: suggestions,
name: field,
fontsize: 14,
maxHeight: '200px',
}"
@selected="choose"
/>
<p
v-else
class="has-text-grey px-2 py-1"
>
Không có giá trị thoả mãn
</p>
</div>
</div>
</div>
</div>
<div
v-if="clearable && value"
class="control"
>
<button
class="button is-light"
@click="clearValue"
style="height: 100%"
type="button"
>
<span class="icon">
<Icon
name="material-symbols:close-rounded"
:size="16"
/>
</span>
</button>
</div>
<div
v-if="viewaddon"
class="control"
>
<button
class="button is-primary is-light"
@click="viewInfo()"
style="height: 100%"
type="button"
>
<span class="icon">
<Icon
name="material-symbols:visibility-rounded"
:size="20"
/>
</span>
</button>
</div>
<div
v-if="addon"
class="control"
>
<button
class="button is-success is-light"
@click="addNew"
style="height: 100%"
type="button"
>
<span class="icon">
<Icon
name="material-symbols:add-rounded"
:size="18"
/>
</span>
</button>
</div>
</div>
<Modal
v-if="showmodal"
v-bind="showmodal"
@dataevent="dataevent"
@close="showmodal = undefined"
/>
</div>
</template>
<script setup>
import ScrollBox from "@/components/datatable/ScrollBox.vue";
import { debounce } from "es-toolkit";
const props = defineProps({
api: String,
field: String,
column: Array,
first: Boolean,
optionid: String,
filter: Object,
addon: Object,
viewaddon: Object,
position: String,
disabled: Boolean,
vdata: Array,
clearable: Boolean,
placeholder: String,
searchfield: Array,
});
const { $copy, $dialog, $empty, $find, $findapi, $findIndex, $getdata, $id, $nonAccent, $store } = useNuxtApp();
const emit = defineEmits(["option", "modalevent"]);
const suggestions = ref([]);
const isLoading = ref(false);
const value = ref();
const selected = ref();
const showmodal = ref();
const params = ref(props.api && $findapi(props.api).params);
const orgdata = ref();
const focused = ref(false);
const pos = ref();
getPos();
if (props.vdata) {
orgdata.value = props.vdata;
orgdata.value.forEach((v) => (v.search = $nonAccent(v[props.field])));
}
if (props.first) {
suggestions.value = orgdata.value ? $copy(orgdata.value) : await getData();
if (props.optionid) {
let f = {};
f[props.field] = props.optionid;
if (props.optionid > 0) f = { id: props.optionid };
selected.value = $find(suggestions.value, f);
if (selected.value && props.vdata) {
value.value = selected.value[props.field];
}
}
} else if (props.optionid) {
selected.value = await $getdata(props.api, { id: props.optionid }, undefined, true);
}
if (selected.value) doSelect(selected.value);
watch(
() => props.optionid,
() => {
if (props.optionid) selected.value = $find(suggestions.value, { id: props.optionid });
if (selected.value) doSelect(selected.value);
else value.value = undefined;
},
);
watch(
() => props.filter,
async () => {
suggestions.value = await getData();
},
);
watch(
() => props.vdata,
(newVal) => {
if (newVal) {
orgdata.value = $copy(props.vdata);
orgdata.value.forEach((v) => (v.search = $nonAccent(v[props.field])));
suggestions.value = $copy(orgdata.value);
selected.value = undefined;
value.value = undefined;
if (props.optionid) selected.value = $find(suggestions.value, { id: props.optionid });
if (selected.value) doSelect(selected.value);
}
},
);
function choose(v) {
focused.value = false;
doSelect(v);
}
function setFocus() {
focused.value = true;
}
function lostFocus() {
setTimeout(() => {
if (focused.value) focused.value = false;
}, 200);
}
function pressEnter() {
if (suggestions.value.length === 0) return;
choose(suggestions.value[0]);
}
function doSelect(option) {
if ($empty(option)) return;
emit("option", option);
emit("modalevent", { name: "option", data: option });
selected.value = option;
value.value = selected.value[props.field];
}
function clearValue() {
value.value = undefined;
selected.value = undefined;
emit("option", null);
emit("modalevent", { name: "option", data: null });
}
watch(value, (newVal) => {
if (newVal === "") clearValue();
});
function findObject(val) {
const rows = $copy(orgdata.value);
if ($empty(val)) suggestions.value = rows;
else {
const text = $nonAccent(val);
suggestions.value = rows.filter((v) => v.search.toLowerCase().indexOf(text.toLowerCase()) >= 0);
}
}
async function getData() {
isLoading.value = true;
params.value.filter = props.filter;
const data = await $getdata(props.api, undefined, params.value);
isLoading.value = false;
return data;
}
async function getSuggestions(val) {
if (props.vdata) return findObject(val);
const text = val ? val.toLowerCase() : "";
const filter_or = {};
// Sử dụng searchfield nếu có, nếu không thì dùng column
const fieldsToSearch = props.searchfield || props.column;
fieldsToSearch.forEach((v) => {
filter_or[`${v}__icontains`] = text;
});
params.value.filter_or = filter_or;
if (props.filter) params.value.filter = $copy(props.filter);
isLoading.value = true;
suggestions.value = await $getdata(props.api, undefined, params.value);
isLoading.value = false;
}
const debouncedGetSuggestions = debounce(getSuggestions, 200);
function addNew() {
showmodal.value = props.addon;
}
function 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
const idx = $findIndex(suggestions.value, { id: v.id });
if (idx < 0) {
// Nếu chưa có trong danh sách, thêm vào đầu
suggestions.value.unshift(v);
console.log("Added new item to data:", v);
} else {
// Nếu đã có, cập nhật
suggestions.value[idx] = v;
console.log("Updated existing item in data:", v);
}
// Cập nhật orgdata nếu có
if (orgdata.value) {
let orgIdx = $findIndex(orgdata.value, { id: v.id });
if (orgIdx < 0) {
orgdata.value.unshift(v);
// Thêm search field cho orgdata
if (props.field && v[props.field]) {
v.search = $nonAccent(v[props.field]);
}
} else {
orgdata.value[orgIdx] = v;
if (props.field && v[props.field]) {
orgdata.value[orgIdx].search = $nonAccent(v[props.field]);
}
}
}
// **Tự động select item vừa tạo/cập nhật**
doSelect(v);
// Đóng modal
showmodal.value = undefined;
}
function viewInfo() {
if (!selected.value)
return $dialog(
$store.lang === "vi" ? "Vui lòng lựa chọn trước khi xem thông tin." : "Please select before viewing",
$store.lang === "vi" ? "Thông báo" : "Notice",
);
showmodal.value = {
...props.viewaddon,
vbind: { row: selected.value },
};
}
function getPos() {
switch (props.position) {
case "is-top-left":
pos.value = "is-up is-left";
break;
case "is-top-right":
pos.value = "is-up is-right";
break;
case "is-bottom-left":
pos.value = "is-right";
break;
case "is-bottom-right":
pos.value = "is-right";
break;
}
}
</script>
<style scoped>
.control.is-loading::after {
inset-inline-end: 2.75em;
}
.field:not(:last-child) {
margin-bottom: 0;
}
.button.is-success {
&.is-light {
--bulma-button-background-l: 89%;
}
&:hover,
&.is-hovered {
--bulma-button-background-l-delta: -10%;
}
}
</style>