This commit is contained in:
2026-04-24 20:30:52 +08:00
commit 704bc34625
152 changed files with 45612 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
import { ProFormTreeSelect } from '@ant-design/pro-components';
import React, { useEffect, useState } from 'react';
import useUserInfo from '@/hooks/useUserInfo';
import Loading from '@/loading';
import { getAssetsTreeList } from '@/utils/dataCenter';
import { $t } from '@/utils/i18n';
interface AssetsSelectProps {
dataSourceId: number;
value?: string; // Pattern string passed in from outside
onChange?: (val: string) => void; // Callback to pass the Pattern string back to the outside
disabled?: boolean;
}
const AssetsSelect: React.FC<AssetsSelectProps> = ({
dataSourceId,
value,
onChange,
}) => {
const { userInfo } = useUserInfo();
const companyInfo = userInfo?.company;
const [treeData, setTreeData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// Internally maintains a keys array for TreeSelect display
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
const loadData = async (force: boolean = false) => {
setLoading(true);
try {
const data = await getAssetsTreeList(companyInfo, dataSourceId, force);
setCheckedKeys([]);
setTreeData(data);
} finally {
setLoading(false);
}
};
// 1. Listen for changes to dataSourceId and request source data
useEffect(() => {
if (!dataSourceId) {
setTreeData([]);
return;
}
loadData();
}, [dataSourceId]);
// 2. When treeData is loaded and there is an initial Pattern string, convert it to internal keys
useEffect(() => {
if (!value) {
setCheckedKeys([]);
} else if (treeData.length > 0 && typeof value === 'string') {
const keys = value.split(',');
setCheckedKeys(keys);
}
}, [value, treeData]);
// 3. When users manually check/uncheck items, convert internal keys back to string and trigger onChange
const handleTreeChange = (keys: any) => {
// Handle the object format that antd tree may return
const actualKeys = Array.isArray(keys) ? keys : keys.checked;
// Update internal UI state
setCheckedKeys(actualKeys);
if (onChange) {
onChange(actualKeys.join(','));
}
};
if (!dataSourceId) return null;
if (loading) return <Loading padding="0" />;
return (
<div className="addon-select-wrapper">
<ProFormTreeSelect
label={`${$t('pages.rule5_1.item1')} (${$t('pages.tips.emptyIsAll')})`}
extra={$t('form.rule1.tips2')}
placeholder={`${$t('pages.rules.allAssets')}`}
fieldProps={{
listHeight: 410,
treeData: treeData,
treeCheckable: true,
value: checkedKeys, // Use the internally parsed array
onChange: handleTreeChange,
maxTagCount: 'responsive',
virtual: true,
fieldNames: {
label: 'title',
value: 'value',
},
}}
formItemProps={{
style: { marginBottom: 0 },
}}
/>
</div>
);
};
export default AssetsSelect;

View File

@@ -0,0 +1,319 @@
import {
type ProFormInstance,
StepsForm,
useBreakpoint,
} from '@ant-design/pro-components';
import { Button, Divider, Flex, Input, Modal, Space, Typography } from 'antd';
import { createContext, useMemo, useRef, useState } from 'react';
import useRichFormat from '@/hooks/useRichI18n';
import { $t } from '@/utils/i18n';
import '../style.less';
import { flushSync } from 'react-dom';
import { type SchemaType, schemaMap } from '../../ruleFormSchema';
import { handleEmptyValuesFunc } from '../../utils';
import BatchCloneModal from './comp/BatchCloneModal';
import Step1 from './comp/Step1';
import Step2 from './comp/Step2';
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 = {
visible: boolean;
ruleMeta: Record<string, any>;
dataSourceList: API.DataSourceListItem[];
currentRuleType: number;
onDone: (refresh?: boolean) => void;
};
const MultiCopyPopup = ({
visible,
ruleMeta,
onDone,
currentRuleType,
dataSourceList,
}: MultiCopyPopupProps) => {
const { richFormat } = useRichFormat();
const screens = useBreakpoint();
const currentSchemaMap =
schemaMap[currentRuleType as unknown as SchemaType]?.({ richFormat }) || {};
const formMapRef = useRef<
React.RefObject<ProFormInstance<any> | undefined>[]
>([]);
const [targetDataSourceId, setTargetDataSourceId] = useState<
string | number | null
>(null);
const [selectRules, setSelectRules] = useState<API.RuleListItem[]>([]);
const [checkedIdsList, setCheckedIdsList] = useState<Array<string | number>>(
[],
);
const [inputValue, setInputValue] = useState('');
const [batchCloneModalVisible, setBatchCloneModalVisible] = useState(false);
const [selectedRows, setSelectedRows] = useState<Partial<API.RuleListItem>[]>(
[],
);
const submitRuleList = useMemo(() => {
return selectRules.filter((item) => checkedIdsList.includes(item.id));
}, [selectRules, checkedIdsList]);
const targetDataSource = useMemo(() => {
return dataSourceList.find((item) => item.id === targetDataSourceId);
}, [targetDataSourceId, dataSourceList]);
const handleCancel = () => {
setBatchCloneModalVisible(false);
setSelectedRows([]);
setSelectRules([]);
setCheckedIdsList([]);
setInputValue('');
setTargetDataSourceId(null);
};
return (
<Modal
width={['xl', 'xxl'].includes(screens || '') ? '50%' : '95%'}
open={visible}
centered
destroyOnHidden
onCancel={() => {
handleCancel();
onDone();
}}
title={$t('pages.clones.title')}
footer={null}
maskClosable={false}
styles={{
body: {
maxHeight: '90vh',
overflowY: 'auto',
overflowX: 'hidden',
marginRight: -10,
paddingRight: 8,
},
}}
>
<MultiCopyPopupContext.Provider
value={{
ruleMeta,
dataSourceList,
currentRuleType,
targetDataSourceId,
selectRules,
setSelectRules,
currentSchemaMap,
}}
>
<StepsForm
formMapRef={formMapRef}
onFinish={async (values) => {
const editNameRules = values?.step2 || {};
const submitValues = submitRuleList.map((item) => {
const tmpValues = handleEmptyValuesFunc(item);
const tmpValues2 =
currentSchemaMap?.transformSubmitValue?.(tmpValues) ||
tmpValues;
const submitValues = {
...tmpValues2,
dataSourceId: targetDataSourceId,
name:
editNameRules[item.id] ||
`${item.name || ruleMeta?.title || ''} - ${targetDataSource?.sourceName || ''}`,
id: void 0,
groupFilter: '',
};
if (currentSchemaMap?.symbolFormShow) {
submitValues.param2 = '';
}
if (currentSchemaMap?.assetFormShow) {
submitValues.param1 = '';
}
const tmp: Record<string, any> = {
companyId: submitValues.companyId,
dataSourceId: submitValues.dataSourceId,
groupFilter: submitValues.groupFilter,
name: submitValues.name,
type: submitValues.type,
};
for (const key in submitValues) {
if (key.indexOf('param') !== -1) {
tmp[key] = submitValues[key];
}
}
return tmp;
});
setSelectedRows(submitValues);
setBatchCloneModalVisible(true);
}}
submitter={{
render: (props) => {
if (props.step === 0) {
return [
<Button key="cancel" onClick={() => onDone()}>
{$t('pages.model.cancel')}
</Button>,
<Button
disabled={selectRules.length === 0}
key="goToTree"
type="primary"
onClick={() => props.onSubmit?.()}
>
{$t('pages.clones.step1.confirm')}
</Button>,
];
}
if (props.step === 1) {
return [
<Button key="pre" onClick={() => props.onPre?.()}>
{$t('pages.clones.back')}
</Button>,
<Button
disabled={checkedIdsList.length === 0}
type="primary"
key="goToTree"
onClick={() => props.onSubmit?.()}
>
{$t('pages.clones.step2.confirm')}
</Button>,
];
}
return [
<Button
key="gotoTwo"
onClick={() => {
setInputValue('');
props.onPre?.();
}}
>
{$t('pages.clones.backPreview')}
</Button>,
<Button
disabled={
submitRuleList.length === 0 ||
inputValue !== targetDataSource?.sourceName
}
variant="solid"
color="danger"
key="goToTree"
onClick={() => props.onSubmit?.()}
>
{$t('pages.clones.step3.confirm')}
</Button>,
];
},
}}
>
<StepsForm.StepForm
name="step1"
title={$t('pages.clones.step1.title')}
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);
}
}
}
}}
>
<Step1
formMapRef={formMapRef}
setTargetDataSourceId={setTargetDataSourceId}
/>
</StepsForm.StepForm>
<StepsForm.StepForm
name="step2"
title={$t('pages.clones.step2.title')}
>
<Step2 setCheckedIdsList={setCheckedIdsList} />
</StepsForm.StepForm>
<StepsForm.StepForm
name="time"
title={$t('pages.clones.step3.title')}
>
<div className="confirm-container">
<Flex className="confirm-space" align="center" justify="center">
<Space direction="vertical" align="center">
<Title level={5}>
{$t('pages.clones.step3.confirmTitle', {
cloneCount: checkedIdsList.length,
target: targetDataSource?.sourceName || '',
})}
</Title>
<Text type="secondary">
{$t('pages.clones.step3.confirmTips')}
</Text>
<Input
placeholder={targetDataSource?.sourceName || ''}
className="text-center"
size="large"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
/>
<Text type="secondary">
{$t('pages.clones.step3.confirmTips2')}
</Text>
</Space>
</Flex>
</div>
<Divider style={{ margin: '16px 0' }} />
</StepsForm.StepForm>
</StepsForm>
</MultiCopyPopupContext.Provider>
<BatchCloneModal
visible={batchCloneModalVisible}
selectedRows={selectedRows}
onClose={(cb) => {
setBatchCloneModalVisible(false);
cb?.();
}}
onFinish={(cb) => {
flushSync(() => {
setBatchCloneModalVisible(false);
});
cb?.();
handleCancel();
onDone?.(true);
}}
/>
</Modal>
);
};
export default MultiCopyPopup;

View File

