feat(platform): add platform access control with quota management
- Add userPlatform hook to derive available platforms from company config - Add platform access section in company popup with MT4/MT5 switch and quota limits - Add platform access column in company list showing quota usage - Replace hardcoded platform options with dynamic userPlatform in account/alert/data-source pages - Add getPlatformList API and extend CompanyListItem typings - Add i18n translations for platform access related keys - Update dev proxy target port to 8082 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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': '',
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
@@ -603,6 +604,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':
|
||||||
|
|||||||
@@ -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': '操作动作',
|
||||||
@@ -569,6 +570,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;
|
||||||
|
|
||||||
@@ -257,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 }) => {
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
ModalForm,
|
ModalForm,
|
||||||
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 { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import useUserInfo from '@/hooks/useUserInfo';
|
import useUserInfo from '@/hooks/useUserInfo';
|
||||||
import {
|
import {
|
||||||
@@ -110,6 +111,75 @@ export default (props: {
|
|||||||
{formValues?.id !== void 0 && (
|
{formValues?.id !== void 0 && (
|
||||||
<>
|
<>
|
||||||
<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,7 @@ 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 { Badge, Button, Col, Row, Space } from 'antd';
|
||||||
import React, { useRef, useState } from 'react';
|
import React, { useRef, useState } from 'react';
|
||||||
import HeadStatisticCard, {
|
import HeadStatisticCard, {
|
||||||
type CardInfo,
|
type CardInfo,
|
||||||
@@ -69,6 +69,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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -612,6 +612,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",
|
||||||
|
|||||||
7
src/services/typings.d.ts
vendored
7
src/services/typings.d.ts
vendored
@@ -113,6 +113,13 @@ declare namespace API {
|
|||||||
fromName: string | null;
|
fromName: string | null;
|
||||||
mailHostConfigId: number | string | null;
|
mailHostConfigId: number | string | null;
|
||||||
mailStatus: number | null;
|
mailStatus: number | null;
|
||||||
|
|
||||||
|
mt4Count: number;
|
||||||
|
mt4Open: number;
|
||||||
|
mt5Count: number;
|
||||||
|
mt5Open: number;
|
||||||
|
sourceType4Count: number;
|
||||||
|
sourceType5Count: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
// "DataSource" Type
|
// "DataSource" Type
|
||||||
|
|||||||
@@ -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