Files
dukang/apps/mini-user/src/pages/order-detail/index.tsx
T
jacy 83b1b0e5f6 feat(mini-user): configurable WeChat mini share scenes in system settings
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>
2026-08-06 15:45:57 +08:00

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>
);
}