@@ -0,0 +1,248 @@
import { ArrowDownOutlined, ArrowRightOutlined } from '@ant-design/icons';
import {
BetaSchemaForm,
ProForm,
ProFormDependency,
ProFormSelect,
useBreakpoint,
} from '@ant-design/pro-components';
import {
Alert,
App,
Col,
Divider,
Flex,
type FormInstance,
Modal,
Row,
Typography,
} from 'antd';
import { useEffect, useRef, useState } from 'react';
import useRichFormat from '@/hooks/useRichI18n';
import { $t } from '@/utils/i18n';
import { type SchemaType, schemaMap } from '../../ruleFormSchema';
import { handleEmptyValuesFunc, processSingleRule } from '../../utils';
import AssetsSelect from '../AssetsSelect';
import SymbolSelect from '../SymbolSelect';
type RuleListItem = Partial<API.RuleListItem>;
type CopyPopupProps = {
visible: boolean;
formValues: Partial<RuleListItem | undefined>;
dataSourceList: API.DataSourceListItem[];
currentRuleType: number;
onDone: (refresh?: boolean) => void;
};
const { Text } = Typography;
const SingleCopyPopup = ({
visible,
onDone,
currentRuleType,
formValues,
dataSourceList,
}: CopyPopupProps) => {
const { richFormat } = useRichFormat();
const screens = useBreakpoint();
const { message, modal } = App.useApp();
const formRef = useRef<FormInstance>(null);
const [confirmLoading, setConfirmLoading] = useState(false);
const [errMsg, setErrMsg] = useState('');
useEffect(() => {
if (!visible) {
setErrMsg('');
}
return () => {
setErrMsg('');
};
}, [visible]);
const {
leftForm,
symbolFormShow,
assetFormShow,
transformInitialValue,
transformSubmitValue,
} =
schemaMap[currentRuleType as unknown as SchemaType]?.({ richFormat }) || {};
const rightDataSourceList = dataSourceList.filter(
(item) => item.id !== formValues?.dataSourceId,
);
const initialValues = transformInitialValue?.(formValues) || formValues;
const rightFirstDataSource = rightDataSourceList[0];
const copyFormValues = {
...initialValues,
name: initialValues?.name
? `${initialValues.name} - ${rightFirstDataSource?.sourceName}`
: `${Date.now()} - ${rightFirstDataSource?.sourceName}`,
groupFilter: '',
id: void 0,
dataSourceId: rightFirstDataSource?.id || '',
};
if (symbolFormShow) {
copyFormValues.param2 = '';
}
if (assetFormShow) {
copyFormValues.param1 = '';
}
const formContent = ({
dataSourceList,
formAttr,
}: {
dataSourceList: API.DataSourceListItem[];
formAttr: Record<string, any>;
}) => {
return (
<ProForm {...formAttr} submitter={false}>
<ProFormSelect
name="dataSourceId"
label={$t('table.dataSource')}
options={dataSourceList}
rules={[{ required: true, message: $t('form.required') }]}
fieldProps={{
allowClear: false,
fieldNames: {
label: 'sourceName',
value: 'id',
},
}}
/>
<BetaSchemaForm layoutType="Embed" columns={leftForm} />
{(symbolFormShow || assetFormShow) && (
<ProFormDependency name={['dataSourceId']}>
{({ dataSourceId }) => (
<>
{symbolFormShow && (
<ProForm.Item name="param2" noStyle>
<SymbolSelect dataSourceId={dataSourceId} />
</ProForm.Item>
)}
{assetFormShow && (
<ProForm.Item name="param1" noStyle>
<AssetsSelect dataSourceId={dataSourceId} />
</ProForm.Item>
)}
</>
)}
</ProFormDependency>
)}
</ProForm>
);
};
return (
<Modal
title={$t('pages.rules.cloneRule')}
open={visible}
onCancel={() => onDone()}
destroyOnHidden
centered
width={['xl', 'xxl'].includes(screens || '') ? '60%' : '95%'}
confirmLoading={confirmLoading}
okText={$t('pages.rules.cloneConfirm')}
onOk={async () => {
setConfirmLoading(true);
const values = formRef.current?.getFieldsValue();
const tmpValues = handleEmptyValuesFunc(values);
const submitValues = transformSubmitValue?.(tmpValues) || tmpValues;
submitValues.type = currentRuleType;
try {
await processSingleRule({ payload: submitValues });
message.success($t('pages.tips.operationSuccess'));
onDone(true);
} catch (error: any) {
console.log('error', error);
switch (error?.errType) {
case 1:
case 2:
modal.warning({
content: error?.respMessage || $t(error?.errMsg),
centered: true,
onOk: () => {
setErrMsg('');
setConfirmLoading(false);
onDone(true);
},
});
return;
case 3:
setErrMsg($t(error?.errMsg));
formRef.current?.setFieldsValue({
...error.data,
});
break;
default:
break;
}
}
setConfirmLoading(false);
}}
styles={{
body: {
maxHeight: '82vh',
overflowY: 'auto',
overflowX: 'hidden',
marginRight: -10,
paddingRight: 8,
},
}}
>
<Row>
<Col xs={24} sm={11}>
<div style={{ marginBottom: 12 }}>
<Text strong>📋 {$t('pages.rules.originalRule')}</Text>
</div>
{formContent({
dataSourceList,
formAttr: { initialValues, disabled: true },
})}
</Col>
<Col xs={0} sm={1}>
<Flex vertical align="center" style={{ height: '100%' }}>
<ArrowRightOutlined style={{ marginTop: 2, fontSize: 18 }} />
<Divider type="vertical" style={{ flex: 1, marginTop: 16 }} />
</Flex>
</Col>
<Col xs={24} sm={0}>
<div className="text-center" style={{ marginBottom: 16 }}>
<ArrowDownOutlined />
</div>
</Col>
<Col xs={24} sm={12}>
<div style={{ marginBottom: 12 }}>
<Text strong> {$t('pages.rules.cloneRulePreview')}</Text>
</div>
{formContent({
dataSourceList: rightDataSourceList,
formAttr: {
initialValues: copyFormValues,
formRef: formRef,
disabled: confirmLoading,
},
})}
{errMsg && (
<Alert
message={errMsg}
type="error"
showIcon
style={{ marginBottom: 16 }}
/>
)}
</Col>
</Row>
</Modal>
);
};
export default SingleCopyPopup;

View File

@@ -0,0 +1,205 @@
import {
CheckCircleOutlined,
CloseCircleOutlined,
ExclamationCircleOutlined,
SyncOutlined,
} from '@ant-design/icons';
import {
Alert,
type AlertProps,
Button,
Divider,
Flex,
List,
Modal,
Progress,
Space,
Tag,
Tooltip,
} from 'antd';
import { useEffect, useMemo, useState } from 'react';
import { processSingleRule } from '@/pages/rules/utils';
import { $t } from '@/utils/i18n';
type BatchCloneModalProps = {
visible: boolean;
selectedRows: Partial<API.RuleListItem>[];
onClose?: (cb?: () => void) => void;
onFinish?: (cb?: () => void) => void;
};
const BatchCloneModal = ({
visible,
selectedRows,
onClose,
onFinish,
}: BatchCloneModalProps) => {
const [tasks, setTasks] = useState<any[]>([]);
const [isRunning, setIsRunning] = useState(false);
const [currentRunning, setCurrentRunning] = useState(0);
useEffect(() => {
const initialTasks = selectedRows.map((row) => ({
name: row.name,
status: 'waiting', // waiting, processing, success, error
errorMsg: '',
}));
setTasks(initialTasks);
}, [visible, selectedRows]);
const finishedTasks = useMemo(
() => tasks.filter((t) => ['success', 'warning'].includes(t.status)),
[tasks],
);
const finishedMessage = useMemo(() => {
const hasWarning = finishedTasks.find((t) => t.status === 'warning');
const type: AlertProps['type'] = hasWarning
? 'warning'
: finishedTasks.length === tasks.length
? 'success'
: 'error';
return {
type,
message:
type === 'success'
? $t('pages.clones.step3.success')
: type === 'warning'
? $t('pages.clones.step3.warning')
: $t('pages.clones.step3.error'),
};
}, [finishedTasks, tasks]);
const startBatch = async () => {
setIsRunning(true);
for (let i = 0; i < selectedRows.length; i++) {
setCurrentRunning(i + 1);
setTasks((prev) => {
const next = [...prev];
next[i].status = 'processing';
return next;
});
try {
await processSingleRule({ payload: selectedRows[i] });
setTasks((prev) => {
const next = [...prev];
next[i].status = 'success';
return next;
});
} catch (e: any) {
setTasks((prev) => {
const next = [...prev];
next[i].status = [1, 2, 3].includes(e.errType) ? 'warning' : 'error';
next[i].errorMsg = e.errMsg ? $t(e.errMsg) : e.respMessage;
return next;
});
}
}
setIsRunning(false);
};
const handleReset = () => {
setTasks([]);
setIsRunning(false);
setCurrentRunning(0);
};
return (
<Modal
title={$t('pages.clones.step3.progress')}
open={visible}
closable={false}
maskClosable={false}
footer={null}
centered
destroyOnHidden
>
<Progress
percent={Math.round((finishedTasks.length / tasks.length) * 100)}
style={{ marginBottom: 20 }}
/>
<div style={{ maxHeight: '80vh', overflowY: 'auto' }}>
<List
size="small"
dataSource={tasks}
renderItem={(item) => (
<List.Item style={{ display: 'flex', flexWrap: 'wrap' }}>
<span style={{ whiteSpace: 'nowrap' }}>{item.name}</span>
{item.status === 'processing' && (
<Tag color="blue" icon={<SyncOutlined spin />}>
{$t('pages.clones.step3.status1')}
</Tag>
)}
{item.status === 'success' && (
<Tag color="green" icon={<CheckCircleOutlined />}>
{$t('pages.clones.step3.status2')}
</Tag>
)}
{item.status === 'error' && (
<Tooltip
title={item.errorMsg || $t('pages.clones.step3.status3')}
>
<Tag color="red" icon={<CloseCircleOutlined />}>
{$t('pages.clones.step3.status3')}
</Tag>
<ExclamationCircleOutlined />
</Tooltip>
)}
{item.status === 'warning' && (
<Tooltip
title={item.errorMsg || $t('pages.clones.step3.status4')}
>
<Tag color="orange" icon={<CloseCircleOutlined />}>
{$t('pages.clones.step3.status4')}
</Tag>
<ExclamationCircleOutlined />
</Tooltip>
)}
{item.status === 'waiting' && (
<Tag color="default">{$t('pages.clones.step3.status5')}</Tag>
)}
</List.Item>
)}
/>
</div>
<Space direction="vertical" className="w-100">
<Divider style={{ margin: '10px 0' }} />
{!isRunning && currentRunning === tasks.length && (
<>
<Alert
message={finishedMessage.message}
type={finishedMessage.type}
/>
<Flex justify="flex-end">
<Button
type="primary"
onClick={() => {
onFinish?.(handleReset);
}}
>
{$t('pages.clones.step3.understand')}
</Button>
</Flex>
</>
)}
{!isRunning && currentRunning === 0 && (
<Flex justify="flex-end" gap={10}>
<Button
onClick={() => {
onClose?.(handleReset);
}}
>
{$t('pages.model.cancel')}
</Button>
<Button type="primary" onClick={startBatch}>
{$t('pages.clones.step3.start')}
</Button>
</Flex>
)}
</Space>
</Modal>
);
};
export default BatchCloneModal;

View File

@@ -0,0 +1,88 @@
import { CheckCard, ProCard } from '@ant-design/pro-components';
import { useRequest } from '@umijs/max';
import { Col, Row, Spin } from 'antd';
import { useContext, useState } from 'react';
import { getRuleList } from '@/services/api';
import { $t } from '@/utils/i18n';
import { RuleCardBodyInfo } from '../../RuleCardBody';
import { MultiCopyPopupContext } from '../MultiCopyPopup';
type RuleCheckCardProps = {
dataSourceId: string | number;
onChange?: (id: string | number) => void;
};
const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
const [ruleList, setRuleList] = useState<API.RuleListItem[]>([]);
const { ruleMeta, currentRuleType, setSelectRules } = useContext(
MultiCopyPopupContext,
);
const { loading } = useRequest(
() =>
getRuleList({
type: currentRuleType,
dataSourceId,
}),
{
// manual: true,
refreshDeps: [dataSourceId, currentRuleType],
onSuccess: ({ data = [] }: { data: API.RuleListItem[] }) => {
setRuleList(data);
setSelectRules(data);
},
onError: () => {
setRuleList([]);
},
},
);
if (!dataSourceId) return <div>{$t('pages.clones.step1.tips')}</div>;
if (loading) return <Spin />;
// const
return (
<ProCard
title={$t('pages.clones.step1.sourceRules', {
count: ruleList?.length,
ruleType: ruleMeta?.title || '-',
})}
collapsible
defaultCollapsed
className="clone-rule-card"
>
<CheckCard.Group
multiple
onChange={(value) => {
if (Array.isArray(value)) {
const selectedRules = value.map((item) =>
ruleList.find((rule) => rule.id === item),
) as API.RuleListItem[];
setSelectRules(selectedRules);
}
}}
className="w-100"
defaultValue={ruleList.map((item) => item.id)}
>
<Row gutter={12} style={{ maxHeight: '45vh', overflowY: 'auto' }}>
{ruleList.map((rule) => (
<Col span={8} key={rule.id}>
<CheckCard
title={$t('pages.rules.ruleName')}
extra={rule.name || 'N/A'}
value={rule.id}
description={
<RuleCardBodyInfo ruleMeta={ruleMeta} rule={rule} />
}
className="w-100"
/>
</Col>
))}
</Row>
</CheckCard.Group>
</ProCard>
);
};
export default RuleCheckCard;

