feat: 重构测试结构并添加动态表单组件

主要更改:
- 重构测试文件结构,删除旧的测试文件并添加新的测试覆盖
- 添加 DynamicForm 组件及其测试
- 更新 Jest 配置以支持新的测试结构
- 更新组件、抽屉和国际化文件

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
imeepos
2026-01-19 14:31:48 +08:00
parent 6e85460b2e
commit 1fc79d29ed
23 changed files with 3425 additions and 1232 deletions

View File

@@ -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,
},
})

View File

@@ -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,