介面優化

This commit is contained in:
koko 2025-03-19 14:26:36 +08:00
parent a5dcec3045
commit 8ba6905047
22 changed files with 683 additions and 357 deletions

1
.env Normal file
View File

@ -0,0 +1 @@
VITE_FILE_API_BASEURL = "https://192.168.0.206:8500"

View File

@ -3,6 +3,10 @@
<head>
<meta charset="UTF-8" />
<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" />
<title>監控系統</title>
<script type="text/javascript" src="/requirejs/config.js?"></script>
@ -13,6 +17,7 @@
</head>
<body>
<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>
</body>
</html>

9
package-lock.json generated
View File

@ -16,7 +16,7 @@
"axios": "^1.7.9",
"echarts": "^5.6.0",
"pinia": "^2.3.1",
"tailwind-merge": "^3.0.1",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.3",
"vue": "^3.5.13",
"vue-router": "^4.5.0"
@ -1926,10 +1926,9 @@
"license": "MIT"
},
"node_modules/tailwind-merge": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.1.tgz",
"integrity": "sha512-AvzE8FmSoXC7nC+oU5GlQJbip2UO7tmOhOfQyOmPhrStOGXHU08j8mZEHZ4BmCqY5dWTCo4ClWkNyRNx1wpT0g==",
"license": "MIT",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz",
"integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"

View File

@ -17,7 +17,7 @@
"axios": "^1.7.9",
"echarts": "^5.6.0",
"pinia": "^2.3.1",
"tailwind-merge": "^3.0.1",
"tailwind-merge": "^3.0.2",
"tailwindcss": "^4.0.3",
"vue": "^3.5.13",
"vue-router": "^4.5.0"

View File

