ProductList.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import { Image } from 'expo-image';
  2. import { useRouter } from 'expo-router';
  3. import React from 'react';
  4. import { ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
  5. import { Images } from '@/constants/images';
  6. interface ProductItem {
  7. id: string;
  8. name: string;
  9. cover: string;
  10. level: string;
  11. probability: number;
  12. price?: number;
  13. quantity?: number;
  14. spu?: {
  15. id: string;
  16. cover: string;
  17. name: string;
  18. };
  19. }
  20. interface ProductListProps {
  21. products: ProductItem[];
  22. levelList?: any[];
  23. poolId: string;
  24. price: number;
  25. }
  26. // 等级配置 - 对应小程序的 LEVEL_MAP
  27. const LEVEL_CONFIG: Record<string, { title: string; color: string; bgColor: string; productItem: string }> = {
  28. A: { title: '超神款', color: '#fff', bgColor: '#FF4444', productItem: Images.box.detail.productItemA },
  29. B: { title: '欧皇款', color: '#fff', bgColor: '#FF9900', productItem: Images.box.detail.productItemB },
  30. C: { title: '隐藏款', color: '#fff', bgColor: '#9966FF', productItem: Images.box.detail.productItemC },
  31. D: { title: '普通款', color: '#fff', bgColor: '#00CCFF', productItem: Images.box.detail.productItemD },
  32. };
  33. const ignoreRatio0 = (val: number) => {
  34. // Revert to original logic: Do NOT multiply. Assume API sends WYSIWYG value (e.g. 99.05 or 0.001)
  35. // Just format string to remove trailing zeros.
  36. let str = String(val);
  37. // Match original logic: strip trailing zeros if decimal
  38. if (str.indexOf('.') > -1) {
  39. str = str.replace(/0+$/, '').replace(/\.$/, '');
  40. }
  41. return str;
  42. };
  43. export const ProductList: React.FC<ProductListProps> = ({ products, levelList, poolId }) => {
  44. const router = useRouter();
  45. // 按等级分组
  46. const groupedProducts = products.reduce(
  47. (acc, item) => {
  48. const level = item.level || 'D';
  49. if (!acc[level]) acc[level] = [];
  50. acc[level].push(item);
  51. return acc;
  52. },
  53. {} as Record<string, ProductItem[]>
  54. );
  55. // 计算各等级概率
  56. const getLevelProbability = (level: string) => {
  57. const item = levelList?.find((e) => e.level === level);
  58. return item ? `${item.probability}%` : '0%';
  59. };
  60. // 点击产品跳转到详情页
  61. const handleProductPress = (item: ProductItem) => {
  62. // Look up by object reference to handle duplicate IDs correctly
  63. const index = products.indexOf(item);
  64. router.push({
  65. pathname: '/award-detail/swipe' as any,
  66. params: { poolId, index: index >= 0 ? index : 0 },
  67. });
  68. };
  69. const renderLevelSection = (level: string, items: ProductItem[]) => {
  70. const config = LEVEL_CONFIG[level] || LEVEL_CONFIG['D'];
  71. return (
  72. <View key={level} style={styles.levelBox}>
  73. {/* 等级标题行 */}
  74. <View style={styles.levelTitleRow}>
  75. <Text style={[styles.levelTitle, { color: config.bgColor }]}>{config.title}</Text>
  76. <View style={styles.levelProportion}>
  77. <Text style={styles.probabilityLabel}>概率:</Text>
  78. <Text style={styles.probabilityValue}>{getLevelProbability(level)}</Text>
  79. </View>
  80. </View>
  81. {/* 商品横向滚动列表 */}
  82. <ScrollView
  83. horizontal
  84. showsHorizontalScrollIndicator={false}
  85. contentContainerStyle={styles.scrollContent}
  86. >
  87. {items.map((item, index) => {
  88. const cover = item.spu?.cover || item.cover;
  89. return (
  90. <TouchableOpacity
  91. key={item.id || index}
  92. onPress={() => handleProductPress(item)}
  93. activeOpacity={0.8}
  94. >
  95. <ImageBackground
  96. source={{ uri: Images.box.detail.levelBoxBg }}
  97. style={styles.productItem}
  98. resizeMode="stretch"
  99. >
  100. {/* 商品图片 */}
  101. <Image
  102. source={{ uri: cover }}
  103. style={styles.productImage}
  104. contentFit="contain"
  105. />
  106. {/* 概率标签背景 */}
  107. <ImageBackground
  108. source={{ uri: config.productItem }}
  109. style={styles.levelTagBg}
  110. resizeMode="stretch"
  111. >
  112. <View style={styles.levelTagContent}>
  113. <Text style={styles.levelTagLabel}>概率:</Text>
  114. <Text style={styles.levelTagText}>{ignoreRatio0(item.probability)}%</Text>
  115. </View>
  116. </ImageBackground>
  117. </ImageBackground>
  118. </TouchableOpacity>
  119. );
  120. })}
  121. </ScrollView>
  122. </View>
  123. );
  124. };
  125. const levelOrder = ['A', 'B', 'C', 'D'];
  126. return (
  127. <View style={styles.container}>
  128. {/* 标题 */}
  129. <View style={styles.titleBox}>
  130. <Image source={{ uri: Images.box.detail.productTitle }} style={styles.titleImg} contentFit="contain" />
  131. </View>
  132. {levelOrder.map((level) => {
  133. const items = groupedProducts[level];
  134. if (!items || items.length === 0) return null;
  135. return renderLevelSection(level, items);
  136. })}
  137. </View>
  138. );
  139. };
  140. const styles = StyleSheet.create({
  141. container: {
  142. paddingHorizontal: 10,
  143. marginTop: -40,
  144. },
  145. titleBox: {
  146. alignItems: 'center',
  147. marginBottom: 15,
  148. },
  149. titleImg: {
  150. width: 121,
  151. height: 29,
  152. },
  153. levelBox: {
  154. marginBottom: 20,
  155. },
  156. levelTitleRow: {
  157. flexDirection: 'row',
  158. alignItems: 'center',
  159. justifyContent: 'center',
  160. paddingHorizontal: 14,
  161. paddingBottom: 10,
  162. position: 'relative',
  163. },
  164. levelTitle: {
  165. fontSize: 18,
  166. fontWeight: 'bold',
  167. textAlign: 'center',
  168. textShadowColor: '#000',
  169. textShadowOffset: { width: 1, height: 1 },
  170. textShadowRadius: 1,
  171. },
  172. levelProportion: {
  173. position: 'absolute',
  174. right: 0,
  175. bottom: 10,
  176. flexDirection: 'row',
  177. alignItems: 'center',
  178. },
  179. probabilityLabel: {
  180. fontSize: 12,
  181. color: '#ffc901',
  182. },
  183. probabilityValue: {
  184. fontSize: 12,
  185. color: '#fff',
  186. },
  187. scrollContent: {
  188. paddingHorizontal: 5,
  189. },
  190. productItem: {
  191. width: 88,
  192. height: 110,
  193. marginRight: 10,
  194. alignItems: 'center',
  195. },
  196. productImage: {
  197. width: 88,
  198. height: 90,
  199. },
  200. levelTagBg: {
  201. width: 88,
  202. height: 53,
  203. marginTop: -18,
  204. justifyContent: 'center',
  205. paddingTop: 12,
  206. },
  207. levelTagContent: {
  208. flexDirection: 'row',
  209. justifyContent: 'center',
  210. alignItems: 'center',
  211. },
  212. levelTagLabel: {
  213. fontSize: 8,
  214. color: '#fff',
  215. },
  216. levelTagText: {
  217. fontSize: 9,
  218. color: '#fff',
  219. fontWeight: 'bold',
  220. },
  221. });