From 00b52b9aaa7cb083f7d6aea5ecda46cd8ef69069 Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Fri, 17 Jul 2026 09:06:53 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AB=AF=E8=B7=A8=E5=9F=8E?= =?UTF-8?q?=E8=AE=A2=E5=8D=95=E7=95=8C=E9=9D=A2=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/pages/order-confirm/index.tsx | 65 +++++++++++++------ apps/mini-user/src/styles/order.css | 20 ++++++ .../src/modules/trade/trade.service.ts | 8 ++- 3 files changed, 73 insertions(+), 20 deletions(-) diff --git a/apps/mini-user/src/pages/order-confirm/index.tsx b/apps/mini-user/src/pages/order-confirm/index.tsx index 5213ddb..eb63ffc 100644 --- a/apps/mini-user/src/pages/order-confirm/index.tsx +++ b/apps/mini-user/src/pages/order-confirm/index.tsx @@ -42,6 +42,9 @@ type OrderPreview = { payAmount: number; benefitAmount: number; city?: { localMinQty: number; crossMinQty: number }; + quantityOk?: boolean; + quantityMessage?: string | null; + minQty?: number; }; function formatAddress(a: Address) { @@ -53,7 +56,7 @@ export default function OrderConfirmPage() { const checkoutCtx = readCheckoutContext(router.params); const productId = checkoutCtx.productId ?? ''; 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([]); const [addressId, setAddressId] = useState(checkoutCtx.addressId || ''); const [preview, setPreview] = useState(null); @@ -91,7 +94,7 @@ export default function OrderConfirmPage() { .then((data) => { if (!cancelled) { setPreview(data); - setMsg(''); + setMsg(data.quantityOk === false ? (data.quantityMessage || '') : ''); } }) .catch((e) => { @@ -115,18 +118,14 @@ export default function OrderConfirmPage() { ); 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) { - if (next < minQty) { - setMsg( - !isCross - ? `同城配送至少购买 ${minQty} 瓶` - : `跨城配送至少购买 ${minQty} 瓶(1箱)`, - ); - return; - } - setMsg(''); + if (next < 1) return; setQuantity(next); } @@ -158,8 +157,18 @@ export default function OrderConfirmPage() { } async function submit() { - if (!addressId) { - setMsg('请选择收货地址'); + if (!canSubmit) { + if (!addressId) { + setMsg('请选择收货地址'); + return; + } + if (!quantityOk) { + setMsg( + isCross + ? `跨城配送至少购买 ${minQty} 瓶(1箱)` + : `同城配送至少购买 ${minQty} 瓶`, + ); + } return; } @@ -206,6 +215,13 @@ export default function OrderConfirmPage() { } const productImage = preview?.product ? getProductMainImage(preview.product) : ''; + const submitLabel = loading + ? '提交中…' + : !addressId + ? '请选择地址' + : !quantityOk + ? `至少购买 ${minQty} 瓶` + : '提交订单'; return ( @@ -241,7 +257,8 @@ export default function OrderConfirmPage() { {isCross ? ( - 该地址超出同城配送范围,将由总部物流发货,运费到付。 + 该地址超出同城配送范围,将由总部物流发货,运费到付 + {quantity < minQty ? `;跨城至少购买 ${minQty} 瓶(1箱)` : ''}。 ) : null} @@ -269,7 +286,7 @@ export default function OrderConfirmPage() { 购买数量 updateQuantity(quantity - 1)} > @@ -283,6 +300,13 @@ export default function OrderConfirmPage() { + {!quantityOk ? ( + + {isCross + ? `跨城配送至少购买 ${minQty} 瓶(1箱),请调整数量` + : `同城配送至少购买 ${minQty} 瓶,请调整数量`} + + ) : null} @@ -320,10 +344,13 @@ export default function OrderConfirmPage() { !loading && void submit()} + className={`order-confirm-submit${canSubmit ? '' : ' order-confirm-submit--disabled'}`} + onClick={() => { + if (!canSubmit) return; + void submit(); + }} > - {loading ? '提交中…' : !addressId ? '请选择地址' : '提交订单'} + {submitLabel} diff --git a/apps/mini-user/src/styles/order.css b/apps/mini-user/src/styles/order.css index 5e0eed8..4f64095 100644 --- a/apps/mini-user/src/styles/order.css +++ b/apps/mini-user/src/styles/order.css @@ -93,6 +93,11 @@ color: var(--color-on-surface); } +.order-qty-btn--disabled { + opacity: 0.4; + pointer-events: none; +} + .order-qty-value { margin: 0 14px; font-size: 16px; @@ -101,6 +106,14 @@ 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 { display: flex; justify-content: space-between; @@ -180,6 +193,13 @@ justify-content: center; } +.order-confirm-submit--disabled { + background: #c9c5c0; + color: #fff; + opacity: 0.85; + pointer-events: none; +} + .order-tabs { display: flex; padding: 0 var(--space-page); diff --git a/server/dukang-api/src/modules/trade/trade.service.ts b/server/dukang-api/src/modules/trade/trade.service.ts index ffcce37..59dbb1d 100644 --- a/server/dukang-api/src/modules/trade/trade.service.ts +++ b/server/dukang-api/src/modules/trade/trade.service.ts @@ -75,7 +75,6 @@ export class TradeService { city.localMinQty, city.crossMinQty, ); - if (!check.ok) throw new BadRequestException(check.message); const unitPrice = Number(product.price); const productAmount = unitPrice * body.quantity; @@ -96,6 +95,10 @@ export class TradeService { payAmount: productAmount, benefitAmount: benefitPerUnit * body.quantity, 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, ) { const preview = await this.preview(userId, body); + if (preview.quantityOk === false) { + throw new BadRequestException(preview.quantityMessage || '购买数量不满足起购要求'); + } const address = await this.prisma.userAddress.findFirst({ where: { id: BigInt(body.addressId), userId }, });