feat(project): base smatec's frontend
This commit is contained in:
3
.eslintrc.js
Normal file
3
.eslintrc.js
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: require.resolve('@umijs/max/eslint'),
|
||||||
|
};
|
||||||
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/node_modules
|
||||||
|
/.env.local
|
||||||
|
/.umirc.local.ts
|
||||||
|
/config/config.local.ts
|
||||||
|
/src/.umi
|
||||||
|
/src/.umi-production
|
||||||
|
/src/.umi-test
|
||||||
|
/.umi
|
||||||
|
/.umi-production
|
||||||
|
/.umi-test
|
||||||
|
/dist
|
||||||
|
/.mfsu
|
||||||
|
.swc
|
||||||
|
.turbopack
|
||||||
|
/dist
|
||||||
|
.DS_Store
|
||||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx --no-install max verify-commit $1
|
||||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
npx --no-install lint-staged --quiet
|
||||||
17
.lintstagedrc
Normal file
17
.lintstagedrc
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"*.{md,json}": [
|
||||||
|
"prettier --cache --write"
|
||||||
|
],
|
||||||
|
"*.{js,jsx}": [
|
||||||
|
"max lint --fix --eslint-only",
|
||||||
|
"prettier --cache --write"
|
||||||
|
],
|
||||||
|
"*.{css,less}": [
|
||||||
|
"max lint --fix --stylelint-only",
|
||||||
|
"prettier --cache --write"
|
||||||
|
],
|
||||||
|
"*.ts?(x)": [
|
||||||
|
"max lint --fix --eslint-only",
|
||||||
|
"prettier --cache --parser=typescript --write"
|
||||||
|
]
|
||||||
|
}
|
||||||
3
.prettierignore
Normal file
3
.prettierignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
.umi
|
||||||
|
.umi-production
|
||||||
8
.prettierrc
Normal file
8
.prettierrc
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 80,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"proseWrap": "never",
|
||||||
|
"overrides": [{ "files": ".prettierrc", "options": { "parser": "json" } }],
|
||||||
|
"plugins": ["prettier-plugin-organize-imports", "prettier-plugin-packagejson"]
|
||||||
|
}
|
||||||
11
.stylelintrc.js
Normal file
11
.stylelintrc.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
module.exports = {
|
||||||
|
extends: require.resolve('@umijs/max/stylelint'),
|
||||||
|
rules: {
|
||||||
|
'at-rule-no-unknown': [
|
||||||
|
true,
|
||||||
|
{
|
||||||
|
ignoreAtRules: ['tailwind'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
36
.umirc.gms.ts
Normal file
36
.umirc.gms.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { defineConfig } from '@umijs/max';
|
||||||
|
import {
|
||||||
|
alarmsRoute,
|
||||||
|
commonManagerRoutes,
|
||||||
|
loginRoute,
|
||||||
|
managerRouteBase,
|
||||||
|
notFoundRoute,
|
||||||
|
profileRoute,
|
||||||
|
} from './config/routes';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
favicons: ['/gms-logo.png'],
|
||||||
|
layout: {
|
||||||
|
title: 'GMS',
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
loginRoute,
|
||||||
|
alarmsRoute,
|
||||||
|
profileRoute,
|
||||||
|
{
|
||||||
|
name: 'gms.monitor',
|
||||||
|
icon: 'icon-monitor',
|
||||||
|
path: '/monitor',
|
||||||
|
component: './Slave/GMS/Monitor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...managerRouteBase,
|
||||||
|
routes: [...commonManagerRoutes],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/monitor',
|
||||||
|
},
|
||||||
|
notFoundRoute,
|
||||||
|
],
|
||||||
|
});
|
||||||
66
.umirc.sgw.ts
Normal file
66
.umirc.sgw.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { defineConfig } from '@umijs/max';
|
||||||
|
import {
|
||||||
|
alarmsRoute,
|
||||||
|
commonManagerRoutes,
|
||||||
|
loginRoute,
|
||||||
|
managerRouteBase,
|
||||||
|
notFoundRoute,
|
||||||
|
profileRoute,
|
||||||
|
} from './config/routes';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
favicons: ['/sgw-logo.png'],
|
||||||
|
layout: {
|
||||||
|
title: 'SGW',
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
loginRoute,
|
||||||
|
alarmsRoute,
|
||||||
|
profileRoute,
|
||||||
|
{
|
||||||
|
name: 'sgw.map',
|
||||||
|
icon: 'icon-map',
|
||||||
|
path: '/map',
|
||||||
|
component: './Slave/SGW/Map',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sgw.trips',
|
||||||
|
icon: 'icon-trip',
|
||||||
|
path: '/trips',
|
||||||
|
component: './Slave/SGW/Trip',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...managerRouteBase,
|
||||||
|
routes: [
|
||||||
|
...commonManagerRoutes,
|
||||||
|
{
|
||||||
|
name: 'sgw.fishes',
|
||||||
|
icon: 'icon-fish',
|
||||||
|
path: '/manager/fish',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/manager/fish',
|
||||||
|
component: './Slave/SGW/Manager/Fish',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'sgw.zones',
|
||||||
|
icon: 'icon-prohibited',
|
||||||
|
path: '/manager/area',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/manager/area',
|
||||||
|
component: './Slave/SGW/Manager/Area',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/map',
|
||||||
|
},
|
||||||
|
notFoundRoute,
|
||||||
|
],
|
||||||
|
});
|
||||||
109
.umirc.spole.ts
Normal file
109
.umirc.spole.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { defineConfig } from '@umijs/max';
|
||||||
|
import {
|
||||||
|
alarmsRoute,
|
||||||
|
commonManagerRoutes,
|
||||||
|
loginRoute,
|
||||||
|
managerRouteBase,
|
||||||
|
notFoundRoute,
|
||||||
|
profileRoute,
|
||||||
|
} from './config/routes';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
favicons: ['/spole-logo.png'],
|
||||||
|
layout: {
|
||||||
|
title: 'Spole',
|
||||||
|
},
|
||||||
|
routes: [
|
||||||
|
loginRoute,
|
||||||
|
alarmsRoute,
|
||||||
|
profileRoute,
|
||||||
|
{
|
||||||
|
name: 'spole.monitoring',
|
||||||
|
icon: 'icon-monitoring',
|
||||||
|
path: '/home',
|
||||||
|
component: './Slave/Spole/Monitor',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'spole.mira',
|
||||||
|
icon: 'icon-speaker',
|
||||||
|
path: '/mira',
|
||||||
|
component: './Slave/Spole/Mira',
|
||||||
|
routes: [
|
||||||
|
// {
|
||||||
|
// path: '/manager/mira',
|
||||||
|
// component: './Slave/Spole/Manager/Mira',
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'spole.miva',
|
||||||
|
icon: 'icon-led',
|
||||||
|
path: '/miva',
|
||||||
|
component: './Slave/Spole/Miva',
|
||||||
|
routes: [
|
||||||
|
// {
|
||||||
|
// path: '/manager/miva',
|
||||||
|
// component: './Slave/Spole/Manager/Miva',
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'spole.traffic-light',
|
||||||
|
icon: 'icon-traffic-light',
|
||||||
|
path: '/traffic-light',
|
||||||
|
component: './Slave/Spole/TrafficLight',
|
||||||
|
routes: [
|
||||||
|
// {
|
||||||
|
// path: '/manager/miva',
|
||||||
|
// component: './Slave/Spole/Manager/Miva',
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'spole.street-light',
|
||||||
|
icon: 'icon-street-light',
|
||||||
|
path: '/street-light',
|
||||||
|
component: './Slave/Spole/StreetLight',
|
||||||
|
routes: [
|
||||||
|
// {
|
||||||
|
// path: '/manager/miva',
|
||||||
|
// component: './Slave/Spole/Manager/Miva',
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'spole.media',
|
||||||
|
icon: 'icon-media-upload',
|
||||||
|
path: '/media',
|
||||||
|
component: './Slave/Spole/Media',
|
||||||
|
routes: [
|
||||||
|
// {
|
||||||
|
// path: '/manager/miva',
|
||||||
|
// component: './Slave/Spole/Manager/Miva',
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...managerRouteBase,
|
||||||
|
routes: [
|
||||||
|
...commonManagerRoutes,
|
||||||
|
// {
|
||||||
|
// name: 'light',
|
||||||
|
// icon: 'icon-light',
|
||||||
|
// path: '/manager/light',
|
||||||
|
// routes: [
|
||||||
|
// {
|
||||||
|
// path: '/manager/light',
|
||||||
|
// component: './Slave/Spole/Manager/Light',
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
redirect: '/home',
|
||||||
|
},
|
||||||
|
notFoundRoute,
|
||||||
|
],
|
||||||
|
});
|
||||||
58
.umirc.ts
Normal file
58
.umirc.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { defineConfig } from '@umijs/max';
|
||||||
|
import proxyDev from './config/proxy_dev';
|
||||||
|
import proxyProd from './config/proxy_prod';
|
||||||
|
import {
|
||||||
|
alarmsRoute,
|
||||||
|
commonManagerRoutes,
|
||||||
|
loginRoute,
|
||||||
|
managerRouteBase,
|
||||||
|
notFoundRoute,
|
||||||
|
} from './config/routes';
|
||||||
|
|
||||||
|
const envConfig = process.env as { REACT_APP_ENV?: 'dev' | 'test' | 'prod' };
|
||||||
|
const rawEnv = envConfig.REACT_APP_ENV;
|
||||||
|
const isProdBuild = process.env.NODE_ENV === 'production';
|
||||||
|
const resolvedEnv = isProdBuild && !rawEnv ? 'prod' : rawEnv || 'dev';
|
||||||
|
const proxyConfig = isProdBuild ? proxyProd : proxyDev;
|
||||||
|
const routes = [
|
||||||
|
loginRoute,
|
||||||
|
alarmsRoute,
|
||||||
|
{
|
||||||
|
...managerRouteBase,
|
||||||
|
routes: commonManagerRoutes,
|
||||||
|
},
|
||||||
|
notFoundRoute,
|
||||||
|
];
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
antd: {},
|
||||||
|
access: {},
|
||||||
|
model: {},
|
||||||
|
initialState: {},
|
||||||
|
request: {},
|
||||||
|
tailwindcss: {},
|
||||||
|
layout: {
|
||||||
|
title: 'BaseProject',
|
||||||
|
},
|
||||||
|
extraBabelPlugins:
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
? [['transform-remove-console', { exclude: ['error'] }]]
|
||||||
|
: [],
|
||||||
|
mfsu: false,
|
||||||
|
locale: {
|
||||||
|
useLocalStorage: true,
|
||||||
|
default: 'vi-VN',
|
||||||
|
baseNavigator: false,
|
||||||
|
antd: true,
|
||||||
|
title: false,
|
||||||
|
baseSeparator: '-',
|
||||||
|
},
|
||||||
|
// Only use base routes when not in a specific environment that provides its own routes
|
||||||
|
routes: process.env.DOMAIN_ENV ? undefined : routes,
|
||||||
|
npmClient: 'pnpm',
|
||||||
|
// Use the dev configuration from proxy_dev
|
||||||
|
proxy: proxyConfig[resolvedEnv],
|
||||||
|
define: {
|
||||||
|
'process.env.DOMAIN_ENV': process.env.DOMAIN_ENV,
|
||||||
|
},
|
||||||
|
});
|
||||||
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
|
// "typescript.preferences.autoImportFileExcludePatterns": [
|
||||||
|
// "node_modules"
|
||||||
|
// ]
|
||||||
|
}
|
||||||
44
config/proxy_dev.ts
Normal file
44
config/proxy_dev.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const getTarget = () => {
|
||||||
|
const env = process.env.DOMAIN_ENV;
|
||||||
|
switch (env) {
|
||||||
|
case 'gms':
|
||||||
|
return 'https://gms.smatec.com.vn';
|
||||||
|
case 'spole':
|
||||||
|
return 'https://spole.gms.vn';
|
||||||
|
case 'sgw':
|
||||||
|
return 'https://sgw.gms.vn';
|
||||||
|
default:
|
||||||
|
return 'https://sgw.gms.vn';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const target = getTarget();
|
||||||
|
|
||||||
|
const proxyDev: Record<string, any> = {
|
||||||
|
dev: {
|
||||||
|
'/api': {
|
||||||
|
target: target,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
'/geoserver': {
|
||||||
|
target: target,
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
'/test': {
|
||||||
|
target: 'https://proapi.azurewebsites.net',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prod: {
|
||||||
|
'/test': {
|
||||||
|
target: 'https://prod-sgw-device.gms.vn',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default proxyDev;
|
||||||
86
config/proxy_prod.ts
Normal file
86
config/proxy_prod.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Lấy hostname từ URL hiện tại của trang web
|
||||||
|
const getCurrentHostname = () => {
|
||||||
|
// Trong build-time, sử dụng environment variable
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return process.env.REACT_APP_HOSTNAME || 'localhost';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trong runtime, lấy từ URL của trang web
|
||||||
|
return window.location.hostname;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lấy port cho GeoServer từ environment variable hoặc default là 8080
|
||||||
|
const getGeoserverPort = () => {
|
||||||
|
return process.env.REACT_APP_GEOSERVER_PORT || '8080';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tạo proxy config động dựa trên hostname hiện tại
|
||||||
|
const createDynamicProxy = () => {
|
||||||
|
const hostname = getCurrentHostname();
|
||||||
|
const geoserverPort = getGeoserverPort();
|
||||||
|
const isLocalhost =
|
||||||
|
hostname === 'localhost' ||
|
||||||
|
hostname.startsWith('192.168.') ||
|
||||||
|
hostname.startsWith('10.') ||
|
||||||
|
hostname.startsWith('127.');
|
||||||
|
|
||||||
|
// Với production, ta sẽ proxy:
|
||||||
|
// - /api đến cùng hostname với port 81
|
||||||
|
// - /geoserver đến cùng hostname với port GEOSERVER_PORT (default: 8080)
|
||||||
|
// Ví dụ: nếu truy cập https://example.com thì API sẽ đến http://example.com:81/api
|
||||||
|
// và GeoServer sẽ đến http://example.com:[GEOSERVER_PORT]/geoserver
|
||||||
|
return {
|
||||||
|
dev: {
|
||||||
|
'/api': {
|
||||||
|
target: `http://${hostname}:81`,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: { '^/api': '/api' },
|
||||||
|
},
|
||||||
|
'/geoserver': {
|
||||||
|
target: `http://${hostname}:${geoserverPort}`,
|
||||||
|
changeOrigin: true,
|
||||||
|
pathRewrite: { '^/geoserver': '/geoserver' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
test: {
|
||||||
|
'/api': {
|
||||||
|
target: isLocalhost
|
||||||
|
? `http://${hostname}:81`
|
||||||
|
: `https://${hostname}:81`,
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: !isLocalhost,
|
||||||
|
pathRewrite: { '^/api': '/api' },
|
||||||
|
},
|
||||||
|
'/geoserver': {
|
||||||
|
target: isLocalhost
|
||||||
|
? `http://${hostname}:${geoserverPort}`
|
||||||
|
: `https://${hostname}:${geoserverPort}`,
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: !isLocalhost,
|
||||||
|
pathRewrite: { '^/geoserver': '/geoserver' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prod: {
|
||||||
|
'/api': {
|
||||||
|
target: isLocalhost
|
||||||
|
? `http://${hostname}:81`
|
||||||
|
: `https://${hostname}:81`,
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: !isLocalhost,
|
||||||
|
pathRewrite: { '^/api': '/api' },
|
||||||
|
},
|
||||||
|
'/geoserver': {
|
||||||
|
target: isLocalhost
|
||||||
|
? `http://${hostname}:${geoserverPort}`
|
||||||
|
: `https://${hostname}:${geoserverPort}`,
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: !isLocalhost,
|
||||||
|
pathRewrite: { '^/geoserver': '/geoserver' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const proxyProd: Record<string, any> = createDynamicProxy();
|
||||||
|
|
||||||
|
export default proxyProd;
|
||||||
131
config/request_dev.ts
Normal file
131
config/request_dev.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { HTTPSTATUS } from '@/constants';
|
||||||
|
import { ROUTE_LOGIN } from '@/constants/routes';
|
||||||
|
import { getToken, removeToken } from '@/utils/storage';
|
||||||
|
import { history, RequestConfig } from '@umijs/max';
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
// Error handling scheme: Error types
|
||||||
|
// enum ErrorShowType {
|
||||||
|
// SILENT = 0,
|
||||||
|
// WARN_MESSAGE = 1,
|
||||||
|
// ERROR_MESSAGE = 2,
|
||||||
|
// NOTIFICATION = 3,
|
||||||
|
// REDIRECT = 9,
|
||||||
|
// }
|
||||||
|
// Response data structure agreed with the backend
|
||||||
|
// interface ResponseStructure<T = any> {
|
||||||
|
// success: boolean;
|
||||||
|
// data: T;
|
||||||
|
// errorCode?: number;
|
||||||
|
// errorMessage?: string;
|
||||||
|
// showType?: ErrorShowType;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const codeMessage = {
|
||||||
|
// 200: 'The server successfully returned the requested data。',
|
||||||
|
// 201: 'New or modified data succeeded。',
|
||||||
|
// 202: 'A request has been queued in the background (asynchronous task)。',
|
||||||
|
// 204: 'Data deleted successfully。',
|
||||||
|
// 400: 'There is an error in the request sent, the server did not perform the operation of creating or modifying data。',
|
||||||
|
// 401: 'The user does not have permission (token, username, password is wrong) 。',
|
||||||
|
// 403: 'User is authorized, but access is prohibited。',
|
||||||
|
// 404: 'The request issued was for a non-existent record, the server did not operate。',
|
||||||
|
// 406: 'The requested format is not available。',
|
||||||
|
// 410: 'The requested resource is permanently deleted and will no longer be available。',
|
||||||
|
// 422: 'When creating an object, a validation error occurred。',
|
||||||
|
// 500: 'Server error, please check the server。',
|
||||||
|
// 502: 'Gateway error。',
|
||||||
|
// 503: 'Service unavailable, server temporarily overloaded or maintained。',
|
||||||
|
// 504: 'Gateway timeout。',
|
||||||
|
// };
|
||||||
|
|
||||||
|
// Runtime configuration
|
||||||
|
export const handleRequestConfig: RequestConfig = {
|
||||||
|
// Unified request settings
|
||||||
|
timeout: 20000,
|
||||||
|
validateStatus: (status) => {
|
||||||
|
return (
|
||||||
|
(status >= 200 && status < 300) || status === HTTPSTATUS.HTTP_NOTFOUND
|
||||||
|
);
|
||||||
|
},
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
|
// Error handling: umi@3's error handling scheme.
|
||||||
|
errorConfig: {
|
||||||
|
// Error throwing
|
||||||
|
errorThrower: (res: any) => {
|
||||||
|
// console.log('Response from backend:', res);
|
||||||
|
const { success, data, errorCode, errorMessage, showType } = res;
|
||||||
|
if (!success) {
|
||||||
|
const error: any = new Error(errorMessage);
|
||||||
|
error.name = 'BizError';
|
||||||
|
error.info = { errorCode, errorMessage, showType, data };
|
||||||
|
throw error; // Throw custom error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Error catching and handling
|
||||||
|
errorHandler: (error: any) => {
|
||||||
|
if (error.response) {
|
||||||
|
const { status } = error.response;
|
||||||
|
|
||||||
|
// Ưu tiên: codeMessage → backend message → statusText
|
||||||
|
// const errMsg =
|
||||||
|
// codeMessage[status as keyof typeof codeMessage] ||
|
||||||
|
// data?.message ||
|
||||||
|
// statusText ||
|
||||||
|
// 'Unknown error';
|
||||||
|
|
||||||
|
if (status === HTTPSTATUS.HTTP_UNAUTHORIZED) {
|
||||||
|
removeToken();
|
||||||
|
history.push(ROUTE_LOGIN);
|
||||||
|
} else if (status === HTTPSTATUS.HTTP_SERVERERROR) {
|
||||||
|
// message.error('💥 Internal server error!');
|
||||||
|
} else {
|
||||||
|
message.error(`❌ ${status}: ${error.message || 'Error'}`);
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
message.error('🚨 No response from server!');
|
||||||
|
} else {
|
||||||
|
message.error(`⚠️ Request setup error: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Request interceptors
|
||||||
|
requestInterceptors: [
|
||||||
|
(url: string, options: any) => {
|
||||||
|
const token = getToken();
|
||||||
|
// console.log('Token: ', token);
|
||||||
|
|
||||||
|
return {
|
||||||
|
url,
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
...(token ? { Authorization: `${token}` } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Unwrap data from backend response
|
||||||
|
responseInterceptors: [
|
||||||
|
(response) => {
|
||||||
|
// console.log('Response from server: ', response);
|
||||||
|
|
||||||
|
// const res = response.data as ResponseStructure<any>;
|
||||||
|
// if (res && res.success) {
|
||||||
|
// // ✅ Trả ra data luôn thay vì cả object
|
||||||
|
// return res.data;
|
||||||
|
// }
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: response.headers,
|
||||||
|
config: response.config,
|
||||||
|
data: response.data,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
107
config/request_prod.ts
Normal file
107
config/request_prod.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { HTTPSTATUS } from '@/constants';
|
||||||
|
import { ROUTE_LOGIN } from '@/constants/routes';
|
||||||
|
import { getToken, removeToken } from '@/utils/storage';
|
||||||
|
import { history, RequestConfig } from '@umijs/max';
|
||||||
|
import { message } from 'antd';
|
||||||
|
|
||||||
|
const codeMessage = {
|
||||||
|
200: 'The server successfully returned the requested data。',
|
||||||
|
201: 'New or modified data succeeded。',
|
||||||
|
202: 'A request has been queued in the background (asynchronous task)。',
|
||||||
|
204: 'Data deleted successfully。',
|
||||||
|
400: 'There is an error in the request sent, the server did not perform the operation of creating or modifying data。',
|
||||||
|
401: 'The user does not have permission (token, username, password is wrong) 。',
|
||||||
|
403: 'User is authorized, but access is prohibited。',
|
||||||
|
404: 'The request issued was for a non-existent record, the server did not operate。',
|
||||||
|
406: 'The requested format is not available。',
|
||||||
|
410: 'The requested resource is permanently deleted and will no longer be available。',
|
||||||
|
422: 'When creating an object, a validation error occurred。',
|
||||||
|
500: 'Server error, please check the server。',
|
||||||
|
502: 'Gateway error。',
|
||||||
|
503: 'Service unavailable, server temporarily overloaded or maintained。',
|
||||||
|
504: 'Gateway timeout。',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runtime configuration
|
||||||
|
export const handleRequestConfig: RequestConfig = {
|
||||||
|
// Unified request settings
|
||||||
|
timeout: 20000,
|
||||||
|
validateStatus: (status) => {
|
||||||
|
return (
|
||||||
|
(status >= 200 && status < 300) || status === HTTPSTATUS.HTTP_NOTFOUND
|
||||||
|
);
|
||||||
|
},
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
|
// Error handling: umi@3's error handling scheme.
|
||||||
|
errorConfig: {
|
||||||
|
// Error throwing
|
||||||
|
errorThrower: (res: any) => {
|
||||||
|
// console.log('Response from backend:', res);
|
||||||
|
const { success, data, errorCode, errorMessage, showType } = res;
|
||||||
|
if (!success) {
|
||||||
|
const error: any = new Error(errorMessage);
|
||||||
|
error.name = 'BizError';
|
||||||
|
error.info = { errorCode, errorMessage, showType, data };
|
||||||
|
throw error; // Throw custom error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Error catching and handling
|
||||||
|
errorHandler: (error: any) => {
|
||||||
|
if (error.response) {
|
||||||
|
const { status, statusText, data } = error.response;
|
||||||
|
|
||||||
|
// Ưu tiên: codeMessage → backend message → statusText
|
||||||
|
const errMsg =
|
||||||
|
codeMessage[status as keyof typeof codeMessage] ||
|
||||||
|
data?.message ||
|
||||||
|
statusText ||
|
||||||
|
'Unknown error';
|
||||||
|
|
||||||
|
message.error(`❌ ${status}: ${errMsg}`);
|
||||||
|
if (status === 401) {
|
||||||
|
removeToken();
|
||||||
|
history.push(ROUTE_LOGIN);
|
||||||
|
}
|
||||||
|
} else if (error.request) {
|
||||||
|
message.error('🚨 No response from server!');
|
||||||
|
} else if (status) {
|
||||||
|
// message.error('💥 Internal server error!');
|
||||||
|
} else {
|
||||||
|
message.error(`⚠️ Request setup error: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Request interceptors
|
||||||
|
requestInterceptors: [
|
||||||
|
(url: string, options: any) => {
|
||||||
|
// console.log('URL Request:', url, options);
|
||||||
|
|
||||||
|
// Chỉ cần thêm token, proxy sẽ xử lý việc redirect đến đúng port
|
||||||
|
// URL sẽ bắt đầu với /api và proxy sẽ chuyển đến hostname:81/api
|
||||||
|
const token = getToken();
|
||||||
|
return {
|
||||||
|
url: url,
|
||||||
|
options: {
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
...(token ? { Authorization: `${token}` } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
// Unwrap data from backend response
|
||||||
|
// responseInterceptors: [
|
||||||
|
// (response) => {
|
||||||
|
// const res = response.data as ResponseStructure<any>;
|
||||||
|
// if (res && res.success) {
|
||||||
|
// // ✅ Trả ra data luôn thay vì cả object
|
||||||
|
// return res.data;
|
||||||
|
// }
|
||||||
|
// return response.data;
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
};
|
||||||
89
config/routes.ts
Normal file
89
config/routes.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
export const loginRoute = {
|
||||||
|
title: 'Login',
|
||||||
|
path: '/login',
|
||||||
|
component: './Auth',
|
||||||
|
layout: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const profileRoute = {
|
||||||
|
name: 'profile',
|
||||||
|
icon: 'icon-user',
|
||||||
|
path: '/profile',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/profile',
|
||||||
|
component: './Profile',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
flatMenu: true,
|
||||||
|
};
|
||||||
|
export const alarmsRoute = {
|
||||||
|
name: 'alarms',
|
||||||
|
icon: 'icon-alarm',
|
||||||
|
path: '/alarms',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/alarms',
|
||||||
|
component: './Alarm',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const commonManagerRoutes = [
|
||||||
|
{
|
||||||
|
name: 'devices',
|
||||||
|
icon: 'icon-gateway',
|
||||||
|
path: '/manager/devices',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/manager/devices',
|
||||||
|
component: './Manager/Device',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'groups',
|
||||||
|
icon: 'icon-tree',
|
||||||
|
path: '/manager/groups',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/manager/groups',
|
||||||
|
component: './Manager/Group',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'users',
|
||||||
|
icon: 'icon-users',
|
||||||
|
path: '/manager/users',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/manager/users',
|
||||||
|
component: './Manager/User',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'logs',
|
||||||
|
icon: 'icon-diary',
|
||||||
|
path: '/manager/logs',
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/manager/logs',
|
||||||
|
component: './Manager/Log',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const managerRouteBase = {
|
||||||
|
name: 'manager',
|
||||||
|
icon: 'icon-setting',
|
||||||
|
path: '/manager',
|
||||||
|
access: 'canAdmin_SysAdmin',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const notFoundRoute = {
|
||||||
|
path: '*',
|
||||||
|
component: './Exception/NotFound',
|
||||||
|
};
|
||||||
0
mock/userAPI.ts
Normal file
0
mock/userAPI.ts
Normal file
22459
package-lock.json
generated
Normal file
22459
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
package.json
Normal file
46
package.json
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"author": "",
|
||||||
|
"scripts": {
|
||||||
|
"build": "max build",
|
||||||
|
"build:gms": "UMI_ENV=gms DOMAIN_ENV=gms max build",
|
||||||
|
"build:sgw": "UMI_ENV=sgw DOMAIN_ENV=sgw max build",
|
||||||
|
"build:spole": "UMI_ENV=spole DOMAIN_ENV=spole max build",
|
||||||
|
"dev": "max dev",
|
||||||
|
"format": "prettier --cache --write .",
|
||||||
|
"postinstall": "max setup",
|
||||||
|
"prepare": "husky",
|
||||||
|
"setup": "max setup",
|
||||||
|
"start": "npm run dev",
|
||||||
|
"start:gms": "UMI_ENV=gms DOMAIN_ENV=gms max dev",
|
||||||
|
"start:sgw": "UMI_ENV=sgw DOMAIN_ENV=sgw max dev",
|
||||||
|
"start:spole": "UMI_ENV=spole DOMAIN_ENV=spole max dev"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@ant-design/icons": "^5.0.1",
|
||||||
|
"@ant-design/pro-components": "^2.8.10",
|
||||||
|
"@umijs/max": "^4.6.23",
|
||||||
|
"antd": "^5.4.0",
|
||||||
|
"classnames": "^2.5.1",
|
||||||
|
"dayjs": "^1.11.19",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"ol": "^10.6.1",
|
||||||
|
"reconnecting-websocket": "^4.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.33",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"babel-plugin-transform-remove-console": "^6.9.4",
|
||||||
|
"baseline-browser-mapping": "^2.9.6",
|
||||||
|
"husky": "^9",
|
||||||
|
"lint-staged": "^13.2.0",
|
||||||
|
"prettier": "^2.8.7",
|
||||||
|
"prettier-plugin-organize-imports": "^3.2.2",
|
||||||
|
"prettier-plugin-packagejson": "^2.4.3",
|
||||||
|
"tailwindcss": "^3",
|
||||||
|
"typescript": "^5.0.3"
|
||||||
|
},
|
||||||
|
"gms-version": "1.0.0",
|
||||||
|
"sgw-version": "1.0.0",
|
||||||
|
"spole-version": "1.0.0"
|
||||||
|
}
|
||||||
14482
pnpm-lock.yaml
generated
Normal file
14482
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
public/avatar.svg
Normal file
1
public/avatar.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1679201365371" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1897" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M509.31257004 540.79389452h4.91415801c44.99525866-0.76783723 81.39074114-16.58528313 108.26504222-46.83806846 59.12346278-66.6482671 49.29514673-180.9024399 48.2201749-191.80572705-3.83918618-81.85144307-42.53817965-121.01113989-74.48020676-139.28566479C572.42878628 149.19693275 544.63307997 141.82569571 513.61245812 141.21142578H511.00181181c-17.04598575 0-50.52368662 2.76421361-82.61928101 21.03873851-32.24916171 18.27452489-71.56242513 57.43422101-75.40161058 139.89993473-1.07497185 10.90328787-10.90328787 125.15746067 48.22017491 191.80572704 26.72073376 30.2527846 63.11621625 46.07023049 108.11147491 46.83806846z m-115.32914464-234.80461006c0-0.46070263 0.15356731-0.92140454 0.15356731-1.22853914 5.06772532-110.10785129 83.23355022-121.93254442 116.7112518-121.93254443H512.69105358c41.4632078 0.92140454 111.95066108 17.81382227 116.71125108 121.93254443 0 0.46070263 0 0.92140454 0.15356731 1.22853914 0.15356731 1.07497185 10.90328787 105.50082861-37.93115624 160.47797059-19.34949674 21.80657575-45.14882595 32.55629632-79.08722946 32.86343166h-1.53567446c-33.78483618-0.30713461-59.73773271-11.05685517-78.93366214-32.86343165-48.68087753-54.67000739-38.23829157-159.55656606-38.08472427-160.4779706z" p-id="1898"></path><path d="M827.3507296 730.29611058v-0.46070263c0-1.22853915-0.15356731-2.457079-0.15356731-3.83918618-0.92140454-30.40635263-2.91778164-101.50807511-69.56604874-124.23605539-0.46070263-0.15356731-1.07497185-0.30713461-1.53567375-0.46070264-69.25891341-17.66025497-126.84670245-57.5877883-127.46097237-58.04849021-9.3676134-6.60339979-22.26727838-4.29988808-28.87067744 5.06772532-6.60339979 9.3676134-4.29988808 22.26727838 5.0677253 28.87067743 2.6106463 1.84280907 63.73048619 44.38098873 140.20706862 64.03762079 35.78121256 12.74609694 39.77396603 50.98438852 40.84893787 85.99776458 0 1.38210717 0 2.6106463 0.15356802 3.83918545 0.15356731 13.82106951-0.76783723 35.16694264-3.22491624 47.45233838-24.87792469 14.12820412-122.39324633 62.96264895-270.73938991 62.96264823-147.73187366 0-245.86146522-48.98801213-270.89295721-63.11621625-2.457079-12.28539504-3.53205084-33.63126816-3.22491624-47.45233767 0-1.22853915 0.15356731-2.457079 0.15356802-3.83918545 1.07497185-35.01337533 5.06772532-73.25166689 40.84893786-85.99776457 76.47658314-19.65663206 137.596423-62.34837901 140.20706862-64.03762079 9.3676134-6.60339979 11.67112511-19.50306404 5.06772531-28.87067744-6.60339979-9.3676134-19.50306404-11.67112511-28.87067745-5.06772605-0.61426993 0.46070263-57.89492363 40.38823598-127.46097236 58.04849094-0.61426993 0.15356731-1.07497185 0.30713461-1.53567375 0.46070264-66.6482671 22.88154832-68.64464421 93.9832708-69.56604874 124.2360554 0 1.38210717 0 2.6106463-0.15356731 3.83918617v0.46070191c-0.15356731 7.98550697-0.30713461 48.98801213 7.83193893 69.56604875 1.53567446 3.99275348 4.29988808 7.37123702 7.98550697 9.67474873 4.60702342 3.07134893 115.02200931 73.4052342 299.76363465 73.40523419s295.15661197-70.48745329 299.76363538-73.40523419c3.53205084-2.3035117 6.44983249-5.68199525 7.98550624-9.67474873 7.67837163-20.4244693 7.52480433-61.42697448 7.37123703-69.41248072z" p-id="1899"></path></svg>
|
||||||
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/gms-logo.png
Normal file
BIN
public/gms-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
BIN
public/mobifont-logo.png
Normal file
BIN
public/mobifont-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/sgw-logo.png
Normal file
BIN
public/sgw-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
BIN
public/smatec-logo.png
Normal file
BIN
public/smatec-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
public/spole-logo.png
Normal file
BIN
public/spole-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
131
src/access.ts
Normal file
131
src/access.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { InitialStateResponse } from './app';
|
||||||
|
import { SGW_ROLE } from './constants/slave/sgw';
|
||||||
|
|
||||||
|
export default (initialState: InitialStateResponse) => {
|
||||||
|
// Lấy role của user hiện tại
|
||||||
|
const userType = initialState?.currentUserProfile?.metadata?.user_type;
|
||||||
|
|
||||||
|
// Lấy biến môi trường domain
|
||||||
|
const env = process.env.DOMAIN_ENV;
|
||||||
|
|
||||||
|
// Khởi tạo object quyền mặc định (tất cả là false)
|
||||||
|
let permissions = {
|
||||||
|
canAdmin: false,
|
||||||
|
canAdmin_SysAdmin: false,
|
||||||
|
canEndUser_Admin: false,
|
||||||
|
canEndUser_User_Admin: false,
|
||||||
|
canEndUser_User: false,
|
||||||
|
canEndUser: false,
|
||||||
|
canAll: true,
|
||||||
|
|
||||||
|
// Cờ đánh dấu Domain hiện tại
|
||||||
|
isSGW: env === 'sgw' || !env,
|
||||||
|
isGMS: env === 'gms',
|
||||||
|
isSpole: env === 'spole',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Switch case để xử lý logic quyền riêng cho từng Domain
|
||||||
|
switch (env) {
|
||||||
|
case 'gms':
|
||||||
|
if (userType) {
|
||||||
|
permissions.canAdmin = userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canAdmin_SysAdmin =
|
||||||
|
userType === SGW_ROLE.SYSADMIN || userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canEndUser_Admin =
|
||||||
|
userType === SGW_ROLE.ENDUSER || userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canEndUser_User_Admin = [
|
||||||
|
SGW_ROLE.ENDUSER,
|
||||||
|
SGW_ROLE.USERS,
|
||||||
|
SGW_ROLE.ADMIN,
|
||||||
|
].includes(userType as SGW_ROLE);
|
||||||
|
|
||||||
|
permissions.canEndUser_User = [
|
||||||
|
SGW_ROLE.ENDUSER,
|
||||||
|
SGW_ROLE.USERS,
|
||||||
|
].includes(userType as SGW_ROLE);
|
||||||
|
|
||||||
|
permissions.canEndUser = userType === SGW_ROLE.ENDUSER;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'spole':
|
||||||
|
if (userType) {
|
||||||
|
permissions.canAdmin = userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canAdmin_SysAdmin =
|
||||||
|
userType === SGW_ROLE.SYSADMIN || userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canEndUser_Admin =
|
||||||
|
userType === SGW_ROLE.ENDUSER || userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canEndUser_User_Admin = [
|
||||||
|
SGW_ROLE.ENDUSER,
|
||||||
|
SGW_ROLE.USERS,
|
||||||
|
SGW_ROLE.ADMIN,
|
||||||
|
].includes(userType as SGW_ROLE);
|
||||||
|
|
||||||
|
permissions.canEndUser_User = [
|
||||||
|
SGW_ROLE.ENDUSER,
|
||||||
|
SGW_ROLE.USERS,
|
||||||
|
].includes(userType as SGW_ROLE);
|
||||||
|
|
||||||
|
permissions.canEndUser = userType === SGW_ROLE.ENDUSER;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'sgw':
|
||||||
|
if (userType) {
|
||||||
|
permissions.canAdmin = userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canAdmin_SysAdmin =
|
||||||
|
userType === SGW_ROLE.SYSADMIN || userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canEndUser_Admin =
|
||||||
|
userType === SGW_ROLE.ENDUSER || userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canEndUser_User_Admin = [
|
||||||
|
SGW_ROLE.ENDUSER,
|
||||||
|
SGW_ROLE.USERS,
|
||||||
|
SGW_ROLE.ADMIN,
|
||||||
|
].includes(userType as SGW_ROLE);
|
||||||
|
|
||||||
|
permissions.canEndUser_User = [
|
||||||
|
SGW_ROLE.ENDUSER,
|
||||||
|
SGW_ROLE.USERS,
|
||||||
|
].includes(userType as SGW_ROLE);
|
||||||
|
|
||||||
|
permissions.canEndUser = userType === SGW_ROLE.ENDUSER;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Logic phân quyền cho SGW
|
||||||
|
if (userType) {
|
||||||
|
permissions.canAdmin = userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canAdmin_SysAdmin =
|
||||||
|
userType === SGW_ROLE.SYSADMIN || userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canEndUser_Admin =
|
||||||
|
userType === SGW_ROLE.ENDUSER || userType === SGW_ROLE.ADMIN;
|
||||||
|
|
||||||
|
permissions.canEndUser_User_Admin = [
|
||||||
|
SGW_ROLE.ENDUSER,
|
||||||
|
SGW_ROLE.USERS,
|
||||||
|
SGW_ROLE.ADMIN,
|
||||||
|
].includes(userType as SGW_ROLE);
|
||||||
|
|
||||||
|
permissions.canEndUser_User = [
|
||||||
|
SGW_ROLE.ENDUSER,
|
||||||
|
SGW_ROLE.USERS,
|
||||||
|
].includes(userType as SGW_ROLE);
|
||||||
|
|
||||||
|
permissions.canEndUser = userType === SGW_ROLE.ENDUSER;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return permissions;
|
||||||
|
};
|
||||||
153
src/app.tsx
Normal file
153
src/app.tsx
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
// 运行时配置
|
||||||
|
|
||||||
|
import { getLocale, history, Link, RunTimeLayoutConfig } from '@umijs/max';
|
||||||
|
import { ConfigProvider } from 'antd';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { handleRequestConfig as devRequestConfig } from '../config/request_dev';
|
||||||
|
import { handleRequestConfig as prodRequestConfig } from '../config/request_prod';
|
||||||
|
import { AvatarDropdown } from './components/Avatar/AvatarDropdown';
|
||||||
|
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_LOGIN } from './constants/routes';
|
||||||
|
import NotFoundPage from './pages/Exception/NotFound';
|
||||||
|
import UnAccessPage from './pages/Exception/UnAccess';
|
||||||
|
import { apiQueryProfile } from './services/master/AuthController';
|
||||||
|
import { checkTokenExpired } from './utils/jwt';
|
||||||
|
import { getLogoImage } from './utils/logo';
|
||||||
|
import {
|
||||||
|
clearAllData,
|
||||||
|
clearSessionData,
|
||||||
|
getToken,
|
||||||
|
removeToken,
|
||||||
|
} from './utils/storage';
|
||||||
|
const isProdBuild = process.env.NODE_ENV === 'production';
|
||||||
|
export type InitialStateResponse = {
|
||||||
|
getUserProfile?: () => Promise<MasterModel.ProfileResponse | undefined>;
|
||||||
|
currentUserProfile?: MasterModel.ProfileResponse;
|
||||||
|
theme?: 'light' | 'dark';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
|
||||||
|
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
|
||||||
|
export async function getInitialState(): Promise<InitialStateResponse> {
|
||||||
|
const userToken: string = getToken();
|
||||||
|
const { pathname } = history.location;
|
||||||
|
|
||||||
|
dayjs.locale(getLocale() === 'en-US' ? 'en' : 'vi');
|
||||||
|
if (!userToken) {
|
||||||
|
if (pathname !== ROUTE_LOGIN) {
|
||||||
|
history.push(`${ROUTE_LOGIN}?redirect=${pathname}`);
|
||||||
|
} else {
|
||||||
|
history.push(ROUTE_LOGIN);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const isTokenExpried = checkTokenExpired(userToken);
|
||||||
|
if (isTokenExpried) {
|
||||||
|
removeToken();
|
||||||
|
clearAllData();
|
||||||
|
clearSessionData();
|
||||||
|
window.location.href = ROUTE_LOGIN;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const getUserProfile = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await apiQueryProfile();
|
||||||
|
return resp;
|
||||||
|
} catch (error) {
|
||||||
|
removeToken();
|
||||||
|
clearAllData();
|
||||||
|
clearSessionData();
|
||||||
|
window.location.href = ROUTE_LOGIN;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const resp = await getUserProfile();
|
||||||
|
const currentTheme =
|
||||||
|
(localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light';
|
||||||
|
return {
|
||||||
|
getUserProfile: getUserProfile!,
|
||||||
|
currentUserProfile: resp,
|
||||||
|
theme: currentTheme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
||||||
|
const isDark = initialState?.theme === 'dark';
|
||||||
|
return {
|
||||||
|
logo: getLogoImage(),
|
||||||
|
menu: {
|
||||||
|
locale: true,
|
||||||
|
},
|
||||||
|
fixedHeader: true,
|
||||||
|
contentWidth: 'Fluid',
|
||||||
|
navTheme: isDark ? 'realDark' : 'light',
|
||||||
|
splitMenus: true,
|
||||||
|
iconfontUrl: '//at.alicdn.com/t/c/font_5096559_y5mqyqd86f.js',
|
||||||
|
contentStyle: {
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
paddingInline: 0,
|
||||||
|
},
|
||||||
|
actionsRender: () => [
|
||||||
|
<ThemeSwitcher key="theme-switcher" />,
|
||||||
|
<LanguageSwitcher key="lang-switcher" type="dropdown" />,
|
||||||
|
],
|
||||||
|
avatarProps: {
|
||||||
|
size: 'small',
|
||||||
|
src: '/avatar.svg',
|
||||||
|
render: () => (
|
||||||
|
<AvatarDropdown currentUserProfile={initialState?.currentUserProfile} />
|
||||||
|
),
|
||||||
|
},
|
||||||
|
childrenRender: (children) => {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
components: {
|
||||||
|
Collapse: {
|
||||||
|
contentPadding: '0px',
|
||||||
|
borderlessContentPadding: '0px',
|
||||||
|
headerPadding: '0px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ConfigProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
layout: 'top',
|
||||||
|
menuHeaderRender: undefined,
|
||||||
|
menuItemRender: (item, dom) => {
|
||||||
|
if (item.path) {
|
||||||
|
// Coerce values to string to satisfy TypeScript expectations
|
||||||
|
const to = String(item.path ?? '');
|
||||||
|
const iconType = String(item.icon ?? '');
|
||||||
|
const label = String(item.name ?? '');
|
||||||
|
return (
|
||||||
|
<Link to={to}>
|
||||||
|
<IconFont type={iconType} />
|
||||||
|
<span>{label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return dom;
|
||||||
|
},
|
||||||
|
token: {
|
||||||
|
header: {
|
||||||
|
colorBgMenuItemSelected: '#EEF7FF', // background khi chọn
|
||||||
|
colorTextMenuSelected: isDark ? '#fff' : '#1A2130', // màu chữ khi chọn
|
||||||
|
},
|
||||||
|
},
|
||||||
|
unAccessible: <UnAccessPage />,
|
||||||
|
noFound: <NotFoundPage />,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const request = isProdBuild ? prodRequestConfig : devRequestConfig;
|
||||||
0
src/assets/.gitkeep
Normal file
0
src/assets/.gitkeep
Normal file
61
src/components/Avatar/AvatarDropdown.tsx
Normal file
61
src/components/Avatar/AvatarDropdown.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { ROUTE_LOGIN, ROUTE_PROFILE } from '@/constants/routes';
|
||||||
|
import { clearAllData, clearSessionData, removeToken } from '@/utils/storage';
|
||||||
|
import { LogoutOutlined, ProfileOutlined } from '@ant-design/icons';
|
||||||
|
import { history, useIntl } from '@umijs/max';
|
||||||
|
|
||||||
|
import { Dropdown } from 'antd';
|
||||||
|
|
||||||
|
// Avatar component with i18n support
|
||||||
|
export const AvatarDropdown = ({
|
||||||
|
currentUserProfile,
|
||||||
|
}: {
|
||||||
|
currentUserProfile?: MasterModel.ProfileResponse;
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
key: ROUTE_PROFILE,
|
||||||
|
icon: <ProfileOutlined />,
|
||||||
|
label: intl.formatMessage({ id: 'menu.profile' }),
|
||||||
|
onClick: () => {
|
||||||
|
history.push(ROUTE_PROFILE);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'logout',
|
||||||
|
icon: <LogoutOutlined />,
|
||||||
|
label: intl.formatMessage({ id: 'common.logout' }),
|
||||||
|
onClick: () => {
|
||||||
|
removeToken();
|
||||||
|
clearAllData();
|
||||||
|
clearSessionData();
|
||||||
|
window.location.href = ROUTE_LOGIN;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<img
|
||||||
|
src="/avatar.svg"
|
||||||
|
alt="avatar"
|
||||||
|
style={{
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: '50%',
|
||||||
|
cursor: 'pointer',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="font-bold">
|
||||||
|
{currentUserProfile?.metadata?.full_name || ''}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
47
src/components/Footer/index.tsx
Normal file
47
src/components/Footer/index.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { DefaultFooter } from '@ant-design/pro-components';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import packageJson from '../../../package.json';
|
||||||
|
import './style.less';
|
||||||
|
const Footer = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const defaultMessage = intl.formatMessage({
|
||||||
|
id: 'app.copyright.produced',
|
||||||
|
defaultMessage: 'by SMATEC Team',
|
||||||
|
});
|
||||||
|
const getDomainVersion = () => {
|
||||||
|
switch (process.env.DOMAIN_ENV) {
|
||||||
|
case 'gms':
|
||||||
|
return packageJson['gms-version'];
|
||||||
|
case 'sgw':
|
||||||
|
return packageJson['sgw-version'];
|
||||||
|
case 'spole':
|
||||||
|
return packageJson['spole-version'];
|
||||||
|
default:
|
||||||
|
return '-';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaultFooter
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
}}
|
||||||
|
copyright={`${currentYear} ${defaultMessage} v${getDomainVersion()}`}
|
||||||
|
links={
|
||||||
|
[
|
||||||
|
// {
|
||||||
|
// key: 'smatec',
|
||||||
|
// title: 'Smatec JSC',
|
||||||
|
// href: 'https://smatec.com.vn',
|
||||||
|
// blankTarget: true,
|
||||||
|
// }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
3
src/components/Footer/style.less
Normal file
3
src/components/Footer/style.less
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.ant-pro-global-footer-copyright {
|
||||||
|
color: white !important; /* hoặc mã màu bạn muốn */
|
||||||
|
}
|
||||||
4
src/components/Guide/Guide.less
Normal file
4
src/components/Guide/Guide.less
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.title {
|
||||||
|
margin: 0 auto;
|
||||||
|
font-weight: 200;
|
||||||
|
}
|
||||||
23
src/components/Guide/Guide.tsx
Normal file
23
src/components/Guide/Guide.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Layout, Row, Typography } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
import styles from './Guide.less';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 脚手架示例组件
|
||||||
|
const Guide: React.FC<Props> = (props) => {
|
||||||
|
const { name } = props;
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Row>
|
||||||
|
<Typography.Title level={3} className={styles.title}>
|
||||||
|
欢迎使用 <strong>{name}</strong> !
|
||||||
|
</Typography.Title>
|
||||||
|
</Row>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Guide;
|
||||||
2
src/components/Guide/index.ts
Normal file
2
src/components/Guide/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
import Guide from './Guide';
|
||||||
|
export default Guide;
|
||||||
7
src/components/IconFont/index.tsx
Normal file
7
src/components/IconFont/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { createFromIconfontCN } from '@ant-design/icons';
|
||||||
|
|
||||||
|
const IconFont = createFromIconfontCN({
|
||||||
|
scriptUrl: '//at.alicdn.com/t/c/font_5096559_y5mqyqd86f.js',
|
||||||
|
});
|
||||||
|
|
||||||
|
export default IconFont;
|
||||||
93
src/components/Lang/LanguageSwitcher.tsx
Normal file
93
src/components/Lang/LanguageSwitcher.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import { getLocale, setLocale, useIntl } from '@umijs/max';
|
||||||
|
import { Button, Dropdown, Space } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface LanguageSwitcherProps {
|
||||||
|
className?: string;
|
||||||
|
type?: 'dropdown' | 'button';
|
||||||
|
size?: 'small' | 'middle' | 'large';
|
||||||
|
}
|
||||||
|
|
||||||
|
const LanguageSwitcher: React.FC<LanguageSwitcherProps> = ({
|
||||||
|
className,
|
||||||
|
type = 'dropdown',
|
||||||
|
size = 'middle',
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const languageItems = [
|
||||||
|
{
|
||||||
|
key: 'vi-VN',
|
||||||
|
label: intl.formatMessage({ id: 'common.vietnamese' }),
|
||||||
|
flag: '🇻🇳',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'en-US',
|
||||||
|
label: intl.formatMessage({ id: 'common.english' }),
|
||||||
|
flag: '🇺🇸',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const handleLanguageChange = (locale: string) => {
|
||||||
|
setLocale(locale, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentLanguage = () => {
|
||||||
|
const currentLocale = intl.locale || getLocale() || 'vi-VN';
|
||||||
|
return (
|
||||||
|
languageItems.find((item) => item.key === currentLocale) ||
|
||||||
|
languageItems[0]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dropdownItems = languageItems.map((item) => ({
|
||||||
|
key: item.key,
|
||||||
|
label: (
|
||||||
|
<Space>
|
||||||
|
<span>{item.flag}</span>
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</Space>
|
||||||
|
),
|
||||||
|
onClick: () => handleLanguageChange(item.key),
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (type === 'button') {
|
||||||
|
const currentLang = getCurrentLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
size={size}
|
||||||
|
onClick={() => {
|
||||||
|
const nextLocale = currentLang.key === 'vi-VN' ? 'en-US' : 'vi-VN';
|
||||||
|
handleLanguageChange(nextLocale);
|
||||||
|
}}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<Space>
|
||||||
|
<span>{currentLang.flag}</span>
|
||||||
|
<span>{currentLang.label}</span>
|
||||||
|
</Space>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLang = getCurrentLanguage();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
menu={{ items: dropdownItems }}
|
||||||
|
placement="bottomRight"
|
||||||
|
trigger={['click']}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<Button size={size} type="text">
|
||||||
|
<Space>
|
||||||
|
<span>{currentLang.flag}</span>
|
||||||
|
<span>{currentLang.label}</span>
|
||||||
|
</Space>
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LanguageSwitcher;
|
||||||
43
src/components/Lang/LanguageSwitcherAuth.tsx
Normal file
43
src/components/Lang/LanguageSwitcherAuth.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { getLocale, setLocale } from '@umijs/max';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
const LangSwitches = () => {
|
||||||
|
const [isEnglish, setIsEnglish] = useState(false);
|
||||||
|
|
||||||
|
// Keep checkbox in sync with current Umi locale without forcing a reload
|
||||||
|
useEffect(() => {
|
||||||
|
const syncLocale = () => setIsEnglish(getLocale() === 'en-US');
|
||||||
|
|
||||||
|
syncLocale();
|
||||||
|
window.addEventListener('languagechange', syncLocale);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('languagechange', syncLocale);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLanguageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newLocale = e.target.checked ? 'en-US' : 'vi-VN';
|
||||||
|
setIsEnglish(e.target.checked);
|
||||||
|
setLocale(newLocale, false); // Update locale instantly without full page reload
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className="relative inline-flex items-center cursor-pointer">
|
||||||
|
<input
|
||||||
|
className="sr-only peer"
|
||||||
|
type="checkbox"
|
||||||
|
checked={isEnglish}
|
||||||
|
onChange={handleLanguageChange}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="w-[70px] h-9 rounded-full bg-gradient-to-r from-orange-400 to-red-400 peer-checked:from-blue-400 peer-checked:to-indigo-500
|
||||||
|
transition-all duration-500 after:content-['🇻🇳'] after:absolute after:top-1 peer-checked:left-0 after:left-1 peer-checked:after:left-[-8px] after:bg-white after:rounded-full after:h-7
|
||||||
|
after:w-7 after:flex after:items-center after:justify-center after:transition-all after:duration-500 peer-checked:after:translate-x-10
|
||||||
|
peer-checked:after:content-['🏴'] after:shadow-md after:text-2xl"
|
||||||
|
></div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LangSwitches;
|
||||||
37
src/components/Theme/ThemeProvider.tsx
Normal file
37
src/components/Theme/ThemeProvider.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { ConfigProvider, theme } from 'antd';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { getTheme } from './ThemeSwitcher';
|
||||||
|
|
||||||
|
interface ThemeProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
|
||||||
|
const [isDark, setIsDark] = useState(getTheme() === 'dark');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleThemeChange = (e: CustomEvent) => {
|
||||||
|
setIsDark(e.detail.theme === 'dark');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('theme-change', handleThemeChange as EventListener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
'theme-change',
|
||||||
|
handleThemeChange as EventListener,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ConfigProvider
|
||||||
|
theme={{
|
||||||
|
algorithm: isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ConfigProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeProvider;
|
||||||
81
src/components/Theme/ThemeSwitcher.tsx
Normal file
81
src/components/Theme/ThemeSwitcher.tsx
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { THEME_KEY } from '@/constants';
|
||||||
|
import { MoonOutlined, SunOutlined } from '@ant-design/icons';
|
||||||
|
import { useIntl, useModel } from '@umijs/max';
|
||||||
|
import { Button, Dropdown } from 'antd';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export interface ThemeSwitcherProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeSwitcher: React.FC<ThemeSwitcherProps> = ({ className }) => {
|
||||||
|
const { initialState, setInitialState } = useModel('@@initialState');
|
||||||
|
const intl = useIntl();
|
||||||
|
const [isDark, setIsDark] = useState(
|
||||||
|
(initialState?.theme as 'light' | 'dark') === 'dark',
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleThemeChange = (e: CustomEvent) => {
|
||||||
|
setIsDark(e.detail.theme === 'dark');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('theme-change', handleThemeChange as EventListener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
'theme-change',
|
||||||
|
handleThemeChange as EventListener,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleThemeChange = (newTheme: 'light' | 'dark') => {
|
||||||
|
localStorage.setItem(THEME_KEY, newTheme);
|
||||||
|
// Update global state để trigger layout re-render
|
||||||
|
setInitialState({
|
||||||
|
...initialState,
|
||||||
|
theme: newTheme,
|
||||||
|
} as any).then(() => {
|
||||||
|
// Dispatch event để notify ThemeProvider
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('theme-change', { detail: { theme: newTheme } }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: 'light',
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'common.theme.light',
|
||||||
|
defaultMessage: 'Light Theme',
|
||||||
|
}),
|
||||||
|
icon: <SunOutlined />,
|
||||||
|
onClick: () => handleThemeChange('light'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dark',
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'common.theme.dark',
|
||||||
|
defaultMessage: 'Dark Theme',
|
||||||
|
}),
|
||||||
|
icon: <MoonOutlined />,
|
||||||
|
onClick: () => handleThemeChange('dark'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown menu={{ items }} placement="bottomRight" trigger={['click']}>
|
||||||
|
<Button type="text" className={className}>
|
||||||
|
{isDark ? <MoonOutlined /> : <SunOutlined />}
|
||||||
|
</Button>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeSwitcher;
|
||||||
|
|
||||||
|
// Helper function để get theme từ localStorage
|
||||||
|
export const getTheme = (): 'light' | 'dark' => {
|
||||||
|
return (localStorage.getItem(THEME_KEY) as 'light' | 'dark') || 'light';
|
||||||
|
};
|
||||||
69
src/components/Theme/ThemeSwitcherAuth.tsx
Normal file
69
src/components/Theme/ThemeSwitcherAuth.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { THEME_KEY } from '@/constants';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { getTheme } from './ThemeSwitcher';
|
||||||
|
import './style.less';
|
||||||
|
|
||||||
|
const ThemeSwitcherAuth = () => {
|
||||||
|
const [isDark, setIsDark] = useState(getTheme() === 'dark');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleThemeChange = (e: CustomEvent) => {
|
||||||
|
setIsDark(e.detail.theme === 'dark');
|
||||||
|
};
|
||||||
|
window.addEventListener('theme-change', handleThemeChange as EventListener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(
|
||||||
|
'theme-change',
|
||||||
|
handleThemeChange as EventListener,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSwitch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newTheme = e.target.checked ? 'dark' : 'light';
|
||||||
|
localStorage.setItem(THEME_KEY, newTheme);
|
||||||
|
setIsDark(newTheme === 'dark');
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent('theme-change', { detail: { theme: newTheme } }),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className="theme-switch">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="theme-switch-checkbox"
|
||||||
|
checked={isDark}
|
||||||
|
onChange={handleSwitch}
|
||||||
|
/>
|
||||||
|
<div className="theme-switch-container">
|
||||||
|
<div className="theme-switch-clouds"></div>
|
||||||
|
<div className="theme-switch-stars-container">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 144 55"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d="M135.831 3.00688C135.055 3.85027 134.111 4.29946 133 4.35447C134.111 4.40947 135.055 4.85867 135.831 5.71123C136.607 6.55462 136.996 7.56303 136.996 8.72727C136.996 7.95722 137.172 7.25134 137.525 6.59129C137.886 5.93124 138.372 5.39954 138.98 5.00535C139.598 4.60199 140.268 4.39114 141 4.35447C139.88 4.2903 138.936 3.85027 138.16 3.00688C137.384 2.16348 136.996 1.16425 136.996 0C136.996 1.16425 136.607 2.16348 135.831 3.00688ZM31 23.3545C32.1114 23.2995 33.0551 22.8503 33.8313 22.0069C34.6075 21.1635 34.9956 20.1642 34.9956 19C34.9956 20.1642 35.3837 21.1635 36.1599 22.0069C36.9361 22.8503 37.8798 23.2903 39 23.3545C38.2679 23.3911 37.5976 23.602 36.9802 24.0053C36.3716 24.3995 35.8864 24.9312 35.5248 25.5913C35.172 26.2513 34.9956 26.9572 34.9956 27.7273C34.9956 26.563 34.6075 25.5546 33.8313 24.7112C33.0551 23.8587 32.1114 23.4095 31 23.3545ZM0 36.3545C1.11136 36.2995 2.05513 35.8503 2.83131 35.0069C3.6075 34.1635 3.99559 33.1642 3.99559 32C3.99559 33.1642 4.38368 34.1635 5.15987 35.0069C5.93605 35.8503 6.87982 36.2903 8 36.3545C7.26792 36.3911 6.59757 36.602 5.98015 37.0053C5.37155 37.3995 4.88644 37.9312 4.52481 38.5913C4.172 39.2513 3.99559 39.9572 3.99559 40.7273C3.99559 39.563 3.6075 38.5546 2.83131 37.7112C2.05513 36.8587 1.11136 36.4095 0 36.3545ZM56.8313 24.0069C56.0551 24.8503 55.1114 25.2995 54 25.3545C55.1114 25.4095 56.0551 25.8587 56.8313 26.7112C57.6075 27.5546 57.9956 28.563 57.9956 29.7273C57.9956 28.9572 58.172 28.2513 58.5248 27.5913C58.8864 26.9312 59.3716 26.3995 59.9802 26.0053C60.5976 25.602 61.2679 25.3911 62 25.3545C60.8798 25.2903 59.9361 24.8503 59.1599 24.0069C58.3837 23.1635 57.9956 22.1642 57.9956 21C57.9956 22.1642 57.6075 23.1635 56.8313 24.0069ZM81 25.3545C82.1114 25.2995 83.0551 24.8503 83.8313 24.0069C84.6075 23.1635 84.9956 22.1642 84.9956 21C84.9956 22.1642 85.3837 23.1635 86.1599 24.0069C86.9361 24.8503 87.8798 25.2903 89 25.3545C88.2679 25.3911 87.5976 25.602 86.9802 26.0053C86.3716 26.3995 85.8864 26.9312 85.5248 27.5913C85.172 28.2513 84.9956 28.9572 84.9956 29.7273C84.9956 28.563 84.6075 27.5546 83.8313 26.7112C83.0551 25.8587 82.1114 25.4095 81 25.3545ZM136 36.3545C137.111 36.2995 138.055 35.8503 138.831 35.0069C139.607 34.1635 139.996 33.1642 139.996 32C139.996 33.1642 140.384 34.1635 141.16 35.0069C141.936 35.8503 142.88 36.2903 144 36.3545C143.268 36.3911 142.598 36.602 141.98 37.0053C141.372 37.3995 140.886 37.9312 140.525 38.5913C140.172 39.2513 139.996 39.9572 139.996 40.7273C139.996 39.563 139.607 38.5546 138.831 37.7112C138.055 36.8587 137.111 36.4095 136 36.3545ZM101.831 49.0069C101.055 49.8503 100.111 50.2995 99 50.3545C100.111 50.4095 101.055 50.8587 101.831 51.7112C102.607 52.5546 102.996 53.563 102.996 54.7273C102.996 53.9572 103.172 53.2513 103.525 52.5913C103.886 51.9312 104.372 51.3995 104.98 51.0053C105.598 50.602 106.268 50.3911 107 50.3545C105.88 50.2903 104.936 49.8503 104.16 49.0069C103.384 48.1635 102.996 47.1642 102.996 46C102.996 47.1642 102.607 48.1635 101.831 49.0069Z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="theme-switch-circle-container">
|
||||||
|
<div className="theme-switch-sun-moon-container">
|
||||||
|
<div className="theme-switch-moon">
|
||||||
|
<div className="theme-switch-spot"></div>
|
||||||
|
<div className="theme-switch-spot"></div>
|
||||||
|
<div className="theme-switch-spot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThemeSwitcherAuth;
|
||||||
202
src/components/Theme/style.less
Normal file
202
src/components/Theme/style.less
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
/* From Uiverse.io by Galahhad */
|
||||||
|
.theme-switch {
|
||||||
|
--toggle-size: 20px;
|
||||||
|
--container-width: 3.75em;
|
||||||
|
--container-height: 1.7em;
|
||||||
|
--container-radius: 3.5em;
|
||||||
|
--container-light-bg: #3d7eae;
|
||||||
|
--container-night-bg: #1d1f2c;
|
||||||
|
--circle-container-diameter: 2.1em;
|
||||||
|
--sun-moon-diameter: 1.3em;
|
||||||
|
--sun-bg: #ecca2f;
|
||||||
|
--moon-bg: #c4c9d1;
|
||||||
|
--spot-color: #959db1;
|
||||||
|
--circle-container-offset: calc(
|
||||||
|
(var(--circle-container-diameter) - var(--container-height)) / 2 * -1
|
||||||
|
);
|
||||||
|
--stars-color: #fff;
|
||||||
|
--clouds-color: #f3fdff;
|
||||||
|
--back-clouds-color: #aacadf;
|
||||||
|
--transition: 0.5s cubic-bezier(0, -0.02, 0.4, 1.25);
|
||||||
|
--circle-transition: 0.3s cubic-bezier(0, -0.02, 0.35, 1.17);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch,
|
||||||
|
.theme-switch *,
|
||||||
|
.theme-switch *::before,
|
||||||
|
.theme-switch *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: var(--toggle-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-container {
|
||||||
|
width: var(--container-width);
|
||||||
|
height: var(--container-height);
|
||||||
|
background-color: var(--container-light-bg);
|
||||||
|
border-radius: var(--container-radius);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 -0.062em 0.062em rgba(0, 0, 0, 25%),
|
||||||
|
0 0.062em 0.125em rgba(255, 255, 255, 94%);
|
||||||
|
transition: var(--transition);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-container::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
inset: 0;
|
||||||
|
box-shadow: 0 0.05em 0.187em rgba(0, 0, 0, 25%) inset;
|
||||||
|
border-radius: var(--container-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-checkbox {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-circle-container {
|
||||||
|
width: var(--circle-container-diameter);
|
||||||
|
height: var(--circle-container-diameter);
|
||||||
|
background-color: rgba(255, 255, 255, 10%);
|
||||||
|
position: absolute;
|
||||||
|
left: var(--circle-container-offset);
|
||||||
|
top: var(--circle-container-offset);
|
||||||
|
border-radius: var(--container-radius);
|
||||||
|
box-shadow: inset 0 0 0 3.375em rgba(255, 255, 255, 10%),
|
||||||
|
0 0 0 0.625em rgba(255, 255, 255, 10%),
|
||||||
|
0 0 0 1.25em rgba(255, 255, 255, 10%);
|
||||||
|
display: flex;
|
||||||
|
transition: var(--circle-transition);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-sun-moon-container {
|
||||||
|
pointer-events: auto;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
width: var(--sun-moon-diameter);
|
||||||
|
height: var(--sun-moon-diameter);
|
||||||
|
margin: auto;
|
||||||
|
border-radius: var(--container-radius);
|
||||||
|
background-color: var(--sun-bg);
|
||||||
|
box-shadow: 0.062em 0.062em 0.062em 0 rgba(254, 255, 239, 61%) inset,
|
||||||
|
0 -0.062em 0.062em 0 #a1872a inset;
|
||||||
|
filter: drop-shadow(0.062em 0.125em 0.125em rgba(0, 0, 0, 25%))
|
||||||
|
drop-shadow(0 0.062em 0.125em rgba(0, 0, 0, 25%));
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-moon {
|
||||||
|
transform: translateX(100%);
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--moon-bg);
|
||||||
|
border-radius: inherit;
|
||||||
|
box-shadow: 0.062em 0.062em 0.062em 0 rgba(254, 255, 239, 61%) inset,
|
||||||
|
0 -0.062em 0.062em 0 #969696 inset;
|
||||||
|
transition: var(--transition);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-spot {
|
||||||
|
position: absolute;
|
||||||
|
top: 0.75em;
|
||||||
|
left: 0.312em;
|
||||||
|
width: 0.75em;
|
||||||
|
height: 0.75em;
|
||||||
|
border-radius: var(--container-radius);
|
||||||
|
background-color: var(--spot-color);
|
||||||
|
box-shadow: 0 0.0312em 0.062em rgba(0, 0, 0, 25%) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-spot:nth-of-type(2) {
|
||||||
|
width: 0.375em;
|
||||||
|
height: 0.375em;
|
||||||
|
top: 0.937em;
|
||||||
|
left: 1.375em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-spot:nth-last-of-type(3) {
|
||||||
|
width: 0.25em;
|
||||||
|
height: 0.25em;
|
||||||
|
top: 0.312em;
|
||||||
|
left: 0.812em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-clouds {
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
background-color: var(--clouds-color);
|
||||||
|
border-radius: var(--container-radius);
|
||||||
|
position: absolute;
|
||||||
|
bottom: -0.625em;
|
||||||
|
left: 0.312em;
|
||||||
|
box-shadow: 0.937em 0.312em var(--clouds-color),
|
||||||
|
-0.312em -0.312em var(--back-clouds-color),
|
||||||
|
1.437em 0.375em var(--clouds-color), 0.5em -0.125em var(--back-clouds-color),
|
||||||
|
2.187em 0 var(--clouds-color), 1.25em -0.062em var(--back-clouds-color),
|
||||||
|
2.937em 0.312em var(--clouds-color), 2em -0.312em var(--back-clouds-color),
|
||||||
|
3.625em -0.062em var(--clouds-color), 2.625em 0 var(--back-clouds-color),
|
||||||
|
4.5em -0.312em var(--clouds-color),
|
||||||
|
3.375em -0.437em var(--back-clouds-color),
|
||||||
|
4.625em -1.75em 0 0.437em var(--clouds-color),
|
||||||
|
4em -0.625em var(--back-clouds-color),
|
||||||
|
4.125em -2.125em 0 0.437em var(--back-clouds-color);
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-stars-container {
|
||||||
|
position: absolute;
|
||||||
|
color: var(--stars-color);
|
||||||
|
top: -100%;
|
||||||
|
left: 0.312em;
|
||||||
|
width: 2.75em;
|
||||||
|
height: auto;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* actions */
|
||||||
|
|
||||||
|
.theme-switch-checkbox:checked + .theme-switch-container {
|
||||||
|
background-color: var(--container-night-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-checkbox:checked
|
||||||
|
+ .theme-switch-container
|
||||||
|
.theme-switch-circle-container {
|
||||||
|
left: calc(
|
||||||
|
100% - var(--circle-container-offset) - var(--circle-container-diameter)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-checkbox:checked
|
||||||
|
+ .theme-switch-container
|
||||||
|
.theme-switch-circle-container:hover {
|
||||||
|
left: calc(
|
||||||
|
100% - var(--circle-container-offset) - var(--circle-container-diameter) -
|
||||||
|
0.187em
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-circle-container:hover {
|
||||||
|
left: calc(var(--circle-container-offset) + 0.187em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-checkbox:checked + .theme-switch-container .theme-switch-moon {
|
||||||
|
transform: translate(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-checkbox:checked + .theme-switch-container .theme-switch-clouds {
|
||||||
|
bottom: -4.062em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-switch-checkbox:checked
|
||||||
|
+ .theme-switch-container
|
||||||
|
.theme-switch-stars-container {
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
196
src/components/shared/ThingFilterModal.tsx
Normal file
196
src/components/shared/ThingFilterModal.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import { DEFAULT_PAGE_SIZE, DURATION_POLLING } from '@/constants';
|
||||||
|
import { apiSearchThings } from '@/services/master/ThingController';
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
ProCard,
|
||||||
|
ProColumns,
|
||||||
|
ProTable,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { FormattedMessage, useIntl } from '@umijs/max';
|
||||||
|
import { Grid, Modal, Typography } from 'antd';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import TreeGroup from './TreeGroup';
|
||||||
|
|
||||||
|
const { Paragraph } = Typography;
|
||||||
|
type ThingsFilterProps = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
thingIds?: string | string[] | null;
|
||||||
|
onSubmit?: (thingIds: string[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ThingsFilter = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
thingIds,
|
||||||
|
onSubmit,
|
||||||
|
}: ThingsFilterProps) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { useBreakpoint } = Grid;
|
||||||
|
const screens = useBreakpoint();
|
||||||
|
const tableRef = useRef<ActionType>();
|
||||||
|
const [groupsIds, setGroupIds] = useState<string | string[] | null>(null);
|
||||||
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const domain = process.env.DOMAIN_ENV || 'gms';
|
||||||
|
useEffect(() => {
|
||||||
|
if (thingIds) {
|
||||||
|
const ids = Array.isArray(thingIds) ? thingIds : [thingIds];
|
||||||
|
setSelectedRowKeys(ids);
|
||||||
|
} else {
|
||||||
|
setSelectedRowKeys([]);
|
||||||
|
}
|
||||||
|
}, [thingIds]);
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
onSubmit?.(selectedRowKeys as string[]);
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
const columns: ProColumns<MasterModel.Thing>[] = [
|
||||||
|
{
|
||||||
|
key: 'name',
|
||||||
|
title: <FormattedMessage id="master.thing.name" defaultMessage="Name" />,
|
||||||
|
dataIndex: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'external_id',
|
||||||
|
title: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="master.thing.external_id"
|
||||||
|
defaultMessage="External ID"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
dataIndex: 'metadata.external_id',
|
||||||
|
render: (_, record) =>
|
||||||
|
record?.metadata?.external_id ? (
|
||||||
|
<Paragraph
|
||||||
|
style={{
|
||||||
|
marginBottom: 0,
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
display: 'inline-block',
|
||||||
|
}}
|
||||||
|
copyable
|
||||||
|
>
|
||||||
|
{record?.metadata?.external_id}
|
||||||
|
</Paragraph>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'address',
|
||||||
|
title: (
|
||||||
|
<FormattedMessage id="master.thing.address" defaultMessage="Address" />
|
||||||
|
),
|
||||||
|
dataIndex: 'metadata.address',
|
||||||
|
hideInSearch: true,
|
||||||
|
render: (_, record) => record?.metadata?.address || '-',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={isOpen}
|
||||||
|
centered
|
||||||
|
width="80%"
|
||||||
|
onOk={handleOk}
|
||||||
|
onCancel={handleCancel}
|
||||||
|
maskClosable={false}
|
||||||
|
okText={<FormattedMessage id="common.search" defaultMessage="Search" />}
|
||||||
|
cancelText={
|
||||||
|
<FormattedMessage id="common.cancel" defaultMessage="Cancel" />
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
|
||||||
|
<ProCard colSpan={{ xs: 24, sm: 8, md: 8, lg: 6, xl: 6 }}>
|
||||||
|
<TreeGroup
|
||||||
|
disable={isLoading}
|
||||||
|
multiple
|
||||||
|
onSelected={(value) => {
|
||||||
|
setGroupIds(value);
|
||||||
|
tableRef.current?.reload();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
<ProCard colSpan={{ xs: 24, sm: 16, md: 16, lg: 18, xl: 18 }}>
|
||||||
|
<ProTable<MasterModel.Thing>
|
||||||
|
tableClassName="table-row-select cursor-pointer-row"
|
||||||
|
actionRef={tableRef}
|
||||||
|
toolbar={{
|
||||||
|
title: null,
|
||||||
|
}}
|
||||||
|
bordered={true}
|
||||||
|
polling={DURATION_POLLING * 4} // 1 minute
|
||||||
|
rowSelection={{
|
||||||
|
alwaysShowAlert: true,
|
||||||
|
selectedRowKeys,
|
||||||
|
onChange: (keys) => setSelectedRowKeys(keys),
|
||||||
|
}}
|
||||||
|
pagination={{
|
||||||
|
size: 'small',
|
||||||
|
defaultPageSize: DEFAULT_PAGE_SIZE * 2,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: ['10', '15', '20'],
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`${range[0]}-${range[1]} ${intl.formatMessage({
|
||||||
|
id: 'common.paginations.of',
|
||||||
|
})} ${total} ${intl.formatMessage({
|
||||||
|
id: 'common.paginations.things',
|
||||||
|
defaultMessage: 'things',
|
||||||
|
})}`,
|
||||||
|
}}
|
||||||
|
columns={columns}
|
||||||
|
request={async (params) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const { current = 1, pageSize, name, external_id } = params;
|
||||||
|
const size = pageSize || DEFAULT_PAGE_SIZE * 2;
|
||||||
|
const offset = current === 1 ? 0 : (current - 1) * size;
|
||||||
|
const query: MasterModel.SearchThingPaginationBody = {
|
||||||
|
offset: offset,
|
||||||
|
limit: size,
|
||||||
|
order: 'name',
|
||||||
|
dir: 'asc',
|
||||||
|
};
|
||||||
|
if (name) {
|
||||||
|
query.name = name;
|
||||||
|
}
|
||||||
|
if (!query.metadata) query.metadata = {};
|
||||||
|
if (groupsIds)
|
||||||
|
query.metadata.group_id = Array.isArray(groupsIds)
|
||||||
|
? groupsIds.join(',')
|
||||||
|
: groupsIds || undefined;
|
||||||
|
|
||||||
|
if (external_id) {
|
||||||
|
query.metadata = {
|
||||||
|
...query.metadata,
|
||||||
|
external_id: external_id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const resp = await apiSearchThings(query, domain);
|
||||||
|
setIsLoading(false);
|
||||||
|
return {
|
||||||
|
data: resp.things || [],
|
||||||
|
success: true,
|
||||||
|
total: resp.total || 0,
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
search: false,
|
||||||
|
setting: false,
|
||||||
|
density: false,
|
||||||
|
reload: true,
|
||||||
|
}}
|
||||||
|
defaultSize="small"
|
||||||
|
rowKey="id"
|
||||||
|
dateFormatter="string"
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
</ProCard>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThingsFilter;
|
||||||
139
src/components/shared/TreeGroup.tsx
Normal file
139
src/components/shared/TreeGroup.tsx
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { useModel } from '@umijs/max';
|
||||||
|
import { Tree } from 'antd';
|
||||||
|
import type { DataNode } from 'antd/es/tree';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface TreeGroupProps {
|
||||||
|
groupIds?: string | string[] | null;
|
||||||
|
multiple?: boolean;
|
||||||
|
allowedGroupIds?: string[];
|
||||||
|
disable?: boolean;
|
||||||
|
onSelected?: (value: string | string[] | null) => void;
|
||||||
|
titleRender?: (nodeData: DataNode) => React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TreeGroup = ({
|
||||||
|
groupIds,
|
||||||
|
multiple = false,
|
||||||
|
allowedGroupIds,
|
||||||
|
disable = false,
|
||||||
|
onSelected,
|
||||||
|
titleRender,
|
||||||
|
}: TreeGroupProps) => {
|
||||||
|
const { initialState } = useModel('@@initialState');
|
||||||
|
const { currentUserProfile } = initialState || {};
|
||||||
|
const [treeData, setTreeData] = useState<DataNode[]>([]);
|
||||||
|
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
|
||||||
|
const { groups, getGroups } = useModel('master.useGroups');
|
||||||
|
|
||||||
|
const filterGroupsByAllowedIds = (
|
||||||
|
groups: MasterModel.GroupNode[],
|
||||||
|
allowedIds: Set<string>,
|
||||||
|
): MasterModel.GroupNode[] => {
|
||||||
|
return groups
|
||||||
|
.map((group) => {
|
||||||
|
const filteredChildren = group.children
|
||||||
|
? filterGroupsByAllowedIds(group.children, allowedIds)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (allowedIds.has(group.id) || filteredChildren.length > 0) {
|
||||||
|
return {
|
||||||
|
...group,
|
||||||
|
children: filteredChildren,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as MasterModel.GroupNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformGroupNodeToTreeData = (
|
||||||
|
groups: MasterModel.GroupNode[],
|
||||||
|
): DataNode[] =>
|
||||||
|
groups.map((group) => ({
|
||||||
|
title: group.name,
|
||||||
|
key: group.id,
|
||||||
|
children: group.children?.length
|
||||||
|
? transformGroupNodeToTreeData(group.children)
|
||||||
|
: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!groups) return;
|
||||||
|
|
||||||
|
let displayGroups = groups;
|
||||||
|
|
||||||
|
if (allowedGroupIds && allowedGroupIds.length > 0) {
|
||||||
|
const allowedSet = new Set(allowedGroupIds);
|
||||||
|
displayGroups = filterGroupsByAllowedIds(groups, allowedSet);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTreeData(transformGroupNodeToTreeData(displayGroups));
|
||||||
|
}, [groups, allowedGroupIds]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!groups) {
|
||||||
|
getGroups();
|
||||||
|
}
|
||||||
|
}, [groups]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTreeData = async () => {
|
||||||
|
const transformedData = transformGroupNodeToTreeData(groups || []);
|
||||||
|
setTreeData(transformedData);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTreeData();
|
||||||
|
}, [currentUserProfile, groups]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Set selected keys from groupIds prop
|
||||||
|
if (groupIds) {
|
||||||
|
setSelectedKeys(Array.isArray(groupIds) ? groupIds : [groupIds]);
|
||||||
|
} else {
|
||||||
|
setSelectedKeys([]);
|
||||||
|
}
|
||||||
|
}, [groupIds]);
|
||||||
|
|
||||||
|
const handleSelect = (selectedKeys: React.Key[]) => {
|
||||||
|
const keys = selectedKeys.map((k) => String(k));
|
||||||
|
setSelectedKeys(selectedKeys);
|
||||||
|
|
||||||
|
if (multiple) {
|
||||||
|
onSelected?.(keys.length > 0 ? keys : null);
|
||||||
|
} else {
|
||||||
|
onSelected?.(keys.length > 0 ? keys[0] : null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheck = (
|
||||||
|
checkedKeys:
|
||||||
|
| React.Key[]
|
||||||
|
| { checked: React.Key[]; halfChecked: React.Key[] },
|
||||||
|
) => {
|
||||||
|
const keys = Array.isArray(checkedKeys) ? checkedKeys : checkedKeys.checked;
|
||||||
|
const stringKeys = keys.map((k) => String(k));
|
||||||
|
setSelectedKeys(keys);
|
||||||
|
|
||||||
|
onSelected?.(stringKeys.length > 0 ? stringKeys : null);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tree
|
||||||
|
disabled={disable}
|
||||||
|
checkable={multiple}
|
||||||
|
treeData={treeData}
|
||||||
|
checkedKeys={multiple ? selectedKeys : undefined}
|
||||||
|
selectedKeys={!multiple ? selectedKeys : undefined}
|
||||||
|
onCheck={multiple ? handleCheck : undefined}
|
||||||
|
onSelect={!multiple ? handleSelect : undefined}
|
||||||
|
defaultExpandAll={true}
|
||||||
|
showLine
|
||||||
|
autoExpandParent
|
||||||
|
{...(titleRender && { titleRender })}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TreeGroup;
|
||||||
140
src/components/shared/TreeSelectedGroup.tsx
Normal file
140
src/components/shared/TreeSelectedGroup.tsx
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import { useIntl, useModel } from '@umijs/max';
|
||||||
|
import { Spin, TreeSelect } from 'antd';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface TreeSelectedGroupProps {
|
||||||
|
groupIds?: string | string[] | null;
|
||||||
|
multiple?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
isLoading?: boolean;
|
||||||
|
onSelected?: (value: string | string[] | null) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
allowClear?: boolean;
|
||||||
|
showSearch?: boolean;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TreeSelectedGroup = ({
|
||||||
|
groupIds,
|
||||||
|
multiple = false,
|
||||||
|
disabled = false,
|
||||||
|
isLoading = false,
|
||||||
|
onSelected,
|
||||||
|
placeholder,
|
||||||
|
allowClear = true,
|
||||||
|
showSearch = true,
|
||||||
|
style = { width: '100%' },
|
||||||
|
}: TreeSelectedGroupProps) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const { initialState } = useModel('@@initialState');
|
||||||
|
const { currentUserProfile } = initialState || {};
|
||||||
|
const [treeData, setTreeData] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [selectedValue, setSelectedValue] = useState<string | string[] | null>(
|
||||||
|
() => {
|
||||||
|
// If disabled is true, don't show any selected values
|
||||||
|
if (disabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return groupIds || null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { groups, getGroups } = useModel('master.useGroups');
|
||||||
|
useEffect(() => {
|
||||||
|
if (!groups) {
|
||||||
|
getGroups();
|
||||||
|
}
|
||||||
|
}, [groups]);
|
||||||
|
// Update internal state when prop changes
|
||||||
|
useEffect(() => {
|
||||||
|
// If disabled is true, clear the selection
|
||||||
|
if (disabled) {
|
||||||
|
setSelectedValue(null);
|
||||||
|
} else {
|
||||||
|
setSelectedValue(groupIds || null);
|
||||||
|
}
|
||||||
|
}, [groupIds, disabled]);
|
||||||
|
|
||||||
|
const transformGroupNodeToTreeData = (
|
||||||
|
groups: MasterModel.GroupNode[],
|
||||||
|
): any[] => {
|
||||||
|
// console.log('Group tranform: ', groups);
|
||||||
|
|
||||||
|
// Convert groupIds to array for easier checking
|
||||||
|
const groupIdsArray = Array.isArray(groupIds)
|
||||||
|
? groupIds
|
||||||
|
: groupIds
|
||||||
|
? [groupIds]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return groups.map((group: any) => ({
|
||||||
|
title: group.name,
|
||||||
|
value: group.id,
|
||||||
|
key: group.id,
|
||||||
|
disabled: disabled && groupIdsArray.includes(group.id),
|
||||||
|
children: group.children
|
||||||
|
? transformGroupNodeToTreeData(group.children)
|
||||||
|
: undefined,
|
||||||
|
data: group,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const loadTreeData = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const transformedData = transformGroupNodeToTreeData(groups || []);
|
||||||
|
setTreeData(transformedData);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
loadTreeData();
|
||||||
|
}, [currentUserProfile, groups, disabled, groupIds]);
|
||||||
|
|
||||||
|
const handleChange = (value: string | string[] | null) => {
|
||||||
|
setSelectedValue(value);
|
||||||
|
onSelected?.(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<TreeSelect
|
||||||
|
disabled={isLoading}
|
||||||
|
value={selectedValue}
|
||||||
|
multiple={multiple}
|
||||||
|
treeCheckable={multiple}
|
||||||
|
showSearch={showSearch}
|
||||||
|
style={style}
|
||||||
|
styles={{
|
||||||
|
popup: {
|
||||||
|
root: {
|
||||||
|
maxHeight: 400,
|
||||||
|
overflow: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
placeholder={
|
||||||
|
placeholder ||
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.treeselect.placeholder',
|
||||||
|
defaultMessage: 'Please select group',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
allowClear={allowClear}
|
||||||
|
treeDefaultExpandAll
|
||||||
|
treeData={treeData}
|
||||||
|
onChange={handleChange}
|
||||||
|
treeLine
|
||||||
|
filterTreeNode={(search, node) => {
|
||||||
|
return (
|
||||||
|
node.title
|
||||||
|
?.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.toLowerCase()) ?? false
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Spin>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TreeSelectedGroup;
|
||||||
22
src/constants/api.ts
Normal file
22
src/constants/api.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Auth API Paths
|
||||||
|
export const API_PATH_LOGIN = '/api/tokens';
|
||||||
|
export const API_PATH_GET_PROFILE = '/api/users/profile';
|
||||||
|
export const API_CHANGE_PASSWORD = '/api/password';
|
||||||
|
// Alarm API Constants
|
||||||
|
export const API_ALARMS = '/api/alarms';
|
||||||
|
export const API_ALARMS_CONFIRM = '/api/alarms/confirm';
|
||||||
|
|
||||||
|
// Thing API Constants
|
||||||
|
export const API_THINGS_SEARCH = '/api/things/search';
|
||||||
|
|
||||||
|
// Group API Constants
|
||||||
|
export const API_GROUPS = '/api/groups';
|
||||||
|
export const API_GROUP_MEMBERS = '/api/members';
|
||||||
|
export const API_GROUP_CHILDREN = '/api/groups';
|
||||||
|
|
||||||
|
// Log API Constants
|
||||||
|
export const API_LOGS = '/api/reader/channels';
|
||||||
|
|
||||||
|
// User API Constants
|
||||||
|
export const API_USERS = '/api/users';
|
||||||
|
export const API_USERS_BY_GROUP = '/api/users/groups';
|
||||||
38
src/constants/index.ts
Normal file
38
src/constants/index.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
export const DURATION_POLLING = 15000; //15s
|
||||||
|
export const DURATION_CHART = 300000; //5m
|
||||||
|
export const DEFAULT_LIMIT = 200;
|
||||||
|
export const DATE_TIME_FORMAT = 'DD/MM/YYYY HH:mm:ss';
|
||||||
|
export const TIME_FORMAT = 'HH:mm:ss';
|
||||||
|
export const DATE_FORMAT = 'DD/MM/YYYY';
|
||||||
|
|
||||||
|
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';
|
||||||
|
export const COLOR_NORMAL = '#389e0d';
|
||||||
|
export const COLOR_WARNING = '#d48806';
|
||||||
|
export const COLOR_DANGEROUS = '#d9363e';
|
||||||
|
export const COLOR_SOS = '#ff0000';
|
||||||
|
|
||||||
|
export const TOKEN = 'token';
|
||||||
|
export const THEME_KEY = 'theme';
|
||||||
|
// Global Constants
|
||||||
|
export const LIMIT_TREE_LEVEL = 5;
|
||||||
|
export const DEFAULT_PAGE_SIZE = 5;
|
||||||
|
|
||||||
|
export const PADDING_IN_LINE = 4;
|
||||||
|
export const PADDING_BLOCK = 4;
|
||||||
|
|
||||||
|
export enum HTTPSTATUS {
|
||||||
|
HTTP_SUCCESS = 200,
|
||||||
|
HTTP_BADREQUEST = 400,
|
||||||
|
HTTP_UNAUTHORIZED = 401,
|
||||||
|
HTTP_FORBIDDEN = 403,
|
||||||
|
HTTP_NOTFOUND = 404,
|
||||||
|
HTTP_SERVERERROR = 500,
|
||||||
|
HTTP_ACCEPTED = 202,
|
||||||
|
HTTP_CREATED = 201,
|
||||||
|
HTTP_NOCONTENT = 204,
|
||||||
|
}
|
||||||
3
src/constants/routes.ts
Normal file
3
src/constants/routes.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export const ROUTE_LOGIN = '/login';
|
||||||
|
export const ROUTER_HOME = '/';
|
||||||
|
export const ROUTE_PROFILE = '/profile';
|
||||||
6
src/constants/slave/sgw/index.ts
Normal file
6
src/constants/slave/sgw/index.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export enum SGW_ROLE {
|
||||||
|
SYSADMIN = 'sysadmin',
|
||||||
|
ADMIN = 'admin',
|
||||||
|
USERS = 'users',
|
||||||
|
ENDUSER = 'enduser',
|
||||||
|
}
|
||||||
4
src/constants/slave/sgw/routes.ts
Normal file
4
src/constants/slave/sgw/routes.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export const SGW_ROUTE_HOME = '/maps';
|
||||||
|
export const SGW_ROUTE_TRIP = '/trip';
|
||||||
|
export const SGW_ROUTE_CREATE_BANZONE = '/manager/banzones/create';
|
||||||
|
export const SGW_ROUTE_MANAGER_BANZONE = '/manager/banzones';
|
||||||
5
src/env.d.ts
vendored
Normal file
5
src/env.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
declare namespace NodeJS {
|
||||||
|
interface ProcessEnv {
|
||||||
|
DOMAIN_ENV?: 'gms' | 'sgw' | 'spole'; // hoặc các giá trị bạn dùng
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/global.d.ts
vendored
Normal file
14
src/global.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/// <reference types="@ant-design/pro-components" />
|
||||||
|
|
||||||
|
declare module '@ant-design/pro-components' {
|
||||||
|
export * from '@ant-design/pro-card';
|
||||||
|
export * from '@ant-design/pro-descriptions';
|
||||||
|
export * from '@ant-design/pro-field';
|
||||||
|
export * from '@ant-design/pro-form';
|
||||||
|
export * from '@ant-design/pro-layout';
|
||||||
|
export * from '@ant-design/pro-list';
|
||||||
|
export * from '@ant-design/pro-provider';
|
||||||
|
export * from '@ant-design/pro-skeleton';
|
||||||
|
export * from '@ant-design/pro-table';
|
||||||
|
export * from '@ant-design/pro-utils';
|
||||||
|
}
|
||||||
63
src/locales/en-US.ts
Normal file
63
src/locales/en-US.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import masterEn from './en-US/master/master-en';
|
||||||
|
import gmsEn from './en-US/slave/gms/gms-en';
|
||||||
|
import sgwEn from './en-US/slave/sgw/sgw-en';
|
||||||
|
import spoleEn from './en-US/slave/spole/spole-en';
|
||||||
|
export default {
|
||||||
|
'app.copyright.produced': 'Produced by Mobifone.',
|
||||||
|
'common.save': 'Save',
|
||||||
|
'common.cancel': 'Cancel',
|
||||||
|
'common.delete': 'Delete',
|
||||||
|
'common.delete_confirm': 'Confirm delete?',
|
||||||
|
'common.delete_messgage': 'Do you want to delete',
|
||||||
|
'common.deleting': 'Deleting...',
|
||||||
|
'common.edit': 'Edit',
|
||||||
|
'common.add': 'Add',
|
||||||
|
'common.actions': 'Actions',
|
||||||
|
'common.search': 'Search',
|
||||||
|
'common.filter': 'Filter',
|
||||||
|
'common.loading': 'Loading...',
|
||||||
|
'common.updating': 'Updating...',
|
||||||
|
'common.confirm': 'Confirm',
|
||||||
|
'common.notification': 'Notification',
|
||||||
|
'common.back': 'Back',
|
||||||
|
'common.next': 'Next',
|
||||||
|
'common.logout': 'Log out',
|
||||||
|
'common.yes': 'Yes',
|
||||||
|
'common.sure': 'Sure',
|
||||||
|
'common.no': 'No',
|
||||||
|
'common.ok': 'OK',
|
||||||
|
'common.error': 'Error',
|
||||||
|
'common.vietnamese': 'Vietnamese',
|
||||||
|
'common.english': 'English',
|
||||||
|
'common.theme.light': 'Light Theme',
|
||||||
|
'common.theme.dark': 'Dark Theme',
|
||||||
|
'common.paginations.things': 'things',
|
||||||
|
'common.paginations.of': 'of',
|
||||||
|
'common.name': 'Name',
|
||||||
|
'common.name.required': 'Name is required',
|
||||||
|
'common.type': 'Type',
|
||||||
|
'common.type.placeholder': 'Select Type',
|
||||||
|
'common.status': 'Status',
|
||||||
|
'common.province': 'Province',
|
||||||
|
'common.description': 'Description',
|
||||||
|
'common.description.required': 'Description is required',
|
||||||
|
'common.description.placeholder': 'Enter description',
|
||||||
|
'common.active': 'Active',
|
||||||
|
'common.inactive': 'Inactive',
|
||||||
|
'common.created_at': 'Created At',
|
||||||
|
'common.updated_at': 'Updated At',
|
||||||
|
'common.undefined': 'Undefined',
|
||||||
|
'common.not_empty': 'Cannot be empty!',
|
||||||
|
'common.level.normal': 'Normal',
|
||||||
|
'common.level.warning': 'Warning',
|
||||||
|
'common.level.critical': 'Critical',
|
||||||
|
'common.level.sos': 'SOS',
|
||||||
|
'common.unaccess': 'Sorry, you do not have permission to access this page',
|
||||||
|
'common.notfound': 'The page you visited does not exist',
|
||||||
|
'common.internalserver': 'Sorry, something went wrong on the server',
|
||||||
|
'common.required_field': 'This field cannot be empty',
|
||||||
|
...masterEn,
|
||||||
|
...sgwEn,
|
||||||
|
...gmsEn,
|
||||||
|
...spoleEn,
|
||||||
|
};
|
||||||
17
src/locales/en-US/master/master-alarm-en.ts
Normal file
17
src/locales/en-US/master/master-alarm-en.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
export default {
|
||||||
|
'master.alarms.table.pagination': 'alarms',
|
||||||
|
'master.alarms.severity': 'Severity',
|
||||||
|
'master.alarms.occurred_at': 'Occurred time',
|
||||||
|
'master.alarms.confirmed': 'Confirmed',
|
||||||
|
'master.alarms.confirm.description.required': 'The description is required',
|
||||||
|
'master.alarms.confirm.title': 'Confirm Alarm',
|
||||||
|
'master.alarms.confirm.success': 'Confirm alarm successfully',
|
||||||
|
'master.alarms.confirm.fail': 'Confirm alarm failed',
|
||||||
|
'master.alarms.unconfirm.title': 'Unconfirm',
|
||||||
|
'master.alarms.unconfirm.body':
|
||||||
|
'Are you sure you want to unconfirm this alarm?',
|
||||||
|
'master.alarms.unconfirm.success': 'Unconfirm alarm successfully',
|
||||||
|
'master.alarms.unconfirm.fail': 'Unconfirm alarm failed',
|
||||||
|
'master.alarms.not_found': 'Alarm has expired or does not exist',
|
||||||
|
'master.alarms.filter_things': 'Filter by device group',
|
||||||
|
};
|
||||||
15
src/locales/en-US/master/master-auth-en.ts
Normal file
15
src/locales/en-US/master/master-auth-en.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
// Authentication
|
||||||
|
'master.auth.login.title': 'Login',
|
||||||
|
'master.auth.login.email': 'Email',
|
||||||
|
'master.auth.validation.email': 'Email is required',
|
||||||
|
'master.auth.password': 'Password',
|
||||||
|
'master.auth.validation.password': 'Password is required',
|
||||||
|
'master.auth.login.subtitle': 'Ship Monitoring System',
|
||||||
|
'master.auth.login.description': 'Login to continue monitoring vessels',
|
||||||
|
'master.auth.login.invalid': 'Invalid username or password',
|
||||||
|
'master.auth.login.success': 'Login successful',
|
||||||
|
'master.auth.logout.title': 'Logout',
|
||||||
|
'master.auth.logout.confirm': 'Are you sure you want to logout?',
|
||||||
|
'master.auth.logout.success': 'Logout successful',
|
||||||
|
};
|
||||||
18
src/locales/en-US/master/master-en.ts
Normal file
18
src/locales/en-US/master/master-en.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import masterAlarmEn from './master-alarm-en';
|
||||||
|
import masterAuthEn from './master-auth-en';
|
||||||
|
import masterGroupEn from './master-group-en';
|
||||||
|
import masterSysLogEn from './master-log-en';
|
||||||
|
import masterMenuEn from './master-menu-en';
|
||||||
|
import masterMenuProfileEn from './master-profile-en';
|
||||||
|
import masterThingEn from './master-thing-en';
|
||||||
|
import masterUserEn from './master-user-en';
|
||||||
|
export default {
|
||||||
|
...masterAuthEn,
|
||||||
|
...masterMenuEn,
|
||||||
|
...masterMenuProfileEn,
|
||||||
|
...masterAlarmEn,
|
||||||
|
...masterThingEn,
|
||||||
|
...masterSysLogEn,
|
||||||
|
...masterUserEn,
|
||||||
|
...masterGroupEn,
|
||||||
|
};
|
||||||
21
src/locales/en-US/master/master-group-en.ts
Normal file
21
src/locales/en-US/master/master-group-en.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
export default {
|
||||||
|
'master.groups.root': 'Add root',
|
||||||
|
'master.groups.cannot-add-group':
|
||||||
|
'Cannot add child to a group that has things',
|
||||||
|
'master.groups.add': 'Add child',
|
||||||
|
'master.groups.delete.confirm': 'Are you sure you want to delete this group?',
|
||||||
|
'master.groups.code': 'Code',
|
||||||
|
'master.groups.code.exists': 'The code already exists',
|
||||||
|
'master.groups.short_name': 'Short name',
|
||||||
|
'master.groups.short_name.exists': 'The short name already exists',
|
||||||
|
'master.groups.update.success': 'Updated group successfully',
|
||||||
|
'master.groups.update.failed': 'Update group failed, please try again!',
|
||||||
|
'master.groups.create.success': 'Created group successfully',
|
||||||
|
'master.groups.create.failed': 'Create group failed, please try again!',
|
||||||
|
'master.groups.delete.success': 'Deleted group successfully',
|
||||||
|
'master.groups.delete.failed': 'Delete group failed',
|
||||||
|
'master.groups.delete.failed_internal':
|
||||||
|
'The group contains devices or users and cannot be deleted',
|
||||||
|
'master.groups.parent': 'Parent',
|
||||||
|
'master.groups.treeselect.placeholder': 'Please select group',
|
||||||
|
};
|
||||||
51
src/locales/en-US/master/master-log-en.ts
Normal file
51
src/locales/en-US/master/master-log-en.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export default {
|
||||||
|
'master.logs.search.yesterday': 'Yesterday',
|
||||||
|
'master.logs.search.last_week': 'Last week',
|
||||||
|
'master.logs.search.last_month': 'Last month',
|
||||||
|
'master.logs.table.pagination': 'activities',
|
||||||
|
'master.logs.things.create': 'Create new thing',
|
||||||
|
'master.logs.things.update': 'Update thing',
|
||||||
|
'master.logs.things.remove': 'Remove thing',
|
||||||
|
'master.logs.things.share': 'Share thing',
|
||||||
|
'master.logs.things.unshare': 'Unshare thing',
|
||||||
|
'master.logs.things.update_key': 'Update key thing',
|
||||||
|
'master.logs.users.create': 'Register user',
|
||||||
|
'master.logs.users.update': 'Update user',
|
||||||
|
'master.logs.users.remove': 'Remove user',
|
||||||
|
'master.logs.users.share': 'User share',
|
||||||
|
'master.logs.groups.create': 'Create new group',
|
||||||
|
'master.logs.groups.update': 'Update group',
|
||||||
|
'master.logs.groups.remove': 'Remove group',
|
||||||
|
'master.logs.groups.assign_thing': 'Assign thing to group',
|
||||||
|
'master.logs.groups.assign_user': 'Assign user to group',
|
||||||
|
'master.logs.groups.unassign_thing': 'Remove thing from group',
|
||||||
|
'master.logs.groups.unassign_user': 'Remove user from group',
|
||||||
|
'master.logs.action.text': 'Action',
|
||||||
|
'master.logs.email.text': 'Email',
|
||||||
|
'master.logs.date.text': 'Date',
|
||||||
|
'master.logs.things.alarm.confirm': 'Alarm confirm',
|
||||||
|
'master.logs.things.alarm.unconfirm': 'Alarm unconfirm',
|
||||||
|
'master.logs.users.login': 'User login',
|
||||||
|
'master.logs.title': 'Logs',
|
||||||
|
'master.logs.things.confirm': 'Confirm',
|
||||||
|
'master.logs.things.unconfirm': 'Unconfirm',
|
||||||
|
'master.logs.things': 'Things',
|
||||||
|
'master.logs.users': 'Users',
|
||||||
|
'master.logs.groups': 'Groups',
|
||||||
|
'master.logs.ships': 'Ships',
|
||||||
|
'master.logs.ships.create': 'Create new ship',
|
||||||
|
'master.logs.ships.update': 'Update ship',
|
||||||
|
'master.logs.ships.remove': 'Remove ship',
|
||||||
|
'master.logs.ships.assign_thing': 'Assign device to ship',
|
||||||
|
'master.logs.ships.assign_user': 'Assign user to ship',
|
||||||
|
'master.logs.ships.unassign_thing': 'Remove device from ship',
|
||||||
|
'master.logs.ships.unassign_user': 'Remove user from ship',
|
||||||
|
'master.logs.trips': 'Trips',
|
||||||
|
'master.logs.trips.create': 'Create new trip',
|
||||||
|
'master.logs.trips.update': 'Update trip',
|
||||||
|
'master.logs.trips.remove': 'Remove trip',
|
||||||
|
'master.logs.trips.approve': 'Approve trip',
|
||||||
|
'master.logs.trips.request_approve': 'Request approval for trip',
|
||||||
|
'master.logs.trips.unassign_thing': 'Remove device from trip',
|
||||||
|
'master.logs.trips.unassign_user': 'Remove user from trip',
|
||||||
|
};
|
||||||
10
src/locales/en-US/master/master-menu-en.ts
Normal file
10
src/locales/en-US/master/master-menu-en.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export default {
|
||||||
|
'menu.alarms': 'Alarms',
|
||||||
|
'menu.monitoring': 'Monitoring',
|
||||||
|
'menu.manager': 'Manager',
|
||||||
|
'menu.manager.devices': 'Devices',
|
||||||
|
'menu.manager.groups': 'Groups',
|
||||||
|
'menu.manager.users': 'Users',
|
||||||
|
'menu.manager.logs': 'Logs',
|
||||||
|
'menu.profile': 'Profile',
|
||||||
|
};
|
||||||
20
src/locales/en-US/master/master-profile-en.ts
Normal file
20
src/locales/en-US/master/master-profile-en.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
'master.profile.change-profile': 'Basic Information',
|
||||||
|
'master.profile.change-password': 'Change Password',
|
||||||
|
'master.profile.two-factor-authentication': 'Two-Factor Authentication',
|
||||||
|
'master.profile.change-profile.title': 'Update Personal Information',
|
||||||
|
'master.profile.change-profile.full_name': 'Full Name',
|
||||||
|
'master.profile.change-profile.phone_number': 'Phone Number',
|
||||||
|
'master.profile.change-password.old_password': 'Old Password',
|
||||||
|
'master.profile.change-password.new_password': 'New Password',
|
||||||
|
'master.profile.change-password.confirm_password': 'Confirm Password',
|
||||||
|
'master.profile.change-password.password_not_match':
|
||||||
|
'Password confirmation does not match',
|
||||||
|
'master.profile.change-profile.update-success':
|
||||||
|
'Update information successfully',
|
||||||
|
'master.profile.change-profile.update-fail': 'Update information failed',
|
||||||
|
'master.profile.change-password.success': 'Change password successfully',
|
||||||
|
'master.profile.change-password.fail': 'Change password failed',
|
||||||
|
'master.profile.change-password.password.strong':
|
||||||
|
'A password contains at least 8 characters, including at least one number and includes both lower and uppercase letters and special characters, for example #, ?, !',
|
||||||
|
};
|
||||||
6
src/locales/en-US/master/master-thing-en.ts
Normal file
6
src/locales/en-US/master/master-thing-en.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
'master.thing.name': 'Name',
|
||||||
|
'master.thing.external_id': 'External ID',
|
||||||
|
'master.thing.group': 'Group',
|
||||||
|
'master.thing.address': 'Address',
|
||||||
|
};
|
||||||
35
src/locales/en-US/master/master-user-en.ts
Normal file
35
src/locales/en-US/master/master-user-en.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export default {
|
||||||
|
'master.users.title': 'Users',
|
||||||
|
'master.users.table.pagination': 'users',
|
||||||
|
'master.users.register': 'Register',
|
||||||
|
'master.users.register.title': 'Register user',
|
||||||
|
'master.users.email.exists': 'The email is exists',
|
||||||
|
'master.users.email': 'Email',
|
||||||
|
'master.users.email.tip': 'The email is the unique key',
|
||||||
|
'master.users.email.required': 'The email is required',
|
||||||
|
'master.users.email.invalid': 'Invalid email address',
|
||||||
|
'master.users.role': 'Role',
|
||||||
|
'master.users.role.placeholder': 'Please select a role',
|
||||||
|
'master.users.role.tip': 'The role is the unique key',
|
||||||
|
'master.users.password.required': 'Password is required',
|
||||||
|
'master.users.password.minimum': 'Minimum password length is 8',
|
||||||
|
'master.users.password': 'Password',
|
||||||
|
'master.users.full_name': 'Full name',
|
||||||
|
'master.users.full_name.placeholder': 'Enter full name',
|
||||||
|
'master.users.full_name.required': 'Please enter full name',
|
||||||
|
'master.users.password.placeholder': 'Password',
|
||||||
|
'master.users.confirmpassword.required': 'Confirm password is required',
|
||||||
|
'master.users.email.placeholder': 'Email',
|
||||||
|
'master.users.phone_number': 'Phone number',
|
||||||
|
'master.users.phone_number.tip': 'The phone number is the unique key',
|
||||||
|
'master.users.phone_number.required': 'The phone number is required',
|
||||||
|
'master.users.phone_number.notvalid': 'Invalid phone number',
|
||||||
|
'master.users.groups': 'Groups',
|
||||||
|
'master.users.groups.required': 'Please select groups',
|
||||||
|
'master.users.role.sysadmin': 'System Administrator',
|
||||||
|
'master.users.role.admin': 'Unit Manager',
|
||||||
|
'master.users.role.user': 'Unit Supervisor',
|
||||||
|
'master.users.role.sgw.end_user': 'Ship Owner',
|
||||||
|
'master.users.create.error': 'User creation failed',
|
||||||
|
'master.users.create.success': 'User created successfully',
|
||||||
|
};
|
||||||
5
src/locales/en-US/slave/gms/gms-en.ts
Normal file
5
src/locales/en-US/slave/gms/gms-en.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import gmsMenuEn from './gms-menu-en';
|
||||||
|
export default {
|
||||||
|
'gms.title': 'SmartGMS',
|
||||||
|
...gmsMenuEn,
|
||||||
|
};
|
||||||
3
src/locales/en-US/slave/gms/gms-menu-en.ts
Normal file
3
src/locales/en-US/slave/gms/gms-menu-en.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
'menu.gms.monitor': 'Monitor',
|
||||||
|
};
|
||||||
6
src/locales/en-US/slave/sgw/sgw-en.ts
Normal file
6
src/locales/en-US/slave/sgw/sgw-en.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import sgwMenu from './sgw-menu-en';
|
||||||
|
export default {
|
||||||
|
'sgw.title': 'Sea Gateway',
|
||||||
|
'sgw.ship': 'Ship',
|
||||||
|
...sgwMenu,
|
||||||
|
};
|
||||||
6
src/locales/en-US/slave/sgw/sgw-menu-en.ts
Normal file
6
src/locales/en-US/slave/sgw/sgw-menu-en.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
'menu.sgw.map': 'Maps',
|
||||||
|
'menu.sgw.trips': 'Trips',
|
||||||
|
'menu.manager.sgw.fishes': 'Fishes',
|
||||||
|
'menu.manager.sgw.zones': 'Zones',
|
||||||
|
};
|
||||||
5
src/locales/en-US/slave/spole/spole-en.ts
Normal file
5
src/locales/en-US/slave/spole/spole-en.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import spoleMenuEN from './spole-menu-en';
|
||||||
|
export default {
|
||||||
|
'spole.title': 'Spole',
|
||||||
|
...spoleMenuEN,
|
||||||
|
};
|
||||||
8
src/locales/en-US/slave/spole/spole-menu-en.ts
Normal file
8
src/locales/en-US/slave/spole/spole-menu-en.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
'menu.spole.monitoring': 'Monitoring',
|
||||||
|
'menu.spole.mira': 'Mira',
|
||||||
|
'menu.spole.miva': 'Miva',
|
||||||
|
'menu.spole.traffic-light': 'Traffic Light',
|
||||||
|
'menu.spole.street-light': 'Street Light',
|
||||||
|
'menu.spole.media': 'Media',
|
||||||
|
};
|
||||||
64
src/locales/vi-VN.ts
Normal file
64
src/locales/vi-VN.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import masterVI from './vi-VN/master/master-vi';
|
||||||
|
import gmsVI from './vi-VN/slave/gms/gms-vi';
|
||||||
|
import sgwVI from './vi-VN/slave/sgw/sgw-vi';
|
||||||
|
import spoleVI from './vi-VN/slave/spole/spole-vi';
|
||||||
|
export default {
|
||||||
|
'app.copyright.produced': 'Sản phẩm của Mobifone.',
|
||||||
|
'common.save': 'Lưu',
|
||||||
|
'common.cancel': 'Hủy',
|
||||||
|
'common.delete': 'Xóa',
|
||||||
|
'common.delete_confirm': 'Xác nhận xóa?',
|
||||||
|
'common.deleting': 'Đang xóa...',
|
||||||
|
'common.edit': 'Chỉnh sửa',
|
||||||
|
'common.add': 'Thêm',
|
||||||
|
'common.actions': 'Hoạt động',
|
||||||
|
'common.search': 'Tìm kiếm',
|
||||||
|
'common.filter': 'Bộ lọc',
|
||||||
|
'common.loading': 'Đang tải...',
|
||||||
|
'common.updating': 'Đang cập nhật...',
|
||||||
|
'common.confirm': 'Xác nhận',
|
||||||
|
'common.notification': 'Thông báo',
|
||||||
|
'common.back': 'Quay lại',
|
||||||
|
'common.next': 'Tiếp theo',
|
||||||
|
'common.logout': 'Đăng xuất',
|
||||||
|
'common.yes': 'Có',
|
||||||
|
'common.sure': 'Chắc chắn',
|
||||||
|
'common.no': 'Không',
|
||||||
|
'common.ok': 'OK',
|
||||||
|
'common.error': 'Lỗi',
|
||||||
|
'common.vietnamese': 'Tiếng Việt',
|
||||||
|
'common.english': 'Tiếng Anh',
|
||||||
|
'common.theme.light': 'Sáng',
|
||||||
|
'common.theme.dark': 'Tối',
|
||||||
|
'common.paginations.things': 'thiết bị',
|
||||||
|
'common.paginations.of': 'trên',
|
||||||
|
'common.name': 'Tên',
|
||||||
|
'common.name.required': 'Tên không được để trống',
|
||||||
|
'common.note': 'Ghi chú',
|
||||||
|
'common.image': 'Ảnh',
|
||||||
|
'common.type': 'Loại',
|
||||||
|
'common.type.placeholder': 'Chọn loại',
|
||||||
|
'common.status': 'Trạng thái',
|
||||||
|
'common.province': 'Tỉnh',
|
||||||
|
'common.description': 'Mô tả',
|
||||||
|
'common.description.required': 'Mô tả không được để trống',
|
||||||
|
'common.description.placeholder': 'Nhập mô tả',
|
||||||
|
'common.active': 'Kích hoạt',
|
||||||
|
'common.inactive': 'Vô hiệu hóa',
|
||||||
|
'common.created_at': 'Ngày tạo',
|
||||||
|
'common.updated_at': 'Ngày cập nhật',
|
||||||
|
'common.undefined': 'Chưa xác định',
|
||||||
|
'common.not_empty': 'Không được để trống!',
|
||||||
|
'common.level.normal': 'Bình thường',
|
||||||
|
'common.level.warning': 'Cảnh báo',
|
||||||
|
'common.level.critical': 'Nguy hiểm',
|
||||||
|
'common.level.sos': 'Khẩn cấp',
|
||||||
|
'common.unaccess': 'Bạn không có quyền truy cập trang này',
|
||||||
|
'common.notfound': 'Trang bạn tìm kiếm không tồn tại',
|
||||||
|
'common.internalserver': 'Xin lỗi, đã có lỗi xảy ra ở máy chủ',
|
||||||
|
'common.required_field': 'Trường này không được để trống',
|
||||||
|
...masterVI,
|
||||||
|
...sgwVI,
|
||||||
|
...gmsVI,
|
||||||
|
...spoleVI,
|
||||||
|
};
|
||||||
16
src/locales/vi-VN/master/master-alarm-vi.ts
Normal file
16
src/locales/vi-VN/master/master-alarm-vi.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export default {
|
||||||
|
'master.alarms.table.pagination': 'cảnh báo',
|
||||||
|
'master.alarms.severity': 'Mức độ',
|
||||||
|
'master.alarms.occurred_at': 'Thời gian xảy ra',
|
||||||
|
'master.alarms.confirmed': 'Đã xác nhận',
|
||||||
|
'master.alarms.confirm.description.required': 'Mô tả không được để trống',
|
||||||
|
'master.alarms.confirm.title': 'Xác nhận cảnh báo',
|
||||||
|
'master.alarms.confirm.success': 'Xác nhận cảnh báo thành công',
|
||||||
|
'master.alarms.confirm.fail': 'Xác nhận cảnh báo thất bại',
|
||||||
|
'master.alarms.unconfirm.title': 'Ngừng xác nhận',
|
||||||
|
'master.alarms.unconfirm.body': 'Bạn chắc chắn ngừng xác nhận cảnh báo này?',
|
||||||
|
'master.alarms.unconfirm.success': 'Ngừng xác nhận cảnh báo thành công',
|
||||||
|
'master.alarms.unconfirm.fail': 'Ngừng xác nhận cảnh báo thất bại',
|
||||||
|
'master.alarms.not_found': 'Cảnh báo đã hết hạn hoặc không tồn tại',
|
||||||
|
'master.alarms.filter_things': 'Lọc theo nhóm thiết bị',
|
||||||
|
};
|
||||||
15
src/locales/vi-VN/master/master-auth-vi.ts
Normal file
15
src/locales/vi-VN/master/master-auth-vi.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export default {
|
||||||
|
// Authentication
|
||||||
|
'master.auth.login.email': 'Email',
|
||||||
|
'master.auth.login.title': 'Đăng nhập',
|
||||||
|
'master.auth.login.subtitle': 'Hệ thống giám sát tàu cá',
|
||||||
|
'master.auth.login.description': 'Đăng nhập để tiếp tục giám sát tàu thuyền',
|
||||||
|
'master.auth.login.invalid': 'Tên người dùng hoặc mật khẩu không hợp lệ',
|
||||||
|
'master.auth.login.success': 'Đăng nhập thành công',
|
||||||
|
'master.auth.logout.title': 'Đăng xuất',
|
||||||
|
'master.auth.logout.confirm': 'Bạn có chắc chắn muốn đăng xuất?',
|
||||||
|
'master.auth.logout.success': 'Đăng xuất thành công',
|
||||||
|
'master.auth.validation.email': 'Email không được để trống!',
|
||||||
|
'master.auth.password': 'Mật khẩu',
|
||||||
|
'master.auth.validation.password': 'Mật khẩu không được để trống!',
|
||||||
|
};
|
||||||
22
src/locales/vi-VN/master/master-group-vi.ts
Normal file
22
src/locales/vi-VN/master/master-group-vi.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
export default {
|
||||||
|
'master.groups.root': 'Thêm gốc',
|
||||||
|
'master.groups.cannot-add-group':
|
||||||
|
'Không thể tạo đơn vị con khi gốc đã có thiết bị',
|
||||||
|
'master.groups.add': 'Tạo đơn vị cấp dưới',
|
||||||
|
'master.groups.delete.confirm': 'Bạn có chắc muốn xóa nhóm này không?',
|
||||||
|
'master.groups.code': 'Mã',
|
||||||
|
'master.groups.code.exists': 'Mã đã tồn tại',
|
||||||
|
'master.groups.short_name': 'Tên viết tắt',
|
||||||
|
'master.groups.short_name.exists': 'Tên viết tắt đã tồn tại',
|
||||||
|
'master.groups.update.success': 'Cập nhật đơn vị thành công',
|
||||||
|
'master.groups.update.failed': 'Cập nhật đơn vị thất bại, vui lòng thử lại!',
|
||||||
|
'master.groups.create.success': 'Tạo đơn vị thành công',
|
||||||
|
'master.groups.create.failed': 'Tạo đơn vị thất bại, vui lòng thử lại!',
|
||||||
|
|
||||||
|
'master.groups.delete.success': 'Xóa đơn vị thành công',
|
||||||
|
'master.groups.delete.failed': 'Xóa đơn vị thất bại',
|
||||||
|
'master.groups.delete.failed_internal':
|
||||||
|
'Đơn vị có chứa thiết bị hoặc người dùng, không thể xóa',
|
||||||
|
'master.groups.parent': 'Nhóm gốc',
|
||||||
|
'master.groups.treeselect.placeholder': 'Chọn khu vực',
|
||||||
|
};
|
||||||
51
src/locales/vi-VN/master/master-log-vi.ts
Normal file
51
src/locales/vi-VN/master/master-log-vi.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
export default {
|
||||||
|
'master.logs.search.yesterday': 'Hôm qua',
|
||||||
|
'master.logs.search.last_week': 'Tuần trước',
|
||||||
|
'master.logs.search.last_month': 'Tháng trước',
|
||||||
|
'master.logs.table.pagination': 'hoạt động',
|
||||||
|
'master.logs.things.create': 'Tạo mới thiết bị',
|
||||||
|
'master.logs.things.update': 'Sửa thông tin thiết bị',
|
||||||
|
'master.logs.things.remove': 'Xoá thiết bị',
|
||||||
|
'master.logs.things.share': 'Chia sẻ thiết bị',
|
||||||
|
'master.logs.things.unshare': 'Ngừng chia sẻ thiết bị',
|
||||||
|
'master.logs.things.update_key': 'Cập nhật mã khoá thiết bị',
|
||||||
|
'master.logs.users.create': 'Đăng kí người dùng mới',
|
||||||
|
'master.logs.users.update': 'Cập nhật thông tin người dùng',
|
||||||
|
'master.logs.users.remove': 'Xoá người dùng',
|
||||||
|
'master.logs.users.share': 'Đăng nhập',
|
||||||
|
'master.logs.groups.create': 'Tạo mới đơn vị',
|
||||||
|
'master.logs.groups.update': 'Sửa thông tin đơn vị',
|
||||||
|
'master.logs.groups.remove': 'Xoá đơn vị',
|
||||||
|
'master.logs.groups.assign_thing': 'Thêm thiết bị vào đơn vị',
|
||||||
|
'master.logs.groups.assign_user': 'Thêm người dùng vào đơn vị',
|
||||||
|
'master.logs.groups.unassign_thing': 'Xoá thiết bị khỏi đơn vị',
|
||||||
|
'master.logs.groups.unassign_user': 'Xoá người dùng khỏi đơn vị',
|
||||||
|
'master.logs.action.text': 'Thao tác',
|
||||||
|
'master.logs.email.text': 'Người dùng',
|
||||||
|
'master.logs.date.text': 'Ngày giờ',
|
||||||
|
'master.logs.things.alarm.confirm': 'Xác nhận cảnh báo',
|
||||||
|
'master.logs.things.alarm.unconfirm': 'Ngừng xác nhận cảnh báo',
|
||||||
|
'master.logs.users.login': 'Đăng nhập',
|
||||||
|
'master.logs.title': 'Nhật ký',
|
||||||
|
'master.logs.things.confirm': 'Xác nhận',
|
||||||
|
'master.logs.things.unconfirm': 'Ngừng xác nhận',
|
||||||
|
'master.logs.things': 'Thiết bị',
|
||||||
|
'master.logs.users': 'Người dùng',
|
||||||
|
'master.logs.groups': 'Đơn vị',
|
||||||
|
'master.logs.ships': 'Tàu',
|
||||||
|
'master.logs.ships.create': 'Tạo mới tàu',
|
||||||
|
'master.logs.ships.update': 'Sửa thông tin tàu',
|
||||||
|
'master.logs.ships.remove': 'Xoá tàu',
|
||||||
|
'master.logs.ships.assign_thing': 'Thêm thiết bị vào tàu',
|
||||||
|
'master.logs.ships.assign_user': 'Thêm người dùng vào tàu',
|
||||||
|
'master.logs.ships.unassign_thing': 'Xoá thiết bị khỏi tàu',
|
||||||
|
'master.logs.ships.unassign_user': 'Xoá người dùng khỏi tàu',
|
||||||
|
'master.logs.trips': 'Chuyến đi',
|
||||||
|
'master.logs.trips.create': 'Tạo mới chuyến đi',
|
||||||
|
'master.logs.trips.update': 'Sửa thông tin chuyến đi',
|
||||||
|
'master.logs.trips.remove': 'Xoá chuyến đi',
|
||||||
|
'master.logs.trips.approve': 'Duyệt chuyến đi',
|
||||||
|
'master.logs.trips.request_approve': 'Yêu cầu xác nhận chuyến đi',
|
||||||
|
'master.logs.trips.unassign_thing': 'Xoá thiết bị khỏi chuyến đi',
|
||||||
|
'master.logs.trips.unassign_user': 'Xoá người dùng khỏi chuyến đi',
|
||||||
|
};
|
||||||
10
src/locales/vi-VN/master/master-menu-vi.ts
Normal file
10
src/locales/vi-VN/master/master-menu-vi.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export default {
|
||||||
|
'menu.monitoring': 'Giám sát',
|
||||||
|
'menu.alarms': 'Cảnh báo',
|
||||||
|
'menu.manager': 'Quản lý',
|
||||||
|
'menu.manager.devices': 'Thiết bị',
|
||||||
|
'menu.manager.groups': 'Đơn vị',
|
||||||
|
'menu.manager.users': 'Người dùng',
|
||||||
|
'menu.manager.logs': 'Hoạt động',
|
||||||
|
'menu.profile': 'Thông tin cá nhân',
|
||||||
|
};
|
||||||
20
src/locales/vi-VN/master/master-profile-vi.ts
Normal file
20
src/locales/vi-VN/master/master-profile-vi.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
export default {
|
||||||
|
'master.profile.change-profile': 'Thông tin cơ bản',
|
||||||
|
'master.profile.change-password': 'Đổi mật khẩu',
|
||||||
|
'master.profile.two-factor-authentication': 'Xác thực 2 bước',
|
||||||
|
'master.profile.change-profile.title': 'Cập nhật thông tin cá nhân',
|
||||||
|
'master.profile.change-profile.full_name': 'Họ và tên',
|
||||||
|
'master.profile.change-profile.phone_number': 'Số điện thoại',
|
||||||
|
'master.profile.change-password.old_password': 'Mật khẩu cũ',
|
||||||
|
'master.profile.change-password.new_password': 'Mật khẩu mới',
|
||||||
|
'master.profile.change-password.password.strong':
|
||||||
|
'Mật khẩu phải chứa ít nhất 8 ký tự, bao gồm ít nhất một số và bao gồm cả chữ thường, chữ hoa và ký tự đặc biệt, ví dụ #, ?, !',
|
||||||
|
'master.profile.change-password.confirm_password': 'Xác nhận mật khẩu',
|
||||||
|
'master.profile.change-password.password_not_match':
|
||||||
|
'Mật khẩu xác nhận không khớp',
|
||||||
|
'master.profile.change-profile.update-success':
|
||||||
|
'Cập nhật thông tin thành công',
|
||||||
|
'master.profile.change-profile.update-fail': 'Cập nhật thông tin thất bại',
|
||||||
|
'master.profile.change-password.success': 'Đổi mật khẩu thành công',
|
||||||
|
'master.profile.change-password.fail': 'Đổi mật khẩu thất bại',
|
||||||
|
};
|
||||||
6
src/locales/vi-VN/master/master-thing-vi.ts
Normal file
6
src/locales/vi-VN/master/master-thing-vi.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
'master.thing.name': 'Tên',
|
||||||
|
'master.thing.external_id': 'External ID',
|
||||||
|
'master.thing.group': 'Nhóm',
|
||||||
|
'master.thing.address': 'Địa chỉ',
|
||||||
|
};
|
||||||
35
src/locales/vi-VN/master/master-user-vi.ts
Normal file
35
src/locales/vi-VN/master/master-user-vi.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
export default {
|
||||||
|
'master.users.title': 'Quản lý người dùng',
|
||||||
|
'master.users.table.pagination': 'người dùng',
|
||||||
|
'master.users.register': 'Thêm người dùng',
|
||||||
|
'master.users.register.title': 'Đăng kí người dùng mới',
|
||||||
|
'master.users.email': 'Email',
|
||||||
|
'master.users.email.tip': 'Email là duy nhất',
|
||||||
|
'master.users.email.required': 'Vui lòng nhập email',
|
||||||
|
'master.users.email.invalid': 'Email không hợp lệ',
|
||||||
|
'master.users.email.exists': 'Email đã sử dụng',
|
||||||
|
'master.users.role': 'Vai trò',
|
||||||
|
'master.users.role.placeholder': 'Vui lòng chọn vai trò',
|
||||||
|
'master.users.role.tip': 'Vai trò là duy nhất',
|
||||||
|
'master.users.password.required': 'Vui lòng nhập mật khẩu',
|
||||||
|
'master.users.password.minimum': 'Mật khẩu ít nhất 8 kí tự',
|
||||||
|
'master.users.password': 'Mật khẩu',
|
||||||
|
'master.users.password.placeholder': 'Nhập mật khẩu',
|
||||||
|
'master.users.confirmpassword.required': 'Vui lòng nhập lại mật khẩu',
|
||||||
|
'master.users.full_name': 'Tên đầy đủ',
|
||||||
|
'master.users.full_name.placeholder': 'Nhập tên đầy đủ',
|
||||||
|
'master.users.full_name.required': 'Vui lòng nhập tên đầy đủ',
|
||||||
|
'master.users.email.placeholder': 'Email',
|
||||||
|
'master.users.phone_number': 'Số điện thoại',
|
||||||
|
'master.users.phone_number.tip': 'Số điện thoại là duy nhất',
|
||||||
|
'master.users.phone_number.required': 'Vui lòng nhập số điện thoại',
|
||||||
|
'master.users.phone_number.notvalid': 'Số điện thoại không hợp lệ',
|
||||||
|
'master.users.groups': 'Đơn vị',
|
||||||
|
'master.users.groups.required': 'Vui lòng chọn đơn vị',
|
||||||
|
'master.users.role.sysadmin': 'Quản lý hệ thống',
|
||||||
|
'master.users.role.admin': 'Quản lý đơn vị',
|
||||||
|
'master.users.role.user': 'Giám sát đơn vị',
|
||||||
|
'master.users.role.sgw.end_user': 'Chủ tàu',
|
||||||
|
'master.users.create.error': 'Tạo người dùng lỗi',
|
||||||
|
'master.users.create.success': 'Tạo người dùng thành công',
|
||||||
|
};
|
||||||
18
src/locales/vi-VN/master/master-vi.ts
Normal file
18
src/locales/vi-VN/master/master-vi.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import masterAlarmVi from './master-alarm-vi';
|
||||||
|
import masterAuthVi from './master-auth-vi';
|
||||||
|
import masterGroupVi from './master-group-vi';
|
||||||
|
import masterSysLogVi from './master-log-vi';
|
||||||
|
import masterMenuVi from './master-menu-vi';
|
||||||
|
import masterProfileVi from './master-profile-vi';
|
||||||
|
import masterThingVi from './master-thing-vi';
|
||||||
|
import masterUserVi from './master-user-vi';
|
||||||
|
export default {
|
||||||
|
...masterAuthVi,
|
||||||
|
...masterMenuVi,
|
||||||
|
...masterProfileVi,
|
||||||
|
...masterAlarmVi,
|
||||||
|
...masterThingVi,
|
||||||
|
...masterSysLogVi,
|
||||||
|
...masterUserVi,
|
||||||
|
...masterGroupVi,
|
||||||
|
};
|
||||||
3
src/locales/vi-VN/slave/gms/gms-menu-vi.ts
Normal file
3
src/locales/vi-VN/slave/gms/gms-menu-vi.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
'menu.gms.monitor': 'Giám sát',
|
||||||
|
};
|
||||||
5
src/locales/vi-VN/slave/gms/gms-vi.ts
Normal file
5
src/locales/vi-VN/slave/gms/gms-vi.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import gmsMenuVi from './gms-menu-vi';
|
||||||
|
export default {
|
||||||
|
'gms.title': 'Quản lý giám sát thông minh',
|
||||||
|
...gmsMenuVi,
|
||||||
|
};
|
||||||
6
src/locales/vi-VN/slave/sgw/sgw-menu-vi.ts
Normal file
6
src/locales/vi-VN/slave/sgw/sgw-menu-vi.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
'menu.sgw.map': 'Bản đồ',
|
||||||
|
'menu.sgw.trips': 'Chuyến đi',
|
||||||
|
'menu.manager.sgw.fishes': 'Loài cá',
|
||||||
|
'menu.manager.sgw.zones': 'Khu vực',
|
||||||
|
};
|
||||||
6
src/locales/vi-VN/slave/sgw/sgw-vi.ts
Normal file
6
src/locales/vi-VN/slave/sgw/sgw-vi.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import sgwMenu from './sgw-menu-vi';
|
||||||
|
export default {
|
||||||
|
'sgw.title': 'Hệ thống giám sát tàu cá',
|
||||||
|
'sgw.ship': 'Tàu',
|
||||||
|
...sgwMenu,
|
||||||
|
};
|
||||||
8
src/locales/vi-VN/slave/spole/spole-menu-vi.ts
Normal file
8
src/locales/vi-VN/slave/spole/spole-menu-vi.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export default {
|
||||||
|
'menu.spole.monitoring': 'Giám sát',
|
||||||
|
'menu.spole.mira': 'Mira',
|
||||||
|
'menu.spole.miva': 'Miva',
|
||||||
|
'menu.spole.traffic-light': 'Đèn giao thông',
|
||||||
|
'menu.spole.street-light': 'Đèn',
|
||||||
|
'menu.spole.media': 'Media',
|
||||||
|
};
|
||||||
5
src/locales/vi-VN/slave/spole/spole-vi.ts
Normal file
5
src/locales/vi-VN/slave/spole/spole-vi.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import spoleMenuVI from './spole-menu-vi';
|
||||||
|
export default {
|
||||||
|
'spole.title': 'Quản lý giám sát thông minh',
|
||||||
|
...spoleMenuVI,
|
||||||
|
};
|
||||||
90
src/models/master/useGroups.ts
Normal file
90
src/models/master/useGroups.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { InitialStateResponse } from '@/app';
|
||||||
|
import { LIMIT_TREE_LEVEL } from '@/constants';
|
||||||
|
import { apiQueryGroups } from '@/services/master/GroupController';
|
||||||
|
import { useModel } from '@umijs/max';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
type UseGetGroupsResult = {
|
||||||
|
groups: MasterModel.GroupNode[] | null;
|
||||||
|
groupMap: GroupMap;
|
||||||
|
loading: boolean;
|
||||||
|
getGroups: () => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GroupMap = Record<string, { name: string; code?: string }>;
|
||||||
|
|
||||||
|
function buildGroupMap(
|
||||||
|
groups: MasterModel.GroupNode[],
|
||||||
|
map: GroupMap = {},
|
||||||
|
): GroupMap {
|
||||||
|
groups.forEach((g) => {
|
||||||
|
map[g.id] = {
|
||||||
|
name: g.name,
|
||||||
|
code: g.metadata?.code,
|
||||||
|
};
|
||||||
|
if (g.children?.length) {
|
||||||
|
buildGroupMap(g.children, map);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function useGetGroups(): UseGetGroupsResult {
|
||||||
|
const [groups, setGroups] = useState<MasterModel.GroupNode[] | null>(null);
|
||||||
|
const [groupMap, setGroupMap] = useState<GroupMap>({});
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const { initialState } = useModel('@@initialState') as {
|
||||||
|
initialState?: InitialStateResponse;
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentUserProfile = initialState?.currentUserProfile;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ FIX CACHE
|
||||||
|
* Khi user đổi (logout / login user khác)
|
||||||
|
* → clear groups cũ
|
||||||
|
*/
|
||||||
|
const prevUserIdRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentUserId = currentUserProfile?.id ?? null;
|
||||||
|
|
||||||
|
if (prevUserIdRef.current && prevUserIdRef.current !== currentUserId) {
|
||||||
|
// user đổi → clear cache
|
||||||
|
setGroups(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
prevUserIdRef.current = currentUserId;
|
||||||
|
}, [currentUserProfile?.id]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ✅ GIỮ NGUYÊN LOGIC CŨ
|
||||||
|
* chỉ thêm guard + dependency đúng
|
||||||
|
**/
|
||||||
|
const getGroups = useCallback(async (): Promise<void> => {
|
||||||
|
if (!currentUserProfile?.id) return;
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const tree = await apiQueryGroups({
|
||||||
|
level: LIMIT_TREE_LEVEL,
|
||||||
|
tree: true,
|
||||||
|
});
|
||||||
|
setGroups(tree?.groups || []);
|
||||||
|
setGroupMap(buildGroupMap(tree?.groups || []));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Fetch data failed', err);
|
||||||
|
setGroups([]);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}, [currentUserProfile]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
groups,
|
||||||
|
groupMap,
|
||||||
|
loading,
|
||||||
|
getGroups,
|
||||||
|
};
|
||||||
|
}
|
||||||
44
src/pages/Alarm/components/AlarmDescription.tsx
Normal file
44
src/pages/Alarm/components/AlarmDescription.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CommentOutlined,
|
||||||
|
UserOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { Space, Typography } from 'antd';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
interface AlarmDescriptionProps {
|
||||||
|
alarm: MasterModel.Alarm;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AlarmDescription = ({ alarm }: AlarmDescriptionProps) => {
|
||||||
|
if (!alarm?.confirmed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Space size="large" wrap>
|
||||||
|
<Space align="baseline" size={10}>
|
||||||
|
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||||
|
<Text type="secondary" style={{ fontSize: 15 }}>
|
||||||
|
{alarm.confirmed_time
|
||||||
|
? moment.unix(alarm.confirmed_time).format('YYYY-MM-DD HH:mm:ss')
|
||||||
|
: '-'}
|
||||||
|
</Text>
|
||||||
|
</Space>
|
||||||
|
<Space size={10}>
|
||||||
|
<UserOutlined style={{ color: '#1890ff' }} />
|
||||||
|
<Text style={{ fontSize: 15 }}>{alarm.confirmed_email}</Text>
|
||||||
|
</Space>
|
||||||
|
{alarm.confirmed_desc && (
|
||||||
|
<Space size={10}>
|
||||||
|
<CommentOutlined style={{ color: '#faad14' }} />
|
||||||
|
<Text style={{ fontSize: 15 }}>{alarm.confirmed_desc}</Text>
|
||||||
|
</Space>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlarmDescription;
|
||||||
152
src/pages/Alarm/components/AlarmFormConfirm.tsx
Normal file
152
src/pages/Alarm/components/AlarmFormConfirm.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { HTTPSTATUS } from '@/constants';
|
||||||
|
import { apiConfirmAlarm } from '@/services/master/AlarmController';
|
||||||
|
import { CheckOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
ModalForm,
|
||||||
|
ProForm,
|
||||||
|
ProFormInstance,
|
||||||
|
ProFormText,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { FormattedMessage, useIntl } from '@umijs/max';
|
||||||
|
import { Button, Flex } from 'antd';
|
||||||
|
import { MessageInstance } from 'antd/es/message/interface';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
|
||||||
|
type AlarmForm = {
|
||||||
|
name: string;
|
||||||
|
time: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
type AlarmFormConfirmProps = {
|
||||||
|
alarm: MasterModel.Alarm;
|
||||||
|
message: MessageInstance;
|
||||||
|
onFinish?: (reload: boolean) => void;
|
||||||
|
};
|
||||||
|
const AlarmFormConfirm = ({
|
||||||
|
alarm,
|
||||||
|
message,
|
||||||
|
onFinish,
|
||||||
|
}: AlarmFormConfirmProps) => {
|
||||||
|
const [modalVisit, setModalVisit] = useState(false);
|
||||||
|
const formRef = useRef<ProFormInstance<AlarmForm>>();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalForm<AlarmForm>
|
||||||
|
open={modalVisit}
|
||||||
|
formRef={formRef}
|
||||||
|
title={
|
||||||
|
<Flex align="center" justify="center">
|
||||||
|
<FormattedMessage id="alarms.confirm.title" />
|
||||||
|
</Flex>
|
||||||
|
}
|
||||||
|
width="40%"
|
||||||
|
layout="horizontal"
|
||||||
|
modalProps={{
|
||||||
|
destroyOnHidden: true,
|
||||||
|
}}
|
||||||
|
onOpenChange={setModalVisit}
|
||||||
|
request={async () => {
|
||||||
|
return {
|
||||||
|
name: alarm.name ?? '',
|
||||||
|
time: moment.unix(alarm.time!).format('DD/MM/YYYY HH:mm:ss'),
|
||||||
|
description: alarm.confirmed_desc ?? '',
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const body: MasterModel.ConfirmAlarmRequest = {
|
||||||
|
id: alarm.id,
|
||||||
|
description: values.description,
|
||||||
|
thing_id: alarm.thing_id,
|
||||||
|
time: alarm.time,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const resp = await apiConfirmAlarm(body);
|
||||||
|
if (resp.status === HTTPSTATUS.HTTP_ACCEPTED) {
|
||||||
|
message.success({
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'alarms.confirm.success',
|
||||||
|
defaultMessage: 'Confirm alarm successfully',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
onFinish?.(true);
|
||||||
|
} else if (resp.status === HTTPSTATUS.HTTP_NOTFOUND) {
|
||||||
|
message.warning({
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'alarms.not_found',
|
||||||
|
defaultMessage: 'Alarm has expired or does not exist',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
onFinish?.(true);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to confirm alarm');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error when confirm alarm: ', error);
|
||||||
|
message.error({
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'alarms.confirm.fail',
|
||||||
|
defaultMessage: 'Confirm alarm failed',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
trigger={
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
variant="solid"
|
||||||
|
color="green"
|
||||||
|
icon={<CheckOutlined />}
|
||||||
|
onClick={() => setModalVisit(true)}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'common.confirm',
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ProForm.Group>
|
||||||
|
<ProFormText
|
||||||
|
name="name"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'common.name',
|
||||||
|
defaultMessage: 'Name',
|
||||||
|
})}
|
||||||
|
readonly={true}
|
||||||
|
/>
|
||||||
|
<ProFormText
|
||||||
|
name="time"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'alarms.occurred_at',
|
||||||
|
defaultMessage: 'When',
|
||||||
|
})}
|
||||||
|
readonly={true}
|
||||||
|
/>
|
||||||
|
</ProForm.Group>
|
||||||
|
<ProFormText
|
||||||
|
fieldProps={{
|
||||||
|
autoFocus: true,
|
||||||
|
}}
|
||||||
|
name="description"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'common.description',
|
||||||
|
defaultMessage: 'Description',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'alarms.confirm.description.required',
|
||||||
|
defaultMessage: 'The description is required',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ModalForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlarmFormConfirm;
|
||||||
332
src/pages/Alarm/index.tsx
Normal file
332
src/pages/Alarm/index.tsx
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import ThingsFilter from '@/components/shared/ThingFilterModal';
|
||||||
|
import { DATE_TIME_FORMAT, DEFAULT_PAGE_SIZE, HTTPSTATUS } from '@/constants';
|
||||||
|
import {
|
||||||
|
apiGetAlarms,
|
||||||
|
apiUnconfirmAlarm,
|
||||||
|
} from '@/services/master/AlarmController';
|
||||||
|
import {
|
||||||
|
CloseOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
FilterOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||||
|
import { FormattedMessage, useIntl } from '@umijs/max';
|
||||||
|
import { Button, Flex, message, Popconfirm, Progress, Tooltip } from 'antd';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
import AlarmDescription from './components/AlarmDescription';
|
||||||
|
import AlarmFormConfirm from './components/AlarmFormConfirm';
|
||||||
|
|
||||||
|
const AlarmPage = () => {
|
||||||
|
const tableRef = useRef<ActionType>();
|
||||||
|
const [selectedKey, setSelectedKey] = useState<React.Key>();
|
||||||
|
const [thingFilterVisible, setThingFilterVisible] = useState<boolean>(false);
|
||||||
|
const [thingFilterDatas, setThingFilterDatas] = useState<string[]>([]);
|
||||||
|
const intl = useIntl();
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
const columns: ProColumns<MasterModel.Alarm>[] = [
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'common.name',
|
||||||
|
defaultMessage: 'Name',
|
||||||
|
}),
|
||||||
|
dataIndex: 'name',
|
||||||
|
key: 'name',
|
||||||
|
copyable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.alarms.severity',
|
||||||
|
defaultMessage: 'Severity',
|
||||||
|
}),
|
||||||
|
dataIndex: 'level',
|
||||||
|
key: 'level',
|
||||||
|
responsive: ['lg', 'md', 'sm'],
|
||||||
|
filters: true,
|
||||||
|
onFilter: true,
|
||||||
|
valueType: 'select',
|
||||||
|
valueEnum: {
|
||||||
|
0: {
|
||||||
|
text: intl.formatMessage({
|
||||||
|
id: 'common.level.normal',
|
||||||
|
defaultMessage: 'Normal',
|
||||||
|
}),
|
||||||
|
status: 'Success',
|
||||||
|
},
|
||||||
|
1: {
|
||||||
|
text: intl.formatMessage({
|
||||||
|
id: 'common.level.warning',
|
||||||
|
defaultMessage: 'Warning',
|
||||||
|
}),
|
||||||
|
status: 'Warning',
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
text: intl.formatMessage({
|
||||||
|
id: 'common.level.critical',
|
||||||
|
defaultMessage: 'Critical',
|
||||||
|
}),
|
||||||
|
status: 'Error',
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
text: intl.formatMessage({
|
||||||
|
id: 'common.level.sos',
|
||||||
|
defaultMessage: 'SOS',
|
||||||
|
}),
|
||||||
|
status: 'Error',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'sgw.ship',
|
||||||
|
defaultMessage: 'Source',
|
||||||
|
}),
|
||||||
|
dataIndex: 'thing_name',
|
||||||
|
key: 'thing_name',
|
||||||
|
responsive: ['lg', 'md', 'sm'],
|
||||||
|
ellipsis: true,
|
||||||
|
copyable: true,
|
||||||
|
// render: (dom, row) => {
|
||||||
|
// return (
|
||||||
|
// <Paragraph copyable>
|
||||||
|
// <Link
|
||||||
|
// onClick={() => {
|
||||||
|
// const t = row?.thing_type;
|
||||||
|
// const path = `/devices/${row?.thing_id}/${t}`;
|
||||||
|
// history.push({
|
||||||
|
// pathname: path,
|
||||||
|
// });
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// {dom}
|
||||||
|
// </Link>
|
||||||
|
// </Paragraph>
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
// render: (_, item) => {
|
||||||
|
// return <Paragraph copyable>{item?.thing_name}</Paragraph>
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.alarms.occurred_at',
|
||||||
|
defaultMessage: 'Occured time',
|
||||||
|
}),
|
||||||
|
dataIndex: 'time',
|
||||||
|
key: 'time',
|
||||||
|
search: false,
|
||||||
|
render: (_, item) => {
|
||||||
|
return moment.unix(item.time!).format(DATE_TIME_FORMAT);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.alarms.confirmed',
|
||||||
|
defaultMessage: 'Confirmed',
|
||||||
|
}),
|
||||||
|
dataIndex: 'confirmed',
|
||||||
|
responsive: ['lg', 'md'],
|
||||||
|
align: 'center',
|
||||||
|
width: '10%',
|
||||||
|
search: false,
|
||||||
|
render: (_, item) => {
|
||||||
|
return item?.confirmed ? (
|
||||||
|
<Progress type="circle" percent={100} size={24} />
|
||||||
|
) : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ThingsFilter
|
||||||
|
isOpen={thingFilterVisible}
|
||||||
|
setIsOpen={setThingFilterVisible}
|
||||||
|
thingIds={thingFilterDatas}
|
||||||
|
onSubmit={(thingIds) => {
|
||||||
|
console.log('ThingIDs: ', thingIds);
|
||||||
|
setThingFilterDatas(thingIds);
|
||||||
|
tableRef.current?.reload();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{contextHolder}
|
||||||
|
<ProTable<MasterModel.Alarm>
|
||||||
|
actionRef={tableRef}
|
||||||
|
rowKey={(record) => `${record.thing_id}_${record.id}`}
|
||||||
|
columns={columns}
|
||||||
|
rowSelection={{
|
||||||
|
type: 'checkbox',
|
||||||
|
selectedRowKeys: selectedKey ? [selectedKey] : [],
|
||||||
|
onChange: (selectedKeys) => {
|
||||||
|
// Checkbox with max 1 row selection - toggle behavior
|
||||||
|
if (selectedKeys.length === 0) {
|
||||||
|
setSelectedKey(undefined);
|
||||||
|
} else {
|
||||||
|
// Get the key that's different from current selection (the newly clicked row)
|
||||||
|
const newKey = selectedKeys.find((key) => key !== selectedKey);
|
||||||
|
setSelectedKey(newKey ?? selectedKeys[0]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
size="large"
|
||||||
|
tableAlertRender={({ selectedRows }) => {
|
||||||
|
const alarm = selectedRows[0];
|
||||||
|
return <AlarmDescription alarm={alarm} />;
|
||||||
|
}}
|
||||||
|
options={{
|
||||||
|
search: false,
|
||||||
|
setting: false,
|
||||||
|
density: false,
|
||||||
|
reload: true,
|
||||||
|
}}
|
||||||
|
toolbar={{
|
||||||
|
actions: [
|
||||||
|
<Tooltip
|
||||||
|
title={intl.formatMessage({ id: 'master.alarms.filter_things' })}
|
||||||
|
key="thing-filter-tooltip"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color={thingFilterDatas.length > 0 ? 'red' : 'default'}
|
||||||
|
icon={<FilterOutlined />}
|
||||||
|
variant="text"
|
||||||
|
onClick={() => setThingFilterVisible(true)}
|
||||||
|
/>
|
||||||
|
</Tooltip>,
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
tableAlertOptionRender={({ selectedRows, onCleanSelected }) => {
|
||||||
|
const alarm = selectedRows[0];
|
||||||
|
return (
|
||||||
|
<Flex gap={10}>
|
||||||
|
{alarm?.confirmed || false ? (
|
||||||
|
<Popconfirm
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'master.alarms.unconfirm.body',
|
||||||
|
defaultMessage:
|
||||||
|
'Are you sure you want to unconfirm this alarm?',
|
||||||
|
})}
|
||||||
|
okText={intl.formatMessage({
|
||||||
|
id: 'common.yes',
|
||||||
|
defaultMessage: 'Yes',
|
||||||
|
})}
|
||||||
|
cancelText={intl.formatMessage({
|
||||||
|
id: 'common.no',
|
||||||
|
defaultMessage: 'No',
|
||||||
|
})}
|
||||||
|
onConfirm={async () => {
|
||||||
|
const body: MasterModel.ConfirmAlarmRequest = {
|
||||||
|
id: alarm.id,
|
||||||
|
thing_id: alarm.thing_id,
|
||||||
|
time: alarm.time,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const resp = await apiUnconfirmAlarm(body);
|
||||||
|
if (resp.status === HTTPSTATUS.HTTP_SUCCESS) {
|
||||||
|
message.success({
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'master.alarms.unconfirm.success',
|
||||||
|
defaultMessage: 'Confirm alarm successfully',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} else if (resp.status === HTTPSTATUS.HTTP_NOTFOUND) {
|
||||||
|
message.warning({
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'master.alarms.not_found',
|
||||||
|
defaultMessage:
|
||||||
|
'Alarm has expired or does not exist',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
tableRef.current?.reload();
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to confirm alarm');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error when unconfirm alarm: ', error);
|
||||||
|
message.error({
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'master.alarms.unconfirm.fail',
|
||||||
|
defaultMessage: 'Unconfirm alarm failed',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Button danger icon={<DeleteOutlined />} size="small">
|
||||||
|
<FormattedMessage id="master.alarms.unconfirm.title" />
|
||||||
|
</Button>
|
||||||
|
</Popconfirm>
|
||||||
|
) : (
|
||||||
|
<AlarmFormConfirm
|
||||||
|
alarm={alarm}
|
||||||
|
message={messageApi}
|
||||||
|
onFinish={(isReload) => {
|
||||||
|
if (isReload && tableRef.current) {
|
||||||
|
tableRef.current.reload();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
icon={<CloseOutlined />}
|
||||||
|
onClick={onCleanSelected}
|
||||||
|
danger
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
pagination={{
|
||||||
|
defaultPageSize: DEFAULT_PAGE_SIZE * 2,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: ['5', '10', '15', '20'],
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`${range[0]}-${range[1]}
|
||||||
|
${intl.formatMessage({
|
||||||
|
id: 'common.paginations.of',
|
||||||
|
defaultMessage: 'of',
|
||||||
|
})}
|
||||||
|
${total} ${intl.formatMessage({
|
||||||
|
id: 'master.alarms.table.pagination',
|
||||||
|
defaultMessage: 'alarms',
|
||||||
|
})}`,
|
||||||
|
}}
|
||||||
|
request={async (params) => {
|
||||||
|
const { current, pageSize, name, level, thing_name } = params;
|
||||||
|
const offset = current === 1 ? 0 : (current! - 1) * pageSize!;
|
||||||
|
const body: MasterModel.SearchAlarmPaginationBody = {
|
||||||
|
name: name,
|
||||||
|
order: 'name',
|
||||||
|
limit: pageSize,
|
||||||
|
offset: offset,
|
||||||
|
level: level as number | undefined,
|
||||||
|
thing_name: thing_name,
|
||||||
|
dir: 'desc',
|
||||||
|
};
|
||||||
|
if (thingFilterDatas.length > 0) {
|
||||||
|
body.thing_id = Array.isArray(thingFilterDatas)
|
||||||
|
? thingFilterDatas.join(',')
|
||||||
|
: thingFilterDatas;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiGetAlarms(body);
|
||||||
|
return {
|
||||||
|
data: res.alarms,
|
||||||
|
total: res.total,
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></ProTable>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AlarmPage;
|
||||||
212
src/pages/Auth/index.tsx
Normal file
212
src/pages/Auth/index.tsx
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import Footer from '@/components/Footer';
|
||||||
|
import LangSwitches from '@/components/Lang/LanguageSwitcherAuth';
|
||||||
|
import ThemeSwitcherAuth from '@/components/Theme/ThemeSwitcherAuth';
|
||||||
|
import { ROUTER_HOME } from '@/constants/routes';
|
||||||
|
import { apiLogin, apiQueryProfile } from '@/services/master/AuthController';
|
||||||
|
import { parseJwt } from '@/utils/jwt';
|
||||||
|
import { getLogoImage } from '@/utils/logo';
|
||||||
|
import { getBrowserId, getToken, removeToken, setToken } from '@/utils/storage';
|
||||||
|
import { LockOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
|
import { LoginFormPage, ProFormText } from '@ant-design/pro-components';
|
||||||
|
import { history, useIntl, useModel } from '@umijs/max';
|
||||||
|
import { Image, theme } from 'antd';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
|
import mobifontLogo from '../../../public/mobifont-logo.png';
|
||||||
|
const LoginPage = () => {
|
||||||
|
const { token } = theme.useToken();
|
||||||
|
const urlParams = new URL(window.location.href).searchParams;
|
||||||
|
const redirect = urlParams.get('redirect');
|
||||||
|
const intl = useIntl();
|
||||||
|
const { setInitialState } = useModel('@@initialState');
|
||||||
|
const getDomainTitle = () => {
|
||||||
|
switch (process.env.DOMAIN_ENV) {
|
||||||
|
case 'gms':
|
||||||
|
return 'gms.title';
|
||||||
|
case 'sgw':
|
||||||
|
return 'sgw.title';
|
||||||
|
case 'spole':
|
||||||
|
return 'spole.title';
|
||||||
|
default:
|
||||||
|
return 'Smatec Master';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkLogin = async () => {
|
||||||
|
const token = getToken();
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsed = parseJwt(token);
|
||||||
|
const { exp } = parsed;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const oneHour = 60 * 60;
|
||||||
|
if (exp - now < oneHour) {
|
||||||
|
removeToken();
|
||||||
|
} else {
|
||||||
|
const userInfo = await apiQueryProfile();
|
||||||
|
if (userInfo) {
|
||||||
|
flushSync(() => {
|
||||||
|
setInitialState((s: any) => ({
|
||||||
|
...s,
|
||||||
|
currentUserProfile: userInfo,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (redirect) {
|
||||||
|
history.push(redirect);
|
||||||
|
} else {
|
||||||
|
history.push(ROUTER_HOME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkLogin();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogin = async (values: MasterModel.LoginRequestBody) => {
|
||||||
|
try {
|
||||||
|
const { email, password } = values;
|
||||||
|
const resp = await apiLogin({
|
||||||
|
guid: getBrowserId(),
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
});
|
||||||
|
if (resp?.token) {
|
||||||
|
setToken(resp.token);
|
||||||
|
const userInfo = await apiQueryProfile();
|
||||||
|
if (userInfo) {
|
||||||
|
flushSync(() => {
|
||||||
|
setInitialState((s: any) => ({
|
||||||
|
...s,
|
||||||
|
currentUserProfile: userInfo,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (redirect) {
|
||||||
|
history.push(redirect);
|
||||||
|
} else {
|
||||||
|
history.push(ROUTER_HOME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'white',
|
||||||
|
height: '100vh',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<LoginFormPage
|
||||||
|
backgroundImageUrl="https://mdn.alipayobjects.com/huamei_gcee1x/afts/img/A*y0ZTS6WLwvgAAAAAAAAAAAAADml6AQ/fmt.webp"
|
||||||
|
logo={getLogoImage()}
|
||||||
|
backgroundVideoUrl="https://gw.alipayobjects.com/v/huamei_gcee1x/afts/video/jXRBRK_VAwoAAAAAAAAAAAAAK4eUAQBr"
|
||||||
|
title={
|
||||||
|
<span style={{ color: token.colorBgContainer }}>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: getDomainTitle(),
|
||||||
|
defaultMessage: 'Smatec',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
containerStyle={{
|
||||||
|
backgroundColor: 'rgba(0, 0, 0,0.65)',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
}}
|
||||||
|
subTitle={<Image preview={false} src={mobifontLogo} />}
|
||||||
|
submitter={{
|
||||||
|
searchConfig: {
|
||||||
|
submitText: intl.formatMessage({
|
||||||
|
id: 'master.auth.login.title',
|
||||||
|
defaultMessage: 'Đăng nhập',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onFinish={async (values: MasterModel.LoginRequestBody) =>
|
||||||
|
handleLogin(values)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<ProFormText
|
||||||
|
name="email"
|
||||||
|
fieldProps={{
|
||||||
|
autoComplete: 'email',
|
||||||
|
autoFocus: true,
|
||||||
|
size: 'large',
|
||||||
|
prefix: (
|
||||||
|
<UserOutlined
|
||||||
|
style={{
|
||||||
|
color: token.colorText,
|
||||||
|
}}
|
||||||
|
className={'prefixIcon'}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.auth.login.email',
|
||||||
|
defaultMessage: 'Email',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.auth.validation.email',
|
||||||
|
defaultMessage: 'Email không được để trống!',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<ProFormText.Password
|
||||||
|
name="password"
|
||||||
|
fieldProps={{
|
||||||
|
size: 'large',
|
||||||
|
autoComplete: 'current-password',
|
||||||
|
prefix: (
|
||||||
|
<LockOutlined
|
||||||
|
style={{
|
||||||
|
color: token.colorText,
|
||||||
|
}}
|
||||||
|
className={'prefixIcon'}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.auth.password',
|
||||||
|
defaultMessage: 'Mật khẩu',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.auth.validation.password',
|
||||||
|
defaultMessage: 'Mật khẩu không được để trống!',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</LoginFormPage>
|
||||||
|
<div className="absolute top-5 right-5 z-50 flex gap-4">
|
||||||
|
<ThemeSwitcherAuth />
|
||||||
|
<LangSwitches />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 99,
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default LoginPage;
|
||||||
22
src/pages/Exception/Internal/index.tsx
Normal file
22
src/pages/Exception/Internal/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ROUTER_HOME } from '@/constants/routes';
|
||||||
|
import { history, useIntl } from '@umijs/max';
|
||||||
|
import { Button, Result } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const InternalServerErrorPage: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="500"
|
||||||
|
title="500"
|
||||||
|
subTitle={intl.formatMessage({ id: 'common.internalserver' })}
|
||||||
|
extra={
|
||||||
|
<Button onClick={() => history.push(ROUTER_HOME)} type="primary">
|
||||||
|
{intl.formatMessage({ id: 'common.back' })}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InternalServerErrorPage;
|
||||||
22
src/pages/Exception/NotFound/index.tsx
Normal file
22
src/pages/Exception/NotFound/index.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { ROUTER_HOME } from '@/constants/routes';
|
||||||
|
import { history, useIntl } from '@umijs/max';
|
||||||
|
import { Button, Result } from 'antd';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
const NotFoundPage: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="404"
|
||||||
|
title="404"
|
||||||
|
subTitle={intl.formatMessage({ id: 'common.notfound' })}
|
||||||
|
extra={
|
||||||
|
<Button onClick={() => history.push(ROUTER_HOME)} type="primary">
|
||||||
|
{intl.formatMessage({ id: 'common.back' })}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NotFoundPage;
|
||||||
26
src/pages/Exception/UnAccess/index.tsx
Normal file
26
src/pages/Exception/UnAccess/index.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { ROUTER_HOME } from '@/constants/routes';
|
||||||
|
import { history, useIntl } from '@umijs/max';
|
||||||
|
import { Button, Result } from 'antd';
|
||||||
|
|
||||||
|
const UnAccessPage = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
return (
|
||||||
|
<Result
|
||||||
|
status="403"
|
||||||
|
title="403"
|
||||||
|
subTitle={intl.formatMessage({ id: 'common.unaccess' })}
|
||||||
|
extra={
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
onClick={() => {
|
||||||
|
history.push(ROUTER_HOME);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({ id: 'common.back' })}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UnAccessPage;
|
||||||
5
src/pages/Manager/Device/index.tsx
Normal file
5
src/pages/Manager/Device/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const ManagerDevicePage = () => {
|
||||||
|
return <div>ManagerDevicePage</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManagerDevicePage;
|
||||||
300
src/pages/Manager/Group/components/CreateOrUpdateGroup.tsx
Normal file
300
src/pages/Manager/Group/components/CreateOrUpdateGroup.tsx
Normal file
@@ -0,0 +1,300 @@
|
|||||||
|
import {
|
||||||
|
apiCreateGroup,
|
||||||
|
apiUpdateGroup,
|
||||||
|
} from '@/services/master/GroupController';
|
||||||
|
import {
|
||||||
|
ModalForm,
|
||||||
|
ProFormInstance,
|
||||||
|
ProFormText,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { FormattedMessage, useIntl, useModel } from '@umijs/max';
|
||||||
|
import { MessageInstance } from 'antd/es/message/interface';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
type CreateOrUpdateGroupProps = {
|
||||||
|
type: 'create-root' | 'create-child' | 'update';
|
||||||
|
isOpen?: boolean;
|
||||||
|
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
group?: MasterModel.GroupNode | null;
|
||||||
|
message: MessageInstance;
|
||||||
|
};
|
||||||
|
type HandleGroupForm = {
|
||||||
|
parent_id?: string;
|
||||||
|
parent_name?: string;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
short_name: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
const CreateOrUpdateGroup = ({
|
||||||
|
type,
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
group,
|
||||||
|
message,
|
||||||
|
}: CreateOrUpdateGroupProps) => {
|
||||||
|
const formRef = useRef<ProFormInstance<HandleGroupForm>>();
|
||||||
|
const intl = useIntl();
|
||||||
|
const { groups, getGroups } = useModel('master.useGroups');
|
||||||
|
|
||||||
|
// Sync form values when modal opens or group/type changes
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(
|
||||||
|
'useEffect trigger - isOpen:',
|
||||||
|
isOpen,
|
||||||
|
'group:',
|
||||||
|
group,
|
||||||
|
'type:',
|
||||||
|
type,
|
||||||
|
);
|
||||||
|
if (isOpen && formRef.current) {
|
||||||
|
if (type === 'update' && group) {
|
||||||
|
formRef.current.setFieldsValue({
|
||||||
|
parent_id: group?.id || '',
|
||||||
|
parent_name: '',
|
||||||
|
name: group?.name || '',
|
||||||
|
code: group?.metadata?.code || '',
|
||||||
|
short_name: group?.metadata?.short_name || '',
|
||||||
|
description: group?.description || '',
|
||||||
|
});
|
||||||
|
} else if (type === 'create-child' && group) {
|
||||||
|
formRef.current.setFieldsValue({
|
||||||
|
parent_id: group?.id,
|
||||||
|
parent_name: group?.name || '',
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
short_name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
} else if (type === 'create-root') {
|
||||||
|
formRef.current.setFieldsValue({
|
||||||
|
parent_id: '',
|
||||||
|
parent_name: '',
|
||||||
|
name: '',
|
||||||
|
code: '',
|
||||||
|
short_name: '',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isOpen, group, type]);
|
||||||
|
|
||||||
|
const handleOpenChange = (open: boolean) => {
|
||||||
|
setIsOpen(open);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ModalForm<HandleGroupForm>
|
||||||
|
open={isOpen}
|
||||||
|
onOpenChange={handleOpenChange}
|
||||||
|
layout="vertical"
|
||||||
|
formRef={formRef}
|
||||||
|
title={
|
||||||
|
group === undefined ? (
|
||||||
|
<FormattedMessage id="master.groups.root" />
|
||||||
|
) : (
|
||||||
|
<FormattedMessage id="master.groups.add" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onFinish={async (values) => {
|
||||||
|
const body: Partial<MasterModel.GroupBodyRequest> = {
|
||||||
|
name: values.name,
|
||||||
|
description: values.description,
|
||||||
|
metadata: {
|
||||||
|
code: values.code,
|
||||||
|
short_name: values.short_name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
switch (type) {
|
||||||
|
case 'update': {
|
||||||
|
body.id = group?.id;
|
||||||
|
try {
|
||||||
|
const response = await apiUpdateGroup(body);
|
||||||
|
if (response.id) {
|
||||||
|
message?.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.update.success',
|
||||||
|
defaultMessage: 'Updated successfully',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
getGroups();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error when update group: ', error);
|
||||||
|
message?.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.update.failed',
|
||||||
|
defaultMessage: 'Update failed, please try again!',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'create-child': {
|
||||||
|
body.parent_id = group?.id;
|
||||||
|
try {
|
||||||
|
const response = await apiCreateGroup(body);
|
||||||
|
if (response.id) {
|
||||||
|
message?.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.create.success',
|
||||||
|
defaultMessage: 'Created group successfully',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setIsOpen(false);
|
||||||
|
getGroups();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error when create group: ', error);
|
||||||
|
message?.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.create.failed',
|
||||||
|
defaultMessage: 'Create group failed, please try again!',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'create-root': {
|
||||||
|
try {
|
||||||
|
const response = await apiCreateGroup(body);
|
||||||
|
if (response.id) {
|
||||||
|
message?.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.create.success',
|
||||||
|
defaultMessage: 'Created group successfully',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setIsOpen(false);
|
||||||
|
getGroups();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error('Create group failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('Error when create group: ', error);
|
||||||
|
message?.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.create.failed',
|
||||||
|
defaultMessage: 'Create group failed, please try again!',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormText name="parent_id" hidden />
|
||||||
|
|
||||||
|
{type !== 'update' && (
|
||||||
|
<ProFormText
|
||||||
|
name="parent_name"
|
||||||
|
disabled
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.groups.parent',
|
||||||
|
defaultMessage: 'Parent',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ProFormText
|
||||||
|
name="name"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'common.name',
|
||||||
|
defaultMessage: 'Name',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="common.name.required"
|
||||||
|
defaultMessage="The name is required"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProFormText
|
||||||
|
name="code"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.groups.code',
|
||||||
|
defaultMessage: 'Code',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
{
|
||||||
|
validator: async (_, value) => {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
groups!.some(
|
||||||
|
(g) => g?.metadata?.code === value && type !== 'update',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.code.exists',
|
||||||
|
defaultMessage: 'The code already exists',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProFormText
|
||||||
|
name="short_name"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.groups.short_name',
|
||||||
|
defaultMessage: 'Short name',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{ required: true },
|
||||||
|
{
|
||||||
|
validator: async (_, value) => {
|
||||||
|
if (
|
||||||
|
value &&
|
||||||
|
groups!.some(
|
||||||
|
(g) => g?.metadata?.short_name === value && type !== 'update',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.short_name.exists',
|
||||||
|
defaultMessage: 'The short name already exists',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ProFormText
|
||||||
|
name="description"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'common.description',
|
||||||
|
defaultMessage: 'Description',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="common.description.required"
|
||||||
|
defaultMessage="The description is required"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</ModalForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateOrUpdateGroup;
|
||||||
451
src/pages/Manager/Group/index.tsx
Normal file
451
src/pages/Manager/Group/index.tsx
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
import IconFont from '@/components/IconFont';
|
||||||
|
import TreeGroup from '@/components/shared/TreeGroup';
|
||||||
|
import { HTTPSTATUS } from '@/constants';
|
||||||
|
import {
|
||||||
|
apiDeleteGroup,
|
||||||
|
apiUpdateGroup,
|
||||||
|
} from '@/services/master/GroupController';
|
||||||
|
import {
|
||||||
|
DeleteOutlined,
|
||||||
|
EditOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
PlusOutlined,
|
||||||
|
} from '@ant-design/icons';
|
||||||
|
import { ProCard, ProDescriptions } from '@ant-design/pro-components';
|
||||||
|
import { FormattedMessage, useIntl, useModel } from '@umijs/max';
|
||||||
|
import { Button, Grid, message, Modal } from 'antd';
|
||||||
|
import { Dropdown, MenuProps, Tooltip } from 'antd/lib';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import CreateOrUpdateGroup from './components/CreateOrUpdateGroup';
|
||||||
|
type MenuItem = Required<MenuProps>['items'][number];
|
||||||
|
const ManagerGroupPage = () => {
|
||||||
|
const { useBreakpoint } = Grid;
|
||||||
|
const { initialState } = useModel('@@initialState');
|
||||||
|
const { currentUserProfile } = initialState || {};
|
||||||
|
const intl = useIntl();
|
||||||
|
const screens = useBreakpoint();
|
||||||
|
const [selectedItem, setSelectedItem] = useState<
|
||||||
|
MasterModel.GroupNode | undefined
|
||||||
|
>();
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState<
|
||||||
|
MasterModel.GroupNode | undefined
|
||||||
|
>();
|
||||||
|
|
||||||
|
const { groups, getGroups } = useModel('master.useGroups');
|
||||||
|
const [handleChildModal, setHandleChildModal] = useState<boolean>(false);
|
||||||
|
const [type, setType] = useState<'create-root' | 'create-child' | 'update'>(
|
||||||
|
'create-root',
|
||||||
|
);
|
||||||
|
const [messageApi, contextHolder] = message.useMessage();
|
||||||
|
const [modalKey, setModalKey] = useState(0);
|
||||||
|
|
||||||
|
const findGroupById = (
|
||||||
|
groups: MasterModel.GroupNode[],
|
||||||
|
id: string,
|
||||||
|
): MasterModel.GroupNode | undefined => {
|
||||||
|
for (const group of groups) {
|
||||||
|
if (group.id === id) return group;
|
||||||
|
if (group.children) {
|
||||||
|
const found = findGroupById(group.children, id);
|
||||||
|
if (found) return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdate = async (group: Partial<MasterModel.GroupNode>) => {
|
||||||
|
const key = 'update_group';
|
||||||
|
const { name, description, parent_id, code, short_name } = group;
|
||||||
|
const editGroup = parent_id
|
||||||
|
? {
|
||||||
|
...selectedItem,
|
||||||
|
name: name,
|
||||||
|
parent_id: parent_id,
|
||||||
|
metadata: {
|
||||||
|
code: code,
|
||||||
|
short_name: short_name,
|
||||||
|
},
|
||||||
|
description: description,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...selectedItem,
|
||||||
|
name: name,
|
||||||
|
metadata: {
|
||||||
|
code: code,
|
||||||
|
short_name: short_name,
|
||||||
|
},
|
||||||
|
description: description,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
messageApi.open({
|
||||||
|
type: 'loading',
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'common.updating',
|
||||||
|
defaultMessage: 'updating...',
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
await apiUpdateGroup({ ...editGroup });
|
||||||
|
messageApi.open({
|
||||||
|
type: 'success',
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'master.groups.update.success',
|
||||||
|
defaultMessage: 'Updated successfully',
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
await getGroups();
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
messageApi.open({
|
||||||
|
type: 'error',
|
||||||
|
content: intl.formatMessage({
|
||||||
|
id: 'master.groups.update.failed',
|
||||||
|
defaultMessage: 'Updating failed, please try again!',
|
||||||
|
}),
|
||||||
|
key,
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const showDeleteConfirm = (group: MasterModel.GroupNode) => {
|
||||||
|
Modal.confirm({
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.groups.delete.confirm',
|
||||||
|
defaultMessage: 'Are you sure delete this group',
|
||||||
|
}),
|
||||||
|
content: selectedItem?.name,
|
||||||
|
icon: <ExclamationCircleOutlined />,
|
||||||
|
okText: intl.formatMessage({ id: 'common.yes', defaultMessage: 'Yes' }),
|
||||||
|
okType: 'danger',
|
||||||
|
cancelText: intl.formatMessage({ id: 'common.no', defaultMessage: 'No' }),
|
||||||
|
async onOk() {
|
||||||
|
try {
|
||||||
|
const resp = await apiDeleteGroup(group.id);
|
||||||
|
if (resp.status === HTTPSTATUS.HTTP_NOCONTENT) {
|
||||||
|
messageApi.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.delete.success',
|
||||||
|
defaultMessage: 'Deleted group successfully',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
await getGroups();
|
||||||
|
} else if (resp.status === HTTPSTATUS.HTTP_SERVERERROR) {
|
||||||
|
messageApi.warning(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.delete.failed_internal',
|
||||||
|
defaultMessage:
|
||||||
|
'The group contains devices or users and cannot be deleted',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw new Error('Delete group failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error when delete group ', error);
|
||||||
|
|
||||||
|
messageApi.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.groups.delete.failed',
|
||||||
|
defaultMessage: 'Delete group failed',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onItemClick = ({ key }: { key: string }) => {
|
||||||
|
const splitIndex = key.indexOf('-');
|
||||||
|
const action = key.substring(0, splitIndex);
|
||||||
|
const groupId = key.substring(splitIndex + 1);
|
||||||
|
|
||||||
|
const groupNode = findGroupById(groups || [], groupId);
|
||||||
|
console.log('GroupName', groupNode?.name);
|
||||||
|
|
||||||
|
setSelectedItem(groupNode);
|
||||||
|
setSelectedGroup(groupNode);
|
||||||
|
if (action === '1') {
|
||||||
|
setType('create-child');
|
||||||
|
} else if (action === '2') {
|
||||||
|
setType('update');
|
||||||
|
} else if (action === '3') {
|
||||||
|
showDeleteConfirm(groupNode!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment modalKey to force remount component
|
||||||
|
setModalKey((prev) => prev + 1);
|
||||||
|
setHandleChildModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{contextHolder}
|
||||||
|
|
||||||
|
<CreateOrUpdateGroup
|
||||||
|
type={type}
|
||||||
|
key={modalKey}
|
||||||
|
isOpen={handleChildModal}
|
||||||
|
setIsOpen={setHandleChildModal}
|
||||||
|
group={selectedGroup}
|
||||||
|
message={messageApi}
|
||||||
|
/>
|
||||||
|
<ProCard split={screens.md ? 'vertical' : 'horizontal'}>
|
||||||
|
<ProCard
|
||||||
|
colSpan={{ xs: 24, sm: 24, md: 10, lg: 10, xl: 10 }}
|
||||||
|
extra={[
|
||||||
|
currentUserProfile?.metadata?.user_type === 'sysadmin' && (
|
||||||
|
<Button
|
||||||
|
type="primary"
|
||||||
|
key="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setType('create-root');
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
setModalKey((prev) => prev + 1);
|
||||||
|
setHandleChildModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="master.groups.root"
|
||||||
|
defaultMessage="Add root"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TreeGroup
|
||||||
|
titleRender={(item) => {
|
||||||
|
const groupNode = findGroupById(groups || [], item.key as string);
|
||||||
|
const menus: MenuItem[] = [
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
groupNode?.metadata?.has_thing
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.groups.cannot-add-group',
|
||||||
|
defaultMessage:
|
||||||
|
'Cannot add child to a group that has things',
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'master.groups.add',
|
||||||
|
defaultMessage: 'Add child',
|
||||||
|
})}
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
key: `1-${groupNode?.id}`,
|
||||||
|
|
||||||
|
icon: <IconFont type="icon-leaf" />,
|
||||||
|
disabled: groupNode?.metadata?.has_thing === true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'common.edit',
|
||||||
|
defaultMessage: 'Update',
|
||||||
|
}),
|
||||||
|
key: `2-${groupNode?.id}`,
|
||||||
|
icon: <EditOutlined />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
groupNode?.metadata?.has_thing
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.groups.delete.failed_internal',
|
||||||
|
defaultMessage:
|
||||||
|
'The group contains devices or users and cannot be deleted',
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{intl.formatMessage({
|
||||||
|
id: 'common.delete',
|
||||||
|
defaultMessage: 'Delete',
|
||||||
|
})}
|
||||||
|
</Tooltip>
|
||||||
|
),
|
||||||
|
key: `3-${groupNode?.id}`,
|
||||||
|
icon: <DeleteOutlined />,
|
||||||
|
disabled: groupNode?.metadata?.has_thing === true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
menu={{
|
||||||
|
items: menus,
|
||||||
|
onClick: onItemClick,
|
||||||
|
}}
|
||||||
|
trigger={['contextMenu']}
|
||||||
|
placement="bottomRight"
|
||||||
|
arrow
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{typeof item.title === 'function'
|
||||||
|
? item.title(item)
|
||||||
|
: item.title}
|
||||||
|
</span>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
multiple={false}
|
||||||
|
onSelected={(value: string | string[] | null) => {
|
||||||
|
if (!groups) {
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Find the selected group from model
|
||||||
|
if (value && !Array.isArray(value)) {
|
||||||
|
const found = findGroupById(groups, value);
|
||||||
|
setSelectedItem(found);
|
||||||
|
} else {
|
||||||
|
setSelectedItem(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ProCard>
|
||||||
|
<ProCard colSpan={{ xs: 24, sm: 24, md: 14, lg: 14, xl: 14 }}>
|
||||||
|
{selectedItem?.name && (
|
||||||
|
<>
|
||||||
|
<ProDescriptions<Partial<MasterModel.GroupNode>>
|
||||||
|
column={1}
|
||||||
|
bordered
|
||||||
|
title={selectedItem.name}
|
||||||
|
extra={[
|
||||||
|
<Tooltip
|
||||||
|
key="add"
|
||||||
|
title={
|
||||||
|
selectedItem?.metadata?.has_thing
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.groups.cannot-add-group',
|
||||||
|
defaultMessage:
|
||||||
|
'Cannot add child to a group that has things',
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
key="add"
|
||||||
|
type="primary"
|
||||||
|
icon={<PlusOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setType('create-child');
|
||||||
|
setModalKey((prev) => prev + 1);
|
||||||
|
setHandleChildModal(true);
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
selectedItem.metadata?.has_thing === true ? true : false
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="master.groups.add"
|
||||||
|
defaultMessage="Add Group"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</Tooltip>,
|
||||||
|
<Tooltip
|
||||||
|
key="add-fail"
|
||||||
|
title={
|
||||||
|
selectedItem?.metadata?.has_thing
|
||||||
|
? intl.formatMessage({
|
||||||
|
id: 'master.groups.delete.failed_internal',
|
||||||
|
defaultMessage:
|
||||||
|
'Cannot add child to a group that has things',
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
danger
|
||||||
|
key="delete"
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'master.groups.delete.confirm',
|
||||||
|
defaultMessage: 'Delete this group?',
|
||||||
|
})}
|
||||||
|
onClick={() => {
|
||||||
|
showDeleteConfirm(selectedItem);
|
||||||
|
}}
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
disabled={
|
||||||
|
selectedItem.metadata?.has_thing === true ? true : false
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="common.delete"
|
||||||
|
defaultMessage="Delete"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
,
|
||||||
|
</Tooltip>,
|
||||||
|
]}
|
||||||
|
dataSource={{
|
||||||
|
name: selectedItem.name,
|
||||||
|
code: selectedItem.metadata?.code || '',
|
||||||
|
short_name: selectedItem.metadata?.short_name || '',
|
||||||
|
description: selectedItem.description || '',
|
||||||
|
}}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'common.name',
|
||||||
|
defaultMessage: 'Name',
|
||||||
|
}),
|
||||||
|
dataIndex: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.groups.code',
|
||||||
|
defaultMessage: 'Code',
|
||||||
|
}),
|
||||||
|
dataIndex: 'code',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.groups.short_name',
|
||||||
|
defaultMessage: 'Short name',
|
||||||
|
}),
|
||||||
|
dataIndex: 'short_name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'common.description',
|
||||||
|
defaultMessage: 'Description',
|
||||||
|
}),
|
||||||
|
dataIndex: 'description',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
editable={{
|
||||||
|
onSave: async (_, record) => {
|
||||||
|
const updatedData: MasterModel.GroupNode = {
|
||||||
|
...selectedItem,
|
||||||
|
name: record.name || '',
|
||||||
|
description: record.description || '',
|
||||||
|
metadata: {
|
||||||
|
...selectedItem.metadata,
|
||||||
|
code: record.code,
|
||||||
|
short_name: record.short_name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const success = await handleUpdate(updatedData);
|
||||||
|
if (success) {
|
||||||
|
setSelectedItem(updatedData);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ProCard>
|
||||||
|
</ProCard>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ManagerGroupPage;
|
||||||
484
src/pages/Manager/Log/index.tsx
Normal file
484
src/pages/Manager/Log/index.tsx
Normal file
@@ -0,0 +1,484 @@
|
|||||||
|
import { DATE_TIME_FORMAT, DEFAULT_PAGE_SIZE } from '@/constants';
|
||||||
|
import { apiQueryLogs } from '@/services/master/LogController';
|
||||||
|
import { apiQueryUsers } from '@/services/master/UserController';
|
||||||
|
import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components';
|
||||||
|
import { useIntl } from '@umijs/max';
|
||||||
|
import { DatePicker } from 'antd/lib';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
|
const SystemLogs = () => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const tableRef = useRef<ActionType>();
|
||||||
|
const actions = [
|
||||||
|
//Alarm
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things.alarm.confirm',
|
||||||
|
defaultMessage: 'Alarm confirm',
|
||||||
|
}),
|
||||||
|
value: '0-0',
|
||||||
|
selectable: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 'things.alarm_confirm',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things.confirm',
|
||||||
|
defaultMessage: 'Confirm',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'things.alarm_unconfirm',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things.unconfirm',
|
||||||
|
defaultMessage: 'Unconfirm',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
//Things
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things',
|
||||||
|
defaultMessage: 'Things',
|
||||||
|
}),
|
||||||
|
value: '0-1',
|
||||||
|
selectable: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 'things.create',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things.create',
|
||||||
|
defaultMessage: 'Create new thing',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'things.update',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things.update',
|
||||||
|
defaultMessage: 'Update thing',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'things.remove',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things.remove',
|
||||||
|
defaultMessage: 'Remove thing',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'things.share',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things.share',
|
||||||
|
defaultMessage: 'Share thing',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'things.unshare',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things.unshare',
|
||||||
|
defaultMessage: 'Unshare thing',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'things.update_key',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.things.update_key',
|
||||||
|
defaultMessage: 'Update key thing',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Users
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.users',
|
||||||
|
defaultMessage: 'Users',
|
||||||
|
}),
|
||||||
|
value: '0-2',
|
||||||
|
selectable: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 'users.create',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.users.create',
|
||||||
|
defaultMessage: 'Register user',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'users.update',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.users.update',
|
||||||
|
defaultMessage: 'Update user',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'users.remove',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.users.remove',
|
||||||
|
defaultMessage: 'Remove user',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'users.login',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.users.login',
|
||||||
|
defaultMessage: 'User login',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Groups
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.groups',
|
||||||
|
defaultMessage: 'Groups',
|
||||||
|
}),
|
||||||
|
value: '0-3',
|
||||||
|
selectable: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 'group.create',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.groups.create',
|
||||||
|
defaultMessage: 'Create new group',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'group.update',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.groups.update',
|
||||||
|
defaultMessage: 'Update group',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'group.remove',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.groups.remove',
|
||||||
|
defaultMessage: 'Remove group',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'group.assign_thing',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.groups.assign_thing',
|
||||||
|
defaultMessage: 'Assign thing to group',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'group.assign_user',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.groups.assign_user',
|
||||||
|
defaultMessage: 'Assign user to group',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'group.unassign_thing',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.groups.unassign_thing',
|
||||||
|
defaultMessage: 'Remove thing from group',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'group.unassign_user',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.groups.unassign_user',
|
||||||
|
defaultMessage: 'Remove user from group',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Ships
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.ships',
|
||||||
|
defaultMessage: 'Ships',
|
||||||
|
}),
|
||||||
|
value: '0-4',
|
||||||
|
selectable: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 'ships.create',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.ships.create',
|
||||||
|
defaultMessage: 'Create new ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ships.update',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.ships.update',
|
||||||
|
defaultMessage: 'Update ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ships.remove',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.ships.remove',
|
||||||
|
defaultMessage: 'Remove ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ships.assign_thing',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.ships.assign_thing',
|
||||||
|
defaultMessage: 'Assign thing to ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ships.assign_user',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.ships.assign_user',
|
||||||
|
defaultMessage: 'Assign user to ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ships.unassign_thing',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.ships.unassign_thing',
|
||||||
|
defaultMessage: 'Remove thing from ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ships.unassign_user',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.ships.unassign_user',
|
||||||
|
defaultMessage: 'Remove user from ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Trips
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.trips',
|
||||||
|
defaultMessage: 'Trips',
|
||||||
|
}),
|
||||||
|
value: '0-5',
|
||||||
|
selectable: false,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
value: 'trips.create',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.trips.create',
|
||||||
|
defaultMessage: 'Create new ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'trips.update',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.trips.update',
|
||||||
|
defaultMessage: 'Update ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'trips.remove',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.trips.remove',
|
||||||
|
defaultMessage: 'Remove ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'trips.approve',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.trips.approve',
|
||||||
|
defaultMessage: 'Approve trip',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'trips.request_approve',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.trips.request_approve',
|
||||||
|
defaultMessage: 'Request approval for trip',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'trips.unassign_thing',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.trips.unassign_thing',
|
||||||
|
defaultMessage: 'Remove thing from ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'trips.unassign_user',
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.trips.unassign_user',
|
||||||
|
defaultMessage: 'Remove user from ship',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const queryUserSource = async (): Promise<MasterModel.ProfileResponse[]> => {
|
||||||
|
try {
|
||||||
|
const body: MasterModel.SearchUserPaginationBody = {
|
||||||
|
offset: 0,
|
||||||
|
limit: 100,
|
||||||
|
order: 'email',
|
||||||
|
dir: 'asc',
|
||||||
|
};
|
||||||
|
const resp = await apiQueryUsers(body);
|
||||||
|
return resp?.users ?? [];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const columns: ProColumns<MasterModel.Message>[] = [
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.date.text',
|
||||||
|
defaultMessage: 'Date',
|
||||||
|
}),
|
||||||
|
key: 'dateTimeRange',
|
||||||
|
dataIndex: 'time',
|
||||||
|
width: '20%',
|
||||||
|
valueType: 'dateTimeRange',
|
||||||
|
initialValue: [dayjs().add(-1, 'day'), dayjs()],
|
||||||
|
search: {
|
||||||
|
transform: (value) => ({ startTime: value[0], endTime: value[1] }),
|
||||||
|
},
|
||||||
|
render: (_, row) => {
|
||||||
|
return dayjs.unix(row?.time || 0).format(DATE_TIME_FORMAT);
|
||||||
|
},
|
||||||
|
renderFormItem: (_, config, form) => {
|
||||||
|
return (
|
||||||
|
<DatePicker.RangePicker
|
||||||
|
width="50%"
|
||||||
|
presets={[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.logs.search.yesterday',
|
||||||
|
defaultMessage: 'Yesterday',
|
||||||
|
}),
|
||||||
|
value: [dayjs().add(-1, 'day'), dayjs()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.logs.search.last_week',
|
||||||
|
defaultMessage: 'Last Week',
|
||||||
|
}),
|
||||||
|
value: [dayjs().add(-7, 'day'), dayjs()],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.logs.search.last_month',
|
||||||
|
defaultMessage: 'Last Month',
|
||||||
|
}),
|
||||||
|
value: [dayjs().add(-30, 'day'), dayjs()],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
onChange={(dates) => {
|
||||||
|
form.setFieldsValue({ dateTimeRange: dates });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.email.text',
|
||||||
|
defaultMessage: 'Email',
|
||||||
|
}),
|
||||||
|
dataIndex: 'publisher',
|
||||||
|
valueType: 'select',
|
||||||
|
fieldProps: { mode: 'multiple' },
|
||||||
|
width: '25%',
|
||||||
|
request: async () => {
|
||||||
|
const users = await queryUserSource();
|
||||||
|
return users.map((u) => ({
|
||||||
|
label: u.email,
|
||||||
|
value: u.email,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: intl.formatMessage({
|
||||||
|
id: 'master.logs.action.text',
|
||||||
|
defaultMessage: 'Action',
|
||||||
|
}),
|
||||||
|
dataIndex: 'subtopic',
|
||||||
|
valueType: 'treeSelect',
|
||||||
|
width: '25%',
|
||||||
|
fieldProps: {
|
||||||
|
treeCheckable: true,
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
request: async () => actions,
|
||||||
|
render: (_, item) => {
|
||||||
|
const childs = actions.flatMap((a) => a.children || []);
|
||||||
|
const action = childs.find((a) => a?.value === item.subtopic);
|
||||||
|
return action?.title ?? '...';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<ProTable<MasterModel.Message>
|
||||||
|
actionRef={tableRef}
|
||||||
|
columns={columns}
|
||||||
|
rowKey={(item) =>
|
||||||
|
`${item.subtopic}:${item.time}:${item.publisher}:${item.name}`
|
||||||
|
}
|
||||||
|
size="large"
|
||||||
|
options={{
|
||||||
|
search: false,
|
||||||
|
setting: false,
|
||||||
|
density: false,
|
||||||
|
reload: true,
|
||||||
|
}}
|
||||||
|
pagination={{
|
||||||
|
defaultPageSize: DEFAULT_PAGE_SIZE * 2,
|
||||||
|
showSizeChanger: true,
|
||||||
|
pageSizeOptions: ['10', '15', '20'],
|
||||||
|
showTotal: (total, range) =>
|
||||||
|
`${range[0]}-${range[1]}
|
||||||
|
${intl.formatMessage({
|
||||||
|
id: 'common.paginations.of',
|
||||||
|
defaultMessage: 'of',
|
||||||
|
})}
|
||||||
|
${total} ${intl.formatMessage({
|
||||||
|
id: 'master.logs.table.pagination',
|
||||||
|
defaultMessage: 'activities',
|
||||||
|
})}`,
|
||||||
|
}}
|
||||||
|
request={async (params) => {
|
||||||
|
try {
|
||||||
|
const { current, pageSize, subtopic, publisher, startTime, endTime } =
|
||||||
|
params;
|
||||||
|
const offset = current === 1 ? 0 : (current! - 1) * pageSize!;
|
||||||
|
const body: MasterModel.SearchLogPaginationBody = {
|
||||||
|
limit: pageSize,
|
||||||
|
offset: offset,
|
||||||
|
from: startTime
|
||||||
|
? typeof startTime === 'string'
|
||||||
|
? dayjs(startTime).unix()
|
||||||
|
: startTime.unix()
|
||||||
|
: undefined,
|
||||||
|
to: endTime
|
||||||
|
? typeof endTime === 'string'
|
||||||
|
? dayjs(endTime).unix()
|
||||||
|
: endTime.unix()
|
||||||
|
: undefined,
|
||||||
|
publisher: publisher?.length ? publisher.join(',') : undefined,
|
||||||
|
subtopic: subtopic?.length ? subtopic.join(',') : undefined,
|
||||||
|
};
|
||||||
|
const resp = await apiQueryLogs(body, 'user_logs');
|
||||||
|
return {
|
||||||
|
data: resp.messages || [],
|
||||||
|
success: true,
|
||||||
|
total: resp.total || 0,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error when get logs: ', error);
|
||||||
|
return {
|
||||||
|
data: [],
|
||||||
|
success: false,
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SystemLogs;
|
||||||
368
src/pages/Manager/User/components/CreateUser.tsx
Normal file
368
src/pages/Manager/User/components/CreateUser.tsx
Normal file
@@ -0,0 +1,368 @@
|
|||||||
|
import TreeSelectedGroup from '@/components/shared/TreeSelectedGroup';
|
||||||
|
import { HTTPSTATUS } from '@/constants';
|
||||||
|
import {
|
||||||
|
apiCreateUsers,
|
||||||
|
apiQueryUsers,
|
||||||
|
} from '@/services/master/UserController';
|
||||||
|
import { LockOutlined, PlusOutlined, UserOutlined } from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
ModalForm,
|
||||||
|
ProForm,
|
||||||
|
ProFormInstance,
|
||||||
|
ProFormSelect,
|
||||||
|
ProFormText,
|
||||||
|
} from '@ant-design/pro-components';
|
||||||
|
import { FormattedMessage, useIntl } from '@umijs/max';
|
||||||
|
import { Button } from 'antd';
|
||||||
|
import { MessageInstance } from 'antd/es/message/interface';
|
||||||
|
import { useRef, useState } from 'react';
|
||||||
|
type CreateUserProps = {
|
||||||
|
message: MessageInstance;
|
||||||
|
onSuccess?: (isSuccess: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CreateUserFormValues = {
|
||||||
|
full_name: string;
|
||||||
|
phone_number: string;
|
||||||
|
user_type: 'admin' | 'enduser' | 'sysadmin' | 'users';
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
group_id?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RoleSelectForm = () => {
|
||||||
|
const domain = process.env.DOMAIN_ENV;
|
||||||
|
const intl = useIntl();
|
||||||
|
switch (domain) {
|
||||||
|
case 'sgw':
|
||||||
|
return (
|
||||||
|
<ProFormSelect
|
||||||
|
name="user_type"
|
||||||
|
label={intl.formatMessage({ id: 'master.users.role' })}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({
|
||||||
|
id: 'master.users.role.sgw.end_user',
|
||||||
|
}),
|
||||||
|
value: 'endusers',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: 'master.users.role.user' }),
|
||||||
|
value: 'users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: 'master.users.role.admin' }),
|
||||||
|
value: 'admin',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.users.role.placeholder',
|
||||||
|
defaultMessage: 'Please select a role',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'spole':
|
||||||
|
return (
|
||||||
|
<ProFormSelect
|
||||||
|
name="user_type"
|
||||||
|
label={intl.formatMessage({ id: 'master.users.role' })}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: 'master.users.role.user' }),
|
||||||
|
value: 'users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: 'master.users.role.admin' }),
|
||||||
|
value: 'admin',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.users.role.placeholder',
|
||||||
|
defaultMessage: 'Please select a role',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'gms':
|
||||||
|
default:
|
||||||
|
return (
|
||||||
|
<ProFormSelect
|
||||||
|
name="user_type"
|
||||||
|
label={intl.formatMessage({ id: 'master.users.role' })}
|
||||||
|
options={[
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: 'master.users.role.user' }),
|
||||||
|
value: 'users',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: intl.formatMessage({ id: 'master.users.role.admin' }),
|
||||||
|
value: 'admin',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.users.role.placeholder',
|
||||||
|
defaultMessage: 'Please select a role',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const CreateUser = ({ message, onSuccess }: CreateUserProps) => {
|
||||||
|
const formRef = useRef<ProFormInstance<CreateUserFormValues>>();
|
||||||
|
const intl = useIntl();
|
||||||
|
const [group_id, setGroupId] = useState<string | string[] | null>(null);
|
||||||
|
const handleGroupSelect = (group: string | string[] | null) => {
|
||||||
|
setGroupId(group);
|
||||||
|
// Đồng bộ giá trị vào form để validation hoạt động
|
||||||
|
formRef.current?.setFieldsValue({
|
||||||
|
group_id: Array.isArray(group) ? group.join(',') : group || undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<ModalForm<CreateUserFormValues>
|
||||||
|
title={intl.formatMessage({
|
||||||
|
id: 'master.users.register.title',
|
||||||
|
defaultMessage: 'Register New User',
|
||||||
|
})}
|
||||||
|
formRef={formRef}
|
||||||
|
trigger={
|
||||||
|
<Button type="primary" key="primary" icon={<PlusOutlined />}>
|
||||||
|
<FormattedMessage
|
||||||
|
id="master.users.register"
|
||||||
|
defaultMessage="Register"
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
autoFocusFirstInput
|
||||||
|
onFinish={async (values: CreateUserFormValues) => {
|
||||||
|
const body: MasterModel.CreateUserBodyRequest = {
|
||||||
|
email: values.email,
|
||||||
|
password: values.password,
|
||||||
|
metadata: {
|
||||||
|
full_name: values.full_name,
|
||||||
|
phone_number: values.phone_number,
|
||||||
|
user_type: values.user_type || 'enduser',
|
||||||
|
group_id: values.group_id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const resp = await apiCreateUsers(body);
|
||||||
|
if (resp.status === HTTPSTATUS.HTTP_CREATED) {
|
||||||
|
message.success(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.create.success',
|
||||||
|
defaultMessage: 'Create user successfully',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
formRef.current?.resetFields();
|
||||||
|
onSuccess?.(true);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error('Create user failed');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.create.error',
|
||||||
|
defaultMessage: 'Failed to create user',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
onSuccess?.(false);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormText
|
||||||
|
name={'full_name'}
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.users.full_name',
|
||||||
|
defaultMessage: 'Full name',
|
||||||
|
})}
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.users.full_name.placeholder',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.users.full_name.required',
|
||||||
|
defaultMessage: 'The full name is required',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RoleSelectForm />
|
||||||
|
|
||||||
|
<ProFormText
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="master.users.email.required"
|
||||||
|
defaultMessage="The email is required"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'email',
|
||||||
|
message: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="master.users.email.invalid"
|
||||||
|
defaultMessage="Invalid email address"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: async (rule, value) => {
|
||||||
|
if (value) {
|
||||||
|
const query: MasterModel.SearchUserPaginationBody = {
|
||||||
|
offset: 0,
|
||||||
|
limit: 1,
|
||||||
|
order: 'email',
|
||||||
|
email: value,
|
||||||
|
dir: 'asc',
|
||||||
|
};
|
||||||
|
const resp = await apiQueryUsers(query);
|
||||||
|
const { total } = resp;
|
||||||
|
if (total && total > 0) {
|
||||||
|
return Promise.reject(
|
||||||
|
new Error(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.email.exists',
|
||||||
|
defaultMessage: 'The email is exists',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
name="email"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.users.email',
|
||||||
|
defaultMessage: 'Email',
|
||||||
|
})}
|
||||||
|
fieldProps={{
|
||||||
|
prefix: <UserOutlined />,
|
||||||
|
}}
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.users.email.placeholder',
|
||||||
|
defaultMessage: 'Email',
|
||||||
|
})}
|
||||||
|
validateTrigger={['onBlur']}
|
||||||
|
/>
|
||||||
|
<ProFormText
|
||||||
|
name="phone_number"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.users.phone_number',
|
||||||
|
defaultMessage: 'Phone number',
|
||||||
|
})}
|
||||||
|
fieldProps={{
|
||||||
|
autoComplete: 'phone-number',
|
||||||
|
}}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.users.phone_number.required',
|
||||||
|
defaultMessage: 'The phone number is required',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /((09|03|07|08|05)+([0-9]{8})\b)/g,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.users.phone_number.notvalid',
|
||||||
|
defaultMessage: 'Invalid phone number',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<ProFormText.Password
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: (
|
||||||
|
<FormattedMessage
|
||||||
|
id="master.users.password"
|
||||||
|
defaultMessage="Password is required"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*.]{6,16}$/g,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.profile.change-password.password.strong',
|
||||||
|
defaultMessage:
|
||||||
|
'A password contains at least 8 characters, including at least one number and includes both lower and uppercase letters and special characters, for example #, ?, !',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
validator: async (rule, value) => {
|
||||||
|
if (value && value.length < 8) {
|
||||||
|
return Promise.reject(
|
||||||
|
intl.formatMessage({
|
||||||
|
id: 'master.users.password.minimum',
|
||||||
|
defaultMessage: 'Minimum password length is 8',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
name="password"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.users.password',
|
||||||
|
defaultMessage: 'Password',
|
||||||
|
})}
|
||||||
|
fieldProps={{
|
||||||
|
prefix: <LockOutlined />,
|
||||||
|
autoComplete: 'current-password',
|
||||||
|
}}
|
||||||
|
placeholder={intl.formatMessage({
|
||||||
|
id: 'master.users.password.placeholder',
|
||||||
|
defaultMessage: 'Password',
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<ProForm.Item
|
||||||
|
name="group_id"
|
||||||
|
label={intl.formatMessage({
|
||||||
|
id: 'master.users.groups',
|
||||||
|
defaultMessage: 'Groups',
|
||||||
|
})}
|
||||||
|
rules={[
|
||||||
|
{
|
||||||
|
required: true,
|
||||||
|
message: intl.formatMessage({
|
||||||
|
id: 'master.users.groups.required',
|
||||||
|
defaultMessage: 'Please select groups!',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<TreeSelectedGroup groupIds={group_id} onSelected={handleGroupSelect} />
|
||||||
|
</ProForm.Item>
|
||||||
|
</ModalForm>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateUser;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user