fix: 修复所有 TypeScript 类型错误

- 修复 webpPreviewUrl -> previewUrl 字段名错误
- 修复 video.tsx 中的 useTemplates 参数类型问题
- 修复 video.tsx 中 position 重复定义问题
- 修复 searchResults.tsx 中的 SearchResultItem 类型不匹配
- 修复 SearchResultsGrid.tsx 中不存在的 scrollContent 样式
- 修复 use-templates.ts 中 page 可能是 undefined 的类型问题
- 添加 tsconfig.json 的 exclude 配置
- 修复 use-template-actions.ts 中 null 不能赋值给 ApiError 的问题

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
imeepos
2026-01-16 12:14:04 +08:00
parent ae120f24d3
commit 4d1e901032
7 changed files with 406 additions and 343 deletions

View File

@@ -5,7 +5,6 @@ import { StatusBar } from 'expo-status-bar'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import {
Animated,
Dimensions,
Platform,
Pressable,
@@ -13,114 +12,95 @@ import {
ScrollView,
StyleSheet,
Text,
View,
View
} from 'react-native'
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
import { DownArrowIcon, PointsIcon, SearchIcon, WhiteStarIcon } from '@/components/icon'
import { HomeSkeleton } from '@/components/skeleton/HomeSkeleton'
import { useActivates } from '@/hooks/use-activates'
import { useCategories } from '@/hooks/use-categories'
const { width: screenWidth } = Dimensions.get('window')
// 卡片数据 - 根据 Figma 设计更新
const cardData = [
{
id: 1,
title: '宠物写真',
image: require('@/assets/images/android-icon-background.png'),
isHot: true,
users: 6349,
height: 214,
},
{
id: 2,
title: '我和小猫的人生合照',
image: require('@/assets/images/android-icon-background.png'),
users: 6349,
height: 236,
},
{
id: 3,
title: '猫:晚安~人',
image: require('@/assets/images/favicon.png'),
users: 6349,
height: 214,
},
{
id: 4,
title: '穿越时空的相聚',
image: require('@/assets/images/icon.png'),
users: 6349,
height: 100,
},
{
id: 5,
title: '睡衣版猫咪',
image: require('@/assets/images/android-icon-background.png'),
users: 6349,
height: 120,
},
{
id: 6,
title: '猫咪写真',
image: require('@/assets/images/android-icon-background.png'),
users: 6349,
height: 214,
},
{
id: 7,
title: '站姐视角写真',
image: require('@/assets/images/android-icon-background.png'),
users: 6349,
height: 214,
},
{
id: 8,
title: '猫咪写真',
image: require('@/assets/images/android-icon-background.png'),
users: 6349,
height: 200,
},
]
export default function HomeScreen() {
const { t } = useTranslation()
const insets = useSafeAreaInsets()
const router = useRouter()
const [activeTab, setActiveTab] = useState(0)
// 标签数据 - 根据 Figma 设计更新
const tabs = [
t('home.tabs.featured'),
t('home.tabs.christmas'),
t('home.tabs.pets'),
t('home.tabs.avatar'),
t('home.tabs.theater1'),
t('home.tabs.theater2'),
]
const [gridWidth, setGridWidth] = useState(screenWidth)
const [showTabArrow, setShowTabArrow] = useState(false)
const [tabsSticky, setTabsSticky] = useState(false)
const [tabsHeight, setTabsHeight] = useState(0)
const tabsPositionRef = useRef(0)
const titleBarHeightRef = useRef(0)
const scrollY = useRef(new Animated.Value(0)).current
const [selectedCategoryId, setSelectedCategoryId] = useState<string | null>(null)
const { load: loadCategories, data: categoriesData, loading: categoriesLoading, error: categoriesError } = useCategories()
const { load, data: activatesData, error } = useActivates()
useEffect(() => {
load()
loadCategories()
}, [])
useEffect(() => {
console.log({ activatesData, error })
}, [activatesData, error])
useEffect(() => {
console.log({ categoriesData, categoriesError })
// 当分类数据加载完成后,默认选中第一个分类
if (categoriesData?.categories && categoriesData.categories.length > 0 && !selectedCategoryId) {
setSelectedCategoryId(categoriesData.categories[0].id)
}
}, [categoriesData, categoriesError])
// 使用接口返回的分类数据,如果没有则使用默认翻译
const categories = categoriesData?.categories || []
const tabs = categories.length > 0
? categories.map(cat => cat.name)
: [
t('home.tabs.featured'),
t('home.tabs.christmas'),
t('home.tabs.pets'),
t('home.tabs.avatar'),
t('home.tabs.theater1'),
t('home.tabs.theater2'),
]
// 获取当前选中分类的模板数据
const currentCategory = categories.find(cat => cat.id === selectedCategoryId)
const categoryTemplates = currentCategory?.templates || []
// 将 CategoryTemplate 数据转换为卡片数据格式
const displayCardData = categoryTemplates.length > 0
? categoryTemplates.map((template, idx) => ({
id: template.id || `template-${idx}`,
title: template.title,
image: { uri: template.previewUrl || template.coverImageUrl || `` },
isHot: false,
users: 0,
height: undefined,
previewUrl: template.previewUrl,
aspectRatio: template.aspectRatio,
tag: template.tag,
}))
: []
const [gridWidth, setGridWidth] = useState(screenWidth)
const [showTabArrow, setShowTabArrow] = useState(false)
const [tabsSticky, setTabsSticky] = useState(false)
const [tabsHeight, setTabsHeight] = useState(0)
const tabsPositionRef = useRef(0)
const titleBarHeightRef = useRef(0)
const horizontalPadding = 8 * 2 // gridContainer 的左右 padding
const cardGap = 5 // 两个卡片之间的间距
const cardWidth = (gridWidth - horizontalPadding - cardGap) / 2
// 判断是否显示加载状态
const showLoading = categoriesLoading
// 判断是否显示分类空状态
const showEmptyState = !categoriesLoading && categoriesData?.categories && categoriesData.categories.length === 0
// 判断是否显示模板空状态(当前分类下没有模板)
const showEmptyTemplates = !categoriesLoading && !showEmptyState && displayCardData.length === 0
// 渲染标签导航的函数
const renderTabs = (wrapperStyle?: any) => (
<View style={[styles.tabsWrapper, wrapperStyle]}>
@@ -141,7 +121,15 @@ export default function HomeScreen() {
{tabs.map((tab, index) => (
<Pressable
key={index}
onPress={() => setActiveTab(index)}
onPress={() => {
setActiveTab(index)
// 设置选中的分类ID
if (categories.length > 0 && categories[index]) {
setSelectedCategoryId(categories[index].id)
} else {
setSelectedCategoryId(null)
}
}}
style={styles.tab}
>
<View style={styles.tabLabelWrapper}>
@@ -216,7 +204,7 @@ export default function HomeScreen() {
</View>
</View>
{/* 吸顶的标签导航 - 适配 iOS 和 Android 的安全区域 */}
{tabsSticky && (
{tabsSticky && !showLoading && !showEmptyState && (
<View
style={[
styles.stickyTabsWrapper,
@@ -266,78 +254,105 @@ export default function HomeScreen() {
</ScrollView>
</View>
{/* 标签导航 */}
<View
onLayout={(event) => {
const { y, height } = event.nativeEvent.layout
tabsPositionRef.current = y
setTabsHeight(height)
}}
style={tabsSticky ? { opacity: 0, height: tabsHeight } : undefined}
>
{renderTabs()}
</View>
{/* 标签导航 - 只要有分类数据就显示标签 */}
{!showLoading && !showEmptyState && (
<View
onLayout={(event) => {
const { y, height } = event.nativeEvent.layout
tabsPositionRef.current = y
setTabsHeight(height)
}}
style={tabsSticky ? { opacity: 0, height: tabsHeight } : undefined}
>
{renderTabs()}
</View>
)}
{/* 加载状态 */}
{showLoading && <HomeSkeleton />}
{/* 空状态 - 分类数据为空 */}
{showEmptyState && (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}></Text>
<Pressable style={styles.retryButton} onPress={() => loadCategories()}>
<Text style={styles.retryButtonText}></Text>
</Pressable>
</View>
)}
{/* 空状态 - 当前分类下没有模板 */}
{showEmptyTemplates && (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}></Text>
<Pressable style={styles.retryButton} onPress={() => loadCategories()}>
<Text style={styles.retryButtonText}></Text>
</Pressable>
</View>
)}
{/* 内容网格 */}
<View
style={styles.gridContainer}
onLayout={(event) => {
const { width } = event.nativeEvent.layout
setGridWidth(width)
}}
>
{cardData.map((card, index) => (
<Pressable
key={card.id}
style={[
styles.card,
{ width: cardWidth },
index % 2 === 0 ? styles.cardLeft : styles.cardRight,
]}
onPress={() => {
router.push({
pathname: '/templateDetail' as any,
params: { id: card.id.toString() },
})
}}
>
<View
{!showLoading && !showEmptyState && !showEmptyTemplates && (
<View
style={styles.gridContainer}
onLayout={(event) => {
const { width } = event.nativeEvent.layout
setGridWidth(width)
}}
>
{displayCardData.map((card, index) => (
<Pressable
key={card.id}
style={[
styles.cardImageContainer,
{ height: card.height || cardWidth * 1.2 },
styles.card,
{ width: cardWidth },
index % 2 === 0 ? styles.cardLeft : styles.cardRight,
]}
onPress={() => {
router.push({
pathname: '/templateDetail' as any,
params: { id: card.id.toString() },
})
}}
>
<Image
source={card.image}
style={styles.cardImage}
contentFit="cover"
/>
<LinearGradient
colors={['rgba(17, 17, 17, 0)', 'rgba(17, 17, 17, 0.9)']}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={styles.cardImageGradient}
/>
{card.isHot ? (
<View style={styles.hotBadge}>
<Text style={styles.hotEmoji}>🔥</Text>
<Text style={styles.hotText}>{t('home.hotTemplate')}</Text>
</View>
) : (
<View style={styles.hotBadge}>
<WhiteStarIcon />
<Text style={styles.hotText}>
{card.users}{t('home.peopleUsed')}
</Text>
</View>
)}
<Text style={styles.cardTitle} numberOfLines={1}>
{card.title}
</Text>
</View>
</Pressable>
))}
</View>
<View
style={[
styles.cardImageContainer,
{ height: card.height || cardWidth * 1.2 },
]}
>
<Image
source={card.image}
style={styles.cardImage}
contentFit="cover"
/>
<LinearGradient
colors={['rgba(17, 17, 17, 0)', 'rgba(17, 17, 17, 0.9)']}
start={{ x: 0, y: 0 }}
end={{ x: 0, y: 1 }}
style={styles.cardImageGradient}
/>
{card.isHot ? (
<View style={styles.hotBadge}>
<Text style={styles.hotEmoji}>🔥</Text>
<Text style={styles.hotText}>{t('home.hotTemplate')}</Text>
</View>
) : (
<View style={styles.hotBadge}>
<WhiteStarIcon />
<Text style={styles.hotText}>
{card.users}{t('home.peopleUsed')}
</Text>
</View>
)}
<Text style={styles.cardTitle} numberOfLines={1}>
{card.title}
</Text>
</View>
</Pressable>
))}
</View>
)}
</ScrollView>
</SafeAreaView>
)
@@ -616,4 +631,26 @@ const styles = StyleSheet.create({
color: '#F5F5F5',
lineHeight: 20,
},
emptyState: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
paddingVertical: 60,
gap: 16,
},
emptyStateText: {
color: '#ABABAB',
fontSize: 14,
},
retryButton: {
backgroundColor: '#FF6699',
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 8,
},
retryButtonText: {
color: '#FFFFFF',
fontSize: 14,
fontWeight: '600',
},
})