| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428 |
- import { Images } from '@/constants/images';
- import { getBoxList } from '@/services/award';
- import { Image, ImageBackground } from 'expo-image';
- import React, { forwardRef, useCallback, useImperativeHandle, useState } from 'react';
- import {
- ActivityIndicator,
- Dimensions,
- FlatList,
- Modal,
- ScrollView,
- StyleSheet,
- Text,
- TouchableOpacity,
- View
- } from 'react-native';
- const { width: SCREEN_WIDTH } = Dimensions.get('window');
- interface BoxSelectionModalProps {
- poolId: string;
- onSelect: (box: any) => void;
- }
- export interface BoxSelectionModalRef {
- show: () => void;
- close: () => void;
- }
- const TABS = [
- { title: '全部', value: '' },
- { title: '超神款', value: 'A' },
- { title: '欧皇款', value: 'B' },
- { title: '隐藏款', value: 'C' },
- { title: '普通款', value: 'D' },
- ];
- export const BoxSelectionModal = forwardRef<BoxSelectionModalRef, BoxSelectionModalProps>(
- ({ poolId, onSelect }, ref) => {
- const [visible, setVisible] = useState(false);
- const [activeTab, setActiveTab] = useState(TABS[0]);
- const [list, setList] = useState<any[]>([]);
- const [loading, setLoading] = useState(false);
- const [refreshing, setRefreshing] = useState(false);
-
- // Bucket Logic
- const [total, setTotal] = useState(0);
- const [activeBucket, setActiveBucket] = useState(0);
- const BUCKET_SIZE = 100;
- const loadData = useCallback(async (bucketIndex = 0, tabValue = '', isRefresh = false) => {
- if (isRefresh) setRefreshing(true);
- setLoading(true);
- try {
- // Page is bucketIndex + 1, Size is 100
- const res = await getBoxList(poolId, tabValue as any, bucketIndex + 1, BUCKET_SIZE);
- const newData = res?.records || [];
-
- setList(newData);
- setTotal(res?.total || 0); // Assuming API returns total
- setActiveBucket(bucketIndex);
-
- } catch (error) {
- console.error('Failed to load boxes:', error);
- } finally {
- setLoading(false);
- setRefreshing(false);
- }
- }, [poolId]);
- useImperativeHandle(ref, () => ({
- show: () => {
- setVisible(true);
- setActiveTab(TABS[0]);
- setActiveBucket(0);
- setTotal(0);
- loadData(0, TABS[0].value);
- },
- close: () => {
- setVisible(false);
- },
- }));
- const handleTabChange = (tab: typeof TABS[0]) => {
- setActiveTab(tab);
- // Reset to first bucket when changing level tab
- loadData(0, tab.value);
- };
- const handleBucketChange = (index: number) => {
- loadData(index, activeTab.value);
- };
- const handleRefresh = () => {
- loadData(activeBucket, activeTab.value, true);
- }
- const renderItem = ({ item, index }: { item: any; index: number }) => {
- if (item.leftQuantity <= 0) return null;
- return (
- <TouchableOpacity
- style={styles.item}
- onPress={() => {
- setVisible(false);
- onSelect(item);
- }}
- >
- <View style={styles.itemIndex}>
- {/* Index should be absolute based on bucket */}
- <Text style={styles.indexText}>{(activeBucket * BUCKET_SIZE) + index + 1}</Text>
- </View>
-
- <View style={styles.itemContent}>
- {/* Left Icon & Count */}
- <View style={styles.leftSection}>
- <Image source={{ uri: Images.box.detail.boxIcon }} style={styles.boxIcon} contentFit="contain"/>
- <Text style={styles.leftText}>剩{item.leftQuantity}发</Text>
- </View>
-
- <View style={styles.divider} />
- {/* Breakdown */}
- <View style={styles.breakdown}>
- <BreakdownItem label="A" current={item.leftQuantityA} total={item.quantityA} icon={Images.box.detail.levelTextA} />
- <BreakdownItem label="B" current={item.leftQuantityB} total={item.quantityB} icon={Images.box.detail.levelTextB} />
- <BreakdownItem label="C" current={item.leftQuantityC} total={item.quantityC} icon={Images.box.detail.levelTextC} />
- <BreakdownItem label="D" current={item.leftQuantityD} total={item.quantityD} icon={Images.box.detail.levelTextD} />
- </View>
- </View>
- </TouchableOpacity>
- );
- };
- const buckets = Math.ceil(total / BUCKET_SIZE);
- return (
- <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
- <View style={styles.overlay}>
- <TouchableOpacity style={styles.mask} activeOpacity={1} onPress={() => setVisible(false)} />
-
- <ImageBackground
- source={{ uri: Images.box.detail.recordBg }}
- style={styles.container}
- resizeMode="stretch"
- >
- {/* Header */}
- <View style={styles.header}>
- <Image source={{ uri: Images.box.detail.recordTitleLeft }} style={styles.titleDecor} contentFit="contain" />
- <Text style={styles.title}>换盒</Text>
- <Image source={{ uri: Images.box.detail.recordTitleRight }} style={styles.titleDecor} contentFit="contain" />
-
- <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
- <Text style={styles.closeText}>×</Text>
- </TouchableOpacity>
- </View>
- {/* Level Tabs */}
- <View style={styles.tabs}>
- {TABS.map(tab => (
- <TouchableOpacity
- key={tab.value}
- style={[styles.tab, activeTab.value === tab.value && styles.activeTab]}
- onPress={() => handleTabChange(tab)}
- >
- <Text style={[styles.tabText, activeTab.value === tab.value && styles.activeTabText]}>
- {tab.title}
- </Text>
- </TouchableOpacity>
- ))}
- </View>
- {/* Bucket Selector (Range 1-100, etc.) */}
- {buckets > 1 && (
- <View style={styles.bucketContainer}>
- <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.bucketScroll}>
- {Array.from({ length: buckets }).map((_, index) => {
- const start = index * BUCKET_SIZE + 1;
- const end = Math.min((index + 1) * BUCKET_SIZE, total);
- const isActive = activeBucket === index;
- return (
- <TouchableOpacity
- key={index}
- style={[styles.bucketItem, isActive && styles.bucketItemActive]}
- onPress={() => handleBucketChange(index)}
- >
- <Text style={[styles.bucketText, isActive && styles.bucketTextActive]}>
- {start}~{end}
- </Text>
- </TouchableOpacity>
- );
- })}
- </ScrollView>
- </View>
- )}
- {/* List */}
- <View style={styles.listContainer}>
- <FlatList
- data={list}
- renderItem={renderItem}
- keyExtractor={(item, index) => item.id || String(index)}
- contentContainerStyle={styles.listContent}
- refreshing={refreshing}
- onRefresh={handleRefresh}
- ListEmptyComponent={
- !loading ? <Text style={styles.emptyText}>暂无数据</Text> : null
- }
- ListFooterComponent={loading && !refreshing ? <ActivityIndicator style={{ marginTop: 20 }} /> : null}
- />
- </View>
- </ImageBackground>
- </View>
- </Modal>
- );
- }
- );
- const BreakdownItem = ({ label, current, total, icon }: { label: string, current: number, total: number, icon: string }) => (
- <View style={styles.breakdownItem}>
- <Image source={{ uri: icon }} style={styles.levelIcon} contentFit="contain" />
- <Text style={styles.breakdownText}>
- <Text style={styles.currentNum}>{current}</Text>/{total}
- </Text>
- </View>
- );
- const styles = StyleSheet.create({
- overlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.5)',
- justifyContent: 'flex-end',
- },
- mask: { flex: 1 },
- container: {
- height: 600, // wrapperWidth rpx in Vue
- paddingTop: 15,
- borderTopLeftRadius: 15,
- borderTopRightRadius: 15,
- overflow: 'hidden',
- backgroundColor: '#fff', // Fallback
- },
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: 15,
- position: 'relative',
- height: 44,
- },
- title: {
- fontSize: 18,
- fontWeight: 'bold',
- color: '#fff',
- marginHorizontal: 10,
- textShadowColor: '#000',
- textShadowOffset: { width: 1, height: 1 },
- textShadowRadius: 1,
- },
- titleDecor: {
- width: 17,
- height: 19,
- },
- closeBtn: {
- position: 'absolute',
- right: 15,
- top: 0,
- width: 24,
- height: 24,
- backgroundColor: '#ebebeb',
- borderRadius: 12,
- justifyContent: 'center',
- alignItems: 'center',
- },
- closeText: {
- fontSize: 18,
- color: '#a2a2a2',
- marginTop: -2,
- },
- tabs: {
- flexDirection: 'row',
- justifyContent: 'space-around',
- paddingHorizontal: 15,
- marginBottom: 10,
- },
- tab: {
- paddingVertical: 6,
- paddingHorizontal: 12,
- borderRadius: 15,
- backgroundColor: '#f5f5f5',
- },
- activeTab: {
- backgroundColor: '#ffdb4d',
- },
- tabText: {
- fontSize: 12,
- color: '#666',
- },
- activeTabText: {
- color: '#333',
- fontWeight: 'bold',
- },
- listContainer: {
- flex: 1,
- backgroundColor: '#f3f3f3',
- marginHorizontal: 15,
- marginBottom: 20,
- borderWidth: 2,
- borderColor: '#000',
- padding: 10,
- },
- listContent: {
- paddingBottom: 20,
- },
- item: {
- backgroundColor: '#fff',
- borderWidth: 3,
- borderColor: '#000',
- marginBottom: 10,
- flexDirection: 'row',
- height: 80,
- position: 'relative',
- overflow: 'hidden',
- },
- itemIndex: {
- position: 'absolute',
- left: 0,
- top: 0,
- width: 22,
- height: 22,
- backgroundColor: '#fff',
- borderRightWidth: 2,
- borderBottomWidth: 2,
- borderColor: '#000',
- justifyContent: 'center',
- alignItems: 'center',
- zIndex: 1,
- },
- indexText: {
- fontSize: 12,
- fontWeight: 'bold',
- },
- itemContent: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 10,
- paddingLeft: 22, // Space for index
- },
- leftSection: {
- width: 60,
- alignItems: 'center',
- justifyContent: 'center',
- },
- boxIcon: {
- width: 24,
- height: 24,
- marginBottom: 2,
- },
- leftText: {
- fontSize: 10,
- color: '#333',
- },
- divider: {
- width: 1,
- height: 40,
- backgroundColor: '#dcdad3',
- marginHorizontal: 10,
- opacity: 0.5,
- },
- breakdown: {
- flex: 1,
- flexDirection: 'row',
- flexWrap: 'wrap',
- },
- breakdownItem: {
- width: '50%',
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 4,
- },
- levelIcon: {
- width: 45,
- height: 16,
- marginRight: 4,
- },
- breakdownText: {
- fontSize: 12,
- color: '#666',
- },
- currentNum: {
- color: '#000',
- fontWeight: 'bold',
- },
- emptyText: {
- textAlign: 'center',
- marginTop: 20,
- color: '#999',
- },
- // Bucket Styles
- bucketContainer: {
- height: 44,
- marginBottom: 10,
- },
- bucketScroll: {
- paddingHorizontal: 15,
- },
- bucketItem: {
- paddingHorizontal: 15,
- paddingVertical: 8,
- borderRadius: 20,
- backgroundColor: '#f5f5f5',
- marginRight: 10,
- justifyContent: 'center',
- },
- bucketItemActive: {
- backgroundColor: '#ffdb4d',
- },
- bucketText: {
- fontSize: 12,
- color: '#666',
- },
- bucketTextActive: {
- color: '#333',
- fontWeight: 'bold',
- },
- });
|