shop.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  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. Alert,
  7. Clipboard,
  8. ImageBackground,
  9. RefreshControl,
  10. ScrollView,
  11. StatusBar,
  12. StyleSheet,
  13. Text,
  14. TouchableOpacity,
  15. View
  16. } from 'react-native';
  17. import { useSafeAreaInsets } from 'react-native-safe-area-context';
  18. import { Images } from '@/constants/images';
  19. import { getOrders, OrderItem } from '@/services/mall';
  20. const tabs = [
  21. { label: '全部', value: 'all' },
  22. { label: '待支付', value: 'to_pay' },
  23. { label: '待补款', value: 'to_payLeft' },
  24. { label: '待发货', value: 'to_delivery' },
  25. { label: '待收货', value: 'to_receive' },
  26. { label: '已完成', value: 'complete' },
  27. { label: '已取消', value: 'uncomplete' },
  28. ];
  29. export default function ShopOrdersScreen() {
  30. const router = useRouter();
  31. const insets = useSafeAreaInsets();
  32. const params = useLocalSearchParams();
  33. const [loading, setLoading] = useState(true);
  34. const [refreshing, setRefreshing] = useState(false);
  35. const [activeTab, setActiveTab] = useState(() => {
  36. if (params.active) return parseInt(params.active as string, 10);
  37. return 0;
  38. });
  39. const [orders, setOrders] = useState<OrderItem[]>([]);
  40. const [page, setPage] = useState(1);
  41. const loadData = useCallback(async (tabValue?: string, refresh = false) => {
  42. if (refresh) {
  43. setRefreshing(true);
  44. setPage(1);
  45. } else {
  46. setLoading(true);
  47. }
  48. try {
  49. let tabId: any = tabValue;
  50. if (tabValue === 'all') tabId = undefined;
  51. const data = await getOrders(refresh ? 1 : page, 10, tabId);
  52. if (data?.records) {
  53. setOrders(refresh ? data.records : [...orders, ...data.records]);
  54. }
  55. } catch (error) {
  56. console.error('加载商城订单失败:', error);
  57. }
  58. setLoading(false);
  59. setRefreshing(false);
  60. }, [page, orders]);
  61. useEffect(() => {
  62. loadData(tabs[activeTab].value, true);
  63. }, [activeTab]);
  64. const onRefresh = () => {
  65. loadData(tabs[activeTab].value, true);
  66. };
  67. const switchTab = (index: number) => {
  68. if (index !== activeTab) {
  69. setActiveTab(index);
  70. setOrders([]);
  71. }
  72. };
  73. const goBack = () => {
  74. router.back();
  75. };
  76. const goBox = () => {
  77. router.replace('/orders' as any);
  78. };
  79. const copyTradeNo = (tradeNo: string) => {
  80. Clipboard.setString(tradeNo);
  81. Alert.alert('提示', '订单号已复制');
  82. };
  83. const getStatusInfo = (status: number) => {
  84. switch (status) {
  85. case 0: return { text: '待支付', color: '#F62C71' };
  86. case 99: return { text: '已完成', color: '#20D7EF' };
  87. case 1: return { text: '待发货', color: '#F62C71' };
  88. case 2: return { text: '待收货', color: '#F62C71' };
  89. case 10: return { text: '已取消', color: '#999' };
  90. case 11: return { text: '超时取消', color: '#999' };
  91. case 12: return { text: '系统取消', color: '#999' };
  92. default: return { text: '未知', color: '#999' };
  93. }
  94. };
  95. return (
  96. <View style={styles.container}>
  97. <StatusBar barStyle="light-content" />
  98. <ImageBackground
  99. source={{ uri: Images.mine.kaixinMineBg }}
  100. style={styles.background}
  101. resizeMode="cover"
  102. >
  103. <View style={[styles.header, { paddingTop: insets.top }]}>
  104. <TouchableOpacity style={styles.backBtn} onPress={goBack}>
  105. <Text style={styles.backText}>‹</Text>
  106. </TouchableOpacity>
  107. <Text style={styles.headerTitle}>商城订单</Text>
  108. <View style={styles.placeholder} />
  109. </View>
  110. <View style={styles.tabBar}>
  111. <ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabScroll}>
  112. {tabs.map((tab, index) => (
  113. <TouchableOpacity
  114. key={index}
  115. onPress={() => switchTab(index)}
  116. activeOpacity={0.8}
  117. >
  118. <ImageBackground
  119. source={{ uri: activeTab === index ? Images.home.typeBgOn : Images.home.typeBg }}
  120. style={styles.tabItem}
  121. resizeMode="stretch"
  122. >
  123. <Text style={[styles.tabText, activeTab === index && styles.tabTextActive]}>
  124. {tab.label}
  125. </Text>
  126. </ImageBackground>
  127. </TouchableOpacity>
  128. ))}
  129. </ScrollView>
  130. </View>
  131. <ScrollView
  132. style={styles.scrollView}
  133. showsVerticalScrollIndicator={false}
  134. refreshControl={
  135. <RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor="#fff" />
  136. }
  137. >
  138. {loading && orders.length === 0 ? (
  139. <View style={styles.loadingContainer}>
  140. <ActivityIndicator size="large" color="#fff" />
  141. </View>
  142. ) : orders.length === 0 ? (
  143. <View style={styles.emptyContainer}>
  144. <Text style={styles.emptyText}>暂无订单</Text>
  145. </View>
  146. ) : (
  147. orders.map((item) => {
  148. const statusInfo = getStatusInfo(item.status);
  149. return (
  150. <ImageBackground
  151. key={item.tradeNo}
  152. source={{ uri: Images.common.itemBg }}
  153. style={styles.orderCard}
  154. resizeMode="stretch"
  155. >
  156. <View style={styles.cardContent}>
  157. {/* 顶部信息 */}
  158. <View style={styles.orderTop}>
  159. <View style={styles.flexRow}>
  160. <Text style={styles.orderTimeText}>下单时间:{item.createTime}</Text>
  161. {item.type === 2 && <View style={styles.presellTag}><Text style={styles.presellText}>预售订单</Text></View>}
  162. </View>
  163. <Text style={[styles.orderStatusText, { color: statusInfo.color }]}>
  164. {statusInfo.text}
  165. </Text>
  166. </View>
  167. {item.status === 0 && item.paymentTimeoutTime && (
  168. <Text style={styles.timeoutText}>
  169. {item.paymentTimeoutTime}将自动取消该订单,如有优惠券,将自动退回
  170. </Text>
  171. )}
  172. {/* 商品信息 */}
  173. <View style={styles.orderMiddle}>
  174. <ImageBackground
  175. source={{ uri: Images.box.detail.firstItemBg }}
  176. style={styles.productImgBg}
  177. resizeMode="stretch"
  178. >
  179. <Image source={{ uri: item.goodsCover }} style={styles.goodsImage} contentFit="cover" />
  180. </ImageBackground>
  181. <View style={styles.goodsInfo}>
  182. <Text style={styles.goodsName} numberOfLines={1}>{item.goodsName}</Text>
  183. <View style={styles.priceRow}>
  184. <Text style={styles.goodsPrice}>¥{item.paymentAmount}</Text>
  185. <Text style={styles.goodsQty}>X{item.quantity}</Text>
  186. </View>
  187. {(item.couponAmount ?? 0) > 0 && (
  188. <Text style={styles.discountText}>使用优惠券-{item.couponAmount}</Text>
  189. )}
  190. <View style={styles.totalRow}>
  191. <View style={styles.totalItem}>
  192. <Text style={styles.totalLabel}>总价:</Text>
  193. <Text style={styles.totalValueSymbol}>¥</Text>
  194. <Text style={styles.totalValue}>{item.totalAmount || item.paymentAmount}</Text>
  195. </View>
  196. <View style={[styles.totalItem, { marginLeft: 12 }]}>
  197. <Text style={styles.payLabel}>{item.status === 0 ? '需付款:' : '实付款:'}</Text>
  198. <Text style={styles.payValueSymbol}>¥</Text>
  199. <Text style={styles.payValue}>{item.paidAmount || item.paymentAmount}</Text>
  200. </View>
  201. </View>
  202. </View>
  203. </View>
  204. {/* 订单号 & 复制 */}
  205. <View style={styles.orderNoRow}>
  206. <Text style={styles.orderNo} numberOfLines={1}>订单号:{item.tradeNo}</Text>
  207. <TouchableOpacity style={styles.copyBtn} onPress={() => copyTradeNo(item.tradeNo)}>
  208. <Text style={styles.copyBtnText}>复制</Text>
  209. </TouchableOpacity>
  210. </View>
  211. {/* 功能按钮 */}
  212. <View style={styles.orderFooter}>
  213. {item.status === 0 && (
  214. <TouchableOpacity style={styles.actionBtn}>
  215. <Text style={styles.actionBtnText}>付款</Text>
  216. </TouchableOpacity>
  217. )}
  218. {item.status === 1 && (
  219. <TouchableOpacity style={[styles.actionBtn, styles.infoBtn]}>
  220. <Text style={styles.actionBtnText}>修改地址</Text>
  221. </TouchableOpacity>
  222. )}
  223. {item.status === 2 && (
  224. <TouchableOpacity style={styles.actionBtn}>
  225. <Text style={styles.actionBtnText}>确认收货</Text>
  226. </TouchableOpacity>
  227. )}
  228. {[2, 99].includes(item.status) && (
  229. <TouchableOpacity style={[styles.actionBtn, { marginLeft: 8 }]}>
  230. <Text style={styles.actionBtnText}>物流信息</Text>
  231. </TouchableOpacity>
  232. )}
  233. </View>
  234. </View>
  235. </ImageBackground>
  236. );
  237. })
  238. )}
  239. <View style={{ height: 80 }} />
  240. </ScrollView>
  241. {/* 奖池订单入口 */}
  242. <TouchableOpacity style={[styles.floatBtn, { bottom: insets.bottom + 154 }]} onPress={goBox}>
  243. <Image source={{ uri: Images.mine.boxOrder }} style={styles.floatIcon} contentFit="contain" />
  244. </TouchableOpacity>
  245. </ImageBackground>
  246. </View>
  247. );
  248. }
  249. const styles = StyleSheet.create({
  250. container: {
  251. flex: 1,
  252. backgroundColor: '#1a1a2e',
  253. },
  254. background: {
  255. flex: 1,
  256. },
  257. header: {
  258. flexDirection: 'row',
  259. alignItems: 'center',
  260. justifyContent: 'space-between',
  261. paddingHorizontal: 15,
  262. paddingBottom: 10,
  263. },
  264. backBtn: {
  265. width: 40,
  266. height: 40,
  267. justifyContent: 'center',
  268. alignItems: 'center',
  269. },
  270. backText: {
  271. color: '#fff',
  272. fontSize: 32,
  273. fontWeight: 'bold',
  274. },
  275. headerTitle: {
  276. color: '#fff',
  277. fontSize: 15,
  278. fontWeight: 'bold',
  279. },
  280. placeholder: {
  281. width: 40,
  282. },
  283. tabBar: {
  284. height: 50,
  285. marginHorizontal: 10,
  286. marginBottom: 7,
  287. },
  288. tabScroll: {
  289. flex: 1,
  290. },
  291. tabItem: {
  292. width: 80,
  293. height: 40,
  294. justifyContent: 'center',
  295. alignItems: 'center',
  296. marginRight: 10,
  297. paddingTop: 4,
  298. },
  299. tabText: {
  300. color: '#000',
  301. fontSize: 14,
  302. fontWeight: '400',
  303. },
  304. tabTextActive: {
  305. fontWeight: 'bold',
  306. },
  307. scrollView: {
  308. flex: 1,
  309. paddingHorizontal: 10,
  310. },
  311. loadingContainer: {
  312. paddingTop: 100,
  313. alignItems: 'center',
  314. },
  315. emptyContainer: {
  316. paddingTop: 100,
  317. alignItems: 'center',
  318. },
  319. emptyText: {
  320. color: '#999',
  321. fontSize: 14,
  322. },
  323. orderCard: {
  324. marginVertical: 10,
  325. minHeight: 220,
  326. },
  327. cardContent: {
  328. paddingHorizontal: 20,
  329. paddingVertical: 10,
  330. },
  331. orderTop: {
  332. flexDirection: 'row',
  333. justifyContent: 'space-between',
  334. paddingVertical: 15,
  335. borderBottomWidth: 1,
  336. borderBottomColor: '#CDCDCD',
  337. alignItems: 'center',
  338. },
  339. flexRow: {
  340. flexDirection: 'row',
  341. alignItems: 'center',
  342. },
  343. orderTimeText: {
  344. fontSize: 12,
  345. color: '#333',
  346. },
  347. presellTag: {
  348. backgroundColor: '#F1423D',
  349. borderRadius: 11,
  350. paddingHorizontal: 8,
  351. height: 22,
  352. justifyContent: 'center',
  353. marginLeft: 8,
  354. },
  355. presellText: {
  356. color: '#fff',
  357. fontSize: 12,
  358. },
  359. orderStatusText: {
  360. fontSize: 12,
  361. fontWeight: '500',
  362. },
  363. timeoutText: {
  364. fontSize: 11,
  365. color: '#999',
  366. marginTop: 5,
  367. },
  368. orderMiddle: {
  369. flexDirection: 'row',
  370. paddingVertical: 12,
  371. borderBottomWidth: 1,
  372. borderBottomColor: '#eee',
  373. },
  374. productImgBg: {
  375. width: 70,
  376. height: 70,
  377. padding: 7,
  378. },
  379. goodsImage: {
  380. width: '100%',
  381. height: '100%',
  382. borderRadius: 3,
  383. },
  384. goodsInfo: {
  385. flex: 1,
  386. marginLeft: 12,
  387. justifyContent: 'center',
  388. },
  389. goodsName: {
  390. fontSize: 14,
  391. color: '#333',
  392. fontWeight: 'bold',
  393. },
  394. priceRow: {
  395. flexDirection: 'row',
  396. justifyContent: 'space-between',
  397. marginTop: 8,
  398. },
  399. goodsPrice: {
  400. fontSize: 14,
  401. color: '#333',
  402. fontWeight: 'bold',
  403. },
  404. goodsQty: {
  405. fontSize: 12,
  406. color: '#333',
  407. },
  408. discountText: {
  409. fontSize: 12,
  410. color: '#F1423D',
  411. marginTop: 4,
  412. },
  413. totalRow: {
  414. flexDirection: 'row',
  415. alignItems: 'center',
  416. marginTop: 8,
  417. },
  418. totalItem: {
  419. flexDirection: 'row',
  420. alignItems: 'baseline',
  421. },
  422. totalLabel: {
  423. fontSize: 11,
  424. color: '#666',
  425. },
  426. totalValueSymbol: {
  427. fontSize: 11,
  428. color: '#666',
  429. },
  430. totalValue: {
  431. fontSize: 14,
  432. color: '#666',
  433. fontWeight: 'bold',
  434. },
  435. payLabel: {
  436. fontSize: 12,
  437. color: '#F1423D',
  438. },
  439. payValueSymbol: {
  440. fontSize: 12,
  441. color: '#F1423D',
  442. },
  443. payValue: {
  444. fontSize: 18,
  445. color: '#F1423D',
  446. fontWeight: 'bold',
  447. },
  448. orderNoRow: {
  449. flexDirection: 'row',
  450. alignItems: 'center',
  451. justifyContent: 'space-between',
  452. paddingVertical: 12,
  453. borderBottomWidth: 1,
  454. borderBottomColor: '#CDCDCD',
  455. },
  456. orderNo: {
  457. flex: 1,
  458. fontSize: 12,
  459. color: '#666',
  460. },
  461. copyBtn: {
  462. paddingHorizontal: 6,
  463. paddingVertical: 3,
  464. backgroundColor: '#1FA4FF',
  465. borderRadius: 4,
  466. },
  467. copyBtnText: {
  468. color: '#fff',
  469. fontSize: 12,
  470. },
  471. orderFooter: {
  472. flexDirection: 'row',
  473. justifyContent: 'flex-end',
  474. paddingVertical: 12,
  475. },
  476. actionBtn: {
  477. height: 24,
  478. paddingHorizontal: 10,
  479. backgroundColor: '#1FA4FF',
  480. borderRadius: 12,
  481. justifyContent: 'center',
  482. alignItems: 'center',
  483. },
  484. infoBtn: {
  485. backgroundColor: '#1FA4FF',
  486. },
  487. actionBtnText: {
  488. color: '#fff',
  489. fontSize: 12,
  490. },
  491. floatBtn: {
  492. position: 'absolute',
  493. left: 10,
  494. width: 60,
  495. height: 63,
  496. },
  497. floatIcon: {
  498. width: '100%',
  499. height: '100%',
  500. },
  501. });