import React, { useEffect, useState, useRef } from 'react'; import { Card, Button, Space, Select, message, Drawer, Form, Input, InputNumber, Switch, TimePicker, Divider } from 'antd'; import { Stage, Layer, Rect, Line, Circle, Text as KonvaText } from 'react-konva'; import { RangePickerProps } from 'antd/es/date-picker'; import axios from 'axios'; import dayjs from 'dayjs'; interface WorkingHours { start: number[]; end: number[]; } interface ROI { id: number; name: string; type: string; points: number[][]; rule: string; enabled: boolean; threshold_sec: number; confirm_sec: number; return_sec: number; working_hours: WorkingHours[] | null; } 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 [imageDim, setImageDim] = useState({ width: 800, height: 600 }); const [selectedROI, setSelectedROI] = useState(null); const [drawerVisible, setDrawerVisible] = useState(false); const [form] = Form.useForm(); const [workingHoursList, setWorkingHoursList] = useState<{start: dayjs.Dayjs | null, end: dayjs.Dayjs | null}[]>([]); const [isDrawing, setIsDrawing] = useState(false); const [tempPoints, setTempPoints] = useState([]); const [backgroundImage, setBackgroundImage] = useState(null); const stageRef = useRef(null); const addWorkingHours = () => { setWorkingHoursList([...workingHoursList, { start: null, end: null }]); }; const removeWorkingHours = (index: number) => { const newList = workingHoursList.filter((_, i) => i !== index); setWorkingHoursList(newList); }; const updateWorkingHours = (index: number, field: 'start' | 'end', value: dayjs.Dayjs | null) => { const newList = [...workingHoursList]; newList[index] = { ...newList[index], [field]: value }; setWorkingHoursList(newList); }; const updateWorkingHoursRange = (index: number, start: dayjs.Dayjs | null, end: dayjs.Dayjs | null) => { setWorkingHoursList(prev => { const newList = [...prev]; if (newList[index]) { newList[index] = { start, end }; } return newList; }); }; 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]); useEffect(() => { if (snapshot) { 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 }); setBackgroundImage(img); }; img.src = `data:image/jpeg;base64,${snapshot}`; } }, [snapshot]); const fetchSnapshot = async () => { if (!selectedCamera) return; try { const res = await axios.get(`/api/camera/${selectedCamera}/snapshot/base64`); setSnapshot(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 { const workingHours = workingHoursList .filter(item => item.start && item.end) .map(item => ({ start: [item.start!.hour(), item.start!.minute()], end: [item.end!.hour(), item.end!.minute()], })); await axios.put(`/api/camera/${selectedCamera}/roi/${selectedROI.id}`, { name: values.name, roi_type: values.roi_type, rule_type: values.rule_type, threshold_sec: values.threshold_sec, confirm_sec: values.confirm_sec, working_hours: workingHours, enabled: values.enabled, }); message.success('保存成功'); setDrawerVisible(false); setWorkingHoursList([]); fetchROIs(); } catch (err: any) { message.error(`保存失败: ${err.response?.data?.detail || '未知错误'}`); } }; const handleAddROI = () => { if (!selectedCamera) { message.warning('请先选择摄像头'); return; } setIsDrawing(true); setTempPoints([]); setSelectedROI(null); message.info('点击画布绘制ROI区域,双击完成绘制'); }; const handleStageClick = (e: any) => { if (!isDrawing) return; const stage = e.target.getStage(); const pos = stage.getPointerPosition(); if (pos) { setTempPoints(prev => [...prev, [pos.x, pos.y]]); } }; const handleStageDblClick = () => { if (!isDrawing || tempPoints.length < 3) { if (tempPoints.length > 0 && tempPoints.length < 3) { message.warning('至少需要3个点才能形成多边形'); } return; } const roi_id = `roi_${Date.now()}`; axios.post(`/api/camera/${selectedCamera}/roi`, { roi_id, name: `区域${rois.length + 1}`, roi_type: 'polygon', points: tempPoints, rule_type: 'intrusion', threshold_sec: 60, confirm_sec: 5, return_sec: 5, working_hours: [], }) .then(() => { message.success('ROI添加成功'); setIsDrawing(false); setTempPoints([]); fetchROIs(); }) .catch((err) => { message.error(`添加失败: ${err.response?.data?.detail || '未知错误'}`); setIsDrawing(false); setTempPoints([]); }); }; const handleCancelDrawing = () => { setIsDrawing(false); setTempPoints([]); message.info('已取消绘制'); }; const handleDeleteROI = async (roiId: number) => { if (!selectedCamera) return; try { await axios.delete(`/api/camera/${selectedCamera}/roi/${roiId}`); message.success('删除成功'); if (selectedROI?.id === roiId) { setSelectedROI(null); setDrawerVisible(false); } fetchROIs(); } catch (err: any) { message.error(`删除失败: ${err.response?.data?.detail || '未知错误'}`); } }; const getROIStrokeColor = (rule: string) => { return rule === 'intrusion' ? '#ff4d4f' : '#faad14'; }; const renderROI = (roi: ROI) => { const points = roi.points.flat(); const color = getROIStrokeColor(roi.rule); const isSelected = selectedROI?.id === roi.id; return ( { setSelectedROI(roi); form.setFieldsValue({ name: roi.name, roi_type: roi.type, rule_type: roi.rule, threshold_sec: roi.threshold_sec, confirm_sec: roi.confirm_sec, enabled: roi.enabled, }); setWorkingHoursList(roi.working_hours?.map((wh: WorkingHours) => ({ start: wh.start ? dayjs().hour(wh.start[0]).minute(wh.start[1]) : null, end: wh.end ? dayjs().hour(wh.end[0]).minute(wh.end[1]) : null, })) || []); setDrawerVisible(true); }} onMouseEnter={(e) => { const container = e.target.getStage()?.container(); if (container) { container.style.cursor = 'pointer'; } }} onMouseLeave={(e) => { const container = e.target.getStage()?.container(); if (container) { container.style.cursor = 'default'; } }} /> ); }; return (
{selectedROI?.rule === 'leave_post' && ( <> 工作时间配置(可选)
{workingHoursList.map((item, index) => ( { if (dates && Array.isArray(dates) && dates.length >= 2 && dates[0] && dates[1]) { updateWorkingHoursRange(index, dates[0], dates[1]); } else { updateWorkingHoursRange(index, null, null); } }} /> ))}
不配置工作时间则使用系统全局设置 )}
); }; export default ROIEditor;