View File

@@ -0,0 +1,91 @@
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 { $t } from '@/utils/i18n';
import { MultiCopyPopupContext } from '../MultiCopyPopup';
import RuleCheckCard from './RuleCheckCard';
type Step1Props = {
formMapRef: any;
setTargetDataSourceId: (id: string | number | null) => void;
};
const Step1 = ({ formMapRef, setTargetDataSourceId }: Step1Props) => {
const { dataSourceList } = useContext(MultiCopyPopupContext);
useEffect(() => {
if (dataSourceList?.length) {
// waitTime(1000).then(() => {
const sourceDataSourceId = dataSourceList[0]?.id || '';
const targetDataSourceId = dataSourceList[1]?.id || '';
const curFormRef = formMapRef?.current?.[0]?.current;
if (curFormRef) {
curFormRef?.setFieldsValue({
sourceDataSourceId,
targetDataSourceId,
});
setTargetDataSourceId(targetDataSourceId);
}
// });
}
}, [dataSourceList]);
return (
<>
<ProFormSelect
name="sourceDataSourceId"
label={$t('pages.clones.step1.source')}
options={dataSourceList}
rules={[{ required: true, message: $t('form.required') }]}
fieldProps={{
allowClear: false,
fieldNames: {
label: 'sourceName',
value: 'id',
},
}}
/>
<ProFormDependency name={['sourceDataSourceId']}>
{({ sourceDataSourceId }) => {
if (!sourceDataSourceId) return null;
return <RuleCheckCard dataSourceId={sourceDataSourceId} />;
}}
</ProFormDependency>
<Divider>
<Tag style={{ borderRadius: 30, padding: '4px 16px' }}>
<ArrowDownOutlined /> {$t('pages.clones.step1.dividerText')}
</Tag>
</Divider>
<ProFormDependency name={['sourceDataSourceId']}>
{({ sourceDataSourceId }) => {
if (!sourceDataSourceId) return null;
const dataSourceOptions = dataSourceList.filter(
(item) => item.id !== sourceDataSourceId,
);
return (
<ProFormSelect
name="targetDataSourceId"
label={$t('pages.clones.step1.target')}
options={dataSourceOptions}
rules={[{ required: true, message: $t('form.required') }]}
fieldProps={{
allowClear: false,
fieldNames: {
label: 'sourceName',
value: 'id',
},
}}
onChange={(value: string | number | null) => {
setTargetDataSourceId(value || null);
}}
/>
);
}}
</ProFormDependency>
</>
);
};
export default Step1;

View File

@@ -0,0 +1,193 @@
import { FormOutlined, SwapRightOutlined } from '@ant-design/icons';
import { ProCard } from '@ant-design/pro-components';
import {
Checkbox,
type CheckboxProps,
Col,
Collapse,
Divider,
Flex,
Form,
Input,
Row,
Space,
Typography,
} from 'antd';
import { useContext, useEffect, useMemo, useState } from 'react';
import { $t } from '@/utils/i18n';
import { RuleCardBodyInfo } from '../../RuleCardBody';
import { MultiCopyPopupContext } from '../MultiCopyPopup';
const { Text } = Typography;
type CollapseItemProps = {
leftItem: API.RuleListItem;
targetDataSource?: API.DataSourceListItem;
};
const CollapseItem = ({ leftItem, targetDataSource }: CollapseItemProps) => {
const { ruleMeta, currentSchemaMap } = useContext(MultiCopyPopupContext);
const rightForm = {
...leftItem,
groupFilter: '',
};
if (currentSchemaMap?.symbolFormShow) {
rightForm.param2 = '';
}
if (currentSchemaMap?.assetFormShow) {
rightForm.param1 = '';
}
return (
<Row gutter={12}>
<Col span={12}>
<ProCard bordered style={{ backgroundColor: '#f6f6f6' }}>
<Space
direction="vertical"
style={{ marginBottom: 8 }}
className="w-100"
>
<Text type="secondary" strong>
{$t('pages.clones.step2.cardTitle1')}
</Text>
<Flex justify="space-between" align="center" style={{ height: 30 }}>
<Text type="secondary" style={{ whiteSpace: 'nowrap' }}>
{$t('pages.rules.ruleName')}
</Text>
<Text type="secondary">{leftItem.name || 'N/A'}</Text>
</Flex>
</Space>
<RuleCardBodyInfo ruleMeta={ruleMeta} rule={leftItem} />
</ProCard>
</Col>
<Col span={12}>
<ProCard bordered style={{ height: '100%' }}>
<Space
direction="vertical"
style={{ marginBottom: 8 }}
className="w-100"
>
<Text strong>{$t('pages.clones.step2.cardTitle2')}</Text>
<Flex justify="space-between" align="center" style={{ height: 30 }}>
<Text style={{ marginRight: 8, whiteSpace: 'nowrap' }}>
{$t('pages.rules.ruleName')}
</Text>
<Form.Item
name={['step2', `${leftItem.id}`]}
style={{ flex: 1 }}
noStyle
>
<Space.Compact>
<Input
autoComplete="off"
size="small"
defaultValue={`${leftItem.name || ruleMeta?.title || ''} - ${targetDataSource?.sourceName || ''}`}
variant="underlined"
style={{ textAlign: 'right' }}
/>
<FormOutlined />
</Space.Compact>
</Form.Item>
</Flex>
</Space>
<RuleCardBodyInfo ruleMeta={ruleMeta} rule={rightForm} />
</ProCard>
</Col>
</Row>
);
};
type Step2Props = {
setCheckedIdsList: (list: string[]) => void;
};
const Step2 = ({ setCheckedIdsList }: Step2Props) => {
const { dataSourceList, ruleMeta, targetDataSourceId, selectRules } =
useContext(MultiCopyPopupContext);
const [checkedList, setCheckedList] = useState<string[]>([]);
const targetDataSource = useMemo(() => {
return dataSourceList.find((item) => item.id === targetDataSourceId);
}, [targetDataSourceId, dataSourceList]);
const ruleList = useMemo(() => {
return selectRules.map((item) => ({
key: item.id,
// forceRender: true,
label: (
<Space align="center">
<Text>{item.name || 'N/A'} </Text>
<SwapRightOutlined />
<Text>
{item.name || ruleMeta?.title || ''} -{' '}
{targetDataSource?.sourceName || ''}
</Text>
</Space>
),
extra: <Checkbox value={item.id as string}></Checkbox>,
children: (
<CollapseItem leftItem={item} targetDataSource={targetDataSource} />
),
}));
}, [selectRules, targetDataSource]);
useEffect(() => {
const selectRuleIds = selectRules.map((item) => item.id as string);
setCheckedList(selectRuleIds);
setCheckedIdsList(selectRuleIds);
}, [selectRules]);
const checkAll = selectRules.length === checkedList.length;
const indeterminate =
checkedList.length > 0 && checkedList.length < selectRules.length;
const onChange = (list: string[]) => {
setCheckedList(list);
setCheckedIdsList(list);
};
const onCheckAllChange: CheckboxProps['onChange'] = (e) => {
const checkedIdList = e.target.checked
? selectRules.map((item) => item.id as string)
: [];
setCheckedList(checkedIdList);
setCheckedIdsList(checkedIdList);
};
return (
<Flex vertical>
<Flex justify="space-between">
<span>
{$t('pages.clones.step2.summary', {
count: selectRules.length,
cloneCount: checkedList.length,
target: targetDataSource?.sourceName || '-',
})}
</span>
<Checkbox
indeterminate={indeterminate}
onChange={onCheckAllChange}
checked={checkAll}
>
{$t('pages.clones.step2.checkAll')}
</Checkbox>
</Flex>
<Divider style={{ margin: '16px 0' }} />
<div style={{ flex: 1, overflow: 'auto', maxHeight: '70vh' }}>
<Checkbox.Group
className="w-100"
name="selectedRules"
onChange={onChange}
value={checkedList}
>
<Collapse
className="clone-rule-collapse w-100"
items={ruleList}
collapsible="header"
/>
</Checkbox.Group>
</div>
<Divider style={{ margin: '16px 0' }} />
</Flex>
);
};
export default Step2;

View File

