ProductList.tsx 6.7 KB

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