feat: add camera configuration translations and enhance Camera components with internationalization support

This commit is contained in:
2026-02-09 21:55:56 +07:00
parent 674d53bcc5
commit 9d211ed43c
7 changed files with 489 additions and 80 deletions

View File

@@ -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',
}; };

View File

@@ -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',
}; };

View File

@@ -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>

View File

@@ -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,
}} }}
/> />

View File

@@ -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>
); );
}; };

View File

@@ -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>

View File

@@ -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>
), ),