packages.tsx 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import { Image } from 'expo-image';
  2. import { useLocalSearchParams, useRouter } from 'expo-router';
  3. import React, { useCallback, useEffect, useState } from 'react';
  4. import {
  5. ActivityIndicator,
  6. ImageBackground,
  7. ScrollView,
  8. StatusBar,
  9. StyleSheet,
  10. Text,
  11. TouchableOpacity,
  12. View,
  13. } from 'react-native';
  14. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  15. import { Images } from '@/constants/images';
  16. import { getAwardExpress, getAwardPackages } from '@/services/award';
  17. interface PackageItem {
  18. id: string;
  19. expressNo: string;
  20. expressCompany: string;
  21. itemList: Array<{ cover: string; name: string }>;
  22. }
  23. interface ExpressDetail {
  24. expressNo: string;
  25. expressCompany: string;
  26. traces: Array<{ time: string; content: string }>;
  27. }
  28. export default function PackagesScreen() {
  29. const router = useRouter();
  30. const insets = useSafeAreaInsets();
  31. const { tradeNo } = useLocalSearchParams<{ tradeNo: string }>();
  32. const [loading, setLoading] = useState(true);
  33. const [packages, setPackages] = useState<PackageItem[]>([]);
  34. const [selectedPackage, setSelectedPackage] = useState<PackageItem | null>(null);
  35. const [expressDetail, setExpressDetail] = useState<ExpressDetail | null>(null);
  36. const [loadingExpress, setLoadingExpress] = useState(false);
  37. const loadPackages = useCallback(async () => {
  38. if (!tradeNo) return;
  39. setLoading(true);
  40. try {
  41. const res = await getAwardPackages(tradeNo);
  42. if (res && Array.isArray(res)) {
  43. setPackages(res);
  44. if (res.length > 0) {
  45. setSelectedPackage(res[0]);
  46. loadExpressDetail(res[0].id);
  47. }
  48. }
  49. } catch (e) {
  50. console.error('加载包裹信息失败:', e);
  51. }
  52. setLoading(false);
  53. }, [tradeNo]);
  54. const loadExpressDetail = async (packageId: string) => {
  55. if (!tradeNo) return;
  56. setLoadingExpress(true);
  57. try {
  58. const res = await getAwardExpress(tradeNo, packageId);
  59. setExpressDetail(res);
  60. } catch (e) {
  61. console.error('加载物流详情失败:', e);
  62. }
  63. setLoadingExpress(false);
  64. };
  65. useEffect(() => {
  66. loadPackages();
  67. }, [loadPackages]);
  68. const selectPackage = (pkg: PackageItem) => {
  69. setSelectedPackage(pkg);
  70. loadExpressDetail(pkg.id);
  71. };
  72. if (loading) {
  73. return (
  74. <View style={styles.loadingContainer}>
  75. <ActivityIndicator size="large" color="#fff" />
  76. </View>
  77. );
  78. }
  79. return (
  80. <View style={styles.container}>
  81. <StatusBar barStyle="light-content" />
  82. <ImageBackground source={{ uri: Images.mine.kaixinMineBg }} style={styles.background} resizeMode="cover">
  83. <Image source={{ uri: Images.mine.kaixinMineHeadBg }} style={styles.headerBg} contentFit="cover" />
  84. {/* 顶部导航 */}
  85. <View style={[styles.header, { paddingTop: insets.top }]}>
  86. <TouchableOpacity style={styles.backBtn} onPress={() => router.back()}>
  87. <Text style={styles.backIcon}>‹</Text>
  88. </TouchableOpacity>
  89. <Text style={styles.title}>物流信息</Text>
  90. <View style={styles.placeholder} />
  91. </View>
  92. <ScrollView style={[styles.content, { paddingTop: insets.top + 50 }]} showsVerticalScrollIndicator={false}>
  93. {packages.length === 0 ? (
  94. <View style={styles.emptyBox}>
  95. <Text style={styles.emptyText}>暂无物流信息</Text>
  96. </View>
  97. ) : (
  98. <>
  99. {/* 包裹选择 */}
  100. {packages.length > 1 && (
  101. <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.packageTabs}>
  102. {packages.map((pkg, idx) => (
  103. <TouchableOpacity
  104. key={pkg.id}
  105. style={[styles.packageTab, selectedPackage?.id === pkg.id && styles.packageTabActive]}
  106. onPress={() => selectPackage(pkg)}
  107. >
  108. <Text style={[styles.packageTabText, selectedPackage?.id === pkg.id && styles.packageTabTextActive]}>
  109. 包裹{idx + 1}
  110. </Text>
  111. </TouchableOpacity>
  112. ))}
  113. </ScrollView>
  114. )}
  115. {/* 快递信息 */}
  116. {selectedPackage && (
  117. <View style={styles.expressInfo}>
  118. <Text style={styles.expressCompany}>{selectedPackage.expressCompany || '快递公司'}</Text>
  119. <Text style={styles.expressNo}>快递单号:{selectedPackage.expressNo || '暂无'}</Text>
  120. </View>
  121. )}
  122. {/* 商品列表 */}
  123. {selectedPackage && selectedPackage.itemList && (
  124. <View style={styles.goodsSection}>
  125. <ScrollView horizontal showsHorizontalScrollIndicator={false}>
  126. {selectedPackage.itemList.map((item, idx) => (
  127. <View key={idx} style={styles.goodsItem}>
  128. <Image source={{ uri: item.cover }} style={styles.goodsImg} contentFit="contain" />
  129. </View>
  130. ))}
  131. </ScrollView>
  132. </View>
  133. )}
  134. {/* 物流轨迹 */}
  135. {loadingExpress ? (
  136. <ActivityIndicator color="#fff" style={{ marginTop: 20 }} />
  137. ) : expressDetail && expressDetail.traces && expressDetail.traces.length > 0 ? (
  138. <View style={styles.tracesSection}>
  139. <Text style={styles.tracesTitle}>物流轨迹</Text>
  140. {expressDetail.traces.map((trace, idx) => (
  141. <View key={idx} style={styles.traceItem}>
  142. <View style={styles.traceDot} />
  143. {idx < expressDetail.traces.length - 1 && <View style={styles.traceLine} />}
  144. <View style={styles.traceContent}>
  145. <Text style={styles.traceTime}>{trace.time}</Text>
  146. <Text style={styles.traceText}>{trace.content}</Text>
  147. </View>
  148. </View>
  149. ))}
  150. </View>
  151. ) : (
  152. <View style={styles.noTraces}>
  153. <Text style={styles.noTracesText}>暂无物流轨迹</Text>
  154. </View>
  155. )}
  156. </>
  157. )}
  158. <View style={{ height: 50 }} />
  159. </ScrollView>
  160. </ImageBackground>
  161. </View>
  162. );
  163. }
  164. const styles = StyleSheet.create({
  165. container: { flex: 1, backgroundColor: '#1a1a2e' },
  166. background: { flex: 1 },
  167. loadingContainer: { flex: 1, backgroundColor: '#1a1a2e', justifyContent: 'center', alignItems: 'center' },
  168. headerBg: { position: 'absolute', top: 0, left: 0, width: '100%', height: 160 },
  169. header: { position: 'absolute', top: 0, left: 0, right: 0, zIndex: 100, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 10, paddingBottom: 10 },
  170. backBtn: { width: 40, height: 40, justifyContent: 'center', alignItems: 'center' },
  171. backIcon: { fontSize: 32, color: '#fff', fontWeight: 'bold' },
  172. title: { color: '#fff', fontSize: 16, fontWeight: 'bold' },
  173. placeholder: { width: 40 },
  174. content: { flex: 1, paddingHorizontal: 15 },
  175. emptyBox: { marginTop: 100, alignItems: 'center' },
  176. emptyText: { color: '#999', fontSize: 14 },
  177. packageTabs: { flexDirection: 'row', marginBottom: 15 },
  178. packageTab: { paddingHorizontal: 20, paddingVertical: 10, backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 20, marginRight: 10 },
  179. packageTabActive: { backgroundColor: '#FC7D2E' },
  180. packageTabText: { color: '#aaa', fontSize: 14 },
  181. packageTabTextActive: { color: '#fff' },
  182. expressInfo: { backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 10, padding: 15, marginBottom: 15 },
  183. expressCompany: { color: '#fff', fontSize: 16, fontWeight: 'bold', marginBottom: 5 },
  184. expressNo: { color: '#aaa', fontSize: 14 },
  185. goodsSection: { backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 10, padding: 15, marginBottom: 15 },
  186. goodsItem: { width: 60, height: 60, backgroundColor: '#fff', borderRadius: 6, marginRight: 10, alignItems: 'center', justifyContent: 'center' },
  187. goodsImg: { width: 50, height: 50 },
  188. tracesSection: { backgroundColor: 'rgba(255,255,255,0.1)', borderRadius: 10, padding: 15 },
  189. tracesTitle: { color: '#fff', fontSize: 16, fontWeight: 'bold', marginBottom: 15 },
  190. traceItem: { flexDirection: 'row', position: 'relative', paddingLeft: 20, marginBottom: 15 },
  191. traceDot: { position: 'absolute', left: 0, top: 6, width: 10, height: 10, borderRadius: 5, backgroundColor: '#FC7D2E' },
  192. traceLine: { position: 'absolute', left: 4, top: 16, width: 2, height: '100%', backgroundColor: 'rgba(255,255,255,0.2)' },
  193. traceContent: { flex: 1 },
  194. traceTime: { color: '#aaa', fontSize: 12, marginBottom: 4 },
  195. traceText: { color: '#fff', fontSize: 14 },
  196. noTraces: { marginTop: 20, alignItems: 'center' },
  197. noTracesText: { color: '#999', fontSize: 14 },
  198. });