| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438 |
- 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,
- ScrollView,
- StyleSheet,
- Text,
- TouchableOpacity,
- View,
- } 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";
- interface GroupedGoods {
- total: number;
- 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;
- }>;
- onClose: () => void;
- onSuccess: () => void;
- }
- 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);
- };
- useEffect(() => {
- if (visible && selectedItems.length > 0) {
- loadData();
- }
- }, [visible, selectedItems]);
- const loadData = async () => {
- setLoading(true);
- try {
- // 获取默认地址
- const addr = await getDefaultAddress();
- setAddress(addr);
- // 获取提货预览
- const ids = selectedItems.map((item) => item.id);
- const res = await takePreview(ids, addr?.id || "");
- if (res) {
- setExpressAmount(res.expressAmount || 0);
- }
- // 合并相同商品
- const goodsMap: Record<string, GroupedGoods> = {};
- selectedItems.forEach((item) => {
- const key = `${item.spu.id}_${item.level}`;
- if (goodsMap[key]) {
- goodsMap[key].total += 1;
- } else {
- goodsMap[key] = {
- total: 1,
- data: {
- id: item.id,
- cover: item.spu.cover,
- spuId: item.spu.id,
- level: item.level,
- name: item.spu.name,
- },
- };
- }
- });
- setGoodsList(Object.values(goodsMap));
- } catch (e) {
- console.error("加载提货信息失败:", e);
- }
- setLoading(false);
- };
- const goToAddress = () => {
- onClose();
- router.push("/address?type=1" as any);
- };
- /*
- * Handle Submit with Payment Choice
- */
- const handleSubmit = async () => {
- if (!address) {
- showAlert("请选择收货地址");
- return;
- }
- if (expressAmount > 0) {
- Alert.alert("支付运费", `需支付运费 ¥${expressAmount}`, [
- { text: "取消", style: "cancel" },
- { text: "钱包支付", onPress: () => processTakeApply("WALLET") },
- { text: "支付宝支付", onPress: () => processTakeApply("ALIPAY_APP") },
- ]);
- } else {
- processTakeApply("WALLET");
- }
- };
- const processTakeApply = async (paymentType: string) => {
- if (submitting) return;
- setSubmitting(true);
- try {
- 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) {
- 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();
- }
- } catch (e) {
- console.error("提货失败:", e);
- // Usually axios interceptor handles error alerts, but just in case
- // showAlert('提货失败');
- }
- setSubmitting(false);
- };
- return (
- <Modal
- visible={visible}
- transparent
- animationType="slide"
- onRequestClose={onClose}
- >
- <View style={styles.overlay}>
- <TouchableOpacity
- style={styles.overlayBg}
- onPress={onClose}
- activeOpacity={1}
- />
- <View style={[styles.container, { paddingBottom: insets.bottom + 20 }]}>
- {/* 标题 */}
- <View style={styles.header}>
- <Text style={styles.title}>提货</Text>
- <TouchableOpacity style={styles.closeBtn} onPress={onClose}>
- <Text style={styles.closeBtnText}>×</Text>
- </TouchableOpacity>
- </View>
- {loading ? (
- <View style={styles.loadingBox}>
- <ActivityIndicator size="large" color="#FC7D2E" />
- </View>
- ) : (
- <>
- {/* 商品列表 */}
- <View style={styles.goodsSection}>
- <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"
- />
- <View style={styles.goodsCount}>
- <Text style={styles.goodsCountText}>
- x{goods.total}
- </Text>
- </View>
- </View>
- ))}
- </ScrollView>
- </View>
- {/* 运费 */}
- {expressAmount > 0 && (
- <View style={styles.feeRow}>
- <Text style={styles.feeLabel}>运费</Text>
- <Text style={styles.feeValue}>¥{expressAmount}</Text>
- </View>
- )}
- {/* 收货地址 */}
- <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>
- </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>
- </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({
- 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",
- },
- 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" },
- 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" },
- });
|