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:
2026-06-12 17:57:25 +08:00
parent 43d6e9207a
commit dfcba94cf5
13 changed files with 178 additions and 35 deletions

View File

@@ -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': '',

View File

@@ -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 };
} }

View File

@@ -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':

View File

@@ -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':

View File

@@ -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',
},
], ],
}, },
}, },

View File

@@ -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 }) => {

View File

@@ -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>

View File

@@ -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',

View File

@@ -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 && (

View File

@@ -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,

View File

@@ -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",

View File

@@ -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

View File

@@ -1,5 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
"ignoreDeprecations": "6.0",
"baseUrl": "./", "baseUrl": "./",
"target": "esnext", "target": "esnext",
"moduleResolution": "node", "moduleResolution": "node",