import { useEffect, useMemo, useRef, useState } from 'react';
import { View, Text, Image } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import { goLogin } from '../../lib/auth-nav';
import { buildAddressListUrl, buildPayUrl, readCheckoutContext } from '../../lib/checkout-nav';
import { tryGetClientGpsLocation } from '../../lib/client-location';
import { maskPhone } from '../../lib/phone';
import { ensurePayReady } from '../../lib/pay-ready';
import { fetchUserProfile } from '../../lib/pay-wechat';
import { request } from '../../lib/api';
import { getProductMainImage } from '../../lib/product-images';
type Address = {
id: string;
receiverName: string;
phone: string;
province: string;
city: string;
district: string;
detail: string;
isDefault?: number | boolean;
};
type PreviewProduct = {
id: string;
name: string;
spec?: string;
subtitle?: string;
price: number;
mainImageUrl?: string | null;
carouselUrls?: string[] | null;
};
type OrderPreview = {
product: PreviewProduct;
quantity: number;
deliveryType: 'LOCAL' | 'CROSS_CITY';
productAmount: number;
freightPayType: 'COD' | null;
payAmount: number;
benefitAmount: number;
city?: { localMinQty: number; crossMinQty: number };
quantityOk?: boolean;
quantityMessage?: string | null;
minQty?: number;
};
function formatAddress(a: Address) {
return `${a.province}${a.city}${a.district}${a.detail}`;
}
export default function OrderConfirmPage() {
const router = useRouter();
const checkoutCtx = readCheckoutContext(router.params);
const productId = checkoutCtx.productId ?? '';
const forceCross = checkoutCtx.cross === true;
const [quantity, setQuantity] = useState(Math.max(1, Number(checkoutCtx.qty || 2)));
const [addresses, setAddresses] = useState
([]);
const [addressId, setAddressId] = useState(checkoutCtx.addressId || '');
const [preview, setPreview] = useState(null);
const [previewLoading, setPreviewLoading] = useState(false);
const [loading, setLoading] = useState(false);
const [msg, setMsg] = useState('');
const phonePromptSkipped = useRef(false);
useEffect(() => {
request('/user/addresses')
.then((list) => {
setAddresses(list);
const fromUrl = checkoutCtx.addressId;
if (fromUrl && list.some((a) => String(a.id) === fromUrl)) {
setAddressId(fromUrl);
return;
}
const def = list.find((a) => a.isDefault === 1 || a.isDefault === true) || list[0];
if (def) setAddressId(String(def.id));
})
.catch(() => setAddresses([]));
}, [checkoutCtx.addressId]);
useEffect(() => {
if (!productId) return;
let cancelled = false;
setPreviewLoading(true);
const body: { productId: string; quantity: number; addressId?: string } = {
productId,
quantity,
};
if (addressId) body.addressId = addressId;
request('/trade/orders/preview', { method: 'POST', data: body })
.then((data) => {
if (!cancelled) {
setPreview(data);
setMsg(data.quantityOk === false ? (data.quantityMessage || '') : '');
}
})
.catch((e) => {
if (!cancelled) {
setPreview(null);
setMsg(e instanceof Error ? e.message : '加载失败');
}
})
.finally(() => {
if (!cancelled) setPreviewLoading(false);
});
return () => {
cancelled = true;
};
}, [productId, quantity, addressId]);
const selectedAddress = useMemo(
() => addresses.find((a) => String(a.id) === addressId),
[addresses, addressId],
);
const isCross = forceCross || preview?.deliveryType === 'CROSS_CITY';
const minQty =
preview?.minQty ??
(isCross ? (preview?.city?.crossMinQty ?? 6) : (preview?.city?.localMinQty ?? 2));
const quantityOk = preview ? preview.quantityOk !== false && quantity >= minQty : false;
const canSubmit = !!addressId && !!preview && quantityOk && !loading && !previewLoading;
function updateQuantity(next: number) {
if (next < 1) return;
setQuantity(next);
}
async function doSubmit() {
let clientLocation = null;
try {
clientLocation = await tryGetClientGpsLocation();
} catch {
/* GPS 获取失败不阻塞下单 */
}
const order = await request<{ id: string }>('/trade/orders', {
method: 'POST',
data: {
productId,
quantity,
addressId,
...(clientLocation ? { clientLocation } : {}),
},
});
Taro.redirectTo({
url: buildPayUrl({
orderId: order.id,
productId,
qty: String(quantity),
addressId,
cross: forceCross,
}),
});
}
async function submit() {
if (!canSubmit) {
if (!addressId) {
setMsg('请选择收货地址');
return;
}
if (!quantityOk) {
setMsg(
isCross
? `跨城配送至少购买 ${minQty} 瓶(1箱)`
: `同城配送至少购买 ${minQty} 瓶`,
);
}
return;
}
const returnPath = `/pages/order-confirm/index?productId=${productId}&qty=${quantity}&addressId=${addressId}${forceCross ? '&cross=1' : ''}`;
if (!phonePromptSkipped.current) {
try {
const profile = await fetchUserProfile();
const phoneBound =
!!profile.phoneVerified ||
(!!profile.phone && /^1[3-9]\d{9}$/.test(String(profile.phone)));
if (!phoneBound) {
const { confirm, cancel } = await Taro.showModal({
title: '建议绑定手机号',
content: '绑定后便于订单通知与售后联系;也可跳过,不绑定也能继续下单。',
confirmText: '去绑定',
cancelText: '暂不绑定',
});
if (confirm) {
goLogin(returnPath, { needPhone: '1' });
return;
}
if (cancel) {
phonePromptSkipped.current = true;
}
}
} catch {
/* 拉取档案失败不阻塞下单 */
}
}
const ready = await ensurePayReady(returnPath);
if (!ready) return;
setLoading(true);
setMsg('');
try {
await doSubmit();
} catch (e) {
setMsg(e instanceof Error ? e.message : '下单失败');
} finally {
setLoading(false);
}
}
const productImage = preview?.product ? getProductMainImage(preview.product) : '';
const submitLabel = loading
? '提交中…'
: !addressId
? '请选择地址'
: !quantityOk
? `至少购买 ${minQty} 瓶`
: '提交订单';
return (
Taro.navigateTo({
url: buildAddressListUrl({
productId,
qty: String(quantity),
addressId,
cross: forceCross,
}),
})
}
>
收货地址
{selectedAddress ? (
{selectedAddress.receiverName}
{maskPhone(selectedAddress.phone)}
{formatAddress(selectedAddress)}
) : (
点击选择收货地址
)}
{isCross ? (
该地址超出同城配送范围,将由总部物流发货,运费到付
{quantity < minQty ? `;跨城至少购买 ${minQty} 瓶(1箱)` : ''}。
) : null}
{preview ? (
<>
商品信息
{productImage ? (
) : null}
{preview.product.name}
¥{Number(preview.product.price).toFixed(2)}
购买数量
updateQuantity(quantity - 1)}
>
−
{quantity}
updateQuantity(quantity + 1)}
>
+
{!quantityOk ? (
{isCross
? `跨城配送至少购买 ${minQty} 瓶(1箱),请调整数量`
: `同城配送至少购买 ${minQty} 瓶,请调整数量`}
) : null}
费用明细
商品金额
¥{preview.productAmount.toFixed(2)}
好客权益
¥{preview.benefitAmount.toFixed(2)}
运费
{isCross ? '到付' : '免运费'}
>
) : null}
{previewLoading && !preview && productId ? (
加载订单信息…
) : null}
{!previewLoading && !preview && productId ? (
无法加载商品信息
) : null}
{msg ? (
{msg}
) : null}
应付合计
¥{preview ? preview.payAmount.toFixed(2) : '—'}
{
if (!canSubmit) return;
void submit();
}}
>
{submitLabel}
);
}