| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- import { Image } from 'expo-image';
- import { useRouter } from 'expo-router';
- import React, { forwardRef, useImperativeHandle, useState } from 'react';
- import {
- Alert,
- Modal,
- ScrollView,
- StyleSheet,
- Switch,
- Text,
- TextInput,
- TouchableOpacity,
- View,
- } from 'react-native';
- import { useSafeAreaInsets } from 'react-native-safe-area-context';
- import { Address, getDefaultAddress } from '@/services/address';
- import { GoodsDetail, previewSubmit, PreviewSubmitResult, submitOrder } from '@/services/mall';
- interface CheckoutModalProps {
- data: GoodsDetail;
- goodsId: string;
- subjectId?: string;
- }
- export interface CheckoutModalRef {
- show: (preview: PreviewSubmitResult) => void;
- close: () => void;
- }
- export const CheckoutModal = forwardRef<CheckoutModalRef, CheckoutModalProps>(
- ({ data, goodsId, subjectId }, ref) => {
- const router = useRouter();
- const insets = useSafeAreaInsets();
- const [visible, setVisible] = useState(false);
- const [preview, setPreview] = useState<PreviewSubmitResult | null>(null);
- const [address, setAddress] = useState<Address | null>(null);
- const [quantity, setQuantity] = useState(1);
- const [phone, setPhone] = useState('');
- const [checked, setChecked] = useState(true);
- const [useWallet, setUseWallet] = useState(false);
- const [payType, setPayType] = useState<'alipay' | 'wxpay'>('alipay');
- const [submitting, setSubmitting] = useState(false);
- useImperativeHandle(ref, () => ({
- show: (previewData: PreviewSubmitResult) => {
- setPreview(previewData);
- setVisible(true);
- loadAddress();
- },
- close: () => {
- setVisible(false);
- },
- }));
- // 加载默认地址
- const loadAddress = async () => {
- try {
- const addr = await getDefaultAddress();
- setAddress(addr);
- } catch (error) {
- console.error('加载地址失败:', error);
- }
- };
- // 计算最终价格
- const lastPrice = preview
- ? data.sellType === 2
- ? preview.depositAmount || 0
- : preview.paymentAmount
- : data.price;
- // 增加数量
- const increment = async () => {
- if (subjectId) return; // 秒杀不能改数量
- if (quantity >= 5) return;
- const newQty = quantity + 1;
- setQuantity(newQty);
- // 重新获取价格
- const newPreview = await previewSubmit({ goodsId, quantity: newQty, subjectId });
- if (newPreview) setPreview(newPreview);
- };
- // 减少数量
- const decrement = async () => {
- if (quantity <= 1) return;
- const newQty = quantity - 1;
- setQuantity(newQty);
- const newPreview = await previewSubmit({ goodsId, quantity: newQty, subjectId });
- if (newPreview) setPreview(newPreview);
- };
- // 提交订单
- const handlePay = async () => {
- if (!address) {
- Alert.alert('提示', '请选择收货地址');
- return;
- }
- if (data.sellType === 2 && !phone) {
- Alert.alert('提示', '请填写尾款提醒手机号');
- return;
- }
- if (!checked) {
- Alert.alert('提示', '请勾选协议');
- return;
- }
- setSubmitting(true);
- try {
- const paymentType = useWallet ? 'WALLET' : payType === 'alipay' ? 'ALIPAY' : 'WXPAY';
- const result = await submitOrder({
- goodsId: data.id,
- paymentType,
- addressBookId: address.id,
- quantity,
- restNotifyMobile: phone,
- subjectId,
- });
- if (result) {
- if (result.paySuccess) {
- setVisible(false);
- Alert.alert('成功', '支付成功', [
- { text: '查看订单', onPress: () => router.push('/orders' as any) },
- ]);
- } else {
- // 需要跳转支付
- Alert.alert('提示', '请在支付页面完成支付');
- }
- }
- } catch (error) {
- console.error('提交订单失败:', error);
- Alert.alert('错误', '提交订单失败');
- }
- setSubmitting(false);
- };
- // 选择地址
- const selectAddress = () => {
- setVisible(false);
- router.push('/address?type=1' as any);
- };
- return (
- <Modal visible={visible} transparent animationType="slide">
- <View style={styles.overlay}>
- <TouchableOpacity style={styles.mask} onPress={() => setVisible(false)} />
- <View style={[styles.content, { paddingBottom: insets.bottom + 20 }]}>
- {/* 标题 */}
- <View style={styles.header}>
- <Text style={styles.title}>确认订单</Text>
- <TouchableOpacity onPress={() => setVisible(false)}>
- <Text style={styles.closeBtn}>×</Text>
- </TouchableOpacity>
- </View>
- <ScrollView style={styles.scrollContent} showsVerticalScrollIndicator={false}>
- {/* 商品信息 */}
- <View style={styles.goodsInfo}>
- <Image source={data.cover} style={styles.goodsImage} contentFit="cover" />
- <View style={styles.goodsDetail}>
- <Text style={styles.goodsName} numberOfLines={2}>{data.name}</Text>
- <View style={styles.priceRow}>
- <Text style={styles.price}>¥{data.subjectPrice || data.price}</Text>
- {data.sellType === 2 && (
- <Text style={styles.deposit}>定金:¥{data.deposit}</Text>
- )}
- </View>
- {/* 数量选择 */}
- <View style={styles.quantityRow}>
- <TouchableOpacity style={styles.qtyBtn} onPress={decrement}>
- <Text style={styles.qtyBtnText}>-</Text>
- </TouchableOpacity>
- <Text style={styles.qtyNum}>{quantity}</Text>
- <TouchableOpacity
- style={[styles.qtyBtn, subjectId ? styles.qtyBtnDisabled : null]}
- onPress={increment}
- >
- <Text style={styles.qtyBtnText}>+</Text>
- </TouchableOpacity>
- </View>
- </View>
- </View>
- {/* 优惠券 */}
- <View style={styles.row}>
- <Text style={styles.rowLabel}>优惠券</Text>
- <Text style={styles.rowValue}>-¥{preview?.couponAmount || 0}</Text>
- </View>
- {/* 钱包支付 */}
- {preview?.cash && (
- <View style={styles.row}>
- <Text style={styles.rowLabel}>
- 使用钱包支付 <Text style={styles.balance}>(余额:¥{preview.cash.balance})</Text>
- </Text>
- <Switch value={useWallet} onValueChange={setUseWallet} />
- </View>
- )}
- {/* 支付方式 */}
- {!useWallet && (
- <View style={styles.payTypeSection}>
- <TouchableOpacity
- style={styles.payTypeItem}
- onPress={() => setPayType('alipay')}
- >
- <Text style={styles.payTypeName}>支付宝</Text>
- <View style={[styles.radio, payType === 'alipay' && styles.radioActive]} />
- </TouchableOpacity>
- <TouchableOpacity
- style={styles.payTypeItem}
- onPress={() => setPayType('wxpay')}
- >
- <Text style={styles.payTypeName}>微信支付</Text>
- <View style={[styles.radio, payType === 'wxpay' && styles.radioActive]} />
- </TouchableOpacity>
- </View>
- )}
- {/* 尾款手机号 */}
- {data.sellType === 2 && (
- <View style={styles.phoneRow}>
- <Text style={styles.phoneLabel}>
- <Text style={styles.required}>*</Text> 尾款提醒手机号
- </Text>
- <TextInput
- style={styles.phoneInput}
- value={phone}
- onChangeText={setPhone}
- placeholder="尾款支付提醒短信将发至此号码"
- placeholderTextColor="#999"
- keyboardType="phone-pad"
- />
- </View>
- )}
- {/* 收货地址 */}
- <TouchableOpacity style={styles.addressRow} onPress={selectAddress}>
- {address ? (
- <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.noAddress}>
- <Text style={styles.required}>*</Text> 请填写收货地址
- </Text>
- )}
- <Text style={styles.arrow}>›</Text>
- </TouchableOpacity>
- {/* 协议 */}
- <View style={styles.agreementRow}>
- <Text style={styles.agreementText}>同意《平台服务协议》详情</Text>
- <Switch value={checked} onValueChange={setChecked} />
- </View>
- </ScrollView>
- {/* 支付按钮 */}
- <TouchableOpacity
- style={[styles.payBtn, submitting && styles.payBtnDisabled]}
- onPress={handlePay}
- disabled={submitting}
- >
- <Text style={styles.payBtnText}>
- ¥{lastPrice} 立即支付
- </Text>
- </TouchableOpacity>
- </View>
- </View>
- </Modal>
- );
- }
- );
- const styles = StyleSheet.create({
- overlay: {
- flex: 1,
- justifyContent: 'flex-end',
- },
- mask: {
- ...StyleSheet.absoluteFillObject,
- backgroundColor: 'rgba(0,0,0,0.5)',
- },
- content: {
- backgroundColor: '#fff',
- borderTopLeftRadius: 15,
- borderTopRightRadius: 15,
- maxHeight: '80%',
- },
- header: {
- flexDirection: 'row',
- justifyContent: 'center',
- alignItems: 'center',
- paddingVertical: 15,
- borderBottomWidth: 1,
- borderBottomColor: '#eee',
- },
- title: {
- fontSize: 16,
- fontWeight: '600',
- color: '#333',
- },
- closeBtn: {
- position: 'absolute',
- right: 15,
- fontSize: 28,
- color: '#999',
- },
- scrollContent: {
- paddingHorizontal: 15,
- },
- goodsInfo: {
- flexDirection: 'row',
- paddingVertical: 15,
- borderBottomWidth: 1,
- borderBottomColor: '#eee',
- },
- goodsImage: {
- width: 90,
- height: 90,
- borderRadius: 8,
- backgroundColor: '#f5f5f5',
- },
- goodsDetail: {
- flex: 1,
- marginLeft: 12,
- justifyContent: 'space-between',
- },
- goodsName: {
- fontSize: 14,
- color: '#333',
- lineHeight: 20,
- },
- priceRow: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- price: {
- fontSize: 18,
- fontWeight: 'bold',
- color: '#ff6b00',
- },
- deposit: {
- fontSize: 12,
- color: '#8b3dff',
- marginLeft: 10,
- backgroundColor: 'rgba(139,61,255,0.1)',
- paddingHorizontal: 8,
- paddingVertical: 2,
- borderRadius: 10,
- },
- quantityRow: {
- flexDirection: 'row',
- alignItems: 'center',
- },
- qtyBtn: {
- width: 28,
- height: 28,
- borderRadius: 14,
- backgroundColor: '#f5f5f5',
- justifyContent: 'center',
- alignItems: 'center',
- },
- qtyBtnDisabled: {
- opacity: 0.3,
- },
- qtyBtnText: {
- fontSize: 18,
- color: '#333',
- },
- qtyNum: {
- fontSize: 16,
- color: '#333',
- marginHorizontal: 15,
- },
- row: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingVertical: 15,
- borderBottomWidth: 1,
- borderBottomColor: '#eee',
- },
- rowLabel: {
- fontSize: 14,
- color: '#333',
- },
- rowValue: {
- fontSize: 14,
- color: '#ff6b00',
- },
- balance: {
- color: '#ff6b00',
- },
- payTypeSection: {
- paddingVertical: 10,
- borderBottomWidth: 1,
- borderBottomColor: '#eee',
- },
- payTypeItem: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingVertical: 10,
- },
- payTypeName: {
- fontSize: 14,
- color: '#333',
- },
- radio: {
- width: 20,
- height: 20,
- borderRadius: 10,
- borderWidth: 2,
- borderColor: '#ddd',
- },
- radioActive: {
- borderColor: '#ff6b00',
- backgroundColor: '#ff6b00',
- },
- phoneRow: {
- paddingVertical: 15,
- borderBottomWidth: 1,
- borderBottomColor: '#eee',
- },
- phoneLabel: {
- fontSize: 14,
- color: '#333',
- marginBottom: 10,
- },
- required: {
- color: '#dd524d',
- },
- phoneInput: {
- height: 40,
- backgroundColor: '#f5f5f5',
- borderRadius: 8,
- paddingHorizontal: 12,
- fontSize: 14,
- },
- addressRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingVertical: 15,
- borderBottomWidth: 1,
- borderBottomColor: '#eee',
- },
- addressInfo: {
- flex: 1,
- },
- addressName: {
- fontSize: 14,
- color: '#333',
- fontWeight: '500',
- },
- addressDetail: {
- fontSize: 12,
- color: '#666',
- marginTop: 4,
- },
- noAddress: {
- fontSize: 14,
- color: '#999',
- },
- arrow: {
- fontSize: 20,
- color: '#999',
- marginLeft: 10,
- },
- agreementRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingVertical: 15,
- },
- agreementText: {
- fontSize: 14,
- color: '#666',
- },
- payBtn: {
- marginHorizontal: 15,
- marginTop: 15,
- height: 50,
- backgroundColor: '#ff6b00',
- borderRadius: 25,
- justifyContent: 'center',
- alignItems: 'center',
- },
- payBtnDisabled: {
- opacity: 0.6,
- },
- payBtnText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: '600',
- },
- });
|