import React, { useState, useCallback, useImperativeHandle, forwardRef, useRef } from 'react' import { View, TextInput, Pressable, StyleSheet, Platform, ActivityIndicator, ScrollView, KeyboardAvoidingView, } from 'react-native' import { Image } from 'expo-image' import { useTranslation } from 'react-i18next' import { Button } from './ui/button' import Text from './ui/Text' import { uploadFile } from '@/lib/uploadFile' import Toast from './ui/Toast' export type NodeType = 'text' | 'image' | 'video' | 'select' export interface SelectOption { label: string value: string } export interface StartNode { id: string type: NodeType data?: { text?: string label?: string description?: string usage?: { unit: string price: number price_desc: string } output?: { texts?: string[] images?: Array<{ url: string }> selections?: string videos?: Array<{ url: string }> usage?: { unit: string price: number price_desc: string } } actionData?: { options?: SelectOption[] required?: boolean placeholder?: string allowMultiple?: boolean prompt?: string duration?: string resolution?: string aspectRatio?: string selectedModel?: any advancedParams?: any } autoPlay?: boolean controls?: boolean flowType?: string } } export interface FormSchema { startNodes?: StartNode[] } export interface DynamicFormRef { updateFieldValue: (nodeId: string, value: string, previewUri?: string) => void } interface DynamicFormProps { formSchema: FormSchema onSubmit: (data: Record) => Promise<{ generationId?: string; error?: any }> loading?: boolean onOpenDrawer?: (nodeId: string) => void points?: number } export const DynamicForm = forwardRef( function DynamicForm({ formSchema, onSubmit, loading = false, onOpenDrawer, points }, ref) { const { t } = useTranslation() const startNodes = formSchema.startNodes || [] // Initialize form state for each node const [formData, setFormData] = useState>(() => { const initialData: Record = {} startNodes.forEach((node) => { if (node.type === 'text') { // Get default value from data.text or output.texts[0] const texts = node.data?.output?.texts initialData[node.id] = texts?.[0] || node.data?.text || '' } else if (node.type === 'select') { // Get default value from output.selections initialData[node.id] = node.data?.output?.selections || '' } else if (node.type === 'image') { // Get default value from output.images const images = node.data?.output?.images initialData[node.id] = images?.[0]?.url || '' } else if (node.type === 'video') { // Get default value from output.videos const videos = node.data?.output?.videos initialData[node.id] = videos?.[0]?.url || '' } else { initialData[node.id] = '' } }) return initialData }) const [drawerVisible, setDrawerVisible] = useState(false) const [currentNodeId, setCurrentNodeId] = useState(null) const [previewImages, setPreviewImages] = useState>(() => { const initialPreviews: Record = {} startNodes.forEach((node) => { if (node.type === 'image') { const images = node.data?.output?.images if (images?.[0]?.url) { initialPreviews[node.id] = images[0].url } } else if (node.type === 'video') { const videos = node.data?.output?.videos if (videos?.[0]?.url) { initialPreviews[node.id] = videos[0].url } } }) return initialPreviews }) const [errors, setErrors] = useState>({}) const updateFormData = useCallback((nodeId: string, value: string) => { setFormData((prev) => ({ ...prev, [nodeId]: value })) // Clear error for this field when user updates it if (errors[nodeId]) { setErrors((prev) => { const newErrors = { ...prev } delete newErrors[nodeId] return newErrors }) } }, [errors]) // 暴露方法给父组件 useImperativeHandle(ref, () => ({ updateFieldValue: (nodeId: string, value: string, previewUri?: string) => { updateFormData(nodeId, value) if (previewUri) { setPreviewImages((prev) => ({ ...prev, [nodeId]: previewUri })) } }, }), [updateFormData]) const handleSelectImage = useCallback(async (imageUri: string, mimeType?: string, fileName?: string) => { if (!currentNodeId) return try { const url = await uploadFile({ uri: imageUri, mimeType, fileName }) updateFormData(currentNodeId, url) setPreviewImages((prev) => ({ ...prev, [currentNodeId]: imageUri })) } catch (error) { console.error('Upload failed:', error) Toast.show({ title: t('dynamicForm.uploadFailed') || '上传失败,请重试' }) } finally { setDrawerVisible(false) setCurrentNodeId(null) } }, [currentNodeId, t, updateFormData]) const handleSelectVideo = useCallback(async (videoUri: string, mimeType?: string, fileName?: string) => { if (!currentNodeId) return try { const url = await uploadFile({ uri: videoUri, mimeType, fileName }) updateFormData(currentNodeId, url) setPreviewImages((prev) => ({ ...prev, [currentNodeId]: videoUri })) } catch (error) { console.error('Upload failed:', error) Toast.show({ title: t('dynamicForm.uploadFailed') || '上传失败,请重试' }) } finally { setDrawerVisible(false) setCurrentNodeId(null) } }, [currentNodeId, t, updateFormData]) const validateForm = useCallback(() => { const newErrors: Record = {} startNodes.forEach((node) => { const value = formData[node.id] if (!value || value.trim() === '') { const label = node.data?.label || t('dynamicForm.field') || '字段' newErrors[node.id] = `${label}${t('dynamicForm.required') || '不能为空'}` } }) setErrors(newErrors) return Object.keys(newErrors).length === 0 }, [formData, startNodes, t]) const handleSubmit = useCallback(async () => { if (!validateForm()) { Toast.show({ title: t('dynamicForm.fillRequiredFields') || '请填写所有必填项' }) return } Toast.showLoading() try { const result = await onSubmit(formData) Toast.hideLoading() if (result.error) { Toast.show({ title: result.error.message || t('dynamicForm.submitFailed') || '提交失败' }) } } catch (error) { Toast.hideLoading() Toast.show({ title: t('dynamicForm.submitFailed') || '提交失败' }) } }, [formData, onSubmit, validateForm, t]) const renderTextField = (node: StartNode) => { const value = formData[node.id] || '' const error = errors[node.id] const placeholder = node.data?.description || t('dynamicForm.enterText') || '请输入文本' return ( {node.data?.label && ( {node.data.label} * )} updateFormData(node.id, text)} placeholder={placeholder} placeholderTextColor="#8A8A8A" multiline numberOfLines={4} textAlignVertical="top" /> {error && {error}} ) } const renderImageField = (node: StartNode) => { const value = formData[node.id] const previewUri = previewImages[node.id] const error = errors[node.id] const label = node.data?.label || t('dynamicForm.image') || '图片' return ( {label} * { setCurrentNodeId(node.id) if (onOpenDrawer) { onOpenDrawer(node.id) } else { setDrawerVisible(true) } }} > {previewUri ? ( ) : ( {t('dynamicForm.uploadImage') || '点击上传图片'} )} {error && {error}} ) } const renderVideoField = (node: StartNode) => { const value = formData[node.id] const previewUri = previewImages[node.id] const error = errors[node.id] const label = node.data?.label || t('dynamicForm.video') || '视频' return ( {label} * { setCurrentNodeId(node.id) if (onOpenDrawer) { onOpenDrawer(node.id) } else { setDrawerVisible(true) } }} > {previewUri ? ( {t('dynamicForm.videoUploaded') || '视频已上传'} ) : ( {t('dynamicForm.uploadVideo') || '点击上传视频'} )} {error && {error}} ) } const renderSelectField = (node: StartNode) => { const value = formData[node.id] const error = errors[node.id] const label = node.data?.label || t('dynamicForm.select') || '选择' const options = node.data?.actionData?.options || [] const placeholder = node.data?.actionData?.placeholder || t('dynamicForm.selectPlaceholder') || '请选择' const allowMultiple = node.data?.actionData?.allowMultiple || false return ( {label} * {allowMultiple ? ( // Multi-select: render as a list of checkboxes {options.map((option, index) => { const isSelected = value === option.value return ( updateFormData(node.id, option.value)} > {option.label} ) })} ) : ( // Single select: render as a list of radio buttons {options.map((option, index) => { const isSelected = value === option.value return ( updateFormData(node.id, option.value)} > {isSelected && } {option.label} ) })} )} {error && {error}} ) } const renderField = (node: StartNode) => { switch (node.type) { case 'text': return renderTextField(node) case 'image': return renderImageField(node) case 'video': return renderVideoField(node) case 'select': return renderSelectField(node) default: return null } } if (startNodes.length === 0) { return ( {t('dynamicForm.noFields') || '暂无可填写的表单项'} ) } return ( {startNodes.map(renderField)} ) } ) const styles = StyleSheet.create({ keyboardAvoidingView: { flex: 1, }, scrollView: { flex: 1, }, scrollContent: { padding: 16, gap: 20, }, fieldContainer: { gap: 8, }, label: { color: '#F5F5F5', fontSize: 14, fontWeight: '500', }, required: { color: '#FF6699', }, textInput: { minHeight: 100, backgroundColor: '#262A31', borderRadius: 12, padding: 12, color: '#F5F5F5', fontSize: 14, lineHeight: 20, borderWidth: 1, borderColor: 'transparent', }, textInputError: { borderColor: '#FF6699', }, uploadButton: { height: 140, backgroundColor: '#262A31', borderRadius: 12, overflow: 'hidden', alignItems: 'center', justifyContent: 'center', }, uploadPlaceholder: { alignItems: 'center', justifyContent: 'center', gap: 8, }, previewImage: { width: '100%', height: '100%', }, videoPreview: { alignItems: 'center', justifyContent: 'center', gap: 8, }, uploadText: { color: '#F5F5F5', fontSize: 14, fontWeight: '500', }, errorText: { color: '#FF6699', fontSize: 12, marginTop: 4, }, submitButtonContainer: { marginTop: 12, paddingTop: 12, }, submitButton: { width: '100%', height: 56, minHeight: 56, }, submitButtonContent: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, }, submitButtonText: { color: '#FFFFFF', fontSize: 16, fontWeight: '500', }, pointsContainer: { backgroundColor: 'rgba(255, 255, 255, 0.2)', paddingHorizontal: 8, paddingVertical: 2, borderRadius: 10, }, pointsText: { color: '#FFFFFF', fontSize: 12, fontWeight: '600', }, emptyContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 20, }, emptyText: { color: '#8A8A8A', fontSize: 14, textAlign: 'center', }, optionsContainer: { gap: 8, }, optionButton: { backgroundColor: '#262A31', borderRadius: 12, padding: 16, borderWidth: 1, borderColor: 'transparent', }, optionButtonSelected: { backgroundColor: '#FF6699', borderColor: '#FF6699', }, optionText: { color: '#F5F5F5', fontSize: 14, fontWeight: '500', }, optionTextSelected: { color: '#FFFFFF', }, radioContainer: { flexDirection: 'row', alignItems: 'center', gap: 12, }, radioCircle: { width: 20, height: 20, borderRadius: 10, borderWidth: 2, borderColor: '#8A8A8A', alignItems: 'center', justifyContent: 'center', }, radioCircleSelected: { borderColor: '#FF6699', backgroundColor: '#FF6699', }, radioDot: { width: 8, height: 8, borderRadius: 4, backgroundColor: '#FFFFFF', }, })