From feba269e582a34986ab6b76cb4aa8884f461c4cb Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Tue, 20 Jan 2026 17:46:32 +0800 Subject: [PATCH] =?UTF-8?q?ROI=E9=80=89=E5=8C=BA01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main.tsx | 10 + frontend/src/pages/ROIEditor.tsx | 304 +++++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 frontend/src/main.tsx create mode 100644 frontend/src/pages/ROIEditor.tsx diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..aa6ed2f --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import ReactDOM from 'react-dom/client' +import AppWrapper from './AppWrapper' +import './index.css' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +) diff --git a/frontend/src/pages/ROIEditor.tsx b/frontend/src/pages/ROIEditor.tsx new file mode 100644 index 0000000..7c129c0 --- /dev/null +++ b/frontend/src/pages/ROIEditor.tsx @@ -0,0 +1,304 @@ +import React, { useEffect, useState, useRef } from 'react'; +import { Card, Button, Space, Select, message, Modal, Form, Input, InputNumber, Drawer } from 'antd'; +import { Stage, Layer, Rect, Line, Circle, Text as KonvaText } from 'react-konva'; +import axios from 'axios'; + +interface ROI { + id: number; + roi_id: string; + name: string; + type: string; + points: number[][]; + rule: string; + direction: string | null; + enabled: boolean; + threshold_sec: number; + confirm_sec: number; + return_sec: number; +} + +interface Camera { + id: number; + name: string; +} + +const ROIEditor: React.FC = () => { + const [cameras, setCameras] = useState([]); + const [selectedCamera, setSelectedCamera] = useState(null); + const [rois, setRois] = useState([]); + const [snapshot, setSnapshot] = useState(''); + const [loading, setLoading] = useState(false); + const [imageDim, setImageDim] = useState({ width: 800, height: 600 }); + const [selectedROI, setSelectedROI] = useState(null); + const [drawerVisible, setDrawerVisible] = useState(false); + const [form] = Form.useForm(); + + const stageRef = useRef(null); + + const fetchCameras = async () => { + try { + const res = await axios.get('/api/cameras?enabled_only=true'); + setCameras(res.data); + if (res.data.length > 0 && !selectedCamera) { + setSelectedCamera(res.data[0].id); + } + } catch (err) { + message.error('获取摄像头列表失败'); + } + }; + + useEffect(() => { + fetchCameras(); + }, []); + + useEffect(() => { + if (selectedCamera) { + fetchSnapshot(); + fetchROIs(); + } + }, [selectedCamera]); + + const fetchSnapshot = async () => { + if (!selectedCamera) return; + try { + const res = await axios.get(`/api/camera/${selectedCamera}/snapshot/base64`); + setSnapshot(res.data.image); + const img = new Image(); + img.onload = () => { + const maxWidth = 800; + const maxHeight = 600; + const scale = Math.min(maxWidth / img.width, maxHeight / img.height); + setImageDim({ width: img.width * scale, height: img.height * scale }); + }; + img.src = `data:image/jpeg;base64,${res.data.image}`; + } catch (err) { + message.error('获取截图失败'); + } + }; + + const fetchROIs = async () => { + if (!selectedCamera) return; + try { + const res = await axios.get(`/api/camera/${selectedCamera}/rois`); + setRois(res.data); + } catch (err) { + message.error('获取ROI列表失败'); + } + }; + + const handleSaveROI = async (values: any) => { + if (!selectedCamera || !selectedROI) return; + try { + await axios.put(`/api/camera/${selectedCamera}/roi/${selectedROI.id}`, values); + message.success('保存成功'); + setDrawerVisible(false); + fetchROIs(); + } catch (err) { + message.error('保存失败'); + } + }; + + const handleAddROI = async () => { + if (!selectedCamera) return; + const roi_id = `roi_${Date.now()}`; + try { + await axios.post(`/api/camera/${selectedCamera}/roi`, { + roi_id, + name: '新区域', + roi_type: 'polygon', + points: [[100, 100], [300, 100], [300, 300], [100, 300]], + rule_type: 'leave_post', + threshold_sec: 360, + confirm_sec: 30, + return_sec: 5, + }); + message.success('添加成功'); + fetchROIs(); + } catch (err) { + message.error('添加失败'); + } + }; + + const handleDeleteROI = async (roiId: number) => { + if (!selectedCamera) return; + try { + await axios.delete(`/api/camera/${selectedCamera}/roi/${roiId}`); + message.success('删除成功'); + fetchROIs(); + } catch (err) { + message.error('删除失败'); + } + }; + + const getROIStrokeColor = (rule: string) => { + return rule === 'intrusion' ? '#ff4d4f' : '#faad14'; + }; + + const renderROI = (roi: ROI) => { + const points = roi.points.flat(); + const color = getROIStrokeColor(roi.rule); + + if (roi.type === 'polygon') { + return ( + { + setSelectedROI(roi); + form.setFieldsValue(roi); + setDrawerVisible(true); + }} + /> + ); + } else if (roi.type === 'line') { + return ( + { + setSelectedROI(roi); + form.setFieldsValue(roi); + setDrawerVisible(true); + }} + /> + ); + } + return null; + }; + + return ( +
+ + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default ROIEditor;