feat: 添加 @react-native-picker/picker 依赖,更新任务表单以支持动态选择参数,优化任务卡片和底部操作栏样式

This commit is contained in:
xudan 2025-07-23 14:26:53 +08:00
parent d5b2158c1d
commit de9d2ad3df
11 changed files with 548 additions and 274 deletions

14
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "0.0.1",
"dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-picker/picker": "^2.11.1",
"@react-native/new-app-screen": "0.80.1",
"@react-navigation/bottom-tabs": "^7.4.2",
"@react-navigation/native": "^7.1.14",
@ -2989,6 +2990,19 @@
"node": ">=10"
}
},
"node_modules/@react-native-picker/picker": {
"version": "2.11.1",
"resolved": "https://registry.npmmirror.com/@react-native-picker/picker/-/picker-2.11.1.tgz",
"integrity": "sha512-ThklnkK4fV3yynnIIRBkxxjxR4IFbdMNJVF6tlLdOJ/zEFUEFUEdXY0KmH0iYzMwY8W4/InWsLiA7AkpAbnexA==",
"license": "MIT",
"workspaces": [
"example"
],
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.80.1",
"resolved": "https://registry.npmmirror.com/@react-native/assets-registry/-/assets-registry-0.80.1.tgz",

View File

@ -11,6 +11,7 @@
},
"dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0",
"@react-native-picker/picker": "^2.11.1",
"@react-native/new-app-screen": "0.80.1",
"@react-navigation/bottom-tabs": "^7.4.2",
"@react-navigation/native": "^7.1.14",

View File

@ -1,13 +1,11 @@
import React from 'react';
import { View, StyleSheet } from 'react-native';
import { Button, useTheme } from '@rneui/themed';
import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
interface BottomActionBarProps {
onRun: () => void;
onSave: () => void;
onUndo: () => void;
onRestore: () => void;
onBack: () => void;
isSaveDisabled?: boolean;
}
@ -16,34 +14,78 @@ const BottomActionBar: React.FC<BottomActionBarProps> = ({
onRun,
onSave,
onUndo,
onRestore,
onBack,
isSaveDisabled = true,
}) => {
const { theme } = useTheme();
return (
<View style={[styles.bottom, { backgroundColor: theme.colors.background }]}>
<Button type="clear" onPress={onBack} icon={<Icon name="arrow-left" size={24} color={theme.colors.primary} />} />
<Button type="clear" onPress={onRun} icon={<Icon name="play" size={24} color={theme.colors.primary} />} />
<Button type="clear" onPress={onSave} disabled={isSaveDisabled} icon={<Icon name="content-save" size={24} color={isSaveDisabled ? theme.colors.disabled : theme.colors.primary} />} />
<Button type="clear" onPress={onUndo} icon={<Icon name="undo" size={24} color={theme.colors.primary} />} />
<Button type="clear" onPress={onRestore} icon={<Icon name="restore" size={24} color={theme.colors.primary} />} />
<View style={styles.container}>
<TouchableOpacity onPress={onBack} style={styles.button}>
<Icon name="arrow-left" size={24} color="#AAAAAA" />
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
<TouchableOpacity onPress={onUndo} style={styles.button}>
<Icon name="undo" size={24} color="#AAAAAA" />
<Text style={styles.buttonText}></Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onSave}
disabled={isSaveDisabled}
style={[styles.button, isSaveDisabled && styles.disabledButton]}
>
<Icon
name="content-save"
size={24}
color={isSaveDisabled ? '#555555' : '#00ff00'}
/>
<Text
style={[
styles.buttonText,
{ color: isSaveDisabled ? '#555555' : '#00ff00' },
]}
>
</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={onRun}
style={[styles.button, styles.runButton]}
>
<Icon name="play" size={24} color="#FFFFFF" />
<Text style={[styles.buttonText, styles.runButtonText]}></Text>
</TouchableOpacity>
</View>
);
};
const styles = StyleSheet.create({
bottom: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
container: {
flexDirection: 'row',
justifyContent: 'space-around',
paddingVertical: 8,
paddingVertical: 12,
paddingHorizontal: 10,
backgroundColor: 'rgba(26, 26, 26, 0.9)',
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
borderTopColor: '#333333',
},
button: {
alignItems: 'center',
},
buttonText: {
color: '#AAAAAA',
fontSize: 12,
marginTop: 4,
},
runButton: {
backgroundColor: '#007BFF',
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 20,
},
runButtonText: {
color: '#FFFFFF',
},
disabledButton: {
opacity: 0.5,
},
});

