672a995c59
Co-authored-by: Cursor <cursoragent@cursor.com>
210 lines
7.7 KiB
TypeScript
210 lines
7.7 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
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,
|
|
formatTrackNodeTime,
|
|
shouldLoadOrderTrack,
|
|
sortTrackNodesOldestFirst,
|
|
type OrderDelivery,
|
|
type OrderTrackNode,
|
|
} from '../../lib/order-logistics-utils';
|
|
import { fetchOrderTrack } from '../../lib/order-logistics';
|
|
import { usePageView } from '../../lib/usePageView';
|
|
|
|
type OrderDetail = {
|
|
id: string;
|
|
orderNo?: string;
|
|
status?: string;
|
|
delivery?: OrderDelivery | null;
|
|
};
|
|
|
|
export default function OrderLogisticsPage() {
|
|
const router = useRouter();
|
|
const orderId = router.params.id ?? '';
|
|
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(
|
|
() => (Array.isArray(trackNodes) ? sortTrackNodesOldestFirst(trackNodes) : []),
|
|
[trackNodes],
|
|
);
|
|
const timelineComplete = ['PENDING_RECEIVE', 'DELIVERED', 'COMPLETED'].includes(
|
|
order?.status || '',
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!orderId) return;
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const data = await request<OrderDetail>(`/trade/orders/${orderId}`);
|
|
if (cancelled) return;
|
|
setOrder(data);
|
|
if (!shouldLoadOrderTrack(data.delivery)) {
|
|
setTrackNodes([]);
|
|
setSignPhotoUrls([]);
|
|
return;
|
|
}
|
|
setTrackLoading(true);
|
|
try {
|
|
const track = await fetchOrderTrack(orderId);
|
|
if (!cancelled) {
|
|
setTrackNodes(track.nodes ?? []);
|
|
setSignPhotoUrls(track.signPhotoUrls ?? []);
|
|
}
|
|
} catch {
|
|
if (!cancelled) {
|
|
setTrackNodes([]);
|
|
setSignPhotoUrls([]);
|
|
}
|
|
} finally {
|
|
if (!cancelled) setTrackLoading(false);
|
|
}
|
|
} catch (e) {
|
|
if (!cancelled) toast(e instanceof Error ? e.message : '加载失败');
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [orderId]);
|
|
|
|
function openManualQuery() {
|
|
const url = order?.delivery?.manualQueryUrl?.trim();
|
|
if (!url) {
|
|
toast('暂无物流查询链接');
|
|
return;
|
|
}
|
|
if (process.env.TARO_ENV === 'h5' && typeof window !== 'undefined') {
|
|
window.open(url, '_blank');
|
|
return;
|
|
}
|
|
Taro.setClipboardData({ data: url })
|
|
.then(() => toast('查询链接已复制,请在浏览器中打开'))
|
|
.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 (
|
|
<PageShell variant="sub" className="order-logistics-page">
|
|
<SubPageHeader title="物流详情" />
|
|
<View className="sub-page-body order-logistics-body">
|
|
{!order ? (
|
|
<View className="u-empty">加载中…</View>
|
|
) : (
|
|
<>
|
|
<View className="order-card order-logistics-info-card">
|
|
<Text className="order-card-title">物流信息</Text>
|
|
<View className="order-row">
|
|
<Text className="order-row-label">配送方式</Text>
|
|
<Text className="order-row-value">
|
|
{deliveryProviderLabel(delivery?.provider, delivery?.logisticsCompany)}
|
|
</Text>
|
|
</View>
|
|
{delivery?.trackingNo ? (
|
|
<View className="order-row">
|
|
<Text className="order-row-label">运单号</Text>
|
|
<Text className="order-row-value">{delivery.trackingNo}</Text>
|
|
</View>
|
|
) : null}
|
|
{delivery?.manualQueryUrl ? (
|
|
<View className="order-row">
|
|
<Text className="order-row-label">物流查询</Text>
|
|
<Text className="order-row-value order-row-value--link" onClick={openManualQuery}>
|
|
查看物流
|
|
</Text>
|
|
</View>
|
|
) : null}
|
|
{!delivery?.trackingNo && !delivery?.manualQueryUrl ? (
|
|
<Text className="u-muted">物流信息待更新,请稍后查看</Text>
|
|
) : 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>
|
|
<Text className="u-muted">加载中…</Text>
|
|
</View>
|
|
) : sortedTrackNodes.length > 0 ? (
|
|
<View className="order-card">
|
|
<Text className="order-card-title">物流动态</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>
|
|
) : shouldLoadOrderTrack(delivery) ? (
|
|
<View className="order-card">
|
|
<Text className="order-card-title">物流动态</Text>
|
|
<Text className="u-muted">暂无路由信息,请稍后刷新</Text>
|
|
</View>
|
|
) : null}
|
|
</>
|
|
)}
|
|
</View>
|
|
</PageShell>
|
|
);
|
|
}
|