This commit is contained in:
Viet An
2026-05-19 10:00:15 +07:00
parent 1283757903
commit 5768a8a9fb
7 changed files with 170 additions and 146 deletions

View File

@@ -4,7 +4,7 @@
class="field has-addons" class="field has-addons"
:id="$id()" :id="$id()"
> >
<div class="control has-icons-left has-icons-right is-expanded"> <div :class="['control has-icons-left has-icons-right is-expanded', isLoading && 'is-loading']">
<div <div
:class="['dropdown', pos, { 'is-active': focused }]" :class="['dropdown', pos, { 'is-active': focused }]"
style="width: 100%" style="width: 100%"
@@ -26,7 +26,7 @@
@blur="lostFocus" @blur="lostFocus"
@keyup.enter="pressEnter" @keyup.enter="pressEnter"
@keyup.esc="lostFocus" @keyup.esc="lostFocus"
@keyup="beginSearch" @input="(e) => debouncedGetSuggestions(e.target.value)"
v-model="value" v-model="value"
:placeholder="placeholder" :placeholder="placeholder"
/> />
@@ -47,20 +47,18 @@
class="dropdown-menu" class="dropdown-menu"
style="min-width: 100%" style="min-width: 100%"
role="menu" role="menu"
@click="doClick()"
> >
<div <div
class="dropdown-content px-3" class="dropdown-content"
style="min-width: 100%" style="min-width: 100%"
> >
<ScrollBox <ScrollBox
v-if="data.length > 0" v-if="suggestions.length > 0"
v-bind="{ v-bind="{
data: data, data: suggestions,
name: field, name: field,
fontsize: 14, fontsize: 14,
maxheight: '200px', maxHeight: '200px',
notick: true,
}" }"
@selected="choose" @selected="choose"
/> />
@@ -68,12 +66,7 @@
v-else v-else
class="has-text-grey px-2 py-1" class="has-text-grey px-2 py-1"
> >
<Icon Không có giá trị thoả mãn
v-if="isLoading"
name="svg-spinners:90-ring"
:size="22"
/>
<span v-else>Không có giá trị thoả mãn</span>
</p> </p>
</div> </div>
</div> </div>
@@ -116,12 +109,12 @@
</button> </button>
</div> </div>
<div <div
class="control"
v-if="addon" v-if="addon"
class="control"
> >
<button <button
class="button is-success is-light" class="button is-success is-light"
@click="addNew()" @click="addNew"
style="height: 100%" style="height: 100%"
type="button" type="button"
> >
@@ -144,6 +137,9 @@
</template> </template>
<script setup> <script setup>
import ScrollBox from "@/components/datatable/ScrollBox.vue";
import { debounce } from "es-toolkit";
const props = defineProps({ const props = defineProps({
api: String, api: String,
field: String, field: String,
@@ -160,20 +156,17 @@ const props = defineProps({
placeholder: String, placeholder: String,
searchfield: Array, searchfield: Array,
}); });
const { $copy, $dialog, $empty, $find, $findapi, $findIndex, $getdata, $id, $nonAccent, $store } = useNuxtApp(); const { $copy, $dialog, $empty, $find, $findapi, $findIndex, $getdata, $id, $nonAccent, $store } = useNuxtApp();
const emit = defineEmits(["option", "modalevent"]); const emit = defineEmits(["option", "modalevent"]);
const search = ref(); const suggestions = ref([]);
const data = ref([]);
const isLoading = ref(false); const isLoading = ref(false);
const timer = ref();
const value = ref(); const value = ref();
const selected = ref(); const selected = ref();
const showmodal = ref(); const showmodal = ref();
const params = ref(props.api && $findapi(props.api).params); const params = ref(props.api && $findapi(props.api).params);
const orgdata = ref(); const orgdata = ref();
const focused = ref(false); const focused = ref(false);
const count1 = ref(0);
const count2 = ref(0);
const pos = ref(); const pos = ref();
getPos(); getPos();
@@ -183,12 +176,12 @@ if (props.vdata) {
orgdata.value.forEach((v) => (v.search = $nonAccent(v[props.field]))); orgdata.value.forEach((v) => (v.search = $nonAccent(v[props.field])));
} }
if (props.first) { if (props.first) {
data.value = orgdata.value ? $copy(orgdata.value) : await getData(); suggestions.value = orgdata.value ? $copy(orgdata.value) : await getData();
if (props.optionid) { if (props.optionid) {
let f = {}; let f = {};
f[props.field] = props.optionid; f[props.field] = props.optionid;
if (props.optionid > 0) f = { id: props.optionid }; if (props.optionid > 0) f = { id: props.optionid };
selected.value = $find(data.value, f); selected.value = $find(suggestions.value, f);
if (selected.value && props.vdata) { if (selected.value && props.vdata) {
value.value = selected.value[props.field]; value.value = selected.value[props.field];
} }
@@ -201,7 +194,7 @@ if (selected.value) doSelect(selected.value);
watch( watch(
() => props.optionid, () => props.optionid,
() => { () => {
if (props.optionid) selected.value = $find(data.value, { id: props.optionid }); if (props.optionid) selected.value = $find(suggestions.value, { id: props.optionid });
if (selected.value) doSelect(selected.value); if (selected.value) doSelect(selected.value);
else value.value = undefined; else value.value = undefined;
}, },
@@ -210,7 +203,7 @@ watch(
watch( watch(
() => props.filter, () => props.filter,
async () => { async () => {
data.value = await getData(); suggestions.value = await getData();
}, },
); );
@@ -220,10 +213,10 @@ watch(
if (newVal) { if (newVal) {
orgdata.value = $copy(props.vdata); orgdata.value = $copy(props.vdata);
orgdata.value.forEach((v) => (v.search = $nonAccent(v[props.field]))); orgdata.value.forEach((v) => (v.search = $nonAccent(v[props.field])));
data.value = $copy(orgdata.value); suggestions.value = $copy(orgdata.value);
selected.value = undefined; selected.value = undefined;
value.value = undefined; value.value = undefined;
if (props.optionid) selected.value = $find(data.value, { id: props.optionid }); if (props.optionid) selected.value = $find(suggestions.value, { id: props.optionid });
if (selected.value) doSelect(selected.value); if (selected.value) doSelect(selected.value);
} }
}, },
@@ -231,30 +224,22 @@ watch(
function choose(v) { function choose(v) {
focused.value = false; focused.value = false;
count1.value = 0;
count2.value = 0;
doSelect(v); doSelect(v);
} }
function setFocus() { function setFocus() {
focused.value = true; focused.value = true;
count1.value = 0;
count2.value = 0;
} }
function lostFocus() { function lostFocus() {
setTimeout(() => { setTimeout(() => {
if (focused.value && count1.value === 0) focused.value = false; if (focused.value) focused.value = false;
}, 200); }, 200);
} }
function pressEnter() { function pressEnter() {
if (data.value.length === 0) return; if (suggestions.value.length === 0) return;
choose(data.value[0]); choose(suggestions.value[0]);
}
function doClick() {
count1.value += 1;
} }
function doSelect(option) { function doSelect(option) {
@@ -272,45 +257,46 @@ function clearValue() {
emit("modalevent", { name: "option", data: null }); emit("modalevent", { name: "option", data: null });
} }
watch(value, (newVal) => {
if (newVal === "") clearValue();
});
function findObject(val) { function findObject(val) {
const rows = $copy(orgdata.value); const rows = $copy(orgdata.value);
if ($empty(val)) data.value = rows; if ($empty(val)) suggestions.value = rows;
else { else {
const text = $nonAccent(val); const text = $nonAccent(val);
data.value = rows.filter((v) => v.search.toLowerCase().indexOf(text.toLowerCase()) >= 0); suggestions.value = rows.filter((v) => v.search.toLowerCase().indexOf(text.toLowerCase()) >= 0);
} }
} }
async function getData() { async function getData() {
isLoading.value = false; isLoading.value = true;
params.value.filter = props.filter; params.value.filter = props.filter;
const data = await $getdata(props.api, undefined, params.value); const data = await $getdata(props.api, undefined, params.value);
isLoading.value = true; isLoading.value = false;
return data; return data;
} }
async function getApi(val) { async function getSuggestions(val) {
if (props.vdata) return findObject(val); if (props.vdata) return findObject(val);
const text = val ? val.toLowerCase() : ""; const text = val ? val.toLowerCase() : "";
const f = {}; const filter_or = {};
// Sử dụng searchfield nếu có, nếu không thì dùng column // Sử dụng searchfield nếu có, nếu không thì dùng column
const fieldsToSearch = props.searchfield || props.column; const fieldsToSearch = props.searchfield || props.column;
fieldsToSearch.map((v) => { fieldsToSearch.forEach((v) => {
f[`${v}__icontains`] = text; filter_or[`${v}__icontains`] = text;
}); });
params.value.filter_or = f; params.value.filter_or = filter_or;
if (props.filter) params.value.filter = $copy(props.filter); if (props.filter) params.value.filter = $copy(props.filter);
data.value = await $getdata(props.api, undefined, params.value); isLoading.value = true;
suggestions.value = await $getdata(props.api, undefined, params.value);
isLoading.value = false;
} }
function beginSearch(e) { const debouncedGetSuggestions = debounce(getSuggestions, 200);
const val = e.target.value;
search.value = val;
clearTimeout(timer.value);
timer.value = setTimeout(() => getApi(val), 150);
}
function addNew() { function addNew() {
showmodal.value = props.addon; showmodal.value = props.addon;
@@ -325,14 +311,14 @@ function dataevent(v) {
} }
// Tìm và cập nhật trong danh sách // Tìm và cập nhật trong danh sách
const idx = $findIndex(data.value, { id: v.id }); const idx = $findIndex(suggestions.value, { id: v.id });
if (idx < 0) { if (idx < 0) {
// Nếu chưa có trong danh sách, thêm vào đầu // Nếu chưa có trong danh sách, thêm vào đầu
data.value.unshift(v); suggestions.value.unshift(v);
console.log("Added new item to data:", v); console.log("Added new item to data:", v);
} else { } else {
// Nếu đã có, cập nhật // Nếu đã có, cập nhật
data.value[idx] = v; suggestions.value[idx] = v;
console.log("Updated existing item in data:", v); console.log("Updated existing item in data:", v);
} }
@@ -358,7 +344,6 @@ function dataevent(v) {
// Đóng modal // Đóng modal
showmodal.value = undefined; showmodal.value = undefined;
console.log("SearchBox data after update:", data.value);
} }
function viewInfo() { function viewInfo() {
@@ -391,9 +376,14 @@ function getPos() {
} }
</script> </script>
<style scoped> <style scoped>
.control.is-loading::after {
inset-inline-end: 2.75em;
}
.field:not(:last-child) { .field:not(:last-child) {
margin-bottom: 0; margin-bottom: 0;
} }
.button.is-success { .button.is-success {
&.is-light { &.is-light {
--bulma-button-background-l: 89%; --bulma-button-background-l: 89%;

View File

@@ -406,7 +406,7 @@
v-bind="{ v-bind="{
data: props.filterData, data: props.filterData,
name: props.field.name, name: props.field.name,
maxheight: '380px', maxHeight: '380px',
perpage: 20, perpage: 20,
}" }"
@selected="doSelect" @selected="doSelect"

View File

@@ -349,9 +349,6 @@ const getStyle = function (field, record) {
return val; return val;
}; };
const getSettingStyle = function (name, field) { const getSettingStyle = function (name, field) {
if (name === "table") {
console.log("tablesetting", tablesetting);
}
if (!tablesetting || tablesetting.length === 0) { if (!tablesetting || tablesetting.length === 0) {
// manual temp fix // manual temp fix
tablesetting = [ tablesetting = [

View File

@@ -1,23 +1,29 @@
<template> <template>
<div :style="`max-height: ${maxheight}; overflow-y: auto;`"> <div
:style="{
maxHeight,
overflowY: 'auto',
}"
>
<div <div
v-for="(v, i) in rows" v-for="(v, i) in rows"
:key="i" :key="i"
class="field is-grouped my-0" class=""
> >
<button <button
class="button is-white fs-14 font-normal w-full is-justify-content-start py-1.5" class="button is-white rounded-none fs-14 font-normal w-full is-justify-content-space-between"
type="button" type="button"
@click="doClick(v, i)" @click="doClick(v, i)"
> >
<span>{{ $stripHtml(v[name] || v.fullname || v.code || "n/a", 75) }}</span> <span>{{ $stripHtml(v[name] || v.fullname || v.code || "n/a", 75) }}</span>
<span <span
v-if="checked[i] && notick !== true" v-if="checked[i] && notick !== true"
class="icon has-text-primary" class="icon right-3"
> >
<Icon <Icon
name="material-symbols:check-rounded" name="material-symbols:check-rounded"
:size="17" :size="17"
class="has-text-primary"
/> />
</span> </span>
</button> </button>
@@ -82,85 +88,98 @@
</div> </div>
</div> </div>
</template> </template>
<script> <script setup>
export default { const props = defineProps({
props: ["data", "name", "maxheight", "perpage", "sort", "selects", "keyval", "show", "notick"], data: Array,
data() { name: String,
return { maxHeight: String,
currentPage: 1, perpage: Number,
total: this.data.length, sort: String,
rows: this.data.slice(0, this.perpage), selects: String,
selected: [], keyval: String,
checked: {}, show: Object,
time: undefined, notick: Boolean,
array: [], });
};
const emit = defineEmits(["selected"]);
const { $copy, $multiSort, $remove } = useNuxtApp();
const currentPage = ref(1);
const total = ref(props.data.length);
const rows = ref(props.data.slice(0, props.perpage));
const selected = ref([]);
const checked = ref({});
const array = ref([]);
getdata();
watch(
() => props.data,
() => getdata(),
{ deep: true },
);
watch(
() => props.selects,
(newVal) => {
console.log("props.selects changed", newVal);
getSelect();
}, },
created() { { deep: true },
this.getdata(); );
},
watch: { function getdata() {
data: function (newVal) { currentPage.value = 1;
this.getdata(); array.value = $copy(props.data);
}, if (props.sort) {
selects: function (newVal) {
this.getSelect();
},
},
methods: {
getdata() {
this.currentPage = 1;
this.array = this.$copy(this.data);
if (this.sort) {
const f = {}; const f = {};
if (this.show?.time) { if (props.show?.time) {
f.create_time = "desc"; f.create_time = "desc";
} }
if (this.sort.startsWith("-")) { if (props.sort.startsWith("-")) {
f[this.sort.slice(1)] = "desc"; f[props.sort.slice(1)] = "desc";
} else { } else {
f[this.sort] = "asc"; f[props.sort] = "asc";
} }
this.$multiSort(this.array, f); $multiSort(array.value, f);
} }
this.rows = this.array.slice(0, this.perpage); rows.value = array.value.slice(0, props.perpage);
this.getSelect(); getSelect();
}, }
getSelect() {
if (!this.selects) return; function getSelect() {
this.selected = []; if (!props.selects) return;
this.checked = {}; selected.value = [];
this.selects.map((v) => { checked.value = {};
let idx = this.rows.findIndex((x) => x[this.keyval ? this.keyval : this.name] === v); props.selects.map((v) => {
const idx = rows.value.findIndex((x) => x[props.keyval ? props.keyval : props.name] === v);
if (idx >= 0) { if (idx >= 0) {
this.selected.push(this.rows[idx]); selected.value.push(rows.value[idx]);
this.checked[idx] = true; checked.value[idx] = true;
} }
}); });
}, }
doClick(v, i, type) {
this.checked[i] = this.checked[i] ? false : true; function doClick(v, i, type) {
this.checked = this.$copy(this.checked); checked.value = { [i]: true };
let idx = this.selected.findIndex((x) => x.id === v.id); const idx = selected.value.findIndex((x) => x.id === v.id);
idx >= 0 ? this.$remove(this.selected) : this.selected.push(v); idx >= 0 ? $remove(selected.value) : selected.value.push(v);
this.$emit("selected", v, type); emit("selected", v, type);
}, }
handleScroll(e) {
function handleScroll(e) {
const bottom = e.target.scrollHeight - e.target.scrollTop - 5 < e.target.clientHeight; const bottom = e.target.scrollHeight - e.target.scrollTop - 5 < e.target.clientHeight;
if (bottom) { if (bottom) {
if (this.total ? this.total > this.rows.length : true) { if (total.value ? total.value > rows.value.length : true) {
this.currentPage += 1; currentPage.value += 1;
let arr = this.array.filter( let arr = array.value.filter(
(ele, index) => index >= (this.currentPage - 1) * this.perpage && index < this.currentPage * this.perpage, (_, index) => index >= (currentPage.value - 1) * props.perpage && index < currentPage.value * props.perpage,
); );
this.rows = this.rows.concat(arr); rows.value = rows.value.concat(arr);
}
} }
} }
},
},
};
</script> </script>
<style scoped> <style scoped>
.button.is-ghost { .button.is-ghost {

View File

@@ -73,9 +73,23 @@ export default defineNuxtPlugin(() => {
return val === undefined || val === null || val === ""; return val === undefined || val === null || val === "";
}; };
const toRawDeep = function (observed) {
const val = toRaw(observed);
if (val instanceof Date) return val;
if (Array.isArray(val)) return val.map(toRawDeep);
if (val === null) return null;
if (typeof val === "object") {
const entries = Object.entries(val).map(([key, val]) => [key, toRawDeep(val)]);
return Object.fromEntries(entries);
}
return val;
};
const copy = function (val) { const copy = function (val) {
if (empty(val)) return val; return structuredClone(toRawDeep(val));
return JSON.parse(JSON.stringify(val));
}; };
const clone = function (obj) { const clone = function (obj) {

View File

@@ -573,6 +573,10 @@ export default defineNuxtPlugin((nuxtApp) => {
params: { params: {
values: values:
"id,code,name,manufacturer,manufacturer__name,os,os__name,battery,battery__code,screen,cpu,cpu__name,gpu,gpu__name,camera_system,camera_system__code,sim,sim__code,network_technology,network_technology__name,charging_technology,charging_technology__code,external_storage,external_storage__max_capacity,ip_rating,ip_rating__code,design,create_time,update_time", "id,code,name,manufacturer,manufacturer__name,os,os__name,battery,battery__code,screen,cpu,cpu__name,gpu,gpu__name,camera_system,camera_system__code,sim,sim__code,network_technology,network_technology__name,charging_technology,charging_technology__code,external_storage,external_storage__max_capacity,ip_rating,ip_rating__code,design,create_time,update_time",
distinct_values: {
label: { type: "Concat", field: ["name", "os__name", "manufacturer__name"] },
},
summary: "annotate",
}, },
}, },
{ {

View File

@@ -6,5 +6,5 @@ python3 envprod.py
PROJECT="erp" PROJECT="erp"
IMAGE="web" IMAGE="web"
docker build -t docker.bigdatatech.vn/$PROJECT/$IMAGE:latest . docker build -t docker.bigdatatech.vn/erp/web:latest .
docker push docker.bigdatatech.vn/$PROJECT/$IMAGE:latest docker push docker.bigdatatech.vn/erp/web:latest