| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- import { Image } from 'expo-image';
- import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
- import {
- ActivityIndicator,
- Animated,
- Dimensions,
- ImageBackground,
- Modal,
- ScrollView,
- StyleSheet,
- Text,
- TouchableOpacity,
- View
- } from 'react-native';
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
- import { convertApply, getApplyResult } from '@/services/award';
- const { width: SCREEN_WIDTH } = Dimensions.get('window');
- const CARD_WIDTH = (SCREEN_WIDTH - 60) / 3;
- const CARD_HEIGHT = CARD_WIDTH * 1.5;
- const CDN_BASE = 'https://cdn.acetoys.cn';
- const imgUrl = `${CDN_BASE}/kai_xin_ma_te/supermart`;
- const imgUrlSupermart = `${CDN_BASE}/supermart`;
- const LEVEL_MAP: Record<string, { title: string; color: string; rgba: string; resultBg: string; borderImg: string }> = {
- A: {
- title: '超神款',
- color: '#F62C71',
- rgba: 'rgba(246, 44, 113, 1)',
- resultBg: `${imgUrlSupermart}/supermart/box/resultBgA.png`,
- borderImg: `${imgUrlSupermart}/supermart/box/borderImgA.png`
- },
- B: {
- title: '欧皇款',
- color: '#E9C525',
- rgba: 'rgba(233,197,37, 1)',
- resultBg: `${imgUrlSupermart}/supermart/box/resultBgB.png`,
- borderImg: `${imgUrlSupermart}/supermart/box/borderImgB.png`
- },
- C: {
- title: '隐藏款',
- color: '#A72CF6',
- rgba: 'rgba(167, 44, 246, 1)',
- resultBg: `${imgUrlSupermart}/supermart/box/resultBgC.png`,
- borderImg: `${imgUrlSupermart}/supermart/box/borderImgC.png`
- },
- D: {
- title: '普通款',
- color: '#40c9d7',
- rgba: 'rgba(64, 201, 215, 1)',
- resultBg: `${imgUrlSupermart}/supermart/box/resultBgD.png`,
- borderImg: `${imgUrlSupermart}/supermart/box/borderImgD.png`
- },
- };
- const LotteryImages = {
- lotteryBg: `${imgUrlSupermart}/supermart/box/sequence/sequence0.jpg`,
- cardBack: `${imgUrl}/box/back1.png`,
- halo: `${imgUrlSupermart}/supermart/box/halo.gif`,
- };
- interface LotteryItem {
- id: string;
- name: string;
- cover: string;
- level: string;
- magicAmount?: number;
- spu?: { marketPrice: number };
- }
- export interface LotteryResultModalRef {
- show: (tradeNo: string) => void;
- close: () => void;
- }
- interface LotteryResultModalProps {
- onClose?: () => void;
- onGoStore?: () => void;
- }
- export const LotteryResultModal = forwardRef<LotteryResultModalRef, LotteryResultModalProps>(
- ({ onClose, onGoStore }, ref) => {
- const insets = useSafeAreaInsets();
-
- const [visible, setVisible] = useState(false);
- const [loading, setLoading] = useState(true);
- const [tableData, setTableData] = useState<LotteryItem[]>([]);
- const [total, setTotal] = useState(0);
- const [showResult, setShowResult] = useState(false);
- const [showDh, setShowDh] = useState(false);
- const [haloShow, setHaloShow] = useState(false);
- const [rebateAmount, setRebateAmount] = useState(0);
- const [animationEnabled, setAnimationEnabled] = useState(true);
- const [isSkip, setIsSkip] = useState(true);
- const [tradeNo, setTradeNo] = useState('');
- const flipAnims = useRef<Animated.Value[]>([]);
- const dataLoadedRef = useRef(false);
- useImperativeHandle(ref, () => ({
- show: (tNo: string) => {
- setTradeNo(tNo);
- setVisible(true);
- setLoading(true);
- setTableData([]);
- setTotal(0);
- setShowResult(false);
- setShowDh(false);
- setHaloShow(false);
- setRebateAmount(0);
- setIsSkip(true);
- dataLoadedRef.current = false;
- flipAnims.current = [];
- },
- close: () => {
- setVisible(false);
- dataLoadedRef.current = false;
- },
- }));
- const flipCards = (data: LotteryItem[]) => {
- setIsSkip(false);
- setHaloShow(true);
- setTimeout(() => setHaloShow(false), 1000);
- const maxCards = Math.min(data.length, 9);
- const animations = flipAnims.current.slice(0, maxCards).map((anim, index) => {
- return Animated.sequence([
- Animated.delay(index * 200),
- Animated.timing(anim, { toValue: 1, duration: 200, useNativeDriver: true }),
- ]);
- });
- Animated.parallel(animations).start(() => {
- setTimeout(() => {
- setShowDh(data.every((item) => item.level !== 'B' && item.level !== 'A'));
- setShowResult(true);
- }, 900);
- });
- };
- useEffect(() => {
- if (!visible || !tradeNo || dataLoadedRef.current) return;
- let attempts = 0;
- const maxAttempts = 13;
- let timeoutId: ReturnType<typeof setTimeout>;
- let isMounted = true;
- const fetchData = async () => {
- if (!isMounted) return;
-
- try {
- const res = await getApplyResult(tradeNo);
-
- if (!isMounted) return;
-
- if (res?.inventoryList && res.inventoryList.length > 0) {
- dataLoadedRef.current = true;
- if (res.rebateAmount) setRebateAmount(res.rebateAmount);
- let array = res.inventoryList;
- if (res.magicFireworksList && res.magicFireworksList.length > 0) {
- array = [...res.magicFireworksList, ...res.inventoryList];
- }
- flipAnims.current = array.map(() => new Animated.Value(0));
- const sum = array.reduce((acc: number, item: LotteryItem) => acc + (item.magicAmount || 0), 0);
- setTotal(sum);
- setTableData(array);
- setLoading(false);
-
- // 直接开始翻牌动画
- setTimeout(() => flipCards(array), 500);
- } else if (attempts < maxAttempts) {
- attempts++;
- timeoutId = setTimeout(fetchData, 400);
- } else {
- setLoading(false);
- window.alert('获取结果超时,请在仓库中查看');
- }
- } catch (error) {
- if (attempts < maxAttempts) {
- attempts++;
- timeoutId = setTimeout(fetchData, 400);
- } else {
- setLoading(false);
- }
- }
- };
- fetchData();
-
- return () => {
- isMounted = false;
- if (timeoutId) clearTimeout(timeoutId);
- };
- }, [visible, tradeNo]);
- const handleSkip = () => {
- setIsSkip(false);
- flipAnims.current.forEach((anim) => anim.setValue(1));
- setShowDh(tableData.every((item) => item.level !== 'B' && item.level !== 'A'));
- setShowResult(true);
- };
- const handleDhAll = async () => {
- if (!total) return;
- try {
- const ids = tableData.filter((item) => item.magicAmount).map((item) => item.id);
- const res = await convertApply(ids);
- if (res) {
- setTableData((prev) => prev.map((item) => ({ ...item, magicAmount: 0 })));
- setTotal(0);
- window.alert('兑换成功');
- }
- } catch {
- window.alert('兑换失败,请重试');
- }
- };
- const handleBack = () => {
- setVisible(false);
- onClose?.();
- };
- const handleGoStore = () => {
- setVisible(false);
- onGoStore?.();
- };
- const renderCard = (item: LotteryItem, index: number) => {
- const levelConfig = LEVEL_MAP[item.level] || LEVEL_MAP.D;
- const flipAnim = flipAnims.current[index] || new Animated.Value(1);
- const isFirst9 = index < 9;
- // 前9张卡片有翻转动画
- if (isFirst9) {
- const backRotate = flipAnim.interpolate({
- inputRange: [0, 1],
- outputRange: ['0deg', '90deg']
- });
- const frontRotate = flipAnim.interpolate({
- inputRange: [0, 1],
- outputRange: ['-90deg', '0deg']
- });
- const backOpacity = flipAnim.interpolate({
- inputRange: [0, 0.5, 1],
- outputRange: [1, 0, 0]
- });
- const frontOpacity = flipAnim.interpolate({
- inputRange: [0, 0.5, 1],
- outputRange: [0, 0, 1]
- });
- return (
- <View key={item.id || index} style={styles.cardWrapper}>
- {/* 背面 - 卡牌背面 */}
- <Animated.View style={[styles.cardBack, { transform: [{ rotateY: backRotate }], opacity: backOpacity }]}>
- <Image source={{ uri: LotteryImages.cardBack }} style={styles.cardBackImage} contentFit="cover" />
- </Animated.View>
- {/* 正面 - 商品信息 */}
- <Animated.View style={[styles.cardFront, { transform: [{ rotateY: frontRotate }], opacity: frontOpacity }]}>
- <ImageBackground source={{ uri: levelConfig.resultBg }} style={styles.cardFrontBg} resizeMode="cover">
- <Image source={{ uri: item.cover }} style={styles.productImage} contentFit="contain" />
- <Image source={{ uri: levelConfig.borderImg }} style={styles.borderImage} contentFit="cover" />
- <View style={styles.cardInfo}>
- <View style={styles.infoRow}>
- <Text style={styles.levelText}>{levelConfig.title}</Text>
- <Text style={styles.priceText}>¥{item.spu?.marketPrice || 0}</Text>
- </View>
- <View style={styles.exchangeRow}>
- <Text style={styles.exchangeText}>价值:{item.magicAmount || 0}果实</Text>
- </View>
- <Text style={styles.nameText} numberOfLines={1}>{item.name}</Text>
- </View>
- </ImageBackground>
- </Animated.View>
- </View>
- );
- }
- // 9张以后的卡片直接显示正面
- return (
- <View key={item.id || index} style={styles.cardWrapper}>
- <View style={styles.cardFrontStatic}>
- <ImageBackground source={{ uri: levelConfig.resultBg }} style={styles.cardFrontBg} resizeMode="cover">
- <Image source={{ uri: item.cover }} style={styles.productImage} contentFit="contain" />
- <Image source={{ uri: levelConfig.borderImg }} style={styles.borderImage} contentFit="cover" />
- <View style={styles.cardInfo}>
- <View style={styles.infoRow}>
- <Text style={styles.levelText}>{levelConfig.title}</Text>
- <Text style={styles.priceText}>¥{item.spu?.marketPrice || 0}</Text>
- </View>
- <View style={styles.exchangeRow}>
- <Text style={styles.exchangeText}>价值:{item.magicAmount || 0}果实</Text>
- </View>
- <Text style={styles.nameText} numberOfLines={1}>{item.name}</Text>
- </View>
- </ImageBackground>
- </View>
- </View>
- );
- };
- return (
- <Modal visible={visible} transparent animationType="fade" onRequestClose={handleBack}>
- <View style={styles.container}>
- <ImageBackground source={{ uri: LotteryImages.lotteryBg }} style={styles.background} resizeMode="cover">
- {/* 光晕效果 */}
- {haloShow && (
- <View style={styles.haloSection}>
- <Image source={{ uri: LotteryImages.halo }} style={styles.halo} contentFit="cover" />
- </View>
- )}
-
- {/* 标题 */}
- <View style={[styles.titleSection, { marginTop: insets.top + 40 }]}>
- <Text style={styles.titleText}>恭喜您获得</Text>
- </View>
-
- {/* 主内容区 */}
- <View style={styles.mainSection}>
- {loading ? (
- <View style={styles.loadingBox}>
- <ActivityIndicator size="large" color="#fff" />
- <Text style={styles.loadingText}>正在开启宝箱...</Text>
- </View>
- ) : (
- <ScrollView style={styles.cardList} showsVerticalScrollIndicator={false}>
- <View style={styles.cardGrid}>
- {tableData.map((item, index) => renderCard(item, index))}
- </View>
- </ScrollView>
- )}
- </View>
- {/* 底部按钮区 */}
- {showResult && (
- <View style={[styles.bottomSection, { paddingBottom: insets.bottom + 20 }]}>
- <View style={styles.bottomBtns}>
- {total > 0 && showDh && (
- <TouchableOpacity style={styles.dhBtn} onPress={handleDhAll}>
- <Text style={styles.dhBtnText}>全部兑换</Text>
- <Text style={styles.dhBtnSubText}>共兑换果实 {total}</Text>
- </TouchableOpacity>
- )}
- <TouchableOpacity style={styles.againBtn} onPress={handleBack}>
- <Text style={styles.againBtnText}>再来一发</Text>
- </TouchableOpacity>
- </View>
- <TouchableOpacity style={styles.storeLink} onPress={handleGoStore}>
- <Text style={styles.storeLinkText}>前往 <Text style={styles.storeHighlight}>仓库</Text> 查看</Text>
- </TouchableOpacity>
- {rebateAmount > 0 && (
- <Text style={styles.rebateText}>本次支付返还果实 <Text style={styles.rebateAmount}>{rebateAmount}</Text> 枚</Text>
- )}
- <View style={styles.animationSwitch}>
- <Text style={styles.switchLabel}>是否开启动画</Text>
- <TouchableOpacity
- style={[styles.switchBtn, animationEnabled && styles.switchBtnActive]}
- onPress={() => setAnimationEnabled(!animationEnabled)}
- >
- <View style={[styles.switchThumb, animationEnabled && styles.switchThumbActive]} />
- </TouchableOpacity>
- </View>
- </View>
- )}
-
- {/* 跳过动画按钮 */}
- {isSkip && !loading && (
- <TouchableOpacity style={styles.skipBtn} onPress={handleSkip}>
- <Text style={styles.skipText}>跳过动画</Text>
- </TouchableOpacity>
- )}
- </ImageBackground>
- </View>
- </Modal>
- );
- }
- );
- const styles = StyleSheet.create({
- container: { flex: 1, backgroundColor: '#1a1a2e' },
- background: { flex: 1 },
- haloSection: { position: 'absolute', left: 0, top: 0, right: 0, bottom: 0, zIndex: 9999 },
- halo: { width: '100%', height: '100%' },
- titleSection: { alignItems: 'center', marginBottom: 15 },
- titleText: {
- fontSize: 31,
- fontWeight: 'bold',
- color: '#fffecc',
- textShadowColor: '#a06939',
- textShadowOffset: { width: 1, height: 1 },
- textShadowRadius: 2
- },
- mainSection: { flex: 1, paddingTop: 20 },
- loadingBox: { flex: 1, justifyContent: 'center', alignItems: 'center' },
- loadingText: { marginTop: 15, fontSize: 14, color: '#fff' },
- cardList: { flex: 1 },
- cardGrid: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- paddingHorizontal: 15,
- justifyContent: 'flex-start'
- },
- cardWrapper: {
- width: CARD_WIDTH,
- height: CARD_HEIGHT,
- marginHorizontal: 5,
- marginBottom: 15,
- position: 'relative'
- },
- cardBack: {
- position: 'absolute',
- width: '100%',
- height: '100%',
- backfaceVisibility: 'hidden'
- },
- cardBackImage: { width: '100%', height: '100%', borderRadius: 10 },
- cardFront: {
- position: 'absolute',
- width: '100%',
- height: '100%',
- backfaceVisibility: 'hidden',
- borderRadius: 10,
- overflow: 'hidden'
- },
- cardFrontStatic: {
- width: '100%',
- height: '100%',
- borderRadius: 10,
- overflow: 'hidden'
- },
- cardFrontBg: {
- width: '100%',
- height: '100%',
- paddingTop: 15,
- borderRadius: 10,
- overflow: 'hidden'
- },
- productImage: { width: '85%', height: '55%', alignSelf: 'center' },
- borderImage: { position: 'absolute', left: 0, top: 0, width: '100%', height: '100%' },
- cardInfo: { position: 'absolute', left: 0, right: 0, bottom: 7, paddingHorizontal: 10 },
- infoRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 2
- },
- levelText: { fontSize: 13, fontWeight: 'bold', color: '#fff' },
- priceText: { fontSize: 12, fontWeight: 'bold', color: '#fff' },
- exchangeRow: { marginBottom: 2 },
- exchangeText: { fontSize: 10, color: '#fff', fontWeight: 'bold' },
- nameText: { fontSize: 12, fontWeight: 'bold', color: '#fff' },
- bottomSection: { paddingHorizontal: 20, paddingTop: 20 },
- bottomBtns: { flexDirection: 'row', justifyContent: 'center', alignItems: 'center' },
- dhBtn: {
- backgroundColor: '#fff7e3',
- borderRadius: 20,
- paddingHorizontal: 18,
- paddingVertical: 8,
- marginRight: 10,
- alignItems: 'center'
- },
- dhBtnText: { fontSize: 14, fontWeight: '500', color: '#000' },
- dhBtnSubText: { fontSize: 10, color: '#735200' },
- againBtn: {
- backgroundColor: '#fec433',
- borderRadius: 20,
- paddingHorizontal: 25,
- paddingVertical: 12
- },
- againBtnText: { fontSize: 14, fontWeight: '600', color: '#000' },
- storeLink: { alignItems: 'center', marginTop: 12 },
- storeLinkText: { fontSize: 12, color: '#fff' },
- storeHighlight: { color: '#ff9600' },
- rebateText: { textAlign: 'center', marginTop: 13, fontSize: 13, color: '#fff' },
- rebateAmount: { color: '#ffeb3b', fontSize: 14, fontWeight: 'bold' },
- animationSwitch: {
- flexDirection: 'row',
- justifyContent: 'center',
- alignItems: 'center',
- marginTop: 8
- },
- switchLabel: { fontSize: 12, color: '#dedede', marginRight: 10 },
- switchBtn: {
- width: 44,
- height: 24,
- borderRadius: 12,
- backgroundColor: '#666',
- justifyContent: 'center',
- paddingHorizontal: 2
- },
- switchBtnActive: { backgroundColor: '#ff9600' },
- switchThumb: { width: 20, height: 20, borderRadius: 10, backgroundColor: '#fff' },
- switchThumbActive: { alignSelf: 'flex-end' },
- skipBtn: {
- position: 'absolute',
- bottom: '10%',
- alignSelf: 'center',
- backgroundColor: 'rgba(0,0,0,0.4)',
- paddingHorizontal: 15,
- paddingVertical: 7,
- borderRadius: 15
- },
- skipText: { fontSize: 14, color: '#fff' },
- });
|