feat: rtsp 分頁串接 API (query-alarm-log)
This commit is contained in:
parent
d01d3e7581
commit
5ff030dc40
@ -25,3 +25,5 @@ export const POST_ALERT_SCHEDULE = `api/Alarm/SaveAlarmSchedule`;
|
|||||||
export const DELETE_ALERT_SCHEDULE = `api/Alarm/DeleteAlarmSchedule`;
|
export const DELETE_ALERT_SCHEDULE = `api/Alarm/DeleteAlarmSchedule`;
|
||||||
|
|
||||||
export const POST_ALERT_MQTT_REFRESH = `api/Alarm/MQTTRefresh`;
|
export const POST_ALERT_MQTT_REFRESH = `api/Alarm/MQTTRefresh`;
|
||||||
|
|
||||||
|
export const POST_QUERY_ALARM_LOG = `api/alarm/query-alarm-log`; // RTSP 分頁顯示告警 log
|
||||||
|
@ -21,6 +21,7 @@ import {
|
|||||||
POST_ALERT_SCHEDULE,
|
POST_ALERT_SCHEDULE,
|
||||||
DELETE_ALERT_SCHEDULE,
|
DELETE_ALERT_SCHEDULE,
|
||||||
POST_ALERT_MQTT_REFRESH,
|
POST_ALERT_MQTT_REFRESH,
|
||||||
|
POST_QUERY_ALARM_LOG,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
import instance from "@/util/request";
|
import instance from "@/util/request";
|
||||||
import apihandler from "@/util/apihandler";
|
import apihandler from "@/util/apihandler";
|
||||||
@ -236,3 +237,24 @@ export const postMQTTRefresh = async () => {
|
|||||||
code: res.code,
|
code: res.code,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查詢裝置告警紀錄(支援起訖日)
|
||||||
|
* @param {{ id: number, start_date: string, end_date: string }} payload
|
||||||
|
* - id: main_id(裝置/攝影機/樓層主鍵等)
|
||||||
|
* - start_date: "YYYY-MM-DD"
|
||||||
|
* - end_date: "YYYY-MM-DD"
|
||||||
|
* @returns {Promise<any>} apihandler 標準回傳(成功時回 data 陣列)
|
||||||
|
*/
|
||||||
|
export const postQueryAlarmLog = async ({ id, start_date, end_date }) => {
|
||||||
|
const res = await instance.post(POST_QUERY_ALARM_LOG, {
|
||||||
|
id,
|
||||||
|
start_date,
|
||||||
|
end_date,
|
||||||
|
});
|
||||||
|
|
||||||
|
return apihandler(res.code, res.data, {
|
||||||
|
msg: res.msg,
|
||||||
|
code: res.code,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -179,6 +179,7 @@
|
|||||||
"normal": "已复归",
|
"normal": "已复归",
|
||||||
"unacked": "未确认",
|
"unacked": "未确认",
|
||||||
"acked": "已确认",
|
"acked": "已确认",
|
||||||
|
"7days": "近7天",
|
||||||
"30days": "近30天",
|
"30days": "近30天",
|
||||||
"start_date": "起始日期",
|
"start_date": "起始日期",
|
||||||
"end_date": "结束日期",
|
"end_date": "结束日期",
|
||||||
@ -424,18 +425,9 @@
|
|||||||
},
|
},
|
||||||
"rtsp": {
|
"rtsp": {
|
||||||
"title": "影像串流",
|
"title": "影像串流",
|
||||||
"start": "开始侦测",
|
|
||||||
"stop": "结束侦测",
|
|
||||||
"selectPath": "选择存储位置",
|
"selectPath": "选择存储位置",
|
||||||
"selectDevice": "选择设备",
|
"selectDevice": "选择设备",
|
||||||
"pleaseSelectDevice": "请先选择设备",
|
"normalQuery":"已复归查询"
|
||||||
"selectPathFirst": "请先选择存储文件夹",
|
|
||||||
"startSuccess": "已开始侦测…",
|
|
||||||
"startFail": "开始侦测失败,请稍后再试",
|
|
||||||
"stopSuccess": "已请求结束侦测…",
|
|
||||||
"stopFail": "结束侦测失败,请稍后再试",
|
|
||||||
"noPermission": "未获得写入权限,请重新选择文件夹并授权",
|
|
||||||
"selectFolderSuccess": "已选择文件夹:{name}",
|
|
||||||
"selectFolderFail": "选择文件夹失败,请再试一次"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,6 +179,7 @@
|
|||||||
"normal": "已復歸",
|
"normal": "已復歸",
|
||||||
"unacked": "未確認",
|
"unacked": "未確認",
|
||||||
"acked": "已確認",
|
"acked": "已確認",
|
||||||
|
"7days": "近7天",
|
||||||
"30days": "近30天",
|
"30days": "近30天",
|
||||||
"start_date": "起始日期",
|
"start_date": "起始日期",
|
||||||
"end_date": "結束日期",
|
"end_date": "結束日期",
|
||||||
@ -424,18 +425,7 @@
|
|||||||
},
|
},
|
||||||
"rtsp": {
|
"rtsp": {
|
||||||
"title": "影像串流",
|
"title": "影像串流",
|
||||||
"start": "開始偵測",
|
|
||||||
"stop": "結束偵測",
|
|
||||||
"selectPath": "選擇儲存位置",
|
|
||||||
"selectDevice": "選擇設備",
|
"selectDevice": "選擇設備",
|
||||||
"pleaseSelectDevice": "請先選擇設備",
|
"normalQuery": "已復歸查詢"
|
||||||
"selectPathFirst": "請先選擇儲存資料夾",
|
|
||||||
"startSuccess": "已開始偵測…",
|
|
||||||
"startFail": "開始偵測失敗,請稍後再試",
|
|
||||||
"stopSuccess": "結束偵測",
|
|
||||||
"stopFail": "結束偵測失敗,請稍後再試",
|
|
||||||
"noPermission": "沒有取得寫入權限,請重新選擇資料夾並允許",
|
|
||||||
"selectFolderSuccess": "已選擇資料夾:{name}",
|
|
||||||
"selectFolderFail": "選擇資料夾失敗,請再試一次"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -179,6 +179,7 @@
|
|||||||
"normal": "Normal",
|
"normal": "Normal",
|
||||||
"unacked": "Unacked",
|
"unacked": "Unacked",
|
||||||
"acked": "Acked",
|
"acked": "Acked",
|
||||||
|
"7days": "Last 7 Days",
|
||||||
"30days": "Last 30 Days",
|
"30days": "Last 30 Days",
|
||||||
"start_date": "Start Date",
|
"start_date": "Start Date",
|
||||||
"end_date": "End Date",
|
"end_date": "End Date",
|
||||||
@ -424,18 +425,7 @@
|
|||||||
},
|
},
|
||||||
"rtsp": {
|
"rtsp": {
|
||||||
"title": "Video Stream",
|
"title": "Video Stream",
|
||||||
"start": "Start Detection",
|
|
||||||
"stop": "Stop Detection",
|
|
||||||
"selectPath": "Select Folder",
|
|
||||||
"selectDevice": "Select Device",
|
"selectDevice": "Select Device",
|
||||||
"pleaseSelectDevice": "Please select a device first",
|
"normalQuery": "Query normal records"
|
||||||
"selectPathFirst": "Please select a folder first",
|
|
||||||
"startSuccess": "Detection started…",
|
|
||||||
"startFail": "Failed to start detection, please try again later",
|
|
||||||
"stopSuccess": "Detection stop requested…",
|
|
||||||
"stopFail": "Failed to stop detection, please try again later",
|
|
||||||
"noPermission": "No write permission. Please select a folder again and grant access",
|
|
||||||
"selectFolderSuccess": "Folder selected: {name}",
|
|
||||||
"selectFolderFail": "Failed to select folder, please try again"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,55 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch } from "vue";
|
import { ref, onMounted, watch, computed } from "vue";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import useSearchParam from "@/hooks/useSearchParam";
|
import useSearchParam from "@/hooks/useSearchParam";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const { searchParams, changeParams } = useSearchParam();
|
const { searchParams, changeParams } = useSearchParam();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 可調整 props:
|
||||||
|
* - showQuickButton:是否顯示快捷按鈕(預設 true,可關掉)
|
||||||
|
* - quickButtonType:快捷類型 '30d' | '7d'(預設 '30d')
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
showQuickButton: { type: Boolean, default: true },
|
||||||
|
quickButtonType: { type: String, default: "30d" }, // '30d' | '7d'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 統一設定區間的 helper
|
||||||
|
const setRange = (start, end) => {
|
||||||
|
const newRange = [
|
||||||
|
{
|
||||||
|
key: "start_at",
|
||||||
|
value: dayjs(start),
|
||||||
|
dateFormat: "yyyy-MM-dd",
|
||||||
|
placeholder: t("alert.start_date"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "end_at",
|
||||||
|
value: dayjs(end),
|
||||||
|
dateFormat: "yyyy-MM-dd",
|
||||||
|
placeholder: t("alert.end_date"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
dateRange.value = newRange;
|
||||||
|
|
||||||
|
// 同步到共用查詢參數(字串格式)
|
||||||
|
changeParams({
|
||||||
|
...searchParams.value,
|
||||||
|
Start_date: dayjs(start).format("YYYY-MM-DD"),
|
||||||
|
End_date: dayjs(end).format("YYYY-MM-DD"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始 dateRange:若有既有搜尋值就吃既有;否則依 quickButtonType
|
||||||
const dateRange = ref([
|
const dateRange = ref([
|
||||||
{
|
{
|
||||||
key: "start_at",
|
key: "start_at",
|
||||||
value: searchParams.value.Start_date
|
value: searchParams.value.Start_date
|
||||||
? dayjs(searchParams.value.Start_date)
|
? dayjs(searchParams.value.Start_date)
|
||||||
|
: props.quickButtonType === "7d"
|
||||||
|
? dayjs().subtract(7, "day")
|
||||||
: dayjs().subtract(30, "day"),
|
: dayjs().subtract(30, "day"),
|
||||||
dateFormat: "yyyy-MM-dd",
|
dateFormat: "yyyy-MM-dd",
|
||||||
placeholder: t("alert.start_date"),
|
placeholder: t("alert.start_date"),
|
||||||
@ -25,42 +64,28 @@ const dateRange = ref([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// 快捷按鈕:依 quickButtonType 決定區間
|
||||||
const changeTimeRange = () => {
|
const changeTimeRange = () => {
|
||||||
const newRange = [
|
if (props.quickButtonType === "7d") {
|
||||||
{
|
// 近7天:起始 = 今天往回 7 天,結束 = 今天
|
||||||
key: "start_at",
|
setRange(dayjs().subtract(7, "day"), dayjs().endOf("day"));
|
||||||
value: dayjs().subtract(30, "day"),
|
} else {
|
||||||
dateFormat: "yyyy-MM-dd",
|
// 近30天:起始 = 今天往回 30 天,結束 = 今天
|
||||||
placeholder: t("alert.start_date"),
|
setRange(dayjs().subtract(30, "day"), dayjs().endOf("day"));
|
||||||
},
|
}
|
||||||
{
|
|
||||||
key: "end_at",
|
|
||||||
value: dayjs().endOf("day"),
|
|
||||||
dateFormat: "yyyy-MM-dd",
|
|
||||||
placeholder: t("alert.end_date"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
dateRange.value = newRange;
|
|
||||||
|
|
||||||
changeParams({
|
|
||||||
...searchParams.value,
|
|
||||||
Start_date: newRange[0].value,
|
|
||||||
End_date: newRange[1].value,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 進頁面初始化:若沒有既有 Start/End,依 quickButtonType 設定
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 初始化日期範圍
|
if (!searchParams.value.Start_date || !searchParams.value.End_date) {
|
||||||
if (
|
|
||||||
!searchParams.value.Start_date ||
|
|
||||||
!searchParams.value.End_date
|
|
||||||
) {
|
|
||||||
changeTimeRange();
|
changeTimeRange();
|
||||||
|
} else {
|
||||||
|
// 若已存在既有值,把區間同步回 dateRange(避免格式落差)
|
||||||
|
setRange(searchParams.value.Start_date, searchParams.value.End_date);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 監聽搜尋變化
|
// 使用者調整日期 → 同步到共用查詢參數
|
||||||
watch(
|
watch(
|
||||||
dateRange,
|
dateRange,
|
||||||
() => {
|
() => {
|
||||||
@ -70,24 +95,34 @@ watch(
|
|||||||
End_date: dayjs(dateRange.value[1].value).format("YYYY-MM-DD"),
|
End_date: dayjs(dateRange.value[1].value).format("YYYY-MM-DD"),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ deep: true } // 確保在初始化立即觸發
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 語系切換時,更新 placeholder
|
||||||
watch(locale, () => {
|
watch(locale, () => {
|
||||||
dateRange.value[0].placeholder = t("alert.start_date");
|
dateRange.value[0].placeholder = t("alert.start_date");
|
||||||
dateRange.value[1].placeholder = t("alert.end_date");
|
dateRange.value[1].placeholder = t("alert.end_date");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 快捷按鈕顯示文字
|
||||||
|
const quickBtnLabel = computed(() =>
|
||||||
|
props.quickButtonType === "7d" ? t("alert.7days") : t("alert.30days")
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap items-center">
|
<div class="flex flex-wrap items-center">
|
||||||
|
<!-- 快捷按鈕:可關閉;可切換 30天 / 7天 -->
|
||||||
<button
|
<button
|
||||||
|
v-if="showQuickButton"
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-outline-success mr-3"
|
class="btn btn-outline-success mr-3"
|
||||||
@click="changeTimeRange"
|
@click="changeTimeRange"
|
||||||
>
|
>
|
||||||
{{ $t("alert.30days") }}
|
{{ quickBtnLabel }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- 日期群組 -->
|
||||||
<DateGroup :items="dateRange" :withLine="true" />
|
<DateGroup :items="dateRange" :withLine="true" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,102 +1,29 @@
|
|||||||
<template>
|
|
||||||
<section class="min-h-[600px] h-screen">
|
|
||||||
<h1 class="text-2xl font-extrabold mb-2">{{ $t("rtsp.title") }}</h1>
|
|
||||||
|
|
||||||
<!-- Tabs:選擇攝影機 -->
|
|
||||||
<div class="flex items-center gap-4 mb-6">
|
|
||||||
<h2 class="text-lg font-bold whitespace-nowrap">
|
|
||||||
{{ $t("rtsp.selectDevice") }} :
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<ButtonConnectedGroup
|
|
||||||
:items="items"
|
|
||||||
:onclick="
|
|
||||||
(e, item) => {
|
|
||||||
changeActiveBtn(item);
|
|
||||||
const found = rtspDevices.find((r) => r.main_id === item.key);
|
|
||||||
if (found) selectDevice(found);
|
|
||||||
}
|
|
||||||
"
|
|
||||||
:className="`flex flex-wrap`"
|
|
||||||
size="sm"
|
|
||||||
color="info"
|
|
||||||
>
|
|
||||||
<template #buttonContent="{ item }">
|
|
||||||
<span class="text-base">{{ item.title }}</span>
|
|
||||||
</template>
|
|
||||||
</ButtonConnectedGroup>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex h-[70%] gap-4">
|
|
||||||
<!-- 左側:即時監控 -->
|
|
||||||
<div class="relative w-full flex-1 rounded border overflow-hidden">
|
|
||||||
<iframe
|
|
||||||
:src="monitorUrl"
|
|
||||||
class="absolute inset-0 w-full h-full"
|
|
||||||
allow="autoplay; fullscreen; picture-in-picture"
|
|
||||||
referrerpolicy="no-referrer"
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 右側:開始/結束偵測 -->
|
|
||||||
<aside class="w-1/2 flex flex-col gap-6 p-4">
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button
|
|
||||||
class="btn btn-add w-40"
|
|
||||||
@click="startDetection"
|
|
||||||
:disabled="
|
|
||||||
isStarting || !selectedMainId || !currentDevice?.start_btn_enable
|
|
||||||
"
|
|
||||||
:title="!selectedMainId ? $t('rtsp.pleaseSelectDevice') : ''"
|
|
||||||
>
|
|
||||||
{{ $t("rtsp.start") }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-error text-white w-40"
|
|
||||||
@click="stopDetection"
|
|
||||||
:disabled="
|
|
||||||
isStopping || !selectedMainId || !currentDevice?.stop_btn_enable
|
|
||||||
"
|
|
||||||
:title="!selectedMainId ? $t('rtsp.pleaseSelectDevice') : ''"
|
|
||||||
>
|
|
||||||
{{ $t("rtsp.stop") }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 顯示後端傳來的提示文字 -->
|
|
||||||
<p v-if="currentDevice?.alarm_message" class="text-sm text-info">
|
|
||||||
{{ currentDevice.alarm_message }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p v-if="message" class="text-sm">{{ message }}</p>
|
|
||||||
</aside>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { getRtspDevices, setRtspEnable } from "@/apis/rtsp";
|
import { getRtspDevices } from "@/apis/rtsp";
|
||||||
|
import { postQueryAlarmLog } from "@/apis/alert";
|
||||||
import useActiveBtn from "@/hooks/useActiveBtn";
|
import useActiveBtn from "@/hooks/useActiveBtn";
|
||||||
|
import useSearchParam from "@/hooks/useSearchParam";
|
||||||
|
import AlertSearchTimeRange from "@/views/alert/components/AlertQuery/AlertSearchTimeRange.vue";
|
||||||
|
import Table from "@/components/customUI/Table.vue";
|
||||||
|
|
||||||
const DEFAULT_MONITOR_URL =
|
const DEFAULT_MONITOR_URL =
|
||||||
"http://192.168.0.219:8026/?url=rtsp://admin02:mjmAdmin_99@192.168.0.200:554/stream1";
|
"http://192.168.0.219:8026/?url=rtsp://admin02:mjmAdmin_99@192.168.0.200:554/stream1";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "Rtsp",
|
name: "Rtsp",
|
||||||
|
components: { Table, AlertSearchTimeRange },
|
||||||
setup() {
|
setup() {
|
||||||
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
||||||
return { items, changeActiveBtn, setItems, selectedBtn };
|
const { searchParams } = useSearchParam();
|
||||||
|
return { items, changeActiveBtn, setItems, selectedBtn, searchParams };
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
monitorUrl: DEFAULT_MONITOR_URL,
|
monitorUrl: DEFAULT_MONITOR_URL,
|
||||||
isStarting: false,
|
|
||||||
isStopping: false,
|
|
||||||
message: "",
|
|
||||||
rtspDevices: [],
|
rtspDevices: [],
|
||||||
selectedMainId: null, // 目前選中的設備 main_id
|
selectedMainId: null,
|
||||||
pollTimer: null, // ← 用於記錄輪詢計時器
|
tableLoading: false,
|
||||||
|
dataSource: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -106,13 +33,21 @@ export default {
|
|||||||
this.rtspDevices.find((d) => d.main_id === this.selectedMainId) || null
|
this.rtspDevices.find((d) => d.main_id === this.selectedMainId) || null
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
columns() {
|
||||||
|
return [
|
||||||
|
{ key: "id", title: this.$t("alert.uuid") || "UUID" },
|
||||||
|
{ key: "created_at", title: this.$t("alert.time") || "Time" },
|
||||||
|
{ key: "reason", title: this.$t("alert.error_msg") || "Reason" },
|
||||||
|
];
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
await this.loadRtspDevices();
|
await this.loadRtspDevices();
|
||||||
},
|
// ★ 設定近 7 天並立即查一次
|
||||||
beforeUnmount() {
|
this.setLast7Days();
|
||||||
// 離開頁面時確實清掉輪詢
|
if (this.selectedMainId) {
|
||||||
this.stopPolling();
|
await this.searchLogs();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedBtn: {
|
selectedBtn: {
|
||||||
@ -125,6 +60,22 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 工具:轉成 YYYY-MM-DD(考慮時區)
|
||||||
|
toISO(d) {
|
||||||
|
return new Date(d.getTime() - d.getTimezoneOffset() * 60000)
|
||||||
|
.toISOString()
|
||||||
|
.slice(0, 10);
|
||||||
|
},
|
||||||
|
// ★ 將查詢區間設為「近 7 天」
|
||||||
|
setLast7Days() {
|
||||||
|
const today = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
// 與你在 AlertSearchTimeRange 的 7d 快捷一致:往回 7 天到今天
|
||||||
|
start.setDate(start.getDate() - 7);
|
||||||
|
this.searchParams.Start_date = this.toISO(start);
|
||||||
|
this.searchParams.End_date = this.toISO(today);
|
||||||
|
},
|
||||||
|
|
||||||
async loadRtspDevices() {
|
async loadRtspDevices() {
|
||||||
try {
|
try {
|
||||||
const res = await getRtspDevices({});
|
const res = await getRtspDevices({});
|
||||||
@ -166,82 +117,137 @@ export default {
|
|||||||
selectDevice(d) {
|
selectDevice(d) {
|
||||||
this.selectedMainId = d.main_id;
|
this.selectedMainId = d.main_id;
|
||||||
this.monitorUrl = d.rtsp_url || DEFAULT_MONITOR_URL;
|
this.monitorUrl = d.rtsp_url || DEFAULT_MONITOR_URL;
|
||||||
|
this.dataSource = [];
|
||||||
},
|
},
|
||||||
|
|
||||||
// === 輪詢控制 ===
|
// 查詢告警紀錄(讀共用的 searchParams)
|
||||||
startPolling() {
|
async searchLogs() {
|
||||||
// 若已在輪詢就不重複啟動
|
if (!this.selectedMainId) return;
|
||||||
if (this.pollTimer) return;
|
const start = this.searchParams?.Start_date;
|
||||||
|
const end = this.searchParams?.End_date;
|
||||||
|
if (!start || !end) return;
|
||||||
|
|
||||||
const tick = async () => {
|
|
||||||
const keepId = this.selectedMainId;
|
|
||||||
await this.loadRtspDevices();
|
|
||||||
// 保持目前選中的設備不變(若仍存在)
|
|
||||||
const found = this.rtspDevices.find((d) => d.main_id === keepId);
|
|
||||||
if (found) this.selectDevice(found);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 立即跑一次,之後每 5 秒跑一次
|
|
||||||
tick();
|
|
||||||
this.pollTimer = setInterval(tick, 5000);
|
|
||||||
},
|
|
||||||
|
|
||||||
stopPolling() {
|
|
||||||
if (this.pollTimer) {
|
|
||||||
clearInterval(this.pollTimer);
|
|
||||||
this.pollTimer = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 開始偵測
|
|
||||||
async startDetection() {
|
|
||||||
if (!this.selectedMainId) {
|
|
||||||
this.message = this.$t("rtsp.pleaseSelectDevice");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isStarting = true;
|
|
||||||
const keepId = this.selectedMainId;
|
|
||||||
try {
|
try {
|
||||||
await setRtspEnable({ main_id: keepId, enable: true });
|
this.tableLoading = true;
|
||||||
await this.loadRtspDevices();
|
const res = await postQueryAlarmLog({
|
||||||
const found = this.rtspDevices.find((d) => d.main_id === keepId);
|
id: this.selectedMainId,
|
||||||
if (found) this.selectDevice(found);
|
start_date: start,
|
||||||
this.message = this.$t("rtsp.startSuccess");
|
end_date: end,
|
||||||
|
});
|
||||||
// ★ 啟動後開始每 5 秒抓一次
|
const rows = Array.isArray(res) ? res : res?.data || [];
|
||||||
this.startPolling();
|
this.dataSource = rows.map((d) => ({
|
||||||
|
...d,
|
||||||
|
key: d.id,
|
||||||
|
// 後端目前未回傳 formId,保留欄位供未來擴充
|
||||||
|
formId: d.formId,
|
||||||
|
}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error("searchLogs() 失敗", e);
|
||||||
this.message = this.$t("rtsp.startFail");
|
this.dataSource = [];
|
||||||
} finally {
|
} finally {
|
||||||
this.isStarting = false;
|
this.tableLoading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 結束偵測
|
onRepairOrder(record) {
|
||||||
async stopDetection() {
|
console.log("repair order click:", record);
|
||||||
if (!this.selectedMainId) {
|
|
||||||
this.message = this.$t("rtsp.pleaseSelectDevice");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.isStopping = true;
|
|
||||||
const keepId = this.selectedMainId;
|
|
||||||
try {
|
|
||||||
await setRtspEnable({ main_id: keepId, enable: false });
|
|
||||||
await this.loadRtspDevices();
|
|
||||||
const found = this.rtspDevices.find((d) => d.main_id === keepId);
|
|
||||||
if (found) this.selectDevice(found);
|
|
||||||
this.message = this.$t("rtsp.stopSuccess");
|
|
||||||
|
|
||||||
// ★ 停止後結束輪詢
|
|
||||||
this.stopPolling();
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
this.message = this.$t("rtsp.stopFail");
|
|
||||||
} finally {
|
|
||||||
this.isStopping = false;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="min-h-[600px]">
|
||||||
|
<h1 class="text-2xl font-extrabold mb-2">{{ $t("rtsp.title") }}</h1>
|
||||||
|
|
||||||
|
<!-- Tabs:選擇攝影機 -->
|
||||||
|
<div class="flex items-center gap-4 mb-6">
|
||||||
|
<h2 class="text-lg font-bold whitespace-nowrap">
|
||||||
|
{{ $t("rtsp.selectDevice") }} :
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<ButtonConnectedGroup
|
||||||
|
:items="items"
|
||||||
|
:onclick="
|
||||||
|
(e, item) => {
|
||||||
|
changeActiveBtn(item);
|
||||||
|
const found = rtspDevices.find((r) => r.main_id === item.key);
|
||||||
|
if (found) selectDevice(found);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:className="`flex flex-wrap`"
|
||||||
|
size="sm"
|
||||||
|
color="info"
|
||||||
|
>
|
||||||
|
<template #buttonContent="{ item }">
|
||||||
|
<span class="text-base">{{ item.title }}</span>
|
||||||
|
</template>
|
||||||
|
</ButtonConnectedGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4 items-start">
|
||||||
|
<!-- 左側:即時監控 -->
|
||||||
|
<div
|
||||||
|
class="relative w-full flex-1 rounded border overflow-hidden h-[420px] md:h-[520px]"
|
||||||
|
>
|
||||||
|
<iframe
|
||||||
|
:src="monitorUrl"
|
||||||
|
class="absolute inset-0 w-full h-full"
|
||||||
|
allow="autoplay; fullscreen; picture-in-picture"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右側:查詢告警紀錄(日期 + 表格) -->
|
||||||
|
<aside class="w-1/2 flex flex-col gap-4 px-4">
|
||||||
|
<div
|
||||||
|
class="w-full flex flex-wrap items-end gap-3 custom-border px-4 pt-3 pb-4"
|
||||||
|
>
|
||||||
|
<div class="w-full flex flex-wrap items-center justify-start gap-4">
|
||||||
|
<h2 class="text-lg font-bold">{{ $t("rtsp.normalQuery") }} :</h2>
|
||||||
|
|
||||||
|
<!-- 選擇開始與結束時間 -->
|
||||||
|
<AlertSearchTimeRange quickButtonType="7d" />
|
||||||
|
<button
|
||||||
|
class="btn btn-search"
|
||||||
|
:disabled="
|
||||||
|
!selectedMainId ||
|
||||||
|
!searchParams?.Start_date ||
|
||||||
|
!searchParams?.End_date ||
|
||||||
|
tableLoading
|
||||||
|
"
|
||||||
|
:title="!selectedMainId ? $t('rtsp.pleaseSelectDevice') : ''"
|
||||||
|
@click.stop.prevent="searchLogs"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'search']" class="text-lg" />
|
||||||
|
{{ $t("button.search") || "Search" }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格(交由 Table 內建分頁處理,給全量 dataSource) -->
|
||||||
|
<Table
|
||||||
|
:loading="tableLoading"
|
||||||
|
:columns="columns"
|
||||||
|
:dataSource="dataSource"
|
||||||
|
rowKey="key"
|
||||||
|
>
|
||||||
|
<template #bodyCell="{ record, column }">
|
||||||
|
<template v-if="column.key === 'repairOrder'">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-success text-white whitespace-nowrap"
|
||||||
|
@click.stop.prevent="onRepairOrder(record)"
|
||||||
|
>
|
||||||
|
<span v-if="record.formId">{{ record.formId }}</span>
|
||||||
|
<span v-else>
|
||||||
|
<font-awesome-icon :icon="['fas', 'plus']" />
|
||||||
|
{{ $t("alert.repair_order_number") || "Repair Order" }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
Loading…
Reference in New Issue
Block a user