首頁重新切版 | 樓層與部門放置store | 設定MQTT重新切版

This commit is contained in:
koko 2025-03-03 10:46:53 +08:00
parent c8d8fbf254
commit f956402648
32 changed files with 1371 additions and 217 deletions

View File

@ -1,22 +1,15 @@
<script setup>
import { getBuildings } from "@/apis/building";
import { onMounted, ref } from "vue";
import { onMounted } from "vue";
import useBuildingStore from "@/stores/useBuildingStore";
const store = useBuildingStore();
const getBui = async () => {
const res = await getBuildings();
store.buildings = res.data;
store.selectedBuilding = res?.data[0];
};
const selectBuilding = (bui) => {
store.selectedBuilding = bui;
store.selectedBuilding = bui; // selectedBuildingwatch
};
onMounted(() => {
getBui();
store.initialize(); //
});
</script>

View File

@ -46,11 +46,11 @@ const getSubPage = async (system_type) => {
const res = await getSideBar(system_type);
menu_array.value = res.data;
};
const showDrawer = (authCode) => {
const showDrawer = async (authCode) => {
if (authCode === "PF1") {
getSubMonitorPage();
await getSubMonitorPage();
} else if (authCode === "PF2" || authCode === "PF11") {
getSubPage(authCode === "PF2" ? "Energy" : "Setting");
await getSubPage(authCode === "PF2" ? "Energy" : "Setting");
}
currentAuthCode.value = authCode;
open.value = true;

View File

@ -101,7 +101,37 @@
"total_amount": "金额总计",
"elec_price_list": "电价表",
"residential": "住宅型",
"standard": "标准型"
"standard": "标准型",
"simple_elec_price_two_stage": "简易型时间电价二段式",
"simple_elec_price_three_stage": "简易型时间电价三段式",
"classification": "分类",
"summer_months": "夏月",
"non_summer_months": "非夏月",
"time_outside_summer_months": "夏月以外的时间",
"basic_elec_charge": "基本电费",
"charged_per_household": "按户计收",
"per_household_month": "每户每月",
"mon_to_friday": "周一~周五",
"peak_hours": "尖峰时间",
"semi_peak_hours": "半尖峰时间",
"off_peak_hours": "离峰时间",
"price_per_kwh": "每度",
"all_day": "全日",
"sat_sun_off_peak_days": "周六、周日及离峰日",
"usage_over_2000kwh": "每月总度数超过2000度之部分",
"add": "加",
"standard_time_of_use_tariff_2_stage": "标准型时间电价二段式",
"standard_time_of_use_tariff_3_stage": "标准型时间电价三段式",
"single_phase": "单相",
"three_phase": "三相",
"frequent_contract": "经常契约",
"per_kw_per_month": "每瓩每月",
"non_summer_contract": "非夏日契约",
"saturday_semi_peak_contract": "周六半尖峰契约",
"off_peak_contract": "离峰契约",
"variable_electricity_charge": "流动电费",
"saturday": "周六",
"sunday_and_off_peak_days": "周日及离峰日"
},
"alarm": {
"title": "显示警告",

View File

@ -101,7 +101,37 @@
"total_amount": "金額總計",
"elec_price_list": "電價表",
"residential": "住宅型",
"standard": "標準型"
"standard": "標準型",
"simple_elec_price_two_stage":"簡易型時間電價二段式",
"simple_elec_price_three_stage":"簡易型時間電價三段式",
"classification":"分類",
"summer_months":"夏月",
"non_summer_months":"非夏月",
"time_outside_summer_months":"夏月以外的時間",
"basic_elec_charge": "基本電費",
"charged_per_household": "按戶計收",
"per_household_month": "每戶每月",
"mon_to_friday": "週一~週五",
"peak_hours": "尖峰時間",
"semi_peak_hours": "半尖峰時間",
"off_peak_hours": "離峰時間",
"price_per_kwh": "每度",
"all_day": "全日",
"sat_sun_off_peak_days": "週六、週日及離峰日",
"usage_over_2000kwh": "每月總度數超過2000度之部分",
"add": "加",
"standard_time_of_use_tariff_2_stage": "標準型時間電價二段式",
"standard_time_of_use_tariff_3_stage": "標準型時間電價三段式",
"single_phase": "單相",
"three_phase": "三相",
"frequent_contract": "經常契約",
"per_kw_per_month": "每瓩每月",
"non_summer_contract": "非夏日契約",
"saturday_semi_peak_contract": "週六半尖峰契約",
"off_peak_contract": "離峰契約",
"variable_electricity_charge": "流動電費",
"saturday": "週六",
"sunday_and_off_peak_days": "週日及離峰日"
},
"alarm": {
"title": "顯示警告",

View File

@ -101,7 +101,37 @@
"total_amount": "Total amount",
"elec_price_list": "Electricity Price List",
"residential": "Residential",
"standard": "Standard"
"standard": "Standard",
"simple_elec_price_two_stage": "Simple Time-of-Use Electricity Price (Two-Tier)",
"simple_elec_price_three_stage": "Simple Time-of-Use Electricity Price (Three-Tier)",
"classification": "Classification",
"summer_months": "Summer Months",
"non_summer_months": "Non-Summer Months",
"time_outside_summer_months": "Time Outside Summer Months",
"basic_elec_charge": "Basic Electricity Charge",
"charged_per_household": "Charged Per Household",
"per_household_month": "Per Household Per Month",
"mon_to_friday": "Monday to Friday",
"peak_hours": "Peak Hours",
"semi_peak_hours": "Semi-Peak Hours",
"off_peak_hours": "Off-Peak Hours",
"price_per_kwh": "Price Per kWh",
"all_day": "All Day",
"sat_sun_off_peak_days": "Saturday, Sunday, and Off-Peak Days",
"usage_over_2000kwh": "Usage Over 2000 kWh Per Month",
"add": "Add",
"standard_time_of_use_tariff_2_stage": "Standard Time-of-Use Tariff (Two-Tier)",
"standard_time_of_use_tariff_3_stage": "Standard Time-of-Use Tariff (Three-Tier)",
"single_phase": "Single Phase",
"three_phase": "Three Phase",
"frequent_contract": "Demand Charge",
"per_kw_per_month": "Per kW Per Month",
"non_summer_contract": "Non-Summer Demand Charge",
"saturday_semi_peak_contract": "Saturday Semi-Peak Demand Charge",
"off_peak_contract": "Off-Peak Demand Charge",
"variable_electricity_charge": "Variable Electricity Charge",
"saturday": "Saturday",
"sunday_and_off_peak_days": "Sunday and Off-Peak Days"
},
"alarm": {
"title": "Warning",

View File

@ -61,6 +61,7 @@ import {
faDownload,
faStream,
faSave,
faCrown
} from "@fortawesome/free-solid-svg-icons";
/* add icons to the library */
@ -122,7 +123,8 @@ library.add(
faGlobe,
faDownload,
faStream,
faSave
faSave,
faCrown
);
export default library;

View File

@ -1,17 +1,18 @@
import { defineStore } from "pinia";
import { ref, computed } from "vue";
import { ref, computed, watch } from "vue";
import { useRoute } from "vue-router";
import { getBuildings } from "@/apis/building";
import { getAssetFloorList, getDepartmentList } from "@/apis/asset";
const useBuildingStore = defineStore("buildingInfo", () => {
// 所有棟別
// 狀態定義
const buildings = ref([]);
const selectedBuilding = ref(null);
// 所有大小類系統
const floorList = ref([]);
const deptList = ref([]);
const mainSubSys = ref([]);
// 所有大類
// 計算屬性
const mainSys = computed(() =>
mainSubSys.value.map(({ main_system_tag, full_name }) => ({
main_system_tag,
@ -21,7 +22,6 @@ const useBuildingStore = defineStore("buildingInfo", () => {
const subSys = computed(() => {
let subPages = [];
mainSubSys.value.forEach(({ main_system_tag, history_Sub_systems }) => {
subPages = [
...subPages,
@ -32,7 +32,6 @@ const useBuildingStore = defineStore("buildingInfo", () => {
})),
];
});
return subPages;
});
@ -44,14 +43,60 @@ const useBuildingStore = defineStore("buildingInfo", () => {
return null;
});
// 獲取所有建築物
const fetchBuildings = async () => {
const res = await getBuildings();
buildings.value = res.data;
if (res.data.length > 0 && !selectedBuilding.value) {
selectedBuilding.value = res.data[0]; // 預設選第一個建築
}
};
// 獲取樓層資料
const fetchFloorList = async () => {
const res = await getAssetFloorList();
floorList.value = res.data[0]?.floors.map((d) => ({
...d,
title: d.full_name,
key: d.floor_guid,
})) || [];
};
// 獲取部門資料
const fetchDepartmentList = async () => {
const res = await getDepartmentList();
deptList.value = res.data.map((d) => ({
...d,
title: d.name,
key: d.id,
})) || [];
};
// 當 selectedBuilding 改變時,更新 floorList 和 deptList
watch(selectedBuilding, async (newBuilding) => {
if (newBuilding) {
await Promise.all([fetchFloorList(), fetchDepartmentList()]);
}
});
// 初始化資料
const initialize = async () => {
await fetchBuildings();
};
return {
buildings,
selectedBuilding,
floorList,
deptList,
mainSubSys,
mainSys,
subSys,
selectedSystem,
fetchBuildings,
fetchFloorList,
fetchDepartmentList,
initialize,
};
});
export default useBuildingStore;

View File

@ -4,8 +4,10 @@ import AssetMainList from "./components/AssetMainList.vue";
import AssetSubList from "./components/AssetSubList.vue";
import AssetTable from "./components/AssetTable.vue";
import { getOperationCompanyList } from "@/apis/operation";
import { getDepartmentList, getIOTSchema, getElecTypeList, getAssetFloorList } from "@/apis/asset";
import { getIOTSchema, getElecTypeList } from "@/apis/asset";
import useSearchParam from "@/hooks/useSearchParam";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
const { searchParams, changeParams } = useSearchParam();
const companyOptions = ref([]);
const iotSchemaOptions = ref([]);
@ -24,20 +26,12 @@ const getElecType = async () => {
const res = await getElecTypeList();
elecTypeOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
const getDepartment = async () => {
const res = await getDepartmentList();
departmentList.value = res.data.map((d) => ({ ...d, key: d.id }));
};
const getFloors = async () => {
const res = await getAssetFloorList();
floors.value = res.data[0]?.floors.map((d) => ({ ...d, key: d.floor_guid }));
};
onMounted(() => {
getCompany();
getElecType();
getDepartment();
getFloors();
floors.value = storeBuild.floorList;
departmentList.value = storeBuild.deptList;
});
watch(

View File

@ -1,11 +1,13 @@
<script setup>
import { onMounted, ref, provide, watch } from "vue";
import { getDashboardInit } from "@/apis/dashboard";
import Forge from "@/components/forge/Forge.vue";
import DashboardStat from "./components/DashboardStat.vue";
import DashboardElecChart from "./components/DashboardElecChart.vue";
import DashboardSysCard from "./components/DashboardSysCard.vue";
import DashboardSysProgress from "./components/DashboardSysProgress.vue";
import { getDashboardInit } from "@/apis/dashboard";
import { onMounted, ref, provide, watch } from "vue";
import DashboardElecRank from "./components/DashboardElecRank.vue";
import DashboardElecTrends from "./components/DashboardElecTrends.vue";
import DashboardElecCompare from "./components/DashboardElecCompare.vue";
const initialData = ref(null);
// const forgeData = ref([]);
@ -52,24 +54,37 @@ provide("dashboard_items", {
<div class="w-full xl:w-2/3">
<!-- 用電數據 -->
<DashboardStat />
<!-- 用電圖表 -->
<DashboardElecChart />
<div class="flex flex-wrap pt-4">
<!-- 當月能耗排行 -->
<div class="lg:w-1/3 w-full ">
<DashboardElecRank />
</div>
<!-- 近30天能耗趨勢 -->
<div class="lg:w-2/3 w-full ">
<DashboardElecTrends />
</div>
</div>
</div>
<!-- 設備小卡 -->
<div class="w-full lg:w-2/3">
<div class="w-full lg:w-1/3">
<DashboardSysCard />
</div>
<!--狀態進度-->
<div class="w-full lg:w-1/3">
<DashboardSysProgress />
</div>
<!-- 環比能耗 -->
<div class="w-full lg:w-1/3">
<DashboardElecCompare />
</div>
</div>
</template>
<style lang="css" scoped>
.area-img-box {
@apply border border-light-info bg-dark-info w-full h-[400px] block relative rounded-sm mb-4;
@apply border border-light-info bg-dark-info w-full h-[460px] block relative rounded-sm mt-3;
}
.area-img-box::before {

View File

@ -0,0 +1,235 @@
<script setup>
import { ref, onMounted } from "vue";
import * as echarts from "echarts";
import BarChart from "@/components/chart/BarChart.vue";
const chartData = ref([
{
category: "日環比",
this: 230.68,
last: 377.33,
change: -39,
},
{
category: "周環比",
this: 608.01,
last: 2711.09,
change: -78,
},
{
category: "月環比",
this: 6473.8,
last: 12701.69,
change: -49,
},
{
category: "年環比",
this: 46687.17,
last: null,
change: null,
},
]);
const labels = ["今日", "昨日", "本周", "上周", "本月", "上月", "今年", "去年"];
const barWidth = 30; // Set barWidth
const barChartOptions = ref({
xAxis: {
type: "category",
data: chartData.value.map((item) => item.category),
axisLine: { lineStyle: { color: "#fff" } },
},
yAxis: { type: "value", show: false },
grid: {
left: "-10%",
right: "0%",
bottom: "3%",
top: "10%",
containLabel: true,
},
series: [
{
name: "當前週期",
data: chartData.value.map((item) => item.this),
type: "bar",
barWidth: barWidth,
barGap: "-10%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#186B80" },
{ offset: 1, color: "#50C3E3" },
]),
shadowBlur: 5,
shadowColor: "rgba(0, 0, 0, 0.3)",
shadowOffsetY: 2,
shadowOffsetX: 5,
},
z: 3,
},
{
name: "對比週期",
data: chartData.value.map((item) => item.last),
type: "bar",
barWidth: barWidth,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#988F2C" },
{ offset: 1, color: "#FFF26D" },
]),
shadowBlur: 5,
shadowColor: "rgba(0, 0, 0, 0.3)",
shadowOffsetY: 2,
shadowOffsetX: 5,
},
},
{
// this top
z: 6,
type: "pictorialBar",
symbolPosition: "end",
data: chartData.value.map((item) => item.this),
symbol: "diamond",
symbolOffset: ["-45%", "-50%"],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
borderWidth: 0,
color: "#50C3E3",
},
},
{
// this bot
z: 6,
type: "pictorialBar",
symbolPosition: "start",
data: chartData.value.map((item) => item.this),
symbol: "diamond",
symbolOffset: ["-45%", "50%"],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
borderWidth: 0,
color: "#50C3E3",
},
},
{
// last top
z: 3,
type: "pictorialBar",
symbolPosition: "end",
data: chartData.value.map((item) => item.last),
symbol: "diamond",
symbolOffset: ["45%", "-50%"],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
borderWidth: 0,
color: "#FFF26D",
},
},
{
// last bot
z: 3,
type: "pictorialBar",
symbolPosition: "start",
data: chartData.value.map((item) => item.last),
symbol: "diamond",
symbolOffset: ["45%", "50%"],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
borderWidth: 0,
color: "#FFF26D",
},
},
],
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" },
formatter: function (params) {
let tooltipText = `<div>${params[0].axisValueLabel}</div>`;
const filteredParams = params.filter((item) => item.seriesType === "bar");
filteredParams.forEach((item) => {
tooltipText += `<div>${item.marker} ${
item.value ? item.value : "-"
}</div>`;
});
return tooltipText;
},
},
});
</script>
<template>
<div class="flex flex-wrap">
<div class="w-full chart-data relative px-8 py-3">
<div class="flex flex-wrap items-center justify-between">
<h2 class="font-light">環比能耗</h2>
</div>
<div class="h-[180px]">
<BarChart
id="dashboard_chart_compare"
class="h-full"
:option="barChartOptions"
/>
</div>
<!-- 表格數據展示 -->
<div class="flex justify-between">
<div
v-for="(data, index) in chartData"
:key="index"
class="w-1/4 text-center mx-1"
>
<div
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
{{ labels[index * 2] }}
</div>
<div
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
{{ data.this ?? "-" }}
</div>
<div
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
{{ labels[index * 2 + 1] }}
</div>
<div
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
{{ data.last ?? "-" }}
</div>
<div
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
<span
:class="{
'text-red-500': data.change > 0,
'text-green-500': data.change < 0,
}"
>
{{
data.change
? (data.change > 0 ? "+" : "") + data.change + "%"
: "-"
}}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.chart-data:before {
@apply absolute right-0 left-2 top-0 h-10 w-10 bg-no-repeat z-10;
content: "";
background: url(@ASSET/img/chart-data-background01.svg) center center;
}
.chart-data::after {
@apply absolute right-0 bottom-0 h-10 w-10 bg-no-repeat z-10;
content: "";
background: url(@ASSET/img/chart-data-background02.svg) center center;
}
</style>

