index.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import { Images } from '@/constants/images';
  2. import ServiceWallet from '@/services/wallet';
  3. import { Ionicons } from '@expo/vector-icons';
  4. import { Stack, useRouter } from 'expo-router';
  5. import React, { useEffect, useState } from 'react';
  6. import {
  7. Dimensions,
  8. ImageBackground,
  9. ScrollView,
  10. StatusBar,
  11. StyleSheet,
  12. Text,
  13. TouchableOpacity,
  14. View,
  15. } from 'react-native';
  16. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  17. const { width: SCREEN_WIDTH } = Dimensions.get('window');
  18. interface CouponItem {
  19. id: string;
  20. name: string;
  21. amount: number;
  22. fullAmount: number; // For "Man X Keyong"
  23. status: number; // 1: Valid
  24. endTime: string;
  25. scene?: string; // LUCK, MALL, TRADE
  26. }
  27. export default function CouponScreen() {
  28. const router = useRouter();
  29. const insets = useSafeAreaInsets();
  30. const [list, setList] = useState<CouponItem[]>([]);
  31. const [loading, setLoading] = useState(false);
  32. useEffect(() => {
  33. loadData();
  34. }, []);
  35. const loadData = async () => {
  36. try {
  37. setLoading(true);
  38. const res = await ServiceWallet.coupons();
  39. // Ensure res is array or fetch from res.records if paginated
  40. const data = Array.isArray(res) ? res : (res?.records || []);
  41. setList(data);
  42. } catch (e) {
  43. console.error(e);
  44. } finally {
  45. setLoading(false);
  46. }
  47. };
  48. const handleUse = (item: CouponItem) => {
  49. if (item.scene === 'LUCK') {
  50. // Go to Box/Award
  51. router.push('/box' as any); // Adapt route as needed
  52. } else if (item.scene === 'MALL') {
  53. // Go to Mall/VIP usually
  54. router.push('/(tabs)' as any);
  55. } else {
  56. // Default
  57. router.push('/(tabs)' as any);
  58. }
  59. };
  60. const formatTime = (time: string) => {
  61. return time ? time.slice(0, 10) : '';
  62. };
  63. return (
  64. <View style={styles.container}>
  65. <Stack.Screen options={{ headerShown: false }} />
  66. <StatusBar barStyle="light-content" />
  67. <View style={[styles.header, { paddingTop: insets.top }]}>
  68. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  69. <Ionicons name="chevron-back" size={24} color="#fff" />
  70. </TouchableOpacity>
  71. <Text style={styles.title}>优惠券</Text>
  72. </View>
  73. <ImageBackground
  74. source={{ uri: Images.common.commonBg }}
  75. style={styles.background}
  76. resizeMode="cover"
  77. >
  78. <ScrollView
  79. style={styles.scrollView}
  80. contentContainerStyle={{ paddingTop: insets.top + 50, paddingBottom: 50, paddingHorizontal: 10 }}
  81. >
  82. {list.length > 0 ? (
  83. list.map((item, index) => (
  84. <ImageBackground
  85. key={index}
  86. source={{ uri: Images.mine.couponBg }}
  87. style={[styles.couponItem, item.status !== 1 && styles.grayscale]}
  88. resizeMode="stretch"
  89. >
  90. <View style={styles.left}>
  91. <View style={styles.amountBox}>
  92. <Text style={styles.symbol}>¥</Text>
  93. <Text style={styles.amount}>{item.amount}</Text>
  94. </View>
  95. <Text style={styles.fullAmount}>满{item.fullAmount}可用</Text>
  96. </View>
  97. <View style={styles.divider} />
  98. <View style={styles.right}>
  99. <View style={styles.info}>
  100. <Text style={styles.name}>{item.name}</Text>
  101. <Text style={styles.time}>{formatTime(item.endTime)}过期</Text>
  102. </View>
  103. <TouchableOpacity onPress={() => handleUse(item)}>
  104. <ImageBackground
  105. source={{ uri: Images.mine.couponItemButBg }}
  106. style={styles.useBtn}
  107. resizeMode="contain"
  108. >
  109. <Text style={styles.useText}>立即使用</Text>
  110. </ImageBackground>
  111. </TouchableOpacity>
  112. </View>
  113. </ImageBackground>
  114. ))
  115. ) : (
  116. <View style={styles.emptyBox}>
  117. <Text style={styles.emptyText}>暂无优惠券</Text>
  118. </View>
  119. )}
  120. </ScrollView>
  121. </ImageBackground>
  122. </View>
  123. );
  124. }
  125. const styles = StyleSheet.create({
  126. container: {
  127. flex: 1,
  128. backgroundColor: '#1a1a2e',
  129. },
  130. header: {
  131. position: 'absolute',
  132. top: 0,
  133. left: 0,
  134. right: 0,
  135. zIndex: 100,
  136. alignItems: 'center',
  137. paddingBottom: 10,
  138. },
  139. backBtn: {
  140. position: 'absolute',
  141. left: 10,
  142. bottom: 10,
  143. zIndex: 101,
  144. },
  145. title: {
  146. color: '#fff',
  147. fontSize: 16,
  148. fontWeight: 'bold',
  149. },
  150. background: {
  151. flex: 1,
  152. width: '100%',
  153. height: '100%',
  154. },
  155. scrollView: {
  156. flex: 1,
  157. },
  158. couponItem: {
  159. width: '100%',
  160. height: 92, // 184rpx / 2 = 92
  161. flexDirection: 'row',
  162. alignItems: 'center',
  163. marginBottom: 10,
  164. paddingHorizontal: 15, // Adjust padding based on image visual
  165. },
  166. grayscale: {
  167. opacity: 0.6,
  168. // React Native doesn't support grayscale prop directly on ImageBackground without filter or tintColor tricks
  169. // but opactity is a good enough approximation for invalid state
  170. },
  171. left: {
  172. width: 80,
  173. alignItems: 'center',
  174. justifyContent: 'center',
  175. },
  176. amountBox: {
  177. flexDirection: 'row',
  178. alignItems: 'baseline',
  179. },
  180. symbol: {
  181. fontSize: 12,
  182. color: '#404040',
  183. },
  184. amount: {
  185. fontSize: 30,
  186. fontWeight: 'bold',
  187. color: '#404040',
  188. },
  189. fullAmount: {
  190. fontSize: 12,
  191. color: '#8F8F8F',
  192. marginTop: 2,
  193. },
  194. divider: {
  195. width: 1,
  196. height: 24,
  197. backgroundColor: '#C3B3DF',
  198. marginHorizontal: 16,
  199. opacity: 0, // Hidden in layout if image has it, just spacing
  200. },
  201. right: {
  202. flex: 1,
  203. flexDirection: 'row',
  204. justifyContent: 'space-between',
  205. alignItems: 'center',
  206. paddingLeft: 10,
  207. },
  208. info: {
  209. justifyContent: 'center',
  210. },
  211. name: {
  212. fontSize: 15,
  213. fontWeight: 'bold',
  214. color: '#404040',
  215. marginBottom: 5,
  216. },
  217. time: {
  218. fontSize: 12,
  219. color: '#8F8F8F',
  220. },
  221. useBtn: {
  222. width: 87, // 174rpx / 2
  223. height: 37, // 74rpx / 2
  224. justifyContent: 'center',
  225. alignItems: 'center',
  226. },
  227. useText: {
  228. color: '#fff',
  229. fontSize: 12,
  230. },
  231. emptyBox: {
  232. marginTop: 100,
  233. alignItems: 'center',
  234. },
  235. emptyText: {
  236. color: '#999',
  237. fontSize: 14,
  238. },
  239. });