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 { StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
import { Input, BottomSheet, ListItem } from '@rneui/themed';
import { StyleSheet, ScrollView, TouchableOpacity, View } from 'react-native';
import { Input, BottomSheet, ListItem, Button } from '@rneui/themed';
import { Task, RobotAction } from '../types/task';
import { LOCATIONS, ROBOT_ACTIONS, PAYLOADS } from '../data/mockData';
interface TaskFormProps {
task: Task;
onTaskChange: (updatedTask: Task) => void;
onLocationBayChange?: (task: Task) => void; // 库位变化时的回调
}
const TaskForm: React.FC<TaskFormProps> = ({
task,
onTaskChange,
onLocationBayChange,
}) => {
const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => {
const [isVisible, setIsVisible] = useState(false);
const [currentField, setCurrentField] = useState('');
const [currentItems, setCurrentItems] = useState<
{ label: string; value: string }[]
>([]);
const [isQrInputFocused, setIsQrInputFocused] = useState(true);
// 创建库位输入框的ref
const locationBayInputRef = useRef<any>(null);
// 用于防抖的ref
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
// 保存上一次的库位值,用于检测变化
const previousLocationBayRef = useRef<string>(
task.parameters.locationBay || '',
);
// 创建隐藏的二维码扫描输入框的ref
const qrScanInputRef = useRef<any>(null);
// 组件挂载时自动聚焦到库位输入框
// 用于防止循环更新的标志
const isUpdatingFromQrRef = useRef(false);
const isUpdatingFromFormRef = useRef(false);
// 组件挂载时自动聚焦到隐藏的扫描输入框
useEffect(() => {
const timer = setTimeout(() => {
if (locationBayInputRef.current) {
locationBayInputRef.current.focus();
if (qrScanInputRef.current) {
qrScanInputRef.current.focus();
}
}, 100);
return () => clearTimeout(timer);
}, []);
// 监听库位值变化,自动触发运行任务
useEffect(() => {
const currentLocationBay = task.parameters.locationBay || '';
const previousLocationBay = previousLocationBayRef.current;
// 解析二维码信息对象字面量格式
const parseQrCodeInfo = (infoString: string): Partial<Task['parameters']> => {
const result: Partial<Task['parameters']> = {};
// 只有当库位值真正发生变化且不为空时才触发
if (
currentLocationBay !== previousLocationBay &&
currentLocationBay.trim() !== ''
) {
// 清除之前的防抖定时器
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
try {
// 移除花括号和换行符,准备解析
let cleanString = infoString.trim();
if (cleanString.startsWith('{')) {
cleanString = cleanString.substring(1);
}
if (cleanString.endsWith('}')) {
cleanString = cleanString.substring(0, cleanString.length - 1);
}
// 设置新的防抖定时器500ms后触发
debounceTimerRef.current = setTimeout(() => {
if (onLocationBayChange) {
onLocationBayChange(task);
// 按逗号分割各个属性
const items = cleanString.split(',');
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);
}
// 更新上一次的库位值
previousLocationBayRef.current = currentLocationBay;
return result;
};
// 清理函数
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
// 处理二维码扫描输入,更新表单字段
const handleQrCodeScan = (scanData: string) => {
if (isUpdatingFromFormRef.current || !scanData.trim()) return;
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) => {
if (isUpdatingFromQrRef.current) return;
isUpdatingFromFormRef.current = true;
const updatedTask = {
...task,
parameters: {
@ -84,6 +155,11 @@ const TaskForm: React.FC<TaskFormProps> = ({
},
};
onTaskChange(updatedTask);
// 重置标志
setTimeout(() => {
isUpdatingFromFormRef.current = false;
}, 0);
};
const openBottomSheet = (
@ -113,10 +189,43 @@ const TaskForm: React.FC<TaskFormProps> = ({
return (
<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
label="任务名称"
value={task.name}
onChangeText={text => onTaskChange({ ...task, name: text })}
onFocus={handleOtherInputFocus}
/>
{renderDropdown(
@ -136,6 +245,7 @@ const TaskForm: React.FC<TaskFormProps> = ({
label="途经点 (可选)"
value={task.parameters.waypoint || ''}
onChangeText={text => handleParamChange('waypoint', text)}
onFocus={handleOtherInputFocus}
/>
{renderDropdown(
@ -146,15 +256,13 @@ const TaskForm: React.FC<TaskFormProps> = ({
)}
{renderDropdown('payload', '载荷', task.parameters.payload, PAYLOADS)}
{/* 库位字段改为普通输入框 */}
{/* 库位字段 */}
<Input
ref={locationBayInputRef}
label="库位"
value={task.parameters.locationBay || ''}
onChangeText={text => handleParamChange('locationBay', text)}
showSoftInputOnFocus={false}
autoFocus={false}
placeholder="请输入库位"
onFocus={handleOtherInputFocus}
/>
<BottomSheet
@ -185,6 +293,25 @@ const styles = StyleSheet.create({
container: {
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;

View File

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