fix: bug
This commit is contained in:
152
app/(tabs)/__tests__/my.test.tsx
Normal file
152
app/(tabs)/__tests__/my.test.tsx
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { render, waitFor } from '@testing-library/react-native'
|
||||||
|
import My from '../my'
|
||||||
|
|
||||||
|
jest.mock('expo-router', () => ({
|
||||||
|
useRouter: () => ({
|
||||||
|
push: jest.fn(),
|
||||||
|
replace: jest.fn(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('react-i18next', () => ({
|
||||||
|
useTranslation: () => ({
|
||||||
|
t: (key: string) => key,
|
||||||
|
i18n: { language: 'zh-CN', changeLanguage: jest.fn() },
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/lib/auth', () => ({
|
||||||
|
signOut: jest.fn(),
|
||||||
|
useSession: () => ({
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id: 'test-user-id',
|
||||||
|
name: 'Test User',
|
||||||
|
username: 'testuser',
|
||||||
|
image: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
jest.mock('@/hooks/use-user-balance', () => ({
|
||||||
|
useUserBalance: () => ({
|
||||||
|
balance: 100,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mockRefetch = jest.fn()
|
||||||
|
const mockLoadMore = jest.fn()
|
||||||
|
|
||||||
|
jest.mock('@/hooks', () => ({
|
||||||
|
useTemplateGenerations: jest.fn(() => ({
|
||||||
|
generations: [],
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
loadMore: mockLoadMore,
|
||||||
|
hasMore: true,
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
|
||||||
|
describe('My Page - Pagination', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should load first page with limit 20 on mount', async () => {
|
||||||
|
render(<My />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should refresh with limit 20 on pull to refresh', async () => {
|
||||||
|
const { useTemplateGenerations } = require('@/hooks')
|
||||||
|
const mockOnRefresh = jest.fn()
|
||||||
|
|
||||||
|
useTemplateGenerations.mockReturnValue({
|
||||||
|
generations: [{ id: '1', status: 'completed' }],
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
loadMore: mockLoadMore,
|
||||||
|
hasMore: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<My />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should call loadMore when scrolling near bottom', async () => {
|
||||||
|
const { useTemplateGenerations } = require('@/hooks')
|
||||||
|
|
||||||
|
useTemplateGenerations.mockReturnValue({
|
||||||
|
generations: Array(20).fill(null).map((_, i) => ({
|
||||||
|
id: `${i}`,
|
||||||
|
status: 'completed',
|
||||||
|
resultUrl: ['url'],
|
||||||
|
})),
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
loadMore: mockLoadMore,
|
||||||
|
hasMore: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<My />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not load more when loadingMore is true', async () => {
|
||||||
|
const { useTemplateGenerations } = require('@/hooks')
|
||||||
|
|
||||||
|
useTemplateGenerations.mockReturnValue({
|
||||||
|
generations: Array(20).fill(null).map((_, i) => ({
|
||||||
|
id: `${i}`,
|
||||||
|
status: 'completed'
|
||||||
|
})),
|
||||||
|
loading: false,
|
||||||
|
loadingMore: true,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
loadMore: mockLoadMore,
|
||||||
|
hasMore: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<My />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not load more when hasMore is false', async () => {
|
||||||
|
const { useTemplateGenerations } = require('@/hooks')
|
||||||
|
|
||||||
|
useTemplateGenerations.mockReturnValue({
|
||||||
|
generations: Array(10).fill(null).map((_, i) => ({
|
||||||
|
id: `${i}`,
|
||||||
|
status: 'completed'
|
||||||
|
})),
|
||||||
|
loading: false,
|
||||||
|
loadingMore: false,
|
||||||
|
refetch: mockRefetch,
|
||||||
|
loadMore: mockLoadMore,
|
||||||
|
hasMore: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
render(<My />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockRefetch).toHaveBeenCalledWith({ page: 1, limit: 20 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,657 +0,0 @@
|
|||||||
import { FlatList } from 'react-native';
|
|
||||||
import { Image } from 'expo-image';
|
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
|
||||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
|
||||||
import { StatusBar } from 'expo-status-bar';
|
|
||||||
import { memo as ReactMemo, useEffect, useRef, useState } from 'react';
|
|
||||||
import React from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import {
|
|
||||||
Dimensions,
|
|
||||||
Platform,
|
|
||||||
Pressable,
|
|
||||||
StatusBar as RNStatusBar,
|
|
||||||
ScrollView,
|
|
||||||
StyleSheet,
|
|
||||||
Text,
|
|
||||||
View
|
|
||||||
} from 'react-native';
|
|
||||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
||||||
|
|
||||||
import { DownArrowIcon, PointsIcon, SearchIcon } from '@/components/icon';
|
|
||||||
import ErrorState from '@/components/ErrorState';
|
|
||||||
import LoadingState from '@/components/LoadingState';
|
|
||||||
import { useActivates } from '@/hooks/use-activates';
|
|
||||||
import { useCategories } from '@/hooks/use-categories';
|
|
||||||
import { CategoryTemplate } from '@repo/sdk';
|
|
||||||
|
|
||||||
const { width: screenWidth } = Dimensions.get('window')
|
|
||||||
|
|
||||||
// 卡片组件 - 使用 useCallback 缓存以优化 FlashList 性能
|
|
||||||
const Card = ReactMemo(({ card, cardWidth, t, onPress }: {
|
|
||||||
card: CategoryTemplate & { webpPreviewUrl?: string }
|
|
||||||
cardWidth: number
|
|
||||||
t: any
|
|
||||||
onPress: (id: string) => void
|
|
||||||
}) => {
|
|
||||||
// 解析 aspectRatio 字符串为数字(如 "128:128" -> 1)
|
|
||||||
const aspectRatio = card.aspectRatio?.includes(':')
|
|
||||||
? (() => {
|
|
||||||
const [w, h] = card.aspectRatio.split(':').map(Number)
|
|
||||||
return w / h
|
|
||||||
})()
|
|
||||||
: card.aspectRatio
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
style={[styles.card, { width: cardWidth }]}
|
|
||||||
onPress={() => onPress(card.id!)}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.cardImageContainer,
|
|
||||||
{ aspectRatio },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={{ uri: card.webpPreviewUrl || card.previewUrl }}
|
|
||||||
style={[
|
|
||||||
styles.cardImage,
|
|
||||||
{ aspectRatio },
|
|
||||||
]}
|
|
||||||
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}
|
|
||||||
/>
|
|
||||||
<Text style={styles.cardTitle} numberOfLines={1}>
|
|
||||||
{card.title}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
export default function HomeScreen() {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
const router = useRouter()
|
|
||||||
const params = useLocalSearchParams()
|
|
||||||
const [activeTab, setActiveTab] = useState(0)
|
|
||||||
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(() => {
|
|
||||||
// 当分类数据加载完成后,默认选中第一个分类
|
|
||||||
if (categoriesData?.categories && categoriesData.categories.length > 0 && !selectedCategoryId) {
|
|
||||||
setSelectedCategoryId(categoriesData.categories[0].id)
|
|
||||||
}
|
|
||||||
}, [categoriesData, categoriesError])
|
|
||||||
|
|
||||||
// 监听从 channels 页面传递过来的 categoryId 参数
|
|
||||||
useEffect(() => {
|
|
||||||
const categoryIdFromParams = params.categoryId as string | undefined
|
|
||||||
if (categoryIdFromParams && categoriesData?.categories) {
|
|
||||||
setSelectedCategoryId(categoryIdFromParams)
|
|
||||||
// 同时更新 activeTab 索引
|
|
||||||
const categoryIndex = categoriesData.categories.findIndex(cat => cat.id === categoryIdFromParams)
|
|
||||||
if (categoryIndex !== -1) {
|
|
||||||
setActiveTab(categoryIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [params.categoryId, categoriesData])
|
|
||||||
|
|
||||||
// 使用接口返回的分类数据,如果没有则使用默认翻译
|
|
||||||
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
|
|
||||||
.filter((template) => {
|
|
||||||
// 过滤掉视频类型的模板,只显示图片
|
|
||||||
const previewUrl = (template as any).webpPreviewUrl || template.previewUrl || template.coverImageUrl || ``
|
|
||||||
const isVideo = previewUrl.includes('.mp4') || previewUrl.includes('.mov') || previewUrl.includes('.webm')
|
|
||||||
return !isVideo
|
|
||||||
})
|
|
||||||
: []
|
|
||||||
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 numColumns = 3 // 每行显示3个卡片
|
|
||||||
const cardWidth = (gridWidth - horizontalPadding - cardGap * (numColumns - 1)) / numColumns
|
|
||||||
|
|
||||||
// 判断是否显示加载状态
|
|
||||||
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]}>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
style={styles.tabsContainer}
|
|
||||||
contentContainerStyle={[
|
|
||||||
styles.tabsContent,
|
|
||||||
showTabArrow && styles.tabsContentWithArrow,
|
|
||||||
]}
|
|
||||||
onContentSizeChange={(contentWidth) => {
|
|
||||||
const tabsContainerPadding = 16 * 2 // tabsContent 的左右 padding
|
|
||||||
const availableWidth = screenWidth - tabsContainerPadding
|
|
||||||
setShowTabArrow(contentWidth > availableWidth)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{tabs.map((tab, index) => (
|
|
||||||
<Pressable
|
|
||||||
key={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}>
|
|
||||||
{activeTab === index && (
|
|
||||||
<LinearGradient
|
|
||||||
colors={['#FF9966', '#FF6699', '#9966FF']}
|
|
||||||
start={{ x: 0, y: 0 }}
|
|
||||||
end={{ x: 1, y: 0 }}
|
|
||||||
style={styles.tabUnderline}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.tabText,
|
|
||||||
activeTab === index && styles.tabTextActive,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
{showTabArrow && (
|
|
||||||
<View style={styles.tabArrowContainer}>
|
|
||||||
<LinearGradient
|
|
||||||
colors={['#090A0B', 'rgba(9, 10, 11, 0)']}
|
|
||||||
locations={[0.38, 1.0]}
|
|
||||||
start={{ x: 1, y: 0 }}
|
|
||||||
end={{ x: 0, y: 0 }}
|
|
||||||
style={styles.tabArrowGradient}
|
|
||||||
>
|
|
||||||
<Pressable
|
|
||||||
style={styles.tabArrow}
|
|
||||||
onPress={() => router.push({
|
|
||||||
pathname: '/channels' as any,
|
|
||||||
params: selectedCategoryId ? { categoryId: selectedCategoryId } : undefined,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<DownArrowIcon />
|
|
||||||
</Pressable>
|
|
||||||
</LinearGradient>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
|
||||||
<StatusBar style="light" />
|
|
||||||
<RNStatusBar barStyle="light-content" />
|
|
||||||
{/* 标题栏 */}
|
|
||||||
<View
|
|
||||||
style={styles.titleBar}
|
|
||||||
onLayout={(event) => {
|
|
||||||
const { height } = event.nativeEvent.layout
|
|
||||||
titleBarHeightRef.current = height
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={styles.appTitle}>Popcore</Text>
|
|
||||||
<View style={styles.headerRight}>
|
|
||||||
<Pressable
|
|
||||||
style={styles.pointsContainer}
|
|
||||||
onPress={() => router.push('/membership' as any)}
|
|
||||||
>
|
|
||||||
<PointsIcon />
|
|
||||||
<Text style={styles.pointsText}>60</Text>
|
|
||||||
</Pressable>
|
|
||||||
<Pressable
|
|
||||||
style={styles.searchButton}
|
|
||||||
onPress={() => router.push('/searchTemplate')}
|
|
||||||
>
|
|
||||||
<SearchIcon />
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{/* 吸顶的标签导航 - 适配 iOS 和 Android 的安全区域 */}
|
|
||||||
{tabsSticky && !showLoading && !showEmptyState && (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.stickyTabsWrapper,
|
|
||||||
// 加上 insets.top 以适配不同设备的状态栏高度(iOS 刘海屏、Android 状态栏等)
|
|
||||||
{ top: titleBarHeightRef.current + insets.top },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{renderTabs(styles.stickyTabs)}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
<ScrollView
|
|
||||||
style={styles.scrollView}
|
|
||||||
contentContainerStyle={styles.scrollContent}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
onScroll={(event) => {
|
|
||||||
const scrollY = event.nativeEvent.contentOffset.y
|
|
||||||
if (scrollY >= tabsPositionRef.current) {
|
|
||||||
setTabsSticky(true)
|
|
||||||
} else {
|
|
||||||
setTabsSticky(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// iOS 使用 16ms (60fps),Android 使用 50ms 以获得更好的性能
|
|
||||||
scrollEventThrottle={Platform.OS === 'ios' ? 16 : 50}
|
|
||||||
>
|
|
||||||
{/* 图片区域 */}
|
|
||||||
<View style={styles.heroSection}>
|
|
||||||
<ScrollView
|
|
||||||
horizontal
|
|
||||||
pagingEnabled
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
contentContainerStyle={styles.heroSliderContent}
|
|
||||||
>
|
|
||||||
{activatesData?.activities.map((activity) => (
|
|
||||||
<Pressable key={activity.id} style={styles.heroMainSlide} onPress={() => router.push(activity.link as any)}>
|
|
||||||
<Image
|
|
||||||
source={{ uri: activity.coverUrl }}
|
|
||||||
style={styles.heroMainImage}
|
|
||||||
contentFit="cover"
|
|
||||||
/>
|
|
||||||
<View style={styles.heroTextContainer}>
|
|
||||||
<Text style={styles.heroText}>{activity.title}</Text>
|
|
||||||
<Text style={styles.heroSubtext}>{activity.desc}</Text>
|
|
||||||
</View>
|
|
||||||
</Pressable>
|
|
||||||
))}
|
|
||||||
</ScrollView>
|
|
||||||
</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 && <LoadingState />}
|
|
||||||
|
|
||||||
{/* 错误状态 - 分类加载失败 */}
|
|
||||||
{categoriesError && (
|
|
||||||
<ErrorState message="加载分类失败" onRetry={() => loadCategories()} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 空状态 - 分类数据为空 */}
|
|
||||||
{showEmptyState && !categoriesError && (
|
|
||||||
<ErrorState message="暂无分类数据" onRetry={() => loadCategories()} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 空状态 - 当前分类下没有模板 */}
|
|
||||||
{showEmptyTemplates && (
|
|
||||||
<ErrorState message="该分类暂无模板" onRetry={() => loadCategories()} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 内容网格 - 使用 FlashList 优化性能 */}
|
|
||||||
{!showLoading && !showEmptyState && !showEmptyTemplates && (
|
|
||||||
<View
|
|
||||||
style={styles.gridContainer}
|
|
||||||
onLayout={(event) => {
|
|
||||||
const { width } = event.nativeEvent.layout
|
|
||||||
setGridWidth(width)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FlatList
|
|
||||||
data={displayCardData}
|
|
||||||
renderItem={({ item, index }) => (
|
|
||||||
<Card
|
|
||||||
card={item}
|
|
||||||
cardWidth={cardWidth}
|
|
||||||
t={t}
|
|
||||||
onPress={(id) => {
|
|
||||||
router.push({
|
|
||||||
pathname: '/templateDetail' as any,
|
|
||||||
params: { id: id.toString() },
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
keyExtractor={(item) => item.id!}
|
|
||||||
numColumns={numColumns}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
scrollEnabled={false}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</SafeAreaView>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#090A0B',
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
backgroundColor: '#090A0B',
|
|
||||||
paddingTop: 8,
|
|
||||||
paddingBottom: 12,
|
|
||||||
},
|
|
||||||
statusBar: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingBottom: 8,
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
statusIcons: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 4,
|
|
||||||
},
|
|
||||||
signalBars: {
|
|
||||||
width: 18,
|
|
||||||
height: 12,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderRadius: 2,
|
|
||||||
},
|
|
||||||
wifiIcon: {
|
|
||||||
width: 16,
|
|
||||||
height: 12,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderRadius: 2,
|
|
||||||
},
|
|
||||||
batteryIcon: {
|
|
||||||
width: 24,
|
|
||||||
height: 12,
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
borderRadius: 2,
|
|
||||||
},
|
|
||||||
titleBar: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
paddingBottom: 7,
|
|
||||||
paddingTop: 19,
|
|
||||||
},
|
|
||||||
appTitle: {
|
|
||||||
color: '#F5F5F5',
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
headerRight: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
pointsContainer: {
|
|
||||||
backgroundColor: '#1C1E22',
|
|
||||||
borderRadius: 12,
|
|
||||||
paddingLeft: 8,
|
|
||||||
paddingRight: 10,
|
|
||||||
paddingVertical: 4,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
pointsText: {
|
|
||||||
color: '#FFCF00',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
searchButton: {
|
|
||||||
padding: 4,
|
|
||||||
},
|
|
||||||
scrollView: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: '#090A0B',
|
|
||||||
},
|
|
||||||
scrollContent: {
|
|
||||||
flexGrow: 1,
|
|
||||||
backgroundColor: '#090A0B',
|
|
||||||
},
|
|
||||||
heroSection: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
paddingLeft: 12,
|
|
||||||
paddingTop: 12,
|
|
||||||
marginBottom: 40,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
heroSliderContent: {
|
|
||||||
gap: 12,
|
|
||||||
},
|
|
||||||
heroMainSlide: {
|
|
||||||
width: '100%',
|
|
||||||
},
|
|
||||||
heroMainImage: {
|
|
||||||
width: 265,
|
|
||||||
height: 150,
|
|
||||||
borderRadius: 12,
|
|
||||||
},
|
|
||||||
heroTextContainer: {
|
|
||||||
paddingTop: 12,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
},
|
|
||||||
heroText: {
|
|
||||||
color: '#ABABAB',
|
|
||||||
fontSize: 12,
|
|
||||||
marginBottom: 4,
|
|
||||||
},
|
|
||||||
heroSubtext: {
|
|
||||||
color: '#F5F5F5',
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
heroSide: {
|
|
||||||
width: 120,
|
|
||||||
borderRadius: 12,
|
|
||||||
overflow: 'hidden',
|
|
||||||
},
|
|
||||||
heroSideImage: {
|
|
||||||
width: '100%',
|
|
||||||
height: 140,
|
|
||||||
},
|
|
||||||
heroSideTextContainer: {
|
|
||||||
padding: 8,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.6)',
|
|
||||||
},
|
|
||||||
heroSideText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: '500',
|
|
||||||
marginBottom: 2,
|
|
||||||
},
|
|
||||||
heroSideSubtext: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 11,
|
|
||||||
opacity: 0.9,
|
|
||||||
},
|
|
||||||
tabsWrapper: {
|
|
||||||
position: 'relative',
|
|
||||||
marginBottom: 18,
|
|
||||||
},
|
|
||||||
stickyTabsWrapper: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
zIndex: 100,
|
|
||||||
backgroundColor: '#090A0B',
|
|
||||||
},
|
|
||||||
stickyTabs: {
|
|
||||||
marginBottom: 0,
|
|
||||||
},
|
|
||||||
tabsContainer: {
|
|
||||||
marginBottom: 0,
|
|
||||||
},
|
|
||||||
tabsContent: {
|
|
||||||
paddingHorizontal: 16,
|
|
||||||
gap: 20,
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
tabsContentWithArrow: {
|
|
||||||
paddingRight: 60,
|
|
||||||
},
|
|
||||||
tab: {
|
|
||||||
paddingBottom: 4,
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
tabLabelWrapper: {
|
|
||||||
position: 'relative',
|
|
||||||
paddingBottom: 2,
|
|
||||||
alignSelf: 'flex-start',
|
|
||||||
justifyContent: 'flex-end',
|
|
||||||
},
|
|
||||||
tabText: {
|
|
||||||
color: '#FFFFFF',
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
tabTextActive: {
|
|
||||||
opacity: 1,
|
|
||||||
fontWeight: '600',
|
|
||||||
},
|
|
||||||
tabUnderline: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: 12,
|
|
||||||
},
|
|
||||||
tabArrowContainer: {
|
|
||||||
position: 'absolute',
|
|
||||||
right: 0,
|
|
||||||
top: 0,
|
|
||||||
zIndex: 10,
|
|
||||||
},
|
|
||||||
tabArrowGradient: {
|
|
||||||
paddingLeft: 30,
|
|
||||||
paddingRight: 16,
|
|
||||||
paddingVertical: 4,
|
|
||||||
},
|
|
||||||
tabArrow: {
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
},
|
|
||||||
gridContainer: {
|
|
||||||
flex: 1,
|
|
||||||
paddingHorizontal: 8,
|
|
||||||
},
|
|
||||||
card: {
|
|
||||||
marginBottom: 12,
|
|
||||||
paddingHorizontal: 5,
|
|
||||||
},
|
|
||||||
flashListContent: {
|
|
||||||
gap: 10,
|
|
||||||
},
|
|
||||||
cardImageContainer: {
|
|
||||||
width: '100%',
|
|
||||||
borderRadius: 16,
|
|
||||||
overflow: 'hidden',
|
|
||||||
position: 'relative',
|
|
||||||
},
|
|
||||||
cardImage: {
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
cardImageGradient: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
height: '33.33%',
|
|
||||||
},
|
|
||||||
hotBadge: {
|
|
||||||
position: 'absolute',
|
|
||||||
top: 8,
|
|
||||||
left: 8,
|
|
||||||
backgroundColor: '#191A1F80',
|
|
||||||
paddingHorizontal: 7,
|
|
||||||
paddingVertical: 4,
|
|
||||||
borderRadius: 100,
|
|
||||||
flexDirection: 'row',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 1,
|
|
||||||
},
|
|
||||||
hotEmoji: {
|
|
||||||
fontSize: 10,
|
|
||||||
},
|
|
||||||
hotText: {
|
|
||||||
color: '#F5F5F5',
|
|
||||||
fontSize: 11,
|
|
||||||
fontWeight: '500',
|
|
||||||
},
|
|
||||||
cardTitle: {
|
|
||||||
position: 'absolute',
|
|
||||||
bottom: 12,
|
|
||||||
left: 12,
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: '500',
|
|
||||||
color: '#F5F5F5',
|
|
||||||
lineHeight: 20,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
@@ -70,7 +70,7 @@ export default function My() {
|
|||||||
|
|
||||||
// 初始化加载作品列表
|
// 初始化加载作品列表
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch({ page: 1, limit: 50 })
|
refetch({ page: 1, limit: 20 })
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ export default function My() {
|
|||||||
// 下拉刷新处理
|
// 下拉刷新处理
|
||||||
const onRefresh = useCallback(async () => {
|
const onRefresh = useCallback(async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
await refetch({ page: 1, limit: 50 })
|
await refetch({ page: 1, limit: 20 })
|
||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
}, [refetch])
|
}, [refetch])
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user