@ -44,7 +44,7 @@ onMounted(async () => {
:style="{
margin: '5px',
padding: '0px',
background: '#fff',
background: '#fafafa',
minHeight: '280px',
}"
>

View File

@ -5,95 +5,62 @@ import useAlarmDataStore from "@/stores/useAlarmDataStore";
const niagaraStore = useNiagaraDataStore();
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>
<template>
<div>
<a-table
:columns="columns"
:data-source="alarmDataStore.alarmData"
bordered
>
<template #bodyCell="{ column, record }">
<span>{{ record[column.key] }}</span>
</template>
</a-table>
</div>
<a-card class="card">
<h5>Alarm</h5>
<table className="table w-full">
<thead>
<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
>
</td>
</tr>
</tbody>
</table>
</a-card>
</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>

View 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>

View File

@ -3,41 +3,80 @@ const mockData = [
{
value: 305.5,
label: "今日用電量",
unit:"kWH"
unit: "kWH",
icon: "leaf",
},
{
value: 886.75,
label: "昨日用電量",
unit:"kWH"
unit: "kWH",
icon: "leaf",
},
{
value: 7.84,
label: "即時功率",
unit:"kW"
unit: "kW",
icon: "bolt",
},
{
value: 20.96,
label: "容積占比",
unit:"%"
unit: "%",
icon: "charging-station",
},
];
</script>
<template>
<a-row :gutter="24" class="p-5">
<a-col v-for="(item, index) in mockData" :key="index" :span="6">
<a-card class="shadow">
<a-statistic
:title="item.label"
:value="item.value"
:precision="2"
:suffix="item.unit"
:value-style="{ color: index % 2 === 0 ? '#3f8600' : '#1677ff' }"
>
</a-statistic>
<a-row :gutter="24">
<a-col v-for="(item, index) in mockData" :key="index" :span="6" class="mb-5">
<a-card class="number">
<a-row :gutter="24" align="middle" justify="space-between">
<a-col :span="18">
<span>{{ item.label }}</span>
<h3>
{{ item.value }}<small>{{ item.unit }}</small>
</h3>
</a-col>
<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-col>
</a-row>
</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>

View File

@ -1,48 +1,45 @@
<script setup>
import { ref, computed, watch } from "vue";
import NavBuild from "../navbar/NavBuild.vue";
import useNavDataStore from "@/stores/useNavDataStore";
import { useRouter } from "vue-router";
const navStore = useNavDataStore();
const router = useRouter();
const filteredItems = computed(() => {
const flatten = (items) => {
const flattenedItems = [];
const selectedBuildingOrd = navStore.selectedBuildingOrd;
//
const flatten = (items) => {
if (!items) return;
const recurse = (items) => {
if (!items) return;
items.forEach((item) => {
// ord null
if (item.ord !== "null") {
flattenedItems.push({
key: item.key,
name: item.title,
ord: item.ord,
icon: item.icon,
});
flattenedItems.push(item);
}
//
if (item.children) {
flatten(item.children);
recurse(item.children);
}
});
};
//
recurse(items);
return flattenedItems;
};
const flattenedItems = computed(() => {
const selectedBuildingOrd = navStore.selectedBuildingOrd;
if (
navStore.menuList &&
navStore.menuList.length > 0 &&
selectedBuildingOrd
) {
const buildingMenu = navStore.menuList.find(
const buildingMenu = navStore.menuList[0].children.find(
(item) => item.label === selectedBuildingOrd
);
flatten(buildingMenu.children);
return flatten(buildingMenu.children);
}
return flattenedItems;
return [];
});
const handleClick = (ord) => {
@ -56,31 +53,47 @@ const handleClick = (ord) => {
</script>
<template>
<div v-if="filteredItems && filteredItems.length > 0">
<a-row :gutter="[8, 16]">
<a-col :span="6" v-for="(item, index) in filteredItems" :key="index">
<a-card
@click="handleClick(item.ord)"
class="shadow"
:bodyStyle="{
display: 'flex',
alignItems: 'center',
gap: '8px',
cursor: 'pointer',
}"
>
<img
v-if="item.icon"
:src="item.icon"
alt="Icon"
style="width: 30px; height: 30px; vertical-align: middle"
/>
<span class="text-lg">{{ item.name }}</span>
</a-card>
</a-col>
</a-row>
</div>
<div v-else>No items to display.</div>
<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-col :span="6" v-for="item in flattenedItems" :key="item.key">
<a-card
@click="handleClick(item.ord)"
class="shadow"
:bodyStyle="{
display: 'flex',
alignItems: 'center',
gap: '8px',
cursor: 'pointer',
}"
>
<img
v-if="item.icon"
:src="item.icon"
alt="Icon"
style="width: 30px; height: 30px; vertical-align: middle"
/>
<span class="text-lg">{{ item.label }}</span>
</a-card>
</a-col>
</a-row>
</div>
<div v-else>No items to display.</div>
</a-card>
</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>

View 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>

View File

@ -12,20 +12,13 @@ const props = defineProps({
},
});
const navStore = useNavDataStore();
const selectedKeys = ref([]); // menu item
// const selectedKeys = ref([]); // menu item
const openKeys = ref([]); // submenu
const preOpenKeys = ref([]); // submenu
const filteredItems = computed(() => {
const selectedBuildingOrd = navStore.selectedBuildingOrd;
if (
navStore.menuList &&
navStore.menuList.length > 0 &&
selectedBuildingOrd
) {
const buildingMenu = navStore.menuList[0].children.find(
(item) => item.key === selectedBuildingOrd
);
return buildingMenu.children;
if (navStore.menuList && navStore.menuList.length > 0) {
return navStore.menuList[0].children;
}
return [];
});
@ -36,7 +29,7 @@ const handleClick = (ord) => {
name: "baja",
query: { ord: encodeURIComponent(ord) },
});
}
}
};
// openKeys
@ -60,13 +53,36 @@ watch(
>
<a-menu mode="inline" theme="light">
<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 }}
</a-menu-item>
<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)">
{{ child.label }}
</a-menu-item>
<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 }}
</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>
</template>
</a-menu>

View File