View File

@ -0,0 +1,103 @@
<script setup>
import { ref } from "vue";
// -
const mockEnergyData = {
monthly: [
{ id: "62090003", name: "電機-100HP 排氣扇", energy: 450, unit: "kWh" },
{ id: "62090113", name: "電機-75P 泵浦", energy: 80, unit: "kWh" },
{ id: "62090043", name: "電機-50P 電II", energy: 40, unit: "kWh" },
{ id: "62090038", name: "電機-50P 電II", energy: 35, unit: "kWh" },
{ id: "62090106", name: "電機-50P 泵浦", energy: 25, unit: "kWh" },
{ id: "62090086", name: "電機-30P 排氣", energy: 20, unit: "kWh" },
{ id: "19010225", name: "VOC 廢氣處理設備", energy: 15, unit: "kWh" },
{ id: "14010138", name: "固體燃料燒結機", energy: 10, unit: "kWh" },
],
daily: [
{ id: "62090003", name: "電機-100HP 排氣扇", energy: 132533, unit: "kWh" },
{ id: "62090113", name: "電機-75P 泵浦", energy: 24445, unit: "kWh" },
{ id: "62090043", name: "電機-50P 電II", energy: 11775, unit: "kWh" },
{ id: "62090038", name: "電機-50P 電II", energy: 10600, unit: "kWh" },
{ id: "62090106", name: "電機-50P 泵浦", energy: 6660, unit: "kWh" },
{ id: "62090086", name: "電機-30P 排氣", energy: 6579, unit: "kWh" },
{ id: "19010225", name: "VOC 廢氣處理設備", energy: 6367, unit: "kWh" },
{ id: "14010138", name: "固體燃料燒結機", energy: 2392, unit: "kWh" },
],
};
//
const currentEnergyType = ref("monthly");
//
const toggleEnergyType = () => {
currentEnergyType.value =
currentEnergyType.value === "monthly" ? "daily" : "monthly";
};
//
const getCurrentEnergyData = () => {
return currentEnergyType.value === "monthly"
? mockEnergyData.monthly
: mockEnergyData.daily;
};
</script>
<template>
<div class="state-box-col relative ps-2">
<div class="state-box">
<!-- 標題和切換按鈕 -->
<div class="flex justify-between items-center mb-4">
<h2 class="font-light w-1/2 relative">當月能耗排行</h2>
<button @click="toggleEnergyType" class="btn btn-info btn-xs">
{{ currentEnergyType === "monthly" ? "本日能耗" : "當月能耗" }}
</button>
</div>
<!-- 能耗排名列表 -->
<table class="table table-sm text-center">
<tbody>
<tr
v-for="(item, index) in getCurrentEnergyData()"
:key="item.id"
:class="[
{ 'text-red-300': index + 1 === 1 },
{ 'text-orange-300': index + 1 === 2 },
{ 'text-yellow-300': index + 1 === 3 },
{ 'text-teal-300': index + 1 > 3 },
]"
>
<td class="flex items-center">
<font-awesome-icon :icon="['fas', 'crown']" class="me-1" />{{
index + 1
}}
</td>
<td>{{ item.name }}</td>
<td>{{ item.energy }} {{ item.unit }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<style lang="scss" scoped>
.state-box {
@apply border-2 border-light-info rounded-sm p-2 text-white relative;
}
.state-box-col:before {
@apply absolute left-0 right-0 -top-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/state-box-top.png);
}
.state-box-col:after {
@apply absolute left-0 right-0 -bottom-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/state-box-bottom.png);
}
tr td {
@apply text-[13px] text-start;
}
</style>

