feat: 添加二维码扫描功能,优化任务表单输入体验

This commit is contained in:
xudan 2025-07-22 13:57:42 +08:00
parent 726b7db95f
commit 0e0cef6eba
2 changed files with 176 additions and 62 deletions

View File

@ -1,81 +1,152 @@
import React, { useState, useRef, useEffect } from 'react'; import React, { useState, useRef, useEffect } from 'react';
import { StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; import { StyleSheet, ScrollView, TouchableOpacity, View } from 'react-native';
import { Input, BottomSheet, ListItem } from '@rneui/themed'; import { Input, BottomSheet, ListItem, Button } from '@rneui/themed';
import { Task, RobotAction } from '../types/task'; import { Task, RobotAction } from '../types/task';
import { LOCATIONS, ROBOT_ACTIONS, PAYLOADS } from '../data/mockData'; import { LOCATIONS, ROBOT_ACTIONS, PAYLOADS } from '../data/mockData';
interface TaskFormProps { interface TaskFormProps {
task: Task; task: Task;
onTaskChange: (updatedTask: Task) => void; onTaskChange: (updatedTask: Task) => void;
onLocationBayChange?: (task: Task) => void; // 库位变化时的回调
} }
const TaskForm: React.FC<TaskFormProps> = ({ const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => {
task,
onTaskChange,
onLocationBayChange,
}) => {
const [isVisible, setIsVisible] = useState(false); const [isVisible, setIsVisible] = useState(false);
const [currentField, setCurrentField] = useState(''); const [currentField, setCurrentField] = useState('');
const [currentItems, setCurrentItems] = useState< const [currentItems, setCurrentItems] = useState<
{ label: string; value: string }[] { label: string; value: string }[]
>([]); >([]);
const [isQrInputFocused, setIsQrInputFocused] = useState(true);
// 创建库位输入框的ref // 创建隐藏的二维码扫描输入框的ref
const locationBayInputRef = useRef<any>(null); const qrScanInputRef = useRef<any>(null);
// 用于防抖的ref
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
// 保存上一次的库位值,用于检测变化
const previousLocationBayRef = useRef<string>(
task.parameters.locationBay || '',
);
// 组件挂载时自动聚焦到库位输入框 // 用于防止循环更新的标志
const isUpdatingFromQrRef = useRef(false);
const isUpdatingFromFormRef = useRef(false);
// 组件挂载时自动聚焦到隐藏的扫描输入框
useEffect(() => { useEffect(() => {
const timer = setTimeout(() => { const timer = setTimeout(() => {
if (locationBayInputRef.current) { if (qrScanInputRef.current) {
locationBayInputRef.current.focus(); qrScanInputRef.current.focus();
} }
}, 100); }, 100);
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, []);
// 监听库位值变化,自动触发运行任务 // 解析二维码信息对象字面量格式
useEffect(() => { const parseQrCodeInfo = (infoString: string): Partial<Task['parameters']> => {
const currentLocationBay = task.parameters.locationBay || ''; const result: Partial<Task['parameters']> = {};
const previousLocationBay = previousLocationBayRef.current;
// 只有当库位值真正发生变化且不为空时才触发 try {
if ( // 移除花括号和换行符,准备解析
currentLocationBay !== previousLocationBay && let cleanString = infoString.trim();
currentLocationBay.trim() !== '' if (cleanString.startsWith('{')) {
) { cleanString = cleanString.substring(1);
// 清除之前的防抖定时器 }
if (debounceTimerRef.current) { if (cleanString.endsWith('}')) {
clearTimeout(debounceTimerRef.current); cleanString = cleanString.substring(0, cleanString.length - 1);
} }
// 设置新的防抖定时器500ms后触发 // 按逗号分割各个属性
debounceTimerRef.current = setTimeout(() => { const items = cleanString.split(',');
if (onLocationBayChange) {
onLocationBayChange(task); for (const item of items) {
const trimmedItem = item.trim();
if (!trimmedItem) continue;
// 解析 "key: value" 格式
const colonIndex = trimmedItem.indexOf(':');
if (colonIndex === -1) continue;
const key = trimmedItem.substring(0, colonIndex).trim();
const value = trimmedItem.substring(colonIndex + 1).trim();
// 映射字段名
switch (key) {
case 'startLocation':
result.startLocation = value;
break;
case 'endLocation':
result.endLocation = value;
break;
case 'waypoint':
result.waypoint = value;
break;
case 'robotAction':
result.robotAction = value as RobotAction;
break;
case 'payload':
result.payload = value;
break;
case 'locationBay':
result.locationBay = value;
break;
} }
}, 500); }
} catch (error) {
// 解析失败时不报错,返回空对象
console.log('解析二维码信息失败:', error);
} }
// 更新上一次的库位值 return result;
previousLocationBayRef.current = currentLocationBay; };
// 清理函数 // 处理二维码扫描输入,更新表单字段
return () => { const handleQrCodeScan = (scanData: string) => {
if (debounceTimerRef.current) { if (isUpdatingFromFormRef.current || !scanData.trim()) return;
clearTimeout(debounceTimerRef.current);
} isUpdatingFromQrRef.current = true;
const parsedParams = parseQrCodeInfo(scanData);
const updatedTask = {
...task,
parameters: {
...task.parameters,
...parsedParams,
},
}; };
}, [task.parameters.locationBay, task, onLocationBayChange]);
onTaskChange(updatedTask);
// 清空扫描输入框
if (qrScanInputRef.current) {
qrScanInputRef.current.clear();
}
// 重置标志
setTimeout(() => {
isUpdatingFromQrRef.current = false;
}, 0);
};
// 重新扫描按钮点击处理
const handleRescan = () => {
if (qrScanInputRef.current) {
qrScanInputRef.current.focus();
}
};
// 处理其他输入框获得焦点
const handleOtherInputFocus = () => {
setIsQrInputFocused(false);
};
// 处理二维码输入框焦点事件
const handleQrInputFocus = () => {
setIsQrInputFocused(true);
};
const handleQrInputBlur = () => {
setIsQrInputFocused(false);
};
const handleParamChange = (field: string, value: string | RobotAction) => { const handleParamChange = (field: string, value: string | RobotAction) => {
if (isUpdatingFromQrRef.current) return;
isUpdatingFromFormRef.current = true;
const updatedTask = { const updatedTask = {
...task, ...task,
parameters: { parameters: {
@ -84,6 +155,11 @@ const TaskForm: React.FC<TaskFormProps> = ({
}, },
}; };
onTaskChange(updatedTask); onTaskChange(updatedTask);
// 重置标志
setTimeout(() => {
isUpdatingFromFormRef.current = false;
}, 0);
}; };
const openBottomSheet = ( const openBottomSheet = (
@ -113,10 +189,43 @@ const TaskForm: React.FC<TaskFormProps> = ({
return ( return (
<ScrollView contentContainerStyle={styles.container}> <ScrollView contentContainerStyle={styles.container}>
{/* 隐藏的二维码扫描输入框 */}
<Input
ref={qrScanInputRef}
style={styles.hiddenInput}
containerStyle={styles.hiddenContainer}
inputContainerStyle={styles.hiddenContainer}
onChangeText={handleQrCodeScan}
onFocus={handleQrInputFocus}
onBlur={handleQrInputBlur}
autoFocus={false}
showSoftInputOnFocus={false}
/>
{/* 扫描二维码按钮 */}
<View style={styles.scanButtonContainer}>
<Button
title={
isQrInputFocused ? '正在等待二维码扫描...' : '扫描二维码填充表单'
}
onPress={handleRescan}
icon={{
name: isQrInputFocused ? 'hourglass-empty' : 'qr-code-scanner',
type: 'material',
}}
buttonStyle={[
styles.scanButton,
isQrInputFocused && styles.waitingButton,
]}
disabled={isQrInputFocused}
/>
</View>
<Input <Input
label="任务名称" label="任务名称"
value={task.name} value={task.name}
onChangeText={text => onTaskChange({ ...task, name: text })} onChangeText={text => onTaskChange({ ...task, name: text })}
onFocus={handleOtherInputFocus}
/> />
{renderDropdown( {renderDropdown(
@ -136,6 +245,7 @@ const TaskForm: React.FC<TaskFormProps> = ({
label="途经点 (可选)" label="途经点 (可选)"
value={task.parameters.waypoint || ''} value={task.parameters.waypoint || ''}
onChangeText={text => handleParamChange('waypoint', text)} onChangeText={text => handleParamChange('waypoint', text)}
onFocus={handleOtherInputFocus}
/> />
{renderDropdown( {renderDropdown(
@ -146,15 +256,13 @@ const TaskForm: React.FC<TaskFormProps> = ({
)} )}
{renderDropdown('payload', '载荷', task.parameters.payload, PAYLOADS)} {renderDropdown('payload', '载荷', task.parameters.payload, PAYLOADS)}
{/* 库位字段改为普通输入框 */} {/* 库位字段 */}
<Input <Input
ref={locationBayInputRef}
label="库位" label="库位"
value={task.parameters.locationBay || ''} value={task.parameters.locationBay || ''}
onChangeText={text => handleParamChange('locationBay', text)} onChangeText={text => handleParamChange('locationBay', text)}
showSoftInputOnFocus={false}
autoFocus={false}
placeholder="请输入库位" placeholder="请输入库位"
onFocus={handleOtherInputFocus}
/> />
<BottomSheet <BottomSheet
@ -185,6 +293,25 @@ const styles = StyleSheet.create({
container: { container: {
padding: 16, padding: 16,
}, },
hiddenInput: {
height: 0,
opacity: 0,
},
hiddenContainer: {
height: 0,
margin: 0,
padding: 0,
},
scanButtonContainer: {
marginBottom: 16,
},
scanButton: {
backgroundColor: '#2196F3',
borderRadius: 8,
},
waitingButton: {
backgroundColor: '#9E9E9E', // 浅灰色,表示等待状态
},
}); });
export default TaskForm; export default TaskForm;

View File

@ -65,15 +65,6 @@ export default function TaskEditScreen() {
setIsModified(false); setIsModified(false);
}; };
// 处理库位变化,自动运行任务
const handleLocationBayChange = (updatedTask: Task) => {
console.log(
'库位已变化,自动运行任务:',
updatedTask.parameters.locationBay,
);
runTask(updatedTask.id);
};
if (!task) { if (!task) {
return ( return (
<Dialog isVisible={true}> <Dialog isVisible={true}>
@ -84,11 +75,7 @@ export default function TaskEditScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<TaskForm <TaskForm task={task} onTaskChange={handleTaskChange} />
task={task}
onTaskChange={handleTaskChange}
onLocationBayChange={handleLocationBayChange}
/>
<BottomActionBar <BottomActionBar
onRun={handleRun} onRun={handleRun}
onSave={handleSave} onSave={handleSave}