|
|
@@ -1,8 +1,7 @@
|
|
|
-import { LEVEL_MAP } from '@/constants/config';
|
|
|
import { Images } from '@/constants/images';
|
|
|
-import { Image, ImageBackground } from 'expo-image';
|
|
|
+import { Image } from 'expo-image';
|
|
|
import React, { useMemo } from 'react';
|
|
|
-import { Dimensions, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
|
+import { Dimensions, ImageBackground, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
|
|
|
|
|
const { width } = Dimensions.get('window');
|
|
|
|
|
|
@@ -13,43 +12,78 @@ interface ProductListYfsProps {
|
|
|
onProductClick?: (product: any) => void;
|
|
|
}
|
|
|
|
|
|
+// Level Configuration - reused from ProductList logic
|
|
|
+const LEVEL_CONFIG: Record<string, { title: string; color: string; bgColor: string; productItem: string }> = {
|
|
|
+ A: { title: '超神款', color: '#fff', bgColor: '#FF4444', productItem: Images.box.detail.productItemA },
|
|
|
+ B: { title: '欧皇款', color: '#fff', bgColor: '#FF9900', productItem: Images.box.detail.productItemB },
|
|
|
+ C: { title: '隐藏款', color: '#fff', bgColor: '#9966FF', productItem: Images.box.detail.productItemC },
|
|
|
+ D: { title: '普通款', color: '#fff', bgColor: '#00CCFF', productItem: Images.box.detail.productItemD },
|
|
|
+};
|
|
|
+
|
|
|
export default function ProductListYfs({ products = [], poolId, box, onProductClick }: ProductListYfsProps) {
|
|
|
|
|
|
const levels = useMemo(() => {
|
|
|
- console.log('ProductListYfs products:', products ? products.length : 'null');
|
|
|
+ // Group by level
|
|
|
const grouped: Record<string, any[]> = { A: [], B: [], C: [], D: [] };
|
|
|
products.forEach(p => {
|
|
|
- console.log('Processing item level:', p.level);
|
|
|
- if (grouped[p.level]) {
|
|
|
- grouped[p.level].push(p);
|
|
|
- } else {
|
|
|
- console.log('Unknown level:', p.level);
|
|
|
+ const level = p.level || 'D';
|
|
|
+ if (grouped[level]) {
|
|
|
+ grouped[level].push(p);
|
|
|
}
|
|
|
});
|
|
|
|
|
|
const result = [
|
|
|
- { level: 'A', list: grouped.A, ...LEVEL_MAP.A },
|
|
|
- { level: 'B', list: grouped.B, ...LEVEL_MAP.B },
|
|
|
- { level: 'C', list: grouped.C, ...LEVEL_MAP.C },
|
|
|
- { level: 'D', list: grouped.D, ...LEVEL_MAP.D },
|
|
|
+ { level: 'A', list: grouped.A },
|
|
|
+ { level: 'B', list: grouped.B },
|
|
|
+ { level: 'C', list: grouped.C },
|
|
|
+ { level: 'D', list: grouped.D },
|
|
|
].filter(g => g.list && g.list.length > 0);
|
|
|
|
|
|
- console.log('ProductListYfs levels:', result.length);
|
|
|
return result;
|
|
|
}, [products]);
|
|
|
|
|
|
const getLeftNum = (item: any) => {
|
|
|
- if (!box || !box.usedStat || !box.usedStat[item.spu.id]) {
|
|
|
- return item.quantity;
|
|
|
+ // Robust check for box and usedStat (camelCase or snake_case)
|
|
|
+ const usedStat = box?.usedStat || box?.used_stat;
|
|
|
+
|
|
|
+ if (!box || !usedStat) {
|
|
|
+ return item.quantity;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Try multiple key variations for robustness
|
|
|
+ const spuId = String(item.spu?.id || item.spu_id);
|
|
|
+ const itemId = String(item.id);
|
|
|
+
|
|
|
+ let used: any = null;
|
|
|
+
|
|
|
+ // Check if usedStat is an Array (based on logs showing keys 0,1,2...)
|
|
|
+ if (Array.isArray(usedStat)) {
|
|
|
+ // Debug log to see structure of array items once
|
|
|
+ if (!global.hasLoggedUsedStatStructure) {
|
|
|
+ console.log('[DEBUG-ICHIBAN] usedStat is Array. First item:', usedStat[0]);
|
|
|
+ global.hasLoggedUsedStatStructure = true;
|
|
|
+ }
|
|
|
+ // Search in array
|
|
|
+ used = usedStat.find((u: any) => {
|
|
|
+ const uSpuId = String(u.spuId || u.spu_id || u.id);
|
|
|
+ return uSpuId === spuId || uSpuId === itemId;
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // Object lookup
|
|
|
+ used = usedStat[spuId] || usedStat[itemId] || (item.spu?.id && usedStat[item.spu.id]);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (used) {
|
|
|
+ return item.quantity - (used.quantity || 0);
|
|
|
}
|
|
|
- return item.quantity - (box.usedStat[item.spu.id].quantity || 0);
|
|
|
+ return item.quantity;
|
|
|
};
|
|
|
|
|
|
const getProbability = (item: any) => {
|
|
|
- if (!box || !box.leftQuantity) return '0%';
|
|
|
+ if (!box || !box.leftQuantity) return '0';
|
|
|
const left = getLeftNum(item);
|
|
|
const prob = (left / box.leftQuantity * 100).toFixed(4);
|
|
|
- return parseFloat(prob) === 0 ? '0%' : `${prob}%`;
|
|
|
+ return parseFloat(prob) === 0 ? '0' : prob;
|
|
|
};
|
|
|
|
|
|
const getLevelProbability = (level: string) => {
|
|
|
@@ -66,50 +100,51 @@ export default function ProductListYfs({ products = [], poolId, box, onProductCl
|
|
|
|
|
|
return (
|
|
|
<View style={styles.container}>
|
|
|
- {levels.map((levelItem) => (
|
|
|
- <View key={levelItem.level} style={styles.levelGroup}>
|
|
|
- {/* Header with Title and Probability */}
|
|
|
- <View style={styles.levelHeader}>
|
|
|
- <Text style={[styles.levelTitle, { color: levelItem.color }]}>{levelItem.title}</Text>
|
|
|
- <Text style={styles.levelProb}>{getLevelProbability(levelItem.level)}</Text>
|
|
|
-
|
|
|
- {/* 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.
|
|
|
- Legacy: :style="{ color: LEVEL_MAP[levelItem.level].color }" and centered.
|
|
|
- */}
|
|
|
- </View>
|
|
|
-
|
|
|
- {/* List Container - Legacy uses levelBoxBg for EACH item container?
|
|
|
- Legacy:
|
|
|
- <view class="levelListScroll"> ... <scroll-view ...>
|
|
|
- <view ... class="item" :style="{ backgroundImage: 'url(' + ossurl.box.detail.levelBoxBg + ')' }">
|
|
|
- */}
|
|
|
- <View style={styles.listWrapper}>
|
|
|
+ {levels.map((levelItem) => {
|
|
|
+ const config = LEVEL_CONFIG[levelItem.level] || LEVEL_CONFIG['D'];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <View key={levelItem.level} style={styles.levelGroup}>
|
|
|
+ {/* Level Title Row */}
|
|
|
+ <View style={styles.levelHeader}>
|
|
|
+ <Text style={[styles.levelTitle, { color: config.bgColor }]}>{config.title}</Text>
|
|
|
+ <View style={styles.levelProportion}>
|
|
|
+ <Text style={styles.probabilityLabel}>概率:</Text>
|
|
|
+ <Text style={styles.probabilityValue}>{getLevelProbability(levelItem.level)}</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+
|
|
|
+ {/* Horizontal List */}
|
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.scrollContent}>
|
|
|
{levelItem.list.map((item, index) => (
|
|
|
<TouchableOpacity
|
|
|
key={index}
|
|
|
style={styles.itemContainer}
|
|
|
onPress={() => onProductClick && onProductClick(item)}
|
|
|
+ activeOpacity={0.8}
|
|
|
>
|
|
|
- {/* Item Background (levelBoxBg) */}
|
|
|
<ImageBackground
|
|
|
source={{ uri: Images.box.detail.levelBoxBg }}
|
|
|
style={styles.itemBg}
|
|
|
resizeMode="stretch"
|
|
|
>
|
|
|
+ {/* Cover Image */}
|
|
|
<Image source={{ uri: item.spu.cover }} style={styles.itemImage} contentFit="contain" />
|
|
|
|
|
|
- {/* Info Box (productItemA/B/C/D) */}
|
|
|
+ {/* Bottom Banner (Probability + Quantity Badge) */}
|
|
|
<ImageBackground
|
|
|
- source={{ uri: levelItem.productItem }}
|
|
|
+ source={{ uri: config.productItem }}
|
|
|
style={styles.textBox}
|
|
|
resizeMode="stretch"
|
|
|
>
|
|
|
+ {/* Probability centered */}
|
|
|
<View style={styles.probTag}>
|
|
|
- <Text style={[styles.probLabel, { color: levelItem.color }]}>概率:</Text>
|
|
|
- <Text style={styles.probValue}>{getProbability(item)}</Text>
|
|
|
+ <Text style={styles.probLabel}>概率:</Text>
|
|
|
+ <Text style={styles.probValue}>{getProbability(item)}%</Text>
|
|
|
</View>
|
|
|
- <View style={[styles.countTag, { backgroundColor: levelItem.color }]}>
|
|
|
+
|
|
|
+ {/* Quantity Tag (positioned top right of the banner, overlapping image) */}
|
|
|
+ <View style={[styles.countTag, { backgroundColor: config.bgColor }]}>
|
|
|
<Text style={styles.countText}>{getLeftNum(item)}/{item.quantity}</Text>
|
|
|
</View>
|
|
|
</ImageBackground>
|
|
|
@@ -117,28 +152,30 @@ export default function ProductListYfs({ products = [], poolId, box, onProductCl
|
|
|
</TouchableOpacity>
|
|
|
))}
|
|
|
</ScrollView>
|
|
|
- </View>
|
|
|
- </View>
|
|
|
- ))}
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+ })}
|
|
|
</View>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
+ // ... (previous styles)
|
|
|
+ // Only changing countTag and related if needed
|
|
|
container: {
|
|
|
paddingHorizontal: 10,
|
|
|
- paddingVertical: 10,
|
|
|
+ marginTop: 0,
|
|
|
},
|
|
|
levelGroup: {
|
|
|
- marginBottom: 5,
|
|
|
+ marginBottom: 20,
|
|
|
},
|
|
|
levelHeader: {
|
|
|
flexDirection: 'row',
|
|
|
justifyContent: 'center',
|
|
|
alignItems: 'center',
|
|
|
- marginBottom: 5,
|
|
|
+ marginBottom: 10,
|
|
|
position: 'relative',
|
|
|
- height: 40,
|
|
|
+ height: 30,
|
|
|
},
|
|
|
levelTitle: {
|
|
|
fontSize: 18,
|
|
|
@@ -147,71 +184,77 @@ const styles = StyleSheet.create({
|
|
|
textShadowOffset: { width: 1, height: 1 },
|
|
|
textShadowRadius: 1,
|
|
|
},
|
|
|
- levelProb: {
|
|
|
+ levelProportion: {
|
|
|
position: 'absolute',
|
|
|
- right: 15,
|
|
|
+ right: 0,
|
|
|
bottom: 5,
|
|
|
- color: '#fff',
|
|
|
+ flexDirection: 'row',
|
|
|
+ alignItems: 'center',
|
|
|
+ },
|
|
|
+ probabilityLabel: {
|
|
|
fontSize: 12,
|
|
|
- fontFamily: 'System', // Prevent potential font missing issues
|
|
|
+ color: '#ffc901',
|
|
|
},
|
|
|
- listWrapper: {
|
|
|
- paddingBottom: 10,
|
|
|
+ probabilityValue: {
|
|
|
+ fontSize: 12,
|
|
|
+ color: '#fff',
|
|
|
},
|
|
|
scrollContent: {
|
|
|
paddingRight: 10,
|
|
|
},
|
|
|
itemContainer: {
|
|
|
- width: 88, // 175rpx / 2
|
|
|
- height: 110, // 220rpx / 2
|
|
|
- marginRight: 8,
|
|
|
+ width: 88,
|
|
|
+ height: 110,
|
|
|
+ marginRight: 10,
|
|
|
},
|
|
|
itemBg: {
|
|
|
width: '100%',
|
|
|
height: '100%',
|
|
|
- position: 'relative',
|
|
|
+ alignItems: 'center',
|
|
|
},
|
|
|
itemImage: {
|
|
|
width: 88,
|
|
|
height: 90,
|
|
|
- position: 'absolute',
|
|
|
- top: 0,
|
|
|
- left: 0,
|
|
|
- zIndex: 1,
|
|
|
},
|
|
|
textBox: {
|
|
|
- width: '100%',
|
|
|
- height: 53, // 106rpx / 2
|
|
|
- position: 'absolute',
|
|
|
- bottom: 0, // Adjusted from top: -30rpx legacy logic which was weird, simplified for RN
|
|
|
- zIndex: 2,
|
|
|
- justifyContent: 'flex-start',
|
|
|
- paddingTop: 18, // Push content down below the curve
|
|
|
+ width: 88,
|
|
|
+ height: 53,
|
|
|
+ marginTop: -18,
|
|
|
+ justifyContent: 'center',
|
|
|
+ paddingTop: 12,
|
|
|
alignItems: 'center',
|
|
|
+ position: 'relative', // Ensure absolute children position relative to this
|
|
|
},
|
|
|
probTag: {
|
|
|
flexDirection: 'row',
|
|
|
alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
},
|
|
|
probLabel: {
|
|
|
fontSize: 8,
|
|
|
+ color: '#fff',
|
|
|
marginRight: 2,
|
|
|
},
|
|
|
probValue: {
|
|
|
- fontSize: 8,
|
|
|
+ fontSize: 9,
|
|
|
fontWeight: 'bold',
|
|
|
- color: '#333'
|
|
|
+ color: '#fff',
|
|
|
},
|
|
|
countTag: {
|
|
|
position: 'absolute',
|
|
|
- top: 0,
|
|
|
+ top: -6, // Shift up to overlap image (~ -15rpx)
|
|
|
right: 0,
|
|
|
paddingHorizontal: 4,
|
|
|
+ borderTopLeftRadius: 4,
|
|
|
+ borderBottomRightRadius: 4, // Maybe just border radius? Original had border-radius: 5rpx (2.5px)
|
|
|
borderRadius: 2,
|
|
|
- zIndex: 3,
|
|
|
+ zIndex: 10,
|
|
|
+ minWidth: 30,
|
|
|
+ alignItems: 'center',
|
|
|
},
|
|
|
countText: {
|
|
|
color: '#fff',
|
|
|
- fontSize: 9,
|
|
|
+ fontSize: 8,
|
|
|
+ fontWeight: 'bold',
|
|
|
}
|
|
|
});
|