預設繁體中文 | 拿掉右上告警icon | 新創賦能 : 告警初始化後全選+search | 歷史資料加最大最小值chart、table

This commit is contained in:
koko 2025-07-24 14:32:48 +08:00
parent c48072a41c
commit 6e61679c1f
33 changed files with 1233 additions and 119 deletions

View File

@ -1,3 +1,5 @@
export const GET_SYSTEM_FLOOR_LIST_API = `/api/Device/GetFloor`;
export const GET_SYSTEM_DEVICE_LIST_API = `/api/Device/GetDeviceList`;
export const GET_SYSTEM_REALTIME_API = `/api/Device/GetRealTimeData`;
export const GET_SYSTEM_CONFIG_API = `/api/GetSystemConfig`;

View File

@ -2,6 +2,7 @@ import {
GET_SYSTEM_FLOOR_LIST_API,
GET_SYSTEM_DEVICE_LIST_API,
GET_SYSTEM_REALTIME_API,
GET_SYSTEM_CONFIG_API
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
@ -36,3 +37,11 @@ export const getSystemRealTime = async (device_list) => {
code: res.code,
});
};
export const getSystemConfig = async (building_guid) => {
const res = await instance.post(GET_SYSTEM_CONFIG_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -121,9 +121,9 @@ watch(locale, () => {
<li>
<NavbarLang />
</li>
<li>
<!-- <li>
<AlarmDrawer />
</li>
</li> -->
<li>
<NavbarUser />
</li>

View File

@ -6,7 +6,7 @@ const store = useBuildingStore();
const selectBuilding = (bui) => {
store.selectedBuilding = bui; // selectedBuildingwatch
localStorage.setItem("CviBuilding", JSON.stringify(bui));
localStorage.setItem("EmpowerBuilding", JSON.stringify(bui));
};
onMounted(() => {

View File

@ -6,7 +6,7 @@ const { locale } = useI18n(); // 使用 I18n
//
const toggleLanguage = (lang) => {
locale.value = lang;
localStorage.setItem("CviLanguage", lang);
localStorage.setItem("EmpowerLanguage", lang);
};
</script>

View File

@ -1,9 +1,16 @@
<script setup>
import useUserInfoStore from "@/stores/useUserInfoStore";
import { ref, onMounted } from "vue";
import { useRouter } from 'vue-router';
const router = useRouter();
const store = useUserInfoStore();
const user = ref("");
function logout() {
router.push('/logout');
}
onMounted(() => {
const name = store.user.user_name;
if (name) {
@ -30,12 +37,12 @@ onMounted(() => {
class="dropdown-content translate-y-2 z-[100] menu py-3 shadow rounded w-32 bg-[#4c625e] border text-center"
>
<li class="text-white">
<router-link
to="logout"
type="link"
<a
type="button"
class="flex flex-col justify-center items-center"
@click="logout"
>{{ $t("sign_out") }}
</router-link>
</a>
</li>
</ul>
</div>

View File

@ -30,12 +30,12 @@ const messages = {
us,
};
const storedLanguage = localStorage.getItem("CviLanguage") || "us";
const storedLanguage = localStorage.getItem("EmpowerLanguage") || "tw";
const i18n = createI18n({
legacy: false,
locale: storedLanguage,
fallbackLocale: 'us',
fallbackLocale: 'tw',
messages,
});
const app = createApp(App);

View File

@ -106,6 +106,7 @@ router.beforeEach(async (to, from, next) => {
document.cookie = "user_name=; Max-Age=0";
auth.user.token = "";
auth.user.user_name = "";
localStorage.removeItem("EmpowerBuilding");
window.location.reload();
next({ path: "/login" });
}

View File

@ -3,6 +3,7 @@ import { ref, computed, watch } from "vue";
import { useRoute } from "vue-router";
import { getBuildings, getAllSysSidebar } from "@/apis/building";
import { getAssetFloorList, getDepartmentList } from "@/apis/asset";
import { getSystemConfig } from "@/apis/system";
const useBuildingStore = defineStore("buildingInfo", () => {
// 狀態定義
@ -11,6 +12,7 @@ const useBuildingStore = defineStore("buildingInfo", () => {
const floorList = ref([]);
const deptList = ref([]);
const mainSubSys = ref([]);
const sysConfig = ref([]);
// 計算屬性
const mainSys = computed(() =>
@ -47,9 +49,10 @@ const useBuildingStore = defineStore("buildingInfo", () => {
const fetchBuildings = async () => {
const res = await getBuildings();
buildings.value = res.data;
if (res.data.length > 0 && !selectedBuilding.value) {
const storedBuilding = JSON.parse(localStorage.getItem("CviBuilding"));
selectedBuilding.value = storedBuilding || res.data[0]; // 預設選第一個建築
if (res.data.length > 0 ) {
selectedBuilding.value = res.data[0]; // 預設選第一個建築
}else{
selectedBuilding.value = null; // 如果沒有建築物則設為null
}
};
@ -81,13 +84,21 @@ const useBuildingStore = defineStore("buildingInfo", () => {
mainSubSys.value = res.data.history_Main_Systems;
};
// 取得系統設定
const getSysConfig = async (building_guid) => {
const res = await getSystemConfig(building_guid);
sysConfig.value = res.data;
};
// 當 selectedBuilding 改變時,更新 floorList 和 deptList 和 mainSubSys
watch(selectedBuilding, async (newBuilding) => {
if (newBuilding) {
localStorage.setItem("EmpowerBuilding", JSON.stringify(newBuilding));
await Promise.all([
fetchFloorList(newBuilding.building_guid),
fetchDepartmentList(),
getSubMonitorPage(newBuilding.building_guid)
getSubMonitorPage(newBuilding.building_guid),
getSysConfig(newBuilding.building_guid),
]);
}
});
@ -106,9 +117,11 @@ const useBuildingStore = defineStore("buildingInfo", () => {
mainSys,
subSys,
selectedSystem,
sysConfig,
fetchBuildings,
fetchFloorList,
fetchDepartmentList,
getSubMonitorPage,
initialize,
};
});

View File

@ -12,11 +12,23 @@ const instance = axios.create({
instance.interceptors.request.use(
function (config) {
// Do something before request is sent
const token = useGetCookie("JWT-Authorization");
config.headers = {
Authorization: `Bearer ${token}`,
};
return config;
const token = useGetCookie("JWT-Authorization");
// 取得 building_guid 並加到 headers
let buildingGuid = "";
try {
const cviBuilding = localStorage.getItem("EmpowerBuilding");
if (cviBuilding) {
const parsed = JSON.parse(cviBuilding);
buildingGuid = parsed.building_guid || "";
}
} catch (e) {
buildingGuid = "";
}
config.headers = {
Authorization: `Bearer ${token}`,
"X-Building-GUID": buildingGuid,
};
return config;
},
function (error) {
// Do something with request error
@ -55,11 +67,23 @@ export const fileInstance = axios.create({
fileInstance.interceptors.request.use(
function (config) {
// Do something before request is sent
const token = useGetCookie("JWT-Authorization");
config.headers = {
Authorization: `Bearer ${token}`,
};
return config;
const token = useGetCookie("JWT-Authorization");
// 取得 building_guid 並加到 headers
let buildingGuid = "";
try {
const cviBuilding = localStorage.getItem("EmpowerBuilding");
if (cviBuilding) {
const parsed = JSON.parse(cviBuilding);
buildingGuid = parsed.building_guid || "";
}
} catch (e) {
buildingGuid = "";
}
config.headers = {
Authorization: `Bearer ${token}`,
"X-Building-GUID": buildingGuid,
};
return config;
},
function (error) {
// Do something with request error

View File

@ -51,6 +51,7 @@ const formState = ref({
system_value: "",
system_parent_id: 0,
file: [],
device_image: "",
});
@ -68,6 +69,7 @@ const openModal = (item) => {
}
: {};
formState.value.file = [subFile];
formState.value.device_image = item.device_image;
}
} else {
formState.value = {
@ -76,6 +78,7 @@ const openModal = (item) => {
system_value: "",
system_parent_id: 0,
file: [],
device_image: "",
};
}
asset_add_sub_item.showModal();

View File

@ -20,6 +20,7 @@ const mainSystem = ref([]);
const updateFileList = (files) => {
console.log("file", files);
props.formState.file = files;
props.formState.device_image = files[0]?.name;
};
const getMainSystems = async () => {
@ -44,6 +45,7 @@ const onCancel = () => {
system_value: "",
system_parent_id: 0,
file: [],
device_image: "",
};
asset_add_sub_item.close();
updateFileList([]);
@ -71,16 +73,15 @@ const onOk = async () => {
if (props.formState.file[0]) {
formData.append("file", props.formState.file[0]);
}
if (props.formState.Device_image) {
formData.append("Device_image", props.formState.Device_image);
formData.append("device_image", props.formState.device_image);
}
// const res = await postAssetSubList(formData);
// if (res.isSuccess) {
// props.getData(parseInt(searchParams.value.mainSys_id));
// onCancel();
// }
const res = await postAssetSubList(formData);
if (res.isSuccess) {
props.getData(parseInt(searchParams.value.mainSys_id));
store.getSubMonitorPage(store.selectedBuilding.building_guid);
onCancel();
}
};
</script>
@ -101,7 +102,7 @@ const onOk = async () => {
>
<template #modalContent>
<form ref="form" class="mt-5 flex flex-col items-center">
<Input name="system_key" :value="formState" >
<Input name="system_key" :value="formState">
<template #topLeft>{{ $t("assetManagement.system_name") }}</template>
<template #bottomLeft
><span class="text-error text-base">

View File

@ -2,7 +2,7 @@
import AlertSearch from "./AlertSearch.vue";
import AlertTable from "./AlertTable.vue";
import AlertTableModal from "./AlertTableModal.vue";
import { ref, provide, onMounted } from "vue";
import { ref, provide, onMounted, watch } from "vue";
import useSearchParam from "@/hooks/useSearchParam";
import { getAlertLog } from "@/apis/alert";
import {
@ -15,7 +15,7 @@ import useBuildingStore from "@/stores/useBuildingStore";
const { searchParams } = useSearchParam();
const store = useBuildingStore();
const hasSearched = ref(false);
const tableLoading = ref(false);
const dataSource = ref([]);
const model_data = ref({
@ -82,7 +82,7 @@ const openModal = async (record) => {
updateEditRecord({
...res.data,
uuid: res.data.error_code,
device_number: record.device_number
device_number: record.device_number,
});
} else {
updateEditRecord({
@ -98,11 +98,31 @@ const openModal = async (record) => {
onMounted(() => {
getAllOptions();
if (Object.keys(searchParams.value).length !== 0) {
search();
}
});
watch(
() => ({
isRecovery: searchParams.value.isRecovery,
Start_date: searchParams.value.Start_date,
End_date: searchParams.value.End_date,
device_name_tag: searchParams.value.device_name_tag,
}),
(val) => {
//
if (
val.isRecovery !== undefined &&
val.Start_date &&
val.End_date &&
val.device_name_tag &&
!hasSearched.value
) {
hasSearched.value = true;
search();
}
},
{ immediate: true, deep: true }
);
provide("alert_modal", { model_data, search, updateEditRecord });
provide("alert_table", {
openModal,

View File

@ -26,10 +26,6 @@ onMounted(() => {
initializeItems();
});
watch(locale, () => {
initializeItems();
});
//
watch(
selectedBtn,

View File

@ -12,7 +12,7 @@ const changeCheckedItem = () => {
if (checkedItem.value.length === store.subSys.length) {
changeParams({
...searchParams.value,
device_name_tag: [],
device_name_tag: [store.subSys[0]?.sub_system_tag],
});
} else {
changeParams({
@ -50,11 +50,12 @@ const checkedItem = computed(() =>
: []
);
//
watch(searchParams, (newValue) => {
if (!newValue.device_name_tag) {
changeParams({
...newValue,
device_name_tag: [store.subSys[0]?.sub_system_tag],
device_name_tag: store.subSys.map(({ sub_system_tag }) => sub_system_tag),
});
}
});

View File

@ -2,7 +2,10 @@
import DashboardFloorBar from "./components/DashboardFloorBar.vue";
import DashboardEffectScatter from "./components/DashboardEffectScatter.vue";
import DashboardSysCard from "./components/DashboardSysCard.vue";
import DashboardProduct from "./components/DashboardProduct.vue";
import DashboardProductComplete from "./components/DashboardProductComplete.vue";
import DashboardTemp from "./components/DashboardTemp.vue";
import DashboardHumidity from "./components/DashboardHumidity.vue";
import DashboardRefrigTemp from "./components/DashboardRefrigTemp.vue";
import DashboardIndoorTemp from "./components/DashboardIndoorTemp.vue";
import DashboardElectricity from "./components/DashboardElectricity.vue";
@ -111,10 +114,16 @@ onUnmounted(() => {
class="order-3 lg:order-1 w-full lg:w-1/4 h-full flex flex-col justify-start z-10 border-dashboard px-12"
>
<div>
<DashboardProduct />
</div>
<div class="mt-6">
<DashboardProductComplete />
</div>
<div class="mt-6">
<DashboardTemp />
</div>
<div class="mt-10">
<DashboardAlert />
<DashboardHumidity />
</div>
</div>
<div
@ -135,6 +144,9 @@ onUnmounted(() => {
<div class="mt-10">
<DashboardEmission />
</div>
<div class="mt-10">
<DashboardAlert />
</div>
</div>
</div>
</template>

View File

@ -9,12 +9,16 @@ let intervalId = null; // 用來儲存 setInterval 的 ID
const getAlarmData = async (building_guid) => {
const res = await getAlertLogList(building_guid);
dataSource.value = (res.data || []);
dataSource.value = res.data || [];
};
watch(
() => store.selectedBuilding,
(newBuilding) => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null;
}
if (newBuilding) {
getAlarmData(newBuilding.building_guid);
@ -38,7 +42,7 @@ onUnmounted(() => {
<h3 class="text-info text-xl text-center">
{{ $t("dashboard.alerts_data") }} Top 5
</h3>
<div class="overflow-x-auto">
<div class="overflow-x-auto min-h-[300px]">
<table class="table">
<thead>
<tr class="border-b-2 border-info text-base">

View File

@ -41,7 +41,7 @@ const defaultOption = (map, data = []) => {
},
map,
roam: true, //
layoutSize: window.innerWidth <= 768 ? "110%" : "100%",
layoutSize: window.innerWidth <= 768 ? "110%" : "80%",
layoutCenter: ["50%", "50%"],
scaleLimit: { min: 1, max: 2 },
},

View File

@ -72,7 +72,7 @@ const onCancel = () => {
<template>
<button
class="btn btn-sm btn-success absolute top-0 right-0"
class="btn btn-xs btn-success absolute top-0 right-0"
@click.stop.prevent="openModal"
>
{{ $t("button.edit") }}

View File

@ -67,7 +67,7 @@ const closeModal = () => {
</script>
<template>
<button class="btn btn-sm btn-success absolute top-0 right-0" @click.stop.prevent="openModal">
<button class="btn btn-xs btn-success absolute top-0 right-0" @click.stop.prevent="openModal">
{{ $t("button.edit") }}
</button>
<Modal

View File

@ -0,0 +1,213 @@
<script setup>
import LineChart from "@/components/chart/LineChart.vue";
import { SECOND_CHART_COLOR } from "@/constant";
import dayjs from "dayjs";
import { ref, watch, onUnmounted } from "vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { getDashboardTemp } from "@/apis/dashboard";
import useSearchParams from "@/hooks/useSearchParam";
import useBuildingStore from "@/stores/useBuildingStore";
const { searchParams } = useSearchParams();
const buildingStore = useBuildingStore();
const intervalType = "immediateTemp";
const timeoutTimer = ref("");
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const data = ref([]);
const other_real_temp_chart = ref(null);
const defaultChartOption = ref({
tooltip: {
trigger: "axis",
},
legend: {
data: [],
textStyle: {
color: "#ffffff",
fontSize: 16,
},
},
grid: {
top: "10%",
left: "0%",
right: "0%",
bottom: "0%",
containLabel: true,
},
xAxis: {
// type: 'time',
type: "category",
splitLine: {
show: false,
},
axisLabel: {
color: "#ffffff",
},
data: [],
},
yAxis: {
type: "value",
splitLine: {
show: false,
},
axisLabel: {
color: "#ffffff",
},
},
series: [],
});
const getData = async (tempOption) => {
const res = await getDashboardTemp({
building_guid: buildingStore.selectedBuilding.building_guid,
tempOption, // 1 2:
timeInterval: 1, // =>1.4.8
});
if (res.isSuccess) {
if (tempOption === 1) {
console.log("室內溫度資料:", res.data["室溫"]);
data.value = res.data["室溫"] || [];
} else {
console.log("冷藏溫度資料:", res.data["冷藏溫度"]);
data.value = res.data["冷藏溫度"] || [];
}
}
};
//
watch(
() => buildingStore.sysConfig,
(newValue) => {
if (newValue) {
// sysConfig
const itemsArr = [];
if (buildingStore.sysConfig?.show_room) {
itemsArr.push({
title: "室內溫度",
key: 1,
active: false,
});
}
if (buildingStore.sysConfig?.show_refrigeration) {
itemsArr.push({
title: "冷藏溫度",
key: 2,
active: false,
});
}
if (itemsArr.length > 0) {
itemsArr[0].active = true;
// getData(itemsArr[0].key);
// timeoutTimer.value = setInterval(() => {
// getData(itemsArr[0].key);
// }, 60 * 1000);
}
setItems(itemsArr);
} else {
//
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
}
},
{
immediate: true,
}
);
watch(
selectedBtn,
(newValue) => {
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
if (newValue?.key) {
getData(newValue.key);
//
timeoutTimer.value = setInterval(() => {
getData(newValue.key);
}, 60 * 1000);
}
},
{
immediate: true,
deep: true,
}
);
watch(
data,
(newValue) => {
if (newValue?.length > 0 && other_real_temp_chart.value?.chart) {
const firstItem = newValue[0];
if (firstItem?.data?.length > 0) {
const validData = firstItem.data.filter(
(item) => item.value !== null && item.value !== undefined
);
if (validData.length > 0) {
const minValue = Math.min(...validData.map(({ value }) => value));
const maxValue = Math.max(...validData.map(({ value }) => value));
other_real_temp_chart.value.chart.setOption({
legend: {
data: newValue.map(({ full_name }) => full_name),
},
xAxis: {
data: firstItem.data.map(({ time }) =>
dayjs(time).format("HH:mm:ss")
),
},
yAxis: {
min: Math.floor(minValue),
max: Math.ceil(maxValue),
},
series: newValue.map(({ full_name, data }, index) => ({
name: full_name,
type: "line",
data: data.map(({ value }) => value),
showSymbol: false,
itemStyle: {
color: SECOND_CHART_COLOR[index % SECOND_CHART_COLOR.length],
},
})),
});
}
}
}
},
{ deep: true }
);
onUnmounted(() => {
//
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
});
</script>
<template>
<h3 class="text-info text-xl text-center">濕度趨勢</h3>
<div className="my-3 w-full flex justify-center relative">
<ButtonConnectedGroup
:items="items"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
</div>
<LineChart
id="dashboard_other_real_temp"
class="min-h-[260px] max-h-fit"
:option="defaultChartOption"
ref="other_real_temp_chart"
/>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,153 @@
<script setup>
import { ref, onMounted } from "vue";
import GaugeChart from "@/components/chart/GaugeChart.vue";
import { CHART_COLOR, SECOND_CHART_COLOR } from "@/constant";
const sameOptionAttr = {
type: "gauge",
center: ["50%", "60%"],
startAngle: 90,
endAngle: -270,
axisTick: {
show: false,
},
splitLine: {
show: false,
},
axisLabel: {
show: false,
},
anchor: {
show: false,
},
title: {
show: false,
},
};
const defaultChartOption = (text, lightColor, darkColor, value = 0) => ({
title: {
text,
left: "center",
textStyle: {
color: "#ffffff",
fontSize: 16,
fontWeight: "normal",
},
top: "0%",
},
series: [
{
...sameOptionAttr,
min: 0,
max: 100,
itemStyle: {
color: lightColor,
},
progress: {
show: true,
width: 10,
},
axisLine: {
lineStyle: {
width: 10,
},
},
pointer: {
itemStyle: {
color: darkColor,
},
},
detail: {
valueAnimation: true,
formatter: "{value} %",
fontSize: 16,
color: "#ffffff",
},
data: [
{
value,
},
],
},
{
...sameOptionAttr,
min: 0,
max: 100,
itemStyle: {
color: darkColor,
},
progress: {
show: true,
width: 4,
},
axisLine: {
show: false,
},
pointer: {
show: false,
},
detail: {
show: false,
},
data: [
{
value,
},
],
},
],
});
const mesona_chart = ref(null);
const aiyu_chart = ref(null);
const blackTea_chart = ref(null);
const chart_data = ref([
defaultChartOption("產品 A", CHART_COLOR[0], SECOND_CHART_COLOR[0], 75),
defaultChartOption("產品 B", CHART_COLOR[1], SECOND_CHART_COLOR[1], 60),
defaultChartOption("產品 C", CHART_COLOR[2], SECOND_CHART_COLOR[2], 90),
]);
onMounted(() => {
//
// setTimeout(() => {
// chart_data.value = [
// defaultChartOption("", CHART_COLOR[0], SECOND_CHART_COLOR[0], 80),
// defaultChartOption("", CHART_COLOR[1], SECOND_CHART_COLOR[1], 70),
// defaultChartOption("", CHART_COLOR[2], SECOND_CHART_COLOR[2], 95),
// ];
// }, 2000);
});
</script>
<template>
<div>
<h3 class="text-info text-xl text-center mb-3">生產量</h3>
<div class="w-full grid grid-cols-3">
<div>
<GaugeChart
id="dashboard_mesona_production"
class="h-32"
:option="chart_data[0]"
ref="mesona_chart"
/>
</div>
<div>
<GaugeChart
id="dashboard_aiyu_production"
class="h-32"
:option="chart_data[1]"
ref="aiyu_chart"
/>
</div>
<div>
<GaugeChart
id="dashboard_blackTea_production"
class="h-32"
:option="chart_data[2]"
ref="blackTea_chart"
/>
</div>
</div>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,95 @@
<script setup>
import useActiveBtn from "@/hooks/useActiveBtn";
import { ref, onMounted } from "vue";
import DashboardProductCompleteModal from "./DashboardProductCompleteModal.vue";
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
//
const progress_data = [
{
data: [
{ pac: "130g", value: 80, target: 100, percentage: 80 },
{ pac: "150g", value: 70, target: 100, percentage: 70 },
{ pac: "1kg", value: 75, target: 100, percentage: 75 },
],
},
{
data: [
{ pac: "500g", value: 70, target: 100, percentage: 70 },
{ pac: "1kg", value: 60, target: 100, percentage: 60 },
{ pac: "2kg", value: 50, target: 100, percentage: 50 },
],
},
{
data: [
{ pac: "300g", value: 90, target: 100, percentage: 90 },
{ pac: "500g", value: 96, target: 100, percentage: 96 },
{ pac: "1kg", value: 84, target: 100, percentage: 84 },
],
},
];
const openModal = () => {
dashboard_product.showModal();
};
onMounted(() => {
//
setItems([
{ title: "產品 A", key: 1, active: true },
{ title: "產品 B", key: 2, active: false },
{ title: "產品 C", key: 3, active: false },
]);
});
</script>
<template>
<DashboardProductCompleteModal/>
<div class="mb-3 relative">
<h3 class="text-info text-xl text-center">今日生產完成率 </h3>
<button
type="button"
class="btn btn-xs btn-success absolute top-0 right-0"
@click.stop="openModal"
>
設定
</button>
</div>
<div className="my-3 w-full flex justify-center relative">
<ButtonConnectedGroup
:items="items"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
</div>
<template
v-if="progress_data && selectedBtn && progress_data[selectedBtn.key - 1]"
>
<div
class="grid grid-cols-6 gap-3 justify-items-end items-center"
v-for="{ pac, value, target, percentage } in progress_data[
selectedBtn.key - 1
].data"
:key="pac"
>
<span class="col-span-1 text-lg">{{ pac }}</span>
<progress
v-if="target !== 0"
class="progress progress-info col-span-4"
:value="value"
:max="target"
></progress>
<span class="col-span-1 text-lg justify-self-start"
>{{ percentage }} %</span
>
</div>
</template>
<template v-else>
<p class="text-center mt-8 text-lg">尚未設定目標值</p>
</template>
</template>
<style lang="css" scoped></style>

View File

@ -0,0 +1,104 @@
<script setup>
import { ref, inject, onBeforeMount, defineProps, watch, computed } from "vue";
import DashboardProductCompleteModalTarget from "./DashboardProductCompleteModalTarget.vue";
import DashboardProductCompleteModalRecord from "./DashboardProductCompleteModalRecord.vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import dayjs from "dayjs";
import { postDashboardProductTarget } from "@/apis/dashboard";
const { items, changeActiveBtn, setItems } = useActiveBtn();
const date = ref([]);
const formState = ref({});
const selectedBtn = ref({});
const onOk = async () => {
const res = await postDashboardProductTarget({
date: dayjs(date.value[0].value).format("YYYY-MM-DD"),
type: selectedBtn.value?.typeOption || 1,
data: formState.value,
});
if (res.isSuccess) {
// getCompletion();
onCancel();
}
};
const onCancel = () => {
dashboard_product.close();
};
const changeComponent = (e, item) => {
changeActiveBtn(item);
};
onBeforeMount(() => {
setItems([
{
title: "目標設定",
key: "Target",
active: true,
component: DashboardProductCompleteModalTarget,
},
{
title: "執行紀錄",
key: "Record",
active: false,
component: DashboardProductCompleteModalRecord,
},
]);
});
const activeTab = computed(() => {
return items.value.find(({ active }) => active);
});
</script>
<template>
<Modal
id="dashboard_product"
title="顯示設定"
:onCancel="onCancel"
width="1000"
modalClass="bg-body"
>
<template #modalContent>
<ButtonGroup
:items="items"
:withLine="true"
class="mt-8 mb-6"
:onclick="changeComponent"
/>
<component :is="activeTab.component"></component>
</template>
<template #modalAction>
<div v-if="activeTab.key === 'Target'">
<button
type="reset"
class="btn btn-outline-success mr-2"
@click.prevent="onCancel"
>
取消
</button>
<button
type="submit"
class="btn btn-outline-success"
@click.stop.prevent="onOk"
>
確定
</button>
</div>
<div v-else="activeTab.key === 'Record'">
<button
type="reset"
class="btn btn-outline-success mr-2"
@click.prevent="onCancel"
>
取消
</button>
</div>
</template>
</Modal>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,125 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import dayjs from "dayjs";
import useActiveBtn from "@/hooks/useActiveBtn";
import {
getDashboardProductRecord,
} from "@/apis/dashboard";
const tableLoading = ref(false);
const dataSource = ref([]);
const {
items: dateRange,
setItems: setDateItems,
} = useActiveBtn();
const columns = [
{
key: "date",
title: "日期",
},
{
key: "full_name",
title: "項目",
width: 80,
},
{
key: "device_value",
title: "實際生產量(L)",
},
{
key: "target_value",
title: "目標生產量(L)",
},
{
key: "f2_value",
title: "二廠實際包裝數",
},
{
key: "f3_value",
title: "三廠實際包裝數",
},
{
key: "target",
title: "目標包裝數",
},
];
const submitBtns = computed(() => [
{
title: "查詢",
key: "search",
active: true,
onClick: getRecord,
disabled: tableLoading.value,
},
]);
const getRecord = async () => {
tableLoading.value = true;
try {
const res = await getDashboardProductRecord({
start_time: dayjs(dateRange.value[0].value).format("YYYY-MM-DD"),
end_time: dayjs(dateRange.value[1].value).format("YYYY-MM-DD"),
});
dataSource.value = res.data;
} catch (error) {
console.error("Error fetching data:", error);
} finally {
tableLoading.value = false;
}
};
onMounted(() => {
setDateItems([
{
key: "start_time",
value: dayjs(),
dateFormat: "yyyy-MM-dd",
placeholder: "起始日期",
},
{
key: "end_time",
value: dayjs(),
dateFormat: "yyyy-MM-dd",
placeholder: "結束時間",
},
]);
});
</script>
<template>
<div class="flex flex-row items-end">
<DateGroup :items="dateRange" :withLine="true" width="500">
<template #topLeft>日期</template>
</DateGroup>
<ButtonGroup :items="submitBtns" :withLine="false" class="ml-8 mr-8 mb-4" />
</div>
<Table :loading="tableLoading" :columns="columns" :dataSource="dataSource">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'f2_value'">
<div v-for="(item, index) in record.data" :key="index">
<p class="text-start">{{ item.pac }} : {{ item.f2_value }}</p>
</div>
</template>
<template v-else-if="column.key === 'f3_value'">
<div v-for="(item, index) in record.data" :key="index">
<p class="text-start">{{ item.pac }} : {{ item.f3_value }}</p>
</div>
</template>
<template v-else-if="column.key === 'target'">
<div v-for="(item, index) in record.data" :key="index">
<p class="text-start">{{ item.pac }} : {{ item.target }}</p>
</div>
</template>
<template v-else>
{{ record[column.key] }}
</template>
</template>
</Table>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,74 @@
<script setup>
import { ref, inject, onMounted, defineProps, watch, watchEffect } from "vue";
import dayjs from "dayjs";
import useActiveBtn from "@/hooks/useActiveBtn";
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const formState = ref({
pot: 0,
"130g": 0,
"150g": 0,
"1kg": 0,
"2kg": 0,
});
const date = ref([
{
key: "date",
value: dayjs(),
dateFormat: "yyyy-MM-dd",
placeholder: "日期設定",
},
]);
onMounted(() => {
setItems([
{ title: "產品 A", key: 1, active: true },
{ title: "產品 B", key: 2, active: false },
{ title: "產品 C", key: 3, active: false },
]);
});
</script>
<template>
<DateGroup :isBottomLabelExist="false" :items="date" :withLine="false">
<template #topLeft>日期</template>
</DateGroup>
<ButtonConnectedGroup
:items="items"
:className="`w-full mt-8 mb-4`"
size="md"
color="info"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
<div class="flex flex-col items-start">
<div class="flex justify-between">
<InputNumber :value="formState" width="150" class="mr-4" name="pot">
<template #topLeft>總數</template>
</InputNumber>
</div>
</div>
<div class="flex flex-col items-start">
<p class="text-base">包裝類型</p>
<div class="flex justify-between">
<InputNumber
v-for="size in ['130g', '150g', '1kg', '2kg']"
:key="size"
:value="formState"
width="150"
class="mr-4"
:name="size"
>
<template #topLeft>{{ size }}</template>
</InputNumber>
</div>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -77,32 +77,40 @@ const getData = async (tempOption) => {
//
watch(
() => buildingStore.selectedBuilding?.building_guid,
(newBuildingGuid) => {
if (newBuildingGuid) {
getData(1);
timeoutTimer.value = setInterval(() => {
getData(1);
}, 60 * 1000);
() => buildingStore.sysConfig,
(newValue) => {
if (newValue) {
// sysConfig
const itemsArr = [];
if (buildingStore.sysConfig?.show_room) {
itemsArr.push({
title: "室內溫度",
key: 1,
active: false,
});
}
if (buildingStore.sysConfig?.show_refrigeration) {
itemsArr.push({
title: "冷藏溫度",
key: 2,
active: false,
});
}
if (itemsArr.length > 0) {
itemsArr[0].active = true;
// getData(itemsArr[0].key);
// timeoutTimer.value = setInterval(() => {
// getData(itemsArr[0].key);
// }, 60 * 1000);
}
setItems(itemsArr);
} else {
//
if (timeoutTimer.value) {
clearInterval(timeoutTimer.value);
}
}
setItems([
{
title: "室內溫度",
key: 1,
active: true,
},
{
title: "冷藏溫度",
key: 2,
active: false,
},
]);
},
{
immediate: true,
@ -125,6 +133,7 @@ watch(
}
},
{
immediate: true,
deep: true,
}
);
@ -135,7 +144,9 @@ watch(
if (newValue?.length > 0 && other_real_temp_chart.value?.chart) {
const firstItem = newValue[0];
if (firstItem?.data?.length > 0) {
const validData = firstItem.data.filter((item) => item.value !== null && item.value !== undefined);
const validData = firstItem.data.filter(
(item) => item.value !== null && item.value !== undefined
);
if (validData.length > 0) {
const minValue = Math.min(...validData.map(({ value }) => value));
@ -146,7 +157,7 @@ watch(
data: newValue.map(({ full_name }) => full_name),
},
xAxis: {
data: firstItem.data.map(({ time }) => time), // 使 time
data: firstItem.data.map(({ time }) => dayjs(time).format("HH:mm:ss")),
},
yAxis: {
min: Math.floor(minValue),
@ -178,23 +189,20 @@ onUnmounted(() => {
</script>
<template>
<div class="flex flex-col justify-center mb-3">
<h3 class="text-info font-bold text-xl text-center">溫度趨勢</h3>
<div className="mt-2 w-full flex justify-center relative">
<ButtonConnectedGroup
:items="items"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
</div>
<h3 class="text-info text-xl text-center">溫度趨勢</h3>
<div className="my-3 w-full flex justify-center relative">
<ButtonConnectedGroup
:items="items"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
</div>
<LineChart
id="dashboard_other_real_temp"
class="min-h-[350px] max-h-fit"
class="min-h-[260px] max-h-fit"
:option="defaultChartOption"
ref="other_real_temp_chart"
/>

View File

@ -24,15 +24,16 @@ watch(
item.costPeak + item.costHalfPeak + item.costOffPeak + item.costBase;
return sum + monthlyBill;
}, 0)
.toFixed(2);
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
totalElecDegree.value = newData
.reduce((sum, item) => {
const monthlyBill =
item.degreePeak + item.degreeHalfPeak + item.degreeOffPeak;
return sum + monthlyBill;
}, 0)
.toFixed(2);
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
//
const latestData = newData[newData.length - 1];
@ -41,12 +42,16 @@ watch(
latestData.costHalfPeak +
latestData.costOffPeak +
latestData.costBase
).toFixed(2);
)
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
IntervalElecDegree.value = (
latestData.degreePeak +
latestData.degreeHalfPeak +
latestData.degreeOffPeak
).toFixed(2);
)
.toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
const monthDays = latestData.time ? daysInMonth(latestData.time) : 0;
IntervalDate.value = latestData.time
? `${latestData.time}-01~${latestData.time}-${monthDays}`

View File

@ -8,28 +8,28 @@ import { ref, provide } from "vue";
const tableData = ref([]);
const loading = ref(false);
const updateTableData = (data) => {
tableData.value = data
? data.items.map((d) => ({
...d,
key: `${d.device_number}_${d.points}_${dayjs(d.timestamp).format("x")}`,
}))
: [];
tableData.value = data ? data : [];
};
const updateLoading = () => {
loading.value = !loading.value;
};
provide("history_table_data", { tableData, updateTableData, loading, updateLoading });
provide("history_table_data", {
tableData,
updateTableData,
loading,
updateLoading,
});
</script>
<template>
<h1 class="text-2xl font-extrabold mb-2">{{ $t('history.title') }}</h1>
<h1 class="text-2xl font-extrabold mb-2">{{ $t("history.title") }}</h1>
<div class="grid grid-cols-10 gap-2">
<div class="col-span-10 lg:col-span-2 ">
<div class="col-span-10 lg:col-span-2">
<HistorySidebar />
</div>
<div class="col-span-10 lg:col-span-8 ">
<div class="col-span-10 lg:col-span-8">
<HistorySearch />
<HistoryTable />
</div>

View File

@ -8,7 +8,6 @@ const { searchParams } = useSearchParam();
const props = defineProps({
form: Object,
selectedType: String,
});
const { updateTableData, updateLoading } = inject("history_table_data");
@ -53,6 +52,7 @@ const submit = async (e, type = "") => {
updateLoading();
const res = await getHistoryData({
type: searchParams.value.selectedType,
table_type:1,
...params,
...searchParams.value,
});
@ -118,13 +118,18 @@ watch(
</script>
<template>
<Toast
<Toast
:content="isToastOpen.content"
:open="isToastOpen.open"
status="info"
:cancel="cancelToastOpen"
/>
<ButtonGroup class="lg:ml-5" :items="submitBtns" :withLine="false" :withBtnClass="true"/>
<ButtonGroup
class="lg:ml-5"
:items="submitBtns"
:withLine="false"
:withBtnClass="true"
/>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,164 @@
<script setup>
import { inject, computed, ref, watch } from "vue";
import { useI18n } from "vue-i18n";
import LineChart from "@/components/chart/LineChart.vue";
import { SECOND_CHART_COLOR } from "@/constant";
import useSearchParam from "@/hooks/useSearchParam";
import dayjs from "dayjs";
const { searchParams } = useSearchParam();
const { t } = useI18n();
const { tableData } = inject("history_table_data");
const history_chart = ref(null);
//
const defaultChartOption = {
tooltip: {
trigger: "axis",
},
legend: {
data: [],
textStyle: {
color: "#ffffff",
fontSize: 16,
},
},
grid: {
top: "25%",
left: "0%",
right: "0%",
bottom: "0%",
containLabel: true,
},
toolbox: {
show: true,
feature: {
saveAsImage: {
type: "png",
name: "Energy_management_chart",
backgroundColor: "rgba(29, 36, 44, 1)",
iconStyle: {
borderColor: "rgba(53,237,237, 1)",
},
},
},
},
xAxis: {
type: "category",
splitLine: { show: false },
axisLabel: {
color: "#ffffff",
formatter: (value) =>
searchParams.value.Type == 2
? dayjs(value).format("HH:mm")
: dayjs(value).format("MM-DD"), //
},
data: [],
},
yAxis: {
type: "value",
splitLine: { show: false },
axisLabel: { color: "#ffffff" },
// interval: 100, //Y
min: "dataMin",
max: "dataMax",
// splitArea: { show: false },
},
series: [],
};
//
const formatChartData = (data) => {
return data.reduce((acc, item) => {
const seriesKey = `${item.device_name || ""}_${item.item_name || ""}`;
acc[seriesKey] = {
timestamps: item.data.map((d) =>
dayjs(d.time).format("YYYY-MM-DD HH:mm")
),
values: item.data.map((d) =>
d.value == "無資料" ? null : parseFloat(d.value)
),
maxValue: parseFloat(item.maxValue),
minValue: parseFloat(item.minValue),
averageValue: parseFloat(item.averageValue),
};
return acc;
}, {});
};
// tableData
watch(
tableData,
(newData) => {
if (newData?.length > 0) {
const formattedData = formatChartData(newData);
const series = Object.keys(formattedData).map((seriesKey, index) => {
const { maxValue, minValue, averageValue } = formattedData[seriesKey];
return {
name: seriesKey,
type: "line",
data: formattedData[seriesKey].values,
showSymbol: false,
itemStyle: {
color: SECOND_CHART_COLOR[index % SECOND_CHART_COLOR.length],
},
markPoint: {
data: [
maxValue !== null
? { type: "max", name: "Max", value: maxValue }
: null,
minValue !== null
? { type: "min", name: "Min", value: minValue }
: null,
].filter(Boolean),
},
markLine: {
data:
averageValue !== null
? [
{
yAxis: averageValue,
name: "Avg",
},
]
: [],
},
};
});
//
history_chart.value?.chart.setOption(
{
...defaultChartOption,
legend: {
...defaultChartOption.legend,
data: Object.keys(formattedData),
},
xAxis: {
...defaultChartOption.xAxis,
data:
Object.keys(formattedData).length > 0
? formattedData[Object.keys(formattedData)[0]].timestamps
: [],
},
series,
},
true
);
}
},
{ deep: true }
);
</script>
<template>
<LineChart
id="history_chart"
class="min-h-[350px] max-h-fit"
:option="defaultChartOption"
ref="history_chart"
/>
</template>
<style lang="scss" scoped></style>

View File

@ -15,7 +15,11 @@ const searchTerm = ref(""); //搜尋文字
const activeSearchTerm = ref("");
const getDeviceData = async (sub_system_tag, department_id) => {
const deptArray = department_id ? department_id.map(Number) : [];
const deptArray = Array.isArray(department_id)
? department_id.map(Number)
: department_id
? [Number(department_id)]
: [];
const res = await getHistorySideBar({
sub_system_tag: sub_system_tag,
department_id: deptArray,
@ -110,7 +114,7 @@ const checkedBuilding = computed(() => {
if (
selectedDeviceNumber.value?.filter(
(d) => d.split("_")[1] === building_tag
(d) => d && d.split("_")[1] === building_tag // d &&
).length === allDevices.length
) {
selected.push(building_tag);

View File

@ -1,14 +1,20 @@
<script setup>
import Table from "@/components/customUI/Table.vue";
import { inject, computed } from "vue";
import HistoryDataCahrt from "./HistoryDataCahrt.vue";
import { ref, inject, computed, watch } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { tableData, loading } = inject("history_table_data");
const processedTableData = ref([]);
const columns = computed(() => [
{
title: t("history.building_name"),
key: "building_name",
sort: true,
title: t("assetManagement.department"),
key: "department_name",
},
{
title: t("energy.floor"),
key: "floor_name",
},
{
title: t("history.device_name"),
@ -18,7 +24,7 @@ const columns = computed(() => [
sort: true,
},
{
title: t("history.category"),
title: t("assetManagement.point"),
key: "item_name",
},
{
@ -35,11 +41,76 @@ const columns = computed(() => [
},
]);
const { tableData, loading } = inject("history_table_data");
const columns2 = computed(() => [
{
title: t("assetManagement.department"),
key: "department_name",
},
{
title: t("energy.floor"),
key: "floor_name",
},
{
title: t("history.device_name"),
key: "device_name",
filter: true,
},
{
title: t("assetManagement.point"),
key: "item_name",
},
{
title: t("energy.maximum"),
key: "maxValue",
},
{
title: t("energy.maximum_time"),
key: "maxTime",
},
{
title: t("energy.minimum"),
key: "minValue",
},
{
title: t("energy.minimum_time"),
key: "minTime",
},
{
title: t("energy.average_value"),
key: "averageValue",
},
]);
//
const processData = (data) => {
if (!data || !Array.isArray(data) || data.length === 0) {
return [];
}
// data
return data.flatMap((item) =>
(item.data || []).map((d) => ({
...item,
value: d.value,
timestamp: d.time,
}))
);
};
watch(
tableData,
(newTableData) => {
processedTableData.value = processData(newTableData);
},
{ immediate: true } //
);
</script>
<template>
<Table :columns="columns" :dataSource="tableData" :loading="loading"></Table>
<Table :columns="columns2" :dataSource="tableData" :loading="loading"
><template #beforeTable> <HistoryDataCahrt class="mb-10" /></template
></Table>
<Table :columns="columns" :dataSource="processedTableData" :loading="loading">
</Table>
</template>
<style lang="scss" scoped></style>