import { FontAwesome, Fontisto, Ionicons } from '@expo/vector-icons' import { FlashList } from '@shopify/flash-list' import { useFocusEffect, useRouter } from 'expo-router' import { observer } from 'mobx-react-lite' import React, { memo, useCallback, useEffect, useMemo, useState } from 'react' import { ActivityIndicator, RefreshControl } from 'react-native' import { useAnimatedStyle } from 'react-native-reanimated' import { useSafeAreaInsets } from 'react-native-safe-area-context' import { imgPicker } from '@/@share/apis/imgPicker' import { Block, ConfirmModal, Img, Input, ListEmpty, Text, Toast, VideoBox } from '@/@share/components' import BannerSection from '@/components/BannerSection' import { useTemplateActions } from '@/hooks/actions/use-template-actions' import { useTemplates } from '@/hooks/data/use-templates' import { userBalanceStore, userStore } from '@/stores' import { screenWidth, uploadFile } from '@/utils' const CATEGORY_ID = process.env.EXPO_PUBLIC_GENERATE_GROUP_ID type Template = { id: string name: string image: string videoUrl: string type: 'video' price?: number data?: any } /** ========================= * Entry page * ========================= */ const Generate = observer(function Generate() { const router = useRouter() const { user, isAuthenticated } = userStore const [prompt, setPrompt] = useState(``) const [selectedTemplateId, setSelectedTemplateId] = useState('') const [meImg, setMeImg] = useState({ uri: '', url: '' }) const [friendImg, setFriendImg] = useState({ uri: '', url: '' }) const templates = useTemplates() const { runTemplate } = useTemplateActions() useFocusEffect(() => { if (!isAuthenticated) { router.replace('/') router.push('/auth') } }) useEffect(() => { templates.execute({ categoryId: CATEGORY_ID, page: 1, limit: 12, sortBy: 'createdAt', sortOrder: 'desc' }) }, []) const displayTemplates = useMemo(() => { const regular = templates.data?.templates || [] const all = regular return all.map((t: any): Template => { return { id: t.id, name: t.title, image: t.coverImageUrl, videoUrl: t.previewUrl, type: 'video' as const, price: t.price, data: { startNodes: t?.formSchema?.startNodes || [], }, } }) }, [templates.data]) const selectedTemplate = useMemo(() => { return displayTemplates.find((t) => t.id === selectedTemplateId) }, [displayTemplates, selectedTemplateId]) useEffect(() => { if (displayTemplates.length > 0 && !selectedTemplateId) { setSelectedTemplateId(displayTemplates[0].id) } }, [displayTemplates, selectedTemplateId]) const handleSearch = useCallback(() => { router.push('/') }, [router]) const handleGenerate = async () => { if (!selectedTemplate) return if (!isAuthenticated) { Toast.show({ title: '请先登录' }) return } if (!selectedTemplate) { Toast.show({ title: '请先选择一个模板' }) return } // 显示加载状态并刷新余额 Toast.showLoading() try { await userBalanceStore.load(true) // 生成前刷新余额 // 使用最新的余额数据进行检查 const currentBalance = userBalanceStore.balance if (currentBalance < selectedTemplate?.price) { Toast.show({ title: '余额不足,请充值' }) return } } catch (error) { Toast.show({ title: '余额加载失败,请重试' }) return } finally { Toast.hideLoading() } Toast.showModal( 生成将消耗 {selectedTemplate.price} } onCancel={Toast.hideModal} onConfirm={handleConfirmGenerate} />, ) } const handleConfirmGenerate = async () => { Toast.hideModal() Toast.showLoading() console.log('meImg.url-------', meImg) const data = {} as any const startNodes = selectedTemplate?.data?.startNodes || [] startNodes.map((node: any) => { if (node.type === 'text') { data[node.id] = prompt } else if (node.type === 'image') { data[node.id] = meImg.url } }) console.log('data==========', data) try { // 先进行乐观更新,扣减余额 userBalanceStore.deductBalance(selectedTemplate?.price as number) const { generationId, error } = await runTemplate({ templateId: selectedTemplate?.id as string, data: data, }) if (generationId && user?.id) { // 生成成功后强制刷新余额以获取准确数据 await userBalanceStore.load(true) Toast.show({ title: '生成任务开启,请在我的生成中查看' }) } else { // 生成失败,恢复余额 userBalanceStore.setBalance(userBalanceStore.balance + selectedItem.price) Toast.show({ title: error?.message || '生成失败' }) } } catch (error) { // 异常情况下恢复余额 userBalanceStore.setBalance(userBalanceStore.balance + selectedItem.price) Toast.show({ title: '网络异常,请重试' }) } finally { Toast.hideLoading() } } const pickImage = useCallback(async (target: 'me' | 'friend') => { const assetList = (await imgPicker({ maxImages: 1, resultType: 'asset' })) as string[] const result = assetList[0] as any if (!result) return const uri = result?.uri if (target === 'me') setMeImg({ uri, url: '' }) else setFriendImg({ uri, url: '' }) const url = await uploadFile({ uri: result.uri, mimeType: result.mimeType, fileName: result.fileName, }) console.log('handlePick------------', url) // console.log('pickImage---------url:', url, 'error:', error) if (target === 'me') { setMeImg((state) => { return { uri: state.uri, url } }) } else { setFriendImg((state) => { return { uri: state.uri, url } }) } }, []) const handleRandom = useCallback(() => { if (displayTemplates.length === 0) return const random = displayTemplates[Math.floor(Math.random() * displayTemplates.length)] setSelectedTemplateId(random.id) }, [displayTemplates]) const handleSelectTemplate = useCallback((t: Template) => { setSelectedTemplateId(t.id) }, []) const onPickMe = useCallback(() => pickImage('me'), [pickImage]) const onPickFriend = useCallback(() => pickImage('friend'), [pickImage]) const isLoading = templates.loading const isLoadingMore = templates.loadingMore const hasError = templates.error const handleRetry = useCallback(() => { templates.refetch({ categoryId: CATEGORY_ID, page: 1, limit: 20, sortBy: 'createdAt', sortOrder: 'desc' }) }, [templates]) const [refreshing, setRefreshing] = useState(false) const onRefresh = useCallback(async () => { setRefreshing(true) try { await templates.refetch({ categoryId: CATEGORY_ID, page: 1, limit: 20, sortBy: 'createdAt', sortOrder: 'desc' }) } finally { setRefreshing(false) } }, [templates]) const onLoadMore = useCallback(() => { templates.loadMore({ categoryId: CATEGORY_ID, limit: 20, sortBy: 'createdAt', sortOrder: 'desc' }) }, [templates]) const itemWidth = useMemo(() => { return Math.floor((screenWidth - 24 - 12 * 2) / 3) }, []) const renderItem = useCallback( ({ item }: { item: Template }) => { const isSelected = selectedTemplateId === item.id return ( handleSelectTemplate(item)} /> ) }, [selectedTemplateId, itemWidth, handleSelectTemplate], ) const ListHeader = useMemo(() => { const startNodes = selectedTemplate?.data?.startNodes || [] const textCount = startNodes.filter((node: any) => node?.type === 'text').length return (
{!!textCount && } ) }, [handleSearch, friendImg, meImg, onPickFriend, onPickMe, prompt, handleRandom, selectedTemplate]) const ListFooter = useMemo(() => { if (isLoadingMore) { return ( ) } return }, [isLoadingMore]) return ( item.id} ListEmptyComponent={} ListFooterComponent={ListFooter} ListHeaderComponent={ListHeader} numColumns={3} maxItemsInRecyclePool={0} renderItem={renderItem} showsVerticalScrollIndicator={false} refreshControl={ } onEndReached={onLoadMore} onEndReachedThreshold={0.3} /> ) }) export default Generate /** ========================= * Small memo components * ========================= */ type HeaderProps = { onSearch: () => void } const Header = memo(function Header({ onSearch }) { return ( {/* 创造终端 GEN_STUDIO */} 创造终端 ) }) type UploadCardProps = { variant: 'me' | 'friend' img: string onPick: () => void } const UploadCard = memo(function UploadCard({ variant, img, onPick }) { const isMe = variant === 'me' return ( {isMe ? ( ) : ( 朋友 )} {img ? ( ) : ( {isMe ? ( + ) : ( + )} 点击上传 )} ) }) type UploadSectionProps = { meImg: string friendImg: string onPickMe: () => void onPickFriend: () => void selectedTemplate: Template | undefined } const UploadSection = memo(function UploadSection({ meImg, friendImg, onPickMe, onPickFriend, selectedTemplate, }) { const imageCount = useMemo(() => { if (!selectedTemplate) return true const startNodes = selectedTemplate?.data?.startNodes || [] const imageNodeCount = startNodes.filter((node: any) => node?.type === 'image').length return imageNodeCount }, [selectedTemplate]) if (imageCount === 0) return null return ( {imageCount >= 1 && } {imageCount >= 2 && } ) }) type PromptSectionProps = { prompt: string editable?: boolean onChangePrompt: (v: string) => void } const PromptSection = memo(function PromptSection({ prompt, onChangePrompt, editable = true }) { return ( 提示词 ) }) type TemplateItemProps = { item: Template itemWidth: number isSelected: boolean onSelect: () => void } const TemplateItem = memo(function TemplateItem({ item, itemWidth, isSelected, onSelect }) { return ( {/* */} {isSelected && } {isSelected && } {item.name} {item.type === 'video' && ( )} ) }) type TemplateSectionHeaderProps = { onRandom: () => void } const TemplateSectionHeader = memo(function TemplateSectionHeader({ onRandom }) { const style = useAnimatedStyle(() => ({ transform: [{ skewX: '-6deg' }], backgroundColor: '#FFE500', })) return ( 视频模版 换一波 ) }) type GenerateSectionProps = { selectedTemplate: Template | undefined onGenerate: () => void } const GenerateSection = memo(function GenerateSection({ selectedTemplate, onGenerate }) { const insets = useSafeAreaInsets() if (!selectedTemplate) return null const price = selectedTemplate.price || selectedTemplate.data?.price || 0 return ( {/* 左侧文字 */} 立即生成 开始创作 {/* 右侧 标签 */} {price} ) })