feat: add MQTT client for camera configuration and enhance camera management

This commit is contained in:
2026-02-08 11:58:57 +07:00
parent 78162fc0cb
commit d619534a73
14 changed files with 1254 additions and 111 deletions

View File

@@ -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}

View File

@@ -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]) =>

View File

@@ -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>