| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541 |
- import { Barrage } from '@/components/Barrage';
- import { Image } from 'expo-image';
- import { useRouter } from 'expo-router';
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
- import {
- ActivityIndicator,
- FlatList,
- ImageBackground,
- RefreshControl,
- ScrollView,
- StatusBar,
- StyleSheet,
- Text,
- TextInput,
- TouchableOpacity,
- View
- } from 'react-native';
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
- import { Images } from '@/constants/images';
- import { getFeedbackList, getPoolList, PoolItem } from '@/services/award';
- const typeList = [
- { label: '全部', value: '', type: 0, img: Images.box.type1, imgOn: Images.box.type1On },
- { label: '高保赏', value: 'UNLIMITED', type: 2, img: Images.box.type3, imgOn: Images.box.type3On },
- { label: '高爆赏', value: 'UNLIMITED', type: 1, img: Images.box.type2, imgOn: Images.box.type2On },
- { label: '擂台赏', value: 'YFS_PRO', type: 7, img: Images.box.type5, imgOn: Images.box.type5On },
- { label: '一番赏', value: 'YFS_PRO', type: 0, img: Images.box.type4, imgOn: Images.box.type4On },
- ];
- interface BarrageItem {
- id: string;
- content: string;
- nickname?: string;
- avatar: string;
- poolName?: string;
- type?: string;
- text?: string;
- poolId?: string;
- }
- // Static Header Component - Memoized to prevent re-renders
- const StaticHeader = React.memo(({ barrageList }: { barrageList: BarrageItem[] }) => (
- <View>
- {/* 占位空间 - 给顶部搜索栏留出空间 */}
- <View style={{ height: 53 }} />
- {/* 顶部主图 - 绝对定位,叠在背景上 */}
- <View style={styles.mainImageContainer}>
- <Image
- source={{ uri: Images.box.awardMainImg }}
- style={styles.mainImage}
- contentFit="fill"
- />
- </View>
- {/* 占位空间 - 主图高度 */}
- <View style={{ height: 360 }} />
- {/* 弹幕区域 */}
- {barrageList && barrageList.length > 0 && (
- <View style={styles.barrageSection}>
- <Barrage data={barrageList.slice(0, Math.ceil(barrageList.length / 2))} />
- <View style={{ height: 6 }} />
- <Barrage
- data={barrageList.slice(Math.ceil(barrageList.length / 2))}
- speed={35}
- />
- </View>
- )}
- </View>
- ));
- // Type Selector Component
- const TypeSelector = React.memo(({
- typeIndex,
- priceSort,
- onTypeChange,
- onSortChange
- }: {
- typeIndex: number;
- priceSort: number;
- onTypeChange: (index: number) => void;
- onSortChange: () => void;
- }) => (
- <View style={styles.typeSection}>
- <View style={styles.typeListContainer}>
- <ScrollView
- horizontal
- showsHorizontalScrollIndicator={false}
- contentContainerStyle={styles.typeListContent}
- >
- {typeList.map((item, index) => (
- <TouchableOpacity
- key={index}
- style={styles.typeItem}
- onPress={() => onTypeChange(index)}
- activeOpacity={0.7}
- >
- <Image
- source={{ uri: typeIndex === index ? item.imgOn : item.img }}
- style={styles.typeImage}
- contentFit="fill"
- />
- </TouchableOpacity>
- ))}
- </ScrollView>
- </View>
- <TouchableOpacity style={styles.sortBtn} onPress={onSortChange} activeOpacity={0.7}>
- <Image
- source={{
- uri: priceSort === 0
- ? Images.box.sortAmount
- : priceSort === 1
- ? Images.box.sortAmountOnT
- : Images.box.sortAmountOnB,
- }}
- style={styles.sortIcon}
- contentFit="contain"
- />
- </TouchableOpacity>
- </View>
- ));
- export default function BoxScreen() {
- const router = useRouter();
- const insets = useSafeAreaInsets();
- const [keyword, setKeyword] = useState('');
- const [typeIndex, setTypeIndex] = useState(0);
- const [priceSort, setPriceSort] = useState(0);
- const [list, setList] = useState<PoolItem[]>([]);
- const [loading, setLoading] = useState(false);
- const [refreshing, setRefreshing] = useState(false);
- const [current, setCurrent] = useState(1);
- const [total, setTotal] = useState(0);
- const [hasMore, setHasMore] = useState(true);
- const [barrageList, setBarrageList] = useState<BarrageItem[]>([]);
- // 加载弹幕
- const loadBarrage = useCallback(async () => {
- try {
- const res = await getFeedbackList();
- if (res.data) {
- setBarrageList(res.data);
- }
- } catch (error) {
- console.error('加载弹幕失败:', error);
- }
- }, []);
- const loadData = useCallback(async (isRefresh = false) => {
- if (loading) return;
-
- const page = isRefresh ? 1 : current;
- if (!isRefresh && !hasMore) return;
- setLoading(true);
- try {
- const selectedType = typeList[typeIndex];
- const res = await getPoolList({
- current: page,
- size: 10,
- mode: selectedType.value || undefined,
- type: selectedType.type,
- keyword: keyword || undefined,
- priceSort: priceSort || undefined,
- });
- if (res.success && res.data) {
- const newList = isRefresh ? res.data : [...list, ...res.data];
- setList(newList);
- setTotal(res.count || 0);
- setCurrent(page + 1);
- setHasMore(newList.length < (res.count || 0));
- }
- } catch (error) {
- console.error('加载奖池列表失败:', error);
- }
- setLoading(false);
- setRefreshing(false);
- }, [current, hasMore, loading, typeIndex, list, keyword, priceSort]);
- useEffect(() => {
- loadData(true);
- loadBarrage();
- }, [typeIndex, priceSort]);
- // 执行搜索
- const handleSearch = () => {
- setList([]);
- setCurrent(1);
- setHasMore(true);
- // 需要延迟一下让状态更新
- setTimeout(() => {
- loadData(true);
- }, 100);
- };
- const handleRefresh = () => {
- setRefreshing(true);
- loadData(true);
- };
- const handleLoadMore = () => {
- if (!loading && hasMore) {
- loadData(false);
- }
- };
- const handleTypeChange = useCallback((index: number) => {
- setTypeIndex(index);
- setList([]);
- setCurrent(1);
- setHasMore(true);
- }, []);
- const handlePriceSort = useCallback(() => {
- setPriceSort((prev) => (prev + 1) % 3);
- }, []);
- const handleItemPress = useCallback((item: PoolItem) => {
- // 检查商品状态
- if (item.status !== undefined && item.status !== 1) return;
-
- console.log('点击商品:', item.id, 'mode:', item.mode, 'type:', item.type);
-
- // 根据类型跳转到不同页面 - 按照小程序逻辑
- if (item.type === 7) {
- // 擂台赏跳转到 boxInBox 页面
- router.push({ pathname: '/boxInBox', params: { poolId: item.id } } as any);
- } else if (item.mode === 'UNLIMITED') {
- // 高爆赏/高保赏
- router.push({ pathname: '/award-detail', params: { poolId: item.id } } as any);
- } else if (item.mode === 'YFS_PRO') {
- // 一番赏
- router.push({ pathname: '/award-detail-yfs', params: { poolId: item.id } } as any);
- } else {
- // 其他商品
- router.push(`/product/${item.id}` as any);
- }
- }, [router]);
- const renderItem = useCallback(({ item }: { item: PoolItem }) => (
- <TouchableOpacity
- style={styles.itemContainer}
- onPress={() => handleItemPress(item)}
- activeOpacity={0.8}
- >
- <ImageBackground
- source={{ uri: Images.box.goodsItemBg }}
- style={styles.itemBg}
- resizeMode="stretch"
- >
- <Image
- source={{ uri: item.cover }}
- style={styles.itemImage}
- contentFit="cover"
- />
- <View style={styles.itemInfo}>
- <Text style={styles.itemName} numberOfLines={1}>{item.name}</Text>
- <Text style={styles.itemPrice}>
- <Text style={styles.priceUnit}>¥</Text>
- {item.price}起
- </Text>
- </View>
- </ImageBackground>
- </TouchableOpacity>
- ), [handleItemPress]);
- const ListHeader = useMemo(() => (
- <View>
- <StaticHeader barrageList={barrageList} />
- <TypeSelector
- typeIndex={typeIndex}
- priceSort={priceSort}
- onTypeChange={handleTypeChange}
- onSortChange={handlePriceSort}
- />
- </View>
- ), [barrageList, typeIndex, priceSort, handleTypeChange, handlePriceSort]);
- const renderFooter = useCallback(() => {
- if (!loading) return null;
- return (
- <View style={styles.footer}>
- <ActivityIndicator size="small" color="#fff" />
- <Text style={styles.footerText}>加载中...</Text>
- </View>
- );
- }, [loading]);
- const renderEmpty = useCallback(() => {
- if (loading) return null;
- return (
- <View style={styles.empty}>
- <Text style={styles.emptyText}>暂无数据</Text>
- </View>
- );
- }, [loading]);
- return (
- <View style={styles.container}>
- <StatusBar barStyle="light-content" />
- <ImageBackground
- source={{ uri: Images.common.indexBg }}
- style={styles.background}
- resizeMode="cover"
- >
- {/* 顶部搜索栏 */}
- <View style={[styles.header, { paddingTop: insets.top + 10 }]}>
- <Image
- source={{ uri: Images.home.portrait }}
- style={styles.logo}
- contentFit="contain"
- />
- <View style={styles.searchBar}>
- <Image
- source={{ uri: Images.home.search }}
- style={styles.searchIcon}
- contentFit="contain"
- />
- <TextInput
- style={styles.searchInput}
- value={keyword}
- onChangeText={setKeyword}
- placeholder="搜索"
- placeholderTextColor="rgba(255,255,255,0.5)"
- returnKeyType="search"
- onSubmitEditing={handleSearch}
- />
- {keyword.length > 0 && (
- <TouchableOpacity onPress={handleSearch} style={styles.searchBtn}>
- <Text style={styles.searchBtnText}>搜索</Text>
- </TouchableOpacity>
- )}
- </View>
- </View>
- {/* 列表 */}
- <FlatList
- data={list}
- renderItem={renderItem}
- keyExtractor={(item) => item.id}
- ListHeaderComponent={ListHeader}
- ListFooterComponent={renderFooter}
- ListEmptyComponent={renderEmpty}
- contentContainerStyle={styles.listContent}
- showsVerticalScrollIndicator={false}
- refreshControl={
- <RefreshControl
- refreshing={refreshing}
- onRefresh={handleRefresh}
- tintColor="#fff"
- />
- }
- onEndReached={handleLoadMore}
- onEndReachedThreshold={0.3}
- />
- </ImageBackground>
- </View>
- );
- }
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: '#1a1a2e',
- },
- background: {
- flex: 1,
- },
- header: {
- position: 'relative',
- zIndex: 11,
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 15,
- paddingBottom: 10,
- },
- logo: {
- width: 67,
- height: 25,
- marginRight: 20,
- },
- searchBar: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: 'rgba(255,255,255,0.38)',
- borderRadius: 180,
- paddingHorizontal: 15,
- height: 28,
- },
- searchIcon: {
- width: 15,
- height: 15,
- marginRight: 5,
- },
- searchInput: {
- flex: 1,
- color: '#fff',
- fontSize: 12,
- padding: 0,
- },
- searchBtn: {
- paddingHorizontal: 8,
- paddingVertical: 2,
- },
- searchBtnText: {
- color: '#fff',
- fontSize: 12,
- },
- mainImageContainer: {
- position: 'absolute',
- left: 0,
- top: 0,
- zIndex: 2,
- width: '100%',
- height: 395,
- },
- mainImage: {
- width: '100%',
- height: '100%',
- },
- typeSection: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 10,
- paddingVertical: 10,
- position: 'relative',
- zIndex: 10,
- backgroundColor: 'transparent',
- },
- typeListContainer: {
- flex: 1,
- marginRight: 10,
- },
- typeListContent: {
- paddingRight: 10,
- },
- typeItem: {
- width: 73,
- height: 34,
- marginRight: 4,
- },
- typeImage: {
- width: '100%',
- height: '100%',
- },
- sortBtn: {
- width: 38,
- height: 38,
- alignItems: 'center',
- justifyContent: 'center',
- },
- sortIcon: {
- width: 38,
- height: 38,
- },
- listContent: {
- paddingHorizontal: 10,
- paddingBottom: 100,
- },
- itemContainer: {
- marginBottom: 8,
- },
- itemBg: {
- width: '100%',
- height: 230, // Increased total height slightly for safety
- // Remove padding to control children individually
- },
- itemImage: {
- width: '95%', // Very tight fit (leaving ~2.5% gap on sides = small gap)
- height: 162, // expand height downwards
- borderRadius: 6,
- alignSelf: 'center',
- marginTop: 13, // Tighter top fit (approx 1px-2px visual gap)
- },
- itemInfo: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingHorizontal: 20, // Visual padding for text
- paddingTop: 18, // Move text downwards
- },
- itemName: {
- flex: 1,
- color: '#fff',
- fontSize: 14,
- },
- itemPrice: {
- color: '#ff0000',
- fontSize: 12,
- fontWeight: 'bold',
- marginLeft: 10,
- },
- priceUnit: {
- fontSize: 12,
- marginRight: 2,
- },
- footer: {
- flexDirection: 'row',
- justifyContent: 'center',
- alignItems: 'center',
- paddingVertical: 15,
- },
- footerText: {
- color: 'rgba(255,255,255,0.6)',
- fontSize: 12,
- marginLeft: 8,
- },
- empty: {
- alignItems: 'center',
- paddingVertical: 50,
- },
- emptyText: {
- color: 'rgba(255,255,255,0.6)',
- fontSize: 14,
- },
- barrageSection: {
- marginVertical: 10,
- paddingHorizontal: 10,
- },
- barrageRow: {
- flexDirection: 'row',
- marginBottom: 5,
- },
- barrageItem: {
- backgroundColor: 'rgba(0,0,0,0.5)',
- borderRadius: 15,
- paddingHorizontal: 12,
- paddingVertical: 6,
- marginRight: 8,
- maxWidth: 150,
- },
- barrageText: {
- color: '#fff',
- fontSize: 12,
- },
- });
|