@ -1,71 +1,23 @@
<script setup>
import { ref, watch, computed } from "vue";
import { ref, computed, onBeforeUnmount, watch } from "vue";
import { useRouter } from "vue-router";
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
import useAlarmDataStore from "@/stores/useAlarmDataStore";
const router = useRouter();
const niagaraStore = useNiagaraDataStore();
const alarmDataStore = useAlarmDataStore();
// alarmCount
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 = () => {
// 使 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");
});
});
});
alarmDataStore.createAlarmData(niagaraStore.alarmList);
};
watch(
@ -76,8 +28,12 @@ watch(
initializeAlarmData();
}
},
{ immediate: true } //
{ immediate: true }
);
onBeforeUnmount(() => {
alarmDataStore.clearAllSubscriber();
});
</script>
<template>
@ -98,8 +54,7 @@ watch(
</a-menu-item>
</a-menu>
</template>
<a-badge :count="totalAlarmCount" :overflow-count="999"
>
<a-badge :count="totalAlarmCount" :overflow-count="999">
<a class="flex flex-col items-center">
<font-awesome-icon :icon="['fas', 'bell']" size="2x" />
<span class="text-sm">告警</span>

View File

@ -8,8 +8,8 @@ const router = useRouter();
const buildmenu = ref([]);
const handleBuildClick = (key) => {
navStore.setSelectedBuildingOrd(key);
const handleBuildClick = (label) => {
navStore.setSelectedBuildingOrd(label);
};
watch(
@ -17,7 +17,7 @@ watch(
(newValue, oldValue) => {
if (newValue && newValue.length > 0 && newValue[0].children) {
buildmenu.value = newValue[0].children;
navStore.setSelectedBuildingOrd(newValue[0].children[0].key);
navStore.setSelectedBuildingOrd(newValue[0].children[0].label);
}
},
{ immediate: true }
@ -27,14 +27,14 @@ watch(
<template>
<a-select
v-if="buildmenu && buildmenu.length > 0"
:default-value="buildmenu[0] ? buildmenu[0].key : null"
:default-value="buildmenu[0] ? buildmenu[0].label : null"
@change="handleBuildClick"
placeholder="請選擇建築"
>
<a-select-option
v-for="item in buildmenu"
:key="item.key"
:value="item.key"
:value="item.label"
>
{{ item.label }}
</a-select-option>

View File

@ -1,83 +1,85 @@
<script setup>
import { ref, onMounted, watch, onUnmounted } from "vue";
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
import { ref, onMounted, onUnmounted } from "vue";
import {
imagesWeatherDay,
imagesWeatherNight,
orderWeather,
} from "@/constants";
const niagaraStore = useNiagaraDataStore();
const weatherStateText = ref("N/A");
const weatherStateImage = ref(null);
const actualWeather = ref("N/A");
const actualWeather = ref("Clear");
const temperature = ref("N/A");
const humidity = ref("N/A");
const actualNighttime = ref("N/A");
const actualNighttime = ref(false);
const dateTime = ref("N/A");
let intervalId = null;
let subscriber = null; // subscriber
//
const initializeData = () => {
niagaraStore.weatherList.children.forEach((item, index) => {
if (!item.ord) return;
window.require &&
window.requirejs(["baja!"], (baja) => {
// subscriber
const subscriber = new baja.Subscriber();
subscriber.attach("changed", (prop) => {
console.log(
"weather",
prop,
prop.$getDisplayName(),
prop.$getValue().getValueDisplay()
);
if (window.require && window.requirejs) {
window.requirejs(["baja!"], (baja) => {
subscriber = new baja.Subscriber(); // subscriber
subscriber.attach("changed", (prop) => {
console.log(
"weather",
prop,
prop.$getDisplayName(),
prop.$getValue().getValueDisplay()
);
if (item.name === "Out temperature" && prop.$displayName === "Out") {
temperature.value = prop.$getValue().getValueDisplay();
} else if (
item.name === "Out humidity" &&
prop.$displayName === "Out"
) {
humidity.value = prop.$getValue().getValueDisplay();
} else if (item.name === "Weather" && prop.$displayName === "Out") {
actualWeather.value = prop.$getValue().getValueDisplay();
} else if (item.name === "Nighttime" && prop.$displayName === "Out") {
actualNighttime.value = !(prop.$getValue().getValueDisplay());
}
loadWeather(actualWeather.value);
});
// baja.Ord.make
baja.Ord.make(item.ord)
.get({ subscriber })
.then((result) => {
console.log(`Successfuly subscribed to weather ${item.name}`);
})
.catch((err) => {
console.error(
`訂閱 weather ${item.name || index} 失敗: ${err.message}`
);
subscriber.detach("changed");
});
// slotName
if (prop.$slotName === "temp") {
temperature.value = prop.$getValue().getValueDisplay();
} else if (prop.$slotName === "humidity") {
humidity.value = prop.$getValue().getValueDisplay();
} else if (prop.$slotName === "state") {
actualWeather.value = prop.$getValue().getValueDisplay();
loadWeather(actualWeather.value); //
} else if (prop.$slotName === "sunDown") {
actualNighttime.value = !prop.$getValue().getValueDisplay();
loadWeather(actualWeather.value); //
}
});
});
// Niagara
baja.Ord.make(
"station:|slot:/Services/WeatherService/WeatherService/WeatherService/current"
)
.get({ subscriber })
.then((result) => {
console.log("Successfuly subscribed to weather data", result);
})
.catch((err) => {
console.error(`訂閱 weather 失敗: ${err.message}`);
subscriber.detach("changed"); //
});
});
} else {
console.error("未能載入 Baja 環境,請確認依賴已正確加載。");
}
};
//
const loadWeather = (actualWeather) => {
console.log(
"actualWeather",
actualWeather.replace(/\s/g, ""),
actualNighttime.value
);
let weatherStateTextTemp = "Unknown";
let weatherStateImageTemp = null;
const orderLength = orderWeather.length - 1;
if (orderLength < actualWeather) {
console.error(
"天氣狀態超出範圍!最大限度:" +
orderLength +
"| 實際值:" +
actualWeather
);
// actualWeather orderWeather
const actualWeatherIndex = orderWeather.indexOf(
actualWeather.replace(/\s/g, "")
);
if (actualWeatherIndex === -1) {
// weather orderWeather
console.error("天氣狀態不在 orderWeather 陣列中!實際值:" + actualWeather);
weatherStateTextTemp = "Unknown";
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
} else if (actualWeather === "none") {
@ -85,17 +87,17 @@ const loadWeather = (actualWeather) => {
weatherStateTextTemp = "Unknown";
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
} else {
const actualWeatherNum = Number(actualWeather);
if (actualNighttime.value) {
console.log("Nighttime activ");
weatherStateTextTemp = orderWeather[actualWeatherNum];
weatherStateTextTemp = orderWeather[actualWeatherIndex];
weatherStateImageTemp = imagesWeatherNight[weatherStateTextTemp];
} else {
console.log("Daytime activ");
weatherStateTextTemp = orderWeather[actualWeatherNum];
weatherStateTextTemp = orderWeather[actualWeatherIndex];
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
}
}
weatherStateImage.value = weatherStateImageTemp;
weatherStateText.value = weatherStateTextTemp; //
};
@ -103,33 +105,27 @@ const loadWeather = (actualWeather) => {
//
const updateTime = () => {
const date = new Date();
window.require &&
if (window.require && window.requirejs) {
window.requirejs(["baja!"], (baja) => {
const bAbsTime = baja.AbsTime.make({ jsDate: date });
bAbsTime.toDateTimeString().then((dateTimeStr) => {
dateTime.value = dateTimeStr;
});
});
}
};
watch(
() => niagaraStore.weatherList.children,
(newValue, oldValue) => {
if (newValue) {
console.log("niagaraStore.weatherList changed:", newValue);
initializeData();
}
},
{ immediate: true } //
);
onMounted(() => {
updateTime();
initializeData(); //
updateTime(); //
intervalId = setInterval(updateTime, 1000);
});
onUnmounted(() => {
clearInterval(intervalId);
if (subscriber) {
subscriber.detachAll(); //
}
});
</script>

View File

@ -16,15 +16,50 @@ const props = defineProps({
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(
() => niagaraStore.headerList.children?.[0]?.ord || ""
);
const systemName = computed(
() => niagaraStore.headerList.children?.[1]?.name || "系統監控"
);
const homeData = computed(() => niagaraStore.headerList.children?.[2] || {});
const systemData = computed(() => niagaraStore.headerList.children?.[3] || {});
const dynamicMenu = computed(() => niagaraStore.DynamicList.children || []);
const homeData = computed(() => {
const data = niagaraStore.headerList.children?.[2] || {};
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 || []);
</script>
@ -36,7 +71,7 @@ const userList = computed(() => niagaraStore.userList?.children || []);
<a href="./index.html" class="text-2xl font-bold mx-4">{{
systemName
}}</a>
<NavBuild />
<!-- <NavBuild /> -->
</div>
<ul class="nav-menu flex gap-10">
<li>
@ -51,12 +86,10 @@ const userList = computed(() => niagaraStore.userList?.children || []);
"
>
<img
v-if="homeData.icon"
:src="homeData.icon"
alt="home_icon"
class="icon"
/>
<font-awesome-icon v-else :icon="['fas', 'home']" size="2x" />
<span class="text-sm">首頁</span>
</router-link>
</li>
@ -66,13 +99,10 @@ const userList = computed(() => niagaraStore.userList?.children || []);
@click="props.open"
>
<img
v-if="systemData.icon"
:src="systemData.icon"
alt="system_icon"
class="icon"
/>
<font-awesome-icon v-else :icon="['fas', 'tv']" size="2x" />
<span class="text-sm">系統監控</span>
</a>
</li>
@ -80,8 +110,7 @@ const userList = computed(() => niagaraStore.userList?.children || []);
<router-link
:to="{ name: 'baja', query: { ord: encodeURIComponent(item.ord) } }"
>
<img v-if="item.icon" :src="item.icon" alt="menu_icon" class="icon" />
<font-awesome-icon v-else :icon="['fas', 'tv']" size="2x" />
<img :src="item.icon" alt="menu_icon" class="icon" />
<span class="text-sm">{{ item.name }}</span>
</router-link>
</li>

View File

@ -19,7 +19,10 @@ import {
faUserEdit,
faInfoCircle,
faSignOutAlt,
faAngleDown
faAngleDown,
faLeaf,
faBolt,
faChargingStation
} from "@fortawesome/free-solid-svg-icons";
library.add(
faTv,
@ -34,7 +37,10 @@ library.add(
faUserEdit,
faInfoCircle,
faSignOutAlt,
faAngleDown
faAngleDown,
faLeaf,
faBolt,
faChargingStation
);
const pinia = createPinia();
const app = createApp(App);

View File

@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from "vue-router";
import DashboardPage from "@/views/dashboard/DashboardPage.vue";
import SystemPage from "@/views/system/SystemPage.vue";
import EnergyChart from "@/views/energy/EnergyChart.vue";
import NotFound from "@/views/system/NotFound.vue";
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
@ -20,6 +21,11 @@ const router = createRouter({
path: "/energy",
name: "energy",
component: EnergyChart,
},
{
path: '/:pathMatch(.*)*', // 匹配所有未定義的路徑
name: 'NotFound',
component: NotFound
}
],
});

View File

@ -4,7 +4,7 @@ import { ref } from "vue";
const useAlarmDataStore = defineStore("alarmData", () => {
const alarmData = ref([]);
const subscribers = ref([]); // 用於儲存 Subscriber 對象
// 建立 alarmData 的函數
const createAlarmData = (alarmList) => {
alarmData.value = alarmList.children.map((item, index) => ({
@ -13,8 +13,10 @@ const useAlarmDataStore = defineStore("alarmData", () => {
alarmCount: 0,
unackedCount: 0,
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 數據的函數
@ -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;

View File

@ -6,33 +6,38 @@ const useUserStore = defineStore("user", () => {
const userRole = ref("");
const loadUserInfo = () => {
return new Promise((resolve, reject) => {
window.require &&
return new Promise((resolve, reject) => {
if (window.require && window.requirejs) {
window.requirejs(["baja!"], (baja) => {
let currentUserName = baja.getUserName();
userName.value = currentUserName;
baja.Ord.make(`station:|slot:/Services/UserService/${currentUserName}`)
baja.Ord.make(
`station:|slot:/Services/UserService/${currentUserName}`
)
.get()
.then((user) => {
const rolesString = user.getRoles(); // 取得角色字串
if (rolesString) {
const rolesArray = rolesString.split(",");
if (rolesArray.length > 0) {
userRole.value = rolesArray[0].trim();
console.log("選取的角色:", userRole.value);
}
const rolesString = user.getRoles(); // 取得角色字串
if (rolesString) {
const rolesArray = rolesString.split(",");
if (rolesArray.length > 0) {
userRole.value = rolesArray[0].trim();
console.log("選取的角色:", userRole.value);
}
resolve();
}
resolve();
})
.catch((err) => {
console.error(`訂閱失敗: ${err.message}`);
reject(err);
reject(err);
});
});
} else {
console.error("未能載入 Baja 環境,請確認依賴已正確加載。");
}
});
};
return { userName, userRole, loadUserInfo };
});
export default useUserStore;
export default useUserStore;

View File

@ -1,17 +1,18 @@
<script setup>
import { ref, onMounted, watch } from "vue";
import DashboardStat from "@/components/dashboard/dashboardStat.vue";
import DashboardBuild from "@/components/dashboard/dashboardBuild.vue";
import DashboardElecChart from "@/components/dashboard/dashboardElecChart.vue";
import DashboardTag from "@/components/dashboard/dashboardTag.vue";
import DashboardAlert from "@/components/dashboard/dashboardAlert.vue";
</script>
<template>
<a-row :gutter="24" class="p-5">
<a-row :gutter="24" class="px-5 py-2">
<a-col :span="8">
<a-image width="100%" src="./build.jpg" class="rounded shadow-lg" />
<!-- 建築物 -->
<DashboardBuild />
</a-col>
<a-col :span="16">
<a-col :span="16" class="">
<!-- 用電數據 -->
<DashboardStat />
<!-- 用電圖表 -->
@ -19,13 +20,14 @@ import DashboardAlert from "@/components/dashboard/dashboardAlert.vue";
</a-col>
</a-row>
<a-row :gutter="24" class="p-5">
<a-col :span="16">
<!-- <DashboardTag /> -->
<a-row :gutter="24" class="px-5">
<a-col :span="14">
<!-- 系統小類 -->
<DashboardTag />
</a-col>
<a-col :span="8" class="">
<a-col :span="10">
<!-- 告警 -->
<!-- <DashboardAlert /> -->
<DashboardAlert />
</a-col>
</a-row>
</template>

View 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>

View File

@ -1,38 +1,74 @@
<script setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import { ref, computed, onMounted, onUnmounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { nextTick } from "vue"; // iframe
const route = useRoute();
const router = useRouter();
const iframeRef = ref(null);
const loading = ref(true);
const ordUrl = computed(() => {
const ord = route.query.ord || "";
const decodedOrd = decodeURIComponent(ord);
console.log("route.query.ord:", ord); //
console.log("decodedOrd:", decodedOrd); //
console.log("route.query.ord:", ord);
console.log("decodedOrd:", decodedOrd);
return decodedOrd;
});
const iframeSrc = computed(() => {
if (!ordUrl.value) {
return ""; // ordUrl src
return "";
}
if (
ordUrl.value.startsWith("http://") ||
ordUrl.value.startsWith("https://")
) {
return ordUrl.value; // http 使 ordUrl src
return ordUrl.value;
} 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>
<template>
<iframe
v-if="iframeSrc"
:src="iframeSrc"
width="100%"
:style="{ height: 'calc(100vh - 90px)' }"
></iframe>
<a-spin :spinning="loading" tip="Loading..." size="large">
<iframe
v-if="iframeSrc"
ref="iframeRef"
:src="iframeSrc"
width="100%"
:style="{ height: 'calc(100vh - 90px)' }"
@load="iframeLoad"
></iframe>
</a-spin>
</template>
<style scoped></style>