ProductListYfs.tsx 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import { Images } from '@/constants/images';
  2. import { Image } from 'expo-image';
  3. import React, { useMemo } from 'react';
  4. import { Dimensions, ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
  5. const { width } = Dimensions.get('window');
  6. interface ProductListYfsProps {
  7. products: any[];
  8. poolId: string;
  9. box: any;
  10. onProductClick?: (product: any) => void;
  11. }
  12. // Level Configuration - reused from ProductList logic
  13. const LEVEL_CONFIG: Record<string, { title: string; color: string; bgColor: string; productItem: string }> = {
  14. A: { title: '超神款', color: '#fff', bgColor: '#FF4444', productItem: Images.box.detail.productItemA },
  15. B: { title: '欧皇款', color: '#fff', bgColor: '#FF9900', productItem: Images.box.detail.productItemB },
  16. C: { title: '隐藏款', color: '#fff', bgColor: '#9966FF', productItem: Images.box.detail.productItemC },
  17. D: { title: '普通款', color: '#fff', bgColor: '#00CCFF', productItem: Images.box.detail.productItemD },
  18. };
  19. export default function ProductListYfs({ products = [], poolId, box, onProductClick }: ProductListYfsProps) {
  20. const levels = useMemo(() => {
  21. // Group by level
  22. const grouped: Record<string, any[]> = { A: [], B: [], C: [], D: [] };
  23. products.forEach(p => {
  24. const level = p.level || 'D';
  25. if (grouped[level]) {
  26. grouped[level].push(p);
  27. }
  28. });
  29. const result = [
  30. { level: 'A', list: grouped.A },
  31. { level: 'B', list: grouped.B },
  32. { level: 'C', list: grouped.C },
  33. { level: 'D', list: grouped.D },
  34. ].filter(g => g.list && g.list.length > 0);
  35. return result;
  36. }, [products]);
  37. const getLeftNum = (item: any) => {
  38. // Robust check for box and usedStat (camelCase or snake_case)
  39. const usedStat = box?.usedStat || box?.used_stat;
  40. if (!box || !usedStat) {
  41. return item.quantity;
  42. }
  43. // Try multiple key variations for robustness
  44. const spuId = String(item.spu?.id || item.spu_id);
  45. const itemId = String(item.id);
  46. let used: any = null;
  47. // Check if usedStat is an Array (based on logs showing keys 0,1,2...)
  48. if (Array.isArray(usedStat)) {
  49. // Debug log to see structure of array items once
  50. if (!global.hasLoggedUsedStatStructure) {
  51. console.log('[DEBUG-ICHIBAN] usedStat is Array. First item:', usedStat[0]);
  52. global.hasLoggedUsedStatStructure = true;
  53. }
  54. // Search in array
  55. used = usedStat.find((u: any) => {
  56. const uSpuId = String(u.spuId || u.spu_id || u.id);
  57. return uSpuId === spuId || uSpuId === itemId;
  58. });
  59. } else {
  60. // Object lookup
  61. used = usedStat[spuId] || usedStat[itemId] || (item.spu?.id && usedStat[item.spu.id]);
  62. }
  63. if (used) {
  64. return item.quantity - (used.quantity || 0);
  65. }
  66. return item.quantity;
  67. };
  68. const getProbability = (item: any) => {
  69. if (!box || !box.leftQuantity) return '0';
  70. const left = getLeftNum(item);
  71. const prob = (left / box.leftQuantity * 100).toFixed(4);
  72. return parseFloat(prob) === 0 ? '0' : prob;
  73. };
  74. const getLevelProbability = (level: string) => {
  75. if (!box || !box.leftQuantity) return '0%';
  76. let sumLeft = 0;
  77. products.filter(p => p.level === level).forEach(p => {
  78. sumLeft += getLeftNum(p);
  79. });
  80. const prob = (sumLeft / box.leftQuantity * 100).toFixed(4);
  81. return parseFloat(prob) === 0 ? '0%' : `${prob}%`;
  82. };
  83. if (!products || products.length === 0) return null;
  84. return (
  85. <View style={styles.container}>
  86. {levels.map((levelItem) => {
  87. const config = LEVEL_CONFIG[levelItem.level] || LEVEL_CONFIG['D'];
  88. return (
  89. <View key={levelItem.level} style={styles.levelGroup}>
  90. {/* Level Title Row */}
  91. <View style={styles.levelHeader}>
  92. <Text style={[styles.levelTitle, { color: config.bgColor }]}>{config.title}</Text>
  93. <View style={styles.levelProportion}>
  94. <Text style={styles.probabilityLabel}>概率:</Text>
  95. <Text style={styles.probabilityValue}>{getLevelProbability(levelItem.level)}</Text>
  96. </View>
  97. </View>
  98. {/* Horizontal List */}
  99. <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
  100. {levelItem.list.map((item, index) => (
  101. <TouchableOpacity
  102. key={index}
  103. style={styles.itemContainer}
  104. onPress={() => onProductClick && onProductClick(item)}
  105. activeOpacity={0.8}
  106. >
  107. <ImageBackground
  108. source={{ uri: Images.box.detail.levelBoxBg }}
  109. style={styles.itemBg}
  110. resizeMode="stretch"
  111. >
  112. {/* Cover Image */}
  113. <Image source={{ uri: item.spu.cover }} style={styles.itemImage} contentFit="contain" />
  114. {/* Bottom Banner (Probability + Quantity Badge) */}
  115. <ImageBackground
  116. source={{ uri: config.productItem }}
  117. style={styles.textBox}
  118. resizeMode="stretch"
  119. >
  120. {/* Probability centered */}
  121. <View style={styles.probTag}>
  122. <Text style={styles.probLabel}>概率:</Text>
  123. <Text style={styles.probValue}>{getProbability(item)}%</Text>
  124. </View>
  125. {/* Quantity Tag (positioned top right of the banner, overlapping image) */}
  126. <View style={[styles.countTag, { backgroundColor: config.bgColor }]}>
  127. <Text style={styles.countText}>{getLeftNum(item)}/{item.quantity}</Text>
  128. </View>
  129. </ImageBackground>
  130. </ImageBackground>
  131. </TouchableOpacity>
  132. ))}
  133. </ScrollView>
  134. </View>
  135. );
  136. })}
  137. </View>
  138. );
  139. }
  140. const styles = StyleSheet.create({
  141. // ... (previous styles)
  142. // Only changing countTag and related if needed
  143. container: {
  144. paddingHorizontal: 10,
  145. marginTop: 0,
  146. },
  147. levelGroup: {
  148. marginBottom: 20,
  149. },
  150. levelHeader: {
  151. flexDirection: 'row',
  152. justifyContent: 'center',
  153. alignItems: 'center',
  154. marginBottom: 10,
  155. position: 'relative',
  156. height: 30,
  157. },
  158. levelTitle: {
  159. fontSize: 18,
  160. fontWeight: 'bold',
  161. textShadowColor: '#000',
  162. textShadowOffset: { width: 1, height: 1 },
  163. textShadowRadius: 1,
  164. },
  165. levelProportion: {
  166. position: 'absolute',
  167. right: 0,
  168. bottom: 5,
  169. flexDirection: 'row',
  170. alignItems: 'center',
  171. },
  172. probabilityLabel: {
  173. fontSize: 12,
  174. color: '#ffc901',
  175. },
  176. probabilityValue: {
  177. fontSize: 12,
  178. color: '#fff',
  179. },
  180. scrollContent: {
  181. paddingRight: 10,
  182. },
  183. itemContainer: {
  184. width: 88,
  185. height: 110,
  186. marginRight: 10,
  187. },
  188. itemBg: {
  189. width: '100%',
  190. height: '100%',
  191. alignItems: 'center',
  192. },
  193. itemImage: {
  194. width: 88,
  195. height: 90,
  196. },
  197. textBox: {
  198. width: 88,
  199. height: 53,
  200. marginTop: -18,
  201. justifyContent: 'center',
  202. paddingTop: 12,
  203. alignItems: 'center',
  204. position: 'relative', // Ensure absolute children position relative to this
  205. },
  206. probTag: {
  207. flexDirection: 'row',
  208. alignItems: 'center',
  209. justifyContent: 'center',
  210. },
  211. probLabel: {
  212. fontSize: 8,
  213. color: '#fff',
  214. marginRight: 2,
  215. },
  216. probValue: {
  217. fontSize: 9,
  218. fontWeight: 'bold',
  219. color: '#fff',
  220. },
  221. countTag: {
  222. position: 'absolute',
  223. top: -6, // Shift up to overlap image (~ -15rpx)
  224. right: 0,
  225. paddingHorizontal: 4,
  226. borderTopLeftRadius: 4,
  227. borderBottomRightRadius: 4, // Maybe just border radius? Original had border-radius: 5rpx (2.5px)
  228. borderRadius: 2,
  229. zIndex: 10,
  230. minWidth: 30,
  231. alignItems: 'center',
  232. },
  233. countText: {
  234. color: '#fff',
  235. fontSize: 8,
  236. fontWeight: 'bold',
  237. }
  238. });