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 { getLocale, history, Link, RunTimeLayoutConfig } from '@umijs/max';
import { ConfigProvider } from 'antd'; import { ConfigProvider } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@@ -10,7 +11,6 @@ import IconFont from './components/IconFont';
import LanguageSwitcher from './components/Lang/LanguageSwitcher'; import LanguageSwitcher from './components/Lang/LanguageSwitcher';
import ThemeProvider from './components/Theme/ThemeProvider'; import ThemeProvider from './components/Theme/ThemeProvider';
import ThemeSwitcher from './components/Theme/ThemeSwitcher'; import ThemeSwitcher from './components/Theme/ThemeSwitcher';
import { THEME_KEY } from './constants';
import { ROUTE_FORGOT_PASSWORD, ROUTE_LOGIN } from './constants/routes'; import { ROUTE_FORGOT_PASSWORD, ROUTE_LOGIN } from './constants/routes';
import NotFoundPage from './pages/Exception/NotFound'; import NotFoundPage from './pages/Exception/NotFound';
import UnAccessPage from './pages/Exception/UnAccess'; import UnAccessPage from './pages/Exception/UnAccess';
@@ -52,8 +52,7 @@ export async function getInitialState(): Promise<InitialStateResponse> {
// Public routes that don't require authentication // Public routes that don't require authentication
if (publicRoutes.includes(pathname)) { if (publicRoutes.includes(pathname)) {
const currentTheme = const currentTheme = (getTheme() as 'light' | 'dark') || 'light';
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light';
return { return {
theme: currentTheme, theme: currentTheme,
}; };
@@ -101,8 +100,7 @@ export async function getInitialState(): Promise<InitialStateResponse> {
} }
}; };
const resp = await getUserProfile(); const resp = await getUserProfile();
const currentTheme = const currentTheme = (getTheme() as 'light' | 'dark') || 'light';
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light';
return { return {
getUserProfile: getUserProfile!, getUserProfile: getUserProfile!,
currentUserProfile: resp, 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 { MoonOutlined, SunOutlined } from '@ant-design/icons';
import { useModel } from '@umijs/max'; import { useModel } from '@umijs/max';
import { Segmented } from 'antd'; import { Segmented } from 'antd';
@@ -34,7 +34,7 @@ const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
if (!supportsViewTransition) { if (!supportsViewTransition) {
// Fallback: just change theme without animation // Fallback: just change theme without animation
localStorage.setItem(THEME_KEY, newTheme); setTheme(newTheme);
setIsDark(newTheme === 'dark'); setIsDark(newTheme === 'dark');
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('theme-change', { detail: { theme: newTheme } }), new CustomEvent('theme-change', { detail: { theme: newTheme } }),
@@ -58,7 +58,7 @@ const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
// Start the view transition // Start the view transition
const transition = (document as any).startViewTransition(() => { const transition = (document as any).startViewTransition(() => {
localStorage.setItem(THEME_KEY, newTheme); setTheme(newTheme);
setIsDark(newTheme === 'dark'); setIsDark(newTheme === 'dark');
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('theme-change', { detail: { theme: newTheme } }), new CustomEvent('theme-change', { detail: { theme: newTheme } }),
@@ -107,8 +107,3 @@ const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
}; };
export default ThemeSwitcher; 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 React, { useEffect, useState } from 'react';
import { getTheme } from './ThemeSwitcher';
import './style.less'; import './style.less';
const ThemeSwitcherAuth = () => { const ThemeSwitcherAuth = () => {
@@ -21,7 +20,7 @@ const ThemeSwitcherAuth = () => {
const handleSwitch = (e: React.ChangeEvent<HTMLInputElement>) => { const handleSwitch = (e: React.ChangeEvent<HTMLInputElement>) => {
const newTheme = e.target.checked ? 'dark' : 'light'; const newTheme = e.target.checked ? 'dark' : 'light';
localStorage.setItem(THEME_KEY, newTheme); setTheme(newTheme);
setIsDark(newTheme === 'dark'); setIsDark(newTheme === 'dark');
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('theme-change', { detail: { theme: newTheme } }), 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_NORMAL = 0;
export const STATUS_WARNING = 1; export const STATUS_WARNING = 1;
export const STATUS_DANGEROUS = 2; export const STATUS_DANGEROUS = 2;
export const STATUS_SOS = 3; export const STATUS_SOS = 3;
export const COLOR_DISCONNECT = '#d9d9d9'; export const COLOR_DISCONNECT = '#d9d9d9';
@@ -22,6 +23,7 @@ export const COLOR_SOS = '#ff0000';
export const ACCESS_TOKEN = 'access_token'; export const ACCESS_TOKEN = 'access_token';
export const REFRESH_TOKEN = 'refresh_token'; export const REFRESH_TOKEN = 'refresh_token';
export const THEME_KEY = 'theme'; export const THEME_KEY = 'theme';
export const TERMINAL_THEME_KEY = 'terminal_theme_key';
// Global Constants // Global Constants
export const LIMIT_TREE_LEVEL = 5; export const LIMIT_TREE_LEVEL = 5;
export const DEFAULT_PAGE_SIZE = 5; export const DEFAULT_PAGE_SIZE = 5;

View File

@@ -46,56 +46,79 @@ export default {
'master.devices.location.update.error': 'Location update failed', 'master.devices.location.update.error': 'Location update failed',
// Camera translations // Camera translations
'master.camera.loading': 'Loading...', 'master.devices.camera.loading': 'Loading...',
'master.camera.config.success': 'Configuration sent successfully', 'master.devices.camera.config.success': 'Configuration sent successfully',
'master.camera.config.error.deviceOffline': 'master.devices.camera.config.error.deviceOffline':
'Device is offline, cannot send configuration', 'Device is offline, cannot send configuration',
'master.camera.config.error.missingConfig': 'master.devices.camera.config.error.missingConfig':
'Missing device configuration information', 'Missing device configuration information',
'master.camera.config.error.mqttNotConnected': 'MQTT not connected', 'master.devices.camera.config.error.mqttNotConnected': 'MQTT not connected',
// Camera Form Modal // Camera Form Modal
'master.camera.form.title.add': 'Add New Camera', 'master.devices.camera.form.title.add': 'Add New Camera',
'master.camera.form.title.edit': 'Edit Camera', 'master.devices.camera.form.title.edit': 'Edit Camera',
'master.camera.form.name': 'Name', 'master.devices.camera.form.name': 'Name',
'master.camera.form.name.placeholder': 'Enter name', 'master.devices.camera.form.name.placeholder': 'Enter name',
'master.camera.form.name.required': 'Please enter name', 'master.devices.camera.form.name.required': 'Please enter name',
'master.camera.form.type': 'Type', 'master.devices.camera.form.type': 'Type',
'master.camera.form.type.required': 'Please select type', 'master.devices.camera.form.type.required': 'Please select type',
'master.camera.form.username': 'Username', 'master.devices.camera.form.username': 'Username',
'master.camera.form.username.placeholder': 'Enter username', 'master.devices.camera.form.username.placeholder': 'Enter username',
'master.camera.form.username.required': 'Please enter username', 'master.devices.camera.form.username.required': 'Please enter username',
'master.camera.form.password': 'Password', 'master.devices.camera.form.password': 'Password',
'master.camera.form.password.placeholder': 'Enter password', 'master.devices.camera.form.password.placeholder': 'Enter password',
'master.camera.form.password.required': 'Please enter password', 'master.devices.camera.form.password.required': 'Please enter password',
'master.camera.form.ip': 'IP Address', 'master.devices.camera.form.ip': 'IP Address',
'master.camera.form.ip.placeholder': '192.168.1.10', 'master.devices.camera.form.ip.placeholder': '192.168.1.10',
'master.camera.form.ip.required': 'Please enter IP address', 'master.devices.camera.form.ip.required': 'Please enter IP address',
'master.camera.form.rtspPort': 'RTSP Port', 'master.devices.camera.form.rtspPort': 'RTSP Port',
'master.camera.form.rtspPort.required': 'Please enter RTSP port', 'master.devices.camera.form.rtspPort.required': 'Please enter RTSP port',
'master.camera.form.httpPort': 'HTTP Port', 'master.devices.camera.form.httpPort': 'HTTP Port',
'master.camera.form.httpPort.required': 'Please enter HTTP port', 'master.devices.camera.form.httpPort.required': 'Please enter HTTP port',
'master.camera.form.stream': 'Stream', 'master.devices.camera.form.stream': 'Stream',
'master.camera.form.stream.required': 'Please enter stream', 'master.devices.camera.form.stream.required': 'Please enter stream',
'master.camera.form.channel': 'Channel', 'master.devices.camera.form.channel': 'Channel',
'master.camera.form.channel.required': 'Please enter channel', 'master.devices.camera.form.channel.required': 'Please enter channel',
'master.camera.form.cancel': 'Cancel', 'master.devices.camera.form.cancel': 'Cancel',
'master.camera.form.submit': 'OK', 'master.devices.camera.form.submit': 'OK',
'master.camera.form.update': 'Update', 'master.devices.camera.form.update': 'Update',
// Camera Table // Camera Table
'master.camera.table.add': 'Add New Camera', 'master.devices.camera.table.add': 'Add New Camera',
'master.camera.table.column.name': 'Name', 'master.devices.camera.table.column.name': 'Name',
'master.camera.table.column.type': 'Type', 'master.devices.camera.table.column.type': 'Type',
'master.camera.table.column.ip': 'IP Address', 'master.devices.camera.table.column.ip': 'IP Address',
'master.camera.table.column.action': 'Actions', 'master.devices.camera.table.column.action': 'Actions',
'master.camera.table.offline.tooltip': 'Device is offline', 'master.devices.camera.table.offline.tooltip': 'Device is offline',
'master.camera.table.pagination': 'Showing {0}-{1} of {2} cameras', 'master.devices.camera.table.pagination': 'Showing {0}-{1} of {2} cameras',
// Camera Config V6 // Camera Config V6
'master.camera.config.recording': 'Camera Recording', 'master.devices.camera.config.recording': 'Camera Recording',
'master.camera.config.send': 'Send', 'master.devices.camera.config.send': 'Send',
'master.camera.config.alarmList': 'Alarm List', 'master.devices.camera.config.alarmList': 'Alarm List',
'master.camera.config.selected': '{0} items selected', 'master.devices.camera.config.selected': '{0} items selected',
'master.camera.config.clear': 'Clear', 'master.devices.camera.config.clear': 'Clear',
'master.camera.config.recordingMode.none': 'No Recording', 'master.devices.camera.config.recordingMode.none': 'No Recording',
'master.camera.config.recordingMode.alarm': 'On Alarm', 'master.devices.camera.config.recordingMode.alarm': 'On Alarm',
'master.camera.config.recordingMode.all': '24/7', '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', 'master.devices.location.update.error': 'Cập nhật vị trí thất bại',
// Camera translations // Camera translations
'master.camera.loading': 'Đang tải...', 'master.devices.camera.loading': 'Đang tải...',
'master.camera.config.success': 'Đã gửi cấu hình thành công', 'master.devices.camera.config.success': 'Đã gửi cấu hình thành công',
'master.camera.config.error.deviceOffline': 'master.devices.camera.config.error.deviceOffline':
'Thiết bị đang ngoại tuyến, không thể gửi cấu hình', '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ị', '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 // Camera Form Modal
'master.camera.form.title.add': 'Tạo mới camera', 'master.devices.camera.form.title.add': 'Tạo mới camera',
'master.camera.form.title.edit': 'Chỉnh sửa camera', 'master.devices.camera.form.title.edit': 'Chỉnh sửa camera',
'master.camera.form.name': 'Tên', 'master.devices.camera.form.name': 'Tên',
'master.camera.form.name.placeholder': 'Nhập tên', 'master.devices.camera.form.name.placeholder': 'Nhập tên',
'master.camera.form.name.required': 'Vui lòng nhập tên', 'master.devices.camera.form.name.required': 'Vui lòng nhập tên',
'master.camera.form.type': 'Loại', 'master.devices.camera.form.type': 'Loại',
'master.camera.form.type.required': 'Vui lòng chọn loại', 'master.devices.camera.form.type.required': 'Vui lòng chọn loại',
'master.camera.form.username': 'Tài khoản', 'master.devices.camera.form.username': 'Tài khoản',
'master.camera.form.username.placeholder': 'Nhập tài khoản', 'master.devices.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.devices.camera.form.username.required': 'Vui lòng nhập tài khoản',
'master.camera.form.password': 'Mật khẩu', 'master.devices.camera.form.password': 'Mật khẩu',
'master.camera.form.password.placeholder': 'Nhập mật khẩu', 'master.devices.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.devices.camera.form.password.required': 'Vui lòng nhập mật khẩu',
'master.camera.form.ip': 'Địa chỉ IP', 'master.devices.camera.form.ip': 'Địa chỉ IP',
'master.camera.form.ip.placeholder': '192.168.1.10', 'master.devices.camera.form.ip.placeholder': '192.168.1.10',
'master.camera.form.ip.required': 'Vui lòng nhập địa chỉ IP', 'master.devices.camera.form.ip.required': 'Vui lòng nhập địa chỉ IP',
'master.camera.form.rtspPort': 'Cổng RTSP', 'master.devices.camera.form.rtspPort': 'Cổng RTSP',
'master.camera.form.rtspPort.required': 'Vui lòng nhập cổng RTSP', 'master.devices.camera.form.rtspPort.required': 'Vui lòng nhập cổng RTSP',
'master.camera.form.httpPort': 'Cổng HTTP', 'master.devices.camera.form.httpPort': 'Cổng HTTP',
'master.camera.form.httpPort.required': 'Vui lòng nhập cổng HTTP', 'master.devices.camera.form.httpPort.required': 'Vui lòng nhập cổng HTTP',
'master.camera.form.stream': 'Luồng', 'master.devices.camera.form.stream': 'Luồng',
'master.camera.form.stream.required': 'Vui lòng nhập luồng', 'master.devices.camera.form.stream.required': 'Vui lòng nhập luồng',
'master.camera.form.channel': 'Kênh', 'master.devices.camera.form.channel': 'Kênh',
'master.camera.form.channel.required': 'Vui lòng nhập kênh', 'master.devices.camera.form.channel.required': 'Vui lòng nhập kênh',
'master.camera.form.cancel': 'Hủy', 'master.devices.camera.form.cancel': 'Hủy',
'master.camera.form.submit': 'Đồng ý', 'master.devices.camera.form.submit': 'Đồng ý',
'master.camera.form.update': 'Cập nhật', 'master.devices.camera.form.update': 'Cập nhật',
// Camera Table // Camera Table
'master.camera.table.add': 'Tạo mới camera', 'master.devices.camera.table.add': 'Tạo mới camera',
'master.camera.table.column.name': 'Tên', 'master.devices.camera.table.column.name': 'Tên',
'master.camera.table.column.type': 'Loại', 'master.devices.camera.table.column.type': 'Loại',
'master.camera.table.column.ip': 'Địa chỉ IP', 'master.devices.camera.table.column.ip': 'Địa chỉ IP',
'master.camera.table.column.action': 'Thao tác', 'master.devices.camera.table.column.action': 'Thao tác',
'master.camera.table.offline.tooltip': 'Thiết bị đang ngoại tuyến', 'master.devices.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.pagination': 'Hiển thị {0}-{1} của {2} camera',
// Camera Config V6 // Camera Config V6
'master.camera.config.recording': 'Ghi dữ liệu camera', 'master.devices.camera.config.recording': 'Ghi dữ liệu camera',
'master.camera.config.send': 'Gửi đi', 'master.devices.camera.config.send': 'Gửi đi',
'master.camera.config.alarmList': 'Danh sách cảnh báo', 'master.devices.camera.config.alarmList': 'Danh sách cảnh báo',
'master.camera.config.selected': 'đã chọn {0} mục', 'master.devices.camera.config.selected': 'đã chọn {0} mục',
'master.camera.config.clear': 'Xóa', 'master.devices.camera.config.clear': 'Xóa',
'master.camera.config.recordingMode.none': 'Không ghi', 'master.devices.camera.config.recordingMode.none': 'Không ghi',
'master.camera.config.recordingMode.alarm': 'Theo cảnh báo', 'master.devices.camera.config.recordingMode.alarm': 'Theo cảnh báo',
'master.camera.config.recordingMode.all': '24/24', '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 Footer from '@/components/Footer';
import LangSwitches from '@/components/Lang/LanguageSwitcherAuth'; import LangSwitches from '@/components/Lang/LanguageSwitcherAuth';
import ThemeSwitcherAuth from '@/components/Theme/ThemeSwitcherAuth'; import ThemeSwitcherAuth from '@/components/Theme/ThemeSwitcherAuth';
import { THEME_KEY } from '@/constants';
import { ROUTE_LOGIN } from '@/constants/routes'; import { ROUTE_LOGIN } from '@/constants/routes';
import { apiUserResetPassword } from '@/services/master/UserController'; import { apiUserResetPassword } from '@/services/master/UserController';
import { parseAccessToken } from '@/utils/jwt'; import { parseAccessToken } from '@/utils/jwt';
import { getDomainTitle, getLogoImage } from '@/utils/logo'; import { getDomainTitle, getLogoImage } from '@/utils/logo';
import { getTheme } from '@/utils/storage';
import { ProForm, ProFormText } from '@ant-design/pro-components'; import { ProForm, ProFormText } from '@ant-design/pro-components';
import { import {
FormattedMessage, FormattedMessage,
@@ -28,9 +28,7 @@ const ResetPassword = () => {
const [tokenValid, setTokenValid] = useState(false); const [tokenValid, setTokenValid] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false); const [success, setSuccess] = useState(false);
const [isDark, setIsDark] = useState( const [isDark, setIsDark] = useState(getTheme() === 'dark');
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') === 'dark',
);
const { token } = theme.useToken(); const { token } = theme.useToken();
const [messageApi, contextHolder] = message.useMessage(); const [messageApi, contextHolder] = message.useMessage();
const intl = useIntl(); const intl = useIntl();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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