Compare commits
1 Commits
ea07d0c99e
...
MinhNN
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bc15192ec |
@@ -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',
|
||||||
|
|||||||
@@ -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ị',
|
||||||
|
|||||||
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 { 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 {
|
console.log('Camera values:', values);
|
||||||
const values = await form.validateFields();
|
// TODO: Call API to create camera
|
||||||
console.log('Camera values:', values);
|
handleCloseModal();
|
||||||
// TODO: Call API to create camera
|
};
|
||||||
setCameras([
|
|
||||||
...cameras,
|
// Helper function to determine which camera component to render
|
||||||
{
|
const renderCameraRecordingComponent = () => {
|
||||||
id: String(cameras.length + 1),
|
const thingType = thing?.metadata?.type;
|
||||||
name: values.name,
|
|
||||||
type: values.type,
|
if (thingType === 'gmsv5') {
|
||||||
ipAddress: values.ipAddress,
|
return <ConfigCameraV5 thing={thing} />;
|
||||||
},
|
|
||||||
]);
|
|
||||||
handleCloseModal();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Validation failed:', error);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
9
src/services/master/typings/log.d.ts
vendored
9
src/services/master/typings/log.d.ts
vendored
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user