expo-popcore-old/components/forms/dynamic-form-field.tsx

433 lines
11 KiB
TypeScript

import { TemplateGraphNode } from '@/lib/types/template';
import { Feather } from '@expo/vector-icons';
import * as ImagePicker from 'expo-image-picker';
import { UploadCloud } from 'lucide-react';
import React from 'react';
import {
Alert,
Image,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
interface DynamicFormFieldProps {
node: TemplateGraphNode;
value: any;
onChange: (value: any) => void;
error?: string;
}
export function DynamicFormField({ node, value, onChange, error }: DynamicFormFieldProps) {
const { type, data } = node;
const { label, description, actionData } = data;
const renderField = () => {
switch (type) {
case 'select':
return renderSelectField();
case 'text':
return renderTextField();
case 'image':
return renderImageField();
case 'video':
return renderVideoField();
default:
return null;
}
};
const renderSelectField = () => {
const options = actionData?.options || [];
const allowMultiple = actionData?.allowMultiple || false;
const handleSelect = (optionValue: string) => {
if (allowMultiple) {
const currentValues = Array.isArray(value) ? value : [];
const newValues = currentValues.includes(optionValue)
? currentValues.filter((v: string) => v !== optionValue)
: [...currentValues, optionValue];
onChange(newValues);
} else {
onChange(optionValue);
}
};
const isSelected = (optionValue: string) => {
if (allowMultiple) {
return Array.isArray(value) && value.includes(optionValue);
}
return value === optionValue;
};
return (
<View style={styles.fieldContainer}>
<Text style={styles.fieldLabel}>{label}</Text>
{description && <Text style={styles.fieldDescription}>{description}</Text>}
<View style={styles.selectOptions}>
{options.map((option: any, index: number) => (
<TouchableOpacity
key={index}
style={[
styles.selectOption,
isSelected(option.value) && styles.selectOptionSelected,
error && styles.selectOptionError,
]}
onPress={() => handleSelect(option.value)}
>
<Text
style={[
styles.selectOptionText,
isSelected(option.value) && styles.selectOptionTextSelected,
]}
>
{option.label}
</Text>
{isSelected(option.value) && (
<Feather name="check" size={18} color="#050505" />
)}
</TouchableOpacity>
))}
</View>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
const renderTextField = () => {
const placeholder = actionData?.placeholder || '请输入内容';
const multiline = actionData?.multiline || false;
return (
<View style={styles.fieldContainer}>
<Text style={styles.fieldLabel}>{label}</Text>
{description && <Text style={styles.fieldDescription}>{description}</Text>}
<TextInput
style={[
styles.textInput,
multiline && styles.textInputMultiline,
error && styles.textInputError,
]}
placeholder={placeholder}
placeholderTextColor="#898A8B"
value={value || ''}
onChangeText={onChange}
multiline={multiline}
textAlignVertical={multiline ? 'top' : 'center'}
/>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
const renderImageField = () => {
const handleImagePick = async () => {
try {
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permission.granted) {
Alert.alert(
'需要权限',
'请在设置中允许访问相册以上传图片',
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
quality: 0.9,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
onChange(result.assets[0].uri);
}
} catch (error) {
console.error('Failed to pick image:', error);
Alert.alert('错误', '无法选择图片,请稍后重试');
}
};
const handleRemove = () => {
onChange(null);
};
return (
<View style={styles.fieldContainer}>
<Text style={styles.fieldLabel}>{label}</Text>
{description && <Text style={styles.fieldDescription}>{description}</Text>}
<TouchableOpacity
style={[
styles.uploadCard,
value && styles.uploadCardFilled,
error && styles.uploadCardError,
]}
onPress={handleImagePick}
activeOpacity={0.9}
>
{value ? (
<>
<Image source={{ uri: value }} style={styles.uploadImage} />
<TouchableOpacity
style={styles.removeButton}
onPress={handleRemove}
activeOpacity={0.8}
>
<Feather name="x" size={16} color="#050505" />
</TouchableOpacity>
</>
) : (
<>
<View style={styles.uploadIconWrap}>
<UploadCloud size={26} color="#D1FF00" strokeWidth={1.4} />
</View>
<Text style={styles.uploadLabel}></Text>
<Text style={styles.uploadDescription}>
{actionData?.placeholder || 'PNG, JPG 格式'}
</Text>
</>
)}
</TouchableOpacity>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
const renderVideoField = () => {
const handleVideoPick = async () => {
try {
const permission = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (!permission.granted) {
Alert.alert(
'需要权限',
'请在设置中允许访问相册以上传视频',
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Videos,
allowsEditing: true,
quality: 0.9,
});
if (!result.canceled && result.assets && result.assets.length > 0) {
onChange(result.assets[0].uri);
}
} catch (error) {
console.error('Failed to pick video:', error);
Alert.alert('错误', '无法选择视频,请稍后重试');
}
};
const handleRemove = () => {
onChange(null);
};
return (
<View style={styles.fieldContainer}>
<Text style={styles.fieldLabel}>{label}</Text>
{description && <Text style={styles.fieldDescription}>{description}</Text>}
<TouchableOpacity
style={[
styles.uploadCard,
value && styles.uploadCardFilled,
error && styles.uploadCardError,
]}
onPress={handleVideoPick}
activeOpacity={0.9}
>
{value ? (
<>
<View style={styles.videoPreview}>
<Feather name="video" size={40} color="#D1FF00" />
<Text style={styles.videoPreviewText}></Text>
</View>
<TouchableOpacity
style={styles.removeButton}
onPress={handleRemove}
activeOpacity={0.8}
>
<Feather name="x" size={16} color="#050505" />
</TouchableOpacity>
</>
) : (
<>
<View style={styles.uploadIconWrap}>
<UploadCloud size={26} color="#D1FF00" strokeWidth={1.4} />
</View>
<Text style={styles.uploadLabel}></Text>
<Text style={styles.uploadDescription}>
{actionData?.placeholder || 'MP4, MOV 格式'}
</Text>
</>
)}
</TouchableOpacity>
{error && <Text style={styles.errorText}>{error}</Text>}
</View>
);
};
return renderField();
}
const styles = StyleSheet.create({
fieldContainer: {
marginBottom: 24,
},
fieldLabel: {
fontSize: 16,
fontWeight: '700',
letterSpacing: 0.4,
color: '#FFFFFF',
marginBottom: 8,
textTransform: 'uppercase',
},
fieldDescription: {
fontSize: 13,
lineHeight: 18,
color: 'rgba(255, 255, 255, 0.6)',
marginBottom: 12,
},
selectOptions: {
gap: 12,
},
selectOption: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingVertical: 16,
borderRadius: 24,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
backgroundColor: 'rgba(17, 19, 24, 0.95)',
},
selectOptionSelected: {
backgroundColor: '#D1FF00',
borderColor: '#D1FF00',
},
selectOptionError: {
borderColor: '#FF6B6B',
},
selectOptionText: {
fontSize: 15,
fontWeight: '600',
color: '#FFFFFF',
},
selectOptionTextSelected: {
color: '#050505',
},
textInput: {
borderRadius: 24,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
backgroundColor: 'rgba(17, 19, 24, 0.95)',
paddingHorizontal: 20,
paddingVertical: 16,
fontSize: 15,
color: '#FFFFFF',
minHeight: 105,
},
textInputMultiline: {
minHeight: 140,
paddingTop: 16,
paddingBottom: 16,
},
textInputError: {
borderColor: '#FF6B6B',
},
uploadCard: {
borderRadius: 28,
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.08)',
backgroundColor: 'rgba(17, 19, 24, 0.95)',
paddingHorizontal: 20,
paddingVertical: 28,
alignItems: 'center',
justifyContent: 'center',
minHeight: 188,
overflow: 'hidden',
},
uploadCardFilled: {
paddingHorizontal: 0,
paddingVertical: 0,
},
uploadCardError: {
borderColor: '#FF6B6B',
},
uploadIconWrap: {
width: 60,
height: 60,
borderRadius: 22,
backgroundColor: 'rgba(209, 255, 0, 0.12)',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 16,
},
uploadLabel: {
fontSize: 14,
fontWeight: '700',
letterSpacing: 0.6,
color: '#FFFFFF',
textAlign: 'center',
textTransform: 'uppercase',
marginBottom: 6,
},
uploadDescription: {
fontSize: 12,
lineHeight: 16,
color: 'rgba(255, 255, 255, 0.55)',
textAlign: 'center',
},
uploadImage: {
width: '100%',
height: '100%',
resizeMode: 'cover',
},
videoPreview: {
alignItems: 'center',
justifyContent: 'center',
padding: 40,
},
videoPreviewText: {
marginTop: 12,
fontSize: 14,
color: '#D1FF00',
fontWeight: '600',
},
errorText: {
marginTop: 8,
fontSize: 12,
color: '#FF6B6B',
letterSpacing: 0.2,
},
removeButton: {
position: 'absolute',
top: 12,
right: 12,
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#D1FF00',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 4,
elevation: 5,
},
});