首頁設備modal
This commit is contained in:
parent
b3ae2db111
commit
19370f5d86
@ -72,10 +72,19 @@ const getData = async () => {
|
|||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
{
|
{
|
||||||
full_name: device.full_name,
|
device_number: device.device_number || "", // 設備編號
|
||||||
|
device_coordinate: device.device_coordinate || "", // 設備座標
|
||||||
|
device_image_url: device.device_image_url,
|
||||||
|
full_name: device.full_name, // 設備名稱
|
||||||
|
main_id: device.main_id,
|
||||||
|
points: device.points || [],
|
||||||
|
floor: floor.full_name,
|
||||||
state: state,
|
state: state,
|
||||||
icon: device.device_image ? `${FILE_BASEURL}/upload/device_icon/${device.device_image}` : '',
|
icon: device.device_image ? `${FILE_BASEURL}/upload/device_icon/${device.device_image}` : '',
|
||||||
bgColor: bgColor,
|
bgColor: bgColor,
|
||||||
|
Online_color: device.device_normal_color,
|
||||||
|
Offline_color: device.device_close_color,
|
||||||
|
Error_color: device.device_error_color,
|
||||||
bgSize: 50,
|
bgSize: 50,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
import EffectScatter from "@/components/chart/EffectScatter.vue";
|
import EffectScatter from "@/components/chart/EffectScatter.vue";
|
||||||
|
import DashboardEffectScatterModal from "./DashboardEffectScatterModal.vue";
|
||||||
import useSearchParam from "@/hooks/useSearchParam";
|
import useSearchParam from "@/hooks/useSearchParam";
|
||||||
import { computed, inject, ref, watch } from "vue";
|
import { computed, inject, ref, watch } from "vue";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
@ -19,26 +20,12 @@ const props = defineProps({
|
|||||||
|
|
||||||
const asset_floor_chart = ref(null);
|
const asset_floor_chart = ref(null);
|
||||||
const currentFloorId = ref(null);
|
const currentFloorId = ref(null);
|
||||||
|
const selectedDevice = ref(null);
|
||||||
|
|
||||||
const defaultOption = (map, data = []) => {
|
const defaultOption = (map, data = []) => {
|
||||||
return {
|
return {
|
||||||
animation: false,
|
animation: false,
|
||||||
tooltip: {},
|
|
||||||
geo: {
|
geo: {
|
||||||
tooltip: {
|
|
||||||
show: true,
|
|
||||||
confine: true,
|
|
||||||
formatter: function (params) {
|
|
||||||
return params.data[2]?.full_name
|
|
||||||
? `
|
|
||||||
<div class="p-2 w-auto min-w-24">
|
|
||||||
<div class="text-lg">${params.data[2].full_name}</div>
|
|
||||||
<div class="text-sm text-gray-500">${t("operation.status")} : ${
|
|
||||||
params.data[2].state || ""
|
|
||||||
}</div>`
|
|
||||||
: "";
|
|
||||||
},
|
|
||||||
},
|
|
||||||
map,
|
map,
|
||||||
roam: true, // 允許縮放和平移
|
roam: true, // 允許縮放和平移
|
||||||
layoutSize: window.innerWidth <= 768 ? "110%" : "80%",
|
layoutSize: window.innerWidth <= 768 ? "110%" : "80%",
|
||||||
@ -63,9 +50,6 @@ const defaultOption = (map, data = []) => {
|
|||||||
type: "scatter",
|
type: "scatter",
|
||||||
coordinateSystem: "geo",
|
coordinateSystem: "geo",
|
||||||
geoIndex: 0,
|
geoIndex: 0,
|
||||||
encode: {
|
|
||||||
tooltip: 2,
|
|
||||||
},
|
|
||||||
symbolSize: 30,
|
symbolSize: 30,
|
||||||
itemStyle: {
|
itemStyle: {
|
||||||
color: "rgba(225,225,225,0.8)",
|
color: "rgba(225,225,225,0.8)",
|
||||||
@ -98,6 +82,13 @@ const currentIconData = computed(() => {
|
|||||||
return data;
|
return data;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 處理圖表項目點擊事件
|
||||||
|
const handleItemClick = (params) => {
|
||||||
|
if (params.data && params.data[2]) {
|
||||||
|
selectedDevice.value = params.data[2];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
[searchParams, () => asset_floor_chart.value, () => props.data],
|
[searchParams, () => asset_floor_chart.value, () => props.data],
|
||||||
([newValue, newChart, newData], [oldValue]) => {
|
([newValue, newChart, newData], [oldValue]) => {
|
||||||
@ -115,6 +106,14 @@ watch(
|
|||||||
},
|
},
|
||||||
defaultOption(newValue.floor_id, currentIconData.value)
|
defaultOption(newValue.floor_id, currentIconData.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 添加點擊事件監聽
|
||||||
|
setTimeout(() => {
|
||||||
|
if (newChart.chart) {
|
||||||
|
newChart.chart.off('click'); // 移除舊的監聽器
|
||||||
|
newChart.chart.on('click', handleItemClick);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
} else if (currentFloorId.value === newValue.floor_id && newChart.chart) {
|
} else if (currentFloorId.value === newValue.floor_id && newChart.chart) {
|
||||||
// 只是資料更新時,只更新圖表資料,不重新載入 SVG
|
// 只是資料更新時,只更新圖表資料,不重新載入 SVG
|
||||||
console.log("Data updated, refreshing chart data only");
|
console.log("Data updated, refreshing chart data only");
|
||||||
@ -132,6 +131,7 @@ watch(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<DashboardEffectScatterModal :data="selectedDevice" />
|
||||||
<EffectScatter
|
<EffectScatter
|
||||||
id="system_floor_chart"
|
id="system_floor_chart"
|
||||||
ref="asset_floor_chart"
|
ref="asset_floor_chart"
|
||||||
|
287
src/views/dashboard/components/DashboardEffectScatterModal.vue
Normal file
287
src/views/dashboard/components/DashboardEffectScatterModal.vue
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
<script setup>
|
||||||
|
import Modal from "@/components/customUI/Modal.vue";
|
||||||
|
import DashboardEffectScatterModalChart from "./DashboardEffectScatterModalChart.vue";
|
||||||
|
import { ref, watch, computed } from "vue";
|
||||||
|
import { useI18n } from "vue-i18n";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
const { t } = useI18n();
|
||||||
|
const currentTab = ref("desktop");
|
||||||
|
const changeOpenKey = (key) => {
|
||||||
|
currentTab.value = key;
|
||||||
|
};
|
||||||
|
const props = defineProps({
|
||||||
|
data: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const modal = ref(null);
|
||||||
|
|
||||||
|
// 當 data 有值時自動開啟 modal
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
(newData) => {
|
||||||
|
if (newData) {
|
||||||
|
dashboard_effectScatter_modal.showModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
// 關閉 modal 的處理函數
|
||||||
|
const handleCancel = () => {
|
||||||
|
dashboard_effectScatter_modal.close();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
id="dashboard_effectScatter_modal"
|
||||||
|
ref="modal"
|
||||||
|
:title="props.data?.full_name"
|
||||||
|
:onCancel="handleCancel"
|
||||||
|
:width="600"
|
||||||
|
:draggable="true"
|
||||||
|
modalClass="max-h-[80vh]"
|
||||||
|
>
|
||||||
|
<template #modalTitle>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<span>{{ props.data?.full_name }}</span>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-base btn-link btn-text-without-border px-2"
|
||||||
|
@click="() => changeOpenKey('desktop')"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'desktop']"
|
||||||
|
size="lg"
|
||||||
|
:class="
|
||||||
|
twMerge(
|
||||||
|
currentTab === 'desktop' ? 'text-success' : 'text-[#a5abb1]'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-base btn-link btn-text-without-border px-2"
|
||||||
|
@click="() => changeOpenKey('image')"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'image']"
|
||||||
|
size="lg"
|
||||||
|
:class="
|
||||||
|
twMerge(
|
||||||
|
currentTab === 'image' ? 'text-success' : 'text-[#a5abb1]'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-base btn-link btn-text-without-border px-2"
|
||||||
|
@click="() => changeOpenKey('cog')"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'cog']"
|
||||||
|
size="lg"
|
||||||
|
:class="
|
||||||
|
twMerge(
|
||||||
|
currentTab === 'cog' ? 'text-success' : 'text-[#a5abb1]'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
class="text-base btn-link btn-text-without-border px-2"
|
||||||
|
@click="() => changeOpenKey('chart')"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'chart-line']"
|
||||||
|
size="lg"
|
||||||
|
:class="
|
||||||
|
twMerge(
|
||||||
|
currentTab === 'chart' ? 'text-success' : 'text-[#a5abb1]'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn-link btn-text-without-border px-2"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', 'times']"
|
||||||
|
class="text-[#a5abb1]"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #modalContent>
|
||||||
|
<div class="space-y-4 py-4">
|
||||||
|
<!-- Desktop Tab - 基本資訊 -->
|
||||||
|
<div v-if="currentTab === 'desktop'" class="grid grid-cols-1 gap-4">
|
||||||
|
<table
|
||||||
|
v-if="props.data?.points && props.data.points.length"
|
||||||
|
class="min-w-full bg-gray-700 border text-gray-100"
|
||||||
|
>
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-600">
|
||||||
|
<th class="p-2 border text-left">名稱</th>
|
||||||
|
<th class="p-2 border text-left">點位</th>
|
||||||
|
<th class="p-2 border text-left">數值</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(point, idx) in props.data.points"
|
||||||
|
:key="idx"
|
||||||
|
class="hover:bg-gray-600"
|
||||||
|
>
|
||||||
|
<td class="p-2 border">{{ point.full_name }}</td>
|
||||||
|
<td class="p-2 py-1 border">{{ point.points }}</td>
|
||||||
|
<td class="p-2 py-1 border">{{ point.value }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Tab - 可視化資訊 -->
|
||||||
|
<div v-if="currentTab === 'image'" class="grid grid-cols-1 gap-4">
|
||||||
|
<table class="min-w-full bg-gray-700 border text-gray-100">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-600">
|
||||||
|
<th class="p-2 border text-left">項目</th>
|
||||||
|
<th class="p-2 border text-left">內容</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="props.data?.icon" class="hover:bg-gray-600">
|
||||||
|
<td class="p-2 border">設備圖示</td>
|
||||||
|
<td class="p-2 border">
|
||||||
|
<img
|
||||||
|
:src="props.data.icon"
|
||||||
|
alt="設備圖示"
|
||||||
|
class="w-12 h-12"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="props.data.Online_color" class="hover:bg-gray-600">
|
||||||
|
<td class="p-2 border">Online 顏色</td>
|
||||||
|
<td class="p-2 border">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
class="w-6 h-6 rounded border border-gray-300"
|
||||||
|
:style="{ backgroundColor: props.data.Online_color }"
|
||||||
|
></div>
|
||||||
|
<span class="text-gray-100 text-sm font-mono">{{
|
||||||
|
props.data.Online_color
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="props.data.Offline_color" class="hover:bg-gray-600">
|
||||||
|
<td class="p-2 border">Offline 顏色</td>
|
||||||
|
<td class="p-2 border">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
class="w-6 h-6 rounded border border-gray-300"
|
||||||
|
:style="{ backgroundColor: props.data.Offline_color }"
|
||||||
|
></div>
|
||||||
|
<span class="text-gray-100 text-sm font-mono">{{
|
||||||
|
props.data.Offline_color
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="props.data.Error_color" class="hover:bg-gray-600">
|
||||||
|
<td class="p-2 border">Error 顏色</td>
|
||||||
|
<td class="p-2 border">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<div
|
||||||
|
class="w-6 h-6 rounded border border-gray-300"
|
||||||
|
:style="{ backgroundColor: props.data.Error_color }"
|
||||||
|
></div>
|
||||||
|
<span class="text-gray-100 text-sm font-mono">{{
|
||||||
|
props.data.Error_color
|
||||||
|
}}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- chart Tab - 圖表資訊 -->
|
||||||
|
<div v-if="currentTab === 'chart'" class="grid grid-cols-1 gap-4">
|
||||||
|
<DashboardEffectScatterModalChart :data="props.data" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Cog Tab - 完整資料 -->
|
||||||
|
<div v-if="currentTab === 'cog'" class="grid grid-cols-1 gap-4">
|
||||||
|
<table class="min-w-full bg-gray-700 border text-gray-100">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-600">
|
||||||
|
<th class="p-2 border text-left">項目</th>
|
||||||
|
<th class="p-2 border text-left">內容</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="hover:bg-gray-600">
|
||||||
|
<td class="p-2 border">
|
||||||
|
{{ $t("assetManagement.device_number") }}
|
||||||
|
</td>
|
||||||
|
<td class="p-2 border">{{ props.data.device_number }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="hover:bg-gray-600">
|
||||||
|
<td class="p-2 border">
|
||||||
|
{{ $t("assetManagement.device_name") }}
|
||||||
|
</td>
|
||||||
|
<td class="p-2 border">{{ props.data.full_name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="hover:bg-gray-600">
|
||||||
|
<td class="p-2 border">{{ $t("assetManagement.floor") }}</td>
|
||||||
|
<td class="p-2 border">{{ props.data.floor }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="hover:bg-gray-600">
|
||||||
|
<td class="p-2 border">
|
||||||
|
{{ $t("assetManagement.device_coordinate") }}
|
||||||
|
</td>
|
||||||
|
<td class="p-2 border">{{ props.data.device_coordinate }}</td>
|
||||||
|
</tr>
|
||||||
|
<!--
|
||||||
|
<tr>
|
||||||
|
<td class="p-2 border">{{ $t("assetManagement.brand_and_modal") }}</td>
|
||||||
|
<td class="p-2 border">{{ deviceInfo.brand }} / {{ deviceInfo.modal }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="p-2 border">{{ $t("assetManagement.company_and_contact") }}</td>
|
||||||
|
<td class="p-2 border">{{ deviceInfo.company }} / {{ deviceInfo.contact }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="p-2 border">{{ $t("assetManagement.buying_date") }}</td>
|
||||||
|
<td class="p-2 border">{{ deviceInfo.buying_date }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="p-2 border">{{ $t("assetManagement.created_at") }}</td>
|
||||||
|
<td class="p-2 border">{{ deviceInfo.created_at }}</td>
|
||||||
|
</tr>
|
||||||
|
-->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
/* 可以添加額外的樣式 */
|
||||||
|
</style>
|
@ -0,0 +1,227 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps, onMounted, onUnmounted, ref, nextTick, watch } from "vue";
|
||||||
|
import { getHistoryData } from "@/apis/history";
|
||||||
|
import LineChart from "@/components/chart/LineChart.vue";
|
||||||
|
import { SECOND_CHART_COLOR } from "@/constant";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: Object,
|
||||||
|
});
|
||||||
|
const pointsList = ref([]);
|
||||||
|
const timeList = ref([
|
||||||
|
{ value: 1, name: "1小時" },
|
||||||
|
{ value: 4, name: "4小時" },
|
||||||
|
{ value: 8, name: "8小時" },
|
||||||
|
]);
|
||||||
|
const chartData = ref([]);
|
||||||
|
const forge_chart = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 預設圖表選項
|
||||||
|
const defaultChartOption = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: [],
|
||||||
|
textStyle: {
|
||||||
|
color: "#ffffff",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: "0%",
|
||||||
|
left: "0%",
|
||||||
|
right: "0%",
|
||||||
|
bottom: "0%",
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
splitLine: { show: false },
|
||||||
|
axisLabel: {
|
||||||
|
color: "#ffffff",
|
||||||
|
formatter: (value) => dayjs(value).format("HH:mm"), // 格式化為時間
|
||||||
|
},
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
splitLine: { show: false },
|
||||||
|
axisLabel: { color: "#ffffff" },
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formState = ref({
|
||||||
|
Cumulant: 1,
|
||||||
|
Type: 2,
|
||||||
|
Points: [],
|
||||||
|
Start_date: dayjs().format("YYYY-MM-DD"),
|
||||||
|
Start_time: dayjs().format("HH:00"),
|
||||||
|
End_date: dayjs().format("YYYY-MM-DD"),
|
||||||
|
End_time: dayjs().format("HH:00"),
|
||||||
|
Device_list: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTimeRange = (hours) => {
|
||||||
|
const now = dayjs();
|
||||||
|
const startTime = now.subtract(hours, "hour");
|
||||||
|
formState.value.Start_date = startTime.format("YYYY-MM-DD");
|
||||||
|
formState.value.Start_time = startTime.format("HH:00");
|
||||||
|
formState.value.End_date = now.format("YYYY-MM-DD");
|
||||||
|
formState.value.End_time = now.format("HH:00");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getHistoryData(formState.value);
|
||||||
|
if (res.isSuccess) {
|
||||||
|
if (res.data.items.length > 0) {
|
||||||
|
chartData.value = res.data.items
|
||||||
|
.map((d) => ({
|
||||||
|
timestamp: d.timestamp,
|
||||||
|
value: parseFloat(d.value),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||||
|
|
||||||
|
// 更新圖表
|
||||||
|
await nextTick();
|
||||||
|
if (forge_chart.value?.chart) {
|
||||||
|
forge_chart.value.chart.setOption({
|
||||||
|
xAxis: {
|
||||||
|
data: chartData.value.map((d) => d.timestamp),
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: props.data?.full_name,
|
||||||
|
type: "line",
|
||||||
|
data: chartData.value.map((d) => d.value),
|
||||||
|
showSymbol: false,
|
||||||
|
itemStyle: {
|
||||||
|
color: SECOND_CHART_COLOR[0], // 使用預設顏色
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chartData.value = [];
|
||||||
|
if (forge_chart.value?.chart) {
|
||||||
|
forge_chart.value.chart.clear(); // 清空圖表
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("API Error:", res.msg);
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("Initial data:", props.data);
|
||||||
|
if (props.data?.device_number) {
|
||||||
|
formState.value.Device_list = [props.data.device_number];
|
||||||
|
}
|
||||||
|
if (props.data?.points) {
|
||||||
|
if (Array.isArray(props.data.points)) {
|
||||||
|
pointsList.value = props.data.points.map(item => ({
|
||||||
|
name: item.full_name,
|
||||||
|
value: item.points
|
||||||
|
}));
|
||||||
|
formState.value.Cumulant = 1;
|
||||||
|
} else {
|
||||||
|
// 舊格式物件處理
|
||||||
|
const filteredKeys = Object.keys(props.data.points).filter(
|
||||||
|
(key) => !["ST", "Type", "Light", "Size"].includes(key)
|
||||||
|
);
|
||||||
|
if (props.data?.subSys === "Wtr") {
|
||||||
|
pointsList.value = [
|
||||||
|
{ name: "Total", value: "Total" },
|
||||||
|
...filteredKeys.map((key) => ({
|
||||||
|
name: key,
|
||||||
|
value: key,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
formState.value.Cumulant = 2;
|
||||||
|
} else {
|
||||||
|
pointsList.value = filteredKeys.map((key) => ({
|
||||||
|
name: key,
|
||||||
|
value: key,
|
||||||
|
}));
|
||||||
|
formState.value.Cumulant = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointsList.value.length > 0) {
|
||||||
|
formState.value.Points = pointsList.value[0].value;
|
||||||
|
}
|
||||||
|
if (timeList.value.length > 0) {
|
||||||
|
formState.value.time = timeList.value[0].value;
|
||||||
|
updateTimeRange(timeList.value[0].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
formState.value = {};
|
||||||
|
chartData.value = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => formState.value.Points,
|
||||||
|
(newPoints) => {
|
||||||
|
if (newPoints.includes("Total")) {
|
||||||
|
formState.value.Cumulant = 2;
|
||||||
|
} else {
|
||||||
|
formState.value.Cumulant = 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<Select
|
||||||
|
:value="formState"
|
||||||
|
class=""
|
||||||
|
selectClass="border-info focus-within:border-info"
|
||||||
|
name="Points"
|
||||||
|
Attribute="name"
|
||||||
|
:options="pointsList"
|
||||||
|
></Select>
|
||||||
|
<Select
|
||||||
|
:value="formState"
|
||||||
|
class=""
|
||||||
|
selectClass="border-info focus-within:border-info"
|
||||||
|
name="time"
|
||||||
|
Attribute="name"
|
||||||
|
:options="timeList"
|
||||||
|
@change="(value) => updateTimeRange(value)"
|
||||||
|
></Select>
|
||||||
|
<button class="btn btn-success" @click.stop.prevent="onSearch">
|
||||||
|
<font-awesome-icon :icon="['fas', 'search']" class="" />搜尋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="min-h-[300px] relative">
|
||||||
|
<span
|
||||||
|
v-if="loading"
|
||||||
|
className="loading loading-spinner loading-lg text-info absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-20"
|
||||||
|
></span>
|
||||||
|
<LineChart
|
||||||
|
v-if="chartData.length > 0"
|
||||||
|
id="forge_chart"
|
||||||
|
class="min-h-[300px] max-h-fit"
|
||||||
|
:option="defaultChartOption"
|
||||||
|
ref="forge_chart"
|
||||||
|
/>
|
||||||
|
<p class="text-center text-xl" v-if="!loading && chartData.length === 0">
|
||||||
|
沒有資料
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
@ -86,7 +86,7 @@ const updateChart = () => {
|
|||||||
{
|
{
|
||||||
name: t("energy.real_time_Trend"),
|
name: t("energy.real_time_Trend"),
|
||||||
type: "line",
|
type: "line",
|
||||||
data: realTimeDemand.value.map((d) => d.value),
|
data: realTimeDemand.value.map((d) => d.value / 1000),
|
||||||
smooth: true,
|
smooth: true,
|
||||||
lineStyle: { width: 3 },
|
lineStyle: { width: 3 },
|
||||||
itemStyle: { color: "#17CEE3" },
|
itemStyle: { color: "#17CEE3" },
|
||||||
@ -159,7 +159,7 @@ onUnmounted(() => {
|
|||||||
{{ $t("energy.immediate_demand") }}
|
{{ $t("energy.immediate_demand") }}
|
||||||
{{
|
{{
|
||||||
realTimeDemand.length > 0
|
realTimeDemand.length > 0
|
||||||
? realTimeDemand[realTimeDemand.length - 1].value
|
? realTimeDemand[realTimeDemand.length - 1].value / 1000
|
||||||
: "---"
|
: "---"
|
||||||
}}
|
}}
|
||||||
kw
|
kw
|
||||||
|
219
src/views/dashboard/components/ForgeInfoModalChart.vue
Normal file
219
src/views/dashboard/components/ForgeInfoModalChart.vue
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
<script setup>
|
||||||
|
import { defineProps, onMounted, onUnmounted, ref, nextTick, watch } from "vue";
|
||||||
|
import { getHistoryData } from "@/apis/history";
|
||||||
|
import LineChart from "@/components/chart/LineChart.vue";
|
||||||
|
import { SECOND_CHART_COLOR } from "@/constant";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
data: Object,
|
||||||
|
});
|
||||||
|
const pointsList = ref([]);
|
||||||
|
const timeList = ref([
|
||||||
|
{ value: 1, name: "1小時" },
|
||||||
|
{ value: 4, name: "4小時" },
|
||||||
|
{ value: 8, name: "8小時" },
|
||||||
|
]);
|
||||||
|
const chartData = ref([]);
|
||||||
|
const forge_chart = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 預設圖表選項
|
||||||
|
const defaultChartOption = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: "axis",
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
data: [],
|
||||||
|
textStyle: {
|
||||||
|
color: "#ffffff",
|
||||||
|
fontSize: 16,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
top: "25%",
|
||||||
|
left: "0%",
|
||||||
|
right: "0%",
|
||||||
|
bottom: "0%",
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
splitLine: { show: false },
|
||||||
|
axisLabel: {
|
||||||
|
color: "#ffffff",
|
||||||
|
formatter: (value) => dayjs(value).format("HH:mm"), // 格式化為時間
|
||||||
|
},
|
||||||
|
data: [],
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
splitLine: { show: false },
|
||||||
|
axisLabel: { color: "#ffffff" },
|
||||||
|
},
|
||||||
|
series: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const formState = ref({
|
||||||
|
Cumulant: 1,
|
||||||
|
Type: 2,
|
||||||
|
Points: [],
|
||||||
|
Start_date: dayjs().format("YYYY-MM-DD"),
|
||||||
|
Start_time: dayjs().format("HH:00"),
|
||||||
|
End_date: dayjs().format("YYYY-MM-DD"),
|
||||||
|
End_time: dayjs().format("HH:00"),
|
||||||
|
Device_list: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateTimeRange = (hours) => {
|
||||||
|
const now = dayjs();
|
||||||
|
const startTime = now.subtract(hours, "hour");
|
||||||
|
formState.value.Start_date = startTime.format("YYYY-MM-DD");
|
||||||
|
formState.value.Start_time = startTime.format("HH:00");
|
||||||
|
formState.value.End_date = now.format("YYYY-MM-DD");
|
||||||
|
formState.value.End_time = now.format("HH:00");
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getHistoryData(formState.value);
|
||||||
|
if (res.isSuccess) {
|
||||||
|
if (res.data.items.length > 0) {
|
||||||
|
chartData.value = res.data.items
|
||||||
|
.map((d) => ({
|
||||||
|
timestamp: d.timestamp,
|
||||||
|
value: parseFloat(d.value),
|
||||||
|
}))
|
||||||
|
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
|
||||||
|
|
||||||
|
// 更新圖表
|
||||||
|
await nextTick();
|
||||||
|
if (forge_chart.value?.chart) {
|
||||||
|
forge_chart.value.chart.setOption({
|
||||||
|
xAxis: {
|
||||||
|
data: chartData.value.map((d) => d.timestamp),
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: props.data?.value.full_name,
|
||||||
|
type: "line",
|
||||||
|
data: chartData.value.map((d) => d.value),
|
||||||
|
showSymbol: false,
|
||||||
|
itemStyle: {
|
||||||
|
color: SECOND_CHART_COLOR[0], // 使用預設顏色
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
chartData.value = [];
|
||||||
|
if (forge_chart.value?.chart) {
|
||||||
|
forge_chart.value.chart.clear(); // 清空圖表
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error("API Error:", res.msg);
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
console.log("Initial data:", props.data.value);
|
||||||
|
if (props.data?.value.device_number) {
|
||||||
|
formState.value.Device_list = [props.data.value.device_number];
|
||||||
|
}
|
||||||
|
if (props.data?.value.points) {
|
||||||
|
const filteredKeys = Object.keys(props.data.value.points).filter(
|
||||||
|
(key) => !["ST", "Type", "Light", "Size"].includes(key)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (props.data?.value.subSys === "Wtr") {
|
||||||
|
pointsList.value = [
|
||||||
|
{ name: "Total", value: "Total" },
|
||||||
|
...filteredKeys.map((key) => ({
|
||||||
|
name: key,
|
||||||
|
value: key,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
formState.value.Cumulant = 2;
|
||||||
|
} else {
|
||||||
|
pointsList.value = filteredKeys.map((key) => ({
|
||||||
|
name: key,
|
||||||
|
value: key,
|
||||||
|
}));
|
||||||
|
formState.value.Cumulant = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pointsList.value.length > 0) {
|
||||||
|
formState.value.Points = pointsList.value[0].value;
|
||||||
|
}
|
||||||
|
if (timeList.value.length > 0) {
|
||||||
|
formState.value.time = timeList.value[0].value;
|
||||||
|
updateTimeRange(timeList.value[0].value);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
formState.value = {};
|
||||||
|
chartData.value = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => formState.value.Points,
|
||||||
|
(newPoints) => {
|
||||||
|
if (newPoints.includes("Total")) {
|
||||||
|
formState.value.Cumulant = 2;
|
||||||
|
} else {
|
||||||
|
formState.value.Cumulant = 1;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<Select
|
||||||
|
:value="formState"
|
||||||
|
class=""
|
||||||
|
selectClass="border-info focus-within:border-info"
|
||||||
|
name="Points"
|
||||||
|
Attribute="name"
|
||||||
|
:options="pointsList"
|
||||||
|
></Select>
|
||||||
|
<Select
|
||||||
|
:value="formState"
|
||||||
|
class=""
|
||||||
|
selectClass="border-info focus-within:border-info"
|
||||||
|
name="time"
|
||||||
|
Attribute="name"
|
||||||
|
:options="timeList"
|
||||||
|
@change="(value) => updateTimeRange(value)"
|
||||||
|
></Select>
|
||||||
|
<button class="btn btn-success" @click.stop.prevent="onSearch">
|
||||||
|
<font-awesome-icon :icon="['fas', 'search']" class="" />搜尋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="min-h-[300px] relative">
|
||||||
|
<span
|
||||||
|
v-if="loading"
|
||||||
|
className="loading loading-spinner loading-lg text-info absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-20"
|
||||||
|
></span>
|
||||||
|
<LineChart
|
||||||
|
v-if="chartData.length > 0"
|
||||||
|
id="forge_chart"
|
||||||
|
class="min-h-[300px] max-h-fit"
|
||||||
|
:option="defaultChartOption"
|
||||||
|
ref="forge_chart"
|
||||||
|
/>
|
||||||
|
<p class="text-center text-xl" v-if="!loading && chartData.length === 0">
|
||||||
|
沒有資料
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
Loading…
Reference in New Issue
Block a user