import { LEVEL_MAP } from '@/constants/config'; import { Images } from '@/constants/images'; import { getUnavailableSeatNumbers, previewOrder } from '@/services/award'; import { Image, ImageBackground } from 'expo-image'; import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react'; import { ActivityIndicator, Alert, Dimensions, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; const { width: SCREEN_WIDTH } = Dimensions.get('window'); interface NumSelectionModalProps { poolId: string; onPay: (params: { previewRes: any, chooseNum: number[], boxNum: string }) => void; } export interface NumSelectionModalRef { show: (box: any) => void; close: () => void; } export const NumSelectionModal = forwardRef( ({ poolId, onPay }, ref) => { const [visible, setVisible] = useState(false); const [box, setBox] = useState(null); const [tabs, setTabs] = useState([]); const [activeTab, setActiveTab] = useState(null); // Status Maps const [useMap, setUseMap] = useState>({}); // Sold/Opened (seatNum -> level) const [lockMap, setLockMap] = useState>({}); // Locked/Pending (seatNum -> seatNum) const [checkMap, setCheckMap] = useState>({}); // Selected by user (seatNum -> seatNum) const [loading, setLoading] = useState(false); useImperativeHandle(ref, () => ({ show: (boxItem: any) => { setBox(boxItem); setVisible(true); setCheckMap({}); // Initialize Tabs based on box quantity // Assuming max 100 per tab like legacy const total = boxItem.quantity || 100; // Default or from box const newTabs = []; const itemsPerTab = 100; const count = Math.ceil(total / itemsPerTab); for (let i = 0; i < count; i++) { const start = i * itemsPerTab + 1; const end = Math.min((i + 1) * itemsPerTab, total); const data = Array.from({ length: end - start + 1 }, (_, k) => k + start); newTabs.push({ title: `${start}~${end}`, value: i, data }); } setTabs(newTabs); if (newTabs.length > 0) { setActiveTab(newTabs[0]); loadData(newTabs[0], boxItem.number); } }, close: () => { setVisible(false); }, })); const loadData = async (tab: any, boxNum: string) => { if (!tab || !boxNum) return; try { const start = tab.data[0]; const end = tab.data[tab.data.length - 1]; const res = await getUnavailableSeatNumbers(poolId, boxNum, start, end); if (res) { // Determine used (sold) and locked seats const newUseMap: Record = { ...useMap }; // Merge? Or reset per tab? Legacy merges logic via object assignment but here standard React state update // Legacy clears map if not careful? No, it sets properties. // Let's reset for the current range to avoid stale data visually if we revisit, but merging is safer for 'global' knowledge. // Efficiency-wise, let's keep it simple. if (res.usedSeatNumbers) { res.usedSeatNumbers.forEach((item: any) => { newUseMap[item.seatNumber] = item.level; }); } const newLockMap: Record = { ...lockMap }; if (res.applyedSeatNumbers) { res.applyedSeatNumbers.forEach((num: number) => { newLockMap[num] = num; }); } setUseMap(newUseMap); setLockMap(newLockMap); // Remove selected if they became unavailable const newCheckMap = { ...checkMap }; let changed = false; Object.keys(newCheckMap).forEach(key => { const num = Number(key); if (newUseMap[num] || newLockMap[num]) { delete newCheckMap[num]; changed = true; } }); if (changed) setCheckMap(newCheckMap); } } catch (error) { console.error('Failed to load seat info', error); } }; useEffect(() => { if (activeTab && box) { loadData(activeTab, box.number); } }, [activeTab]); const handleTabChange = (tab: any) => { setActiveTab(tab); }; const toggleSelect = (num: number) => { if (useMap[num] || lockMap[num]) return; setCheckMap(prev => { const next = { ...prev }; if (next[num]) { delete next[num]; } else { if (Object.keys(next).length >= 50) { Alert.alert('提示', '最多不超过50发'); return prev; } next[num] = num; } return next; }); }; const handleConfirm = async () => { const selectedNums = Object.values(checkMap).map(Number); if (selectedNums.length === 0) { Alert.alert('提示', '请选择号码'); return; } setLoading(true); try { const res = await previewOrder(poolId, selectedNums.length, box.number, selectedNums); if (res) { if (res.duplicateSeatNumbers && res.duplicateSeatNumbers.length > 0) { Alert.alert('提示', `${res.duplicateSeatNumbers.join(',')}号被占用`); // Remove duplicates const newCheckMap = { ...checkMap }; res.duplicateSeatNumbers.forEach((n: number) => delete newCheckMap[n]); setCheckMap(newCheckMap); // Refresh data loadData(activeTab, box.number); return; } // Proceed to pay callback onPay({ previewRes: res, chooseNum: selectedNums, boxNum: box.number }); setVisible(false); } } catch (error: any) { Alert.alert('错误', error?.message || '请求失败'); } finally { setLoading(false); } }; const renderItem = (num: number) => { const isUsed = !!useMap[num]; const isLocked = !!lockMap[num]; const isSelected = !!checkMap[num]; let content = {num}号; let style = [styles.item]; if (isUsed) { style.push(styles.levelItem as any); const level = useMap[num]; content = ( {num}号 {LEVEL_MAP[level]?.title || level} ); } else if (isLocked) { style.push(styles.lockedItem as any); content = ( <> {num}号 {/* Icon placeholder or text */} 🔒 ); } else if (isSelected) { style.push(styles.selectedItem as any); content = ( <> {num}号 ); } return ( toggleSelect(num)} disabled={isUsed || isLocked} > {content} ); }; const selectedList = Object.values(checkMap); return ( setVisible(false)}> setVisible(false)} /> {/* Header */} 换盒 setVisible(false)}> × {/* Tabs for Ranges */} {tabs.length > 1 && ( {tabs.map(tab => ( handleTabChange(tab)} > {tab.title} ))} )} {/* Grid */} {activeTab?.data.map((num: number) => renderItem(num))} {/* Bottom Selection Bar */} {selectedList.map((num) => ( {num}号 toggleSelect(Number(num))} style={styles.chipClose}> × ))} {loading ? : ( 确定选择 已选择({selectedList.length})发 )} ); } ); const styles = StyleSheet.create({ overlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'flex-end', }, mask: { flex: 1 }, container: { height: 600, paddingTop: 15, borderTopLeftRadius: 15, borderTopRightRadius: 15, overflow: 'hidden', backgroundColor: '#fff', }, 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 }, tabsContainer: { height: 40, marginBottom: 10, }, tabsScroll: { paddingHorizontal: 10, }, tab: { paddingVertical: 6, paddingHorizontal: 12, backgroundColor: '#f5f5f5', borderRadius: 20, marginRight: 10, justifyContent: 'center', }, activeTab: { backgroundColor: '#FFC900' }, tabText: { fontSize: 12, color: '#666' }, activeTabText: { color: '#000', fontWeight: 'bold' }, grid: { flexDirection: 'row', flexWrap: 'wrap', paddingHorizontal: 10, paddingBottom: 20, }, item: { width: (SCREEN_WIDTH - 20 - 45) / 5, // Approx 5 items per row height: ((SCREEN_WIDTH - 20 - 45) / 5) * 0.5, margin: 4, backgroundColor: '#FFC900', borderWidth: 3, borderColor: '#000', justifyContent: 'center', alignItems: 'center', borderRadius: 0, }, itemText: { fontSize: 12, fontWeight: 'bold', color: '#000' }, disabledText: { color: 'rgba(255,255,255,0.3)' }, levelItem: { backgroundColor: '#e8e8e8', borderColor: '#e8e8e8' }, lockedItem: { backgroundColor: 'rgba(98, 99, 115, 0.3)', borderColor: 'transparent', borderWidth: 0 }, selectedItem: { backgroundColor: '#FFC900', borderColor: '#000' }, // Same as default but with icon usedContent: { alignItems: 'center' }, usedNum: { fontSize: 10, opacity: 0.5 }, levelText: { fontSize: 10, fontWeight: 'bold' }, lockIcon: { position: 'absolute', bottom: 0, right: 0, backgroundColor: '#000', padding: 2, borderTopLeftRadius: 5 }, checkIcon: { position: 'absolute', bottom: 0, right: 0, backgroundColor: '#fff', paddingHorizontal: 4, borderTopLeftRadius: 5, }, bottomSection: { paddingBottom: 30, paddingTop: 10, borderTopWidth: 1, borderColor: '#eee', backgroundColor: '#fff', }, selectedScroll: { maxHeight: 50, marginBottom: 10 }, selectedContent: { paddingHorizontal: 10 }, selectedChip: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#FFC900', borderWidth: 2, borderColor: '#000', paddingHorizontal: 8, paddingVertical: 4, borderRadius: 4, marginRight: 8, height: 30, }, selectedChipText: { fontSize: 12, fontWeight: 'bold', marginRight: 5 }, chipClose: { backgroundColor: 'rgba(255,255,255,0.5)', borderRadius: 10, width: 16, height: 16, alignItems: 'center', justifyContent: 'center' }, chipCloseText: { fontSize: 12, lineHeight: 14 }, confirmBtn: { alignSelf: 'center', width: 200, height: 50, }, confirmBtnBg: { flex: 1, justifyContent: 'center', alignItems: 'center', }, confirmText: { color: '#fff', fontSize: 16, fontWeight: 'bold', textShadowColor: '#000', textShadowOffset: { width: 1, height: 1 }, textShadowRadius: 1, }, subConfirmText: { color: '#fff', fontSize: 10, marginTop: -2, }, });