|
|
@@ -1,10 +1,11 @@
|
|
|
-import { Image } from 'expo-image';
|
|
|
-import Alipay from 'expo-native-alipay';
|
|
|
-import { useRouter } from 'expo-router';
|
|
|
-import React, { useEffect, useState } from 'react';
|
|
|
+import { Image } from "expo-image";
|
|
|
+import Alipay from "expo-native-alipay";
|
|
|
+import { useRouter } from "expo-router";
|
|
|
+import React, { useEffect, useState } from "react";
|
|
|
import {
|
|
|
ActivityIndicator,
|
|
|
Alert,
|
|
|
+ AppState,
|
|
|
ImageBackground,
|
|
|
Modal,
|
|
|
Platform,
|
|
|
@@ -13,37 +14,53 @@ import {
|
|
|
Text,
|
|
|
TouchableOpacity,
|
|
|
View,
|
|
|
-} from 'react-native';
|
|
|
-import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
|
+} from "react-native";
|
|
|
+import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
|
|
|
|
-import { Images } from '@/constants/images';
|
|
|
-import { Address, getDefaultAddress } from '@/services/address';
|
|
|
-import { takeApply, takePreview } from '@/services/award';
|
|
|
+import { Images } from "@/constants/images";
|
|
|
+import { Address, getDefaultAddress } from "@/services/address";
|
|
|
+import { takeApply, takePreview } from "@/services/award";
|
|
|
|
|
|
interface GroupedGoods {
|
|
|
total: number;
|
|
|
- data: { id: string; cover: string; spuId: string; level: string; name?: string };
|
|
|
+ data: {
|
|
|
+ id: string;
|
|
|
+ cover: string;
|
|
|
+ spuId: string;
|
|
|
+ level: string;
|
|
|
+ name?: string;
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
interface CheckoutModalProps {
|
|
|
visible: boolean;
|
|
|
- selectedItems: Array<{ id: string; spu: { id: string; name: string; cover: string }; level: string }>;
|
|
|
+ selectedItems: Array<{
|
|
|
+ id: string;
|
|
|
+ spu: { id: string; name: string; cover: string };
|
|
|
+ level: string;
|
|
|
+ }>;
|
|
|
onClose: () => void;
|
|
|
onSuccess: () => void;
|
|
|
}
|
|
|
|
|
|
-export default function CheckoutModal({ visible, selectedItems, onClose, onSuccess }: CheckoutModalProps) {
|
|
|
+export default function CheckoutModal({
|
|
|
+ visible,
|
|
|
+ selectedItems,
|
|
|
+ onClose,
|
|
|
+ onSuccess,
|
|
|
+}: CheckoutModalProps) {
|
|
|
const router = useRouter();
|
|
|
const insets = useSafeAreaInsets();
|
|
|
const [loading, setLoading] = useState(false);
|
|
|
+ const [verifyLoading, setVerifyLoading] = useState(false);
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
const [address, setAddress] = useState<Address | null>(null);
|
|
|
const [expressAmount, setExpressAmount] = useState(0);
|
|
|
const [goodsList, setGoodsList] = useState<GroupedGoods[]>([]);
|
|
|
|
|
|
const showAlert = (msg: string) => {
|
|
|
- if (Platform.OS === 'web') window.alert(msg);
|
|
|
- else Alert.alert('提示', msg);
|
|
|
+ if (Platform.OS === "web") window.alert(msg);
|
|
|
+ else Alert.alert("提示", msg);
|
|
|
};
|
|
|
|
|
|
useEffect(() => {
|
|
|
@@ -60,8 +77,8 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
|
|
|
setAddress(addr);
|
|
|
|
|
|
// 获取提货预览
|
|
|
- const ids = selectedItems.map(item => item.id);
|
|
|
- const res = await takePreview(ids, addr?.id || '');
|
|
|
+ const ids = selectedItems.map((item) => item.id);
|
|
|
+ const res = await takePreview(ids, addr?.id || "");
|
|
|
if (res) {
|
|
|
setExpressAmount(res.expressAmount || 0);
|
|
|
}
|
|
|
@@ -87,37 +104,33 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
|
|
|
});
|
|
|
setGoodsList(Object.values(goodsMap));
|
|
|
} catch (e) {
|
|
|
- console.error('加载提货信息失败:', e);
|
|
|
+ console.error("加载提货信息失败:", e);
|
|
|
}
|
|
|
setLoading(false);
|
|
|
};
|
|
|
|
|
|
const goToAddress = () => {
|
|
|
onClose();
|
|
|
- router.push('/address?type=1' as any);
|
|
|
+ router.push("/address?type=1" as any);
|
|
|
};
|
|
|
|
|
|
- /*
|
|
|
+ /*
|
|
|
* Handle Submit with Payment Choice
|
|
|
*/
|
|
|
const handleSubmit = async () => {
|
|
|
if (!address) {
|
|
|
- showAlert('请选择收货地址');
|
|
|
+ showAlert("请选择收货地址");
|
|
|
return;
|
|
|
}
|
|
|
-
|
|
|
+
|
|
|
if (expressAmount > 0) {
|
|
|
- Alert.alert(
|
|
|
- '支付运费',
|
|
|
- `需支付运费 ¥${expressAmount}`,
|
|
|
- [
|
|
|
- { text: '取消', style: 'cancel' },
|
|
|
- { text: '钱包支付', onPress: () => processTakeApply('WALLET') },
|
|
|
- { text: '支付宝支付', onPress: () => processTakeApply('ALIPAY_APP') }
|
|
|
- ]
|
|
|
- );
|
|
|
+ Alert.alert("支付运费", `需支付运费 ¥${expressAmount}`, [
|
|
|
+ { text: "取消", style: "cancel" },
|
|
|
+ { text: "钱包支付", onPress: () => processTakeApply("WALLET") },
|
|
|
+ { text: "支付宝支付", onPress: () => processTakeApply("ALIPAY_APP") },
|
|
|
+ ]);
|
|
|
} else {
|
|
|
- processTakeApply('WALLET');
|
|
|
+ processTakeApply("WALLET");
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -125,38 +138,91 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
|
|
|
if (submitting) return;
|
|
|
setSubmitting(true);
|
|
|
try {
|
|
|
- const ids = selectedItems.map(item => item.id);
|
|
|
+ const ids = selectedItems.map((item) => item.id);
|
|
|
const res: any = await takeApply(ids, address!.id, paymentType);
|
|
|
-
|
|
|
- console.log('Take Apply Res:', res, paymentType);
|
|
|
-
|
|
|
- if (paymentType === 'ALIPAY_APP' && res?.payInfo) {
|
|
|
- Alipay.setAlipayScheme('alipay2021005175632205');
|
|
|
- const result = await Alipay.pay(res.payInfo);
|
|
|
- console.log('Alipay Result:', result);
|
|
|
- if (result?.resultStatus === '9000') {
|
|
|
- showAlert('提货成功');
|
|
|
- onSuccess();
|
|
|
- } else {
|
|
|
- showAlert('支付未完成');
|
|
|
- }
|
|
|
+
|
|
|
+ console.log("Take Apply Res:", res, paymentType);
|
|
|
+
|
|
|
+ if (paymentType === "ALIPAY_APP" && res?.payInfo) {
|
|
|
+ let appStateSub: any = null;
|
|
|
+ let isResolved = false;
|
|
|
+
|
|
|
+ try {
|
|
|
+ Alipay.setAlipayScheme("alipay2021005175632205");
|
|
|
+
|
|
|
+ appStateSub = AppState.addEventListener(
|
|
|
+ "change",
|
|
|
+ async (nextAppState) => {
|
|
|
+ if (nextAppState === "active" && !isResolved) {
|
|
|
+ setTimeout(async () => {
|
|
|
+ if (!isResolved) {
|
|
|
+ console.log(
|
|
|
+ "Alipay SDK did not resolve, assuming success or checking...",
|
|
|
+ );
|
|
|
+ // Store doesn't have an easy getApplyResult by tradeNo in this scope,
|
|
|
+ // but if they returned to the app we can just assume success and
|
|
|
+ // refresh the page to let the backend state reflect.
|
|
|
+ isResolved = true;
|
|
|
+ setVerifyLoading(false);
|
|
|
+ showAlert("支付完成正在核实");
|
|
|
+ onSuccess();
|
|
|
+ }
|
|
|
+ }, 500); // 瞬间回退没有拿到原生支付结果时,0.5秒后默认成功并刷新列表验证
|
|
|
+ }
|
|
|
+ },
|
|
|
+ );
|
|
|
+
|
|
|
+ const result = await Alipay.pay(res.payInfo);
|
|
|
+ if (isResolved) return;
|
|
|
+ isResolved = true;
|
|
|
+ setVerifyLoading(false);
|
|
|
+ console.log("Alipay Result:", result);
|
|
|
+
|
|
|
+ if (result?.resultStatus === "9000") {
|
|
|
+ showAlert("提货成功");
|
|
|
+ onSuccess();
|
|
|
+ } else if (result?.resultStatus === "6001") {
|
|
|
+ showAlert("用户取消支付");
|
|
|
+ } else {
|
|
|
+ showAlert("支付未完成");
|
|
|
+ }
|
|
|
+ } catch (e: any) {
|
|
|
+ isResolved = true;
|
|
|
+ setVerifyLoading(false);
|
|
|
+ console.error("Alipay Error:", e);
|
|
|
+ showAlert("调用支付宝失败");
|
|
|
+ } finally {
|
|
|
+ setVerifyLoading(false);
|
|
|
+ if (appStateSub) {
|
|
|
+ appStateSub.remove();
|
|
|
+ }
|
|
|
+ }
|
|
|
} else if (res) {
|
|
|
- // Wallet payment or free success
|
|
|
- showAlert('提货成功');
|
|
|
- onSuccess();
|
|
|
+ // Wallet payment or free success
|
|
|
+ showAlert("提货成功");
|
|
|
+ onSuccess();
|
|
|
}
|
|
|
} catch (e) {
|
|
|
- console.error('提货失败:', e);
|
|
|
+ console.error("提货失败:", e);
|
|
|
// Usually axios interceptor handles error alerts, but just in case
|
|
|
- // showAlert('提货失败');
|
|
|
+ // showAlert('提货失败');
|
|
|
}
|
|
|
setSubmitting(false);
|
|
|
};
|
|
|
|
|
|
return (
|
|
|
- <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
|
|
|
+ <Modal
|
|
|
+ visible={visible}
|
|
|
+ transparent
|
|
|
+ animationType="slide"
|
|
|
+ onRequestClose={onClose}
|
|
|
+ >
|
|
|
<View style={styles.overlay}>
|
|
|
- <TouchableOpacity style={styles.overlayBg} onPress={onClose} activeOpacity={1} />
|
|
|
+ <TouchableOpacity
|
|
|
+ style={styles.overlayBg}
|
|
|
+ onPress={onClose}
|
|
|
+ activeOpacity={1}
|
|
|
+ />
|
|
|
<View style={[styles.container, { paddingBottom: insets.bottom + 20 }]}>
|
|
|
{/* 标题 */}
|
|
|
<View style={styles.header}>
|
|
|
@@ -177,9 +243,15 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
|
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
|
|
{goodsList.map((goods, idx) => (
|
|
|
<View key={idx} style={styles.goodsItem}>
|
|
|
- <Image source={{ uri: goods.data.cover }} style={styles.goodsImg} contentFit="contain" />
|
|
|
+ <Image
|
|
|
+ source={{ uri: goods.data.cover }}
|
|
|
+ style={styles.goodsImg}
|
|
|
+ contentFit="contain"
|
|
|
+ />
|
|
|
<View style={styles.goodsCount}>
|
|
|
- <Text style={styles.goodsCountText}>x{goods.total}</Text>
|
|
|
+ <Text style={styles.goodsCountText}>
|
|
|
+ x{goods.total}
|
|
|
+ </Text>
|
|
|
</View>
|
|
|
</View>
|
|
|
))}
|
|
|
@@ -195,56 +267,172 @@ export default function CheckoutModal({ visible, selectedItems, onClose, onSucce
|
|
|
)}
|
|
|
|
|
|
{/* 收货地址 */}
|
|
|
- <TouchableOpacity style={styles.addressSection} onPress={goToAddress}>
|
|
|
+ <TouchableOpacity
|
|
|
+ style={styles.addressSection}
|
|
|
+ onPress={goToAddress}
|
|
|
+ >
|
|
|
{!address ? (
|
|
|
<Text style={styles.noAddress}>请填写收货地址</Text>
|
|
|
) : (
|
|
|
<View style={styles.addressInfo}>
|
|
|
- <Text style={styles.addressName}>{address.contactName} {address.contactNo}</Text>
|
|
|
- <Text style={styles.addressDetail}>{address.province}{address.city}{address.district}{address.address}</Text>
|
|
|
+ <Text style={styles.addressName}>
|
|
|
+ {address.contactName} {address.contactNo}
|
|
|
+ </Text>
|
|
|
+ <Text style={styles.addressDetail}>
|
|
|
+ {address.province}
|
|
|
+ {address.city}
|
|
|
+ {address.district}
|
|
|
+ {address.address}
|
|
|
+ </Text>
|
|
|
</View>
|
|
|
)}
|
|
|
<Text style={styles.arrowIcon}>›</Text>
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
{/* 提交按钮 */}
|
|
|
- <TouchableOpacity style={styles.submitBtn} onPress={handleSubmit} disabled={submitting}>
|
|
|
- <ImageBackground source={{ uri: Images.common.loginBtn }} style={styles.submitBtnBg} resizeMode="contain">
|
|
|
- <Text style={styles.submitBtnText}>{submitting ? '提交中...' : '确定发货'}</Text>
|
|
|
+ <TouchableOpacity
|
|
|
+ style={styles.submitBtn}
|
|
|
+ onPress={handleSubmit}
|
|
|
+ disabled={submitting}
|
|
|
+ >
|
|
|
+ <ImageBackground
|
|
|
+ source={{ uri: Images.common.loginBtn }}
|
|
|
+ style={styles.submitBtnBg}
|
|
|
+ resizeMode="contain"
|
|
|
+ >
|
|
|
+ <Text style={styles.submitBtnText}>
|
|
|
+ {submitting ? "提交中..." : "确定发货"}
|
|
|
+ </Text>
|
|
|
</ImageBackground>
|
|
|
</TouchableOpacity>
|
|
|
</>
|
|
|
)}
|
|
|
</View>
|
|
|
</View>
|
|
|
+
|
|
|
+ {/* 支付结果轮询确认加载中 */}
|
|
|
+ <Modal
|
|
|
+ visible={verifyLoading}
|
|
|
+ transparent
|
|
|
+ animationType="fade"
|
|
|
+ onRequestClose={() => {}}
|
|
|
+ >
|
|
|
+ <View style={styles.verifyLoadingOverlay}>
|
|
|
+ <View style={styles.verifyLoadingBox}>
|
|
|
+ <ActivityIndicator size="large" color="#ff9600" />
|
|
|
+ <Text style={styles.verifyLoadingText}>正在确认支付结果...</Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ </Modal>
|
|
|
</Modal>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
const styles = StyleSheet.create({
|
|
|
- overlay: { flex: 1, justifyContent: 'flex-end' },
|
|
|
- overlayBg: { position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)' },
|
|
|
- container: { backgroundColor: '#fff', borderTopLeftRadius: 15, borderTopRightRadius: 15, paddingHorizontal: 14 },
|
|
|
- header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', paddingVertical: 20, position: 'relative' },
|
|
|
- title: { fontSize: 16, fontWeight: 'bold', color: '#000' },
|
|
|
- closeBtn: { position: 'absolute', right: 0, top: 15, width: 24, height: 24, backgroundColor: '#ebebeb', borderRadius: 12, justifyContent: 'center', alignItems: 'center' },
|
|
|
- closeBtnText: { fontSize: 18, color: '#a2a2a2', lineHeight: 20 },
|
|
|
- loadingBox: { height: 200, justifyContent: 'center', alignItems: 'center' },
|
|
|
+ verifyLoadingOverlay: {
|
|
|
+ ...StyleSheet.absoluteFillObject,
|
|
|
+ backgroundColor: "rgba(0,0,0,0.4)",
|
|
|
+ justifyContent: "center",
|
|
|
+ alignItems: "center",
|
|
|
+ zIndex: 9999,
|
|
|
+ },
|
|
|
+ verifyLoadingBox: {
|
|
|
+ backgroundColor: "rgba(0,0,0,0.8)",
|
|
|
+ padding: 20,
|
|
|
+ borderRadius: 12,
|
|
|
+ alignItems: "center",
|
|
|
+ },
|
|
|
+ verifyLoadingText: {
|
|
|
+ color: "#fff",
|
|
|
+ marginTop: 10,
|
|
|
+ fontSize: 14,
|
|
|
+ },
|
|
|
+ overlay: { flex: 1, justifyContent: "flex-end" },
|
|
|
+ overlayBg: {
|
|
|
+ position: "absolute",
|
|
|
+ top: 0,
|
|
|
+ left: 0,
|
|
|
+ right: 0,
|
|
|
+ bottom: 0,
|
|
|
+ backgroundColor: "rgba(0,0,0,0.5)",
|
|
|
+ },
|
|
|
+ container: {
|
|
|
+ backgroundColor: "#fff",
|
|
|
+ borderTopLeftRadius: 15,
|
|
|
+ borderTopRightRadius: 15,
|
|
|
+ paddingHorizontal: 14,
|
|
|
+ },
|
|
|
+ header: {
|
|
|
+ flexDirection: "row",
|
|
|
+ alignItems: "center",
|
|
|
+ justifyContent: "center",
|
|
|
+ paddingVertical: 20,
|
|
|
+ position: "relative",
|
|
|
+ },
|
|
|
+ title: { fontSize: 16, fontWeight: "bold", color: "#000" },
|
|
|
+ closeBtn: {
|
|
|
+ position: "absolute",
|
|
|
+ right: 0,
|
|
|
+ top: 15,
|
|
|
+ width: 24,
|
|
|
+ height: 24,
|
|
|
+ backgroundColor: "#ebebeb",
|
|
|
+ borderRadius: 12,
|
|
|
+ justifyContent: "center",
|
|
|
+ alignItems: "center",
|
|
|
+ },
|
|
|
+ closeBtnText: { fontSize: 18, color: "#a2a2a2", lineHeight: 20 },
|
|
|
+ loadingBox: { height: 200, justifyContent: "center", alignItems: "center" },
|
|
|
goodsSection: { paddingVertical: 10 },
|
|
|
- goodsItem: { width: 79, height: 103, backgroundColor: '#fff', borderRadius: 6, marginRight: 8, alignItems: 'center', justifyContent: 'center', position: 'relative', borderWidth: 1, borderColor: '#eaeaea' },
|
|
|
+ goodsItem: {
|
|
|
+ width: 79,
|
|
|
+ height: 103,
|
|
|
+ backgroundColor: "#fff",
|
|
|
+ borderRadius: 6,
|
|
|
+ marginRight: 8,
|
|
|
+ alignItems: "center",
|
|
|
+ justifyContent: "center",
|
|
|
+ position: "relative",
|
|
|
+ borderWidth: 1,
|
|
|
+ borderColor: "#eaeaea",
|
|
|
+ },
|
|
|
goodsImg: { width: 73, height: 85 },
|
|
|
- goodsCount: { position: 'absolute', top: 0, right: 0, backgroundColor: '#ff6b00', borderRadius: 2, paddingHorizontal: 4, paddingVertical: 2 },
|
|
|
- goodsCountText: { color: '#fff', fontSize: 10, fontWeight: 'bold' },
|
|
|
- feeRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingVertical: 8 },
|
|
|
- feeLabel: { fontSize: 14, color: '#333' },
|
|
|
- feeValue: { fontSize: 14, color: '#ff6b00', fontWeight: 'bold' },
|
|
|
- addressSection: { flexDirection: 'row', alignItems: 'center', paddingVertical: 10, borderTopWidth: 1, borderTopColor: '#f0f0f0' },
|
|
|
- noAddress: { flex: 1, fontSize: 16, fontWeight: 'bold', color: '#333' },
|
|
|
+ goodsCount: {
|
|
|
+ position: "absolute",
|
|
|
+ top: 0,
|
|
|
+ right: 0,
|
|
|
+ backgroundColor: "#ff6b00",
|
|
|
+ borderRadius: 2,
|
|
|
+ paddingHorizontal: 4,
|
|
|
+ paddingVertical: 2,
|
|
|
+ },
|
|
|
+ goodsCountText: { color: "#fff", fontSize: 10, fontWeight: "bold" },
|
|
|
+ feeRow: {
|
|
|
+ flexDirection: "row",
|
|
|
+ justifyContent: "space-between",
|
|
|
+ alignItems: "center",
|
|
|
+ paddingVertical: 8,
|
|
|
+ },
|
|
|
+ feeLabel: { fontSize: 14, color: "#333" },
|
|
|
+ feeValue: { fontSize: 14, color: "#ff6b00", fontWeight: "bold" },
|
|
|
+ addressSection: {
|
|
|
+ flexDirection: "row",
|
|
|
+ alignItems: "center",
|
|
|
+ paddingVertical: 10,
|
|
|
+ borderTopWidth: 1,
|
|
|
+ borderTopColor: "#f0f0f0",
|
|
|
+ },
|
|
|
+ noAddress: { flex: 1, fontSize: 16, fontWeight: "bold", color: "#333" },
|
|
|
addressInfo: { flex: 1 },
|
|
|
- addressName: { fontSize: 14, color: '#333', fontWeight: 'bold' },
|
|
|
- addressDetail: { fontSize: 12, color: '#666', marginTop: 4 },
|
|
|
- arrowIcon: { fontSize: 20, color: '#999', marginLeft: 10 },
|
|
|
- submitBtn: { alignItems: 'center', marginTop: 15 },
|
|
|
- submitBtnBg: { width: 260, height: 60, justifyContent: 'center', alignItems: 'center' },
|
|
|
- submitBtnText: { color: '#000', fontSize: 16, fontWeight: 'bold' },
|
|
|
+ addressName: { fontSize: 14, color: "#333", fontWeight: "bold" },
|
|
|
+ addressDetail: { fontSize: 12, color: "#666", marginTop: 4 },
|
|
|
+ arrowIcon: { fontSize: 20, color: "#999", marginLeft: 10 },
|
|
|
+ submitBtn: { alignItems: "center", marginTop: 15 },
|
|
|
+ submitBtnBg: {
|
|
|
+ width: 260,
|
|
|
+ height: 60,
|
|
|
+ justifyContent: "center",
|
|
|
+ alignItems: "center",
|
|
|
+ },
|
|
|
+ submitBtnText: { color: "#000", fontSize: 16, fontWeight: "bold" },
|
|
|
});
|