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:
@@ -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) ? (
|
||||
|
||||
Reference in New Issue
Block a user