83b1b0e5f6
Add HQ group for default and per-page share title/desc/image; expose via client-config and wire all mini-user share entry points. Co-authored-by: Cursor <cursoragent@cursor.com>
411 lines
15 KiB
TypeScript
411 lines
15 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { View, Text } from '@tarojs/components';
|
|
import Taro, { useDidShow, useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
|
|
import PageShell from '../../components/PageShell';
|
|
import SubPageHeader from '../../components/SubPageHeader';
|
|
import ShareNavButton from '../../components/ShareNavButton';
|
|
import WechatShareReady from '../../components/WechatShareReady';
|
|
import ContactCsButton from '../../components/ContactCsButton';
|
|
import LogisticsRichText from '../../components/LogisticsRichText';
|
|
import { request, toast } from '../../lib/api';
|
|
import { buildPayUrl } from '../../lib/checkout-nav';
|
|
import {
|
|
formatEstimatedArrival,
|
|
isLogisticsNotArrived,
|
|
latestTrackNode,
|
|
ORDER_LOGISTICS_PREVIEW_STATUSES,
|
|
shouldLoadOrderTrack,
|
|
type OrderTrackEstimatedArrival,
|
|
type OrderTrackNode,
|
|
} from '../../lib/order-logistics-utils';
|
|
import { fetchOrderTrack } from '../../lib/order-logistics';
|
|
import { maskPhone } from '../../lib/phone';
|
|
import { confirmOrderReceive, type WechatConfirmPayload } from '../../lib/wechat-order-confirm';
|
|
import {
|
|
buildSceneSharePayload,
|
|
toWeappShareMessage,
|
|
toWeappShareTimeline,
|
|
} from '../../lib/wechat-share';
|
|
import { usePageView } from '../../lib/usePageView';
|
|
|
|
const isWeapp = process.env.TARO_ENV === 'weapp';
|
|
|
|
type OrderItem = {
|
|
productName?: string;
|
|
productSpec?: string;
|
|
quantity?: number;
|
|
};
|
|
|
|
type OrderDetail = {
|
|
id: string;
|
|
orderNo?: string;
|
|
status?: string;
|
|
payAmount?: number;
|
|
productName?: string;
|
|
quantity?: number;
|
|
qty?: number;
|
|
receiverName?: string;
|
|
receiverPhone?: string;
|
|
receiverProvince?: string;
|
|
receiverCity?: string;
|
|
receiverDistrict?: string;
|
|
receiverAddress?: string;
|
|
createdAt?: string;
|
|
originOrderId?: string | null;
|
|
orderType?: string;
|
|
isProxyOrder?: boolean;
|
|
proxyPartnerName?: string | null;
|
|
deliveryType?: string;
|
|
items?: OrderItem[];
|
|
wechatConfirm?: WechatConfirmPayload | null;
|
|
delivery?: {
|
|
provider?: string;
|
|
trackingNo?: string;
|
|
logisticsCompany?: string;
|
|
manualQueryUrl?: string;
|
|
} | null;
|
|
};
|
|
|
|
const STATUS_LABELS: Record<string, string> = {
|
|
PENDING_PAY: '待付款',
|
|
PENDING_SHIP: '待发货',
|
|
OUT_WAREHOUSE: '出库中',
|
|
SHIPPING: '配送中',
|
|
SHIPPED: '配送中',
|
|
PENDING_RECEIVE: '待签收',
|
|
DELIVERED: '待签收',
|
|
COMPLETED: '已完成',
|
|
CANCELLED: '已取消',
|
|
REFUNDING: '退款中',
|
|
REFUNDED: '已退款',
|
|
};
|
|
|
|
function fullReceiverAddress(order: OrderDetail) {
|
|
const detail = (order.receiverAddress || '').trim();
|
|
const region = [order.receiverProvince, order.receiverCity, order.receiverDistrict]
|
|
.filter(Boolean)
|
|
.join('');
|
|
if (!region && !detail) return '';
|
|
if (region && detail.startsWith(region)) return detail;
|
|
return `${region}${detail}`;
|
|
}
|
|
|
|
export default function OrderDetailPage() {
|
|
const router = useRouter();
|
|
const orderId = router.params.id ?? '';
|
|
usePageView('order_detail_view', orderId ? { orderId } : undefined);
|
|
const [order, setOrder] = useState<OrderDetail | null>(null);
|
|
const [confirming, setConfirming] = useState(false);
|
|
const [latestTrack, setLatestTrack] = useState<OrderTrackNode | null>(null);
|
|
const [estimatedArrival, setEstimatedArrival] = useState<OrderTrackEstimatedArrival | null>(null);
|
|
const [trackLoading, setTrackLoading] = useState(false);
|
|
|
|
async function loadOrderTrack(delivery: OrderDetail['delivery'], orderStatus?: string) {
|
|
if (!orderId || !shouldLoadOrderTrack(delivery)) {
|
|
setLatestTrack(null);
|
|
setEstimatedArrival(null);
|
|
return;
|
|
}
|
|
setTrackLoading(true);
|
|
try {
|
|
const track = await fetchOrderTrack(orderId);
|
|
const nodes = track.nodes ?? [];
|
|
setLatestTrack(latestTrackNode(nodes));
|
|
setEstimatedArrival(
|
|
isLogisticsNotArrived(nodes, orderStatus) && track.estimatedArrival
|
|
? track.estimatedArrival
|
|
: null,
|
|
);
|
|
} catch {
|
|
setLatestTrack(null);
|
|
setEstimatedArrival(null);
|
|
} finally {
|
|
setTrackLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!orderId) return;
|
|
request<OrderDetail>(`/trade/orders/${orderId}`)
|
|
.then((data) => {
|
|
setOrder(data);
|
|
void loadOrderTrack(data.delivery, data.status);
|
|
})
|
|
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
|
}, [orderId]);
|
|
|
|
useDidShow(() => {
|
|
if (!orderId) return;
|
|
// 从微信确认收货组件返回后刷新
|
|
request<OrderDetail>(`/trade/orders/${orderId}`)
|
|
.then((data) => {
|
|
setOrder(data);
|
|
void loadOrderTrack(data.delivery, data.status);
|
|
})
|
|
.catch(() => {});
|
|
});
|
|
|
|
const isReship = !!order?.originOrderId;
|
|
const isProxy = !!order && (order.isProxyOrder || order.orderType === 'PROXY');
|
|
const canPay = !!order && order.status === 'PENDING_PAY' && !isReship;
|
|
const canConfirmReceive =
|
|
!!order && !isReship && !isProxy && ['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '');
|
|
const canViewLogistics =
|
|
!!order &&
|
|
order.deliveryType !== 'ON_SITE_PICKUP' &&
|
|
!isReship &&
|
|
['PENDING_SHIP', 'OUT_WAREHOUSE', 'SHIPPING', 'SHIPPED', 'PENDING_RECEIVE', 'DELIVERED', 'COMPLETED'].includes(
|
|
order.status || '',
|
|
);
|
|
const showLogisticsPreview =
|
|
canViewLogistics &&
|
|
ORDER_LOGISTICS_PREVIEW_STATUSES.includes(
|
|
(order?.status || '') as (typeof ORDER_LOGISTICS_PREVIEW_STATUSES)[number],
|
|
);
|
|
|
|
const item = order?.items?.[0];
|
|
const productName = item?.productName || order?.productName || '杜康商品';
|
|
const quantity = item?.quantity ?? order?.quantity ?? order?.qty ?? 1;
|
|
const addressText = order ? fullReceiverAddress(order) : '';
|
|
const receiverLine = order
|
|
? [order.receiverName, order.receiverPhone ? maskPhone(String(order.receiverPhone)) : '']
|
|
.filter(Boolean)
|
|
.join(' ')
|
|
: '';
|
|
|
|
const sharePayload = useMemo(
|
|
() =>
|
|
buildSceneSharePayload('orderDetail', {
|
|
path: orderId ? `/pages/order-detail/index?id=${orderId}` : '/pages/home/index',
|
|
titleVars: {
|
|
productName: productName && productName !== '杜康商品' ? productName : '',
|
|
},
|
|
}),
|
|
[productName, orderId],
|
|
);
|
|
|
|
useShareAppMessage(() => toWeappShareMessage(sharePayload));
|
|
useShareTimeline(() =>
|
|
toWeappShareTimeline(sharePayload, orderId ? `id=${orderId}` : ''),
|
|
);
|
|
|
|
function goPay() {
|
|
if (!order) return;
|
|
Taro.navigateTo({ url: buildPayUrl({ orderId: order.id }) });
|
|
}
|
|
|
|
function goCustomerService() {
|
|
Taro.navigateTo({ url: '/pages/customer-service/index' });
|
|
}
|
|
|
|
function goLogistics() {
|
|
if (!order) return;
|
|
Taro.navigateTo({ url: `/pages/order-logistics/index?id=${order.id}` });
|
|
}
|
|
|
|
async function confirmReceive() {
|
|
if (!order || !canConfirmReceive || confirming) return;
|
|
|
|
const { confirm } = await Taro.showModal({
|
|
title: '确认收货?',
|
|
content: isWeapp
|
|
? '将打开微信确认收货,完成后订单即完结,无需再点服务通知。'
|
|
: '请确认已收到商品。确认后订单将完成,好客权益可正常使用。',
|
|
confirmText: '确认收货',
|
|
cancelText: '再想想',
|
|
});
|
|
if (!confirm) return;
|
|
|
|
setConfirming(true);
|
|
try {
|
|
const mode = await confirmOrderReceive({
|
|
orderId: order.id,
|
|
wechatConfirm: order.wechatConfirm,
|
|
onLocalSuccess: async () => {
|
|
const updated = await request<OrderDetail>(`/trade/orders/${order.id}`);
|
|
setOrder(updated);
|
|
toast('已确认收货');
|
|
},
|
|
});
|
|
if (mode === 'wechat') {
|
|
// 回跳后由 App.onShow / 本页 useDidShow 处理
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
toast(e instanceof Error ? e.message : '确认收货失败');
|
|
} finally {
|
|
setConfirming(false);
|
|
}
|
|
}
|
|
|
|
const pageClass = [
|
|
'order-detail-page',
|
|
order ? 'order-detail-page--with-actions' : '',
|
|
canPay || canConfirmReceive ? 'order-detail-page--with-pay' : '',
|
|
]
|
|
.filter(Boolean)
|
|
.join(' ');
|
|
|
|
return (
|
|
<PageShell variant="sub" className={pageClass}>
|
|
<WechatShareReady payload={sharePayload} />
|
|
<SubPageHeader
|
|
title="订单详情"
|
|
onBack={() => {
|
|
// 支付完成后 reLaunch 进详情:栈仅一页时 navigateBack 会退出小程序,统一回首页
|
|
const fromPay = String(router.params.from || '') === 'pay';
|
|
if (fromPay || Taro.getCurrentPages().length <= 1) {
|
|
Taro.switchTab({ url: '/pages/home/index' }).catch(() => {
|
|
Taro.reLaunch({ url: '/pages/home/index' });
|
|
});
|
|
return;
|
|
}
|
|
Taro.navigateBack();
|
|
}}
|
|
right={<ShareNavButton payload={sharePayload} />}
|
|
/>
|
|
<View className="sub-page-body">
|
|
{!order ? (
|
|
<View className="u-empty">加载中…</View>
|
|
) : (
|
|
<>
|
|
<View className="order-card">
|
|
<Text className="order-card-title">订单状态</Text>
|
|
<View className="order-status-row">
|
|
<Text className="order-list-status">
|
|
{STATUS_LABELS[order.status || ''] || order.status || '处理中'}
|
|
</Text>
|
|
{isProxy ? <Text className="order-proxy-badge">代下单</Text> : null}
|
|
</View>
|
|
{isProxy && order.proxyPartnerName ? (
|
|
<Text className="order-proxy-hint">由合伙人 {order.proxyPartnerName} 代下</Text>
|
|
) : null}
|
|
{showLogisticsPreview ? (
|
|
<View className="order-logistics-preview-block">
|
|
<View className="order-logistics-preview" onClick={goLogistics}>
|
|
<View className="order-logistics-preview-main">
|
|
{trackLoading ? (
|
|
<Text className="order-logistics-preview-info u-muted">物流信息加载中…</Text>
|
|
) : latestTrack?.trackInfo ? (
|
|
<LogisticsRichText
|
|
text={latestTrack.trackInfo}
|
|
className="order-logistics-preview-info"
|
|
/>
|
|
) : order.delivery?.manualQueryUrl ? (
|
|
<Text className="order-logistics-preview-info u-muted">物流已更新,点击查看详情</Text>
|
|
) : (
|
|
<Text className="order-logistics-preview-info u-muted">物流信息待更新,请稍后查看</Text>
|
|
)}
|
|
</View>
|
|
<Text className="order-logistics-preview-link">物流详情</Text>
|
|
</View>
|
|
{estimatedArrival ? (
|
|
<Text className="order-logistics-eta">{formatEstimatedArrival(estimatedArrival)}</Text>
|
|
) : null}
|
|
</View>
|
|
) : canViewLogistics ? (
|
|
<Text className="order-logistics-link" onClick={goLogistics}>
|
|
查看物流追踪
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
<View className="order-card">
|
|
<Text className="order-card-title">商品信息</Text>
|
|
<View className="order-row">
|
|
<Text className="order-row-label">{productName}</Text>
|
|
<Text className="order-row-value">x{quantity}</Text>
|
|
</View>
|
|
<View className="order-row">
|
|
<Text className="order-row-label">实付金额</Text>
|
|
<Text className="order-row-value--price">
|
|
¥{Number(order.payAmount ?? 0).toFixed(2)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
<View className="order-card">
|
|
<Text className="order-card-title">收货信息</Text>
|
|
{receiverLine || addressText ? (
|
|
<>
|
|
{receiverLine ? (
|
|
<View className="order-row">
|
|
<Text className="order-row-label">收货人</Text>
|
|
<Text className="order-row-value">{receiverLine}</Text>
|
|
</View>
|
|
) : null}
|
|
{addressText ? (
|
|
<View className="order-row order-row--address">
|
|
<Text className="order-row-label">收货地址</Text>
|
|
<Text className="order-row-value order-row-value--wrap">{addressText}</Text>
|
|
</View>
|
|
) : null}
|
|
</>
|
|
) : (
|
|
<Text className="u-muted">地址信息待完善</Text>
|
|
)}
|
|
</View>
|
|
<View className="order-card">
|
|
<Text className="order-card-title">订单信息</Text>
|
|
<View className="order-row">
|
|
<Text className="order-row-label">订单编号</Text>
|
|
<Text className="order-row-value">{order.orderNo || order.id}</Text>
|
|
</View>
|
|
<View className="order-row">
|
|
<Text className="order-row-label">下单时间</Text>
|
|
<Text className="order-row-value">
|
|
{order.createdAt ? String(order.createdAt).slice(0, 19).replace('T', ' ') : '-'}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</>
|
|
)}
|
|
</View>
|
|
|
|
{order ? (
|
|
<View
|
|
className={`order-detail-actionbar${
|
|
canPay || canConfirmReceive ? ' order-detail-actionbar--with-pay' : ''
|
|
}`}
|
|
>
|
|
{isWeapp ? (
|
|
<ContactCsButton
|
|
className="order-detail-cs-btn"
|
|
session={{
|
|
from: 'order-detail',
|
|
orderId: order.id,
|
|
orderNo: order.orderNo,
|
|
}}
|
|
>
|
|
联系客服
|
|
</ContactCsButton>
|
|
) : (
|
|
<View className="order-detail-cs-btn" onClick={goCustomerService}>
|
|
<Text>联系客服</Text>
|
|
</View>
|
|
)}
|
|
{canPay ? (
|
|
<>
|
|
<View className="order-confirm-total">
|
|
<Text className="order-confirm-total-label">待支付</Text>
|
|
<Text className="order-confirm-total-value">
|
|
¥{Number(order.payAmount ?? 0).toFixed(2)}
|
|
</Text>
|
|
</View>
|
|
<View className="order-confirm-submit" onClick={goPay}>
|
|
去付款
|
|
</View>
|
|
</>
|
|
) : null}
|
|
{canConfirmReceive ? (
|
|
<View
|
|
className={`order-confirm-submit${confirming ? ' order-confirm-submit--disabled' : ''}`}
|
|
onClick={confirming ? undefined : () => void confirmReceive()}
|
|
>
|
|
{confirming ? '提交中…' : '确认收货'}
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
) : null}
|
|
</PageShell>
|
|
);
|
|
}
|