expo-popcore-app/components/drawer/PointsDrawer.tsx

449 lines
14 KiB
TypeScript

import { useState, useRef, useMemo, useCallback, useEffect } from 'react'
import {
View,
Text,
StyleSheet,
ScrollView,
Pressable,
useWindowDimensions,
} from 'react-native'
import { LinearGradient } from 'expo-linear-gradient'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import { useTranslation } from 'react-i18next'
import BottomSheet, { BottomSheetView, BottomSheetBackdrop, BottomSheetScrollView } from '@gorhom/bottom-sheet'
import { CloseIcon } from '@/components/icon'
import TopUpDrawer, { TopUpOption } from '@/components/drawer/TopUpDrawer'
export type PointsTabType = 'all' | 'consume' | 'obtain'
export interface PointsTransaction {
id: string
title: string
date: string
points: number // 正数表示获得,负数表示消耗
}
export interface PointsDrawerProps {
/**
* 是否显示抽屉
*/
visible: boolean
/**
* 关闭回调
*/
onClose: () => void
/**
* 当前积分总额
*/
totalPoints?: number
/**
* 订阅积分
*/
subscriptionPoints?: number
/**
* 额外充值积分
*/
topUpPoints?: number
/**
* 交易记录列表
*/
transactions?: PointsTransaction[]
}
export default function PointsDrawer({
visible,
onClose,
totalPoints = 60,
subscriptionPoints = 0,
topUpPoints = 0,
transactions = [],
}: PointsDrawerProps) {
const { t } = useTranslation()
const { height: screenHeight } = useWindowDimensions()
const insets = useSafeAreaInsets()
const bottomSheetRef = useRef<BottomSheet>(null)
const [pointsTab, setPointsTab] = useState<PointsTabType>('all')
const [topUpDrawerVisible, setTopUpDrawerVisible] = useState(false)
const snapPoints = useMemo(() => [screenHeight * 0.85], [screenHeight])
useEffect(() => {
if (visible) {
bottomSheetRef.current?.expand()
} else {
bottomSheetRef.current?.close()
}
}, [visible])
const handleSheetChanges = useCallback((index: number) => {
if (index === -1) {
onClose()
}
}, [onClose])
const renderBackdrop = useCallback(
(props: any) => (
<BottomSheetBackdrop
{...props}
disappearsOnIndex={-1}
appearsOnIndex={0}
opacity={0.5}
/>
),
[]
)
// 标签页配置
const tabOptions: Array<{ value: PointsTabType; label: string }> = [
{ value: 'all', label: t('pointsDrawer.all') },
{ value: 'consume', label: t('pointsDrawer.consume') },
{ value: 'obtain', label: t('pointsDrawer.obtain') },
]
// 根据标签页过滤交易记录
const filteredTransactions = transactions.filter((transaction) => {
if (pointsTab === 'all') return true
if (pointsTab === 'consume') return transaction.points < 0
if (pointsTab === 'obtain') return transaction.points > 0
return true
})
// 如果没有提供交易记录,使用示例数据
const displayTransactions =
filteredTransactions.length > 0
? filteredTransactions
: [
{
id: '1',
title: t('pointsDrawer.dailyFreePoints'),
date: '2025年11月28日 10:33',
points: 60,
},
...Array.from({ length: 60 }, (_, i) => ({
id: `example-${i + 2}`,
title: t('pointsDrawer.dailyFreePoints'),
date: '2025年11月28日 10:33',
points: -60,
})),
]
return (
<BottomSheet
ref={bottomSheetRef}
index={visible ? 0 : -1}
snapPoints={snapPoints}
onChange={handleSheetChanges}
enablePanDownToClose
backgroundStyle={styles.bottomSheetBackground}
handleComponent={null}
backdropComponent={renderBackdrop}
>
<BottomSheetView style={styles.container}>
{/* 顶部标题栏 */}
<View style={styles.header}>
<Text></Text>
<Pressable
style={styles.closeButton}
onPress={onClose}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
<CloseIcon />
</Pressable>
</View>
<View style={styles.titleContainer}>
<Text style={styles.title}>{t('pointsDrawer.title')}</Text>
{/* 积分总额 */}
<View style={styles.balance}>
<Text style={styles.balanceValue}>{totalPoints}</Text>
</View>
{/* 积分类型细分 */}
<View style={styles.breakdown}>
<Text style={styles.breakdownText}>{t('pointsDrawer.subscriptionPoints')}
<Text style={styles.breakdownTextValue}>{subscriptionPoints}</Text>
</Text>
<View style={styles.breakdownTextSeparator} />
<Text style={styles.breakdownText}>{t('pointsDrawer.topUpPoints')}
<Text style={styles.breakdownTextValue}>{topUpPoints}</Text>
</Text>
</View>
</View>
{/* 标签页 */}
<View style={styles.tabs}>
{tabOptions.map((tab) => {
const isActive = pointsTab === tab.value
return (
<Pressable
key={tab.value}
style={[styles.tab]}
onPress={() => setPointsTab(tab.value)}
>
<View style={styles.tabContent}>
{isActive && (
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.tabGradient}
/>
)}
<Text style={[styles.tabText, isActive && styles.tabTextActive]}>{tab.label}</Text>
</View>
</Pressable>
)
})}
</View>
{/* 交易历史列表 */}
<BottomSheetScrollView
style={styles.list}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
>
{displayTransactions.map((transaction) => (
<View key={transaction.id} style={styles.item}>
<View style={styles.itemLeft}>
<Text style={styles.itemTitle}>{transaction.title}</Text>
<Text style={styles.itemDate}>{transaction.date}</Text>
</View>
<Text
style={[
styles.itemPoints,
transaction.points < 0 && styles.itemPointsNegative,
]}
>
{transaction.points > 0 ? '+' : ''}
{transaction.points}
</Text>
</View>
))}
</BottomSheetScrollView>
{/* 底部按钮 */}
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, 16) }]}>
<Pressable
style={styles.subscribeButton}
onPress={() => {
onClose()
}}
>
<LinearGradient
colors={['#FF9966', '#FF6699', '#9966FF']}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={styles.subscribeButtonGradient}
>
<Text style={styles.subscribeButtonText}>{t('pointsDrawer.subscribeForPoints')}</Text>
</LinearGradient>
</Pressable>
<Pressable
style={styles.topUpButton}
onPress={() => {
setTopUpDrawerVisible(true)
}}
>
<Text style={styles.topUpButtonText}>{t('pointsDrawer.topUpPointsButton')}</Text>
</Pressable>
</View>
</BottomSheetView>
{/* 充值抽屉 */}
<TopUpDrawer
visible={topUpDrawerVisible}
onClose={() => setTopUpDrawerVisible(false)}
onNavigate={() => {
setTopUpDrawerVisible(false)
onClose()
}}
requiredPoints={100}
remainingPoints={totalPoints}
topUpTitle={t('topUp.title')}
onConfirm={(option: TopUpOption) => {
// 处理充值确认逻辑
console.log('确认充值:', option)
setTopUpDrawerVisible(false)
}}
/>
</BottomSheet>
)
}
const styles = StyleSheet.create({
bottomSheetBackground: {
backgroundColor: '#090A0B',
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
},
handleIndicator: {
backgroundColor: '#666666',
},
container: {
flex: 1,
backgroundColor: '#090A0B',
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
overflow: 'hidden',
},
header: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingTop: 12,
marginRight: 12,
},
titleContainer: {
paddingLeft: 20,
marginTop: -4,
borderBottomWidth: 1,
borderBottomColor: '#3A3A3A',
},
title: {
color: '#F5F5F5',
fontSize: 12,
fontWeight: '500',
},
closeButton: {
width: 24,
height: 24,
alignItems: 'center',
justifyContent: 'center',
},
balance: {
marginBottom: 13,
},
balanceValue: {
color: '#F5F5F5',
fontSize: 40,
fontWeight: '500',
},
breakdown: {
flexDirection: 'row',
gap: 16,
marginBottom: 24,
},
breakdownText: {
color: '#ABABAB',
fontSize: 12,
fontWeight: '400',
},
breakdownTextValue: {
color: '#F5F5F5',
fontSize: 12,
fontWeight: '500',
marginLeft: 6,
},
breakdownTextSeparator: {
width: 1,
height: 14,
backgroundColor: '#3A3A3A',
marginTop: 2,
},
tabs: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
marginTop:20,
marginBottom:24,
},
tab: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
tabContent: {
position: 'relative',
alignSelf: 'center',
},
tabGradient: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: 10,
backgroundColor: '#FF9966',
},
tabText: {
color: '#ABABAB',
fontSize: 14,
textAlign: 'center',
},
tabTextActive: {
color: '#F5F5F5',
fontSize: 14,
textAlign: 'center',
},
list: {
height: 400,
},
listContent: {
paddingBottom: 16,
paddingHorizontal: 16,
},
item: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 16,
borderBottomWidth: 1,
borderBottomColor: '#1C1E20',
},
itemLeft: {
flex: 1,
},
itemTitle: {
color: '#F5F5F5',
fontSize: 14,
fontWeight: '500',
marginBottom: 4,
},
itemDate: {
color: '#ABABAB',
fontSize: 12,
fontWeight: '400',
},
itemPoints: {
color: '#4CAF50',
fontSize: 16,
fontWeight: '600',
},
itemPointsNegative: {
color: '#F5F5F5',
},
footer: {
paddingTop: 20,
paddingHorizontal: 16,
gap: 4,
},
subscribeButton: {
width: '100%',
height: 48,
borderRadius: 12,
overflow: 'hidden',
},
subscribeButtonGradient: {
width: '100%',
height: 48,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 12,
},
subscribeButtonText: {
color: '#F5F5F5',
fontSize: 16,
fontWeight: '500',
},
topUpButton: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
},
topUpButtonText: {
color: '#F5F5F5',
fontSize: 12,
fontWeight: '400',
},
})