@@ -0,0 +1,104 @@
import { ProFormTreeSelect } from '@ant-design/pro-components';
import React, { useEffect, useState } from 'react';
import useUserInfo from '@/hooks/useUserInfo';
import Loading from '@/loading';
import { getGroupTreeList } from '@/utils/dataCenter';
import { generatePattern2, parsePatternToKeys2 } from '@/utils/groupUtil';
import { $t } from '@/utils/i18n';
interface GroupSelectProps {
dataSourceId: number;
value?: string; // Pattern string passed in from outside
onChange?: (val: string) => void; // Callback to pass the Pattern string back to the outside
disabled?: boolean;
}
const GroupSelect: React.FC<GroupSelectProps> = ({
dataSourceId,
value,
onChange,
}) => {
const { userInfo } = useUserInfo();
const companyInfo = userInfo?.company;
const [treeData, setTreeData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// Internally maintains a keys array for TreeSelect display
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
const loadData = async (force: boolean = false) => {
setLoading(true);
try {
const data = await getGroupTreeList(companyInfo, dataSourceId, force);
setCheckedKeys([]);
setTreeData(data);
} finally {
setLoading(false);
}
};
// 1. Listen for changes to dataSourceId and request source data
useEffect(() => {
if (!dataSourceId) {
setTreeData([]);
return;
}
loadData();
}, [dataSourceId]);
// 2. When treeData is loaded and there is an initial Pattern string, convert it to internal keys
useEffect(() => {
if (!value) {
setCheckedKeys([]);
} else if (treeData.length > 0 && typeof value === 'string') {
const keys = parsePatternToKeys2(value, treeData);
setCheckedKeys(keys);
}
}, [value, treeData]);
// 3. When users manually check/uncheck items, convert internal keys back to string and trigger onChange
const handleTreeChange = (keys: any) => {
// Handle the object format that antd tree may return
const actualKeys = Array.isArray(keys) ? keys : keys.checked;
// Update internal UI state
setCheckedKeys(actualKeys);
// Generate string to pass to external Form
if (onChange) {
const patternString = generatePattern2(treeData, actualKeys);
onChange(patternString);
}
};
if (!dataSourceId) return null;
if (loading) return <Loading padding="0" />;
return (
<div className="addon-select-wrapper">
<ProFormTreeSelect
label={`${$t('pages.rules.monitoredGroups')} (${$t('pages.tips.emptyIsAll')})`}
extra={$t('form.rule1.tips2')}
placeholder={`${$t('pages.rules.allGroups')}`}
fieldProps={{
listHeight: 410,
treeData: treeData,
treeCheckable: true,
value: checkedKeys, // Use the internally parsed array
onChange: handleTreeChange,
maxTagCount: 'responsive',
virtual: true,
fieldNames: {
label: 'title',
value: 'key',
},
}}
formItemProps={{
style: { marginBottom: 0 },
}}
/>
</div>
);
};
export default GroupSelect;

View File

@@ -0,0 +1,16 @@
import { ProForm, ProFormDependency } from '@ant-design/pro-components';
import GroupSelect from './GroupSelect';
const GroupSelectWrapper = () => {
return (
<ProFormDependency name={['dataSourceId']}>
{({ dataSourceId }) => (
<ProForm.Item name="groupFilter" noStyle>
<GroupSelect dataSourceId={dataSourceId} />
</ProForm.Item>
)}
</ProFormDependency>
);
};
export default GroupSelectWrapper;

View File

@@ -0,0 +1,163 @@
import {
BetaSchemaForm,
ModalForm,
ProForm,
ProFormDependency,
type ProFormInstance,
ProFormSelect,
} from '@ant-design/pro-components';
import { Alert, Col, Divider, Row } from 'antd';
import { useRef } from 'react';
import useRichI18n from '@/hooks/useRichI18n';
import { $t } from '@/utils/i18n';
import { type SchemaType, schemaMap } from '../ruleFormSchema';
import { handleEmptyValuesFunc } from '../utils';
import AssetsSelect from './AssetsSelect';
import SymbolSelect from './SymbolSelect';
type RuleListItem = Partial<API.RuleListItem>;
export default (props: {
action: EX.Action;
visible: boolean;
ruleMeta: Record<string, any>;
formValues: Partial<RuleListItem | undefined>;
dataSourceList: API.DataSourceListItem[];
currentRuleType: number;
curDataSourceId: string | number;
onSubmit: (values: RuleListItem) => void;
onDone: () => void;
}) => {
const {
visible,
formValues,
onSubmit,
onDone,
action,
dataSourceList,
curDataSourceId,
currentRuleType,
ruleMeta,
} = props;
const formRef = useRef<ProFormInstance>(null);
const { richFormat } = useRichI18n();
const {
leftForm,
symbolFormShow,
assetFormShow,
popFormTips,
tipsComponent,
transformInitialValue,
transformSubmitValue,
} =
schemaMap[currentRuleType as unknown as SchemaType]?.({
richFormat,
}) || {};
const filteredDataSourceList = dataSourceList.filter(
(item) => !item.disabled,
);
const initialValues = transformInitialValue?.(formValues) || formValues;
return (
<ModalForm
formRef={formRef}
disabled={action === 'view'}
title={`${action === 'add' ? $t('table.action.add') : $t('table.action.edit')}${ruleMeta?.title}`}
initialValues={
initialValues?.id
? initialValues
: {
...initialValues,
dataSourceId: curDataSourceId || filteredDataSourceList[0]?.id,
}
}
open={visible}
omitNil={false}
onFinish={async (values) => {
const tmpValues = handleEmptyValuesFunc(values);
const submitValues = transformSubmitValue?.(tmpValues) || tmpValues;
return onSubmit({ ...submitValues, type: currentRuleType });
}}
modalProps={{
onCancel: () => onDone(),
destroyOnHidden: true,
maskClosable: action === 'view',
centered: true,
styles: {
body: {
maxHeight: '82vh',
overflowY: 'auto',
overflowX: 'hidden',
marginRight: -10,
paddingRight: 8,
},
},
}}
>
<Row>
<ProFormDependency name={popFormTips?.dependencies || []}>
{(params) => (
<Alert
message={popFormTips?.title}
description={popFormTips?.content(params)}
type="info"
style={{ width: '100%', marginBottom: 16 }}
/>
)}
</ProFormDependency>
</Row>
<Row gutter={16}>
<Col xs={24} xl={symbolFormShow || assetFormShow ? 11 : 24}>
<ProFormSelect
name="dataSourceId"
label={$t('table.dataSource')}
options={dataSourceList}
rules={[{ required: true, message: $t('form.required') }]}
fieldProps={{
disabled: action !== 'add',
allowClear: false,
fieldNames: {
label: 'sourceName',
value: 'id',
},
}}
/>
<BetaSchemaForm layoutType="Embed" columns={leftForm} />
{typeof tipsComponent === 'string' ? (
<Alert message={tipsComponent} type="info" showIcon />
) : (
tipsComponent
)}
</Col>
{(symbolFormShow || assetFormShow) && (
<>
<Col xs={0} xl={1}>
<Divider type="vertical" style={{ height: '100%' }} />
</Col>
<Col xs={24} xl={0}>
<Divider />
</Col>
<Col xs={24} xl={12}>
<ProFormDependency name={['dataSourceId']}>
{({ dataSourceId }) => (
<>
{symbolFormShow && (
<ProForm.Item name="param2" noStyle>
<SymbolSelect dataSourceId={dataSourceId} />
</ProForm.Item>
)}
{assetFormShow && (
<ProForm.Item name="param1" noStyle>
<AssetsSelect dataSourceId={dataSourceId} />
</ProForm.Item>
)}
</>
)}
</ProFormDependency>
</Col>
</>
)}
</Row>
</ModalForm>
);
};

View File

@@ -0,0 +1,54 @@
import { Divider, Space, Tag, Typography } from 'antd';
import { $t } from '@/utils/i18n';
import type { pageCardInfo } from '../hooks/useRuleMeta';
const { Text } = Typography;
type RuleCardBodyProps = {
ruleMeta: Record<string, any>;
rule: API.RuleListItem;
};
export const RuleCardBodyInfo = ({ ruleMeta, rule }: RuleCardBodyProps) => {
return (
<Space direction="vertical" className="w-100">
{ruleMeta.pageCardInfo.map((item: pageCardInfo) => (
<Tag key={item.key} className="w-100" style={{ padding: '4px 8px' }}>
<div className="w-100 flex">
<Text strong style={{ marginRight: 8 }}>
{item.label}:
</Text>
<Text ellipsis>
{item.right
? item.right(rule[item.key] as string, rule)
: rule[item.key] || 'N/A'}
</Text>
</div>
</Tag>
))}
</Space>
);
};
const RuleCardBody = ({ ruleMeta, rule }: RuleCardBodyProps) => {
return (
<>
{/* description */}
<Text type="secondary">{ruleMeta.desc}</Text>
{/* data source */}
<div>
<Space>
<Text type="secondary">{$t('table.dataSource')}:</Text>
<Text>{rule.sourceName}</Text>
</Space>
</div>
<Divider style={{ margin: '14px 0' }} />
{/* threshold information */}
<RuleCardBodyInfo ruleMeta={ruleMeta} rule={rule} />
</>
);
};
export default RuleCardBody;

View File

@@ -0,0 +1,21 @@
import { Space, Typography } from 'antd';
const { Title, Text } = Typography;
type RuleHeaderProps = {
ruleMeta: Record<string, any>;
};
const RuleHeader = ({ ruleMeta }: RuleHeaderProps) => {
return (
<Space>
<Title style={{ margin: 0 }}>{ruleMeta.icon}</Title>
<div>
<Title level={4} style={{ margin: 0 }}>
{ruleMeta.title}
</Title>
<Text type="secondary">{ruleMeta.desc}</Text>
</div>
</Space>
);
};
export default RuleHeader;

View File

@@ -0,0 +1,103 @@
import { ProFormTreeSelect } from '@ant-design/pro-components';
import React, { useEffect, useState } from 'react';
import useUserInfo from '@/hooks/useUserInfo';
import Loading from '@/loading';
import { getSymbolTreeList } from '@/utils/dataCenter';
import { $t } from '@/utils/i18n';
import { generatePattern, parsePatternToKeys } from '@/utils/symbolUtil';
interface SymbolSelectProps {
dataSourceId: number;
value?: string; // Pattern string passed in from outside
onChange?: (val: string) => void; // Callback to pass the Pattern string back to the outside
disabled?: boolean;
}
const SymbolSelect: React.FC<SymbolSelectProps> = ({
dataSourceId,
value,
onChange,
}) => {
const { userInfo } = useUserInfo();
const companyInfo = userInfo?.company;
const [treeData, setTreeData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
// Internally maintains a keys array for TreeSelect display
const [checkedKeys, setCheckedKeys] = useState<string[]>([]);
const loadData = async (force: boolean = false) => {
setLoading(true);
try {
const data = await getSymbolTreeList(companyInfo, dataSourceId, force);
setCheckedKeys([]);
setTreeData(data);
} finally {
setLoading(false);
}
};
// 1. Listen for changes to dataSourceId and request source data
useEffect(() => {
if (!dataSourceId) {
setTreeData([]);
return;
}
loadData();
}, [dataSourceId]);
// 2. When treeData is loaded and there is an initial Pattern string, convert it to internal keys
useEffect(() => {
if (!value) {
setCheckedKeys([]);
} else if (treeData.length > 0 && typeof value === 'string') {
const keys = parsePatternToKeys(value, treeData);
setCheckedKeys(keys);
}
}, [value, treeData]);
// 3. When users manually check/uncheck items, convert internal keys back to string and trigger onChange
const handleTreeChange = (keys: any) => {
// Handle the object format that antd tree may return
const actualKeys = Array.isArray(keys) ? keys : keys.checked;
// Update internal UI state
setCheckedKeys(actualKeys);
// Generate string to pass to external Form
if (onChange) {
const patternString = generatePattern(treeData, actualKeys);
onChange(patternString);
}
};
if (!dataSourceId) return null;
if (loading) return <Loading padding="0" />;
return (
<div className="addon-select-wrapper">
<ProFormTreeSelect
label={`${$t('pages.rules.monitoredSymbols')} (${$t('pages.tips.emptyIsAll')})`}
extra={$t('form.rule1.tips2')}
placeholder={`${$t('pages.rules.allSymbols')}`}
fieldProps={{
listHeight: 410,
treeData: treeData,
treeCheckable: true,
value: checkedKeys, // Use the internally parsed array
onChange: handleTreeChange,
maxTagCount: 'responsive',
virtual: true,
fieldNames: {
label: 'title',
value: 'key',
},
}}
formItemProps={{
style: { marginBottom: 0 },
}}
/>
</div>
);
};
export default SymbolSelect;

View File

@@ -0,0 +1,27 @@
.addon-select-wrapper {
.ant-form-item-control-input {
flex: 1;
}
}
.clone-rule-card {
.ant-pro-card-header {
padding: 0;
.ant-pro-card-title {
font-size: 14px;
}
}
}
.ant-pro-steps-form-container {
width: inherit;
}
.confirm-container {
position: relative;
.confirm-space {
width: 100%;
height: 30vh;
background: rgba(239, 68, 68, 0.04);
border: 1px solid rgba(239, 68, 68, 0.2);
border-radius: 12px;
}
}

View File

@@ -0,0 +1,498 @@
import { $t } from '@/utils/i18n';
import { aggregationLogicOptions } from '../ruleFormSchema/liquidityTrade';
import { volatilityMap } from '../ruleFormSchema/volatility';
import { getLabels } from '../ruleFormSchema/watchList';
export type pageCardInfo = {
label: string;
key: keyof API.RuleListItem;
// key: string;
right?: (p: string, item: API.RuleListItem) => string;
};
type RuleTypeBaseMeta = {
icon?: string;
title?: string;
desc?: string;
value: number;
path?: string;
name?: string;
};
type RuleTypeExtraMeta = {
pageCardInfo: pageCardInfo[];
};
export type RuleType = {
id: number;
value: number;
name: string;
type: number;
meta?: RuleTypeBaseMeta;
};
export default function useRuleMeta() {
const translatedVolatilityMap = volatilityMap.map((item) => ({
...item,
label: $t(item.label),
}));
const translatedAggregationLogicOptions = aggregationLogicOptions.map(
(item) => ({
...item,
label: $t(item.label),
}),
);
const ruleBaseMeta: Record<string, RuleTypeBaseMeta> = {
'1': {
icon: '💰',
title: $t('menu.rules.largeTradeLots'),
desc: $t('menu.rules.largeTradeLots.desc'),
path: 'large-trade-lots',
name: 'largeTradeLots',
value: 1,
},
'2': {
icon: '💵',
title: $t('menu.rules.largeTradeUSD'),
desc: $t('menu.rules.largeTradeUSD.desc'),
path: 'large-trade-usd',
name: 'largeTradeUSD',
value: 2,
},
'3': {
icon: '🌊',
title: $t('menu.rules.liquidityTrade'),
desc: $t('menu.rules.liquidityTrade.desc'),
path: 'liquidity-trade',
name: 'liquidityTrade',
value: 3,
},
'4': {
icon: '⚡',
title: $t('menu.rules.scalping'),
desc: $t('menu.rules.scalping.desc'),
path: 'scalping',
name: 'scalping',
value: 4,
},
'5': {
icon: '📊',
title: $t('menu.rules.exposureAlert'),
desc: $t('menu.rules.exposureAlert.desc'),
path: 'exposure-alert',
name: 'exposureAlert',
value: 5,
},
// '6': {
// icon: '📈',
// title: $t('menu.rules.pricingVolatility'),
// desc: $t('menu.rules.pricingVolatility.desc'),
// value: 6,
// },
'6': {
icon: '⏱️',
title: $t('menu.rules.pricing'),
desc: $t('menu.rules.pricing.desc'),
path: 'pricing',
name: 'pricing',
value: 6,
},
'11': {
icon: '📈',
title: $t('menu.rules.volatility'),
desc: $t('menu.rules.volatility.desc'),
path: 'volatility',
name: 'volatility',
value: 11,
},
'7': {
icon: '📐',
title: $t('menu.rules.NOPLimit'),
desc: $t('menu.rules.NOPLimit.desc'),
path: 'nop-limit',
name: 'NOPLimit',
value: 7,
},
'8': {
icon: '👁️',
title: $t('menu.rules.watchList'),
desc: $t('menu.rules.watchList.desc'),
path: 'watch-list',
name: 'watchList',
value: 8,
},
'9': {
icon: '🔀',
title: $t('menu.rules.reversePositions'),
desc: $t('menu.rules.reversePositions.desc'),
path: 'reverse-positions',
name: 'reversePositions',
value: 9,
},
'10': {
icon: '💳',
title: $t('menu.rules.depositWithdrawal'),
desc: $t('menu.rules.depositWithdrawal.desc'),
path: 'deposit-withdrawal',
name: 'depositWithdrawal',
value: 10,
},
};
const ruleExtraMeta: Record<string, RuleTypeExtraMeta> = {
// largeTradeLots
'1': {
pageCardInfo: [
{
label: $t('pages.rules.triggerValue'),
key: 'param1',
right: (p: string) => `${p} Lots`,
},
{
label: $t('pages.rules.monitoredGroups'),
key: 'groupFilter',
right: (p: string) => `${p || $t('pages.rules.allGroups')}`,
},
{
label: $t('pages.rules.monitoredSymbols'),
key: 'param2',
right: (p: string) => `${p || $t('pages.rules.allSymbols')}`,
},
],
},
// largeTradeUSD
'2': {
pageCardInfo: [
{
label: $t('pages.rules.triggerValueUSD'),
key: 'param1',
right: (p: string) => `$${p}`,
},
// {
// label: $t('form.rule2.keyword'),
// key: 'param3',
// right: (p: string) => `${p || 'N/A'}`,
// },
{
label: $t('pages.rules.monitoredGroups'),
key: 'groupFilter',
right: (p: string) => `${p || $t('pages.rules.allGroups')}`,
},
{
label: $t('pages.rules.monitoredSymbols'),
key: 'param2',
right: (p: string) => `${p || $t('pages.rules.allSymbols')}`,
},
],
},
// liquidityTrade
'3': {
pageCardInfo: [
{
label: $t('pages.rule3.timeWindow'),
key: 'param1',
right: (p: string) => `${p} ${$t('pages.rules.seconds')}`,
},
{
label: $t('pages.rule3.minOrderCount'),
key: 'param3',
right: (p: string) =>
`${p} ${$t('pages.rule3.minOrderCount.tips')}`,
},
{
label: $t('pages.rule3.totalLotsThreshold'),
key: 'param4',
right: (p: string) => `${p} ${$t('pages.rules.lots')}`,
},
{
label: $t('pages.rule3.aggregationLogic'),
key: 'param5',
right: (p: string) =>
p
? `${translatedAggregationLogicOptions[+p || 0]?.label || 'N/A'}`
: 'N/A',
},
{
label: $t('pages.rules.monitoredGroups'),
key: 'groupFilter',
right: (p: string) => `${p || $t('pages.rules.allGroups')}`,
},
{
label: $t('pages.rules.monitoredSymbols'),
key: 'param2',
right: (p: string) => `${p || $t('pages.rules.allSymbols')}`,
},
],
},
// scalping
'4': {
pageCardInfo: [
{
label: $t('pages.rule4.item1'),
key: 'param1',
right: (p: string) => `< ${p} ${$t('pages.rules.seconds')}`,
},
{
label: `${$t('pages.rule4.item3')}`,
key: 'param3',
right: (p: string) => `${p || 0}`,
},
{
label: `${$t('pages.rule4.item2')}`,
key: 'param5',
right: (p: string) => `${p || 0}`,
},
// {
// label: $t('form.rule2.keyword'),
// key: 'param6',
// right: (p: string) => `${p || 'N/A'}`,
// },
{
label: $t('pages.rules.monitoredGroups'),
key: 'groupFilter',
right: (p: string) => `${p || $t('pages.rules.allGroups')}`,
},
{
label: $t('pages.rules.monitoredSymbols'),
key: 'param2',
right: (p: string) => `${p || $t('pages.rules.allSymbols')}`,
},
],
},
// exposureAlert
'5': {
pageCardInfo: [
// {
// label: $t('pages.rule5.item1'),
// key: 'param1',
// },
// {
// label: $t('pages.rule5.item2'),
// key: 'param2',
// },
// {
// label: $t('pages.rules.reportInterval'),
// key: 'param3',
// right: (p: string) => `${p} ${$t('pages.rules.seconds')}`,
// },
{
label: $t('pages.rule5_1.item1'),
key: 'param1',
right: (p: string) => `${p || $t('pages.rules.allAssets')}`,
},
{
label: $t('pages.rule5_1.item2'),
key: 'param2',
},
{
label: $t('pages.rule5_1.item4'),
key: 'param4',
},
{
label: $t('pages.rules.reportInterval'),
key: 'param3',
right: (p: string) => `${p} ${$t('pages.rules.seconds')}`,
},
],
},
// pricingVolatility
// '6': {
// pageCardInfo: [
// {
// label: $t('pages.rule6.item1'),
// key: 'param1',
// right: (p: string) => `${p} ${$t('pages.rules.seconds')}`,
// },
// {
// label: $t('pages.rule6.item2'),
// key: 'param4',
// right: (p: string) => `${p}`,
// },
// {
// label: $t('pages.rules.monitoredSymbols'),
// key: 'param2',
// right: (p: string) => `${p || $t('pages.rules.allSymbols')}`,
// },
// ],
// },
// pricing
'6': {
pageCardInfo: [
{
label: $t('pages.rule6_1.item1'),
key: 'param1',
right: (p: string) => `${p} ${$t('pages.rules.seconds')}`,
},
{
label: $t('pages.rules.reportInterval'),
key: 'param3',
right: (p: string) => `${p} ${$t('pages.rules.seconds')}`,
},
{
label: $t('pages.rules.monitoredSymbols'),
key: 'param2',
right: (p: string) => `${p || $t('pages.rules.allSymbols')}`,
},
],
},
// volatility
'11': {
pageCardInfo: [
{
label: $t('pages.rule11.item1'),
key: 'param1',
right: (p: string) =>
`${translatedVolatilityMap[+p]?.label || 'N/A'}`,
},
{
label: $t('pages.rule11.item2'),
key: 'param3',
right: (p: string) => `${p} Minutes`,
},
{
label: $t('pages.rule11.item3'),
key: 'param4',
right: (p: string, rule) => {
const param1 = rule.param1 ? +rule.param1 : 0;
return `${p} ${translatedVolatilityMap[param1]?.unitStr || 'N/A'}`;
},
},
{
label: $t('pages.rules.monitoredSymbols'),
key: 'param2',
right: (p: string) => `${p || $t('pages.rules.allSymbols')}`,
},
],
},
// NOPLimit
'7': {
pageCardInfo: [
{
label: $t('pages.rule7.item2'),
key: 'param1',
},
{
label: $t('pages.rules.monitoredSymbols'),
key: 'param2',
right: (p: string) => `${p || $t('pages.rules.allSymbols')}`,
},
],
},
// watchList
'8': {
pageCardInfo: [
{
label: $t('pages.rule8.item1'),
key: 'param1',
},
{
label: $t('pages.rule8.item2'),
key: 'param2',
right: (p: string) => `${getLabels(+p, $t)}`,
},
{
label: $t('pages.rule8.item3'),
key: 'param3',
right: (p: string) => `${p} ${$t('pages.rules.lots')}`,
},
],
},
// reversePositions
'9': {
pageCardInfo: [
{
label: $t('pages.rule9.item1'),
key: 'param1',
right: (p: string) => `${p} ${$t('pages.rules.seconds')}`,
},
{
label: $t('pages.rule9.item2'),
key: 'param3',
right: (p: string) => `${p || '-'} ${$t('pages.rules.lots')}`,
},
{
label: $t('pages.rule9.item3'),
key: 'param4',
right: (p: string) => `$${p || '-'}`,
},
// {
// label: $t('form.rule2.keyword'),
// key: 'param5',
// right: (p: string) => `${p || 'N/A'}`,
// },
{
label: $t('pages.rules.monitoredGroups'),
key: 'groupFilter',
right: (p: string) => `${p || $t('pages.rules.allGroups')}`,
},
{
label: $t('pages.rules.monitoredSymbols'),
key: 'param2',
right: (p: string) => `${p || $t('pages.rules.allSymbols')}`,
},
],
},
// depositWithdrawal
'10': {
pageCardInfo: [
{
label: $t('pages.rule10.item1'),
key: 'param1',
},
{
label: $t('pages.rule10.item2'),
key: 'param2',
},
{
label: $t('pages.rule10.item3'),
key: 'param3',
right: (p: string) => `${p || 'N/A'}`,
},
{
label: $t('pages.rules.monitoredGroups'),
key: 'groupFilter',
right: (p: string) => `${p || $t('pages.rules.allGroups')}`,
},
],
},
};
const symbolsShowFunc = (symbols: string | string[] | undefined) => {
if (!symbols) {
return $t('pages.rules.allSymbols');
}
const isArray = Array.isArray(symbols);
const symbolsArray = isArray ? symbols : symbols?.split(',') || [];
if (symbolsArray.length > 3) {
return `${symbolsArray.slice(0, 3).join(',')}...`;
}
return symbolsArray.join(',');
};
function extendRulesMeta(reqRuleList: RuleType[]) {
return reqRuleList.map((item) => {
const ruleType = ruleBaseMeta[item.value];
if (ruleType) {
item.meta = ruleType;
}
return item;
});
}
function getAllMetaByType(type: number | string) {
return {
...ruleBaseMeta[type],
...ruleExtraMeta[type],
};
}
return { ruleBaseMeta, extendRulesMeta, getAllMetaByType, symbolsShowFunc };
}

539
src/pages/rules/index.tsx Normal file
View File

@@ -0,0 +1,539 @@
import {
BellOutlined,
CopyOutlined,
PlusOutlined,
RedoOutlined,
} from '@ant-design/icons';
import { ProCard } from '@ant-design/pro-components';
import { useAccess, useRequest, useRouteProps } from '@umijs/max';
import {
App,
Badge,
Button,
Col,
Divider,
Empty,
Flex,
Modal,
Pagination,
Row,
Select,
Space,
Table,
type TableColumnType,
Typography,
} from 'antd';
import React, { useEffect, useMemo, useState } from 'react';
import MyTag from '@/components/MyTag';
import useUserInfo from '@/hooks/useUserInfo';
import Loading from '@/loading';
import {
getAlertPages,
getRulePages,
ruleHandle,
ruleStatusHandle,
} from '@/services/api/api';
import { getAllDataSourceList } from '@/utils/dataCenter';
import { $t } from '@/utils/i18n';
import { formatToUserTimezone } from '@/utils/timeFormat';
import MultiCopyPopup from './components/CopyRule/MultiCopyPopup';
import SingleCopyPopup from './components/CopyRule/SingleCopyPopup';
import PopUp from './components/PopUp';
import RuleCardBody from './components/RuleCardBody';
import RuleHeader from './components/RuleHeader';
import useRuleMeta from './hooks/useRuleMeta';
const { Text } = Typography;
const rulePageSize = 6;
const RuleCommon: React.FC = () => {
const { ruleType: CURRENT_RULE_TYPE } = useRouteProps();
const { getAllMetaByType } = useRuleMeta();
const { userInfo } = useUserInfo();
const { isGlobalCompany } = useAccess();
const [rules, setRules] = useState<API.RuleListItem[]>([]);
const [alerts, setAlerts] = useState<API.AlertListItem[]>([]);
const [visible, setVisible] = useState(false);
const [copyVisible, setCopyVisible] = useState(false);
const [multiCopyVisible, setMultiCopyVisible] = useState(false);
const [ruleStatusHandleLoading, setRuleStatusHandleLoading] = useState(false);
const [action, setAction] = useState<EX.Action>('add');
const [formValues, setFormValues] = useState<Partial<API.RuleListItem>>({});
const [dataSourceList, setDataSourceList] = useState<
API.DataSourceListItem[]
>([]);
const { message } = App.useApp();
const [curDataSourceId, setCurDataSourceId] = useState<string | number>('');
const [ruleTotal, setRuleTotal] = useState<number>(0);
const [ruleCurrent, setRuleCurrent] = useState<number>(1);
const ruleMeta = useMemo(() => {
return getAllMetaByType(CURRENT_RULE_TYPE);
}, [CURRENT_RULE_TYPE]);
const companyInfo = userInfo?.company;
const userDataSourceList = userInfo?.userDataSourceList || [];
useEffect(() => {
getAllDataSourceList(companyInfo, true).then((list) => {
const newList = list.filter((item) => {
item.disabled = item.status !== 1;
const find = userDataSourceList.find(
(userItem: any) => userItem.dataSourceId === item.id,
);
return find !== void 0;
});
setDataSourceList(newList);
});
}, []);
useEffect(() => {
refreshRules(1);
}, [curDataSourceId, CURRENT_RULE_TYPE]);
const handleDone = (refresh?: boolean) => {
setVisible(false);
setCopyVisible(false);
setMultiCopyVisible(false);
setFormValues({});
if (refresh) {
setCurDataSourceId('');
refreshRules(1);
}
};
const handleSubmit = async (values: Partial<API.RuleListItem>) => {
// console.log(values);
// return;
const { code } = await ruleHandle(values);
if (code === 200) {
message.success($t('pages.tips.operationSuccess'));
handleDone();
refreshRules(1);
}
};
// recent alert list
const { loading: alertLoading } = useRequest(
() => {
return getAlertPages({
pageNum: 1,
pageSize: 10,
ruleType: +CURRENT_RULE_TYPE,
});
},
{
refreshDeps: [CURRENT_RULE_TYPE],
onSuccess: ({ data }) => {
setAlerts(data.list);
},
},
);
const { loading: rulesLoading, run: refreshRules } = useRequest(
(pageNum = 1) => {
setRuleCurrent(pageNum);
return getRulePages({
pageNum,
pageSize: rulePageSize,
type: CURRENT_RULE_TYPE,
dataSourceId: curDataSourceId,
});
},
{
// refreshDeps: [CURRENT_RULE_TYPE],
manual: true,
onSuccess: ({ data }) => {
setRules(data.list);
setRuleTotal(data.total);
},
},
);
// Enable/disable and delete handlers
const handleToggleStatus = async (item: API.RuleListItem) => {
setRuleStatusHandleLoading(true);
const newStatus = item.status === 2 ? 1 : 2;
const { code } = await ruleStatusHandle({
id: item.id,
status: newStatus,
}).catch(() => setRuleStatusHandleLoading(false));
if (code === 200) {
setRules((prev) => {
return prev.map((v) => {
if (v.id === item.id) {
return { ...v, status: newStatus };
}
return v;
});
});
}
setRuleStatusHandleLoading(false);
};
const handleDelete = (item: API.RuleListItem) => {
Modal.confirm({
title: $t('pages.model.delete'),
content: $t('pages.model.delete.content'),
okText: $t('pages.model.confirm'),
okType: 'danger',
cancelText: $t('pages.model.cancel'),
onOk: async () => {
setRuleStatusHandleLoading(true);
const { code } = await ruleStatusHandle({
id: item.id,
status: 4,
}).finally(() => setRuleStatusHandleLoading(false));
if (code === 200) {
refreshRules(ruleCurrent);
}
},
});
};
const ruleStatusMap = [
'#3b82f6',
'#10b981',
'#64748b',
'#f97316',
'#ef4444',
'#64748b',
];
// Rendering rules cards
const renderRuleCard = (rule: API.RuleListItem) => (
<ProCard key={rule.id} bordered hoverable style={{ height: '100%' }}>
<div
onClick={() => {
setAction('view');
setFormValues(rule);
setVisible(true);
}}
>
{/* card header */}
<div
className="flex justify-between align-center flex-wrap"
style={{
marginBottom: 16,
}}
>
<Space>
<div
style={{
width: 40,
height: 40,
borderRadius: 8,
backgroundColor: '#6366f1',
fontSize: 20,
}}
className="flex justify-center align-center"
>
{ruleMeta.icon}
</div>
<Text strong style={{ fontSize: 16 }}>
{rule.name || ruleMeta.title || ''}
</Text>
</Space>
<Space>
<MyTag color={ruleStatusMap[rule.status]}>
{$t(`pages.rules.status${rule.status}`)}
</MyTag>
</Space>
</div>
<RuleCardBody ruleMeta={ruleMeta} rule={rule} />
<Divider style={{ margin: '14px 0' }} />
{/* card footer */}
<Space className="w-100 justify-between flex-wrap">
<Space>
<span>📊</span>
<Text type="secondary">
{`${$t('pages.rules.trigger')}: ${rule.count} ${$t('pages.rules.triggeredCount')}`}
</Text>
</Space>
</Space>
</div>
{!isGlobalCompany && (
<>
<Divider style={{ margin: '14px 0' }} />
<Flex justify="end">
<Space wrap style={{ justifyContent: 'flex-end' }}>
<Button
variant="solid"
color={rule.status === 2 ? 'cyan' : 'orange'}
disabled={
![1, 2].includes(rule.status) || ruleStatusHandleLoading
}
onClick={() => handleToggleStatus(rule)}
>
{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);
setCopyVisible(true);
}}
>
{$t('pages.rules.clone')}
</Button>
</Space>
</Flex>
</>
)}
</ProCard>
);
const statusMap = [
{
color: '#f97316',
text: $t('table.new'),
},
{
color: '#10b981',
text: $t('table.viewed'),
},
{
color: '#64748b',
text: $t('table.ignored'),
},
];
const alertColumns: TableColumnType<API.AlertListItem>[] = [
{
title: `${$t('table.triggerTime')} (UTC+0)`,
dataIndex: 'triggerTime',
render: (_, { triggerTime }) => {
return formatToUserTimezone(triggerTime);
},
},
{
title: $t('table.dataSourceTime'),
dataIndex: 'dataSourceServerTime',
render: (_, { dataSourceServerTime, timeZone }) => {
if (!dataSourceServerTime) return '-';
return (
<Space wrap>
<Text>
{formatToUserTimezone(dataSourceServerTime as string) || '-'}{' '}
</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
{timeZone || '-'}{' '}
</Text>
</Space>
);
},
},
{
title: $t('table.dataSource'),
dataIndex: 'dataSourceId',
render: (_, { dataSourceId }) => {
const finded = dataSourceList.find((item) => item.id === dataSourceId);
return (
<MyTag color={finded?.sourceType === 4 ? '#818cf8' : '#10b981'}>
{finded?.sourceName ?? '-'}
</MyTag>
);
},
},
{
title: $t('table.account'),
dataIndex: 'tradeAccount',
render: (_, { tradeAccount }) => {
return tradeAccount || 'SYSTEM';
},
},
{
title: $t('table.symbol'),
dataIndex: 'product',
render: (_, { product }) => {
return product || 'N/A';
},
},
{
title: $t('table.trigger'),
dataIndex: 'trigger',
},
{
title: $t('table.status'),
dataIndex: 'status',
render: (_, { status = 1 }) => {
return (
<Badge
color={statusMap[status - 1]?.color || '#64748b'}
text={statusMap[status - 1]?.text || '-'}
/>
);
},
},
];
return (
<>
<Space wrap className="justify-between" style={{ marginBottom: 24 }}>
<RuleHeader ruleMeta={ruleMeta} />
<Space wrap size={16}>
{dataSourceList.length > 1 ? (
<Space size={16}>
<span>{$t('table.dataSource')}:</span>
<Select
value={curDataSourceId}
onChange={(val) => setCurDataSourceId(val)}
options={[
{
label: $t('table.all'),
value: '',
},
...dataSourceList.map((item) => ({
label: item.sourceName,
value: item.id,
})),
]}
style={{ minWidth: 150 }}
popupMatchSelectWidth={false}
/>
</Space>
) : null}
{ruleTotal ? (
<Pagination
disabled={rulesLoading}
simple
current={ruleCurrent}
pageSize={rulePageSize}
total={ruleTotal}
onChange={(pageNum) => refreshRules(pageNum)}
/>
) : null}
<Button
shape="circle"
icon={<RedoOutlined />}
onClick={() => refreshRules(1)}
/>
{!isGlobalCompany && (
<>
<Button
key="1"
type="primary"
onClick={() => {
setAction('add');
setFormValues({});
setVisible(true);
}}
>
<PlusOutlined />
{`${$t('table.action.add')}${$t('menu.rule')}`}
</Button>
<Button
icon={<CopyOutlined />}
onClick={() => {
if (dataSourceList.length < 2) {
message.warning($t('pages.rules.cloneRuleWarning'));
return;
}
setMultiCopyVisible(true);
}}
>
{$t('pages.clones.title')}
</Button>
</>
)}
</Space>
</Space>
<Divider style={{ marginBottom: '24px', marginTop: '0' }} />
{/* rules card grid */}
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
{rulesLoading ? (
<Loading padding="0" />
) : rules.length ? (
rules.map((rule) => (
<Col xs={24} sm={24} md={12} lg={12} xl={12} xxl={8} key={rule.id}>
{renderRuleCard(rule)}
</Col>
))
) : (
<Space className="flex justify-center" style={{ width: '100%' }}>
<Empty description={$t('common.content.noRules')} />
</Space>
)}
</Row>
{/* recent alerts table */}
<ProCard
title={
<Space>
<BellOutlined />
<Text strong>{$t('pages.dashboard.recentAlerts')}</Text>
</Space>
}
// extra={<Button type="primary">{ $t('table.action.viewAll') }</Button>}
>
<Table
pagination={false}
dataSource={alerts}
columns={alertColumns}
loading={alertLoading}
rowKey="id"
size="small"
/>
</ProCard>
<PopUp
action={action}
currentRuleType={+CURRENT_RULE_TYPE}
ruleMeta={ruleMeta}
visible={visible}
curDataSourceId={curDataSourceId}
dataSourceList={dataSourceList}
formValues={formValues}
onSubmit={handleSubmit}
onDone={handleDone}
/>
<SingleCopyPopup
currentRuleType={+CURRENT_RULE_TYPE}
visible={copyVisible}
dataSourceList={dataSourceList}
formValues={formValues}
onDone={handleDone}
/>
<MultiCopyPopup
ruleMeta={ruleMeta}
currentRuleType={+CURRENT_RULE_TYPE}
visible={multiCopyVisible}
dataSourceList={dataSourceList}
onDone={handleDone}
/>
</>
);
};
export default RuleCommon;

