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,
} from '@ant-design/pro-components';
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 { $t } from '@/utils/i18n';
import '../style.less';
@@ -14,26 +14,9 @@ import { handleEmptyValuesFunc } from '../../utils';
import BatchCloneModal from './comp/BatchCloneModal';
import Step1 from './comp/Step1';
import Step2 from './comp/Step2';
import { MultiCopyPopupContext } from './context';
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>;

View File

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

View File

@@ -1,22 +1,20 @@
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 { useContext, useEffect, useState } from 'react';
import { getRuleList } from '@/services/api';
import { $t } from '@/utils/i18n';
import { RuleCardBodyInfo } from '../../RuleCardBody';
import { MultiCopyPopupContext } from '../MultiCopyPopup';
import { MultiCopyPopupContext } from '../context';
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 [ruleList, setRuleList] = useState<API.RuleListItem[]>([]);
const { loading } = useRequest(
() =>
@@ -25,7 +23,6 @@ const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
dataSourceId,
}),
{
// manual: true,
refreshDeps: [dataSourceId, currentRuleType],
onSuccess: ({ data = [] }: { data: API.RuleListItem[] }) => {
setRuleList(data);
@@ -37,15 +34,17 @@ const RuleCheckCard = ({ dataSourceId }: RuleCheckCardProps) => {
},
);
useEffect(() => {
return () => setSelectRules([]);
}, []);
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,
count: ruleList.length,
ruleType: ruleMeta?.title || '-',
})}
collapsible

View File

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

View File

@@ -16,7 +16,7 @@ import {
import { useContext, useEffect, useMemo, useState } from 'react';
import { $t } from '@/utils/i18n';
import { RuleCardBodyInfo } from '../../RuleCardBody';
import { MultiCopyPopupContext } from '../MultiCopyPopup';
import { MultiCopyPopupContext } from '../context';
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: {},
});