feat: add MQTT client for camera configuration and enhance camera management
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
Row,
|
||||
Select,
|
||||
} from 'antd';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
// Camera types
|
||||
const CAMERA_TYPES = [
|
||||
@@ -31,20 +32,58 @@ interface CameraFormValues {
|
||||
interface CameraFormModalProps {
|
||||
open: boolean;
|
||||
onCancel: () => void;
|
||||
onSubmit: (values: CameraFormValues) => void;
|
||||
onSubmit: (camera: MasterModel.Camera) => void;
|
||||
isOnline?: boolean;
|
||||
editingCamera?: MasterModel.Camera | null;
|
||||
}
|
||||
|
||||
const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
||||
open,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
isOnline = true,
|
||||
editingCamera,
|
||||
}) => {
|
||||
const [form] = Form.useForm<CameraFormValues>();
|
||||
const isEditMode = !!editingCamera;
|
||||
|
||||
// Populate form when editing
|
||||
useEffect(() => {
|
||||
if (open && editingCamera) {
|
||||
form.setFieldsValue({
|
||||
name: editingCamera.name || '',
|
||||
type: editingCamera.cate_id || 'HIKVISION',
|
||||
account: editingCamera.username || '',
|
||||
password: editingCamera.password || '',
|
||||
ipAddress: editingCamera.ip || '',
|
||||
rtspPort: editingCamera.rtsp_port || 554,
|
||||
httpPort: editingCamera.http_port || 80,
|
||||
stream: editingCamera.stream || 0,
|
||||
channel: editingCamera.channel || 0,
|
||||
});
|
||||
} else if (open) {
|
||||
form.resetFields();
|
||||
}
|
||||
}, [open, editingCamera, form]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
onSubmit(values);
|
||||
// Convert form values to MasterModel.Camera format
|
||||
const camera: MasterModel.Camera = {
|
||||
// Keep existing ID when editing, generate new ID when creating
|
||||
id: editingCamera?.id || `cam_${Date.now()}`,
|
||||
name: values.name,
|
||||
cate_id: values.type,
|
||||
ip: values.ipAddress,
|
||||
rtsp_port: values.rtspPort,
|
||||
http_port: values.httpPort,
|
||||
stream: values.stream,
|
||||
channel: values.channel,
|
||||
username: values.account,
|
||||
password: values.password,
|
||||
};
|
||||
onSubmit(camera);
|
||||
form.resetFields();
|
||||
} catch (error) {
|
||||
console.error('Validation failed:', error);
|
||||
@@ -58,7 +97,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="Tạo mới camera"
|
||||
title={isEditMode ? 'Chỉnh sửa camera' : 'Tạo mới camera'}
|
||||
open={open}
|
||||
onCancel={handleCancel}
|
||||
footer={[
|
||||
@@ -66,7 +105,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
||||
Hủy
|
||||
</Button>,
|
||||
<Button key="submit" type="primary" onClick={handleSubmit}>
|
||||
Đồng ý
|
||||
{isEditMode ? 'Cập nhật' : 'Đồng ý'}
|
||||
</Button>,
|
||||
]}
|
||||
width={500}
|
||||
|
||||
@@ -4,22 +4,30 @@ import {
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { Button, Card, Checkbox, Space, Table, theme } from 'antd';
|
||||
import { Button, Card, Space, Table, theme, Tooltip } from 'antd';
|
||||
import { useState } from 'react';
|
||||
|
||||
interface CameraTableProps {
|
||||
cameraData: MasterModel.Camera[] | null;
|
||||
onCreateCamera: () => void;
|
||||
onEditCamera?: (camera: MasterModel.Camera) => void;
|
||||
onDeleteCameras?: (cameraIds: string[]) => void;
|
||||
onReload?: () => void;
|
||||
loading?: boolean;
|
||||
isOnline?: boolean;
|
||||
}
|
||||
|
||||
const CameraTable: React.FC<CameraTableProps> = ({
|
||||
cameraData,
|
||||
onCreateCamera,
|
||||
onEditCamera,
|
||||
onDeleteCameras,
|
||||
onReload,
|
||||
loading = false,
|
||||
isOnline = false,
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||
|
||||
const handleReload = () => {
|
||||
console.log('Reload cameras');
|
||||
@@ -27,22 +35,18 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
console.log('Delete selected cameras');
|
||||
// TODO: Implement delete functionality
|
||||
if (selectedRowKeys.length === 0) {
|
||||
return;
|
||||
}
|
||||
onDeleteCameras?.(selectedRowKeys as string[]);
|
||||
setSelectedRowKeys([]);
|
||||
};
|
||||
|
||||
const handleEdit = (camera: MasterModel.Camera) => {
|
||||
console.log('Edit camera:', camera);
|
||||
// TODO: Implement edit functionality
|
||||
onEditCamera?.(camera);
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '',
|
||||
dataIndex: 'checkbox',
|
||||
width: 50,
|
||||
render: () => <Checkbox />,
|
||||
},
|
||||
{
|
||||
title: 'Tên',
|
||||
dataIndex: 'name',
|
||||
@@ -67,11 +71,14 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
||||
title: 'Thao tác',
|
||||
key: 'action',
|
||||
render: (_: any, record: MasterModel.Camera) => (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
/>
|
||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
disabled={!isOnline}
|
||||
/>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -79,11 +86,29 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
||||
return (
|
||||
<Card bodyStyle={{ padding: 16 }}>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={onCreateCamera}>
|
||||
Tạo mới camera
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={handleReload} />
|
||||
<Button icon={<DeleteOutlined />} onClick={handleDelete} />
|
||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={onCreateCamera}
|
||||
disabled={!isOnline}
|
||||
>
|
||||
Tạo mới camera
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReload}
|
||||
loading={loading}
|
||||
/>
|
||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||
<Button
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={handleDelete}
|
||||
disabled={!isOnline || selectedRowKeys.length === 0}
|
||||
danger
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
@@ -92,6 +117,12 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
rowSelection={{
|
||||
type: 'checkbox',
|
||||
selectedRowKeys,
|
||||
onChange: (newSelectedRowKeys) =>
|
||||
setSelectedRowKeys(newSelectedRowKeys),
|
||||
}}
|
||||
pagination={{
|
||||
size: 'small',
|
||||
showTotal: (total: number, range: [number, number]) =>
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import { apiQueryConfigAlarm } from '@/services/master/MessageController';
|
||||
import { useModel } from '@umijs/max';
|
||||
import { Button, Card, Col, Row, Select, theme, Typography } from 'antd';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Row,
|
||||
Select,
|
||||
theme,
|
||||
Tooltip,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const { Text } = Typography;
|
||||
@@ -15,15 +24,24 @@ const RECORDING_MODES = [
|
||||
interface CameraV6Props {
|
||||
thing: MasterModel.Thing | null;
|
||||
cameraConfig?: MasterModel.CameraV6 | null;
|
||||
onSubmit?: (config: {
|
||||
recordingMode: MasterModel.CameraV6['record_type'];
|
||||
selectedAlerts: string[];
|
||||
}) => void;
|
||||
isOnline?: boolean;
|
||||
}
|
||||
|
||||
const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
||||
const CameraV6: React.FC<CameraV6Props> = ({
|
||||
thing,
|
||||
cameraConfig,
|
||||
onSubmit,
|
||||
isOnline = false,
|
||||
}) => {
|
||||
const { token } = theme.useToken();
|
||||
const { initialState } = useModel('@@initialState');
|
||||
const [selectedAlerts, setSelectedAlerts] = useState<string[]>([]);
|
||||
const [recordingMode, setRecordingMode] = useState<'none' | 'alarm' | 'all'>(
|
||||
'none',
|
||||
);
|
||||
const [recordingMode, setRecordingMode] =
|
||||
useState<MasterModel.CameraV6['record_type']>('none');
|
||||
const [alarmConfig, setAlarmConfig] = useState<MasterModel.Alarm[] | null>(
|
||||
null,
|
||||
);
|
||||
@@ -95,31 +113,39 @@ const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
||||
setSelectedAlerts([]);
|
||||
};
|
||||
|
||||
const handleSubmitAlerts = () => {
|
||||
console.log('Submit alerts:', {
|
||||
const handleSubmitConfig = () => {
|
||||
onSubmit?.({
|
||||
recordingMode,
|
||||
selectedAlerts,
|
||||
});
|
||||
// TODO: Call API to save alert configuration
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
{/* Recording Mode */}
|
||||
<div className="mb-6">
|
||||
<Text strong className="block mb-2">
|
||||
Ghi dữ liệu camera
|
||||
</Text>
|
||||
<div className="flex gap-8 items-center">
|
||||
<Select
|
||||
value={recordingMode}
|
||||
onChange={setRecordingMode}
|
||||
options={RECORDING_MODES}
|
||||
className="w-full sm:w-1/2 md:w-1/3 lg:w-1/4"
|
||||
/>
|
||||
<Button type="primary" onClick={handleSubmitAlerts}>
|
||||
Gửi đi
|
||||
</Button>
|
||||
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4 items-start sm:items-end">
|
||||
<div className="w-full sm:w-1/3 lg:w-1/4">
|
||||
<Text strong className="block mb-2">
|
||||
Ghi dữ liệu camera
|
||||
</Text>
|
||||
<Select
|
||||
value={recordingMode}
|
||||
onChange={setRecordingMode}
|
||||
options={RECORDING_MODES}
|
||||
className="w-full"
|
||||
popupMatchSelectWidth={false}
|
||||
/>
|
||||
</div>
|
||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSubmitConfig}
|
||||
disabled={!isOnline}
|
||||
>
|
||||
Gửi đi
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -155,7 +181,7 @@ const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
||||
size="small"
|
||||
hoverable
|
||||
onClick={() => handleAlertToggle(alarmId)}
|
||||
className="cursor-pointer h-20 flex items-center justify-center"
|
||||
className="cursor-pointer h-24 flex items-center justify-center"
|
||||
style={{
|
||||
borderColor: isSelected
|
||||
? token.colorPrimary
|
||||
@@ -166,14 +192,21 @@ const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
|
||||
: token.colorBgContainer,
|
||||
}}
|
||||
>
|
||||
<div className="p-2 text-center w-full">
|
||||
<div className="p-1 text-center w-full flex items-center justify-center h-full">
|
||||
<Text
|
||||
className="text-xs break-words"
|
||||
className="text-xs"
|
||||
style={{
|
||||
color: isSelected
|
||||
? token.colorPrimary
|
||||
: token.colorText,
|
||||
display: '-webkit-box',
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: 'vertical',
|
||||
overflow: 'hidden',
|
||||
wordBreak: 'break-word',
|
||||
lineHeight: '1.2em',
|
||||
}}
|
||||
title={alarm.name}
|
||||
>
|
||||
{alarm.name}
|
||||
</Text>
|
||||
|
||||
Reference in New Issue
Block a user