ProductListYfs.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import { LEVEL_MAP } from '@/constants/config';
  2. import { Images } from '@/constants/images';
  3. import { Image, ImageBackground } from 'expo-image';
  4. import React, { useMemo } from 'react';
  5. import { Dimensions, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
  6. const { width } = Dimensions.get('window');
  7. interface ProductListYfsProps {
  8. products: any[];
  9. poolId: string;
  10. box: any;
  11. onProductClick?: (product: any) => void;
  12. }
  13. export default function ProductListYfs({ products = [], poolId, box, onProductClick }: ProductListYfsProps) {
  14. const levels = useMemo(() => {
  15. console.log('ProductListYfs products:', products ? products.length : 'null');
  16. const grouped: Record<string, any[]> = { A: [], B: [], C: [], D: [] };
  17. products.forEach(p => {
  18. console.log('Processing item level:', p.level);
  19. if (grouped[p.level]) {
  20. grouped[p.level].push(p);
  21. } else {
  22. console.log('Unknown level:', p.level);
  23. }
  24. });
  25. const result = [
  26. { level: 'A', list: grouped.A, ...LEVEL_MAP.A },
  27. { level: 'B', list: grouped.B, ...LEVEL_MAP.B },
  28. { level: 'C', list: grouped.C, ...LEVEL_MAP.C },
  29. { level: 'D', list: grouped.D, ...LEVEL_MAP.D },
  30. ].filter(g => g.list && g.list.length > 0);
  31. console.log('ProductListYfs levels:', result.length);
  32. return result;
  33. }, [products]);
  34. const getLeftNum = (item: any) => {
  35. if (!box || !box.usedStat || !box.usedStat[item.spu.id]) {
  36. return item.quantity;
  37. }
  38. return item.quantity - (box.usedStat[item.spu.id].quantity || 0);
  39. };
  40. const getProbability = (item: any) => {
  41. if (!box || !box.leftQuantity) return '0%';
  42. const left = getLeftNum(item);
  43. const prob = (left / box.leftQuantity * 100).toFixed(4);
  44. return parseFloat(prob) === 0 ? '0%' : `${prob}%`;
  45. };
  46. const getLevelProbability = (level: string) => {
  47. if (!box || !box.leftQuantity) return '0%';
  48. let sumLeft = 0;
  49. products.filter(p => p.level === level).forEach(p => {
  50. sumLeft += getLeftNum(p);
  51. });
  52. const prob = (sumLeft / box.leftQuantity * 100).toFixed(4);
  53. return parseFloat(prob) === 0 ? '0%' : `${prob}%`;
  54. };
  55. if (!products || products.length === 0) return null;
  56. return (
  57. <View style={styles.container}>
  58. {levels.map((levelItem) => (
  59. <View key={levelItem.level} style={styles.levelGroup}>
  60. {/* Header with Title and Probability */}
  61. <View style={styles.levelHeader}>
  62. <Text style={[styles.levelTitle, { color: levelItem.color }]}>{levelItem.title}</Text>
  63. <Text style={styles.levelProb}>{getLevelProbability(levelItem.level)}</Text>
  64. {/* The legacy code used borders and backgrounds for titles, but for now text color + shadow mimics the look well enough or we can add ImageBackground if needed.
  65. Legacy: :style="{ color: LEVEL_MAP[levelItem.level].color }" and centered.
  66. */}
  67. </View>
  68. {/* List Container - Legacy uses levelBoxBg for EACH item container?
  69. Legacy:
  70. <view class="levelListScroll"> ... <scroll-view ...>
  71. <view ... class="item" :style="{ backgroundImage: 'url(' + ossurl.box.detail.levelBoxBg + ')' }">
  72. */}
  73. <View style={styles.listWrapper}>
  74. <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
  75. {levelItem.list.map((item, index) => (
  76. <TouchableOpacity
  77. key={index}
  78. style={styles.itemContainer}
  79. onPress={() => onProductClick && onProductClick(item)}
  80. >
  81. {/* Item Background (levelBoxBg) */}
  82. <ImageBackground
  83. source={{ uri: Images.box.detail.levelBoxBg }}
  84. style={styles.itemBg}
  85. resizeMode="stretch"
  86. >
  87. <Image source={{ uri: item.spu.cover }} style={styles.itemImage} contentFit="contain" />
  88. {/* Info Box (productItemA/B/C/D) */}
  89. <ImageBackground
  90. source={{ uri: levelItem.productItem }}
  91. style={styles.textBox}
  92. resizeMode="stretch"
  93. >
  94. <View style={styles.probTag}>
  95. <Text style={[styles.probLabel, { color: levelItem.color }]}>概率:</Text>
  96. <Text style={styles.probValue}>{getProbability(item)}</Text>
  97. </View>
  98. <View style={[styles.countTag, { backgroundColor: levelItem.color }]}>
  99. <Text style={styles.countText}>{getLeftNum(item)}/{item.quantity}</Text>
  100. </View>
  101. </ImageBackground>
  102. </ImageBackground>
  103. </TouchableOpacity>
  104. ))}
  105. </ScrollView>
  106. </View>
  107. </View>
  108. ))}
  109. </View>
  110. );
  111. }
  112. const styles = StyleSheet.create({
  113. container: {
  114. paddingHorizontal: 10,
  115. paddingVertical: 10,
  116. },
  117. levelGroup: {
  118. marginBottom: 5,
  119. },
  120. levelHeader: {
  121. flexDirection: 'row',
  122. justifyContent: 'center',
  123. alignItems: 'center',
  124. marginBottom: 5,
  125. position: 'relative',
  126. height: 40,
  127. },
  128. levelTitle: {
  129. fontSize: 18,
  130. fontWeight: 'bold',
  131. textShadowColor: '#000',
  132. textShadowOffset: { width: 1, height: 1 },
  133. textShadowRadius: 1,
  134. },
  135. levelProb: {
  136. position: 'absolute',
  137. right: 15,
  138. bottom: 5,
  139. color: '#fff',
  140. fontSize: 12,
  141. fontFamily: 'System', // Prevent potential font missing issues
  142. },
  143. listWrapper: {
  144. paddingBottom: 10,
  145. },
  146. scrollContent: {
  147. paddingRight: 10,
  148. },
  149. itemContainer: {
  150. width: 88, // 175rpx / 2
  151. height: 110, // 220rpx / 2
  152. marginRight: 8,
  153. },
  154. itemBg: {
  155. width: '100%',
  156. height: '100%',
  157. position: 'relative',
  158. },
  159. itemImage: {
  160. width: 88,
  161. height: 90,
  162. position: 'absolute',
  163. top: 0,
  164. left: 0,
  165. zIndex: 1,
  166. },
  167. textBox: {
  168. width: '100%',
  169. height: 53, // 106rpx / 2
  170. position: 'absolute',
  171. bottom: 0, // Adjusted from top: -30rpx legacy logic which was weird, simplified for RN
  172. zIndex: 2,
  173. justifyContent: 'flex-start',
  174. paddingTop: 18, // Push content down below the curve
  175. alignItems: 'center',
  176. },
  177. probTag: {
  178. flexDirection: 'row',
  179. alignItems: 'center',
  180. },
  181. probLabel: {
  182. fontSize: 8,
  183. marginRight: 2,
  184. },
  185. probValue: {
  186. fontSize: 8,
  187. fontWeight: 'bold',
  188. color: '#333'
  189. },
  190. countTag: {
  191. position: 'absolute',
  192. top: 0,
  193. right: 0,
  194. paddingHorizontal: 4,
  195. borderRadius: 2,
  196. zIndex: 3,
  197. },
  198. countText: {
  199. color: '#fff',
  200. fontSize: 9,
  201. }
  202. });