From 1800e792b149fb3a71c82598c1b1ac362e8d7dbb Mon Sep 17 00:00:00 2001
From: Jacy <18049821889@163.com>
Date: Wed, 1 Jul 2026 00:29:39 +0800
Subject: [PATCH] =?UTF-8?q?=E8=AE=A2=E5=8D=95=E8=AF=A6=E6=83=85=E9=A1=B5?=
=?UTF-8?q?=E5=86=85=E5=AE=B9=E8=B0=83=E6=95=B4=EF=BC=88=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?=E9=85=8D=E9=80=81=E6=96=B9=E5=BC=8F=E3=80=81=E8=AE=A2=E5=8D=95?=
=?UTF-8?q?=E5=8F=B7=E7=AD=89=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
apps/h5-user/src/lib/navigation.ts | 4 +
apps/h5-user/src/pages/AddressListPage.tsx | 78 ++-
apps/h5-user/src/pages/OrderDetailPage.tsx | 455 ++++++++++-----
apps/h5-user/src/styles.css | 524 ++++++++++++++++++
.../src/modules/trade/trade.controller.ts | 5 +
.../src/modules/trade/trade.service.ts | 19 +-
6 files changed, 925 insertions(+), 160 deletions(-)
diff --git a/apps/h5-user/src/lib/navigation.ts b/apps/h5-user/src/lib/navigation.ts
index af7f27f..d98af2c 100644
--- a/apps/h5-user/src/lib/navigation.ts
+++ b/apps/h5-user/src/lib/navigation.ts
@@ -53,6 +53,10 @@ export function buildProductDetailUrl(productId?: string | null) {
return productId ? `/product/${productId}` : '/';
}
+export function buildOrderAddressSelectUrl(orderId: string) {
+ return `/addresses?orderId=${orderId}&select=1`;
+}
+
export function hasCheckoutContext(ctx: CheckoutContext) {
return Boolean(ctx.productId || ctx.select === true || ctx.select === '1');
}
diff --git a/apps/h5-user/src/pages/AddressListPage.tsx b/apps/h5-user/src/pages/AddressListPage.tsx
index 2104e9a..2fb390d 100644
--- a/apps/h5-user/src/pages/AddressListPage.tsx
+++ b/apps/h5-user/src/pages/AddressListPage.tsx
@@ -24,10 +24,12 @@ function formatAddress(a: Address) {
}
export default function AddressListPage() {
- const [list, setList] = useState
([]);
+ const [pendingAddress, setPendingAddress] = useState(null);
+ const [savingOrderAddress, setSavingOrderAddress] = useState(false);
const [params] = useSearchParams();
const navigate = useNavigate();
const selectMode = params.get('select') === '1';
+ const orderId = params.get('orderId') || '';
const productId = params.get('productId') || '';
const qty = params.get('qty') || '';
const cross = params.get('cross') === '1';
@@ -41,16 +43,44 @@ export default function AddressListPage() {
loadList();
}, [loadList]);
- function selectAddress(id: string) {
+ function selectAddress(addr: Address) {
if (!selectMode) return;
+ if (orderId) {
+ setPendingAddress(addr);
+ return;
+ }
const qs = new URLSearchParams();
if (productId) qs.set('productId', productId);
if (qty) qs.set('qty', qty);
if (cross) qs.set('cross', '1');
- qs.set('addressId', id);
+ qs.set('addressId', addr.id);
navigate(`/order/confirm?${qs.toString()}`);
}
+ async function confirmOrderAddress() {
+ if (!orderId || !pendingAddress) return;
+ setSavingOrderAddress(true);
+ try {
+ await request('USER_H5', `/trade/orders/${orderId}/address`, {
+ method: 'PUT',
+ body: JSON.stringify({
+ receiverName: pendingAddress.receiverName,
+ receiverPhone: pendingAddress.phone,
+ receiverProvince: pendingAddress.province,
+ receiverCity: pendingAddress.city,
+ receiverDistrict: pendingAddress.district,
+ receiverAddress: `${pendingAddress.province}${pendingAddress.city}${pendingAddress.district}${pendingAddress.detail}`,
+ }),
+ });
+ navigate(`/orders/${orderId}`);
+ } catch (e) {
+ window.alert(e instanceof Error ? e.message : '修改地址失败');
+ } finally {
+ setSavingOrderAddress(false);
+ setPendingAddress(null);
+ }
+ }
+
async function setDefault(addr: Address, e: React.MouseEvent) {
e.stopPropagation();
if (addr.isDefault === 1) return;
@@ -84,6 +114,10 @@ export default function AddressListPage() {
}
function goBack() {
+ if (orderId) {
+ navigate(`/orders/${orderId}`);
+ return;
+ }
if (hasCheckoutContext(checkoutCtx)) {
navigate(buildOrderConfirmUrl(checkoutCtx));
return;
@@ -93,7 +127,7 @@ export default function AddressListPage() {
return (
-
+
{list.length === 0 && (
@@ -108,11 +142,11 @@ export default function AddressListPage() {
selectAddress(String(a.id))}
+ onClick={() => selectAddress(a)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
- selectAddress(String(a.id));
+ selectAddress(a);
}
}}
role={selectMode ? 'button' : undefined}
@@ -187,6 +221,38 @@ export default function AddressListPage() {
新增收货地址
+
+ {pendingAddress && (
+ setPendingAddress(null)}>
+
e.stopPropagation()}>
+
+ location_on
+
修改收货地址
+
+
+
确认将订单收货地址修改为:
+
+ {pendingAddress.receiverName} {maskPhone(pendingAddress.phone)}
+
+ {formatAddress(pendingAddress)}
+
+
+
+
+
+
+
+
+ )}
);
}
diff --git a/apps/h5-user/src/pages/OrderDetailPage.tsx b/apps/h5-user/src/pages/OrderDetailPage.tsx
index c2f3928..6ebc7ce 100644
--- a/apps/h5-user/src/pages/OrderDetailPage.tsx
+++ b/apps/h5-user/src/pages/OrderDetailPage.tsx
@@ -1,8 +1,8 @@
import { useEffect, useMemo, useState } from 'react';
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
-import SubPageHeader from '../components/SubPageHeader';
import AppImage from '@dukang/shared-ui/AppImage';
import { request } from '../lib/api';
+import { buildOrderAddressSelectUrl } from '../lib/navigation';
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
type OrderItem = {
@@ -13,10 +13,12 @@ type OrderItem = {
quantity: number;
};
-type StatusLog = {
- toStatus: string;
- createdAt: string;
- remark?: string;
+type OrderDelivery = {
+ provider?: string;
+};
+
+type OrderPayment = {
+ status?: string;
};
type Order = {
@@ -28,34 +30,48 @@ type Order = {
remark?: string | null;
receiverName: string;
receiverPhone: string;
+ receiverProvince?: string;
+ receiverCity?: string;
+ receiverDistrict?: string;
receiverAddress: string;
+ productAmount?: number;
+ freightAmount?: number;
payAmount: number;
benefitAmount: number;
createdAt: string;
items?: OrderItem[];
- statusLogs?: StatusLog[];
+ delivery?: OrderDelivery | null;
+ payment?: OrderPayment | null;
};
-const RESHIP_TIMELINE = [
- { key: 'placed', title: '补发单已下达', desc: '' },
- { key: 'pickup', title: '包裹揽收中', desc: '包裹正由物流网点揽收处理' },
- { key: 'transit', title: '运输中', desc: '暂无物流信息' },
+const PROGRESS_STEPS = [
+ { label: '下单成功' },
+ { label: '出库中' },
+ { label: '配送中' },
+ { label: '待签收' },
+ { label: '完成' },
] as const;
const STATUS_BANNER: Record = {
PENDING_PAY: { title: '待付款', subtitle: '请尽快完成支付' },
- PENDING_SHIP: { title: '待发货', subtitle: '商家正在备货' },
- OUT_WAREHOUSE: { title: '出库中', subtitle: '商品正在出库' },
- SHIPPING: { title: '配送中', subtitle: '包裹正在配送途中' },
- PENDING_RECEIVE: { title: '待收货', subtitle: '请注意查收' },
- COMPLETED: { title: '已完成', subtitle: '感谢您的购买' },
+ PENDING_SHIP: { title: '待发货', subtitle: '商家正在备货,请耐心等待' },
+ OUT_WAREHOUSE: { title: '出库中', subtitle: '商品正在出库打包' },
+ SHIPPING: { title: '配送中', subtitle: '您的佳酿正在赶往您的餐桌,请保持电话畅通' },
+ PENDING_RECEIVE: { title: '待签收', subtitle: '包裹已送达,请确认签收' },
+ COMPLETED: { title: '已完成', subtitle: '感谢您的购买,期待再次光临' },
RESHIP: { title: '补发中', subtitle: '包裹正在揽收,请耐心等待' },
};
+const EDITABLE_STATUSES = new Set(['PENDING_PAY', 'PENDING_SHIP', 'OUT_WAREHOUSE']);
+
function maskPhone(phone: string) {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
+function formatMoney(amount: number) {
+ return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
+}
+
function formatDateTime(value: string) {
const d = new Date(value);
if (Number.isNaN(d.getTime())) return value;
@@ -63,13 +79,38 @@ function formatDateTime(value: string) {
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
-function reshipTimelineStep(status: string, index: number): 'done' | 'active' | 'pending' {
- if (status === 'COMPLETED') return 'done';
- let activeIndex = 1;
- if (['SHIPPING', 'PENDING_RECEIVE'].includes(status)) activeIndex = 2;
- if (index < activeIndex) return 'done';
- if (index === activeIndex) return 'active';
- return 'pending';
+function progressActiveIndex(status: string) {
+ switch (status) {
+ case 'PENDING_PAY':
+ return 0;
+ case 'PENDING_SHIP':
+ return 1;
+ case 'OUT_WAREHOUSE':
+ return 1;
+ case 'SHIPPING':
+ return 2;
+ case 'PENDING_RECEIVE':
+ return 3;
+ case 'COMPLETED':
+ return 4;
+ default:
+ return 0;
+ }
+}
+
+function deliveryProviderLabel(provider?: string) {
+ if (!provider || provider === 'MOCK' || provider === 'XIAOFEIXIA') return '小飞侠配送';
+ return provider;
+}
+
+function fullReceiverAddress(order: Order) {
+ const region = [order.receiverProvince, order.receiverCity, order.receiverDistrict]
+ .filter(Boolean)
+ .join('');
+ if (region && order.receiverAddress.startsWith(region)) {
+ return order.receiverAddress;
+ }
+ return `${region}${order.receiverAddress}`;
}
export default function OrderDetailPage() {
@@ -78,168 +119,278 @@ export default function OrderDetailPage() {
const navigate = useNavigate();
const [order, setOrder] = useState(null);
const [showCs, setShowCs] = useState(false);
-
- useEffect(() => {
- if (id) request('USER_H5', `/trade/orders/${id}`).then(setOrder);
- }, [id]);
+ const [copyHint, setCopyHint] = useState('');
+ const [confirming, setConfirming] = useState(false);
const isReship = order?.orderType === 'RESHIPMENT' || params.get('type') === 'reship';
+ async function loadOrder() {
+ if (!id) return;
+ const data = await request('USER_H5', `/trade/orders/${id}`);
+ setOrder(data);
+ }
+
+ useEffect(() => {
+ loadOrder().catch(() => {});
+ }, [id]);
+
const banner = useMemo(() => {
if (!order) return { title: '', subtitle: '' };
if (isReship) return STATUS_BANNER.RESHIP;
return STATUS_BANNER[order.status] ?? { title: order.status, subtitle: '' };
}, [order, isReship]);
+ const activeProgress = order ? progressActiveIndex(order.status) : 0;
const item = order?.items?.[0];
const productImage = item?.productImage || STITCH_ORDER_PRODUCT_IMAGE;
- const placedAt = order?.statusLogs?.find((l) => l.toStatus === 'PENDING_SHIP')?.createdAt
- ?? order?.createdAt
- ?? '';
+ const canEditAddress = order ? EDITABLE_STATUSES.has(order.status) : false;
+ const canConfirmReceive = order?.status === 'PENDING_RECEIVE' && !isReship;
+ const productTotal = Number(order?.productAmount ?? order?.payAmount ?? 0);
+ const freightTotal = Number(order?.freightAmount ?? 0);
+
+ async function copyOrderNo() {
+ if (!order) return;
+ try {
+ await navigator.clipboard.writeText(order.orderNo);
+ setCopyHint('已复制');
+ window.setTimeout(() => setCopyHint(''), 2000);
+ } catch {
+ setCopyHint('复制失败');
+ }
+ }
+
+ async function confirmReceive() {
+ if (!id || !canConfirmReceive) return;
+ setConfirming(true);
+ try {
+ const updated = await request('USER_H5', `/trade/orders/${id}/confirm-receive`, {
+ method: 'POST',
+ });
+ setOrder(updated);
+ } catch (e) {
+ window.alert(e instanceof Error ? e.message : '确认收货失败');
+ } finally {
+ setConfirming(false);
+ }
+ }
if (!order) return 加载中...
;
return (
-
-
navigate(-1)} />
+
+
-
-
-
-
{banner.title}
+
+
+
+ local_shipping
+
+
+
+ local_shipping
+
{banner.title}
+
{banner.subtitle}
- local_shipping
-
- {isReship && (
-
- 物流动态
-
- {RESHIP_TIMELINE.map((step, index) => {
- const state = reshipTimelineStep(order.status, index);
- return (
-
-
- {state === 'done' && (
- check
- )}
- {state === 'active' && }
- {state === 'pending' && }
-
-
-
{step.title}
- {index === 0 && placedAt && (
-
{formatDateTime(placedAt)}
- )}
- {step.desc && state !== 'pending' && (
-
{step.desc}
- )}
- {step.desc && state === 'pending' && (
-
{step.desc}
- )}
-
+ {!isReship && (
+
+
+ {PROGRESS_STEPS.map((step, index) => {
+ const done = index < activeProgress;
+ const active = index === activeProgress;
+ const pending = index > activeProgress;
+ return (
+
+ {index < PROGRESS_STEPS.length - 1 && (
+
+ )}
+
+ {done && check}
+ {active && }
+ {pending && }
- );
- })}
-
-
- )}
-
- {isReship && (
-
-
- 补发原因
- {order.remark || '商品破损'}
-
-
- 关联原订单
- {order.originOrderId ? (
-
- 查看原订单
-
- ) : (
- DK20231005002
- )}
-
-
- )}
-
- {item && (
-
-
-
-
-
-
{item.productName}
-
{item.productSpec}
+
{step.label}
-
-
- ¥{isReship ? '0.00' : Number(item.unitPrice).toFixed(2)}
-
- x{item.quantity}
-
-
-
- {Number(order.benefitAmount) > 0 && (
-
-
-
- ¥{order.benefitAmount}好客权益
-
- 随单赠送
-
-
info
-
- )}
-
- )}
-
-
-
- location_on
-
收货信息
-
-
-
- {order.receiverName}
- {maskPhone(order.receiverPhone)}
-
-
{order.receiverAddress}
+ );
+ })}
+ )}
- {!isReship && (
-
-
-
订单号
-
{order.orderNo}
+ {isReship && (
+
+
+ 补发原因
+ {order.remark || '商品破损'}
+
+
+ 关联原订单
+ {order.originOrderId ? (
+
+ 查看原订单
+
+ ) : (
+ —
+ )}
+
+
+ )}
+
+ {item && (
+
+
+
-
+
+
+
{item.productName}
+
规格:{item.productSpec}
+
+
+
+ ¥
+ {formatMoney(isReship ? 0 : Number(item.unitPrice)).replace('.00', '')}
+
+ x{item.quantity}
+
+
+
+
+ {Number(order.benefitAmount) > 0 && !isReship && (
+
+
+
+ confirmation_number
+
+
+
+ ¥{Math.round(Number(order.benefitAmount))}
+ 好客权益
+
+
全城100+餐厅通用
+
+
+
+
+ )}
+
+ )}
+
+
+
+
+ location_on
+ 收货信息
+
+ {canEditAddress && id && (
+
+ )}
+
+
+
+ 收货人
+
+ {order.receiverName} {maskPhone(order.receiverPhone)}
+
+
+
+ 收货地址
+ {fullReceiverAddress(order)}
+
+
+ 配送方式
+ {deliveryProviderLabel(order.delivery?.provider)}
+
+
+
+
+ {!isReship && (
+
+
+ info
+ 订单信息
+
+
+
+ 订单编号
+
+ {order.orderNo}
+
+
+
+
+ 创建时间
+ {formatDateTime(order.createdAt)}
+
+
+ 支付方式
+ {order.payment?.status === 'SUCCESS' ? '微信支付' : '—'}
+
+
+
+
+ 商品总额
+ ¥{formatMoney(productTotal)}
+
+
+ 运费
+ ¥{formatMoney(freightTotal)}
+
+
实付金额
- ¥{order.payAmount}
+ ¥{formatMoney(Number(order.payAmount))}
-
- )}
-
+
+
+ )}
-