BoxPopup.tsx 6.3 KB

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