View File

@@ -0,0 +1,59 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import useRuleMeta from '../hooks/useRuleMeta';
import type { returnType, SchemaProps } from '../type';
export const NOPLimitSchema = ({ richFormat }: SchemaProps): returnType => {
const { symbolsShowFunc } = useRuleMeta();
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2'],
content: ({ param1, param2 }) => {
return richFormat('form.rules.header.content7', {
nopLimit: param1 || 0,
symbols: symbolsShowFunc(param2),
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: $t('pages.rule7.item2'),
dataIndex: 'param1',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 5000,
};
},
tipsComponent: $t('form.rule7.tips'),
symbolFormShow: true,
};
};

View File

@@ -0,0 +1,81 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import GroupSelectWrapper from '../components/GroupSelectWrapper';
import type { returnType, SchemaProps } from '../type';
export const depositWithdrawalSchema = ({
richFormat,
}: SchemaProps): returnType => {
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3'],
content: ({ param1, param2, param3 }) => {
return richFormat('form.rules.header.content10', {
depositValue: param1 || 0,
withdrawValue: param2 || 0,
keywords: param3 || '---',
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: `${$t('pages.rule10.item1')}`,
dataIndex: 'param1',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(),
},
},
{
title: $t('pages.rule10.item2'),
dataIndex: 'param2',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(),
},
},
{
title: `${$t('pages.rule10.item3')} (${$t('pages.rule10.item3.tips')})`,
dataIndex: 'param3',
width: '100%',
},
{
renderFormItem: () => <GroupSelectWrapper />,
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 10000,
param2: 5000,
};
},
tipsComponent: $t('form.rule10.tips'),
symbolFormShow: false,
};
};

