feat(camera): Refactor camera management with new components, update localization keys, and enhance API integration

This commit is contained in:
2026-01-28 17:07:09 +07:00
parent ea07d0c99e
commit 9bc15192ec
9 changed files with 690 additions and 418 deletions

View File

@@ -8,8 +8,8 @@ export default {
'master.devices.title': 'Devices', 'master.devices.title': 'Devices',
'master.devices.name': 'Name', 'master.devices.name': 'Name',
'master.devices.name.tip': 'The device name', 'master.devices.name.tip': 'The device name',
'master.devices.external_id': 'External ID', 'master.devices.external_id': 'Hardware ID',
'master.devices.external_id.tip': 'The external identifier', 'master.devices.external_id.tip': 'The hardware identifier',
'master.devices.type': 'Type', 'master.devices.type': 'Type',
'master.devices.type.tip': 'The device type', 'master.devices.type.tip': 'The device type',
'master.devices.online': 'Online', 'master.devices.online': 'Online',

View File

@@ -8,7 +8,7 @@ export default {
'master.devices.title': 'Quản lý thiết bị', 'master.devices.title': 'Quản lý thiết bị',
'master.devices.name': 'Tên', 'master.devices.name': 'Tên',
'master.devices.name.tip': 'Tên thiết bị', 'master.devices.name.tip': 'Tên thiết bị',
'master.devices.external_id': 'External ID', 'master.devices.external_id': 'Hardware ID',
'master.devices.external_id.tip': 'Mã định danh bên ngoài', 'master.devices.external_id.tip': 'Mã định danh bên ngoài',
'master.devices.type': 'Loại', 'master.devices.type': 'Loại',
'master.devices.type.tip': 'Loại thiết bị', 'master.devices.type.tip': 'Loại thiết bị',

View File

@@ -0,0 +1,174 @@
import {
Button,
Col,
Form,
Input,
InputNumber,
Modal,
Row,
Select,
} from 'antd';
// Camera types
const CAMERA_TYPES = [
{ label: 'HIKVISION', value: 'HIKVISION' },
{ label: 'DAHUA', value: 'DAHUA' },
{ label: 'GENERIC', value: 'GENERIC' },
];
interface CameraFormValues {
name: string;
type: string;
account: string;
password: string;
ipAddress: string;
rtspPort: number;
httpPort: number;
stream: number;
channel: number;
}
interface CameraFormModalProps {
open: boolean;
onCancel: () => void;
onSubmit: (values: CameraFormValues) => void;
}
const CameraFormModal: React.FC<CameraFormModalProps> = ({
open,
onCancel,
onSubmit,
}) => {
const [form] = Form.useForm<CameraFormValues>();
const handleSubmit = async () => {
try {
const values = await form.validateFields();
onSubmit(values);
form.resetFields();
} catch (error) {
console.error('Validation failed:', error);
}
};
const handleCancel = () => {
form.resetFields();
onCancel();
};
return (
<Modal
title="Tạo mới camera"
open={open}
onCancel={handleCancel}
footer={[
<Button key="cancel" onClick={handleCancel}>
Hủy
</Button>,
<Button key="submit" type="primary" onClick={handleSubmit}>
Đng ý
</Button>,
]}
width={500}
>
<Form
form={form}
layout="vertical"
initialValues={{
type: 'HIKVISION',
rtspPort: 554,
httpPort: 80,
stream: 0,
channel: 0,
}}
>
<Form.Item
label="Tên"
name="name"
rules={[{ required: true, message: 'Vui lòng nhập tên' }]}
>
<Input placeholder="nhập dữ liệu" />
</Form.Item>
<Form.Item
label="Loại"
name="type"
rules={[{ required: true, message: 'Vui lòng chọn loại' }]}
>
<Select options={CAMERA_TYPES} />
</Form.Item>
<Form.Item
label="Tài khoản"
name="account"
rules={[{ required: true, message: 'Vui lòng nhập tài khoản' }]}
>
<Input placeholder="nhập tài khoản" autoComplete="off" />
</Form.Item>
<Form.Item
label="Mật khẩu"
name="password"
rules={[{ required: true, message: 'Vui lòng nhập mật khẩu' }]}
>
<Input.Password
placeholder="nhập mật khẩu"
autoComplete="new-password"
/>
</Form.Item>
<Form.Item
label="Địa chỉ IP"
name="ipAddress"
rules={[{ required: true, message: 'Vui lòng nhập địa chỉ IP' }]}
>
<Input placeholder="192.168.1.10" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Cổng RTSP"
name="rtspPort"
rules={[{ required: true, message: 'Vui lòng nhập cổng RTSP' }]}
>
<InputNumber style={{ width: '100%' }} min={0} max={65535} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Cổng HTTP"
name="httpPort"
rules={[{ required: true, message: 'Vui lòng nhập cổng HTTP' }]}
>
<InputNumber style={{ width: '100%' }} min={0} max={65535} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Luồng"
name="stream"
rules={[{ required: true, message: 'Vui lòng nhập luồng' }]}
>
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Kênh"
name="channel"
rules={[{ required: true, message: 'Vui lòng nhập kênh' }]}
>
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
);
};
export default CameraFormModal;

View File

@@ -0,0 +1,106 @@
import {
DeleteOutlined,
EditOutlined,
PlusOutlined,
ReloadOutlined,
} from '@ant-design/icons';
import { Button, Card, Checkbox, Space, Table, theme } from 'antd';
interface CameraTableProps {
cameraData: MasterModel.Camera[] | null;
onCreateCamera: () => void;
onReload?: () => void;
loading?: boolean;
}
const CameraTable: React.FC<CameraTableProps> = ({
cameraData,
onCreateCamera,
onReload,
loading = false,
}) => {
const { token } = theme.useToken();
const handleReload = () => {
console.log('Reload cameras');
onReload?.();
};
const handleDelete = () => {
console.log('Delete selected cameras');
// TODO: Implement delete functionality
};
const handleEdit = (camera: MasterModel.Camera) => {
console.log('Edit camera:', camera);
// TODO: Implement edit functionality
};
const columns = [
{
title: '',
dataIndex: 'checkbox',
width: 50,
render: () => <Checkbox />,
},
{
title: 'Tên',
dataIndex: 'name',
key: 'name',
render: (text: string) => (
<a style={{ color: token.colorPrimary }}>{text || '-'}</a>
),
},
{
title: 'Loại',
dataIndex: 'cate_id',
key: 'cate_id',
render: (text: string) => text || '-',
},
{
title: 'Địa chỉ IP',
dataIndex: 'ip',
key: 'ip',
render: (text: string) => text || '-',
},
{
title: 'Thao tác',
key: 'action',
render: (_: any, record: MasterModel.Camera) => (
<Button
size="small"
icon={<EditOutlined />}
onClick={() => handleEdit(record)}
/>
),
},
];
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} />
</Space>
<Table
dataSource={cameraData || []}
columns={columns}
rowKey="id"
size="small"
loading={loading}
pagination={{
size: 'small',
showTotal: (total: number, range: [number, number]) =>
`Hiển thị ${range[0]}-${range[1]} của ${total} camera`,
pageSize: 10,
}}
/>
</Card>
);
};
export default CameraTable;

