首頁初切版
This commit is contained in:
parent
ac5c88a047
commit
8d23b695c6
@ -1,4 +1,4 @@
|
|||||||
VITE_API_BASEURL = "https://ibms-cvilux-demo-api.production.mjmtech.com.tw"
|
VITE_API_BASEURL = "https://ibms-cvilux-api.production.mjmtech.com.tw"
|
||||||
VITE_FILE_API_BASEURL = "https://cgems.cvilux-group.com:8088"
|
VITE_FILE_API_BASEURL = "https://cgems.cvilux-group.com:8088"
|
||||||
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
|
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
|
||||||
VITE_FORGE_BASEURL = "https://cgems.cvilux-group.com:8088/dist"
|
VITE_FORGE_BASEURL = "https://cgems.cvilux-group.com:8088/dist"
|
@ -9,6 +9,7 @@ import AlertManagement from "@/views/alert/AlertManagement.vue";
|
|||||||
import ProductSetting from "@/views/productSetting/ProductSetting.vue";
|
import ProductSetting from "@/views/productSetting/ProductSetting.vue";
|
||||||
import EnergyManagement from "@/views/energyManagement/EnergyManagement.vue";
|
import EnergyManagement from "@/views/energyManagement/EnergyManagement.vue";
|
||||||
import SettingManagement from "@/views/setting/SettingManagement.vue";
|
import SettingManagement from "@/views/setting/SettingManagement.vue";
|
||||||
|
import HeadquartersManagement from "@/views/headquarters/HeadquartersManagement.vue";
|
||||||
import Login from "@/views/login/Login.vue";
|
import Login from "@/views/login/Login.vue";
|
||||||
import useUserInfoStore from "@/stores/useUserInfoStore";
|
import useUserInfoStore from "@/stores/useUserInfoStore";
|
||||||
import useGetCookie from "@/hooks/useGetCookie";
|
import useGetCookie from "@/hooks/useGetCookie";
|
||||||
@ -90,6 +91,11 @@ const router = createRouter({
|
|||||||
name: "setting",
|
name: "setting",
|
||||||
component: SettingManagement,
|
component: SettingManagement,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/headquarters",
|
||||||
|
name: "headquarters",
|
||||||
|
component: HeadquartersManagement,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/mytestfile/mjm",
|
path: "/mytestfile/mjm",
|
||||||
name: "mytestfile",
|
name: "mytestfile",
|
||||||
@ -101,7 +107,7 @@ const router = createRouter({
|
|||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
console.log("route", to, location, document.cookie);
|
console.log("route", to, location, document.cookie);
|
||||||
// redirect to login page if not logged in and trying to access a restricted page
|
// redirect to login page if not logged in and trying to access a restricted page
|
||||||
const publicPages = ["/login", "/"];
|
const publicPages = ["/login", "/", "/headquarters"];
|
||||||
const authRequired = !publicPages.includes(to.path);
|
const authRequired = !publicPages.includes(to.path);
|
||||||
const auth = useUserInfoStore();
|
const auth = useUserInfoStore();
|
||||||
const token = useGetCookie("JWT-Authorization");
|
const token = useGetCookie("JWT-Authorization");
|
||||||
@ -110,7 +116,7 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
if ((authRequired && !token) || to.path === "/") {
|
if ((authRequired && !token) || to.path === "/") {
|
||||||
auth.user.token = "";
|
auth.user.token = "";
|
||||||
next({ path: "/login" });
|
next({ path: "/login" });
|
||||||
} else if (!authRequired) {
|
} else if (!authRequired && (to.path === "/login" || to.path === "/")) {
|
||||||
document.cookie = "JWT-Authorization=; Max-Age=0";
|
document.cookie = "JWT-Authorization=; Max-Age=0";
|
||||||
document.cookie = "user_name=; Max-Age=0";
|
document.cookie = "user_name=; Max-Age=0";
|
||||||
auth.user.token = "";
|
auth.user.token = "";
|
||||||
|
103
src/views/headquarters/HeadquartersManagement.vue
Normal file
103
src/views/headquarters/HeadquartersManagement.vue
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onUnmounted } from "vue";
|
||||||
|
import SysProgress from "./components/SysProgress.vue";
|
||||||
|
import ElecRank from "./components/ElecRank.vue";
|
||||||
|
import ElecTrends from "./components/ElecTrends.vue";
|
||||||
|
import ElecCompare from "./components/ElecCompare.vue";
|
||||||
|
import { getEnergyCost } from "@/apis/dashboard";
|
||||||
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
|
|
||||||
|
const store = useBuildingStore();
|
||||||
|
let intervalId = null;
|
||||||
|
const energyCostData = ref({});
|
||||||
|
const formState = ref({
|
||||||
|
building_guid: null,
|
||||||
|
floor_guid: "all",
|
||||||
|
department_id: "all",
|
||||||
|
});
|
||||||
|
|
||||||
|
const getEnergyCostData = async (params) => {
|
||||||
|
const res = await getEnergyCost(params);
|
||||||
|
energyCostData.value = res.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.selectedBuilding,
|
||||||
|
(newBuilding) => {
|
||||||
|
if (newBuilding) {
|
||||||
|
formState.value.building_guid = newBuilding.building_guid;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => formState.value,
|
||||||
|
(newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
const params = { ...newVal };
|
||||||
|
|
||||||
|
if (params.floor_guid === "all") {
|
||||||
|
delete params.floor_guid;
|
||||||
|
}
|
||||||
|
if (params.department_id === "all") {
|
||||||
|
delete params.department_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.building_guid) {
|
||||||
|
getEnergyCostData(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
getEnergyCostData(params);
|
||||||
|
}, 3600000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true, deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<div class="w-full xl:w-1/4 mt-2">
|
||||||
|
<div class="area-img-box">
|
||||||
|
<img
|
||||||
|
alt="build"
|
||||||
|
src="/build_img.jpg"
|
||||||
|
class="w-full h-48 object-cover border-cyan-400 shadow-cyan-500/40"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
class="p-4 text-gray-100 text-base font-light bg-gray-800/60 rounded-b-xl backdrop-blur-md border-t border-cyan-400/30 shadow-inner"
|
||||||
|
>
|
||||||
|
深耕電子精密連接器、光通信元件、軟性排線、線纜組件、PCBA電子機板、電子成品專業製造廠並代理電子零組件做為整合行銷。公司創立於1990年,產品行銷全球以穩定,快速以及高品質知名;
|
||||||
|
未來,瀚荃會持續精進提供更快、更好以及高附加價值的產品與服務來滿足您的需求。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<!--狀態、進度-->
|
||||||
|
<SysProgress />
|
||||||
|
</div>
|
||||||
|
<div class="w-full xl:w-2/4 mt-2"></div>
|
||||||
|
<div class="w-full xl:w-1/4 mt-2">
|
||||||
|
<ElecRank :energyCostData="energyCostData" />
|
||||||
|
<ElecTrends
|
||||||
|
:formState="formState"
|
||||||
|
:energyCostData="energyCostData"
|
||||||
|
:getEnergyCostData="getEnergyCostData"
|
||||||
|
/>
|
||||||
|
<ElecCompare :energyCostData="energyCostData" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.area-img-box {
|
||||||
|
@apply border border-light-info bg-gray-900/80 backdrop-blur-lg relative overflow-hidden shadow-md shadow-blue-300 mb-3;
|
||||||
|
}
|
||||||
|
</style>
|
305
src/views/headquarters/components/ElecCompare.vue
Normal file
305
src/views/headquarters/components/ElecCompare.vue
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, computed } from "vue";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
import BarChart from "@/components/chart/BarChart.vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { locale, t } = useI18n();
|
||||||
|
const props = defineProps({
|
||||||
|
energyCostData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const chartData = ref([]); // 初始化為空陣列
|
||||||
|
|
||||||
|
const labels = computed(() => [
|
||||||
|
t("dashboard.today"),
|
||||||
|
t("dashboard.yesterday"),
|
||||||
|
t("dashboard.this_week"),
|
||||||
|
t("dashboard.last_week"),
|
||||||
|
t("dashboard.this_month"),
|
||||||
|
t("dashboard.last_month"),
|
||||||
|
t("dashboard.this_year"),
|
||||||
|
t("dashboard.last_year"),
|
||||||
|
]);
|
||||||
|
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 },
|
||||||
|
series: [], // 初始化為空陣列
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateChartData(newEnergyCostData) {
|
||||||
|
if (newEnergyCostData && newEnergyCostData.compare) {
|
||||||
|
// 從 props.energyCostData.compare 中提取資料
|
||||||
|
const compareData = newEnergyCostData.compare;
|
||||||
|
|
||||||
|
// 轉換資料格式
|
||||||
|
chartData.value = [
|
||||||
|
{
|
||||||
|
category: t("dashboard.daily_relative_change"),
|
||||||
|
this: compareData.day.current,
|
||||||
|
last: compareData.day.last,
|
||||||
|
change: compareData.day.percentage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: t("dashboard.weekly_relative_change"),
|
||||||
|
this: compareData.week.current,
|
||||||
|
last: compareData.week.last,
|
||||||
|
change: compareData.week.percentage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: t("dashboard.monthly_relative_change"),
|
||||||
|
this: compareData.month.current,
|
||||||
|
last: compareData.month.last,
|
||||||
|
change: compareData.month.percentage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
category: t("dashboard.yearly_relative_change"),
|
||||||
|
this: compareData.year.current,
|
||||||
|
last: compareData.year.last,
|
||||||
|
change: compareData.year.percentage,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 更新 barChartOptions
|
||||||
|
barChartOptions.value = {
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
data: chartData.value.map((item) => item.category),
|
||||||
|
axisLine: { lineStyle: { color: "#fff" } },
|
||||||
|
},
|
||||||
|
yAxis: { type: "value", show: false },
|
||||||
|
grid: {
|
||||||
|
left: "-10%",
|
||||||
|
right: "1%",
|
||||||
|
bottom: "3%",
|
||||||
|
top: "4%",
|
||||||
|
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;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 使用 watch 監聽 energyCostData 的變化
|
||||||
|
watch(
|
||||||
|
() => props.energyCostData,
|
||||||
|
(newEnergyCostData) => {
|
||||||
|
updateChartData(newEnergyCostData);
|
||||||
|
},
|
||||||
|
{ immediate: true } // 立即執行一次,確保初始資料載入
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(locale, () => {
|
||||||
|
updateChartData(props.energyCostData);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
<div class="w-full chart-data relative px-3 mb-3">
|
||||||
|
<div class="flex flex-wrap items-center justify-between">
|
||||||
|
<h2 class="font-light">
|
||||||
|
{{ $t("dashboard.relative_energy_consumption") }}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div class="h-[100px]">
|
||||||
|
<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 -left-0 -top-2 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-1 -bottom-3 h-10 w-10 bg-no-repeat z-10;
|
||||||
|
content: "";
|
||||||
|
background: url(@ASSET/img/chart-data-background02.svg) center center;
|
||||||
|
}
|
||||||
|
</style>
|
109
src/views/headquarters/components/ElecRank.vue
Normal file
109
src/views/headquarters/components/ElecRank.vue
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, watch } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
energyCostData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const energyTypeList = ref([
|
||||||
|
{
|
||||||
|
title: t("dashboard.today_energy_consumption"),
|
||||||
|
key: "today",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: t("dashboard.this_month_energy_consumption"),
|
||||||
|
key: "month",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
const currentEnergyType = ref({
|
||||||
|
name: "month",
|
||||||
|
});
|
||||||
|
|
||||||
|
// 取得當前能耗資料
|
||||||
|
const getCurrentEnergyData = () => {
|
||||||
|
if (!props.energyCostData || !props.energyCostData.rank) {
|
||||||
|
return []; // 或者返回一些默认值
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentEnergyType.value.name === "month"
|
||||||
|
? props.energyCostData?.rank.month || []
|
||||||
|
: props.energyCostData?.rank.day || [];
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="state-box-col relative h-full max-h-[200px] mb-3">
|
||||||
|
<div class="state-box h-full">
|
||||||
|
<!-- 標題和切換按鈕 -->
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h2 class="font-light relative">
|
||||||
|
{{ $t("dashboard.energy_ranking") }}
|
||||||
|
</h2>
|
||||||
|
<Select
|
||||||
|
:value="currentEnergyType"
|
||||||
|
class="!w-24"
|
||||||
|
selectClass="border-info focus-within:border-info btn-xs text-xs"
|
||||||
|
name="name"
|
||||||
|
Attribute="title"
|
||||||
|
:options="energyTypeList"
|
||||||
|
:isTopLabelExist="false"
|
||||||
|
:isBottomLabelExist="false"
|
||||||
|
>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 能耗排名列表 -->
|
||||||
|
<div class="max-h-[150px] overflow-y-auto">
|
||||||
|
<table class="table table-sm text-center">
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(item, index) in getCurrentEnergyData()"
|
||||||
|
:key="index"
|
||||||
|
: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.value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</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>
|
226
src/views/headquarters/components/ElecTrends.vue
Normal file
226
src/views/headquarters/components/ElecTrends.vue
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch } from "vue";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
import BarChart from "@/components/chart/BarChart.vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
|
|
||||||
|
const storeBuild = useBuildingStore();
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
formState: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
energyCostData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
getEnergyCostData: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const chartData = ref([]);
|
||||||
|
const floorList = ref([]);
|
||||||
|
const deptList = ref([]);
|
||||||
|
const weekComparisonOption = ref({});
|
||||||
|
|
||||||
|
// 生成柱狀圖的 option
|
||||||
|
const generateCylinderChartOption = (data) => {
|
||||||
|
const barWidth = 15;
|
||||||
|
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: 15,
|
||||||
|
type: "pictorialBar",
|
||||||
|
symbolPosition: "end",
|
||||||
|
data: data.map((item) => item.energy),
|
||||||
|
symbol: "diamond",
|
||||||
|
symbolOffset: [0, -5],
|
||||||
|
symbolSize: [barWidth, barWidth * 0.5],
|
||||||
|
itemStyle: {
|
||||||
|
color: "#62E39A",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
z: 10,
|
||||||
|
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: "10%",
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
formatter: function (params) {
|
||||||
|
const item = params[0];
|
||||||
|
return `<p>${item.name}</p> <p>${item.marker}Energy consumption : ${item.value}</p>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const processEnergyData = () => {
|
||||||
|
if (!props.energyCostData || !props.energyCostData.trend) {
|
||||||
|
chartData.value = [];
|
||||||
|
weekComparisonOption.value = generateCylinderChartOption(chartData.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dailyData = [...props.energyCostData.trend].sort(
|
||||||
|
(a, b) => new Date(a.time) - new Date(b.time)
|
||||||
|
);
|
||||||
|
|
||||||
|
chartData.value = dailyData.map((item) => ({
|
||||||
|
date: dayjs(item.time).format("MM/DD"),
|
||||||
|
energy: item.value,
|
||||||
|
}));
|
||||||
|
|
||||||
|
weekComparisonOption.value = generateCylinderChartOption(chartData.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.energyCostData,
|
||||||
|
(newEnergyCostData) => {
|
||||||
|
processEnergyData();
|
||||||
|
},
|
||||||
|
{ deep: true, immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => storeBuild.floorList,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
console.log('newValue',newValue);
|
||||||
|
|
||||||
|
floorList.value = [
|
||||||
|
{
|
||||||
|
title: "All",
|
||||||
|
key: "all",
|
||||||
|
},
|
||||||
|
...storeBuild.floorList,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => storeBuild.floorList,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
floorList.value = [
|
||||||
|
{
|
||||||
|
title: "All",
|
||||||
|
key: "all",
|
||||||
|
},
|
||||||
|
...storeBuild.floorList,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => storeBuild.deptList,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
deptList.value = [
|
||||||
|
{
|
||||||
|
title: "All",
|
||||||
|
key: "all",
|
||||||
|
},
|
||||||
|
...storeBuild.deptList,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="w-full chart-data relative px-3 mb-3">
|
||||||
|
<div class="flex flex-wrap items-center justify-between">
|
||||||
|
<h2 class="font-light">{{ $t("dashboard.last_30_days_energy_trend") }}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="h-[200px]">
|
||||||
|
<BarChart
|
||||||
|
id="dashboard_chart_week_comparison"
|
||||||
|
class="h-full"
|
||||||
|
:option="weekComparisonOption"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.chart-data:before {
|
||||||
|
@apply absolute -left-0 -top-1 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-1 -bottom-1 h-10 w-10 bg-no-repeat z-10;
|
||||||
|
content: "";
|
||||||
|
background: url(@ASSET/img/chart-data-background02.svg) center center;
|
||||||
|
}
|
||||||
|
</style>
|
143
src/views/headquarters/components/SysProgress.vue
Normal file
143
src/views/headquarters/components/SysProgress.vue
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch, onUnmounted } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
import { getAlarmOperationInfo } from "@/apis/dashboard";
|
||||||
|
import useBuildingStore from "@/stores/useBuildingStore";
|
||||||
|
// import DashboardSysProgressModal from "./DashboardSysProgressModal.vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const router = useRouter();
|
||||||
|
const store = useBuildingStore();
|
||||||
|
const equipmentData = ref({
|
||||||
|
title: t("dashboard.system_status"),
|
||||||
|
items: [],
|
||||||
|
});
|
||||||
|
const modalData = ref({});
|
||||||
|
let intervalId = null;
|
||||||
|
|
||||||
|
const getAlarmsInfos = async () => {
|
||||||
|
try {
|
||||||
|
const res = await getAlarmOperationInfo(
|
||||||
|
store.selectedBuilding.building_guid
|
||||||
|
);
|
||||||
|
const apiData = res.data;
|
||||||
|
|
||||||
|
// 轉換 equipmentData 的資料格式
|
||||||
|
if (apiData && apiData.alarm) {
|
||||||
|
equipmentData.value.items = apiData.alarm.map((item) => ({
|
||||||
|
label: item.name,
|
||||||
|
online: item.online || 0,
|
||||||
|
offline: item.offline || 0,
|
||||||
|
alarm: item.alarm || 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching alarm info:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => store.selectedBuilding,
|
||||||
|
(newBuilding) => {
|
||||||
|
if (newBuilding) {
|
||||||
|
getAlarmsInfos();
|
||||||
|
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
getAlarmsInfos();
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- <DashboardSysProgressModal :onCancel="onCancel" :modalData="modalData" /> -->
|
||||||
|
<div class="w-full state-box-col relative">
|
||||||
|
<div class="state-box">
|
||||||
|
<div class="title">
|
||||||
|
<img class="state-title01" src="@ASSET/img/state-title01.svg" />
|
||||||
|
<span class="">{{ equipmentData.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>{{ $t("alert.online") }}</th>
|
||||||
|
<th>{{ $t("alert.offline") }}</th>
|
||||||
|
<th>{{ $t("alert.alarm") }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(item, index) in equipmentData.items"
|
||||||
|
:key="index"
|
||||||
|
class="border-cyan-400 cursor-pointer hover:text-info"
|
||||||
|
>
|
||||||
|
<th class="px-0 text-start">{{ item.label }}</th>
|
||||||
|
<td>
|
||||||
|
{{ item.online.length }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ item.offline.length }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ item.alarm.length }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-box {
|
||||||
|
@apply h-[21rem] border border-light-info shadow-md shadow-blue-300 rounded-sm py-2 px-6 mb-5 text-white relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-box:after {
|
||||||
|
@apply absolute right-3 top-3 w-4 h-4 bg-no-repeat bg-center z-10;
|
||||||
|
content: "";
|
||||||
|
background-image: url(@ASSET/img/state-title01.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-box:before {
|
||||||
|
@apply absolute right-0.5 bottom-5 w-4 h-32 bg-no-repeat bg-center z-10;
|
||||||
|
content: "";
|
||||||
|
background-image: url(@ASSET/img/state-ul-background02.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-box .title {
|
||||||
|
@apply relative flex items-center mb-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-box .title .state-title01 {
|
||||||
|
@apply w-4 mr-1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.state-box .title .state-title02 {
|
||||||
|
@apply w-5 ml-1.5;
|
||||||
|
}
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user