feat: 添加二维码扫描功能至任务编辑页面,支持解析二维码信息并自动填充表单,同时优化相关状态管理和动画效果

This commit is contained in:
xudan 2025-07-24 14:40:40 +08:00
parent 03f8e8d74a
commit 75ac33ca5d
3 changed files with 268 additions and 34 deletions

View File

@ -45,7 +45,7 @@ const TaskForm: React.FC<TaskFormProps> = ({ task, onTaskChange }) => {
<View key={param.name} style={styles.inputGroup}> <View key={param.name} style={styles.inputGroup}>
{label} {label}
<Input <Input
value={value as string} value={String(value || '')}
onChangeText={text => handleParamChange(param.name, text)} onChangeText={text => handleParamChange(param.name, text)}
placeholder={param.remark || '请输入...'} placeholder={param.remark || '请输入...'}
inputContainerStyle={styles.inputContainer} inputContainerStyle={styles.inputContainer}

View File

@ -13,7 +13,7 @@ import { AppSettings } from '../types/config';
import { import {
getSettings, getSettings,
saveSettings, saveSettings,
downloadConfig, // downloadConfig,
getConfig, getConfig,
clearCachedConfig, clearCachedConfig,
} from '../services/configService'; } from '../services/configService';
@ -46,11 +46,7 @@ export default function SettingsScreen() {
const checkConfigStatus = async () => { const checkConfigStatus = async () => {
const config = await getConfig(); const config = await getConfig();
if (config) { if (config) {
setConfigStatus( setConfigStatus(`已加载 (版本: ${config.version})`);
`已加载 (版本: ${config.version}, 任务数: ${
config.tasks?.length || 0
})`,
);
} else { } else {
setConfigStatus('未加载'); setConfigStatus('未加载');
} }
@ -107,26 +103,20 @@ export default function SettingsScreen() {
await saveSettings(settings); await saveSettings(settings);
// 下载配置文件 // 下载配置文件
const config = await downloadConfig( // const config = await downloadConfig(
settings.serverUrl, // settings.serverUrl,
settings.configFileName, // settings.configFileName,
); // );
Alert.alert( Alert.alert('成功', `配置文件下载成功!\n版本: `, [
'成功', {
`配置文件下载成功!\n版本: ${config.version}\n任务数量: ${ text: '确定',
config.tasks?.length || 0 onPress: async () => {
}`, await refreshConfig();
[ checkConfigStatus();
{
text: '确定',
onPress: async () => {
await refreshConfig();
checkConfigStatus();
},
}, },
], },
); ]);
} catch (error) { } catch (error) {
Alert.alert( Alert.alert(
'下载失败', '下载失败',

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { import {
View, View,
StyleSheet, StyleSheet,
@ -6,7 +6,11 @@ import {
ScrollView, ScrollView,
ActivityIndicator, ActivityIndicator,
Text, Text,
TouchableOpacity,
TextInput,
Animated,
} from 'react-native'; } from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
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';
@ -36,6 +40,14 @@ export default function TaskEditScreen() {
null, null,
); );
// 添加二维码相关状态
const [qrCodeInfo, setQrCodeInfo] = useState<string>('');
const [isWaitingForQrCode, setIsWaitingForQrCode] = useState(true); // 默认为等待扫描状态
const qrCodeInputRef = useRef<TextInput>(null);
// 添加动画相关状态
const rotateAnim = useRef(new Animated.Value(0)).current;
useEffect(() => { useEffect(() => {
const loadTask = async () => { const loadTask = async () => {
let taskData = getTaskById(taskId); let taskData = getTaskById(taskId);
@ -64,6 +76,121 @@ export default function TaskEditScreen() {
loadTask(); loadTask();
}, [taskId, getTaskById, tasks, fetchTaskDetail]); }, [taskId, getTaskById, tasks, fetchTaskDetail]);
// 聚焦到二维码输入框和启动动画
useEffect(() => {
if (isWaitingForQrCode && qrCodeInputRef.current) {
// 清空二维码信息输入框
setQrCodeInfo('');
setTimeout(() => {
qrCodeInputRef.current?.focus();
}, 500);
}
// 启动或停止旋转动画
if (isWaitingForQrCode) {
// 启动旋转动画
const rotateAnimation = Animated.loop(
Animated.timing(rotateAnim, {
toValue: 1,
duration: 2000,
useNativeDriver: true,
}),
);
rotateAnimation.start();
return () => {
rotateAnimation.stop();
};
} else {
// 停止动画并重置
rotateAnim.setValue(0);
}
}, [isWaitingForQrCode, rotateAnim]);
// 解析二维码信息并填充表单
const parseQrCodeAndFillForm = (qrContent: string) => {
if (
!task ||
!task.detail ||
!task.detail.inputParams ||
!qrContent.trim()
) {
return;
}
try {
// 清理二维码内容:移除换行符和多余空格
const cleanedContent = qrContent
.replace(/[\r\n\t]/g, '') // 移除所有换行符和制表符
.replace(/\s+/g, ' ') // 多个空格替换为单个空格
.trim();
const qrData = JSON.parse(cleanedContent);
console.log('二维码解析成功:', qrData);
// 创建新的任务对象,保持原有数据不变
const updatedTask = { ...task };
// 确保parameters对象存在
if (!updatedTask.parameters) {
updatedTask.parameters = {};
}
// 先清空所有表单字段为空字符串
task.detail!.inputParams.forEach(param => {
const currentParam = updatedTask.parameters[param.name] || {};
updatedTask.parameters[param.name] = {
...currentParam,
value: '', // 清空为空字符串
};
});
// 然后用二维码数据填充匹配的字段
Object.keys(qrData).forEach(qrKey => {
const matchingParam = task.detail!.inputParams.find(
param => param.name === qrKey,
);
if (matchingParam) {
// 根据字段类型转换值
let convertedValue = qrData[qrKey];
if (matchingParam.type.toLowerCase() === 'string') {
convertedValue = String(qrData[qrKey]);
} else if (matchingParam.type.toLowerCase() === 'number') {
convertedValue = Number(qrData[qrKey]);
}
// 如果找到匹配的参数,更新其值
const currentParam = updatedTask.parameters[qrKey] || {};
updatedTask.parameters[qrKey] = {
...currentParam,
value: convertedValue,
};
console.log(`二维码填充: ${qrKey} = ${convertedValue}`);
}
});
setTask(updatedTask);
if (!isModified) {
setIsModified(true);
}
// 扫描成功后,按钮状态变回"扫描二维码获取信息"
setIsWaitingForQrCode(false);
} catch (error) {
// 解析失败不报错,按照需求静默处理
console.log('二维码信息解析失败,但不影响使用:', error);
}
};
// 处理二维码输入框内容变化
const handleQrCodeChange = (text: string) => {
setQrCodeInfo(text);
if (text.trim()) {
parseQrCodeAndFillForm(text);
}
};
const handleTaskChange = (updatedTask: Task) => { const handleTaskChange = (updatedTask: Task) => {
setTask(updatedTask); setTask(updatedTask);
if (!isModified) { if (!isModified) {
@ -71,6 +198,17 @@ export default function TaskEditScreen() {
} }
}; };
// 处理二维码扫描按钮点击
const handleQrCodeScanButton = () => {
if (!isWaitingForQrCode) {
setIsWaitingForQrCode(true);
// 聚焦到二维码输入框 (清空操作已在useEffect中处理)
setTimeout(() => {
qrCodeInputRef.current?.focus();
}, 100);
}
};
const handleSave = () => { const handleSave = () => {
if (task) { if (task) {
updateTask(task); updateTask(task);
@ -90,14 +228,25 @@ export default function TaskEditScreen() {
setRunResponse(null); setRunResponse(null);
try { try {
const params = task.detail.inputParams.map(p => ({ const params = task.detail.inputParams.map(p => {
name: p.name, // 获取用户设置的实际值,如果用户没有设置任何值才使用默认值
type: p.type, const parameter = task.parameters?.[p.name];
label: p.label, const hasUserInput = parameter !== undefined;
required: p.required, const actualValue = hasUserInput ? parameter.value : p.defaultValue;
defaultValue: p.defaultValue,
remark: p.remark, console.log(
})); `运行参数 ${p.name}: 用户是否输入=${hasUserInput}, 实际值="${actualValue}" (原默认值="${p.defaultValue}")`,
);
return {
name: p.name,
type: p.type,
label: p.label,
required: p.required,
defaultValue: actualValue, // 如果用户有输入(包括空值)就用用户值,否则用默认值
remark: p.remark,
};
});
const request: RunTaskRequest = { const request: RunTaskRequest = {
taskId: task.id, taskId: task.id,
@ -120,6 +269,9 @@ export default function TaskEditScreen() {
const handleUndo = () => { const handleUndo = () => {
setTask(originalTask); setTask(originalTask);
setIsModified(false); setIsModified(false);
// 重置二维码相关状态
setQrCodeInfo('');
setIsWaitingForQrCode(true); // 重置为默认的等待扫描状态
}; };
if (!task) { if (!task) {
@ -133,6 +285,65 @@ export default function TaskEditScreen() {
return ( return (
<View style={styles.container}> <View style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer}> <ScrollView contentContainerStyle={styles.scrollContainer}>
{/* 二维码扫描按钮 */}
<TouchableOpacity
style={[
styles.qrCodeButton,
isWaitingForQrCode
? styles.qrCodeButtonWaiting
: styles.qrCodeButtonReady,
]}
onPress={handleQrCodeScanButton}
>
<View style={styles.qrCodeButtonContent}>
{isWaitingForQrCode ? (
<Animated.View
style={[
styles.qrCodeButtonIcon,
{
transform: [
{
rotate: rotateAnim.interpolate({
inputRange: [0, 1],
outputRange: ['0deg', '360deg'],
}),
},
],
},
]}
>
<Icon name="hourglass-empty" size={20} color="#ffffff" />
</Animated.View>
) : (
<Icon
name="qr-code"
size={20}
color="#ffffff"
style={styles.qrCodeButtonIcon}
/>
)}
<Text style={styles.qrCodeButtonText}>
{isWaitingForQrCode ? '等待扫描二维码' : '点击启动扫码'}
</Text>
</View>
</TouchableOpacity>
{/* 隐藏的二维码信息输入框 */}
<TextInput
ref={qrCodeInputRef}
style={styles.hiddenQrCodeInput}
value={qrCodeInfo}
onChangeText={handleQrCodeChange}
onFocus={() => setIsWaitingForQrCode(true)}
onBlur={() => setIsWaitingForQrCode(false)}
placeholder="二维码信息将在此处显示"
multiline={true}
autoFocus={isWaitingForQrCode}
showSoftInputOnFocus={false}
caretHidden={true}
editable={true}
/>
<TaskForm task={task} onTaskChange={handleTaskChange} /> <TaskForm task={task} onTaskChange={handleTaskChange} />
{runResponse && ( {runResponse && (
<View style={styles.responseContainer}> <View style={styles.responseContainer}>
@ -170,6 +381,39 @@ const styles = StyleSheet.create({
padding: 16, padding: 16,
paddingBottom: 80, paddingBottom: 80,
}, },
qrCodeButton: {
padding: 12,
borderRadius: 8,
marginBottom: 16,
alignItems: 'center',
},
qrCodeButtonWaiting: {
backgroundColor: '#2196F3',
},
qrCodeButtonReady: {
backgroundColor: '#4CAF50',
},
qrCodeButtonContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
},
qrCodeButtonIcon: {
marginRight: 8,
},
qrCodeButtonText: {
color: '#ffffff',
fontSize: 16,
fontWeight: 'bold',
},
hiddenQrCodeInput: {
position: 'absolute',
top: -1000,
left: -1000,
width: 1,
height: 1,
opacity: 0,
},
responseContainer: { responseContainer: {
marginTop: 20, marginTop: 20,
padding: 15, padding: 15,