View File

@ -0,0 +1,181 @@
<script setup>
import { ref, onMounted } from "vue";
import * as echarts from "echarts";
import BarChart from "@/components/chart/BarChart.vue";
import { useI18n } from "vue-i18n";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
const { t } = useI18n();
const formState = ref({
floorId: 0,
deptId: 0,
});
// API store
const energyData = ref([
{ date: "11-18", energy: 500.1 },
{ date: "11-20", energy: 400 },
{ date: "11-22", energy: 450 },
{ date: "11-24", energy: 420 },
{ date: "11-26", energy: 430 },
{ date: "11-28", energy: 460 },
{ date: "11-30", energy: 480 },
{ date: "12-02", energy: 410 },
{ date: "12-04", energy: 440 },
{ date: "12-06", energy: 450 },
{ date: "12-08", energy: 470 },
{ date: "12-10", energy: 399.26 },
{ date: "12-12", energy: 380 },
{ date: "12-14", energy: 320 },
{ date: "12-16", energy: 230.68 },
]);
// option
const generateCylinderChartOption = (data) => {
const barWidth = 25;
return {
xAxis: {
type: "category",
data: data.map((item) => item.date),
axisLine: {
lineStyle: {
color: "#fff",
},
},
},
yAxis: {
type: "value",
name: "kWh",
axisLine: {
lineStyle: {
color: "#fff",
},
},
splitLine: {
show: false,
},
},
series: [
{
data: data.map((item) => item.energy),
type: "bar",
barWidth: barWidth,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#1F7B47" },
{ offset: 1, color: "#247E95" },
]),
shadowBlur: 5,
shadowColor: "rgba(0, 0, 0, 0.5)",
shadowOffsetY: 5,
},
},
{
z: 12,
type: "pictorialBar",
symbolPosition: "end",
data: data.map((item) => item.energy),
symbol: "diamond",
symbolOffset: [0, -5],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
color: "#62E39A",
},
},
{
z: 12,
type: "pictorialBar",
data: data.map((item) => item.energy),
symbol: "diamond",
symbolSize: [barWidth, barWidth * 0.5],
symbolOffset: [0, 6],
itemStyle: {
color: "#247E95",
},
},
],
grid: {
left: "0%",
right: "0%",
bottom: "3%",
top: "15%",
containLabel: true,
},
tooltip: {
trigger: "axis",
formatter: function (params) {
const item = params[0];
return `<p>${item.name}</p> <p>${item.marker}能耗 : ${item.value}</p>`;
},
},
};
};
const weekComparisonOption = generateCylinderChartOption(energyData.value);
onMounted(() => {
if (
storeBuild.deptList.length > 0 &&
storeBuild.floorList.length > 0
) {
formState.value = {
...formState.value,
floorId: storeBuild.floorList[0].key,
deptId: storeBuild.deptList[0].key,
};
}
});
</script>
<template>
<div class="w-full chart-data relative px-8 py-1">
<div class="flex flex-wrap items-center justify-between">
<h2 class="font-light">近30天能耗趨勢</h2>
<div class="flex items-center w-52 gap-4">
<Select
:value="formState"
class="my-2"
selectClass="border-info focus-within:border-info btn-xs text-xs"
name="floorId"
Attribute="title"
:options="storeBuild.floorList"
:isTopLabelExist="false"
:isBottomLabelExist="false"
>
</Select>
<Select
:value="formState"
class="my-2"
selectClass="border-info focus-within:border-info btn-xs text-xs"
name="deptId"
Attribute="title"
:options="storeBuild.deptList"
:isTopLabelExist="false"
:isBottomLabelExist="false"
>
</Select>
</div>
</div>
<div class="h-[300px]">
<BarChart
id="dashboard_chart_week_comparison"
class="h-full"
:option="weekComparisonOption"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.chart-data:before {
@apply absolute right-0 left-2 top-0 h-10 w-10 bg-no-repeat z-10;
content: "";
background: url(@ASSET/img/chart-data-background01.svg) center center;
}
.chart-data::after {
@apply absolute right-0 bottom-0 h-10 w-10 bg-no-repeat z-10;
content: "";
background: url(@ASSET/img/chart-data-background02.svg) center center;
}
</style>

