@@ -42,6 +42,9 @@ type OrderPreview = {
|
|||||||
payAmount: number;
|
payAmount: number;
|
||||||
benefitAmount: number;
|
benefitAmount: number;
|
||||||
city?: { localMinQty: number; crossMinQty: number };
|
city?: { localMinQty: number; crossMinQty: number };
|
||||||
|
quantityOk?: boolean;
|
||||||
|
quantityMessage?: string | null;
|
||||||
|
minQty?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function formatAddress(a: Address) {
|
function formatAddress(a: Address) {
|
||||||
@@ -53,7 +56,7 @@ export default function OrderConfirmPage() {
|
|||||||
const checkoutCtx = readCheckoutContext(router.params);
|
const checkoutCtx = readCheckoutContext(router.params);
|
||||||
const productId = checkoutCtx.productId ?? '';
|
const productId = checkoutCtx.productId ?? '';
|
||||||
const forceCross = checkoutCtx.cross === true;
|
const forceCross = checkoutCtx.cross === true;
|
||||||
const [quantity, setQuantity] = useState(Math.max(2, Number(checkoutCtx.qty || 2)));
|
const [quantity, setQuantity] = useState(Math.max(1, Number(checkoutCtx.qty || 2)));
|
||||||
const [addresses, setAddresses] = useState<Address[]>([]);
|
const [addresses, setAddresses] = useState<Address[]>([]);
|
||||||
const [addressId, setAddressId] = useState(checkoutCtx.addressId || '');
|
const [addressId, setAddressId] = useState(checkoutCtx.addressId || '');
|
||||||
const [preview, setPreview] = useState<OrderPreview | null>(null);
|
const [preview, setPreview] = useState<OrderPreview | null>(null);
|
||||||
@@ -91,7 +94,7 @@ export default function OrderConfirmPage() {
|
|||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setPreview(data);
|
setPreview(data);
|
||||||
setMsg('');
|
setMsg(data.quantityOk === false ? (data.quantityMessage || '') : '');
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@@ -115,18 +118,14 @@ export default function OrderConfirmPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const isCross = forceCross || preview?.deliveryType === 'CROSS_CITY';
|
const isCross = forceCross || preview?.deliveryType === 'CROSS_CITY';
|
||||||
const minQty = isCross ? (preview?.city?.crossMinQty ?? 6) : (preview?.city?.localMinQty ?? 2);
|
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) {
|
function updateQuantity(next: number) {
|
||||||
if (next < minQty) {
|
if (next < 1) return;
|
||||||
setMsg(
|
|
||||||
!isCross
|
|
||||||
? `同城配送至少购买 ${minQty} 瓶`
|
|
||||||
: `跨城配送至少购买 ${minQty} 瓶(1箱)`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setMsg('');
|
|
||||||
setQuantity(next);
|
setQuantity(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,8 +157,18 @@ export default function OrderConfirmPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submit() {
|
async function submit() {
|
||||||
if (!addressId) {
|
if (!canSubmit) {
|
||||||
setMsg('请选择收货地址');
|
if (!addressId) {
|
||||||
|
setMsg('请选择收货地址');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!quantityOk) {
|
||||||
|
setMsg(
|
||||||
|
isCross
|
||||||
|
? `跨城配送至少购买 ${minQty} 瓶(1箱)`
|
||||||
|
: `同城配送至少购买 ${minQty} 瓶`,
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +215,13 @@ export default function OrderConfirmPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const productImage = preview?.product ? getProductMainImage(preview.product) : '';
|
const productImage = preview?.product ? getProductMainImage(preview.product) : '';
|
||||||
|
const submitLabel = loading
|
||||||
|
? '提交中…'
|
||||||
|
: !addressId
|
||||||
|
? '请选择地址'
|
||||||
|
: !quantityOk
|
||||||
|
? `至少购买 ${minQty} 瓶`
|
||||||
|
: '提交订单';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageShell variant="sub" className="order-confirm-page" hasFixedFooter>
|
<PageShell variant="sub" className="order-confirm-page" hasFixedFooter>
|
||||||
@@ -241,7 +257,8 @@ export default function OrderConfirmPage() {
|
|||||||
{isCross ? (
|
{isCross ? (
|
||||||
<View className="order-card">
|
<View className="order-card">
|
||||||
<Text className="u-muted">
|
<Text className="u-muted">
|
||||||
该地址超出同城配送范围,将由总部物流发货,运费到付。
|
该地址超出同城配送范围,将由总部物流发货,运费到付
|
||||||
|
{quantity < minQty ? `;跨城至少购买 ${minQty} 瓶(1箱)` : ''}。
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -269,7 +286,7 @@ export default function OrderConfirmPage() {
|
|||||||
<Text>购买数量</Text>
|
<Text>购买数量</Text>
|
||||||
<View className="order-qty-controls">
|
<View className="order-qty-controls">
|
||||||
<View
|
<View
|
||||||
className="order-qty-btn"
|
className={`order-qty-btn${quantity <= 1 ? ' order-qty-btn--disabled' : ''}`}
|
||||||
onClick={() => updateQuantity(quantity - 1)}
|
onClick={() => updateQuantity(quantity - 1)}
|
||||||
>
|
>
|
||||||
<Text>−</Text>
|
<Text>−</Text>
|
||||||
@@ -283,6 +300,13 @@ export default function OrderConfirmPage() {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
{!quantityOk ? (
|
||||||
|
<Text className="order-qty-hint">
|
||||||
|
{isCross
|
||||||
|
? `跨城配送至少购买 ${minQty} 瓶(1箱),请调整数量`
|
||||||
|
: `同城配送至少购买 ${minQty} 瓶,请调整数量`}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View className="order-card">
|
<View className="order-card">
|
||||||
@@ -320,10 +344,13 @@ export default function OrderConfirmPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
className="order-confirm-submit"
|
className={`order-confirm-submit${canSubmit ? '' : ' order-confirm-submit--disabled'}`}
|
||||||
onClick={() => !loading && void submit()}
|
onClick={() => {
|
||||||
|
if (!canSubmit) return;
|
||||||
|
void submit();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Text>{loading ? '提交中…' : !addressId ? '请选择地址' : '提交订单'}</Text>
|
<Text>{submitLabel}</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</PageShell>
|
</PageShell>
|
||||||
|
|||||||
@@ -93,6 +93,11 @@
|
|||||||
color: var(--color-on-surface);
|
color: var(--color-on-surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-qty-btn--disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.order-qty-value {
|
.order-qty-value {
|
||||||
margin: 0 14px;
|
margin: 0 14px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -101,6 +106,14 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-qty-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-heritage-red);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
.order-row {
|
.order-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -180,6 +193,13 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.order-confirm-submit--disabled {
|
||||||
|
background: #c9c5c0;
|
||||||
|
color: #fff;
|
||||||
|
opacity: 0.85;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.order-tabs {
|
.order-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 var(--space-page);
|
padding: 0 var(--space-page);
|
||||||
|
|||||||
@@ -75,7 +75,6 @@ export class TradeService {
|
|||||||
city.localMinQty,
|
city.localMinQty,
|
||||||
city.crossMinQty,
|
city.crossMinQty,
|
||||||
);
|
);
|
||||||
if (!check.ok) throw new BadRequestException(check.message);
|
|
||||||
|
|
||||||
const unitPrice = Number(product.price);
|
const unitPrice = Number(product.price);
|
||||||
const productAmount = unitPrice * body.quantity;
|
const productAmount = unitPrice * body.quantity;
|
||||||
@@ -96,6 +95,10 @@ export class TradeService {
|
|||||||
payAmount: productAmount,
|
payAmount: productAmount,
|
||||||
benefitAmount: benefitPerUnit * body.quantity,
|
benefitAmount: benefitPerUnit * body.quantity,
|
||||||
city: serializeBigInt(city),
|
city: serializeBigInt(city),
|
||||||
|
/** 起购未满足时仍返回预览,供确认页改数量;下单接口仍会硬校验 */
|
||||||
|
quantityOk: check.ok,
|
||||||
|
quantityMessage: check.ok ? null : (check.message ?? null),
|
||||||
|
minQty: deliveryType === 'LOCAL' ? city.localMinQty : city.crossMinQty,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,6 +113,9 @@ export class TradeService {
|
|||||||
req: Request,
|
req: Request,
|
||||||
) {
|
) {
|
||||||
const preview = await this.preview(userId, body);
|
const preview = await this.preview(userId, body);
|
||||||
|
if (preview.quantityOk === false) {
|
||||||
|
throw new BadRequestException(preview.quantityMessage || '购买数量不满足起购要求');
|
||||||
|
}
|
||||||
const address = await this.prisma.userAddress.findFirst({
|
const address = await this.prisma.userAddress.findFirst({
|
||||||
where: { id: BigInt(body.addressId), userId },
|
where: { id: BigInt(body.addressId), userId },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user