介面優化
This commit is contained in:
parent
a5dcec3045
commit
8ba6905047
@ -3,6 +3,10 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.css"
|
||||||
|
/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>監控系統</title>
|
<title>監控系統</title>
|
||||||
<script type="text/javascript" src="/requirejs/config.js?"></script>
|
<script type="text/javascript" src="/requirejs/config.js?"></script>
|
||||||
@ -13,6 +17,7 @@
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.js"></script>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
9
package-lock.json
generated
9
package-lock.json
generated
@ -16,7 +16,7 @@
|
|||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.3",
|
"tailwindcss": "^4.0.3",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
@ -1926,10 +1926,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
|
||||||
"integrity": "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g==",
|
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/dcastil"
|
"url": "https://github.com/sponsors/dcastil"
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"echarts": "^5.6.0",
|
"echarts": "^5.6.0",
|
||||||
"pinia": "^2.3.1",
|
"pinia": "^2.3.1",
|
||||||
"tailwind-merge": "^3.0.1",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.3",
|
"tailwindcss": "^4.0.3",
|
||||||
"vue": "^3.5.13",
|
"vue": "^3.5.13",
|
||||||
"vue-router": "^4.5.0"
|
"vue-router": "^4.5.0"
|
||||||
|
@ -44,7 +44,7 @@ onMounted(async () => {
|
|||||||
:style="{
|
:style="{
|
||||||
margin: '5px',
|
margin: '5px',
|
||||||
padding: '0px',
|
padding: '0px',
|
||||||
background: '#fff',
|
background: '#fafafa',
|
||||||
minHeight: '280px',
|
minHeight: '280px',
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
|
@ -5,95 +5,62 @@ import useAlarmDataStore from "@/stores/useAlarmDataStore";
|
|||||||
|
|
||||||
const niagaraStore = useNiagaraDataStore();
|
const niagaraStore = useNiagaraDataStore();
|
||||||
const alarmDataStore = useAlarmDataStore();
|
const alarmDataStore = useAlarmDataStore();
|
||||||
|
|
||||||
// 定義表格欄位
|
|
||||||
const columns = [
|
|
||||||
{ title: "Name", key: "name" },
|
|
||||||
{ title: "In Alarm Count", key: "alarmCount" },
|
|
||||||
{ title: "Unacked Count", key: "unackedCount" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 初始化資料與訂閱更新
|
|
||||||
const initializeAlarmData = () => {
|
|
||||||
// 使用 useAlarmDataStore 建立 alarmData
|
|
||||||
if(!alarmDataStore.alarmData.length){
|
|
||||||
alarmDataStore.createAlarmData(niagaraStore.alarmList);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 對每個 alarm 項目建立訂閱
|
|
||||||
alarmDataStore.alarmData.forEach((alarm, index) => {
|
|
||||||
if (!alarm.alarmOrd) return;
|
|
||||||
|
|
||||||
window.require &&
|
|
||||||
window.requirejs(["baja!"], (baja) => {
|
|
||||||
// 建立訂閱器
|
|
||||||
const subscriber = new baja.Subscriber();
|
|
||||||
// 定義 changed 事件的處理函數
|
|
||||||
subscriber.attach("changed", (prop) => {
|
|
||||||
// console.log("prop", prop.$getDisplayName(), prop.$getValue());
|
|
||||||
try {
|
|
||||||
let alarmCount = alarmDataStore.alarmData[index].alarmCount;
|
|
||||||
let unackedCount = alarmDataStore.alarmData[index].unackedCount;
|
|
||||||
|
|
||||||
if (prop.$getDisplayName() === "In Alarm Count") {
|
|
||||||
// 取得 In Alarm Count
|
|
||||||
alarmCount = prop.$getValue();
|
|
||||||
}
|
|
||||||
if (prop.$getDisplayName() === "Unacked Alarm Count") {
|
|
||||||
// 取得 Unacked Alarm Count
|
|
||||||
unackedCount = prop.$getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新 useAlarmDataStore 中的資料
|
|
||||||
alarmDataStore.updateAlarmItem(index, alarmCount, unackedCount);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`處理 ${alarm.name || index} 告警變化失敗: ${error.message}`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 訂閱 alarm 資訊
|
|
||||||
baja.Ord.make(alarm.alarmOrd)
|
|
||||||
.get({ subscriber })
|
|
||||||
.then((result) => {
|
|
||||||
console.log(`Successfuly subscribed to alarm ${alarm.name}`);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(
|
|
||||||
`訂閱 Alarm ${alarm.name || index} 失敗: ${err.message}`
|
|
||||||
);
|
|
||||||
subscriber.detach("changed");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => niagaraStore.alarmList,
|
|
||||||
(newValue, oldValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
console.log("niagaraStore.alarmList changed:", newValue);
|
|
||||||
initializeAlarmData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true } // 立即執行一次
|
|
||||||
);
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<a-card class="card">
|
||||||
<a-table
|
<h5>Alarm</h5>
|
||||||
:columns="columns"
|
<table className="table w-full">
|
||||||
:data-source="alarmDataStore.alarmData"
|
<thead>
|
||||||
bordered
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>In Alarm Count</th>
|
||||||
|
<th>Unacked Count</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(item, index) in alarmDataStore.alarmData" :key="index">
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.alarmCount }}</td>
|
||||||
|
<td>{{ item.unackedCount }}</td>
|
||||||
|
<td>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'baja',
|
||||||
|
query: { ord: encodeURIComponent(item.Ord) },
|
||||||
|
}"
|
||||||
|
class="flex items-center justify-between gap-8"
|
||||||
|
>view</router-link
|
||||||
>
|
>
|
||||||
<template #bodyCell="{ column, record }">
|
</td>
|
||||||
<span>{{ record[column.key] }}</span>
|
</tr>
|
||||||
</template>
|
</tbody>
|
||||||
</a-table>
|
</table>
|
||||||
</div>
|
</a-card>
|
||||||
</template>
|
</template>
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 20px 27px rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #141414;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 8px 0;
|
||||||
|
color: #8c8c8c;
|
||||||
|
}
|
||||||
|
.table td {
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 16px 0px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
72
src/components/dashboard/dashboardBuild.vue
Normal file
72
src/components/dashboard/dashboardBuild.vue
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from "vue";
|
||||||
|
import Forge from "@/components/forge/Forge.vue";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||||
|
const type = ref("3d");
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<a-card class="card">
|
||||||
|
<div class="w-full relative">
|
||||||
|
<img
|
||||||
|
alt="build"
|
||||||
|
:src="`${FILE_BASEURL}/file/UI_images/build/2D/build.jpg`"
|
||||||
|
:class="
|
||||||
|
twMerge(
|
||||||
|
'absolute left-1/2 translate-x-[-50%] rounded shadow-lg transition-opacity duration-300',
|
||||||
|
type == '2d' ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
style="width: 400px; height: 400px; vertical-align: middle"
|
||||||
|
/>
|
||||||
|
<Forge
|
||||||
|
:class="
|
||||||
|
twMerge(
|
||||||
|
'absolute transition-opacity duration-300',
|
||||||
|
type == '3d' ? 'opacity-100 z-10' : 'opacity-0 z-0'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center justify-between mt-2">
|
||||||
|
<div>
|
||||||
|
<h5>Building</h5>
|
||||||
|
<p class="text-gray-400">
|
||||||
|
掌握建築用電、系統健康狀態,打造智慧節能醫院
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a-radio-group v-model:value="type">
|
||||||
|
<a-radio-button value="2d">2D</a-radio-button>
|
||||||
|
<a-radio-button value="3d">3D</a-radio-button>
|
||||||
|
</a-radio-group>
|
||||||
|
</div>
|
||||||
|
<!-- <a-row>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="用戶數" :value="3.6" suffix="萬" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="智慧電錶" :value="82" suffix="個" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="電費支出" :value="-7.2" suffix="萬" />
|
||||||
|
</a-col>
|
||||||
|
<a-col :span="6">
|
||||||
|
<a-statistic title="警示次數" :value="15" suffix="次" />
|
||||||
|
</a-col>
|
||||||
|
</a-row> -->
|
||||||
|
</a-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 20px 27px rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #141414;
|
||||||
|
}
|
||||||
|
</style>
|
@ -3,41 +3,80 @@ const mockData = [
|
|||||||
{
|
{
|
||||||
value: 305.5,
|
value: 305.5,
|
||||||
label: "今日用電量",
|
label: "今日用電量",
|
||||||
unit:"kWH"
|
unit: "kWH",
|
||||||
|
icon: "leaf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 886.75,
|
value: 886.75,
|
||||||
label: "昨日用電量",
|
label: "昨日用電量",
|
||||||
unit:"kWH"
|
unit: "kWH",
|
||||||
|
icon: "leaf",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 7.84,
|
value: 7.84,
|
||||||
label: "即時功率",
|
label: "即時功率",
|
||||||
unit:"kW"
|
unit: "kW",
|
||||||
|
icon: "bolt",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 20.96,
|
value: 20.96,
|
||||||
label: "容積占比",
|
label: "容積占比",
|
||||||
unit:"%"
|
unit: "%",
|
||||||
|
icon: "charging-station",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a-row :gutter="24" class="p-5">
|
<a-row :gutter="24">
|
||||||
<a-col v-for="(item, index) in mockData" :key="index" :span="6">
|
<a-col v-for="(item, index) in mockData" :key="index" :span="6" class="mb-5">
|
||||||
<a-card class="shadow">
|
<a-card class="number">
|
||||||
<a-statistic
|
<a-row :gutter="24" align="middle" justify="space-between">
|
||||||
:title="item.label"
|
<a-col :span="18">
|
||||||
:value="item.value"
|
<span>{{ item.label }}</span>
|
||||||
:precision="2"
|
<h3>
|
||||||
:suffix="item.unit"
|
{{ item.value }}<small>{{ item.unit }}</small>
|
||||||
:value-style="{ color: index % 2 === 0 ? '#3f8600' : '#1677ff' }"
|
</h3>
|
||||||
>
|
</a-col>
|
||||||
</a-statistic>
|
<a-col :span="6" class="pl-0">
|
||||||
|
<div class="icon-box">
|
||||||
|
<font-awesome-icon :icon="['fas', item.icon]" size="2x"/>
|
||||||
|
</div>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped></style>
|
<style scoped>
|
||||||
|
.number {
|
||||||
|
box-shadow: 0 20px 27px rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
.number span {
|
||||||
|
color: #8c8c8c;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.number h3 {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
.number h3 small {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #8fbce6;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-box {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
text-align: center;
|
||||||
|
background: #1890ff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-left: auto;
|
||||||
|
line-height: 55px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,48 +1,45 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from "vue";
|
import { ref, computed, watch } from "vue";
|
||||||
|
import NavBuild from "../navbar/NavBuild.vue";
|
||||||
import useNavDataStore from "@/stores/useNavDataStore";
|
import useNavDataStore from "@/stores/useNavDataStore";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const navStore = useNavDataStore();
|
const navStore = useNavDataStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const filteredItems = computed(() => {
|
const flatten = (items) => {
|
||||||
const flattenedItems = [];
|
const flattenedItems = [];
|
||||||
const selectedBuildingOrd = navStore.selectedBuildingOrd;
|
|
||||||
// 遞迴函數,用於展開多層級的選單項目
|
|
||||||
const flatten = (items) => {
|
|
||||||
if (!items) return;
|
|
||||||
|
|
||||||
|
const recurse = (items) => {
|
||||||
|
if (!items) return;
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
// 過濾掉 ord 為 null 的項目
|
|
||||||
if (item.ord !== "null") {
|
if (item.ord !== "null") {
|
||||||
flattenedItems.push({
|
flattenedItems.push(item);
|
||||||
key: item.key,
|
|
||||||
name: item.title,
|
|
||||||
ord: item.ord,
|
|
||||||
icon: item.icon,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// 如果有子項目,則遞迴處理
|
|
||||||
if (item.children) {
|
if (item.children) {
|
||||||
flatten(item.children);
|
recurse(item.children);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// 展開選單項目
|
recurse(items);
|
||||||
|
|
||||||
|
return flattenedItems;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenedItems = computed(() => {
|
||||||
|
const selectedBuildingOrd = navStore.selectedBuildingOrd;
|
||||||
if (
|
if (
|
||||||
navStore.menuList &&
|
navStore.menuList &&
|
||||||
navStore.menuList.length > 0 &&
|
navStore.menuList.length > 0 &&
|
||||||
selectedBuildingOrd
|
selectedBuildingOrd
|
||||||
) {
|
) {
|
||||||
const buildingMenu = navStore.menuList.find(
|
const buildingMenu = navStore.menuList[0].children.find(
|
||||||
(item) => item.label === selectedBuildingOrd
|
(item) => item.label === selectedBuildingOrd
|
||||||
);
|
);
|
||||||
flatten(buildingMenu.children);
|
return flatten(buildingMenu.children);
|
||||||
}
|
}
|
||||||
|
return [];
|
||||||
return flattenedItems;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClick = (ord) => {
|
const handleClick = (ord) => {
|
||||||
@ -56,9 +53,14 @@ const handleClick = (ord) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="filteredItems && filteredItems.length > 0">
|
<a-card class="card h-full">
|
||||||
|
<div class="flex items-cemter justify-between mb-4">
|
||||||
|
<h5>System</h5>
|
||||||
|
<NavBuild />
|
||||||
|
</div>
|
||||||
|
<div v-if="flattenedItems.length > 0">
|
||||||
<a-row :gutter="[8, 16]">
|
<a-row :gutter="[8, 16]">
|
||||||
<a-col :span="6" v-for="(item, index) in filteredItems" :key="index">
|
<a-col :span="6" v-for="item in flattenedItems" :key="item.key">
|
||||||
<a-card
|
<a-card
|
||||||
@click="handleClick(item.ord)"
|
@click="handleClick(item.ord)"
|
||||||
class="shadow"
|
class="shadow"
|
||||||
@ -75,12 +77,23 @@ const handleClick = (ord) => {
|
|||||||
alt="Icon"
|
alt="Icon"
|
||||||
style="width: 30px; height: 30px; vertical-align: middle"
|
style="width: 30px; height: 30px; vertical-align: middle"
|
||||||
/>
|
/>
|
||||||
<span class="text-lg">{{ item.name }}</span>
|
<span class="text-lg">{{ item.label }}</span>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>No items to display.</div>
|
<div v-else>No items to display.</div>
|
||||||
|
</a-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped></style>
|
<style scoped>
|
||||||
|
.card {
|
||||||
|
box-shadow: 0 20px 27px rgb(0 0 0 / 5%);
|
||||||
|
}
|
||||||
|
h5 {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #141414;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
86
src/components/forge/Forge.vue
Normal file
86
src/components/forge/Forge.vue
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
|
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||||
|
const forgeDom = ref(null);
|
||||||
|
let viewer = null;
|
||||||
|
|
||||||
|
// 初始化 Forge Viewer
|
||||||
|
const initViewer = (container) => {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
Autodesk.Viewing.Initializer(
|
||||||
|
{
|
||||||
|
env: "Local",
|
||||||
|
language: "en",
|
||||||
|
},
|
||||||
|
function () {
|
||||||
|
const config = {
|
||||||
|
extensions: [
|
||||||
|
"Autodesk.DataVisualization",
|
||||||
|
"Autodesk.DocumentBrowser",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
|
||||||
|
Autodesk.Viewing.Private.InitParametersSetting.alpha = true;
|
||||||
|
viewer.start();
|
||||||
|
viewer.setGroundShadow(false);
|
||||||
|
viewer.impl.renderer().setClearAlpha(0);
|
||||||
|
viewer.impl.glrenderer().setClearColor(0xffffff, 0);
|
||||||
|
viewer.impl.invalidate(true);
|
||||||
|
resolve(viewer);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用本地 .svf 文件加載模型
|
||||||
|
const loadModel = (filePath) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
viewer.loadModel(
|
||||||
|
filePath,
|
||||||
|
{},
|
||||||
|
(model) => {
|
||||||
|
viewer.impl.invalidate(true);
|
||||||
|
viewer.fitToView();
|
||||||
|
resolve(model);
|
||||||
|
console.log("模型加載完成");
|
||||||
|
},
|
||||||
|
reject
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
console.log("Forge 加載");
|
||||||
|
await initViewer(forgeDom.value);
|
||||||
|
const filePath = `${FILE_BASEURL}/file/UI_images/build/3D/0.svf`;
|
||||||
|
loadModel(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
console.log("Forge 銷毀");
|
||||||
|
if (viewer) {
|
||||||
|
viewer.tearDown();
|
||||||
|
viewer.finish();
|
||||||
|
viewer = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
id="forge-preview"
|
||||||
|
ref="forgeDom"
|
||||||
|
class="relative w-full h-full min-h-[400px]"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
.adsk-viewing-viewer {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#guiviewer3d-toolbar {
|
||||||
|
display: none;
|
||||||
|
bottom: 200px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -12,20 +12,13 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const navStore = useNavDataStore();
|
const navStore = useNavDataStore();
|
||||||
const selectedKeys = ref([]); // 用於儲存選中的 menu item
|
// const selectedKeys = ref([]); // 用於儲存選中的 menu item
|
||||||
const openKeys = ref([]); // 用於儲存展開的 submenu
|
const openKeys = ref([]); // 用於儲存展開的 submenu
|
||||||
const preOpenKeys = ref([]); // 用於儲存之前展開的 submenu
|
const preOpenKeys = ref([]); // 用於儲存之前展開的 submenu
|
||||||
|
|
||||||
const filteredItems = computed(() => {
|
const filteredItems = computed(() => {
|
||||||
const selectedBuildingOrd = navStore.selectedBuildingOrd;
|
if (navStore.menuList && navStore.menuList.length > 0) {
|
||||||
if (
|
return navStore.menuList[0].children;
|
||||||
navStore.menuList &&
|
|
||||||
navStore.menuList.length > 0 &&
|
|
||||||
selectedBuildingOrd
|
|
||||||
) {
|
|
||||||
const buildingMenu = navStore.menuList[0].children.find(
|
|
||||||
(item) => item.key === selectedBuildingOrd
|
|
||||||
);
|
|
||||||
return buildingMenu.children;
|
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
@ -60,13 +53,36 @@ watch(
|
|||||||
>
|
>
|
||||||
<a-menu mode="inline" theme="light">
|
<a-menu mode="inline" theme="light">
|
||||||
<template v-for="(item, index) in filteredItems" :key="index">
|
<template v-for="(item, index) in filteredItems" :key="index">
|
||||||
<a-menu-item v-if="!item.children" :key="item.key" @click="handleClick(item.ord)">
|
<a-menu-item
|
||||||
|
v-if="!item.children"
|
||||||
|
:key="item.key"
|
||||||
|
@click="handleClick(item.ord)"
|
||||||
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<a-sub-menu v-else :title="item.label" :key="`submenu-${item.key}`">
|
<a-sub-menu v-else :title="item.label" :key="`submenu-${item.key}`">
|
||||||
<a-menu-item v-for="child in item.children" :key="child.key" @click="handleClick(child.ord)">
|
<template v-for="child in item.children" :key="child.key">
|
||||||
|
<a-menu-item
|
||||||
|
v-if="!child.children"
|
||||||
|
:key="child.key"
|
||||||
|
@click="handleClick(child.ord)"
|
||||||
|
>
|
||||||
{{ child.label }}
|
{{ child.label }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
|
<a-sub-menu
|
||||||
|
v-else
|
||||||
|
:title="child.label"
|
||||||
|
:key="`submenu-item-${child.key}`"
|
||||||
|
>
|
||||||
|
<a-menu-item
|
||||||
|
v-for="kid in child.children"
|
||||||
|
:key="kid.key"
|
||||||
|
@click="handleClick(kid.ord)"
|
||||||
|
>
|
||||||
|
{{ kid.label }}
|
||||||
|
</a-menu-item>
|
||||||
|
</a-sub-menu>
|
||||||
|
</template>
|
||||||
</a-sub-menu>
|
</a-sub-menu>
|
||||||
</template>
|
</template>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
|
@ -1,71 +1,23 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, computed } from "vue";
|
import { ref, computed, onBeforeUnmount, watch } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
|
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
|
||||||
import useAlarmDataStore from "@/stores/useAlarmDataStore";
|
import useAlarmDataStore from "@/stores/useAlarmDataStore";
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
const niagaraStore = useNiagaraDataStore();
|
const niagaraStore = useNiagaraDataStore();
|
||||||
const alarmDataStore = useAlarmDataStore();
|
const alarmDataStore = useAlarmDataStore();
|
||||||
|
|
||||||
// 計算 alarmCount 的總和
|
// 計算 alarmCount 的總和
|
||||||
const totalAlarmCount = computed(() => {
|
const totalAlarmCount = computed(() => {
|
||||||
return alarmDataStore.alarmData.reduce((total, child) => total + (child.alarmCount || 0), 0);
|
return alarmDataStore.alarmData.reduce(
|
||||||
|
(total, child) => total + (child.alarmCount || 0),
|
||||||
|
0
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const initializeAlarmData = () => {
|
const initializeAlarmData = () => {
|
||||||
// 使用 useAlarmDataStore 建立 alarmData
|
|
||||||
if (!alarmDataStore.alarmData.length) {
|
|
||||||
alarmDataStore.createAlarmData(niagaraStore.alarmList);
|
alarmDataStore.createAlarmData(niagaraStore.alarmList);
|
||||||
}
|
|
||||||
|
|
||||||
// 對每個 alarm 項目建立訂閱
|
|
||||||
alarmDataStore.alarmData.forEach((alarm, index) => {
|
|
||||||
if (!alarm.alarmOrd) return;
|
|
||||||
|
|
||||||
window.require &&
|
|
||||||
window.requirejs(["baja!"], (baja) => {
|
|
||||||
// 建立訂閱器
|
|
||||||
const subscriber = new baja.Subscriber();
|
|
||||||
// 定義 changed 事件的處理函數
|
|
||||||
subscriber.attach("changed", (prop) => {
|
|
||||||
// console.log("prop", prop.$getDisplayName(), prop.$getValue());
|
|
||||||
try {
|
|
||||||
let alarmCount = alarmDataStore.alarmData[index].alarmCount;
|
|
||||||
let unackedCount = alarmDataStore.alarmData[index].unackedCount;
|
|
||||||
|
|
||||||
if (prop.$getDisplayName() === "In Alarm Count") {
|
|
||||||
// 取得 In Alarm Count
|
|
||||||
alarmCount = prop.$getValue();
|
|
||||||
}
|
|
||||||
if (prop.$getDisplayName() === "Unacked Alarm Count") {
|
|
||||||
// 取得 Unacked Alarm Count
|
|
||||||
unackedCount = prop.$getValue();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新 useAlarmDataStore 中的資料
|
|
||||||
alarmDataStore.updateAlarmItem(index, alarmCount, unackedCount);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`處理 ${alarm.name || index} 告警變化失敗: ${error.message}`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 訂閱 alarm 資訊
|
|
||||||
baja.Ord.make(alarm.alarmOrd)
|
|
||||||
.get({ subscriber })
|
|
||||||
.then((result) => {
|
|
||||||
console.log(`Successfuly subscribed to alarm ${alarm.name}`);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(
|
|
||||||
`訂閱 Alarm ${alarm.name || index} 失敗: ${err.message}`
|
|
||||||
);
|
|
||||||
subscriber.detach("changed");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -76,8 +28,12 @@ watch(
|
|||||||
initializeAlarmData();
|
initializeAlarmData();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true } // 立即執行一次
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
alarmDataStore.clearAllSubscriber();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -98,8 +54,7 @@ watch(
|
|||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</template>
|
</template>
|
||||||
<a-badge :count="totalAlarmCount" :overflow-count="999"
|
<a-badge :count="totalAlarmCount" :overflow-count="999">
|
||||||
>
|
|
||||||
<a class="flex flex-col items-center">
|
<a class="flex flex-col items-center">
|
||||||
<font-awesome-icon :icon="['fas', 'bell']" size="2x" />
|
<font-awesome-icon :icon="['fas', 'bell']" size="2x" />
|
||||||
<span class="text-sm">告警</span>
|
<span class="text-sm">告警</span>
|
||||||
|
@ -8,8 +8,8 @@ const router = useRouter();
|
|||||||
|
|
||||||
const buildmenu = ref([]);
|
const buildmenu = ref([]);
|
||||||
|
|
||||||
const handleBuildClick = (key) => {
|
const handleBuildClick = (label) => {
|
||||||
navStore.setSelectedBuildingOrd(key);
|
navStore.setSelectedBuildingOrd(label);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -17,7 +17,7 @@ watch(
|
|||||||
(newValue, oldValue) => {
|
(newValue, oldValue) => {
|
||||||
if (newValue && newValue.length > 0 && newValue[0].children) {
|
if (newValue && newValue.length > 0 && newValue[0].children) {
|
||||||
buildmenu.value = newValue[0].children;
|
buildmenu.value = newValue[0].children;
|
||||||
navStore.setSelectedBuildingOrd(newValue[0].children[0].key);
|
navStore.setSelectedBuildingOrd(newValue[0].children[0].label);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@ -27,14 +27,14 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<a-select
|
<a-select
|
||||||
v-if="buildmenu && buildmenu.length > 0"
|
v-if="buildmenu && buildmenu.length > 0"
|
||||||
:default-value="buildmenu[0] ? buildmenu[0].key : null"
|
:default-value="buildmenu[0] ? buildmenu[0].label : null"
|
||||||
@change="handleBuildClick"
|
@change="handleBuildClick"
|
||||||
placeholder="請選擇建築"
|
placeholder="請選擇建築"
|
||||||
>
|
>
|
||||||
<a-select-option
|
<a-select-option
|
||||||
v-for="item in buildmenu"
|
v-for="item in buildmenu"
|
||||||
:key="item.key"
|
:key="item.key"
|
||||||
:value="item.key"
|
:value="item.label"
|
||||||
>
|
>
|
||||||
{{ item.label }}
|
{{ item.label }}
|
||||||
</a-select-option>
|
</a-select-option>
|
||||||
|
@ -1,32 +1,27 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
import { ref, onMounted, onUnmounted } from "vue";
|
||||||
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
|
|
||||||
import {
|
import {
|
||||||
imagesWeatherDay,
|
imagesWeatherDay,
|
||||||
imagesWeatherNight,
|
imagesWeatherNight,
|
||||||
orderWeather,
|
orderWeather,
|
||||||
} from "@/constants";
|
} from "@/constants";
|
||||||
|
|
||||||
const niagaraStore = useNiagaraDataStore();
|
|
||||||
|
|
||||||
const weatherStateText = ref("N/A");
|
const weatherStateText = ref("N/A");
|
||||||
const weatherStateImage = ref(null);
|
const weatherStateImage = ref(null);
|
||||||
const actualWeather = ref("N/A");
|
const actualWeather = ref("Clear");
|
||||||
const temperature = ref("N/A");
|
const temperature = ref("N/A");
|
||||||
const humidity = ref("N/A");
|
const humidity = ref("N/A");
|
||||||
const actualNighttime = ref("N/A");
|
const actualNighttime = ref(false);
|
||||||
const dateTime = ref("N/A");
|
const dateTime = ref("N/A");
|
||||||
|
|
||||||
let intervalId = null;
|
let intervalId = null;
|
||||||
|
let subscriber = null; // 宣告 subscriber 變數
|
||||||
|
|
||||||
// 初始化資料與訂閱更新
|
// 初始化資料與訂閱更新
|
||||||
const initializeData = () => {
|
const initializeData = () => {
|
||||||
niagaraStore.weatherList.children.forEach((item, index) => {
|
if (window.require && window.requirejs) {
|
||||||
if (!item.ord) return;
|
|
||||||
window.require &&
|
|
||||||
window.requirejs(["baja!"], (baja) => {
|
window.requirejs(["baja!"], (baja) => {
|
||||||
// 將 subscriber 移到此處定義
|
subscriber = new baja.Subscriber(); // 初始化 subscriber
|
||||||
const subscriber = new baja.Subscriber();
|
|
||||||
subscriber.attach("changed", (prop) => {
|
subscriber.attach("changed", (prop) => {
|
||||||
console.log(
|
console.log(
|
||||||
"weather",
|
"weather",
|
||||||
@ -35,49 +30,56 @@ const initializeData = () => {
|
|||||||
prop.$getValue().getValueDisplay()
|
prop.$getValue().getValueDisplay()
|
||||||
);
|
);
|
||||||
|
|
||||||
if (item.name === "Out temperature" && prop.$displayName === "Out") {
|
// 根據 slotName 判斷資料類型
|
||||||
|
if (prop.$slotName === "temp") {
|
||||||
temperature.value = prop.$getValue().getValueDisplay();
|
temperature.value = prop.$getValue().getValueDisplay();
|
||||||
} else if (
|
} else if (prop.$slotName === "humidity") {
|
||||||
item.name === "Out humidity" &&
|
|
||||||
prop.$displayName === "Out"
|
|
||||||
) {
|
|
||||||
humidity.value = prop.$getValue().getValueDisplay();
|
humidity.value = prop.$getValue().getValueDisplay();
|
||||||
} else if (item.name === "Weather" && prop.$displayName === "Out") {
|
} else if (prop.$slotName === "state") {
|
||||||
actualWeather.value = prop.$getValue().getValueDisplay();
|
actualWeather.value = prop.$getValue().getValueDisplay();
|
||||||
} else if (item.name === "Nighttime" && prop.$displayName === "Out") {
|
loadWeather(actualWeather.value); // 載入天氣資料
|
||||||
actualNighttime.value = !(prop.$getValue().getValueDisplay());
|
} else if (prop.$slotName === "sunDown") {
|
||||||
|
actualNighttime.value = !prop.$getValue().getValueDisplay();
|
||||||
|
loadWeather(actualWeather.value); // 重新載入天氣資料
|
||||||
}
|
}
|
||||||
loadWeather(actualWeather.value);
|
|
||||||
});
|
});
|
||||||
// 將 baja.Ord.make 移到此處
|
|
||||||
baja.Ord.make(item.ord)
|
// 訂閱 Niagara 資料
|
||||||
|
baja.Ord.make(
|
||||||
|
"station:|slot:/Services/WeatherService/WeatherService/WeatherService/current"
|
||||||
|
)
|
||||||
.get({ subscriber })
|
.get({ subscriber })
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
console.log(`Successfuly subscribed to weather ${item.name}`);
|
console.log("Successfuly subscribed to weather data", result);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(
|
console.error(`訂閱 weather 失敗: ${err.message}`);
|
||||||
`訂閱 weather ${item.name || index} 失敗: ${err.message}`
|
subscriber.detach("changed"); // 發生錯誤時取消訂閱
|
||||||
);
|
|
||||||
subscriber.detach("changed");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.error("未能載入 Baja 環境,請確認依賴已正確加載。");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 載入天氣資料函數
|
// 載入天氣資料函數
|
||||||
const loadWeather = (actualWeather) => {
|
const loadWeather = (actualWeather) => {
|
||||||
|
console.log(
|
||||||
|
"actualWeather",
|
||||||
|
actualWeather.replace(/\s/g, ""),
|
||||||
|
actualNighttime.value
|
||||||
|
);
|
||||||
let weatherStateTextTemp = "Unknown";
|
let weatherStateTextTemp = "Unknown";
|
||||||
let weatherStateImageTemp = null;
|
let weatherStateImageTemp = null;
|
||||||
const orderLength = orderWeather.length - 1;
|
|
||||||
|
|
||||||
if (orderLength < actualWeather) {
|
// 找到 actualWeather 在 orderWeather 中的索引
|
||||||
console.error(
|
const actualWeatherIndex = orderWeather.indexOf(
|
||||||
"天氣狀態超出範圍!最大限度:" +
|
actualWeather.replace(/\s/g, "")
|
||||||
orderLength +
|
|
||||||
"| 實際值:" +
|
|
||||||
actualWeather
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (actualWeatherIndex === -1) {
|
||||||
|
// 如果找不到,表示 weather 狀態不在 orderWeather 中
|
||||||
|
console.error("天氣狀態不在 orderWeather 陣列中!實際值:" + actualWeather);
|
||||||
weatherStateTextTemp = "Unknown";
|
weatherStateTextTemp = "Unknown";
|
||||||
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
|
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
|
||||||
} else if (actualWeather === "none") {
|
} else if (actualWeather === "none") {
|
||||||
@ -85,17 +87,17 @@ const loadWeather = (actualWeather) => {
|
|||||||
weatherStateTextTemp = "Unknown";
|
weatherStateTextTemp = "Unknown";
|
||||||
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
|
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
|
||||||
} else {
|
} else {
|
||||||
const actualWeatherNum = Number(actualWeather);
|
|
||||||
if (actualNighttime.value) {
|
if (actualNighttime.value) {
|
||||||
console.log("Nighttime activ");
|
console.log("Nighttime activ");
|
||||||
weatherStateTextTemp = orderWeather[actualWeatherNum];
|
weatherStateTextTemp = orderWeather[actualWeatherIndex];
|
||||||
weatherStateImageTemp = imagesWeatherNight[weatherStateTextTemp];
|
weatherStateImageTemp = imagesWeatherNight[weatherStateTextTemp];
|
||||||
} else {
|
} else {
|
||||||
console.log("Daytime activ");
|
console.log("Daytime activ");
|
||||||
weatherStateTextTemp = orderWeather[actualWeatherNum];
|
weatherStateTextTemp = orderWeather[actualWeatherIndex];
|
||||||
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
|
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
weatherStateImage.value = weatherStateImageTemp;
|
weatherStateImage.value = weatherStateImageTemp;
|
||||||
weatherStateText.value = weatherStateTextTemp; // 更新天氣狀態
|
weatherStateText.value = weatherStateTextTemp; // 更新天氣狀態
|
||||||
};
|
};
|
||||||
@ -103,33 +105,27 @@ const loadWeather = (actualWeather) => {
|
|||||||
// 時間
|
// 時間
|
||||||
const updateTime = () => {
|
const updateTime = () => {
|
||||||
const date = new Date();
|
const date = new Date();
|
||||||
window.require &&
|
if (window.require && window.requirejs) {
|
||||||
window.requirejs(["baja!"], (baja) => {
|
window.requirejs(["baja!"], (baja) => {
|
||||||
const bAbsTime = baja.AbsTime.make({ jsDate: date });
|
const bAbsTime = baja.AbsTime.make({ jsDate: date });
|
||||||
bAbsTime.toDateTimeString().then((dateTimeStr) => {
|
bAbsTime.toDateTimeString().then((dateTimeStr) => {
|
||||||
dateTime.value = dateTimeStr;
|
dateTime.value = dateTimeStr;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(
|
|
||||||
() => niagaraStore.weatherList.children,
|
|
||||||
(newValue, oldValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
console.log("niagaraStore.weatherList changed:", newValue);
|
|
||||||
initializeData();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true } // 立即執行一次
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateTime();
|
initializeData(); // 初始化資料
|
||||||
|
updateTime(); // 更新時間
|
||||||
intervalId = setInterval(updateTime, 1000);
|
intervalId = setInterval(updateTime, 1000);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
|
if (subscriber) {
|
||||||
|
subscriber.detachAll(); // 取消所有訂閱
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -16,15 +16,50 @@ const props = defineProps({
|
|||||||
userName: String,
|
userName: String,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function correctImageUrl(imageUrl) {
|
||||||
|
if (!imageUrl) return ''; // 處理空值的情況
|
||||||
|
|
||||||
|
const stringUrlNiagara = "file:^";
|
||||||
|
const stringUrlNiagara2 = "station:|file:^";
|
||||||
|
const stringUrlNeeded = "/file/";
|
||||||
|
|
||||||
|
if (imageUrl.startsWith(stringUrlNiagara)) {
|
||||||
|
return imageUrl.replace(stringUrlNiagara, stringUrlNeeded);
|
||||||
|
} else if (imageUrl.startsWith(stringUrlNiagara2)) {
|
||||||
|
return imageUrl.replace(stringUrlNiagara2, stringUrlNeeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageUrl; // 如果不是 Niagara 格式,則直接返回
|
||||||
|
}
|
||||||
|
|
||||||
const logoUrl = computed(
|
const logoUrl = computed(
|
||||||
() => niagaraStore.headerList.children?.[0]?.ord || ""
|
() => niagaraStore.headerList.children?.[0]?.ord || ""
|
||||||
);
|
);
|
||||||
const systemName = computed(
|
const systemName = computed(
|
||||||
() => niagaraStore.headerList.children?.[1]?.name || "系統監控"
|
() => niagaraStore.headerList.children?.[1]?.name || "系統監控"
|
||||||
);
|
);
|
||||||
const homeData = computed(() => niagaraStore.headerList.children?.[2] || {});
|
const homeData = computed(() => {
|
||||||
const systemData = computed(() => niagaraStore.headerList.children?.[3] || {});
|
const data = niagaraStore.headerList.children?.[2] || {};
|
||||||
const dynamicMenu = computed(() => niagaraStore.DynamicList.children || []);
|
return {
|
||||||
|
...data,
|
||||||
|
icon: correctImageUrl(data.icon), // 轉換 homeData.icon
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const systemData = computed(() => {
|
||||||
|
const data = niagaraStore.headerList.children?.[3] || {};
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
icon: correctImageUrl(data.icon), // 轉換 systemData.icon
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const dynamicMenu = computed(() =>
|
||||||
|
niagaraStore.DynamicList.children
|
||||||
|
? niagaraStore.DynamicList.children.map((item) => ({
|
||||||
|
...item,
|
||||||
|
icon: correctImageUrl(item.icon), // 轉換 dynamicMenu item 的 icon
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
);
|
||||||
const userList = computed(() => niagaraStore.userList?.children || []);
|
const userList = computed(() => niagaraStore.userList?.children || []);
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@ -36,7 +71,7 @@ const userList = computed(() => niagaraStore.userList?.children || []);
|
|||||||
<a href="./index.html" class="text-2xl font-bold mx-4">{{
|
<a href="./index.html" class="text-2xl font-bold mx-4">{{
|
||||||
systemName
|
systemName
|
||||||
}}</a>
|
}}</a>
|
||||||
<NavBuild />
|
<!-- <NavBuild /> -->
|
||||||
</div>
|
</div>
|
||||||
<ul class="nav-menu flex gap-10">
|
<ul class="nav-menu flex gap-10">
|
||||||
<li>
|
<li>
|
||||||
@ -51,12 +86,10 @@ const userList = computed(() => niagaraStore.userList?.children || []);
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="homeData.icon"
|
|
||||||
:src="homeData.icon"
|
:src="homeData.icon"
|
||||||
alt="home_icon"
|
alt="home_icon"
|
||||||
class="icon"
|
class="icon"
|
||||||
/>
|
/>
|
||||||
<font-awesome-icon v-else :icon="['fas', 'home']" size="2x" />
|
|
||||||
<span class="text-sm">首頁</span>
|
<span class="text-sm">首頁</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
@ -66,13 +99,10 @@ const userList = computed(() => niagaraStore.userList?.children || []);
|
|||||||
@click="props.open"
|
@click="props.open"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
v-if="systemData.icon"
|
|
||||||
:src="systemData.icon"
|
:src="systemData.icon"
|
||||||
alt="system_icon"
|
alt="system_icon"
|
||||||
class="icon"
|
class="icon"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<font-awesome-icon v-else :icon="['fas', 'tv']" size="2x" />
|
|
||||||
<span class="text-sm">系統監控</span>
|
<span class="text-sm">系統監控</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -80,8 +110,7 @@ const userList = computed(() => niagaraStore.userList?.children || []);
|
|||||||
<router-link
|
<router-link
|
||||||
:to="{ name: 'baja', query: { ord: encodeURIComponent(item.ord) } }"
|
:to="{ name: 'baja', query: { ord: encodeURIComponent(item.ord) } }"
|
||||||
>
|
>
|
||||||
<img v-if="item.icon" :src="item.icon" alt="menu_icon" class="icon" />
|
<img :src="item.icon" alt="menu_icon" class="icon" />
|
||||||
<font-awesome-icon v-else :icon="['fas', 'tv']" size="2x" />
|
|
||||||
<span class="text-sm">{{ item.name }}</span>
|
<span class="text-sm">{{ item.name }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</li>
|
</li>
|
||||||
|
10
src/main.js
10
src/main.js
@ -19,7 +19,10 @@ import {
|
|||||||
faUserEdit,
|
faUserEdit,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faSignOutAlt,
|
faSignOutAlt,
|
||||||
faAngleDown
|
faAngleDown,
|
||||||
|
faLeaf,
|
||||||
|
faBolt,
|
||||||
|
faChargingStation
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
library.add(
|
library.add(
|
||||||
faTv,
|
faTv,
|
||||||
@ -34,7 +37,10 @@ library.add(
|
|||||||
faUserEdit,
|
faUserEdit,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faSignOutAlt,
|
faSignOutAlt,
|
||||||
faAngleDown
|
faAngleDown,
|
||||||
|
faLeaf,
|
||||||
|
faBolt,
|
||||||
|
faChargingStation
|
||||||
);
|
);
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from "vue-router";
|
|||||||
import DashboardPage from "@/views/dashboard/DashboardPage.vue";
|
import DashboardPage from "@/views/dashboard/DashboardPage.vue";
|
||||||
import SystemPage from "@/views/system/SystemPage.vue";
|
import SystemPage from "@/views/system/SystemPage.vue";
|
||||||
import EnergyChart from "@/views/energy/EnergyChart.vue";
|
import EnergyChart from "@/views/energy/EnergyChart.vue";
|
||||||
|
import NotFound from "@/views/system/NotFound.vue";
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||||
@ -20,6 +21,11 @@ const router = createRouter({
|
|||||||
path: "/energy",
|
path: "/energy",
|
||||||
name: "energy",
|
name: "energy",
|
||||||
component: EnergyChart,
|
component: EnergyChart,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*', // 匹配所有未定義的路徑
|
||||||
|
name: 'NotFound',
|
||||||
|
component: NotFound
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -4,7 +4,7 @@ import { ref } from "vue";
|
|||||||
|
|
||||||
const useAlarmDataStore = defineStore("alarmData", () => {
|
const useAlarmDataStore = defineStore("alarmData", () => {
|
||||||
const alarmData = ref([]);
|
const alarmData = ref([]);
|
||||||
|
const subscribers = ref([]); // 用於儲存 Subscriber 對象
|
||||||
// 建立 alarmData 的函數
|
// 建立 alarmData 的函數
|
||||||
const createAlarmData = (alarmList) => {
|
const createAlarmData = (alarmList) => {
|
||||||
alarmData.value = alarmList.children.map((item, index) => ({
|
alarmData.value = alarmList.children.map((item, index) => ({
|
||||||
@ -15,6 +15,8 @@ const useAlarmDataStore = defineStore("alarmData", () => {
|
|||||||
alarmOrd: item.children && item.children[0] ? item.children[0].ord : null, // 儲存 alarmOrd
|
alarmOrd: item.children && item.children[0] ? item.children[0].ord : null, // 儲存 alarmOrd
|
||||||
Ord: item.ord ? item.ord : null,
|
Ord: item.ord ? item.ord : null,
|
||||||
}));
|
}));
|
||||||
|
// 在 store 初始化時,進行訂閱
|
||||||
|
subscribeToAlarms();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 更新特定 alarm 數據的函數
|
// 更新特定 alarm 數據的函數
|
||||||
@ -27,8 +29,66 @@ const useAlarmDataStore = defineStore("alarmData", () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const subscribeToAlarms = () => {
|
||||||
|
if (typeof window.requirejs === "undefined") return;
|
||||||
|
window.requirejs(["baja!"], (baja) => {
|
||||||
|
// 對每個 alarm 項目建立訂閱
|
||||||
|
alarmData.value.forEach((alarm, index) => {
|
||||||
|
if (!alarm.alarmOrd) return;
|
||||||
|
|
||||||
return { alarmData, createAlarmData, updateAlarmItem };
|
// 建立訂閱器
|
||||||
|
const subscriber = new baja.Subscriber();
|
||||||
|
|
||||||
|
// 定義 changed 事件的處理函數
|
||||||
|
subscriber.attach("changed", (prop) => {
|
||||||
|
try {
|
||||||
|
let alarmCount = alarmData.value[index].alarmCount;
|
||||||
|
let unackedCount = alarmData.value[index].unackedCount;
|
||||||
|
|
||||||
|
if (prop.$getDisplayName() === "In Alarm Count") {
|
||||||
|
// 取得 In Alarm Count
|
||||||
|
alarmCount = prop.$getValue();
|
||||||
|
}
|
||||||
|
if (prop.$getDisplayName() === "Unacked Alarm Count") {
|
||||||
|
// 取得 Unacked Alarm Count
|
||||||
|
unackedCount = prop.$getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新 alarmData
|
||||||
|
updateAlarmItem(index, alarmCount, unackedCount);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`處理 ${alarm.name || index} 告警變化失敗: ${error.message}`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 訂閱 alarm 資訊
|
||||||
|
baja.Ord.make(alarm.alarmOrd)
|
||||||
|
.get({ subscriber })
|
||||||
|
.then((result) => {
|
||||||
|
console.log(`Successfuly subscribed to alarm ${alarm.name}`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(
|
||||||
|
`訂閱 Alarm ${alarm.name || index} 失敗: ${err.message}`
|
||||||
|
);
|
||||||
|
subscriber.detach("changed");
|
||||||
|
});
|
||||||
|
subscribers.value.push(subscriber);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearAllSubscriber = () => {
|
||||||
|
subscribers.value.forEach((subscriber) => {
|
||||||
|
subscriber.detach("changed");
|
||||||
|
});
|
||||||
|
subscribers.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
return { alarmData, createAlarmData, updateAlarmItem, clearAllSubscriber };
|
||||||
});
|
});
|
||||||
|
|
||||||
export default useAlarmDataStore;
|
export default useAlarmDataStore;
|
@ -7,11 +7,13 @@ const useUserStore = defineStore("user", () => {
|
|||||||
|
|
||||||
const loadUserInfo = () => {
|
const loadUserInfo = () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
window.require &&
|
if (window.require && window.requirejs) {
|
||||||
window.requirejs(["baja!"], (baja) => {
|
window.requirejs(["baja!"], (baja) => {
|
||||||
let currentUserName = baja.getUserName();
|
let currentUserName = baja.getUserName();
|
||||||
userName.value = currentUserName;
|
userName.value = currentUserName;
|
||||||
baja.Ord.make(`station:|slot:/Services/UserService/${currentUserName}`)
|
baja.Ord.make(
|
||||||
|
`station:|slot:/Services/UserService/${currentUserName}`
|
||||||
|
)
|
||||||
.get()
|
.get()
|
||||||
.then((user) => {
|
.then((user) => {
|
||||||
const rolesString = user.getRoles(); // 取得角色字串
|
const rolesString = user.getRoles(); // 取得角色字串
|
||||||
@ -29,6 +31,9 @@ const useUserStore = defineStore("user", () => {
|
|||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
console.error("未能載入 Baja 環境,請確認依賴已正確加載。");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,17 +1,18 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from "vue";
|
import { ref, onMounted, watch } from "vue";
|
||||||
import DashboardStat from "@/components/dashboard/dashboardStat.vue";
|
import DashboardStat from "@/components/dashboard/dashboardStat.vue";
|
||||||
|
import DashboardBuild from "@/components/dashboard/dashboardBuild.vue";
|
||||||
import DashboardElecChart from "@/components/dashboard/dashboardElecChart.vue";
|
import DashboardElecChart from "@/components/dashboard/dashboardElecChart.vue";
|
||||||
import DashboardTag from "@/components/dashboard/dashboardTag.vue";
|
import DashboardTag from "@/components/dashboard/dashboardTag.vue";
|
||||||
import DashboardAlert from "@/components/dashboard/dashboardAlert.vue";
|
import DashboardAlert from "@/components/dashboard/dashboardAlert.vue";
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<a-row :gutter="24" class="p-5">
|
<a-row :gutter="24" class="px-5 py-2">
|
||||||
<a-col :span="8">
|
<a-col :span="8">
|
||||||
<a-image width="100%" src="./build.jpg" class="rounded shadow-lg" />
|
<!-- 建築物 -->
|
||||||
|
<DashboardBuild />
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="16">
|
<a-col :span="16" class="">
|
||||||
<!-- 用電數據 -->
|
<!-- 用電數據 -->
|
||||||
<DashboardStat />
|
<DashboardStat />
|
||||||
<!-- 用電圖表 -->
|
<!-- 用電圖表 -->
|
||||||
@ -19,13 +20,14 @@ import DashboardAlert from "@/components/dashboard/dashboardAlert.vue";
|
|||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
|
|
||||||
<a-row :gutter="24" class="p-5">
|
<a-row :gutter="24" class="px-5">
|
||||||
<a-col :span="16">
|
<a-col :span="14">
|
||||||
<!-- <DashboardTag /> -->
|
<!-- 系統小類 -->
|
||||||
|
<DashboardTag />
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="8" class="">
|
<a-col :span="10">
|
||||||
<!-- 告警 -->
|
<!-- 告警 -->
|
||||||
<!-- <DashboardAlert /> -->
|
<DashboardAlert />
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</template>
|
</template>
|
||||||
|
33
src/views/system/NotFound.vue
Normal file
33
src/views/system/NotFound.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
|
||||||
|
const niagaraStore = useNiagaraDataStore();
|
||||||
|
const homeData = computed(() => {
|
||||||
|
const data = niagaraStore.headerList.children?.[2] || {};
|
||||||
|
return {
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<a-result status="404" title="404" sub-title="抱歉,您造訪的頁面不存在">
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary"
|
||||||
|
><router-link
|
||||||
|
:to="
|
||||||
|
homeData.ord !== 'null'
|
||||||
|
? {
|
||||||
|
name: 'baja',
|
||||||
|
query: { ord: encodeURIComponent(homeData.ord) },
|
||||||
|
}
|
||||||
|
: '/'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
首頁
|
||||||
|
</router-link>
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
</a-result>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
@ -1,38 +1,74 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { ref, computed, onMounted, onUnmounted } from "vue";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
|
import { nextTick } from "vue"; // 確保 iframe 渲染完成
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
const iframeRef = ref(null);
|
||||||
|
const loading = ref(true);
|
||||||
|
|
||||||
const ordUrl = computed(() => {
|
const ordUrl = computed(() => {
|
||||||
const ord = route.query.ord || "";
|
const ord = route.query.ord || "";
|
||||||
const decodedOrd = decodeURIComponent(ord);
|
const decodedOrd = decodeURIComponent(ord);
|
||||||
console.log("route.query.ord:", ord); // 除錯用
|
console.log("route.query.ord:", ord);
|
||||||
console.log("decodedOrd:", decodedOrd); // 除錯用
|
console.log("decodedOrd:", decodedOrd);
|
||||||
return decodedOrd;
|
return decodedOrd;
|
||||||
});
|
});
|
||||||
|
|
||||||
const iframeSrc = computed(() => {
|
const iframeSrc = computed(() => {
|
||||||
if (!ordUrl.value) {
|
if (!ordUrl.value) {
|
||||||
return ""; // 如果 ordUrl 為空,則不設定 src
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
ordUrl.value.startsWith("http://") ||
|
ordUrl.value.startsWith("http://") ||
|
||||||
ordUrl.value.startsWith("https://")
|
ordUrl.value.startsWith("https://")
|
||||||
) {
|
) {
|
||||||
return ordUrl.value; // 如果以 http 開頭,則直接使用 ordUrl 作為 src
|
return ordUrl.value;
|
||||||
} else {
|
} else {
|
||||||
return `/ord?${ordUrl.value}|view:?fullScreen=true`; // 否則,添加 Niagara 前綴
|
return `/ord?${ordUrl.value}|view:?fullScreen=true`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const iframeLoad = () => {
|
||||||
|
console.log("iframe loaded!");
|
||||||
|
// const iframeDocument =
|
||||||
|
// iframeRef.value?.contentDocument ||
|
||||||
|
// iframeRef.value?.contentWindow?.document;
|
||||||
|
// if (iframeDocument) {
|
||||||
|
// const bodyText = iframeDocument.body.innerText || "";
|
||||||
|
// if (bodyText.includes("Bad Request") || bodyText.includes("Problem")) {
|
||||||
|
// console.error("iframe 內容顯示 400 錯誤,跳轉到 NotFound");
|
||||||
|
// router.push({ name: "NotFound" });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
// 確保 iframe 已經渲染到 DOM
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
if (iframeRef.value) {
|
||||||
|
iframeRef.value.onload = iframeLoad;
|
||||||
|
} else {
|
||||||
|
console.warn("iframeRef is null!");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<a-spin :spinning="loading" tip="Loading..." size="large">
|
||||||
<iframe
|
<iframe
|
||||||
v-if="iframeSrc"
|
v-if="iframeSrc"
|
||||||
|
ref="iframeRef"
|
||||||
:src="iframeSrc"
|
:src="iframeSrc"
|
||||||
width="100%"
|
width="100%"
|
||||||
:style="{ height: 'calc(100vh - 90px)' }"
|
:style="{ height: 'calc(100vh - 90px)' }"
|
||||||
|
@load="iframeLoad"
|
||||||
></iframe>
|
></iframe>
|
||||||
|
</a-spin>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
Loading…
Reference in New Issue
Block a user