調整儀表板與電表卡片的顯示邏輯 | 新增電表卡片即時數據獲取與圖表顯示功能

This commit is contained in:
koko 2025-09-04 14:04:17 +08:00
parent cea34c2e9d
commit babef0e2ae
8 changed files with 490 additions and 128 deletions

View File

@ -107,21 +107,6 @@ const initForge = () => {
viewer.isLoadDone()
);
updateForgeViewer(viewer);
// const tree = viewer.model.getData().instanceTree;
// hideAllObjects(tree, visibleDbid.value);
// visibleDbid.value.forEach((dbid) => {
// if (dbid === 58) {
// viewer.setThemingColor(dbid, new THREE.Vector4(1, 0, 0, 1));
// }
// });
// dbid
// viewer.addEventListener(
// Autodesk.Viewing.SELECTION_CHANGED_EVENT,
// function (event) {
// console.log(" forge_dbid", event.dbIdArray);
// }
// );
}
);
viewer.addEventListener(

View File

@ -495,22 +495,6 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
}
});
watch(initialData, (newValue) => {
if (newValue) {
getDevice(searchParams.value.option);
}
});
watch(
searchParams,
(newValue) => {
getDevice(newValue.option);
},
{
deep: true,
}
);
// 動態Sprites創建函數
const createSprites = async (viewer, dbId, reverse = false) => {
try {

View File

@ -9,13 +9,26 @@ import DashboardElectricity from "./components/DashboardElectricity.vue";
import DashboardAlert from "./components/DashboardAlert.vue";
import DashboardForgeOptionButton from "./components/DashboardForgeOptionButton.vue";
import DashboardForgeOptionCard from "./components/DashboardForgeOptionCard.vue";
import { getDashboardInit, getDashboardOptionRealTimeData } from "@/apis/dashboard";
import {
getDashboardInit,
getDashboardOptionRealTimeData,
} from "@/apis/dashboard";
import { getAssetFloorList } from "@/apis/asset";
import { getOperationCompanyList } from "@/apis/operation";
import useSearchParams from "@/hooks/useSearchParam";
import dayjs from "dayjs";
const initialData = ref(null);
const realTimeData = ref(null);
const realTime = ref(null);
//
const meterList = ref([]);
const selectedMeter = ref(null);
//
const floors = ref([]);
//
const companyOptions = ref([]);
const { searchParams } = useSearchParams();
let intervalId = null;
@ -34,6 +47,11 @@ const getDevice = async (option = 1) => {
option: parseInt(option),
});
realTimeData.value = res.data;
if (res.data?.meterData) {
meterList.value = (res.data?.meterData || []).sort();
} else {
meterList.value = [];
}
realTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss");
console.log("實時數據:", realTimeData.value);
} catch (err) {
@ -41,6 +59,16 @@ const getDevice = async (option = 1) => {
}
};
const getFloors = async () => {
const res = await getAssetFloorList();
floors.value = res.data[0]?.floors.map((d) => ({ ...d, key: d.floor_guid }));
};
const getCompany = async () => {
const res = await getOperationCompanyList();
companyOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
//
const startInterval = (option) => {
//
@ -80,6 +108,8 @@ watch(
onMounted(() => {
init();
getFloors();
getCompany();
});
onUnmounted(() => {
@ -99,8 +129,18 @@ onUnmounted(() => {
</div>
</div>
<Forge :fullScreen="true" :initialData="initialData" :realTime="realTime" />
<DashboardForgeOptionButton :initialData="initialData" />
<DashboardForgeOptionCard :realTimeData="realTimeData"/>
<DashboardForgeOptionButton
:initialData="initialData"
:meterList="meterList"
:selectedMeter="selectedMeter"
@update:selectedMeter="selectedMeter = $event"
/>
<DashboardForgeOptionCard
:realTimeData="realTimeData"
:selectedMeter="selectedMeter"
:floors="floors"
:companyOptions="companyOptions"
/>
<div class="w-1/4 flex flex-col justify-start border-dashboard z-20">
<div class=""><DashboardImmediateTemp /></div>
<div class="mt-5">

View File

@ -5,15 +5,18 @@ import { twMerge } from "tailwind-merge";
const props = defineProps({
initialData: Object,
meterList: Array,
selectedMeter: [String, Number, null],
});
const { changeParams, searchParams } = useSearchParams();
const emit = defineEmits(["update:selectedMeter"]);
watch(
() => props.initialData,
(newValue) => {
if (newValue?.options[0]) {
const { option, camera_position, target_position, top } = newValue.options[0];
const { option, camera_position, target_position, top } =
newValue.options[0];
changeParams({ option, camera_position, target_position, top });
}
},
@ -27,6 +30,55 @@ watch(
v-for="option in initialData.options"
:key="`option_${option.option}`"
>
<template v-if="option.option === 5 && option.visible">
<div class="dropdown inline-block">
<button
type="button"
class="btn mx-1"
:class="
parseInt(searchParams?.option) === 5
? 'btn-info'
: 'btn-outline-info'
"
@click.prevent="
() => {
changeParams({
option: option.option,
});
}
"
tabindex="0"
>
{{ option.text }}
</button>
<ul
class="dropdown-content menu bg-base-100 rounded-box z-[1] w-32 p-2 shadow"
>
<li v-for="meter in props.meterList" :key="meter.main_id">
<a
href="#"
:style="{
background:
props.selectedMeter === meter.main_id ? '#2563eb' : 'transparent',
}"
@click.prevent="
() => {
changeParams({
option: 5,
camera_position: meter.camera_position,
target_position: meter.target_position,
});
emit('update:selectedMeter', meter.main_id);
}
"
>
{{ meter.name }}
</a>
</li>
</ul>
</div>
</template>
<template v-else>
<button
v-if="option.visible"
:key="`option_${option.option}`"
@ -45,13 +97,14 @@ watch(
option: option.option,
camera_position: option.camera_position,
target_position: option.target_position,
top: option.top
top: option.top,
})
"
>
{{ option.text }}
</button>
</template>
</template>
</div>
</template>

View File

@ -15,16 +15,11 @@ import ValveCard from "./dashboardForgeCards/ValveCard.vue";
const { searchParams, changeParams } = useSearchParams();
const props = defineProps({
realTimeData: Object,
selectedMeter: String,
floors: Array,
companyOptions: Array,
});
const tabs = [
{ label: "生產資訊" },
{ label: "投料進度" },
{ label: "品檢" },
{ label: "流量計" },
{ label: "SIP" },
];
//
const productionData = computed(() => {
return props.realTimeData?.productionData || [];
@ -36,24 +31,9 @@ const heaterData = computed(() => {
return props.realTimeData?.heaterData || [];
});
// activeTab
const deviceActiveTabs = ref(new Map());
// activeTab
const getDeviceActiveTab = (deviceName, defaultTab = "生產資訊") => {
if (!deviceActiveTabs.value.has(deviceName)) {
deviceActiveTabs.value.set(deviceName, ref(defaultTab));
}
return deviceActiveTabs.value.get(deviceName);
};
//
const vesselsData = computed(() => {
const data = props.realTimeData?.productionData || [];
return data.map((vessel) => ({
...vessel,
activeTab: getDeviceActiveTab(vessel.name),
}));
return props.realTimeData?.productionData || [];
});
//
@ -63,11 +43,7 @@ const heaterPotData = computed(() => {
// 調
const cookingPotData = computed(() => {
const data = props.realTimeData?.cookingData || [];
return data.map((pot) => ({
...pot,
activeTab2: getDeviceActiveTab(pot.name),
}));
return props.realTimeData?.cookingData || [];
});
//
@ -78,7 +54,11 @@ const refrigerationData = computed(() => {
//
const meterData = computed(() => {
const data = props.realTimeData?.meterData || [];
return data.sort((a, b) => a.name.localeCompare(b.name));
// selectedMeter
const filtered = props.selectedMeter
? data.filter((meter) => meter.main_id === props.selectedMeter)
: [];
return filtered;
});
//
@ -105,7 +85,6 @@ const valveData = computed(() => {
v-for="(vessel, index) in vesselsData"
:key="index"
:vessel="vessel"
:tabs="tabs.filter(tab => tab.label !== '品檢')"
/>
</div>
</template>
@ -120,7 +99,6 @@ const valveData = computed(() => {
v-for="(pot, index) in cookingPotData"
:key="index"
:pot="pot"
:tabs="tabs"
/>
</div>
</template>
@ -137,6 +115,8 @@ const valveData = computed(() => {
v-for="(meter, index) in meterData"
:key="index"
:meter="meter"
:floors="props.floors"
:companyOptions="props.companyOptions"
/>
</div>
</template>

View File

@ -1,8 +1,16 @@
<script setup>
import { ref } from "vue";
const props = defineProps({
pot: Object,
tabs: Array,
});
const tabs = [
{ label: "生產資訊" },
{ label: "投料進度" },
{ label: "品檢" },
{ label: "流量計" },
{ label: "SIP" },
];
const activeTab = ref(tabs[0].label);
</script>
<template>
<div class="card bg-slate-200 text-accent-content rounded-md w-[25rem]">
@ -19,15 +27,15 @@ const props = defineProps({
class="tab"
:class="{
'tab-active !bg-green-500 !text-white shadow-sm shadow-slate-800':
pot.activeTab2.value === tab.label,
activeTab === tab.label,
}"
@click.prevent="pot.activeTab2.value = tab.label"
@click.prevent="activeTab = tab.label"
>
{{ tab.label }}
</a>
</div>
<div class="p-0">
<div v-if="pot.activeTab2.value === '生產資訊'">
<div v-if="activeTab === '生產資訊'">
<ul class="leading-7 tracking-wider text-slate-700 px-2">
<li><b>品名:</b> {{ pot.productInfo?.product }}</li>
<li><b>鍋次:</b> {{ pot.productInfo?.batch || "__" }}</li>
@ -36,7 +44,7 @@ const props = defineProps({
<li><b>狀態:</b> {{ pot.productInfo?.status }}</li>
</ul>
</div>
<div v-else-if="pot.activeTab2.value === '投料進度'">
<div v-else-if="activeTab === '投料進度'">
<div class="h-40 overflow-x-auto">
<table class="table table-sm table-pin-rows">
<thead>
@ -68,7 +76,7 @@ const props = defineProps({
</table>
</div>
</div>
<div v-else-if="pot.activeTab2.value === '品檢'">
<div v-else-if="activeTab === '品檢'">
<div class="h-40 overflow-x-auto">
<table class="table table-sm table-pin-rows whitespace-nowrap">
<thead>
@ -98,14 +106,17 @@ const props = defineProps({
<td>{{ flow.actual_PH }}</td>
<td>{{ flow.actual_Brix }}</td>
</tr>
<tr v-if="!pot.qcResult || !pot.qcResult.length" class="h-28 border-0">
<tr
v-if="!pot.qcResult || !pot.qcResult.length"
class="h-28 border-0"
>
<td colspan="8" class="text-center">無品檢資料</td>
</tr>
</tbody>
</table>
</div>
</div>
<div v-else-if="pot.activeTab2.value === '流量計'">
<div v-else-if="activeTab === '流量計'">
<div class="h-40 overflow-x-auto">
<table class="table table-sm">
<thead>
@ -133,7 +144,7 @@ const props = defineProps({
</table>
</div>
</div>
<div v-else-if="pot.activeTab2.value === 'SIP'">
<div v-else-if="activeTab === 'SIP'">
<div class="">
<table class="table table-sm whitespace-nowrap">
<thead>
@ -147,15 +158,13 @@ const props = defineProps({
<tr
v-for="(value, index) in pot.sip"
:key="index"
class="border-0">
class="border-0"
>
<th>{{ value.count }}</th>
<td>{{ value.startTime }}</td>
<td>{{ value.endTime }}</td>
</tr>
<tr
v-if="!pot.sip || !pot.sip.length"
class="h-28 border-0"
>
<tr v-if="!pot.sip || !pot.sip.length" class="h-28 border-0">
<td colspan="3" class="text-center">無SIP資料</td>
</tr>
</tbody>

View File

@ -1,11 +1,218 @@
<script setup>
defineProps({ meter: Object })
import { defineProps, onMounted, onUnmounted, ref, nextTick, watch } from "vue";
import { getHistoryPoints, getHistoryData } from "@/apis/history";
import { getAssetSingle } from "@/apis/asset";
import { getOperationCompanyList } from "@/apis/operation";
import LineChart from "@/components/chart/LineChart.vue";
import { SECOND_CHART_COLOR } from "@/constant";
import dayjs from "dayjs";
const props = defineProps({
meter: Object,
floors: Array,
companyOptions: Array,
});
const tabs = ["即時資訊", "設備資訊", "趨勢查詢"];
const activeTab = ref(tabs[0]);
const meterDetails = ref({});
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;
};
const getPoint = async (Device_list, Cumulant) => {
const res = await getHistoryPoints({ Device_list, Cumulant });
pointsList.value = res.data.map((d, index) => ({
...d,
name: d.item_name,
key: d.points,
}));
if (pointsList.value.length > 0) {
formState.value.Points = pointsList.value[0].points;
}
};
onMounted(() => {
getPoint(["CHY_F1_EE_E4_U1F_NA_CPM12D_N1"], 1);
if (timeList.value.length > 0) {
formState.value.time = timeList.value[0].value;
updateTimeRange(timeList.value[0].value);
}
onSearch();
});
onUnmounted(() => {
formState.value = {};
chartData.value = [];
});
watch(
() => props.meter?.main_id,
async (main_id, prev_main_id) => {
if (!main_id) {
meterDetails.value = {};
return;
}
// main_id API
if (main_id === prev_main_id) return;
const res = await getAssetSingle(main_id);
if (res.isSuccess) {
meterDetails.value = {
...res.data,
key: res.data.main_id,
floor:
props.floors.find(
({ floor_guid }) => res.data.floor_guid === floor_guid
)?.full_name || "",
company:
props.companyOptions.find(({ id }) => res.data.operation_id === id)
?.name || "",
contact_person:
props.companyOptions.find(({ id }) => res.data.operation_id === id)
?.contact_person || "",
buying_date: res.data.buying_date
? dayjs(res.data.buying_date).format("YYYY-MM-DD")
: "",
created_at: res.data.created_at
? dayjs(res.data.created_at).format("YYYY-MM-DD")
: "",
};
formState.value.Device_list = res.data.device_number || [];
}
},
{ immediate: true }
);
</script>
<template>
<div class="card bg-slate-200 text-accent-content rounded-md w-60">
<div class="card bg-slate-200 text-accent-content rounded-md w-96">
<div class="card-body p-3">
<h2 class="card-title">{{ meter.name }}</h2>
<div class="shadow-inner shadow-slate-400 rounded-md p-2">
<div
role="tablist"
class="tabs tabs-boxed tabs-sm bg-opacity-50 shadow-inner shadow-slate-600"
>
<a
v-for="tab in tabs"
:key="tab"
role="tab"
class="tab"
:class="{
'tab-active !bg-green-500 !text-white shadow-sm shadow-slate-800':
activeTab === tab,
}"
@click.prevent="activeTab = tab"
>
{{ tab }}
</a>
</div>
<div class="p-0">
<div v-if="activeTab === '即時資訊'">
<ul class="leading-7 tracking-wider text-slate-700 px-2">
<li><b>電流:</b> {{ meter.current }}</li>
<li><b>電壓:</b> {{ meter.voltage }}</li>
@ -13,6 +220,103 @@ defineProps({ meter: Object })
<li><b>用電量:</b> {{ meter.energyConsumption }}</li>
</ul>
</div>
<div v-else-if="activeTab === '設備資訊'">
<table class="table table-sm w-full text-slate-600">
<tbody>
<tr>
<td class="font-bold">設備編號</td>
<td>{{ meterDetails?.device_number || "—" }}</td>
</tr>
<tr>
<td class="font-bold">設備名稱</td>
<td>
{{ meterDetails?.full_name || meter.name || "—" }}
</td>
</tr>
<tr>
<td class="font-bold">資產編號</td>
<td>{{ meterDetails?.asset_number || "—" }}</td>
</tr>
<tr>
<td class="font-bold">設備位置</td>
<td>{{ meterDetails?.floor || "—" }}</td>
</tr>
<tr>
<td class="font-bold">圖面標識</td>
<td>{{ meterDetails?.device_coordinate || "—" }}</td>
</tr>
<tr>
<td class="font-bold">品牌 / 型號</td>
<td>
{{ meterDetails?.brand || "—" }} /
{{ meterDetails?.device_model || "—" }}
</td>
</tr>
<tr>
<td class="font-bold">廠商 / 聯絡人</td>
<td>
{{ meterDetails?.company || "—" }} /
{{ meterDetails?.contact_person || "—" }}
</td>
</tr>
<tr>
<td class="font-bold">購買日期</td>
<td>{{ meterDetails?.buying_date || "—" }}</td>
</tr>
<tr>
<td class="font-bold">建立時間</td>
<td>{{ meterDetails?.created_at || "—" }}</td>
</tr>
</tbody>
</table>
</div>
<div v-else-if="activeTab === '趨勢查詢'">
<div class="flex items-center gap-4">
<Select
:value="formState"
class=""
selectClass="select-sm text-sm text-white bg-gray-500/80 shadow-inner shadow-slate-500 border-info focus-within:border-info"
name="Points"
Attribute="name"
:options="pointsList"
></Select>
<Select
:value="formState"
class=""
selectClass="select-sm text-sm text-white bg-gray-500/80 shadow-inner shadow-slate-500 border-info focus-within:border-info"
name="time"
Attribute="name"
:options="timeList"
@change="(value) => updateTimeRange(value)"
></Select>
<button
class="btn btn-sm 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-base text-gray-500 pt-16"
v-if="!loading && chartData.length === 0"
>
沒有資料
</p>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,8 +1,15 @@
<script setup>
import { ref } from "vue";
const props = defineProps({
vessel: Object,
tabs: Array,
});
const tabs = [
{ label: "生產資訊" },
{ label: "投料進度" },
{ label: "流量計" },
{ label: "SIP" },
];
const activeTab = ref(tabs[0].label);
</script>
<template>
<div class="card bg-slate-200 text-accent-content rounded-md w-[25rem]">
@ -19,15 +26,15 @@ const props = defineProps({
class="tab"
:class="{
'tab-active !bg-green-500 !text-white shadow-sm shadow-slate-800':
vessel.activeTab.value === tab.label,
activeTab === tab.label,
}"
@click.prevent="vessel.activeTab.value = tab.label"
@click.prevent="activeTab = tab.label"
>
{{ tab.label }}
</a>
</div>
<div class="p-0">
<div v-if="vessel.activeTab.value === '生產資訊'">
<div v-if="activeTab === '生產資訊'">
<ul class="leading-7 tracking-wider text-slate-700 px-2">
<li><b>品名:</b> {{ vessel.productInfo?.product }}</li>
<li><b>鍋次:</b> {{ vessel.productInfo?.batch || "__" }}</li>
@ -35,7 +42,7 @@ const props = defineProps({
<li><b>狀態:</b> {{ vessel.productInfo?.status }}</li>
</ul>
</div>
<div v-else-if="vessel.activeTab.value === '投料進度'">
<div v-else-if="activeTab === '投料進度'">
<div class="h-40 overflow-x-auto">
<table class="table table-sm table-pin-rows">
<thead>
@ -67,7 +74,7 @@ const props = defineProps({
</table>
</div>
</div>
<div v-else-if="vessel.activeTab.value === '流量計'">
<div v-else-if="activeTab === '流量計'">
<div class="h-40 overflow-x-auto">
<table class="table table-sm table-pin-rows ">
<thead>
@ -95,7 +102,7 @@ const props = defineProps({
</table>
</div>
</div>
<div v-else-if="vessel.activeTab.value === 'SIP'">
<div v-else-if="activeTab === 'SIP'">
<div class="h-40 overflow-x-auto">
<table class="table table-sm table-pin-rows">
<thead>