Initial commit: expo-popcore-app
This commit is contained in:
624
app/(tabs)/index.tsx
Normal file
624
app/(tabs)/index.tsx
Normal file
@@ -0,0 +1,624 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
ScrollView,
|
||||
Pressable,
|
||||
StatusBar as RNStatusBar,
|
||||
Animated,
|
||||
Platform,
|
||||
} from 'react-native'
|
||||
import { StatusBar } from 'expo-status-bar'
|
||||
import { LinearGradient } from 'expo-linear-gradient'
|
||||
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { Image } from 'expo-image'
|
||||
import { useRouter } from 'expo-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { PointsIcon, SearchIcon, DownArrowIcon, WhiteStarIcon } from '@/components/icon'
|
||||
import { AuthForm } from '@/components/blocks/AuthForm'
|
||||
import { useActivates } from '@/hooks/use-activates'
|
||||
|
||||
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 { load, data: activatesData, error } = useActivates()
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
console.log({ activatesData, error })
|
||||
}, [activatesData, error])
|
||||
|
||||
const horizontalPadding = 8 * 2 // gridContainer 的左右 padding
|
||||
const cardGap = 5 // 两个卡片之间的间距
|
||||
const cardWidth = (gridWidth - horizontalPadding - cardGap) / 2
|
||||
|
||||
// 渲染标签导航的函数
|
||||
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)}
|
||||
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('/channels')}
|
||||
>
|
||||
<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 && (
|
||||
<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>
|
||||
|
||||
{/* 标签导航 */}
|
||||
<View
|
||||
onLayout={(event) => {
|
||||
const { y, height } = event.nativeEvent.layout
|
||||
tabsPositionRef.current = y
|
||||
setTabsHeight(height)
|
||||
}}
|
||||
style={tabsSticky ? { opacity: 0, height: tabsHeight } : undefined}
|
||||
>
|
||||
{renderTabs()}
|
||||
</View>
|
||||
|
||||
<View className='py-6'>
|
||||
<AuthForm mode='register' />
|
||||
</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
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
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: {
|
||||
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: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
paddingHorizontal: 8,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
card: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
cardLeft: {
|
||||
marginRight: 0,
|
||||
},
|
||||
cardRight: {
|
||||
marginLeft: 0,
|
||||
},
|
||||
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,
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user