Merge branch 'feature/system'
This commit is contained in:
commit
20d45b4277
2
src/apis/system/api.js
Normal file
2
src/apis/system/api.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const GET_SYSTEM_FLOOR_LIST_API = `/api/Device/GetFloor`;
|
||||||
|
export const GET_SYSTEM_DEVICE_LIST_API = `/api/Device/GetDeviceList`;
|
32
src/apis/system/index.js
Normal file
32
src/apis/system/index.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { GET_SYSTEM_FLOOR_LIST_API, GET_SYSTEM_DEVICE_LIST_API } from "./api";
|
||||||
|
import instance from "@/util/request";
|
||||||
|
import apihandler from "@/util/apihandler";
|
||||||
|
|
||||||
|
export const getSystemFloors = async (building_tag, sub_system_tag) => {
|
||||||
|
const res = await instance.post(GET_SYSTEM_FLOOR_LIST_API, {
|
||||||
|
building_tag,
|
||||||
|
sub_system_tag,
|
||||||
|
});
|
||||||
|
|
||||||
|
return apihandler(res.code, res.data, {
|
||||||
|
msg: res.msg,
|
||||||
|
code: res.code,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getSystemDevices = async ({
|
||||||
|
sub_system_tag,
|
||||||
|
building_tag,
|
||||||
|
floor_tag,
|
||||||
|
}) => {
|
||||||
|
const res = await instance.post(GET_SYSTEM_DEVICE_LIST_API, {
|
||||||
|
sub_system_tag,
|
||||||
|
building_tag,
|
||||||
|
floor_tag,
|
||||||
|
});
|
||||||
|
|
||||||
|
return apihandler(res.code, res.data, {
|
||||||
|
msg: res.msg,
|
||||||
|
code: res.code,
|
||||||
|
});
|
||||||
|
};
|
1
src/assets/img/equipment/replay01.svg
Normal file
1
src/assets/img/equipment/replay01.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.6 KiB |
1
src/assets/img/equipment/replay02.svg
Normal file
1
src/assets/img/equipment/replay02.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.66 12.66"><defs><style>.cls-1{opacity:0.8;}.cls-2{fill:#35eded;}.cls-3{fill:#ffe448;}</style></defs><g class="cls-1"><path class="cls-2" d="M4.89.76a5.87,5.87,0,0,0-1.48.62L2.87.84l-2,2,.54.54A5.87,5.87,0,0,0,.76,4.89H0V6.14H2.25A4.07,4.07,0,0,1,6.14,2.25V0H4.89Z"/><path class="cls-2" d="M10.41,6.51a4.09,4.09,0,0,1-3.9,3.9v2.25H7.76v-.77a5.57,5.57,0,0,0,1.49-.61l.54.54,2-2-.54-.54a5.57,5.57,0,0,0,.61-1.49h.77V6.51Z"/></g><g class="cls-1"><path class="cls-3" d="M2.25,6.51H0V7.76H.76a5.94,5.94,0,0,0,.62,1.49l-.54.54,2,2,.54-.54a5.5,5.5,0,0,0,1.48.61v.77H6.14V10.41A4.08,4.08,0,0,1,2.25,6.51Z"/><path class="cls-3" d="M11.89,4.89a5.5,5.5,0,0,0-.61-1.48l.54-.54-2-2-.54.54A5.94,5.94,0,0,0,7.76.76V0H6.51V2.25a4.08,4.08,0,0,1,3.9,3.89h2.25V4.89Z"/></g></svg>
|
After Width: | Height: | Size: 850 B |
1
src/assets/img/equipment/state-background.svg
Normal file
1
src/assets/img/equipment/state-background.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.73 25.44"><defs><style>.cls-1{opacity:0.8;}.cls-2{fill:#64ed81;}</style></defs><g class="cls-1"><path class="cls-2" d="M2.52,4.79A2.4,2.4,0,0,1,2.52,0,2.42,2.42,0,0,1,4.84,1.79h8.89V3H4.84A2.42,2.42,0,0,1,2.52,4.79Zm0-4.54A2.15,2.15,0,1,0,4.61,2.84l0-.09h8.84V2H4.64l0-.1A2.15,2.15,0,0,0,2.52.25Zm0,3.58A1.44,1.44,0,1,1,4,2.39,1.44,1.44,0,0,1,2.52,3.83Zm0-2.62A1.19,1.19,0,1,0,3.71,2.39,1.18,1.18,0,0,0,2.52,1.21Z"/><path class="cls-2" d="M2.48,11.67a2.4,2.4,0,1,1,2.32-3h8.9V9.88H4.8A2.41,2.41,0,0,1,2.48,11.67Zm0-4.54a2.15,2.15,0,1,0,2.1,2.6l0-.1h8.85V8.92H4.6l0-.09A2.16,2.16,0,0,0,2.48,7.13Zm0,3.59A1.44,1.44,0,1,1,3.92,9.28,1.44,1.44,0,0,1,2.48,10.72Zm0-2.63A1.19,1.19,0,1,0,3.67,9.28,1.18,1.18,0,0,0,2.48,8.09Z"/><path class="cls-2" d="M2.43,18.55a2.39,2.39,0,1,1,2.31-3h8.9v1.2H4.74A2.4,2.4,0,0,1,2.43,18.55Zm0-4.53a2.14,2.14,0,1,0,2.09,2.59l0-.1h8.85v-.7H4.54l0-.1A2.15,2.15,0,0,0,2.43,14Zm0,3.58a1.44,1.44,0,1,1,1.44-1.44A1.45,1.45,0,0,1,2.43,17.6Zm0-2.63a1.19,1.19,0,0,0,0,2.38,1.19,1.19,0,1,0,0-2.38Z"/><path class="cls-2" d="M2.39,25.44a2.4,2.4,0,1,1,2.32-3H13.6v1.21H4.71A2.41,2.41,0,0,1,2.39,25.44Zm0-4.54a2.15,2.15,0,1,0,2.1,2.59l0-.09h8.84v-.71H4.51l0-.1A2.15,2.15,0,0,0,2.39,20.9Zm0,3.58A1.44,1.44,0,1,1,3.83,23,1.43,1.43,0,0,1,2.39,24.48Zm0-2.63A1.19,1.19,0,1,0,3.58,23,1.19,1.19,0,0,0,2.39,21.85Z"/></g></svg>
|
After Width: | Height: | Size: 1.4 KiB |
1
src/assets/img/equipment/state-title.svg
Normal file
1
src/assets/img/equipment/state-title.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44.63 9.62"><defs><style>.cls-1{opacity:0.8;}.cls-2{fill:#35eded;}.cls-3{fill:#64ed81;}</style></defs><g class="cls-1"><rect class="cls-2" y="7.2" width="44.63" height="0.5"/></g><rect class="cls-3" x="34.71" y="4.68" width="9.62" height="0.25"/><rect class="cls-3" x="39.4" width="0.25" height="9.62"/></svg>
|
After Width: | Height: | Size: 397 B |
7
src/assets/img/equipment/table-item-w.svg
Normal file
7
src/assets/img/equipment/table-item-w.svg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 16.1 11.6" style="enable-background:new 0 0 16.1 11.6;" xml:space="preserve">
|
||||||
|
|
||||||
|
<polygon class="st0" fill="currentColor" points="0,11.6 10.3,11.6 16.1,5.8 10.3,0 0,0 5.8,5.8 "/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 460 B |
@ -8,7 +8,10 @@ const props = defineProps({
|
|||||||
className: String,
|
className: String,
|
||||||
id: String,
|
id: String,
|
||||||
svg: Object,
|
svg: Object,
|
||||||
getCoordinate: Function,
|
getCoordinate: {
|
||||||
|
type: Function,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let chart = ref(null);
|
let chart = ref(null);
|
||||||
@ -22,33 +25,23 @@ async function updateSvg(svg, option) {
|
|||||||
axios.get(svg.path).then(({ data }) => {
|
axios.get(svg.path).then(({ data }) => {
|
||||||
echarts.registerMap(svg.full_name, { svg: data });
|
echarts.registerMap(svg.full_name, { svg: data });
|
||||||
chart.value.setOption(option);
|
chart.value.setOption(option);
|
||||||
chart.value.getZr().on("click", function (params) {
|
if (props.getCoordinate) {
|
||||||
var pixelPoint = [params.offsetX, params.offsetY];
|
chart.value.getZr().on("click", function (params) {
|
||||||
var dataPoint = chart.value.convertFromPixel({ geoIndex: 0 }, pixelPoint);
|
var pixelPoint = [params.offsetX, params.offsetY];
|
||||||
currentClickPosition.value = dataPoint;
|
var dataPoint = chart.value.convertFromPixel({ geoIndex: 0 }, pixelPoint);
|
||||||
props.getCoordinate(dataPoint);
|
currentClickPosition.value = dataPoint;
|
||||||
chart.value.setOption({
|
props.getCoordinate(dataPoint);
|
||||||
series: {
|
chart.value.setOption({
|
||||||
data: [dataPoint],
|
series: {
|
||||||
},
|
data: [dataPoint],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
console.log(chart.value.getOption());
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
console.log("updateSvg", svg.path);
|
console.log("updateSvg", svg.path);
|
||||||
// fetch(svg.path)
|
|
||||||
// .then((res) => console.log(res))
|
|
||||||
// .then(function (svg) {
|
|
||||||
// console.log(svg);
|
|
||||||
// // echarts.registerMap(svg.full_name, { svg });
|
|
||||||
// // chart.setOption(option);
|
|
||||||
// // chart.getZr().on("click", function (params) {
|
|
||||||
// // var pixelPoint = [params.offsetX, params.offsetY];
|
|
||||||
// // var dataPoint = curChart.convertFromPixel({ geoIndex: 0 }, pixelPoint);
|
|
||||||
// // console.log(dataPoint);
|
|
||||||
// // currentClickPosition.value = dataPoint;
|
|
||||||
// // });
|
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@ -69,7 +62,7 @@ defineExpose({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div :id="id" class="min-h-[500px] max-h-fit w-full" ref="dom"></div>
|
<div :id="id" class="min-h-[70vh] max-h-fit w-full bg-white" ref="dom"></div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style lang="scss" scoped></style>
|
||||||
|
@ -7,28 +7,22 @@ const props = defineProps({
|
|||||||
withLine: Boolean,
|
withLine: Boolean,
|
||||||
// this is for change active button
|
// this is for change active button
|
||||||
onclick: Function,
|
onclick: Function,
|
||||||
|
className: String
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap text-white items-center">
|
<div class="flex flex-wrap text-white items-center">
|
||||||
<button
|
<button v-for="item in items" :class="twMerge(
|
||||||
v-for="item in items"
|
'btn my-1',
|
||||||
:class="
|
item.active ? 'btn-success' : 'btn-outline-success',
|
||||||
twMerge(
|
withLine ? 'line' : 'mx-2 first:ml-0 ',
|
||||||
'btn my-1',
|
className
|
||||||
item.active ? 'btn-success' : 'btn-outline-success',
|
)
|
||||||
withLine ? 'line' : 'mx-2 first:ml-0 '
|
" :disabled="item.disabled" :key="item.key" @click.stop.prevent="(e) => {
|
||||||
)
|
|
||||||
"
|
|
||||||
:disabled="item.disabled"
|
|
||||||
:key="item.key"
|
|
||||||
@click.stop.prevent="
|
|
||||||
(e) => {
|
|
||||||
item.onClick ? item.onClick(e, item) : onclick(e, item);
|
item.onClick ? item.onClick(e, item) : onclick(e, item);
|
||||||
}
|
}
|
||||||
"
|
">
|
||||||
>
|
|
||||||
<slot name="buttonContent" v-bind="{ record: item }">
|
<slot name="buttonContent" v-bind="{ record: item }">
|
||||||
{{ item.title }}
|
{{ item.title }}
|
||||||
</slot>
|
</slot>
|
||||||
|
257
src/components/forge/ForgeForSystem.vue
Normal file
257
src/components/forge/ForgeForSystem.vue
Normal file
@ -0,0 +1,257 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
ref,
|
||||||
|
onMounted,
|
||||||
|
defineProps,
|
||||||
|
computed,
|
||||||
|
onUnmounted,
|
||||||
|
watch,
|
||||||
|
provide,
|
||||||
|
} from "vue";
|
||||||
|
import { getUrn, getAccessToken } from "@/apis/forge";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import hexToRgb from "@/util/hexToRgb";
|
||||||
|
import getModalPosition from "@/util/getModalPosition";
|
||||||
|
import useSystemStatusByBaja from "@/hooks/baja/useSystemStatusByBaja";
|
||||||
|
import ForgeInfoModal from "./ForgeInfoModal.vue";
|
||||||
|
import useAlarmStore from "@/stores/useAlarmStore";
|
||||||
|
import useForgeSprite from "@/hooks/forge/useForgeSprite";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
initialData: Object,
|
||||||
|
cubeStyle: {
|
||||||
|
type: Object,
|
||||||
|
default: {
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const heat_bar_isShow = ref(false);
|
||||||
|
const updateHeatBarIsShow = (isShow) => {
|
||||||
|
heat_bar_isShow.value = isShow;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { updateDataVisualization, createSprites, hideAllObjects } = useForgeSprite()
|
||||||
|
|
||||||
|
const forgeDom = ref(null);
|
||||||
|
|
||||||
|
const initViewer = (container) => {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
// Autodesk.Viewing.Initializer({ getAccessToken }, function () {
|
||||||
|
// const config = {
|
||||||
|
// extensions: ["Autodesk.DataVisualization", "Autodesk.DocumentBrowser"],
|
||||||
|
// };
|
||||||
|
// let viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
|
||||||
|
// Autodesk.Viewing.Private.InitParametersSetting.alpha = true;
|
||||||
|
// viewer.start();
|
||||||
|
// resolve(viewer);
|
||||||
|
// });
|
||||||
|
|
||||||
|
Autodesk.Viewing.Initializer(
|
||||||
|
{
|
||||||
|
env: "Local",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
const config = {
|
||||||
|
extensions: [
|
||||||
|
"Autodesk.DataVisualization",
|
||||||
|
"Autodesk.DocumentBrowser",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
|
||||||
|
Autodesk.Viewing.Private.InitParametersSetting.alpha = true;
|
||||||
|
viewer.start();
|
||||||
|
resolve(viewer);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用本地 .svf 文件加載模型
|
||||||
|
const loadModel = (viewer, filePath) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
viewer.loadModel(
|
||||||
|
filePath,
|
||||||
|
{},
|
||||||
|
(model) => {
|
||||||
|
viewer.impl.invalidate(true);
|
||||||
|
viewer.fitToView();
|
||||||
|
resolve(model);
|
||||||
|
console.log("模型加載完成");
|
||||||
|
},
|
||||||
|
reject
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// const loadModel = (viewer, urn) => {
|
||||||
|
// return new Promise(function (resolve, reject) {
|
||||||
|
// async function onDocumentLoadSuccess(doc) {
|
||||||
|
// viewer.setGroundShadow(false);
|
||||||
|
// viewer.impl.renderer().setClearAlpha(0); //clear alpha channel
|
||||||
|
// viewer.impl.glrenderer().setClearColor(0xffffff, 0); //set transparent background, color code does not matter
|
||||||
|
// viewer.impl.invalidate(true); //trigger rendering
|
||||||
|
|
||||||
|
// const documentNode = await viewer.loadDocumentNode(
|
||||||
|
// doc,
|
||||||
|
// doc.getRoot().getDefaultGeometry()
|
||||||
|
// );
|
||||||
|
// updateDataVisualization(viewer)
|
||||||
|
// resolve(documentNode);
|
||||||
|
// }
|
||||||
|
// function onDocumentLoadFailure(code, message, errors) {
|
||||||
|
// reject({ code, message, errors });
|
||||||
|
// }
|
||||||
|
// Autodesk.Viewing.Document.load(
|
||||||
|
// "urn:" + urn,
|
||||||
|
// onDocumentLoadSuccess,
|
||||||
|
// onDocumentLoadFailure
|
||||||
|
// );
|
||||||
|
// });
|
||||||
|
// };
|
||||||
|
|
||||||
|
const initForge = async () => {
|
||||||
|
// getUrn().then((res) => {
|
||||||
|
// if (!res.isSuccess) return;
|
||||||
|
|
||||||
|
// initViewer(forgeDom.value).then((viewer) => {
|
||||||
|
// loadModel(viewer, res.data[0].urn_3D).then(() => {
|
||||||
|
// viewer.addEventListener(
|
||||||
|
// Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
|
||||||
|
// async function () {
|
||||||
|
// console.log(
|
||||||
|
// "Autodesk.Viewing.GEOMETRY_LOADED_EVENT",
|
||||||
|
// viewer.isLoadDone()
|
||||||
|
// );
|
||||||
|
// // updateForgeViewer(viewer);
|
||||||
|
// createSprites()
|
||||||
|
// hideAllObjects();
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// viewer.addEventListener(
|
||||||
|
// Autodesk.Viewing.CAMERA_CHANGE_EVENT,
|
||||||
|
// function (e) {
|
||||||
|
// // viewer.isLoadDone() &&
|
||||||
|
// // updateDbidPosition(this, subscribeData.value);
|
||||||
|
// console.log(
|
||||||
|
// "camera position changed: ",
|
||||||
|
// NOP_VIEWER.navigation.getTarget(),
|
||||||
|
// e.camera.position
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||||
|
const viewer = await initViewer(forgeDom.value)
|
||||||
|
const filePath = `${FILE_BASEURL}/upload/forge/0.svf`;
|
||||||
|
await loadModel(viewer, filePath)
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("Forge 加載");
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
onUnmounted(() => {
|
||||||
|
console.log("Forge 銷毀");
|
||||||
|
|
||||||
|
NOP_VIEWER.tearDown();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ForgeInfoModal :data="currentInfoModalData" />
|
||||||
|
<div id="forge-preview" ref="forgeDom" class="relative w-full h-full min-h-full">
|
||||||
|
<div v-show="heat_bar_isShow" class="absolute z-10 heatbar">
|
||||||
|
<div class="w-40 flex justify-between text-[10px] mb-1">
|
||||||
|
<span class="text-gradient-1">10 °C</span>
|
||||||
|
<span class="text-gradient-2">20 °C</span>
|
||||||
|
<span class="text-gradient-3">30 °C</span>
|
||||||
|
<span class="text-gradient-4">40 °C</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-40 h-3" style="
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
#0000ff 0%,
|
||||||
|
#00ff00 33%,
|
||||||
|
#ffff00 66%,
|
||||||
|
#ff0000 100%
|
||||||
|
);
|
||||||
|
"></div>
|
||||||
|
</div>
|
||||||
|
<!-- label -->
|
||||||
|
<!-- https://github.com/augustogoncalves/forge-plant-operation/blob/master/forgeSample/wwwroot/js/iconExtension.js -->
|
||||||
|
<!-- https://github.com/dukedhx/viewer-iot-react-feathersjs/blob/master/src/client/iconExtension.js -->
|
||||||
|
|
||||||
|
<label v-for="value in subscribeData" :key="key" :data-dbid="value.forge_dbid" :class="twMerge(
|
||||||
|
`after:border-t-[${value.currentColor}]`,
|
||||||
|
'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',
|
||||||
|
'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'
|
||||||
|
)
|
||||||
|
" :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
|
||||||
|
)
|
||||||
|
">
|
||||||
|
<span class="mr-2">{{ value.full_name }}</span>
|
||||||
|
<span v-if="value.alarmMsg">{{ value.alarmMsg }}</span>
|
||||||
|
<span v-else>{{ value.show_value }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
.adsk-viewing-viewer {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#guiviewer3d-toolbar {
|
||||||
|
display: none;
|
||||||
|
bottom: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewcubeWrapper {
|
||||||
|
right: v-bind("`${props.cubeStyle.right}%`") !important;
|
||||||
|
top: v-bind("`${props.cubeStyle.top}%`") !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.homeViewWrapper {
|
||||||
|
transform: scale(0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.heatbar {
|
||||||
|
right: v-bind("`${props.cubeStyle.right + 2}%`") !important;
|
||||||
|
top: 0% !important;
|
||||||
|
}
|
||||||
|
</style>
|
@ -17,9 +17,9 @@ const iniFroList = async () => {
|
|||||||
res.data.map((d) =>
|
res.data.map((d) =>
|
||||||
AUTHPAGES.find(({ authCode }) => authCode === d.authCode)
|
AUTHPAGES.find(({ authCode }) => authCode === d.authCode)
|
||||||
? {
|
? {
|
||||||
...d,
|
...d,
|
||||||
...AUTHPAGES.find(({ authCode }) => authCode === d.authCode),
|
...AUTHPAGES.find(({ authCode }) => authCode === d.authCode),
|
||||||
}
|
}
|
||||||
: d
|
: d
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@ -43,6 +43,8 @@ const onClose = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navigateToSub = (sub) => {
|
const navigateToSub = (sub) => {
|
||||||
|
// buildingStore.selectedSystem = sub;
|
||||||
|
onClose()
|
||||||
// console.log("navigateToSub", sub);
|
// console.log("navigateToSub", sub);
|
||||||
// let pageAct = JSON.parse(sessionStorage.getItem("pageAct"));
|
// let pageAct = JSON.parse(sessionStorage.getItem("pageAct"));
|
||||||
// pageAct = {
|
// pageAct = {
|
||||||
@ -119,24 +121,20 @@ onMounted(() => {
|
|||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<a-drawer
|
<a-drawer :width="200" placement="left" :open="open" :closable="false" @close="onClose" class="sub-drawer"
|
||||||
:width="200"
|
:maskStyle="{ opacity: 0.5 }" :bodyStyle="{ paddingLeft: 0, paddingRight: 0 }">
|
||||||
placement="left"
|
|
||||||
:open="open"
|
|
||||||
:closable="false"
|
|
||||||
@close="onClose"
|
|
||||||
class="sub-drawer"
|
|
||||||
:maskStyle="{ opacity: 0.5 }"
|
|
||||||
:bodyStyle="{ paddingLeft: 0, paddingRight: 0 }"
|
|
||||||
>
|
|
||||||
<ul>
|
<ul>
|
||||||
<li
|
<li v-for="sub in buildingStore.subSys" :key="sub.sub_system_tag"
|
||||||
v-for="sub in buildingStore.subSys"
|
class="group text-xl text-center py-3 hover:bg-black" @click="() => navigateToSub(sub)">
|
||||||
:key="sub.sub_system_tag"
|
|
||||||
@click.prevent="() => navigateToSub(sub)"
|
|
||||||
class="text-xl text-center py-3 hover:bg-black hover:text-info"
|
<router-link
|
||||||
>
|
:to="{ name: 'sub_system', params: { main_system_id: sub.main_system_tag, sub_system_id: sub.sub_system_tag }, }"
|
||||||
{{ sub.full_name }}
|
type="link" class="group-hover:text-info">
|
||||||
|
|
||||||
|
{{ sub.full_name }}
|
||||||
|
</router-link>
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
|
@ -7,7 +7,7 @@ export const AUTHPAGES = [
|
|||||||
{
|
{
|
||||||
authCode: "PF1",
|
authCode: "PF1",
|
||||||
icon: "tv",
|
icon: "tv",
|
||||||
navigate: "/",
|
navigate: "/system",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
authCode: "PF2",
|
authCode: "PF2",
|
||||||
|
@ -31,7 +31,7 @@ export default function useAlarmData() {
|
|||||||
timer = null;
|
timer = null;
|
||||||
},
|
},
|
||||||
each: (record) => {
|
each: (record) => {
|
||||||
const alarmDisplayName = record.get("alarmData").get("sourceName");
|
let alarmDisplayName = record.get("alarmData").get("sourceName");
|
||||||
alarmDisplayName = alarmDisplayName.replace(/\$2d/g, "_"); // 檢查並替換 $2d 為 _
|
alarmDisplayName = alarmDisplayName.replace(/\$2d/g, "_"); // 檢查並替換 $2d 為 _
|
||||||
const sourceTmp = alarmDisplayName.split("_");
|
const sourceTmp = alarmDisplayName.split("_");
|
||||||
const bfName = sourceTmp[1] + "-" + sourceTmp[4];
|
const bfName = sourceTmp[1] + "-" + sourceTmp[4];
|
||||||
|
@ -291,6 +291,7 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fitToView = () => {
|
const fitToView = () => {
|
||||||
|
if(!searchParams.value.camera_position) return
|
||||||
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); //!<<< 相机的新位置
|
||||||
|
|
||||||
|
47
src/hooks/forge/useForgeHeatmap.js
Normal file
47
src/hooks/forge/useForgeHeatmap.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
export default function useForgeHeatmap(){
|
||||||
|
|
||||||
|
const createHeatMap = async (heatMapName) => {
|
||||||
|
const {
|
||||||
|
SurfaceShadingData,
|
||||||
|
SurfaceShadingPoint,
|
||||||
|
SurfaceShadingNode,
|
||||||
|
SurfaceShadingGroup,
|
||||||
|
} = Autodesk.DataVisualization.Core;
|
||||||
|
const shadingGroup = new SurfaceShadingGroup(`iot_heatmap_${heatMapName}`);
|
||||||
|
const rooms = new Map();
|
||||||
|
|
||||||
|
for (const { id, roomDbId, position, sensorTypes } of deviceList.value) {
|
||||||
|
if (!id || roomDbId == -1 || !roomDbId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rooms.has(roomDbId)) {
|
||||||
|
const room = new SurfaceShadingNode(id, roomDbId);
|
||||||
|
shadingGroup.addChild(room);
|
||||||
|
rooms.set(roomDbId, room);
|
||||||
|
}
|
||||||
|
const room = rooms.get(roomDbId);
|
||||||
|
room.addPoint(new SurfaceShadingPoint(id, position, sensorTypes));
|
||||||
|
}
|
||||||
|
|
||||||
|
const shadingData = new SurfaceShadingData();
|
||||||
|
shadingData.addChild(shadingGroup);
|
||||||
|
shadingData.initialize(forgeViewer.value?.model);
|
||||||
|
|
||||||
|
await dataVizExtension.value.setupSurfaceShading(
|
||||||
|
forgeViewer.value.model,
|
||||||
|
shadingData
|
||||||
|
);
|
||||||
|
dataVizExtension.value.registerSurfaceShadingColors(
|
||||||
|
"temperature",
|
||||||
|
[0x0000ff, 0x00ff00, 0xffff00, 0xff0000]
|
||||||
|
);
|
||||||
|
dataVizExtension.value.renderSurfaceShading(
|
||||||
|
`iot_heatmap_${heatMapName}`,
|
||||||
|
"temperature",
|
||||||
|
getSensorValue
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(dataVizExtension.value);
|
||||||
|
};
|
||||||
|
}
|
101
src/hooks/forge/useForgeSprite.js
Normal file
101
src/hooks/forge/useForgeSprite.js
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { watch, inject, markRaw, ref } from "vue";
|
||||||
|
import useAlarmStore from "@/stores/useAlarmStore";
|
||||||
|
import hexToRgb from "@/util/hexToRgb";
|
||||||
|
|
||||||
|
export default function useForgeSprite() {
|
||||||
|
const store = useAlarmStore();
|
||||||
|
const { subscribeData } = inject("system_deviceList");
|
||||||
|
const forgeViewer = ref(null);
|
||||||
|
const dataVizExtn = ref(null);
|
||||||
|
const updateDataVisualization = async (viewer) => {
|
||||||
|
if (!forgeViewer.value) {
|
||||||
|
forgeViewer.value = markRaw(viewer);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataVisualization = await NOP_VIEWER.loadExtension(
|
||||||
|
"Autodesk.DataVisualization"
|
||||||
|
);
|
||||||
|
dataVizExtn.value = markRaw(dataVisualization);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSpriteClicked = (event) => {
|
||||||
|
event.hasStopped = true;
|
||||||
|
const data = deviceList.value.find((d) => d.spriteDbId === event.dbId);
|
||||||
|
modalContent.value = data;
|
||||||
|
store.getDbIdStore(data.forge_dbid);
|
||||||
|
toggleModal(event.originalEvent);
|
||||||
|
};
|
||||||
|
// 創建 sprites
|
||||||
|
const createSprites = async () => {
|
||||||
|
const DataVizCore = Autodesk.DataVisualization.Core;
|
||||||
|
const viewableType = DataVizCore.ViewableType.SPRITE;
|
||||||
|
let spriteColor = new THREE.Color(0xffffff);
|
||||||
|
const BASEURL = import.meta.env.VITE_FORGE_BASEURL;
|
||||||
|
const spriteIconUrl = `${BASEURL}/hotspot.svg`;
|
||||||
|
const style = new DataVizCore.ViewableStyle(
|
||||||
|
viewableType,
|
||||||
|
spriteColor,
|
||||||
|
spriteIconUrl
|
||||||
|
);
|
||||||
|
const viewableData = new DataVizCore.ViewableData();
|
||||||
|
viewableData.spriteSize = 24; // Sprites as points of size 24 x 24 pixels
|
||||||
|
subscribeData.value?.forEach((d, index) => {
|
||||||
|
const position = d.device_coordinate_3d;
|
||||||
|
style.color = new THREE.Color(hexToRgb(d.device_normal_color));
|
||||||
|
const viewable = new DataVizCore.SpriteViewable(
|
||||||
|
position,
|
||||||
|
style,
|
||||||
|
d.spriteDbId
|
||||||
|
);
|
||||||
|
viewableData.addViewable(viewable);
|
||||||
|
});
|
||||||
|
await viewableData.finish();
|
||||||
|
dataVizExtn.value.addViewables(viewableData);
|
||||||
|
|
||||||
|
NOP_VIEWER.addEventListener(DataVizCore.MOUSE_CLICK, onSpriteClicked);
|
||||||
|
NOP_VIEWER.addEventListener(
|
||||||
|
Autodesk.Viewing.SELECTION_CHANGED_EVENT,
|
||||||
|
onSpriteClicked
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fitToView = () => {
|
||||||
|
const { x, y, z } = JSON.parse(searchParams.value.camera_position);
|
||||||
|
const newPosition = new THREE.Vector3(x, y, z); //!<<< 相机的新位置
|
||||||
|
|
||||||
|
const {
|
||||||
|
x: x1,
|
||||||
|
y: y1,
|
||||||
|
z: z1,
|
||||||
|
} = JSON.parse(searchParams.value.target_position); //!<<< 计算新焦点位置
|
||||||
|
const newTarget = new THREE.Vector3(x1, y1, z1); //!<<< 焦點的新位置
|
||||||
|
|
||||||
|
NOP_VIEWER.navigation.getCamera().setView({
|
||||||
|
position: newPosition.clone(),
|
||||||
|
target: newTarget.clone(),
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
updateDbidPosition(NOP_VIEWER, subscribeData.value);
|
||||||
|
}, 700);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideAllObjects = () => {
|
||||||
|
const tree = NOP_VIEWER.model?.getData().instanceTree;
|
||||||
|
const allDbIdsStr = Object.keys(tree.nodeAccess.dbIdToIndex);
|
||||||
|
for (var i = 0; i < allDbIdsStr.length; i++) {
|
||||||
|
NOP_VIEWER.hide(parseInt(allDbIdsStr[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribeData.value.forEach((value, index) => {
|
||||||
|
NOP_VIEWER.show(value.forge_dbid);
|
||||||
|
});
|
||||||
|
|
||||||
|
NOP_VIEWER.impl.invalidate(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createSprites,
|
||||||
|
updateDataVisualization,
|
||||||
|
hideAllObjects,
|
||||||
|
};
|
||||||
|
}
|
@ -11,8 +11,11 @@ import EnergyManagement from "@/views/energyManagement/EnergyManagement.vue";
|
|||||||
import Login from "@/views/login/Login.vue";
|
import Login from "@/views/login/Login.vue";
|
||||||
import useUserInfoStore from "@/stores/useUserInfoStore";
|
import useUserInfoStore from "@/stores/useUserInfoStore";
|
||||||
import useGetCookie from "@/hooks/useGetCookie";
|
import useGetCookie from "@/hooks/useGetCookie";
|
||||||
|
import System from "@/views/system/System.vue";
|
||||||
|
import SystemFloor from "@/views/system/SystemFloor.vue";
|
||||||
|
|
||||||
import Test from "@/views/Test.vue";
|
import Test from "@/views/Test.vue";
|
||||||
|
import SystemMain from "@/views/system/SystemMain.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
@ -29,6 +32,23 @@ const router = createRouter({
|
|||||||
name: "dashboard",
|
name: "dashboard",
|
||||||
component: Dashboard,
|
component: Dashboard,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/system/:main_system_id/:sub_system_id",
|
||||||
|
name: "system",
|
||||||
|
component: System,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "",
|
||||||
|
name: "sub_system",
|
||||||
|
component: SystemMain,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "floor/:floor_id",
|
||||||
|
name: "floor",
|
||||||
|
component: SystemFloor,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/historyData",
|
path: "/historyData",
|
||||||
name: "history",
|
name: "history",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { defineStore } from "pinia";
|
import { defineStore } from "pinia";
|
||||||
import { ref, computed } from "vue";
|
import { ref, computed } from "vue";
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
const useBuildingStore = defineStore("buildingInfo", () => {
|
const useBuildingStore = defineStore("buildingInfo", () => {
|
||||||
// 所有棟別
|
// 所有棟別
|
||||||
@ -35,12 +36,21 @@ const useBuildingStore = defineStore("buildingInfo", () => {
|
|||||||
return subPages;
|
return subPages;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const selectedSystem = computed(() => {
|
||||||
|
if (route.params.sub_system_id && subSys.value.length > 0) {
|
||||||
|
return subSys.value.find((s) => s.key === route.params.sub_system_id);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buildings,
|
buildings,
|
||||||
selectedBuilding,
|
selectedBuilding,
|
||||||
mainSubSys,
|
mainSubSys,
|
||||||
mainSys,
|
mainSys,
|
||||||
subSys,
|
subSys,
|
||||||
|
selectedSystem,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
151
src/views/system/System.vue
Normal file
151
src/views/system/System.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<script setup>
|
||||||
|
import { RouterView, useRoute } from 'vue-router';
|
||||||
|
import { computed, watch, provide, ref } from "vue";
|
||||||
|
import SystemFloorBar from './components/SystemFloorBar.vue';
|
||||||
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
|
import ForgeForSystem from "@/components/forge/ForgeForSystem.vue";
|
||||||
|
import { getSystemDevices } from "@/apis/system";
|
||||||
|
import SystemSubBar from './components/SystemSubBar.vue';
|
||||||
|
|
||||||
|
const buildingStore = useBuildingStore()
|
||||||
|
|
||||||
|
const statusList = computed(() => {
|
||||||
|
const sub = buildingStore.selectedSystem
|
||||||
|
|
||||||
|
if (sub) {
|
||||||
|
return {
|
||||||
|
device_normal_color: sub.device_normal_color,
|
||||||
|
device_normal_text: sub.device_normal_text,
|
||||||
|
device_close_color: sub.device_close_color,
|
||||||
|
device_close_text: sub.device_close_text,
|
||||||
|
device_error_color: sub.device_error_color,
|
||||||
|
device_error_text: sub.device_error_text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
|
const raw_data = ref([])
|
||||||
|
const data = ref([])
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const getData = async () => {
|
||||||
|
const res = await getSystemDevices({
|
||||||
|
sub_system_tag: route.params.sub_system_id,
|
||||||
|
building_tag: buildingStore.selectedBuilding?.building_tag,
|
||||||
|
})
|
||||||
|
const data = res.data.map(d => ({ ...d, key: d.full_name }));
|
||||||
|
raw_data.value = data
|
||||||
|
data.value = data
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscribeData = ref([]);
|
||||||
|
|
||||||
|
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 getSubData = (value) => {
|
||||||
|
let items = [];
|
||||||
|
value.forEach((device) => {
|
||||||
|
items = [
|
||||||
|
...items,
|
||||||
|
...device.device_list.map((d, index) => ({
|
||||||
|
...d,
|
||||||
|
forge_dbid: parseInt(d.forge_dbid),
|
||||||
|
device_coordinate_3d: d.device_coordinate_3d
|
||||||
|
? JSON.parse(d.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
|
||||||
|
// ),
|
||||||
|
alarmMsg: "",
|
||||||
|
is_show: true,
|
||||||
|
currentColor: d.device_normal_point_color,
|
||||||
|
spriteDbId: 10 + index,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
data.value = raw_data.value;
|
||||||
|
subscribeData.value = items;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(raw_data, (newValue) => {
|
||||||
|
updateDataByGas("all")
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateDataByGas = (gas) => {
|
||||||
|
console.log(gas)
|
||||||
|
if (gas === "all") {
|
||||||
|
getSubData(raw_data.value)
|
||||||
|
} else {
|
||||||
|
data.value = raw_data.value.map((d) => ({
|
||||||
|
...d,
|
||||||
|
device_list: d.device_list.filter(({ points }) => points.some(({ points: p }) => p === gas))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([() => route, () => buildingStore.selectedBuilding], ([newRoute, newBuilding]) => {
|
||||||
|
if (newBuilding && newRoute.params.sub_system_id) {
|
||||||
|
getData()
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentFloor = ref(null);
|
||||||
|
|
||||||
|
const updateCurrentFloor = (floor) => {
|
||||||
|
currentFloor.value = floor;
|
||||||
|
}
|
||||||
|
|
||||||
|
provide("system_deviceList", { data, subscribeData, currentFloor, updateCurrentFloor, updateDataByGas })
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SystemFloorBar />
|
||||||
|
<div class="grid grid-cols-2 gap-5 mt-8 mb-4">
|
||||||
|
<div class="col-span-1 h-[80vh] flex flex-col justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="flex mb-4 items-center">
|
||||||
|
<span class="flex items-center mr-3" v-if="statusList?.device_normal_text">
|
||||||
|
<span class="w-7 h-7 rounded-full inline-block mr-1"
|
||||||
|
:style="{ backgroundColor: statusList.device_normal_color }"></span>
|
||||||
|
<span>{{ statusList.device_normal_text }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center mr-3" v-if="statusList?.device_close_text">
|
||||||
|
<span class="w-7 h-7 rounded-full inline-block mr-1"
|
||||||
|
:style="{ backgroundColor: statusList.device_close_color }"></span>
|
||||||
|
<span>{{ statusList.device_close_text }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center mr-3" v-if="statusList?.device_error_text">
|
||||||
|
<span class="w-7 h-7 rounded-full inline-block mr-1"
|
||||||
|
:style="{ backgroundColor: statusList.device_error_color }"></span>
|
||||||
|
<span>{{ statusList.device_error_text }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<SystemSubBar class="mt-2 mb-4" />
|
||||||
|
</div>
|
||||||
|
<div class="max-h-[75vh] pr-2 overflow-y-auto">
|
||||||
|
<RouterView />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-1 h-full">
|
||||||
|
<ForgeForSystem :initialData="{}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='scss' scoped></style>
|
62
src/views/system/SystemFloor.vue
Normal file
62
src/views/system/SystemFloor.vue
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import EffectScatter from "@/components/chart/EffectScatter.vue";
|
||||||
|
import { inject, ref, watch } from 'vue';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
const { currentFloor, subscribeData } = inject("system_deviceList")
|
||||||
|
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||||
|
|
||||||
|
const asset_floor_chart = ref(null);
|
||||||
|
const defaultOption = (map, data = []) => ({
|
||||||
|
tooltip: {},
|
||||||
|
geo: {
|
||||||
|
tooltip: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
map,
|
||||||
|
roam: true, // 一定要关闭拖拽
|
||||||
|
},
|
||||||
|
series: {
|
||||||
|
type: "effectScatter",
|
||||||
|
coordinateSystem: "geo",
|
||||||
|
geoIndex: 0,
|
||||||
|
symbolSize: 10,
|
||||||
|
itemStyle: {
|
||||||
|
color: "#b02a02",
|
||||||
|
},
|
||||||
|
encode: {
|
||||||
|
tooltip: 2,
|
||||||
|
},
|
||||||
|
data,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
watch([() => currentFloor, () => asset_floor_chart], ([newValue, newChart]) => {
|
||||||
|
if (newValue.value && newChart.value) {
|
||||||
|
asset_floor_chart.value.updateSvg(
|
||||||
|
{
|
||||||
|
full_name: newValue.value?.title,
|
||||||
|
path: `${FILE_BASEURL}/upload/floor_map/${newValue.value.map_url}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
defaultOption(newValue.value?.title, subscribeData.value.filter(d => d.device_coordinate).map(d => JSON.parse(d.device_coordinate)))
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<EffectScatter id="system_floor_chart" ref="asset_floor_chart" :class="twMerge(
|
||||||
|
currentFloor?.key ? 'opacity-100' : 'opacity-0'
|
||||||
|
)
|
||||||
|
" />
|
||||||
|
<!-- <div class="text-lg" v-if="!currentFloor?.key">尚未上傳樓層平面圖</div> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='scss' scoped></style>
|
11
src/views/system/SystemMain.vue
Normal file
11
src/views/system/SystemMain.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import SystemCard from './components/SystemCard.vue';
|
||||||
|
const route = useRoute()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<SystemCard />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='scss' scoped></style>
|
122
src/views/system/components/SystemCard.vue
Normal file
122
src/views/system/components/SystemCard.vue
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
<script setup>
|
||||||
|
import { inject } from "vue"
|
||||||
|
|
||||||
|
const { data } = inject("system_deviceList")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- <InfoModal :data="currentInfoModalData" /> -->
|
||||||
|
|
||||||
|
<template v-if="data.length > 0">
|
||||||
|
<div class="equipment-show" v-for="d in data" :key="d.full_name">
|
||||||
|
<template v-if="d.device_list.length > 0">
|
||||||
|
<p class="title">{{ d.full_name }}</p>
|
||||||
|
<div class="grid grid-cols-3 gap-5">
|
||||||
|
<div class="col-auto relative" v-for="device in d.device_list" :key="device.device_guid">
|
||||||
|
<div class="item">
|
||||||
|
<div class="left w-4/5">
|
||||||
|
<div class="sec02">
|
||||||
|
<img v-if="device.device_image_url" :src="device.device_image_url" alt="" class="w-8 h-8">
|
||||||
|
<span class="w-8 h-8" v-else></span>
|
||||||
|
<span>{{ device.full_name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="sec03">
|
||||||
|
<span></span>
|
||||||
|
<span>狀態:</span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='css' scoped>
|
||||||
|
/*設備顯示*/
|
||||||
|
.title {
|
||||||
|
@apply text-lg text-white relative inline-block mb-5 after:absolute after:-bottom-2 after:left-1/4 after:w-4/5 after:h-[1px] after:bg-info;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
@apply flex items-center border border-success py-4 px-5 relative mb-5 after:absolute after:right-0 after:top-3 after:w-6 after:h-10 after:bg-[url(/src/assets/img/equipment/state-background.svg)] after:z-10;
|
||||||
|
;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-show .item .sec01 span:nth-child(1) {
|
||||||
|
font-size: 1rem;
|
||||||
|
position: relative;
|
||||||
|
margin-right: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-show .item .sec01 span:nth-child(1)::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 5px;
|
||||||
|
right: -47px;
|
||||||
|
display: block;
|
||||||
|
transform: translateY(50%);
|
||||||
|
width: 45px;
|
||||||
|
height: 1px;
|
||||||
|
background-color: #35eded;
|
||||||
|
border-radius: 25px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-show .item .sec01 span:nth-child(2) {
|
||||||
|
font-size: .6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-show .item .sec02 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-show .item .sec02::after {
|
||||||
|
content: "";
|
||||||
|
background: url(/src/assets/img/equipment/state-title.svg) center center;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
bottom: -10px;
|
||||||
|
height: 25px;
|
||||||
|
width: 105px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-show .item .sec02 span:nth-child(1) {
|
||||||
|
background-color: #31afdf;
|
||||||
|
border: 3px solid #fff;
|
||||||
|
display: block;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-show .item .sec02 span:nth-child(2) {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-show .item .sec03 {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.equipment-show .item .sec03 span:nth-child(1) {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border-radius: 100%;
|
||||||
|
display: block;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
86
src/views/system/components/SystemFloorBar.vue
Normal file
86
src/views/system/components/SystemFloorBar.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<script setup>
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import { getSystemFloors } from "@/apis/system"
|
||||||
|
import { onMounted, ref, watch, inject } from 'vue';
|
||||||
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
|
import useActiveBtn from "@/hooks/useActiveBtn"
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const store = useBuildingStore();
|
||||||
|
|
||||||
|
const { updateCurrentFloor } = inject("system_deviceList")
|
||||||
|
|
||||||
|
|
||||||
|
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
||||||
|
const getFloors = async () => {
|
||||||
|
const res = await getSystemFloors(store.selectedBuilding?.building_tag, route.params.sub_system_id)
|
||||||
|
setItems([
|
||||||
|
{
|
||||||
|
title: "總覽",
|
||||||
|
key: "main",
|
||||||
|
active: route.params.floor_id ? false : true,
|
||||||
|
},
|
||||||
|
...res.data.map((d, idx) => ({
|
||||||
|
title: d.floor_tag,
|
||||||
|
key: d.floor_guid,
|
||||||
|
active: route.params.floor_id === d.floor_guid,
|
||||||
|
map_url: d.floor_map_name
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClick = (item) => {
|
||||||
|
changeActiveBtn(item)
|
||||||
|
updateCurrentFloor(item)
|
||||||
|
if (item.key == "main") {
|
||||||
|
router.push({
|
||||||
|
name: 'sub_system', params: {
|
||||||
|
...route.params
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
router.push({
|
||||||
|
name: 'floor', params: {
|
||||||
|
...route.params, floor_id: item.key
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(selectedBtn, (newValue) => {
|
||||||
|
console.log(newValue);
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => route.params.sub_system_id, (newValue, oldValue) => {
|
||||||
|
console.log(newValue !== oldValue)
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
setItems(items.value.map((item, index) => ({ ...item, active: index === 0 })));
|
||||||
|
}
|
||||||
|
|
||||||
|
}, {
|
||||||
|
deep: true
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => store.selectedBuilding, (newValue) => {
|
||||||
|
console.log(newValue)
|
||||||
|
newValue && getFloors()
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center border border-info px-4 py-2">
|
||||||
|
<h5 class="text-md font-extrabold me-4">{{ store.selectedSystem?.full_name }}</h5>
|
||||||
|
<ButtonGroup :items="items" :withLine="false" className="btn-xs rounded-md" :onclick="(e, item) => onClick(item)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='scss' scoped></style>
|
34
src/views/system/components/SystemSubBar.vue
Normal file
34
src/views/system/components/SystemSubBar.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<script setup>
|
||||||
|
import useActiveBtn from "@/hooks/useActiveBtn"
|
||||||
|
import { inject, watch } from "vue";
|
||||||
|
import useBuildingStore from "@/stores/useBuildingStore"
|
||||||
|
|
||||||
|
const { updateDataByGas } = inject("system_deviceList")
|
||||||
|
const buildingStore = useBuildingStore()
|
||||||
|
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
||||||
|
|
||||||
|
watch(() => buildingStore, (newValue) => {
|
||||||
|
newValue.selectedSystem?.points?.length > 0 && setItems(newValue.selectedSystem.points.map(d => ({
|
||||||
|
title: d.full_name,
|
||||||
|
key: d.points,
|
||||||
|
active: false,
|
||||||
|
})))
|
||||||
|
}, {
|
||||||
|
deep: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onClick = (item) => {
|
||||||
|
changeActiveBtn(item)
|
||||||
|
updateDataByGas(item.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<template v-if="items.length > 0">
|
||||||
|
<ButtonGroup :items="items" :withLine="false" className="btn-xs rounded-md" :onclick="(e, item) => onClick(item)" />
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang='scss' scoped></style>
|
Loading…
Reference in New Issue
Block a user