View File

@ -41,7 +41,7 @@ const mockData = [
<style lang="scss" scoped>
.item-data {
@apply relative mb-4 min-h-[135px] h-full flex flex-col justify-center;
@apply relative mb-4 min-h-[100px] h-full flex flex-col justify-center;
}
.item-data-box:after {

View File

@ -2,6 +2,8 @@
import { ref } from "vue";
import { useRouter } from "vue-router";
import { twMerge } from "tailwind-merge";
import useBuildingStore from "@/stores/useBuildingStore";
const store = useBuildingStore();
const router = useRouter();
//
const mockData = ref([
@ -129,16 +131,18 @@ const navigateToSubSystem = (mainSystemId, subSystemId) => {
},
});
};
console.log("subSys",store.subSys)
</script>
<template>
<div class="flex flex-wrap -mx-1">
<div class="flex flex-wrap -mx-1 h-[21rem] overflow-y-scroll">
<div
v-for="(item, index) in mockData"
v-for="(item, index) in store.subSys"
:key="index"
:class="
twMerge(
'w-full sm:w-1/2 lg:w-1/4 relative my-2 ',
'w-full sm:w-1/2 relative my-2 ',
item.sub_system_tag
? 'saturate-200 cursor-pointer text-base text-info'
: 'grayscale opacity-70 cursor-not-allowed text-sm'
@ -154,14 +158,15 @@ const navigateToSubSystem = (mainSystemId, subSystemId) => {
<div class="equipment-item">
<div class="w-16">
<FontAwesomeIcon
<!-- <FontAwesomeIcon
class="text-2xl mt-1 m-auto"
:icon="['fas', item.icon]"
></FontAwesomeIcon>
:icon="['fas', 'tv']"
></FontAwesomeIcon> -->
<img class="w-7 m-auto" src="https://cgems.cvilux-group.com:8088/upload/device_icon/3454f5e0-3afa-4ace-ae54-a68bf7183e7d.png">
</div>
<div class="icon-text">
<div class="">
{{ item.title }}
{{ item.full_name }}
</div>
</div>
</div>

View File

@ -1,5 +1,6 @@
<script setup>
import { ref, computed } from "vue";
import DashboardSysProgressModal from "./DashboardSysProgressModal.vue";
const equipmentData = ref({
title: "System Status",
@ -20,10 +21,46 @@ const orderData = ref({
{ label: "Completed", value: 1 },
],
});
const openModal = () => {
system_status_modal.showModal();
};
</script>
<template>
<DashboardSysProgressModal
/>
<div class="flex flex-wrap">
<div class="w-full sm:w-2/5 state-box-col relative ps-2">
<div class="state-box">
<div class="title">
<img class="state-title01" src="@ASSET/img/state-title01.svg" />
<span>{{ orderData.title }}</span>
<img class="state-title02" src="@ASSET/img/state-title02.svg" />
</div>
<table class="table table-sm text-center">
<thead>
<tr class="border-cyan-400 text-cyan-100">
<th></th>
<th>value</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in orderData.items"
:key="index"
class="border-cyan-400 cursor-pointer hover:text-info"
@click.stop.prevent="() => openModal()"
>
<th class="px-0 text-start">
<span>{{ item.label }}</span>
</th>
<td>{{ item.value }}</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="w-full sm:w-3/5 state-box-col relative ps-2">
<div class="state-box">
<div class="title">
@ -44,7 +81,8 @@ const orderData = ref({
<tr
v-for="(item, index) in equipmentData.items"
:key="index"
class="border-cyan-400"
class="border-cyan-400 cursor-pointer hover:text-info"
@click.stop.prevent="() => openModal()"
>
<th class="px-0 text-start">{{ item.label }}</th>
<td>{{ item.online }}</td>
@ -55,35 +93,6 @@ const orderData = ref({
</table>
</div>
</div>
<div class="w-full sm:w-2/5 state-box-col relative ps-2">
<div class="state-box">
<div class="title">
<img class="state-title01" src="@ASSET/img/state-title01.svg" />
<span>{{ orderData.title }}</span>
<img class="state-title02" src="@ASSET/img/state-title02.svg" />
</div>
<table class="table table-sm text-center">
<thead>
<tr class="border-cyan-400 text-cyan-100">
<th></th>
<th>value</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in orderData.items"
:key="index"
class="border-cyan-400"
>
<th class="px-0 text-start">
<span>{{ item.label }}</span>
</th>
<td>{{ item.value }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
@ -101,7 +110,7 @@ const orderData = ref({
}
.state-box {
@apply h-80 border-2 border-light-info rounded-sm py-2 px-6 text-white relative;
@apply h-[22rem] border-2 border-light-info rounded-sm py-2 px-6 text-white relative;
}
.state-box:after {

View File

@ -0,0 +1,83 @@
<script setup>
import { ref, onMounted, defineProps, inject, watch } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast } = inject("app_toast");
const onCancel = () => {
system_status_modal.close();
};
// -
const mockEquipmentData = ref([
{ name: "12030182 (放空機-30噸)", lastOnline: "2024-12-17 17:15:00" },
{ name: "12080043 (空壓機-50P 日立)", lastOnline: "2024-12-17 17:15:00" },
{ name: "12030204 (注塑機-40噸)", lastOnline: "2024-12-17 17:15:00" },
{ name: "12040084 (冷卻水)", lastOnline: "2024-12-17 17:15:00" },
{ name: "11060241 (蒸騰式冷氣機)", lastOnline: "2024-12-17 17:15:00" },
{ name: "12060010 (串壓機-啟業)", lastOnline: "2024-12-17 17:15:00" },
{ name: "12020005 (串壓機-工業30噸)", lastOnline: "2024-12-17 17:15:00" },
{ name: "12030179 (注塑機-30噸)", lastOnline: "2024-12-17 17:15:00" },
{ name: "12040145 (串壓機-三銘)", lastOnline: "2024-12-17 17:15:00" },
{
name: "12030163 (注氨全自動加氨型-00:00-01:00)",
lastOnline: "2024-12-17 17:15:00",
},
]);
</script>
<template>
<Modal id="system_status_modal" :onCancel="onCancel" :width="600">
<template #modalTitle>
<div class="flex items-center justify-between">
<h2>在線設備</h2>
<Button
type="link"
class="btn-link btn-text-without-border px-2"
@click="onCancel"
>
<font-awesome-icon
:icon="['fas', 'times']"
class="text-[#a5abb1]"
/>
</Button>
</div>
</template>
<template #modalContent>
<div class="overflow-x-auto">
<table class="table text-base mt-5">
<thead>
<tr>
<th
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
>
設備名稱
</th>
<th
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
>
最近在線時間
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(equipment, index) in mockEquipmentData"
:key="index"
class="hover:bg-gray-700"
>
<td class="border text-white text-center">
{{ equipment.name }}
</td>
<td class="border text-white text-center">
{{ equipment.lastOnline }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
</Modal>
</template>
<style lang="scss" scoped></style>

View File

@ -1,4 +1,5 @@
<script setup>
import { ref, onMounted, provide } from "vue";
import ImmediateDemandChart from "./ImmediateDemandChart.vue";
import ElecConsumption from "./ElecConsumption.vue";
import UsageInformation from "./UsageInformation.vue";
@ -6,8 +7,25 @@ import MonthlyElecBillChart from "./MonthlyElecBillChart.vue";
import CarbonEmissionChart from "./CarbonEmissionChart.vue";
import BillingDegreeChart from "./BillingDegreeChart.vue";
import IntervalBillChart from "./IntervalBillChart.vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { getTaipower } from "@/apis/energy";
import { ref, onMounted, provide } from "vue";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
//
const {
items: sysFloorItems,
changeActiveBtn: changeFloorActiveBtn,
setItems: setFloorItems,
selectedBtn: selectedFloorItems,
} = useActiveBtn("multiple");
//
const {
items: sysDeptItems,
changeActiveBtn: changeDeptActiveBtn,
setItems: setDeptItems,
selectedBtn: selectedDeptItems,
} = useActiveBtn("multiple");
const taipower_data = ref([]);
const getData = async () => {
@ -19,6 +37,8 @@ const getData = async () => {
onMounted(() => {
getData();
setDeptItems(storeBuild.deptList);
setFloorItems(storeBuild.floorList);
});
provide("energy_data", { taipower_data });
@ -26,6 +46,36 @@ provide("energy_data", { taipower_data });
<template>
<div class="flex flex-wrap items-center mb-4">
<div class="w-full border border-info px-4 py-2 rounded my-3">
<div class="flex items-center gap-3">
<span class="text-md font-extrabold">{{ $t("energy.floor") }} :</span>
<ButtonGroup
:items="sysFloorItems"
:withLine="true"
className="btn-xs rounded-md"
:onclick="
(e, item) => {
changeFloorActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-3">
<span class="text-md font-extrabold"
>{{ $t("assetManagement.department") }} :</span
>
<ButtonGroup
:items="sysDeptItems"
:withLine="true"
className="btn-xs rounded-md"
:onclick="
(e, item) => {
changeDeptActiveBtn(item);
}
"
/>
</div>
</div>
<div class="w-full xl:w-5/12 lg:w-1/2">
<ElecConsumption />
</div>

View File

@ -14,9 +14,11 @@ import EnergyActionButton from "./EnergyActionButton.vue";
import EnergySearchTime from "./EnergySearchTime.vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { getEnergySearch } from "@/apis/energy";
import { getDepartmentList, getElecTypeList } from "@/apis/asset";
import { getElecTypeList } from "@/apis/asset";
import useSearchParam from "@/hooks/useSearchParam";
import dayjs from "dayjs";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
const { deptData, elecType, subSystem } = inject("energy_table_data");
const { searchParams, changeParams } = useSearchParam();
const route = useRoute();
@ -57,17 +59,6 @@ const {
selectedBtn: selectedPoints,
} = useActiveBtn("multiple");
const getDepartment = async () => {
const res = await getDepartmentList();
const dept = res.data.map((d, index) => ({
...d,
title: d.name,
key: d.id,
active: false,
}));
setDeptItems(dept);
};
const getElecType = async () => {
const res = await getElecTypeList();
const elecType = res.data.map((d, index) => ({
@ -162,7 +153,7 @@ watch(
);
onMounted(() => {
getDepartment();
setDeptItems(storeBuild.deptList);
getElecType();
});
</script>

View File

@ -3,13 +3,12 @@ import { ref, onMounted, watch, inject } from "vue";
import { useRoute } from "vue-router";
import useActiveBtn from "@/hooks/useActiveBtn";
import EnergyReportTimeRange from "./EnergyReportTimeRange.vue";
import {
getDepartmentList,
getAssetFloorList,
getElecTypeList,
} from "@/apis/asset";
import { getElecTypeList } from "@/apis/asset";
import { getReport, getExcel } from "@/apis/energy";
import { useI18n } from "vue-i18n";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
const { t } = useI18n();
const route = useRoute();
const { tableData, loading } = inject("energy_table_data");
@ -43,39 +42,16 @@ const {
selectedBtn: selectedFloorItems,
} = useActiveBtn("multiple");
const getDepartment = async () => {
const res = await getDepartmentList();
const dept = res.data.map((d, index) => ({
...d,
title: d.name,
key: d.id,
active: index === 0,
}));
setDeptItems(dept);
};
const getElecType = async () => {
const res = await getElecTypeList();
const elecType = res.data.map((d, index) => ({
...d,
title: d.name,
key: d.id,
active: index === 0,
key: d.id
}));
setElecItems(elecType);
};
const getFloor = async () => {
const res = await getAssetFloorList();
const Floor = res.data[0]?.floors.map((d, index) => ({
...d,
title: d.full_name,
key: d.floor_guid,
active: index === 0,
}));
setFloorItems(Floor);
};
const onSearch = async () => {
loading.value = true;
const res = await getReport(formState.value);
@ -124,9 +100,9 @@ watch(
);
onMounted(() => {
getDepartment();
setDeptItems(storeBuild.deptList);
setFloorItems(storeBuild.floorList);
getElecType();
getFloor();
});
</script>

View File

@ -26,6 +26,13 @@ import dayjs from "dayjs";
const { searchParams, changeParams } = useSearchParam();
const store = useBuildingStore();
//
const {
items: sysDeptItems,
changeActiveBtn: changeDeptActiveBtn,
setItems: setDeptItems,
selectedBtn: selectedDeptItems,
} = useActiveBtn("multiple");
//
const {
items: sysMainTagItems,
@ -40,6 +47,13 @@ const {
setItems: setSysItems,
selectedBtn: selectedSysItems,
} = useActiveBtn();
//
const {
items: points,
changeActiveBtn: changeActivePoint,
setItems: setPoints,
selectedBtn: selectedPoints,
} = useActiveBtn("multiple");
watch(
() => store.mainSys,
@ -60,7 +74,9 @@ watch(
() => selectedMainSysItems,
(newVal, oldVal) => {
setSysItems(
store.subSys.filter((s) => s.main_system_tag === newVal.value?.key).map(({ full_name, sub_system_tag }, index) => ({
store.subSys
.filter((s) => s.main_system_tag === newVal.value?.key)
.map(({ full_name, sub_system_tag }, index) => ({
title: full_name,
key: sub_system_tag,
active: index === 0,
@ -93,14 +109,6 @@ watch(
}
);
//
const {
items: points,
changeActiveBtn: changeActivePoint,
setItems: setPoints,
selectedBtn: selectedPoints,
} = useActiveBtn("multiple");
const getPoint = async (deviceList) => {
const res = await getHistoryPoints(deviceList);
setPoints(
@ -126,6 +134,19 @@ watch(
}
);
watch(
selectedDeptItems,
(newVal, oldVal) => {
changeParams({
...searchParams.value,
Dept: newVal.map((d) => d.id),
});
},
{
immediate: true,
}
);
const form = ref(null);
watch(searchParams, (newVal, oldValue) => {
@ -149,6 +170,13 @@ onMounted(() => {
active: index === 0,
}))
);
setDeptItems(
store.deptList.map((d, index) => ({
...d,
title: d.name,
key: d.id,
}))
);
});
onBeforeMount(() => {
@ -159,6 +187,20 @@ onBeforeMount(() => {
<template>
<div class="flex flex-col custom-border p-4 mb-4">
<!-- <HistoryFavoriteOption class="mb-4" />-->
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold ps-2 whitespace-nowrap">
{{ $t("assetManagement.department") }} :
</h2>
<ButtonGroup
:items="sysDeptItems"
:withLine="true"
:onclick="
(e, item) => {
changeDeptActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold ps-2 whitespace-nowrap">
{{ $t("history.system_category") }} :

View File

@ -13,8 +13,9 @@ const deviceData = ref([]);
const searchTerm = ref(""); //
const activeSearchTerm = ref("");
const getDeviceData = async (sub_system_tag, renew) => {
const res = await getHistorySideBar({sub_system_tag: sub_system_tag});
const getDeviceData = async (sub_system_tag,department_id) => {
const deptArray = department_id? department_id.map(Number):null;
const res = await getHistorySideBar({sub_system_tag: sub_system_tag,department_id:deptArray});
deviceData.value = res.data.map((building) => ({
building_tag: building.building_tag,
building_name: building.building_name,
@ -30,8 +31,7 @@ const getDeviceData = async (sub_system_tag, renew) => {
selectedBuilding.value = res.data.map((d) => d.building_tag);
changeSelected(
[res.data[0]?.floor_list[0]?.device_list[0]?.device_number],
renew
[res.data[0]?.floor_list[0]?.device_list[0]?.device_number]
);
};
@ -57,7 +57,7 @@ watch(
newVal.sub_system_tag &&
newVal.sub_system_tag != oldVal?.sub_system_tag
) {
getDeviceData(newVal.sub_system_tag, true);
getDeviceData(newVal.sub_system_tag,searchParams.value.Dept);
}
},
{
@ -66,6 +66,21 @@ watch(
}
);
watch(
searchParams,
(newVal, oldVal) => {
if (
newVal.Dept &&
newVal.Dept?.length != oldVal?.Dept?.length
) {
getDeviceData(searchParams.value.sub_system_tag,newVal.Dept);
}
},
{
immediate: true,
}
);
const selectedDeviceNumber = computed(() => {
return typeof searchParams.value.Device_list === "string"
? [searchParams.value.Device_list]

View File

@ -8,7 +8,8 @@ import Vendor from "./components/Vendor.vue";
import Floors from "./components/Floors.vue";
import Building from "./components/Building.vue";
import ElecPriceManagement from "./components/ElecPriceManagement.vue";
import MITTList from "./components/MITTList.vue";
import MQTTList from "./components/MQTTList.vue";
// import PointList from "./components/PointList.vue";
const route = useRoute();
const currentComponent = ref(null);
@ -29,7 +30,7 @@ const updateComponent = () => {
} else if (sub_system_id === "ElecPricing") {
currentComponent.value = ElecPriceManagement;
} else if (sub_system_id === "MQTT_Result") {
currentComponent.value = MITTList;
currentComponent.value = MQTTList;
}
};

View File

@ -1,9 +1,12 @@
<script setup>
import Table from "@/components/customUI/Table.vue";
import DeptModal from "./DeptModal.vue";
import { getDepartmentList, deleteDepartmentItem } from "@/apis/asset";
import { deleteDepartmentItem } from "@/apis/asset";
import { onMounted, ref, inject, computed } from "vue";
import { useI18n } from "vue-i18n";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
@ -23,20 +26,8 @@ const columns = computed(() => [
},
]);
const dataSource = ref([]);
const loading = ref(false);
const getDataSource = async () => {
loading.value = true;
const res = await getDepartmentList();
dataSource.value = res.data.map((d) => ({ ...d, key: d.id }));
loading.value = false;
};
onMounted(() => {
getDataSource();
});
const formState = ref({
id: 0,
name: "",
@ -59,13 +50,16 @@ const remove = async (id) => {
await cancelToastOpen();
const res = await deleteDepartmentItem(id);
if (res.isSuccess) {
getDataSource();
storeBuild.fetchDepartmentList();
openToast("success", t("msg.delete_success"));
} else {
openToast("error", res.msg);
}
});
};
onMounted(() => {
storeBuild.fetchDepartmentList();
});
</script>
<template>
@ -73,11 +67,10 @@ const remove = async (id) => {
<h3 class="text-xl mr-5">{{ $t("assetManagement.department") }}</h3>
<DeptModal
:formState="formState"
:getData="getDataSource"
:openModal="openModal"
/>
</div>
<Table :columns="columns" :dataSource="dataSource" :loading="loading">
<Table :columns="columns" :dataSource="storeBuild.deptList" :loading="loading">
<template #bodyCell="{ record, column, index }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
<template v-else-if="column.key === 'operation'">

View File

@ -5,12 +5,14 @@ import "yup-phone-lite";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import { postDepartmentList } from "@/apis/asset";
import { useI18n } from "vue-i18n";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
const { t } = useI18n();
const { openToast } = inject("app_toast");
const props = defineProps({
formState: Object,
getData: Function,
openModal: Function
});
@ -30,7 +32,7 @@ const onOk = async () => {
const value = await handleSubmit(deptScheme, props.formState);
const res = await postDepartmentList(value);
if (res.isSuccess) {
props.getData();
storeBuild.fetchDepartmentList();
onCancel();
} else {
openToast("error", res.msg, "#dept_modal");

View File

@ -0,0 +1,180 @@
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import { getAssetMainList, getAssetSubList, getIOTSchema } from "@/apis/asset";
import useActiveBtn from "@/hooks/useActiveBtn";
import MQTTListAddModal from "./MQTTListAddModal.vue";
import PointListAddModal from "./PointListAddModal.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const formState = ref({});
const MQTTDataSource = ref([]);
const MQTTloading = ref(false);
const PointsDataSource = ref([]);
const Pointsloading = ref(false);
//
const {
items: mainSysItems,
changeActiveBtn: changeMainSysActiveBtn,
setItems: setMainSysItems,
selectedBtn: selectedMainSysItems,
} = useActiveBtn();
//
const {
items: subSysItems,
changeActiveBtn: changeSubSysActiveBtn,
setItems: setSubSysItems,
selectedBtn: selectedSubSysItems,
} = useActiveBtn();
const getMainSystems = async () => {
const res = await getAssetMainList();
const cate = res.data.map((d, index) => ({
...d,
title: d.system_key,
key: d.id,
active: index === 0,
}));
setMainSysItems(cate);
};
const getSubSystems = async (id) => {
const res = await getAssetSubList(id);
const sub = res.data.map((d, index) => ({
...d,
title: d.system_key,
key: d.id,
active: index === 0,
}));
setSubSysItems(sub);
};
const MQTTColumns = computed(() => [
{
title: "schema",
key: "schema_name",
},
{
title: "point",
key: "pointOrg",
filter: true,
},
{
title: "Description",
key: "description",
},
]);
const PointsColumns = computed(() => [
{
title: "IoT 點位名稱",
key: "IoT_point_name",
},
{
title: "小數點個數",
key: "decimal",
},
{
title: "bool 值",
key: "is_bool",
},
{
title: "點位隱藏",
key: "is_open",
},
]);
const getDataSource = async (id) => {
MQTTloading.value = true;
const res = await getIOTSchema(id);
MQTTDataSource.value = res.data.flatMap((d) =>
d.details.map((detail) => ({
key: `${d.id}-${detail.pointOrg}`,
schema_name: d.name,
pointOrg: detail.pointOrg,
description: detail.describe,
}))
);
MQTTloading.value = false;
};
watch(
selectedMainSysItems,
(newValue) => {
if (newValue && newValue.key) {
getSubSystems(parseInt(newValue.key));
}
},
{
deep: true,
}
);
watch(
selectedSubSysItems,
(newValue) => {
if (newValue && newValue.key) {
getDataSource(parseInt(newValue.key));
}
},
{
deep: true,
}
);
onMounted(() => {
getMainSystems();
});
</script>
<template>
<div class="">
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold whitespace-nowrap">
{{ $t("history.system_category") }} :
</h2>
<ButtonGroup
:items="mainSysItems"
:withLine="true"
:onclick="
(e, item) => {
changeMainSysActiveBtn(item);
}
"
/>
</div>
<div class="flex items-center gap-4 mb-4">
<h2 class="text-lg font-bold whitespace-nowrap">
{{ $t("history.device_category") }} :
</h2>
<ButtonGroup
:items="subSysItems"
:withLine="true"
:onclick="
(e, item) => {
changeSubSysActiveBtn(item);
}
"
/>
</div>
<div class="flex justify-start items-center mt-5 mb-2">
<h3 class="text-xl mr-5">MQTT_Parse : </h3>
<MQTTListAddModal />
</div>
<Table
:columns="MQTTColumns"
:dataSource="MQTTDataSource"
:loading="MQTTloading"
></Table>
<div class="flex justify-start items-center mt-5 mb-2">
<h3 class="text-xl mr-5">Points : </h3>
<PointListAddModal />
</div>
<Table
:columns="PointsColumns"
:dataSource="PointsDataSource"
:loading="Pointsloading"
></Table>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -2,14 +2,10 @@
import { ref, onMounted, watch, defineProps } from "vue";
import { getAssetMainList, getAssetSubList, getIOTSchema } from "@/apis/asset";
import useActiveBtn from "@/hooks/useActiveBtn";
import MITTCheckboxTree from "./MITTCheckboxTree.vue";
import MQTTCheckboxTree from "./MQTTCheckboxTree.vue";
import generateSchema from "json-schema-generator";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps({
openModal: Function,
onCancel: Function,
});
// jsonInput
const jsonInput = ref("");
const treeData = ref(null);
@ -105,6 +101,14 @@ watch(
},
{ immediate: true }
);
const openModal = () => {
MQTT_Parse_item.showModal();
};
const onCancel = () => {
MQTT_Parse_item.close();
};
</script>
<template>
@ -114,23 +118,20 @@ watch(
<Modal
id="MQTT_Parse_item"
title="MQTT_Parse"
:open="open"
:onCancel="onCancel"
width="1600"
:width="1600"
>
<template #modalContent>
<div class="flex w-full gap-4 mt-5">
<div class="w-1/2 relative">
<div class="w-full">
<textarea
v-model="jsonInput"
placeholder="請貼上 JSON 格式數據"
class="w-full h-[500px] p-4 border rounded-lg font-mono text-white"
></textarea>
<button @click="clearAll" class="bg-error btn absolute top-5 right-5">
<font-awesome-icon :icon="['fas', 'trash-alt']" />
</button>
</div>
<button @click="parseJson" class="btn-success btn my-auto">
<div class="my-auto">
<button @click="parseJson" class="btn-success btn w-24 mb-4">
轉換
<font-awesome-icon
:icon="['fas', 'chevron-right']"
@ -138,8 +139,12 @@ watch(
class="block"
/>
</button>
<div class="w-1/2 p-4 rounded-lg border overflow-y-scroll h-[500px]">
<MITTCheckboxTree
<button @click="clearAll" class="bg-error btn w-24">
刪除<font-awesome-icon :icon="['fas', 'trash-alt']" />
</button>
</div>
<div class="w-full p-4 rounded-lg border overflow-y-scroll h-[500px]">
<MQTTCheckboxTree
v-if="treeData"
:data="treeData"
v-model:checked-nodes="checkedNodes"
@ -147,30 +152,28 @@ watch(
<div v-else class="text-white">請在左側輸入 JSON 並點擊轉換按鈕</div>
</div>
</div>
<form ref="form" class="">
<form ref="form" class="" v-if="checkedNodes.length">
<div class="flex items-center mt-5">
<h2 class="text-lg font-bold whitespace-nowrap me-2">
結構名稱 :
</h2>
<h2 class="text-lg font-bold whitespace-nowrap me-2">結構名稱 :</h2>
<Input v-model="schemaName" />
</div>
<table class="table" v-if="checkedNodes.length">
<table class="table">
<thead>
<tr>
<th>IoT 點位名稱</th>
<!-- <th>IoT 點位名稱</th> -->
<th>IoT 點位結構</th>
<th>系統點位名稱</th>
<th>bool </th>
<!-- <th>bool </th>
<th>小數點個數</th>
<th>點位開關</th>
<th>點位隱藏</th> -->
</tr>
</thead>
<tbody>
<tr v-for="(node, index) in checkedNodes" :key="node">
<td>
<!-- <td>
<Input :value="formStates[index]" name="IoT_point_name" />
</td>
<td>
</td> -->
<td class="w-1/2">
<Input
:value="formStates[index]"
name="IoT_point_schema"
@ -184,7 +187,7 @@ watch(
<option value="option3">選項 3</option>
</select>
</td>
<td>
<!-- <td>
<div class="flex w-36">
<Select
:value="formStates[index]"
@ -215,7 +218,7 @@ watch(
>
</Select>
</div>
</td>
</td> -->
</tr>
</tbody>
</table>

View File

@ -2,7 +2,7 @@
import { ref, onMounted, watch, computed } from "vue";
import { getAssetMainList, getAssetSubList, getIOTSchema } from "@/apis/asset";
import useActiveBtn from "@/hooks/useActiveBtn";
import MITTlISTAddModal from "./MITTlISTAddModal.vue";
import PointListAddModal from "./PointListAddModal.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const formState = ref({});
@ -46,14 +46,9 @@ const getSubSystems = async (id) => {
};
const columns = computed(() => [
{
title: "schema",
key: "schema_name",
},
{
title: "point",
key: "pointOrg",
filter: true,
},
{
title: "Description",
@ -76,12 +71,11 @@ const getDataSource = async (id) => {
};
const openModal = () => {
MQTT_Parse_item.showModal();
point_list_item.showModal();
};
const onCancel = () => {
MQTT_Parse_item.close();
getDataSource(parseInt(selectedSubSysItems.key))
point_list_item.close();
};
watch(
@ -144,8 +138,8 @@ onMounted(() => {
/>
</div>
<div class="flex justify-start items-center mt-5 mb-2">
<h3 class="text-xl mr-5">MQTT_Parse : </h3>
<MITTlISTAddModal
<h3 class="text-xl mr-5">Points : </h3>
<PointListAddModal
:openModal="openModal"
:onCancel="onCancel"
/>

View File

@ -0,0 +1,101 @@
<script setup>
import { ref, onMounted, watch, defineProps } from "vue";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import { useI18n } from "vue-i18n";
import * as yup from "yup";
const { t } = useI18n();
const form = ref(null);
const formState = ref({
id: 0,
IoT_point_name: "",
is_bool: 0,
decimal: 2,
is_open: 0,
});
let schema = ref(
yup.object({
IoT_point_name: yup.string().required(t("button.required")),
is_bool: yup.string().required(t("button.required")),
decimal: yup.string().required(t("button.required")),
is_open: yup.string().required(t("button.required")),
})
);
const { formErrorMsg, handleSubmit, handleErrorReset, updateScheme } =
useFormErrorMessage(schema.value);
const openModal = () => {
point_list_item.showModal();
};
const onCancel = () => {
point_list_item.close();
};
</script>
<template>
<button class="btn btn-sm btn-add mr-3" @click.stop.prevent="openModal">
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button>
<Modal id="point_list_item" title="Points" :onCancel="onCancel" :width="710">
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
<Input :value="formState" class="my-2" name="IoT_point_name">
<template #topLeft>IoT 點位名稱</template>
<template #bottomLeft
><span class="text-error text-base">
{{ formErrorMsg.IoT_point_name }}
</span></template
></Input
>
<InputNumber :value="formState" class="my-2" name="decimal">
<template #topLeft>小數點個數</template>
</InputNumber>
<RadioGroup
class="my-2"
name="is_bool"
:value="formState"
:items="[
{
key: 1,
value: 1,
title: $t('alert.yes'),
},
{
key: 0,
value: 0,
title: $t('alert.no'),
},
]"
:required="true"
>
<template #topLeft>bool </template>
</RadioGroup>
<RadioGroup
class="my-2"
name="is_open"
:value="formState"
:items="[
{
key: 1,
value: 1,
title: $t('alert.yes'),
},
{
key: 0,
value: 0,
title: $t('alert.no'),
},
]"
:required="true"
>
<template #topLeft>點位隱藏</template>
</RadioGroup>
</form>
</template>
</Modal>
</template>
<style lang="scss" scoped></style>

View File

@ -2,6 +2,7 @@
import { RouterView, useRoute } from 'vue-router';
import { computed, watch, provide, ref, onMounted, onBeforeUnmount } from "vue";
import SystemFloorBar from './components/SystemFloorBar.vue';
import SystemDeptBar from './components/SystemDeptBar.vue';
import useBuildingStore from "@/stores/useBuildingStore";
import ForgeForSystem from "@/components/forge/ForgeForSystem.vue";
import { getSystemDevices, getSystemRealTime } from "@/apis/system";
@ -238,7 +239,11 @@ onBeforeUnmount(() => {
<template>
<SystemInfoModal :data="selectedDevice" />
<div class="border border-info px-4 py-2">
<h5 class="text-md font-extrabold me-4">{{ buildingStore.selectedSystem?.full_name }}</h5>
<SystemFloorBar />
<SystemDeptBar />
</div>
<div class="grid grid-cols-2 gap-5 mt-8 mb-4">
<div class="col-span-1 h-[calc(100vh-170px)] flex flex-col justify-start">
<div>

View File

@ -0,0 +1,46 @@
<script setup>
import { useRoute, useRouter } from "vue-router";
import { getDepartmentList } from "@/apis/asset";
import { onMounted, ref, watch, inject } from "vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
//
const {
items: sysDeptItems,
changeActiveBtn: changeDeptActiveBtn,
setItems: setDeptItems,
selectedBtn: selectedDeptItems,
} = useActiveBtn();
onMounted(() => {
const deptList = [
{
title: "All",
key: "main",
},
...storeBuild.deptList,
];
setDeptItems(deptList);
});
</script>
<template>
<div class="flex items-center gap-3">
<span class="text-md font-extrabold"
>{{ $t("assetManagement.department") }} :</span
>
<ButtonGroup
:items="sysDeptItems"
className="btn-xs rounded-md"
:onclick="
(e, item) => {
changeDeptActiveBtn(item);
}
"
/>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -65,8 +65,8 @@ watch(() => store.selectedBuilding, (newValue) => {
</script>
<template>
<div class="flex items-center border border-info px-4 py-2">
<h5 class="text-md font-extrabold me-4">{{ store.selectedSystem?.full_name }}</h5>
<div class="flex items-center gap-3">
<span class="text-md font-extrabold">{{ $t("energy.floor") }} :</span>
<ButtonGroup :items="items" :withLine="false" className="btn-xs rounded-md" :onclick="(e, item) => onClick(item)" />
</div>
</template>