feat(mini-user): logistics sign photos, dial, ETA and courier callbacks (v3.4.13)

Extend Courier adapter for XFX sign photos and route callbacks; mini-user logistics UI with timeline, phone dial, and estimated arrival.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-05 23:34:09 +08:00
parent 62652cd072
commit 345d5db9c9
19 changed files with 710 additions and 140 deletions
@@ -1,15 +1,16 @@
import { useEffect, useMemo, useState } from 'react';
import { View, Text } from '@tarojs/components';
import { View, Text, Image } from '@tarojs/components';
import Taro, { useRouter } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import LogisticsRichText from '../../components/LogisticsRichText';
import { request, toast } from '../../lib/api';
import {
deliveryProviderLabel,
fetchOrderTrack,
formatTrackNodeTime,
shouldLoadOrderTrack,
sortTrackNodesNewestFirst,
sortTrackNodesOldestFirst,
type OrderDelivery,
type OrderTrackNode,
} from '../../lib/order-logistics';
@@ -28,9 +29,13 @@ export default function OrderLogisticsPage() {
usePageView('order_logistics_view', orderId ? { orderId } : undefined);
const [order, setOrder] = useState<OrderDetail | null>(null);
const [trackNodes, setTrackNodes] = useState<OrderTrackNode[]>([]);
const [signPhotoUrls, setSignPhotoUrls] = useState<string[]>([]);
const [trackLoading, setTrackLoading] = useState(false);
const sortedTrackNodes = useMemo(() => sortTrackNodesNewestFirst(trackNodes), [trackNodes]);
const sortedTrackNodes = useMemo(() => sortTrackNodesOldestFirst(trackNodes), [trackNodes]);
const timelineComplete = ['PENDING_RECEIVE', 'DELIVERED', 'COMPLETED'].includes(
order?.status || '',
);
useEffect(() => {
if (!orderId) return;
@@ -42,14 +47,21 @@ export default function OrderLogisticsPage() {
setOrder(data);
if (!shouldLoadOrderTrack(data.delivery)) {
setTrackNodes([]);
setSignPhotoUrls([]);
return;
}
setTrackLoading(true);
try {
const track = await fetchOrderTrack(orderId);
if (!cancelled) setTrackNodes(track.nodes ?? []);
if (!cancelled) {
setTrackNodes(track.nodes ?? []);
setSignPhotoUrls(track.signPhotoUrls ?? []);
}
} catch {
if (!cancelled) setTrackNodes([]);
if (!cancelled) {
setTrackNodes([]);
setSignPhotoUrls([]);
}
} finally {
if (!cancelled) setTrackLoading(false);
}
@@ -77,6 +89,13 @@ export default function OrderLogisticsPage() {
.catch(() => toast('无法打开物流查询'));
}
function previewSignPhotos(index: number) {
if (signPhotoUrls.length === 0) return;
Taro.previewImage({ current: signPhotoUrls[index], urls: signPhotoUrls }).catch(() =>
toast('无法预览图片'),
);
}
const delivery = order?.delivery;
return (
@@ -114,6 +133,23 @@ export default function OrderLogisticsPage() {
) : null}
</View>
{signPhotoUrls.length > 0 ? (
<View className="order-card">
<Text className="order-card-title"></Text>
<View className="order-logistics-sign-photos">
{signPhotoUrls.map((url, index) => (
<Image
key={url}
className="order-logistics-sign-photo"
src={url}
mode="aspectFill"
onClick={() => previewSignPhotos(index)}
/>
))}
</View>
</View>
) : null}
{trackLoading ? (
<View className="order-card">
<Text className="order-card-title"></Text>
@@ -122,22 +158,38 @@ export default function OrderLogisticsPage() {
) : sortedTrackNodes.length > 0 ? (
<View className="order-card">
<Text className="order-card-title"></Text>
<View className="order-logistics-timeline">
{sortedTrackNodes.map((node, index) => (
<View
key={`${node.trackInfo}-${node.createTime}-${index}`}
className={`order-logistics-timeline-item${index === 0 ? ' order-logistics-timeline-item--latest' : ''}`}
>
<View className="order-logistics-timeline-dot" />
<View className="order-logistics-timeline-body">
{node.statusName ? (
<Text className="order-logistics-timeline-status">{node.statusName}</Text>
) : null}
<Text className="order-logistics-node-info">{node.trackInfo || '—'}</Text>
<Text className="order-logistics-node-time">{formatTrackNodeTime(node)}</Text>
<View
className={`order-logistics-timeline${
timelineComplete ? ' order-logistics-timeline--complete' : ''
}`}
>
{sortedTrackNodes.map((node, index) => {
const isLatest = index === sortedTrackNodes.length - 1;
return (
<View
key={`${node.trackInfo}-${node.createTime}-${index}`}
className={[
'order-logistics-timeline-item',
'order-logistics-timeline-item--done',
isLatest ? 'order-logistics-timeline-item--latest' : '',
]
.filter(Boolean)
.join(' ')}
>
<View className="order-logistics-timeline-dot" />
<View className="order-logistics-timeline-body">
{node.statusName ? (
<Text className="order-logistics-timeline-status">{node.statusName}</Text>
) : null}
<LogisticsRichText
text={node.trackInfo || '—'}
className="order-logistics-node-info"
/>
<Text className="order-logistics-node-time">{formatTrackNodeTime(node)}</Text>
</View>
</View>
</View>
))}
);
})}
</View>
</View>
) : shouldLoadOrderTrack(delivery) ? (