Przeglądaj źródła

10发以上翻卡片开奖效果

zbb 3 miesięcy temu
rodzic
commit
d4048b53c8

+ 181 - 0
app/lottery/components/LotteryGrid.tsx

@@ -0,0 +1,181 @@
+import { Image } from 'expo-image';
+import React, { useEffect, useRef, useState } from 'react';
+import { Animated, Dimensions, ScrollView, StyleSheet, View } from 'react-native';
+
+import { Images } from '@/constants/images';
+import TransCard from './TransCard';
+
+const { width: SCREEN_WIDTH } = Dimensions.get('window');
+// 3 columns. Uniapp: 220rpx (approx 30%). 
+// Let's use 3 columns with some spacing.
+const COLUMNS = 3;
+const ITEM_WIDTH = (SCREEN_WIDTH - 40) / COLUMNS; // 20px padding left/right
+const ITEM_HEIGHT = ITEM_WIDTH * 1.5; // Aspect ratio
+
+interface FlipCardProps {
+    item: any;
+    index: number;
+    delay: number;
+    onFlipComplete?: () => void;
+}
+
+const FlipCard = ({ item, index, delay, onFlipComplete }: FlipCardProps) => {
+    const animValue = useRef(new Animated.Value(0)).current;
+
+    useEffect(() => {
+        Animated.sequence([
+            Animated.delay(delay),
+            Animated.timing(animValue, {
+                toValue: 180, // We'll map 0-180 to the flip
+                duration: 600, // Flip duration
+                useNativeDriver: true,
+            })
+        ]).start(() => {
+            onFlipComplete && onFlipComplete();
+        });
+    }, [delay]);
+
+    // Front (Back Image) Interpolation: 0 -> 90 derived from 0->180 range
+    // Actually standard flip:
+    // Front: 0deg to 180deg (starts visible, goes back)
+    // Back: 180deg to 360deg (starts invisible back, goes front)
+    
+    // Uniapp Logic:
+    // Back (Card Back): rotateY 0deg -> 90deg (Hide)
+    // Front (Result): rotateY -90deg -> 0deg (Show)
+    
+    // Let's use animValue 0 -> 1.
+    // 0 -> 0.5: Back rotates 0 -> 90.
+    // 0.5 -> 1: Front rotates -90 -> 0.
+    
+    const backStyle = {
+        transform: [
+            {
+                rotateY: animValue.interpolate({
+                    inputRange: [0, 90],
+                    outputRange: ['0deg', '90deg'],
+                    extrapolate: 'clamp',
+                })
+            }
+        ],
+        opacity: animValue.interpolate({
+            inputRange: [0, 89, 90],
+            outputRange: [1, 1, 0], // Hide when 90 to prevent z-fighting/artifacts
+            extrapolate: 'clamp',
+        })
+    };
+
+    const frontStyle = {
+        transform: [
+            {
+                rotateY: animValue.interpolate({
+                    inputRange: [90, 180],
+                    outputRange: ['-90deg', '0deg'], // or 270 to 360
+                    extrapolate: 'clamp',
+                })
+            }
+        ],
+        opacity: animValue.interpolate({
+            inputRange: [0, 90, 91],
+            outputRange: [0, 0, 1],
+            extrapolate: 'clamp',
+        })
+    };
+
+    return (
+        <View style={styles.cardContainer}>
+            {/* Card Back (Initially Visible) */}
+            <Animated.View style={[styles.cardFace, backStyle]}>
+                <Image 
+                    source={{ uri: Images.box.back }}
+                    style={{ width: '100%', height: '100%' }}
+                    contentFit="contain"
+                />
+            </Animated.View>
+
+            {/* Result Face (Initially Hidden) */}
+            <Animated.View style={[styles.cardFace, frontStyle]}>
+                <TransCard 
+                    item={item} 
+                    width={ITEM_WIDTH - 10} // Padding inside
+                    height={ITEM_HEIGHT - 10}
+                    fill={0}
+                    imageWidth={(ITEM_WIDTH - 20) * 0.8}
+                    imageHeight={(ITEM_HEIGHT - 20) * 0.7}
+                />
+            </Animated.View>
+        </View>
+    );
+};
+
+interface LotteryGridProps {
+    results: any[];
+    onFinish: () => void;
+}
+
+export default function LotteryGrid({ results, onFinish }: LotteryGridProps) {
+    // Determine staggered delay
+    // 10 items.
+    // We want them to flip sequentially or semi-sequentially.
+    // Uniapp likely does it index based.
+    
+    const [completedCount, setCompletedCount] = useState(0);
+
+    const handleFlipComplete = () => {
+        setCompletedCount(prev => {
+            const newCount = prev + 1;
+            if (newCount === results.length) {
+                // All done
+                setTimeout(onFinish, 1000); // Wait a bit before finish
+            }
+            return newCount;
+        });
+    };
+
+    return (
+        <ScrollView style={styles.container} contentContainerStyle={styles.scrollContent}>
+            <View style={styles.grid}>
+                {results.map((item, index) => (
+                    <FlipCard 
+                        key={index} 
+                        item={item} 
+                        index={index} 
+                        delay={index * 200} // 200ms stagger
+                        onFlipComplete={handleFlipComplete}
+                    />
+                ))}
+            </View>
+        </ScrollView>
+    );
+}
+
+const styles = StyleSheet.create({
+    container: {
+        flex: 1,
+        width: '100%',
+    },
+    scrollContent: {
+        paddingTop: 20, // Reduced from 100 to avoid being too low (parent has margin)
+        paddingBottom: 150, // Increased to avoid overlapping with absolute bottom button
+    },
+    grid: {
+        flexDirection: 'row',
+        flexWrap: 'wrap',
+        justifyContent: 'center',
+    },
+    cardContainer: {
+        width: ITEM_WIDTH,
+        height: ITEM_HEIGHT,
+        alignItems: 'center',
+        justifyContent: 'center',
+        marginVertical: 10,
+    },
+    cardFace: {
+        position: 'absolute',
+        width: '100%',
+        height: '100%',
+        alignItems: 'center',
+        justifyContent: 'center',
+        backfaceVisibility: 'hidden', // Android support varies, relying on opacity/rotation logic
+    }
+});