View File

@@ -0,0 +1,78 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import type { returnType, SchemaProps } from '../type';
export const exposureAlertSchema = ({
richFormat,
}: SchemaProps): returnType => {
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3'],
content: ({ param1, param2, param3 }) => {
return richFormat('form.rules.header.content5', {
currency: param1 || '---',
exposureValue: param2 || '---',
interval: param3 || 0,
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: `${$t('pages.rule5.item1')}`,
dataIndex: 'param1',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: $t('pages.rule5.item2'),
dataIndex: 'param2',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: `${$t('pages.rules.reportInterval')} (${$t('pages.rules.seconds')})`,
dataIndex: 'param3',
valueType: 'digit',
width: '100%',
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 'USD',
param2: 10000000,
param3: 10,
};
},
tipsComponent: $t('form.rule5.tips'),
symbolFormShow: false,
};
};

View File

@@ -0,0 +1,138 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import type { returnType, SchemaProps } from '../type';
export const exposureAlertSchema = ({
richFormat,
}: SchemaProps): returnType => {
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3', 'param4'],
content: ({ param1, param2, param3, param4 }) => {
const isArray = Array.isArray(param1);
const tmp = isArray ? param1 : param1?.split(',');
const assets = `${tmp?.slice(0, 3)?.join(',')}...`;
return richFormat?.('form.rules.header.content5_1', {
assets: param1 ? assets : $t('pages.rules.allAssets'),
upperThreshold: param2 || '---',
lowerThreshold: param4 || '---',
interval: param3 || 0,
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: `${$t('pages.rule5_1.item2')} (USD)`,
dataIndex: 'param2',
valueType: 'digit',
width: '100%',
dependencies: ['param4'],
formItemProps: {
rules: [
{ required: true, message: $t('form.required') },
({ getFieldValue }) => ({
validator(_, value) {
const minValue = getFieldValue('param4');
if (
value !== undefined &&
minValue !== undefined &&
value < minValue
) {
return Promise.reject(
new Error($t('pages.rule5_1.validateError2')),
);
}
return Promise.resolve();
},
}),
],
},
fieldProps: {
...genDigitComAttr(),
},
},
{
title: `${$t('pages.rule5_1.item4')} (USD)`,
dataIndex: 'param4',
valueType: 'digit',
width: '100%',
dependencies: ['param2'],
formItemProps: {
rules: [
{ required: true, message: $t('form.required') },
({ getFieldValue }) => ({
validator(_, value) {
const maxValue = getFieldValue('param2');
if (
value !== undefined &&
maxValue !== undefined &&
value > maxValue
) {
return Promise.reject(
new Error($t('pages.rule5_1.validateError4')),
);
}
return Promise.resolve();
},
}),
],
},
fieldProps: {
...genDigitComAttr(),
min: -Number.MAX_SAFE_INTEGER,
},
},
{
title: `${$t('pages.rules.reportInterval')} (${$t('pages.rules.seconds')})`,
dataIndex: 'param3',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: $t('pages.rule5_1.item5'),
dataIndex: 'param5',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
hidden: true,
},
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param2: 10000000,
param4: -10000000,
param3: 10,
param5: 'USD',
};
},
tipsComponent: $t('form.rule5.tips'),
assetFormShow: true,
};
};

