Files
web/app/components/viewer/Viewer.vue
2026-03-02 09:45:33 +07:00

1015 lines
32 KiB
Vue

<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';
const { $exportpdf, $getdata, $connectWebSocket, $snackbar, $store, $subscribe } = useNuxtApp();
const { dealer, layersetting } = $store;
const viewerId = 'viewer';
const viewerRef = useTemplateRef(viewerId);
const products = ref([]);
const productStatuses = ref([]);
const saleStatuses = ref([]);
const carts = ref([]);
const dealers = ref([]);
const selectedProductForNote = ref(null);
const statusModes = {
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 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
});
function addMeshes(viewer) {
viewer.toolController.setMouseWheelInputEnabled(false); // disable zoom wheel until colors are added
if (window.colorModel) {
viewer.unloadModel(window.colorModel);
}
viewer.getObjectTree(async (instanceTree) => {
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);
const vpId = 1;
const vpXform = viewer.model.getPageToModelTransform(vpId);
const inverseXform = new THREE.Matrix4();
const pt = new THREE.Vector3();
const material = new THREE.MeshBasicMaterial({
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();
// 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);
viewerPoints.push(new THREE.Vector2(pt.x, pt.y));
}
let sortedPoints = sortByAngle(viewerPoints);
if (productId === 9290) {
// special case - Z.E02.02A, fix manually
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');
// only set Z after calling ShapeGeometry()
for (let i = 0; i < pos.count; i++) {
pos.setZ(i, -1);
}
pos.needsUpdate = true;
const mesh = new THREE.Mesh(bufferGeo, material);
mesh.dbId = productId; // to be able to find product from an added mesh (in applyColors)
return mesh;
}
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 = [];
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;
}
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');
}
});
});
}
/**
* Show/hide certain colors based on filters, using `viewer.setThemingColor`,
* not modifying the materials of the added meshes.
*/
function applyColors() {
const { viewer, colorModel } = window;
if (!viewer || !colorModel) return;
const { statusIds, cartIds, dealerIds } = filters.value;
const white = colorStrToVector4('white');
colorModel.getObjectTree((instanceTree) => {
const dbIdMap = instanceTree.nodeAccess.dbIdToIndex;
const allDbIds = Object.keys(dbIdMap).map((id) => parseInt(id));
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 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 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 showColor = statusCondition && !notSoldDealerCondition && cartCondition && dealerCondition;
viewer.setThemingColor(dbId, showColor ? statusColor : white, colorModel);
});
}
viewer.impl.invalidate(true, true, true);
});
}
const debouncedApplyColors = debounce(applyColors, 50);
const showProductViewModal = ref();
function closeProductViewModal() {
showProductViewModal.value = undefined;
}
function openProductViewModal(product) {
selectedProductForNote.value = product;
showProductViewModal.value = {
component: "product/ProductEdit",
title: "Thông tin chi tiết",
vbind: {
row: product,
view: true
},
width: "80%",
height: "700px",
};
}
function openNoPermissionModal() {
showProductViewModal.value = {
component: "application/NoPermission",
title: "Không có quyền truy cập",
width: "35%",
height: "auto",
};
}
const legendsSFC = ref(null);
const isRefreshing = ref(false);
function mountLegends() {
if (legendsSFC.value) legendsSFC.value.unmount();
const content = document.querySelector('.content.docking-panel-scroll');
legendsSFC.value = createApp({
name: 'Legends',
render() {
return h(Legends, {
products,
carts,
dealers,
defaultCartIds,
defaultDealerIds,
filters,
statuses,
statusModes,
statusMode,
onSwitchStatusMode: (newMode => {
statusMode.value = newMode;
}),
onUpdateFilters: (payload => {
filters.value = {
...filters.value,
...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);
return content;
}
async function refreshFromWebSocket() {
if (isRefreshing.value) return;
isRefreshing.value = true;
try {
await loadData(false);
debouncedApplyColors();
} finally {
isRefreshing.value = false;
}
}
function initViewer(container) {
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, '');
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 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');
}
const content = html('div', { class: 'content docking-panel-scroll' });
this.container.append(content);
mountLegends();
}
}
class NoteHistoryPanel extends Autodesk.Viewing.UI.DockingPanel {
constructor(container, id, title, options) {
super(container, id, title, options);
this.viewer = viewer;
this.app = null;
this.isVisibleState = false;
Autodesk.Viewing.UI.DockingPanel.call(this, container, id, '');
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 panelCloseBtn = this.container.querySelector('.docking-panel-close');
panelCloseBtn.onclick = () => {
this.toggleVisibility();
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' });
this.container.append(content);
this.contentElement = content;
this.mountVueComponent(content);
}
toggleVisibility() {
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');
} else {
this.show();
noteHistoryBtnElem?.classList.add('active');
noteHistoryBtnElem?.classList.remove('inactive');
}
}
show() {
console.log('[NoteHistoryPanel] show() called');
this.isVisibleState = true;
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.setVisible(true);
// Force re-render Vue component nếu cần
if (this.app) {
this.$forceUpdate?.();
}
}
hide() {
console.log('[NoteHistoryPanel] hide() called');
this.isVisibleState = false;
this.container.style.display = 'none';
this.setVisible(false);
}
mountVueComponent(element) {
if (this.app) {
this.app.unmount();
}
this.app = createApp({
name: 'ProductNoteHistory',
render: () => h(ProductNoteHistory, {
selectedProduct: selectedProductForNote.value
})
});
this.app.mount(element);
}
// Thêm method để refresh Vue component khi cần
refreshComponent() {
if (this.contentElement) {
this.mountVueComponent(this.contentElement);
}
}
}
class Print extends Autodesk.Viewing.Extension {
load() {
if (this.viewer.toolbar) {
this.createUI();
} else {
this.onToolbarCreatedBinded = this.onToolbarCreated.bind(this);
this.viewer.addEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
}
return true;
}
unload() {
console.info('Print extension is now unloaded!');
return true;
}
createUI() {
const viewer = this.viewer;
let toolbarGroup = viewer.toolbar.getControl(customToolbarId);
if (!toolbarGroup) {
toolbarGroup = new Autodesk.Viewing.UI.ControlGroup(customToolbarId);
viewer.toolbar.addControl(toolbarGroup);
}
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) });
printBtn.onClick = () => this.print(viewer);
printBtn.setToolTip(printId);
this.subToolbar.addControl(printBtn);
}
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%' })
]);
document.body.append(div);
const fileName = docId;
$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 DPI = 150;
const scale = 3;
const widthInPixels = Math.floor(pageWidth * DPI * scale);
const canvasRatio = cvHeight / cvWidth;
const width = widthInPixels;
const height = Math.floor(widthInPixels * canvasRatio);
const screenshotOptions = { fullPage: false, margin: 0 };
Autodesk.Viewing.ScreenShot.getScreenShotWithBounds(window.viewer, width, height, getPdf, screenshotOptions);
}
onToolbarCreated() {
this.viewer.removeEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
this.onToolbarCreatedBinded = null;
this.createUI();
};
}
class Legend extends Autodesk.Viewing.Extension {
load() {
if (this.viewer.toolbar) {
this.createUI();
} else {
this.onToolbarCreatedBinded = this.onToolbarCreated.bind(this);
this.viewer.addEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
}
return true;
}
unload() {
console.info('Legend extension is now unloaded!');
return true;
}
createUI() {
const viewer = this.viewer;
let panel = this.panel;
if (!panel) {
// auto open panel on first load
panel = new LegendPanel(viewer.container, legendId, legendId);
viewer.addPanel(panel);
panel.setVisible(!panel.isVisible());
}
let toolbarGroup = viewer.toolbar.getControl(customToolbarId);
if (!toolbarGroup) {
toolbarGroup = new Autodesk.Viewing.UI.ControlGroup(customToolbarId);
viewer.toolbar.addControl(toolbarGroup);
}
this.subToolbar = toolbarGroup;
const legendBtn = new Autodesk.Viewing.UI.Button(`${customToolbarId}-${legendId}Tool`);
const legendBtnElem = legendBtn.container;
const adskButtonIcon = legendBtnElem.children.item(0);
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');
});
legendBtn.onClick = () => {
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);
}
onToolbarCreated(e) {
this.viewer.removeEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
this.onToolbarCreatedBinded = null;
if (e.type === 'toolbarCreated') {
this.createUI();
}
};
}
class NoteHistory extends Autodesk.Viewing.Extension {
constructor(viewer, options) {
super(viewer, options);
this.panel = null;
this.button = null;
}
load() {
if (this.viewer.toolbar) {
this.createUI();
} else {
this.onToolbarCreatedBinded = this.onToolbarCreated.bind(this);
this.viewer.addEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
}
return true;
}
unload() {
if (this.panel) {
this.panel.uninitialize();
this.panel = null;
}
if (this.button) {
const toolbarGroup = this.viewer.toolbar.getControl(customToolbarId);
toolbarGroup.removeControl(this.button);
this.button = null;
}
return true;
}
createUI() {
const viewer = this.viewer;
let panel = this.panel;
if (!panel) {
panel = new NoteHistoryPanel(viewer.container, noteHistoryId, 'Note History');
viewer.addPanel(panel);
this.panel = panel;
}
let toolbarGroup = viewer.toolbar.getControl(customToolbarId);
if (!toolbarGroup) {
toolbarGroup = new Autodesk.Viewing.UI.ControlGroup(customToolbarId);
viewer.addControl(toolbarGroup);
}
this.button = new Autodesk.Viewing.UI.Button(`${customToolbarId}-${noteHistoryId}Tool`);
this.button.setToolTip('Lịch sử ghi chú');
const adskButtonIcon = this.button.container.children.item(0);
if (!selectedProductForNote.value) {
adskButtonIcon.style.filter = 'grayscale(100%)';
}
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');
};
toolbarGroup.addControl(this.button);
}
onToolbarCreated() {
this.viewer.removeEventListener(Autodesk.Viewing.TOOLBAR_CREATED_EVENT, this.onToolbarCreatedBinded);
this.onToolbarCreatedBinded = null;
this.createUI();
}
}
return new Promise((resolve, _reject) => {
Autodesk.Viewing.theExtensionManager.registerExtension(printId, Print);
Autodesk.Viewing.theExtensionManager.registerExtension(legendId, Legend);
Autodesk.Viewing.theExtensionManager.registerExtension(noteHistoryId, NoteHistory);
Autodesk.Viewing.Initializer({ env: 'AutodeskProduction', getAccessToken }, () => {
const config = {
extensions: [printId, legendId, noteHistoryId, 'Autodesk.ModelStructure', 'Autodesk.Viewing.SceneBuilder'],
};
const viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
viewer.start();
viewer.setTheme('light-theme');
viewer.setProgressiveRendering(false);
viewer.setReverseZoomDirection(true);
viewer.toolController.setMouseWheelInputEnabled(false);
const zoomOutFactor = 0.3; // larger means zoom out more - https://stackoverflow.com/a/67918257
viewer.navigation.FIT_TO_VIEW_VERTICAL_MARGIN = zoomOutFactor;
viewer.navigation.FIT_TO_VIEW_HORIZONTAL_MARGIN = zoomOutFactor;
// disable Autodesk's "fit everything in view" mouse & keyboard shortcuts
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);
viewer.addEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, function onceLoaded(e) {
const viewer = e.target;
viewer.removeEventListener(Autodesk.Viewing.GEOMETRY_LOADED_EVENT, onceLoaded);
applyLayerSetting(layersetting, $store);
addMeshes(viewer);
});
function resetCameraIfZoomError() {
const { position, target } = viewer.navigation.getCamera();
const threshold = 500;
const pxErr = position.x > threshold || position.x < 0;
const pyErr = position.y > threshold || position.y < 0;
const pzErr = position.z > threshold;
const txErr = target.x > threshold || target.x < 0;
const tyErr = target.y > threshold || target.y < 0;
const tzErr = target.z > threshold;
const zoomError = pxErr || pyErr || pzErr || txErr || tyErr || tzErr;
if (zoomError) {
viewer.navigation.setRequestHomeView(true);
/*
// Debug log
console.log('Reset because zoom error:');
if (pxErr) {
console.log('px', position.x)
} else if (pyErr) {
console.log('py', position.y)
} else if (pzErr) {
console.log('pz', position.z)
} else if (txErr) {
console.log('tx', target.z)
} else if (tyErr) {
console.log('ty', target.z)
} else if (tzErr) {
console.log('tz', target.z)
} */
}
}
viewer.addEventListener(Autodesk.Viewing.CAMERA_CHANGE_EVENT, throttle(resetCameraIfZoomError , 300));
addTemSelectionListener(viewer, products.value, openProductViewModal, openNoPermissionModal);
unloadUnusedExtensions(viewer);
resolve(viewer);
});
});
}
const componentActivated = ref(true);
const isViewerLoading = ref(false);
async function loadViewer() {
if (!viewerRef.value) return;
isViewerLoading.value = true;
const scale = 0.25;
try {
const viewer = await initViewer(viewerRef.value);
window.viewer = viewer;
const transformMatrix = new THREE.Matrix4().scale(new THREE.Vector3(scale, scale, scale));
const mainModel = await loadModel(viewer, utopiaUrn);
const blankModel = await loadModel(viewer, blankUrn, transformMatrix);
isViewerLoading.value = false;
window.mainModel = mainModel;
window.blankModel = blankModel;
blankModel.getObjectTree((instanceTree) => {
const dbIdMap = instanceTree.nodeAccess.dbIdToIndex;
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);
}
}
function mountLayerSetting() {
const observer = new MutationObserver((mutations, observer) => {
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' });
addedNode.append(layerSettingSFCContainer);
const layerSettingSFC = createApp({
name: 'LayerSetting',
render() {
return h(LayerSetting);
}
});
layerSettingSFC.mount(layerSettingSFCContainer);
observer.disconnect();
}
}
});
});
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('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" }
},
summary: "annotate",
sort: "index"
}),
$getdata('dealer'),
]);
products.value = productsData;
productStatuses.value = productStatusesData;
saleStatuses.value = saleStatusesData;
carts.value = cartsData;
dealers.value = dealersData;
if (first) {
filters.value = {
statusIds: defaultStatusIds.value,
cartIds: defaultCartIds.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 for product selection to enable/disable the note history button
watch(selectedProductForNote, (newProduct) => {
const noteHistoryBtn = document.getElementById('customToolbar-noteHistoryTool');
if (noteHistoryBtn) {
const icon = noteHistoryBtn.querySelector('.adsk-button-icon');
if (newProduct) {
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 (ext?.panel) {
ext.panel.setVisible(false);
}
noteHistoryBtn.classList.remove('active');
noteHistoryBtn.classList.add('inactive');
}
}
});
function handleNavigateToProduct(event) {
const { productId } = event.detail;
if (!window.viewer || !window.meshes || !productId) return;
const mesh = window.meshes.find(m => m.dbId === productId);
if (mesh) {
const boundingBox = new THREE.Box3().setFromObject(mesh);
window.viewer.navigation.fitBounds(false, boundingBox);
} else {
console.warn(`Product mesh with ID ${productId} not found.`);
}
}
async function handleWsMessage({ detail: response }) {
if (response.type === 'realtime_update') {
const { name, change_type, record } = response.payload;
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)
$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 viewerSDKPattern = `script[src^="${viewerSDKSharedPath}"],link[href^="${viewerSDKSharedPath}"]`;
async function initialize() {
await loadData();
mountLayerSetting();
// Connect to WebSocket
$connectWebSocket();
if (dealer?.id) {
$subscribe('product', { cart__dealer__code: dealer?.code }, (initialData) => {
if (initialData && initialData.rows) {
products.value = initialData.rows;
}
});
} else {
$subscribe('product', undefined, (initialData) => {
if (initialData && initialData.rows) {
products.value = initialData.rows;
}
});
}
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' });
document.head.append(viewerScript, viewerStyle);
}
}
function cleanup() {
const viewerSDKs = document.querySelectorAll(viewerSDKPattern);
if (viewerSDKs.length > 0) {
viewer.finish();
viewer = null;
Autodesk.Viewing.shutdown();
Autodesk = undefined;
viewerSDKs.forEach(script => script.remove());
isViewerLoading.value = false;
componentActivated.value = false;
window.removeEventListener('navigateToProduct', handleNavigateToProduct);
window.removeEventListener('ws_message', handleWsMessage);
}
}
onMounted(cleanup);
onUnmounted(cleanup);
onActivated(() => {
const viewerSDKs = document.querySelectorAll(viewerSDKPattern);
if (viewerSDKs.length === 0) {
componentActivated.value = true;
initialize();
}
});
onDeactivated(() => {
if (isViewerLoading.value) cleanup();
});
</script>
<template>
<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" />
</div>
</template>
<style>
#ViewerMaster {
--top: 94px;
position: absolute;
width: 100%;
top: var(--top);
left: 0;
}
.adsk-viewing-viewer {
font-family: var(--bulma-body-family) !important;
.docking-panel-footer-resizer {
cursor: nwse-resize !important;
}
}
#viewer,
#overlay {
position: absolute;
width: 100%;
left: 0;
height: calc(100vh - var(--top));
}
#modelTools,
#toolbar-fullscreenTool {
display: none;
}
.viewcube {
display: none !important;
}
.homeViewWrapper {
position: absolute;
right: 12px;
top: 10px;
margin: 0;
}
#customToolbar {
.adsk-button-icon {
padding-top: 0;
svg {
width: 28px;
height: 28px;
}
}
.adsk-button.active svg {
color: #00bfff; /* from Autodesk styles */
}
}
#overlay {
z-index: 1;
background-color: rgba(0, 0, 0, 0.5);
padding: 1em;
display: none;
>.notification {
margin: auto;
padding: 1em;
max-width: 50%;
background: white;
}
}
</style>