init
This commit is contained in:
commit
a5dcec3045
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
18
index.html
Normal file
18
index.html
Normal file
@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>監控系統</title>
|
||||
<script type="text/javascript" src="/requirejs/config.js?"></script>
|
||||
<script
|
||||
type="text/javascript"
|
||||
src="/module/js/com/tridium/js/ext/require/require.min.js?"
|
||||
></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
2133
package-lock.json
generated
Normal file
2133
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "niagara-vue-admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "^3.0.8",
|
||||
"@tailwindcss/vite": "^4.0.3",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.7.9",
|
||||
"echarts": "^5.6.0",
|
||||
"pinia": "^2.3.1",
|
||||
"tailwind-merge": "^3.0.1",
|
||||
"tailwindcss": "^4.0.3",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"vite": "^6.0.5"
|
||||
}
|
||||
}
|
BIN
public/build.jpg
Normal file
BIN
public/build.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
3
public/config.json
Normal file
3
public/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"systemName": "關渡醫院中央監控"
|
||||
}
|
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
56
src/App.vue
Normal file
56
src/App.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, provide } from "vue";
|
||||
import { RouterLink, RouterView } from "vue-router";
|
||||
import LeftSidebar from "./components/navbar/LeftSidebar.vue";
|
||||
import Navbar from "./components/navbar/Navbar.vue";
|
||||
import useUserStore from "@/stores/useUserStore";
|
||||
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
|
||||
import useNavDataStore from "@/stores/useNavDataStore";
|
||||
|
||||
const showSidebar = ref(false);
|
||||
const systemName = ref("");
|
||||
const userStore = useUserStore();
|
||||
const niagaraStore = useNiagaraDataStore();
|
||||
const navStore = useNavDataStore();
|
||||
|
||||
const toggleSidebar = () => {
|
||||
showSidebar.value = !showSidebar.value;
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
const json = await fetch("./config.json");
|
||||
const res = await json.json();
|
||||
systemName.value = res.systemName;
|
||||
|
||||
await userStore.loadUserInfo();
|
||||
await niagaraStore.getSystemData();
|
||||
await navStore.getNavData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-layout class="w-full" style="min-height: 100vh">
|
||||
<LeftSidebar
|
||||
:showSidebar="showSidebar"
|
||||
:toggleSidebar="toggleSidebar"
|
||||
/>
|
||||
<Navbar
|
||||
:systemName="systemName"
|
||||
:open="toggleSidebar"
|
||||
:userName="userStore.userName"
|
||||
/>
|
||||
<a-layout-content
|
||||
class="overflow-x-hidden"
|
||||
:style="{
|
||||
margin: '5px',
|
||||
padding: '0px',
|
||||
background: '#fff',
|
||||
minHeight: '280px',
|
||||
}"
|
||||
>
|
||||
<RouterView />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
1
src/assets/vue.svg
Normal file
1
src/assets/vue.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
After Width: | Height: | Size: 496 B |
1300
src/baja.js
Normal file
1300
src/baja.js
Normal file
File diff suppressed because it is too large
Load Diff
99
src/components/dashboard/dashboardAlert.vue
Normal file
99
src/components/dashboard/dashboardAlert.vue
Normal file
@ -0,0 +1,99 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
|
||||
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>
|
||||
</template>
|
||||
<style scoped></style>
|
111
src/components/dashboard/dashboardElecChart.vue
Normal file
111
src/components/dashboard/dashboardElecChart.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import * as echarts from "echarts"; // 引入 echarts
|
||||
|
||||
// 定義用電比較資料
|
||||
const yesterdayTodayData = {
|
||||
categories: [
|
||||
"00:00",
|
||||
"01:00",
|
||||
"02:00",
|
||||
"03:00",
|
||||
"04:00",
|
||||
"05:00",
|
||||
"06:00",
|
||||
"07:00",
|
||||
"08:00",
|
||||
"09:00",
|
||||
"10:00",
|
||||
"11:00",
|
||||
],
|
||||
values: [
|
||||
{
|
||||
name: `2025.01.8 用電量`,
|
||||
value: [8, 8, 8, 8, 8, 15, 25, 65, 75, 60, 70, 65],
|
||||
},
|
||||
{
|
||||
name: `2025.01.7 用電量`,
|
||||
value: [10, 10, 10, 10, 10, 20, 30, 80, 90, 70, 80, 85],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 本週與上週用電比較資料
|
||||
const weekComparisonData = {
|
||||
categories: ["Fri", "Sat", "Sun", "Mon", "Tue", "Wed", "Thu"],
|
||||
values: [
|
||||
{
|
||||
name: "本週用電量",
|
||||
value: [850, 200, 350, 850, 950, 950, 900],
|
||||
},
|
||||
{
|
||||
name: "上週用電量",
|
||||
value: [800, 150, 300, 750, 900, 900, 800],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const yesterdayTodayChart = ref(null);
|
||||
const weekComparisonChart = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
// 初始化昨天和今天用電比較圖表
|
||||
const yesterdayTodayEChart = echarts.init(yesterdayTodayChart.value);
|
||||
yesterdayTodayEChart.setOption(getBarChartOptions(yesterdayTodayData));
|
||||
|
||||
// 初始化本週和上週用電比較圖表
|
||||
const weekComparisonEChart = echarts.init(weekComparisonChart.value);
|
||||
weekComparisonEChart.setOption(getBarChartOptions(weekComparisonData));
|
||||
});
|
||||
|
||||
// 封裝長條圖的設定
|
||||
const getBarChartOptions = (data) => {
|
||||
return {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: data.values.map((item) => item.name),
|
||||
},
|
||||
grid: {
|
||||
left: "3%",
|
||||
right: "4%",
|
||||
bottom: "3%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: data.categories,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
},
|
||||
series: data.values.map((item) => ({
|
||||
name: item.name,
|
||||
type: "bar",
|
||||
barGap: 0.1,
|
||||
emphasis: {
|
||||
focus: "series",
|
||||
},
|
||||
data: item.value,
|
||||
})),
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-row :gutter="24" class="p-5">
|
||||
<a-col :span="12">
|
||||
<div ref="yesterdayTodayChart" style="width: 100%; height: 350px"></div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<div ref="weekComparisonChart" style="width: 100%; height: 350px"></div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
43
src/components/dashboard/dashboardStat.vue
Normal file
43
src/components/dashboard/dashboardStat.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<script setup>
|
||||
const mockData = [
|
||||
{
|
||||
value: 305.5,
|
||||
label: "今日用電量",
|
||||
unit:"kWH"
|
||||
},
|
||||
{
|
||||
value: 886.75,
|
||||
label: "昨日用電量",
|
||||
unit:"kWH"
|
||||
},
|
||||
{
|
||||
value: 7.84,
|
||||
label: "即時功率",
|
||||
unit:"kW"
|
||||
},
|
||||
{
|
||||
value: 20.96,
|
||||
label: "容積占比",
|
||||
unit:"%"
|
||||
},
|
||||
];
|
||||
</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-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
86
src/components/dashboard/dashboardTag.vue
Normal file
86
src/components/dashboard/dashboardTag.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import { ref, computed, watch } from "vue";
|
||||
import useNavDataStore from "@/stores/useNavDataStore";
|
||||
import { useRouter } from "vue-router";
|
||||
|
||||
const navStore = useNavDataStore();
|
||||
const router = useRouter();
|
||||
|
||||
const filteredItems = computed(() => {
|
||||
const flattenedItems = [];
|
||||
const selectedBuildingOrd = navStore.selectedBuildingOrd;
|
||||
// 遞迴函數,用於展開多層級的選單項目
|
||||
const flatten = (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,
|
||||
});
|
||||
}
|
||||
// 如果有子項目,則遞迴處理
|
||||
if (item.children) {
|
||||
flatten(item.children);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 展開選單項目
|
||||
if (
|
||||
navStore.menuList &&
|
||||
navStore.menuList.length > 0 &&
|
||||
selectedBuildingOrd
|
||||
) {
|
||||
const buildingMenu = navStore.menuList.find(
|
||||
(item) => item.label === selectedBuildingOrd
|
||||
);
|
||||
flatten(buildingMenu.children);
|
||||
}
|
||||
|
||||
return flattenedItems;
|
||||
});
|
||||
|
||||
const handleClick = (ord) => {
|
||||
if (ord) {
|
||||
router.push({
|
||||
name: "baja",
|
||||
query: { ord: encodeURIComponent(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>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
82
src/components/energy/EnergyBar.vue
Normal file
82
src/components/energy/EnergyBar.vue
Normal file
@ -0,0 +1,82 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const chartContainer = ref(null);
|
||||
let chartInstance = null;
|
||||
|
||||
const props = defineProps({
|
||||
chartData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const initChart = () => {
|
||||
chartInstance = echarts.init(chartContainer.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
axisPointer: {
|
||||
type: "shadow",
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: props.chartData.series.map((item) => item.name),
|
||||
bottom: "0%",
|
||||
},
|
||||
grid: {
|
||||
top: "5%",
|
||||
left: "5%",
|
||||
right: "5%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: props.chartData.categories || [],
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
},
|
||||
series: props.chartData.series.map((item) => ({
|
||||
...item,
|
||||
stack: "total",
|
||||
})),
|
||||
};
|
||||
|
||||
chartInstance.setOption(option);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.chartData,
|
||||
(newChartData) => {
|
||||
if (chartInstance) {
|
||||
chartInstance.setOption({
|
||||
xAxis: {
|
||||
data: newChartData.categories || [],
|
||||
},
|
||||
series: newChartData.series.map((item) => ({
|
||||
...item,
|
||||
stack: "total",
|
||||
})),
|
||||
});
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div ref="chartContainer" style="width: 100%; height: 200px"></div>
|
||||
</template>
|
105
src/components/energy/EnergyLine.vue
Normal file
105
src/components/energy/EnergyLine.vue
Normal file
@ -0,0 +1,105 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import * as echarts from "echarts"; // 引入 echarts
|
||||
|
||||
// 假資料
|
||||
const data = {
|
||||
categories: [
|
||||
"16:22:29",
|
||||
"16:22:37",
|
||||
"16:22:47",
|
||||
"16:23:00",
|
||||
"16:23:08",
|
||||
"16:23:18",
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "即時趨勢",
|
||||
type: "line",
|
||||
data: [320, 310, 300, 305, 310, 300],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "契約容量",
|
||||
type: "line",
|
||||
data: [400, 400, 400, 400, 400, 400],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "警戒容量",
|
||||
type: "line",
|
||||
data: [350, 350, 350, 350, 350, 350],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "偵測值",
|
||||
type: "line",
|
||||
data: [280, 300, 290, 295, 300, 290],
|
||||
smooth: true,
|
||||
lineStyle: {
|
||||
width: 3,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const demand_chart = ref(null);
|
||||
const defaultChartOption = ref({
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
legend: {
|
||||
data: data.series.map((s) => s.name),
|
||||
orient: "horizontal",
|
||||
bottom: "0%",
|
||||
},
|
||||
grid: {
|
||||
top: "10%",
|
||||
left: "0%",
|
||||
right: "5%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: data.categories,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
},
|
||||
series: data.series,
|
||||
});
|
||||
|
||||
let chartInstance = null; // 图表实例
|
||||
|
||||
onMounted(() => {
|
||||
chartInstance = echarts.init(demand_chart.value);
|
||||
chartInstance.setOption(defaultChartOption.value);
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
chartInstance.resize();
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
window.removeEventListener("resize", () => {
|
||||
chartInstance.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div ref="demand_chart" style="width: 100%; height: 200px"></div>
|
||||
</template>
|
||||
<style scoped></style>
|
76
src/components/energy/EnergySankey.vue
Normal file
76
src/components/energy/EnergySankey.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const chartContainer = ref(null);
|
||||
let chart = null;
|
||||
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
});
|
||||
|
||||
const initChart = () => {
|
||||
chart = echarts.init(chartContainer.value);
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
triggerOn: "mousemove",
|
||||
},
|
||||
series: [
|
||||
{
|
||||
type: "sankey",
|
||||
layout: "none",
|
||||
emphasis: {
|
||||
focus: "adjacency",
|
||||
},
|
||||
data: [
|
||||
{ name: "總用電" },
|
||||
{ name: "照明" },
|
||||
{ name: "空調" },
|
||||
{ name: "設備A" },
|
||||
{ name: "設備B" },
|
||||
{ name: "其他" },
|
||||
],
|
||||
|
||||
links: [
|
||||
{ source: "總用電", target: "照明", value: 100 },
|
||||
{ source: "總用電", target: "空調", value: 150 },
|
||||
{ source: "總用電", target: "設備A", value: 80 },
|
||||
{ source: "總用電", target: "設備B", value: 120 },
|
||||
{ source: "總用電", target: "其他", value: 50 },
|
||||
],
|
||||
lineStyle: {
|
||||
color: "gradient",
|
||||
opacity: 0.7,
|
||||
curveness: 0.5,
|
||||
},
|
||||
itemStyle: {
|
||||
borderWidth: 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.setOption(option); // 設置圖表配置項
|
||||
|
||||
// 監聽視窗大小變化,調整圖表尺寸
|
||||
window.addEventListener("resize", () => {
|
||||
chart.resize();
|
||||
});
|
||||
};
|
||||
|
||||
// 在組件卸載時銷毀圖表實例,釋放資源
|
||||
onUnmounted(() => {
|
||||
if (chart) {
|
||||
chart.dispose();
|
||||
}
|
||||
window.removeEventListener("resize", () => {
|
||||
chart.resize();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div ref="chartContainer" style="width: 100%; height: 200px"></div>
|
||||
</template>
|
||||
<style scoped></style>
|
74
src/components/navbar/LeftSidebar.vue
Normal file
74
src/components/navbar/LeftSidebar.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import { defineProps, ref, watch, computed, onBeforeUnmount } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import useNavDataStore from "@/stores/useNavDataStore";
|
||||
const router = useRouter();
|
||||
const props = defineProps({
|
||||
showSidebar: {
|
||||
type: Boolean,
|
||||
},
|
||||
toggleSidebar: {
|
||||
type: Function,
|
||||
},
|
||||
});
|
||||
const navStore = useNavDataStore();
|
||||
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;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
const handleClick = (ord) => {
|
||||
if (ord) {
|
||||
router.push({
|
||||
name: "baja",
|
||||
query: { ord: encodeURIComponent(ord) },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// 監聽 openKeys 的變化
|
||||
watch(
|
||||
() => openKeys.value,
|
||||
(_val, oldVal) => {
|
||||
preOpenKeys.value = oldVal;
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-drawer
|
||||
:open="showSidebar"
|
||||
@close="toggleSidebar"
|
||||
:closable="false"
|
||||
placement="left"
|
||||
width="300"
|
||||
bodyStyle="padding: 0"
|
||||
>
|
||||
<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)">
|
||||
{{ 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>
|
||||
</a-sub-menu>
|
||||
</template>
|
||||
</a-menu>
|
||||
</a-drawer>
|
||||
</template>
|
111
src/components/navbar/NavAlarm.vue
Normal file
111
src/components/navbar/NavAlarm.vue
Normal file
@ -0,0 +1,111 @@
|
||||
<script setup>
|
||||
import { ref, watch, computed } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
|
||||
import useAlarmDataStore from "@/stores/useAlarmDataStore";
|
||||
|
||||
const niagaraStore = useNiagaraDataStore();
|
||||
const alarmDataStore = useAlarmDataStore();
|
||||
|
||||
// 計算 alarmCount 的總和
|
||||
const totalAlarmCount = computed(() => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => niagaraStore.alarmList.children,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue) {
|
||||
console.log("niagaraStore.alarmList changed:", newValue);
|
||||
initializeAlarmData();
|
||||
}
|
||||
},
|
||||
{ immediate: true } // 立即執行一次
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-for="child in alarmDataStore.alarmData" :key="child.key">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'baja',
|
||||
query: { ord: encodeURIComponent(child.Ord) },
|
||||
}"
|
||||
class="flex items-center justify-between gap-8"
|
||||
>
|
||||
<span>{{ child.name }}</span>
|
||||
<span>{{ child.alarmCount }}/{{ child.unackedCount }}</span>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<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>
|
||||
</a>
|
||||
</a-badge>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped></style>
|
44
src/components/navbar/NavBuild.vue
Normal file
44
src/components/navbar/NavBuild.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import useNavDataStore from "@/stores/useNavDataStore";
|
||||
|
||||
const navStore = useNavDataStore();
|
||||
const router = useRouter();
|
||||
|
||||
const buildmenu = ref([]);
|
||||
|
||||
const handleBuildClick = (key) => {
|
||||
navStore.setSelectedBuildingOrd(key);
|
||||
};
|
||||
|
||||
watch(
|
||||
() => navStore.menuList,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue && newValue.length > 0 && newValue[0].children) {
|
||||
buildmenu.value = newValue[0].children;
|
||||
navStore.setSelectedBuildingOrd(newValue[0].children[0].key);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-select
|
||||
v-if="buildmenu && buildmenu.length > 0"
|
||||
:default-value="buildmenu[0] ? buildmenu[0].key : null"
|
||||
@change="handleBuildClick"
|
||||
placeholder="請選擇建築"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="item in buildmenu"
|
||||
:key="item.key"
|
||||
:value="item.key"
|
||||
>
|
||||
{{ item.label }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped></style>
|
152
src/components/navbar/NavWeather.vue
Normal file
152
src/components/navbar/NavWeather.vue
Normal file
@ -0,0 +1,152 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
|
||||
import {
|
||||
imagesWeatherDay,
|
||||
imagesWeatherNight,
|
||||
orderWeather,
|
||||
} from "@/constants";
|
||||
|
||||
const niagaraStore = useNiagaraDataStore();
|
||||
|
||||
const weatherStateText = ref("N/A");
|
||||
const weatherStateImage = ref(null);
|
||||
const actualWeather = ref("N/A");
|
||||
const temperature = ref("N/A");
|
||||
const humidity = ref("N/A");
|
||||
const actualNighttime = ref("N/A");
|
||||
const dateTime = ref("N/A");
|
||||
|
||||
let intervalId = null;
|
||||
|
||||
// 初始化資料與訂閱更新
|
||||
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 (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");
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// 載入天氣資料函數
|
||||
const loadWeather = (actualWeather) => {
|
||||
let weatherStateTextTemp = "Unknown";
|
||||
let weatherStateImageTemp = null;
|
||||
const orderLength = orderWeather.length - 1;
|
||||
|
||||
if (orderLength < actualWeather) {
|
||||
console.error(
|
||||
"天氣狀態超出範圍!最大限度:" +
|
||||
orderLength +
|
||||
"| 實際值:" +
|
||||
actualWeather
|
||||
);
|
||||
weatherStateTextTemp = "Unknown";
|
||||
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
|
||||
} else if (actualWeather === "none") {
|
||||
console.warn("未設定天氣狀態或點不存在");
|
||||
weatherStateTextTemp = "Unknown";
|
||||
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
|
||||
} else {
|
||||
const actualWeatherNum = Number(actualWeather);
|
||||
if (actualNighttime.value) {
|
||||
console.log("Nighttime activ");
|
||||
weatherStateTextTemp = orderWeather[actualWeatherNum];
|
||||
weatherStateImageTemp = imagesWeatherNight[weatherStateTextTemp];
|
||||
} else {
|
||||
console.log("Daytime activ");
|
||||
weatherStateTextTemp = orderWeather[actualWeatherNum];
|
||||
weatherStateImageTemp = imagesWeatherDay[weatherStateTextTemp];
|
||||
}
|
||||
}
|
||||
weatherStateImage.value = weatherStateImageTemp;
|
||||
weatherStateText.value = weatherStateTextTemp; // 更新天氣狀態
|
||||
};
|
||||
|
||||
// 時間
|
||||
const updateTime = () => {
|
||||
const date = new Date();
|
||||
window.require &&
|
||||
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();
|
||||
intervalId = setInterval(updateTime, 1000);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="leading-none pt-2">
|
||||
<p class="flex items-center">
|
||||
<img
|
||||
v-if="weatherStateImage"
|
||||
:src="weatherStateImage"
|
||||
alt="Weather Icon"
|
||||
style="width: 20px; height: 20px; vertical-align: middle"
|
||||
/>
|
||||
- {{ weatherStateText }}
|
||||
</p>
|
||||
<p>{{ temperature }} | {{ humidity }}</p>
|
||||
<p>{{ dateTime }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
170
src/components/navbar/Navbar.vue
Normal file
170
src/components/navbar/Navbar.vue
Normal file
@ -0,0 +1,170 @@
|
||||
<script setup>
|
||||
import { computed, defineProps } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import NavBuild from "./NavBuild.vue";
|
||||
import NavWeather from "./NavWeather.vue";
|
||||
import NavAlarm from "./NavAlarm.vue";
|
||||
import useNiagaraDataStore from "@/stores/useNiagaraDataStore";
|
||||
import useNavDataStore from "@/stores/useNavDataStore";
|
||||
|
||||
const router = useRouter();
|
||||
const niagaraStore = useNiagaraDataStore();
|
||||
const navStore = useNavDataStore();
|
||||
|
||||
const props = defineProps({
|
||||
open: Function,
|
||||
userName: String,
|
||||
});
|
||||
|
||||
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 userList = computed(() => niagaraStore.userList?.children || []);
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-layout-header class="header">
|
||||
<div class="flex items-center">
|
||||
<img v-if="logoUrl" :src="logoUrl" alt="Logo" class="logo" />
|
||||
<a href="./index.html" class="text-2xl font-bold mx-4">{{
|
||||
systemName
|
||||
}}</a>
|
||||
<NavBuild />
|
||||
</div>
|
||||
<ul class="nav-menu flex gap-10">
|
||||
<li>
|
||||
<router-link
|
||||
:to="
|
||||
homeData.ord !== 'null'
|
||||
? {
|
||||
name: 'baja',
|
||||
query: { ord: encodeURIComponent(homeData.ord) },
|
||||
}
|
||||
: '/'
|
||||
"
|
||||
>
|
||||
<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>
|
||||
|
||||
<li>
|
||||
<a
|
||||
@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>
|
||||
<li v-for="item in dynamicMenu" :key="item.key">
|
||||
<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" />
|
||||
<span class="text-sm">{{ item.name }}</span>
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="nav-menu flex gap-10">
|
||||
<li>
|
||||
<NavWeather />
|
||||
</li>
|
||||
<li>
|
||||
<NavAlarm />
|
||||
</li>
|
||||
<li>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item v-for="child in userList" :key="child.key">
|
||||
<router-link
|
||||
:to="{
|
||||
name: 'baja',
|
||||
query: { ord: encodeURIComponent(child.ord) },
|
||||
}"
|
||||
>
|
||||
{{ child.name }}
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item>
|
||||
<a href="/logout">登出</a>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a class="flex flex-col items-center pt-3">
|
||||
<font-awesome-icon :icon="['fas', 'user']" size="2x" />
|
||||
<span class="text-sm">{{ props.userName }}</span>
|
||||
</a>
|
||||
</a-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</a-layout-header>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.nav-menu li a {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.header {
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
z-index: 4;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header.active {
|
||||
position: fixed;
|
||||
top: -75px;
|
||||
animation: slide-in 0.5s cubic-bezier(0.33, 0.85, 0.4, 0.96) forwards;
|
||||
box-shadow: 0 2px 25px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(75px);
|
||||
}
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>
|
64
src/constants/images.js
Normal file
64
src/constants/images.js
Normal file
@ -0,0 +1,64 @@
|
||||
// src/constants/images.js
|
||||
export const imagesWeatherDay = {
|
||||
Clear: "/file/UI_images/weather/dayClear.svg",
|
||||
Sunny: "/file/UI_images/weather/daySunny.svg",
|
||||
MostlySunny: "/file/UI_images/weather/dayMostlySunny.svg",
|
||||
PartlySunny: "/file/UI_images/weather/dayPartlySunny.svg",
|
||||
Fair: "/file/UI_images/weather/dayFair.svg",
|
||||
FewClouds: "/file/UI_images/weather/dayPartlySunny.svg",
|
||||
PartlyCloudy: "/file/UI_images/weather/dayPartlyCloudy.svg",
|
||||
MostlyCloudy: "/file/UI_images/weather/dayMostlyCloudy.svg",
|
||||
Cloudy: "/file/UI_images/weather/cloudy.svg",
|
||||
Overcast: "/file/UI_images/weather/cloudy.svg",
|
||||
LightRain: "/file/UI_images/weather/lightRain.svg",
|
||||
Rain: "/file/UI_images/weather/rain.svg",
|
||||
HeavyRain: "/file/UI_images/weather/heavyRain.svg",
|
||||
Thunderstorms: "/file/UI_images/weather/thunderstorms.svg",
|
||||
FreezingRain: "/file/UI_images/weather/freezingRain.svg",
|
||||
Fog: "/file/UI_images/weather/fog.svg",
|
||||
Haze: "/file/UI_images/weather/haze.svg",
|
||||
Snow: "/file/UI_images/weather/snow.svg",
|
||||
Windy: "/file/UI_images/weather/windy.svg",
|
||||
Misty: "/file/UI_images/weather/fog.svg",
|
||||
Dust: "/file/UI_images/weather/dust.svg",
|
||||
Unknown: "/file/UI_images/weather/unknown.svg",
|
||||
Tsunami: "/file/UI_images/weather/unknown.svg",
|
||||
Tornado: "/file/UI_images/weather/unknown.svg",
|
||||
Flood: "/file/UI_images/weather/unknown.svg",
|
||||
Fire: "/file/UI_images/weather/unknown.svg",
|
||||
Hurricane: "/file/UI_images/weather/unknown.svg",
|
||||
Earthquake: "/file/UI_images/weather/unknown.svg",
|
||||
Volcano: "/file/UI_images/weather/unknown.svg",
|
||||
};
|
||||
|
||||
export const imagesWeatherNight = {
|
||||
Clear: "/file/UI_images/weather/nightClear.svg",
|
||||
Sunny: "/file/UI_images/weather/nightSunny.svg",
|
||||
MostlySunny: "/file/UI_images/weather/nightMostlySunny.svg",
|
||||
PartlySunny: "/file/UI_images/weather/nightPartlySunny.svg",
|
||||
Fair: "/file/UI_images/weather/nightFair.svg",
|
||||
FewClouds: "/file/UI_images/weather/nightPartlySunny.svg",
|
||||
PartlyCloudy: "/file/UI_images/weather/nightPartlyCloudy.svg",
|
||||
MostlyCloudy: "/file/UI_images/weather/nightMostlyCloudy.svg",
|
||||
Cloudy: "/file/UI_images/weather/cloudy.svg",
|
||||
Overcast: "/file/UI_images/weather/cloudy.svg",
|
||||
LightRain: "/file/UI_images/weather/lightRain.svg",
|
||||
Rain: "/file/UI_images/weather/rain.svg",
|
||||
HeavyRain: "/file/UI_images/weather/heavyRain.svg",
|
||||
Thunderstorms: "/file/UI_images/weather/thunderstorms.svg",
|
||||
FreezingRain: "/file/UI_images/weather/freezingRain.svg",
|
||||
Fog: "/file/UI_images/weather/fog.svg",
|
||||
Haze: "/file/UI_images/weather/haze.svg",
|
||||
Snow: "/file/UI_images/weather/snow.svg",
|
||||
Windy: "/file/UI_images/weather/windy.svg",
|
||||
Misty: "/file/UI_images/weather/fog.svg",
|
||||
Dust: "/file/UI_images/weather/dust.svg",
|
||||
Unknown: "/file/UI_images/weather/unknown.svg",
|
||||
Tsunami: "/file/UI_images/weather/unknown.svg",
|
||||
Tornado: "/file/UI_images/weather/unknown.svg",
|
||||
Flood: "/file/UI_images/weather/unknown.svg",
|
||||
Fire: "/file/UI_images/weather/unknown.svg",
|
||||
Hurricane: "/file/UI_images/weather/unknown.svg",
|
||||
Earthquake: "/file/UI_images/weather/unknown.svg",
|
||||
Volcano: "/file/UI_images/weather/unknown.svg",
|
||||
};
|
2
src/constants/index.js
Normal file
2
src/constants/index.js
Normal file
@ -0,0 +1,2 @@
|
||||
export * from './weather';
|
||||
export * from './images';
|
31
src/constants/weather.js
Normal file
31
src/constants/weather.js
Normal file
@ -0,0 +1,31 @@
|
||||
export const orderWeather = [
|
||||
"Clear",
|
||||
"Sunny",
|
||||
"MostlySunny",
|
||||
"PartlySunny",
|
||||
"Fair",
|
||||
"FewClouds",
|
||||
"PartlyCloudy",
|
||||
"MostlyCloudy",
|
||||
"Cloudy",
|
||||
"Overcast",
|
||||
"LightRain",
|
||||
"Rain",
|
||||
"HeavyRain",
|
||||
"Thunderstorms",
|
||||
"FreezingRain",
|
||||
"Fog",
|
||||
"Haze",
|
||||
"Snow",
|
||||
"Windy",
|
||||
"Misty",
|
||||
"Dust",
|
||||
"Unknown",
|
||||
"Tsunami",
|
||||
"Tornado",
|
||||
"Flood",
|
||||
"Fire",
|
||||
"Hurricane",
|
||||
"Earthquake",
|
||||
"Volcano",
|
||||
];
|
45
src/main.js
Normal file
45
src/main.js
Normal file
@ -0,0 +1,45 @@
|
||||
import { createApp } from "vue";
|
||||
import "./style.css";
|
||||
import Antd from "ant-design-vue";
|
||||
import { createPinia } from "pinia";
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faTv,
|
||||
faChartPie,
|
||||
faBell,
|
||||
faUser,
|
||||
faDesktop,
|
||||
faCog,
|
||||
faSearch,
|
||||
faHome,
|
||||
faUserCircle,
|
||||
faUserEdit,
|
||||
faInfoCircle,
|
||||
faSignOutAlt,
|
||||
faAngleDown
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
library.add(
|
||||
faTv,
|
||||
faChartPie,
|
||||
faBell,
|
||||
faUser,
|
||||
faDesktop,
|
||||
faCog,
|
||||
faSearch,
|
||||
faHome,
|
||||
faUserCircle,
|
||||
faUserEdit,
|
||||
faInfoCircle,
|
||||
faSignOutAlt,
|
||||
faAngleDown
|
||||
);
|
||||
const pinia = createPinia();
|
||||
const app = createApp(App);
|
||||
app.component("font-awesome-icon", FontAwesomeIcon);
|
||||
app.use(pinia);
|
||||
app.use(router);
|
||||
app.use(Antd);
|
||||
app.mount("#app");
|
27
src/router/index.js
Normal file
27
src/router/index.js
Normal file
@ -0,0 +1,27 @@
|
||||
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";
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: "/",
|
||||
name: "dashboardPage",
|
||||
component: DashboardPage,
|
||||
},
|
||||
{
|
||||
path: "/baja",
|
||||
name: "baja",
|
||||
component: SystemPage,
|
||||
},
|
||||
{
|
||||
path: "/energy",
|
||||
name: "energy",
|
||||
component: EnergyChart,
|
||||
}
|
||||
],
|
||||
});
|
||||
|
||||
export default router;
|
34
src/stores/useAlarmDataStore.js
Normal file
34
src/stores/useAlarmDataStore.js
Normal file
@ -0,0 +1,34 @@
|
||||
// stores/useAlarmDataStore.js
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
const useAlarmDataStore = defineStore("alarmData", () => {
|
||||
const alarmData = ref([]);
|
||||
|
||||
// 建立 alarmData 的函數
|
||||
const createAlarmData = (alarmList) => {
|
||||
alarmData.value = alarmList.children.map((item, index) => ({
|
||||
key: item.key,
|
||||
name: item.name || `Alarm ${index + 1}`,
|
||||
alarmCount: 0,
|
||||
unackedCount: 0,
|
||||
alarmOrd: item.children && item.children[0] ? item.children[0].ord : null, // 儲存 alarmOrd
|
||||
Ord: item.ord ? item.ord : null,
|
||||
}));
|
||||
};
|
||||
|
||||
// 更新特定 alarm 數據的函數
|
||||
const updateAlarmItem = (index, alarmCount, unackedCount) => {
|
||||
if (index >= 0 && index < alarmData.value.length) {
|
||||
alarmData.value.splice(index, 1, {
|
||||
...alarmData.value[index],
|
||||
alarmCount,
|
||||
unackedCount,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { alarmData, createAlarmData, updateAlarmItem };
|
||||
});
|
||||
|
||||
export default useAlarmDataStore;
|
91
src/stores/useNavDataStore.js
Normal file
91
src/stores/useNavDataStore.js
Normal file
@ -0,0 +1,91 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import useUserStore from "@/stores/useUserStore";
|
||||
|
||||
const useNavDataStore = defineStore("nav", () => {
|
||||
const userStore = useUserStore();
|
||||
|
||||
const selectedBuildingOrd = ref(null);
|
||||
const menuList = ref([]);
|
||||
|
||||
const setSelectedBuildingOrd = (ord) => {
|
||||
selectedBuildingOrd.value = ord;
|
||||
};
|
||||
|
||||
const getNavData = () => {
|
||||
if (window.require && window.requirejs) {
|
||||
window.requirejs(["baja!"], (baja) => {
|
||||
console.log("進入 bajaSubscriber 準備執行 BQL 訂閱");
|
||||
|
||||
const ord = `local:|foxs:|file:^UI_Framework/System/S_${userStore.userRole}.nav`;
|
||||
|
||||
baja.Ord.make(ord)
|
||||
.get()
|
||||
.then((nav) => {
|
||||
if (!nav || !nav.$fileData) {
|
||||
console.error("未能取得有效 NAV 資料!");
|
||||
} else {
|
||||
console.log("成功讀取 NAV 資料", nav);
|
||||
// 將 XML 文本解析成 DOM 結構
|
||||
fetch(nav.$fileData.read)
|
||||
.then((response) => response.text())
|
||||
.then((xmlText) => parseXmlData(xmlText))
|
||||
.catch((err) => {
|
||||
console.error(`讀取 NAV 檔案失敗:${err.message}`);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`讀取 NAV 檔案失敗:${err.message}`);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.error("未能載入 Baja 環境,請確認依賴已正確加載。");
|
||||
}
|
||||
};
|
||||
|
||||
const parseXmlData = (xmlText) => {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlText, "application/xml");
|
||||
const navNodes = xmlDoc.getElementsByTagName("node");
|
||||
|
||||
if (!navNodes.length) {
|
||||
console.error("無法解析 NAV XML 結構。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 只處理第一個節點
|
||||
const firstNode = navNodes[0];
|
||||
menuList.value = firstNode ? [parseNode(firstNode, "1")] : [];
|
||||
console.log("menuList.value", menuList.value);
|
||||
};
|
||||
|
||||
// 遞迴解析 XML 結構
|
||||
const parseNode = (node, key) => {
|
||||
const name = node.getAttribute("name");
|
||||
const ord = node.getAttribute("ord");
|
||||
const icon = node.getAttribute("icon");
|
||||
|
||||
// 處理子節點遞迴
|
||||
const childrenNodes = Array.from(node.children);
|
||||
const children = childrenNodes.length > 0 ? childrenNodes.map((child, index) => parseNode(child, `${key}-${index + 1}`)) : null;
|
||||
|
||||
return {
|
||||
key,
|
||||
label: name,
|
||||
title: name,
|
||||
icon,
|
||||
ord,
|
||||
children,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
selectedBuildingOrd,
|
||||
setSelectedBuildingOrd,
|
||||
menuList,
|
||||
getNavData,
|
||||
};
|
||||
});
|
||||
|
||||
export default useNavDataStore;
|
145
src/stores/useNiagaraDataStore.js
Normal file
145
src/stores/useNiagaraDataStore.js
Normal file
@ -0,0 +1,145 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import useUserStore from "@/stores/useUserStore";
|
||||
|
||||
const useNiagaraDataStore = defineStore("niagara", () => {
|
||||
const userStore = useUserStore();
|
||||
|
||||
const systemList = ref([]);
|
||||
const headerList = ref([]);
|
||||
const weatherList = ref([]);
|
||||
const alarmList = ref([]);
|
||||
const userList = ref([]);
|
||||
const DynamicList = ref([]);
|
||||
|
||||
const getSystemData = () => {
|
||||
if (window.require && window.requirejs) {
|
||||
window.requirejs(["baja!"], (baja) => {
|
||||
console.log("進入 bajaSubscriber 準備執行 BQL 訂閱");
|
||||
|
||||
const ord = `local:|foxs:|file:^UI_Framework/Function/F_${userStore.userRole}.nav`;
|
||||
|
||||
baja.Ord.make(ord)
|
||||
.get()
|
||||
.then((nav) => {
|
||||
if (!nav || !nav.$fileData) {
|
||||
console.error("未能取得有效 NAV 資料!");
|
||||
} else {
|
||||
console.log("成功讀取 NAV 資料", nav);
|
||||
// 將 XML 文本解析成 DOM 結構
|
||||
fetch(nav.$fileData.read)
|
||||
.then((response) => response.text())
|
||||
.then((xmlText) => parseXmlData(xmlText))
|
||||
.catch((err) => {
|
||||
console.error(`讀取 NAV 檔案失敗:${err.message}`);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`讀取 NAV 檔案失敗:${err.message}`);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
console.error("未能載入 Baja 環境,請確認依賴已正確加載。");
|
||||
}
|
||||
};
|
||||
|
||||
const parseXmlData = (xmlText) => {
|
||||
const parser = new DOMParser();
|
||||
const xmlDoc = parser.parseFromString(xmlText, "application/xml");
|
||||
const navNodes = xmlDoc.getElementsByTagName("node");
|
||||
|
||||
if (!navNodes.length) {
|
||||
console.error("無法解析 NAV XML 結構。");
|
||||
return;
|
||||
}
|
||||
|
||||
// 轉換 XML node 結構為物件
|
||||
const firstNode = navNodes[0];
|
||||
systemList.value = firstNode ? [parseNode(firstNode, "1")] : [];
|
||||
console.log("systemList.value", systemList.value);
|
||||
|
||||
const Fix_Node = systemList.value[0].children.find(
|
||||
(node) => node.name === "Fix_Item"
|
||||
);
|
||||
const Dynamic_Node = systemList.value[0].children.find(
|
||||
(node) => node.name === "Dynamic_Item"
|
||||
);
|
||||
|
||||
// 查找 "Fix_Item" 節點下各節點的list
|
||||
if (Fix_Node && Fix_Node.children) {
|
||||
headerList.value =
|
||||
Fix_Node.children.find((node) => node.name === "header") || null;
|
||||
weatherList.value =
|
||||
Fix_Node.children.find((node) => node.name === "weather") || null;
|
||||
alarmList.value =
|
||||
Fix_Node.children.find((node) => node.name === "alarmlist") || null;
|
||||
userList.value =
|
||||
Fix_Node.children.find((node) => node.name === "user") || null;
|
||||
}
|
||||
|
||||
if (Dynamic_Node && Dynamic_Node.children) {
|
||||
DynamicList.value = Dynamic_Node || null;
|
||||
}
|
||||
|
||||
console.log(
|
||||
"Fix_Item 節點下各節點的list",
|
||||
headerList.value,
|
||||
weatherList.value,
|
||||
alarmList.value,
|
||||
userList.value
|
||||
);
|
||||
console.log(
|
||||
"Dynamic_Item 節點下各節點的list",
|
||||
DynamicList.value
|
||||
);
|
||||
};
|
||||
|
||||
// 遞迴解析 XML 結構
|
||||
const parseNode = (node, key) => {
|
||||
const name = node.getAttribute("name");
|
||||
const ord = node.getAttribute("ord");
|
||||
const icon = node.getAttribute("icon");
|
||||
|
||||
// 處理子節點遞迴
|
||||
const childrenNodes = Array.from(node.children);
|
||||
const children =
|
||||
childrenNodes.length > 0
|
||||
? childrenNodes.map((child, index) =>
|
||||
parseNode(child, `${key}-${index + 1}`)
|
||||
)
|
||||
: null;
|
||||
|
||||
return {
|
||||
key,
|
||||
name,
|
||||
ord,
|
||||
children,
|
||||
icon,
|
||||
};
|
||||
};
|
||||
|
||||
const findNodeByName = (nodes, name) => {
|
||||
for (const node of nodes) {
|
||||
if (node.name === name) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
const result = findNodeByName(node.children, name);
|
||||
if (result) return result;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return {
|
||||
systemList,
|
||||
headerList,
|
||||
weatherList,
|
||||
alarmList,
|
||||
userList,
|
||||
DynamicList,
|
||||
getSystemData,
|
||||
};
|
||||
});
|
||||
export default useNiagaraDataStore;
|
38
src/stores/useUserStore.js
Normal file
38
src/stores/useUserStore.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
|
||||
const useUserStore = defineStore("user", () => {
|
||||
const userName = ref("webUser");
|
||||
const userRole = ref("");
|
||||
|
||||
const loadUserInfo = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.require &&
|
||||
window.requirejs(["baja!"], (baja) => {
|
||||
let currentUserName = baja.getUserName();
|
||||
userName.value = 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);
|
||||
}
|
||||
}
|
||||
resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(`訂閱失敗: ${err.message}`);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return { userName, userRole, loadUserInfo };
|
||||
});
|
||||
|
||||
export default useUserStore;
|
1
src/style.css
Normal file
1
src/style.css
Normal file
@ -0,0 +1 @@
|
||||
@import "tailwindcss";
|
33
src/views/dashboard/DashboardPage.vue
Normal file
33
src/views/dashboard/DashboardPage.vue
Normal file
@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import DashboardStat from "@/components/dashboard/dashboardStat.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-col :span="8">
|
||||
<a-image width="100%" src="./build.jpg" class="rounded shadow-lg" />
|
||||
</a-col>
|
||||
<a-col :span="16">
|
||||
<!-- 用電數據 -->
|
||||
<DashboardStat />
|
||||
<!-- 用電圖表 -->
|
||||
<DashboardElecChart />
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="24" class="p-5">
|
||||
<a-col :span="16">
|
||||
<!-- <DashboardTag /> -->
|
||||
</a-col>
|
||||
<a-col :span="8" class="">
|
||||
<!-- 告警 -->
|
||||
<!-- <DashboardAlert /> -->
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
126
src/views/energy/EnergyChart.vue
Normal file
126
src/views/energy/EnergyChart.vue
Normal file
@ -0,0 +1,126 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, computed } from "vue";
|
||||
import EnergySankey from "@/components/energy/EnergySankey.vue";
|
||||
import EnergyLine from "@/components/energy/EnergyLine.vue";
|
||||
import EnergyBar from "@/components/energy/EnergyBar.vue";
|
||||
|
||||
const statisticData = ref([
|
||||
{ title: "今年電費累計", value: 58792.56, unit: "元" },
|
||||
{ title: "區間電費", value: 1234.78, unit: "元" },
|
||||
{ title: "今年碳排當量累計", value: 3456.23, unit: "公斤" },
|
||||
{ title: "區間碳排當量", value: 15.67, unit: "公斤" },
|
||||
{ title: "今年用電度數", value: 1890.45, unit: "kWh" },
|
||||
{ title: "區間用電度數", value: 123.9, unit: "kWh" },
|
||||
]);
|
||||
|
||||
// 每月用電分析
|
||||
const monthlyElectricityData = ref({
|
||||
categories: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "基本電量",
|
||||
type: "bar",
|
||||
data: [120, 100, 110, 130, 150, 160, 140, 150, 130, 120, 110, 130],
|
||||
},
|
||||
{
|
||||
name: "流動電量",
|
||||
type: "bar",
|
||||
data: [80, 60, 70, 90, 100, 110, 90, 100, 80, 70, 60, 80],
|
||||
},
|
||||
{
|
||||
name: "總電量",
|
||||
type: "bar",
|
||||
data: [200, 160, 180, 220, 250, 270, 230, 250, 210, 190, 170, 210],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 每月碳排當量
|
||||
const monthlyCarbonData = ref({
|
||||
categories: [
|
||||
"Jan",
|
||||
"Feb",
|
||||
"Mar",
|
||||
"Apr",
|
||||
"May",
|
||||
"Jun",
|
||||
"Jul",
|
||||
"Aug",
|
||||
"Sep",
|
||||
"Oct",
|
||||
"Nov",
|
||||
"Dec",
|
||||
],
|
||||
series: [
|
||||
{
|
||||
name: "碳排當量",
|
||||
type: "bar",
|
||||
data: [50, 40, 45, 55, 60, 65, 55, 60, 50, 45, 40, 50],
|
||||
},
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-row :gutter="24" class="p-5">
|
||||
<!-- 用電即時分佈 -->
|
||||
<a-col :span="12">
|
||||
<h3 class="text-lg font-medium">用電即時分佈</h3>
|
||||
<EnergySankey />
|
||||
</a-col>
|
||||
<!-- 即時需量 -->
|
||||
<a-col :span="12">
|
||||
<h3 class="text-lg font-medium">即時需量</h3>
|
||||
<EnergyLine />
|
||||
</a-col>
|
||||
<!-- 資訊區 -->
|
||||
<a-col :span="8" class="mt-10">
|
||||
<a-card class="">
|
||||
<a-card-grid
|
||||
v-for="(item, index) in statisticData"
|
||||
:key="index"
|
||||
style="width: 50%; padding: 5px"
|
||||
:class="{ 'bg-stone-100': index == 0 || index == 3 || index == 4 }"
|
||||
>
|
||||
<a-statistic
|
||||
:title="item.title"
|
||||
:value="item.value"
|
||||
:suffix="item.unit"
|
||||
/></a-card-grid>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<!-- 每月用電分析 -->
|
||||
<a-col :span="8" class="mt-10">
|
||||
<h3 class="text-lg font-medium">每月用電分析</h3>
|
||||
<EnergyBar :chartData="monthlyElectricityData" />
|
||||
</a-col>
|
||||
<!-- 每月碳排當量 (kgCO2e) -->
|
||||
<a-col :span="8" class="mt-10">
|
||||
<h3 class="text-lg font-medium">每月碳排當量 (kgCO2e)</h3>
|
||||
<EnergyBar :chartData="monthlyCarbonData" />
|
||||
</a-col>
|
||||
<!-- 每月用電分析 -->
|
||||
<a-col :span="8" class="mt-10">
|
||||
<h3 class="text-lg font-medium">每月用電分析</h3>
|
||||
<EnergyBar :chartData="monthlyElectricityData" />
|
||||
</a-col>
|
||||
<!-- 每月碳排當量 (kgCO2e) -->
|
||||
<a-col :span="16" class="mt-10">
|
||||
<h3 class="text-lg font-medium">每月碳排當量 (kgCO2e)</h3>
|
||||
<EnergyBar :chartData="monthlyElectricityData" />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
38
src/views/system/SystemPage.vue
Normal file
38
src/views/system/SystemPage.vue
Normal file
@ -0,0 +1,38 @@
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const ordUrl = computed(() => {
|
||||
const ord = route.query.ord || "";
|
||||
const decodedOrd = decodeURIComponent(ord);
|
||||
console.log("route.query.ord:", ord); // 除錯用
|
||||
console.log("decodedOrd:", decodedOrd); // 除錯用
|
||||
return decodedOrd;
|
||||
});
|
||||
|
||||
const iframeSrc = computed(() => {
|
||||
if (!ordUrl.value) {
|
||||
return ""; // 如果 ordUrl 為空,則不設定 src
|
||||
}
|
||||
|
||||
if (
|
||||
ordUrl.value.startsWith("http://") ||
|
||||
ordUrl.value.startsWith("https://")
|
||||
) {
|
||||
return ordUrl.value; // 如果以 http 開頭,則直接使用 ordUrl 作為 src
|
||||
} else {
|
||||
return `/ord?${ordUrl.value}|view:?fullScreen=true`; // 否則,添加 Niagara 前綴
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<iframe
|
||||
v-if="iframeSrc"
|
||||
:src="iframeSrc"
|
||||
width="100%"
|
||||
:style="{ height: 'calc(100vh - 90px)' }"
|
||||
></iframe>
|
||||
</template>
|
21
vite.config.js
Normal file
21
vite.config.js
Normal file
@ -0,0 +1,21 @@
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { defineConfig } from "vite";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
export default defineConfig({
|
||||
base: process.env.NODE_ENV === "production" ? "./" : "/",
|
||||
build: {
|
||||
outDir: process.env.NODE_ENV === "production" ? "../dist" : "./dist",
|
||||
emptyOutDir: true,
|
||||
},
|
||||
plugins: [vue(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000, // 指定端口為 3000
|
||||
},
|
||||
});
|
Loading…
Reference in New Issue
Block a user