catchDoll.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. import { Image } from 'expo-image';
  2. import { useRouter } from 'expo-router';
  3. import React, { useCallback, useEffect, useRef, useState } from 'react';
  4. import {
  5. Animated,
  6. Dimensions,
  7. ImageBackground,
  8. ScrollView,
  9. StatusBar,
  10. StyleSheet,
  11. Text,
  12. TouchableOpacity,
  13. View,
  14. } from 'react-native';
  15. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  16. import { get } from '@/services/http';
  17. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  18. const CDN_BASE = 'https://cdn.acetoys.cn/kai_xin_ma_te/supermart';
  19. const catchDollImages = {
  20. bg: `${CDN_BASE}/common/commonBg.png`,
  21. dollBox: `${CDN_BASE}/welfare/qijiWelfareDollBox.png`,
  22. dollBall: `${CDN_BASE}/welfare/qijiWelfareDollBall.png`,
  23. dollOne: `${CDN_BASE}/welfare/qijiWelfareDollOne.png`,
  24. dollFive: `${CDN_BASE}/welfare/qijiWelfareDollFive.png`,
  25. recordBg: `${CDN_BASE}/welfare/qijiWelfareRecordBg.png`,
  26. ruleBtn: `${CDN_BASE}/welfare/catchDollRule.png`,
  27. molibiBoxBtn: `${CDN_BASE}/welfare/molibiBoxBtn.png`,
  28. opening: `${CDN_BASE}/welfare/opening.png`,
  29. dollBi1: `${CDN_BASE}/welfare/qijiWelfareDollBi1.png`,
  30. dollBi5: `${CDN_BASE}/welfare/qijiWelfareDollBi5.png`,
  31. };
  32. interface GoodsItem {
  33. id: string;
  34. cover: string;
  35. name: string;
  36. }
  37. export default function CatchDollScreen() {
  38. const router = useRouter();
  39. const insets = useSafeAreaInsets();
  40. const [goodsList, setGoodsList] = useState<GoodsItem[]>([]);
  41. const [molibi, setMolibi] = useState(0);
  42. const [balls] = useState(() =>
  43. Array.from({ length: 50 }, (_, i) => ({
  44. id: i,
  45. bottom: Math.random() * 80,
  46. left: Math.random() * 172,
  47. }))
  48. );
  49. // 动画
  50. const ballAnimations = useRef(balls.map(() => new Animated.ValueXY({ x: 0, y: 0 }))).current;
  51. const ballRotations = useRef(balls.map(() => new Animated.Value(0))).current;
  52. const loadData = useCallback(async () => {
  53. try {
  54. const res = await get('/api/luckWheel/detail');
  55. if (res.data) {
  56. setGoodsList(res.data.luckWheelGoodsList || []);
  57. }
  58. } catch (error) {
  59. console.error('加载扭蛋机数据失败:', error);
  60. }
  61. }, []);
  62. const loadMolibi = useCallback(async () => {
  63. try {
  64. const res = await get('/api/wallet/info', { type: 'MAGIC_POWER_COIN' });
  65. if (res.data) {
  66. setMolibi(res.data.balance || 0);
  67. }
  68. } catch (error) {
  69. console.error('加载源力币失败:', error);
  70. }
  71. }, []);
  72. useEffect(() => {
  73. loadData();
  74. loadMolibi();
  75. }, [loadData, loadMolibi]);
  76. const animateBalls = () => {
  77. const animations = balls.map((_, i) => {
  78. const randomX = (Math.random() - 0.5) * 50;
  79. const randomY = (Math.random() - 0.5) * 100;
  80. const randomRotate = Math.random() * 360;
  81. return Animated.parallel([
  82. Animated.sequence([
  83. Animated.timing(ballAnimations[i], {
  84. toValue: { x: randomX, y: randomY },
  85. duration: 200,
  86. useNativeDriver: true,
  87. }),
  88. Animated.timing(ballAnimations[i], {
  89. toValue: { x: randomX * 0.5, y: randomY * 0.5 },
  90. duration: 300,
  91. useNativeDriver: true,
  92. }),
  93. Animated.timing(ballAnimations[i], {
  94. toValue: { x: 0, y: 0 },
  95. duration: 300,
  96. useNativeDriver: true,
  97. }),
  98. ]),
  99. Animated.timing(ballRotations[i], {
  100. toValue: randomRotate,
  101. duration: 800,
  102. useNativeDriver: true,
  103. }),
  104. ]);
  105. });
  106. Animated.parallel(animations).start(() => {
  107. ballRotations.forEach((rot) => rot.setValue(0));
  108. });
  109. };
  110. const handlePress = (count: number) => {
  111. if (molibi < count) {
  112. // TODO: 显示源力币不足弹窗
  113. return;
  114. }
  115. animateBalls();
  116. // TODO: 调用抽奖接口
  117. };
  118. return (
  119. <View style={styles.container}>
  120. <StatusBar barStyle="light-content" />
  121. <ImageBackground source={{ uri: catchDollImages.bg }} style={styles.background} resizeMode="cover">
  122. {/* 固定头部 */}
  123. <View style={[styles.header, { paddingTop: insets.top }]}>
  124. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  125. <Text style={styles.backText}>←</Text>
  126. </TouchableOpacity>
  127. <Text style={styles.title}>扭蛋机</Text>
  128. <View style={styles.placeholder} />
  129. </View>
  130. {/* 规则按钮 - 固定位置 */}
  131. <TouchableOpacity style={[styles.ruleBtn, { top: insets.top + 200 }]}>
  132. <Image source={{ uri: catchDollImages.ruleBtn }} style={styles.ruleBtnImg} contentFit="contain" />
  133. </TouchableOpacity>
  134. {/* 中奖记录按钮 - 固定位置 */}
  135. <TouchableOpacity style={[styles.recordBtn, { top: insets.top + 120 }]}>
  136. <ImageBackground source={{ uri: catchDollImages.recordBg }} style={styles.recordBtnBg} resizeMode="contain">
  137. <Text style={styles.recordText}>中</Text>
  138. <Text style={styles.recordText}>奖</Text>
  139. <Text style={styles.recordText}>记</Text>
  140. <Text style={styles.recordText}>录</Text>
  141. </ImageBackground>
  142. </TouchableOpacity>
  143. <ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
  144. <View style={{ height: insets.top + 36 }} />
  145. {/* 扭蛋机主体 */}
  146. <View style={styles.machineWrapper}>
  147. <ImageBackground source={{ uri: catchDollImages.dollBox }} style={styles.machineImg} resizeMode="contain">
  148. {/* 奖品列表 */}
  149. <View style={styles.goodsListWrapper}>
  150. <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.goodsScroll}>
  151. {goodsList.map((item, index) => (
  152. <View key={item.id || index} style={styles.goodsItem}>
  153. <Image source={{ uri: item.cover }} style={styles.goodsImg} contentFit="cover" />
  154. </View>
  155. ))}
  156. </ScrollView>
  157. </View>
  158. {/* 扭蛋球区域 */}
  159. <View style={styles.ballsBox}>
  160. {balls.map((ball, i) => (
  161. <Animated.View
  162. key={ball.id}
  163. style={[
  164. styles.ball,
  165. {
  166. bottom: ball.bottom,
  167. left: ball.left,
  168. transform: [
  169. { translateX: ballAnimations[i].x },
  170. { translateY: ballAnimations[i].y },
  171. {
  172. rotate: ballRotations[i].interpolate({
  173. inputRange: [0, 360],
  174. outputRange: ['0deg', '360deg'],
  175. }),
  176. },
  177. ],
  178. },
  179. ]}
  180. >
  181. <Image source={{ uri: catchDollImages.dollBall }} style={styles.ballImg} contentFit="contain" />
  182. </Animated.View>
  183. ))}
  184. </View>
  185. {/* 源力币信息框 */}
  186. <View style={styles.molibiBox}>
  187. <Text style={styles.molibiLabel}>
  188. 源力币:<Text style={styles.molibiNum}>{molibi}</Text> 个
  189. </Text>
  190. <TouchableOpacity style={styles.molibiBtn} onPress={() => router.push('/award' as any)}>
  191. <Image source={{ uri: catchDollImages.molibiBoxBtn }} style={styles.molibiBtnImg} contentFit="contain" />
  192. </TouchableOpacity>
  193. </View>
  194. {/* 开口动画区域 */}
  195. <View style={styles.openingBox}>
  196. <Image source={{ uri: catchDollImages.opening }} style={styles.openingImg} contentFit="contain" />
  197. </View>
  198. {/* 扭蛋把手 */}
  199. <TouchableOpacity style={[styles.switchBox, styles.switchBox1]} onPress={() => handlePress(1)}>
  200. <Image source={{ uri: catchDollImages.dollBi1 }} style={styles.switchImg} contentFit="contain" />
  201. </TouchableOpacity>
  202. <TouchableOpacity style={[styles.switchBox, styles.switchBox5]} onPress={() => handlePress(5)}>
  203. <Image source={{ uri: catchDollImages.dollBi5 }} style={styles.switchImg} contentFit="contain" />
  204. </TouchableOpacity>
  205. </ImageBackground>
  206. </View>
  207. {/* 底部按钮 */}
  208. <View style={styles.bottomBtns}>
  209. <TouchableOpacity style={styles.submitBtn} onPress={() => handlePress(1)}>
  210. <Image source={{ uri: catchDollImages.dollOne }} style={styles.submitBtnImg} contentFit="contain" />
  211. </TouchableOpacity>
  212. <TouchableOpacity style={styles.submitBtn} onPress={() => handlePress(5)}>
  213. <Image source={{ uri: catchDollImages.dollFive }} style={styles.submitBtnImg} contentFit="contain" />
  214. </TouchableOpacity>
  215. </View>
  216. <View style={{ height: 100 }} />
  217. </ScrollView>
  218. </ImageBackground>
  219. </View>
  220. );
  221. }
  222. const styles = StyleSheet.create({
  223. container: { flex: 1, backgroundColor: '#1a1a2e' },
  224. background: { flex: 1 },
  225. header: {
  226. flexDirection: 'row',
  227. alignItems: 'center',
  228. justifyContent: 'space-between',
  229. paddingHorizontal: 10,
  230. paddingBottom: 10,
  231. position: 'absolute',
  232. top: 0,
  233. left: 0,
  234. right: 0,
  235. zIndex: 100,
  236. },
  237. backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
  238. backText: { color: '#fff', fontSize: 20 },
  239. title: { color: '#fff', fontSize: 15, fontWeight: 'bold' },
  240. placeholder: { width: 40 },
  241. scrollView: { flex: 1 },
  242. // 规则按钮
  243. ruleBtn: { position: 'absolute', left: 13, zIndex: 99 },
  244. ruleBtnImg: { width: 62, height: 20 },
  245. // 中奖记录按钮
  246. recordBtn: { position: 'absolute', right: 0, zIndex: 99 },
  247. recordBtnBg: {
  248. width: 26,
  249. height: 70,
  250. justifyContent: 'center',
  251. alignItems: 'center',
  252. paddingVertical: 9,
  253. },
  254. recordText: {
  255. color: '#fff',
  256. fontSize: 12,
  257. fontWeight: 'bold',
  258. textShadowColor: '#6C3200',
  259. textShadowOffset: { width: 1, height: 1 },
  260. textShadowRadius: 1,
  261. },
  262. // 扭蛋机主体
  263. machineWrapper: {
  264. width: SCREEN_WIDTH,
  265. alignItems: 'center',
  266. },
  267. machineImg: {
  268. width: SCREEN_WIDTH,
  269. height: SCREEN_WIDTH * 1.88,
  270. position: 'relative',
  271. },
  272. // 奖品列表
  273. goodsListWrapper: {
  274. position: 'absolute',
  275. top: SCREEN_WIDTH * 0.29,
  276. left: 0,
  277. right: 0,
  278. alignItems: 'center',
  279. },
  280. goodsScroll: {
  281. paddingHorizontal: SCREEN_WIDTH * 0.18,
  282. },
  283. goodsItem: {
  284. width: 46,
  285. height: 46,
  286. borderRadius: 4,
  287. backgroundColor: '#ADAEF6',
  288. borderWidth: 2.5,
  289. borderColor: '#8687E4',
  290. marginRight: 5,
  291. overflow: 'hidden',
  292. },
  293. goodsImg: { width: '100%', height: '100%' },
  294. // 扭蛋球区域
  295. ballsBox: {
  296. position: 'absolute',
  297. top: SCREEN_WIDTH * 0.33,
  298. left: SCREEN_WIDTH * 0.115,
  299. width: SCREEN_WIDTH * 0.73,
  300. height: SCREEN_WIDTH * 0.74,
  301. overflow: 'hidden',
  302. },
  303. ball: {
  304. position: 'absolute',
  305. width: 66,
  306. height: 66,
  307. },
  308. ballImg: { width: '100%', height: '100%' },
  309. // 源力币信息框
  310. molibiBox: {
  311. position: 'absolute',
  312. top: SCREEN_WIDTH * 1.24,
  313. left: 42,
  314. width: 120,
  315. height: 67,
  316. backgroundColor: '#1E1C5B',
  317. borderRadius: 8,
  318. paddingTop: 5,
  319. alignItems: 'center',
  320. },
  321. molibiLabel: {
  322. color: '#7982CB',
  323. fontSize: 12,
  324. },
  325. molibiNum: {
  326. color: '#FF8400',
  327. fontSize: 18,
  328. fontWeight: 'bold',
  329. },
  330. molibiBtn: {
  331. marginTop: 5,
  332. },
  333. molibiBtnImg: {
  334. width: 105,
  335. height: 30,
  336. },
  337. // 开口动画区域
  338. openingBox: {
  339. position: 'absolute',
  340. top: SCREEN_WIDTH * 1.21,
  341. right: 42,
  342. width: 133,
  343. height: 82,
  344. },
  345. openingImg: { width: '100%', height: '100%' },
  346. // 扭蛋把手
  347. switchBox: {
  348. position: 'absolute',
  349. top: SCREEN_WIDTH * 1.49,
  350. width: 65,
  351. height: 65,
  352. },
  353. switchBox1: { left: 42 },
  354. switchBox5: { right: 42 },
  355. switchImg: { width: '100%', height: '100%' },
  356. // 底部按钮
  357. bottomBtns: {
  358. flexDirection: 'row',
  359. justifyContent: 'center',
  360. marginTop: -SCREEN_WIDTH * 0.24,
  361. paddingHorizontal: 20,
  362. },
  363. submitBtn: {
  364. marginHorizontal: SCREEN_WIDTH * 0.08,
  365. },
  366. submitBtnImg: {
  367. width: 73,
  368. height: 50,
  369. },
  370. });