+ 25 - 1
app/lottery/components/TransCard.tsx

@@ -53,6 +53,12 @@ export default function TransCard({
         style={styles.itemBottom}
         contentFit="fill"
       />
+      
+      <View style={styles.nameContainer}>
+          <Text style={styles.nameText} numberOfLines={1}>
+            {item.name}
+          </Text>
+      </View>
     </View>
   );
 }
@@ -76,7 +82,25 @@ const styles = StyleSheet.create({
     bottom: 0,
     left: 0,
     width: '100%',
-    height: 40, 
+    height: 20, 
     zIndex: 10,
   },
+  nameContainer: {
+      position: 'absolute',
+      top: 8, // Moved to top as requested
+      left: 0,
+      width: '100%',
+      alignItems: 'center',
+      paddingHorizontal: 2,
+      zIndex: 20,
+  },
+  nameText: {
+      color: '#fff',
+      fontSize: 8,
+      textAlign: 'center',
+      textShadowColor: 'rgba(0,0,0,0.8)',
+      textShadowOffset: { width: 1, height: 1 },
+      textShadowRadius: 1,
+      fontWeight: 'bold',
+  }
 });

+ 88 - 49
app/lottery/index.tsx

@@ -5,6 +5,7 @@ import React, { useEffect, useRef, useState } from 'react';
 import { Dimensions, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
 
+import LotteryGrid from './components/LotteryGrid';
 import LotteryReel from './components/LotteryReel';
 
 import { Images } from '@/constants/images';
@@ -21,6 +22,8 @@ export default function LotteryScreen() {
   const insets = useSafeAreaInsets();
   
   const num = Number(params.num) || 1;
+  const isGrid = num >= 10;
+
   const tradeNo = params.tradeNo as string;
   const poolId = params.poolId as string;
 
@@ -28,6 +31,7 @@ export default function LotteryScreen() {
   const [pool, setPool] = useState<any[]>([]);
   const [loading, setLoading] = useState(true);
   const [sound, setSound] = useState<Audio.Sound>();
+  const [animationFinished, setAnimationFinished] = useState(false);
   
   // Timer Ref for cleanup
   const timerRef = useRef<any>(null);
@@ -78,22 +82,24 @@ export default function LotteryScreen() {
              setResults(list);
              
              // 2. Fetch Pool (Goods) if not passed
-             // In a real app we might pass this via context or params if small
-             // For now, fetch detail to get the goods list
-             if (poolId) {
+             if (poolId && !isGrid) {
                  const detailRes = await services.award.getPoolDetail(poolId);
                  const goods = detailRes?.luckGoodsList || [];
                  setPool(goods);
-             } else {
-                 // Fallback if no poolId, just use result as pool (not ideal as it's small)
+             } else if (!isGrid) {
                  setPool(list);
              }
     
              // Auto show result after animation
              if (timerRef.current) clearTimeout(timerRef.current);
-             timerRef.current = setTimeout(() => {
-                 handleFinish(list);
-             }, 2800); // 2000ms duration + buffer
+             
+             if (!isGrid) {
+                // Reel mode uses timer
+                timerRef.current = setTimeout(() => {
+                    handleFinish(list);
+                }, 2800); 
+             }
+             // Grid mode handles finish via callback
          } catch (error) {
              console.error('Load Data Error', error);
              // Safety fallback?
@@ -106,6 +112,14 @@ export default function LotteryScreen() {
         clearTimeout(timerRef.current);
         timerRef.current = null;
     }
+    
+    if (isGrid) {
+        // Stop sound and mark finished, stay on page
+        sound?.stopAsync();
+        setAnimationFinished(true);
+        return;
+    }
+
     // Navigate to result modal/screen
     const data = finalResults || results;
     // We can navigate to a result page or show a local modal components
@@ -120,8 +134,21 @@ export default function LotteryScreen() {
   };
   
   const handleSkip = () => {
-      sound?.stopAsync();
-      handleFinish();
+      if (isGrid) {
+          if (animationFinished) {
+              // User clicked "Claim Prize", exit
+              router.back(); 
+          } else {
+              // User clicked "Skip Animation", finish immediately
+              // Ideally LotteryGrid should expose a "finish" method or we force state
+              // But for now calling handleFinish stops navigation
+              handleFinish();
+              // Note: This doesn't force cards to flip instantly unless we implement that prop in LotteryGrid
+          }
+      } else {
+          sound?.stopAsync();
+          handleFinish();
+      }
   };
 
   if (loading) return (
@@ -137,49 +164,61 @@ export default function LotteryScreen() {
       <View style={styles.maskPage} />
 
       <View style={[styles.wrapper, { paddingTop: padding }]}>
-         <View style={styles.reelsContainer}>
-            {/* Left Column Masks */}
-            <View style={[styles.maskLeft, { width: maskWidth }]} />
-            
-            {/* Reels */}
-            <View style={num === 1 ? styles.height1 : styles.height5}>
-                {results.map((item, index) => (
-                    <View key={index} style={styles.reelRow}> 
-                        <LotteryReel 
-                            key={`reel-${index}-${pool.length}-${results.length}`} 
-                            pool={pool.length > 0 ? pool : results} 
-                            result={item}
-                            width={SCREEN_WIDTH} 
-                            itemWidth={itemWidth}
-                            index={index}
-                            delay={index * 30} // Very fast stagger
-                            duration={2000} // Faster spin (was 3000)
-                        />
-                    </View>
-                ))}
-            </View>
+         {isGrid ? (
+             <LotteryGrid 
+                results={results} 
+                onFinish={() => handleFinish()} 
+             />
+         ) : (
+             <View style={styles.reelsContainer}>
+                {/* Left Column Masks */}
+                <View style={[styles.maskLeft, { width: maskWidth }]} />
+                
+                {/* Reels */}
+                <View style={num === 1 ? styles.height1 : styles.height5}>
+                    {results.map((item, index) => (
+                        <View key={index} style={styles.reelRow}> 
+                            <LotteryReel 
+                                key={`reel-${index}-${pool.length}-${results.length}`} 
+                                pool={pool.length > 0 ? pool : results} 
+                                result={item}
+                                width={SCREEN_WIDTH} 
+                                itemWidth={itemWidth}
+                                index={index}
+                                delay={index * 30} // Very fast stagger
+                                duration={2000} // Faster spin (was 3000)
+                            />
+                        </View>
+                    ))}
+                </View>
 
-            {/* Right Column Masks */}
-            <View style={[styles.maskRight, { width: maskWidth }]} />
-            
-            {/* Middle Frame */}
-            <Image 
-                source={{ uri: num === 1 ? Images.resource.lottery_middle_s : Images.resource.lottery_middle_l }}
-                style={[
-                    styles.middleImage, 
-                    { 
-                        height: num === 1 ? 200 : 540, 
-                        top: num === 1 ? -35 : -10,
-                    }
-                ]}
-                contentFit="contain"
-            />
-         </View>
+                {/* Right Column Masks */}
+                <View style={[styles.maskRight, { width: maskWidth }]} />
+                
+                {/* Middle Frame */}
+                <Image 
+                    source={{ uri: num === 1 ? Images.resource.lottery_middle_s : Images.resource.lottery_middle_l }}
+                    style={[
+                        styles.middleImage, 
+                        { 
+                            height: num === 1 ? 200 : 540, 
+                            top: num === 1 ? -35 : -10,
+                        }
+                    ]}
+                    contentFit="contain"
+                />
+             </View>
+         )}
       </View>
 
       <View style={styles.bottom}>
-        <TouchableOpacity style={styles.skipBtn} onPress={handleSkip}>
-            <Text style={styles.skipText}>跳过动画</Text>
+        <TouchableOpacity 
+            style={styles.skipBtn} 
+            onPress={handleSkip}
+        >
+            <Text style={styles.skipText}>
+                {isGrid && animationFinished ? '收下奖品' : '跳过动画'}
+            </Text>
         </TouchableOpacity>
       </View>
     </View>

+ 1 - 0
constants/images.ts

@@ -85,6 +85,7 @@ export const Images = {
     sortAmount: `${CDN_BASE}/box/sortAmount.png`,
     sortAmountOnT: `${CDN_BASE}/box/sortAmountOnT.png`,
     sortAmountOnB: `${CDN_BASE}/box/sortAmountOnB.png`,
+    back: `${CDN_BASE}/box/back1.png`,
     // Result Backgrounds
     resultBgA: `${CDN_SUPERMART}/supermart/box/resultBgA.png`,
     resultBgB: `${CDN_SUPERMART}/supermart/box/resultBgB.png`,