View File

@@ -0,0 +1,55 @@
import { Button, Card, Select, Typography } from 'antd';
import { useState } from 'react';
const { Text } = Typography;
// Recording modes for V5 - chỉ có không ghi và ghi 24/24
const RECORDING_MODES = [
{ label: 'Không ghi', value: 'none' },
{ label: 'Ghi 24/24', value: '24/7' },
];
interface CameraV5Props {
thing: MasterModel.Thing | null;
initialRecordingMode?: string;
}
const CameraV5: React.FC<CameraV5Props> = ({
thing,
initialRecordingMode = 'none',
}) => {
const [recordingMode, setRecordingMode] = useState(initialRecordingMode);
console.log('ConfigCameraV5 - thing:', thing);
const handleSubmit = () => {
console.log('Submit recording mode:', recordingMode);
// TODO: Call API to save recording configuration
};
return (
<Card bodyStyle={{ padding: 16 }}>
{/* Recording Mode */}
<div style={{ marginBottom: 24 }}>
<Text strong style={{ display: 'block', marginBottom: 8 }}>
Ghi dữ liệu camera
</Text>
<Select
value={recordingMode}
onChange={setRecordingMode}
options={RECORDING_MODES}
style={{ width: 200 }}
/>
</div>
{/* Submit Button */}
<div style={{ textAlign: 'center' }}>
<Button type="primary" onClick={handleSubmit}>
Gửi đi
</Button>
</div>
</Card>
);
};
export default CameraV5;

