370 lines
12 KiB
TypeScript
370 lines
12 KiB
TypeScript
import { useState, useRef, useMemo, useCallback, useEffect } from 'react'
|
||
import {
|
||
View,
|
||
Text,
|
||
StyleSheet,
|
||
Pressable,
|
||
Dimensions,
|
||
FlatList,
|
||
Platform,
|
||
ActivityIndicator,
|
||
} from 'react-native'
|
||
import { Image } from 'expo-image'
|
||
import { useTranslation } from 'react-i18next'
|
||
import BottomSheet, { BottomSheetView, BottomSheetBackdrop } from '@gorhom/bottom-sheet'
|
||
import { CloseIcon } from '@/components/icon'
|
||
import { loomart } from '@/lib/auth'
|
||
|
||
const { width: screenWidth } = Dimensions.get('window')
|
||
|
||
type DrawerType = 'ai-record' | 'recent' | 'project-all' | 'project-face'
|
||
|
||
interface AIGenerationRecordDrawerProps {
|
||
visible: boolean
|
||
onClose: () => void
|
||
onSelectImage?: (imageUri: string) => void
|
||
type?: DrawerType
|
||
}
|
||
|
||
interface ImageData {
|
||
id: string
|
||
uri: string
|
||
}
|
||
|
||
export default function AIGenerationRecordDrawer({
|
||
visible,
|
||
onClose,
|
||
onSelectImage,
|
||
type = 'ai-record',
|
||
}: AIGenerationRecordDrawerProps) {
|
||
const { t } = useTranslation()
|
||
const bottomSheetRef = useRef<BottomSheet>(null)
|
||
|
||
const [images, setImages] = useState<ImageData[]>([])
|
||
const [loading, setLoading] = useState(false)
|
||
const [hasMore, setHasMore] = useState(true)
|
||
const [page, setPage] = useState(1)
|
||
|
||
const snapPoints = useMemo(() => ['98%'], [])
|
||
const limit = 20
|
||
|
||
// 根据类型获取数据
|
||
const fetchData = useCallback(async (pageNum: number = 1) => {
|
||
if (loading) return
|
||
|
||
try {
|
||
setLoading(true)
|
||
|
||
switch (type) {
|
||
case 'ai-record':
|
||
// 获取已完成的 AI 生成记录
|
||
const result = await loomart.templateGeneration.list({
|
||
page: pageNum,
|
||
limit,
|
||
status: 'completed',
|
||
})
|
||
|
||
const aiImages: ImageData[] = (result.items || []).flatMap((item: any) => {
|
||
const resultUrls = item.resultUrl || []
|
||
return resultUrls.map((url: string, idx: number) => ({
|
||
id: `${item.id}-${idx}`,
|
||
uri: url,
|
||
}))
|
||
})
|
||
|
||
setImages((prev) => (pageNum === 1 ? aiImages : [...prev, ...aiImages]))
|
||
setHasMore(aiImages.length === limit)
|
||
break
|
||
|
||
case 'recent':
|
||
// 获取最近使用(按更新时间排序)
|
||
const recentResult = await loomart.templateGeneration.list({
|
||
page: pageNum,
|
||
limit,
|
||
})
|
||
|
||
const recentImages: ImageData[] = (recentResult.items || []).flatMap((item: any) => {
|
||
const resultUrls = item.resultUrl || []
|
||
return resultUrls.map((url: string, idx: number) => ({
|
||
id: `${item.id}-${idx}`,
|
||
uri: url,
|
||
}))
|
||
})
|
||
|
||
setImages((prev) => (pageNum === 1 ? recentImages : [...prev, ...recentImages]))
|
||
setHasMore(recentImages.length === limit)
|
||
break
|
||
|
||
case 'project-all':
|
||
// 获取所有项目
|
||
const projects = await loomart.project.list({
|
||
page: pageNum,
|
||
limit,
|
||
})
|
||
|
||
const projectImages: ImageData[] = (projects.items || [])
|
||
.filter((p: any) => p.resultUrl)
|
||
.map((p: any) => ({
|
||
id: p.id,
|
||
uri: p.resultUrl,
|
||
}))
|
||
|
||
setImages((prev) => (pageNum === 1 ? projectImages : [...prev, ...projectImages]))
|
||
setHasMore(projectImages.length === limit)
|
||
break
|
||
|
||
case 'project-face':
|
||
// 获取人脸标签的项目(假设标签名为 'face' 或具体标签ID)
|
||
// 如果需要先获取人脸标签ID,可以调用 loomart.tag.list() 或 loomart.category.list()
|
||
const faceProjects = await loomart.project.list({
|
||
page: pageNum,
|
||
limit,
|
||
tagIds: ['face'], // 替换为实际的人脸标签ID
|
||
})
|
||
|
||
const faceImages: ImageData[] = (faceProjects.items || [])
|
||
.filter((p: any) => p.resultUrl)
|
||
.map((p: any) => ({
|
||
id: p.id,
|
||
uri: p.resultUrl,
|
||
}))
|
||
|
||
setImages((prev) => (pageNum === 1 ? faceImages : [...prev, ...faceImages]))
|
||
setHasMore(faceImages.length === limit)
|
||
break
|
||
}
|
||
} catch (error) {
|
||
console.error('获取图片失败:', error)
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [type, loading])
|
||
|
||
// 类型改变时重置并重新获取数据
|
||
useEffect(() => {
|
||
if (visible) {
|
||
setImages([])
|
||
setPage(1)
|
||
setHasMore(true)
|
||
fetchData(1)
|
||
}
|
||
}, [visible, type])
|
||
|
||
// 滚动加载更多
|
||
const handleLoadMore = useCallback(() => {
|
||
if (!loading && hasMore) {
|
||
const nextPage = page + 1
|
||
setPage(nextPage)
|
||
fetchData(nextPage)
|
||
}
|
||
}, [loading, hasMore, page, fetchData])
|
||
|
||
useEffect(() => {
|
||
if (visible) {
|
||
bottomSheetRef.current?.expand()
|
||
} else {
|
||
bottomSheetRef.current?.close()
|
||
}
|
||
}, [visible])
|
||
|
||
const handleSheetChanges = useCallback((index: number) => {
|
||
if (index === -1) {
|
||
onClose()
|
||
}
|
||
}, [onClose])
|
||
|
||
const handleImageSelect = (imageUri: string) => {
|
||
onSelectImage?.(imageUri)
|
||
onClose()
|
||
}
|
||
|
||
const title = useMemo(() => {
|
||
switch (type) {
|
||
case 'ai-record':
|
||
return t('aiGenerationRecord.title')
|
||
case 'recent':
|
||
return t('aiGenerationRecord.recentUsed')
|
||
case 'project-all':
|
||
return t('aiGenerationRecord.projectAll')
|
||
case 'project-face':
|
||
return t('aiGenerationRecord.projectFace')
|
||
default:
|
||
return ''
|
||
}
|
||
}, [type, t])
|
||
|
||
const renderBackdrop = useCallback(
|
||
(props: any) => (
|
||
<BottomSheetBackdrop
|
||
{...props}
|
||
disappearsOnIndex={-1}
|
||
appearsOnIndex={0}
|
||
opacity={0.5}
|
||
/>
|
||
),
|
||
[]
|
||
)
|
||
|
||
const renderImageItem = ({ item, index }: { item: ImageData; index: number }) => {
|
||
const gap = 2
|
||
const itemWidth = (screenWidth - gap * 2) / 3
|
||
|
||
return (
|
||
<Pressable
|
||
style={[
|
||
styles.imageItem,
|
||
{
|
||
width: itemWidth,
|
||
marginRight: (index + 1) % 3 !== 0 ? gap : 0,
|
||
marginBottom: gap,
|
||
},
|
||
]}
|
||
onPress={() => handleImageSelect(item.uri)}
|
||
android_ripple={{ color: 'rgba(255, 255, 255, 0.1)' }}
|
||
>
|
||
<Image source={{ uri: item.uri }} style={styles.image} contentFit="cover" />
|
||
</Pressable>
|
||
)
|
||
}
|
||
|
||
const renderFooter = () => {
|
||
if (!loading) return null
|
||
return (
|
||
<View style={styles.footerLoader}>
|
||
<ActivityIndicator size="small" color="#666666" />
|
||
</View>
|
||
)
|
||
}
|
||
|
||
const renderEmpty = () => {
|
||
if (loading) return null
|
||
return (
|
||
<View style={styles.emptyContainer}>
|
||
<Text style={styles.emptyText}>{t('common.noData')}</Text>
|
||
</View>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<BottomSheet
|
||
ref={bottomSheetRef}
|
||
index={visible ? 0 : -1}
|
||
snapPoints={snapPoints}
|
||
onChange={handleSheetChanges}
|
||
enablePanDownToClose
|
||
backgroundStyle={styles.bottomSheetBackground}
|
||
handleIndicatorStyle={styles.handleIndicator}
|
||
backdropComponent={renderBackdrop}
|
||
>
|
||
<BottomSheetView style={styles.container}>
|
||
{/* 顶部标题栏 */}
|
||
<View style={styles.header}>
|
||
<Text style={styles.title}>{title}</Text>
|
||
<Pressable
|
||
style={styles.closeButton}
|
||
onPress={onClose}
|
||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||
>
|
||
<CloseIcon />
|
||
</Pressable>
|
||
</View>
|
||
|
||
{/* 图片网格 */}
|
||
<FlatList
|
||
data={images}
|
||
renderItem={renderImageItem}
|
||
keyExtractor={(item) => item.id.toString()}
|
||
numColumns={3}
|
||
contentContainerStyle={styles.imageGrid}
|
||
showsVerticalScrollIndicator={false}
|
||
onEndReached={handleLoadMore}
|
||
onEndReachedThreshold={0.5}
|
||
ListFooterComponent={renderFooter}
|
||
ListEmptyComponent={renderEmpty}
|
||
removeClippedSubviews={Platform.OS === 'android'}
|
||
maxToRenderPerBatch={Platform.OS === 'ios' ? 10 : 5}
|
||
updateCellsBatchingPeriod={Platform.OS === 'ios' ? 50 : 100}
|
||
initialNumToRender={Platform.OS === 'ios' ? 15 : 10}
|
||
windowSize={Platform.OS === 'ios' ? 10 : 5}
|
||
getItemLayout={(data, index) => {
|
||
const gap = 2
|
||
const itemWidth = (screenWidth - gap * 2) / 3
|
||
const rowIndex = Math.floor(index / 3)
|
||
return {
|
||
length: itemWidth,
|
||
offset: rowIndex * (itemWidth + gap),
|
||
index,
|
||
}
|
||
}}
|
||
/>
|
||
</BottomSheetView>
|
||
</BottomSheet>
|
||
)
|
||
}
|
||
|
||
const styles = StyleSheet.create({
|
||
bottomSheetBackground: {
|
||
backgroundColor: '#16181B',
|
||
},
|
||
handleIndicator: {
|
||
backgroundColor: '#666666',
|
||
},
|
||
container: {
|
||
flex: 1,
|
||
backgroundColor: '#16181B',
|
||
paddingTop: 12,
|
||
},
|
||
header: {
|
||
flexDirection: 'row',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingHorizontal: 16,
|
||
paddingBottom: 12,
|
||
position: 'relative',
|
||
},
|
||
title: {
|
||
color: '#F5F5F5',
|
||
fontSize: 15,
|
||
fontWeight: '600',
|
||
},
|
||
closeButton: {
|
||
position: 'absolute',
|
||
right: 16,
|
||
width: 24,
|
||
height: 24,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 10,
|
||
},
|
||
imageGrid: {
|
||
paddingHorizontal: 0,
|
||
paddingBottom: Platform.OS === 'ios' ? 20 : 16,
|
||
},
|
||
imageItem: {
|
||
// aspectRatio = width / height
|
||
// 1 : 1.3 (width : height) => 1 / 1.3
|
||
aspectRatio: 1 / 1.3,
|
||
overflow: 'hidden',
|
||
backgroundColor: '#262A31',
|
||
},
|
||
image: {
|
||
width: '100%',
|
||
height: '100%',
|
||
},
|
||
footerLoader: {
|
||
paddingVertical: 20,
|
||
alignItems: 'center',
|
||
},
|
||
emptyContainer: {
|
||
flex: 1,
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
paddingTop: 100,
|
||
},
|
||
emptyText: {
|
||
color: '#666666',
|
||
fontSize: 14,
|
||
},
|
||
})
|
||
|