Compare commits
10 Commits
8c77d91e00
...
318728b121
| Author | SHA1 | Date | |
|---|---|---|---|
| 318728b121 | |||
| 2a8d213c0f | |||
| 2589205efd | |||
| 4085393db1 | |||
| fc55965555 | |||
| 3c636bcaa2 | |||
| 16999ca583 | |||
| dfcba94cf5 | |||
| 43d6e9207a | |||
| 710da86521 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -16,6 +16,7 @@ deploy.js
|
|||||||
# misc
|
# misc
|
||||||
.env
|
.env
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.claude
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,9 @@ const config: ConfigType = defineConfig({
|
|||||||
* @description Can be used to store global data such as user info or global state. The global initial state is created at the very start of the Umi project.
|
* @description Can be used to store global data such as user info or global state. The global initial state is created at the very start of the Umi project.
|
||||||
* @doc https://umijs.org/docs/max/data-flow#%E5%85%A8%E5%B1%80%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81
|
* @doc https://umijs.org/docs/max/data-flow#%E5%85%A8%E5%B1%80%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81
|
||||||
*/
|
*/
|
||||||
initialState: {},
|
initialState: {
|
||||||
|
loading: '@/loading',
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* @name layout 插件
|
* @name layout 插件
|
||||||
* @doc https://umijs.org/docs/max/layout-menu
|
* @doc https://umijs.org/docs/max/layout-menu
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export default {
|
|||||||
'/api': {
|
'/api': {
|
||||||
// The address to proxy
|
// The address to proxy
|
||||||
// target: 'http://2333z061l7.51mypc.cn',
|
// target: 'http://2333z061l7.51mypc.cn',
|
||||||
target: 'http://172.20.177.121:8080',
|
// target: 'http://172.20.177.121:8080',
|
||||||
|
target: 'http://172.20.177.121:8082',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
pathRewrite: {
|
pathRewrite: {
|
||||||
'^/api': '',
|
'^/api': '',
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const roleRoutes = [
|
|||||||
meta: {
|
meta: {
|
||||||
permissions: ['rule:list'],
|
permissions: ['rule:list'],
|
||||||
},
|
},
|
||||||
wrappers: ['@/wrappers/noBindDataSource'],
|
wrappers: ['@/wrappers/CompanyExpire', '@/wrappers/noBindDataSource'],
|
||||||
component: './rules',
|
component: './rules',
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
@@ -166,6 +166,7 @@ export const roleRoutes = [
|
|||||||
meta: {
|
meta: {
|
||||||
permissions: ['data-source:list'],
|
permissions: ['data-source:list'],
|
||||||
},
|
},
|
||||||
|
wrappers: ['@/wrappers/CompanyExpire'],
|
||||||
component: './data-source/index',
|
component: './data-source/index',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -91,7 +91,20 @@ server {
|
|||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
proxy_pass http://127.0.0.1:8090;
|
proxy_pass http://127.0.0.1:8090;
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /expo-api/ {
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Connection '';
|
||||||
|
proxy_set_header Cache-Control "no-transform";
|
||||||
|
proxy_set_header X-Accel-Buffering "no";
|
||||||
|
proxy_buffering off;
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_pass http://127.0.0.1:4000;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
src/app.tsx
23
src/app.tsx
@@ -1,8 +1,9 @@
|
|||||||
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
|
import type { Settings as LayoutSettings } from '@ant-design/pro-components';
|
||||||
import { PageLoading } from '@ant-design/pro-components';
|
import { PageLoading } from '@ant-design/pro-components';
|
||||||
import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
|
import type { RequestConfig, RunTimeLayoutConfig } from '@umijs/max';
|
||||||
import { history, Link, useModel } from '@umijs/max';
|
import { history, Link, useAccess, useModel } from '@umijs/max';
|
||||||
import '@ant-design/v5-patch-for-react-19';
|
import '@ant-design/v5-patch-for-react-19';
|
||||||
|
import React from 'react';
|
||||||
import { App } from 'antd';
|
import { App } from 'antd';
|
||||||
import { AvatarDropdown, AvatarName, SelectLang } from '@/components';
|
import { AvatarDropdown, AvatarName, SelectLang } from '@/components';
|
||||||
import { currentUser as queryCurrentUser } from '@/services/api';
|
import { currentUser as queryCurrentUser } from '@/services/api';
|
||||||
@@ -17,7 +18,10 @@ import ServerErrorPage from './pages/500';
|
|||||||
import { errorConfig } from './requestErrorConfig';
|
import { errorConfig } from './requestErrorConfig';
|
||||||
import { getDeviceId } from './utils/fp';
|
import { getDeviceId } from './utils/fp';
|
||||||
import './utils/sentry';
|
import './utils/sentry';
|
||||||
|
import ExpireOrApp from './components/ExpireOrApp';
|
||||||
|
import CompanyExpireWarning from './components/CompanyExpireWarning';
|
||||||
import GlobalErrorBoundary from './components/GlobalErrorBoundary';
|
import GlobalErrorBoundary from './components/GlobalErrorBoundary';
|
||||||
|
import PermissionGuard from './components/PermissionGuard';
|
||||||
import SSEController from './components/SSEController';
|
import SSEController from './components/SSEController';
|
||||||
import VoiceCheck from './components/VoiceCheck';
|
import VoiceCheck from './components/VoiceCheck';
|
||||||
|
|
||||||
@@ -103,9 +107,14 @@ export function rootContainer(container: React.ReactNode) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function innerProvider(container: React.ReactNode) {
|
||||||
|
return <ExpireOrApp>{container}</ExpireOrApp>;
|
||||||
|
}
|
||||||
|
|
||||||
// ProLayout support API https://procomponents.ant.design/components/layout
|
// ProLayout support API https://procomponents.ant.design/components/layout
|
||||||
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
||||||
const { fetchUnread, initialized } = useModel('useMenuNoticeNum');
|
const { fetchUnread, initialized } = useModel('useMenuNoticeNum');
|
||||||
|
const { hasPerms } = useAccess() ?? {};
|
||||||
|
|
||||||
const isDark = initialState?.settings?.navTheme === 'realDark';
|
const isDark = initialState?.settings?.navTheme === 'realDark';
|
||||||
const currentUser = initialState?.currentUser;
|
const currentUser = initialState?.currentUser;
|
||||||
@@ -123,8 +132,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
|||||||
bgLayout: '#f8fafc',
|
bgLayout: '#f8fafc',
|
||||||
};
|
};
|
||||||
|
|
||||||
if (currentUser && !initialized) {
|
if (currentUser && !initialized && hasPerms?.('alert:list')) {
|
||||||
console.log('fetchUnread');
|
|
||||||
fetchUnread();
|
fetchUnread();
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -138,6 +146,7 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
|||||||
},
|
},
|
||||||
actionsRender: (_layoutProps) => {
|
actionsRender: (_layoutProps) => {
|
||||||
return [
|
return [
|
||||||
|
<CompanyExpireWarning key="CompanyExpireWarning" />,
|
||||||
<VoiceCheck key="VoiceCheck" />,
|
<VoiceCheck key="VoiceCheck" />,
|
||||||
needChangePass && <ChangePassTips key="ChangePassTips" />,
|
needChangePass && <ChangePassTips key="ChangePassTips" />,
|
||||||
// <ThemeSwitch
|
// <ThemeSwitch
|
||||||
@@ -196,9 +205,15 @@ export const layout: RunTimeLayoutConfig = ({ initialState }) => {
|
|||||||
// Add a loading state
|
// Add a loading state
|
||||||
childrenRender: (children) => {
|
childrenRender: (children) => {
|
||||||
if (initialState?.loading) return <PageLoading />;
|
if (initialState?.loading) return <PageLoading />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{currentUser && <SSEController />}
|
{currentUser && (
|
||||||
|
<>
|
||||||
|
<SSEController />
|
||||||
|
<PermissionGuard />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<GlobalErrorBoundary>{children}</GlobalErrorBoundary>
|
<GlobalErrorBoundary>{children}</GlobalErrorBoundary>
|
||||||
</>
|
</>
|
||||||
|
|||||||
41
src/components/CompanyExpireGuard/index.tsx
Normal file
41
src/components/CompanyExpireGuard/index.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { Button, Result, Space } from 'antd';
|
||||||
|
import { logoutAndToLoginPage } from '@/access';
|
||||||
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
|
import { $t } from '@/utils/i18n';
|
||||||
|
|
||||||
|
const fullscreenClass =
|
||||||
|
'fixed-all-center flex align-center justify-center bg-white';
|
||||||
|
const CompanyExpireGuard: React.FC<{ fullscreen?: boolean }> = ({
|
||||||
|
fullscreen = true,
|
||||||
|
}) => {
|
||||||
|
const { userInfo } = useUserInfo();
|
||||||
|
const isExpired = userInfo?.company?.validityStatus === 2;
|
||||||
|
|
||||||
|
if (!isExpired) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={fullscreen ? fullscreenClass : ''}>
|
||||||
|
<Result
|
||||||
|
status="warning"
|
||||||
|
title={$t('pages.company.expireTitle')}
|
||||||
|
subTitle={$t('pages.company.expireSubTitle')}
|
||||||
|
extra={
|
||||||
|
<Space>
|
||||||
|
<Button type="primary" onClick={() => window.location.reload()}>
|
||||||
|
{$t('pages.refresh')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
logoutAndToLoginPage();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$t('pages.logout')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompanyExpireGuard;
|
||||||
58
src/components/CompanyExpireWarning/index.tsx
Normal file
58
src/components/CompanyExpireWarning/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { ClockCircleOutlined } from '@ant-design/icons';
|
||||||
|
import { useBreakpoint } from '@ant-design/pro-components';
|
||||||
|
import { history as umiHistory, useAccess } from '@umijs/max';
|
||||||
|
import { Alert, Popover, Space } from 'antd';
|
||||||
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
|
import { $t } from '@/utils/i18n';
|
||||||
|
import { utcInstance as dayjs } from '@/utils/timeFormat';
|
||||||
|
|
||||||
|
const CompanyExpireWarning: React.FC = () => {
|
||||||
|
const { isGlobalCompany } = useAccess();
|
||||||
|
const { userInfo } = useUserInfo();
|
||||||
|
const screens = useBreakpoint();
|
||||||
|
const { validityEndTime, validityStatus } = userInfo?.company || {};
|
||||||
|
if (isGlobalCompany) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expireInDays = dayjs(`${validityEndTime}T23:59:59`).diff(
|
||||||
|
dayjs().utc(),
|
||||||
|
'day',
|
||||||
|
);
|
||||||
|
|
||||||
|
const tipsNode = (
|
||||||
|
<Alert
|
||||||
|
message={
|
||||||
|
validityStatus === 2
|
||||||
|
? $t('pages.company.expireTitle')
|
||||||
|
: expireInDays <= 0
|
||||||
|
? $t('pages.company.expireToday')
|
||||||
|
: `${$t('pages.company.expireInDays', { days: Math.abs(expireInDays) })}`
|
||||||
|
}
|
||||||
|
type={validityStatus === 2 ? 'error' : 'warning'}
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<Space align="center" onClick={() => umiHistory.push('/profile')}>
|
||||||
|
{['xs', 'sm'].includes(screens || '') ? (
|
||||||
|
<Popover
|
||||||
|
content={tipsNode}
|
||||||
|
styles={{
|
||||||
|
body: { padding: 0 },
|
||||||
|
}}
|
||||||
|
zIndex={999}
|
||||||
|
defaultOpen={true}
|
||||||
|
>
|
||||||
|
<ClockCircleOutlined
|
||||||
|
className="flex align-center"
|
||||||
|
style={{ color: '#f59e0b', fontSize: 20 }}
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
tipsNode
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default CompanyExpireWarning;
|
||||||
58
src/components/ExpireOrApp/index.tsx
Normal file
58
src/components/ExpireOrApp/index.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { PageLoading } from '@ant-design/pro-components';
|
||||||
|
import { history, useModel } from '@umijs/max';
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { LOGIN_URL } from '@/access';
|
||||||
|
import CompanyExpireGuard from '@/components/CompanyExpireGuard';
|
||||||
|
|
||||||
|
const ExpireOrApp: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const { initialState, loading } = useModel('@@initialState');
|
||||||
|
const [location, setLocation] = useState(history.location);
|
||||||
|
const company = initialState?.currentUser?.company;
|
||||||
|
const guardRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return history.listen((update) => {
|
||||||
|
setLocation(update.location);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isExpired = company?.special !== 0 && company?.validityStatus === 2;
|
||||||
|
const isLoginPage = location.pathname === LOGIN_URL;
|
||||||
|
const shouldBlock = isExpired && !isLoginPage;
|
||||||
|
|
||||||
|
const shouldBlockRef = useRef(shouldBlock);
|
||||||
|
shouldBlockRef.current = shouldBlock;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shouldBlock) return;
|
||||||
|
const el = guardRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
if (!shouldBlockRef.current) return;
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
for (const node of Array.from(mutation.removedNodes)) {
|
||||||
|
if (node === el) {
|
||||||
|
window.location.reload();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(document.body, { childList: true });
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [shouldBlock]);
|
||||||
|
|
||||||
|
if (loading) return <PageLoading />;
|
||||||
|
if (shouldBlock) {
|
||||||
|
return (
|
||||||
|
<div ref={guardRef} id="company-expire-guard-root">
|
||||||
|
<CompanyExpireGuard />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpireOrApp;
|
||||||
48
src/components/PermissionGuard.tsx
Normal file
48
src/components/PermissionGuard.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { history, useAccess } from '@umijs/max';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
|
import { currentUser as queryCurrentUser } from '@/services/api';
|
||||||
|
import { getRequiredPermissions } from '@/utils/routePermissions';
|
||||||
|
|
||||||
|
export default function PermissionGuard() {
|
||||||
|
const { setUserInfo } = useUserInfo();
|
||||||
|
const access = useAccess();
|
||||||
|
const [checkVersion, setCheckVersion] = useState(0);
|
||||||
|
const pathnameRef = useRef('');
|
||||||
|
const refreshing = useRef(false);
|
||||||
|
|
||||||
|
const onAuthRefresh = useCallback(async () => {
|
||||||
|
if (refreshing.current) return;
|
||||||
|
refreshing.current = true;
|
||||||
|
pathnameRef.current = history.location.pathname;
|
||||||
|
|
||||||
|
const res = (await queryCurrentUser()) as API.BaseResponse<API.CurrentUser>;
|
||||||
|
if (res?.code === 200 && res?.data?.data) {
|
||||||
|
await setUserInfo(res.data.data);
|
||||||
|
setCheckVersion((v) => v + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshing.current = false;
|
||||||
|
}, [setUserInfo]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (checkVersion === 0) return;
|
||||||
|
|
||||||
|
const required = getRequiredPermissions(pathnameRef.current);
|
||||||
|
if (required?.length) {
|
||||||
|
const hasAccess = required.some(
|
||||||
|
(perm) => access?.hasPerms?.(perm) ?? false,
|
||||||
|
);
|
||||||
|
if (!hasAccess) {
|
||||||
|
history.replace('/404', { from: pathnameRef.current });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [checkVersion, access]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('auth:refresh', onAuthRefresh);
|
||||||
|
return () => window.removeEventListener('auth:refresh', onAuthRefresh);
|
||||||
|
}, [onAuthRefresh]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useModel } from '@umijs/max';
|
import { useAccess, useModel } from '@umijs/max';
|
||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useMessageBuffer } from '@/hooks/useMessage';
|
import { useMessageBuffer } from '@/hooks/useMessage';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
@@ -11,11 +11,13 @@ const SSEController: React.FC = () => {
|
|||||||
const { disconnectSSE } = useModel('useSSE');
|
const { disconnectSSE } = useModel('useSSE');
|
||||||
const { startConnection } = useMessageBuffer();
|
const { startConnection } = useMessageBuffer();
|
||||||
const { userInfo } = useUserInfo();
|
const { userInfo } = useUserInfo();
|
||||||
|
const { hasPerms } = useAccess();
|
||||||
const userConfig = userInfo?.userConfig;
|
const userConfig = userInfo?.userConfig;
|
||||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
const { setUnreadCountWithSign } = useModel('useMenuNoticeNum');
|
const { setUnreadCountWithSign } = useModel('useMenuNoticeNum');
|
||||||
|
|
||||||
const refreshCount = async () => {
|
const refreshCount = async () => {
|
||||||
|
if (!hasPerms('alert:list')) return;
|
||||||
try {
|
try {
|
||||||
const { data } = await getAlertPages({
|
const { data } = await getAlertPages({
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
@@ -30,6 +32,7 @@ const SSEController: React.FC = () => {
|
|||||||
};
|
};
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
if (!hasPerms('alert:list')) return;
|
||||||
timerRef.current = setInterval(() => {
|
timerRef.current = setInterval(() => {
|
||||||
refreshCount();
|
refreshCount();
|
||||||
}, TIMER_INTERVAL);
|
}, TIMER_INTERVAL);
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { ChromeOutlined, SoundOutlined } from '@ant-design/icons';
|
import { ChromeOutlined, SoundOutlined } from '@ant-design/icons';
|
||||||
|
import { useBreakpoint } from '@ant-design/pro-components';
|
||||||
import { getLocale, Icon } from '@umijs/max';
|
import { getLocale, Icon } from '@umijs/max';
|
||||||
import { Alert, Divider, Image, Modal, Space, Tabs, Typography } from 'antd';
|
import {
|
||||||
|
Alert,
|
||||||
|
Divider,
|
||||||
|
Flex,
|
||||||
|
Image,
|
||||||
|
Modal,
|
||||||
|
Popover,
|
||||||
|
Space,
|
||||||
|
Tabs,
|
||||||
|
Typography,
|
||||||
|
} from 'antd';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import useRichI18n from '@/hooks/useRichI18n';
|
import useRichI18n from '@/hooks/useRichI18n';
|
||||||
import { checkAudioPermission } from '@/utils';
|
import { checkAudioPermission } from '@/utils';
|
||||||
@@ -8,9 +19,9 @@ import bus from '@/utils/eventBus';
|
|||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
|
|
||||||
const { Paragraph } = Typography;
|
const { Paragraph } = Typography;
|
||||||
const ChangePassTips: React.FC = () => {
|
const VoiceCheck: React.FC = () => {
|
||||||
const { richFormat } = useRichI18n();
|
const { richFormat } = useRichI18n();
|
||||||
|
const screens = useBreakpoint();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [hasAudioPermission, setHasAudioPermission] = useState(true);
|
const [hasAudioPermission, setHasAudioPermission] = useState(true);
|
||||||
|
|
||||||
@@ -69,25 +80,46 @@ const ChangePassTips: React.FC = () => {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
const tipsNode = (
|
||||||
|
<>
|
||||||
|
<Alert
|
||||||
|
message={$t('pages.voiceCheck.alertMessage')}
|
||||||
|
type="warning"
|
||||||
|
showIcon
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Space align="center" onClick={() => setVisible(true)}>
|
<Flex align="center" onClick={() => setVisible(true)}>
|
||||||
{!hasAudioPermission && (
|
{!hasAudioPermission &&
|
||||||
<>
|
(['xs', 'sm'].includes(screens || '') ? (
|
||||||
<Alert
|
<Popover
|
||||||
message={$t('pages.voiceCheck.alertMessage')}
|
content={tipsNode}
|
||||||
type="warning"
|
styles={{
|
||||||
showIcon
|
body: { padding: 0 },
|
||||||
/>
|
}}
|
||||||
<Icon
|
zIndex={999}
|
||||||
icon="local:silence"
|
defaultOpen={true}
|
||||||
height="20px"
|
>
|
||||||
fill="#f59e0b"
|
<Icon
|
||||||
className="flex align-center"
|
icon="local:silence"
|
||||||
/>
|
height="20px"
|
||||||
</>
|
fill="#f59e0b"
|
||||||
)}
|
className="flex align-center"
|
||||||
|
/>
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{tipsNode}
|
||||||
|
<Icon
|
||||||
|
icon="local:silence"
|
||||||
|
height="20px"
|
||||||
|
fill="#f59e0b"
|
||||||
|
className="flex align-center"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
))}
|
||||||
{/* {hasAudioPermission ? (
|
{/* {hasAudioPermission ? (
|
||||||
<Icon
|
<Icon
|
||||||
icon="local:voice"
|
icon="local:voice"
|
||||||
@@ -103,7 +135,7 @@ const ChangePassTips: React.FC = () => {
|
|||||||
className="flex align-center"
|
className="flex align-center"
|
||||||
/>
|
/>
|
||||||
)} */}
|
)} */}
|
||||||
</Space>
|
</Flex>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
@@ -154,4 +186,4 @@ const ChangePassTips: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
export default ChangePassTips;
|
export default VoiceCheck;
|
||||||
|
|||||||
@@ -135,6 +135,16 @@ ol {
|
|||||||
padding-inline: 15px;
|
padding-inline: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fixed-all-center {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.ant-table {
|
.ant-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useModel } from '@umijs/max';
|
import { useModel } from '@umijs/max';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
export default function useUserInfo() {
|
export default function useUserInfo() {
|
||||||
const { initialState, setInitialState } = useModel('@@initialState');
|
const { initialState, setInitialState } = useModel('@@initialState');
|
||||||
@@ -14,5 +15,24 @@ export default function useUserInfo() {
|
|||||||
setUserInfo(info);
|
setUserInfo(info);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { userInfo, refreshUserInfo, setUserInfo };
|
const userPlatform = useMemo(() => {
|
||||||
|
const company = userInfo?.company;
|
||||||
|
const originalPlatform = [
|
||||||
|
{
|
||||||
|
label: 'MT5',
|
||||||
|
value: 5,
|
||||||
|
isOpen: company?.mt5Open === 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'MT4',
|
||||||
|
value: 4,
|
||||||
|
isOpen: company?.mt4Open === 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// return originalPlatform.filter((item) => item.isOpen);
|
||||||
|
return originalPlatform;
|
||||||
|
}, [userInfo]);
|
||||||
|
|
||||||
|
return { userInfo, refreshUserInfo, setUserInfo, userPlatform };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ export default {
|
|||||||
'menu.rules': 'Rules',
|
'menu.rules': 'Rules',
|
||||||
'menu.rule': 'Rule',
|
'menu.rule': 'Rule',
|
||||||
'menu.rules.desc': 'Manage Rules',
|
'menu.rules.desc': 'Manage Rules',
|
||||||
|
'menu.rules.desc2':
|
||||||
|
'Add, edit, enable/disable, delete, and clone rules. View Rules will be selected automatically.',
|
||||||
|
'menu.rules.edit.badge': 'Write access',
|
||||||
|
'menu.rules.edit': 'Manage Rules',
|
||||||
|
'menu.rules.view.badge': 'Read only',
|
||||||
|
'menu.rules.view': 'View Rules',
|
||||||
|
'menu.rules.view.desc':
|
||||||
|
'Enter Rule Management and view rule settings. No create, edit, clone, or delete actions.',
|
||||||
'menu.rules.largeTradeLots': 'Large Trade (Lots)',
|
'menu.rules.largeTradeLots': 'Large Trade (Lots)',
|
||||||
'menu.rules.largeTradeLots.desc':
|
'menu.rules.largeTradeLots.desc':
|
||||||
'Monitor trades with lots exceeding threshold',
|
'Monitor trades with lots exceeding threshold',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export default {
|
|||||||
'common.loading': 'Loading...',
|
'common.loading': 'Loading...',
|
||||||
'common.content.noRules': 'No rules available',
|
'common.content.noRules': 'No rules available',
|
||||||
'common.content.noData': 'No data available',
|
'common.content.noData': 'No data available',
|
||||||
|
'table.platformAccess': 'Platform Quota',
|
||||||
'table.group': 'Group',
|
'table.group': 'Group',
|
||||||
'table.operatorRecords': 'Operator Records',
|
'table.operatorRecords': 'Operator Records',
|
||||||
'table.operatorAction': 'Operator Action',
|
'table.operatorAction': 'Operator Action',
|
||||||
@@ -46,6 +47,8 @@ export default {
|
|||||||
'table.contactEmail': 'Contact Email',
|
'table.contactEmail': 'Contact Email',
|
||||||
'table.dataSourceCount': 'Data Sources',
|
'table.dataSourceCount': 'Data Sources',
|
||||||
'table.userCount': 'Users',
|
'table.userCount': 'Users',
|
||||||
|
'table.validityEndTime': 'Expire At (UTC+0)',
|
||||||
|
'table.validityEndTime.tips': 'Company subscription expiry date ',
|
||||||
'table.createTime': 'Create Time',
|
'table.createTime': 'Create Time',
|
||||||
'table.name': 'Name',
|
'table.name': 'Name',
|
||||||
'table.ipAddress': 'IP Address',
|
'table.ipAddress': 'IP Address',
|
||||||
@@ -87,6 +90,13 @@ export default {
|
|||||||
'pages.totalAlertCount': 'Total Alerts',
|
'pages.totalAlertCount': 'Total Alerts',
|
||||||
'pages.totalCompanyCount': 'Total Companies',
|
'pages.totalCompanyCount': 'Total Companies',
|
||||||
'pages.activeCompanyCount': 'Active Companies',
|
'pages.activeCompanyCount': 'Active Companies',
|
||||||
|
'pages.company.expired': 'Expired',
|
||||||
|
'pages.company.permanent': 'Permanent',
|
||||||
|
'pages.company.expireTitle': 'Company Subscription Expired',
|
||||||
|
'pages.company.expireSubTitle':
|
||||||
|
"Your company's license has expired. All features are temporarily locked. Please contact your sales representative to renew.",
|
||||||
|
'pages.company.expireInDays': 'Subscription expires in {days} days',
|
||||||
|
'pages.company.expireToday': 'Subscription expiring soon; it expires today.',
|
||||||
'pages.totalDataSourceCount': 'Total Data Sources',
|
'pages.totalDataSourceCount': 'Total Data Sources',
|
||||||
'pages.activeDataSourceCount': 'Active Data Sources',
|
'pages.activeDataSourceCount': 'Active Data Sources',
|
||||||
'pages.mt4DataSourceCount': 'MT4 Data Sources',
|
'pages.mt4DataSourceCount': 'MT4 Data Sources',
|
||||||
@@ -329,6 +339,11 @@ export default {
|
|||||||
'form.role.placeholder2': 'System roles cannot modify permission levels',
|
'form.role.placeholder2': 'System roles cannot modify permission levels',
|
||||||
'form.role.badgeColor': 'Badge Color',
|
'form.role.badgeColor': 'Badge Color',
|
||||||
'form.role.menuIds': 'Bound Permissions (Menus)',
|
'form.role.menuIds': 'Bound Permissions (Menus)',
|
||||||
|
'form.company.validityEndTime': 'Expiry At (UTC+0)',
|
||||||
|
'form.company.validityEndTime.placeholder': 'Select expiry date',
|
||||||
|
'form.company.validityEndTime.tips': 'Blank for Permanent',
|
||||||
|
'form.role.permissionGuide': 'Permission Guide',
|
||||||
|
'form.role.otherPermissions': 'Other Menu Permissions',
|
||||||
|
|
||||||
'form.config.timezone': 'Timezone Settings',
|
'form.config.timezone': 'Timezone Settings',
|
||||||
'form.config.timezone.title': 'Default Timezone',
|
'form.config.timezone.title': 'Default Timezone',
|
||||||
@@ -601,6 +616,10 @@ export default {
|
|||||||
'Enter verified sender emails (comma separated)',
|
'Enter verified sender emails (comma separated)',
|
||||||
'pages.email.status.normal': 'Active',
|
'pages.email.status.normal': 'Active',
|
||||||
'pages.email.status.disabled': 'Inactive',
|
'pages.email.status.disabled': 'Inactive',
|
||||||
|
'pages.email.platformAccess': ' Platform Access',
|
||||||
|
'pages.email.platformAccessEnable': ' Access',
|
||||||
|
'pages.email.platformMax': 'Quota Limit',
|
||||||
|
'pages.email.platformAssigned': 'Assigned',
|
||||||
'pages.email.notification': 'Email Notification Config',
|
'pages.email.notification': 'Email Notification Config',
|
||||||
'pages.email.notificationEmail': 'Notification Email',
|
'pages.email.notificationEmail': 'Notification Email',
|
||||||
'pages.email.notificationEmail.tips':
|
'pages.email.notificationEmail.tips':
|
||||||
|
|||||||
@@ -7,6 +7,14 @@ export default {
|
|||||||
'menu.rules': '规则管理',
|
'menu.rules': '规则管理',
|
||||||
'menu.rule': '规则',
|
'menu.rule': '规则',
|
||||||
'menu.rules.desc': '管理规则',
|
'menu.rules.desc': '管理规则',
|
||||||
|
'menu.rules.desc2':
|
||||||
|
'新增、编辑、启停、删除、克隆规则;系统会自动包含“查看规则”。',
|
||||||
|
'menu.rules.edit.badge': '写权限',
|
||||||
|
'menu.rules.edit': '管理规则',
|
||||||
|
'menu.rules.view.badge': '只读',
|
||||||
|
'menu.rules.view': '查看规则',
|
||||||
|
'menu.rules.view.desc':
|
||||||
|
'进入规则管理并查看配置;不能新增、编辑、克隆、删除。',
|
||||||
'menu.rules.largeTradeLots': '大额交易(手数)',
|
'menu.rules.largeTradeLots': '大额交易(手数)',
|
||||||
'menu.rules.largeTradeLots.desc': '监控单笔开仓手数超过设定阈值的大额交易',
|
'menu.rules.largeTradeLots.desc': '监控单笔开仓手数超过设定阈值的大额交易',
|
||||||
'menu.rules.largeTradeUSD': '大额交易(USD)',
|
'menu.rules.largeTradeUSD': '大额交易(USD)',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export default {
|
|||||||
'common.loading': '加载中...',
|
'common.loading': '加载中...',
|
||||||
'common.content.noRules': '暂无规则',
|
'common.content.noRules': '暂无规则',
|
||||||
'common.content.noData': '暂无数据',
|
'common.content.noData': '暂无数据',
|
||||||
|
'table.platformAccess': '平台配额',
|
||||||
'table.group': '所属组',
|
'table.group': '所属组',
|
||||||
'table.operatorRecords': '操作记录',
|
'table.operatorRecords': '操作记录',
|
||||||
'table.operatorAction': '操作动作',
|
'table.operatorAction': '操作动作',
|
||||||
@@ -46,6 +47,8 @@ export default {
|
|||||||
'table.contactEmail': '联系邮箱',
|
'table.contactEmail': '联系邮箱',
|
||||||
'table.dataSourceCount': '数据源',
|
'table.dataSourceCount': '数据源',
|
||||||
'table.userCount': '用户数',
|
'table.userCount': '用户数',
|
||||||
|
'table.validityEndTime': '订阅到期时间(UTC+0)',
|
||||||
|
'table.validityEndTime.tips': '公司订阅服务到期时间',
|
||||||
'table.createTime': '创建时间',
|
'table.createTime': '创建时间',
|
||||||
'table.name': '名称',
|
'table.name': '名称',
|
||||||
'table.ipAddress': 'IP地址',
|
'table.ipAddress': 'IP地址',
|
||||||
@@ -87,6 +90,13 @@ export default {
|
|||||||
'pages.totalAlertCount': '总告警数',
|
'pages.totalAlertCount': '总告警数',
|
||||||
'pages.totalCompanyCount': '总公司数',
|
'pages.totalCompanyCount': '总公司数',
|
||||||
'pages.activeCompanyCount': '活跃公司数',
|
'pages.activeCompanyCount': '活跃公司数',
|
||||||
|
'pages.company.expired': '已过期',
|
||||||
|
'pages.company.permanent': '永久有效',
|
||||||
|
'pages.company.expireTitle': '公司订阅已过期',
|
||||||
|
'pages.company.expireSubTitle':
|
||||||
|
'您的公司在该系统的使用授权已到期,各项功能已暂时锁定。请联系商务代表进行续费操作以恢复访问。',
|
||||||
|
'pages.company.expireInDays': '订阅即将到期,剩余 {days}天',
|
||||||
|
'pages.company.expireToday': '订阅即将到期,今天到期',
|
||||||
'pages.totalDataSourceCount': '数据源总数',
|
'pages.totalDataSourceCount': '数据源总数',
|
||||||
'pages.activeDataSourceCount': '活跃数据源',
|
'pages.activeDataSourceCount': '活跃数据源',
|
||||||
'pages.mt4DataSourceCount': 'MT4 数据源',
|
'pages.mt4DataSourceCount': 'MT4 数据源',
|
||||||
@@ -321,6 +331,11 @@ export default {
|
|||||||
'form.role.placeholder2': '系统角色权限级别不可修改',
|
'form.role.placeholder2': '系统角色权限级别不可修改',
|
||||||
'form.role.badgeColor': '徽章颜色',
|
'form.role.badgeColor': '徽章颜色',
|
||||||
'form.role.menuIds': '绑定权限(菜单)',
|
'form.role.menuIds': '绑定权限(菜单)',
|
||||||
|
'form.company.validityEndTime': '到期时间(UTC+0)',
|
||||||
|
'form.company.validityEndTime.placeholder': '请选择到期时间',
|
||||||
|
'form.company.validityEndTime.tips': '留空为永久有效',
|
||||||
|
'form.role.permissionGuide': '权限怎么选',
|
||||||
|
'form.role.otherPermissions': '其他菜单权限',
|
||||||
|
|
||||||
'form.config.timezone': '时区设置',
|
'form.config.timezone': '时区设置',
|
||||||
'form.config.timezone.title': '默认时区',
|
'form.config.timezone.title': '默认时区',
|
||||||
@@ -567,6 +582,10 @@ export default {
|
|||||||
'pages.email.availableEmails.tips': '在服务商已验证的邮箱,多个用逗号分隔',
|
'pages.email.availableEmails.tips': '在服务商已验证的邮箱,多个用逗号分隔',
|
||||||
'pages.email.status.normal': '正常',
|
'pages.email.status.normal': '正常',
|
||||||
'pages.email.status.disabled': '停用',
|
'pages.email.status.disabled': '停用',
|
||||||
|
'pages.email.platformAccess': '平台接入权限',
|
||||||
|
'pages.email.platformAccessEnable': '接入',
|
||||||
|
'pages.email.platformMax': '最大数量上限',
|
||||||
|
'pages.email.platformAssigned': '已配',
|
||||||
'pages.email.notification': '邮件通知配置',
|
'pages.email.notification': '邮件通知配置',
|
||||||
'pages.email.notificationEmail': '通知邮箱',
|
'pages.email.notificationEmail': '通知邮箱',
|
||||||
'pages.email.notificationEmail.tips':
|
'pages.email.notificationEmail.tips':
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const maxAlertHideCount = 9999;
|
|||||||
|
|
||||||
const Account: React.FC = () => {
|
const Account: React.FC = () => {
|
||||||
const { isGlobalCompany } = useAccess();
|
const { isGlobalCompany } = useAccess();
|
||||||
const { userInfo } = useUserInfo();
|
const { userInfo, userPlatform } = useUserInfo();
|
||||||
const companyInfo = userInfo?.company;
|
const companyInfo = userInfo?.company;
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
@@ -122,14 +122,7 @@ const Account: React.FC = () => {
|
|||||||
label: $t('table.all'),
|
label: $t('table.all'),
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
// {
|
...userPlatform,
|
||||||
// label: 'MT4',
|
|
||||||
// value: '4',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
label: 'MT5',
|
|
||||||
value: '5',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const Alert: React.FC = () => {
|
|||||||
const [dataSourceList, setDataSourceList] = useState<
|
const [dataSourceList, setDataSourceList] = useState<
|
||||||
API.DataSourceListItem[]
|
API.DataSourceListItem[]
|
||||||
>([]);
|
>([]);
|
||||||
const { userInfo } = useUserInfo();
|
const { userInfo, userPlatform } = useUserInfo();
|
||||||
const userDataSourceList = userInfo?.userDataSourceList;
|
const userDataSourceList = userInfo?.userDataSourceList;
|
||||||
const companyInfo = userInfo?.company;
|
const companyInfo = userInfo?.company;
|
||||||
|
|
||||||
@@ -146,6 +146,8 @@ const Alert: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: $t('table.alertId'),
|
title: $t('table.alertId'),
|
||||||
dataIndex: 'id',
|
dataIndex: 'id',
|
||||||
|
width: 100,
|
||||||
|
fixed: 'left',
|
||||||
fieldProps: {
|
fieldProps: {
|
||||||
placeholder: $t('table.alertId'),
|
placeholder: $t('table.alertId'),
|
||||||
},
|
},
|
||||||
@@ -158,6 +160,7 @@ const Alert: React.FC = () => {
|
|||||||
dataIndex: 'companyId',
|
dataIndex: 'companyId',
|
||||||
valueType: 'select',
|
valueType: 'select',
|
||||||
sorter: true,
|
sorter: true,
|
||||||
|
width: 140,
|
||||||
fieldProps: {
|
fieldProps: {
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
popupMatchSelectWidth: false,
|
popupMatchSelectWidth: false,
|
||||||
@@ -182,6 +185,7 @@ const Alert: React.FC = () => {
|
|||||||
dataIndex: 'dataSourceId',
|
dataIndex: 'dataSourceId',
|
||||||
valueType: 'select',
|
valueType: 'select',
|
||||||
sorter: true,
|
sorter: true,
|
||||||
|
width: 160,
|
||||||
fieldProps: {
|
fieldProps: {
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
popupMatchSelectWidth: false,
|
popupMatchSelectWidth: false,
|
||||||
@@ -215,6 +219,7 @@ const Alert: React.FC = () => {
|
|||||||
dataIndex: 'ruleType',
|
dataIndex: 'ruleType',
|
||||||
valueType: 'select',
|
valueType: 'select',
|
||||||
sorter: true,
|
sorter: true,
|
||||||
|
width: 'auto',
|
||||||
fieldProps: {
|
fieldProps: {
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
popupMatchSelectWidth: false,
|
popupMatchSelectWidth: false,
|
||||||
@@ -234,6 +239,7 @@ const Alert: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: $t('pages.rules.ruleName'),
|
title: $t('pages.rules.ruleName'),
|
||||||
dataIndex: 'ruleName',
|
dataIndex: 'ruleName',
|
||||||
|
ellipsis: true,
|
||||||
// sorter: true,
|
// sorter: true,
|
||||||
fieldProps: {
|
fieldProps: {
|
||||||
placeholder: $t('pages.rules.ruleName'),
|
placeholder: $t('pages.rules.ruleName'),
|
||||||
@@ -251,14 +257,7 @@ const Alert: React.FC = () => {
|
|||||||
label: $t('table.all'),
|
label: $t('table.all'),
|
||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
// {
|
...userPlatform,
|
||||||
// label: 'MT4',
|
|
||||||
// value: 4,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
label: 'MT5',
|
|
||||||
value: 5,
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
render: (_, { platform }) => {
|
render: (_, { platform }) => {
|
||||||
@@ -300,16 +299,14 @@ const Alert: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: $t('table.trigger'),
|
title: $t('table.trigger'),
|
||||||
dataIndex: 'trigger',
|
dataIndex: 'trigger',
|
||||||
width: 'auto',
|
ellipsis: true,
|
||||||
className: 'text-bold',
|
className: 'text-bold',
|
||||||
fieldProps: {
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: $t('table.summary'),
|
title: $t('table.summary'),
|
||||||
dataIndex: 'summary',
|
dataIndex: 'summary',
|
||||||
|
ellipsis: true,
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -318,6 +315,7 @@ const Alert: React.FC = () => {
|
|||||||
valueType: 'dateTime',
|
valueType: 'dateTime',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
sorter: true,
|
sorter: true,
|
||||||
|
width: 170,
|
||||||
// renderText: (_, { triggerTime }) => {
|
// renderText: (_, { triggerTime }) => {
|
||||||
// return formatToUserTimezone(triggerTime);
|
// return formatToUserTimezone(triggerTime);
|
||||||
// },
|
// },
|
||||||
@@ -326,6 +324,7 @@ const Alert: React.FC = () => {
|
|||||||
title: $t('table.dataSourceTime'),
|
title: $t('table.dataSourceTime'),
|
||||||
dataIndex: 'dataSourceServerTime',
|
dataIndex: 'dataSourceServerTime',
|
||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
|
width: 180,
|
||||||
render: (_, { dataSourceServerTime, timeZone }) => {
|
render: (_, { dataSourceServerTime, timeZone }) => {
|
||||||
if (!dataSourceServerTime) return '-';
|
if (!dataSourceServerTime) return '-';
|
||||||
return (
|
return (
|
||||||
@@ -344,6 +343,8 @@ const Alert: React.FC = () => {
|
|||||||
title: $t('table.status'),
|
title: $t('table.status'),
|
||||||
dataIndex: 'statusId',
|
dataIndex: 'statusId',
|
||||||
valueType: 'select',
|
valueType: 'select',
|
||||||
|
fixed: 'right',
|
||||||
|
width: 'auto',
|
||||||
fieldProps: {
|
fieldProps: {
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
popupMatchSelectWidth: false,
|
popupMatchSelectWidth: false,
|
||||||
@@ -521,7 +522,7 @@ const Alert: React.FC = () => {
|
|||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<ProTable
|
<ProTable
|
||||||
scroll={{ x: true }}
|
scroll={{ x: 'max-content' }}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
form={{
|
form={{
|
||||||
initialValues: {
|
initialValues: {
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
ModalForm,
|
ModalForm,
|
||||||
|
ProFormDatePicker,
|
||||||
ProFormDependency,
|
ProFormDependency,
|
||||||
|
ProFormDigit,
|
||||||
type ProFormInstance,
|
type ProFormInstance,
|
||||||
ProFormSelect,
|
ProFormSelect,
|
||||||
ProFormSwitch,
|
ProFormSwitch,
|
||||||
ProFormText,
|
ProFormText,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { useAccess } from '@umijs/max';
|
import { useAccess } from '@umijs/max';
|
||||||
import { Divider, Space, Typography } from 'antd';
|
import { Card, Col, Divider, Row, Space, Typography } from 'antd';
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
import {
|
import {
|
||||||
@@ -16,6 +19,7 @@ import {
|
|||||||
getEmailConfigList,
|
getEmailConfigList,
|
||||||
} from '@/services/api';
|
} from '@/services/api';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
|
import { utcInstance as dayjs } from '@/utils/timeFormat';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
type CompanyListItem = Partial<API.CompanyListItem>;
|
type CompanyListItem = Partial<API.CompanyListItem>;
|
||||||
@@ -25,7 +29,7 @@ export default (props: {
|
|||||||
onSubmit: (values: CompanyListItem) => void;
|
onSubmit: (values: CompanyListItem) => void;
|
||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { isGlobalCompany } = useAccess();
|
const { isGlobalCompany, isSuperAdmin } = useAccess();
|
||||||
const { userInfo } = useUserInfo();
|
const { userInfo } = useUserInfo();
|
||||||
const { modalVisit, formValues, onSubmit, onDone } = props;
|
const { modalVisit, formValues, onSubmit, onDone } = props;
|
||||||
|
|
||||||
@@ -43,6 +47,13 @@ export default (props: {
|
|||||||
if (!isGlobalCompany) {
|
if (!isGlobalCompany) {
|
||||||
values.companyId = userInfo?.companyId;
|
values.companyId = userInfo?.companyId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
values.validityEndTime = values.validityEndTime
|
||||||
|
? dayjs.utc(values.validityEndTime).format('YYYY-MM-DD')
|
||||||
|
: '';
|
||||||
|
values.validityStartTime = values.validityEndTime ? '2026-01-01' : '';
|
||||||
|
}
|
||||||
return onSubmit(values);
|
return onSubmit(values);
|
||||||
}}
|
}}
|
||||||
modalProps={{
|
modalProps={{
|
||||||
@@ -70,6 +81,7 @@ export default (props: {
|
|||||||
return {
|
return {
|
||||||
...values,
|
...values,
|
||||||
...companyMailHostConfig,
|
...companyMailHostConfig,
|
||||||
|
validityEndTime: values.validityEndTime || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return values || {};
|
return values || {};
|
||||||
@@ -109,7 +121,96 @@ export default (props: {
|
|||||||
|
|
||||||
{formValues?.id !== void 0 && (
|
{formValues?.id !== void 0 && (
|
||||||
<>
|
<>
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<ProFormDatePicker
|
||||||
|
name="validityEndTime"
|
||||||
|
label={`${$t('form.company.validityEndTime')} (${$t('form.company.validityEndTime.tips')})`}
|
||||||
|
placeholder={$t('form.company.validityEndTime.placeholder')}
|
||||||
|
convertValue={(value: string | Dayjs | null | undefined) =>
|
||||||
|
value ? dayjs.utc(value) : undefined
|
||||||
|
}
|
||||||
|
fieldProps={{
|
||||||
|
style: { width: '100%' },
|
||||||
|
disabledDate: (current: Dayjs) => {
|
||||||
|
// return current?.isBefore(dayjs().startOf('day'));
|
||||||
|
return (
|
||||||
|
current?.format('YYYY-MM-DD') <
|
||||||
|
dayjs.utc().format('YYYY-MM-DD')
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Divider size="small" />
|
<Divider size="small" />
|
||||||
|
|
||||||
|
<Text strong style={{ lineHeight: '40px', fontSize: 16 }}>
|
||||||
|
⚙️ {$t('pages.email.platformAccess')}
|
||||||
|
</Text>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col span={12}>
|
||||||
|
<Card
|
||||||
|
title={`MT4 ${$t('pages.email.platformAccessEnable')}`}
|
||||||
|
extra={
|
||||||
|
<ProFormSwitch
|
||||||
|
name="mt4Open"
|
||||||
|
noStyle
|
||||||
|
transform={(value) => +value}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
styles={{
|
||||||
|
header: { fontSize: 14, minHeight: 40, paddingInline: 14 },
|
||||||
|
body: { paddingInline: 14 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormDependency name={['mt4Open']}>
|
||||||
|
{({ mt4Open }) => (
|
||||||
|
<ProFormDigit
|
||||||
|
name="mt4Count"
|
||||||
|
label={`${$t('pages.email.platformMax')}`}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
disabled={!mt4Open}
|
||||||
|
formItemProps={{
|
||||||
|
style: { marginBottom: 0 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProFormDependency>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
<Col span={12}>
|
||||||
|
<Card
|
||||||
|
title={`MT5 ${$t('pages.email.platformAccessEnable')}`}
|
||||||
|
extra={
|
||||||
|
<ProFormSwitch
|
||||||
|
name="mt5Open"
|
||||||
|
noStyle
|
||||||
|
transform={(value) => +value}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
styles={{
|
||||||
|
header: { fontSize: 14, minHeight: 40, paddingInline: 14 },
|
||||||
|
body: { paddingInline: 14 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ProFormDependency name={['mt5Open']}>
|
||||||
|
{({ mt5Open }) => (
|
||||||
|
<ProFormDigit
|
||||||
|
name="mt5Count"
|
||||||
|
label={`${$t('pages.email.platformMax')}`}
|
||||||
|
min={0}
|
||||||
|
step={1}
|
||||||
|
disabled={!mt5Open}
|
||||||
|
formItemProps={{
|
||||||
|
style: { marginBottom: 0 },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProFormDependency>
|
||||||
|
</Card>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
{/* <Divider size="small" /> */}
|
||||||
<Text strong style={{ lineHeight: '40px', fontSize: 16 }}>
|
<Text strong style={{ lineHeight: '40px', fontSize: 16 }}>
|
||||||
📧 {$t('pages.email.notification')}
|
📧 {$t('pages.email.notification')}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
type ProColumns,
|
type ProColumns,
|
||||||
ProTable,
|
ProTable,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { Badge, Button, Col, Row } from 'antd';
|
import { useAccess } from '@umijs/max';
|
||||||
|
import { Badge, Button, Col, Row, Space, Typography } from 'antd';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import HeadStatisticCard, {
|
import HeadStatisticCard, {
|
||||||
type CardInfo,
|
type CardInfo,
|
||||||
@@ -13,9 +14,13 @@ import HeadStatisticCard, {
|
|||||||
import MyTag from '@/components/MyTag';
|
import MyTag from '@/components/MyTag';
|
||||||
import { companyHandle, getCompanyPages } from '@/services/api';
|
import { companyHandle, getCompanyPages } from '@/services/api';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
|
import { utcInstance as dayjs } from '@/utils/timeFormat';
|
||||||
import PopUp from './Popup';
|
import PopUp from './Popup';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
const Company: React.FC = () => {
|
const Company: React.FC = () => {
|
||||||
|
const { isSuperAdmin } = useAccess();
|
||||||
const [modalVisit, setModalVisit] = useState(false);
|
const [modalVisit, setModalVisit] = useState(false);
|
||||||
|
|
||||||
const [cardInfo, setCardInfo] = useState({
|
const [cardInfo, setCardInfo] = useState({
|
||||||
@@ -69,6 +74,46 @@ const Company: React.FC = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: $t('table.platformAccess'),
|
||||||
|
dataIndex: 'platformAccess',
|
||||||
|
render: (
|
||||||
|
_,
|
||||||
|
{
|
||||||
|
mt4Count,
|
||||||
|
mt4Open,
|
||||||
|
mt5Count,
|
||||||
|
mt5Open,
|
||||||
|
sourceType4Count,
|
||||||
|
sourceType5Count,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<Space direction="vertical" size={4}>
|
||||||
|
<MyTag color={mt4Open === 1 ? '#818cf8' : '#94a3b8'}>
|
||||||
|
MT4:{' '}
|
||||||
|
{mt4Open === 1 ? (
|
||||||
|
<span>
|
||||||
|
{sourceType4Count ?? '-'} / {mt4Count ?? '-'}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
$t('pages.rules.disable')
|
||||||
|
)}
|
||||||
|
</MyTag>
|
||||||
|
<MyTag color={mt5Open === 1 ? '#3b82f6' : '#94a3b8'}>
|
||||||
|
MT5:{' '}
|
||||||
|
{mt5Open === 1 ? (
|
||||||
|
<span>
|
||||||
|
{sourceType5Count ?? '-'} / {mt5Count ?? '-'}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
$t('pages.rules.disable')
|
||||||
|
)}
|
||||||
|
</MyTag>
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: $t('table.userCount'),
|
title: $t('table.userCount'),
|
||||||
dataIndex: 'userCount',
|
dataIndex: 'userCount',
|
||||||
@@ -92,6 +137,28 @@ const Company: React.FC = () => {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: $t('table.validityEndTime'),
|
||||||
|
dataIndex: 'validityEndTime',
|
||||||
|
tooltip: $t('table.validityEndTime.tips'),
|
||||||
|
hideInTable: !isSuperAdmin,
|
||||||
|
render: (_, { validityEndTime, validityStatus }) => {
|
||||||
|
return (
|
||||||
|
<Space>
|
||||||
|
{!validityEndTime ? (
|
||||||
|
<Text type="secondary">{$t('pages.company.permanent')}</Text>
|
||||||
|
) : (
|
||||||
|
<span>{dayjs(validityEndTime).format('YYYY-MM-DD')}</span>
|
||||||
|
)}
|
||||||
|
{validityStatus === 2 && (
|
||||||
|
<Text type="danger" style={{ fontSize: 11 }}>
|
||||||
|
{$t('pages.company.expired')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: $t('table.createTime'),
|
title: $t('table.createTime'),
|
||||||
dataIndex: 'createTime',
|
dataIndex: 'createTime',
|
||||||
@@ -122,7 +189,10 @@ const Company: React.FC = () => {
|
|||||||
size="small"
|
size="small"
|
||||||
key="edit"
|
key="edit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFormValues(item);
|
setFormValues({
|
||||||
|
...item,
|
||||||
|
validityEndTime: item.validityEndTime || undefined,
|
||||||
|
});
|
||||||
setModalVisit(true);
|
setModalVisit(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { useAccess } from '@umijs/max';
|
import { useAccess } from '@umijs/max';
|
||||||
import { Divider, type FormInstance, Typography } from 'antd';
|
import { Divider, type FormInstance, Typography } from 'antd';
|
||||||
import { useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
import { preventScientificNotation } from '@/utils';
|
import { preventScientificNotation } from '@/utils';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
@@ -22,7 +22,7 @@ export default (props: {
|
|||||||
}) => {
|
}) => {
|
||||||
const { visible, formValues, onSubmit, onDone, companyList } = props;
|
const { visible, formValues, onSubmit, onDone, companyList } = props;
|
||||||
|
|
||||||
const { userInfo } = useUserInfo();
|
const { userInfo, userPlatform } = useUserInfo();
|
||||||
const { isGlobalCompany } = useAccess();
|
const { isGlobalCompany } = useAccess();
|
||||||
const globalCompanyId = isGlobalCompany ? userInfo?.companyId : 0;
|
const globalCompanyId = isGlobalCompany ? userInfo?.companyId : 0;
|
||||||
const formRef = useRef<FormInstance>(null);
|
const formRef = useRef<FormInstance>(null);
|
||||||
@@ -41,6 +41,12 @@ export default (props: {
|
|||||||
message: $t('form.placeholder.ipAddress'),
|
message: $t('form.placeholder.ipAddress'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const filteredUserPlatform = useMemo(
|
||||||
|
() => userPlatform.filter((v) => v?.isOpen),
|
||||||
|
[userPlatform],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalForm
|
<ModalForm
|
||||||
width="600px"
|
width="600px"
|
||||||
@@ -50,7 +56,10 @@ export default (props: {
|
|||||||
formValues?.id
|
formValues?.id
|
||||||
? formValues
|
? formValues
|
||||||
: {
|
: {
|
||||||
sourceType: 5,
|
sourceType:
|
||||||
|
filteredUserPlatform.length > 0
|
||||||
|
? filteredUserPlatform[0].value
|
||||||
|
: null,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
open={visible}
|
open={visible}
|
||||||
@@ -78,16 +87,7 @@ export default (props: {
|
|||||||
label={$t('form.dataSource.type')}
|
label={$t('form.dataSource.type')}
|
||||||
allowClear={false}
|
allowClear={false}
|
||||||
rules={[{ required: true, message: $t('form.required') }]}
|
rules={[{ required: true, message: $t('form.required') }]}
|
||||||
options={[
|
options={filteredUserPlatform}
|
||||||
// {
|
|
||||||
// value: 4,
|
|
||||||
// label: 'MT4',
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
value: 5,
|
|
||||||
label: 'MT5',
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{isGlobalCompany && (
|
{isGlobalCompany && (
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
dataSourceHandle,
|
dataSourceHandle,
|
||||||
getCompanyList,
|
getCompanyList,
|
||||||
getDataSourcePages,
|
getDataSourcePages,
|
||||||
|
getPlatformList,
|
||||||
} from '@/services/api';
|
} from '@/services/api';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
import Popup from './Popup';
|
import Popup from './Popup';
|
||||||
@@ -49,6 +50,7 @@ const DataSource: React.FC = () => {
|
|||||||
setCompanyList(data || []);
|
setCompanyList(data || []);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
getPlatformList();
|
||||||
}, []);
|
}, []);
|
||||||
const [cardInfo, setCardInfo] = useState({
|
const [cardInfo, setCardInfo] = useState({
|
||||||
// mt4Count: 0,
|
// mt4Count: 0,
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ const OperLog: React.FC = () => {
|
|||||||
},
|
},
|
||||||
'rule insert': (item) => {
|
'rule insert': (item) => {
|
||||||
const operParam = parseOperParam(item);
|
const operParam = parseOperParam(item);
|
||||||
return `Add Rule: '${ruleBaseMeta[operParam.type].title || '-'}'`;
|
return `Add Rule: '${ruleBaseMeta[operParam?.type]?.title || '-'}'`;
|
||||||
},
|
},
|
||||||
'rule update': (item) => {
|
'rule update': (item) => {
|
||||||
const operParam = parseOperParam(item);
|
const operParam = parseOperParam(item);
|
||||||
return `Update Rule: '${ruleBaseMeta[operParam.type].title || '-'}'`;
|
return `Update Rule: '${ruleBaseMeta[operParam?.type]?.title || '-'}'`;
|
||||||
},
|
},
|
||||||
'rule update status': (item) => {
|
'rule update status': (item) => {
|
||||||
const matched = item.operParam.match(/status=(?<status>\d+)/);
|
const matched = item.operParam.match(/status=(?<status>\d+)/);
|
||||||
@@ -124,7 +124,7 @@ const OperLog: React.FC = () => {
|
|||||||
hideInSearch: true,
|
hideInSearch: true,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
width: 'auto',
|
width: 'auto',
|
||||||
renderText: (_, item) => actionMap[item.title]?.(item) || '-',
|
renderText: (_, item) => actionMap[item?.title]?.(item) || '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: $t('table.ipAddress'),
|
title: $t('table.ipAddress'),
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {
|
|||||||
ProForm,
|
ProForm,
|
||||||
ProFormText,
|
ProFormText,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { history } from '@umijs/max';
|
import { history, useAccess } from '@umijs/max';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
@@ -14,16 +14,20 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Space,
|
Space,
|
||||||
Tag,
|
Tag,
|
||||||
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { clearAccessToken } from '@/access';
|
import { clearAccessToken } from '@/access';
|
||||||
import PasswordLevel from '@/components/PasswordLevel';
|
import PasswordLevel from '@/components/PasswordLevel';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
import { changeUserPass } from '@/services/api';
|
import { changeUserPass } from '@/services/api';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
const Profile: React.FC = () => {
|
const Profile: React.FC = () => {
|
||||||
const { userInfo, setUserInfo } = useUserInfo();
|
const { userInfo, setUserInfo } = useUserInfo();
|
||||||
|
const { isGlobalCompany } = useAccess();
|
||||||
const userCompany = userInfo?.company;
|
const userCompany = userInfo?.company;
|
||||||
const { message: antdMessage, modal } = App.useApp();
|
const { message: antdMessage, modal } = App.useApp();
|
||||||
|
|
||||||
@@ -71,6 +75,28 @@ const Profile: React.FC = () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isGlobalCompany) {
|
||||||
|
items.push({
|
||||||
|
key: '6',
|
||||||
|
span: 'filled',
|
||||||
|
label: $t('table.validityEndTime'),
|
||||||
|
children: userCompany?.validityEndTime ? (
|
||||||
|
<Space>
|
||||||
|
{userCompany.validityEndTime}
|
||||||
|
{userCompany?.validityStatus === 2 && (
|
||||||
|
<Text type="danger" style={{ fontSize: '12px' }}>
|
||||||
|
{$t('pages.company.expired')}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Space>
|
||||||
|
) : (
|
||||||
|
$t('pages.company.permanent')
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [userCompany, isGlobalCompany]);
|
||||||
|
|
||||||
const changePasswordSuccessHadle = async () => {
|
const changePasswordSuccessHadle = async () => {
|
||||||
let countdown = 5;
|
let countdown = 5;
|
||||||
let timer: NodeJS.Timeout | null = null;
|
let timer: NodeJS.Timeout | null = null;
|
||||||
|
|||||||
@@ -1,19 +1,31 @@
|
|||||||
import {
|
import {
|
||||||
ModalForm,
|
ModalForm,
|
||||||
ProForm,
|
ProForm,
|
||||||
ProFormCheckbox,
|
|
||||||
ProFormDependency,
|
ProFormDependency,
|
||||||
// ProFormDigit,
|
// ProFormDigit,
|
||||||
ProFormText,
|
ProFormText,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
// import { useAccess } from '@umijs/max';
|
|
||||||
import { ColorPicker, Form, Space } from 'antd';
|
import { ColorPicker, Form, Space } from 'antd';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import MyTag from '@/components/MyTag';
|
import MyTag from '@/components/MyTag';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
import { getRoleDetail } from '@/services/api';
|
import { getRoleDetail } from '@/services/api';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
// import { preventScientificNotation } from '@/utils';
|
// import { preventScientificNotation } from '@/utils';
|
||||||
import useCardList from './cardList';
|
import useCardList from './cardList';
|
||||||
|
import PermissionCheckboxGroup from './components/PermissionCheckboxGroup';
|
||||||
|
|
||||||
|
function flattenAllIds(nodes: API.MenuListItem[]): (string | number)[] {
|
||||||
|
const ids: (string | number)[] = [];
|
||||||
|
const walk = (items: API.MenuListItem[]) => {
|
||||||
|
items.forEach((item) => {
|
||||||
|
ids.push(item.id);
|
||||||
|
if (item.children?.length) walk(item.children);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
walk(nodes);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
type RolesListItem = Partial<API.RolesListItem>;
|
type RolesListItem = Partial<API.RolesListItem>;
|
||||||
export default (props: {
|
export default (props: {
|
||||||
@@ -22,8 +34,16 @@ export default (props: {
|
|||||||
formValues: Partial<RolesListItem | undefined>;
|
formValues: Partial<RolesListItem | undefined>;
|
||||||
onSubmit: (values: RolesListItem) => void;
|
onSubmit: (values: RolesListItem) => void;
|
||||||
onDone: () => void;
|
onDone: () => void;
|
||||||
|
menuTree?: API.MenuListItem[];
|
||||||
}) => {
|
}) => {
|
||||||
const { visible, formValues, onSubmit, onDone, action } = props;
|
const {
|
||||||
|
visible,
|
||||||
|
formValues,
|
||||||
|
onSubmit,
|
||||||
|
onDone,
|
||||||
|
action,
|
||||||
|
menuTree: propMenuTree,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const { userInfo } = useUserInfo();
|
const { userInfo } = useUserInfo();
|
||||||
// const { isGlobalCompany } = useAccess();
|
// const { isGlobalCompany } = useAccess();
|
||||||
@@ -31,9 +51,14 @@ export default (props: {
|
|||||||
// const level = userRoleInfo?.level || (isGlobalCompany ? 100 : 80);
|
// const level = userRoleInfo?.level || (isGlobalCompany ? 100 : 80);
|
||||||
|
|
||||||
const { getMenuMetaByPermissions } = useCardList();
|
const { getMenuMetaByPermissions } = useCardList();
|
||||||
const menuList = getMenuMetaByPermissions(
|
|
||||||
userInfo?.menuList || ([] as API.MenuListItem[]),
|
const menuTree = useMemo(() => {
|
||||||
);
|
const source = propMenuTree?.length
|
||||||
|
? propMenuTree
|
||||||
|
: getMenuMetaByPermissions(userInfo?.menuList || []);
|
||||||
|
return source as API.MenuListItem[];
|
||||||
|
}, [propMenuTree, userInfo?.menuList]);
|
||||||
|
|
||||||
const actionMap = {
|
const actionMap = {
|
||||||
add: $t('table.action.add'),
|
add: $t('table.action.add'),
|
||||||
view: $t('table.action.view'),
|
view: $t('table.action.view'),
|
||||||
@@ -46,7 +71,7 @@ export default (props: {
|
|||||||
initialValues={formValues?.id ? formValues : { color: '#3b82f6' }}
|
initialValues={formValues?.id ? formValues : { color: '#3b82f6' }}
|
||||||
open={visible}
|
open={visible}
|
||||||
onFinish={async (values: any) => {
|
onFinish={async (values: any) => {
|
||||||
values.menuIds = values.menuIds.join(',') || '';
|
values.menuIds = (values.menuIds || []).join(',') || '';
|
||||||
return onSubmit(values);
|
return onSubmit(values);
|
||||||
}}
|
}}
|
||||||
modalProps={{
|
modalProps={{
|
||||||
@@ -61,12 +86,10 @@ export default (props: {
|
|||||||
} = await getRoleDetail({
|
} = await getRoleDetail({
|
||||||
id: formValues.id,
|
id: formValues.id,
|
||||||
});
|
});
|
||||||
const menuIds = (data?.menuList || []).map(
|
const allSelectedIds = flattenAllIds(data?.menuList || []);
|
||||||
(item: API.MenuListItem) => item.id,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
...formValues,
|
...formValues,
|
||||||
menuIds,
|
menuIds: allSelectedIds,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return formValues || {};
|
return formValues || {};
|
||||||
@@ -114,23 +137,16 @@ export default (props: {
|
|||||||
</ProFormDependency>
|
</ProFormDependency>
|
||||||
</Space>
|
</Space>
|
||||||
</ProForm.Item>
|
</ProForm.Item>
|
||||||
<ProFormCheckbox.Group
|
<ProForm.Item
|
||||||
name="menuIds"
|
name="menuIds"
|
||||||
label={$t('form.role.menuIds')}
|
label={$t('form.role.menuIds')}
|
||||||
rules={[{ required: true, message: $t('form.required') }]}
|
rules={[{ required: true, message: $t('form.required') }]}
|
||||||
options={menuList.map((item: API.MenuListItem) => ({
|
>
|
||||||
label: `${item.meta?.icon || ''} ${item.meta?.title}(${item?.meta?.describe})`,
|
<PermissionCheckboxGroup
|
||||||
value: item.id,
|
menuTree={menuTree}
|
||||||
}))}
|
disabled={action === 'view'}
|
||||||
fieldProps={{
|
/>
|
||||||
style: {
|
</ProForm.Item>
|
||||||
display: 'grid',
|
|
||||||
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
||||||
gap: '8px 16px',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<ProFormText hidden name="id" label="ID" />
|
<ProFormText hidden name="id" label="ID" />
|
||||||
</ModalForm>
|
</ModalForm>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -104,5 +104,38 @@ export default function useRoleCardList() {
|
|||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { roleCardList, getMenuMetaByPermissions };
|
|
||||||
|
/**
|
||||||
|
* Build a permission tree from a flat list using id/parentId.
|
||||||
|
* The backend returns a flat list where children reference their parent via parentId.
|
||||||
|
*/
|
||||||
|
function buildMenuTree(flatList: API.MenuListItem[]): API.MenuListItem[] {
|
||||||
|
const nodeMap = new Map<string | number, API.MenuListItem>();
|
||||||
|
const roots: API.MenuListItem[] = [];
|
||||||
|
|
||||||
|
flatList.forEach((item) => {
|
||||||
|
nodeMap.set(item.id, { ...item, children: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
flatList.forEach((item) => {
|
||||||
|
const node = nodeMap.get(item.id);
|
||||||
|
if (!node) return;
|
||||||
|
const pid = Number(item.parentId);
|
||||||
|
if (pid === 0 || !nodeMap.has(item.parentId)) {
|
||||||
|
roots.push(node);
|
||||||
|
} else {
|
||||||
|
const parent = nodeMap.get(item.parentId);
|
||||||
|
if (parent) {
|
||||||
|
if (!parent.children) parent.children = [];
|
||||||
|
parent.children.push(node);
|
||||||
|
} else {
|
||||||
|
roots.push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return roots;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { roleCardList, getMenuMetaByPermissions, buildMenuTree };
|
||||||
}
|
}
|
||||||
|
|||||||
301
src/pages/role/components/PermissionCheckboxGroup.tsx
Normal file
301
src/pages/role/components/PermissionCheckboxGroup.tsx
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
import { Checkbox, Tag, Typography } from 'antd';
|
||||||
|
import type { CheckboxChangeEvent } from 'antd/es/checkbox';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
|
import { $t } from '@/utils/i18n';
|
||||||
|
import './style.less';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When true, rules permissions are consolidated into 2 summary cards
|
||||||
|
* (View Rules / Manage Rules). Flip to false to show each child node
|
||||||
|
* as an individual checkbox for fine-grained control.
|
||||||
|
*/
|
||||||
|
const CONSOLIDATE_RULES = true;
|
||||||
|
|
||||||
|
interface PermissionCheckboxGroupProps {
|
||||||
|
value?: (string | number)[];
|
||||||
|
onChange?: (checkedIds: (string | number)[]) => void;
|
||||||
|
menuTree: API.MenuListItem[];
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAllDescendantIds(node: API.MenuListItem): (string | number)[] {
|
||||||
|
const ids: (string | number)[] = [node.id];
|
||||||
|
const walk = (children: API.MenuListItem[]) => {
|
||||||
|
children.forEach((child) => {
|
||||||
|
ids.push(child.id);
|
||||||
|
if (child.children?.length) walk(child.children);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (node.children?.length) walk(node.children);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isRulesMenu = (node: API.MenuListItem) =>
|
||||||
|
node.path === 'rule' || node.permission === 'rule:list';
|
||||||
|
|
||||||
|
const PermissionCheckboxGroup: React.FC<PermissionCheckboxGroupProps> = ({
|
||||||
|
value = [],
|
||||||
|
onChange,
|
||||||
|
menuTree,
|
||||||
|
disabled = false,
|
||||||
|
}) => {
|
||||||
|
const checkedSet = useMemo(() => new Set(value), [value]);
|
||||||
|
|
||||||
|
// Split: rules menus (consolidated into cards) vs everything else (preserves original order)
|
||||||
|
const { rulesMenus, belowMenus } = useMemo(() => {
|
||||||
|
const rules: API.MenuListItem[] = [];
|
||||||
|
const below: API.MenuListItem[] = [];
|
||||||
|
menuTree.forEach((node) => {
|
||||||
|
if (CONSOLIDATE_RULES && isRulesMenu(node)) {
|
||||||
|
rules.push(node);
|
||||||
|
} else {
|
||||||
|
below.push(node);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { rulesMenus: rules, belowMenus: below };
|
||||||
|
}, [menuTree]);
|
||||||
|
|
||||||
|
// --- Rules: consolidate all descendants into 2 logical cards ---
|
||||||
|
|
||||||
|
const rulesCardData = useMemo(() => {
|
||||||
|
if (!rulesMenus.length) return null;
|
||||||
|
const parent = rulesMenus[0];
|
||||||
|
const viewIds: (string | number)[] = [];
|
||||||
|
const allIds: (string | number)[] = [];
|
||||||
|
const walk = (children: API.MenuListItem[]) => {
|
||||||
|
children.forEach((child) => {
|
||||||
|
allIds.push(child.id);
|
||||||
|
if (child.permission === 'rule:list') viewIds.push(child.id);
|
||||||
|
if (child.children?.length) walk(child.children);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (parent.children?.length) walk(parent.children);
|
||||||
|
const manageOnlyIds = allIds.filter((id) => !viewIds.includes(id));
|
||||||
|
return { parentId: parent.id, viewIds, allIds, manageOnlyIds };
|
||||||
|
}, [rulesMenus]);
|
||||||
|
|
||||||
|
const handleRulesViewChange = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
if (!rulesCardData) return;
|
||||||
|
const { viewIds, manageOnlyIds } = rulesCardData;
|
||||||
|
const newSet = new Set(value);
|
||||||
|
if (checked) {
|
||||||
|
for (const id of viewIds) newSet.add(id);
|
||||||
|
} else {
|
||||||
|
newSet.delete(rulesCardData.parentId);
|
||||||
|
for (const id of viewIds) newSet.delete(id);
|
||||||
|
for (const id of manageOnlyIds) newSet.delete(id);
|
||||||
|
}
|
||||||
|
onChange?.([...newSet]);
|
||||||
|
},
|
||||||
|
[value, onChange, rulesCardData],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRulesManageChange = useCallback(
|
||||||
|
(checked: boolean) => {
|
||||||
|
if (!rulesCardData) return;
|
||||||
|
const { parentId, allIds, manageOnlyIds } = rulesCardData;
|
||||||
|
const newSet = new Set(value);
|
||||||
|
if (checked) {
|
||||||
|
newSet.add(parentId);
|
||||||
|
for (const id of allIds) newSet.add(id);
|
||||||
|
} else {
|
||||||
|
newSet.delete(parentId);
|
||||||
|
for (const id of manageOnlyIds) newSet.delete(id);
|
||||||
|
}
|
||||||
|
onChange?.([...newSet]);
|
||||||
|
},
|
||||||
|
[value, onChange, rulesCardData],
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Other grouped (parent checkbox, children hidden) ---
|
||||||
|
|
||||||
|
const handleGroupedParentChange = useCallback(
|
||||||
|
(parentNode: API.MenuListItem, checked: boolean) => {
|
||||||
|
const newSet = new Set(value);
|
||||||
|
const ids = getAllDescendantIds(parentNode);
|
||||||
|
if (checked) {
|
||||||
|
for (const id of ids) newSet.add(id);
|
||||||
|
} else {
|
||||||
|
for (const id of ids) newSet.delete(id);
|
||||||
|
}
|
||||||
|
onChange?.([...newSet]);
|
||||||
|
},
|
||||||
|
[value, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Flat checkbox ---
|
||||||
|
|
||||||
|
const handleFlatChange = useCallback(
|
||||||
|
(node: API.MenuListItem, checked: boolean) => {
|
||||||
|
const newSet = new Set(value);
|
||||||
|
if (checked) {
|
||||||
|
newSet.add(node.id);
|
||||||
|
} else {
|
||||||
|
newSet.delete(node.id);
|
||||||
|
}
|
||||||
|
onChange?.([...newSet]);
|
||||||
|
},
|
||||||
|
[value, onChange],
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasRules = rulesCardData !== null;
|
||||||
|
const hasBelow = belowMenus.length > 0;
|
||||||
|
const showDivider = hasRules && hasBelow;
|
||||||
|
|
||||||
|
// Derived checked state for the 2 rules cards
|
||||||
|
const rulesViewChecked = rulesCardData
|
||||||
|
? rulesCardData.viewIds.some((id) => checkedSet.has(id)) ||
|
||||||
|
checkedSet.has(rulesCardData.parentId)
|
||||||
|
: false;
|
||||||
|
const rulesManageChecked = rulesCardData
|
||||||
|
? checkedSet.has(rulesCardData.parentId)
|
||||||
|
: false;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{hasRules && (
|
||||||
|
<>
|
||||||
|
<div className="perm-help-banner">
|
||||||
|
<div className="perm-help-banner__title">
|
||||||
|
<span>💡</span>
|
||||||
|
<strong>
|
||||||
|
{$t('menu.rules')}
|
||||||
|
{$t('form.role.permissionGuide')}
|
||||||
|
</strong>
|
||||||
|
</div>
|
||||||
|
{$t('menu.rules.desc2')}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="perm-rules-grid">
|
||||||
|
{/* View card */}
|
||||||
|
<div
|
||||||
|
className="perm-rules-card perm-rules-card--view"
|
||||||
|
style={{ cursor: disabled ? 'default' : 'pointer' }}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={rulesViewChecked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e: CheckboxChangeEvent) =>
|
||||||
|
handleRulesViewChange(e.target.checked)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="perm-rules-card__inner">
|
||||||
|
<span className="perm-rules-card__icon">👁️</span>
|
||||||
|
<span className="perm-rules-card__body">
|
||||||
|
<span className="perm-rules-card__header">
|
||||||
|
<span className="perm-rules-card__title">
|
||||||
|
{$t('menu.rules.view')}
|
||||||
|
</span>
|
||||||
|
<Tag color="blue" style={{ fontSize: 11, lineHeight: '16px', margin: 0 }}>
|
||||||
|
{$t('menu.rules.view.badge')}
|
||||||
|
</Tag>
|
||||||
|
</span>
|
||||||
|
<Text type="secondary" className="perm-rules-card__desc">
|
||||||
|
{$t('menu.rules.view.desc')}
|
||||||
|
</Text>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Manage card */}
|
||||||
|
<div
|
||||||
|
className="perm-rules-card perm-rules-card--manage"
|
||||||
|
style={{ cursor: disabled ? 'default' : 'pointer' }}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={rulesManageChecked}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e: CheckboxChangeEvent) =>
|
||||||
|
handleRulesManageChange(e.target.checked)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="perm-rules-card__inner">
|
||||||
|
<span className="perm-rules-card__icon">⚙️</span>
|
||||||
|
<span className="perm-rules-card__body">
|
||||||
|
<span className="perm-rules-card__header">
|
||||||
|
<span className="perm-rules-card__title">
|
||||||
|
{$t('menu.rules.edit')}
|
||||||
|
</span>
|
||||||
|
<Tag color="orange" style={{ fontSize: 11, lineHeight: '16px', margin: 0 }}>
|
||||||
|
{$t('menu.rules.edit.badge')}
|
||||||
|
</Tag>
|
||||||
|
</span>
|
||||||
|
<Text type="secondary" className="perm-rules-card__desc">
|
||||||
|
{$t('menu.rules.desc2')}
|
||||||
|
</Text>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showDivider && (
|
||||||
|
<div className="perm-divider">
|
||||||
|
<span className="perm-divider__line" />
|
||||||
|
<span>{$t('form.role.otherPermissions')}</span>
|
||||||
|
<span className="perm-divider__line" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{hasBelow && (
|
||||||
|
<div className="perm-below-grid">
|
||||||
|
{belowMenus.map((node) => {
|
||||||
|
if (node.children?.length) {
|
||||||
|
const childIds = getAllDescendantIds(node).filter(
|
||||||
|
(id) => id !== node.id,
|
||||||
|
);
|
||||||
|
const checkedCount = childIds.filter((id) =>
|
||||||
|
checkedSet.has(id),
|
||||||
|
).length;
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
key={node.id}
|
||||||
|
className="perm-checkbox--grouped"
|
||||||
|
checked={childIds.length > 0 && checkedCount === childIds.length}
|
||||||
|
indeterminate={checkedCount > 0 && checkedCount < childIds.length}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e: CheckboxChangeEvent) =>
|
||||||
|
handleGroupedParentChange(node, e.target.checked)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{node.meta?.icon || ''} {node.meta?.title || node.menuName}
|
||||||
|
{node.meta?.describe && (
|
||||||
|
<Text type="secondary" className="perm-checkbox-desc">
|
||||||
|
({node.meta.describe})
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
key={node.id}
|
||||||
|
className="perm-checkbox--flat"
|
||||||
|
checked={checkedSet.has(node.id)}
|
||||||
|
disabled={disabled}
|
||||||
|
onChange={(e: CheckboxChangeEvent) =>
|
||||||
|
handleFlatChange(node, e.target.checked)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{node.meta?.icon || ''} {node.meta?.title || node.menuName}
|
||||||
|
{node.meta?.describe && (
|
||||||
|
<Text type="secondary" className="perm-checkbox-desc">
|
||||||
|
({node.meta.describe})
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PermissionCheckboxGroup;
|
||||||
152
src/pages/role/components/style.less
Normal file
152
src/pages/role/components/style.less
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
// --- Rules help banner ---
|
||||||
|
.perm-help-banner {
|
||||||
|
padding: 12px 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
rgba(79, 70, 229, 0.08),
|
||||||
|
rgba(14, 165, 233, 0.06)
|
||||||
|
);
|
||||||
|
border: 1px solid rgba(99, 102, 241, 0.18);
|
||||||
|
border-radius: 10px;
|
||||||
|
color: var(--text-secondary, #64748b);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.65;
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Rules cards (2-column) ---
|
||||||
|
.perm-rules-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-rules-card {
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
min-height: 112px;
|
||||||
|
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
|
||||||
|
|
||||||
|
// full-width checkbox so entire card is clickable
|
||||||
|
.ant-checkbox-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card layout: [checkbox-indicator] [icon] [text]
|
||||||
|
&__inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
line-height: 1.35;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--text-primary, #1e293b);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__desc {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// View card (blue)
|
||||||
|
&--view {
|
||||||
|
border: 1px solid rgba(59, 130, 246, 0.46);
|
||||||
|
background: rgba(239, 246, 255, 0.92);
|
||||||
|
|
||||||
|
.perm-rules-card__icon {
|
||||||
|
background: rgba(59, 130, 246, 0.12);
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manage card (orange)
|
||||||
|
&--manage {
|
||||||
|
border: 1px solid rgba(245, 158, 11, 0.48);
|
||||||
|
background: rgba(255, 251, 235, 0.92);
|
||||||
|
|
||||||
|
.perm-rules-card__icon {
|
||||||
|
background: rgba(245, 158, 11, 0.12);
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Divider between rules and other permissions ---
|
||||||
|
.perm-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 12px 0 8px;
|
||||||
|
color: var(--text-muted, #94a3b8);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&__line {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-color, #e2e8f0);
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Below section: other grouped + flat checkboxes ---
|
||||||
|
.perm-below-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 4px 16px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-checkbox--grouped {
|
||||||
|
padding: 6px 10px;
|
||||||
|
// font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-checkbox--flat {
|
||||||
|
padding: 6px 10px;
|
||||||
|
min-width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perm-checkbox-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
@@ -15,7 +15,12 @@ import HeadStatisticCard, {
|
|||||||
} from '@/components/HeadStatisticCard';
|
} from '@/components/HeadStatisticCard';
|
||||||
import MyTag from '@/components/MyTag';
|
import MyTag from '@/components/MyTag';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
import { getCompanyList, getRoleList, roleHandle } from '@/services/api';
|
import {
|
||||||
|
getCompanyList,
|
||||||
|
getRoleDetail,
|
||||||
|
getRoleList,
|
||||||
|
roleHandle,
|
||||||
|
} from '@/services/api';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
import useRoleCardList from './cardList';
|
import useRoleCardList from './cardList';
|
||||||
import Popup from './Popup';
|
import Popup from './Popup';
|
||||||
@@ -33,7 +38,24 @@ const Role: React.FC = () => {
|
|||||||
const { isGlobalCompany, isSuperAdmin, isCompanyAdmin } = useAccess();
|
const { isGlobalCompany, isSuperAdmin, isCompanyAdmin } = useAccess();
|
||||||
const globalCompanyId = isGlobalCompany ? userInfo?.companyId : 0;
|
const globalCompanyId = isGlobalCompany ? userInfo?.companyId : 0;
|
||||||
const tableRef = useRef<ActionType>(null);
|
const tableRef = useRef<ActionType>(null);
|
||||||
const { roleCardList } = useRoleCardList();
|
const { roleCardList, getMenuMetaByPermissions, buildMenuTree } =
|
||||||
|
useRoleCardList();
|
||||||
|
|
||||||
|
const [completeMenuTree, setCompleteMenuTree] = useState<API.MenuListItem[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const roleId = userInfo?.roleList?.[0]?.id;
|
||||||
|
if (!roleId) return;
|
||||||
|
getRoleDetail({ id: roleId }).then(({ data: { data } }) => {
|
||||||
|
const flatList = data?.menuList || [];
|
||||||
|
if (!flatList.length) return;
|
||||||
|
const tree = buildMenuTree(flatList);
|
||||||
|
const treeWithMeta = getMenuMetaByPermissions(tree);
|
||||||
|
setCompleteMenuTree(treeWithMeta);
|
||||||
|
});
|
||||||
|
}, [userInfo?.roleList?.[0]?.id]);
|
||||||
|
|
||||||
const [companyType, setCompanyType] = useState<number | ''>(
|
const [companyType, setCompanyType] = useState<number | ''>(
|
||||||
userInfo?.companyId ?? '',
|
userInfo?.companyId ?? '',
|
||||||
@@ -225,7 +247,7 @@ const Role: React.FC = () => {
|
|||||||
📋 {$t('pages.availablePermissionList')}
|
📋 {$t('pages.availablePermissionList')}
|
||||||
</Title>
|
</Title>
|
||||||
</Col> */}
|
</Col> */}
|
||||||
<Row wrap gutter={[16, 16]}>
|
<Row wrap gutter={[16, 16]} className="w-100">
|
||||||
{roleCardList
|
{roleCardList
|
||||||
.filter((item) => item.curUserShow)
|
.filter((item) => item.curUserShow)
|
||||||
.map((item) => (
|
.map((item) => (
|
||||||
@@ -357,6 +379,7 @@ const Role: React.FC = () => {
|
|||||||
formValues={formValues}
|
formValues={formValues}
|
||||||
onDone={handleDone}
|
onDone={handleDone}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
|
menuTree={completeMenuTree}
|
||||||
/>
|
/>
|
||||||
</PageContainer>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {
|
|||||||
useBreakpoint,
|
useBreakpoint,
|
||||||
} from '@ant-design/pro-components';
|
} from '@ant-design/pro-components';
|
||||||
import { Button, Divider, Flex, Input, Modal, Space, Typography } from 'antd';
|
import { Button, Divider, Flex, Input, Modal, Space, Typography } from 'antd';
|
||||||
import { createContext, useMemo, useRef, useState } from 'react';
|
import { useMemo, useRef, useState } from 'react';
|
||||||
import useRichFormat from '@/hooks/useRichI18n';
|
import useRichFormat from '@/hooks/useRichI18n';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
import '../style.less';
|
import '../style.less';
|
||||||
@@ -14,26 +14,9 @@ import { handleEmptyValuesFunc } from '../../utils';
|
|||||||
import BatchCloneModal from './comp/BatchCloneModal';
|
import BatchCloneModal from './comp/BatchCloneModal';
|
||||||
import Step1 from './comp/Step1';
|
import Step1 from './comp/Step1';
|
||||||
import Step2 from './comp/Step2';
|
import Step2 from './comp/Step2';
|
||||||
|
import { MultiCopyPopupContext } from './context';
|
||||||
|
|
||||||
const { Text, Title } = Typography;
|
const { Text, Title } = Typography;
|
||||||
type MultiCopyPopupContextValue = {
|
|
||||||
ruleMeta: Record<string, any>;
|
|
||||||
dataSourceList: API.DataSourceListItem[];
|
|
||||||
currentRuleType: number;
|
|
||||||
targetDataSourceId: string | number | null;
|
|
||||||
selectRules: API.RuleListItem[];
|
|
||||||
setSelectRules: (rules: API.RuleListItem[]) => void;
|
|
||||||
currentSchemaMap: Record<string, any>;
|
|
||||||
};
|
|
||||||
export const MultiCopyPopupContext = createContext<MultiCopyPopupContextValue>({
|
|
||||||
ruleMeta: {},
|
|
||||||
dataSourceList: [],
|
|
||||||
currentRuleType: 0,
|
|
||||||
targetDataSourceId: null,
|
|
||||||
selectRules: [],
|
|
||||||
setSelectRules: () => {},
|
|
||||||
currentSchemaMap: {},
|
|
||||||
});
|
|
||||||
type MultiCopyPopupProps = {
|
type MultiCopyPopupProps = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
ruleMeta: Record<string, any>;
|
ruleMeta: Record<string, any>;
|
||||||
@@ -232,22 +215,22 @@ const MultiCopyPopup = ({
|
|||||||
onValuesChange={({ sourceDataSourceId }) => {
|
onValuesChange={({ sourceDataSourceId }) => {
|
||||||
if (sourceDataSourceId) {
|
if (sourceDataSourceId) {
|
||||||
const curFormRef = formMapRef?.current?.[0]?.current;
|
const curFormRef = formMapRef?.current?.[0]?.current;
|
||||||
const curTargetDataSourceId =
|
const curTargetId = curFormRef?.getFieldValue('targetDataSourceId');
|
||||||
curFormRef?.getFieldValue('targetDataSourceId');
|
const sourceItem = dataSourceList.find(
|
||||||
if (curTargetDataSourceId === sourceDataSourceId) {
|
(item) => item.id === sourceDataSourceId,
|
||||||
let targetDataSourceId: string | number | null = null;
|
);
|
||||||
for (const item of dataSourceList) {
|
const targetItem = dataSourceList.find(
|
||||||
if (item.id !== sourceDataSourceId) {
|
(item) => item.id === curTargetId,
|
||||||
targetDataSourceId = item.id;
|
);
|
||||||
break;
|
if (targetItem && sourceItem && targetItem.sourceType !== sourceItem.sourceType) {
|
||||||
}
|
const fallback =
|
||||||
}
|
dataSourceList.find(
|
||||||
if (targetDataSourceId) {
|
(item) =>
|
||||||
curFormRef?.setFieldsValue({
|
item.id !== sourceDataSourceId &&
|
||||||
targetDataSourceId,
|
item.sourceType === sourceItem.sourceType,
|
||||||
});
|
)?.id || sourceDataSourceId;
|
||||||
setTargetDataSourceId(targetDataSourceId);
|
curFormRef?.setFieldsValue({ targetDataSourceId: fallback });
|
||||||
}
|
setTargetDataSourceId(fallback);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Typography,
|
Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import useRichFormat from '@/hooks/useRichI18n';
|
import useRichFormat from '@/hooks/useRichI18n';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
import { type SchemaType, schemaMap } from '../../ruleFormSchema';
|
import { type SchemaType, schemaMap } from '../../ruleFormSchema';
|
||||||
@@ -68,49 +68,83 @@ const SingleCopyPopup = ({
|
|||||||
} =
|
} =
|
||||||
schemaMap[currentRuleType as unknown as SchemaType]?.({ richFormat }) || {};
|
schemaMap[currentRuleType as unknown as SchemaType]?.({ richFormat }) || {};
|
||||||
|
|
||||||
const rightDataSourceList = dataSourceList.filter(
|
const sourceSourceType = useMemo(() => {
|
||||||
(item) => item.id !== formValues?.dataSourceId,
|
return dataSourceList.find((item) => item.id === formValues?.dataSourceId)
|
||||||
);
|
?.sourceType;
|
||||||
|
}, [dataSourceList, formValues?.dataSourceId]);
|
||||||
|
|
||||||
|
const dataSourceOptions = useMemo(() => {
|
||||||
|
return dataSourceList.map((item) => ({
|
||||||
|
...item,
|
||||||
|
disabled: item.sourceType !== sourceSourceType,
|
||||||
|
label: (
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<span>{item.sourceName}</span>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: 12, marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
MT{item.sourceType}
|
||||||
|
</Typography.Text>
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}, [dataSourceList, sourceSourceType]);
|
||||||
|
|
||||||
const initialValues = transformInitialValue?.(formValues) || formValues;
|
const initialValues = transformInitialValue?.(formValues) || formValues;
|
||||||
|
|
||||||
const rightFirstDataSource = rightDataSourceList[0];
|
const rightFirstDataSource =
|
||||||
|
dataSourceOptions.find(
|
||||||
|
(item) => item.id !== formValues?.dataSourceId && !item.disabled,
|
||||||
|
) || dataSourceOptions[0];
|
||||||
|
const isSameSource = rightFirstDataSource?.id === formValues?.dataSourceId;
|
||||||
const copyFormValues = {
|
const copyFormValues = {
|
||||||
...initialValues,
|
...initialValues,
|
||||||
name: initialValues?.name
|
name: initialValues?.name
|
||||||
? `${initialValues.name} - ${rightFirstDataSource?.sourceName}`
|
? `${initialValues.name} - ${rightFirstDataSource?.sourceName}`
|
||||||
: `${Date.now()} - ${rightFirstDataSource?.sourceName}`,
|
: `${Date.now()} - ${rightFirstDataSource?.sourceName}`,
|
||||||
groupFilter: '',
|
|
||||||
id: void 0,
|
id: void 0,
|
||||||
dataSourceId: rightFirstDataSource?.id || '',
|
dataSourceId: rightFirstDataSource?.id || '',
|
||||||
|
...(isSameSource
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
groupFilter: '',
|
||||||
|
...(symbolFormShow ? { param2: '' } : {}),
|
||||||
|
...(assetFormShow ? { param1: '' } : {}),
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
if (symbolFormShow) {
|
|
||||||
copyFormValues.param2 = '';
|
|
||||||
}
|
|
||||||
if (assetFormShow) {
|
|
||||||
copyFormValues.param1 = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const formContent = ({
|
const formContent = ({
|
||||||
dataSourceList,
|
dataSourceList: options,
|
||||||
formAttr,
|
formAttr,
|
||||||
|
onDataSourceChange,
|
||||||
}: {
|
}: {
|
||||||
dataSourceList: API.DataSourceListItem[];
|
dataSourceList: any[];
|
||||||
formAttr: Record<string, any>;
|
formAttr: Record<string, any>;
|
||||||
|
onDataSourceChange?: (dataSourceId: string | number) => void;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<ProForm {...formAttr} submitter={false}>
|
<ProForm
|
||||||
|
{...formAttr}
|
||||||
|
submitter={false}
|
||||||
|
onValuesChange={(changedValues) => {
|
||||||
|
if (changedValues.dataSourceId && onDataSourceChange) {
|
||||||
|
onDataSourceChange(changedValues.dataSourceId);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
name="dataSourceId"
|
name="dataSourceId"
|
||||||
label={$t('table.dataSource')}
|
label={$t('table.dataSource')}
|
||||||
options={dataSourceList}
|
options={options}
|
||||||
rules={[{ required: true, message: $t('form.required') }]}
|
rules={[{ required: true, message: $t('form.required') }]}
|
||||||
fieldProps={{
|
fieldProps={{
|
||||||
allowClear: false,
|
allowClear: false,
|
||||||
fieldNames: {
|
fieldNames: {
|
||||||
label: 'sourceName',
|
label: 'label',
|
||||||
value: 'id',
|
value: 'id',
|
||||||
},
|
},
|
||||||
|
optionLabelProp: 'label',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<BetaSchemaForm layoutType="Embed" columns={leftForm} />
|
<BetaSchemaForm layoutType="Embed" columns={leftForm} />
|
||||||
@@ -194,53 +228,64 @@ const SingleCopyPopup = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Row>
|
{visible && (
|
||||||
<Col xs={24} sm={11}>
|
<Row>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<Col xs={24} sm={11}>
|
||||||
<Text strong>📋 {$t('pages.rules.originalRule')}</Text>
|
<div style={{ marginBottom: 12 }}>
|
||||||
</div>
|
<Text strong>📋 {$t('pages.rules.originalRule')}</Text>
|
||||||
{formContent({
|
</div>
|
||||||
dataSourceList,
|
{formContent({
|
||||||
formAttr: { initialValues, disabled: true },
|
dataSourceList: dataSourceOptions,
|
||||||
})}
|
formAttr: { initialValues, disabled: true },
|
||||||
</Col>
|
})}
|
||||||
|
</Col>
|
||||||
|
|
||||||
<Col xs={0} sm={1}>
|
<Col xs={0} sm={1}>
|
||||||
<Flex vertical align="center" style={{ height: '100%' }}>
|
<Flex vertical align="center" style={{ height: '100%' }}>
|
||||||
<ArrowRightOutlined style={{ marginTop: 2, fontSize: 18 }} />
|
<ArrowRightOutlined style={{ marginTop: 2, fontSize: 18 }} />
|
||||||
<Divider type="vertical" style={{ flex: 1, marginTop: 16 }} />
|
<Divider type="vertical" style={{ flex: 1, marginTop: 16 }} />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} sm={0}>
|
<Col xs={24} sm={0}>
|
||||||
<div className="text-center" style={{ marginBottom: 16 }}>
|
<div className="text-center" style={{ marginBottom: 16 }}>
|
||||||
<ArrowDownOutlined />
|
<ArrowDownOutlined />
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
<Col xs={24} sm={12}>
|
<Col xs={24} sm={12}>
|
||||||
<div style={{ marginBottom: 12 }}>
|
<div style={{ marginBottom: 12 }}>
|
||||||
<Text strong>✏️ {$t('pages.rules.cloneRulePreview')}</Text>
|
<Text strong>✏️ {$t('pages.rules.cloneRulePreview')}</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{formContent({
|
{formContent({
|
||||||
dataSourceList: rightDataSourceList,
|
dataSourceList: dataSourceOptions,
|
||||||
formAttr: {
|
formAttr: {
|
||||||
initialValues: copyFormValues,
|
initialValues: copyFormValues,
|
||||||
formRef: formRef,
|
formRef: formRef,
|
||||||
disabled: confirmLoading,
|
disabled: confirmLoading,
|
||||||
},
|
},
|
||||||
})}
|
onDataSourceChange: (dataSourceId) => {
|
||||||
{errMsg && (
|
const isSame = dataSourceId === formValues?.dataSourceId;
|
||||||
<Alert
|
if (!isSame) {
|
||||||
message={errMsg}
|
const fields: Record<string, any> = { groupFilter: '' };
|
||||||
type="error"
|
if (symbolFormShow) fields.param2 = '';
|
||||||
showIcon
|
if (assetFormShow) fields.param1 = '';
|
||||||
style={{ marginBottom: 16 }}
|
formRef.current?.setFieldsValue(fields);
|
||||||
/>
|
}
|
||||||
)}
|
},
|
||||||
</Col>
|
})}
|
||||||
</Row>
|
{errMsg && (
|
||||||
|
<Alert
|
||||||
|
message={errMsg}
|
||||||
|
type="error"
|
||||||
|
showIcon
|
||||||
|
style={{ marginBottom: 16 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,10 +17,13 @@ import {
|
|||||||
Tag,
|
Tag,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { processSingleRule } from '@/pages/rules/utils';
|
import { processSingleRule } from '@/pages/rules/utils';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
|
|
||||||
|
type TaskStatus = 'waiting' | 'processing' | 'success' | 'warning' | 'error';
|
||||||
|
type Task = { name: string; status: TaskStatus; errorMsg: string };
|
||||||
|
|
||||||
type BatchCloneModalProps = {
|
type BatchCloneModalProps = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
selectedRows: Partial<API.RuleListItem>[];
|
selectedRows: Partial<API.RuleListItem>[];
|
||||||
@@ -33,26 +36,33 @@ const BatchCloneModal = ({
|
|||||||
onClose,
|
onClose,
|
||||||
onFinish,
|
onFinish,
|
||||||
}: BatchCloneModalProps) => {
|
}: BatchCloneModalProps) => {
|
||||||
const [tasks, setTasks] = useState<any[]>([]);
|
const [tasks, setTasks] = useState<Task[]>([]);
|
||||||
const [isRunning, setIsRunning] = useState(false);
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
const [currentRunning, setCurrentRunning] = useState(0);
|
const [currentRunning, setCurrentRunning] = useState(0);
|
||||||
|
const selectedRowsRef = useRef(selectedRows);
|
||||||
|
selectedRowsRef.current = selectedRows;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initialTasks = selectedRows.map((row) => ({
|
if (visible) {
|
||||||
name: row.name,
|
setTasks(
|
||||||
status: 'waiting', // waiting, processing, success, error
|
selectedRows.map((row) => ({
|
||||||
errorMsg: '',
|
name: row.name || '',
|
||||||
}));
|
status: 'waiting',
|
||||||
setTasks(initialTasks);
|
errorMsg: '',
|
||||||
}, [visible, selectedRows]);
|
})),
|
||||||
|
);
|
||||||
|
setIsRunning(false);
|
||||||
|
setCurrentRunning(0);
|
||||||
|
}
|
||||||
|
}, [visible]);
|
||||||
|
|
||||||
const finishedTasks = useMemo(
|
const finishedTasks = useMemo(
|
||||||
() => tasks.filter((t) => ['success', 'warning'].includes(t.status)),
|
() => tasks.filter((t) => t.status === 'success' || t.status === 'warning'),
|
||||||
[tasks],
|
[tasks],
|
||||||
);
|
);
|
||||||
|
|
||||||
const finishedMessage = useMemo(() => {
|
const finishedMessage = useMemo(() => {
|
||||||
const hasWarning = finishedTasks.find((t) => t.status === 'warning');
|
const hasWarning = finishedTasks.some((t) => t.status === 'warning');
|
||||||
const type: AlertProps['type'] = hasWarning
|
const type: AlertProps['type'] = hasWarning
|
||||||
? 'warning'
|
? 'warning'
|
||||||
: finishedTasks.length === tasks.length
|
: finishedTasks.length === tasks.length
|
||||||
@@ -69,9 +79,10 @@ const BatchCloneModal = ({
|
|||||||
};
|
};
|
||||||
}, [finishedTasks, tasks]);
|
}, [finishedTasks, tasks]);
|
||||||
|
|
||||||
const startBatch = async () => {
|
const startBatch = useCallback(async () => {
|
||||||
|
const rows = selectedRowsRef.current;
|
||||||
setIsRunning(true);
|
setIsRunning(true);
|
||||||
for (let i = 0; i < selectedRows.length; i++) {
|
for (let i = 0; i < rows.length; i++) {
|
||||||
setCurrentRunning(i + 1);
|
setCurrentRunning(i + 1);
|
||||||
setTasks((prev) => {
|
setTasks((prev) => {
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
@@ -79,8 +90,7 @@ const BatchCloneModal = ({
|
|||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await processSingleRule({ payload: selectedRows[i] });
|
await processSingleRule({ payload: rows[i] });
|
||||||
|
|
||||||
setTasks((prev) => {
|
setTasks((prev) => {
|
||||||
const next = [...prev];
|
const next = [...prev];
|
||||||
next[i].status = 'success';
|
next[i].status = 'success';
|
||||||
@@ -96,13 +106,13 @@ const BatchCloneModal = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = useCallback(() => {
|
||||||
setTasks([]);
|
setTasks([]);
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
setCurrentRunning(0);
|
setCurrentRunning(0);
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
import { CheckCard, ProCard } from '@ant-design/pro-components';
|
import { CheckCard, ProCard } from '@ant-design/pro-components';
|
||||||
import { useRequest } from '@umijs/max';
|
import { useRequest } from '@umijs/max';
|
||||||
import { Col, Row, Spin } from 'antd';
|
import { Col, Row, Spin } from 'antd';
|
||||||
import { useContext, useState } from 'react';
|
import { useContext, useEffect, useState } from 'react';
|
||||||
import { getRuleList } from '@/services/api';
|
import { getRuleList } from '@/services/api';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
import { RuleCardBodyInfo } from '../../RuleCardBody';
|
import { RuleCardBodyInfo } from '../../RuleCardBody';
|
||||||
import { MultiCopyPopupContext } from '../MultiCopyPopup';
|
import { MultiCopyPopupContext } from '../context';
|
||||||
|
|
||||||
type RuleCheckCardProps = {
|
type RuleCheckCardProps = {
|
||||||
dataSourceId: string | number;
|
dataSourceId: string | number;
|
||||||
onChange?: (id: string | number) => void;
|
|
||||||
};
|
};
|
||||||
const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
|
const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
|
||||||
const [ruleList, setRuleList] = useState<API.RuleListItem[]>([]);
|
|
||||||
|
|
||||||
const { ruleMeta, currentRuleType, setSelectRules } = useContext(
|
const { ruleMeta, currentRuleType, setSelectRules } = useContext(
|
||||||
MultiCopyPopupContext,
|
MultiCopyPopupContext,
|
||||||
);
|
);
|
||||||
|
const [ruleList, setRuleList] = useState<API.RuleListItem[]>([]);
|
||||||
|
|
||||||
const { loading } = useRequest(
|
const { loading } = useRequest(
|
||||||
() =>
|
() =>
|
||||||
@@ -25,7 +23,6 @@ const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
|
|||||||
dataSourceId,
|
dataSourceId,
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
// manual: true,
|
|
||||||
refreshDeps: [dataSourceId, currentRuleType],
|
refreshDeps: [dataSourceId, currentRuleType],
|
||||||
onSuccess: ({ data = [] }: { data: API.RuleListItem[] }) => {
|
onSuccess: ({ data = [] }: { data: API.RuleListItem[] }) => {
|
||||||
setRuleList(data);
|
setRuleList(data);
|
||||||
@@ -37,15 +34,17 @@ const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => setSelectRules([]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!dataSourceId) return <div>{$t('pages.clones.step1.tips')}</div>;
|
if (!dataSourceId) return <div>{$t('pages.clones.step1.tips')}</div>;
|
||||||
if (loading) return <Spin />;
|
if (loading) return <Spin />;
|
||||||
|
|
||||||
// const
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProCard
|
<ProCard
|
||||||
title={$t('pages.clones.step1.sourceRules', {
|
title={$t('pages.clones.step1.sourceRules', {
|
||||||
count: ruleList?.length,
|
count: ruleList.length,
|
||||||
ruleType: ruleMeta?.title || '-',
|
ruleType: ruleMeta?.title || '-',
|
||||||
})}
|
})}
|
||||||
collapsible
|
collapsible
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ArrowDownOutlined } from '@ant-design/icons';
|
import { ArrowDownOutlined } from '@ant-design/icons';
|
||||||
import { ProFormDependency, ProFormSelect } from '@ant-design/pro-components';
|
import { ProFormDependency, ProFormSelect } from '@ant-design/pro-components';
|
||||||
import { Divider, Tag } from 'antd';
|
import { Divider, Flex, Tag, Typography } from 'antd';
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect, useMemo } from 'react';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
import { MultiCopyPopupContext } from '../MultiCopyPopup';
|
import { MultiCopyPopupContext } from '../context';
|
||||||
import RuleCheckCard from './RuleCheckCard';
|
import RuleCheckCard from './RuleCheckCard';
|
||||||
|
|
||||||
type Step1Props = {
|
type Step1Props = {
|
||||||
@@ -13,11 +13,52 @@ type Step1Props = {
|
|||||||
const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => {
|
const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => {
|
||||||
const { dataSourceList } = useContext(MultiCopyPopupContext);
|
const { dataSourceList } = useContext(MultiCopyPopupContext);
|
||||||
|
|
||||||
|
const buildOptions = (sourceId?: string | number) => {
|
||||||
|
const sourceType = dataSourceList.find((item) => item.id === sourceId)
|
||||||
|
?.sourceType;
|
||||||
|
return dataSourceList.map((item) => ({
|
||||||
|
...item,
|
||||||
|
disabled: item.sourceType !== sourceType,
|
||||||
|
label: (
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<span>{item.sourceName}</span>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: 12, marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
MT{item.sourceType}
|
||||||
|
</Typography.Text>
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const sourceOptions = useMemo(
|
||||||
|
() => dataSourceList.map((item) => ({
|
||||||
|
...item,
|
||||||
|
label: (
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<span>{item.sourceName}</span>
|
||||||
|
<Typography.Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: 12, marginLeft: 8 }}
|
||||||
|
>
|
||||||
|
MT{item.sourceType}
|
||||||
|
</Typography.Text>
|
||||||
|
</Flex>
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
[dataSourceList],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dataSourceList?.length) {
|
if (dataSourceList?.length) {
|
||||||
// waitTime(1000).then(() => {
|
const sourceItem = dataSourceList[0];
|
||||||
const sourceDataSourceId = dataSourceList[0]?.id || '';
|
const sourceDataSourceId = sourceItem?.id || '';
|
||||||
const targetDataSourceId = dataSourceList[1]?.id || '';
|
const sameTypeItem = dataSourceList.find(
|
||||||
|
(item) => item.id !== sourceDataSourceId && item.sourceType === sourceItem?.sourceType,
|
||||||
|
);
|
||||||
|
const targetDataSourceId = sameTypeItem?.id || sourceDataSourceId;
|
||||||
const curFormRef = formMapRef?.current?.[0]?.current;
|
const curFormRef = formMapRef?.current?.[0]?.current;
|
||||||
if (curFormRef) {
|
if (curFormRef) {
|
||||||
curFormRef?.setFieldsValue({
|
curFormRef?.setFieldsValue({
|
||||||
@@ -26,7 +67,6 @@ const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => {
|
|||||||
});
|
});
|
||||||
setTargetDataSourceId(targetDataSourceId);
|
setTargetDataSourceId(targetDataSourceId);
|
||||||
}
|
}
|
||||||
// });
|
|
||||||
}
|
}
|
||||||
}, [dataSourceList]);
|
}, [dataSourceList]);
|
||||||
|
|
||||||
@@ -35,14 +75,15 @@ const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => {
|
|||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
name="sourceDataSourceId"
|
name="sourceDataSourceId"
|
||||||
label={$t('pages.clones.step1.source')}
|
label={$t('pages.clones.step1.source')}
|
||||||
options={dataSourceList}
|
options={sourceOptions}
|
||||||
rules={[{ required: true, message: $t('form.required') }]}
|
rules={[{ required: true, message: $t('form.required') }]}
|
||||||
fieldProps={{
|
fieldProps={{
|
||||||
allowClear: false,
|
allowClear: false,
|
||||||
fieldNames: {
|
fieldNames: {
|
||||||
label: 'sourceName',
|
label: 'label',
|
||||||
value: 'id',
|
value: 'id',
|
||||||
},
|
},
|
||||||
|
optionLabelProp: 'label',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<ProFormDependency name={['sourceDataSourceId']}>
|
<ProFormDependency name={['sourceDataSourceId']}>
|
||||||
@@ -61,21 +102,20 @@ const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => {
|
|||||||
<ProFormDependency name={['sourceDataSourceId']}>
|
<ProFormDependency name={['sourceDataSourceId']}>
|
||||||
{({ sourceDataSourceId }) => {
|
{({ sourceDataSourceId }) => {
|
||||||
if (!sourceDataSourceId) return null;
|
if (!sourceDataSourceId) return null;
|
||||||
const dataSourceOptions = dataSourceList.filter(
|
const targetOptions = buildOptions(sourceDataSourceId);
|
||||||
(item) => item.id !== sourceDataSourceId,
|
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<ProFormSelect
|
<ProFormSelect
|
||||||
name="targetDataSourceId"
|
name="targetDataSourceId"
|
||||||
label={$t('pages.clones.step1.target')}
|
label={$t('pages.clones.step1.target')}
|
||||||
options={dataSourceOptions}
|
options={targetOptions}
|
||||||
rules={[{ required: true, message: $t('form.required') }]}
|
rules={[{ required: true, message: $t('form.required') }]}
|
||||||
fieldProps={{
|
fieldProps={{
|
||||||
allowClear: false,
|
allowClear: false,
|
||||||
fieldNames: {
|
fieldNames: {
|
||||||
label: 'sourceName',
|
label: 'label',
|
||||||
value: 'id',
|
value: 'id',
|
||||||
},
|
},
|
||||||
|
optionLabelProp: 'label',
|
||||||
}}
|
}}
|
||||||
onChange={(value: string | number | null) => {
|
onChange={(value: string | number | null) => {
|
||||||
setTargetDataSourceId(value || null);
|
setTargetDataSourceId(value || null);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { useContext, useEffect, useMemo, useState } from 'react';
|
import { useContext, useEffect, useMemo, useState } from 'react';
|
||||||
import { $t } from '@/utils/i18n';
|
import { $t } from '@/utils/i18n';
|
||||||
import { RuleCardBodyInfo } from '../../RuleCardBody';
|
import { RuleCardBodyInfo } from '../../RuleCardBody';
|
||||||
import { MultiCopyPopupContext } from '../MultiCopyPopup';
|
import { MultiCopyPopupContext } from '../context';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
|||||||
21
src/pages/rules/components/CopyRule/context.ts
Normal file
21
src/pages/rules/components/CopyRule/context.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
type MultiCopyPopupContextValue = {
|
||||||
|
ruleMeta: Record<string, any>;
|
||||||
|
dataSourceList: API.DataSourceListItem[];
|
||||||
|
currentRuleType: number;
|
||||||
|
targetDataSourceId: string | number | null;
|
||||||
|
selectRules: API.RuleListItem[];
|
||||||
|
setSelectRules: (rules: API.RuleListItem[]) => void;
|
||||||
|
currentSchemaMap: Record<string, any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultiCopyPopupContext = createContext<MultiCopyPopupContextValue>({
|
||||||
|
ruleMeta: {},
|
||||||
|
dataSourceList: [],
|
||||||
|
currentRuleType: 0,
|
||||||
|
targetDataSourceId: null,
|
||||||
|
selectRules: [],
|
||||||
|
setSelectRules: () => {},
|
||||||
|
currentSchemaMap: {},
|
||||||
|
});
|
||||||
@@ -50,7 +50,7 @@ const RuleCommon: React.FC = () => {
|
|||||||
const { ruleType: CURRENT_RULE_TYPE } = useRouteProps();
|
const { ruleType: CURRENT_RULE_TYPE } = useRouteProps();
|
||||||
const { getAllMetaByType } = useRuleMeta();
|
const { getAllMetaByType } = useRuleMeta();
|
||||||
const { userInfo } = useUserInfo();
|
const { userInfo } = useUserInfo();
|
||||||
const { isGlobalCompany } = useAccess();
|
const { isGlobalCompany, hasPerms } = useAccess();
|
||||||
const [rules, setRules] = useState<API.RuleListItem[]>([]);
|
const [rules, setRules] = useState<API.RuleListItem[]>([]);
|
||||||
const [alerts, setAlerts] = useState<API.AlertListItem[]>([]);
|
const [alerts, setAlerts] = useState<API.AlertListItem[]>([]);
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
@@ -253,62 +253,60 @@ const RuleCommon: React.FC = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isGlobalCompany && (
|
{!isGlobalCompany &&
|
||||||
<>
|
hasPerms('rule:edit') &&
|
||||||
<Divider style={{ margin: '14px 0' }} />
|
companyInfo?.validityStatus !== 2 && (
|
||||||
<Flex justify="end">
|
<>
|
||||||
<Space wrap style={{ justifyContent: 'flex-end' }}>
|
<Divider style={{ margin: '14px 0' }} />
|
||||||
<Button
|
<Flex justify="end">
|
||||||
variant="solid"
|
<Space wrap style={{ justifyContent: 'flex-end' }}>
|
||||||
color={rule.status === 2 ? 'cyan' : 'orange'}
|
<Button
|
||||||
disabled={
|
variant="solid"
|
||||||
![1, 2].includes(rule.status) || ruleStatusHandleLoading
|
color={rule.status === 2 ? 'cyan' : 'orange'}
|
||||||
}
|
disabled={
|
||||||
onClick={() => handleToggleStatus(rule)}
|
![1, 2].includes(rule.status) || ruleStatusHandleLoading
|
||||||
>
|
|
||||||
{rule.status === 1
|
|
||||||
? $t('pages.rules.disable')
|
|
||||||
: $t('pages.rules.enable')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={
|
|
||||||
![1, 2].includes(rule.status) || ruleStatusHandleLoading
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
setAction('edit');
|
|
||||||
setFormValues(rule);
|
|
||||||
setVisible(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{$t('table.action.edit')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="danger"
|
|
||||||
variant="solid"
|
|
||||||
disabled={
|
|
||||||
![1, 2].includes(rule.status) || ruleStatusHandleLoading
|
|
||||||
}
|
|
||||||
onClick={() => handleDelete(rule)}
|
|
||||||
>
|
|
||||||
{$t('table.action.delete')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
icon={<CopyOutlined />}
|
|
||||||
onClick={() => {
|
|
||||||
if (dataSourceList.length < 2) {
|
|
||||||
message.warning($t('pages.rules.cloneRuleWarning'));
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
setFormValues(rule);
|
onClick={() => handleToggleStatus(rule)}
|
||||||
setCopyVisible(true);
|
>
|
||||||
}}
|
{rule.status === 1
|
||||||
>
|
? $t('pages.rules.disable')
|
||||||
{$t('pages.rules.clone')}
|
: $t('pages.rules.enable')}
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
<Button
|
||||||
</Flex>
|
disabled={
|
||||||
</>
|
![1, 2].includes(rule.status) || ruleStatusHandleLoading
|
||||||
)}
|
}
|
||||||
|
onClick={() => {
|
||||||
|
setAction('edit');
|
||||||
|
setFormValues(rule);
|
||||||
|
setVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$t('table.action.edit')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
color="danger"
|
||||||
|
variant="solid"
|
||||||
|
disabled={
|
||||||
|
![1, 2].includes(rule.status) || ruleStatusHandleLoading
|
||||||
|
}
|
||||||
|
onClick={() => handleDelete(rule)}
|
||||||
|
>
|
||||||
|
{$t('table.action.delete')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => {
|
||||||
|
setFormValues(rule);
|
||||||
|
setCopyVisible(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{$t('pages.rules.clone')}
|
||||||
|
</Button>
|
||||||
|
</Space>
|
||||||
|
</Flex>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</ProCard>
|
</ProCard>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -438,34 +436,32 @@ const RuleCommon: React.FC = () => {
|
|||||||
icon={<RedoOutlined />}
|
icon={<RedoOutlined />}
|
||||||
onClick={() => refreshRules(1)}
|
onClick={() => refreshRules(1)}
|
||||||
/>
|
/>
|
||||||
{!isGlobalCompany && (
|
{!isGlobalCompany &&
|
||||||
<>
|
hasPerms('rule:edit') &&
|
||||||
<Button
|
companyInfo?.validityStatus !== 2 && (
|
||||||
key="1"
|
<>
|
||||||
type="primary"
|
<Button
|
||||||
onClick={() => {
|
key="1"
|
||||||
setAction('add');
|
type="primary"
|
||||||
setFormValues({});
|
onClick={() => {
|
||||||
setVisible(true);
|
setAction('add');
|
||||||
}}
|
setFormValues({});
|
||||||
>
|
setVisible(true);
|
||||||
<PlusOutlined />
|
}}
|
||||||
{`${$t('table.action.add')}${$t('menu.rule')}`}
|
>
|
||||||
</Button>
|
<PlusOutlined />
|
||||||
<Button
|
{`${$t('table.action.add')}${$t('menu.rule')}`}
|
||||||
icon={<CopyOutlined />}
|
</Button>
|
||||||
onClick={() => {
|
<Button
|
||||||
if (dataSourceList.length < 2) {
|
icon={<CopyOutlined />}
|
||||||
message.warning($t('pages.rules.cloneRuleWarning'));
|
onClick={() => {
|
||||||
return;
|
setMultiCopyVisible(true);
|
||||||
}
|
}}
|
||||||
setMultiCopyVisible(true);
|
>
|
||||||
}}
|
{$t('pages.clones.title')}
|
||||||
>
|
</Button>
|
||||||
{$t('pages.clones.title')}
|
</>
|
||||||
</Button>
|
)}
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Space>
|
</Space>
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ export const errorConfig: RequestConfig = {
|
|||||||
errorConfig: {
|
errorConfig: {
|
||||||
// Error reception and processing
|
// Error reception and processing
|
||||||
errorHandler: async (error: any) => {
|
errorHandler: async (error: any) => {
|
||||||
console.log('error', error);
|
|
||||||
const response = error?.response;
|
const response = error?.response;
|
||||||
const config = error?.config;
|
const config = error?.config;
|
||||||
if (response?.status === 401) {
|
if (response?.status === 401) {
|
||||||
@@ -58,15 +57,20 @@ export const errorConfig: RequestConfig = {
|
|||||||
const { data } = response as { data: API.BaseResponse };
|
const { data } = response as { data: API.BaseResponse };
|
||||||
const config = response?.config as RequestConfig & {
|
const config = response?.config as RequestConfig & {
|
||||||
skip417Error?: boolean;
|
skip417Error?: boolean;
|
||||||
|
skip401Refresh?: boolean;
|
||||||
};
|
};
|
||||||
const skip417Error = config?.skip417Error;
|
if (config?.skip417Error && data?.code === 417) {
|
||||||
if (skip417Error && data?.code === 417) {
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data?.success === false) {
|
if (data?.success === false) {
|
||||||
antdMessage.error(data?.message || 'Error');
|
antdMessage.error(data?.message || 'Error');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data?.code === 401 && !config?.skip401Refresh) {
|
||||||
|
window.dispatchEvent(new CustomEvent('auth:refresh'));
|
||||||
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -83,6 +83,8 @@ export async function companyHandle(
|
|||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
status?: number;
|
status?: number;
|
||||||
|
validityStartTime?: string | null;
|
||||||
|
validityEndTime?: string | null;
|
||||||
},
|
},
|
||||||
options?: ObjType,
|
options?: ObjType,
|
||||||
) {
|
) {
|
||||||
@@ -254,6 +256,7 @@ export async function getAlertPages(
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
params,
|
params,
|
||||||
skip417Error: true,
|
skip417Error: true,
|
||||||
|
skip401Refresh: true,
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -261,6 +264,7 @@ export async function getAlertDetail(id: number | string, options?: ObjType) {
|
|||||||
return request(`/alert-record/${id}`, {
|
return request(`/alert-record/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
skip417Error: true,
|
skip417Error: true,
|
||||||
|
skip401Refresh: true,
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -376,6 +380,7 @@ export async function getRulePages(
|
|||||||
export async function getRuleDetail(id: number | string, options?: ObjType) {
|
export async function getRuleDetail(id: number | string, options?: ObjType) {
|
||||||
return request(`/rule/${id}`, {
|
return request(`/rule/${id}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
skip401Refresh: true,
|
||||||
...(options || {}),
|
...(options || {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -609,6 +614,12 @@ export async function closeSSEWithApi(options?: ObjType) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPlatformList(options?: ObjType) {
|
||||||
|
return request("/system/platform-list", {
|
||||||
|
method: "GET",
|
||||||
|
...(options || {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
export async function getDashboardTotal(options?: ObjType) {
|
export async function getDashboardTotal(options?: ObjType) {
|
||||||
return request("/dashboard/total", {
|
return request("/dashboard/total", {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
|
|||||||
10
src/services/typings.d.ts
vendored
10
src/services/typings.d.ts
vendored
@@ -113,6 +113,16 @@ declare namespace API {
|
|||||||
fromName: string | null;
|
fromName: string | null;
|
||||||
mailHostConfigId: number | string | null;
|
mailHostConfigId: number | string | null;
|
||||||
mailStatus: number | null;
|
mailStatus: number | null;
|
||||||
|
validityStartTime?: string | null;
|
||||||
|
validityEndTime?: string | null;
|
||||||
|
validityStatus?: number | null;
|
||||||
|
|
||||||
|
mt4Count: number;
|
||||||
|
mt4Open: number;
|
||||||
|
mt5Count: number;
|
||||||
|
mt5Open: number;
|
||||||
|
sourceType4Count: number;
|
||||||
|
sourceType5Count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// "DataSource" Type
|
// "DataSource" Type
|
||||||
|
|||||||
61
src/utils/routePermissions.ts
Normal file
61
src/utils/routePermissions.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { commonRoutes, roleRoutes } from '../../config/routes';
|
||||||
|
|
||||||
|
interface RouteItem {
|
||||||
|
path?: string;
|
||||||
|
access?: string;
|
||||||
|
meta?: { permissions?: string[] };
|
||||||
|
routes?: RouteItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenPermissions(
|
||||||
|
routes: RouteItem[],
|
||||||
|
parentPath = '',
|
||||||
|
inheritedPermissions?: string[],
|
||||||
|
): Map<string, string[]> {
|
||||||
|
const map = new Map<string, string[]>();
|
||||||
|
for (const route of routes) {
|
||||||
|
const fullPath = parentPath + (route.path || '');
|
||||||
|
const ownPermissions =
|
||||||
|
route.access === 'normalRouteFilter' && route.meta?.permissions?.length
|
||||||
|
? route.meta.permissions
|
||||||
|
: undefined;
|
||||||
|
const effectivePermissions = ownPermissions ?? inheritedPermissions;
|
||||||
|
|
||||||
|
if (effectivePermissions?.length) {
|
||||||
|
map.set(fullPath, effectivePermissions);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.routes) {
|
||||||
|
const children = flattenPermissions(
|
||||||
|
route.routes,
|
||||||
|
`${fullPath}/`,
|
||||||
|
effectivePermissions,
|
||||||
|
);
|
||||||
|
for (const [path, perms] of children) {
|
||||||
|
map.set(path, perms);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const routePermissions = flattenPermissions([
|
||||||
|
...commonRoutes,
|
||||||
|
...roleRoutes,
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Look up the required permissions for a pathname.
|
||||||
|
* Falls back to parent paths if the exact path isn't found (e.g. /rules/liquidity-trade → /rules).
|
||||||
|
*/
|
||||||
|
export function getRequiredPermissions(pathname: string): string[] | undefined {
|
||||||
|
let path = pathname.replace(/\/$/, '') || '/';
|
||||||
|
while (path) {
|
||||||
|
const perms = routePermissions.get(path);
|
||||||
|
if (perms) return perms;
|
||||||
|
const i = path.lastIndexOf('/');
|
||||||
|
if (i <= 0) break;
|
||||||
|
path = path.slice(0, i);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
13
src/wrappers/CompanyExpire.tsx
Normal file
13
src/wrappers/CompanyExpire.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Outlet, useAccess } from '@umijs/max';
|
||||||
|
import CompanyExpireGuard from '@/components/CompanyExpireGuard';
|
||||||
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
const { isGlobalCompany } = useAccess();
|
||||||
|
const { userInfo } = useUserInfo();
|
||||||
|
const { company } = userInfo || {};
|
||||||
|
if (!isGlobalCompany && company?.validityStatus === 2) {
|
||||||
|
return <CompanyExpireGuard fullscreen={false} />;
|
||||||
|
}
|
||||||
|
return <Outlet />;
|
||||||
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"ignoreDeprecations": "6.0",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"target": "esnext",
|
"target": "esnext",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
|
|||||||
Reference in New Issue
Block a user