433 lines
11 KiB
TypeScript
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,
|
|
},
|
|
});
|