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}>
{label}
<Input
value={value as string}
value={String(value || '')}
onChangeText={text => handleParamChange(param.name, text)}
placeholder={param.remark || '请输入...'}
inputContainerStyle={styles.inputContainer}

View File

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

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import {
View,
StyleSheet,
@ -6,7 +6,11 @@ import {
ScrollView,
ActivityIndicator,
Text,
TouchableOpacity,
TextInput,
Animated,
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialIcons';
import { useRoute, useNavigation } from '@react-navigation/native';
import { RouteProp } from '@react-navigation/native';
import { useTasks } from '../context/TasksContext';
@ -36,6 +40,14 @@ export default function TaskEditScreen() {
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(() => {
const loadTask = async () => {
let taskData = getTaskById(taskId);
@ -64,6 +76,121 @@ export default function TaskEditScreen() {
loadTask();
}, [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) => {
setTask(updatedTask);
if (!isModified) {
@ -71,6 +198,17 @@ export default function TaskEditScreen() {
}
};
// 处理二维码扫描按钮点击
const handleQrCodeScanButton = () => {
if (!isWaitingForQrCode) {
setIsWaitingForQrCode(true);
// 聚焦到二维码输入框 (清空操作已在useEffect中处理)
setTimeout(() => {
qrCodeInputRef.current?.focus();
}, 100);
}
};
const handleSave = () => {
if (task) {
updateTask(task);
@ -90,14 +228,25 @@ export default function TaskEditScreen() {
setRunResponse(null);
try {
const params = task.detail.inputParams.map(p => ({
name: p.name,
type: p.type,
label: p.label,
required: p.required,
defaultValue: p.defaultValue,
remark: p.remark,
}));
const params = task.detail.inputParams.map(p => {
// 获取用户设置的实际值,如果用户没有设置任何值才使用默认值
const parameter = task.parameters?.[p.name];
const hasUserInput = parameter !== undefined;
const actualValue = hasUserInput ? parameter.value : p.defaultValue;
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 = {
taskId: task.id,
@ -120,6 +269,9 @@ export default function TaskEditScreen() {
const handleUndo = () => {
setTask(originalTask);
setIsModified(false);
// 重置二维码相关状态
setQrCodeInfo('');
setIsWaitingForQrCode(true); // 重置为默认的等待扫描状态
};
if (!task) {
@ -133,6 +285,65 @@ export default function TaskEditScreen() {
return (
<View style={styles.container}>
<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} />
{runResponse && (
<View style={styles.responseContainer}>
@ -170,6 +381,39 @@ const styles = StyleSheet.create({
padding: 16,
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: {
marginTop: 20,
padding: 15,