feat: Implement camera management page and device location updates, including API, typings, and routing.
This commit is contained in:
@@ -3,6 +3,7 @@ import {
|
|||||||
alarmsRoute,
|
alarmsRoute,
|
||||||
commonManagerRoutes,
|
commonManagerRoutes,
|
||||||
loginRoute,
|
loginRoute,
|
||||||
|
managerCameraRoute,
|
||||||
managerRouteBase,
|
managerRouteBase,
|
||||||
notFoundRoute,
|
notFoundRoute,
|
||||||
profileRoute,
|
profileRoute,
|
||||||
@@ -25,7 +26,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
...managerRouteBase,
|
...managerRouteBase,
|
||||||
routes: [...commonManagerRoutes],
|
routes: [...commonManagerRoutes, managerCameraRoute],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
|
|||||||
@@ -80,6 +80,11 @@ export const commonManagerRoutes = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const managerCameraRoute = {
|
||||||
|
path: '/manager/devices/:thingId/camera',
|
||||||
|
component: './Manager/Device/Camera',
|
||||||
|
};
|
||||||
|
|
||||||
export const managerRouteBase = {
|
export const managerRouteBase = {
|
||||||
name: 'manager',
|
name: 'manager',
|
||||||
icon: 'icon-setting',
|
icon: 'icon-setting',
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export default {
|
|||||||
'master.devices.create.error': 'Device creation failed',
|
'master.devices.create.error': 'Device creation failed',
|
||||||
'master.devices.groups': 'Groups',
|
'master.devices.groups': 'Groups',
|
||||||
'master.devices.groups.required': 'Please select groups',
|
'master.devices.groups.required': 'Please select groups',
|
||||||
|
// Update info device
|
||||||
|
'master.devices.update.success': 'Updated successfully',
|
||||||
|
'master.devices.update.error': 'Update failed',
|
||||||
// Edit device modal
|
// Edit device modal
|
||||||
'master.devices.update.title': 'Update device',
|
'master.devices.update.title': 'Update device',
|
||||||
'master.devices.ok': 'OK',
|
'master.devices.ok': 'OK',
|
||||||
@@ -32,4 +35,13 @@ export default {
|
|||||||
'master.devices.address': 'Address',
|
'master.devices.address': 'Address',
|
||||||
'master.devices.address.placeholder': 'Enter address',
|
'master.devices.address.placeholder': 'Enter address',
|
||||||
'master.devices.address.required': 'Please enter address',
|
'master.devices.address.required': 'Please enter address',
|
||||||
|
// Location modal
|
||||||
|
'master.devices.location.title': 'Update location',
|
||||||
|
'master.devices.location.latitude': 'Latitude',
|
||||||
|
'master.devices.location.latitude.required': 'Please enter latitude',
|
||||||
|
'master.devices.location.longitude': 'Longitude',
|
||||||
|
'master.devices.location.longitude.required': 'Please enter longitude',
|
||||||
|
'master.devices.location.placeholder': 'Enter data',
|
||||||
|
'master.devices.location.update.success': 'Location updated successfully',
|
||||||
|
'master.devices.location.update.error': 'Location update failed',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export default {
|
|||||||
'master.thing.external_id': 'External ID',
|
'master.thing.external_id': 'External ID',
|
||||||
'master.thing.group': 'Nhóm',
|
'master.thing.group': 'Nhóm',
|
||||||
'master.thing.address': 'Địa chỉ',
|
'master.thing.address': 'Địa chỉ',
|
||||||
|
|
||||||
// Device translations
|
// Device translations
|
||||||
'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',
|
||||||
@@ -20,6 +21,9 @@ export default {
|
|||||||
'master.devices.create.error': 'Tạo thiết bị lỗi',
|
'master.devices.create.error': 'Tạo thiết bị lỗi',
|
||||||
'master.devices.groups': 'Đơn vị',
|
'master.devices.groups': 'Đơn vị',
|
||||||
'master.devices.groups.required': 'Vui lòng chọn đơn vị',
|
'master.devices.groups.required': 'Vui lòng chọn đơn vị',
|
||||||
|
// Update info device
|
||||||
|
'master.devices.update.success': 'Cập nhật thành công',
|
||||||
|
'master.devices.update.error': 'Cập nhật thất bại',
|
||||||
// Edit device modal
|
// Edit device modal
|
||||||
'master.devices.update.title': 'Cập nhật thiết bị',
|
'master.devices.update.title': 'Cập nhật thiết bị',
|
||||||
'master.devices.ok': 'Đồng ý',
|
'master.devices.ok': 'Đồng ý',
|
||||||
@@ -31,4 +35,13 @@ export default {
|
|||||||
'master.devices.address': 'Địa chỉ',
|
'master.devices.address': 'Địa chỉ',
|
||||||
'master.devices.address.placeholder': 'Nhập địa chỉ',
|
'master.devices.address.placeholder': 'Nhập địa chỉ',
|
||||||
'master.devices.address.required': 'Vui lòng nhập địa chỉ',
|
'master.devices.address.required': 'Vui lòng nhập địa chỉ',
|
||||||
|
// Location modal
|
||||||
|
'master.devices.location.title': 'Cập nhật vị trí',
|
||||||
|
'master.devices.location.latitude': 'Vị độ',
|
||||||
|
'master.devices.location.latitude.required': 'Vui lòng nhập vị độ',
|
||||||
|
'master.devices.location.longitude': 'Kinh độ',
|
||||||
|
'master.devices.location.longitude.required': 'Vui lòng nhập kinh độ',
|
||||||
|
'master.devices.location.placeholder': 'Nhập dữ liệu',
|
||||||
|
'master.devices.location.update.success': 'Cập nhật vị trí thành công',
|
||||||
|
'master.devices.location.update.error': 'Cập nhật vị trí thất bại',
|
||||||
};
|
};
|
||||||
|
|||||||
483
src/pages/Manager/Device/Camera/index.tsx
Normal file
483
src/pages/Manager/Device/Camera/index.tsx
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
import { apiSearchThings } from '@/services/master/ThingController';
|
||||||
|
import {
|
||||||
|
ArrowLeftOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
ReloadOutlined,
|
||||||
|
SettingOutlined,
|
||||||
|
} 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,
|
||||||
|
Typography,
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CameraConfigPage = () => {
|
||||||
|
const { thingId } = useParams<{ thingId: string }>();
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Fetch thing info on mount
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchThingInfo = async () => {
|
||||||
|
if (!thingId) return;
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await apiSearchThings({
|
||||||
|
offset: 0,
|
||||||
|
limit: 1,
|
||||||
|
id: thingId,
|
||||||
|
});
|
||||||
|
if (response?.things && response.things.length > 0) {
|
||||||
|
setThingName(response.things[0].name || thingId);
|
||||||
|
} else {
|
||||||
|
setThingName(thingId);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch thing info:', error);
|
||||||
|
setThingName(thingId);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchThingInfo();
|
||||||
|
}, [thingId]);
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
history.push('/manager/devices');
|
||||||
|
};
|
||||||
|
|
||||||
|
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 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:', 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: '#1890ff' }}>{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
|
||||||
|
header={{
|
||||||
|
title: (
|
||||||
|
<Space>
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<ArrowLeftOutlined />}
|
||||||
|
onClick={handleBack}
|
||||||
|
/>
|
||||||
|
<span>{thingName || 'Loading...'}</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</Col>
|
||||||
|
|
||||||
|
{/* Right Column - Alert 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: '#f5f5f5',
|
||||||
|
borderRadius: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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 ? '#1890ff' : '#d9d9d9',
|
||||||
|
borderWidth: isSelected ? 2 : 1,
|
||||||
|
background: isSelected ? '#e6f7ff' : '#fff',
|
||||||
|
height: 80,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
bodyStyle={{
|
||||||
|
padding: 8,
|
||||||
|
textAlign: 'center',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 12,
|
||||||
|
color: isSelected ? '#1890ff' : 'inherit',
|
||||||
|
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>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
{/* Create Camera Modal */}
|
||||||
|
<Modal
|
||||||
|
title="Tạo mới"
|
||||||
|
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>
|
||||||
|
</PageContainer>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CameraConfigPage;
|
||||||
@@ -6,11 +6,7 @@ interface Props {
|
|||||||
visible: boolean;
|
visible: boolean;
|
||||||
device: MasterModel.Thing | null;
|
device: MasterModel.Thing | null;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onSubmit: (values: {
|
onSubmit: (values: MasterModel.Thing) => void;
|
||||||
name: string;
|
|
||||||
external_id: string;
|
|
||||||
address?: string;
|
|
||||||
}) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditDeviceModal: React.FC<Props> = ({
|
const EditDeviceModal: React.FC<Props> = ({
|
||||||
@@ -34,6 +30,24 @@ const EditDeviceModal: React.FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [device, form]);
|
}, [device, form]);
|
||||||
|
|
||||||
|
const handleFinish = (values: {
|
||||||
|
name: string;
|
||||||
|
external_id: string;
|
||||||
|
address?: string;
|
||||||
|
}) => {
|
||||||
|
const payload: MasterModel.Thing = {
|
||||||
|
...device,
|
||||||
|
name: values.name,
|
||||||
|
metadata: {
|
||||||
|
...(device?.metadata || {}),
|
||||||
|
external_id: values.external_id,
|
||||||
|
address: values.address,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit(payload);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={intl.formatMessage({
|
title={intl.formatMessage({
|
||||||
@@ -53,7 +67,12 @@ const EditDeviceModal: React.FC<Props> = ({
|
|||||||
})}
|
})}
|
||||||
destroyOnClose
|
destroyOnClose
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={onSubmit} preserve={false}>
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={handleFinish}
|
||||||
|
preserve={false}
|
||||||
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
name="name"
|
name="name"
|
||||||
label={intl.formatMessage({
|
label={intl.formatMessage({
|
||||||
|
|||||||
121
src/pages/Manager/Device/components/LocationModal.tsx
Normal file
121
src/pages/Manager/Device/components/LocationModal.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { Form, Input, Modal } from 'antd';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
visible: boolean;
|
||||||
|
device: MasterModel.Thing | null;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSubmit: (values: MasterModel.Thing) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocationModal: React.FC<Props> = ({
|
||||||
|
visible,
|
||||||
|
device,
|
||||||
|
onCancel,
|
||||||
|
onSubmit,
|
||||||
|
}) => {
|
||||||
|
const [form] = Form.useForm();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (device) {
|
||||||
|
form.setFieldsValue({
|
||||||
|
lat: device?.metadata?.lat || '',
|
||||||
|
lng: device?.metadata?.lng || '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.resetFields();
|
||||||
|
}
|
||||||
|
}, [device, form]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'master.devices.location.title',
|
||||||
|
defaultMessage: 'Update location',
|
||||||
|
})}
|
||||||
|
open={visible}
|
||||||
|
onCancel={onCancel}
|
||||||
|
onOk={() => form.submit()}
|
||||||
|
okText={intl.formatMessage({
|
||||||
|
id: 'master.devices.ok',
|
||||||
|
defaultMessage: 'OK',
|
||||||
|
})}
|
||||||
|
cancelText={intl.formatMessage({
|
||||||
|
id: 'master.devices.cancel',
|
||||||
|
defaultMessage: 'Cancel',
|
||||||
|
})}
|
||||||
|
destroyOnClose
|
||||||
|
>
|
||||||
|
<Form
|
||||||
|
form={form}
|
||||||
|
layout="vertical"
|
||||||
|
onFinish={(values) => {
|
||||||
|
const payload: MasterModel.Thing = {
|
||||||
|
id: device?.id,
|
||||||
|
name: device?.name,
|
||||||
|
key: device?.key,
|
||||||
|
metadata: {
|
||||||
|
...device?.metadata,
|
||||||
|
lat: values.lat,
|
||||||
|
lng: values.lng,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
onSubmit(payload);
|
||||||
|
}}
|
||||||
|
preserve={false}
|
||||||
|
>
|
||||||
|
<Form.Item
|
||||||
|
name="lat"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.devices.location.latitude',
|
||||||
|
defaultMessage: 'Latitude',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.devices.location.latitude.required',
|
||||||
|
defaultMessage: 'Please enter latitude',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.devices.location.placeholder',
|
||||||
|
defaultMessage: 'Enter data',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item
|
||||||
|
name="lng"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.devices.location.longitude',
|
||||||
|
defaultMessage: 'Longitude',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.devices.location.longitude.required',
|
||||||
|
defaultMessage: 'Please enter longitude',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.devices.location.placeholder',
|
||||||
|
defaultMessage: 'Enter data',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
</Form>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LocationModal;
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import IconFont from '@/components/IconFont';
|
import IconFont from '@/components/IconFont';
|
||||||
import TreeGroup from '@/components/shared/TreeGroup';
|
import TreeGroup from '@/components/shared/TreeGroup';
|
||||||
import { DEFAULT_PAGE_SIZE } from '@/constants';
|
import { DEFAULT_PAGE_SIZE } from '@/constants';
|
||||||
import { apiSearchThings } from '@/services/master/ThingController';
|
import {
|
||||||
|
apiSearchThings,
|
||||||
|
apiUpdateThing,
|
||||||
|
} from '@/services/master/ThingController';
|
||||||
import { EditOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
import { EditOutlined, EnvironmentOutlined } from '@ant-design/icons';
|
||||||
import {
|
import {
|
||||||
ActionType,
|
ActionType,
|
||||||
@@ -9,13 +12,14 @@ import {
|
|||||||
ProColumns,
|
ProColumns,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { FormattedMessage, useIntl } from '@umijs/max';
|
import { FormattedMessage, history, useIntl } from '@umijs/max';
|
||||||
import { Button, Divider, Grid, Space, Tag, theme } from 'antd';
|
import { Button, Divider, Grid, Space, Tag, theme } from 'antd';
|
||||||
import message from 'antd/es/message';
|
import message from 'antd/es/message';
|
||||||
import Paragraph from 'antd/lib/typography/Paragraph';
|
import Paragraph from 'antd/lib/typography/Paragraph';
|
||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import CreateDevice from './components/CreateDevice';
|
import CreateDevice from './components/CreateDevice';
|
||||||
import EditDeviceModal from './components/EditDeviceModal';
|
import EditDeviceModal from './components/EditDeviceModal';
|
||||||
|
import LocationModal from './components/LocationModal';
|
||||||
|
|
||||||
const ManagerDevicePage = () => {
|
const ManagerDevicePage = () => {
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
@@ -35,9 +39,14 @@ const ManagerDevicePage = () => {
|
|||||||
const [editingDevice, setEditingDevice] = useState<MasterModel.Thing | null>(
|
const [editingDevice, setEditingDevice] = useState<MasterModel.Thing | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [isLocationModalVisible, setIsLocationModalVisible] =
|
||||||
|
useState<boolean>(false);
|
||||||
|
const [locationDevice, setLocationDevice] =
|
||||||
|
useState<MasterModel.Thing | null>(null);
|
||||||
|
|
||||||
const handleClickAssign = (device: MasterModel.Thing) => {
|
const handleLocation = (device: MasterModel.Thing) => {
|
||||||
console.log('Device ', device);
|
setLocationDevice(device);
|
||||||
|
setIsLocationModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (device: MasterModel.Thing) => {
|
const handleEdit = (device: MasterModel.Thing) => {
|
||||||
@@ -50,13 +59,53 @@ const ManagerDevicePage = () => {
|
|||||||
setEditingDevice(null);
|
setEditingDevice(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditSubmit = async (values: any) => {
|
const handleLocationCancel = () => {
|
||||||
// TODO: call update API here if available. For now just simulate success.
|
setIsLocationModalVisible(false);
|
||||||
console.log('Update values for', editingDevice?.id, values);
|
setLocationDevice(null);
|
||||||
messageApi.success('Cập nhật thành công');
|
};
|
||||||
|
|
||||||
|
const handleLocationSubmit = async (values: MasterModel.Thing) => {
|
||||||
|
try {
|
||||||
|
await apiUpdateThing(values);
|
||||||
|
messageApi.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.devices.location.update.success',
|
||||||
|
defaultMessage: 'Location updated successfully',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setIsLocationModalVisible(false);
|
||||||
|
setLocationDevice(null);
|
||||||
|
actionRef.current?.reload();
|
||||||
|
} catch (error) {
|
||||||
|
messageApi.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.devices.location.update.error',
|
||||||
|
defaultMessage: 'Location update failed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditSubmit = async (values: MasterModel.Thing) => {
|
||||||
|
try {
|
||||||
|
await apiUpdateThing(values);
|
||||||
|
messageApi.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.devices.update.success',
|
||||||
|
defaultMessage: 'Updated successfully',
|
||||||
|
}),
|
||||||
|
);
|
||||||
setIsEditModalVisible(false);
|
setIsEditModalVisible(false);
|
||||||
setEditingDevice(null);
|
setEditingDevice(null);
|
||||||
actionRef.current?.reload();
|
actionRef.current?.reload();
|
||||||
|
} catch (error) {
|
||||||
|
messageApi.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.devices.update.error',
|
||||||
|
defaultMessage: 'Update failed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: ProColumns<MasterModel.Thing>[] = [
|
const columns: ProColumns<MasterModel.Thing>[] = [
|
||||||
@@ -172,20 +221,21 @@ const ManagerDevicePage = () => {
|
|||||||
shape="default"
|
shape="default"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EnvironmentOutlined />}
|
icon={<EnvironmentOutlined />}
|
||||||
// onClick={() => handleClickAssign(device)}
|
onClick={() => handleLocation(device)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
shape="default"
|
shape="default"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<IconFont type="icon-camera" />}
|
icon={<IconFont type="icon-camera" />}
|
||||||
// onClick={() => handleClickAssign(device)}
|
onClick={() => {
|
||||||
|
history.push(`/manager/devices/${device.id}/camera`);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
{device?.metadata?.type === 'gmsv6' && (
|
{device?.metadata?.type === 'gmsv6' && (
|
||||||
<Button
|
<Button
|
||||||
shape="default"
|
shape="default"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<IconFont type="icon-terminal" />}
|
icon={<IconFont type="icon-terminal" />}
|
||||||
// onClick={() => handleClickAssign(device)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@@ -202,6 +252,12 @@ const ManagerDevicePage = () => {
|
|||||||
onCancel={handleEditCancel}
|
onCancel={handleEditCancel}
|
||||||
onSubmit={handleEditSubmit}
|
onSubmit={handleEditSubmit}
|
||||||
/>
|
/>
|
||||||
|
<LocationModal
|
||||||
|
visible={isLocationModalVisible}
|
||||||
|
device={locationDevice}
|
||||||
|
onCancel={handleLocationCancel}
|
||||||
|
onSubmit={handleLocationSubmit}
|
||||||
|
/>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
|
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
|
||||||
<ProCard colSpan={{ xs: 24, sm: 24, md: 6, lg: 6, xl: 6 }}>
|
<ProCard colSpan={{ xs: 24, sm: 24, md: 6, lg: 6, xl: 6 }}>
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ export async function apiSearchThings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function apiUpdateThing(value: MasterModel.Thing) {
|
||||||
|
if (!value.id) throw new Error('Thing id is required');
|
||||||
|
return request<MasterModel.Thing>(`${API_SHARE_THING}/${value.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
data: value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function apiGetThingPolicyByUser(
|
export async function apiGetThingPolicyByUser(
|
||||||
params: Partial<MasterModel.SearchPaginationBody>,
|
params: Partial<MasterModel.SearchPaginationBody>,
|
||||||
userId: string,
|
userId: string,
|
||||||
|
|||||||
2
src/services/master/typings/thing.d.ts
vendored
2
src/services/master/typings/thing.d.ts
vendored
@@ -30,6 +30,8 @@ declare namespace MasterModel {
|
|||||||
state_updated_time?: number;
|
state_updated_time?: number;
|
||||||
type?: string;
|
type?: string;
|
||||||
updated_time?: number;
|
updated_time?: number;
|
||||||
|
lat?: string;
|
||||||
|
lng?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThingsResponse<
|
interface ThingsResponse<
|
||||||
|
|||||||
Reference in New Issue
Block a user