refactor: 重构 RunScreen 组件,提取任务项为独立组件并优化性能,使用 useMemo 和 useCallback 提高渲染效率
This commit is contained in:
parent
97164ff220
commit
cdffc19db1
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
Text,
|
||||
@ -25,16 +25,6 @@ const statusOptions = [
|
||||
{ label: '已终止', value: TaskStatus.TERMINATED },
|
||||
];
|
||||
|
||||
// 状态显示配置
|
||||
const statusDisplayMap: { [key: number]: { text: string; color: string } } = {
|
||||
[TaskStatus.WAITING]: { text: '待分配', color: '#9E9E9E' },
|
||||
[TaskStatus.RUNNING]: { text: '执行中', color: '#2196F3' },
|
||||
[TaskStatus.PAUSED]: { text: '已暂停', color: '#FF9800' },
|
||||
[TaskStatus.COMPLETED]: { text: '已完成', color: '#4CAF50' },
|
||||
[TaskStatus.FAILED]: { text: '已失败', color: '#F44336' },
|
||||
[TaskStatus.TERMINATED]: { text: '已终止', color: '#9C27B0' },
|
||||
};
|
||||
|
||||
interface TaskRunItem {
|
||||
id: string;
|
||||
parentId: string;
|
||||
@ -56,184 +46,62 @@ interface TaskRunItem {
|
||||
runDuration: number;
|
||||
}
|
||||
|
||||
export default function RunScreen() {
|
||||
const [tasks, setTasks] = useState<TaskRunItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// 筛选状态
|
||||
const [filterStatus, setFilterStatus] = useState<TaskStatus | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [filterName, setFilterName] = useState('');
|
||||
|
||||
// 操作状态
|
||||
const [operatingTaskId, setOperatingTaskId] = useState<string | null>(null);
|
||||
|
||||
// 批量操作状态
|
||||
const [selectedTasks, setSelectedTasks] = useState<Set<string>>(new Set());
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
const [batchOperating, setBatchOperating] = useState(false);
|
||||
|
||||
// 加载任务列表
|
||||
const loadTasks = useCallback(
|
||||
async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
status: filterStatus,
|
||||
name: filterName.trim() || undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
};
|
||||
|
||||
const response = await getTaskRunList(params);
|
||||
setTasks(response || []);
|
||||
} catch (error) {
|
||||
console.error('加载任务列表失败:', error);
|
||||
Alert.alert('错误', '加载任务列表失败,请重试');
|
||||
setTasks([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[filterStatus, filterName],
|
||||
);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, [loadTasks]);
|
||||
|
||||
// 刷新任务列表
|
||||
const handleRefresh = () => {
|
||||
loadTasks(true);
|
||||
};
|
||||
|
||||
// 重置筛选
|
||||
const handleResetFilter = () => {
|
||||
setFilterStatus(undefined);
|
||||
setFilterName('');
|
||||
};
|
||||
|
||||
// 切换选择模式
|
||||
const toggleSelectionMode = () => {
|
||||
setIsSelectionMode(!isSelectionMode);
|
||||
setSelectedTasks(new Set());
|
||||
};
|
||||
|
||||
// 选择/取消选择任务
|
||||
const toggleTaskSelection = (taskId: string) => {
|
||||
const newSelection = new Set(selectedTasks);
|
||||
if (newSelection.has(taskId)) {
|
||||
newSelection.delete(taskId);
|
||||
} else {
|
||||
newSelection.add(taskId);
|
||||
}
|
||||
setSelectedTasks(newSelection);
|
||||
};
|
||||
|
||||
// 全选/取消全选
|
||||
const toggleSelectAll = () => {
|
||||
if (selectedTasks.size === tasks.length) {
|
||||
setSelectedTasks(new Set());
|
||||
} else {
|
||||
setSelectedTasks(new Set(tasks.map(task => task.id)));
|
||||
}
|
||||
};
|
||||
|
||||
// 批量终止任务
|
||||
const handleBatchTerminate = async () => {
|
||||
if (selectedTasks.size === 0) {
|
||||
Alert.alert('提示', '请先选择要终止的任务');
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'确认操作',
|
||||
`确定要终止选中的 ${selectedTasks.size} 个任务吗?`,
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '确定',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
setBatchOperating(true);
|
||||
try {
|
||||
const promises = Array.from(selectedTasks).map(taskId =>
|
||||
terminateTask(taskId),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
Alert.alert('成功', `已成功终止 ${selectedTasks.size} 个任务`);
|
||||
setSelectedTasks(new Set());
|
||||
setIsSelectionMode(false);
|
||||
loadTasks();
|
||||
} catch (error) {
|
||||
console.error('批量终止任务失败:', error);
|
||||
Alert.alert('错误', '批量终止任务失败,请重试');
|
||||
} finally {
|
||||
setBatchOperating(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
};
|
||||
|
||||
// 任务操作 - 只保留终止操作
|
||||
const handleTaskOperation = async (taskId: string) => {
|
||||
setOperatingTaskId(taskId);
|
||||
|
||||
try {
|
||||
await terminateTask(taskId);
|
||||
Alert.alert('成功', '终止任务成功');
|
||||
// 重新加载任务列表
|
||||
loadTasks();
|
||||
} catch (error) {
|
||||
console.error('终止任务失败:', error);
|
||||
Alert.alert('错误', '终止任务失败,请重试');
|
||||
} finally {
|
||||
setOperatingTaskId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timeString?: string) => {
|
||||
if (!timeString) return '--';
|
||||
try {
|
||||
return new Date(timeString).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return timeString;
|
||||
}
|
||||
};
|
||||
|
||||
// 渲染任务项
|
||||
const renderTaskItem = ({ item }: { item: TaskRunItem }) => {
|
||||
// 提取任务项组件并使用 memo 优化
|
||||
const TaskItem = React.memo<{
|
||||
item: TaskRunItem;
|
||||
isSelectionMode: boolean;
|
||||
isSelected: boolean;
|
||||
isOperating: boolean;
|
||||
onToggleSelection: (taskId: string) => void;
|
||||
onTaskOperation: (taskId: string) => void;
|
||||
statusDisplayMap: { [key: number]: { text: string; color: string } };
|
||||
}>(
|
||||
({
|
||||
item,
|
||||
isSelectionMode,
|
||||
isSelected,
|
||||
isOperating,
|
||||
onToggleSelection,
|
||||
onTaskOperation,
|
||||
statusDisplayMap,
|
||||
}) => {
|
||||
const statusInfo =
|
||||
statusDisplayMap[item.status] || statusDisplayMap[TaskStatus.WAITING];
|
||||
const isOperating = operatingTaskId === item.id;
|
||||
const isSelected = selectedTasks.has(item.id);
|
||||
|
||||
// 格式化时间 - 移到组件内部避免重复计算
|
||||
const formatTime = useCallback((timeString?: string) => {
|
||||
if (!timeString) return '--';
|
||||
try {
|
||||
return new Date(timeString).toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return timeString;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
if (isSelectionMode) {
|
||||
onToggleSelection(item.id);
|
||||
}
|
||||
}, [isSelectionMode, onToggleSelection, item.id]);
|
||||
|
||||
const handleCheckboxPress = useCallback(() => {
|
||||
onToggleSelection(item.id);
|
||||
}, [onToggleSelection, item.id]);
|
||||
|
||||
const handleOperationPress = useCallback(() => {
|
||||
onTaskOperation(item.id);
|
||||
}, [onTaskOperation, item.id]);
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={[styles.taskItem, isSelected ? styles.taskItemSelected : null]}
|
||||
onPress={() => {
|
||||
if (isSelectionMode) {
|
||||
toggleTaskSelection(item.id);
|
||||
}
|
||||
}}
|
||||
onPress={handlePress}
|
||||
activeOpacity={isSelectionMode ? 0.7 : 1}
|
||||
>
|
||||
<View style={styles.taskHeader}>
|
||||
@ -241,7 +109,7 @@ export default function RunScreen() {
|
||||
{isSelectionMode && (
|
||||
<TouchableOpacity
|
||||
style={styles.checkboxContainer}
|
||||
onPress={() => toggleTaskSelection(item.id)}
|
||||
onPress={handleCheckboxPress}
|
||||
>
|
||||
<MaterialIcons
|
||||
name={isSelected ? 'check-box' : 'check-box-outline-blank'}
|
||||
@ -301,7 +169,7 @@ export default function RunScreen() {
|
||||
item.status === TaskStatus.PAUSED) && (
|
||||
<TouchableOpacity
|
||||
style={[styles.actionButton, styles.terminateButton]}
|
||||
onPress={() => handleTaskOperation(item.id)}
|
||||
onPress={handleOperationPress}
|
||||
disabled={isOperating}
|
||||
>
|
||||
{isOperating ? (
|
||||
@ -318,7 +186,223 @@ export default function RunScreen() {
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
TaskItem.displayName = 'TaskItem';
|
||||
|
||||
export default function RunScreen() {
|
||||
const [tasks, setTasks] = useState<TaskRunItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
|
||||
// 筛选状态
|
||||
const [filterStatus, setFilterStatus] = useState<TaskStatus | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [filterName, setFilterName] = useState('');
|
||||
|
||||
// 操作状态
|
||||
const [operatingTaskId, setOperatingTaskId] = useState<string | null>(null);
|
||||
|
||||
// 批量操作状态
|
||||
const [selectedTasks, setSelectedTasks] = useState<Set<string>>(new Set());
|
||||
const [isSelectionMode, setIsSelectionMode] = useState(false);
|
||||
const [batchOperating, setBatchOperating] = useState(false);
|
||||
|
||||
// 使用 useMemo 缓存状态显示配置
|
||||
const statusDisplayMap = useMemo(
|
||||
() => ({
|
||||
[TaskStatus.WAITING]: { text: '待分配', color: '#9E9E9E' },
|
||||
[TaskStatus.RUNNING]: { text: '执行中', color: '#2196F3' },
|
||||
[TaskStatus.PAUSED]: { text: '已暂停', color: '#FF9800' },
|
||||
[TaskStatus.COMPLETED]: { text: '已完成', color: '#4CAF50' },
|
||||
[TaskStatus.FAILED]: { text: '已失败', color: '#F44336' },
|
||||
[TaskStatus.TERMINATED]: { text: '已终止', color: '#9C27B0' },
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
// 加载任务列表
|
||||
const loadTasks = useCallback(
|
||||
async (isRefresh = false) => {
|
||||
if (isRefresh) {
|
||||
setRefreshing(true);
|
||||
} else {
|
||||
setLoading(true);
|
||||
}
|
||||
|
||||
try {
|
||||
const params = {
|
||||
status: filterStatus,
|
||||
name: filterName.trim() || undefined,
|
||||
pageNum: 1,
|
||||
pageSize: 100,
|
||||
};
|
||||
|
||||
const response = await getTaskRunList(params);
|
||||
setTasks(response || []);
|
||||
} catch (error) {
|
||||
console.error('加载任务列表失败:', error);
|
||||
Alert.alert('错误', '加载任务列表失败,请重试');
|
||||
setTasks([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRefreshing(false);
|
||||
}
|
||||
},
|
||||
[filterStatus, filterName],
|
||||
);
|
||||
|
||||
// 初始加载
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, [loadTasks]);
|
||||
|
||||
// 刷新任务列表
|
||||
const handleRefresh = useCallback(() => {
|
||||
loadTasks(true);
|
||||
}, [loadTasks]);
|
||||
|
||||
// 重置筛选
|
||||
const handleResetFilter = useCallback(() => {
|
||||
setFilterStatus(undefined);
|
||||
setFilterName('');
|
||||
}, []);
|
||||
|
||||
// 切换选择模式
|
||||
const toggleSelectionMode = useCallback(() => {
|
||||
setIsSelectionMode(!isSelectionMode);
|
||||
setSelectedTasks(new Set());
|
||||
}, [isSelectionMode]);
|
||||
|
||||
// 选择/取消选择任务
|
||||
const toggleTaskSelection = useCallback((taskId: string) => {
|
||||
setSelectedTasks(prev => {
|
||||
const newSelection = new Set(prev);
|
||||
if (newSelection.has(taskId)) {
|
||||
newSelection.delete(taskId);
|
||||
} else {
|
||||
newSelection.add(taskId);
|
||||
}
|
||||
return newSelection;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 全选/取消全选
|
||||
const toggleSelectAll = useCallback(() => {
|
||||
if (selectedTasks.size === tasks.length) {
|
||||
setSelectedTasks(new Set());
|
||||
} else {
|
||||
setSelectedTasks(new Set(tasks.map(task => task.id)));
|
||||
}
|
||||
}, [selectedTasks.size, tasks]);
|
||||
|
||||
// 批量终止任务
|
||||
const handleBatchTerminate = useCallback(async () => {
|
||||
if (selectedTasks.size === 0) {
|
||||
Alert.alert('提示', '请先选择要终止的任务');
|
||||
return;
|
||||
}
|
||||
|
||||
Alert.alert(
|
||||
'确认操作',
|
||||
`确定要终止选中的 ${selectedTasks.size} 个任务吗?`,
|
||||
[
|
||||
{ text: '取消', style: 'cancel' },
|
||||
{
|
||||
text: '确定',
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
setBatchOperating(true);
|
||||
try {
|
||||
const promises = Array.from(selectedTasks).map(taskId =>
|
||||
terminateTask(taskId),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
Alert.alert('成功', `已成功终止 ${selectedTasks.size} 个任务`);
|
||||
setSelectedTasks(new Set());
|
||||
setIsSelectionMode(false);
|
||||
loadTasks();
|
||||
} catch (error) {
|
||||
console.error('批量终止任务失败:', error);
|
||||
Alert.alert('错误', '批量终止任务失败,请重试');
|
||||
} finally {
|
||||
setBatchOperating(false);
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
);
|
||||
}, [selectedTasks, loadTasks]);
|
||||
|
||||
// 任务操作 - 只保留终止操作
|
||||
const handleTaskOperation = useCallback(
|
||||
async (taskId: string) => {
|
||||
setOperatingTaskId(taskId);
|
||||
|
||||
try {
|
||||
await terminateTask(taskId);
|
||||
Alert.alert('成功', '终止任务成功');
|
||||
// 重新加载任务列表
|
||||
loadTasks();
|
||||
} catch (error) {
|
||||
console.error('终止任务失败:', error);
|
||||
Alert.alert('错误', '终止任务失败,请重试');
|
||||
} finally {
|
||||
setOperatingTaskId(null);
|
||||
}
|
||||
},
|
||||
[loadTasks],
|
||||
);
|
||||
|
||||
// 优化的渲染任务项函数
|
||||
const renderTaskItem = useCallback(
|
||||
({ item }: { item: TaskRunItem }) => {
|
||||
const isOperating = operatingTaskId === item.id;
|
||||
const isSelected = selectedTasks.has(item.id);
|
||||
|
||||
return (
|
||||
<TaskItem
|
||||
item={item}
|
||||
isSelectionMode={isSelectionMode}
|
||||
isSelected={isSelected}
|
||||
isOperating={isOperating}
|
||||
onToggleSelection={toggleTaskSelection}
|
||||
onTaskOperation={handleTaskOperation}
|
||||
statusDisplayMap={statusDisplayMap}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[
|
||||
operatingTaskId,
|
||||
selectedTasks,
|
||||
isSelectionMode,
|
||||
toggleTaskSelection,
|
||||
handleTaskOperation,
|
||||
statusDisplayMap,
|
||||
],
|
||||
);
|
||||
|
||||
// 优化 keyExtractor
|
||||
const keyExtractor = useCallback((item: TaskRunItem) => item.id, []);
|
||||
|
||||
// 渲染分隔符
|
||||
const renderSeparator = useCallback(
|
||||
() => <View style={styles.separator} />,
|
||||
[],
|
||||
);
|
||||
|
||||
// 渲染空列表组件
|
||||
const renderEmptyComponent = useCallback(
|
||||
() => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="assignment" size={48} color="#666" />
|
||||
<Text style={styles.emptyText}>暂无任务数据</Text>
|
||||
</View>
|
||||
),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@ -443,19 +527,26 @@ export default function RunScreen() {
|
||||
<FlatList
|
||||
data={tasks}
|
||||
renderItem={renderTaskItem}
|
||||
keyExtractor={item => item.id}
|
||||
keyExtractor={keyExtractor}
|
||||
refreshing={refreshing}
|
||||
onRefresh={handleRefresh}
|
||||
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
||||
ListEmptyComponent={() => (
|
||||
<View style={styles.emptyContainer}>
|
||||
<MaterialIcons name="assignment" size={48} color="#666" />
|
||||
<Text style={styles.emptyText}>暂无任务数据</Text>
|
||||
</View>
|
||||
)}
|
||||
ItemSeparatorComponent={renderSeparator}
|
||||
ListEmptyComponent={renderEmptyComponent}
|
||||
contentContainerStyle={
|
||||
tasks.length === 0 ? styles.emptyList : undefined
|
||||
}
|
||||
// 性能优化配置
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
windowSize={10}
|
||||
initialNumToRender={10}
|
||||
updateCellsBatchingPeriod={50}
|
||||
// 避免重复渲染
|
||||
extraData={{
|
||||
selectedTasks: selectedTasks.size,
|
||||
isSelectionMode,
|
||||
operatingTaskId,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
@ -508,6 +599,8 @@ const styles = StyleSheet.create({
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 4,
|
||||
height: 40,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
resetButtonText: {
|
||||
color: '#ffffff',
|
||||
|
Loading…
x
Reference in New Issue
Block a user