feat: add camera configuration translations and enhance Camera components with internationalization support
This commit is contained in:
@@ -44,4 +44,58 @@ export default {
|
|||||||
'master.devices.location.placeholder': 'Enter data',
|
'master.devices.location.placeholder': 'Enter data',
|
||||||
'master.devices.location.update.success': 'Location updated successfully',
|
'master.devices.location.update.success': 'Location updated successfully',
|
||||||
'master.devices.location.update.error': 'Location update failed',
|
'master.devices.location.update.error': 'Location update failed',
|
||||||
|
|
||||||
|
// Camera translations
|
||||||
|
'master.camera.loading': 'Loading...',
|
||||||
|
'master.camera.config.success': 'Configuration sent successfully',
|
||||||
|
'master.camera.config.error.deviceOffline':
|
||||||
|
'Device is offline, cannot send configuration',
|
||||||
|
'master.camera.config.error.missingConfig':
|
||||||
|
'Missing device configuration information',
|
||||||
|
'master.camera.config.error.mqttNotConnected': 'MQTT not connected',
|
||||||
|
// Camera Form Modal
|
||||||
|
'master.camera.form.title.add': 'Add New Camera',
|
||||||
|
'master.camera.form.title.edit': 'Edit Camera',
|
||||||
|
'master.camera.form.name': 'Name',
|
||||||
|
'master.camera.form.name.placeholder': 'Enter name',
|
||||||
|
'master.camera.form.name.required': 'Please enter name',
|
||||||
|
'master.camera.form.type': 'Type',
|
||||||
|
'master.camera.form.type.required': 'Please select type',
|
||||||
|
'master.camera.form.username': 'Username',
|
||||||
|
'master.camera.form.username.placeholder': 'Enter username',
|
||||||
|
'master.camera.form.username.required': 'Please enter username',
|
||||||
|
'master.camera.form.password': 'Password',
|
||||||
|
'master.camera.form.password.placeholder': 'Enter password',
|
||||||
|
'master.camera.form.password.required': 'Please enter password',
|
||||||
|
'master.camera.form.ip': 'IP Address',
|
||||||
|
'master.camera.form.ip.placeholder': '192.168.1.10',
|
||||||
|
'master.camera.form.ip.required': 'Please enter IP address',
|
||||||
|
'master.camera.form.rtspPort': 'RTSP Port',
|
||||||
|
'master.camera.form.rtspPort.required': 'Please enter RTSP port',
|
||||||
|
'master.camera.form.httpPort': 'HTTP Port',
|
||||||
|
'master.camera.form.httpPort.required': 'Please enter HTTP port',
|
||||||
|
'master.camera.form.stream': 'Stream',
|
||||||
|
'master.camera.form.stream.required': 'Please enter stream',
|
||||||
|
'master.camera.form.channel': 'Channel',
|
||||||
|
'master.camera.form.channel.required': 'Please enter channel',
|
||||||
|
'master.camera.form.cancel': 'Cancel',
|
||||||
|
'master.camera.form.submit': 'OK',
|
||||||
|
'master.camera.form.update': 'Update',
|
||||||
|
// Camera Table
|
||||||
|
'master.camera.table.add': 'Add New Camera',
|
||||||
|
'master.camera.table.column.name': 'Name',
|
||||||
|
'master.camera.table.column.type': 'Type',
|
||||||
|
'master.camera.table.column.ip': 'IP Address',
|
||||||
|
'master.camera.table.column.action': 'Actions',
|
||||||
|
'master.camera.table.offline.tooltip': 'Device is offline',
|
||||||
|
'master.camera.table.pagination': 'Showing {0}-{1} of {2} cameras',
|
||||||
|
// Camera Config V6
|
||||||
|
'master.camera.config.recording': 'Camera Recording',
|
||||||
|
'master.camera.config.send': 'Send',
|
||||||
|
'master.camera.config.alarmList': 'Alarm List',
|
||||||
|
'master.camera.config.selected': '{0} items selected',
|
||||||
|
'master.camera.config.clear': 'Clear',
|
||||||
|
'master.camera.config.recordingMode.none': 'No Recording',
|
||||||
|
'master.camera.config.recordingMode.alarm': 'On Alarm',
|
||||||
|
'master.camera.config.recordingMode.all': '24/7',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,4 +44,58 @@ export default {
|
|||||||
'master.devices.location.placeholder': 'Nhập dữ liệu',
|
'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.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',
|
'master.devices.location.update.error': 'Cập nhật vị trí thất bại',
|
||||||
|
|
||||||
|
// Camera translations
|
||||||
|
'master.camera.loading': 'Đang tải...',
|
||||||
|
'master.camera.config.success': 'Đã gửi cấu hình thành công',
|
||||||
|
'master.camera.config.error.deviceOffline':
|
||||||
|
'Thiết bị đang ngoại tuyến, không thể gửi cấu hình',
|
||||||
|
'master.camera.config.error.missingConfig':
|
||||||
|
'Thiếu thông tin cấu hình thiết bị',
|
||||||
|
'master.camera.config.error.mqttNotConnected': 'MQTT chưa kết nối',
|
||||||
|
// Camera Form Modal
|
||||||
|
'master.camera.form.title.add': 'Tạo mới camera',
|
||||||
|
'master.camera.form.title.edit': 'Chỉnh sửa camera',
|
||||||
|
'master.camera.form.name': 'Tên',
|
||||||
|
'master.camera.form.name.placeholder': 'Nhập tên',
|
||||||
|
'master.camera.form.name.required': 'Vui lòng nhập tên',
|
||||||
|
'master.camera.form.type': 'Loại',
|
||||||
|
'master.camera.form.type.required': 'Vui lòng chọn loại',
|
||||||
|
'master.camera.form.username': 'Tài khoản',
|
||||||
|
'master.camera.form.username.placeholder': 'Nhập tài khoản',
|
||||||
|
'master.camera.form.username.required': 'Vui lòng nhập tài khoản',
|
||||||
|
'master.camera.form.password': 'Mật khẩu',
|
||||||
|
'master.camera.form.password.placeholder': 'Nhập mật khẩu',
|
||||||
|
'master.camera.form.password.required': 'Vui lòng nhập mật khẩu',
|
||||||
|
'master.camera.form.ip': 'Địa chỉ IP',
|
||||||
|
'master.camera.form.ip.placeholder': '192.168.1.10',
|
||||||
|
'master.camera.form.ip.required': 'Vui lòng nhập địa chỉ IP',
|
||||||
|
'master.camera.form.rtspPort': 'Cổng RTSP',
|
||||||
|
'master.camera.form.rtspPort.required': 'Vui lòng nhập cổng RTSP',
|
||||||
|
'master.camera.form.httpPort': 'Cổng HTTP',
|
||||||
|
'master.camera.form.httpPort.required': 'Vui lòng nhập cổng HTTP',
|
||||||
|
'master.camera.form.stream': 'Luồng',
|
||||||
|
'master.camera.form.stream.required': 'Vui lòng nhập luồng',
|
||||||
|
'master.camera.form.channel': 'Kênh',
|
||||||
|
'master.camera.form.channel.required': 'Vui lòng nhập kênh',
|
||||||
|
'master.camera.form.cancel': 'Hủy',
|
||||||
|
'master.camera.form.submit': 'Đồng ý',
|
||||||
|
'master.camera.form.update': 'Cập nhật',
|
||||||
|
// Camera Table
|
||||||
|
'master.camera.table.add': 'Tạo mới camera',
|
||||||
|
'master.camera.table.column.name': 'Tên',
|
||||||
|
'master.camera.table.column.type': 'Loại',
|
||||||
|
'master.camera.table.column.ip': 'Địa chỉ IP',
|
||||||
|
'master.camera.table.column.action': 'Thao tác',
|
||||||
|
'master.camera.table.offline.tooltip': 'Thiết bị đang ngoại tuyến',
|
||||||
|
'master.camera.table.pagination': 'Hiển thị {0}-{1} của {2} camera',
|
||||||
|
// Camera Config V6
|
||||||
|
'master.camera.config.recording': 'Ghi dữ liệu camera',
|
||||||
|
'master.camera.config.send': 'Gửi đi',
|
||||||
|
'master.camera.config.alarmList': 'Danh sách cảnh báo',
|
||||||
|
'master.camera.config.selected': 'đã chọn {0} mục',
|
||||||
|
'master.camera.config.clear': 'Xóa',
|
||||||
|
'master.camera.config.recordingMode.none': 'Không ghi',
|
||||||
|
'master.camera.config.recordingMode.alarm': 'Theo cảnh báo',
|
||||||
|
'master.camera.config.recordingMode.all': '24/24',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useIntl } from '@umijs/max';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Col,
|
Col,
|
||||||
@@ -33,6 +34,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
|||||||
editingCamera,
|
editingCamera,
|
||||||
}) => {
|
}) => {
|
||||||
const [form] = Form.useForm<MasterModel.Camera>();
|
const [form] = Form.useForm<MasterModel.Camera>();
|
||||||
|
const intl = useIntl();
|
||||||
const isEditMode = !!editingCamera;
|
const isEditMode = !!editingCamera;
|
||||||
|
|
||||||
// Populate form when editing
|
// Populate form when editing
|
||||||
@@ -73,15 +75,28 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={isEditMode ? 'Chỉnh sửa camera' : 'Tạo mới camera'}
|
title={intl.formatMessage({
|
||||||
|
id: isEditMode
|
||||||
|
? 'master.camera.form.title.edit'
|
||||||
|
: 'master.camera.form.title.add',
|
||||||
|
defaultMessage: isEditMode ? 'Chỉnh sửa camera' : 'Tạo mới camera',
|
||||||
|
})}
|
||||||
open={open}
|
open={open}
|
||||||
onCancel={handleCancel}
|
onCancel={handleCancel}
|
||||||
footer={[
|
footer={[
|
||||||
<Button key="cancel" onClick={handleCancel}>
|
<Button key="cancel" onClick={handleCancel}>
|
||||||
Hủy
|
{intl.formatMessage({
|
||||||
|
id: 'master.camera.form.cancel',
|
||||||
|
defaultMessage: 'Hủy',
|
||||||
|
})}
|
||||||
</Button>,
|
</Button>,
|
||||||
<Button key="submit" type="primary" onClick={handleSubmit}>
|
<Button key="submit" type="primary" onClick={handleSubmit}>
|
||||||
{isEditMode ? 'Cập nhật' : 'Đồng ý'}
|
{intl.formatMessage({
|
||||||
|
id: isEditMode
|
||||||
|
? 'master.camera.form.update'
|
||||||
|
: 'master.camera.form.submit',
|
||||||
|
defaultMessage: isEditMode ? 'Cập nhật' : 'Đồng ý',
|
||||||
|
})}
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
width={500}
|
width={500}
|
||||||
@@ -99,63 +114,159 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Tên"
|
label={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.name',
|
||||||
|
defaultMessage: 'Tên',
|
||||||
|
})}
|
||||||
name="name"
|
name="name"
|
||||||
rules={[{ required: true, message: 'Vui lòng nhập tên' }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.camera.form.name.required',
|
||||||
|
defaultMessage: 'Vui lòng nhập tên',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="nhập dữ liệu" />
|
<Input
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.name.placeholder',
|
||||||
|
defaultMessage: 'nhập dữ liệu',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Loại"
|
label={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.type',
|
||||||
|
defaultMessage: 'Loại',
|
||||||
|
})}
|
||||||
name="cate_id"
|
name="cate_id"
|
||||||
rules={[{ required: true, message: 'Vui lòng chọn loại' }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.camera.form.type.required',
|
||||||
|
defaultMessage: 'Vui lòng chọn loại',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Select options={CAMERA_TYPES} />
|
<Select options={CAMERA_TYPES} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Tài khoản"
|
label={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.username',
|
||||||
|
defaultMessage: 'Tài khoản',
|
||||||
|
})}
|
||||||
name="username"
|
name="username"
|
||||||
rules={[{ required: true, message: 'Vui lòng nhập tài khoản' }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.camera.form.username.required',
|
||||||
|
defaultMessage: 'Vui lòng nhập tài khoản',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="nhập tài khoản" autoComplete="off" />
|
<Input
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.username.placeholder',
|
||||||
|
defaultMessage: 'nhập tài khoản',
|
||||||
|
})}
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Mật khẩu"
|
label={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.password',
|
||||||
|
defaultMessage: 'Mật khẩu',
|
||||||
|
})}
|
||||||
name="password"
|
name="password"
|
||||||
rules={[{ required: true, message: 'Vui lòng nhập mật khẩu' }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.camera.form.password.required',
|
||||||
|
defaultMessage: 'Vui lòng nhập mật khẩu',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input.Password
|
<Input.Password
|
||||||
placeholder="nhập mật khẩu"
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.password.placeholder',
|
||||||
|
defaultMessage: 'nhập mật khẩu',
|
||||||
|
})}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Địa chỉ IP"
|
label={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.ip',
|
||||||
|
defaultMessage: 'Địa chỉ IP',
|
||||||
|
})}
|
||||||
name="ip"
|
name="ip"
|
||||||
rules={[{ required: true, message: 'Vui lòng nhập địa chỉ IP' }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.camera.form.ip.required',
|
||||||
|
defaultMessage: 'Vui lòng nhập địa chỉ IP',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<Input placeholder="192.168.1.10" />
|
<Input
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.ip.placeholder',
|
||||||
|
defaultMessage: '192.168.1.10',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Cổng RTSP"
|
label={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.rtspPort',
|
||||||
|
defaultMessage: 'Cổng RTSP',
|
||||||
|
})}
|
||||||
name="rtsp_port"
|
name="rtsp_port"
|
||||||
rules={[{ required: true, message: 'Vui lòng nhập cổng RTSP' }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.camera.form.rtspPort.required',
|
||||||
|
defaultMessage: 'Vui lòng nhập cổng RTSP',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber style={{ width: '100%' }} min={0} max={65535} />
|
<InputNumber style={{ width: '100%' }} min={0} max={65535} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Cổng HTTP"
|
label={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.httpPort',
|
||||||
|
defaultMessage: 'Cổng HTTP',
|
||||||
|
})}
|
||||||
name="http_port"
|
name="http_port"
|
||||||
rules={[{ required: true, message: 'Vui lòng nhập cổng HTTP' }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.camera.form.httpPort.required',
|
||||||
|
defaultMessage: 'Vui lòng nhập cổng HTTP',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber style={{ width: '100%' }} min={0} max={65535} />
|
<InputNumber style={{ width: '100%' }} min={0} max={65535} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
@@ -165,18 +276,40 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
|
|||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Luồng"
|
label={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.stream',
|
||||||
|
defaultMessage: 'Luồng',
|
||||||
|
})}
|
||||||
name="stream"
|
name="stream"
|
||||||
rules={[{ required: true, message: 'Vui lòng nhập luồng' }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.camera.form.stream.required',
|
||||||
|
defaultMessage: 'Vui lòng nhập luồng',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber style={{ width: '100%' }} min={0} />
|
<InputNumber style={{ width: '100%' }} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Col>
|
</Col>
|
||||||
<Col span={12}>
|
<Col span={12}>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
label="Kênh"
|
label={intl.formatMessage({
|
||||||
|
id: 'master.camera.form.channel',
|
||||||
|
defaultMessage: 'Kênh',
|
||||||
|
})}
|
||||||
name="channel"
|
name="channel"
|
||||||
rules={[{ required: true, message: 'Vui lòng nhập kênh' }]}
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.camera.form.channel.required',
|
||||||
|
defaultMessage: 'Vui lòng nhập kênh',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<InputNumber style={{ width: '100%' }} min={0} />
|
<InputNumber style={{ width: '100%' }} min={0} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Card, Space, Table, theme, Tooltip } from 'antd';
|
import { Button, Card, Space, Table, theme, Tooltip } from 'antd';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
isOnline = false,
|
isOnline = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
|
const intl = useIntl();
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
|
||||||
const handleReload = () => {
|
const handleReload = () => {
|
||||||
@@ -48,7 +50,10 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
title: 'Tên',
|
title: intl.formatMessage({
|
||||||
|
id: 'master.camera.table.column.name',
|
||||||
|
defaultMessage: 'Tên',
|
||||||
|
}),
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
render: (text: string) => (
|
render: (text: string) => (
|
||||||
@@ -56,22 +61,40 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Loại',
|
title: intl.formatMessage({
|
||||||
|
id: 'master.camera.table.column.type',
|
||||||
|
defaultMessage: 'Loại',
|
||||||
|
}),
|
||||||
dataIndex: 'cate_id',
|
dataIndex: 'cate_id',
|
||||||
key: 'cate_id',
|
key: 'cate_id',
|
||||||
render: (text: string) => text || '-',
|
render: (text: string) => text || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Địa chỉ IP',
|
title: intl.formatMessage({
|
||||||
|
id: 'master.camera.table.column.ip',
|
||||||
|
defaultMessage: 'Địa chỉ IP',
|
||||||
|
}),
|
||||||
dataIndex: 'ip',
|
dataIndex: 'ip',
|
||||||
key: 'ip',
|
key: 'ip',
|
||||||
render: (text: string) => text || '-',
|
render: (text: string) => text || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Thao tác',
|
title: intl.formatMessage({
|
||||||
|
id: 'master.camera.table.column.action',
|
||||||
|
defaultMessage: 'Thao tác',
|
||||||
|
}),
|
||||||
key: 'action',
|
key: 'action',
|
||||||
render: (_: any, record: MasterModel.Camera) => (
|
render: (_: any, record: MasterModel.Camera) => (
|
||||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
!isOnline
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.camera.table.offline.tooltip',
|
||||||
|
defaultMessage: 'Thiết bị đang ngoại tuyến',
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EditOutlined />}
|
icon={<EditOutlined />}
|
||||||
@@ -86,14 +109,26 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Card bodyStyle={{ padding: 16 }}>
|
<Card bodyStyle={{ padding: 16 }}>
|
||||||
<Space style={{ marginBottom: 16 }}>
|
<Space style={{ marginBottom: 16 }}>
|
||||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
!isOnline
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.camera.table.offline.tooltip',
|
||||||
|
defaultMessage: 'Thiết bị đang ngoại tuyến',
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
onClick={onCreateCamera}
|
onClick={onCreateCamera}
|
||||||
disabled={!isOnline}
|
disabled={!isOnline}
|
||||||
>
|
>
|
||||||
Tạo mới camera
|
{intl.formatMessage({
|
||||||
|
id: 'master.camera.table.add',
|
||||||
|
defaultMessage: 'Tạo mới camera',
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button
|
<Button
|
||||||
@@ -101,7 +136,16 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
onClick={handleReload}
|
onClick={handleReload}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
!isOnline
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.camera.table.offline.tooltip',
|
||||||
|
defaultMessage: 'Thiết bị đang ngoại tuyến',
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
icon={<DeleteOutlined />}
|
icon={<DeleteOutlined />}
|
||||||
onClick={handleDelete}
|
onClick={handleDelete}
|
||||||
@@ -126,7 +170,17 @@ const CameraTable: React.FC<CameraTableProps> = ({
|
|||||||
pagination={{
|
pagination={{
|
||||||
size: 'small',
|
size: 'small',
|
||||||
showTotal: (total: number, range: [number, number]) =>
|
showTotal: (total: number, range: [number, number]) =>
|
||||||
`Hiển thị ${range[0]}-${range[1]} của ${total} camera`,
|
intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'master.camera.table.pagination',
|
||||||
|
defaultMessage: 'Hiển thị {0}-{1} của {2} camera',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0: range[0],
|
||||||
|
1: range[1],
|
||||||
|
2: total,
|
||||||
|
},
|
||||||
|
),
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
|
import { useIntl } from '@umijs/max';
|
||||||
import { Button, Card, Select, Typography } from 'antd';
|
import { Button, Card, Select, Typography } from 'antd';
|
||||||
import { useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// Recording modes for V5 - chỉ có không ghi và ghi 24/24
|
// 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 {
|
interface CameraV5Props {
|
||||||
thing: MasterModel.Thing | null;
|
thing: MasterModel.Thing | null;
|
||||||
@@ -18,9 +15,29 @@ const CameraV5: React.FC<CameraV5Props> = ({
|
|||||||
thing,
|
thing,
|
||||||
initialRecordingMode = 'none',
|
initialRecordingMode = 'none',
|
||||||
}) => {
|
}) => {
|
||||||
const [recordingMode, setRecordingMode] = useState(initialRecordingMode);
|
const intl = useIntl();
|
||||||
|
|
||||||
console.log('ConfigCameraV5 - thing:', thing);
|
const RECORDING_MODES = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.camera.config.recordingMode.none',
|
||||||
|
defaultMessage: 'Không ghi',
|
||||||
|
}),
|
||||||
|
value: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.camera.config.recordingMode.all',
|
||||||
|
defaultMessage: '24/24',
|
||||||
|
}),
|
||||||
|
value: 'all',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[intl],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [recordingMode, setRecordingMode] = useState(initialRecordingMode);
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
console.log('Submit recording mode:', recordingMode);
|
console.log('Submit recording mode:', recordingMode);
|
||||||
@@ -30,24 +47,31 @@ const CameraV5: React.FC<CameraV5Props> = ({
|
|||||||
return (
|
return (
|
||||||
<Card bodyStyle={{ padding: 16 }}>
|
<Card bodyStyle={{ padding: 16 }}>
|
||||||
{/* Recording Mode */}
|
{/* Recording Mode */}
|
||||||
<div style={{ marginBottom: 24 }}>
|
<div className="mb-6">
|
||||||
<Text strong style={{ display: 'block', marginBottom: 8 }}>
|
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4 items-start sm:items-end">
|
||||||
Ghi dữ liệu camera
|
<div className="w-full sm:w-1/3 lg:w-1/4">
|
||||||
|
<Text strong className="block mb-2">
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'master.camera.config.recording',
|
||||||
|
defaultMessage: 'Ghi dữ liệu camera',
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<Select
|
<Select
|
||||||
value={recordingMode}
|
value={recordingMode}
|
||||||
onChange={setRecordingMode}
|
onChange={setRecordingMode}
|
||||||
options={RECORDING_MODES}
|
options={RECORDING_MODES}
|
||||||
style={{ width: 200 }}
|
className="w-full"
|
||||||
|
popupMatchSelectWidth={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit Button */}
|
|
||||||
<div style={{ textAlign: 'center' }}>
|
|
||||||
<Button type="primary" onClick={handleSubmit}>
|
<Button type="primary" onClick={handleSubmit}>
|
||||||
Gửi đi
|
{intl.formatMessage({
|
||||||
|
id: 'master.camera.config.send',
|
||||||
|
defaultMessage: 'Gửi đi',
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { apiQueryConfigAlarm } from '@/services/master/MessageController';
|
import { apiQueryConfigAlarm } from '@/services/master/MessageController';
|
||||||
import { useModel } from '@umijs/max';
|
import { useIntl, useModel } from '@umijs/max';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -10,17 +10,10 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
const { Text } = Typography;
|
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 {
|
interface CameraV6Props {
|
||||||
thing: MasterModel.Thing | null;
|
thing: MasterModel.Thing | null;
|
||||||
cameraConfig?: MasterModel.CameraV6 | null;
|
cameraConfig?: MasterModel.CameraV6 | null;
|
||||||
@@ -35,6 +28,7 @@ const CameraV6: React.FC<CameraV6Props> = ({
|
|||||||
isOnline = false,
|
isOnline = false,
|
||||||
}) => {
|
}) => {
|
||||||
const { token } = theme.useToken();
|
const { token } = theme.useToken();
|
||||||
|
const intl = useIntl();
|
||||||
const { initialState } = useModel('@@initialState');
|
const { initialState } = useModel('@@initialState');
|
||||||
const [selectedAlerts, setSelectedAlerts] = useState<string[]>([]);
|
const [selectedAlerts, setSelectedAlerts] = useState<string[]>([]);
|
||||||
const [recordingMode, setRecordingMode] =
|
const [recordingMode, setRecordingMode] =
|
||||||
@@ -43,6 +37,34 @@ const CameraV6: React.FC<CameraV6Props> = ({
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Recording modes for V6 - using useMemo for i18n
|
||||||
|
const RECORDING_MODES = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.camera.config.recordingMode.none',
|
||||||
|
defaultMessage: 'Không ghi',
|
||||||
|
}),
|
||||||
|
value: 'none',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.camera.config.recordingMode.alarm',
|
||||||
|
defaultMessage: 'Theo cảnh báo',
|
||||||
|
}),
|
||||||
|
value: 'alarm',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.camera.config.recordingMode.all',
|
||||||
|
defaultMessage: '24/24',
|
||||||
|
}),
|
||||||
|
value: 'all',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[intl],
|
||||||
|
);
|
||||||
|
|
||||||
// Initialize states from cameraConfig when it's available
|
// Initialize states from cameraConfig when it's available
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (cameraConfig) {
|
if (cameraConfig) {
|
||||||
@@ -125,7 +147,10 @@ const CameraV6: React.FC<CameraV6Props> = ({
|
|||||||
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4 items-start sm:items-end">
|
<div className="flex flex-col sm:flex-row gap-2 sm:gap-4 items-start sm:items-end">
|
||||||
<div className="w-full sm:w-1/3 lg:w-1/4">
|
<div className="w-full sm:w-1/3 lg:w-1/4">
|
||||||
<Text strong className="block mb-2">
|
<Text strong className="block mb-2">
|
||||||
Ghi dữ liệu camera
|
{intl.formatMessage({
|
||||||
|
id: 'master.camera.config.recording',
|
||||||
|
defaultMessage: 'Ghi dữ liệu camera',
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
<Select
|
<Select
|
||||||
value={recordingMode}
|
value={recordingMode}
|
||||||
@@ -135,13 +160,25 @@ const CameraV6: React.FC<CameraV6Props> = ({
|
|||||||
popupMatchSelectWidth={false}
|
popupMatchSelectWidth={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Tooltip title={!isOnline ? 'Thiết bị đang ngoại tuyến' : ''}>
|
<Tooltip
|
||||||
|
title={
|
||||||
|
!isOnline
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.camera.table.offline.tooltip',
|
||||||
|
defaultMessage: 'Thiết bị đang ngoại tuyến',
|
||||||
|
})
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleSubmitConfig}
|
onClick={handleSubmitConfig}
|
||||||
disabled={!isOnline}
|
disabled={!isOnline}
|
||||||
>
|
>
|
||||||
Gửi đi
|
{intl.formatMessage({
|
||||||
|
id: 'master.camera.config.send',
|
||||||
|
defaultMessage: 'Gửi đi',
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,7 +188,10 @@ const CameraV6: React.FC<CameraV6Props> = ({
|
|||||||
{recordingMode === 'alarm' && (
|
{recordingMode === 'alarm' && (
|
||||||
<div>
|
<div>
|
||||||
<Text strong className="block mb-2">
|
<Text strong className="block mb-2">
|
||||||
Danh sách cảnh báo
|
{intl.formatMessage({
|
||||||
|
id: 'master.camera.config.alarmList',
|
||||||
|
defaultMessage: 'Danh sách cảnh báo',
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -161,9 +201,22 @@ const CameraV6: React.FC<CameraV6Props> = ({
|
|||||||
borderColor: token.colorBorder,
|
borderColor: token.colorBorder,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text type="secondary">đã chọn {selectedAlerts.length} mục</Text>
|
<Text type="secondary">
|
||||||
|
{intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'master.camera.config.selected',
|
||||||
|
defaultMessage: 'đã chọn {0} mục',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
0: selectedAlerts.length,
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
<Button type="link" onClick={handleClearAlerts}>
|
<Button type="link" onClick={handleClearAlerts}>
|
||||||
Xóa
|
{intl.formatMessage({
|
||||||
|
id: 'master.camera.config.clear',
|
||||||
|
defaultMessage: 'Xóa',
|
||||||
|
})}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { apiGetThingDetail } from '@/services/master/ThingController';
|
|||||||
import { mqttClient } from '@/utils/mqttClient';
|
import { mqttClient } from '@/utils/mqttClient';
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined } from '@ant-design/icons';
|
||||||
import { PageContainer } from '@ant-design/pro-components';
|
import { PageContainer } from '@ant-design/pro-components';
|
||||||
import { history, useModel, useParams } from '@umijs/max';
|
import { history, useIntl, useModel, useParams } from '@umijs/max';
|
||||||
import { Badge, Button, Col, message, Row, Space, Spin } from 'antd';
|
import { Badge, Button, Col, message, Row, Space, Spin } from 'antd';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
@@ -14,6 +14,7 @@ import ConfigCameraV6 from './components/ConfigCameraV6';
|
|||||||
|
|
||||||
const CameraConfigPage = () => {
|
const CameraConfigPage = () => {
|
||||||
const { thingId } = useParams<{ thingId: string }>();
|
const { thingId } = useParams<{ thingId: string }>();
|
||||||
|
const intl = useIntl();
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [cameraLoading, setCameraLoading] = useState(false);
|
const [cameraLoading, setCameraLoading] = useState(false);
|
||||||
@@ -143,12 +144,22 @@ const CameraConfigPage = () => {
|
|||||||
|
|
||||||
// Check if device is online
|
// Check if device is online
|
||||||
if (!isOnline) {
|
if (!isOnline) {
|
||||||
message.error('Thiết bị đang ngoại tuyến, không thể gửi cấu hình');
|
message.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.camera.config.error.deviceOffline',
|
||||||
|
defaultMessage: 'Thiết bị đang ngoại tuyến, không thể gửi cấu hình',
|
||||||
|
}),
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!thing?.metadata?.cfg_channel_id || !thing?.metadata?.external_id) {
|
if (!thing?.metadata?.cfg_channel_id || !thing?.metadata?.external_id) {
|
||||||
message.error('Thiếu thông tin cấu hình thiết bị');
|
message.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.camera.config.error.missingConfig',
|
||||||
|
defaultMessage: 'Thiếu thông tin cấu hình thiết bị',
|
||||||
|
}),
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,10 +186,20 @@ const CameraConfigPage = () => {
|
|||||||
|
|
||||||
if (mqttClient.isConnected()) {
|
if (mqttClient.isConnected()) {
|
||||||
mqttClient.publish(pubTopic, payload);
|
mqttClient.publish(pubTopic, payload);
|
||||||
message.success('Đã gửi cấu hình thành công');
|
message.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.camera.config.success',
|
||||||
|
defaultMessage: 'Đã gửi cấu hình thành công',
|
||||||
|
}),
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
message.error('MQTT chưa kết nối');
|
message.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.camera.config.error.mqttNotConnected',
|
||||||
|
defaultMessage: 'MQTT chưa kết nối',
|
||||||
|
}),
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -266,10 +287,26 @@ const CameraConfigPage = () => {
|
|||||||
icon={<ArrowLeftOutlined />}
|
icon={<ArrowLeftOutlined />}
|
||||||
onClick={() => history.push('/manager/devices')}
|
onClick={() => history.push('/manager/devices')}
|
||||||
/>
|
/>
|
||||||
<span>{thing?.name || 'Loading...'}</span>
|
<span>
|
||||||
|
{thing?.name ||
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.camera.loading',
|
||||||
|
defaultMessage: 'Loading...',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
<Badge
|
<Badge
|
||||||
status={isOnline ? 'success' : 'default'}
|
status={isOnline ? 'success' : 'default'}
|
||||||
text={isOnline ? 'Trực tuyến' : 'Ngoại tuyến'}
|
text={
|
||||||
|
isOnline
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.devices.online',
|
||||||
|
defaultMessage: 'Trực tuyến',
|
||||||
|
})
|
||||||
|
: intl.formatMessage({
|
||||||
|
id: 'master.devices.offline',
|
||||||
|
defaultMessage: 'Ngoại tuyến',
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
|
|||||||
Reference in New Issue
Block a user