2d圖預設與顯示 | 系統監控 : 根據realtime.value即時顯示狀態

This commit is contained in:
koko 2025-07-28 11:53:18 +08:00
parent e6939183fe
commit a7ed0340b7
7 changed files with 137 additions and 61 deletions

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import * as echarts from "echarts"; import * as echarts from "echarts";
import { onMounted, ref, markRaw } from "vue"; import { onMounted, ref, markRaw, nextTick } from "vue";
import axios from "axios"; import axios from "axios";
const props = defineProps({ const props = defineProps({
@ -24,8 +24,9 @@ async function updateSvg(svg, option) {
} else { } else {
clear(); clear();
} }
axios.get(svg.path).then(({ data }) => { axios.get(svg.path).then(async ({ data }) => {
echarts.registerMap(svg.full_name, { svg: data }); echarts.registerMap(svg.full_name, { svg: data });
await nextTick();
chart.value.setOption(option); chart.value.setOption(option);
if (props.getCoordinate) { if (props.getCoordinate) {
chart.value.getZr().on("click", function (params) { chart.value.getZr().on("click", function (params) {

View File

@ -6,7 +6,7 @@ import useForgeHeatmap from "./useForgeHeatmap";
import useForgeFloor from "./useForgeFloor"; import useForgeFloor from "./useForgeFloor";
export default function useForgeSprite() { export default function useForgeSprite() {
const { subscribeData } = inject("system_deviceList"); const { subscribeData, realtimeData } = inject("system_deviceList");
const { getCurrentInfoModalData, clearSelectedDeviceInfo, selected_dbid } = const { getCurrentInfoModalData, clearSelectedDeviceInfo, selected_dbid } =
inject("system_selectedDevice"); inject("system_selectedDevice");
const forgeViewer = ref(null); const forgeViewer = ref(null);
@ -25,7 +25,7 @@ export default function useForgeSprite() {
forgeViewer.value.navigation.setView(newPosition, newTarget); forgeViewer.value.navigation.setView(newPosition, newTarget);
// 確保 Home 視角 // 確保 Home 視角
forgeViewer.value.autocam.setCurrentViewAsHome(true); forgeViewer.value.autocam.setCurrentViewAsHome(true);
}; };
const updateDataVisualization = async (viewer) => { const updateDataVisualization = async (viewer) => {
@ -65,6 +65,23 @@ export default function useForgeSprite() {
const { flatSubData } = useSystemShowData(); const { flatSubData } = useSystemShowData();
// 根據設備取得即時狀態顏色
const getDeviceRealtimeColor = (d) => {
if (d.full_name === "SmartSocket-AA001") return "#ff0000";
if (
d.full_name === "SmartSocket-AA003" ||
d.full_name === "SmartSocket-AA004"
)
return "#888888";
const realtimeDevice = realtimeData?.value?.find(
(item) => item.device_number === d.device_number
);
const state = realtimeDevice?.state || "";
if (state === "offnormal" || state === "")
return d.device_close_color || "#999999";
return d.device_normal_color || "#009100";
};
// 創建 sprites // 創建 sprites
const createSprites = async () => { const createSprites = async () => {
if (dataVizExtn.value) { if (dataVizExtn.value) {
@ -74,28 +91,25 @@ export default function useForgeSprite() {
let spriteColor = new THREE.Color(0xffffff); let spriteColor = new THREE.Color(0xffffff);
const BASEURL = import.meta.env.VITE_FILE_API_BASEURL; const BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const spriteIconUrl = `${BASEURL}/dist/hotspot.svg`; const spriteIconUrl = `${BASEURL}/dist/hotspot.svg`;
const style = new DataVizCore.ViewableStyle(
viewableType,
spriteColor,
spriteIconUrl
);
const viewableData = new DataVizCore.ViewableData(); const viewableData = new DataVizCore.ViewableData();
viewableData.spriteSize = 24; // Sprites as points of size 24 x 24 pixels viewableData.spriteSize = 24; // Sprites as points of size 24 x 24 pixels
flatSubData.value?.forEach((d, index) => { flatSubData.value?.forEach((d, index) => {
if (d.device_coordinate_3d) { if (d.device_coordinate_3d) {
const position = d.device_coordinate_3d; const position = d.device_coordinate_3d;
style.color = new THREE.Color(hexToRgb(d.device_normal_color)); // 每個都 new 一個 style
const pointStyle = new DataVizCore.ViewableStyle(
viewableType,
new THREE.Color(hexToRgb(getDeviceRealtimeColor(d))),
spriteIconUrl
);
const viewable = new DataVizCore.SpriteViewable( const viewable = new DataVizCore.SpriteViewable(
position, position,
style, pointStyle,
d.spriteDbId d.spriteDbId
); );
viewableData.addViewable(viewable); viewableData.addViewable(viewable);
} }
}); });
// await viewableData.finish();
// dataVizExtn.value.addViewables(viewableData);
// console.log(dataVizExtn.value);
viewableData.finish().then( viewableData.finish().then(
() => { () => {
dataVizExtn.value.addViewables(viewableData); dataVizExtn.value.addViewables(viewableData);
@ -107,6 +121,16 @@ export default function useForgeSprite() {
); );
} }
}; };
// 監聽 realtimeData 變化,重建 sprites
watch(
() => realtimeData?.value,
() => {
if (forgeViewer.value?.isLoadDone()) {
createSprites();
}
},
{ deep: true }
);
watch( watch(
() => flatSubData, () => flatSubData,

View File

@ -18,7 +18,7 @@ const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
let intervalId = null; let intervalId = null;
const energyCostData = ref({}); const energyCostData = ref({});
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL; const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const imgBaseUrl = ref(''); const imgBaseUrl = ref("");
const formState = ref({ const formState = ref({
building_guid: null, building_guid: null,
floor_guid: "all", floor_guid: "all",
@ -35,8 +35,10 @@ watch(
(newBuilding) => { (newBuilding) => {
if (newBuilding) { if (newBuilding) {
formState.value.building_guid = newBuilding.building_guid; formState.value.building_guid = newBuilding.building_guid;
imgBaseUrl.value = `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${store.previewImageExt}`; imgBaseUrl.value = store.previewImageExt
} ? `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${store.previewImageExt}`
: "";
}
}, },
{ immediate: true, deep: true } { immediate: true, deep: true }
); );
@ -119,7 +121,7 @@ onUnmounted(() => {
) )
" "
/> />
<Forge <Forge
:class=" :class="
twMerge( twMerge(

View File

@ -3,6 +3,7 @@ import { onMounted, ref, inject, computed } from "vue";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { posttDashboard2D3D } from "@/apis/dashboard"; import { posttDashboard2D3D } from "@/apis/dashboard";
import useBuildingStore from "@/stores/useBuildingStore"; import useBuildingStore from "@/stores/useBuildingStore";
import { tr } from "date-fns/locale";
const { openToast, cancelToastOpen } = inject("app_toast"); const { openToast, cancelToastOpen } = inject("app_toast");
const buildingStore = useBuildingStore(); const buildingStore = useBuildingStore();
@ -38,6 +39,8 @@ const onOk = async () => {
formData.append("is3DEnabled", formState.value.showForgeArea); formData.append("is3DEnabled", formState.value.showForgeArea);
if (formState.value.lorf && formState.value.lorf.length > 0) { if (formState.value.lorf && formState.value.lorf.length > 0) {
formData.append("file", formState.value.lorf[0]); formData.append("file", formState.value.lorf[0]);
}else {
formData.append("removePreviewImage", true);
} }
const res = await posttDashboard2D3D(formData); const res = await posttDashboard2D3D(formData);
if (res.isSuccess) { if (res.isSuccess) {

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { RouterView, useRoute } from "vue-router"; import { RouterView, useRoute } from "vue-router";
import { computed, watch, provide, ref, onMounted, onBeforeUnmount } from "vue"; import { computed, watch, provide, ref, onMounted, onUnmounted } from "vue";
import SystemFloorBar from "./components/SystemFloorBar.vue"; import SystemFloorBar from "./components/SystemFloorBar.vue";
import SystemDeptBar from "./components/SystemDeptBar.vue"; import SystemDeptBar from "./components/SystemDeptBar.vue";
import useBuildingStore from "@/stores/useBuildingStore"; import useBuildingStore from "@/stores/useBuildingStore";
@ -42,7 +42,7 @@ const floors = ref([]);
const deptData = ref([]); const deptData = ref([]);
const companyOptions = ref([]); const companyOptions = ref([]);
const selected_dbid = ref([]); const selected_dbid = ref([]);
const imgBaseUrl = ref(''); const imgBaseUrl = ref("");
const getFloors = async () => { const getFloors = async () => {
const res = await getAssetFloorList(); const res = await getAssetFloorList();
@ -130,7 +130,9 @@ watch(
(newBuilding) => { (newBuilding) => {
if (Boolean(newBuilding)) { if (Boolean(newBuilding)) {
getData(); getData();
imgBaseUrl.value = `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${buildingStore.previewImageExt}`; imgBaseUrl.value = buildingStore.previewImageExt
? `${FILE_BASEURL}/upload/setting/previewImage/${newBuilding.building_guid}${buildingStore.previewImageExt}`
: "";
} }
}, },
{ {
@ -173,7 +175,7 @@ const updateCurrentFloor = (floor) => {
}; };
const realtimeData = ref([]); const realtimeData = ref([]);
const timeId = ref(null); let timeId = null;
const getAllDeviceRealtime = async () => { const getAllDeviceRealtime = async () => {
// //
const fetchData = async () => { const fetchData = async () => {
@ -184,15 +186,22 @@ const getAllDeviceRealtime = async () => {
realtimeData.value = res.data; realtimeData.value = res.data;
}; };
await fetchData(); // await fetchData(); //
if (timeId) {
clearInterval(timeId);
timeId = null;
}
// 10 // 10
timeId.value = setInterval(fetchData, 10000); timeId = setInterval(fetchData, 10 * 1000);
}; };
watch( watch(
subscribeData, subscribeData,
(newValue) => { (newValue) => {
timeId.value && clearInterval(timeId.value); console.log("subscribeData changed:", newValue);
if (timeId) {
clearInterval(timeId);
}
newValue.length > 0 && getAllDeviceRealtime(); newValue.length > 0 && getAllDeviceRealtime();
}, },
{ deep: true, immediate: true } { deep: true, immediate: true }
@ -273,8 +282,11 @@ provide("system_selectedDevice", {
deptData, deptData,
}); });
onBeforeUnmount(() => { onUnmounted(() => {
clearInterval(timeId.value); if (timeId) {
clearInterval(timeId);
timeId = null;
}
}); });
</script> </script>

View File

@ -1,13 +1,13 @@
<script setup> <script setup>
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import EffectScatter from "@/components/chart/EffectScatter.vue"; import EffectScatter from "@/components/chart/EffectScatter.vue";
import { computed, inject, ref, watch } from "vue"; import { computed, inject, nextTick, ref, watch } from "vue";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import useSelectedFloor from "@/hooks/useSelectedFloor"; import useSelectedFloor from "@/hooks/useSelectedFloor";
const route = useRoute(); const route = useRoute();
const { currentFloor, subscribeData } = inject("system_deviceList"); const { currentFloor, subscribeData, realtimeData } = inject("system_deviceList");
const { getCurrentInfoModalData, selected_dbid } = inject( const { getCurrentInfoModalData, selected_dbid } = inject(
"system_selectedDevice" "system_selectedDevice"
); );
@ -22,6 +22,19 @@ const sameOption = {
tooltip: 2, tooltip: 2,
}, },
}; };
//
const getDeviceRealtimeColor = (device) => {
if (device.full_name === 'SmartSocket-AA001') return 'red';
if (device.full_name === 'SmartSocket-AA003' || device.full_name === 'SmartSocket-AA004') return 'gray';
const realtimeDevice = realtimeData.value?.find(
(item) => item.device_number === device.device_number
);
const state = realtimeDevice?.state || '';
if (state === 'offnormal' || state === '') return device.device_close_color || '#999';
return device.device_normal_color || '#009100';
};
const defaultOption = (map, data = []) => { const defaultOption = (map, data = []) => {
return { return {
animation: false, animation: false,
@ -38,13 +51,7 @@ const defaultOption = (map, data = []) => {
...sameOption, ...sameOption,
symbolSize: 10, symbolSize: 10,
itemStyle: { itemStyle: {
color: (params) => color: (params) => getDeviceRealtimeColor(params.data[2]),
params.data[2].full_name === "SmartSocket-AA001"
? "red"
: params.data[2].full_name === "SmartSocket-AA003" ||
params.data[2].full_name === "SmartSocket-AA004"
? "gray"
: params.data[2].device_normal_color || "#009100",
}, },
data, data,
}, },
@ -52,19 +59,32 @@ const defaultOption = (map, data = []) => {
...sameOption, ...sameOption,
symbolSize: 20, symbolSize: 20,
itemStyle: { itemStyle: {
color: (params) => color: (params) => getDeviceRealtimeColor(params.data[2]),
params.data[2].full_name === "SmartSocket-AA001"
? "red"
: params.data[2].full_name === "SmartSocket-AA003" ||
params.data[2].full_name === "SmartSocket-AA004"
? "gray"
: params.data[2].device_normal_color || "#009100",
}, },
data: [], data: [],
}, },
], ],
}; };
}; };
// realtimeData
watch(
realtimeData,
() => {
nextTick(() => {
if (
selectedFloor.value &&
asset_floor_chart.value &&
asset_floor_chart.value.chart
) {
asset_floor_chart.value.chart.setOption(
defaultOption(selectedFloor.value?.title, selectedData.value),
true
);
}
});
},
{ deep: true }
);
const { selectedFloor } = useSelectedFloor(); const { selectedFloor } = useSelectedFloor();

View File

@ -5,11 +5,40 @@ import useSystemShowData from "@/hooks/useSystemShowData";
const { getCurrentInfoModalData, selected_dbid } = inject( const { getCurrentInfoModalData, selected_dbid } = inject(
"system_selectedDevice" "system_selectedDevice"
); );
const { subscribeData } = inject("system_deviceList"); const { subscribeData, realtimeData } = inject("system_deviceList");
const { showData } = useSystemShowData(); const { showData } = useSystemShowData();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL; const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
//
const getDeviceRealtimeState = (deviceNumber) => {
const realtimeDevice = realtimeData.value?.find(
(item) => item.device_number === deviceNumber
);
return realtimeDevice?.state || '';
};
//
const getDeviceStatusColor = (device) => {
if (device.full_name === 'SmartSocket-AA001') return 'red';
if (device.full_name === 'SmartSocket-AA003' || device.full_name === 'SmartSocket-AA004') return 'gray';
const state = getDeviceRealtimeState(device.device_number);
if (state === 'offnormal' || state === '') return device.device_close_color || 'gray';
return device.device_normal_color;
};
//
const getDeviceStatusText = (device) => {
if (device.full_name === 'SmartSocket-AA001') return 'Error';
if (device.full_name === 'SmartSocket-AA003' || device.full_name === 'SmartSocket-AA004') return 'Offline';
const state = getDeviceRealtimeState(device.device_number);
if (state === 'offnormal' || state === '') return 'Offline';
if (state === 'normal') return 'Online';
return state || device.device_status || 'Online';
};
const fitToView = (forge_dbid, spriteDbId) => { const fitToView = (forge_dbid, spriteDbId) => {
selected_dbid.value = [forge_dbid, spriteDbId]; selected_dbid.value = [forge_dbid, spriteDbId];
}; };
@ -72,25 +101,10 @@ const cancelDialog = () => {
<div class="sec03"> <div class="sec03">
<span <span
class="w-5 h-5 rounded-full" class="w-5 h-5 rounded-full"
:style="{ :style="{ backgroundColor: getDeviceStatusColor(device) }"
backgroundColor:
device.full_name === 'SmartSocket-AA001'
? 'red'
: device.full_name === 'SmartSocket-AA003' ||
device.full_name === 'SmartSocket-AA004'
? 'gray'
: device.device_normal_color,
}"
></span> ></span>
<span class="mx-2">{{ $t("system.status") }}:</span> <span class="mx-2">{{ $t("system.status") }}:</span>
<span>{{ <span>{{ getDeviceStatusText(device) }}</span>
device.full_name === "SmartSocket-AA001"
? "Error"
: device.full_name === "SmartSocket-AA003" ||
device.full_name === "SmartSocket-AA004"
? "Offline"
: device.device_status || 'Online'
}}</span>
</div> </div>
<button <button
class="btn-text border-0" class="btn-text border-0"