feat: Refactor theme management and localization for camera and terminal components

This commit is contained in:
2026-02-10 15:07:46 +07:00
parent 9d211ed43c
commit ea5fc0a617
15 changed files with 265 additions and 200 deletions

View File

@@ -1,5 +1,6 @@
// 运行时配置
import { getTheme } from '@/utils/storage';
import { getLocale, history, Link, RunTimeLayoutConfig } from '@umijs/max';
import { ConfigProvider } from 'antd';
import dayjs from 'dayjs';
@@ -10,7 +11,6 @@ import IconFont from './components/IconFont';
import LanguageSwitcher from './components/Lang/LanguageSwitcher';
import ThemeProvider from './components/Theme/ThemeProvider';
import ThemeSwitcher from './components/Theme/ThemeSwitcher';
import { THEME_KEY } from './constants';
import { ROUTE_FORGOT_PASSWORD, ROUTE_LOGIN } from './constants/routes';
import NotFoundPage from './pages/Exception/NotFound';
import UnAccessPage from './pages/Exception/UnAccess';
@@ -52,8 +52,7 @@ export async function getInitialState(): Promise<InitialStateResponse> {
// Public routes that don't require authentication
if (publicRoutes.includes(pathname)) {
const currentTheme =
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light';
const currentTheme = (getTheme() as 'light' | 'dark') || 'light';
return {
theme: currentTheme,
};
@@ -101,8 +100,7 @@ export async function getInitialState(): Promise<InitialStateResponse> {
}
};
const resp = await getUserProfile();
const currentTheme =
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light';
const currentTheme = (getTheme() as 'light' | 'dark') || 'light';
return {
getUserProfile: getUserProfile!,
currentUserProfile: resp,

View File

@@ -1,4 +1,4 @@
import { THEME_KEY } from '@/constants';
import { setTheme } from '@/utils/storage';
import { MoonOutlined, SunOutlined } from '@ant-design/icons';
import { useModel } from '@umijs/max';
import { Segmented } from 'antd';
@@ -34,7 +34,7 @@ const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
if (!supportsViewTransition) {
// Fallback: just change theme without animation
localStorage.setItem(THEME_KEY, newTheme);
setTheme(newTheme);
setIsDark(newTheme === 'dark');
window.dispatchEvent(
new CustomEvent('theme-change', { detail: { theme: newTheme } }),
@@ -58,7 +58,7 @@ const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
// Start the view transition
const transition = (document as any).startViewTransition(() => {
localStorage.setItem(THEME_KEY, newTheme);
setTheme(newTheme);
setIsDark(newTheme === 'dark');
window.dispatchEvent(
new CustomEvent('theme-change', { detail: { theme: newTheme } }),
@@ -107,8 +107,3 @@ const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
};
export default ThemeSwitcher;
// Helper function để get theme từ localStorage
export const getTheme = (): 'light' | 'dark' => {
return (localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light';
};

View File

@@ -1,6 +1,5 @@
import { THEME_KEY } from '@/constants';
import { getTheme, setTheme } from '@/utils/storage';
import React, { useEffect, useState } from 'react';
import { getTheme } from './ThemeSwitcher';
import './style.less';
const ThemeSwitcherAuth = () => {
@@ -21,7 +20,7 @@ const ThemeSwitcherAuth = () => {
const handleSwitch = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTheme = e.target.checked ? 'dark' : 'light';
localStorage.setItem(THEME_KEY, newTheme);
setTheme(newTheme);
setIsDark(newTheme === 'dark');
window.dispatchEvent(
new CustomEvent('theme-change', { detail: { theme: newTheme } }),

View File

@@ -11,6 +11,7 @@ export const DURATION_POLLING_PRESENTATIONS = 120000; //milliseconds
export const STATUS_NORMAL = 0;
export const STATUS_WARNING = 1;
export const STATUS_DANGEROUS = 2;
export const STATUS_SOS = 3;
export const COLOR_DISCONNECT = '#d9d9d9';
@@ -22,6 +23,7 @@ export const COLOR_SOS = '#ff0000';
export const ACCESS_TOKEN = 'access_token';
export const REFRESH_TOKEN = 'refresh_token';
export const THEME_KEY = 'theme';
export const TERMINAL_THEME_KEY = 'terminal_theme_key';
// Global Constants
export const LIMIT_TREE_LEVEL = 5;
export const DEFAULT_PAGE_SIZE = 5;

View File

@@ -46,56 +46,79 @@ export default {
'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':
'master.devices.camera.loading': 'Loading...',
'master.devices.camera.config.success': 'Configuration sent successfully',
'master.devices.camera.config.error.deviceOffline':
'Device is offline, cannot send configuration',
'master.camera.config.error.missingConfig':
'master.devices.camera.config.error.missingConfig':
'Missing device configuration information',
'master.camera.config.error.mqttNotConnected': 'MQTT not connected',
'master.devices.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',
'master.devices.camera.form.title.add': 'Add New Camera',
'master.devices.camera.form.title.edit': 'Edit Camera',
'master.devices.camera.form.name': 'Name',
'master.devices.camera.form.name.placeholder': 'Enter name',
'master.devices.camera.form.name.required': 'Please enter name',
'master.devices.camera.form.type': 'Type',
'master.devices.camera.form.type.required': 'Please select type',
'master.devices.camera.form.username': 'Username',
'master.devices.camera.form.username.placeholder': 'Enter username',
'master.devices.camera.form.username.required': 'Please enter username',
'master.devices.camera.form.password': 'Password',
'master.devices.camera.form.password.placeholder': 'Enter password',
'master.devices.camera.form.password.required': 'Please enter password',
'master.devices.camera.form.ip': 'IP Address',
'master.devices.camera.form.ip.placeholder': '192.168.1.10',
'master.devices.camera.form.ip.required': 'Please enter IP address',
'master.devices.camera.form.rtspPort': 'RTSP Port',
'master.devices.camera.form.rtspPort.required': 'Please enter RTSP port',
'master.devices.camera.form.httpPort': 'HTTP Port',
'master.devices.camera.form.httpPort.required': 'Please enter HTTP port',
'master.devices.camera.form.stream': 'Stream',
'master.devices.camera.form.stream.required': 'Please enter stream',
'master.devices.camera.form.channel': 'Channel',
'master.devices.camera.form.channel.required': 'Please enter channel',
'master.devices.camera.form.cancel': 'Cancel',
'master.devices.camera.form.submit': 'OK',
'master.devices.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',
'master.devices.camera.table.add': 'Add New Camera',
'master.devices.camera.table.column.name': 'Name',
'master.devices.camera.table.column.type': 'Type',
'master.devices.camera.table.column.ip': 'IP Address',
'master.devices.camera.table.column.action': 'Actions',
'master.devices.camera.table.offline.tooltip': 'Device is offline',
'master.devices.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',
'master.devices.camera.config.recording': 'Camera Recording',
'master.devices.camera.config.send': 'Send',
'master.devices.camera.config.alarmList': 'Alarm List',
'master.devices.camera.config.selected': '{0} items selected',
'master.devices.camera.config.clear': 'Clear',
'master.devices.camera.config.recordingMode.none': 'No Recording',
'master.devices.camera.config.recordingMode.alarm': 'On Alarm',
'master.devices.camera.config.recordingMode.all': '24/7',
// Terminal translations
'master.devices.terminal.pageTitle': 'Terminal',
'master.devices.terminal.loadDeviceError': 'Cannot load device information.',
'master.devices.terminal.mqttError': 'Cannot connect to MQTT.',
'master.devices.terminal.genericError': 'An error occurred',
'master.devices.terminal.unsupported.title':
'Device does not support terminal',
'master.devices.terminal.unsupported.desc':
'GMSv5 devices are not supported. Please use a different device.',
'master.devices.terminal.missingChannel.title':
'Missing control channel information',
'master.devices.terminal.missingChannel.desc':
'Device has not been configured with ctrl_channel_id, cannot open terminal.',
'master.devices.terminal.missingCredential.title':
'Missing authentication information',
'master.devices.terminal.missingCredential.desc':
'Current account has not been granted frontend_thing_id/frontend_thing_key.',
'master.devices.terminal.offline':
'Device is offline. Terminal is in view-only mode.',
'master.devices.terminal.connecting': 'Preparing terminal session...',
'master.devices.terminal.action.clear': 'Clear screen',
'master.devices.terminal.action.theme': 'Theme',
};

View File

@@ -46,56 +46,78 @@ export default {
'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':
'master.devices.camera.loading': 'Đang tải...',
'master.devices.camera.config.success': 'Đã gửi cấu hình thành công',
'master.devices.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':
'master.devices.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',
'master.devices.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',
'master.devices.camera.form.title.add': 'Tạo mới camera',
'master.devices.camera.form.title.edit': 'Chỉnh sửa camera',
'master.devices.camera.form.name': 'Tên',
'master.devices.camera.form.name.placeholder': 'Nhập tên',
'master.devices.camera.form.name.required': 'Vui lòng nhập tên',
'master.devices.camera.form.type': 'Loại',
'master.devices.camera.form.type.required': 'Vui lòng chọn loại',
'master.devices.camera.form.username': 'Tài khoản',
'master.devices.camera.form.username.placeholder': 'Nhập tài khoản',
'master.devices.camera.form.username.required': 'Vui lòng nhập tài khoản',
'master.devices.camera.form.password': 'Mật khẩu',
'master.devices.camera.form.password.placeholder': 'Nhập mật khẩu',
'master.devices.camera.form.password.required': 'Vui lòng nhập mật khẩu',
'master.devices.camera.form.ip': 'Địa chỉ IP',
'master.devices.camera.form.ip.placeholder': '192.168.1.10',
'master.devices.camera.form.ip.required': 'Vui lòng nhập địa chỉ IP',
'master.devices.camera.form.rtspPort': 'Cổng RTSP',
'master.devices.camera.form.rtspPort.required': 'Vui lòng nhập cổng RTSP',
'master.devices.camera.form.httpPort': 'Cổng HTTP',
'master.devices.camera.form.httpPort.required': 'Vui lòng nhập cổng HTTP',
'master.devices.camera.form.stream': 'Luồng',
'master.devices.camera.form.stream.required': 'Vui lòng nhập luồng',
'master.devices.camera.form.channel': 'Kênh',
'master.devices.camera.form.channel.required': 'Vui lòng nhập kênh',
'master.devices.camera.form.cancel': 'Hủy',
'master.devices.camera.form.submit': 'Đồng ý',
'master.devices.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',
'master.devices.camera.table.add': 'Tạo mới camera',
'master.devices.camera.table.column.name': 'Tên',
'master.devices.camera.table.column.type': 'Loại',
'master.devices.camera.table.column.ip': 'Địa chỉ IP',
'master.devices.camera.table.column.action': 'Thao tác',
'master.devices.camera.table.offline.tooltip': 'Thiết bị đang ngoại tuyến',
'master.devices.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',
'master.devices.camera.config.recording': 'Ghi dữ liệu camera',
'master.devices.camera.config.send': 'Gửi đi',
'master.devices.camera.config.alarmList': 'Danh sách cảnh báo',
'master.devices.camera.config.selected': 'đã chọn {0} mục',
'master.devices.camera.config.clear': 'Xóa',
'master.devices.camera.config.recordingMode.none': 'Không ghi',
'master.devices.camera.config.recordingMode.alarm': 'Theo cảnh báo',
'master.devices.camera.config.recordingMode.all': '24/24',
// Terminal translations
'master.devices.terminal.pageTitle': 'Terminal',
'master.devices.terminal.loadDeviceError':
'Không thể tải thông tin thiết bị.',
'master.devices.terminal.mqttError': 'Không thể kết nối MQTT.',
'master.devices.terminal.genericError': 'Đã có lỗi xảy ra',
'master.devices.terminal.unsupported.title': 'Thiết bị không hỗ trợ terminal',
'master.devices.terminal.unsupported.desc':
'Thiết bị GMSv5 không được hỗ trợ. Vui lòng sử dụng thiết bị khác.',
'master.devices.terminal.missingChannel.title':
'Thiếu thông tin kênh điều khiển',
'master.devices.terminal.missingChannel.desc':
'Thiết bị chưa được cấu hình ctrl_channel_id nên không thể mở terminal.',
'master.devices.terminal.missingCredential.title': 'Thiếu thông tin xác thực',
'master.devices.terminal.missingCredential.desc':
'Tài khoản hiện tại chưa được cấp frontend_thing_id/frontend_thing_key.',
'master.devices.terminal.offline':
'Thiết bị đang ngoại tuyến. Terminal chuyển sang chế độ chỉ xem.',
'master.devices.terminal.connecting': 'Đang chuẩn bị phiên terminal...',
'master.devices.terminal.action.clear': 'Xóa màn hình',
'master.devices.terminal.action.theme': 'Giao diện',
};

View File

@@ -1,11 +1,11 @@
import Footer from '@/components/Footer';
import LangSwitches from '@/components/Lang/LanguageSwitcherAuth';
import ThemeSwitcherAuth from '@/components/Theme/ThemeSwitcherAuth';
import { THEME_KEY } from '@/constants';
import { ROUTE_LOGIN } from '@/constants/routes';
import { apiUserResetPassword } from '@/services/master/UserController';
import { parseAccessToken } from '@/utils/jwt';
import { getDomainTitle, getLogoImage } from '@/utils/logo';
import { getTheme } from '@/utils/storage';
import { ProForm, ProFormText } from '@ant-design/pro-components';
import {
FormattedMessage,
@@ -28,9 +28,7 @@ const ResetPassword = () => {
const [tokenValid, setTokenValid] = useState(false);
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const [isDark, setIsDark] = useState(
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') === 'dark',
);
const [isDark, setIsDark] = useState(getTheme() === 'dark');
const { token } = theme.useToken();
const [messageApi, contextHolder] = message.useMessage();
const intl = useIntl();

View File

@@ -1,7 +1,6 @@
import Footer from '@/components/Footer';
import LangSwitches from '@/components/Lang/LanguageSwitcherAuth';
import ThemeSwitcherAuth from '@/components/Theme/ThemeSwitcherAuth';
import { THEME_KEY } from '@/constants';
import { ROUTER_HOME } from '@/constants/routes';
import {
apiForgotPassword,
@@ -14,6 +13,7 @@ import { getDomainTitle, getLogoImage } from '@/utils/logo';
import {
getBrowserId,
getRefreshToken,
getTheme,
removeAccessToken,
removeRefreshToken,
setAccessToken,
@@ -63,9 +63,7 @@ const FormWrapper = ({
};
const LoginPage = () => {
const [isDark, setIsDark] = useState(
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') === 'dark',
);
const [isDark, setIsDark] = useState(getTheme() === 'dark');
const { token } = theme.useToken();
const [messageApi, contextHolder] = message.useMessage();
const urlParams = new URL(window.location.href).searchParams;
@@ -108,8 +106,7 @@ const LoginPage = () => {
setInitialState((s: any) => ({
...s,
currentUserProfile: userInfo,
theme:
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light',
theme: getTheme() as 'light' | 'dark',
}));
});
}
@@ -150,9 +147,7 @@ const LoginPage = () => {
setInitialState((s: any) => ({
...s,
currentUserProfile: userInfo,
theme:
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') ||
'light',
theme: getTheme() as 'light' | 'dark',
}));
});
}
@@ -180,9 +175,7 @@ const LoginPage = () => {
setInitialState((s: any) => ({
...s,
currentUserProfile: userInfo,
theme:
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') ||
'light',
theme: getTheme() as 'light' | 'dark',
}));
});
}

View File

@@ -77,8 +77,8 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
<Modal
title={intl.formatMessage({
id: isEditMode
? 'master.camera.form.title.edit'
: 'master.camera.form.title.add',
? 'master.devices.camera.form.title.edit'
: 'master.devices.camera.form.title.add',
defaultMessage: isEditMode ? 'Chỉnh sửa camera' : 'Tạo mới camera',
})}
open={open}
@@ -86,15 +86,15 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
footer={[
<Button key="cancel" onClick={handleCancel}>
{intl.formatMessage({
id: 'master.camera.form.cancel',
id: 'master.devices.camera.form.cancel',
defaultMessage: 'Hủy',
})}
</Button>,
<Button key="submit" type="primary" onClick={handleSubmit}>
{intl.formatMessage({
id: isEditMode
? 'master.camera.form.update'
: 'master.camera.form.submit',
? 'master.devices.camera.form.update'
: 'master.devices.camera.form.submit',
defaultMessage: isEditMode ? 'Cập nhật' : 'Đồng ý',
})}
</Button>,
@@ -115,7 +115,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
>
<Form.Item
label={intl.formatMessage({
id: 'master.camera.form.name',
id: 'master.devices.camera.form.name',
defaultMessage: 'Tên',
})}
name="name"
@@ -123,7 +123,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
{
required: true,
message: intl.formatMessage({
id: 'master.camera.form.name.required',
id: 'master.devices.camera.form.name.required',
defaultMessage: 'Vui lòng nhập tên',
}),
},
@@ -131,7 +131,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
>
<Input
placeholder={intl.formatMessage({
id: 'master.camera.form.name.placeholder',
id: 'master.devices.camera.form.name.placeholder',
defaultMessage: 'nhập dữ liệu',
})}
/>
@@ -139,7 +139,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
<Form.Item
label={intl.formatMessage({
id: 'master.camera.form.type',
id: 'master.devices.camera.form.type',
defaultMessage: 'Loại',
})}
name="cate_id"
@@ -147,7 +147,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
{
required: true,
message: intl.formatMessage({
id: 'master.camera.form.type.required',
id: 'master.devices.camera.form.type.required',
defaultMessage: 'Vui lòng chọn loại',
}),
},
@@ -158,7 +158,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
<Form.Item
label={intl.formatMessage({
id: 'master.camera.form.username',
id: 'master.devices.camera.form.username',
defaultMessage: 'Tài khoản',
})}
name="username"
@@ -166,7 +166,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
{
required: true,
message: intl.formatMessage({
id: 'master.camera.form.username.required',
id: 'master.devices.camera.form.username.required',
defaultMessage: 'Vui lòng nhập tài khoản',
}),
},
@@ -174,7 +174,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
>
<Input
placeholder={intl.formatMessage({
id: 'master.camera.form.username.placeholder',
id: 'master.devices.camera.form.username.placeholder',
defaultMessage: 'nhập tài khoản',
})}
autoComplete="off"
@@ -183,7 +183,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
<Form.Item
label={intl.formatMessage({
id: 'master.camera.form.password',
id: 'master.devices.camera.form.password',
defaultMessage: 'Mật khẩu',
})}
name="password"
@@ -191,7 +191,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
{
required: true,
message: intl.formatMessage({
id: 'master.camera.form.password.required',
id: 'master.devices.camera.form.password.required',
defaultMessage: 'Vui lòng nhập mật khẩu',
}),
},
@@ -199,7 +199,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
>
<Input.Password
placeholder={intl.formatMessage({
id: 'master.camera.form.password.placeholder',
id: 'master.devices.camera.form.password.placeholder',
defaultMessage: 'nhập mật khẩu',
})}
autoComplete="new-password"
@@ -208,7 +208,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
<Form.Item
label={intl.formatMessage({
id: 'master.camera.form.ip',
id: 'master.devices.camera.form.ip',
defaultMessage: 'Địa chỉ IP',
})}
name="ip"
@@ -216,7 +216,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
{
required: true,
message: intl.formatMessage({
id: 'master.camera.form.ip.required',
id: 'master.devices.camera.form.ip.required',
defaultMessage: 'Vui lòng nhập địa chỉ IP',
}),
},
@@ -224,7 +224,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
>
<Input
placeholder={intl.formatMessage({
id: 'master.camera.form.ip.placeholder',
id: 'master.devices.camera.form.ip.placeholder',
defaultMessage: '192.168.1.10',
})}
/>
@@ -234,7 +234,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
<Col span={12}>
<Form.Item
label={intl.formatMessage({
id: 'master.camera.form.rtspPort',
id: 'master.devices.camera.form.rtspPort',
defaultMessage: 'Cổng RTSP',
})}
name="rtsp_port"
@@ -242,7 +242,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
{
required: true,
message: intl.formatMessage({
id: 'master.camera.form.rtspPort.required',
id: 'master.devices.camera.form.rtspPort.required',
defaultMessage: 'Vui lòng nhập cổng RTSP',
}),
},
@@ -254,7 +254,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
<Col span={12}>
<Form.Item
label={intl.formatMessage({
id: 'master.camera.form.httpPort',
id: 'master.devices.camera.form.httpPort',
defaultMessage: 'Cổng HTTP',
})}
name="http_port"
@@ -262,7 +262,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
{
required: true,
message: intl.formatMessage({
id: 'master.camera.form.httpPort.required',
id: 'master.devices.camera.form.httpPort.required',
defaultMessage: 'Vui lòng nhập cổng HTTP',
}),
},
@@ -277,7 +277,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
<Col span={12}>
<Form.Item
label={intl.formatMessage({
id: 'master.camera.form.stream',
id: 'master.devices.camera.form.stream',
defaultMessage: 'Luồng',
})}
name="stream"
@@ -285,7 +285,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
{
required: true,
message: intl.formatMessage({
id: 'master.camera.form.stream.required',
id: 'master.devices.camera.form.stream.required',
defaultMessage: 'Vui lòng nhập luồng',
}),
},
@@ -297,7 +297,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
<Col span={12}>
<Form.Item
label={intl.formatMessage({
id: 'master.camera.form.channel',
id: 'master.devices.camera.form.channel',
defaultMessage: 'Kênh',
})}
name="channel"
@@ -305,7 +305,7 @@ const CameraFormModal: React.FC<CameraFormModalProps> = ({
{
required: true,
message: intl.formatMessage({
id: 'master.camera.form.channel.required',
id: 'master.devices.camera.form.channel.required',
defaultMessage: 'Vui lòng nhập kênh',
}),
},

View File

@@ -51,7 +51,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
const columns = [
{
title: intl.formatMessage({
id: 'master.camera.table.column.name',
id: 'master.devices.camera.table.column.name',
defaultMessage: 'Tên',
}),
dataIndex: 'name',
@@ -62,7 +62,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
},
{
title: intl.formatMessage({
id: 'master.camera.table.column.type',
id: 'master.devices.camera.table.column.type',
defaultMessage: 'Loại',
}),
dataIndex: 'cate_id',
@@ -71,7 +71,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
},
{
title: intl.formatMessage({
id: 'master.camera.table.column.ip',
id: 'master.devices.camera.table.column.ip',
defaultMessage: 'Địa chỉ IP',
}),
dataIndex: 'ip',
@@ -80,7 +80,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
},
{
title: intl.formatMessage({
id: 'master.camera.table.column.action',
id: 'master.devices.camera.table.column.action',
defaultMessage: 'Thao tác',
}),
key: 'action',
@@ -89,7 +89,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
title={
!isOnline
? intl.formatMessage({
id: 'master.camera.table.offline.tooltip',
id: 'master.devices.camera.table.offline.tooltip',
defaultMessage: 'Thiết bị đang ngoại tuyến',
})
: ''
@@ -113,7 +113,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
title={
!isOnline
? intl.formatMessage({
id: 'master.camera.table.offline.tooltip',
id: 'master.devices.camera.table.offline.tooltip',
defaultMessage: 'Thiết bị đang ngoại tuyến',
})
: ''
@@ -126,7 +126,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
disabled={!isOnline}
>
{intl.formatMessage({
id: 'master.camera.table.add',
id: 'master.devices.camera.table.add',
defaultMessage: 'Tạo mới camera',
})}
</Button>
@@ -140,7 +140,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
title={
!isOnline
? intl.formatMessage({
id: 'master.camera.table.offline.tooltip',
id: 'master.devices.camera.table.offline.tooltip',
defaultMessage: 'Thiết bị đang ngoại tuyến',
})
: ''
@@ -172,7 +172,7 @@ const CameraTable: React.FC<CameraTableProps> = ({
showTotal: (total: number, range: [number, number]) =>
intl.formatMessage(
{
id: 'master.camera.table.pagination',
id: 'master.devices.camera.table.pagination',
defaultMessage: 'Hiển thị {0}-{1} của {2} camera',
},
{

View File

@@ -21,14 +21,14 @@ const CameraV5: React.FC<CameraV5Props> = ({
() => [
{
label: intl.formatMessage({
id: 'master.camera.config.recordingMode.none',
id: 'master.devices.camera.config.recordingMode.none',
defaultMessage: 'Không ghi',
}),
value: 'none',
},
{
label: intl.formatMessage({
id: 'master.camera.config.recordingMode.all',
id: 'master.devices.camera.config.recordingMode.all',
defaultMessage: '24/24',
}),
value: 'all',
@@ -52,7 +52,7 @@ const CameraV5: React.FC<CameraV5Props> = ({
<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',
id: 'master.devices.camera.config.recording',
defaultMessage: 'Ghi dữ liệu camera',
})}
</Text>
@@ -66,7 +66,7 @@ const CameraV5: React.FC<CameraV5Props> = ({
</div>
<Button type="primary" onClick={handleSubmit}>
{intl.formatMessage({
id: 'master.camera.config.send',
id: 'master.devices.camera.config.send',
defaultMessage: 'Gửi đi',
})}
</Button>

View File

@@ -42,21 +42,21 @@ const CameraV6: React.FC<CameraV6Props> = ({
() => [
{
label: intl.formatMessage({
id: 'master.camera.config.recordingMode.none',
id: 'master.devices.camera.config.recordingMode.none',
defaultMessage: 'Không ghi',
}),
value: 'none',
},
{
label: intl.formatMessage({
id: 'master.camera.config.recordingMode.alarm',
id: 'master.devices.camera.config.recordingMode.alarm',
defaultMessage: 'Theo cảnh báo',
}),
value: 'alarm',
},
{
label: intl.formatMessage({
id: 'master.camera.config.recordingMode.all',
id: 'master.devices.camera.config.recordingMode.all',
defaultMessage: '24/24',
}),
value: 'all',
@@ -148,7 +148,7 @@ const CameraV6: React.FC<CameraV6Props> = ({
<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',
id: 'master.devices.camera.config.recording',
defaultMessage: 'Ghi dữ liệu camera',
})}
</Text>
@@ -164,7 +164,7 @@ const CameraV6: React.FC<CameraV6Props> = ({
title={
!isOnline
? intl.formatMessage({
id: 'master.camera.table.offline.tooltip',
id: 'master.devices.camera.table.offline.tooltip',
defaultMessage: 'Thiết bị đang ngoại tuyến',
})
: ''
@@ -176,7 +176,7 @@ const CameraV6: React.FC<CameraV6Props> = ({
disabled={!isOnline}
>
{intl.formatMessage({
id: 'master.camera.config.send',
id: 'master.devices.camera.config.send',
defaultMessage: 'Gửi đi',
})}
</Button>
@@ -189,7 +189,7 @@ const CameraV6: React.FC<CameraV6Props> = ({
<div>
<Text strong className="block mb-2">
{intl.formatMessage({
id: 'master.camera.config.alarmList',
id: 'master.devices.camera.config.alarmList',
defaultMessage: 'Danh sách cảnh báo',
})}
</Text>
@@ -204,7 +204,7 @@ const CameraV6: React.FC<CameraV6Props> = ({
<Text type="secondary">
{intl.formatMessage(
{
id: 'master.camera.config.selected',
id: 'master.devices.camera.config.selected',
defaultMessage: 'đã chọn {0} mục',
},
{
@@ -214,7 +214,7 @@ const CameraV6: React.FC<CameraV6Props> = ({
</Text>
<Button type="link" onClick={handleClearAlerts}>
{intl.formatMessage({
id: 'master.camera.config.clear',
id: 'master.devices.camera.config.clear',
defaultMessage: 'Xóa',
})}
</Button>

View File

@@ -146,7 +146,7 @@ const CameraConfigPage = () => {
if (!isOnline) {
message.error(
intl.formatMessage({
id: 'master.camera.config.error.deviceOffline',
id: 'master.devices.camera.config.error.deviceOffline',
defaultMessage: 'Thiết bị đang ngoại tuyến, không thể gửi cấu hình',
}),
);
@@ -156,7 +156,7 @@ const CameraConfigPage = () => {
if (!thing?.metadata?.cfg_channel_id || !thing?.metadata?.external_id) {
message.error(
intl.formatMessage({
id: 'master.camera.config.error.missingConfig',
id: 'master.devices.camera.config.error.missingConfig',
defaultMessage: 'Thiếu thông tin cấu hình thiết bị',
}),
);
@@ -188,7 +188,7 @@ const CameraConfigPage = () => {
mqttClient.publish(pubTopic, payload);
message.success(
intl.formatMessage({
id: 'master.camera.config.success',
id: 'master.devices.camera.config.success',
defaultMessage: 'Đã gửi cấu hình thành công',
}),
);
@@ -196,7 +196,7 @@ const CameraConfigPage = () => {
} else {
message.error(
intl.formatMessage({
id: 'master.camera.config.error.mqttNotConnected',
id: 'master.devices.camera.config.error.mqttNotConnected',
defaultMessage: 'MQTT chưa kết nối',
}),
);
@@ -290,7 +290,7 @@ const CameraConfigPage = () => {
<span>
{thing?.name ||
intl.formatMessage({
id: 'master.camera.loading',
id: 'master.devices.camera.loading',
defaultMessage: 'Loading...',
})}
</span>

View File

@@ -5,6 +5,7 @@ import { getBadgeConnection } from '@/components/shared/ThingShared';
import { ROUTE_MANAGER_DEVICES } from '@/constants/routes';
import { apiGetThingDetail } from '@/services/master/ThingController';
import { mqttClient } from '@/utils/mqttClient';
import { getTerminalTheme, setTerminalTheme } from '@/utils/storage';
import { BgColorsOutlined, ClearOutlined } from '@ant-design/icons';
import { PageContainer, ProCard } from '@ant-design/pro-components';
import { history, useIntl, useModel, useParams } from '@umijs/max';
@@ -210,7 +211,7 @@ const DeviceTerminalPage = () => {
console.error('Failed to load device details', error);
setTerminalError(
intl.formatMessage({
id: 'terminal.loadDeviceError',
id: 'master.devices.terminal.loadDeviceError',
defaultMessage: 'Không thể tải thông tin thiết bị.',
}),
);
@@ -231,7 +232,7 @@ const DeviceTerminalPage = () => {
// Khôi phục theme từ localStorage khi component mount
useEffect(() => {
const savedTheme = localStorage.getItem('terminal_theme_key');
const savedTheme = getTerminalTheme();
if (savedTheme && TERMINAL_THEMES[savedTheme]) {
setSelectedThemeKey(savedTheme);
}
@@ -326,7 +327,7 @@ const DeviceTerminalPage = () => {
mqttClient.disconnect();
setTerminalError(
intl.formatMessage({
id: 'terminal.mqttError',
id: 'master.devices.terminal.mqttError',
defaultMessage: 'Không thể kết nối MQTT.',
}),
);
@@ -428,7 +429,7 @@ const DeviceTerminalPage = () => {
const pageTitle =
thing?.name ||
intl.formatMessage({
id: 'terminal.pageTitle',
id: 'master.devices.terminal.pageTitle',
defaultMessage: 'Terminal',
});
@@ -456,12 +457,12 @@ const DeviceTerminalPage = () => {
/**
* Xử lý thay đổi theme
* - Cập nhật state
* - Lưu vào localStorage
* - Lưu vào localStorage thông qua storage utility
*/
const handleThemeChange: MenuProps['onClick'] = (e) => {
const themeKey = e.key;
setSelectedThemeKey(themeKey);
localStorage.setItem('terminal_theme_key', themeKey);
setTerminalTheme(themeKey);
};
/**
@@ -507,7 +508,7 @@ const DeviceTerminalPage = () => {
<Result
status="error"
title={intl.formatMessage({
id: 'terminal.genericError',
id: 'master.devices.terminal.genericError',
defaultMessage: 'Đã có lỗi xảy ra',
})}
subTitle={terminalError}
@@ -530,11 +531,11 @@ const DeviceTerminalPage = () => {
renderBlockingResult(
'info',
intl.formatMessage({
id: 'terminal.unsupported.title',
id: 'master.devices.terminal.unsupported.title',
defaultMessage: 'Thiết bị không hỗ trợ terminal',
}),
intl.formatMessage({
id: 'terminal.unsupported.desc',
id: 'master.devices.terminal.unsupported.desc',
defaultMessage:
'Thiết bị GMSv5 không được hỗ trợ. Vui lòng sử dụng thiết bị khác.',
}),
@@ -549,11 +550,11 @@ const DeviceTerminalPage = () => {
renderBlockingResult(
'warning',
intl.formatMessage({
id: 'terminal.missingChannel.title',
id: 'master.devices.terminal.missingChannel.title',
defaultMessage: 'Thiếu thông tin kênh điều khiển',
}),
intl.formatMessage({
id: 'terminal.missingChannel.desc',
id: 'master.devices.terminal.missingChannel.desc',
defaultMessage:
'Thiết bị chưa được cấu hình ctrl_channel_id nên không thể mở terminal.',
}),
@@ -569,11 +570,11 @@ const DeviceTerminalPage = () => {
renderBlockingResult(
'warning',
intl.formatMessage({
id: 'terminal.missingCredential.title',
id: 'master.devices.terminal.missingCredential.title',
defaultMessage: 'Thiếu thông tin xác thực',
}),
intl.formatMessage({
id: 'terminal.missingCredential.desc',
id: 'master.devices.terminal.missingCredential.desc',
defaultMessage:
'Tài khoản hiện tại chưa được cấp frontend_thing_id/frontend_thing_key.',
}),
@@ -588,7 +589,7 @@ const DeviceTerminalPage = () => {
type="warning"
showIcon
message={intl.formatMessage({
id: 'terminal.offline',
id: 'master.devices.terminal.offline',
defaultMessage:
'Thiết bị đang ngoại tuyến. Terminal chuyển sang chế độ chỉ xem.',
})}
@@ -600,8 +601,9 @@ const DeviceTerminalPage = () => {
type="info"
showIcon
message={intl.formatMessage({
id: 'terminal.connecting',
defaultMessage: 'Đang chuẩn bị phiên terminal...',
id: 'master.devices.terminal.connecting',
defaultMessage:
'Đang chuẩn bị phiên master.devices.terminal...',
})}
/>
)}
@@ -631,7 +633,7 @@ const DeviceTerminalPage = () => {
disabled={!isSessionReady}
>
{intl.formatMessage({
id: 'terminal.action.clear',
id: 'master.devices.terminal.action.clear',
defaultMessage: 'Xóa màn hình',
})}
</Button>
@@ -645,7 +647,7 @@ const DeviceTerminalPage = () => {
>
<Button icon={<BgColorsOutlined />}>
{intl.formatMessage({
id: 'terminal.action.theme',
id: 'master.devices.terminal.action.theme',
defaultMessage: 'Theme',
})}
: {TERMINAL_THEMES[selectedThemeKey]?.name || 'Dark'}

View File

@@ -1,4 +1,9 @@
import { ACCESS_TOKEN, REFRESH_TOKEN } from '@/constants';
import {
ACCESS_TOKEN,
REFRESH_TOKEN,
TERMINAL_THEME_KEY,
THEME_KEY,
} from '@/constants';
export function getAccessToken(): string {
return localStorage.getItem(ACCESS_TOKEN) || '';
@@ -24,6 +29,30 @@ export function removeRefreshToken() {
localStorage.removeItem(REFRESH_TOKEN);
}
export function getTerminalTheme(): string {
return localStorage.getItem(TERMINAL_THEME_KEY) || 'dark';
}
export function setTerminalTheme(themeKey: string) {
localStorage.setItem(TERMINAL_THEME_KEY, themeKey);
}
export function removeTerminalTheme() {
localStorage.removeItem(TERMINAL_THEME_KEY);
}
export function getTheme(): string {
return localStorage.getItem(THEME_KEY) || 'light';
}
export function setTheme(themeKey: string) {
localStorage.setItem(THEME_KEY, themeKey);
}
export function removeTheme() {
localStorage.removeItem(THEME_KEY);
}
export function getBrowserId() {
const id = localStorage.getItem('sip-browserid');
if (!id) {
@@ -42,14 +71,18 @@ export function getBrowserId() {
}
/**
* Clear all localStorage data except browserId
* Clear all localStorage data except browserId, theme and terminal theme
*/
export function clearAllData() {
const browserId = localStorage.getItem('sip-browserid');
const theme = getTheme();
const terminalTheme = getTerminalTheme();
localStorage.clear();
if (browserId) {
localStorage.setItem('sip-browserid', browserId);
}
localStorage.setItem(THEME_KEY, theme);
localStorage.setItem(TERMINAL_THEME_KEY, terminalTheme);
}
/**