View File

@@ -0,0 +1,29 @@
import { depositWithdrawalSchema } from './depositWithdrawal';
import { exposureAlertSchema } from './exposureAlert2';
import { largeTradeLotsSchema } from './largeTradeLots';
import { largeTradeUSDchema } from './largeTradeUSD';
import { liquidityTradeSchema } from './liquidityTrade';
import { NOPLimitSchema } from './NOPLimit';
// import { pricingVolatilitySchema } from './pricingVolatility';
import { pricingSchema } from './pricing';
import { reversePositionsSchema } from './reversePositions';
import { scalpingSchema } from './scalping';
import { volatilitySchema } from './volatility';
import { watchListSchema } from './watchList';
export const schemaMap = {
'1': largeTradeLotsSchema,
'2': largeTradeUSDchema,
'3': liquidityTradeSchema,
'4': scalpingSchema,
'5': exposureAlertSchema,
// '6': pricingVolatilitySchema,
'6': pricingSchema,
'11': volatilitySchema,
'7': NOPLimitSchema,
'8': watchListSchema,
'9': reversePositionsSchema,
'10': depositWithdrawalSchema,
};
export type SchemaType = keyof typeof schemaMap;

View File

@@ -0,0 +1,71 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import GroupSelectWrapper from '../components/GroupSelectWrapper';
import useRuleMeta from '../hooks/useRuleMeta';
import type { returnType, SchemaProps } from '../type';
export const largeTradeLotsSchema = ({
richFormat,
}: SchemaProps): returnType => {
const { symbolsShowFunc } = useRuleMeta();
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2'],
content: ({ param1, param2 }) => {
return richFormat?.('form.rules.header.content1', {
lots: param1 || 0,
symbols: symbolsShowFunc(param2),
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: $t('pages.rules.triggerValue'),
dataIndex: 'param1',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(),
},
},
// {
// title: $t('form.rule1.ignoreSimulatedAccount'),
// dataIndex: 'param3',
// valueType: 'switch',
// initialValue: false,
// convertValue: (value) => Boolean(+value),
// },
{
renderFormItem: () => <GroupSelectWrapper />,
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 0.1,
};
},
tipsComponent: $t('form.rule1.tips'),
symbolFormShow: true,
};
};

View File

@@ -0,0 +1,69 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import GroupSelectWrapper from '../components/GroupSelectWrapper';
import useRuleMeta from '../hooks/useRuleMeta';
import type { returnType, SchemaProps } from '../type';
export const largeTradeUSDchema = ({ richFormat }: SchemaProps): returnType => {
const { symbolsShowFunc } = useRuleMeta();
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2'],
content: ({ param1, param2 }) => {
return richFormat('form.rules.header.content2', {
amount: param1 || 0,
symbols: symbolsShowFunc(param2),
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: $t('pages.rules.triggerValueUSD'),
dataIndex: 'param1',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(),
},
},
// {
// title: $t('form.rule2.keyword'),
// dataIndex: 'param3',
// fieldProps: {
// placeholder: $t('form.rule2.keyword.placeholder'),
// },
// },
{
renderFormItem: () => <GroupSelectWrapper />,
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 50000,
};
},
tipsComponent: $t('form.rule2.tips'),
symbolFormShow: true,
};
};

View File

@@ -0,0 +1,122 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import GroupSelectWrapper from '../components/GroupSelectWrapper';
import useRuleMeta from '../hooks/useRuleMeta';
import type { returnType, SchemaProps } from '../type';
export const aggregationLogicOptions = [
{
label: 'pages.rule3.option1',
value: '0',
},
{
label: 'pages.rule3.option2',
value: '1',
},
];
export const liquidityTradeSchema = ({
richFormat,
}: SchemaProps): returnType => {
const { symbolsShowFunc } = useRuleMeta();
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3', 'param4', 'param5'],
content: ({ param1, param2, param3, param4, param5 }) => {
return richFormat('form.rules.header.content3', {
seconds: param1 || 0,
orderCount: param3 || 0,
totalLots: param4 || 0,
aggregationLogic: param5
? $t(aggregationLogicOptions[+param5 || 0]?.label)
: '---',
symbols: symbolsShowFunc(param2),
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: `${$t('pages.rule3.timeWindow')} (${$t('pages.rules.seconds')})`,
dataIndex: 'param1',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: $t('pages.rule3.minOrderCount'),
dataIndex: 'param3',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: $t('pages.rule3.totalLotsThreshold'),
dataIndex: 'param4',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(2),
},
},
{
title: $t('pages.rule3.aggregationLogic'),
dataIndex: 'param5',
valueType: 'select',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
options: aggregationLogicOptions.map((item) => ({
label: $t(item.label),
value: item.value,
})),
allowClear: false,
},
},
{
renderFormItem: () => <GroupSelectWrapper />,
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 60,
param3: 2,
param4: 10,
param5: '1',
};
},
tipsComponent: $t('form.rule3.tips'),
symbolFormShow: true,
};
};

View File

