refactor(copy-rule): extract context and improve batch clone modal

- Extract MultiCopyPopupContext to standalone context.ts file
- Add Task type and improve type safety in BatchCloneModal
- Use useRef/useCallback to stabilize batch clone handlers
- Add cleanup effect for selectRules in RuleCheckCard on unmount

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 23:05:56 +08:00
parent 16999ca583
commit 3c636bcaa2
6 changed files with 61 additions and 48 deletions

View File

@@ -4,7 +4,7 @@ import {
useBreakpoint, useBreakpoint,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { Button, Divider, Flex, Input, Modal, Space, Typography } from 'antd'; import { Button, Divider, Flex, Input, Modal, Space, Typography } from 'antd';
import { createContext, useMemo, useRef, useState } from 'react'; import { useMemo, useRef, useState } from 'react';
import useRichFormat from '@/hooks/useRichI18n'; import useRichFormat from '@/hooks/useRichI18n';
import { $t } from '@/utils/i18n'; import { $t } from '@/utils/i18n';
import '../style.less'; import '../style.less';
@@ -14,26 +14,9 @@ import { handleEmptyValuesFunc } from '../../utils';
import BatchCloneModal from './comp/BatchCloneModal'; import BatchCloneModal from './comp/BatchCloneModal';
import Step1 from './comp/Step1'; import Step1 from './comp/Step1';
import Step2 from './comp/Step2'; import Step2 from './comp/Step2';
import { MultiCopyPopupContext } from './context';
const { Text, Title } = Typography; const { Text, Title } = Typography;
type MultiCopyPopupContextValue = {
ruleMeta: Record<string, any>;
dataSourceList: API.DataSourceListItem[];
currentRuleType: number;
targetDataSourceId: string | number | null;
selectRules: API.RuleListItem[];
setSelectRules: (rules: API.RuleListItem[]) => void;
currentSchemaMap: Record<string, any>;
};
export const MultiCopyPopupContext = createContext<MultiCopyPopupContextValue>({
ruleMeta: {},
dataSourceList: [],
currentRuleType: 0,
targetDataSourceId: null,
selectRules: [],
setSelectRules: () => {},
currentSchemaMap: {},
});
type MultiCopyPopupProps = { type MultiCopyPopupProps = {
visible: boolean; visible: boolean;
ruleMeta: Record<string, any>; ruleMeta: Record<string, any>;

View File

@@ -17,10 +17,13 @@ import {
Tag, Tag,
Tooltip, Tooltip,
} from 'antd'; } from 'antd';
import { useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { processSingleRule } from '@/pages/rules/utils'; import { processSingleRule } from '@/pages/rules/utils';
import { $t } from '@/utils/i18n'; import { $t } from '@/utils/i18n';
type TaskStatus = 'waiting' | 'processing' | 'success' | 'warning' | 'error';
type Task = { name: string; status: TaskStatus; errorMsg: string };
type BatchCloneModalProps = { type BatchCloneModalProps = {
visible: boolean; visible: boolean;
selectedRows: Partial<API.RuleListItem>[]; selectedRows: Partial<API.RuleListItem>[];
@@ -33,26 +36,33 @@ const BatchCloneModal = ({
onClose, onClose,
onFinish, onFinish,
}: BatchCloneModalProps) => { }: BatchCloneModalProps) => {
const [tasks, setTasks] = useState<any[]>([]); const [tasks, setTasks] = useState<Task[]>([]);
const [isRunning, setIsRunning] = useState(false); const [isRunning, setIsRunning] = useState(false);
const [currentRunning, setCurrentRunning] = useState(0); const [currentRunning, setCurrentRunning] = useState(0);
const selectedRowsRef = useRef(selectedRows);
selectedRowsRef.current = selectedRows;
useEffect(() => { useEffect(() => {
const initialTasks = selectedRows.map((row) => ({ if (visible) {
name: row.name, setTasks(
status: 'waiting', // waiting, processing, success, error selectedRows.map((row) => ({
errorMsg: '', name: row.name || '',
})); status: 'waiting',
setTasks(initialTasks); errorMsg: '',
}, [visible, selectedRows]); })),
);
setIsRunning(false);
setCurrentRunning(0);
}
}, [visible]);
const finishedTasks = useMemo( const finishedTasks = useMemo(
() => tasks.filter((t) => ['success', 'warning'].includes(t.status)), () => tasks.filter((t) => t.status === 'success' || t.status === 'warning'),
[tasks], [tasks],
); );
const finishedMessage = useMemo(() => { const finishedMessage = useMemo(() => {
const hasWarning = finishedTasks.find((t) => t.status === 'warning'); const hasWarning = finishedTasks.some((t) => t.status === 'warning');
const type: AlertProps['type'] = hasWarning const type: AlertProps['type'] = hasWarning
? 'warning' ? 'warning'
: finishedTasks.length === tasks.length : finishedTasks.length === tasks.length
@@ -69,9 +79,10 @@ const BatchCloneModal = ({
}; };
}, [finishedTasks, tasks]); }, [finishedTasks, tasks]);
const startBatch = async () => { const startBatch = useCallback(async () => {
const rows = selectedRowsRef.current;
setIsRunning(true); setIsRunning(true);
for (let i = 0; i < selectedRows.length; i++) { for (let i = 0; i < rows.length; i++) {
setCurrentRunning(i + 1); setCurrentRunning(i + 1);
setTasks((prev) => { setTasks((prev) => {
const next = [...prev]; const next = [...prev];
@@ -79,8 +90,7 @@ const BatchCloneModal = ({
return next; return next;
}); });
try { try {
await processSingleRule({ payload: selectedRows[i] }); await processSingleRule({ payload: rows[i] });
setTasks((prev) => { setTasks((prev) => {
const next = [...prev]; const next = [...prev];
next[i].status = 'success'; next[i].status = 'success';
@@ -96,13 +106,13 @@ const BatchCloneModal = ({
} }
} }
setIsRunning(false); setIsRunning(false);
}; }, []);
const handleReset = () => { const handleReset = useCallback(() => {
setTasks([]); setTasks([]);
setIsRunning(false); setIsRunning(false);
setCurrentRunning(0); setCurrentRunning(0);
}; }, []);
return ( return (
<Modal <Modal

View File

@@ -1,22 +1,20 @@
import { CheckCard, ProCard } from '@ant-design/pro-components'; import { CheckCard, ProCard } from '@ant-design/pro-components';
import { useRequest } from '@umijs/max'; import { useRequest } from '@umijs/max';
import { Col, Row, Spin } from 'antd'; import { Col, Row, Spin } from 'antd';
import { useContext, useState } from 'react'; import { useContext, useEffect, useState } from 'react';
import { getRuleList } from '@/services/api'; import { getRuleList } from '@/services/api';
import { $t } from '@/utils/i18n'; import { $t } from '@/utils/i18n';
import { RuleCardBodyInfo } from '../../RuleCardBody'; import { RuleCardBodyInfo } from '../../RuleCardBody';
import { MultiCopyPopupContext } from '../MultiCopyPopup'; import { MultiCopyPopupContext } from '../context';
type RuleCheckCardProps = { type RuleCheckCardProps = {
dataSourceId: string | number; dataSourceId: string | number;
onChange?: (id: string | number) => void;
}; };
const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => { const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
const [ruleList, setRuleList] = useState<API.RuleListItem[]>([]);
const { ruleMeta, currentRuleType, setSelectRules } = useContext( const { ruleMeta, currentRuleType, setSelectRules } = useContext(
MultiCopyPopupContext, MultiCopyPopupContext,
); );
const [ruleList, setRuleList] = useState<API.RuleListItem[]>([]);
const { loading } = useRequest( const { loading } = useRequest(
() => () =>
@@ -25,7 +23,6 @@ const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
dataSourceId, dataSourceId,
}), }),
{ {
// manual: true,
refreshDeps: [dataSourceId, currentRuleType], refreshDeps: [dataSourceId, currentRuleType],
onSuccess: ({ data = [] }: { data: API.RuleListItem[] }) => { onSuccess: ({ data = [] }: { data: API.RuleListItem[] }) => {
setRuleList(data); setRuleList(data);
@@ -37,15 +34,17 @@ const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
}, },
); );
useEffect(() => {
return () => setSelectRules([]);
}, []);
if (!dataSourceId) return <div>{$t('pages.clones.step1.tips')}</div>; if (!dataSourceId) return <div>{$t('pages.clones.step1.tips')}</div>;
if (loading) return <Spin />; if (loading) return <Spin />;
// const
return ( return (
<ProCard <ProCard
title={$t('pages.clones.step1.sourceRules', { title={$t('pages.clones.step1.sourceRules', {
count: ruleList?.length, count: ruleList.length,
ruleType: ruleMeta?.title || '-', ruleType: ruleMeta?.title || '-',
})} })}
collapsible collapsible

View File

@@ -3,7 +3,7 @@ import { ProFormDependency, ProFormSelect } from '@ant-design/pro-components';
import { Divider, Flex, Tag, Typography } from 'antd'; import { Divider, Flex, Tag, Typography } from 'antd';
import { useContext, useEffect, useMemo } from 'react'; import { useContext, useEffect, useMemo } from 'react';
import { $t } from '@/utils/i18n'; import { $t } from '@/utils/i18n';
import { MultiCopyPopupContext } from '../MultiCopyPopup'; import { MultiCopyPopupContext } from '../context';
import RuleCheckCard from './RuleCheckCard'; import RuleCheckCard from './RuleCheckCard';
type Step1Props = { type Step1Props = {

View File

@@ -16,7 +16,7 @@ import {
import { useContext, useEffect, useMemo, useState } from 'react'; import { useContext, useEffect, useMemo, useState } from 'react';
import { $t } from '@/utils/i18n'; import { $t } from '@/utils/i18n';
import { RuleCardBodyInfo } from '../../RuleCardBody'; import { RuleCardBodyInfo } from '../../RuleCardBody';
import { MultiCopyPopupContext } from '../MultiCopyPopup'; import { MultiCopyPopupContext } from '../context';
const { Text } = Typography; const { Text } = Typography;

View File

@@ -0,0 +1,21 @@
import { createContext } from 'react';
type MultiCopyPopupContextValue = {
ruleMeta: Record<string, any>;
dataSourceList: API.DataSourceListItem[];
currentRuleType: number;
targetDataSourceId: string | number | null;
selectRules: API.RuleListItem[];
setSelectRules: (rules: API.RuleListItem[]) => void;
currentSchemaMap: Record<string, any>;
};
export const MultiCopyPopupContext = createContext<MultiCopyPopupContextValue>({
ruleMeta: {},
dataSourceList: [],
currentRuleType: 0,
targetDataSourceId: null,
selectRules: [],
setSelectRules: () => {},
currentSchemaMap: {},
});