From f39f59be94fe3537507e25f42181c4b420c91e81 Mon Sep 17 00:00:00 2001 From: 16337 <1633794139@qq.com> Date: Tue, 20 Jan 2026 17:45:02 +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/App.css | 50 ++++++ frontend/src/App.tsx | 97 ++++++++++ frontend/src/AppRoutes.tsx | 27 +++ frontend/src/AppWrapper.tsx | 32 ++++ frontend/src/index.css | 87 +++++++++ frontend/src/pages/AlertCenter.tsx | 225 ++++++++++++++++++++++++ frontend/src/pages/CameraManagement.tsx | 179 +++++++++++++++++++ frontend/src/pages/Dashboard.tsx | 179 +++++++++++++++++++ 8 files changed, 876 insertions(+) create mode 100644 frontend/src/App.css create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/AppRoutes.tsx create mode 100644 frontend/src/AppWrapper.tsx create mode 100644 frontend/src/index.css create mode 100644 frontend/src/pages/AlertCenter.tsx create mode 100644 frontend/src/pages/CameraManagement.tsx create mode 100644 frontend/src/pages/Dashboard.tsx diff --git a/frontend/src/App.css b/frontend/src/App.css new file mode 100644 index 0000000..c2077de --- /dev/null +++ b/frontend/src/App.css @@ -0,0 +1,50 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.logo { + height: 32px; + margin: 16px; + color: white; + font-size: 18px; + font-weight: bold; + text-align: center; + line-height: 32px; +} + +.site-layout .site-layout-background { + background: #fff; +} + +.ant-layout-sider { + overflow: auto; + height: 100vh; + position: fixed; + left: 0; + top: 0; + bottom: 0; +} + +.ant-layout-content { + margin-left: 200px; + padding: 24px; + min-height: 100vh; +} + +.trigger { + font-size: 18px; + line-height: 64px; + padding: 0 24px; + cursor: pointer; + transition: color 0.3s; +} + +.trigger:hover { + color: #1890ff; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..d0da010 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,97 @@ +import React from 'react'; +import { Layout, Menu, theme } from 'antd'; +import { + VideoCameraOutlined, + AlertOutlined, + SettingOutlined, + DashboardOutlined, + EditOutlined, +} from '@ant-design/icons'; +import { useNavigate, useLocation, Outlet } from 'react-router-dom'; +import './App.css'; + +const { Header, Sider, Content } = Layout; + +const App: React.FC = () => { + const { + token: { colorBgContainer }, + } = theme.useToken(); + const navigate = useNavigate(); + const location = useLocation(); + + const menuItems = [ + { + key: '/', + icon: , + label: '监控面板', + }, + { + key: '/cameras', + icon: , + label: '摄像头管理', + }, + { + key: '/rois', + icon: , + label: 'ROI划定', + }, + { + key: '/alerts', + icon: , + label: '告警中心', + }, + { + key: '/settings', + icon: , + label: '系统设置', + }, + ]; + + return ( + + +
+ 安保监控系统 +
+ navigate(key)} + /> + + +
+
+ 安保异常行为识别系统 v1.0 +
+
+ +
+ +
+
+
+ + ); +}; + +export default App; diff --git a/frontend/src/AppRoutes.tsx b/frontend/src/AppRoutes.tsx new file mode 100644 index 0000000..91f0e5d --- /dev/null +++ b/frontend/src/AppRoutes.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { Routes, Route } from 'react-router-dom'; +import Dashboard from './pages/Dashboard'; +import CameraManagement from './pages/CameraManagement'; +import ROIEditor from './pages/ROIEditor'; +import AlertCenter from './pages/AlertCenter'; + +const Settings: React.FC = () => ( +
+

系统设置

+

配置管理页面开发中...

+
+); + +const AppRoutes: React.FC = () => { + return ( + + } /> + } /> + } /> + } /> + } /> + + ); +}; + +export default AppRoutes; diff --git a/frontend/src/AppWrapper.tsx b/frontend/src/AppWrapper.tsx new file mode 100644 index 0000000..fa96fb7 --- /dev/null +++ b/frontend/src/AppWrapper.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; +import App from './App'; +import Dashboard from './pages/Dashboard'; +import CameraManagement from './pages/CameraManagement'; +import ROIEditor from './pages/ROIEditor'; +import AlertCenter from './pages/AlertCenter'; + +const Settings: React.FC = () => ( +
+

系统设置

+

配置管理页面开发中...

+
+); + +const AppWrapper: React.FC = () => { + return ( + + + + } /> + } /> + } /> + } /> + } /> + + + + ); +}; + +export default AppWrapper; diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..445d52b --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,87 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +.camera-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + gap: 16px; +} + +.camera-card { + background: #f5f5f5; + border-radius: 8px; + overflow: hidden; +} + +.camera-preview { + position: relative; + width: 100%; + aspect-ratio: 16/9; + background: #000; +} + +.camera-info { + padding: 12px; +} + +.roi-editor-container { + width: 100%; + height: 600px; + border: 1px solid #d9d9d9; + border-radius: 8px; + overflow: hidden; +} + +.alert-list .alert-item { + padding: 12px; + border-bottom: 1px solid #f0f0f0; + cursor: pointer; + transition: background 0.3s; +} + +.alert-item:hover { + background: #fafafa; +} + +.alert-item.unprocessed { + background: #fff7e6; +} + +.stat-cards { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 24px; +} + +.stat-card { + padding: 24px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 8px; + color: white; +} + +.stat-card.alert { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); +} + +.stat-card.camera { + background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); +} + +.stat-value { + font-size: 36px; + font-weight: bold; +} + +.stat-label { + opacity: 0.8; + margin-top: 8px; +} diff --git a/frontend/src/pages/AlertCenter.tsx b/frontend/src/pages/AlertCenter.tsx new file mode 100644 index 0000000..f6d1403 --- /dev/null +++ b/frontend/src/pages/AlertCenter.tsx @@ -0,0 +1,225 @@ +import React, { useEffect, useState } from 'react'; +import { Table, Tag, Button, Space, Card, Row, Col, Statistic, Modal, message } from 'antd'; +import { EyeOutlined, CheckOutlined, SendOutlined } from '@ant-design/icons'; +import axios from 'axios'; +import dayjs from 'dayjs'; + +interface Alarm { + id: number; + camera_id: number; + roi_id: string; + event_type: string; + confidence: number; + snapshot_path: string; + llm_checked: boolean; + llm_result: string | null; + processed: boolean; + created_at: string; +} + +const AlertCenter: React.FC = () => { + const [alarms, setAlarms] = useState([]); + const [loading, setLoading] = useState(false); + const [stats, setStats] = useState({ total: 0, unprocessed: 0 }); + const [detailVisible, setDetailVisible] = useState(false); + const [selectedAlarm, setSelectedAlarm] = useState(null); + + const fetchAlarms = async () => { + setLoading(true); + try { + const res = await axios.get('/api/alarms?limit=100'); + setAlarms(res.data); + } catch (err) { + message.error('获取告警列表失败'); + } finally { + setLoading(false); + } + }; + + const fetchStats = async () => { + try { + const res = await axios.get('/api/alarms/stats'); + setStats(res.data); + } catch (err) { + console.error('获取统计失败'); + } + }; + + useEffect(() => { + fetchAlarms(); + fetchStats(); + }, []); + + const handleViewDetail = (alarm: Alarm) => { + setSelectedAlarm(alarm); + setDetailVisible(true); + }; + + const handleMarkProcessed = async (id: number) => { + try { + await axios.put(`/api/alarms/${id}`, { processed: true }); + message.success('已标记为已处理'); + fetchAlarms(); + fetchStats(); + } catch (err) { + message.error('操作失败'); + } + }; + + const handleLLMCheck = async (id: number) => { + try { + await axios.post(`/api/alarms/${id}/llm-check`); + message.success('大模型分析完成'); + fetchAlarms(); + fetchStats(); + } catch (err: any) { + message.error(err.response?.data?.detail || '大模型分析失败'); + } + }; + + const getEventTypeTag = (type: string) => { + const colors: Record = { + intrusion: 'red', + leave_post: 'orange', + }; + return {type === 'intrusion' ? '周界入侵' : '离岗告警'}; + }; + + const columns = [ + { + title: '时间', + dataIndex: 'created_at', + key: 'created_at', + render: (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss'), + }, + { + title: '摄像头', + dataIndex: 'camera_id', + key: 'camera_id', + render: (id: number) => `摄像头 ${id}`, + }, + { + title: '类型', + dataIndex: 'event_type', + key: 'event_type', + render: getEventTypeTag, + }, + { + title: '置信度', + dataIndex: 'confidence', + key: 'confidence', + render: (v: number) => v.toFixed(2), + }, + { + title: 'ROI', + dataIndex: 'roi_id', + key: 'roi_id', + }, + { + title: '大模型', + dataIndex: 'llm_checked', + key: 'llm_checked', + render: (checked: boolean) => ( + {checked ? '已检查' : '待检查'} + ), + }, + { + title: '状态', + dataIndex: 'processed', + key: 'processed', + render: (processed: boolean) => ( + {processed ? '已处理' : '待处理'} + ), + }, + { + title: '操作', + key: 'action', + render: (_: any, record: Alarm) => ( + + + {!record.processed && ( + + )} + {!record.llm_checked && ( + + )} + + ), + }, + ]; + + return ( +
+ + + + + + + + + + + + + + + + setDetailVisible(false)} + footer={[ + , + selectedAlarm && !selectedAlarm.processed && ( + + ), + ]} + > + {selectedAlarm && ( +
+

时间: {dayjs(selectedAlarm.created_at).format('YYYY-MM-DD HH:mm:ss')}

+

摄像头: {selectedAlarm.camera_id}

+

类型: {getEventTypeTag(selectedAlarm.event_type)}

+

置信度: {selectedAlarm.confidence.toFixed(2)}

+

ROI: {selectedAlarm.roi_id}

+

状态: {selectedAlarm.processed ? '已处理' : '待处理'}

+ {selectedAlarm.llm_result && ( +
+ 大模型分析结果: +

+ {selectedAlarm.llm_result} +

+
+ )} +
+ )} +
+ + ); +}; + +export default AlertCenter; diff --git a/frontend/src/pages/CameraManagement.tsx b/frontend/src/pages/CameraManagement.tsx new file mode 100644 index 0000000..c058ca6 --- /dev/null +++ b/frontend/src/pages/CameraManagement.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useState } from 'react'; +import { Table, Button, Modal, Form, Input, InputNumber, Switch, Space, Tag, message } from 'antd'; +import { PlusOutlined, DeleteOutlined, EditOutlined, PlayCircleOutlined, PauseCircleOutlined } from '@ant-design/icons'; +import axios from 'axios'; + +interface Camera { + id: number; + name: string; + rtsp_url: string; + enabled: boolean; + fps_limit: number; + process_every_n_frames: number; +} + +const CameraManagement: React.FC = () => { + const [cameras, setCameras] = useState([]); + const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [editingCamera, setEditingCamera] = useState(null); + const [form] = Form.useForm(); + + const fetchCameras = async () => { + setLoading(true); + try { + const res = await axios.get('/api/cameras?enabled_only=false'); + setCameras(res.data); + } catch (err) { + message.error('获取摄像头列表失败'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchCameras(); + }, []); + + const handleAdd = () => { + setEditingCamera(null); + form.resetFields(); + setModalVisible(true); + }; + + const handleEdit = (camera: Camera) => { + setEditingCamera(camera); + form.setFieldsValue(camera); + setModalVisible(true); + }; + + const handleDelete = async (id: number) => { + try { + await axios.delete(`/api/cameras/${id}`); + message.success('删除成功'); + fetchCameras(); + } catch (err) { + message.error('删除失败'); + } + }; + + const handleToggle = async (camera: Camera) => { + try { + await axios.put(`/api/cameras/${camera.id}`, { enabled: !camera.enabled }); + message.success(camera.enabled ? '已停用' : '已启用'); + fetchCameras(); + } catch (err) { + message.error('操作失败'); + } + }; + + const handleSubmit = async (values: any) => { + try { + if (editingCamera) { + await axios.put(`/api/cameras/${editingCamera.id}`, values); + message.success('更新成功'); + } else { + await axios.post('/api/cameras', values); + message.success('添加成功'); + } + setModalVisible(false); + fetchCameras(); + } catch (err) { + message.error('操作失败'); + } + }; + + const columns = [ + { + title: 'ID', + dataIndex: 'id', + key: 'id', + width: 60, + }, + { + title: '名称', + dataIndex: 'name', + key: 'name', + }, + { + title: 'RTSP地址', + dataIndex: 'rtsp_url', + key: 'rtsp_url', + ellipsis: true, + }, + { + title: '状态', + dataIndex: 'enabled', + key: 'enabled', + render: (enabled: boolean) => ( + + {enabled ? '启用' : '停用'} + + ), + }, + { + title: 'FPS限制', + dataIndex: 'fps_limit', + key: 'fps_limit', + }, + { + title: '处理间隔', + dataIndex: 'process_every_n_frames', + key: 'process_every_n_frames', + }, + { + title: '操作', + key: 'action', + render: (_: any, record: Camera) => ( + + + +
+ setModalVisible(false)} + onOk={() => form.submit()} + > +
+ + + + + + + + + + + + + +
+ + ); +}; + +export default CameraManagement; diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..35a892b --- /dev/null +++ b/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,179 @@ +import React, { useEffect, useState } from 'react'; +import { Card, Row, Col, Statistic, List, Tag, Button, Space, Timeline } from 'antd'; +import { AlertOutlined, VideoCameraOutlined, ClockCircleOutlined } from '@ant-design/icons'; +import axios from 'axios'; + +interface Alert { + id: number; + camera_id: number; + event_type: string; + confidence: number; + snapshot_path: string; + created_at: string; + processed: boolean; +} + +interface DashboardStats { + total: number; + unprocessed: number; + llm_pending: number; +} + +const Dashboard: React.FC = () => { + const [stats, setStats] = useState({ total: 0, unprocessed: 0, llm_pending: 0 }); + const [recentAlerts, setRecentAlerts] = useState([]); + const [cameraStatus, setCameraStatus] = useState([]); + + useEffect(() => { + fetchStats(); + fetchAlerts(); + fetchCameraStatus(); + + const interval = setInterval(() => { + fetchStats(); + fetchAlerts(); + }, 5000); + + return () => clearInterval(interval); + }, []); + + const fetchStats = async () => { + try { + const res = await axios.get('/api/alarms/stats'); + setStats(res.data); + } catch (err) { + console.error('获取统计数据失败', err); + } + }; + + const fetchAlerts = async () => { + try { + const res = await axios.get('/api/alarms?limit=5'); + setRecentAlerts(res.data); + } catch (err) { + console.error('获取告警失败', err); + } + }; + + const fetchCameraStatus = async () => { + try { + const res = await axios.get('/api/pipeline/status'); + const cameras = Object.entries(res.data.cameras || {}).map(([id, info]) => ({ + id, + ...info as any, + })); + setCameraStatus(cameras); + } catch (err) { + console.error('获取摄像头状态失败', err); + } + }; + + const getAlertColor = (type: string) => { + switch (type) { + case 'intrusion': + return 'red'; + case 'leave_post': + return 'orange'; + default: + return 'blue'; + } + }; + + const formatTime = (timeStr: string) => { + const date = new Date(timeStr); + return date.toLocaleString('zh-CN'); + }; + + return ( +
+ +
+ + } + /> + + + + + } + /> + + + + + } + /> + + + + + + + + ( + + + + {alert.event_type === 'intrusion' ? '周界入侵' : '离岗告警'} + + 摄像头 {alert.camera_id} + + {alert.confidence.toFixed(2)} + + + } + description={formatTime(alert.created_at)} + /> + {alert.snapshot_path && ( + + )} + + )} + /> + + + + + ( + + + + {cam.running ? '运行中' : '已停止'} + + {cam.fps?.toFixed(1) || 0} FPS + + } + /> + + )} + /> + + + + + ); +}; + +export default Dashboard;