新增告警日誌API整合,調整告警查詢邏輯與顯示,優化儀表板告警顯示

This commit is contained in:
koko 2025-09-11 10:52:52 +08:00
parent 30b3cb586b
commit a4e56a7e2d
16 changed files with 211 additions and 236 deletions

View File

@ -3,9 +3,12 @@ import { RouterView } from "vue-router";
import Navbar from "./components/navbar/Navbar.vue";
import useUserInfoStore from "@/stores/useUserInfoStore";
import { ref, provide, onUnmounted, onMounted } from "vue";
import { getAlertLog } from "@/apis/alert";
import dayjs from "dayjs";
const store = useUserInfoStore();
const alerts = ref([]);
let isToastOpen = ref({
open: false,
content: "",
@ -24,6 +27,16 @@ const cancelToastOpen = () => {
};
};
let alertInterval = null;
const getAlert = async () => {
const res = await getAlertLog({
isRecovery: 1,
Start_date: dayjs().subtract(30, "day").format("YYYY-MM-DD"),
End_date: dayjs().format("YYYY-MM-DD"),
});
alerts.value = (res.data || []).map((d) => ({ ...d, key: d.id }));
};
const openToast = (status, content, to = "body", confirm = null) => {
isToastOpen.value = {
open: true,
@ -33,8 +46,19 @@ const openToast = (status, content, to = "body", confirm = null) => {
confirm,
};
};
onMounted(() => {
getAlert();
alertInterval = setInterval(getAlert, 30 * 1000);
});
onUnmounted(() => {
if (alertInterval) clearInterval(alertInterval);
});
provide("app_toast", { openToast, cancelToastOpen });
provide("app_toggle", { forgeLock, toggleForgeLock });
provide("app_alerts", alerts);
</script>
<template>

View File

@ -1,5 +1,6 @@
export const POST_ACK_API = `/obix/alarm`;
export const GET_ALERT_FORMID_API = `/Alert/AlertList`;
export const GET_ALERT_LOG_API = `api/Alarm/GetAlarmLog`;
export const POST_OPERATION_RECORD_API = `/operation/SavOpeRecord`;
export const GET_ALERT_SUB_LIST_API = `api/Device/GetMainSub`;

View File

@ -1,6 +1,7 @@
import {
POST_ACK_API,
GET_ALERT_FORMID_API,
GET_ALERT_LOG_API,
POST_OPERATION_RECORD_API,
GET_ALERT_SUB_LIST_API,
GET_OUTLIERS_LIST_API,
@ -58,6 +59,24 @@ export const getAlertFormId = async (uuid) => {
});
};
export const getAlertLog = async ({
Start_date,
End_date,
isRecovery,
device_name_tag,
}) => {
const res = await instance.post(GET_ALERT_LOG_API, {
Start_date,
End_date,
isRecovery,
device_name_tag,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postOperationRecord = async (formData) => {
const res = await instance.post(POST_OPERATION_RECORD_API, formData);

View File

@ -37,7 +37,6 @@ onMounted(() => {
<div class="drawer drawer-end">
<input id="alarm" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<!-- Page content here -->
<label
for="alarm"
class="drawer-button flex flex-col justify-center items-center btn-group"

View File

@ -85,15 +85,6 @@ const updateMeterPositionsLocal = () => {
meterPositions.value = newPositions;
};
// meterList
watch(
() => props.meterList,
() => {
updateMeterPositionsLocal();
},
{ deep: true }
);
const forgeDom = ref(null);
const initViewer = (container) => {
@ -135,8 +126,6 @@ const initForge = () => {
viewer.isLoadDone()
);
updateForgeViewer(viewer);
// meter
updateMeterPositionsLocal();
}
);
@ -199,7 +188,6 @@ onUnmounted(() => {
>
<p class="absolute z-10 top-14 left-[27%]">
更新時間 : {{ props.realTime }}
{{ searchParams?.option == 4 ? " (溫度)" : "" }}
</p>
<div v-show="searchParams?.option == 4" class="absolute z-10 heatbar">
<div class="w-40 flex justify-between text-[10px] mb-1">

View File

@ -76,9 +76,9 @@ onMounted(() => {
>
</button>
</li>
<li>
<!-- <li>
<AlarmDrawer />
</li>
</li> -->
<li>
<div class="dropdown dropdown-bottom dropdown-end">
<button
@ -149,7 +149,6 @@ onMounted(() => {
}
.menu-box .btn-group span {
color: #fff;
display: block;
margin-top: 0px;
}

View File

@ -1,14 +1,17 @@
<script setup>
import { onMounted, ref, watch, computed } from "vue";
import { onMounted, ref, watch, computed, inject } from "vue";
import { AUTHPAGES } from "@/constant";
import { getAuth, getAllSysSidebar } from "@/apis/building";
import useBuildingStore from "@/stores/useBuildingStore";
import useUserInfoStore from "@/stores/useUserInfoStore";
import { twMerge } from "tailwind-merge";
const store = useUserInfoStore();
const buildingStore = useBuildingStore();
const alerts = inject("app_alerts");
const alarmslength = computed(() => (alerts?.value || []).length);
const iniFroList = async () => {
const res = await getAuth();
@ -57,25 +60,58 @@ onMounted(() => {
v-if="$route.path !== page.navigate"
:to="page.navigate"
type="link"
class="flex flex-col justify-center items-center btn-group text-white"
class="flex flex-col justify-center items-center btn-group"
>
<font-awesome-icon
:icon="['fas', page.icon]"
size="2x"
class="menu-icon"
:class="
twMerge(
'menu-icon',
page.showView === 'alert' && alarmslength > 0
? 'text-red-500 animate-pulse'
: 'text-white'
)
"
/>
<span
:class="
twMerge(
page.showView === 'alert' && alarmslength > 0
? 'text-red-200'
: 'text-white'
)
"
>
{{ page.subName }}
</span>
</router-link>
<div
v-else
class="flex flex-col justify-center items-center btn-group text-white router-link-active cursor-pointer"
class="flex flex-col justify-center items-center btn-group router-link-active cursor-pointer"
>
<font-awesome-icon
:icon="['fas', page.icon]"
size="2x"
class="menu-icon"
/>
:class="
twMerge(
'menu-icon',
page.showView === 'alert' && alarmslength > 0
? 'text-red-500 animate-pulse'
: ''
)
"
/><span
:class="
twMerge(
page.showView === 'alert' && alarmslength > 0
? 'text-red-200'
: ''
)
"
>
{{ page.subName }}
</span>
</div>
</li>
</ul>

View File

@ -4,12 +4,8 @@ import AlertTable from "./AlertTable.vue";
import AlertTableModal from "./AlertTableModal.vue";
import { ref, provide, onMounted, watch } from "vue";
import useSearchParam from "@/hooks/useSearchParam";
import useAlarmData from "@/hooks/baja/useAlarmData";
import { getAlertLog } from "@/apis/alert";
import {
getAlertFormId,
} from "@/apis/alert";
import {
getOperationDeviceList,
getOperationCompanyList,
getOperationEditRecord,
} from "@/apis/operation";
@ -17,14 +13,12 @@ import { getAccountUserList } from "@/apis/account";
import useBuildingStore from "@/stores/useBuildingStore";
const { searchParams } = useSearchParam();
const { getAlarmByBaja, alarmData } = useAlarmData();
const store = useBuildingStore();
const tableLoading = ref(false);
const dataSource = ref([]);
const model_data = ref({
model_userList: [],
model_devList: [],
model_companyList: [],
});
@ -49,30 +43,6 @@ const updateEditRecord = (data) => {
}
};
const getFormId = async (uuid) => {
const res = await getAlertFormId(uuid);
return res.data;
};
const getModalDevList = async () => {
const sub_system_tags = searchParams.value.system_tag.map(tag => tag.split('_')[1]);
const res = await getOperationDeviceList({
list_sub_system_tag: sub_system_tags,
device_building_tag: store.buildings[0].building_tag,
device_area_tag: "NTPC",
});
return res.data.map((d) => {
const formattedKey = d.device_number
.replace(/-/g, '_')
.split('_')
.slice(0, 8)
.join('_');
return { ...d, key: formattedKey };
});
};
const getModalUserList = async () => {
const res = await getAccountUserList({});
return res.data.map((d) => ({ ...d, key: d.userinfo_guid }));
@ -84,15 +54,12 @@ const getModalCompanyList = async () => {
};
const getAllOptions = async () => {
Promise.all([
getModalDevList(),
getModalUserList(),
getModalCompanyList(),
]).then(([devices, users, companies]) => {
Promise.all([getModalUserList(), getModalCompanyList()]).then(
([devices, users, companies]) => {
model_data.value.model_userList = users;
model_data.value.model_devList = devices;
model_data.value.model_companyList = companies;
});
}
);
};
const updateDataSource = (data) => {
@ -100,38 +67,13 @@ const updateDataSource = (data) => {
};
const search = async () => {
if (Object.keys(searchParams.value).length !== 0) {
tableLoading.value = true;
await getAlarmByBaja(
searchParams.value.start_created_at,
searchParams.value.end_created_at,
searchParams.value.isRecover,
searchParams.value.isAck,
searchParams.value.system_tag,
async (result) => {
alarmData.value = result.data;
// alarm formId
alarmData.value = alarmData.value.map(alarm => ({
...alarm,
formId: null // formId null
}));
const uuids = alarmData.value.map(alarm => ({ uuid: alarm.uuid }));
const formIds = await getFormId(uuids);
if (Array.isArray(formIds)) {
formIds.forEach((form) => {
if (form && form.uuid) {
const index = alarmData.value.findIndex(alarm => alarm.uuid === form.uuid);
if (index !== -1) {
alarmData.value[index].formId = form.formId || null;
}
}
if (Object.keys(searchParams.value).length !== 0) {
const res = await getAlertLog({
...searchParams.value,
isRecovery: Number(searchParams.value.isRecovery),
});
};
updateDataSource(alarmData.value);
}
);
dataSource.value = (res.data || []).map((d) => ({ ...d, key: d.id }));
tableLoading.value = false;
}
};
@ -140,11 +82,17 @@ const openModal = async (record) => {
try {
if (record.formId) {
const res = await getOperationEditRecord(record.formId);
updateEditRecord({ ...res.data, uuid: record.uuid });
updateEditRecord({
...res.data,
uuid: res.data.error_code,
device_number: record.device_number,
});
} else {
updateEditRecord(record);
updateEditRecord({
...record,
uuid: record.id,
});
}
await getAllOptions();
alert_action_item.showModal();
} catch (error) {
console.error("Error opening modal:", error);
@ -153,20 +101,18 @@ const openModal = async (record) => {
watch(
() => ({
isAck: searchParams.value.isAck,
isRecover: searchParams.value.isRecover,
Start_date: searchParams.value.start_created_at,
End_date: searchParams.value.end_created_at,
system_tag: searchParams.value.system_tag,
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.isAck !== undefined &&
val.isRecover !== undefined &&
val.isRecovery !== undefined &&
val.Start_date &&
val.End_date &&
val.system_tag
val.device_name_tag
) {
search();
}
@ -175,7 +121,13 @@ watch(
);
provide("alert_modal", { model_data, search, updateEditRecord });
provide("alert_table", { openModal, updateEditRecord, dataSource, search, tableLoading });
provide("alert_table", {
openModal,
updateEditRecord,
dataSource,
search,
tableLoading,
});
</script>
<template>

View File

@ -1,7 +1,6 @@
<script setup>
import { inject } from "vue";
import AlertSearchNormalBtns from "./AlertSearchNormalBtns.vue";
import AlertSearchAckBtns from "./AlertSearchAckBtns.vue";
import AlertSearchTimeRange from "./AlertSearchTimeRange.vue";
import AlertSearchTypesButton from "./AlertSearchTypesButton.vue";
@ -13,7 +12,6 @@ const { search } = inject("alert_table");
<div class="w-full flex flex-wrap flex-col custom-border px-4 pt-0 pb-4 mb-4">
<div class="w-full flex flex-wrap items-center justify-start">
<AlertSearchNormalBtns />
<AlertSearchAckBtns />
<AlertSearchTimeRange />
<button class="btn btn-success ml-8" @click.stop.prevent="search">查詢</button>
</div>

View File

@ -29,7 +29,7 @@ onMounted(() => {
watch(
selectedBtn,
(newValue) => {
changeParams({ ...searchParams.value, isAck: newValue.key });
changeParams({ ...searchParams.value });
},
);

View File

@ -11,12 +11,12 @@ onMounted(() => {
setItems([
{
title: "未復歸",
key: "offnormal",
key: 1,
active: true,
},
{
title: "已賦歸",
key: "normal",
key: 2,
active: false,
},
]);
@ -27,7 +27,7 @@ onMounted(() => {
watch(
selectedBtn,
(newValue) => {
changeParams({ ...searchParams.value, isRecover: newValue.key });
changeParams({ ...searchParams.value, isRecovery: newValue.key });
}
);
@ -35,8 +35,8 @@ watch(
watch(
searchParams,
(newSearchParams) => {
if (!newSearchParams.isRecover) {
changeParams({ ...newSearchParams, isRecover: "offnormal" });
if (!newSearchParams.isRecovery) {
changeParams({ ...newSearchParams, isRecovery: 1 });
}
},
{ immediate: true } //

View File

@ -8,13 +8,17 @@ const { searchParams, changeParams } = useSearchParam();
const dateRange = ref([
{
key: "start_at",
value: searchParams.value.start_created_at ? dayjs(searchParams.value.start_created_at).valueOf() : dayjs().subtract(30, 'day').valueOf(),
value: searchParams.value.Start_date
? dayjs(searchParams.value.Start_date)
: dayjs().subtract(30, "day"),
dateFormat: "yyyy-MM-dd",
placeholder: "起始日期",
},
{
key: "end_at",
value: searchParams.value.end_created_at ? dayjs(searchParams.value.end_created_at).valueOf() : dayjs().valueOf(),
value: searchParams.value.End_date
? dayjs(searchParams.value.End_date)
: dayjs(),
dateFormat: "yyyy-MM-dd",
placeholder: "結束日期",
},
@ -24,13 +28,13 @@ const changeTimeRange = () => {
const newRange = [
{
key: "start_at",
value: dayjs().subtract(30, 'day').startOf('day').valueOf(), // 3000:00:00.000
value: dayjs().subtract(30, "day"),
dateFormat: "yyyy-MM-dd",
placeholder: "起始日期",
},
{
key: "end_at",
value: dayjs().endOf('day').valueOf(), // 23:59:59.999
value: dayjs().endOf("day"),
dateFormat: "yyyy-MM-dd",
placeholder: "結束日期",
},
@ -40,14 +44,17 @@ const changeTimeRange = () => {
changeParams({
...searchParams.value,
start_created_at: newRange[0].value,
end_created_at: newRange[1].value,
Start_date: newRange[0].value,
End_date: newRange[1].value,
});
};
onMounted(() => {
//
if (!searchParams.value.start_created_at || !searchParams.value.end_created_at) {
if (
!searchParams.value.Start_date ||
!searchParams.value.End_date
) {
changeTimeRange();
}
});
@ -58,8 +65,8 @@ watch(
() => {
changeParams({
...searchParams.value,
start_created_at: dayjs(dateRange.value[0].value).startOf('day').valueOf(),
end_created_at: dayjs(dateRange.value[1].value).endOf('day').valueOf(),
Start_date: dayjs(dateRange.value[0].value).format("YYYY-MM-DD"),
End_date: dayjs(dateRange.value[1].value).format("YYYY-MM-DD"),
});
},
{ deep: true } //

View File

@ -11,12 +11,12 @@ const changeCheckedItem = () => {
if (checkedItem.value.length === store.subSys.length) {
changeParams({
...searchParams.value,
system_tag: [store.subSys[0]?.main_system_tag+`_`+store.subSys[0]?.sub_system_tag],
device_name_tag: [store.subSys[0]?.sub_system_tag],
});
} else {
changeParams({
...searchParams.value,
system_tag: store.subSys.map(({ main_system_tag, sub_system_tag }) => main_system_tag+`_`+sub_system_tag),
device_name_tag: store.subSys.map(({ sub_system_tag }) => sub_system_tag),
});
}
};
@ -25,16 +25,16 @@ const onChange = (value, checked) => {
if (checked) {
changeParams({
...searchParams.value,
system_tag: searchParams.value.system_tag
? typeof searchParams.value.system_tag === "string"
? [searchParams.value.system_tag, value]
: [...searchParams.value.system_tag, value]
device_name_tag: searchParams.value.device_name_tag
? typeof searchParams.value.device_name_tag === "string"
? [searchParams.value.device_name_tag, value]
: [...searchParams.value.device_name_tag, value]
: [value],
});
} else {
changeParams({
...searchParams.value,
sub_system_tag: searchParams.value.system_tag.filter(
device_name_tag: searchParams.value.device_name_tag.filter(
(sub) => sub !== value
),
});
@ -42,18 +42,18 @@ const onChange = (value, checked) => {
};
const checkedItem = computed(() =>
searchParams.value.system_tag
? typeof searchParams.value.system_tag === "string"
? [searchParams.value.system_tag]
: searchParams.value.system_tag
searchParams.value.device_name_tag
? typeof searchParams.value.device_name_tag === "string"
? [searchParams.value.device_name_tag]
: searchParams.value.device_name_tag
: []
);
watch(searchParams, (newValue) => {
if (!newValue.system_tag) {
if (!newValue.device_name_tag) {
changeParams({
...newValue,
system_tag: store.subSys.map(({ main_system_tag, sub_system_tag }) => main_system_tag+`_`+sub_system_tag),
device_name_tag: store.subSys.map(({ sub_system_tag }) => sub_system_tag),
});
}
});
@ -71,8 +71,8 @@ watch(searchParams, (newValue) => {
v-for="sub in store.subSys"
:key="sub.key"
:title="sub.full_name"
:value="sub.main_system_tag+`_`+sub.sub_system_tag"
:checked="checkedItem.includes(sub.main_system_tag+`_`+sub.sub_system_tag)"
:value="sub.sub_system_tag"
:checked="checkedItem.includes(sub.sub_system_tag)"
class="mx-3 my-3 xl:my-0 text-lg"
:onChange="onChange"
/>

View File

@ -1,48 +1,34 @@
<script setup>
import { inject } from "vue";
import {
postChgAck,
} from "@/apis/alert";
import { postChgAck } from "@/apis/alert";
import { Button } from "ant-design-vue";
const { dataSource, openModal, search, tableLoading } = inject("alert_table");
const columns = [
{
key: "building_tag",
title: "棟別-樓層",
},
{
key: "uuid",
key: "alarm_guid",
title: "異常ID",
},
{
key: "alarmClass",
title: "異常類別",
},
{
key: "full_name",
title: "維修項目",
key: "factor",
title: "告警條件",
},
{
key: "device_number",
title: "設備編號",
title: "設備名稱",
},
{
key: "timestamp_date",
title: "發生日期",
key: "points",
title: "點位名稱",
},
{
key: "timestamp_time",
key: "created_at",
title: "發生時間",
},
{
key: "msg",
key: "reason",
title: "異常原因",
},
{
key: "ackState",
title: "Ack 確認",
},
{
key: "repairOrder",
title: "派工 / 維運單號",
@ -55,25 +41,16 @@ const chgAck = async (devUuid) => {
search?.();
}
};
</script>
<template>
<Table :loading="tableLoading" :columns="columns" :dataSource="dataSource">
<template #bodyCell="{ record, column, index }">
<template v-if="column.key === 'ackState'">
<template v-if="record.ackState === 'Unacked'">
<button class="btn btn-sm btn-success text-white whitespace-nowrap mr-2" @click.stop.prevent="() => chgAck(record.uuid)">
未確認
</button>
</template>
<template v-else>
{{ record.ackedTime }}
</template>
</template>
<template v-if="column.key === 'repairOrder'">
<button class="btn btn-sm btn-success text-white whitespace-nowrap mr-2"
@click.stop.prevent="() => openModal(record)">
<button
class="btn btn-sm btn-success text-white whitespace-nowrap mr-2"
@click.stop.prevent="() => openModal(record)"
>
<span v-if="record.formId">{{ record.formId }}</span>
<span v-else>
<font-awesome-icon :icon="['fas', 'plus']" />維修單

View File

@ -29,12 +29,13 @@ const dateItem = ref([
const formState = ref(
{
formId: null,
uuid: "",
alarm_guid: "",
work_type: 2,
fix_do: "",
fix_do_code: "",
fix_firm: "",
status: 0,
device_number: "",
work_person_id: "",
start_time: dayjs().format("YYYY-MM-DD"),
notice: "",
@ -87,7 +88,7 @@ const onOk = async () => {
props.editRecord.id && formData.append("id", props.editRecord.id);
props.editRecord.uuid && formData.append("error_code", props.editRecord.uuid);
props.editRecord.alarm_guid && formData.append("error_code", props.editRecord.alarm_guid);
formData.append("work_type", 2);
@ -112,12 +113,13 @@ const onOk = async () => {
const onCancel = () => {
formState.value = {
formId: null,
uuid: "",
alarm_guid: "",
work_type: 2,
fix_do: "",
fix_do_code: "",
fix_firm: "",
status: 0,
device_number: "",
work_person_id: "",
start_time: dayjs().format("YYYY-MM-DD"),
notice: "",
@ -143,10 +145,6 @@ watch(
formState.value.start_time = value ? dayjs(value).format("YYYY-MM-DD") : dayjs().format("YYYY-MM-DD");
dateItem.value[0].value = value;
}
//
if (key === "full_name") {
formState.value.fix_do = value;
}
//
if (key === "device_number" || key === "fix_do_code") {
formState.value.fix_do_code = value;
@ -165,8 +163,8 @@ watch(
<Input v-if="formState.value && formState.value.formId" class="my-2" :value="formState" name="formId" readonly>
<template #topLeft>表單編號</template>
</Input>
<Input :value="formState" class="my-2" name="uuid" readonly>
<template #topLeft>異常編號</template>
<Input :value="formState" class="my-2" name="alarm_guid" readonly>
<template #topLeft>異常ID</template>
</Input>
<DateGroup class="my-2" :items="dateItem" inputClass="w-full shadow-none" :required="true">
<template #topLeft>預計開始時間</template>
@ -191,20 +189,25 @@ watch(
]" :required="true" :disabled="true">
<template #topLeft>項目</template>
</Select>
<Input class="my-2" :value="formState" name="fix_do" :required="true" readonly>
<Input class="my-2" :value="formState" name="fix_do" :required="true">
<template #topLeft>維修項目</template>
<template #bottomLeft><span class="text-error text-base">
{{ formErrorMsg.fix_do }}
</span>
</template>
</Input>
<Select :value="formState" class="my-2" selectClass="border-info focus-within:border-info" name="fix_do_code"
Attribute="full_name" :options="model_data.model_devList" :required="true" :disabled="true">
<template #topLeft>維修項目代碼(設備編號)</template>
<Input
class="my-2"
:value="formState"
name="device_number"
:required="true"
:disabled="true"
>
<template #topLeft>設備名稱</template>
<template #bottomLeft><span class="text-error text-base">
{{ formErrorMsg.fix_do_code }}
</span></template>
</Select>
</Input>
<Select :value="formState" class="my-2" selectClass="border-info focus-within:border-info" name="fix_firm"
Attribute="name" :options="model_data.model_companyList" :required="true">
<template #topLeft>負責廠商</template>

View File

@ -1,47 +1,14 @@
<script setup>
import useAlarmStore from "@/stores/useAlarmStore";
import dayjs from "dayjs";
import { ref,computed } from "vue";
const store = useAlarmStore();
// const alarms = computed(() =>
// store.alarmData.slice(0, 5).map((d) => ({
// ...d,
// timestamp_date: dayjs(d.timestamp_date).format("YYYY.MM.DD"),
// timestamp_time: d.timestamp_time.split(":").slice(0, 2).join(":"),
// }))
// );
const alarms = ref([
{
uuid: 1,
timestamp_date: "2020-01-02",
timestamp_time: "11:02",
msg: "xx異常",
status: "處理中",
},
{
uuid: 2,
timestamp_date: "2020-01-03",
timestamp_time: "12:02",
msg: "xx異常",
status: "已處理",
},
{
uuid: 3,
timestamp_date: "2020-01-04",
timestamp_time: "13:02",
msg: "xx異常",
status: "已處理",
},
{
uuid: 4,
timestamp_date: "2020-01-05",
timestamp_time: "14:02",
msg: "xx異常",
status: "處理中",
},
]);
import { ref, computed, inject } from "vue";
const alerts = inject("app_alerts");
const alarms = computed(() =>
(alerts.value || []).slice(0, 5).map((d) => ({
...d,
timestamp_date: dayjs(d.created_at).format("YYYY.MM.DD"),
timestamp_time: dayjs(d.created_at).format("HH:mm:ss"),
})).sort((a, b) => (dayjs(a.created_at).isBefore(dayjs(b.created_at)) ? 1 : -1))
);
</script>
<template>
@ -54,17 +21,22 @@ const alarms = ref([
<tr class="">
<th>日期</th>
<th>時間</th>
<th>備註</th>
<th>狀態</th>
<th>設備名稱</th>
<th>異常原因</th>
</tr>
</thead>
<tbody class="text-base">
<tr>
<tr v-if="alarms.length === 0">
<td colspan="4" class="text-center h-40">
<!-- <span>無資料</span> -->
<div class="text-gray-400">目前無告警訊息</div>
</td>
</tr>
<tr v-else v-for="alarm in alarms" :key="alarm.alarm_guid">
<td>{{ alarm.timestamp_date }}</td>
<td>{{ alarm.timestamp_time }}</td>
<td>{{ alarm.device_number }}</td>
<td>{{ alarm.reason }}</td>
</tr>
</tbody>
</table>
</div>