From dfcba94cf5e231f0f43b2ac6f8465848618dc855 Mon Sep 17 00:00:00 2001 From: Jhonton Chen <504071088@qq.com> Date: Fri, 12 Jun 2026 17:57:25 +0800 Subject: [PATCH 1/3] 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 --- config/proxy.ts | 3 +- src/hooks/useUserInfo.ts | 22 +++++++++- src/locales/en_US/pages.ts | 5 +++ src/locales/zh_CN/pages.ts | 5 +++ src/pages/account/index.tsx | 11 +---- src/pages/alert/index.tsx | 11 +---- src/pages/company/Popup.tsx | 72 ++++++++++++++++++++++++++++++++- src/pages/company/index.tsx | 42 ++++++++++++++++++- src/pages/data-source/Popup.tsx | 26 ++++++------ src/pages/data-source/index.tsx | 2 + src/services/api/api.ts | 6 +++ src/services/typings.d.ts | 7 ++++ tsconfig.json | 1 + 13 files changed, 178 insertions(+), 35 deletions(-) diff --git a/config/proxy.ts b/config/proxy.ts index 5bfce15..ba63b71 100644 --- a/config/proxy.ts +++ b/config/proxy.ts @@ -15,7 +15,8 @@ export default { '/api': { // The address to proxy // 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, pathRewrite: { '^/api': '', diff --git a/src/hooks/useUserInfo.ts b/src/hooks/useUserInfo.ts index d399cbe..d28a06b 100644 --- a/src/hooks/useUserInfo.ts +++ b/src/hooks/useUserInfo.ts @@ -1,4 +1,5 @@ import { useModel } from '@umijs/max'; +import { useMemo } from 'react'; export default function useUserInfo() { const { initialState, setInitialState } = useModel('@@initialState'); @@ -14,5 +15,24 @@ export default function useUserInfo() { 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 }; } diff --git a/src/locales/en_US/pages.ts b/src/locales/en_US/pages.ts index d87314e..f1606ea 100644 --- a/src/locales/en_US/pages.ts +++ b/src/locales/en_US/pages.ts @@ -2,6 +2,7 @@ export default { 'common.loading': 'Loading...', 'common.content.noRules': 'No rules available', 'common.content.noData': 'No data available', + 'table.platformAccess': 'Platform Quota', 'table.group': 'Group', 'table.operatorRecords': 'Operator Records', 'table.operatorAction': 'Operator Action', @@ -603,6 +604,10 @@ export default { 'Enter verified sender emails (comma separated)', 'pages.email.status.normal': 'Active', '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.notificationEmail': 'Notification Email', 'pages.email.notificationEmail.tips': diff --git a/src/locales/zh_CN/pages.ts b/src/locales/zh_CN/pages.ts index 7f1c0b8..0be036b 100644 --- a/src/locales/zh_CN/pages.ts +++ b/src/locales/zh_CN/pages.ts @@ -2,6 +2,7 @@ export default { 'common.loading': '加载中...', 'common.content.noRules': '暂无规则', 'common.content.noData': '暂无数据', + 'table.platformAccess': '平台配额', 'table.group': '所属组', 'table.operatorRecords': '操作记录', 'table.operatorAction': '操作动作', @@ -569,6 +570,10 @@ export default { 'pages.email.availableEmails.tips': '在服务商已验证的邮箱,多个用逗号分隔', 'pages.email.status.normal': '正常', 'pages.email.status.disabled': '停用', + 'pages.email.platformAccess': '平台接入权限', + 'pages.email.platformAccessEnable': '接入', + 'pages.email.platformMax': '最大数量上限', + 'pages.email.platformAssigned': '已配', 'pages.email.notification': '邮件通知配置', 'pages.email.notificationEmail': '通知邮箱', 'pages.email.notificationEmail.tips': diff --git a/src/pages/account/index.tsx b/src/pages/account/index.tsx index 71fd260..85360a2 100644 --- a/src/pages/account/index.tsx +++ b/src/pages/account/index.tsx @@ -21,7 +21,7 @@ const maxAlertHideCount = 9999; const Account: React.FC = () => { const { isGlobalCompany } = useAccess(); - const { userInfo } = useUserInfo(); + const { userInfo, userPlatform } = useUserInfo(); const companyInfo = userInfo?.company; const [visible, setVisible] = useState(false); @@ -122,14 +122,7 @@ const Account: React.FC = () => { label: $t('table.all'), value: '', }, - // { - // label: 'MT4', - // value: '4', - // }, - { - label: 'MT5', - value: '5', - }, + ...userPlatform, ], }, }, diff --git a/src/pages/alert/index.tsx b/src/pages/alert/index.tsx index 236fabc..7a03bc1 100644 --- a/src/pages/alert/index.tsx +++ b/src/pages/alert/index.tsx @@ -64,7 +64,7 @@ const Alert: React.FC = () => { const [dataSourceList, setDataSourceList] = useState< API.DataSourceListItem[] >([]); - const { userInfo } = useUserInfo(); + const { userInfo, userPlatform } = useUserInfo(); const userDataSourceList = userInfo?.userDataSourceList; const companyInfo = userInfo?.company; @@ -257,14 +257,7 @@ const Alert: React.FC = () => { label: $t('table.all'), value: '', }, - // { - // label: 'MT4', - // value: 4, - // }, - { - label: 'MT5', - value: 5, - }, + ...userPlatform, ], }, render: (_, { platform }) => { diff --git a/src/pages/company/Popup.tsx b/src/pages/company/Popup.tsx index 8c7dc45..cd086a3 100644 --- a/src/pages/company/Popup.tsx +++ b/src/pages/company/Popup.tsx @@ -1,13 +1,14 @@ import { ModalForm, ProFormDependency, + ProFormDigit, type ProFormInstance, ProFormSelect, ProFormSwitch, ProFormText, } from '@ant-design/pro-components'; 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 useUserInfo from '@/hooks/useUserInfo'; import { @@ -110,6 +111,75 @@ export default (props: { {formValues?.id !== void 0 && ( <> + + + ⚙️ {$t('pages.email.platformAccess')} + + + + +value} + /> + } + styles={{ + header: { fontSize: 14, minHeight: 40, paddingInline: 14 }, + body: { paddingInline: 14 }, + }} + > + + {({ mt4Open }) => ( + + )} + + + + + +value} + /> + } + styles={{ + header: { fontSize: 14, minHeight: 40, paddingInline: 14 }, + body: { paddingInline: 14 }, + }} + > + + {({ mt5Open }) => ( + + )} + + + + + {/* */} 📧 {$t('pages.email.notification')} diff --git a/src/pages/company/index.tsx b/src/pages/company/index.tsx index 2ef314d..f74bf6b 100644 --- a/src/pages/company/index.tsx +++ b/src/pages/company/index.tsx @@ -5,7 +5,7 @@ import { type ProColumns, ProTable, } 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 HeadStatisticCard, { type CardInfo, @@ -69,6 +69,46 @@ const Company: React.FC = () => { ); }, }, + { + title: $t('table.platformAccess'), + dataIndex: 'platformAccess', + render: ( + _, + { + mt4Count, + mt4Open, + mt5Count, + mt5Open, + sourceType4Count, + sourceType5Count, + }, + ) => { + return ( + + + MT4:{' '} + {mt4Open === 1 ? ( + + {sourceType4Count ?? '-'} / {mt4Count ?? '-'} + + ) : ( + $t('pages.rules.disable') + )} + + + MT5:{' '} + {mt5Open === 1 ? ( + + {sourceType5Count ?? '-'} / {mt5Count ?? '-'} + + ) : ( + $t('pages.rules.disable') + )} + + + ); + }, + }, { title: $t('table.userCount'), dataIndex: 'userCount', diff --git a/src/pages/data-source/Popup.tsx b/src/pages/data-source/Popup.tsx index d908de8..92f0ca3 100644 --- a/src/pages/data-source/Popup.tsx +++ b/src/pages/data-source/Popup.tsx @@ -6,7 +6,7 @@ import { } from '@ant-design/pro-components'; import { useAccess } from '@umijs/max'; import { Divider, type FormInstance, Typography } from 'antd'; -import { useRef } from 'react'; +import { useMemo, useRef } from 'react'; import useUserInfo from '@/hooks/useUserInfo'; import { preventScientificNotation } from '@/utils'; import { $t } from '@/utils/i18n'; @@ -22,7 +22,7 @@ export default (props: { }) => { const { visible, formValues, onSubmit, onDone, companyList } = props; - const { userInfo } = useUserInfo(); + const { userInfo, userPlatform } = useUserInfo(); const { isGlobalCompany } = useAccess(); const globalCompanyId = isGlobalCompany ? userInfo?.companyId : 0; const formRef = useRef(null); @@ -41,6 +41,12 @@ export default (props: { message: $t('form.placeholder.ipAddress'), }, ]; + + const filteredUserPlatform = useMemo( + () => userPlatform.filter((v) => v?.isOpen), + [userPlatform], + ); + return ( 0 + ? filteredUserPlatform[0].value + : null, } } open={visible} @@ -78,16 +87,7 @@ export default (props: { label={$t('form.dataSource.type')} allowClear={false} rules={[{ required: true, message: $t('form.required') }]} - options={[ - // { - // value: 4, - // label: 'MT4', - // }, - { - value: 5, - label: 'MT5', - }, - ]} + options={filteredUserPlatform} /> {isGlobalCompany && ( diff --git a/src/pages/data-source/index.tsx b/src/pages/data-source/index.tsx index 1704abc..f2a174a 100644 --- a/src/pages/data-source/index.tsx +++ b/src/pages/data-source/index.tsx @@ -18,6 +18,7 @@ import { dataSourceHandle, getCompanyList, getDataSourcePages, + getPlatformList, } from '@/services/api'; import { $t } from '@/utils/i18n'; import Popup from './Popup'; @@ -49,6 +50,7 @@ const DataSource: React.FC = () => { setCompanyList(data || []); }); } + getPlatformList(); }, []); const [cardInfo, setCardInfo] = useState({ // mt4Count: 0, diff --git a/src/services/api/api.ts b/src/services/api/api.ts index acc4cc7..c230238 100644 --- a/src/services/api/api.ts +++ b/src/services/api/api.ts @@ -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) { return request("/dashboard/total", { method: "GET", diff --git a/src/services/typings.d.ts b/src/services/typings.d.ts index b4f7866..6db8e2d 100644 --- a/src/services/typings.d.ts +++ b/src/services/typings.d.ts @@ -113,6 +113,13 @@ declare namespace API { fromName: string | null; mailHostConfigId: number | string | null; mailStatus: number | null; + + mt4Count: number; + mt4Open: number; + mt5Count: number; + mt5Open: number; + sourceType4Count: number; + sourceType5Count: number; }; // "DataSource" Type diff --git a/tsconfig.json b/tsconfig.json index 3635ece..bbc395e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "ignoreDeprecations": "6.0", "baseUrl": "./", "target": "esnext", "moduleResolution": "node", From 16999ca583c60b59763dc5019cc51d572477bcbb Mon Sep 17 00:00:00 2001 From: Jhonton Chen <504071088@qq.com> Date: Sun, 14 Jun 2026 21:39:01 +0800 Subject: [PATCH 2/3] feat(clone): allow same-source cloning and disable cross-sourceType targets - Remove the restriction that prevents cloning to the same data source - Disable (instead of hide) target options with different sourceType - Add sourceType label (MT4/MT5) to data source select options - Preserve groupFilter/param1/param2 when cloning to same source - Auto-reset target when source changes to incompatible sourceType - Remove parent-level check requiring at least 2 data sources Co-Authored-By: Claude Opus 4.7 --- .../components/CopyRule/Multi‌CopyPopup.tsx | 32 ++-- .../components/CopyRule/SingleCopyPopup.tsx | 165 +++++++++++------- .../rules/components/CopyRule/comp/Step1.tsx | 66 +++++-- src/pages/rules/index.tsx | 8 - 4 files changed, 174 insertions(+), 97 deletions(-) diff --git a/src/pages/rules/components/CopyRule/Multi‌CopyPopup.tsx b/src/pages/rules/components/CopyRule/Multi‌CopyPopup.tsx index 82291b8..c8a16cb 100644 --- a/src/pages/rules/components/CopyRule/Multi‌CopyPopup.tsx +++ b/src/pages/rules/components/CopyRule/Multi‌CopyPopup.tsx @@ -232,22 +232,22 @@ const MultiCopyPopup = ({ onValuesChange={({ sourceDataSourceId }) => { if (sourceDataSourceId) { const curFormRef = formMapRef?.current?.[0]?.current; - const curTargetDataSourceId = - curFormRef?.getFieldValue('targetDataSourceId'); - if (curTargetDataSourceId === sourceDataSourceId) { - let targetDataSourceId: string | number | null = null; - for (const item of dataSourceList) { - if (item.id !== sourceDataSourceId) { - targetDataSourceId = item.id; - break; - } - } - if (targetDataSourceId) { - curFormRef?.setFieldsValue({ - targetDataSourceId, - }); - setTargetDataSourceId(targetDataSourceId); - } + const curTargetId = curFormRef?.getFieldValue('targetDataSourceId'); + const sourceItem = dataSourceList.find( + (item) => item.id === sourceDataSourceId, + ); + const targetItem = dataSourceList.find( + (item) => item.id === curTargetId, + ); + if (targetItem && sourceItem && targetItem.sourceType !== sourceItem.sourceType) { + const fallback = + dataSourceList.find( + (item) => + item.id !== sourceDataSourceId && + item.sourceType === sourceItem.sourceType, + )?.id || sourceDataSourceId; + curFormRef?.setFieldsValue({ targetDataSourceId: fallback }); + setTargetDataSourceId(fallback); } } }} diff --git a/src/pages/rules/components/CopyRule/SingleCopyPopup.tsx b/src/pages/rules/components/CopyRule/SingleCopyPopup.tsx index e096366..6a34012 100644 --- a/src/pages/rules/components/CopyRule/SingleCopyPopup.tsx +++ b/src/pages/rules/components/CopyRule/SingleCopyPopup.tsx @@ -17,7 +17,7 @@ import { Row, Typography, } from 'antd'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import useRichFormat from '@/hooks/useRichI18n'; import { $t } from '@/utils/i18n'; import { type SchemaType, schemaMap } from '../../ruleFormSchema'; @@ -68,49 +68,83 @@ const SingleCopyPopup = ({ } = schemaMap[currentRuleType as unknown as SchemaType]?.({ richFormat }) || {}; - const rightDataSourceList = dataSourceList.filter( - (item) => item.id !== formValues?.dataSourceId, - ); + const sourceSourceType = useMemo(() => { + 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: ( + + {item.sourceName} + + MT{item.sourceType} + + + ), + })); + }, [dataSourceList, sourceSourceType]); 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 = { ...initialValues, name: initialValues?.name ? `${initialValues.name} - ${rightFirstDataSource?.sourceName}` : `${Date.now()} - ${rightFirstDataSource?.sourceName}`, - groupFilter: '', id: void 0, dataSourceId: rightFirstDataSource?.id || '', + ...(isSameSource + ? {} + : { + groupFilter: '', + ...(symbolFormShow ? { param2: '' } : {}), + ...(assetFormShow ? { param1: '' } : {}), + }), }; - if (symbolFormShow) { - copyFormValues.param2 = ''; - } - if (assetFormShow) { - copyFormValues.param1 = ''; - } const formContent = ({ - dataSourceList, + dataSourceList: options, formAttr, + onDataSourceChange, }: { - dataSourceList: API.DataSourceListItem[]; + dataSourceList: any[]; formAttr: Record; + onDataSourceChange?: (dataSourceId: string | number) => void; }) => { return ( - + { + if (changedValues.dataSourceId && onDataSourceChange) { + onDataSourceChange(changedValues.dataSourceId); + } + }} + > @@ -194,53 +228,64 @@ const SingleCopyPopup = ({ }, }} > - - -
- 📋 {$t('pages.rules.originalRule')} -
- {formContent({ - dataSourceList, - formAttr: { initialValues, disabled: true }, - })} - + {visible && ( + + +
+ 📋 {$t('pages.rules.originalRule')} +
+ {formContent({ + dataSourceList: dataSourceOptions, + formAttr: { initialValues, disabled: true }, + })} + - - - - - - + + + + + + - -
- -
- + +
+ +
+ - -
- ✏️ {$t('pages.rules.cloneRulePreview')} -
+ +
+ ✏️ {$t('pages.rules.cloneRulePreview')} +
- {formContent({ - dataSourceList: rightDataSourceList, - formAttr: { - initialValues: copyFormValues, - formRef: formRef, - disabled: confirmLoading, - }, - })} - {errMsg && ( - - )} - -
+ {formContent({ + dataSourceList: dataSourceOptions, + formAttr: { + initialValues: copyFormValues, + formRef: formRef, + disabled: confirmLoading, + }, + onDataSourceChange: (dataSourceId) => { + const isSame = dataSourceId === formValues?.dataSourceId; + if (!isSame) { + const fields: Record = { groupFilter: '' }; + if (symbolFormShow) fields.param2 = ''; + if (assetFormShow) fields.param1 = ''; + formRef.current?.setFieldsValue(fields); + } + }, + })} + {errMsg && ( + + )} + +
+ )} ); }; diff --git a/src/pages/rules/components/CopyRule/comp/Step1.tsx b/src/pages/rules/components/CopyRule/comp/Step1.tsx index c897168..1b8293e 100644 --- a/src/pages/rules/components/CopyRule/comp/Step1.tsx +++ b/src/pages/rules/components/CopyRule/comp/Step1.tsx @@ -1,7 +1,7 @@ import { ArrowDownOutlined } from '@ant-design/icons'; import { ProFormDependency, ProFormSelect } from '@ant-design/pro-components'; -import { Divider, Tag } from 'antd'; -import { useContext, useEffect } from 'react'; +import { Divider, Flex, Tag, Typography } from 'antd'; +import { useContext, useEffect, useMemo } from 'react'; import { $t } from '@/utils/i18n'; import { MultiCopyPopupContext } from '../Multi‌CopyPopup'; import RuleCheckCard from './RuleCheckCard'; @@ -13,11 +13,52 @@ type Step1Props = { const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => { 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: ( + + {item.sourceName} + + MT{item.sourceType} + + + ), + })); + }; + + const sourceOptions = useMemo( + () => dataSourceList.map((item) => ({ + ...item, + label: ( + + {item.sourceName} + + MT{item.sourceType} + + + ), + })), + [dataSourceList], + ); + useEffect(() => { if (dataSourceList?.length) { - // waitTime(1000).then(() => { - const sourceDataSourceId = dataSourceList[0]?.id || ''; - const targetDataSourceId = dataSourceList[1]?.id || ''; + const sourceItem = dataSourceList[0]; + const sourceDataSourceId = sourceItem?.id || ''; + const sameTypeItem = dataSourceList.find( + (item) => item.id !== sourceDataSourceId && item.sourceType === sourceItem?.sourceType, + ); + const targetDataSourceId = sameTypeItem?.id || sourceDataSourceId; const curFormRef = formMapRef?.current?.[0]?.current; if (curFormRef) { curFormRef?.setFieldsValue({ @@ -26,7 +67,6 @@ const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => { }); setTargetDataSourceId(targetDataSourceId); } - // }); } }, [dataSourceList]); @@ -35,14 +75,15 @@ const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => { @@ -61,21 +102,20 @@ const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => { {({ sourceDataSourceId }) => { if (!sourceDataSourceId) return null; - const dataSourceOptions = dataSourceList.filter( - (item) => item.id !== sourceDataSourceId, - ); + const targetOptions = buildOptions(sourceDataSourceId); return ( { setTargetDataSourceId(value || null); diff --git a/src/pages/rules/index.tsx b/src/pages/rules/index.tsx index b986537..bae3da6 100644 --- a/src/pages/rules/index.tsx +++ b/src/pages/rules/index.tsx @@ -295,10 +295,6 @@ const RuleCommon: React.FC = () => {