View File

@@ -0,0 +1,192 @@
import { apiQueryConfigAlarm } from '@/services/master/MessageController';
import { useModel } from '@umijs/max';
import { Button, Card, Col, Row, Select, theme, Typography } from 'antd';
import { useEffect, useState } from 'react';
const { Text } = Typography;
// Recording modes for V6
const RECORDING_MODES = [
{ label: 'Không ghi', value: 'none' },
{ label: 'Theo cảnh báo', value: 'alarm' },
{ label: '24/24', value: 'all' },
];
interface CameraV6Props {
thing: MasterModel.Thing | null;
cameraConfig?: MasterModel.CameraV6 | null;
}
const CameraV6: React.FC<CameraV6Props> = ({ thing, cameraConfig }) => {
const { token } = theme.useToken();
const { initialState } = useModel('@@initialState');
const [selectedAlerts, setSelectedAlerts] = useState<string[]>([]);
const [recordingMode, setRecordingMode] = useState<'none' | 'alarm' | 'all'>(
'none',
);
const [alarmConfig, setAlarmConfig] = useState<MasterModel.Alarm[] | null>(
null,
);
// Initialize states from cameraConfig when it's available
useEffect(() => {
if (cameraConfig) {
// Set recording mode from config
if (cameraConfig.record_type) {
setRecordingMode(cameraConfig.record_type);
}
// Set selected alerts from config
if (
cameraConfig.record_alarm_list &&
Array.isArray(cameraConfig.record_alarm_list)
) {
setSelectedAlerts(cameraConfig.record_alarm_list);
}
}
}, [cameraConfig]);
// Fetch alarm config when thing data is available and recording mode is 'alarm'
useEffect(() => {
const fetchAlarmConfig = async () => {
if (
!thing ||
!initialState?.currentUserProfile?.metadata?.frontend_thing_key ||
recordingMode !== 'alarm'
) {
return;
}
try {
const resp = await apiQueryConfigAlarm(
thing.metadata?.data_channel_id || '',
initialState.currentUserProfile.metadata.frontend_thing_key,
{
offset: 0,
limit: 1,
subtopic: `config.${thing.metadata?.type}.alarms`,
},
);
if (resp.messages && resp.messages.length > 0) {
const parsed = resp.messages[0].string_value_parsed;
if (Array.isArray(parsed)) {
setAlarmConfig(parsed as MasterModel.Alarm[]);
} else {
setAlarmConfig([]);
}
}
} catch (error) {
console.error('Failed to fetch alarm config:', error);
}
};
fetchAlarmConfig();
}, [thing, initialState, recordingMode]);
const handleAlertToggle = (alertId: string) => {
if (selectedAlerts.includes(alertId)) {
setSelectedAlerts(selectedAlerts.filter((id) => id !== alertId));
} else {
setSelectedAlerts([...selectedAlerts, alertId]);
}
};
const handleClearAlerts = () => {
setSelectedAlerts([]);
};
const handleSubmitAlerts = () => {
console.log('Submit alerts:', {
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>
</div>
{/* Alert List - Only show when mode is 'alarm' */}
{recordingMode === 'alarm' && (
<div>
<Text strong className="block mb-2">
Danh sách cảnh báo
</Text>
<div
className="flex justify-between items-center mb-4 px-3 py-2 rounded border"
style={{
background: token.colorBgContainer,
borderColor: token.colorBorder,
}}
>
<Text type="secondary">đã chọn {selectedAlerts.length} mục</Text>
<Button type="link" onClick={handleClearAlerts}>
Xóa
</Button>
</div>
{/* Alert Cards Grid */}
<Row gutter={[12, 12]}>
{alarmConfig?.map((alarm) => {
const alarmId = alarm.id ?? '';
const isSelected =
alarmId !== '' && selectedAlerts.includes(alarmId);
return (
<Col xs={12} sm={8} md={6} lg={4} xl={4} key={alarmId}>
<Card
size="small"
hoverable
onClick={() => handleAlertToggle(alarmId)}
className="cursor-pointer h-20 flex items-center justify-center"
style={{
borderColor: isSelected
? token.colorPrimary
: token.colorBorder,
borderWidth: isSelected ? 2 : 1,
background: isSelected
? token.colorPrimaryBg
: token.colorBgContainer,
}}
>
<div className="p-2 text-center w-full">
<Text
className="text-xs break-words"
style={{
color: isSelected
? token.colorPrimary
: token.colorText,
}}
>
{alarm.name}
</Text>
</div>
</Card>
</Col>
);
})}
</Row>
</div>
)}
</Card>
);
};
export default CameraV6;

View File

@@ -1,120 +1,34 @@
import { apiQueryCamera } from '@/services/master/MessageController';
import { apiGetThingDetail } from '@/services/master/ThingController'; import { apiGetThingDetail } from '@/services/master/ThingController';
import { wsClient } from '@/utils/wsClient'; import { wsClient } from '@/utils/wsClient';
import { import { ArrowLeftOutlined } from '@ant-design/icons';
ArrowLeftOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
ReloadOutlined,
SettingOutlined,
} from '@ant-design/icons';
import { PageContainer } from '@ant-design/pro-components'; import { PageContainer } from '@ant-design/pro-components';
import { history, useParams } from '@umijs/max'; import { history, useModel, useParams } from '@umijs/max';
import { import { Button, Col, Row, Space, Spin } from 'antd';
Button,
Card,
Checkbox,
Col,
Form,
Input,
InputNumber,
Modal,
Row,
Select,
Space,
Spin,
Table,
theme,
Typography,
} from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import CameraFormModal from './components/CameraFormModal';
const { Text } = Typography; import CameraTable from './components/CameraTable';
import ConfigCameraV5 from './components/ConfigCameraV5';
// Camera types import ConfigCameraV6 from './components/ConfigCameraV6';
const CAMERA_TYPES = [
{ label: 'HIKVISION', value: 'HIKVISION' },
{ label: 'DAHUA', value: 'DAHUA' },
{ label: 'GENERIC', value: 'GENERIC' },
];
// Recording modes
const RECORDING_MODES = [
{ label: 'Theo cảnh báo', value: 'alarm' },
{ label: 'Liên tục', value: 'continuous' },
{ label: 'Thủ công', value: 'manual' },
];
// Alert types for configuration
const ALERT_TYPES = [
{ id: 'motion', name: 'Chuyển Động có cảnh báo' },
{ id: 'smoke', name: 'Khói có cảnh báo' },
{ id: 'door', name: 'Cửa có cảnh báo' },
{ id: 'ac1_high', name: 'Điện AC 1 cao' },
{ id: 'ac1_low', name: 'Điện AC 1 thấp' },
{ id: 'ac1_lost', name: 'Điện AC 1 mất' },
{ id: 'load_high', name: 'Điện tải cao' },
{ id: 'load_low', name: 'Điện tải thấp' },
{ id: 'load_lost', name: 'Điện tải mất' },
{ id: 'grid_high', name: 'Điện lưới cao' },
{ id: 'grid_low', name: 'Điện lưới thấp' },
{ id: 'grid_lost', name: 'Điện lưới mất' },
{ id: 'ac1_on_error', name: 'Điều hòa 1 bật lỗi' },
{ id: 'ac1_off_error', name: 'Điều hòa 1 tắt lỗi' },
{ id: 'ac1_has_error', name: 'Điều hòa 1 có thể lỗi' },
{ id: 'ac2_on_error', name: 'Điều hòa 2 bật lỗi' },
{ id: 'ac2_off_error', name: 'Điều hòa 2 tắt lỗi' },
{ id: 'ac2_has_error', name: 'Điều hòa 2 điều hòa có thể lỗi' },
{ id: 'room_temp_high', name: 'Nhiệt độ phòng máy nhiệt độ phòng máy cao' },
{ id: 'rectifier_error', name: 'Rectifier bật lỗi' },
{ id: 'meter_volt_high', name: 'Công tơ điện điện áp cao' },
{ id: 'meter_volt_low', name: 'Công tơ điện điện áp thấp' },
{ id: 'meter_lost', name: 'Công tơ điện mất điện áp' },
{ id: 'lithium_volt_low', name: 'Pin lithium điện áp thấp' },
{ id: 'lithium_temp_high', name: 'Pin lithium nhiệt độ cao' },
{ id: 'lithium_capacity_low', name: 'Pin lithium dung lượng thấp' },
];
// Camera interface
interface Camera {
id: string;
name: string;
type: string;
ipAddress: string;
}
interface CameraFormValues {
name: string;
type: string;
account: string;
password: string;
ipAddress: string;
rtspPort: number;
httpPort: number;
stream: number;
channel: number;
}
const CameraConfigPage = () => { const CameraConfigPage = () => {
const { thingId } = useParams<{ thingId: string }>(); const { thingId } = useParams<{ thingId: string }>();
const { token } = theme.useToken();
const [form] = Form.useForm<CameraFormValues>();
const [isModalVisible, setIsModalVisible] = useState(false); const [isModalVisible, setIsModalVisible] = useState(false);
const [cameras, setCameras] = useState<Camera[]>([]);
const [selectedAlerts, setSelectedAlerts] = useState<string[]>([
'motion',
'smoke',
'door',
]);
const [recordingMode, setRecordingMode] = useState('alarm');
const [thingName, setThingName] = useState<string>('');
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [cameraLoading, setCameraLoading] = useState(false);
const [thing, setThing] = useState<MasterModel.Thing | null>(null);
const [cameras, setCameras] = useState<MasterModel.Camera[] | null>([]);
const [cameraConfig, setCameraConfig] = useState<MasterModel.CameraV6 | null>(
null,
);
const { initialState } = useModel('@@initialState');
useEffect(() => { useEffect(() => {
wsClient.connect('/mqtt', false); wsClient.connect('/mqtt', false);
const unsubscribe = wsClient.subscribe((data: any) => { const unsubscribe = wsClient.subscribe((data: any) => {
console.log('Received WS data:', data); console.log('Received WS data:', data);
}); });
return () => { return () => {
unsubscribe(); unsubscribe();
}; };
@@ -124,110 +38,90 @@ const CameraConfigPage = () => {
useEffect(() => { useEffect(() => {
const fetchThingInfo = async () => { const fetchThingInfo = async () => {
if (!thingId) return; if (!thingId) return;
try { try {
setLoading(true); setLoading(true);
const thing = await apiGetThingDetail(thingId); const thingData = await apiGetThingDetail(thingId);
setThingName(thing.name || thingId); setThing(thingData);
} catch (error) { } catch (error) {
console.error('Failed to fetch thing info:', error); console.error('Failed to fetch thing info:', error);
setThingName(thingId);
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
fetchThingInfo(); fetchThingInfo();
}, [thingId]); }, [thingId]);
const handleBack = () => { // Fetch camera config when thing data is available
history.push('/manager/devices'); const fetchCameraConfig = async () => {
if (
!thing ||
!initialState?.currentUserProfile?.metadata?.frontend_thing_key
) {
return;
}
try {
setCameraLoading(true);
const resp = await apiQueryCamera(
thing.metadata?.data_channel_id || '',
initialState.currentUserProfile.metadata.frontend_thing_key,
{
offset: 0,
limit: 1,
subtopic: `config.${thing.metadata?.type}.cameras`,
},
);
if (resp.messages!.length > 0) {
setCameras(
resp.messages![0].string_value_parsed?.cams as MasterModel.Camera[],
);
setCameraConfig(
resp.messages![0].string_value_parsed as MasterModel.CameraV6,
);
}
} catch (error) {
console.error('Failed to fetch camera config:', error);
} finally {
setCameraLoading(false);
}
}; };
useEffect(() => {
fetchCameraConfig();
}, [thing, initialState]);
const handleOpenModal = () => { const handleOpenModal = () => {
form.resetFields();
form.setFieldsValue({
type: 'HIKVISION',
rtspPort: 554,
httpPort: 80,
stream: 0,
channel: 0,
});
setIsModalVisible(true); setIsModalVisible(true);
}; };
const handleCloseModal = () => { const handleCloseModal = () => {
setIsModalVisible(false); setIsModalVisible(false);
form.resetFields();
}; };
const handleSubmitCamera = async () => { const handleSubmitCamera = (values: any) => {
try {
const values = await form.validateFields();
console.log('Camera values:', values); console.log('Camera values:', values);
// TODO: Call API to create camera // TODO: Call API to create camera
setCameras([
...cameras,
{
id: String(cameras.length + 1),
name: values.name,
type: values.type,
ipAddress: values.ipAddress,
},
]);
handleCloseModal(); handleCloseModal();
} catch (error) { };
console.error('Validation failed:', error);
// Helper function to determine which camera component to render
const renderCameraRecordingComponent = () => {
const thingType = thing?.metadata?.type;
if (thingType === 'gmsv5') {
return <ConfigCameraV5 thing={thing} />;
} }
};
const handleAlertToggle = (alertId: string) => { if (thingType === 'spole' || thingType === 'gmsv6') {
if (selectedAlerts.includes(alertId)) { return <ConfigCameraV6 thing={thing} cameraConfig={cameraConfig} />;
setSelectedAlerts(selectedAlerts.filter((id) => id !== alertId));
} else {
setSelectedAlerts([...selectedAlerts, alertId]);
} }
};
const handleClearAlerts = () => { return <ConfigCameraV6 thing={thing} cameraConfig={cameraConfig} />;
setSelectedAlerts([]);
}; };
const handleSubmitAlerts = () => {
console.log('Submit alerts:', selectedAlerts);
// TODO: Call API to save alert configuration
};
const columns = [
{
title: '',
dataIndex: 'checkbox',
width: 50,
render: () => <Checkbox />,
},
{
title: 'Tên',
dataIndex: 'name',
key: 'name',
render: (text: string) => (
<a style={{ color: token.colorPrimary }}>{text}</a>
),
},
{
title: 'Loại',
dataIndex: 'type',
key: 'type',
},
{
title: 'Địa chỉ IP',
dataIndex: 'ipAddress',
key: 'ipAddress',
},
{
title: 'Thao tác',
key: 'action',
render: () => <Button size="small" icon={<EditOutlined />} />,
},
];
return ( return (
<Spin spinning={loading}> <Spin spinning={loading}>
<PageContainer <PageContainer
@@ -237,9 +131,9 @@ const CameraConfigPage = () => {
<Button <Button
type="text" type="text"
icon={<ArrowLeftOutlined />} icon={<ArrowLeftOutlined />}
onClick={handleBack} onClick={() => history.push('/manager/devices')}
/> />
<span>{thingName || 'Loading...'}</span> <span>{thing?.name || 'Loading...'}</span>
</Space> </Space>
), ),
}} }}
@@ -247,248 +141,26 @@ const CameraConfigPage = () => {
<Row gutter={24}> <Row gutter={24}>
{/* Left Column - Camera Table */} {/* Left Column - Camera Table */}
<Col xs={24} md={10} lg={8}> <Col xs={24} md={10} lg={8}>
<Card bodyStyle={{ padding: 16 }}> <CameraTable
<Space style={{ marginBottom: 16 }}> cameraData={cameras}
<Button onCreateCamera={handleOpenModal}
type="primary" onReload={fetchCameraConfig}
icon={<PlusOutlined />} loading={cameraLoading}
onClick={handleOpenModal}
>
Tạo mới camera
</Button>
<Button icon={<ReloadOutlined />} />
<Button icon={<SettingOutlined />} />
<Button icon={<DeleteOutlined />} />
</Space>
<Table
dataSource={cameras}
columns={columns}
rowKey="id"
size="small"
pagination={{
size: 'small',
showTotal: (total, range) =>
`${range[0]}-${range[1]} trên ${total} mặt hàng`,
pageSize: 10,
}}
/> />
</Card>
</Col> </Col>
{/* Right Column - Alert Configuration */} {/* Right Column - Camera Recording Configuration */}
<Col xs={24} md={14} lg={16}> <Col xs={24} md={14} lg={16}>
<Card bodyStyle={{ padding: 16 }}> {renderCameraRecordingComponent()}
{/* Recording Mode */}
<div style={{ marginBottom: 24 }}>
<Text strong style={{ display: 'block', marginBottom: 8 }}>
Ghi dữ liệu camera
</Text>
<Select
value={recordingMode}
onChange={setRecordingMode}
options={RECORDING_MODES}
style={{ width: 200 }}
/>
</div>
{/* Alert List */}
<div>
<Text strong style={{ display: 'block', marginBottom: 8 }}>
Danh sách cảnh báo
</Text>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 16,
padding: '8px 12px',
background: token.colorBgContainer,
borderRadius: token.borderRadius,
border: `1px solid ${token.colorBorder}`,
}}
>
<Text type="secondary">
đã chọn {selectedAlerts.length} mục
</Text>
<Button type="link" onClick={handleClearAlerts}>
Xóa
</Button>
</div>
{/* Alert Cards Grid */}
<Row gutter={[12, 12]}>
{ALERT_TYPES.map((alert) => {
const isSelected = selectedAlerts.includes(alert.id);
return (
<Col xs={12} sm={8} md={6} lg={4} xl={4} key={alert.id}>
<Card
size="small"
hoverable
onClick={() => handleAlertToggle(alert.id)}
style={{
cursor: 'pointer',
borderColor: isSelected
? token.colorPrimary
: token.colorBorder,
borderWidth: isSelected ? 2 : 1,
background: isSelected
? token.colorPrimaryBg
: token.colorBgContainer,
height: 80,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
bodyStyle={{
padding: 8,
textAlign: 'center',
width: '100%',
}}
>
<Text
style={{
fontSize: 12,
color: isSelected
? token.colorPrimary
: token.colorText,
wordBreak: 'break-word',
}}
>
{alert.name}
</Text>
</Card>
</Col>
);
})}
</Row>
{/* Submit Button */}
<div style={{ marginTop: 24, textAlign: 'center' }}>
<Button type="primary" onClick={handleSubmitAlerts}>
Gửi đi
</Button>
</div>
</div>
</Card>
</Col> </Col>
</Row> </Row>
{/* Create Camera Modal */} {/* Create Camera Modal */}
<Modal <CameraFormModal
title="Tạo mới"
open={isModalVisible} open={isModalVisible}
onCancel={handleCloseModal} onCancel={handleCloseModal}
footer={[ onSubmit={handleSubmitCamera}
<Button key="cancel" onClick={handleCloseModal}> />
Hủy
</Button>,
<Button key="submit" type="primary" onClick={handleSubmitCamera}>
Đng ý
</Button>,
]}
width={500}
>
<Form
form={form}
layout="vertical"
initialValues={{
type: 'HIKVISION',
rtspPort: 554,
httpPort: 80,
stream: 0,
channel: 0,
}}
>
<Form.Item
label="Tên"
name="name"
rules={[{ required: true, message: 'Vui lòng nhập tên' }]}
>
<Input placeholder="nhập dữ liệu" />
</Form.Item>
<Form.Item
label="Loại"
name="type"
rules={[{ required: true, message: 'Vui lòng chọn loại' }]}
>
<Select options={CAMERA_TYPES} />
</Form.Item>
<Form.Item
label="Tài khoản"
name="account"
rules={[{ required: true, message: 'Vui lòng nhập tài khoản' }]}
>
<Input placeholder="nhập tài khoản" />
</Form.Item>
<Form.Item
label="Mật khẩu"
name="password"
rules={[{ required: true, message: 'Vui lòng nhập mật khẩu' }]}
>
<Input.Password placeholder="nhập mật khẩu" />
</Form.Item>
<Form.Item
label="Địa chỉ IP"
name="ipAddress"
rules={[{ required: true, message: 'Vui lòng nhập địa chỉ IP' }]}
>
<Input placeholder="192.168.1.10" />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Cổng RTSP"
name="rtspPort"
rules={[
{ required: true, message: 'Vui lòng nhập cổng RTSP' },
]}
>
<InputNumber style={{ width: '100%' }} min={0} max={65535} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Cổng HTTP"
name="httpPort"
rules={[
{ required: true, message: 'Vui lòng nhập cổng HTTP' },
]}
>
<InputNumber style={{ width: '100%' }} min={0} max={65535} />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
label="Luồng"
name="stream"
rules={[{ required: true, message: 'Vui lòng nhập luồng' }]}
>
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
label="Kênh"
name="channel"
rules={[{ required: true, message: 'Vui lòng nhập kênh' }]}
>
<InputNumber style={{ width: '100%' }} min={0} />
</Form.Item>
</Col>
</Row>
</Form>
</Modal>
</PageContainer> </PageContainer>
</Spin> </Spin>
); );

View File

@@ -145,3 +145,69 @@ export async function apiQueryNodeConfigMessage(
return resp; return resp;
} }
export async function apiQueryCamera(
dataChanelId: string,
authorization: string,
params: MasterModel.SearchMessagePaginationBody,
) {
const resp = await request<MasterModel.CameraV6MessageResponse>(
`${API_READER}/${dataChanelId}/messages`,
{
method: 'GET',
headers: {
Authorization: authorization,
},
params: params,
},
);
// Process messages to add string_value_parsed
if (resp.messages) {
resp.messages = resp.messages.map((message) => {
if (message.string_value) {
try {
message.string_value_parsed = JSON.parse(message.string_value);
} catch (error) {
console.error('Failed to parse string_value:', error);
}
}
return message;
});
}
return resp;
}
export async function apiQueryConfigAlarm(
dataChanelId: string,
authorization: string,
params: MasterModel.SearchMessagePaginationBody,
) {
const resp = await request<MasterModel.AlarmMessageResponse>(
`${API_READER}/${dataChanelId}/messages`,
{
method: 'GET',
headers: {
Authorization: authorization,
},
params: params,
},
);
// Process messages to add string_value_parsed
if (resp.messages) {
resp.messages = resp.messages.map((message) => {
if (message.string_value) {
try {
message.string_value_parsed = JSON.parse(message.string_value);
} catch (error) {
console.error('Failed to parse string_value:', error);
}
}
return message;
});
}
return resp;
}

View File

@@ -22,6 +22,7 @@ declare namespace MasterModel {
// Response types cho từng domain // Response types cho từng domain
type CameraMessageResponse = MesageReaderResponse<CameraV5>; type CameraMessageResponse = MesageReaderResponse<CameraV5>;
type CameraV6MessageResponse = MesageReaderResponse<CameraV6>; type CameraV6MessageResponse = MesageReaderResponse<CameraV6>;
type AlarmMessageResponse = MesageReaderResponse<Alarm>;
type NodeConfigMessageResponse = MesageReaderResponse<NodeConfig[]>; type NodeConfigMessageResponse = MesageReaderResponse<NodeConfig[]>;
type MessageDataType = NodeConfig[] | CameraV5 | CameraV6; type MessageDataType = NodeConfig[] | CameraV5 | CameraV6;
@@ -46,7 +47,7 @@ declare namespace MasterModel {
cams?: Camera[]; cams?: Camera[];
} }
interface CameraV6 extends CameraV5 { interface CameraV6 extends CameraV5 {
record_type?: string; record_type?: 'none' | 'alarm' | 'all';
record_alarm_list?: string[]; record_alarm_list?: string[];
} }
@@ -62,4 +63,10 @@ declare namespace MasterModel {
ip?: string; ip?: string;
stream?: number; stream?: number;
} }
interface Alarm {
id: string;
type: Type;
name: string;
}
} }