重構儀表板與熱圖功能 | 新增水管數據處理與即時溫度更新邏輯 | 調整電表與冷藏設備的數據傳遞與顯示

This commit is contained in:
koko 2025-09-05 16:09:17 +08:00
parent babef0e2ae
commit be4ff70043
6 changed files with 237 additions and 471 deletions

View File

@ -10,37 +10,40 @@ import {
provide, provide,
inject, inject,
} from "vue"; } from "vue";
import { getUrn, getAccessToken } from "@/apis/forge";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import useSystemStatusByBaja from "@/hooks/baja/useSystemStatusByBaja"; import useSystemStatusByBaja from "@/hooks/baja/useSystemStatusByBaja";
import useSystemHeatmap from "@/hooks/baja/useSystemHeatmap";
import ForgeInfoModal from "./ForgeInfoModal.vue"; import ForgeInfoModal from "./ForgeInfoModal.vue";
import useAlarmStore from "@/stores/useAlarmStore"; import useSearchParams from "@/hooks/useSearchParam";
const { searchParams } = useSearchParams();
const { updateTemp } = useSystemHeatmap();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL; const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const { forgeLock } = inject("app_toggle"); const { forgeLock } = inject("app_toggle");
const props = defineProps({ const props = defineProps({
fullScreen: Boolean, fullScreen: Boolean,
initialData: Object, initialData: Object,
pipeData: Array,
realTime: String, realTime: String,
meterList: Array,
heatmapDevices: Array,
}); });
const heat_bar_isShow = ref(false);
const updateHeatBarIsShow = (isShow) => {
heat_bar_isShow.value = isShow;
};
const { const {
subscribeData,
visibleDbid, visibleDbid,
updateDbidPosition, updateMeterPositions,
hideAllObjects, hideAllObjects,
updateForgeViewer, updateForgeViewer,
forgeViewer, forgeViewer,
urn, urn,
loadModel, loadModel,
updateInitialData, updateInitialData,
updatePipeData,
subComponents, subComponents,
clearSprites clearSprites,
} = useSystemStatusByBaja(updateHeatBarIsShow); } = useSystemStatusByBaja();
// meter label 2D
const meterPositions = ref(new Map());
watch( watch(
() => props.initialData, () => props.initialData,
@ -52,19 +55,44 @@ watch(
} }
); );
const store = useAlarmStore(); watch(
const subscribeDataWithErrorMsg = computed(() => { () => props.pipeData,
let data = { ...subscribeData.value }; (newValue, oldValue) => {
if (JSON.stringify(newValue) === JSON.stringify(oldValue)) return;
for (let [key, value] of Object.entries(subscribeData.value)) { newValue && updatePipeData(newValue);
const alarm = store.alarmData.find( },
({ device_number }) => device_number === key {
); deep: true,
data[key].alarmMsg = alarm ? alarm.msg : "";
} }
console.log("baja update data: ", data); );
return data;
}); watch(
() => props.heatmapDevices,
(newValue) => {
if (!newValue || newValue.length === 0) return;
newValue &&
updateTemp(newValue[0]?.forge_dbid, parseFloat(newValue[0]?.temperature));
},
{
deep: true,
}
);
// meter labels
const updateMeterPositionsLocal = () => {
if (!props.meterList) return;
const newPositions = updateMeterPositions(props.meterList);
meterPositions.value = newPositions;
};
// meterList
watch(
() => props.meterList,
() => {
updateMeterPositionsLocal();
},
{ deep: true }
);
const forgeDom = ref(null); const forgeDom = ref(null);
@ -107,17 +135,17 @@ const initForge = () => {
viewer.isLoadDone() viewer.isLoadDone()
); );
updateForgeViewer(viewer); updateForgeViewer(viewer);
// meter
updateMeterPositionsLocal();
} }
); );
// camera meter
viewer.addEventListener( viewer.addEventListener(
Autodesk.Viewing.CAMERA_CHANGE_EVENT, Autodesk.Viewing.CAMERA_CHANGE_EVENT,
function (e) { () => {
viewer.isLoadDone() && updateDbidPosition(this, subscribeData.value); console.log("CAMERA_CHANGE_EVENT");
console.log( updateMeterPositionsLocal();
"camera position changed: ",
NOP_VIEWER.navigation.getTarget(),
e.camera.position
);
} }
); );
}); });
@ -129,23 +157,6 @@ onMounted(() => {
initForge(); initForge();
}); });
//
const currentInfoModalData = ref(null);
const isMobile = (pointerType) => {
return pointerType !== "mouse"; // is desktop
};
const getCurrentInfoModalData = (e, position, value) => {
const mobile = isMobile(e.pointerType);
currentInfoModalData.value = {
initPos: mobile
? { left: `50%`, top: `50%` }
: { left: `${position.left}px`, top: `${position.top}px` },
value,
isMobile: mobile,
};
forge_info_modal.showModal();
};
watch([forgeViewer, forgeLock], ([newViewer, newLock]) => { watch([forgeViewer, forgeLock], ([newViewer, newLock]) => {
if (newViewer && newLock !== undefined) { if (newViewer && newLock !== undefined) {
newViewer.setNavigationLock(newLock); // newViewer.setNavigationLock(newLock); //
@ -166,7 +177,7 @@ onUnmounted(() => {
</script> </script>
<template> <template>
<ForgeInfoModal :data="currentInfoModalData" /> <!-- <ForgeInfoModal :data="currentInfoModalData" /> -->
<div <div
:class=" :class="
twMerge( twMerge(
@ -186,8 +197,11 @@ onUnmounted(() => {
) )
" "
> >
<p class="absolute z-10 top-14 left-[27%]">更新時間 : {{ props.realTime }}</p> <p class="absolute z-10 top-14 left-[27%]">
<div v-show="heat_bar_isShow" class="absolute z-10 heatbar"> 更新時間 : {{ props.realTime }}
{{ searchParams?.option == 4 ? " (溫度)" : "" }}
</p>
<div v-show="searchParams?.option == 4" class="absolute z-10 heatbar">
<div class="w-40 flex justify-between text-[10px] mb-1"> <div class="w-40 flex justify-between text-[10px] mb-1">
<span class="text-gradient-1">-20°C</span> <span class="text-gradient-1">-20°C</span>
<span class="text-gradient-2">0°C</span> <span class="text-gradient-2">0°C</span>
@ -207,38 +221,27 @@ onUnmounted(() => {
" "
></div> ></div>
</div> </div>
<template v-if="searchParams?.option == 5">
<!-- <label <label
v-for="(value, key) in subscribeDataWithErrorMsg" v-for="value in props.meterList"
:key="key" :key="value.main_id"
:data-dbid="value.forge_dbid" :data-dbid="value.forge_dbid"
:class=" v-show="meterPositions.get(value.main_id)?.visible !== false"
twMerge( :class="
`after:border-t-[${value.currentColor}]`, twMerge(
'flex items-center justify-center h-12 -translate-x-1/2 -translate-y-1/5 absolute z-50 px-5 py-4 text-center rounded-md text-lg border-2 border-white', 'flex items-center justify-center h-12 -translate-x-1/2 -translate-y-1/5 absolute z-50 px-5 py-4 text-center rounded-md text-lg border-2 border-white bg-green-600',
'after:absolute after:border-t-[10px] after:border-x-[12px] after:border-x-transparent after:-bottom-[8px] after:left-1/2 after:-translate-x-1/2 ', 'after:absolute after:border-t-[10px] after:border-x-[12px] after:border-x-transparent after:-bottom-[8px] after:left-1/2 after:-translate-x-1/2 ',
'before:absolute before:border-t-[12px] before:border-x-[14px] before:border-x-transparent before:-bottom-[12px] before:left-1/2 before:-translate-x-1/2 before:border-white' 'before:absolute before:border-t-[12px] before:border-x-[14px] before:border-x-transparent before:-bottom-[12px] before:left-1/2 before:-translate-x-1/2 before:border-white'
)
"
:style="{
left: `${Math.floor(value.device_coordinate_3d.x)}px`,
top: `${Math.floor(value.device_coordinate_3d.y) - 100}px`,
display: value.is_show,
backgroundColor: value.currentColor,
}"
@click.prevent="
(e) =>
getCurrentInfoModalData(
e,
{ left: e.clientX, top: e.clientY },
value
) )
" "
> :style="{
<span class="mr-2">{{ value.full_name }}</span> left: `${meterPositions.get(value.main_id)?.x || 0}px`,
<span v-if="value.alarmMsg">{{ value.alarmMsg }}</span> top: `${meterPositions.get(value.main_id)?.y || 0}px`,
<span v-else>{{ value.show_value }}</span> }"
</label> --> >
<span class="mr-2">{{ value.name }}</span>
</label>
</template>
</div> </div>
</div> </div>
</template> </template>

View File

@ -2,7 +2,7 @@
import { ref, watch, markRaw, onUnmounted } from "vue"; import { ref, watch, markRaw, onUnmounted } from "vue";
import useSearchParams from "@/hooks/useSearchParam"; import useSearchParams from "@/hooks/useSearchParam";
export default function useSystemHeatmap(updateHeatBarIsShow) { export default function useSystemHeatmap() {
const { searchParams } = useSearchParams(); const { searchParams } = useSearchParams();
// Viewer and DataVisualization Extension References // Viewer and DataVisualization Extension References
@ -25,12 +25,13 @@ export default function useSystemHeatmap(updateHeatBarIsShow) {
const updateHeatMapData = (deviceArr) => { const updateHeatMapData = (deviceArr) => {
deviceList.value = Object.values(deviceArr).map((d) => ({ deviceList.value = Object.values(deviceArr).map((d) => ({
...d, ...d,
id: d.device_number, // An ID to identify this device id: d.forge_dbid, // An ID to identify this device
roomDbId: d.room_dbid, roomDbId: d.room_dbid,
position: d.device_coordinate_3d, // World coordinates of this device position: JSON.parse(d.device_coordinate_3d),
sensorTypes: ["temperature"], // The types/properties this device exposes sensorTypes: ["temperature"], // The types/properties this device exposes
temp: 10, temp: 25,
dbId: d.forge_dbid, dbId: d.forge_dbid,
subSys: "freezer",
})); }));
}; };
@ -132,22 +133,10 @@ export default function useSystemHeatmap(updateHeatBarIsShow) {
// Watcher for Device List Changes // Watcher for Device List Changes
watch(deviceList, (newValue) => { watch(deviceList, (newValue) => {
console.log("熱圖更新", newValue); console.log("熱圖更新", newValue, searchParams.value.option);
switch (parseInt(searchParams.value.option)) { switch (parseInt(searchParams.value.option)) {
case 2: case 1:
createHeatMap("frozen"); createHeatMap("frozen");
updateHeatBarIsShow(true);
break;
case 3:
createHeatMap("gland");
updateHeatBarIsShow(true);
break;
case 4:
createHeatMap("packing");
break;
case 5:
createHeatMap("formula");
updateHeatBarIsShow(true);
break; break;
default: default:
if (isHeatMapSetup.value) { if (isHeatMapSetup.value) {
@ -159,7 +148,6 @@ export default function useSystemHeatmap(updateHeatBarIsShow) {
"跳過 removeSurfaceShading因為尚未 setupSurfaceShading" "跳過 removeSurfaceShading因為尚未 setupSurfaceShading"
); );
} }
updateHeatBarIsShow(false);
break; break;
} }
}); });

View File

@ -3,9 +3,12 @@ import { getDashboardDevice } from "@/apis/dashboard";
import useSearchParams from "@/hooks/useSearchParam"; import useSearchParams from "@/hooks/useSearchParam";
import useSystemHeatmap from "./useSystemHeatmap"; import useSystemHeatmap from "./useSystemHeatmap";
export default function useSystemStatusByBaja(updateHeatBarIsShow) { export default function useSystemStatusByBaja() {
const rawData = ref([]); const rawData = ref([]);
const forgeViewer = ref(null); const forgeViewer = ref(null);
const visibleDbid = ref([]);
const heatmapDevices = ref([]);
const urn = ref(""); const urn = ref("");
const lightColorMap = { const lightColorMap = {
1: "#009100", 1: "#009100",
@ -20,106 +23,56 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
let cameraEventAdded = false; // 追蹤是否已經添加了相機事件監聽器 let cameraEventAdded = false; // 追蹤是否已經添加了相機事件監聽器
const initialData = ref(null); const initialData = ref(null);
const pipeList = ref(null);
const subscribeData = ref({});
const updateInitialData = (data = false) => { const updateInitialData = (data = false) => {
initialData.value = data; initialData.value = data;
if (data && Array.isArray(data.dbid_list)) {
visibleDbid.value = data.dbid_list.map((d) => d.forge_dbid);
heatmapDevices.value = data.dbid_list.filter((d) => d.room_dbid !== null);
} else {
visibleDbid.value = [];
}
}; };
const { updateHeatMapData, updateTemp, initHeatMap } = function updatePipeData(data) {
useSystemHeatmap(updateHeatBarIsShow); const newDbids = data.map((item) => item.forge_dbid);
const oldDbids = pipeList.value
? pipeList.value.map((item) => item.forge_dbid)
: [];
const toHide = oldDbids.filter((dbid) => !newDbids.includes(dbid));
console.log("更新水管數據", forgeViewer.value, data);
if (forgeViewer.value) {
if (toHide.length > 0 && forgeViewer.value.hide)
forgeViewer.value.hide(toHide);
if (newDbids.length > 0 && forgeViewer.value.show)
forgeViewer.value.show(newDbids);
}
const updateForgeViewer = (viewer) => { pipeList.value = data;
clearSprites(); // 清理之前的 Sprites
// 如果有特定的 dbid 需要顯示動畫,則創建動畫
if (forgeViewer.value && pipeList.value.length > 0) {
pipeList.value.forEach(({ forge_dbid, reverse }) => {
if (reverse !== null) {
createSprites(forgeViewer.value, forge_dbid, reverse);
}
});
}
}
const { updateHeatMapData, initHeatMap } = useSystemHeatmap();
const updateForgeViewer = async (viewer) => {
if (!viewer) { if (!viewer) {
forgeViewer.value = null; forgeViewer.value = null;
return; return;
} }
forgeViewer.value = markRaw(viewer); forgeViewer.value = markRaw(viewer);
initHeatMap(viewer); await initHeatMap(viewer); // 等待初始化完成
}; updateHeatMapData(heatmapDevices.value);
updatePipeData(pipeList.value || []);
const getSubPoint = (normal, close, error, sub_points = []) => {
let points = {
...Object.fromEntries(sub_points.map((p) => [p, ""])),
};
if (normal) points[normal] = "";
if (close) points[close] = "";
if (error) points[error] = "";
return points;
};
const subscribeData = ref({});
watch(rawData, () => {
let sub_data = {};
rawData.value.forEach((d) => {
sub_data = {
...sub_data,
...Object.fromEntries(
(d.device || []).map((dev) => [
dev.device_number,
{
...dev,
labelText: d.labelText,
show_value: d.labelText,
device_normal_point_name: d.device_normal_point_name,
device_close_point_name: d.device_close_point_name,
device_error_point_name: d.device_error_point_name,
device_normal_point_value: d.device_normal_point_value,
device_close_point_value: d.device_close_point_value,
device_error_point_value: d.device_error_point_value,
device_normal_color: d.device_normal_color,
device_close_color: d.device_close_color,
device_error_color: d.device_error_color,
forge_dbid: parseInt(dev.forge_dbid),
device_coordinate_3d: dev.device_coordinate_3d
? JSON.parse(dev.device_coordinate_3d)
: { x: 0, y: 0 },
points: getSubPoint(
d.device_normal_point_name,
d.device_close_point_name,
d.device_error_point_name,
d.points || []
),
subSys: d.subSys,
is_show: true,
currentColor: d.device_normal_color,
},
])
),
};
});
subscribeData.value = sub_data;
updateHeatMapData(sub_data);
// updateSubscribeDataFromBaja(sub_data);
});
const visibleDbid = computed(() => {
let visible = [];
rawData.value.forEach((d) => {
visible = [
...visible,
...d.device.map((dev) => parseInt(dev.forge_dbid)),
];
});
// return visible;
return [
26, 32, 38, 43, 48, 53, 58, 63, 70, 76, 879, 1068, 1011, 1065, 855, 1109,
867, 963, 1044, 966, 1139, 936, 1136, 957, 1133, 954, 610, 1130, 951,
1041, 939, 1145, 987, 999, 1148, 1002, 1151, 981, 813, 1088, 825, 1091,
804,
];
});
const getDevice = async (option = 1) => {
const res = await getDashboardDevice({
option: parseInt(option),
});
rawData.value = res.data.map((d) => ({
...d,
key: d.subSys,
}));
}; };
// subscribe from baja // subscribe from baja
@ -130,238 +83,64 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
[point]: facets, [point]: facets,
}; };
}; };
const updateDeviceData = (device_number, point, value) => {
// 檢查 subscribeData.value[device_number] 是否存在
if (!subscribeData.value[device_number]) {
console.log(`Device ${device_number} is not initialized.`);
return;
}
const correspondPoint = initialData.value.points.find(
({ name }) => name === point
);
// console.log("sub 回傳值 ", typeof value)
const text = correspondPoint
? correspondPoint.values.find(
({ value: pValue }) => pValue === parseInt(value)
)?.text || ""
: value;
// console.log("baja update data sub", correspondPoint, device_number, point, value);
subscribeData.value[device_number].points[point] = text;
if (
point.toLowerCase() === "temp" &&
parseInt(searchParams.value.option) > 1
) {
updateTemp(device_number, value);
}
// 當點位是 "light",且目前顏色不是錯誤顏色時,從 lightColorMap 取顏色
if (
point.toLowerCase() === "light" &&
subscribeData.value[device_number].currentColor !==
subscribeData.value[device_number].device_error_color
) {
subscribeData.value[device_number].currentColor =
lightColorMap[Number(value)] ||
subscribeData.value[device_number].device_normal_color;
}
// 當點位是錯誤點時,判斷值是否等於錯誤值,決定是否使用錯誤顏色
if (point === subscribeData.value[device_number].device_error_point_name) {
subscribeData.value[device_number].currentColor =
value === subscribeData.value[device_number].device_error_point_value
? subscribeData.value[device_number].device_error_color
: subscribeData.value[device_number].device_normal_color;
}
updateLabelText(device_number, point, text);
};
const transformDeviceNumber = (device_number) => {
const transformed = device_number.replaceAll("_", "/");
// 找到最後一個 / 的位置,並截取到該位置之前
const lastSlashIndex = transformed.lastIndexOf("/");
return lastSlashIndex !== -1
? transformed.substring(0, lastSlashIndex)
: transformed;
};
const updateLabelText = (key, point, value) => {
let text = subscribeData.value[key].labelText.replace(`%${point}`, value);
Object.keys(subscribeData.value[key].points)
.filter((p) => p !== point)
.forEach((p) => {
text = text.replace(`%${p}`, subscribeData.value[key].points[p]);
});
subscribeData.value[key].show_value = text;
};
const subComponents = ref(null); const subComponents = ref(null);
const updateSubscribeDataFromBaja = (data) => {
for (let [key, value] of Object.entries(data)) {
window.require &&
window.requirejs(["baja!"], (baja) => {
console.log("進入 bajaSubscriber 準備執行BQL訂閱");
const ordKey = key; const updateMeterPositions = (meterList) => {
const ordPath = transformDeviceNumber(key); if (!forgeViewer.value || !meterList) return new Map();
console.log("ordPath", ordPath, "ordKey", ordKey, value);
const fullOrdPath = `local:|foxs:|station:|slot:/Drivers/NiagaraNetwork/PCCV/points/${ordPath}/${ordKey}`; // 完整路徑
console.log("嘗試訪問路徑:", fullOrdPath); // 打印完整路徑
baja.Ord.make(fullOrdPath) const viewer = forgeViewer.value;
.get() const tree = viewer.model?.getData()?.instanceTree;
.then((folder) => { const fragList = viewer.model?.getFragmentList();
console.log("成功獲取 folder:", folder);
const batch = new baja.comm.Batch();
const sub = new baja.Subscriber();
sub.attach({
changed: function (prop, cx) {
console.log("數據變更觸發:", prop.$getDisplayName());
if (prop.$getDisplayName() !== "Out") return;
if (
Object.hasOwn(
booleanPointFacets.value,
prop.$complex.$propInParent.$slotName
)
) {
const facets =
booleanPointFacets.value[
prop.$complex.$propInParent.$slotName
];
for (let [facetKey, facetValue] of Object.entries(facets)) { if (!tree || !fragList) return new Map();
if (facetValue === prop.$getValue().getValueDisplay()) {
updateDeviceData(
key,
prop.$complex.$propInParent.$slotName,
facetKey
);
}
}
} else {
updateDeviceData(
key,
prop.$complex.$propInParent.$slotName,
prop.$getValue().getValueDisplay()
);
}
},
});
console.log("開始遍歷控制點");
folder
.getSlots()
.is("control:ControlPoint")
.eachValue((point) => {
console.log("找到控制點:", point.getDisplayName());
console.log("配置的點位:", Object.keys(value.points));
if ( const newPositions = new Map();
Object.keys(value.points).includes(point.getDisplayName())
) {
console.log(
"匹配到點位,開始訂閱:",
point.getDisplayName()
);
baja.Ord.make(
`local:|foxs:|station:|slot:/Drivers/NiagaraNetwork/PCCV/points/${ordPath}/${ordKey}/${point.getDisplayName()}`
)
.get()
.then((component) => {
console.log("獲取到 component:", component);
if (
point.getType().getTypeSpec() ===
"control:BooleanWritable"
) {
const facets = component.getFacets1().toObject();
updateFacets(point.getDisplayName(), facets);
for (let [facetKey, facetValue] of Object.entries(
facets
)) {
if (
facetValue ===
component.getOut().getValue().toString()
) {
updateDeviceData(
key,
point.getDisplayName(),
facetKey
);
}
}
} else {
updateDeviceData(
key,
point.getDisplayName(),
component.getOut().getValue()
);
}
sub meterList.forEach((meter) => {
.subscribe({ if (meter.forge_dbid) {
comps: component, // Can also just be an singular Component instance try {
batch, // if defined, any network calls will be batched into this object (optional) const nodebBox = new window.THREE.Box3();
})
.then(() => { // 取得物件的 bounding box
console.log("subscribed successfully"); tree.enumNodeFragments(
subComponents.value = sub; meter.forge_dbid,
}) (fragId) => {
.catch(function (err) { const fragbBox = new window.THREE.Box3();
baja.error( fragList.getWorldBounds(fragId, fragbBox);
"some components failed to subscribe: " + err nodebBox.union(fragbBox);
); },
}); true
}); );
}
}); if (!nodebBox.isEmpty()) {
const center = nodebBox.getCenter(new window.THREE.Vector3());
const pos2d = viewer.worldToClient(center);
newPositions.set(meter.main_id, {
x: pos2d.x,
y: pos2d.y - 100, // 往上偏移 100px
visible: viewer.isNodeVisible(meter.forge_dbid)
}); });
}); }
} } catch (error) {
}; console.warn(`無法計算 dbid ${meter.forge_dbid} 的位置:`, error);
}
}
});
const updateDbidPosition = (viewer, data) => { return newPositions;
if (!viewer) return;
if (!forgeViewer.value) forgeViewer.value = markRaw(viewer);
const tree = viewer.model.getData().instanceTree;
const fragList = viewer.model.getFragmentList();
for (let [key, value] of Object.entries(data)) {
const nodebBox = new window.THREE.Box3();
// for each fragId on the list, get the bounding box
tree.enumNodeFragments(
value.forge_dbid,
(fragId) => {
const fragbBox = new window.THREE.Box3();
fragList.getWorldBounds(fragId, fragbBox);
nodebBox.union(fragbBox); // create a unifed bounding box
},
true
);
subscribeData.value[key].device_coordinate_3d = viewer.worldToClient(
nodebBox.getCenter()
);
subscribeData.value[key].is_show = viewer.isNodeVisible(value.forge_dbid);
}
}; };
const fitToView = () => { const fitToView = () => {
const { x, y, z } = JSON.parse(searchParams.value.camera_position); const { x, y, z } = JSON.parse(searchParams.value.camera_position);
const newPosition = new THREE.Vector3(x, y, z); //!<<< 相机的新位置 const newPosition = new THREE.Vector3(x, y, z);
const { const {
x: x1, x: x1,
y: y1, y: y1,
z: z1, z: z1,
} = JSON.parse(searchParams.value.target_position); //!<<< 计算新焦点位置 } = JSON.parse(searchParams.value.target_position);
const newTarget = new THREE.Vector3(x1, y1, z1); //!<<< 焦點的新位置 const newTarget = new THREE.Vector3(x1, y1, z1);
forgeViewer.value.navigation.setView(newPosition, newTarget);
forgeViewer.value.navigation.getCamera().setView({
position: newPosition.clone(),
target: newTarget.clone(),
});
setTimeout(() => {
updateDbidPosition(forgeViewer.value, subscribeData.value);
}, 700);
}; };
const hideAllObjects = (instanceTree, filDbids = []) => { const hideAllObjects = (instanceTree, filDbids = []) => {
@ -374,7 +153,6 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
if (filDbids.length > 0) { if (filDbids.length > 0) {
forgeViewer.value.show(filDbids); forgeViewer.value.show(filDbids);
} }
fitToView();
forgeViewer.value.impl.invalidate(true); forgeViewer.value.impl.invalidate(true);
}; };
@ -459,42 +237,21 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
watch([forgeViewer, visibleDbid], ([viewer, dbids]) => { watch([forgeViewer, visibleDbid], ([viewer, dbids]) => {
console.log("監聽到 forgeViewer 或 visibleDbid 的變化", viewer, dbids); console.log("監聽到 forgeViewer 或 visibleDbid 的變化", viewer, dbids);
stopBlinking();
clearSprites(); // 清理之前的 Sprites
if (viewer) { if (viewer) {
hideAllObjects(viewer.model.getData().instanceTree, dbids); hideAllObjects(viewer.model.getData().instanceTree, dbids);
} }
// 判斷新狀態是否需要閃爍和 Sprites
if (viewer && dbids.includes(879)) {
// startBlinking([879, 1068, 1011, 1065, 855, 1109, 867,963,1044,966,1139,936,1136,957,1133,954,610,1130,951,1041,939,1145,987,999,1148,1002,1151,981,813,1088,825,1091,804]);
const spriteConfigs = [
{ dbId: 879, reverse: false },
{ dbId: 1011, reverse: false },
{ dbId: 855, reverse: true },
{ dbId: 867, reverse: true },
{ dbId: 963, reverse: false },
{ dbId: 966, reverse: true },
{ dbId: 936, reverse: true },
{ dbId: 957, reverse: true },
{ dbId: 954, reverse: true },
{ dbId: 951, reverse: true },
{ dbId: 939, reverse: true },
{ dbId: 987, reverse: true },
{ dbId: 999, reverse: true },
{ dbId: 1002, reverse: true },
{ dbId: 981, reverse: true },
{ dbId: 813, reverse: true },
{ dbId: 825, reverse: false },
{ dbId: 804, reverse: false },
];
spriteConfigs.forEach(({ dbId, reverse }) => {
createSprites(viewer, dbId, reverse);
});
}
}); });
watch(
[forgeViewer, () => searchParams.value.camera_position],
([viewer, newValue]) => {
if (viewer && newValue) {
fitToView();
}
}
);
// 動態Sprites創建函數 // 動態Sprites創建函數
const createSprites = async (viewer, dbId, reverse = false) => { const createSprites = async (viewer, dbId, reverse = false) => {
try { try {
@ -612,15 +369,6 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
animEnd = tmp; animEnd = tmp;
} }
console.log(`dbId ${dbId} 動畫資訊:`, {
axis: axis,
size: size,
animStart: animStart,
animEnd: animEnd,
distance: animStart.distanceTo(animEnd),
reverse: reverse,
});
// 4. 建立 SpriteViewable初始在 animStart // 4. 建立 SpriteViewable初始在 animStart
const viewable = new DataVizCore.SpriteViewable( const viewable = new DataVizCore.SpriteViewable(
animStart.clone(), animStart.clone(),
@ -655,19 +403,6 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
arrow.style.transform = `rotate(${angle}deg)`; arrow.style.transform = `rotate(${angle}deg)`;
} }
// camera 移動時也要更新所有箭頭(只添加一次事件監聽器)
if (!cameraEventAdded) {
viewer.addEventListener(
Autodesk.Viewing.CAMERA_CHANGE_EVENT,
() => {
spriteAnimations.forEach((animation) => {
animation.updateArrow(animation.currentPos);
});
}
);
cameraEventAdded = true;
}
// 動畫主程式 // 動畫主程式
let t = 0; let t = 0;
let currentPos = animStart.clone(); let currentPos = animStart.clone();
@ -725,8 +460,6 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
// 移除這個 dbId 的記錄 // 移除這個 dbId 的記錄
spriteAnimations.delete(dbId); spriteAnimations.delete(dbId);
console.log(`已清理 dbId ${dbId} 的 Sprite 資源`);
} }
}; };
@ -751,9 +484,10 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
return { return {
subscribeData, subscribeData,
visibleDbid, visibleDbid,
updateDbidPosition, updateMeterPositions,
hideAllObjects, hideAllObjects,
updateForgeViewer, updateForgeViewer,
updatePipeData,
forgeViewer, forgeViewer,
loadModel, loadModel,
urn, urn,

View File

@ -16,14 +16,18 @@ import {
import { getAssetFloorList } from "@/apis/asset"; import { getAssetFloorList } from "@/apis/asset";
import { getOperationCompanyList } from "@/apis/operation"; import { getOperationCompanyList } from "@/apis/operation";
import useSearchParams from "@/hooks/useSearchParam"; import useSearchParams from "@/hooks/useSearchParam";
import useSystemHeatmap from "@/hooks/baja/useSystemHeatmap";
import dayjs from "dayjs"; import dayjs from "dayjs";
const initialData = ref(null); const initialData = ref(null);
const realTimeData = ref(null); const realTimeData = ref(null);
const pipeData = ref([]);
const realTime = ref(null); const realTime = ref(null);
// //
const meterList = ref([]); const meterList = ref([]);
const selectedMeter = ref(null); const selectedMeter = ref(null);
//
const heatmapDevices = ref([]);
// //
const floors = ref([]); const floors = ref([]);
// //
@ -48,10 +52,37 @@ const getDevice = async (option = 1) => {
}); });
realTimeData.value = res.data; realTimeData.value = res.data;
if (res.data?.meterData) { if (res.data?.meterData) {
meterList.value = (res.data?.meterData || []).sort(); // meterList
let meters = (res.data?.meterData || []).sort();
// dbid_list
const dbidList = initialData.value?.dbid_list || [];
// meter forge_dbid
meters = meters.map((meter) => {
const dbidItem = dbidList.find(
(d) => d.forge_dbid === meter.forge_dbid
);
return {
...meter,
device_coordinate_3d: JSON.parse(dbidItem?.device_coordinate_3d) || null,
};
});
meterList.value = meters;
// selectedMeter meterList
if (
selectedMeter.value == null &&
meterList.value[0]?.main_id !== undefined
) {
selectedMeter.value = meterList.value[0].main_id;
}
} else if (res.data?.refrigerationData) {
heatmapDevices.value = res.data.refrigerationData;
} else { } else {
meterList.value = []; meterList.value = [];
heatmapDevices.value = [];
} }
//
pipeData.value = res.data?.pipeData || [];
//
realTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss"); realTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss");
console.log("實時數據:", realTimeData.value); console.log("實時數據:", realTimeData.value);
} catch (err) { } catch (err) {
@ -82,7 +113,7 @@ const startInterval = (option) => {
// 5 // 5
intervalId = setInterval(() => { intervalId = setInterval(() => {
getDevice(option); getDevice(option);
}, 5000); }, 10 * 1000);
}; };
// //
@ -128,7 +159,14 @@ onUnmounted(() => {
<DashboardAlert /> <DashboardAlert />
</div> </div>
</div> </div>
<Forge :fullScreen="true" :initialData="initialData" :realTime="realTime" /> <Forge
:fullScreen="true"
:initialData="initialData"
:pipeData="pipeData"
:realTime="realTime"
:meterList="meterList"
:heatmapDevices="heatmapDevices"
/>
<DashboardForgeOptionButton <DashboardForgeOptionButton
:initialData="initialData" :initialData="initialData"
:meterList="meterList" :meterList="meterList"

View File

@ -45,6 +45,7 @@ watch(
changeParams({ changeParams({
option: option.option, option: option.option,
}); });
} }
" "
tabindex="0" tabindex="0"
@ -59,7 +60,9 @@ watch(
href="#" href="#"
:style="{ :style="{
background: background:
props.selectedMeter === meter.main_id ? '#2563eb' : 'transparent', props.selectedMeter === meter.main_id
? '#2563eb'
: 'transparent',
}" }"
@click.prevent=" @click.prevent="
() => { () => {

View File

@ -15,7 +15,7 @@ import ValveCard from "./dashboardForgeCards/ValveCard.vue";
const { searchParams, changeParams } = useSearchParams(); const { searchParams, changeParams } = useSearchParams();
const props = defineProps({ const props = defineProps({
realTimeData: Object, realTimeData: Object,
selectedMeter: String, selectedMeter: [String, Number, null],
floors: Array, floors: Array,
companyOptions: Array, companyOptions: Array,
}); });