feat: 重构测试结构并添加动态表单组件
主要更改: - 重构测试文件结构,删除旧的测试文件并添加新的测试覆盖 - 添加 DynamicForm 组件及其测试 - 更新 Jest 配置以支持新的测试结构 - 更新组件、抽屉和国际化文件 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,34 +7,29 @@ import {
|
||||
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'
|
||||
type DrawerType = 'ai-record' | 'recent' | 'project-all' | 'project-face'
|
||||
|
||||
interface AIGenerationRecordDrawerProps {
|
||||
visible: boolean
|
||||
onClose: () => void
|
||||
onSelectImage?: (imageUri: any) => void
|
||||
onSelectImage?: (imageUri: string) => void
|
||||
type?: DrawerType
|
||||
}
|
||||
|
||||
// 模拟 AI 生成记录图片数据
|
||||
const mockAIRecordImages = Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
uri: require('@/assets/images/android-icon-background.png'),
|
||||
}))
|
||||
|
||||
// 模拟最近用过的图片数据
|
||||
const mockRecentImages = Array.from({ length: 12 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
uri: require('@/assets/images/membership.png'),
|
||||
}))
|
||||
interface ImageData {
|
||||
id: string
|
||||
uri: string
|
||||
}
|
||||
|
||||
export default function AIGenerationRecordDrawer({
|
||||
visible,
|
||||
@@ -44,9 +39,126 @@ export default function AIGenerationRecordDrawer({
|
||||
}: 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()
|
||||
@@ -54,20 +166,32 @@ export default function AIGenerationRecordDrawer({
|
||||
bottomSheetRef.current?.close()
|
||||
}
|
||||
}, [visible])
|
||||
|
||||
|
||||
const handleSheetChanges = useCallback((index: number) => {
|
||||
if (index === -1) {
|
||||
onClose()
|
||||
}
|
||||
}, [onClose])
|
||||
|
||||
const handleImageSelect = (imageSource: any) => {
|
||||
onSelectImage?.(imageSource)
|
||||
|
||||
const handleImageSelect = (imageUri: string) => {
|
||||
onSelectImage?.(imageUri)
|
||||
onClose()
|
||||
}
|
||||
|
||||
const title = type === 'ai-record' ? t('aiGenerationRecord.title') : t('aiGenerationRecord.recentUsed')
|
||||
const images = type === 'ai-record' ? mockAIRecordImages : mockRecentImages
|
||||
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) => (
|
||||
@@ -81,10 +205,10 @@ export default function AIGenerationRecordDrawer({
|
||||
[]
|
||||
)
|
||||
|
||||
const renderImageItem = ({ item, index }: { item: typeof mockAIRecordImages[0]; index: number }) => {
|
||||
const renderImageItem = ({ item, index }: { item: ImageData; index: number }) => {
|
||||
const gap = 2
|
||||
const itemWidth = (screenWidth - gap * 2) / 3
|
||||
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
style={[
|
||||
@@ -98,15 +222,29 @@ export default function AIGenerationRecordDrawer({
|
||||
onPress={() => handleImageSelect(item.uri)}
|
||||
android_ripple={{ color: 'rgba(255, 255, 255, 0.1)' }}
|
||||
>
|
||||
<Image
|
||||
source={item.uri}
|
||||
style={styles.image}
|
||||
contentFit="cover"
|
||||
/>
|
||||
<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}
|
||||
@@ -139,6 +277,10 @@ export default function AIGenerationRecordDrawer({
|
||||
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}
|
||||
@@ -209,5 +351,19 @@ const styles = StyleSheet.create({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
footerLoader: {
|
||||
paddingVertical: 20,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyContainer: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
paddingTop: 100,
|
||||
},
|
||||
emptyText: {
|
||||
color: '#666666',
|
||||
fontSize: 14,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useRef, useMemo, useCallback, useEffect } from 'react'
|
||||
import React, { useState, useRef, useMemo, useCallback, useEffect } from 'react'
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -20,9 +20,9 @@ interface UploadReferenceImageDrawerProps {
|
||||
onSelectImage?: (imageUri: any) => void
|
||||
}
|
||||
|
||||
type TabType = 'ai-record' | 'recent'
|
||||
type TabType = 'ai-record' | 'recent' | 'project'
|
||||
|
||||
// 模拟图片数据
|
||||
// 模拟图片数据(保留用于兼容)
|
||||
const mockImages = Array.from({ length: 120 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
uri: require('@/assets/images/android-icon-background.png'),
|
||||
@@ -158,6 +158,20 @@ export default function UploadReferenceImageDrawer({
|
||||
{t('uploadReference.recentUsed')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
style={[styles.tab, activeTab === 'project' && styles.tabActive]}
|
||||
onPress={() => {
|
||||
setActiveTab('project')
|
||||
setAiRecordDrawerVisible(true)
|
||||
}}
|
||||
>
|
||||
<View style={styles.tabIconContainer}>
|
||||
<View style={styles.tabIconFolder} />
|
||||
</View>
|
||||
<Text style={[styles.tabText, activeTab === 'project' && styles.tabTextActive]}>
|
||||
{t('uploadReference.recentProject')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{/* 筛选区域 */}
|
||||
@@ -238,7 +252,13 @@ export default function UploadReferenceImageDrawer({
|
||||
onSelectImage={(imageUri) => {
|
||||
handleImageSelect(imageUri)
|
||||
}}
|
||||
type={activeTab}
|
||||
type={
|
||||
activeTab === 'project'
|
||||
? selectedFilter === 'all'
|
||||
? 'project-all'
|
||||
: 'project-face'
|
||||
: activeTab
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
@@ -311,6 +331,12 @@ const styles = StyleSheet.create({
|
||||
borderRadius: 6,
|
||||
backgroundColor: '#4A4C4F',
|
||||
},
|
||||
tabIconFolder: {
|
||||
width: 24,
|
||||
height: 24,
|
||||
borderRadius: 4,
|
||||
backgroundColor: '#4A4C4F',
|
||||
},
|
||||
tabText: {
|
||||
color: '#F5F5F5',
|
||||
fontSize: 12,
|
||||
|
||||
Reference in New Issue
Block a user