index.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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. // Original behavior: goAward() -> /pages/award/index (Box Screen)
  50. // We replicate this 1:1, or maintain specific routing if mall requires it.
  51. // Given request for 1:1, we prioritize Box screen.
  52. if (item.scene === 'MALL') {
  53. router.push('/(tabs)/store' as any);
  54. } else {
  55. router.push('/(tabs)/box' as any);
  56. }
  57. };
  58. const formatTime = (time: string) => {
  59. return time ? time.slice(0, 10) : '';
  60. };
  61. return (
  62. <View style={styles.container}>
  63. <Stack.Screen options={{ headerShown: false }} />
  64. <StatusBar barStyle="light-content" />
  65. <View style={[styles.header, { paddingTop: insets.top }]}>
  66. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  67. <Ionicons name="chevron-back" size={24} color="#fff" />
  68. </TouchableOpacity>
  69. <Text style={styles.title}>优惠券</Text>
  70. </View>
  71. <ImageBackground
  72. source={{ uri: Images.common.commonBg }}
  73. style={styles.background}
  74. resizeMode="cover"
  75. >
  76. <ScrollView
  77. style={styles.scrollView}
  78. contentContainerStyle={{ paddingTop: insets.top + 50, paddingBottom: 50, paddingHorizontal: 10 }}
  79. >
  80. {list.length > 0 ? (
  81. list.map((item, index) => (
  82. <TouchableOpacity
  83. key={index}
  84. activeOpacity={0.8}
  85. onPress={() => handleUse(item)}
  86. >
  87. <ImageBackground
  88. source={{ uri: Images.mine.couponBg }}
  89. style={[styles.couponItem, item.status !== 1 && styles.grayscale]}
  90. resizeMode="stretch"
  91. >
  92. <View style={styles.left}>
  93. <View style={styles.amountBox}>
  94. <Text style={styles.symbol}>¥</Text>
  95. <Text style={styles.amount}>{item.amount}</Text>
  96. </View>
  97. <Text style={styles.fullAmount}>满{item.fullAmount}可用</Text>
  98. </View>
  99. <View style={styles.divider} />
  100. <View style={styles.right}>
  101. <View style={styles.info}>
  102. <Text style={styles.name}>{item.name}</Text>
  103. <Text style={styles.time}>{formatTime(item.endTime)}过期</Text>
  104. </View>
  105. <View>
  106. <ImageBackground
  107. source={{ uri: Images.mine.couponItemButBg }}
  108. style={styles.useBtn}
  109. resizeMode="contain"
  110. >
  111. <Text style={styles.useText}>立即使用</Text>
  112. </ImageBackground>
  113. </View>
  114. </View>
  115. </ImageBackground>
  116. </TouchableOpacity>
  117. ))
  118. ) : (
  119. <View style={styles.emptyBox}>
  120. <Text style={styles.emptyText}>暂无优惠券</Text>
  121. </View>
  122. )}
  123. </ScrollView>
  124. </ImageBackground>
  125. </View>
  126. );
  127. }
  128. const styles = StyleSheet.create({
  129. container: {
  130. flex: 1,
  131. backgroundColor: '#1a1a2e',
  132. },
  133. header: {
  134. position: 'absolute',
  135. top: 0,
  136. left: 0,
  137. right: 0,
  138. zIndex: 100,
  139. alignItems: 'center',
  140. paddingBottom: 10,
  141. },
  142. backBtn: {
  143. position: 'absolute',
  144. left: 10,
  145. bottom: 10,
  146. zIndex: 101,
  147. },
  148. title: {
  149. color: '#fff',
  150. fontSize: 16,
  151. fontWeight: 'bold',
  152. },
  153. background: {
  154. flex: 1,
  155. width: '100%',
  156. height: '100%',
  157. },
  158. scrollView: {
  159. flex: 1,
  160. },
  161. couponItem: {
  162. width: '100%',
  163. height: 92, // 184rpx / 2 = 92
  164. flexDirection: 'row',
  165. alignItems: 'center',
  166. marginBottom: 10,
  167. paddingHorizontal: 15, // Adjust padding based on image visual
  168. },
  169. grayscale: {
  170. opacity: 0.6,
  171. // React Native doesn't support grayscale prop directly on ImageBackground without filter or tintColor tricks
  172. // but opactity is a good enough approximation for invalid state
  173. },
  174. left: {
  175. width: 80,
  176. alignItems: 'center',
  177. justifyContent: 'center',
  178. },
  179. amountBox: {
  180. flexDirection: 'row',
  181. alignItems: 'baseline',
  182. },
  183. symbol: {
  184. fontSize: 12,
  185. color: '#404040',
  186. },
  187. amount: {
  188. fontSize: 30,
  189. fontWeight: 'bold',
  190. color: '#404040',
  191. },
  192. fullAmount: {
  193. fontSize: 12,
  194. color: '#8F8F8F',
  195. marginTop: 2,
  196. },
  197. divider: {
  198. width: 1,
  199. height: 24,
  200. backgroundColor: '#C3B3DF',
  201. marginHorizontal: 16,
  202. // opacity: 0, // Removed to match original design
  203. },
  204. right: {
  205. flex: 1,
  206. flexDirection: 'row',
  207. justifyContent: 'space-between',
  208. alignItems: 'center',
  209. paddingLeft: 10,
  210. },
  211. info: {
  212. justifyContent: 'center',
  213. },
  214. name: {
  215. fontSize: 15,
  216. fontWeight: 'bold',
  217. color: '#404040',
  218. marginBottom: 5,
  219. },
  220. time: {
  221. fontSize: 12,
  222. color: '#8F8F8F',
  223. },
  224. useBtn: {
  225. width: 87, // 174rpx / 2
  226. height: 37, // 74rpx / 2
  227. justifyContent: 'center',
  228. alignItems: 'center',
  229. },
  230. useText: {
  231. color: '#fff',
  232. fontSize: 12,
  233. },
  234. emptyBox: {
  235. marginTop: 100,
  236. alignItems: 'center',
  237. },
  238. emptyText: {
  239. color: '#999',
  240. fontSize: 14,
  241. },
  242. });