feat: RTSP頁 串接 get device api

This commit is contained in:
MJM_2025_05\polly 2025-09-25 11:08:01 +08:00
parent 2bf5f9042a
commit ba7fda3d07
5 changed files with 142 additions and 168 deletions

View File

@ -1,5 +1,4 @@
// 開關 RTSP啟用/停用)
export const POST_SET_RTSP_ENABLE = `/api/rtsp/set-rtsp-enable`;
// 設定 SAMBA 儲存目錄
export const POST_SET_SAMBA_DIRECTORY = `/api/rtsp/set-samba-directory`;
export const POST_GET_RTSP_DEVICE = `/api/rtsp/get-rtsp-device`;

View File

@ -1,6 +1,8 @@
// src/apis/rtsp/index.js
import {
POST_SET_RTSP_ENABLE,
POST_SET_SAMBA_DIRECTORY,
POST_GET_RTSP_DEVICE,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
@ -16,14 +18,16 @@ export const setRtspEnable = async ({ main_id, enable }) => {
};
/**
* 設定 SAMBA 儲存目錄
* Swagger: POST /api/rtsp/set-samba-directory
* body: { main_id: number, directory: string }
* 取得 RTSP 裝置清單
* Swagger: POST /api/rtsp/get-rtsp-device
* body: 可為空物件 {} 或依後端需求帶 building_guid 等參數
* response.data: [
* { main_id, device_number, full_name, device_ip, device_port, rtsp_url,
* enable_traffic, start_btn_enable, stop_btn_enable, alarm_message }
* ]
*/
export const setSambaDirectory = async ({ main_id, directory }) => {
const res = await instance.post(POST_SET_SAMBA_DIRECTORY, {
main_id,
directory,
});
export const getRtspDevices = async (payload = {}) => {
const res = await instance.post(POST_GET_RTSP_DEVICE, payload);
// 後端回傳格式如題:{ code, msg, data: [...] }
return apihandler(res.code, res.data, { msg: res.msg, code: res.code });
};

View File

@ -432,7 +432,7 @@
"selectPathFirst": "請先選擇儲存資料夾",
"startSuccess": "已開始偵測…",
"startFail": "開始偵測失敗,請稍後再試",
"stopSuccess": "已請求結束偵測",
"stopSuccess": "結束偵測",
"stopFail": "結束偵測失敗,請稍後再試",
"noPermission": "沒有取得寫入權限,請重新選擇資料夾並允許",
"selectFolderSuccess": "已選擇資料夾:{name}",

View File

@ -1,5 +1,13 @@
<script setup>
import { ref, defineProps, watch, inject, nextTick } from "vue";
import {
ref,
defineProps,
watch,
inject,
nextTick,
onMounted,
toRaw,
} from "vue";
import dayjs from "dayjs";
import { postOperationRecord } from "@/apis/alert";
import * as yup from "yup";
@ -13,6 +21,27 @@ const props = defineProps({
editRecord: Object,
});
/** ================= Debug Helpers ================= */
const debugLog = (label, payload) => {
try {
console.log(
`[AlertActionItem] ${label}:`,
JSON.parse(JSON.stringify(payload))
);
} catch (e) {
console.log(`[AlertActionItem] ${label}:`, payload);
}
};
// props editRecord
debugLog("props", props);
debugLog("initial props.editRecord", props?.editRecord);
onMounted(() => {
debugLog("onMounted props", props);
debugLog("onMounted props.editRecord", props?.editRecord);
});
const form = ref(null);
const dateItem = ref([
@ -64,18 +93,43 @@ const updateFileList = (files) => {
formState.value.lorf = files;
};
// ---------------------- API video_url ----------------------
// <Input name="videoLocation" :value="..."> value[name]
// videoLocation { videoLocation: string }
const videoLocation = ref({ videoLocation: "" });
const showTooltip = ref(false);
async function copyToClipboard() {
const text = videoLocation.value.videoLocation || "";
try {
await navigator.clipboard.writeText(text);
// tooltip
showTooltip.value = false;
await nextTick();
showTooltip.value = true;
setTimeout(() => {
showTooltip.value = false;
}, 1500);
} catch (err) {
console.error("複製失敗:", err);
}
}
const onOk = async () => {
const formData = new FormData(form.value);
formData.delete("oriFile");
formState.value?.lorf.forEach((file, index) => {
formData.append(`lorf[${index}].id`, file.id ? file.id : "");
formData.append(`lorf[${index}].file`, file.id ? null : file);
(formState.value?.lorf ?? []).forEach((file, index) => {
formData.append(`lorf[${index}].id`, file?.id ? file.id : "");
//
if (!file?.id && file) {
formData.append(`lorf[${index}].file`, file);
}
formData.append(
`lorf[${index}].save_file_name`,
file.id ? file.save_file_name : ""
file?.id ? file.save_file_name : ""
);
formData.append(`lorf[${index}].ori_file_name`, file.name);
formData.append(`lorf[${index}].ori_file_name`, file?.name ?? "");
});
formData.append(
@ -83,9 +137,9 @@ const onOk = async () => {
dayjs(dateItem.value[0].value).format("YYYY-MM-DD")
);
props.editRecord.id && formData.append("id", props.editRecord.id);
props.editRecord.uuid && formData.append("error_code", props.editRecord.uuid);
if (props.editRecord.id) formData.append("id", props.editRecord.id);
if (props.editRecord.uuid)
formData.append("error_code", props.editRecord.uuid);
formData.append("work_type", 2);
formData.append(
@ -96,7 +150,7 @@ const onOk = async () => {
);
try {
const value = await handleSubmit(alertSchema, formState.value);
await handleSubmit(alertSchema, formState.value);
const res = await postOperationRecord(formData);
if (res.isSuccess) {
search?.();
@ -123,58 +177,40 @@ const onCancel = () => {
description: "",
lorf: [],
};
//
videoLocation.value.videoLocation = "";
handleErrorReset();
updateEditRecord?.(null);
alert_action_item.close();
};
// props.editRecord -> formState / / /
watch(
() => props.editRecord,
(newVal) => {
debugLog("watch props.editRecord changed", newVal);
if (newVal) {
for (let [key, value] of Object.entries(newVal)) {
// formState
if (key in formState.value) {
formState.value[key] = value;
}
// start_time
if (key === "start_time") {
formState.value.start_time = value
? dayjs(value).format("YYYY-MM-DD")
: dayjs().format("YYYY-MM-DD");
dateItem.value[0].value = value;
const d = value ? dayjs(value) : dayjs();
formState.value.start_time = d.format("YYYY-MM-DD");
dateItem.value[0].value = d; // dayjs
}
//
if (key === "full_name") {
formState.value.fix_do = value;
formState.value.fix_do = value ?? "";
}
}
// API device_number
videoLocation.value.videoLocation =
newVal?.video_url ?? newVal?.videoUrl ?? newVal?.video_path ?? "";
debugLog("derived videoLocation", videoLocation.value.videoLocation);
}
},
{ immediate: true }
);
const videoLocation = ref("https://your-video-storage-path.com/alert-video");
const showTooltip = ref(false);
const hasCopiedOnce = ref(false);
async function copyToClipboard() {
const text = videoLocation.value;
try {
await navigator.clipboard.writeText(text);
// tooltip
showTooltip.value = false;
await nextTick();
showTooltip.value = true;
setTimeout(() => {
showTooltip.value = false;
}, 1500);
} catch (err) {
console.error("複製失敗:", err);
}
}
</script>
<template>

View File

@ -2,7 +2,7 @@
<section class="min-h-[600px] h-screen">
<h1 class="text-2xl font-extrabold mb-2">{{ $t("rtsp.title") }}</h1>
<!-- Tabs選擇要顯示的攝影機 -->
<!-- Tabs選擇攝影機 -->
<div class="flex items-center gap-4 mb-6">
<h2 class="text-lg font-bold whitespace-nowrap">
{{ $t("rtsp.selectDevice") }} :
@ -38,41 +38,49 @@
></iframe>
</div>
<!-- 右側開始/結束偵測已移除選擇資料夾相關 UI -->
<!-- 右側開始/結束偵測 -->
<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"
: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"
:disabled="
isStopping || !selectedMainId || !currentDevice?.stop_btn_enable
"
:title="!selectedMainId ? $t('rtsp.pleaseSelectDevice') : ''"
>
{{ $t("rtsp.stop") }}
</button>
</div>
<p v-if="message" class="text-sm text-info">{{ message }}</p>
<!-- 顯示後端傳來的提示文字 -->
<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>
import { getSystemDevices } from "@/apis/system";
import { setRtspEnable } from "@/apis/rtsp"; // setSambaDirectory
import { getRtspDevices, setRtspEnable } from "@/apis/rtsp";
import useActiveBtn from "@/hooks/useActiveBtn";
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const DEFAULT_MONITOR_URL =
"http://192.168.0.219:8026/?url=rtsp://admin02:mjmAdmin_99@192.168.0.200:554/stream1?tcp";
"http://192.168.0.219:8026/?url=rtsp://admin02:mjmAdmin_99@192.168.0.200:554/stream1";
export default {
name: "Rtsp",
@ -83,22 +91,23 @@ export default {
data() {
return {
monitorUrl: DEFAULT_MONITOR_URL,
//
isStarting: false,
isStopping: false,
// UI
message: "",
//
deviceData: {},
rtspDevices: [], // { main_id, full_name, rtsp_url, ... }
rtspDevices: [],
selectedMainId: null, // main_id
};
},
computed: {
currentDevice() {
if (!this.selectedMainId) return null;
return (
this.rtspDevices.find((d) => d.main_id === this.selectedMainId) || null
);
},
},
async mounted() {
await this.getData();
await this.loadRtspDevices();
},
watch: {
selectedBtn: {
@ -111,114 +120,38 @@ export default {
},
},
methods: {
async getData() {
async loadRtspDevices() {
try {
const useBuildingStore = (await import("@/stores/useBuildingStore"))
.default;
const buildingStore = useBuildingStore();
const building_guid =
buildingStore?.selectedBuilding?.building_guid || "";
const res = await getRtspDevices({});
const list = Array.isArray(res) ? res : res?.data || [];
const res = await getSystemDevices({ building_guid });
const transformedData = {};
(res?.data || []).forEach((floor) => {
if (floor?.device_list?.length > 0) {
const fullUrl = floor.floor_map_name;
const uuid = fullUrl ? fullUrl.replace(/\.svg$/, "") : "";
transformedData[uuid] = floor.device_list.map((device) => {
let x = 0,
y = 0;
try {
const coordinates = JSON.parse(
device?.device_coordinate || "[0,0]"
);
x = Number(coordinates?.[0] ?? 0);
y = Number(coordinates?.[1] ?? 0);
} catch (_) {}
let state = "Online";
let bgColor = device?.device_normal_color;
if (
device?.device_status === "Offline" ||
device?.device_status == null
) {
state = "Offline";
bgColor = device?.device_close_color;
}
if (device?.device_status === "Error") {
state = "Error";
bgColor = device?.device_error_color;
}
return [
x,
y,
{
device_number: device?.device_number || "",
device_coordinate: device?.device_coordinate || "",
device_image_url: device?.device_image_url,
full_name: device?.full_name,
main_id: device?.main_id,
points: device?.points || [],
floor: floor?.full_name,
state,
icon: device?.device_image
? `${FILE_BASEURL}/upload/device_icon/${device.device_image}`
: "",
bgColor,
Online_color: device?.device_normal_color,
Offline_color: device?.device_close_color,
Error_color: device?.device_error_color,
brand: device?.brand || "",
device_model: device?.device_model,
operation_name: device?.operation_name,
operation_contact_person: device?.operation_contact_person,
buying_date: device?.buying_date,
created_at: device?.created_at,
bgSize: 50,
is_rtsp: device?.is_rtsp === true,
rtsp_url: device?.rtsp_url || "",
},
];
});
}
});
this.deviceData = transformedData;
const allRows = Object.values(transformedData).flat();
this.rtspDevices = allRows
.map((row) => row?.[2])
.filter((p) => p && p.is_rtsp && p.rtsp_url)
.reduce((acc, cur) => {
if (!acc.find((x) => x.main_id === cur.main_id)) acc.push(cur);
return acc;
}, [])
.sort((a, b) => (a.full_name || "").localeCompare(b.full_name || ""));
this.rtspDevices = list;
// Tabs
const cate = this.rtspDevices.map((d, index) => ({
title: d.full_name || d.main_id,
key: d.main_id,
active: this.selectedMainId
? this.selectedMainId === d.main_id
: index === 0,
...d,
}));
this.setItems(cate);
//
if (this.rtspDevices.length > 0) {
const first = this.rtspDevices[0];
this.selectedMainId = first.main_id;
this.monitorUrl = first.rtsp_url;
const chosenId = this.selectedMainId ?? this.rtspDevices[0].main_id;
const chosen =
this.rtspDevices.find((d) => d.main_id === chosenId) ||
this.rtspDevices[0];
this.selectDevice(chosen);
} else {
this.selectedMainId = null;
this.monitorUrl = DEFAULT_MONITOR_URL;
}
} catch (err) {
console.error("getData() 失敗", err);
this.setItems([]);
console.error("loadRtspDevices() 失敗", err);
this.rtspDevices = [];
this.setItems([]);
this.selectedMainId = null;
this.monitorUrl = DEFAULT_MONITOR_URL;
}
@ -230,18 +163,19 @@ export default {
this.monitorUrl = d.rtsp_url || DEFAULT_MONITOR_URL;
},
// setRtspEnable(true)
//
async startDetection() {
if (!this.selectedMainId) {
this.message = this.$t("rtsp.pleaseSelectDevice");
return;
}
this.isStarting = true;
const keepId = this.selectedMainId;
try {
await setRtspEnable({
main_id: this.selectedMainId,
enable: true,
});
await setRtspEnable({ main_id: keepId, enable: true });
await this.loadRtspDevices();
const found = this.rtspDevices.find((d) => d.main_id === keepId);
if (found) this.selectDevice(found);
this.message = this.$t("rtsp.startSuccess");
} catch (e) {
console.error(e);
@ -251,18 +185,19 @@ export default {
}
},
// setRtspEnable(false)
//
async stopDetection() {
if (!this.selectedMainId) {
this.message = this.$t("rtsp.pleaseSelectDevice");
return;
}
this.isStopping = true;
const keepId = this.selectedMainId;
try {
await setRtspEnable({
main_id: this.selectedMainId,
enable: false,
});
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");
} catch (e) {
console.error(e);