@@ -0,0 +1,70 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import useRuleMeta from '../hooks/useRuleMeta';
import type { returnType, SchemaProps } from '../type';
export const pricingSchema = ({ richFormat }: SchemaProps): returnType => {
const { symbolsShowFunc } = useRuleMeta();
return {
popFormTips: {
title: `⏱️ ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3'],
content: ({ param1, param2, param3 }) => {
return richFormat('form.rules.header.content6_1', {
stopTime: param1 || 0,
symbols: symbolsShowFunc(param2),
interval: param3 || 0,
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: `${$t('pages.rule6_1.item1')} (${$t('pages.rules.seconds')})`,
dataIndex: 'param1',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: `${$t('pages.rules.reportInterval')} (${$t('pages.rules.seconds')})`,
dataIndex: 'param3',
valueType: 'digit',
width: '100%',
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 30,
param3: 10,
};
},
tipsComponent: $t('form.rule6_1.tips'),
symbolFormShow: true,
};
};

View File

@@ -0,0 +1,97 @@
import { genDigitComAttr, preventScientificNotation } from '@/utils';
import { $t } from '@/utils/i18n';
import useRuleMeta from '../hooks/useRuleMeta';
import type { returnType, SchemaProps } from '../type';
export const pricingVolatilitySchema = ({
richFormat,
}: SchemaProps): returnType => {
const { symbolsShowFunc } = useRuleMeta();
const volatilityMap = [
{
label: $t('pages.rule6.option1'),
value: '0',
},
{
label: $t('pages.rule6.option2'),
value: '1',
},
];
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3', 'param4'],
content: ({ param1, param2, param3, param4 }) => {
return richFormat('form.rules.header.content6', {
stopTime: param1 || 0,
volatility: param4 || 0,
volatilityUnit: param3 || 0,
symbols: symbolsShowFunc(param2),
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: `${$t('pages.rule6.item1')} (${$t('pages.rules.seconds')})`,
dataIndex: 'param1',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(0, 0),
},
},
{
title: $t('pages.rule6.item3'),
dataIndex: 'param3',
valueType: 'select',
fieldProps: {
options: volatilityMap,
},
},
{
title: $t('pages.rule6.item2'),
dataIndex: 'param4',
valueType: 'digit',
width: '100%',
fieldProps: {
type: 'number',
min: 0.1,
max: 100,
step: 0.1,
precision: 1,
onKeyDown: preventScientificNotation,
},
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 30,
param3: '0',
param4: 100,
};
},
tipsComponent: $t('form.rule6.tips'),
symbolFormShow: true,
};
};

View File

@@ -0,0 +1,93 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import GroupSelectWrapper from '../components/GroupSelectWrapper';
import useRuleMeta from '../hooks/useRuleMeta';
import type { returnType, SchemaProps } from '../type';
export const reversePositionsSchema = ({
richFormat,
}: SchemaProps): returnType => {
const { symbolsShowFunc } = useRuleMeta();
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3', 'param4'],
content: ({ param1, param2, param3, param4 }) => {
return richFormat('form.rules.header.content9', {
seconds: param1 || 0,
lots: param3 || 0,
usdValue: param4 || 0,
symbols: symbolsShowFunc(param2),
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: $t('pages.rule9.item1'),
dataIndex: 'param1',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: $t('pages.rule9.item2'),
dataIndex: 'param3',
valueType: 'digit',
width: '100%',
fieldProps: {
...genDigitComAttr(),
},
},
{
title: $t('pages.rule9.item3'),
dataIndex: 'param4',
valueType: 'digit',
width: '100%',
fieldProps: {
...genDigitComAttr(),
},
},
// {
// title: $t('form.rule2.keyword'),
// dataIndex: 'param5',
// fieldProps: {
// placeholder: $t('form.rule2.keyword.placeholder'),
// },
// },
{
renderFormItem: () => <GroupSelectWrapper />,
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 5,
param3: 1,
param4: 10000,
};
},
tipsComponent: $t('form.rule9.tips'),
symbolFormShow: true,
};
};

View File

@@ -0,0 +1,105 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import GroupSelectWrapper from '../components/GroupSelectWrapper';
import useRuleMeta from '../hooks/useRuleMeta';
import type { returnType, SchemaProps } from '../type';
export const scalpingSchema = ({ richFormat }: SchemaProps): returnType => {
const { symbolsShowFunc } = useRuleMeta();
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3', 'param4', 'param5'],
content: ({ param1, param2, param3, param4, param5 }) => {
return richFormat('form.rules.header.content4', {
seconds: param1 || 0,
lots: param3 || 0,
usdValue: param4 || 0,
profitValue: param5 || 0,
symbols: symbolsShowFunc(param2),
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: `${$t('pages.rule4.item1')} (${$t('pages.rules.seconds')})`,
dataIndex: 'param1',
valueType: 'digit',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
...genDigitComAttr(0),
},
},
{
title: $t('pages.rule4.item3'),
dataIndex: 'param3',
valueType: 'digit',
width: '100%',
fieldProps: {
...genDigitComAttr(),
},
},
{
title: $t('pages.rule4.item4'),
dataIndex: 'param4',
valueType: 'digit',
width: '100%',
formItemProps: {
hidden: true,
},
fieldProps: {
...genDigitComAttr(),
},
},
{
title: `${$t('pages.rule4.item2')}`,
dataIndex: 'param5',
valueType: 'digit',
width: '100%',
fieldProps: {
...genDigitComAttr(),
},
},
// {
// title: $t('form.rule2.keyword'),
// dataIndex: 'param6',
// fieldProps: {
// placeholder: $t('form.rule2.keyword.placeholder'),
// },
// },
{
renderFormItem: () => <GroupSelectWrapper />,
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: 180,
param3: 0.1,
param4: 0,
param5: 200,
};
},
tipsComponent: $t('form.rule4.tips'),
symbolFormShow: true,
};
};

View File

@@ -0,0 +1,108 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import useRuleMeta from '../hooks/useRuleMeta';
import type { returnType, SchemaProps } from '../type';
export const volatilityMap = [
{
label: 'pages.rule11.option2',
unitStr: '%',
value: '0',
},
{
label: 'pages.rule11.option1',
unitStr: 'Points',
value: '1',
},
];
const timeWindowMap = [
{
label: '1 Minutes',
value: '1',
},
{
label: '5 Minutes',
value: '5',
},
{
label: '15 Minutes',
value: '15',
},
];
export const volatilitySchema = ({ richFormat }: SchemaProps): returnType => {
const { symbolsShowFunc } = useRuleMeta();
const translatedVolatilityMap = volatilityMap.map((item) => ({
...item,
label: $t(item.label),
}));
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3', 'param4'],
content: ({ param1, param2, param3, param4 }) => {
const timeWindow = timeWindowMap.find((item) => item.value === param3);
return richFormat('form.rules.header.content11', {
timeWindow: timeWindow?.label || '---',
volatility: param4 || 0,
volatilityUnit: translatedVolatilityMap[+param1]?.label || '---',
symbols: symbolsShowFunc(param2),
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: $t('pages.rule11.item1'),
dataIndex: 'param1',
valueType: 'select',
fieldProps: {
options: translatedVolatilityMap,
allowClear: false,
},
},
{
title: $t('pages.rule11.item2'),
dataIndex: 'param3',
valueType: 'select',
fieldProps: {
options: timeWindowMap,
allowClear: false,
},
},
{
title: $t('pages.rule11.item3'),
dataIndex: 'param4',
valueType: 'digit',
width: '100%',
fieldProps: {
...genDigitComAttr(),
},
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
transformInitialValue(values) {
if (values.id) return values;
return {
...values,
param1: '1',
param3: '1',
param4: 500,
};
},
tipsComponent: $t('form.rule11.tips'),
symbolFormShow: true,
};
};

View File

@@ -0,0 +1,127 @@
import { genDigitComAttr } from '@/utils';
import { $t } from '@/utils/i18n';
import type { returnType, SchemaProps } from '../type';
const ruleActionMap = [
{ label: 'pages.rule8.option1', value: 1 << 0 }, // 1 (0x01)
{ label: 'pages.rule8.option2', value: 1 << 1 }, // 2 (0x10)
];
export const watchListSchema = ({ richFormat }: SchemaProps): returnType => {
return {
popFormTips: {
title: `📋 ${$t('form.rules.header')}`,
dependencies: ['param1', 'param2', 'param3'],
content: ({ param1, param2, param3 }) => {
const tmpV = getValue(param2);
return richFormat('form.rules.header.content8', {
tradeAccounts: param1 || '---',
action: tmpV.length
? getLabels(getDecimalFromSelected(tmpV || []), $t)
: '---',
lots: param3 || 0,
});
},
},
leftForm: [
{
title: $t('pages.rules.ruleName'),
dataIndex: 'name',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
},
{
title: `${$t('pages.rule8.item1')} (${$t('pages.rule8.item1.tips')})`,
dataIndex: 'param1',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
placeholder: '123456,88888',
},
},
{
title: $t('pages.rule8.item2'),
dataIndex: 'param2',
valueType: 'checkbox',
width: '100%',
formItemProps: {
rules: [{ required: true, message: $t('form.required') }],
},
fieldProps: {
options: ruleActionMap.map((item) => ({
label: $t(item.label),
value: item.value,
})),
},
},
{
title: $t('pages.rule8.item3'),
dataIndex: 'param3',
valueType: 'digit',
width: '100%',
fieldProps: {
...genDigitComAttr(),
},
},
{
title: '',
dataIndex: 'id',
formItemProps: {
hidden: true,
},
},
],
tipsComponent: $t('form.rule8.tips'),
symbolFormShow: false,
transformInitialValue(values) {
if (values.id)
return {
...values,
param2: getSelectedFromDecimal(+values.param2),
};
return {
...values,
param2: [1],
param3: 0.01,
};
},
transformSubmitValue: (values) => ({
...values,
param2: getDecimalFromSelected(values.param2),
}),
};
};
/**
* @param {Number} decimalValue The decimal number returned by the backend
* @returns {Array} Selected value array, for example [1, 2, 4]
*/
function getSelectedFromDecimal(decimalValue: number) {
return ruleActionMap
.filter((option) => (decimalValue & option.value) === option.value)
.map((option) => option.value);
}
/**
* @param {Array} selectedValues Selected value array, for example [1, 2, 4]
* @returns {Number} Calculated decimal result
*/
function getDecimalFromSelected(selectedValues: number[]) {
return selectedValues.reduce((acc, val) => acc | val, 0);
}
function getValue(value: number | string | number[]) {
return Array.isArray(value) ? value : getSelectedFromDecimal(+value);
}
export function getLabels(decimalValue: number, $t: (key: string) => string) {
return ruleActionMap
.filter((opt) => (decimalValue & opt.value) === opt.value)
.map((opt) => $t(opt.label))
.join(',');
}

19
src/pages/rules/type.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
import type { ProFormColumnsType } from '@ant-design/pro-components';
export type returnType = {
leftForm: ProFormColumnsType[];
symbolFormShow?: boolean;
assetFormShow?: boolean;
tipsComponent?: ReactNode | string;
popFormTips?: {
title: string;
dependencies?: string[];
content: (params: Record<string, any>) => string;
};
transformInitialValue?: (values: T) => T;
transformSubmitValue?: (values: T) => T;
};
export type SchemaProps = {
richFormat: (id: string, data?: Record<string, any>) => any;
};

View File

@@ -0,0 +1,108 @@
import { getRuleDetail, ruleHandle } from '@/services/api';
/**
* Handle cloning and status tracking of a single rule
*/
type ProcessSingleRuleProps = {
payload: Record<string, any>;
MAX_RETRY?: number;
WAIT_TIME?: number;
};
export const processSingleRule = async ({
payload,
MAX_RETRY = 10,
WAIT_TIME = 1000,
}: ProcessSingleRuleProps) => {
const { code, data, message } = await ruleHandle(payload);
const ruleId = data?.id || payload?.id;
let attempts = 0;
return new Promise((resolve, reject) => {
if (code !== 200 || !ruleId) {
reject({
errType: 0,
errMsg: 'pages.clone.tips1',
respMessage: message || '',
});
return;
}
const poll = async () => {
try {
attempts++;
if (attempts > MAX_RETRY) {
reject({
errType: 1,
errMsg: 'pages.clone.tips2',
ruleId,
});
return;
}
const {
code,
data: { data },
} = await getRuleDetail(ruleId);
if (code !== 200) {
// reject({
// errType: 2,
// errMsg: 'pages.clone.tips3',
// ruleId,
// respMessage: message || '',
// });
setTimeout(poll, WAIT_TIME);
return;
}
const { status } = data;
if (status === 1) {
// success
resolve(data);
} else if (status === 2) {
reject({ errType: 3, errMsg: 'pages.clone.tips4', data });
} else if (status === 0) {
// The status is 0 (Pending) and training continues.
setTimeout(poll, WAIT_TIME);
}
} catch (error) {
console.error('processSingleRule error:', error);
// reject({
// errType: 2,
// errMsg: 'pages.clone.tips3',
// ruleId,
// respMessage: message || '',
// });
setTimeout(poll, WAIT_TIME);
}
};
poll();
});
};
export const handleEmptyValuesFunc = (values: Record<string, any>) => {
// some fields are not required, but the backend requires them to be empty strings
// so we set omitNil to false to let the framework not handle them, and handle them manually
const tmpValues: Record<string, any> = {};
Object.keys(values).forEach((key) => {
if (values[key] !== void 0 && values[key] !== null) {
tmpValues[key] = values[key];
} else {
if (key.indexOf('param') !== -1) {
tmpValues[key] = '';
}
}
});
if ('groupFilter' in values && values.groupFilter === void 0) {
tmpValues.groupFilter = '';
}
for (const key in tmpValues) {
if (typeof tmpValues[key] === 'string') {
tmpValues[key] = tmpValues[key].trim();
}
}
return tmpValues;
};