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", "version": "0.0.1",
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0", "@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-native/new-app-screen": "0.80.1",
"@react-navigation/bottom-tabs": "^7.4.2", "@react-navigation/bottom-tabs": "^7.4.2",
"@react-navigation/native": "^7.1.14", "@react-navigation/native": "^7.1.14",
@ -2989,6 +2990,19 @@
"node": ">=10" "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": { "node_modules/@react-native/assets-registry": {
"version": "0.80.1", "version": "0.80.1",
"resolved": "https://registry.npmmirror.com/@react-native/assets-registry/-/assets-registry-0.80.1.tgz", "resolved": "https://registry.npmmirror.com/@react-native/assets-registry/-/assets-registry-0.80.1.tgz",

View File

@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@react-native-async-storage/async-storage": "^2.2.0", "@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-native/new-app-screen": "0.80.1",
"@react-navigation/bottom-tabs": "^7.4.2", "@react-navigation/bottom-tabs": "^7.4.2",
"@react-navigation/native": "^7.1.14", "@react-navigation/native": "^7.1.14",

View File

@ -1,13 +1,11 @@
import React from 'react'; import React from 'react';
import { View, StyleSheet } from 'react-native'; import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
import { Button, useTheme } from '@rneui/themed';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons';
interface BottomActionBarProps { interface BottomActionBarProps {
onRun: () => void; onRun: () => void;
onSave: () => void; onSave: () => void;
onUndo: () => void; onUndo: () => void;
onRestore: () => void;
onBack: () => void; onBack: () => void;
isSaveDisabled?: boolean; isSaveDisabled?: boolean;
} }
@ -16,34 +14,78 @@ const BottomActionBar: React.FC<BottomActionBarProps> = ({
onRun, onRun,
onSave, onSave,
onUndo, onUndo,
onRestore,
onBack, onBack,
isSaveDisabled = true, isSaveDisabled = true,
}) => { }) => {
const { theme } = useTheme();
return ( return (
<View style={[styles.bottom, { backgroundColor: theme.colors.background }]}> <View style={styles.container}>
<Button type="clear" onPress={onBack} icon={<Icon name="arrow-left" size={24} color={theme.colors.primary} />} /> <TouchableOpacity onPress={onBack} style={styles.button}>
<Button type="clear" onPress={onRun} icon={<Icon name="play" size={24} color={theme.colors.primary} />} /> <Icon name="arrow-left" size={24} color="#AAAAAA" />
<Button type="clear" onPress={onSave} disabled={isSaveDisabled} icon={<Icon name="content-save" size={24} color={isSaveDisabled ? theme.colors.disabled : theme.colors.primary} />} /> <Text style={styles.buttonText}></Text>
<Button type="clear" onPress={onUndo} icon={<Icon name="undo" size={24} color={theme.colors.primary} />} /> </TouchableOpacity>
<Button type="clear" onPress={onRestore} icon={<Icon name="restore" size={24} color={theme.colors.primary} />} /> <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> </View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
bottom: { container: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'space-around', justifyContent: 'space-around',
paddingVertical: 8, paddingVertical: 12,
paddingHorizontal: 10,
backgroundColor: 'rgba(26, 26, 26, 0.9)',
borderTopWidth: 1, 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 React from 'react';
import { StyleSheet, TouchableOpacity, Dimensions } from 'react-native'; import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { Card, Chip } from '@rneui/themed';
import { Task } from '../types/task'; import { Task } from '../types/task';
interface TaskCardProps { interface TaskCardProps {
@ -9,60 +8,75 @@ interface TaskCardProps {
} }
const statusMap: { [key: number]: { text: string; color: string } } = { const statusMap: { [key: number]: { text: string; color: string } } = {
0: { text: 'IDLE', color: 'grey' }, 0: { text: '待机', color: '#9E9E9E' }, // Grey
1: { text: 'RUNNING', color: 'blue' }, 1: { text: '运行中', color: '#2196F3' }, // Blue
2: { text: 'COMPLETED', color: 'green' }, 2: { text: '完成', color: '#4CAF50' }, // Green
3: { text: 'ERROR', color: 'red' }, 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 TaskCard: React.FC<TaskCardProps> = ({ task, onPress }) => {
const statusInfo = statusMap[task.status] || statusMap[0]; const statusInfo = statusMap[task.status] || statusMap[0];
return ( return (
<TouchableOpacity onPress={() => onPress(task.id)} style={styles.touchable}> <TouchableOpacity onPress={() => onPress(task.id)} style={styles.container}>
<Card containerStyle={styles.card}> <View style={styles.indicatorContainer}>
<Card.Title style={styles.title}>{task.label}</Card.Title> <View
<Card.Divider /> style={[
<Chip styles.statusIndicator,
title={statusInfo.text} { backgroundColor: statusInfo.color },
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 }}
/> />
</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> </TouchableOpacity>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
touchable: { container: {
width: cardWidth, flexDirection: 'row',
marginBottom: 8, alignItems: 'center',
backgroundColor: '#1a1a1a',
paddingVertical: 16,
paddingHorizontal: 16,
}, },
card: { indicatorContainer: {
margin: 4, marginRight: 16,
borderRadius: 8, },
width: cardWidth - 8, // 减去margin statusIndicator: {
width: 12,
height: 12,
borderRadius: 6,
},
contentContainer: {
flex: 1,
}, },
title: { title: {
marginBottom: 12, color: '#FFFFFF',
minHeight: 50, // Ensure cards have similar height fontSize: 16,
textAlign: 'left', fontWeight: 'bold',
marginBottom: 4,
}, },
chip: { description: {
alignSelf: 'flex-start', color: '#999999',
fontSize: 14,
}, },
chipTitle: { statusContainer: {
fontSize: 12, marginLeft: 16,
},
statusText: {
fontSize: 14,
fontWeight: 'bold',
}, },
}); });

View File

@ -1,6 +1,9 @@
import React, { useEffect } from 'react'; import React from 'react';
import { StyleSheet, ScrollView, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import { Input } from '@rneui/themed'; 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'; import { Task, RobotAction, InputParam } from '../types/task';
interface TaskFormProps { interface TaskFormProps {
@ -9,11 +12,12 @@ interface TaskFormProps {
} }
const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => { const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => {
useEffect(() => { const { locations, payloads, robotActions, locationsBays } = useTasks();
console.log('TaskForm task prop updated:', task);
}, [task]);
const handleParamChange = (field: string, value: string | RobotAction) => { const handleParamChange = (
field: string,
value: string | number | RobotAction,
) => {
const currentParam = task.parameters[field] || {}; const currentParam = task.parameters[field] || {};
const updatedTask = { const updatedTask = {
...task, ...task,
@ -30,10 +34,10 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => {
const renderFormInput = (param: InputParam) => { const renderFormInput = (param: InputParam) => {
const parameter = task.parameters?.[param.name]; const parameter = task.parameters?.[param.name];
const value = parameter?.value || param.defaultValue; const value = parameter?.value ?? param.defaultValue;
const label = ( const label = (
<Text> <Text style={styles.label}>
{param.label} {param.label}
{param.required && <Text style={styles.required}> *</Text>} {param.required && <Text style={styles.required}> *</Text>}
</Text> </Text>
@ -42,54 +46,219 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => {
switch (param.type.toLowerCase()) { switch (param.type.toLowerCase()) {
case 'string': case 'string':
return ( return (
<Input <View key={param.name} style={styles.inputGroup}>
key={param.name} {label}
label={label} <Input
value={value as string} value={value as string}
onChangeText={text => handleParamChange(param.name, text)} onChangeText={text => handleParamChange(param.name, text)}
placeholder={param.remark} placeholder={param.remark || '请输入...'}
errorMessage={param.remark} inputContainerStyle={styles.inputContainer}
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: default:
return ( return null;
<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 ( return (
<ScrollView contentContainerStyle={styles.container}> <View style={styles.container}>
<Input <View style={styles.inputGroup}>
label="任务名称" <Text style={styles.label}></Text>
value={task.name} <Input
onChangeText={text => onTaskChange({ ...task, name: text })} value={task.name}
inputContainerStyle={styles.inputContainer} onChangeText={text => onTaskChange({ ...task, name: text })}
/> inputContainerStyle={styles.inputContainer}
inputStyle={styles.inputText}
/>
</View>
{task.detail && task.detail.inputParams ? ( {task.detail && task.detail.inputParams ? (
task.detail.inputParams.map(param => renderFormInput(param)) task.detail.inputParams.map(param => renderFormInput(param))
) : ( ) : (
<View style={styles.loadingContainer}> <View style={styles.loadingContainer}>
<Text>...</Text> <Text style={styles.loadingText}>...</Text>
</View> </View>
)} )}
</ScrollView> </View>
); );
}; };
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { 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: { loadingContainer: {
flex: 1, flex: 1,
@ -97,11 +266,9 @@ const styles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
padding: 20, padding: 20,
}, },
required: { loadingText: {
color: 'red', color: '#AAAAAA',
}, fontSize: 16,
inputContainer: {
marginBottom: 10,
}, },
}); });

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

@ -0,0 +1 @@

View File

@ -4,6 +4,8 @@ import React, {
useContext, useContext,
ReactNode, ReactNode,
useEffect, useEffect,
useMemo,
useCallback,
} from 'react'; } from 'react';
import { Task } from '../types/task'; import { Task } from '../types/task';
import { import {
@ -12,11 +14,7 @@ import {
PayloadOption, PayloadOption,
RobotActionOption, RobotActionOption,
} from '../types/config'; } from '../types/config';
import { import { getConfig, executeTask } from '../services/configService';
getConfig,
executeTask,
clearCachedConfig,
} from '../services/configService';
interface TasksContextData { interface TasksContextData {
tasks: Task[]; tasks: Task[];
@ -30,6 +28,7 @@ interface TasksContextData {
runTask: (id: string) => void; runTask: (id: string) => void;
refreshConfig: () => Promise<void>; refreshConfig: () => Promise<void>;
isConfigLoaded: boolean; isConfigLoaded: boolean;
fetchTaskDetail: (taskId: string) => Promise<void>;
} }
const TasksContext = createContext<TasksContextData>({} as TasksContextData); const TasksContext = createContext<TasksContextData>({} as TasksContextData);
@ -45,7 +44,7 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
const [isConfigLoaded, setIsConfigLoaded] = useState(false); const [isConfigLoaded, setIsConfigLoaded] = useState(false);
const [serverUrl, setServerUrl] = useState<string | null>(null); const [serverUrl, setServerUrl] = useState<string | null>(null);
const fetchTasks = async (baseUrl: string, endpoint: string) => { const fetchTasks = useCallback(async (baseUrl: string, endpoint: string) => {
try { try {
if (baseUrl && endpoint) { if (baseUrl && endpoint) {
const fetchUrl = `${baseUrl}${endpoint}?pageNum=1&pageSize=100`; const fetchUrl = `${baseUrl}${endpoint}?pageNum=1&pageSize=100`;
@ -73,14 +72,14 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
return { return {
...task, ...task,
name: task.label, name: task.label,
parameters: task.parameters || {}, // Ensure parameters object exists parameters: task.parameters || {},
detail: detail, detail: detail,
}; };
}); });
setTasks(fetchedTasks); setTasks(fetchedTasks);
} else { } else {
console.error('获取任务列表失败: responseData.code is not 200'); console.error('获取任务列表失败: responseData.code is not 200');
setTasks([]); // setTasks([]);
} }
} }
} catch (error) { } catch (error) {
@ -88,17 +87,16 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
if (error instanceof Error) { if (error instanceof Error) {
console.error('Error message:', error.message); console.error('Error message:', error.message);
} }
setTasks([]); // setTasks([]);
} }
}; }, []); // Empty dependency array as it has no external dependencies from component scope
useEffect(() => { useEffect(() => {
const loadApp = async () => { const loadApp = async () => {
try { try {
await clearCachedConfig();
const config = await getConfig(); const config = await getConfig();
if (config) { if (config) {
applyConfig(config); // Don't load tasks from config applyConfig(config);
setIsConfigLoaded(true); setIsConfigLoaded(true);
if (config.serverUrl && config.apiEndpoints) { if (config.serverUrl && config.apiEndpoints) {
await fetchTasks(config.serverUrl, config.apiEndpoints.getTasks); await fetchTasks(config.serverUrl, config.apiEndpoints.getTasks);
@ -114,7 +112,7 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
}; };
loadApp(); loadApp();
}, []); }, [fetchTasks]);
const [apiEndpoints, setApiEndpoints] = useState<{ const [apiEndpoints, setApiEndpoints] = useState<{
getTasks: string; getTasks: string;
@ -135,12 +133,12 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
} }
}; };
const refreshConfig = async () => { const refreshConfig = useCallback(async () => {
try { try {
await clearCachedConfig(); // await clearCachedConfig(); // 通常刷新不需要清除缓存
const config = await getConfig(); const config = await getConfig();
if (config) { if (config) {
applyConfig(config); // Don't load tasks from config applyConfig(config);
setIsConfigLoaded(true); setIsConfigLoaded(true);
if (config.serverUrl && config.apiEndpoints) { if (config.serverUrl && config.apiEndpoints) {
await fetchTasks(config.serverUrl, config.apiEndpoints.getTasks); await fetchTasks(config.serverUrl, config.apiEndpoints.getTasks);
@ -153,122 +151,144 @@ export const TasksProvider: React.FC<{ children: ReactNode }> = ({
console.error('刷新配置失败:', error); console.error('刷新配置失败:', error);
setIsConfigLoaded(false); setIsConfigLoaded(false);
} }
}; }, [fetchTasks]);
const getTaskById = (id: string) => { const getTaskById = useCallback(
const task = tasks.find(t => t.id === id); (id: string) => {
if (task && !task.detail) { const task = tasks.find(t => t.id === id);
fetchTaskDetail(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',
);
}
} }
} catch (error) { return task;
console.error('获取任务详情失败:', error); },
} [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 => setTasks(prevTasks =>
prevTasks.map(task => (task.id === updatedTask.id ? updatedTask : task)), prevTasks.map(task => (task.id === updatedTask.id ? updatedTask : task)),
); );
}; }, []);
const runTask = async (id: string) => { const runTask = useCallback(
const task = getTaskById(id); async (id: string) => {
if (!task || !serverUrl || !apiEndpoints) return; 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 => 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 ( return (
<TasksContext.Provider <TasksContext.Provider value={contextValue}>
value={{
tasks,
locations,
locationsBays,
payloads,
robotActions,
serverUrl,
getTaskById,
updateTask,
runTask,
refreshConfig,
isConfigLoaded,
}}
>
{children} {children}
</TasksContext.Provider> </TasksContext.Provider>
); );

View File

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

View File

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

View File

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

View File

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