View File

@ -1,6 +1,5 @@
import React from 'react';
import { StyleSheet, TouchableOpacity, Dimensions } from 'react-native';
import { Card, Chip } from '@rneui/themed';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Task } from '../types/task';
interface TaskCardProps {
@ -9,60 +8,75 @@ interface TaskCardProps {
}
const statusMap: { [key: number]: { text: string; color: string } } = {
0: { text: 'IDLE', color: 'grey' },
1: { text: 'RUNNING', color: 'blue' },
2: { text: 'COMPLETED', color: 'green' },
3: { text: 'ERROR', color: 'red' },
0: { text: '待机', color: '#9E9E9E' }, // Grey
1: { text: '运行中', color: '#2196F3' }, // Blue
2: { text: '完成', color: '#4CAF50' }, // Green
3: { text: '失败', color: '#F44336' }, // Red
};
const { width } = Dimensions.get('window');
const cardWidth = (width - 32) / 2; // 减去padding除以2得到每个卡片的宽度
const TaskCard: React.FC<TaskCardProps> = ({ task, onPress }) => {
const statusInfo = statusMap[task.status] || statusMap[0];
return (
<TouchableOpacity onPress={() => onPress(task.id)} style={styles.touchable}>
<Card containerStyle={styles.card}>
<Card.Title style={styles.title}>{task.label}</Card.Title>
<Card.Divider />
<Chip
title={statusInfo.text}
icon={{
name: 'information',
type: 'material-community',
size: 20,
color: statusInfo.color,
}}
type="outline"
containerStyle={styles.chip}
titleStyle={[styles.chipTitle, { color: statusInfo.color }]}
buttonStyle={{ borderColor: statusInfo.color }}
<TouchableOpacity onPress={() => onPress(task.id)} style={styles.container}>
<View style={styles.indicatorContainer}>
<View
style={[
styles.statusIndicator,
{ backgroundColor: statusInfo.color },
]}
/>
</Card>
</View>
<View style={styles.contentContainer}>
<Text style={styles.title}>{task.label}</Text>
<Text style={styles.description} numberOfLines={1}>
{task.description || '暂无描述'}
</Text>
</View>
<View style={styles.statusContainer}>
<Text style={[styles.statusText, { color: statusInfo.color }]}>
{statusInfo.text}
</Text>
</View>
</TouchableOpacity>
);
};
const styles = StyleSheet.create({
touchable: {
width: cardWidth,
marginBottom: 8,
container: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#1a1a1a',
paddingVertical: 16,
paddingHorizontal: 16,
},
card: {
margin: 4,
borderRadius: 8,
width: cardWidth - 8, // 减去margin
indicatorContainer: {
marginRight: 16,
},
statusIndicator: {
width: 12,
height: 12,
borderRadius: 6,
},
contentContainer: {
flex: 1,
},
title: {
marginBottom: 12,
minHeight: 50, // Ensure cards have similar height
textAlign: 'left',
color: '#FFFFFF',
fontSize: 16,
fontWeight: 'bold',
marginBottom: 4,
},
chip: {
alignSelf: 'flex-start',
description: {
color: '#999999',
fontSize: 14,
},
chipTitle: {
fontSize: 12,
statusContainer: {
marginLeft: 16,
},
statusText: {
fontSize: 14,
fontWeight: 'bold',
},
});

View File

@ -1,6 +1,9 @@
import React, { useEffect } from 'react';
import { StyleSheet, ScrollView, Text, View } from 'react-native';
import { Input } from '@rneui/themed';
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Input, Slider } from '@rneui/themed';
// @ts-ignore
import { Picker } from '@react-native-picker/picker';
import { useTasks } from '../context/TasksContext';
import { Task, RobotAction, InputParam } from '../types/task';
interface TaskFormProps {
@ -9,11 +12,12 @@ interface TaskFormProps {
}
const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => {
useEffect(() => {
console.log('TaskForm task prop updated:', task);
}, [task]);
const { locations, payloads, robotActions, locationsBays } = useTasks();
const handleParamChange = (field: string, value: string | RobotAction) => {
const handleParamChange = (
field: string,
value: string | number | RobotAction,
) => {
const currentParam = task.parameters[field] || {};
const updatedTask = {
...task,
@ -30,10 +34,10 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => {
const renderFormInput = (param: InputParam) => {
const parameter = task.parameters?.[param.name];
const value = parameter?.value || param.defaultValue;
const value = parameter?.value ?? param.defaultValue;
const label = (
<Text>
<Text style={styles.label}>
{param.label}
{param.required && <Text style={styles.required}> *</Text>}
</Text>
@ -42,54 +46,219 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => {
switch (param.type.toLowerCase()) {
case 'string':
return (
<Input
key={param.name}
label={label}
value={value as string}
onChangeText={text => handleParamChange(param.name, text)}
placeholder={param.remark}
errorMessage={param.remark}
inputContainerStyle={styles.inputContainer}
/>
<View key={param.name} style={styles.inputGroup}>
{label}
<Input
value={value as string}
onChangeText={text => handleParamChange(param.name, text)}
placeholder={param.remark || '请输入...'}
inputContainerStyle={styles.inputContainer}
inputStyle={styles.inputText}
placeholderTextColor="#888"
/>
</View>
);
case 'location':
case 'location_bay':
const locationData =
param.type.toLowerCase() === 'location' ? locations : locationsBays;
return (
<View key={param.name} style={styles.inputGroup}>
{label}
<View style={styles.pickerContainer}>
<Picker
selectedValue={value as string}
onValueChange={(itemValue: any) =>
handleParamChange(param.name, itemValue)
}
style={styles.picker}
dropdownIconColor="#00ff00"
>
{locationData.map(loc => (
<Picker.Item
key={loc.value}
label={loc.label}
value={loc.value}
color="#000000"
/>
))}
</Picker>
</View>
</View>
);
case 'payload':
return (
<View key={param.name} style={styles.inputGroup}>
{label}
<View style={styles.pickerContainer}>
<Picker
selectedValue={value as string}
onValueChange={(itemValue: any) =>
handleParamChange(param.name, itemValue)
}
style={styles.picker}
dropdownIconColor="#00ff00"
>
{payloads.map(p => (
<Picker.Item
key={p.value}
label={p.label}
value={p.value}
color="#000000"
/>
))}
</Picker>
</View>
</View>
);
case 'robotaction':
return (
<View key={param.name} style={styles.inputGroup}>
{label}
<View style={styles.pickerContainer}>
<Picker
selectedValue={(value as RobotAction)?.name}
onValueChange={(itemValue: any) => {
const selectedAction = robotActions.find(
a => a.value === itemValue,
);
if (selectedAction) {
handleParamChange(param.name, {
name: selectedAction.label,
actionId: selectedAction.actionId,
});
}
}}
style={styles.picker}
dropdownIconColor="#00ff00"
>
{robotActions.map(action => (
<Picker.Item
key={action.actionId}
label={action.label}
value={action.value}
color="#000000"
/>
))}
</Picker>
</View>
</View>
);
case 'number':
return (
<View key={param.name} style={styles.inputGroup}>
{label}
<Input
value={String(value)}
onChangeText={text =>
handleParamChange(param.name, text.replace(/[^0-9]/g, ''))
}
placeholder={param.remark || '请输入数字...'}
keyboardType="numeric"
inputContainerStyle={styles.inputContainer}
inputStyle={styles.inputText}
placeholderTextColor="#888"
/>
</View>
);
case 'slider':
return (
<View key={param.name} style={styles.inputGroup}>
{label}
<View style={styles.sliderContainer}>
<Slider
value={Number(value) || 0}
onValueChange={val => handleParamChange(param.name, val)}
minimumValue={param.min || 0}
maximumValue={param.max || 100}
step={param.step || 1}
allowTouchTrack
trackStyle={styles.sliderTrack}
thumbStyle={styles.sliderThumb}
/>
<Text style={styles.sliderValue}>{String(value)}</Text>
</View>
</View>
);
default:
return (
<Input
key={param.name}
label={label}
value={value as string}
onChangeText={text => handleParamChange(param.name, text)}
placeholder={param.remark}
errorMessage={param.remark}
inputContainerStyle={styles.inputContainer}
/>
);
return null;
}
};
return (
<ScrollView contentContainerStyle={styles.container}>
<Input
label="任务名称"
value={task.name}
onChangeText={text => onTaskChange({ ...task, name: text })}
inputContainerStyle={styles.inputContainer}
/>
<View style={styles.container}>
<View style={styles.inputGroup}>
<Text style={styles.label}></Text>
<Input
value={task.name}
onChangeText={text => onTaskChange({ ...task, name: text })}
inputContainerStyle={styles.inputContainer}
inputStyle={styles.inputText}
/>
</View>
{task.detail && task.detail.inputParams ? (
task.detail.inputParams.map(param => renderFormInput(param))
) : (
<View style={styles.loadingContainer}>
<Text>...</Text>
<Text style={styles.loadingText}>...</Text>
</View>
)}
</ScrollView>
</View>
);
};
const styles = StyleSheet.create({
container: {
// padding: 16, //
padding: 16,
},
inputGroup: {
marginBottom: 20,
},
label: {
fontSize: 16,
color: '#AAAAAA',
marginBottom: 8,
},
required: {
color: '#ff4d4d',
},
inputContainer: {
backgroundColor: '#333333',
borderBottomWidth: 1,
borderBottomColor: '#555555',
paddingHorizontal: 10,
borderRadius: 5,
},
inputText: {
color: '#FFFFFF',
fontSize: 16,
},
pickerContainer: {
backgroundColor: '#FFFFFF',
borderRadius: 5,
},
picker: {
color: '#000000',
},
sliderContainer: {
flexDirection: 'row',
alignItems: 'center',
marginTop: 10,
},
sliderTrack: {
height: 5,
backgroundColor: '#555',
},
sliderThumb: {
height: 20,
width: 20,
backgroundColor: '#00ff00',
},
sliderValue: {
marginLeft: 16,
color: '#FFFFFF',
fontSize: 16,
},
loadingContainer: {
flex: 1,
@ -97,11 +266,9 @@ const styles = StyleSheet.create({
alignItems: 'center',
padding: 20,
},
required: {
color: 'red',
},
inputContainer: {
marginBottom: 10,
loadingText: {
color: '#AAAAAA',
fontSize: 16,
},
});

1
src/context/Tasks.tsx Normal file
View File

@ -0,0 +1 @@

View File

@ -4,6 +4,8 @@ import React, {
useContext,
ReactNode,
useEffect,
useMemo,
useCallback,
} from 'react';
import { Task } from '../types/task';
import {
@ -12,11 +14,7 @@ import {
PayloadOption,
RobotActionOption,
} from '../types/config';
import {
getConfig,
executeTask,
clearCachedConfig,
} from '../services/configService';
import { getConfig, executeTask } from '../services/configService';
interface TasksContextData {
tasks: Task[];
@ -30,6 +28,7 @@ interface TasksContextData {
runTask: (id: string) => void;
refreshConfig: () => Promise<void>;
isConfigLoaded: boolean;
fetchTaskDetail: (taskId: string) => Promise<void>;
}
const TasksContext = createContext<TasksContextData>({} as TasksContextData);
@ -45,7 +44,7 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
const [isConfigLoaded, setIsConfigLoaded] = useState(false);
const [serverUrl, setServerUrl] = useState<string | null>(null);
const fetchTasks = async (baseUrl: string, endpoint: string) => {
const fetchTasks = useCallback(async (baseUrl: string, endpoint: string) => {
try {
if (baseUrl && endpoint) {
const fetchUrl = `${baseUrl}${endpoint}?pageNum=1&pageSize=100`;
@ -73,14 +72,14 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
return {
...task,
name: task.label,
parameters: task.parameters || {}, // Ensure parameters object exists
parameters: task.parameters || {},
detail: detail,
};
});
setTasks(fetchedTasks);
} else {
console.error('获取任务列表失败: responseData.code is not 200');
setTasks([]); //
setTasks([]);
}
}
} catch (error) {
@ -88,17 +87,16 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
if (error instanceof Error) {
console.error('Error message:', error.message);
}
setTasks([]); //
setTasks([]);
}
};
}, []); // Empty dependency array as it has no external dependencies from component scope
useEffect(() => {
const loadApp = async () => {
try {
await clearCachedConfig();
const config = await getConfig();
if (config) {
applyConfig(config); // Don't load tasks from config
applyConfig(config);
setIsConfigLoaded(true);
if (config.serverUrl && config.apiEndpoints) {
await fetchTasks(config.serverUrl, config.apiEndpoints.getTasks);
@ -114,7 +112,7 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
};
loadApp();
}, []);
}, [fetchTasks]);
const [apiEndpoints, setApiEndpoints] = useState<{
getTasks: string;
@ -135,12 +133,12 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
}
};
const refreshConfig = async () => {
const refreshConfig = useCallback(async () => {
try {
await clearCachedConfig();
// await clearCachedConfig(); // 通常刷新不需要清除缓存
const config = await getConfig();
if (config) {
applyConfig(config); // Don't load tasks from config
applyConfig(config);
setIsConfigLoaded(true);
if (config.serverUrl && config.apiEndpoints) {
await fetchTasks(config.serverUrl, config.apiEndpoints.getTasks);
@ -153,122 +151,144 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
console.error('刷新配置失败:', error);
setIsConfigLoaded(false);
}
};
}, [fetchTasks]);
const getTaskById = (id: string) => {
const task = tasks.find(t => t.id === id);
if (task && !task.detail) {
fetchTaskDetail(id);
}
return task;
};
const fetchTaskDetail = async (taskId: string) => {
try {
if (serverUrl && apiEndpoints) {
const endpoint = apiEndpoints.getTaskDetail.replace('{taskId}', taskId);
const url = `${serverUrl}${endpoint}`;
console.log('Fetching task detail from:', url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const responseData = await response.json();
console.log('task detail responseData', responseData);
if (responseData && responseData.code === 200) {
let taskDetail = responseData.data.detail;
if (taskDetail && typeof taskDetail === 'string') {
try {
taskDetail = JSON.parse(taskDetail);
} catch (e) {
console.error('解析任务详情失败 (detail):', e);
taskDetail = null;
}
}
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === taskId ? { ...task, detail: taskDetail } : task,
),
);
} else {
console.error(
'获取任务详情失败: responseData.code is not 200',
);
}
const getTaskById = useCallback(
(id: string) => {
const task = tasks.find(t => t.id === id);
if (task && !task.detail) {
// fetchTaskDetail(id); // 详情在编辑页面获取
}
} catch (error) {
console.error('获取任务详情失败:', error);
}
};
return task;
},
[tasks],
);
const updateTask = (updatedTask: Task) => {
const fetchTaskDetail = useCallback(
async (taskId: string) => {
try {
if (serverUrl && apiEndpoints) {
const endpoint = apiEndpoints.getTaskDetail.replace(
'{taskId}',
taskId,
);
const url = `${serverUrl}${endpoint}`;
console.log('Fetching task detail from:', url);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const responseData = await response.json();
console.log('task detail responseData', responseData);
if (responseData && responseData.code === 200) {
let taskDetail = responseData.data.detail;
if (taskDetail && typeof taskDetail === 'string') {
try {
taskDetail = JSON.parse(taskDetail);
} catch (e) {
console.error('解析任务详情失败 (detail):', e);
taskDetail = null;
}
}
setTasks(prevTasks =>
prevTasks.map(task =>
task.id === taskId ? { ...task, detail: taskDetail } : task,
),
);
} else {
console.error('获取任务详情失败: responseData.code is not 200');
}
}
} catch (error) {
console.error('获取任务详情失败:', error);
}
},
[serverUrl, apiEndpoints],
);
const updateTask = useCallback((updatedTask: Task) => {
setTasks(prevTasks =>
prevTasks.map(task => (task.id === updatedTask.id ? updatedTask : task)),
);
};
}, []);
const runTask = async (id: string) => {
const task = getTaskById(id);
if (!task || !serverUrl || !apiEndpoints) return;
const runTask = useCallback(
async (id: string) => {
const task = tasks.find(t => t.id === id);
if (!task || !serverUrl || !apiEndpoints) return;
// 更新任务状态为运行中
setTasks(prevTasks =>
prevTasks.map(t => (t.id === id ? { ...t, status: 1 } : t)),
);
try {
// 提取参数值
const parameters = Object.entries(task.parameters).reduce(
(acc, [key, param]) => {
if (param) {
acc[key] = param.value;
}
return acc;
},
{} as { [key: string]: any },
);
// 获取服务器设置并发送任务执行请求
const endpoint = apiEndpoints.runTask.replace('{taskId}', task.id);
await executeTask(serverUrl, endpoint, {
name: task.name,
parameters: parameters,
});
// 模拟任务完成实际项目中应该通过WebSocket或轮询获取任务状态
setTimeout(() => {
setTasks(prevTasks =>
prevTasks.map(t => (t.id === id ? { ...t, status: 2 } : t)),
);
}, 5000);
} catch (error) {
console.error('任务执行失败:', error);
// 任务执行失败,更新状态为错误
setTasks(prevTasks =>
prevTasks.map(t => (t.id === id ? { ...t, status: 3 } : t)),
prevTasks.map(t => (t.id === id ? { ...t, status: 1 } : t)),
);
}
};
try {
const parameters = Object.entries(task.parameters).reduce(
(acc, [key, param]) => {
if (param) {
acc[key] = param.value;
}
return acc;
},
{} as { [key: string]: any },
);
const endpoint = apiEndpoints.runTask.replace('{taskId}', task.id);
await executeTask(serverUrl, endpoint, {
name: task.name,
parameters: parameters,
});
setTimeout(() => {
setTasks(prevTasks =>
prevTasks.map(t => (t.id === id ? { ...t, status: 2 } : t)),
);
}, 5000);
} catch (error) {
console.error('任务执行失败:', error);
setTasks(prevTasks =>
prevTasks.map(t => (t.id === id ? { ...t, status: 3 } : t)),
);
}
},
[tasks, serverUrl, apiEndpoints],
);
const contextValue = useMemo(
() => ({
tasks,
locations,
locationsBays,
payloads,
robotActions,
serverUrl,
getTaskById,
updateTask,
runTask,
refreshConfig,
isConfigLoaded,
fetchTaskDetail,
}),
[
tasks,
locations,
locationsBays,
payloads,
robotActions,
serverUrl,
getTaskById,
updateTask,
runTask,
refreshConfig,
isConfigLoaded,
fetchTaskDetail,
],
);
return (
<TasksContext.Provider
value={{
tasks,
locations,
locationsBays,
payloads,
robotActions,
serverUrl,
getTaskById,
updateTask,
runTask,
refreshConfig,
isConfigLoaded,
}}
>
<TasksContext.Provider value={contextValue}>
{children}
</TasksContext.Provider>
);

View File

@ -1,12 +1,17 @@
import React, { useState, useEffect } from 'react';
import { View, StyleSheet, Alert, ScrollView } from 'react-native';
import {
View,
StyleSheet,
Alert,
ScrollView,
ActivityIndicator,
} from 'react-native';
import { useRoute, useNavigation } from '@react-navigation/native';
import { RouteProp } from '@react-navigation/native';
import { useTasks } from '../context/TasksContext';
import TaskForm from '../components/TaskForm';
import BottomActionBar from '../components/BottomActionBar';
import { Task } from '../types/task';
import { Dialog, Card } from '@rneui/themed';
type RootStackParamList = {
TaskEdit: { taskId: string };
@ -19,19 +24,28 @@ export default function TaskEditScreen() {
const navigation = useNavigation();
const { taskId } = route.params;
const { getTaskById, updateTask, runTask, tasks } = useTasks();
const { getTaskById, updateTask, runTask, tasks, fetchTaskDetail } =
useTasks();
const [task, setTask] = useState<Task | null>(null);
const [originalTask, setOriginalTask] = useState<Task | null>(null);
const [isModified, setIsModified] = useState(false);
useEffect(() => {
const taskData = getTaskById(taskId);
if (taskData) {
setTask(taskData);
setOriginalTask(taskData);
}
}, [taskId, getTaskById, tasks]); // Add tasks to dependency array
const loadTask = async () => {
let taskData = getTaskById(taskId);
if (taskData && !taskData.detail) {
await fetchTaskDetail(taskId);
// Re-fetch task data after details are loaded
taskData = getTaskById(taskId);
}
if (taskData) {
setTask(taskData);
setOriginalTask(taskData);
}
};
loadTask();
}, [taskId, getTaskById, tasks, fetchTaskDetail]);
const handleTaskChange = (updatedTask: Task) => {
setTask(updatedTask);
@ -68,18 +82,16 @@ export default function TaskEditScreen() {
if (!task) {
return (
<Dialog isVisible={true}>
<Dialog.Loading />
</Dialog>
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" color="#00ff00" />
</View>
);
}
return (
<View style={styles.container}>
<ScrollView>
<Card containerStyle={styles.card}>
<TaskForm task={task} onTaskChange={handleTaskChange} />
</Card>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<TaskForm task={task} onTaskChange={handleTaskChange} />
</ScrollView>
<BottomActionBar
onRun={handleRun}
@ -96,10 +108,15 @@ export default function TaskEditScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f0f2f5',
backgroundColor: '#1a1a1a', // 深色背景
},
card: {
borderRadius: 8,
margin: 10,
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#1a1a1a',
},
scrollContainer: {
padding: 16,
},
});

View File

@ -1,5 +1,5 @@
import React from 'react';
import { View, ScrollView, StyleSheet } from 'react-native';
import { View, StyleSheet, FlatList } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import { useTasks } from '../context/TasksContext';
@ -17,23 +17,24 @@ type TaskListNavigationProp = StackNavigationProp<
export default function TaskListScreen() {
const { tasks } = useTasks();
console.log('tasks', tasks);
const navigation = useNavigation<TaskListNavigationProp>();
const handlePressTask = (id: string) => {
navigation.navigate('TaskEdit', { taskId: id });
};
const renderItem = ({ item }: { item: (typeof tasks)[0] }) => (
<TaskCard task={item} onPress={handlePressTask} />
);
return (
<View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer}>
<View style={styles.tasksContainer}>
{tasks.map(task => (
<TaskCard key={task.id} task={task} onPress={handlePressTask} />
))}
</View>
</ScrollView>
<FlatList
data={tasks}
renderItem={renderItem}
keyExtractor={item => item.id}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
</View>
);
}
@ -41,13 +42,11 @@ export default function TaskListScreen() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#1a1a1a', // 深色背景
},
scrollContainer: {
padding: 16,
},
tasksContainer: {
flexDirection: 'row',
flexWrap: 'wrap',
justifyContent: 'space-between',
separator: {
height: 1,
backgroundColor: '#333', // 分隔线颜色
marginLeft: 16,
},
});

View File

@ -1,5 +1,3 @@
import { Task, RobotAction } from './task';
// 配置文件中的位置选项
export interface LocationOption {
label: string;
@ -15,7 +13,8 @@ export interface PayloadOption {
// 配置文件中的机器人动作选项
export interface RobotActionOption {
label: string;
value: RobotAction;
value: string; // 动作的名称,作为 Picker 的 value
actionId: string;
}
// 完整的配置文件结构

View File

@ -1,12 +1,8 @@
// 机器人的具体动作,可以定义为枚举或联合类型
export type RobotAction =
| 'PICKUP'
| 'DROPOFF'
| 'TRANSPORT'
| 'WAIT'
| 'CHARGE'
| 'CLEAN';
// 机器人的具体动作,现在是一个对象
export interface RobotAction {
name: string;
actionId: string;
}
// 参数选项接口
export interface ParameterOption {
@ -18,7 +14,7 @@ export interface ParameterOption {
export interface DynamicParameter {
label?: string;
type?: 'Simple' | 'Select' | 'MultiSelect' | 'Text' | 'Number';
value: string | string[] | number;
value: string | string[] | number | RobotAction;
required?: boolean;
options?: ParameterOption[];
placeholder?: string;
@ -36,6 +32,7 @@ export interface Task {
id: string; // 唯一ID例如使用 uuid
name: string; // 任务名称, e.g., "炉前缓存区到热处理上料交接区运输"
label: string;
description?: string;
version: number;
templateName: string;
periodicTask: boolean;
@ -60,6 +57,9 @@ export interface InputParam {
type: string;
label: string;
required: boolean;
defaultValue: string;
defaultValue: any;
remark: string;
min?: number;
max?: number;
step?: number;
}