feat(camera): Refactor camera management with new components, update localization keys, and enhance API integration
This commit is contained in:
174
src/pages/Manager/Device/Camera/components/CameraFormModal.tsx
Normal file
174
src/pages/Manager/Device/Camera/components/CameraFormModal.tsx
Normal 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;
|
||||
106
src/pages/Manager/Device/Camera/components/CameraTable.tsx
Normal file
106
src/pages/Manager/Device/Camera/components/CameraTable.tsx
Normal 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;
|
||||
@@ -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;
|
||||
192
src/pages/Manager/Device/Camera/components/ConfigCameraV6.tsx
Normal file
192
src/pages/Manager/Device/Camera/components/ConfigCameraV6.tsx
Normal 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;
|
||||
@@ -1,120 +1,34 @@
|
||||
import { apiQueryCamera } from '@/services/master/MessageController';
|
||||
import { apiGetThingDetail } from '@/services/master/ThingController';
|
||||
import { wsClient } from '@/utils/wsClient';
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
DeleteOutlined,
|
||||
EditOutlined,
|
||||
PlusOutlined,
|
||||
ReloadOutlined,
|
||||
SettingOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { history, useParams } from '@umijs/max';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Table,
|
||||
theme,
|
||||
Typography,
|
||||
} from 'antd';
|
||||
import { history, useModel, useParams } from '@umijs/max';
|
||||
import { Button, Col, Row, Space, Spin } from 'antd';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
// Camera types
|
||||
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;
|
||||
}
|
||||
import CameraFormModal from './components/CameraFormModal';
|
||||
import CameraTable from './components/CameraTable';
|
||||
import ConfigCameraV5 from './components/ConfigCameraV5';
|
||||
import ConfigCameraV6 from './components/ConfigCameraV6';
|
||||
|
||||
const CameraConfigPage = () => {
|
||||
const { thingId } = useParams<{ thingId: string }>();
|
||||
const { token } = theme.useToken();
|
||||
const [form] = Form.useForm<CameraFormValues>();
|
||||
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 [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(() => {
|
||||
wsClient.connect('/mqtt', false);
|
||||
const unsubscribe = wsClient.subscribe((data: any) => {
|
||||
console.log('Received WS data:', data);
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribe();
|
||||
};
|
||||
@@ -124,110 +38,90 @@ const CameraConfigPage = () => {
|
||||
useEffect(() => {
|
||||
const fetchThingInfo = async () => {
|
||||
if (!thingId) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
const thing = await apiGetThingDetail(thingId);
|
||||
setThingName(thing.name || thingId);
|
||||
const thingData = await apiGetThingDetail(thingId);
|
||||
setThing(thingData);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch thing info:', error);
|
||||
setThingName(thingId);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchThingInfo();
|
||||
}, [thingId]);
|
||||
|
||||
const handleBack = () => {
|
||||
history.push('/manager/devices');
|
||||
// Fetch camera config when thing data is available
|
||||
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 = () => {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({
|
||||
type: 'HIKVISION',
|
||||
rtspPort: 554,
|
||||
httpPort: 80,
|
||||
stream: 0,
|
||||
channel: 0,
|
||||
});
|
||||
setIsModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setIsModalVisible(false);
|
||||
form.resetFields();
|
||||
};
|
||||
|
||||
const handleSubmitCamera = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
console.log('Camera values:', values);
|
||||
// TODO: Call API to create camera
|
||||
setCameras([
|
||||
...cameras,
|
||||
{
|
||||
id: String(cameras.length + 1),
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
ipAddress: values.ipAddress,
|
||||
},
|
||||
]);
|
||||
handleCloseModal();
|
||||
} catch (error) {
|
||||
console.error('Validation failed:', error);
|
||||
const handleSubmitCamera = (values: any) => {
|
||||
console.log('Camera values:', values);
|
||||
// TODO: Call API to create camera
|
||||
handleCloseModal();
|
||||
};
|
||||
|
||||
// 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 (selectedAlerts.includes(alertId)) {
|
||||
setSelectedAlerts(selectedAlerts.filter((id) => id !== alertId));
|
||||
} else {
|
||||
setSelectedAlerts([...selectedAlerts, alertId]);
|
||||
if (thingType === 'spole' || thingType === 'gmsv6') {
|
||||
return <ConfigCameraV6 thing={thing} cameraConfig={cameraConfig} />;
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAlerts = () => {
|
||||
setSelectedAlerts([]);
|
||||
return <ConfigCameraV6 thing={thing} cameraConfig={cameraConfig} />;
|
||||
};
|
||||
|
||||
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 (
|
||||
<Spin spinning={loading}>
|
||||
<PageContainer
|
||||
@@ -237,9 +131,9 @@ const CameraConfigPage = () => {
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={handleBack}
|
||||
onClick={() => history.push('/manager/devices')}
|
||||
/>
|
||||
<span>{thingName || 'Loading...'}</span>
|
||||
<span>{thing?.name || 'Loading...'}</span>
|
||||
</Space>
|
||||
),
|
||||
}}
|
||||
@@ -247,248 +141,26 @@ const CameraConfigPage = () => {
|
||||
<Row gutter={24}>
|
||||
{/* Left Column - Camera Table */}
|
||||
<Col xs={24} md={10} lg={8}>
|
||||
<Card bodyStyle={{ padding: 16 }}>
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
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>
|
||||
<CameraTable
|
||||
cameraData={cameras}
|
||||
onCreateCamera={handleOpenModal}
|
||||
onReload={fetchCameraConfig}
|
||||
loading={cameraLoading}
|
||||
/>
|
||||
</Col>
|
||||
|
||||
{/* Right Column - Alert Configuration */}
|
||||
{/* Right Column - Camera Recording Configuration */}
|
||||
<Col xs={24} md={14} lg={16}>
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
{renderCameraRecordingComponent()}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* Create Camera Modal */}
|
||||
<Modal
|
||||
title="Tạo mới"
|
||||
<CameraFormModal
|
||||
open={isModalVisible}
|
||||
onCancel={handleCloseModal}
|
||||
footer={[
|
||||
<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>
|
||||
onSubmit={handleSubmitCamera}
|
||||
/>
|
||||
</PageContainer>
|
||||
</Spin>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user