BoxPopup.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import { Image } from 'expo-image';
  2. import React, { forwardRef, useImperativeHandle, useState } from 'react';
  3. import { Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
  4. import { Images } from '@/constants/images';
  5. interface BoxItem {
  6. boxNumber: number;
  7. leftQuantity: number;
  8. quantity: number;
  9. isCompleted: boolean;
  10. }
  11. interface BoxPopupProps {
  12. onSelect: (item: BoxItem) => void;
  13. }
  14. export interface BoxPopupRef {
  15. open: (list: BoxItem[]) => void;
  16. close: () => void;
  17. }
  18. export const BoxPopup = forwardRef<BoxPopupRef, BoxPopupProps>(({ onSelect }, ref) => {
  19. const [activeBucket, setActiveBucket] = useState(0);
  20. const [list, setList] = useState<BoxItem[]>([]);
  21. const [visible, setVisible] = useState(false);
  22. useImperativeHandle(ref, () => ({
  23. open: (data: BoxItem[]) => {
  24. console.log('BoxPopup open with data:', data?.length);
  25. setList(data || []);
  26. setActiveBucket(0); // Reset to first bucket
  27. setVisible(true);
  28. },
  29. close: () => setVisible(false),
  30. }));
  31. const handleSelect = (item: BoxItem) => {
  32. onSelect(item);
  33. setVisible(false);
  34. };
  35. // Bucket logic
  36. const BUCKET_SIZE = 100;
  37. const buckets = Math.ceil(list.length / BUCKET_SIZE);
  38. const displayList = list.slice(activeBucket * BUCKET_SIZE, (activeBucket + 1) * BUCKET_SIZE);
  39. return (
  40. <Modal visible={visible} transparent animationType="slide" onRequestClose={() => setVisible(false)}>
  41. <View style={styles.overlay}>
  42. <View style={styles.container}>
  43. {/* 标题区域 */}
  44. <View style={styles.titleSection}>
  45. <View style={styles.titleBg}>
  46. <Text style={styles.titleText}>选择盒子</Text>
  47. </View>
  48. <TouchableOpacity style={styles.closeBtn} onPress={() => setVisible(false)}>
  49. <Text style={styles.closeBtnText}>×</Text>
  50. </TouchableOpacity>
  51. </View>
  52. {/* 区间选择器 (仅当数量超过100时显示) */}
  53. {buckets > 1 && (
  54. <View style={styles.bucketContainer}>
  55. <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.bucketScroll}>
  56. {Array.from({ length: buckets }).map((_, index) => {
  57. const start = index * BUCKET_SIZE + 1;
  58. const end = Math.min((index + 1) * BUCKET_SIZE, list.length);
  59. const isActive = activeBucket === index;
  60. return (
  61. <TouchableOpacity
  62. key={index}
  63. style={[styles.bucketItem, isActive && styles.bucketItemActive]}
  64. onPress={() => setActiveBucket(index)}
  65. >
  66. <Text style={[styles.bucketText, isActive && styles.bucketTextActive]}>
  67. {start}~{end}
  68. </Text>
  69. </TouchableOpacity>
  70. );
  71. })}
  72. </ScrollView>
  73. </View>
  74. )}
  75. {/* 盒子列表 */}
  76. <View style={styles.content}>
  77. <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
  78. <View style={styles.grid}>
  79. {displayList.map((item, index) => (
  80. <TouchableOpacity key={item.boxNumber} style={styles.item} onPress={() => handleSelect(item)}>
  81. <View style={styles.imgBox}>
  82. <Image
  83. source={{ uri: item.isCompleted ? Images.box.detail.packagingBox2 : Images.box.detail.packagingBox1 }}
  84. style={styles.boxImg}
  85. contentFit="contain"
  86. />
  87. </View>
  88. <View style={styles.numBadge}>
  89. <Text style={styles.numText}>{item.boxNumber}箱</Text>
  90. </View>
  91. <Text style={styles.remaining}>
  92. {item.leftQuantity}/{item.quantity}
  93. </Text>
  94. </TouchableOpacity>
  95. ))}
  96. </View>
  97. </ScrollView>
  98. </View>
  99. </View>
  100. </View>
  101. </Modal>
  102. );
  103. });
  104. const styles = StyleSheet.create({
  105. overlay: {
  106. flex: 1,
  107. backgroundColor: 'rgba(0,0,0,0.5)',
  108. justifyContent: 'flex-end',
  109. },
  110. container: {
  111. backgroundColor: '#ffc901',
  112. borderTopLeftRadius: 10,
  113. borderTopRightRadius: 10,
  114. maxHeight: '85%', // Increased slightly
  115. minHeight: 400, // Ensure popup has height even if list is empty
  116. paddingBottom: 20,
  117. },
  118. titleSection: {
  119. position: 'relative',
  120. alignItems: 'center',
  121. paddingVertical: 15,
  122. },
  123. titleBg: {
  124. backgroundColor: '#ff8c16',
  125. paddingHorizontal: 40,
  126. paddingVertical: 10,
  127. borderRadius: 20,
  128. },
  129. titleText: {
  130. color: '#fff',
  131. fontSize: 16,
  132. fontWeight: 'bold',
  133. },
  134. closeBtn: {
  135. position: 'absolute',
  136. right: 15,
  137. top: 10,
  138. width: 30,
  139. height: 30,
  140. justifyContent: 'center',
  141. alignItems: 'center',
  142. },
  143. closeBtnText: {
  144. fontSize: 24,
  145. color: '#333',
  146. fontWeight: 'bold',
  147. },
  148. // Bucket Styles
  149. bucketContainer: {
  150. marginBottom: 10,
  151. height: 40,
  152. },
  153. bucketScroll: {
  154. paddingHorizontal: 10,
  155. },
  156. bucketItem: {
  157. paddingHorizontal: 15,
  158. paddingVertical: 6,
  159. borderRadius: 15,
  160. backgroundColor: '#fff',
  161. marginRight: 10,
  162. borderWidth: 1,
  163. borderColor: '#eee',
  164. justifyContent: 'center',
  165. },
  166. bucketItemActive: {
  167. backgroundColor: '#ff8c16',
  168. borderColor: '#ff8c16',
  169. },
  170. bucketText: {
  171. fontSize: 12,
  172. color: '#333',
  173. fontWeight: '500',
  174. },
  175. bucketTextActive: {
  176. color: '#fff',
  177. fontWeight: 'bold',
  178. },
  179. content: {
  180. backgroundColor: '#fff',
  181. marginHorizontal: 10,
  182. borderRadius: 13,
  183. borderWidth: 1,
  184. borderColor: '#000',
  185. flex: 1, // Let it take remaining space
  186. },
  187. scrollView: {
  188. padding: 10,
  189. },
  190. grid: {
  191. flexDirection: 'row',
  192. flexWrap: 'wrap',
  193. paddingBottom: 20,
  194. },
  195. item: {
  196. width: '25%',
  197. alignItems: 'center',
  198. marginBottom: 15,
  199. },
  200. imgBox: {
  201. width: 50, // Slightly smaller to fit better
  202. height: 50,
  203. },
  204. boxImg: {
  205. width: '100%',
  206. height: '100%',
  207. },
  208. numBadge: {
  209. backgroundColor: '#959595',
  210. borderRadius: 15,
  211. paddingHorizontal: 8,
  212. paddingVertical: 2,
  213. marginTop: 5,
  214. },
  215. numText: {
  216. color: '#fff',
  217. fontSize: 10, // Smaller font
  218. fontWeight: '500',
  219. },
  220. remaining: {
  221. fontSize: 10,
  222. color: '#999',
  223. fontWeight: 'bold',
  224. marginTop: 3,
  225. },
  226. });