chore: install prettier
This commit is contained in:
@@ -8,20 +8,20 @@ const props = defineProps({
|
||||
|
||||
const store = useStore();
|
||||
const { $getdata } = useNuxtApp();
|
||||
const emit = defineEmits(['modalevent', 'close']);
|
||||
const emit = defineEmits(["modalevent", "close"]);
|
||||
const dealers = ref([]);
|
||||
|
||||
onMounted(async () => {
|
||||
const dealersData = await $getdata('cart', undefined, {
|
||||
values: 'dealer,dealer__name',
|
||||
const dealersData = await $getdata("cart", undefined, {
|
||||
values: "dealer,dealer__name",
|
||||
filter: {
|
||||
dealer__name__icontains: ''
|
||||
dealer__name__icontains: "",
|
||||
},
|
||||
distinct_values: {
|
||||
product_count_by_dealer: { type: 'Count', field: 'prdcart' }
|
||||
product_count_by_dealer: { type: "Count", field: "prdcart" },
|
||||
},
|
||||
sort: 'dealer__name',
|
||||
summary: 'annotate'
|
||||
sort: "dealer__name",
|
||||
summary: "annotate",
|
||||
});
|
||||
|
||||
dealers.value = dealersData;
|
||||
@@ -36,90 +36,110 @@ const localFilterStates = ref({
|
||||
|
||||
function filterTabChange(newTab) {
|
||||
activeFilterTab.value = newTab;
|
||||
store.commit('lastlegendfiltertab', newTab);
|
||||
store.commit("lastlegendfiltertab", newTab);
|
||||
}
|
||||
|
||||
function updateFilters() {
|
||||
emit('close');
|
||||
emit('modalevent', { name: 'updateFilters', data: localFilterStates });
|
||||
emit("close");
|
||||
emit("modalevent", { name: "updateFilters", data: localFilterStates });
|
||||
}
|
||||
|
||||
function resetCartDealerFilters() {
|
||||
emit('close');
|
||||
emit('modalevent', { name: 'resetCartDealerFilters' });
|
||||
emit("close");
|
||||
emit("modalevent", { name: "resetCartDealerFilters" });
|
||||
}
|
||||
|
||||
watch(() => props.filters, () => {
|
||||
localFilterStates.value = {
|
||||
cartIds: props.filters.cartIds,
|
||||
dealerIds: props.filters.dealerIds,
|
||||
};
|
||||
}, { deep: true })
|
||||
watch(
|
||||
() => props.filters,
|
||||
() => {
|
||||
localFilterStates.value = {
|
||||
cartIds: props.filters.cartIds,
|
||||
dealerIds: props.filters.dealerIds,
|
||||
};
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="fs-14">
|
||||
<div class="tabs is-boxed is-fullwidth">
|
||||
<ul class="mx-0">
|
||||
<li
|
||||
<li
|
||||
v-for="tab in filterTabs"
|
||||
:class="{ 'is-active' : tab === activeFilterTab }"
|
||||
@click="() => {
|
||||
if (tab !== activeFilterTab) filterTabChange(tab);
|
||||
}"
|
||||
:class="{ 'is-active': tab === activeFilterTab }"
|
||||
@click="
|
||||
() => {
|
||||
if (tab !== activeFilterTab) filterTabChange(tab);
|
||||
}
|
||||
"
|
||||
>
|
||||
<a :class="[{
|
||||
'has-text-weight-bold has-text-secondary': tab === activeFilterTab,
|
||||
}]">Lọc theo {{ tab }}</a>
|
||||
<a
|
||||
:class="[
|
||||
{
|
||||
'has-text-weight-bold has-text-secondary': tab === activeFilterTab,
|
||||
},
|
||||
]"
|
||||
>Lọc theo {{ tab }}</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="activeFilterTab === filterTabs.CARTS">
|
||||
<li
|
||||
class="is-light p-2 is-flex is-align-items-center is-justify-content-space-between is-gap-2 is-clickable"
|
||||
@click="() => {
|
||||
// toggle all
|
||||
if (localFilterStates.cartIds.length === carts.length) {
|
||||
localFilterStates.cartIds = [];
|
||||
} else {
|
||||
localFilterStates.cartIds = carts.map(c => c.id);
|
||||
@click="
|
||||
() => {
|
||||
// toggle all
|
||||
if (localFilterStates.cartIds.length === carts.length) {
|
||||
localFilterStates.cartIds = [];
|
||||
} else {
|
||||
localFilterStates.cartIds = carts.map((c) => c.id);
|
||||
}
|
||||
}
|
||||
}"
|
||||
"
|
||||
>
|
||||
<span class="has-text-weight-bold">Tất cả</span>
|
||||
<div class="is-flex is-gap-2 is-flex-shrink-0">
|
||||
<span class="has-text-weight-bold">{{ productCount }}</span>
|
||||
<SvgIcon v-bind="{
|
||||
name: localFilterStates.cartIds.length === carts.length
|
||||
? 'checked.svg'
|
||||
: localFilterStates.cartIds.length === 0
|
||||
? 'uncheck.svg'
|
||||
: 'indeterminate.svg',
|
||||
type: localFilterStates.cartIds.length > 0 ? 'primary' : 'twitter',
|
||||
size: 22
|
||||
}" />
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name:
|
||||
localFilterStates.cartIds.length === carts.length
|
||||
? 'checked.svg'
|
||||
: localFilterStates.cartIds.length === 0
|
||||
? 'uncheck.svg'
|
||||
: 'indeterminate.svg',
|
||||
type: localFilterStates.cartIds.length > 0 ? 'primary' : 'twitter',
|
||||
size: 22,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<ul style="max-height: 420px; overflow-y: scroll;">
|
||||
<ul style="max-height: 420px; overflow-y: scroll">
|
||||
<li
|
||||
v-for="cart in carts"
|
||||
class="border-bottom p-2 is-flex is-align-items-center is-justify-content-space-between is-gap-2 is-clickable"
|
||||
@click="() => {
|
||||
const selected = localFilterStates.cartIds.includes(cart.id);
|
||||
if (selected) {
|
||||
localFilterStates.cartIds = localFilterStates.cartIds.filter(id => id !== cart.id);
|
||||
} else {
|
||||
localFilterStates.cartIds = [...localFilterStates.cartIds, cart.id];
|
||||
@click="
|
||||
() => {
|
||||
const selected = localFilterStates.cartIds.includes(cart.id);
|
||||
if (selected) {
|
||||
localFilterStates.cartIds = localFilterStates.cartIds.filter((id) => id !== cart.id);
|
||||
} else {
|
||||
localFilterStates.cartIds = [...localFilterStates.cartIds, cart.id];
|
||||
}
|
||||
}
|
||||
}"
|
||||
"
|
||||
>
|
||||
<span>{{ cart.name }}</span>
|
||||
<div class="is-flex is-gap-2 is-flex-shrink-0">
|
||||
<span>{{ cart.product_count }}</span>
|
||||
<SvgIcon v-bind="{
|
||||
name: localFilterStates.cartIds.includes(cart.id) ? 'checked.svg' : 'uncheck.svg',
|
||||
type: localFilterStates.cartIds.includes(cart.id) ? 'primary' : 'twitter',
|
||||
size: 22
|
||||
}" />
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: localFilterStates.cartIds.includes(cart.id) ? 'checked.svg' : 'uncheck.svg',
|
||||
type: localFilterStates.cartIds.includes(cart.id) ? 'primary' : 'twitter',
|
||||
size: 22,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -127,50 +147,59 @@ watch(() => props.filters, () => {
|
||||
<div v-else>
|
||||
<li
|
||||
class="is-light p-2 is-flex is-align-items-center is-justify-content-space-between is-gap-2 is-clickable"
|
||||
@click="() => {
|
||||
// toggle all
|
||||
if (localFilterStates.dealerIds.length === dealers.length) {
|
||||
localFilterStates.dealerIds = [];
|
||||
} else {
|
||||
localFilterStates.dealerIds = dealers.map(c => c.id);
|
||||
@click="
|
||||
() => {
|
||||
// toggle all
|
||||
if (localFilterStates.dealerIds.length === dealers.length) {
|
||||
localFilterStates.dealerIds = [];
|
||||
} else {
|
||||
localFilterStates.dealerIds = dealers.map((c) => c.id);
|
||||
}
|
||||
}
|
||||
}"
|
||||
"
|
||||
>
|
||||
<span class="has-text-weight-bold">Tất cả</span>
|
||||
<div class="is-flex is-gap-2 is-flex-shrink-0">
|
||||
<span class="has-text-weight-bold">{{ productCount }}</span>
|
||||
<SvgIcon v-bind="{
|
||||
name: localFilterStates.dealerIds.length === dealers.length
|
||||
? 'checked.svg'
|
||||
: localFilterStates.dealerIds.length === 0
|
||||
? 'uncheck.svg'
|
||||
: 'indeterminate.svg',
|
||||
type: localFilterStates.dealerIds.length > 0 ? 'primary' : 'twitter',
|
||||
size: 22
|
||||
}" />
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name:
|
||||
localFilterStates.dealerIds.length === dealers.length
|
||||
? 'checked.svg'
|
||||
: localFilterStates.dealerIds.length === 0
|
||||
? 'uncheck.svg'
|
||||
: 'indeterminate.svg',
|
||||
type: localFilterStates.dealerIds.length > 0 ? 'primary' : 'twitter',
|
||||
size: 22,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
<ul>
|
||||
<li
|
||||
v-for="dealer in dealers"
|
||||
class="border-bottom p-2 is-flex is-align-items-center is-justify-content-space-between is-gap-2 is-clickable"
|
||||
@click="() => {
|
||||
const selected = localFilterStates.dealerIds.includes(dealer.dealer);
|
||||
if (selected) {
|
||||
localFilterStates.dealerIds = localFilterStates.dealerIds.filter(id => id !== dealer.dealer);
|
||||
} else {
|
||||
localFilterStates.dealerIds = [...localFilterStates.dealerIds, dealer.dealer];
|
||||
@click="
|
||||
() => {
|
||||
const selected = localFilterStates.dealerIds.includes(dealer.dealer);
|
||||
if (selected) {
|
||||
localFilterStates.dealerIds = localFilterStates.dealerIds.filter((id) => id !== dealer.dealer);
|
||||
} else {
|
||||
localFilterStates.dealerIds = [...localFilterStates.dealerIds, dealer.dealer];
|
||||
}
|
||||
}
|
||||
}"
|
||||
"
|
||||
>
|
||||
<span>{{ dealer.dealer__name }}</span>
|
||||
<div class="is-flex is-gap-2 is-flex-shrink-0">
|
||||
<span>{{ dealer.product_count_by_dealer }}</span>
|
||||
<SvgIcon v-bind="{
|
||||
name: localFilterStates.dealerIds.includes(dealer.dealer) ? 'checked.svg' : 'uncheck.svg',
|
||||
type: localFilterStates.dealerIds.includes(dealer.dealer) ? 'primary' : 'twitter',
|
||||
size: 22
|
||||
}" />
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: localFilterStates.dealerIds.includes(dealer.dealer) ? 'checked.svg' : 'uncheck.svg',
|
||||
type: localFilterStates.dealerIds.includes(dealer.dealer) ? 'primary' : 'twitter',
|
||||
size: 22,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -195,4 +224,4 @@ watch(() => props.filters, () => {
|
||||
li:hover {
|
||||
background-color: var(--bulma-primary-95);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
<script setup>
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import Modal from "@/components/Modal.vue";
|
||||
const showLayerSettingListModal = ref(null);
|
||||
const showLayerSettingSaveModal = ref(null);
|
||||
|
||||
function openLayerSettingList() {
|
||||
showLayerSettingListModal.value = {
|
||||
component: 'viewer/LayerSettingList',
|
||||
title: 'Chọn thiết lập layer',
|
||||
width: '400px',
|
||||
height: 'auto',
|
||||
component: "viewer/LayerSettingList",
|
||||
title: "Chọn thiết lập layer",
|
||||
width: "400px",
|
||||
height: "auto",
|
||||
};
|
||||
}
|
||||
|
||||
function openLayerSettingSave() {
|
||||
showLayerSettingSaveModal.value = {
|
||||
component: 'viewer/LayerSettingSave',
|
||||
title: 'Lưu thiết lập layer',
|
||||
width: '400px',
|
||||
height: 'auto',
|
||||
component: "viewer/LayerSettingSave",
|
||||
title: "Lưu thiết lập layer",
|
||||
width: "400px",
|
||||
height: "auto",
|
||||
};
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="is-flex is-gap-1">
|
||||
<button
|
||||
class="button is-small p-2 is-white"
|
||||
<button
|
||||
class="button is-small p-2 is-white"
|
||||
title="Mở thiết lập"
|
||||
@click="openLayerSettingList"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'folder.svg', type: 'primary', size: 20 }" />
|
||||
</button>
|
||||
<button
|
||||
class="button is-small p-2 is-white"
|
||||
<button
|
||||
class="button is-small p-2 is-white"
|
||||
title="Lưu thiết lập"
|
||||
@click="openLayerSettingSave"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'save.svg', type: 'primary', size: 20 }" />
|
||||
</button>
|
||||
</div>
|
||||
<Modal
|
||||
<Modal
|
||||
v-bind="showLayerSettingListModal"
|
||||
v-if="showLayerSettingListModal"
|
||||
@close="showLayerSettingListModal = null"
|
||||
/>
|
||||
<Modal
|
||||
<Modal
|
||||
v-bind="showLayerSettingSaveModal"
|
||||
v-if="showLayerSettingSaveModal"
|
||||
@close="showLayerSettingSaveModal = null"
|
||||
@@ -61,5 +61,5 @@ function openLayerSettingSave() {
|
||||
display: flex;
|
||||
padding: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script setup>
|
||||
import SvgIcon from '@/components/SvgIcon.vue';
|
||||
import { applyLayerSetting } from '@/components/viewer/utils/aps-viewer';
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import { applyLayerSetting } from "@/components/viewer/utils/aps-viewer";
|
||||
|
||||
const store = useStore();
|
||||
const { $getdata, $deleteapi } = useNuxtApp();
|
||||
const layerSettings = ref([]);
|
||||
|
||||
async function fetchLayerSettings() {
|
||||
layerSettings.value = await $getdata("layersetting", { user: store.login.id });
|
||||
layerSettings.value = await $getdata("layersetting", {
|
||||
user: store.login.id,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteLayerSetting(id) {
|
||||
@@ -15,7 +17,7 @@ async function deleteLayerSetting(id) {
|
||||
const result = await $deleteapi("layersetting", id);
|
||||
if (result && !result.error) {
|
||||
if (store.layersetting?.id === id) {
|
||||
store.commit('layersetting', undefined);
|
||||
store.commit("layersetting", undefined);
|
||||
}
|
||||
fetchLayerSettings();
|
||||
} else {
|
||||
@@ -24,31 +26,32 @@ async function deleteLayerSetting(id) {
|
||||
} catch (error) {
|
||||
console.error("Lỗi khi xóa thiết lập layer:", error);
|
||||
alert("Đã xảy ra lỗi khi xóa thiết lập layer. Vui lòng thử lại.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(fetchLayerSettings);
|
||||
</script>
|
||||
<template>
|
||||
<ul v-if="layerSettings.length > 0">
|
||||
<li
|
||||
<li
|
||||
v-for="layersetting in layerSettings"
|
||||
:key="layersetting.id"
|
||||
:class="[
|
||||
'border-bottom px-2 py-0 is-flex is-justify-content-space-between is-align-items-center is-clickable hoverable',
|
||||
store.layersetting?.id === layersetting.id && 'has-text-weight-bold has-background-white-ter'
|
||||
store.layersetting?.id === layersetting.id && 'has-text-weight-bold has-background-white-ter',
|
||||
]"
|
||||
@click="applyLayerSetting(layersetting, store)"
|
||||
>
|
||||
<span>{{ layersetting.name }}</span>
|
||||
<button class="delBtn" @click.stop="deleteLayerSetting(layersetting.id)">
|
||||
<button
|
||||
class="delBtn"
|
||||
@click.stop="deleteLayerSetting(layersetting.id)"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'bin1.svg', type: 'primary', size: 18 }" />
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else>
|
||||
Bạn chưa tạo thiết lập layer nào.
|
||||
</p>
|
||||
<p v-else>Bạn chưa tạo thiết lập layer nào.</p>
|
||||
</template>
|
||||
<style scoped>
|
||||
.hoverable:hover {
|
||||
@@ -63,4 +66,4 @@ onMounted(fetchLayerSettings);
|
||||
background-color: hsla(0, 0%, 0%, 0.05);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
<script setup>
|
||||
import { getLayerNames } from '@/components/viewer/utils/aps-viewer';
|
||||
import { getLayerNames } from "@/components/viewer/utils/aps-viewer";
|
||||
|
||||
const store = useStore();
|
||||
const { layersetting } = store;
|
||||
const emit = defineEmits('close');
|
||||
const emit = defineEmits("close");
|
||||
const { $insertapi, $patchapi, $snackbar } = useNuxtApp();
|
||||
const radioSave = ref(layersetting ? "overwrite" : "new");
|
||||
const name = ref(radioSave === 'new' ? '' : layersetting ? layersetting.name : '');
|
||||
const name = ref(radioSave === "new" ? "" : layersetting ? layersetting.name : "");
|
||||
const errors = ref([]);
|
||||
|
||||
watch(radioSave, (val) => {
|
||||
name.value = val === 'new' ? '' : layersetting.name;
|
||||
name.value = val === "new" ? "" : layersetting.name;
|
||||
errors.value = [];
|
||||
});
|
||||
|
||||
function switchRadioSave(newVal) {
|
||||
if (newVal === 'overwrite' && !layersetting)
|
||||
return;
|
||||
|
||||
if (newVal === "overwrite" && !layersetting) return;
|
||||
|
||||
radioSave.value = newVal;
|
||||
}
|
||||
|
||||
@@ -25,29 +24,36 @@ async function saveLayerSetting() {
|
||||
const layerNames = getLayerNames();
|
||||
const user = store.login.id;
|
||||
if (!name.value) {
|
||||
errors.value.push({ name: "name", msg: "Tên thiết lập không được bỏ trống" })
|
||||
errors.value.push({
|
||||
name: "name",
|
||||
msg: "Tên thiết lập không được bỏ trống",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
user,
|
||||
name: name.value,
|
||||
detail: layerNames
|
||||
}
|
||||
detail: layerNames,
|
||||
};
|
||||
|
||||
if (radioSave.value === 'overwrite') {
|
||||
if (radioSave.value === "overwrite") {
|
||||
payload.id = layersetting.id;
|
||||
}
|
||||
|
||||
const res = radioSave.value === 'new' ? await $insertapi('layersetting', payload) : await $patchapi('layersetting', payload);
|
||||
if (res === 'error') {
|
||||
$snackbar(isVietnamese.value ? `Có lỗi xảy ra khi ${radioSave.value === 'new' ? 'tạo' : 'cập nhật' } thiết lập` : `Error ${radioSave.value === 'new' ? 'creating' : 'updating' } layer setting`,
|
||||
const res =
|
||||
radioSave.value === "new" ? await $insertapi("layersetting", payload) : await $patchapi("layersetting", payload);
|
||||
if (res === "error") {
|
||||
$snackbar(
|
||||
isVietnamese.value
|
||||
? `Có lỗi xảy ra khi ${radioSave.value === "new" ? "tạo" : "cập nhật"} thiết lập`
|
||||
: `Error ${radioSave.value === "new" ? "creating" : "updating"} layer setting`,
|
||||
"Lỗi",
|
||||
"Error"
|
||||
"Error",
|
||||
);
|
||||
} else {
|
||||
store.commit('layersetting', res);
|
||||
emit('close');
|
||||
store.commit("layersetting", res);
|
||||
emit("close");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -55,23 +61,34 @@ async function saveLayerSetting() {
|
||||
<div class="field">
|
||||
<label class="label fs-14">Chọn chế độ lưu</label>
|
||||
<div class="control">
|
||||
<button class="button is-white fs-14 px-2 py-1" @click="switchRadioSave('overwrite')" :disabled="layersetting === undefined">
|
||||
<button
|
||||
class="button is-white fs-14 px-2 py-1"
|
||||
@click="switchRadioSave('overwrite')"
|
||||
:disabled="layersetting === undefined"
|
||||
>
|
||||
<span class="icon-text">
|
||||
<SvgIcon v-bind="{
|
||||
name: `radio-${radioSave === 'new' ? 'unchecked' : 'checked'}.svg`,
|
||||
type: 'gray',
|
||||
size: 22,
|
||||
}" />
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: `radio-${radioSave === 'new' ? 'unchecked' : 'checked'}.svg`,
|
||||
type: 'gray',
|
||||
size: 22,
|
||||
}"
|
||||
/>
|
||||
Ghi đè
|
||||
</span>
|
||||
</button>
|
||||
<button class="button is-white fs-14 px-2 py-1" @click="switchRadioSave('new')">
|
||||
<button
|
||||
class="button is-white fs-14 px-2 py-1"
|
||||
@click="switchRadioSave('new')"
|
||||
>
|
||||
<span class="icon-text">
|
||||
<SvgIcon v-bind="{
|
||||
name: `radio-${radioSave === 'new' ? 'checked' : 'unchecked'}.svg`,
|
||||
type: 'gray',
|
||||
size: 22
|
||||
}" />
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: `radio-${radioSave === 'new' ? 'checked' : 'unchecked'}.svg`,
|
||||
type: 'gray',
|
||||
size: 22,
|
||||
}"
|
||||
/>
|
||||
Tạo mới
|
||||
</span>
|
||||
</button>
|
||||
@@ -82,22 +99,29 @@ async function saveLayerSetting() {
|
||||
<div class="control">
|
||||
<input
|
||||
class="input"
|
||||
style="box-sizing: border-box;"
|
||||
style="box-sizing: border-box"
|
||||
type="text"
|
||||
placeholder="e.g. Đường, Mặt hồ"
|
||||
v-model="name"
|
||||
@keydown.enter="saveLayerSetting"
|
||||
/>
|
||||
</div>
|
||||
<div class="help has-text-danger" v-if="errors.find((v) => v.name === 'name')">
|
||||
<div
|
||||
class="help has-text-danger"
|
||||
v-if="errors.find((v) => v.name === 'name')"
|
||||
>
|
||||
{{ errors.find((v) => v.name === "name").msg }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<p class="control">
|
||||
<button class="button is-primary" @click="saveLayerSetting()">Lưu lại</button>
|
||||
<button
|
||||
class="button is-primary"
|
||||
@click="saveLayerSetting()"
|
||||
>
|
||||
Lưu lại
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<style>
|
||||
</style>
|
||||
<style></style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { countBy, difference, isEqual, pick } from 'es-toolkit';
|
||||
import Search from '@/components/viewer/Search.vue';
|
||||
import { countBy, difference, isEqual, pick } from "es-toolkit";
|
||||
import Search from "@/components/viewer/Search.vue";
|
||||
|
||||
const props = defineProps({
|
||||
products: Object,
|
||||
@@ -15,46 +15,36 @@ const props = defineProps({
|
||||
statusMode: Object,
|
||||
});
|
||||
|
||||
const {
|
||||
products,
|
||||
carts,
|
||||
dealers,
|
||||
defaultCartIds,
|
||||
defaultDealerIds,
|
||||
filters,
|
||||
statuses,
|
||||
statusModes,
|
||||
statusMode,
|
||||
} = toRefs(props)
|
||||
const { products, carts, dealers, defaultCartIds, defaultDealerIds, filters, statuses, statusModes, statusMode } =
|
||||
toRefs(props);
|
||||
|
||||
const emit = defineEmits(['switchStatusMode', 'updateFilters', 'resetCartDealerFilters', 'resetFilters']);
|
||||
const emit = defineEmits(["switchStatusMode", "updateFilters", "resetCartDealerFilters", "resetFilters"]);
|
||||
const store = useStore();
|
||||
|
||||
const showFilterModal = ref(null);
|
||||
const { dealer } = store;
|
||||
const filterTabs = dealer ? { CARTS: 'Giỏ hàng' } : { CARTS: 'Giỏ hàng', DEALERS: 'Đại lý' };
|
||||
const filterTabs = dealer ? { CARTS: "Giỏ hàng" } : { CARTS: "Giỏ hàng", DEALERS: "Đại lý" };
|
||||
|
||||
function openFiltersModal() {
|
||||
showFilterModal.value = {
|
||||
component: 'viewer/Filters',
|
||||
title: 'Tuỳ chọn',
|
||||
width: '40%',
|
||||
height: '20%',
|
||||
vbind: {
|
||||
component: "viewer/Filters",
|
||||
title: "Tuỳ chọn",
|
||||
width: "40%",
|
||||
height: "20%",
|
||||
vbind: {
|
||||
carts,
|
||||
dealers,
|
||||
filters,
|
||||
filterTabs,
|
||||
productCount: products.value.length
|
||||
productCount: products.value.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function updateFilters(updatedFilters) {
|
||||
if (isEqual(pick(filters.value, ['cartIds', 'dealerIds']), updatedFilters.value))
|
||||
return;
|
||||
|
||||
let payload;
|
||||
if (isEqual(pick(filters.value, ["cartIds", "dealerIds"]), updatedFilters.value)) return;
|
||||
|
||||
let payload;
|
||||
const { lastlegendfiltertab } = store;
|
||||
|
||||
if (lastlegendfiltertab === filterTabs.CARTS) {
|
||||
@@ -68,30 +58,32 @@ function updateFilters(updatedFilters) {
|
||||
dealerIds: toRaw(updatedFilters.value.dealerIds),
|
||||
};
|
||||
}
|
||||
emit('updateFilters', payload);
|
||||
emit("updateFilters", payload);
|
||||
}
|
||||
|
||||
const cartOrDealerFilterInfo = computed(() => {
|
||||
// dealer or cart filter is modified
|
||||
if (filters.value.cartIds.length < carts.value.length) {
|
||||
const filteredCarts = carts.value.filter(c => filters.value.cartIds.includes(c.id)).map(c => c.name.replace('Giỏ hàng ', ''));
|
||||
if (filters.value.cartIds.length < carts.value.length) {
|
||||
const filteredCarts = carts.value
|
||||
.filter((c) => filters.value.cartIds.includes(c.id))
|
||||
.map((c) => c.name.replace("Giỏ hàng ", ""));
|
||||
return {
|
||||
name: filterTabs.CARTS,
|
||||
values: filteredCarts.join(', '),
|
||||
}
|
||||
values: filteredCarts.join(", "),
|
||||
};
|
||||
}
|
||||
if (filters.value.dealerIds.length < dealers.value.length) {
|
||||
const filteredDealers = dealers.value.filter(d => filters.value.dealerIds.includes(d.id)).map(c => c.code);
|
||||
if (filters.value.dealerIds.length < dealers.value.length) {
|
||||
const filteredDealers = dealers.value.filter((d) => filters.value.dealerIds.includes(d.id)).map((c) => c.code);
|
||||
return {
|
||||
name: filterTabs.DEALERS,
|
||||
values: filteredDealers.join(', '),
|
||||
}
|
||||
values: filteredDealers.join(", "),
|
||||
};
|
||||
}
|
||||
return false;
|
||||
})
|
||||
});
|
||||
|
||||
const productCountByStatus = computed(() => {
|
||||
const productsFilteredBeforeStatus = products.value.filter(product => {
|
||||
const productCountByStatus = computed(() => {
|
||||
const productsFilteredBeforeStatus = products.value.filter((product) => {
|
||||
/*
|
||||
cart__dealer: null means cart doesnt belong to any dealer
|
||||
- when no filter: count null
|
||||
@@ -103,15 +95,17 @@ const productCountByStatus = computed(() => {
|
||||
const diffCartFilter = difference(defaultCartIds.value, cartIds);
|
||||
const diffDealerFilter = difference(defaultDealerIds.value, dealerIds);
|
||||
const noCartFilter = diffCartFilter.length === 0;
|
||||
const noDealerFilter = diffDealerFilter.length === 0;
|
||||
const noDealerFilter = diffDealerFilter.length === 0;
|
||||
const cartCondition = noCartFilter ? cartIds.includes(cart) || cart === null : cartIds.includes(cart);
|
||||
const dealerCondition = noDealerFilter ? dealerIds.includes(cart__dealer) || cart__dealer === null : dealerIds.includes(cart__dealer);
|
||||
|
||||
const dealerCondition = noDealerFilter
|
||||
? dealerIds.includes(cart__dealer) || cart__dealer === null
|
||||
: dealerIds.includes(cart__dealer);
|
||||
|
||||
return cartCondition && dealerCondition;
|
||||
});
|
||||
|
||||
const result = countBy(productsFilteredBeforeStatus, (product) =>
|
||||
statusMode.value === statusModes.value.SIMPLE ? product.status__sale_status : product.status
|
||||
const result = countBy(productsFilteredBeforeStatus, (product) =>
|
||||
statusMode.value === statusModes.value.SIMPLE ? product.status__sale_status : product.status,
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -119,63 +113,73 @@ const productCountByStatus = computed(() => {
|
||||
|
||||
const visibilityAll = computed(() => {
|
||||
return filters.value.statusIds.length > 0;
|
||||
})
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<Teleport to="#legend.docking-panel.docking-panel-container-solid-color-a">
|
||||
<div id="customBtns">
|
||||
<div class="is-sr-only">
|
||||
<!-- hack to speed up first eye-off icon appearance -->
|
||||
<SvgIcon v-bind="{
|
||||
name: 'eye-autodesk-off.svg',
|
||||
type: 'black',
|
||||
size: 15
|
||||
}" />
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: 'eye-autodesk-off.svg',
|
||||
type: 'black',
|
||||
size: 15,
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
<button
|
||||
title="Show/hide all legends"
|
||||
@click="() => emit('updateFilters', {
|
||||
statusIds: visibilityAll ? [] : statuses.map(s => s.id)
|
||||
})"
|
||||
@click="
|
||||
() =>
|
||||
emit('updateFilters', {
|
||||
statusIds: visibilityAll ? [] : statuses.map((s) => s.id),
|
||||
})
|
||||
"
|
||||
>
|
||||
<SvgIcon v-bind="{
|
||||
name: `eye-autodesk-${visibilityAll ? 'on' : 'off'}.svg`,
|
||||
type: 'black',
|
||||
size: 17
|
||||
}" />
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: `eye-autodesk-${visibilityAll ? 'on' : 'off'}.svg`,
|
||||
type: 'black',
|
||||
size: 17,
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Teleport>
|
||||
<Search :products="products" />
|
||||
<div
|
||||
<div
|
||||
class="tabs mb-0 has-background-white is-fullwidth is-flex-shrink-0"
|
||||
style="position: sticky; top: 32.5px; z-index: -1"
|
||||
>
|
||||
>
|
||||
<ul class="mx-0">
|
||||
<li v-for="mode in Object.values(statusModes)"
|
||||
:class="{ 'is-active' : statusMode === mode }"
|
||||
@click="() => {
|
||||
if (statusMode !== mode) emit('switchStatusMode', mode);
|
||||
}"
|
||||
<li
|
||||
v-for="mode in Object.values(statusModes)"
|
||||
:class="{ 'is-active': statusMode === mode }"
|
||||
@click="
|
||||
() => {
|
||||
if (statusMode !== mode) emit('switchStatusMode', mode);
|
||||
}
|
||||
"
|
||||
>
|
||||
<a :class="[
|
||||
'is-size-7',
|
||||
statusMode === mode && 'has-text-weight-bold has-text-secondary'
|
||||
]">{{ mode }}</a>
|
||||
<a :class="['is-size-7', statusMode === mode && 'has-text-weight-bold has-text-secondary']">{{ mode }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="fs-13 has-background-white" style="z-index: -2">
|
||||
<div
|
||||
v-for="{ id, name, color } in statuses.filter(x => {
|
||||
<div
|
||||
class="fs-13 has-background-white"
|
||||
style="z-index: -2"
|
||||
>
|
||||
<div
|
||||
v-for="{ id, name, color } in statuses.filter((x) => {
|
||||
// hide Chưa bán in dealer
|
||||
return dealer ? x.code !== 'not-sold' : true
|
||||
return dealer ? x.code !== 'not-sold' : true;
|
||||
})"
|
||||
class="status is-flex is-gap-1 is-justify-content-space-between is-clickable"
|
||||
@click="() => emit('updateFilters', { statusIds: [id] })"
|
||||
>
|
||||
<div class="statusInfo">
|
||||
<div
|
||||
<div
|
||||
class="swatch"
|
||||
:style="{ backgroundColor: color }"
|
||||
></div>
|
||||
@@ -184,37 +188,43 @@ const visibilityAll = computed(() => {
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="mr-2 has-text-right is-inline-block" style="line-height: 1">
|
||||
{{ productCountByStatus[id] || 0 }}
|
||||
<span
|
||||
class="mr-2 has-text-right is-inline-block"
|
||||
style="line-height: 1"
|
||||
>
|
||||
{{ productCountByStatus[id] || 0 }}
|
||||
</span>
|
||||
<button class="eyeBtn" @click.stop="() => {
|
||||
const selected = filters.statusIds.includes(id);
|
||||
const payload = {
|
||||
statusIds: selected ? filters.statusIds.filter(statusId => statusId !== id) : [...filters.statusIds, id]
|
||||
}
|
||||
emit('updateFilters', payload);
|
||||
}">
|
||||
<SvgIcon v-bind="{
|
||||
name: `eye-autodesk-${filters.statusIds.includes(id) ? 'on' : 'off'}.svg`,
|
||||
type: 'black',
|
||||
size: 17
|
||||
}" />
|
||||
<button
|
||||
class="eyeBtn"
|
||||
@click.stop="
|
||||
() => {
|
||||
const selected = filters.statusIds.includes(id);
|
||||
const payload = {
|
||||
statusIds: selected
|
||||
? filters.statusIds.filter((statusId) => statusId !== id)
|
||||
: [...filters.statusIds, id],
|
||||
};
|
||||
emit('updateFilters', payload);
|
||||
}
|
||||
"
|
||||
>
|
||||
<SvgIcon
|
||||
v-bind="{
|
||||
name: `eye-autodesk-${filters.statusIds.includes(id) ? 'on' : 'off'}.svg`,
|
||||
type: 'black',
|
||||
size: 17,
|
||||
}"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
<button
|
||||
:class="[
|
||||
'button is-fullwidth is-radiusless is-gap-0.5 is-small is-flex-direction-column is-align-items-stretch fs-13',
|
||||
cartOrDealerFilterInfo && 'is-light'
|
||||
]"
|
||||
style="
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
box-sizing: border-box;
|
||||
border-inline-width: 0;
|
||||
border-bottom-width: 0;
|
||||
"
|
||||
'button is-fullwidth is-radiusless is-gap-0.5 is-small is-flex-direction-column is-align-items-stretch fs-13',
|
||||
cartOrDealerFilterInfo && 'is-light',
|
||||
]"
|
||||
style="position: sticky; bottom: 0; box-sizing: border-box; border-inline-width: 0; border-bottom-width: 0"
|
||||
@click="openFiltersModal"
|
||||
>
|
||||
<div class="is-flex is-justify-content-space-between">
|
||||
@@ -227,21 +237,24 @@ const visibilityAll = computed(() => {
|
||||
<span style="margin-left: 9px">SP</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="cartOrDealerFilterInfo"
|
||||
<div
|
||||
v-if="cartOrDealerFilterInfo"
|
||||
class="is-fullwidth is-flex is-flex-direction-column is-align-items-flex-start has-text-left"
|
||||
>
|
||||
<p class="is-fullwidth" style="text-wrap: wrap;">
|
||||
{{ cartOrDealerFilterInfo.name }}:
|
||||
<p
|
||||
class="is-fullwidth"
|
||||
style="text-wrap: wrap"
|
||||
>
|
||||
{{ cartOrDealerFilterInfo.name }}:
|
||||
{{ cartOrDealerFilterInfo.values }}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
<Teleport to="#legend.docking-panel.docking-panel-container-solid-color-a">
|
||||
<Modal
|
||||
v-if="showFilterModal"
|
||||
<Modal
|
||||
v-if="showFilterModal"
|
||||
v-bind="showFilterModal"
|
||||
@close="showFilterModal = null"
|
||||
@close="showFilterModal = null"
|
||||
@updateFilters="updateFilters"
|
||||
@resetCartDealerFilters="emit('resetCartDealerFilters')"
|
||||
/>
|
||||
@@ -254,13 +267,13 @@ const visibilityAll = computed(() => {
|
||||
min-width: 150px;
|
||||
width: 275px;
|
||||
height: fit-content;
|
||||
|
||||
|
||||
--hover-bg: rgba(0, 0, 0, 0.1);
|
||||
|
||||
&.top-initial-important {
|
||||
top: initial !important; /* fixed top until meshes & colors are added */
|
||||
}
|
||||
|
||||
|
||||
.docking-panel-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
@@ -284,7 +297,7 @@ const visibilityAll = computed(() => {
|
||||
.docking-panel-footer {
|
||||
border-radius: 0 0 5px 5px;
|
||||
}
|
||||
|
||||
|
||||
.content.docking-panel-scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -298,9 +311,9 @@ const visibilityAll = computed(() => {
|
||||
&:nth-child(even) {
|
||||
background-color: #f2f2f280;
|
||||
}
|
||||
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0,191,255,.2);
|
||||
background-color: rgba(0, 191, 255, 0.2);
|
||||
}
|
||||
|
||||
.statusInfo {
|
||||
@@ -323,9 +336,9 @@ const visibilityAll = computed(() => {
|
||||
width: 40px;
|
||||
height: 50px;
|
||||
z-index: 1;
|
||||
|
||||
|
||||
display: flex;
|
||||
|
||||
|
||||
& > * {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
@@ -352,4 +365,4 @@ const visibilityAll = computed(() => {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,82 +1,102 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
const store = useStore()
|
||||
const { $connectWebSocket, $subscribe } = useNuxtApp()
|
||||
const { dealer } = store
|
||||
const notes = ref([])
|
||||
const isLoading = ref(true)
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
const store = useStore();
|
||||
const { $connectWebSocket, $subscribe } = useNuxtApp();
|
||||
const { dealer } = store;
|
||||
const notes = ref([]);
|
||||
const isLoading = ref(true);
|
||||
|
||||
function navigateTo(productId) {
|
||||
if (!productId) return
|
||||
window.dispatchEvent(new CustomEvent('navigateToProduct', { detail: { productId } }))
|
||||
if (!productId) return;
|
||||
window.dispatchEvent(new CustomEvent("navigateToProduct", { detail: { productId } }));
|
||||
}
|
||||
|
||||
function handleWsMessageForNotes({ detail: response }) {
|
||||
if (response.type !== 'realtime_update') return
|
||||
if (response.type !== "realtime_update") return;
|
||||
const { name, change_type, record } = response.payload;
|
||||
if (name.toLowerCase() !== 'product_note') return;
|
||||
|
||||
if (name.toLowerCase() !== "product_note") return;
|
||||
|
||||
if (dealer?.id && record.ref__cart__dealer !== dealer.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
let newNotes = [...notes.value]
|
||||
const existingNoteIndex = newNotes.findIndex(n => n.id === record.id)
|
||||
|
||||
if (change_type === 'created') {
|
||||
|
||||
let newNotes = [...notes.value];
|
||||
const existingNoteIndex = newNotes.findIndex((n) => n.id === record.id);
|
||||
|
||||
if (change_type === "created") {
|
||||
if (existingNoteIndex === -1) {
|
||||
newNotes.push(record)
|
||||
newNotes.push(record);
|
||||
}
|
||||
} else if (change_type === 'updated') {
|
||||
} else if (change_type === "updated") {
|
||||
if (existingNoteIndex > -1) {
|
||||
newNotes[existingNoteIndex] = { ...newNotes[existingNoteIndex], ...record }
|
||||
newNotes[existingNoteIndex] = {
|
||||
...newNotes[existingNoteIndex],
|
||||
...record,
|
||||
};
|
||||
}
|
||||
} else if (change_type === 'deleted') {
|
||||
} else if (change_type === "deleted") {
|
||||
if (existingNoteIndex > -1) {
|
||||
newNotes.splice(existingNoteIndex, 1)
|
||||
newNotes.splice(existingNoteIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
notes.value = newNotes.sort((a, b) => new Date(b.create_time) - new Date(a.create_time));
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
$connectWebSocket();
|
||||
const filter = dealer?.id ? { ref__cart__dealer: dealer.id } : undefined;
|
||||
|
||||
$subscribe('productnote', filter, (initialData) => {
|
||||
|
||||
$subscribe("productnote", filter, (initialData) => {
|
||||
if (initialData && initialData.rows) {
|
||||
notes.value = initialData.rows.sort((a, b) => new Date(b.create_time) - new Date(a.create_time));
|
||||
}
|
||||
isLoading.value = false;
|
||||
});
|
||||
|
||||
window.addEventListener('ws_message', handleWsMessageForNotes);
|
||||
|
||||
window.addEventListener("ws_message", handleWsMessageForNotes);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('ws_message', handleWsMessageForNotes);
|
||||
window.removeEventListener("ws_message", handleWsMessageForNotes);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="is-flex is-flex-direction-column has-background-white" style="height: 100%; overflow: hidden;">
|
||||
<div
|
||||
class="is-flex is-flex-direction-column has-background-white"
|
||||
style="height: 100%; overflow: hidden"
|
||||
>
|
||||
<!-- Loading -->
|
||||
<div v-if="isLoading" class="is-flex is-flex-grow-1 is-justify-content-center is-align-items-center p-5">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
class="is-flex is-flex-grow-1 is-justify-content-center is-align-items-center p-5"
|
||||
>
|
||||
<div class="has-text-centered">
|
||||
<progress class="progress is-info" max="100" style="width: 100px;"></progress>
|
||||
<progress
|
||||
class="progress is-info"
|
||||
max="100"
|
||||
style="width: 100px"
|
||||
></progress>
|
||||
<p class="has-text-grey is-size-7 mt-2">Đang tải...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty -->
|
||||
<div v-else-if="notes.length === 0" class="is-flex is-flex-grow-1 is-justify-content-center is-align-items-center p-5">
|
||||
<div
|
||||
v-else-if="notes.length === 0"
|
||||
class="is-flex is-flex-grow-1 is-justify-content-center is-align-items-center p-5"
|
||||
>
|
||||
<p class="has-text-grey-light is-size-7">Chưa có ghi chú</p>
|
||||
</div>
|
||||
|
||||
<!-- Notes Table -->
|
||||
<div v-else class="is-flex-grow-1" style="overflow-y: auto; overflow-x: hidden; min-height: 0;">
|
||||
<div
|
||||
v-else
|
||||
class="is-flex-grow-1"
|
||||
style="overflow-y: auto; overflow-x: hidden; min-height: 0"
|
||||
>
|
||||
<table class="table is-fullwidth is-hoverable is-narrow is-size-7 mb-0">
|
||||
<thead style="position: sticky; top: 0px;">
|
||||
<thead style="position: sticky; top: 0px">
|
||||
<tr>
|
||||
<th>Sản phẩm</th>
|
||||
<th>Nội dung</th>
|
||||
@@ -85,14 +105,24 @@ onUnmounted(() => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="note in notes" :key="note.id" class="is-clickable" @click="navigateTo(note.ref)">
|
||||
<tr
|
||||
v-for="note in notes"
|
||||
:key="note.id"
|
||||
class="is-clickable"
|
||||
@click="navigateTo(note.ref)"
|
||||
>
|
||||
<td>
|
||||
<span class="tag is-link is-light is-small">{{ note.ref__trade_code }}</span>
|
||||
</td>
|
||||
<td style="white-space: pre-wrap; word-break: break-word;">{{ note.detail }}</td>
|
||||
<td style="white-space: pre-wrap; word-break: break-word">
|
||||
{{ note.detail }}
|
||||
</td>
|
||||
<td>{{ note.username || note.user__username }}</td>
|
||||
<td class="has-text-right" style="white-space: nowrap;">
|
||||
{{ new Date(note.create_time).toLocaleString('vi-VN') }}
|
||||
<td
|
||||
class="has-text-right"
|
||||
style="white-space: nowrap"
|
||||
>
|
||||
{{ new Date(note.create_time).toLocaleString("vi-VN") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -127,9 +157,10 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.table.is-narrow td, .table.is-narrow th {
|
||||
.table.is-narrow td,
|
||||
.table.is-narrow th {
|
||||
padding: 0.5em;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<script setup>
|
||||
import SvgIcon from '@/components/SvgIcon.vue';
|
||||
import { findLandByTradeCode, findTemInside, objIsTem } from '@/components/viewer/utils/aps-viewer';
|
||||
import { getBounds } from '@/components/viewer/utils/geometry';
|
||||
import SvgIcon from "@/components/SvgIcon.vue";
|
||||
import { findLandByTradeCode, findTemInside, objIsTem } from "@/components/viewer/utils/aps-viewer";
|
||||
import { getBounds } from "@/components/viewer/utils/geometry";
|
||||
|
||||
const props = defineProps({
|
||||
products: Array,
|
||||
})
|
||||
});
|
||||
|
||||
const productInput = ref('');
|
||||
const productInput = ref("");
|
||||
const isSubmitting = ref(false);
|
||||
|
||||
const productMatches = computed(() => {
|
||||
const val = productInput.value.trim().toUpperCase();
|
||||
if (!val) return;
|
||||
return props.products.filter(p => p.trade_code.includes(val));
|
||||
return props.products.filter((p) => p.trade_code.includes(val));
|
||||
});
|
||||
|
||||
function panToLand(trade_code) {
|
||||
@@ -26,20 +26,26 @@ function panToLand(trade_code) {
|
||||
viewer.model.getBulkProperties(allDbIds, {}, (results) => {
|
||||
const frags = viewer.model.getFragmentList();
|
||||
const tems = results.filter(objIsTem);
|
||||
const lands = results.filter(land => {
|
||||
const layerProp = land.properties.find(prop => prop.displayName === 'Layer');
|
||||
const lands = results.filter((land) => {
|
||||
const layerProp = land.properties.find((prop) => prop.displayName === "Layer");
|
||||
if (!layerProp) return false;
|
||||
const globalWidthProp = land.properties.find(prop => prop.displayName === 'Global width');
|
||||
return layerProp.displayValue === '1-bodim' && globalWidthProp.displayValue === 0;
|
||||
const globalWidthProp = land.properties.find((prop) => prop.displayName === "Global width");
|
||||
return layerProp.displayValue === "1-bodim" && globalWidthProp.displayValue === 0;
|
||||
});
|
||||
|
||||
const temsWithBounds = tems.map(tem => ({ ...tem, bounds: getBounds(tem.dbId, frags) }));
|
||||
const landsWithBounds = lands.map(land => ({ ...land, bounds: getBounds(land.dbId, frags) }));
|
||||
const temsInside = landsWithBounds.map(landWithBounds => findTemInside(landWithBounds, temsWithBounds));
|
||||
const temsWithBounds = tems.map((tem) => ({
|
||||
...tem,
|
||||
bounds: getBounds(tem.dbId, frags),
|
||||
}));
|
||||
const landsWithBounds = lands.map((land) => ({
|
||||
...land,
|
||||
bounds: getBounds(land.dbId, frags),
|
||||
}));
|
||||
const temsInside = landsWithBounds.map((landWithBounds) => findTemInside(landWithBounds, temsWithBounds));
|
||||
|
||||
const [foundLand] = findLandByTradeCode(trade_code, landsWithBounds, temsInside);
|
||||
if (!foundLand) return;
|
||||
|
||||
|
||||
viewer.fitToView([foundLand.dbId]);
|
||||
isSubmitting.value = false;
|
||||
});
|
||||
@@ -53,14 +59,14 @@ function clickMatchBtn(product) {
|
||||
|
||||
function submit() {
|
||||
isSubmitting.value = true;
|
||||
const trade_code = productInput.value.toUpperCase();
|
||||
const trade_code = productInput.value.toUpperCase();
|
||||
|
||||
const foundProduct = props.products.find(p => {
|
||||
const foundProduct = props.products.find((p) => {
|
||||
return p.trade_code === trade_code;
|
||||
});
|
||||
|
||||
if (!foundProduct) {
|
||||
isSubmitting.value = false;
|
||||
|
||||
if (!foundProduct) {
|
||||
isSubmitting.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,13 +74,11 @@ function submit() {
|
||||
}
|
||||
|
||||
const showMatches = ref(true);
|
||||
const input = useTemplateRef('input');
|
||||
const matches = useTemplateRef('matches');
|
||||
const input = useTemplateRef("input");
|
||||
const matches = useTemplateRef("matches");
|
||||
|
||||
function clickAwayListener(e) {
|
||||
if (input.value?.contains(e.target)
|
||||
|| matches.value?.contains(e.target)
|
||||
) {
|
||||
if (input.value?.contains(e.target) || matches.value?.contains(e.target)) {
|
||||
showMatches.value = true;
|
||||
} else {
|
||||
showMatches.value = false;
|
||||
@@ -82,30 +86,32 @@ function clickAwayListener(e) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('click', clickAwayListener)
|
||||
document.addEventListener("click", clickAwayListener);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('click', clickAwayListener)
|
||||
document.removeEventListener("click", clickAwayListener);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div class="has-background-white" style="position: sticky; top: 0">
|
||||
<div
|
||||
class="has-background-white"
|
||||
style="position: sticky; top: 0"
|
||||
>
|
||||
<form class="is-flex is-gap-1">
|
||||
<div class="field has-addons is-flex-grow-1">
|
||||
<p ref="input" class="control mb-0 is-flex-grow-1">
|
||||
<p
|
||||
ref="input"
|
||||
class="control mb-0 is-flex-grow-1"
|
||||
>
|
||||
<input
|
||||
v-model="productInput"
|
||||
class="input is-radiusless has-icons-right fs-13"
|
||||
type="text"
|
||||
placeholder="Tìm sản phẩm"
|
||||
style="
|
||||
box-sizing: border-box;
|
||||
border-inline-width: 0;
|
||||
box-shadow: none;
|
||||
"
|
||||
style="box-sizing: border-box; border-inline-width: 0; box-shadow: none"
|
||||
/>
|
||||
<button
|
||||
<button
|
||||
type="button"
|
||||
class="clearInputBtn"
|
||||
v-if="productInput"
|
||||
@@ -117,17 +123,17 @@ onUnmounted(() => {
|
||||
</p>
|
||||
<p class="control">
|
||||
<button
|
||||
:class="[
|
||||
'button is-small is-primary is-radiusless',
|
||||
isSubmitting && 'is-loading'
|
||||
]"
|
||||
:class="['button is-small is-primary is-radiusless', isSubmitting && 'is-loading']"
|
||||
:disabled="!productInput"
|
||||
tabindex="-1"
|
||||
style="box-sizing: border-box; width: 38px; height: 100%"
|
||||
@click.prevent="submit"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="!isSubmitting" class="icon">
|
||||
<span
|
||||
v-if="!isSubmitting"
|
||||
class="icon"
|
||||
>
|
||||
<SvgIcon v-bind="{ name: 'search.svg', type: 'white', size: 18 }" />
|
||||
</span>
|
||||
</button>
|
||||
@@ -153,11 +159,7 @@ onUnmounted(() => {
|
||||
v-for="product in productMatches"
|
||||
@click="clickMatchBtn(product)"
|
||||
class="button is-small is-fullwidth is-radiusless is-justify-content-start"
|
||||
style="
|
||||
box-sizing: border-box;
|
||||
border-inline-width: 0;
|
||||
border-bottom-width: 0;
|
||||
"
|
||||
style="box-sizing: border-box; border-inline-width: 0; border-bottom-width: 0"
|
||||
>
|
||||
<p>{{ product.trade_code }}</p>
|
||||
</button>
|
||||
@@ -165,14 +167,17 @@ onUnmounted(() => {
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.control.is-loading::after, .select.is-loading::after, .button.is-loading::after {
|
||||
.control.is-loading::after,
|
||||
.select.is-loading::after,
|
||||
.button.is-loading::after {
|
||||
/* overrides spinner color */
|
||||
border: 3px solid white;
|
||||
border-right-color: transparent;
|
||||
border-top-color: transparent;
|
||||
}
|
||||
|
||||
.button:focus-visible, .button.is-focused {
|
||||
.button:focus-visible,
|
||||
.button.is-focused {
|
||||
border-color: transparent;
|
||||
box-shadow: none;
|
||||
/* copy styles from is-hovered */
|
||||
@@ -194,7 +199,7 @@ onUnmounted(() => {
|
||||
background-color: hsla(0, 0%, 0%, 0.05);
|
||||
|
||||
&:hover {
|
||||
background-color: hsla(0, 0%, 0%, 0.1)
|
||||
background-color: hsla(0, 0%, 0%, 0.1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,17 +1,31 @@
|
||||
<script setup>
|
||||
import { createApp, ref, watch, h, nextTick } from 'vue'
|
||||
import { difference, throttle, debounce } from 'es-toolkit';
|
||||
import { utopiaUrn, blankUrn, getAccessToken, loadModel } from '@/components/viewer/utils/aps-init';
|
||||
import { addTemSelectionListener, applyLayerSetting, findTemInside, getTradeCodeFromTem, objIsLand, objIsTem, unloadUnusedExtensions } from '@/components/viewer/utils/aps-viewer';
|
||||
import { html, renderSvg } from '@/components/viewer/utils/dom';
|
||||
import { colorStrToVector4, extractPoints, GeometryCallback, getBounds, sortByAngle } from '@/components/viewer/utils/geometry';
|
||||
import Legends from '@/components/viewer/Legends.vue';
|
||||
import LayerSetting from '@/components/viewer/LayerSetting.vue';
|
||||
import ProductNoteHistory from '@/components/viewer/ProductNoteHistory.vue';
|
||||
import { createApp, ref, watch, h, nextTick } from "vue";
|
||||
import { difference, throttle, debounce } from "es-toolkit";
|
||||
import { utopiaUrn, blankUrn, getAccessToken, loadModel } from "@/components/viewer/utils/aps-init";
|
||||
import {
|
||||
addTemSelectionListener,
|
||||
applyLayerSetting,
|
||||
findTemInside,
|
||||
getTradeCodeFromTem,
|
||||
objIsLand,
|
||||
objIsTem,
|
||||
unloadUnusedExtensions,
|
||||
} from "@/components/viewer/utils/aps-viewer";
|
||||
import { html, renderSvg } from "@/components/viewer/utils/dom";
|
||||
import {
|
||||
colorStrToVector4,
|
||||
extractPoints,
|
||||
GeometryCallback,
|
||||
getBounds,
|
||||
sortByAngle,
|
||||
} from "@/components/viewer/utils/geometry";
|
||||
import Legends from "@/components/viewer/Legends.vue";
|
||||
import LayerSetting from "@/components/viewer/LayerSetting.vue";
|
||||
import ProductNoteHistory from "@/components/viewer/ProductNoteHistory.vue";
|
||||
|
||||
const { $exportpdf, $getdata, $connectWebSocket, $snackbar, $store, $subscribe } = useNuxtApp();
|
||||
const { dealer, layersetting } = $store;
|
||||
const viewerId = 'viewer';
|
||||
const viewerId = "viewer";
|
||||
const viewerRef = useTemplateRef(viewerId);
|
||||
|
||||
const products = ref([]);
|
||||
@@ -22,24 +36,22 @@ const dealers = ref([]);
|
||||
const selectedProductForNote = ref(null);
|
||||
|
||||
const statusModes = {
|
||||
SIMPLE: 'Đơn giản',
|
||||
DETAILED: 'Chi tiết',
|
||||
}
|
||||
SIMPLE: "Đơn giản",
|
||||
DETAILED: "Chi tiết",
|
||||
};
|
||||
|
||||
const statusMode = ref(statusModes.SIMPLE);
|
||||
|
||||
const statuses = computed(() =>
|
||||
statusMode.value === statusModes.SIMPLE ? saleStatuses.value : productStatuses.value
|
||||
);
|
||||
const statuses = computed(() => (statusMode.value === statusModes.SIMPLE ? saleStatuses.value : productStatuses.value));
|
||||
|
||||
const defaultStatusIds = computed(() => statuses.value.map(x => x.id));
|
||||
const defaultCartIds = computed(() => carts.value.map(x => x.id));
|
||||
const defaultDealerIds = computed(() => dealers.value.map(x => x.id));
|
||||
const defaultStatusIds = computed(() => statuses.value.map((x) => x.id));
|
||||
const defaultCartIds = computed(() => carts.value.map((x) => x.id));
|
||||
const defaultDealerIds = computed(() => dealers.value.map((x) => x.id));
|
||||
|
||||
const filters = ref({
|
||||
statusIds: defaultStatusIds.value,
|
||||
cartIds: defaultCartIds.value,
|
||||
dealerIds: defaultDealerIds.value
|
||||
dealerIds: defaultDealerIds.value,
|
||||
});
|
||||
|
||||
function addMeshes(viewer) {
|
||||
@@ -50,8 +62,10 @@ function addMeshes(viewer) {
|
||||
}
|
||||
|
||||
viewer.getObjectTree(async (instanceTree) => {
|
||||
const sceneBuilder = await viewer.getExtension('Autodesk.Viewing.SceneBuilder');
|
||||
const modelBuilder = await sceneBuilder.addNewModel({ conserveMemory: false });
|
||||
const sceneBuilder = await viewer.getExtension("Autodesk.Viewing.SceneBuilder");
|
||||
const modelBuilder = await sceneBuilder.addNewModel({
|
||||
conserveMemory: false,
|
||||
});
|
||||
window.colorModel = modelBuilder.model;
|
||||
viewer.setBackgroundColor(255, 255, 255, 255, 255, 255);
|
||||
|
||||
@@ -61,10 +75,10 @@ function addMeshes(viewer) {
|
||||
const pt = new THREE.Vector3();
|
||||
|
||||
const material = new THREE.MeshBasicMaterial({
|
||||
depthWrite: false,
|
||||
depthWrite: false,
|
||||
depthTest: true,
|
||||
});
|
||||
|
||||
|
||||
function createMesh(lines, productId) {
|
||||
// inverse viewer's matrix to convert CAD coords (~400000) to viewer coords (~10)
|
||||
inverseXform.copy(vpXform).invert();
|
||||
@@ -72,7 +86,7 @@ function addMeshes(viewer) {
|
||||
// Extract all unique points to form a polygon
|
||||
const worldPoints = extractPoints(lines);
|
||||
const viewerPoints = [];
|
||||
|
||||
|
||||
for (let i = 0; i < worldPoints.length; i++) {
|
||||
const { x, y } = worldPoints[i];
|
||||
pt.set(x, y, 0).applyMatrix4(inverseXform);
|
||||
@@ -82,14 +96,14 @@ function addMeshes(viewer) {
|
||||
let sortedPoints = sortByAngle(viewerPoints);
|
||||
if (productId === 9290) {
|
||||
// special case - Z.E02.02A, fix manually
|
||||
sortedPoints = sortedPoints.filter(coor => coor.y < 4);
|
||||
}
|
||||
sortedPoints = sortedPoints.filter((coor) => coor.y < 4);
|
||||
}
|
||||
|
||||
const shape = new THREE.Shape(sortedPoints);
|
||||
const shapeGeo = new THREE.ShapeGeometry(shape);
|
||||
const bufferGeo = new THREE.BufferGeometry().fromGeometry(shapeGeo);
|
||||
|
||||
const pos = bufferGeo.getAttribute('position');
|
||||
const pos = bufferGeo.getAttribute("position");
|
||||
// only set Z after calling ShapeGeometry()
|
||||
for (let i = 0; i < pos.count; i++) {
|
||||
pos.setZ(i, -1);
|
||||
@@ -105,58 +119,62 @@ function addMeshes(viewer) {
|
||||
const dbIdMap = instanceTree.nodeAccess.dbIdToIndex;
|
||||
const allDbIds = Object.keys(dbIdMap).map((id) => parseInt(id));
|
||||
|
||||
viewer.model.getBulkProperties(allDbIds, {
|
||||
propFilter: ['name', 'Layer', 'Global width', 'LO'],
|
||||
},
|
||||
(results) => {
|
||||
const frags = viewer.model.getFragmentList();
|
||||
window.meshes = [];
|
||||
let tems = [];
|
||||
let lands = [];
|
||||
viewer.model.getBulkProperties(
|
||||
allDbIds,
|
||||
{
|
||||
propFilter: ["name", "Layer", "Global width", "LO"],
|
||||
},
|
||||
(results) => {
|
||||
const frags = viewer.model.getFragmentList();
|
||||
window.meshes = [];
|
||||
let tems = [];
|
||||
let lands = [];
|
||||
|
||||
for (const obj of results) {
|
||||
if (objIsTem(obj))
|
||||
tems.push({ ...obj, bounds: getBounds(obj.dbId, frags) })
|
||||
else if (objIsLand(obj))
|
||||
lands.push({ ...obj, bounds: getBounds(obj.dbId, frags) })
|
||||
}
|
||||
|
||||
const temsInside = lands.map(land => findTemInside(land, tems));
|
||||
|
||||
lands.forEach((land, i) => {
|
||||
const temInside = temsInside[i];
|
||||
if (!temInside) {
|
||||
if (land.dbId === 70520) {
|
||||
return; // redundant polyline in Z.E02.02A from "26.01.16 - Export TMB - Phan mem.dwg"
|
||||
}
|
||||
console.error("Can't find temInside for land", land);
|
||||
return;
|
||||
for (const obj of results) {
|
||||
if (objIsTem(obj)) tems.push({ ...obj, bounds: getBounds(obj.dbId, frags) });
|
||||
else if (objIsLand(obj)) lands.push({ ...obj, bounds: getBounds(obj.dbId, frags) });
|
||||
}
|
||||
const trade_code = getTradeCodeFromTem(temInside);
|
||||
const product = products.value.find(product => product.trade_code === trade_code);
|
||||
if (!product) return;
|
||||
|
||||
const geoCb = new GeometryCallback(viewer, vpXform);
|
||||
|
||||
instanceTree.enumNodeFragments(land.dbId, (fragId) => {
|
||||
const renderProxy = viewer.impl.getRenderProxy(viewer.model, fragId);
|
||||
const vbr = new Autodesk.Viewing.Private.VertexBufferReader(renderProxy.geometry, viewer.impl.use2dInstancing);
|
||||
vbr.enumGeomsForObject(land.dbId, geoCb);
|
||||
const mesh = createMesh(geoCb.lines, product.id);
|
||||
window.meshes.push(mesh);
|
||||
modelBuilder.addMesh(mesh);
|
||||
const temsInside = lands.map((land) => findTemInside(land, tems));
|
||||
|
||||
lands.forEach((land, i) => {
|
||||
const temInside = temsInside[i];
|
||||
if (!temInside) {
|
||||
if (land.dbId === 70520) {
|
||||
return; // redundant polyline in Z.E02.02A from "26.01.16 - Export TMB - Phan mem.dwg"
|
||||
}
|
||||
console.error("Can't find temInside for land", land);
|
||||
return;
|
||||
}
|
||||
const trade_code = getTradeCodeFromTem(temInside);
|
||||
const product = products.value.find((product) => product.trade_code === trade_code);
|
||||
if (!product) return;
|
||||
|
||||
const geoCb = new GeometryCallback(viewer, vpXform);
|
||||
|
||||
instanceTree.enumNodeFragments(land.dbId, (fragId) => {
|
||||
const renderProxy = viewer.impl.getRenderProxy(viewer.model, fragId);
|
||||
const vbr = new Autodesk.Viewing.Private.VertexBufferReader(
|
||||
renderProxy.geometry,
|
||||
viewer.impl.use2dInstancing,
|
||||
);
|
||||
vbr.enumGeomsForObject(land.dbId, geoCb);
|
||||
const mesh = createMesh(geoCb.lines, product.id);
|
||||
window.meshes.push(mesh);
|
||||
modelBuilder.addMesh(mesh);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
viewer.impl.invalidate(true, true, true);
|
||||
applyColors();
|
||||
viewer.toolController.setMouseWheelInputEnabled(true); // re-enable zoom wheel
|
||||
const legend = document.querySelector('#legend.docking-panel.docking-panel-container-solid-color-a');
|
||||
if (legend) {
|
||||
legend.style.top = 'initial';
|
||||
legend.classList.remove('top-initial-important');
|
||||
}
|
||||
});
|
||||
viewer.impl.invalidate(true, true, true);
|
||||
applyColors();
|
||||
viewer.toolController.setMouseWheelInputEnabled(true); // re-enable zoom wheel
|
||||
const legend = document.querySelector("#legend.docking-panel.docking-panel-container-solid-color-a");
|
||||
if (legend) {
|
||||
legend.style.top = "initial";
|
||||
legend.classList.remove("top-initial-important");
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,9 +185,9 @@ function addMeshes(viewer) {
|
||||
function applyColors() {
|
||||
const { viewer, colorModel } = window;
|
||||
if (!viewer || !colorModel) return;
|
||||
|
||||
|
||||
const { statusIds, cartIds, dealerIds } = filters.value;
|
||||
const white = colorStrToVector4('white');
|
||||
const white = colorStrToVector4("white");
|
||||
|
||||
colorModel.getObjectTree((instanceTree) => {
|
||||
const dbIdMap = instanceTree.nodeAccess.dbIdToIndex;
|
||||
@@ -177,23 +195,35 @@ function applyColors() {
|
||||
|
||||
for (const dbId of allDbIds) {
|
||||
instanceTree.enumNodeFragments(dbId, () => {
|
||||
const product = products.value.find(p => p.id === dbId);
|
||||
const { status, status__code, status__color, status__sale_status, status__sale_status__color, cart, cart__dealer } = product;
|
||||
const product = products.value.find((p) => p.id === dbId);
|
||||
const {
|
||||
status,
|
||||
status__code,
|
||||
status__color,
|
||||
status__sale_status,
|
||||
status__sale_status__color,
|
||||
cart,
|
||||
cart__dealer,
|
||||
} = product;
|
||||
|
||||
const notSoldDealerCondition = dealer && status__code === 'not-sold';
|
||||
const notSoldDealerCondition = dealer && status__code === "not-sold";
|
||||
const currentStatus = statusMode.value === statusModes.SIMPLE ? status__sale_status : status;
|
||||
const statusCondition = statusIds.includes(currentStatus);
|
||||
const statusColor = colorStrToVector4(statusMode.value === statusModes.SIMPLE ? status__sale_status__color : status__color);
|
||||
|
||||
const statusColor = colorStrToVector4(
|
||||
statusMode.value === statusModes.SIMPLE ? status__sale_status__color : status__color,
|
||||
);
|
||||
|
||||
const diffCartFilter = difference(defaultCartIds.value, cartIds);
|
||||
const diffDealerFilter = difference(defaultDealerIds.value, dealerIds);
|
||||
const noCartFilter = diffCartFilter.length === 0;
|
||||
const noDealerFilter = diffDealerFilter.length === 0;
|
||||
const cartCondition = noCartFilter ? cartIds.includes(cart) || cart === null : cartIds.includes(cart);
|
||||
const dealerCondition = noDealerFilter ? dealerIds.includes(cart__dealer) || cart__dealer === null : dealerIds.includes(cart__dealer);
|
||||
const dealerCondition = noDealerFilter
|
||||
? dealerIds.includes(cart__dealer) || cart__dealer === null
|
||||
: dealerIds.includes(cart__dealer);
|
||||
|
||||
const showColor = statusCondition && !notSoldDealerCondition && cartCondition && dealerCondition;
|
||||
viewer.setThemingColor(dbId, showColor ? statusColor : white, colorModel);
|
||||
viewer.setThemingColor(dbId, showColor ? statusColor : white, colorModel);
|
||||
});
|
||||
}
|
||||
viewer.impl.invalidate(true, true, true);
|
||||
@@ -215,7 +245,7 @@ function openProductViewModal(product) {
|
||||
title: "Thông tin chi tiết",
|
||||
vbind: {
|
||||
row: product,
|
||||
view: true
|
||||
view: true,
|
||||
},
|
||||
width: "80%",
|
||||
height: "700px",
|
||||
@@ -236,10 +266,10 @@ const isRefreshing = ref(false);
|
||||
|
||||
function mountLegends() {
|
||||
if (legendsSFC.value) legendsSFC.value.unmount();
|
||||
const content = document.querySelector('.content.docking-panel-scroll');
|
||||
const content = document.querySelector(".content.docking-panel-scroll");
|
||||
|
||||
legendsSFC.value = createApp({
|
||||
name: 'Legends',
|
||||
name: "Legends",
|
||||
render() {
|
||||
return h(Legends, {
|
||||
products,
|
||||
@@ -251,31 +281,31 @@ function mountLegends() {
|
||||
statuses,
|
||||
statusModes,
|
||||
statusMode,
|
||||
onSwitchStatusMode: (newMode => {
|
||||
onSwitchStatusMode: (newMode) => {
|
||||
statusMode.value = newMode;
|
||||
}),
|
||||
onUpdateFilters: (payload => {
|
||||
},
|
||||
onUpdateFilters: (payload) => {
|
||||
filters.value = {
|
||||
...filters.value,
|
||||
...payload
|
||||
}
|
||||
}),
|
||||
...payload,
|
||||
};
|
||||
},
|
||||
onResetCartDealerFilters: () => {
|
||||
filters.value = {
|
||||
...filters.value,
|
||||
cartIds: defaultCartIds.value,
|
||||
dealerIds: defaultDealerIds.value,
|
||||
}
|
||||
};
|
||||
},
|
||||
onResetFilters: () => {
|
||||
filters.value = {
|
||||
statusIds: defaultStatusIds.value,
|
||||
cartIds: defaultCartIds.value,
|
||||
dealerIds: defaultDealerIds.value,
|
||||
}
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
legendsSFC.value.mount(content);
|
||||
@@ -295,33 +325,38 @@ async function refreshFromWebSocket() {
|
||||
}
|
||||
|
||||
function initViewer(container) {
|
||||
const customToolbarId = 'customToolbar';
|
||||
const printId = 'print';
|
||||
const legendId = 'legend';
|
||||
const noteHistoryId = 'noteHistory';
|
||||
const customToolbarId = "customToolbar";
|
||||
const printId = "print";
|
||||
const legendId = "legend";
|
||||
const noteHistoryId = "noteHistory";
|
||||
|
||||
class LegendPanel extends Autodesk.Viewing.UI.DockingPanel {
|
||||
constructor(container, id, title, options) {
|
||||
super(container, id, title, options);
|
||||
this.viewer = viewer;
|
||||
Autodesk.Viewing.UI.DockingPanel.call(this, container, id, '');
|
||||
Autodesk.Viewing.UI.DockingPanel.call(this, container, id, "");
|
||||
|
||||
this.container.classList.add('docking-panel-container-solid-color-a', 'is-flex', 'is-flex-direction-column', 'top-initial-important');
|
||||
this.container.style['overflow-x'] = 'unset';
|
||||
this.container.style['overflow-y'] = 'unset';
|
||||
this.container.classList.add(
|
||||
"docking-panel-container-solid-color-a",
|
||||
"is-flex",
|
||||
"is-flex-direction-column",
|
||||
"top-initial-important",
|
||||
);
|
||||
this.container.style["overflow-x"] = "unset";
|
||||
this.container.style["overflow-y"] = "unset";
|
||||
|
||||
const panelTitle = this.container.querySelector('.docking-panel-title');
|
||||
panelTitle.innerText = 'Chú giải';
|
||||
const panelTitle = this.container.querySelector(".docking-panel-title");
|
||||
panelTitle.innerText = "Chú giải";
|
||||
|
||||
const panelCloseBtn = this.container.querySelector('.docking-panel-close');
|
||||
const panelCloseBtn = this.container.querySelector(".docking-panel-close");
|
||||
panelCloseBtn.onclick = () => {
|
||||
const legendBtnElem = document.getElementById(`${customToolbarId}-${legendId}Tool`);
|
||||
this.container.classList.toggle('is-flex');
|
||||
legendBtnElem.classList.toggle('active');
|
||||
legendBtnElem.classList.toggle('inactive');
|
||||
}
|
||||
this.container.classList.toggle("is-flex");
|
||||
legendBtnElem.classList.toggle("active");
|
||||
legendBtnElem.classList.toggle("inactive");
|
||||
};
|
||||
|
||||
const content = html('div', { class: 'content docking-panel-scroll' });
|
||||
const content = html("div", { class: "content docking-panel-scroll" });
|
||||
this.container.append(content);
|
||||
mountLegends();
|
||||
}
|
||||
@@ -333,52 +368,55 @@ function initViewer(container) {
|
||||
this.viewer = viewer;
|
||||
this.app = null;
|
||||
this.isVisibleState = false;
|
||||
Autodesk.Viewing.UI.DockingPanel.call(this, container, id, '');
|
||||
Autodesk.Viewing.UI.DockingPanel.call(this, container, id, "");
|
||||
|
||||
this.container.classList.add('docking-panel-container-solid-color-a', 'is-flex-direction-column', 'is-hidden');
|
||||
this.container.classList.add("docking-panel-container-solid-color-a", "is-flex-direction-column", "is-hidden");
|
||||
|
||||
const panelTitle = this.container.querySelector('.docking-panel-title');
|
||||
panelTitle.innerText = 'Lịch sử ghi chú';
|
||||
const panelTitle = this.container.querySelector(".docking-panel-title");
|
||||
panelTitle.innerText = "Lịch sử ghi chú";
|
||||
|
||||
const panelCloseBtn = this.container.querySelector('.docking-panel-close');
|
||||
const panelCloseBtn = this.container.querySelector(".docking-panel-close");
|
||||
panelCloseBtn.onclick = () => {
|
||||
this.toggleVisibility();
|
||||
this.container.classList.toggle('is-flex');
|
||||
this.container.classList.toggle('is-hidden');
|
||||
}
|
||||
this.container.classList.toggle("is-flex");
|
||||
this.container.classList.toggle("is-hidden");
|
||||
};
|
||||
|
||||
const content = html('div', { id: 'note-history-content', class: 'content docking-panel-scroll' });
|
||||
const content = html("div", {
|
||||
id: "note-history-content",
|
||||
class: "content docking-panel-scroll",
|
||||
});
|
||||
this.container.append(content);
|
||||
this.contentElement = content;
|
||||
this.mountVueComponent(content);
|
||||
}
|
||||
|
||||
toggleVisibility() {
|
||||
console.log('[NoteHistoryPanel] toggleVisibility called, current state:', this.isVisibleState);
|
||||
console.log("[NoteHistoryPanel] toggleVisibility called, current state:", this.isVisibleState);
|
||||
const noteHistoryBtnElem = document.getElementById(`${customToolbarId}-${noteHistoryId}Tool`);
|
||||
if (this.isVisibleState) {
|
||||
this.hide();
|
||||
noteHistoryBtnElem?.classList.remove('active');
|
||||
noteHistoryBtnElem?.classList.add('inactive');
|
||||
noteHistoryBtnElem?.classList.remove("active");
|
||||
noteHistoryBtnElem?.classList.add("inactive");
|
||||
} else {
|
||||
this.show();
|
||||
noteHistoryBtnElem?.classList.add('active');
|
||||
noteHistoryBtnElem?.classList.remove('inactive');
|
||||
noteHistoryBtnElem?.classList.add("active");
|
||||
noteHistoryBtnElem?.classList.remove("inactive");
|
||||
}
|
||||
}
|
||||
|
||||
show() {
|
||||
console.log('[NoteHistoryPanel] show() called');
|
||||
console.log("[NoteHistoryPanel] show() called");
|
||||
this.isVisibleState = true;
|
||||
this.container.style.display = 'flex';
|
||||
this.container.style.display = "flex";
|
||||
|
||||
// Đảm bảo content element giữ nguyên style
|
||||
if (this.contentElement) {
|
||||
this.contentElement.style.display = 'flex';
|
||||
this.contentElement.style.flex = '1';
|
||||
this.contentElement.style.overflowY = 'auto';
|
||||
this.contentElement.style.overflowX = 'hidden';
|
||||
this.contentElement.style.minHeight = '0';
|
||||
this.contentElement.style.display = "flex";
|
||||
this.contentElement.style.flex = "1";
|
||||
this.contentElement.style.overflowY = "auto";
|
||||
this.contentElement.style.overflowX = "hidden";
|
||||
this.contentElement.style.minHeight = "0";
|
||||
}
|
||||
|
||||
this.setVisible(true);
|
||||
@@ -390,9 +428,9 @@ function initViewer(container) {
|
||||
}
|
||||
|
||||
hide() {
|
||||
console.log('[NoteHistoryPanel] hide() called');
|
||||
console.log("[NoteHistoryPanel] hide() called");
|
||||
this.isVisibleState = false;
|
||||
this.container.style.display = 'none';
|
||||
this.container.style.display = "none";
|
||||
this.setVisible(false);
|
||||
}
|
||||
|
||||
@@ -401,10 +439,11 @@ function initViewer(container) {
|
||||
this.app.unmount();
|
||||
}
|
||||
this.app = createApp({
|
||||
name: 'ProductNoteHistory',
|
||||
render: () => h(ProductNoteHistory, {
|
||||
selectedProduct: selectedProductForNote.value
|
||||
})
|
||||
name: "ProductNoteHistory",
|
||||
render: () =>
|
||||
h(ProductNoteHistory, {
|
||||
selectedProduct: selectedProductForNote.value,
|
||||
}),
|
||||
});
|
||||
this.app.mount(element);
|
||||
}
|
||||
@@ -429,7 +468,7 @@ function initViewer(container) {
|
||||
}
|
||||
|
||||
unload() {
|
||||
console.info('Print extension is now unloaded!');
|
||||
console.info("Print extension is now unloaded!");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -444,7 +483,9 @@ function initViewer(container) {
|
||||
this.subToolbar = toolbarGroup;
|
||||
const printBtn = new Autodesk.Viewing.UI.Button(`${customToolbarId}-${printId}Tool`);
|
||||
const adskButtonIcon = printBtn.container.children.item(0);
|
||||
renderSvg('/icon/print.svg').then((icon) => { adskButtonIcon.append(icon) });
|
||||
renderSvg("/icon/print.svg").then((icon) => {
|
||||
adskButtonIcon.append(icon);
|
||||
});
|
||||
|
||||
printBtn.onClick = () => this.print(viewer);
|
||||
printBtn.setToolTip(printId);
|
||||
@@ -453,21 +494,24 @@ function initViewer(container) {
|
||||
|
||||
print(viewer) {
|
||||
const getPdf = async (blob) => {
|
||||
const docId = 'bieu-do';
|
||||
const div = html('div', { id: docId, style: 'width: 1650px; height: 1030px' }, [
|
||||
html('img', { src: blob, style: 'object-fit: contain; max-height: 100%' })
|
||||
const docId = "bieu-do";
|
||||
const div = html("div", { id: docId, style: "width: 1650px; height: 1030px" }, [
|
||||
html("img", {
|
||||
src: blob,
|
||||
style: "object-fit: contain; max-height: 100%",
|
||||
}),
|
||||
]);
|
||||
document.body.append(div);
|
||||
const fileName = docId;
|
||||
$exportpdf(docId, fileName, 'a3', 'landscape');
|
||||
$snackbar('Đang xuất PDF...', { type: 'is-info' });
|
||||
$exportpdf(docId, fileName, "a3", "landscape");
|
||||
$snackbar("Đang xuất PDF...", { type: "is-info" });
|
||||
div.remove();
|
||||
}
|
||||
};
|
||||
|
||||
const canvasBounds = viewer.impl.getCanvasBoundingClientRect();
|
||||
const cvWidth = canvasBounds.width;
|
||||
const cvHeight = canvasBounds.height;
|
||||
const pageWidth = viewer.model.getMetadata('page_dimensions', 'page_width'); // 11
|
||||
const pageWidth = viewer.model.getMetadata("page_dimensions", "page_width"); // 11
|
||||
|
||||
const DPI = 150;
|
||||
const scale = 3;
|
||||
@@ -485,7 +529,7 @@ function initViewer(container) {
|
||||
this.viewer.removeEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
|
||||
this.onToolbarCreatedBinded = null;
|
||||
this.createUI();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class Legend extends Autodesk.Viewing.Extension {
|
||||
@@ -500,7 +544,7 @@ function initViewer(container) {
|
||||
}
|
||||
|
||||
unload() {
|
||||
console.info('Legend extension is now unloaded!');
|
||||
console.info("Legend extension is now unloaded!");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -524,18 +568,18 @@ function initViewer(container) {
|
||||
const legendBtnElem = legendBtn.container;
|
||||
const adskButtonIcon = legendBtnElem.children.item(0);
|
||||
|
||||
renderSvg('/icon/legend.svg').then((icon) => {
|
||||
renderSvg("/icon/legend.svg").then((icon) => {
|
||||
adskButtonIcon.append(icon);
|
||||
// because legend panel is open on first load
|
||||
legendBtnElem.classList.add('active');
|
||||
legendBtnElem.classList.remove('inactive');
|
||||
legendBtnElem.classList.add("active");
|
||||
legendBtnElem.classList.remove("inactive");
|
||||
});
|
||||
|
||||
legendBtn.onClick = () => {
|
||||
panel.container.classList.toggle('is-flex');
|
||||
panel.container.classList.toggle('is-hidden');
|
||||
legendBtnElem.classList.toggle('active');
|
||||
legendBtnElem.classList.toggle('inactive');
|
||||
panel.container.classList.toggle("is-flex");
|
||||
panel.container.classList.toggle("is-hidden");
|
||||
legendBtnElem.classList.toggle("active");
|
||||
legendBtnElem.classList.toggle("inactive");
|
||||
};
|
||||
legendBtn.setToolTip(legendId);
|
||||
this.subToolbar.addControl(legendBtn);
|
||||
@@ -544,10 +588,10 @@ function initViewer(container) {
|
||||
onToolbarCreated(e) {
|
||||
this.viewer.removeEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
|
||||
this.onToolbarCreatedBinded = null;
|
||||
if (e.type === 'toolbarCreated') {
|
||||
if (e.type === "toolbarCreated") {
|
||||
this.createUI();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class NoteHistory extends Autodesk.Viewing.Extension {
|
||||
@@ -584,7 +628,7 @@ function initViewer(container) {
|
||||
const viewer = this.viewer;
|
||||
let panel = this.panel;
|
||||
if (!panel) {
|
||||
panel = new NoteHistoryPanel(viewer.container, noteHistoryId, 'Note History');
|
||||
panel = new NoteHistoryPanel(viewer.container, noteHistoryId, "Note History");
|
||||
viewer.addPanel(panel);
|
||||
this.panel = panel;
|
||||
}
|
||||
@@ -596,20 +640,22 @@ function initViewer(container) {
|
||||
}
|
||||
|
||||
this.button = new Autodesk.Viewing.UI.Button(`${customToolbarId}-${noteHistoryId}Tool`);
|
||||
this.button.setToolTip('Lịch sử ghi chú');
|
||||
this.button.setToolTip("Lịch sử ghi chú");
|
||||
|
||||
const adskButtonIcon = this.button.container.children.item(0);
|
||||
if (!selectedProductForNote.value) {
|
||||
adskButtonIcon.style.filter = 'grayscale(100%)';
|
||||
adskButtonIcon.style.filter = "grayscale(100%)";
|
||||
}
|
||||
renderSvg('/icon/notes.svg').then((icon) => { adskButtonIcon.append(icon) });
|
||||
renderSvg("/icon/notes.svg").then((icon) => {
|
||||
adskButtonIcon.append(icon);
|
||||
});
|
||||
|
||||
this.button.onClick = () => {
|
||||
panel.toggleVisibility();
|
||||
panel.container.classList.toggle('is-flex');
|
||||
panel.container.classList.toggle('is-hidden');
|
||||
this.button.container.classList.toggle('active');
|
||||
this.button.container.classList.toggle('inactive');
|
||||
panel.container.classList.toggle("is-flex");
|
||||
panel.container.classList.toggle("is-hidden");
|
||||
this.button.container.classList.toggle("active");
|
||||
this.button.container.classList.toggle("inactive");
|
||||
};
|
||||
|
||||
toolbarGroup.addControl(this.button);
|
||||
@@ -626,14 +672,14 @@ function initViewer(container) {
|
||||
Autodesk.Viewing.theExtensionManager.registerExtension(printId, Print);
|
||||
Autodesk.Viewing.theExtensionManager.registerExtension(legendId, Legend);
|
||||
Autodesk.Viewing.theExtensionManager.registerExtension(noteHistoryId, NoteHistory);
|
||||
Autodesk.Viewing.Initializer({ env: 'AutodeskProduction', getAccessToken }, () => {
|
||||
Autodesk.Viewing.Initializer({ env: "AutodeskProduction", getAccessToken }, () => {
|
||||
const config = {
|
||||
extensions: [printId, legendId, noteHistoryId, 'Autodesk.ModelStructure', 'Autodesk.Viewing.SceneBuilder'],
|
||||
extensions: [printId, legendId, noteHistoryId, "Autodesk.ModelStructure", "Autodesk.Viewing.SceneBuilder"],
|
||||
};
|
||||
|
||||
const viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
|
||||
viewer.start();
|
||||
viewer.setTheme('light-theme');
|
||||
viewer.setTheme("light-theme");
|
||||
viewer.setProgressiveRendering(false);
|
||||
viewer.setReverseZoomDirection(true);
|
||||
viewer.toolController.setMouseWheelInputEnabled(false);
|
||||
@@ -642,17 +688,21 @@ function initViewer(container) {
|
||||
viewer.navigation.FIT_TO_VIEW_HORIZONTAL_MARGIN = zoomOutFactor;
|
||||
|
||||
// disable Autodesk's "fit everything in view" mouse & keyboard shortcuts
|
||||
viewer.canvas.addEventListener('dblclick', (e) => {
|
||||
viewer.canvas.addEventListener("dblclick", (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', e => {
|
||||
const canvas = viewerRef.value.querySelector('canvas');
|
||||
if (canvas && (e.key === 'F' || e.key === 'f')) {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
}, true);
|
||||
|
||||
|
||||
document.addEventListener(
|
||||
"keydown",
|
||||
(e) => {
|
||||
const canvas = viewerRef.value.querySelector("canvas");
|
||||
if (canvas && (e.key === "F" || e.key === "f")) {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, function onceLoaded(e) {
|
||||
const viewer = e.target;
|
||||
viewer.removeEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, onceLoaded);
|
||||
@@ -692,7 +742,7 @@ function initViewer(container) {
|
||||
}
|
||||
}
|
||||
|
||||
viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, throttle(resetCameraIfZoomError , 300));
|
||||
viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, throttle(resetCameraIfZoomError, 300));
|
||||
addTemSelectionListener(viewer, products.value, openProductViewModal, openNoPermissionModal);
|
||||
unloadUnusedExtensions(viewer);
|
||||
resolve(viewer);
|
||||
@@ -725,30 +775,29 @@ async function loadViewer() {
|
||||
const allDbIds = Object.keys(dbIdMap).map((id) => parseInt(id));
|
||||
allDbIds.forEach((dbId) => {
|
||||
viewer.setThemingColor(dbId, new THREE.Vector4(1, 1, 1, 1), window.blankModel); // turns white to simulate invisibility
|
||||
})
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Error in loadViewer', error);
|
||||
console.log("Error in loadViewer", error);
|
||||
}
|
||||
}
|
||||
|
||||
function mountLayerSetting() {
|
||||
const observer = new MutationObserver((mutations, observer) => {
|
||||
mutations.forEach(mutation => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.addedNodes.length > 0) {
|
||||
const addedNode = mutation.addedNodes.item(0);
|
||||
if (addedNode.nodeType === 1
|
||||
&& addedNode.nodeName === 'DIV'
|
||||
&& addedNode.id === 'ViewerLayersPanel'
|
||||
) {
|
||||
const layerSettingSFCContainer = html('div', { id: 'layerSettingSFCContainer' });
|
||||
if (addedNode.nodeType === 1 && addedNode.nodeName === "DIV" && addedNode.id === "ViewerLayersPanel") {
|
||||
const layerSettingSFCContainer = html("div", {
|
||||
id: "layerSettingSFCContainer",
|
||||
});
|
||||
addedNode.append(layerSettingSFCContainer);
|
||||
|
||||
const layerSettingSFC = createApp({
|
||||
name: 'LayerSetting',
|
||||
name: "LayerSetting",
|
||||
render() {
|
||||
return h(LayerSetting);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
layerSettingSFC.mount(layerSettingSFCContainer);
|
||||
@@ -761,26 +810,26 @@ function mountLayerSetting() {
|
||||
observer.observe(viewerRef.value, {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
async function loadData(first = true) {
|
||||
const [productsData, productStatusesData, saleStatusesData, cartsData, dealersData] = await Promise.all([
|
||||
$getdata('product', {
|
||||
cart__dealer__code: dealer?.code
|
||||
$getdata("product", {
|
||||
cart__dealer__code: dealer?.code,
|
||||
}),
|
||||
$getdata('productstatus', undefined, { sort: "index" }),
|
||||
$getdata('salestatus', undefined, { sort: "name" }),
|
||||
$getdata('cart', undefined, {
|
||||
$getdata("productstatus", undefined, { sort: "index" }),
|
||||
$getdata("salestatus", undefined, { sort: "name" }),
|
||||
$getdata("cart", undefined, {
|
||||
filter: { dealer: dealer?.id },
|
||||
values: "id,code,name,dealer,index",
|
||||
distinct_values: {
|
||||
product_count: { type: "Count", field: "prdcart" }
|
||||
product_count: { type: "Count", field: "prdcart" },
|
||||
},
|
||||
summary: "annotate",
|
||||
sort: "index"
|
||||
sort: "index",
|
||||
}),
|
||||
$getdata('dealer'),
|
||||
$getdata("dealer"),
|
||||
]);
|
||||
|
||||
products.value = productsData;
|
||||
@@ -793,36 +842,40 @@ async function loadData(first = true) {
|
||||
filters.value = {
|
||||
statusIds: defaultStatusIds.value,
|
||||
cartIds: defaultCartIds.value,
|
||||
dealerIds: defaultDealerIds.value
|
||||
dealerIds: defaultDealerIds.value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
watch([products, filters], debouncedApplyColors, { deep: true });
|
||||
|
||||
watch(statuses, (val, oldVal) => {
|
||||
if (oldVal.length !== val.length) {
|
||||
// only reset status filter if statuses changes because of statusMode,
|
||||
// not when new data is fetched
|
||||
filters.value.statusIds = val.map(s => s.id);
|
||||
}
|
||||
}, { deep: true });
|
||||
watch(
|
||||
statuses,
|
||||
(val, oldVal) => {
|
||||
if (oldVal.length !== val.length) {
|
||||
// only reset status filter if statuses changes because of statusMode,
|
||||
// not when new data is fetched
|
||||
filters.value.statusIds = val.map((s) => s.id);
|
||||
}
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Watch for product selection to enable/disable the note history button
|
||||
watch(selectedProductForNote, (newProduct) => {
|
||||
const noteHistoryBtn = document.getElementById('customToolbar-noteHistoryTool');
|
||||
const noteHistoryBtn = document.getElementById("customToolbar-noteHistoryTool");
|
||||
if (noteHistoryBtn) {
|
||||
const icon = noteHistoryBtn.querySelector('.adsk-button-icon');
|
||||
const icon = noteHistoryBtn.querySelector(".adsk-button-icon");
|
||||
if (newProduct) {
|
||||
if (icon) icon.style.filter = 'none'; // Enabled look
|
||||
if (icon) icon.style.filter = "none"; // Enabled look
|
||||
} else {
|
||||
if (icon) icon.style.filter = 'grayscale(100%)'; // Disabled look
|
||||
const ext = window.viewer?.getExtension?.('noteHistory');
|
||||
if (icon) icon.style.filter = "grayscale(100%)"; // Disabled look
|
||||
const ext = window.viewer?.getExtension?.("noteHistory");
|
||||
if (ext?.panel) {
|
||||
ext.panel.setVisible(false);
|
||||
}
|
||||
noteHistoryBtn.classList.remove('active');
|
||||
noteHistoryBtn.classList.add('inactive');
|
||||
noteHistoryBtn.classList.remove("active");
|
||||
noteHistoryBtn.classList.add("inactive");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -831,7 +884,7 @@ function handleNavigateToProduct(event) {
|
||||
const { productId } = event.detail;
|
||||
if (!window.viewer || !window.meshes || !productId) return;
|
||||
|
||||
const mesh = window.meshes.find(m => m.dbId === productId);
|
||||
const mesh = window.meshes.find((m) => m.dbId === productId);
|
||||
if (mesh) {
|
||||
const boundingBox = new THREE.Box3().setFromObject(mesh);
|
||||
window.viewer.navigation.fitBounds(false, boundingBox);
|
||||
@@ -841,27 +894,26 @@ function handleNavigateToProduct(event) {
|
||||
}
|
||||
|
||||
async function handleWsMessage({ detail: response }) {
|
||||
if (response.type === 'realtime_update') {
|
||||
if (response.type === "realtime_update") {
|
||||
const { name, change_type, record } = response.payload;
|
||||
if (name.toLowerCase() !== 'product') return;
|
||||
if (name.toLowerCase() !== "product") return;
|
||||
|
||||
console.log(`[Viewer] Received WS update for Product: ${change_type}`, record);
|
||||
if(dealer){
|
||||
if(record.cart__dealer__code === dealer.code){
|
||||
console.log(dealer.code)
|
||||
if (dealer) {
|
||||
if (record.cart__dealer__code === dealer.code) {
|
||||
console.log(dealer.code);
|
||||
$snackbar("Căn " + record.trade_code + " đã có cập nhật mới", "Thành công", "Success");
|
||||
}
|
||||
} else {
|
||||
$snackbar("Căn " + record.trade_code + " đã có cập nhật mới", "Thành công", "Success");
|
||||
}
|
||||
|
||||
|
||||
|
||||
await nextTick();
|
||||
await refreshFromWebSocket();
|
||||
}
|
||||
}
|
||||
|
||||
const viewerSDKSharedPath = 'https://developer.api.autodesk.com/modelderivative/v2/viewers/7.115.0';
|
||||
const viewerSDKSharedPath = "https://developer.api.autodesk.com/modelderivative/v2/viewers/7.115.0";
|
||||
const viewerSDKPattern = `script[src^="${viewerSDKSharedPath}"],link[href^="${viewerSDKSharedPath}"]`;
|
||||
|
||||
async function initialize() {
|
||||
@@ -872,28 +924,33 @@ async function initialize() {
|
||||
$connectWebSocket();
|
||||
|
||||
if (dealer?.id) {
|
||||
$subscribe('product', { cart__dealer__code: dealer?.code }, (initialData) => {
|
||||
$subscribe("product", { cart__dealer__code: dealer?.code }, (initialData) => {
|
||||
if (initialData && initialData.rows) {
|
||||
products.value = initialData.rows;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$subscribe('product', undefined, (initialData) => {
|
||||
$subscribe("product", undefined, (initialData) => {
|
||||
if (initialData && initialData.rows) {
|
||||
products.value = initialData.rows;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('navigateToProduct', handleNavigateToProduct);
|
||||
window.addEventListener('ws_message', handleWsMessage);
|
||||
window.addEventListener("navigateToProduct", handleNavigateToProduct);
|
||||
window.addEventListener("ws_message", handleWsMessage);
|
||||
|
||||
if (window.Autodesk) {
|
||||
loadViewer();
|
||||
}
|
||||
else {
|
||||
const viewerScript = html('script', { src: `${viewerSDKSharedPath}/viewer3D.js`, onload: loadViewer });
|
||||
const viewerStyle = html('link', { href: `${viewerSDKSharedPath}/style.css`, rel: 'stylesheet' });
|
||||
} else {
|
||||
const viewerScript = html("script", {
|
||||
src: `${viewerSDKSharedPath}/viewer3D.js`,
|
||||
onload: loadViewer,
|
||||
});
|
||||
const viewerStyle = html("link", {
|
||||
href: `${viewerSDKSharedPath}/style.css`,
|
||||
rel: "stylesheet",
|
||||
});
|
||||
document.head.append(viewerScript, viewerStyle);
|
||||
}
|
||||
}
|
||||
@@ -907,12 +964,12 @@ function cleanup() {
|
||||
Autodesk.Viewing.shutdown();
|
||||
Autodesk = undefined;
|
||||
|
||||
viewerSDKs.forEach(script => script.remove());
|
||||
viewerSDKs.forEach((script) => script.remove());
|
||||
isViewerLoading.value = false;
|
||||
componentActivated.value = false;
|
||||
|
||||
window.removeEventListener('navigateToProduct', handleNavigateToProduct);
|
||||
window.removeEventListener('ws_message', handleWsMessage);
|
||||
window.removeEventListener("navigateToProduct", handleNavigateToProduct);
|
||||
window.removeEventListener("ws_message", handleWsMessage);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,10 +991,20 @@ onDeactivated(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="componentActivated" id="ViewerMaster">
|
||||
<div :ref="viewerId" :id="viewerId"></div>
|
||||
<div
|
||||
v-if="componentActivated"
|
||||
id="ViewerMaster"
|
||||
>
|
||||
<div
|
||||
:ref="viewerId"
|
||||
:id="viewerId"
|
||||
></div>
|
||||
<div id="overlay"></div>
|
||||
<Modal @close="closeProductViewModal" v-if="showProductViewModal" v-bind="showProductViewModal" />
|
||||
<Modal
|
||||
@close="closeProductViewModal"
|
||||
v-if="showProductViewModal"
|
||||
v-bind="showProductViewModal"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1004,7 +1071,7 @@ onDeactivated(() => {
|
||||
padding: 1em;
|
||||
display: none;
|
||||
|
||||
>.notification {
|
||||
> .notification {
|
||||
margin: auto;
|
||||
padding: 1em;
|
||||
max-width: 50%;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
export const utopiaUrn = 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6cGtxYjhrbHBnZWtsZ2tlbHBzanBoYzljMm5neXhtbjY0cXZocHNhcXVodjQ2emVuLWJhc2ljLWFwcC8yNi4wMS4xNiUyMC0lMjBFeHBvcnQlMjBUTUIlMjAtJTIwUGhhbiUyMG1lbS5kd2c';
|
||||
export const blankUrn = 'dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6cGtxYjhrbHBnZWtsZ2tlbHBzanBoYzljMm5neXhtbjY0cXZocHNhcXVodjQ2emVuLWJhc2ljLWFwcC9ibGFuay5kd2c';
|
||||
export const utopiaUrn =
|
||||
"dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6cGtxYjhrbHBnZWtsZ2tlbHBzanBoYzljMm5neXhtbjY0cXZocHNhcXVodjQ2emVuLWJhc2ljLWFwcC8yNi4wMS4xNiUyMC0lMjBFeHBvcnQlMjBUTUIlMjAtJTIwUGhhbiUyMG1lbS5kd2c";
|
||||
export const blankUrn =
|
||||
"dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6cGtxYjhrbHBnZWtsZ2tlbHBzanBoYzljMm5neXhtbjY0cXZocHNhcXVodjQ2emVuLWJhc2ljLWFwcC9ibGFuay5kd2c";
|
||||
|
||||
export async function getAccessToken(callback) {
|
||||
try {
|
||||
const { access_token, expires_in } = await $fetch('/api/apsAuthToken');
|
||||
const { access_token, expires_in } = await $fetch("/api/apsAuthToken");
|
||||
callback(access_token, expires_in);
|
||||
} catch (err) {
|
||||
console.error('Could not obtain access token. Error:', err);
|
||||
console.error("Could not obtain access token. Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,15 +33,15 @@ export function loadModel(viewer, urn, xform) {
|
||||
}
|
||||
|
||||
function showNotification(message) {
|
||||
const overlay = document.getElementById('overlay');
|
||||
const overlay = document.getElementById("overlay");
|
||||
overlay.innerHTML = `<div class="notification">${message}</div>`;
|
||||
overlay.style.display = 'flex';
|
||||
overlay.style.display = "flex";
|
||||
}
|
||||
|
||||
function clearNotification() {
|
||||
const overlay = document.getElementById('overlay');
|
||||
overlay.innerHTML = '';
|
||||
overlay.style.display = 'none';
|
||||
const overlay = document.getElementById("overlay");
|
||||
overlay.innerHTML = "";
|
||||
overlay.style.display = "none";
|
||||
}
|
||||
|
||||
export async function setupModelSelection(viewer) {
|
||||
@@ -52,16 +54,16 @@ export async function setupModelSelection(viewer) {
|
||||
const res = await $fetch(`/api/models/${utopiaUrn}/status`);
|
||||
const { status } = res;
|
||||
switch (status.status) {
|
||||
case 'n/a':
|
||||
case "n/a":
|
||||
showNotification(`Model has not been translated.`);
|
||||
break;
|
||||
case 'inprogress':
|
||||
case "inprogress":
|
||||
showNotification(`Model is being translated (${status.progress})...`);
|
||||
window.onModelSelectedTimeout = setTimeout(onModelSelected, 5000, viewer, utopiaUrn);
|
||||
break;
|
||||
case 'failed':
|
||||
case "failed":
|
||||
showNotification(
|
||||
`Translation failed. <ul>${status.messages.map((msg) => `<li>${JSON.stringify(msg)}</li>`).join('')}</ul>`,
|
||||
`Translation failed. <ul>${status.messages.map((msg) => `<li>${JSON.stringify(msg)}</li>`).join("")}</ul>`,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@@ -70,6 +72,6 @@ export async function setupModelSelection(viewer) {
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Could not load model. Error:', err);
|
||||
console.error("Could not load model. Error:", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import { blankUrn, utopiaUrn } from '@/components/viewer/utils/aps-init';
|
||||
import { isNotNil } from 'es-toolkit';
|
||||
import { blankUrn, utopiaUrn } from "@/components/viewer/utils/aps-init";
|
||||
import { isNotNil } from "es-toolkit";
|
||||
|
||||
function isTemInsideLand(landBounds, temBounds) {
|
||||
const tolerance = 0.007;
|
||||
return (
|
||||
landBounds.min.x - temBounds.min.x <= tolerance
|
||||
&& landBounds.min.y - temBounds.min.y <= tolerance
|
||||
&& landBounds.max.x - temBounds.max.x >= -tolerance
|
||||
&& landBounds.max.y - temBounds.max.y >= -tolerance
|
||||
landBounds.min.x - temBounds.min.x <= tolerance &&
|
||||
landBounds.min.y - temBounds.min.y <= tolerance &&
|
||||
landBounds.max.x - temBounds.max.x >= -tolerance &&
|
||||
landBounds.max.y - temBounds.max.y >= -tolerance
|
||||
);
|
||||
}
|
||||
|
||||
export function findTemInside(land, tems) {
|
||||
const temInside = tems.find(tem => {
|
||||
const temBounds = tem.bounds[0];
|
||||
const temInside = tems.find((tem) => {
|
||||
const temBounds = tem.bounds[0];
|
||||
|
||||
for (const landBounds of land.bounds) {
|
||||
if (isTemInsideLand(landBounds, temBounds)) return true;
|
||||
if (isTemInsideLand(landBounds, temBounds)) return true;
|
||||
}
|
||||
});
|
||||
|
||||
return temInside;
|
||||
}
|
||||
|
||||
export const getTradeCodeFromTem = (tem) => tem.properties.find(prop => prop.displayName === 'LO').displayValue;
|
||||
export const objIsTem = (obj) => obj.name && obj.name.startsWith('Blk003');
|
||||
export const getTradeCodeFromTem = (tem) => tem.properties.find((prop) => prop.displayName === "LO").displayValue;
|
||||
export const objIsTem = (obj) => obj.name && obj.name.startsWith("Blk003");
|
||||
export const objIsLand = (obj) => {
|
||||
const layerProp = obj.properties.find((prop) => prop.displayName === 'Layer');
|
||||
const layerProp = obj.properties.find((prop) => prop.displayName === "Layer");
|
||||
if (!layerProp) return false;
|
||||
const globalWidthProp = obj.properties.find((prop) => prop.displayName === 'Global width');
|
||||
const globalWidthProp = obj.properties.find((prop) => prop.displayName === "Global width");
|
||||
return (
|
||||
layerProp.displayValue === '1-bodim' ||
|
||||
layerProp.displayValue === '0' /* special case - Z.E02.02A */
|
||||
) && globalWidthProp.displayValue === 0;
|
||||
}
|
||||
(layerProp.displayValue === "1-bodim" || layerProp.displayValue === "0") /* special case - Z.E02.02A */ &&
|
||||
globalWidthProp.displayValue === 0
|
||||
);
|
||||
};
|
||||
|
||||
export function findLandByTradeCode(trade_code, lands, temsInside) {
|
||||
const foundTemIndex = temsInside.findIndex(tem => tem && getTradeCodeFromTem(tem) === trade_code);
|
||||
const foundTemIndex = temsInside.findIndex((tem) => tem && getTradeCodeFromTem(tem) === trade_code);
|
||||
return foundTemIndex >= 0 ? [lands[foundTemIndex], foundTemIndex] : undefined;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function pan(viewer) {
|
||||
const position = navigation.getPosition();
|
||||
const target = navigation.getTarget();
|
||||
|
||||
// offset both target and position to maintain angle
|
||||
// offset both target and position to maintain angle
|
||||
const panOffset = new THREE.Vector3(2, 0, 0);
|
||||
navigation.setPosition(position.clone().add(panOffset));
|
||||
navigation.setTarget(target.clone().add(panOffset));
|
||||
@@ -53,53 +53,53 @@ export function pan(viewer) {
|
||||
|
||||
export function unloadUnusedExtensions(viewer) {
|
||||
viewer.addEventListener(Autodesk.Viewing.EXTENSION_LOADED_EVENT, (e) => {
|
||||
if (
|
||||
['Autodesk.Measure', 'Autodesk.DocumentBrowser', 'Autodesk.DefaultTools.NavTools'].includes(e.extensionId)
|
||||
) {
|
||||
if (["Autodesk.Measure", "Autodesk.DocumentBrowser", "Autodesk.DefaultTools.NavTools"].includes(e.extensionId)) {
|
||||
viewer.unloadExtension(e.extensionId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function addTemSelectionListener(viewer, products, openProductViewModal, openNoPermissionModal) {
|
||||
viewer.addEventListener(Autodesk.Viewing.AGGREGATE_SELECTION_CHANGED_EVENT, (e) => {
|
||||
viewer.clearSelection();
|
||||
if (e.selections.length !== 1) return;
|
||||
const [selectedObj] = e.selections;
|
||||
const { model, dbIdArray } = selectedObj;
|
||||
if (dbIdArray.length !== 1) return;
|
||||
viewer.addEventListener(
|
||||
Autodesk.Viewing.AGGREGATE_SELECTION_CHANGED_EVENT,
|
||||
(e) => {
|
||||
viewer.clearSelection();
|
||||
if (e.selections.length !== 1) return;
|
||||
const [selectedObj] = e.selections;
|
||||
const { model, dbIdArray } = selectedObj;
|
||||
if (dbIdArray.length !== 1) return;
|
||||
|
||||
if (model.loader.svfUrn === utopiaUrn) {
|
||||
const [dbId] = dbIdArray;
|
||||
viewer.getProperties(dbId, (obj) => {
|
||||
if (!objIsTem(obj)) return;
|
||||
if (model.loader.svfUrn === utopiaUrn) {
|
||||
const [dbId] = dbIdArray;
|
||||
viewer.getProperties(dbId, (obj) => {
|
||||
if (!objIsTem(obj)) return;
|
||||
|
||||
const trade_code = getTradeCodeFromTem(obj);
|
||||
const product = products.find(p => p.trade_code === trade_code);
|
||||
const trade_code = getTradeCodeFromTem(obj);
|
||||
const product = products.find((p) => p.trade_code === trade_code);
|
||||
product ? openProductViewModal(product) : openNoPermissionModal();
|
||||
});
|
||||
} else if (model.loader.svf.isSceneBuilder) {
|
||||
const [dbId] = dbIdArray;
|
||||
const product = products.find((p) => p.id === dbId);
|
||||
product ? openProductViewModal(product) : openNoPermissionModal();
|
||||
});
|
||||
} else if (model.loader.svf.isSceneBuilder) {
|
||||
const [dbId] = dbIdArray;
|
||||
const product = products.find(p => p.id === dbId);
|
||||
product ? openProductViewModal(product) : openNoPermissionModal();
|
||||
} else if (model.loader.svfUrn === blankUrn) {
|
||||
viewer.clearSelection(); // make unselectable
|
||||
}
|
||||
},
|
||||
(err) => console.error(err),
|
||||
} else if (model.loader.svfUrn === blankUrn) {
|
||||
viewer.clearSelection(); // make unselectable
|
||||
}
|
||||
},
|
||||
(err) => console.error(err),
|
||||
);
|
||||
}
|
||||
|
||||
export function getLayerNames() {
|
||||
const { viewer } = window;
|
||||
if (!viewer) return;
|
||||
if (!viewer.impl) return;
|
||||
|
||||
if (!viewer) return;
|
||||
if (!viewer.impl) return;
|
||||
|
||||
const layerNames = viewer.impl.layers.indexToLayer
|
||||
.filter(obj => isNotNil(obj)) // not counting root
|
||||
.filter(obj => obj.visible)
|
||||
.filter((obj) => isNotNil(obj)) // not counting root
|
||||
.filter((obj) => obj.visible)
|
||||
.map(({ layer }) => layer.name)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
return layerNames;
|
||||
}
|
||||
@@ -108,5 +108,5 @@ export function applyLayerSetting(layersetting, store) {
|
||||
const { viewer } = window;
|
||||
viewer.setLayerVisible(null, false); // first, hide everything
|
||||
viewer.setLayerVisible(layersetting?.detail || null, true); // show specific ones, or all if there's no setting
|
||||
store.commit('layersetting', layersetting);
|
||||
store.commit("layersetting", layersetting);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export async function renderSvg(relativePath) {
|
||||
const svgRes = await fetch(relativePath);
|
||||
const svgText = await svgRes.text();
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(svgText, 'image/svg+xml');
|
||||
const svgDoc = parser.parseFromString(svgText, "image/svg+xml");
|
||||
const svgElement = svgDoc.documentElement;
|
||||
|
||||
return svgElement;
|
||||
@@ -12,21 +12,21 @@ export function html(tag, props = {}, children = []) {
|
||||
const element = document.createElement(tag);
|
||||
|
||||
Object.entries(props).forEach(([key, value]) => {
|
||||
if (key === 'textContent' || key === 'innerHTML') {
|
||||
if (key === "textContent" || key === "innerHTML") {
|
||||
element[key] = value;
|
||||
} else if (key === 'style' && typeof value === 'object') {
|
||||
} else if (key === "style" && typeof value === "object") {
|
||||
Object.assign(element.style, value);
|
||||
} else if (key === 'class') {
|
||||
} else if (key === "class") {
|
||||
element.className = value;
|
||||
} else if (key.startsWith('on')) {
|
||||
} else if (key.startsWith("on")) {
|
||||
element.addEventListener(key.slice(2).toLowerCase(), value);
|
||||
} else {
|
||||
element.setAttribute(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
children.flat().forEach(child => {
|
||||
if (typeof child === 'string') {
|
||||
children.flat().forEach((child) => {
|
||||
if (typeof child === "string") {
|
||||
element.appendChild(document.createTextNode(child));
|
||||
} else if (child instanceof Node) {
|
||||
element.appendChild(child);
|
||||
|
||||
@@ -10,22 +10,22 @@ export class GeometryCallback {
|
||||
let pt2 = new THREE.Vector3().set(x2, y2, 0).applyMatrix4(this.vpXform);
|
||||
|
||||
this.lines.push({
|
||||
x1: pt1.x,
|
||||
x1: pt1.x,
|
||||
y1: pt1.y,
|
||||
x2: pt2.x,
|
||||
y2: pt2.y
|
||||
x2: pt2.x,
|
||||
y2: pt2.y,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function getBounds(dbId, frags) {
|
||||
let fragIds = frags.fragments.dbId2fragId[dbId];
|
||||
if (typeof fragIds === 'number') {
|
||||
let fragIds = frags.fragments.dbId2fragId[dbId];
|
||||
if (typeof fragIds === "number") {
|
||||
fragIds = [fragIds];
|
||||
};
|
||||
}
|
||||
|
||||
const bounds = fragIds.map(fId => {
|
||||
const bound = new THREE.Box3();
|
||||
const bounds = fragIds.map((fId) => {
|
||||
const bound = new THREE.Box3();
|
||||
const boundsCallback = new Autodesk.Viewing.Private.BoundsCallback(bound);
|
||||
const mesh = frags.getVizmesh(fId);
|
||||
const vbr = new Autodesk.Viewing.Private.VertexBufferReader(mesh.geometry, viewer.impl.use2dInstancing);
|
||||
@@ -33,7 +33,7 @@ export function getBounds(dbId, frags) {
|
||||
return bound;
|
||||
});
|
||||
|
||||
return bounds;
|
||||
return bounds;
|
||||
}
|
||||
|
||||
export function extractPoints(lines) {
|
||||
@@ -50,8 +50,10 @@ export function extractPoints(lines) {
|
||||
|
||||
for (let i = 0; i < allPoints.length; i++) {
|
||||
const element = allPoints[i];
|
||||
|
||||
const notFound = !pointsDeduped.find(pt => Math.abs(pt.x - element.x) < tolerance && Math.abs(pt.y - element.y) < tolerance);
|
||||
|
||||
const notFound = !pointsDeduped.find(
|
||||
(pt) => Math.abs(pt.x - element.x) < tolerance && Math.abs(pt.y - element.y) < tolerance,
|
||||
);
|
||||
if (notFound) pointsDeduped.push(element);
|
||||
}
|
||||
|
||||
@@ -61,9 +63,9 @@ export function extractPoints(lines) {
|
||||
export function sortByAngle(points) {
|
||||
// Calculate centroid
|
||||
const centroid = new THREE.Vector3();
|
||||
points.forEach(v => centroid.add(v));
|
||||
points.forEach((v) => centroid.add(v));
|
||||
centroid.divideScalar(points.length);
|
||||
|
||||
|
||||
// Sort by angle around centroid
|
||||
return points.slice().sort((a, b) => {
|
||||
const angleA = Math.atan2(a.y - centroid.y, a.x - centroid.x);
|
||||
@@ -73,23 +75,15 @@ export function sortByAngle(points) {
|
||||
}
|
||||
|
||||
export function getColorByIndex(i) {
|
||||
const colors = [
|
||||
'magenta',
|
||||
'red',
|
||||
'orange',
|
||||
'yellow',
|
||||
'chartreuse',
|
||||
'green',
|
||||
'blue',
|
||||
]
|
||||
if (i === 0) return 'black';
|
||||
const colors = ["magenta", "red", "orange", "yellow", "chartreuse", "green", "blue"];
|
||||
if (i === 0) return "black";
|
||||
return colors[i % 7];
|
||||
}
|
||||
|
||||
/**
|
||||
* - THREE.Color: `{ r: number, g: number, b: number }`
|
||||
* - THREE.Vector4: `{ x: number, y: number, z: number, w: number }`
|
||||
*
|
||||
*
|
||||
* @param {string} colorStr e.g. `'#ff0000'`, `'white'`, `'hotpink'`
|
||||
* @returns A Vector4 object
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user