Files
dukang/apps/admin-web/src/components/OrderTrackDrawer.tsx
T

186 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Button,
Descriptions,
Drawer,
Empty,
Image,
Space,
Spin,
Timeline,
Typography,
message,
} from 'antd';
import type { OrderTrackDto, OrderTrackNodeDto } from '@dukang/shared-types';
import { isXfxProviderCode } from '@dukang/shared-types';
import { request } from '../lib/api';
import { DELIVERY_PROVIDER_LABELS, fmtTime } from '../lib/constants';
type Props = {
open: boolean;
orderId: string | null;
orderNo?: string | null;
orderStatus?: string | null;
onClose: () => void;
};
function providerLabel(provider?: string | null, company?: string | null) {
if (company) return company;
if (!provider) return '—';
if (DELIVERY_PROVIDER_LABELS[provider]) return DELIVERY_PROVIDER_LABELS[provider];
if (isXfxProviderCode(provider)) return '小飞侠';
return provider;
}
function sortOldestFirst(nodes: OrderTrackNodeDto[]) {
return [...nodes].sort((a, b) => {
const ta = new Date(a.createTime).getTime();
const tb = new Date(b.createTime).getTime();
if (Number.isNaN(ta) && Number.isNaN(tb)) return 0;
if (Number.isNaN(ta)) return 1;
if (Number.isNaN(tb)) return -1;
return ta - tb;
});
}
export default function OrderTrackDrawer({
open,
orderId,
orderNo,
orderStatus,
onClose,
}: Props) {
const [loading, setLoading] = useState(false);
const [track, setTrack] = useState<OrderTrackDto | null>(null);
const load = useCallback(async () => {
if (!orderId) return;
setLoading(true);
try {
const data = await request<OrderTrackDto>(`/admin/orders/${orderId}/track`);
setTrack(data);
} catch (e) {
setTrack(null);
message.error(e instanceof Error ? e.message : '加载路由失败');
} finally {
setLoading(false);
}
}, [orderId]);
useEffect(() => {
if (!open || !orderId) {
setTrack(null);
return;
}
void load();
}, [open, orderId, load]);
const nodes = useMemo(
() => (track?.nodes?.length ? sortOldestFirst(track.nodes) : []),
[track?.nodes],
);
const timelineComplete = ['PENDING_RECEIVE', 'DELIVERED', 'COMPLETED'].includes(
orderStatus || '',
);
return (
<Drawer
title="物流路由"
width={520}
open={open}
onClose={onClose}
extra={
<Button onClick={() => void load()} loading={loading} disabled={!orderId}>
刷新
</Button>
}
>
<Spin spinning={loading}>
{(orderNo || track?.trackingNo || track?.provider) && (
<Descriptions column={1} bordered size="small" style={{ marginBottom: 16 }}>
{orderNo ? <Descriptions.Item label="订单号">{orderNo}</Descriptions.Item> : null}
<Descriptions.Item label="配送方式">
{providerLabel(track?.provider, track?.logisticsCompany)}
</Descriptions.Item>
<Descriptions.Item label="运单号">{track?.trackingNo || '—'}</Descriptions.Item>
{track?.manualQueryUrl ? (
<Descriptions.Item label="物流查询">
<a href={track.manualQueryUrl} target="_blank" rel="noreferrer">
打开物流查询
</a>
</Descriptions.Item>
) : null}
{track?.estimatedArrival?.arriveTime ? (
<Descriptions.Item label="预计送达">
{fmtTime(track.estimatedArrival.arriveTime)}
{track.estimatedArrival.siteName
? `${track.estimatedArrival.siteName}`
: ''}
</Descriptions.Item>
) : null}
</Descriptions>
)}
{track?.signPhotoUrls && track.signPhotoUrls.length > 0 ? (
<div style={{ marginBottom: 16 }}>
<Typography.Title level={5} style={{ marginTop: 0 }}>
签收照片
</Typography.Title>
<Image.PreviewGroup>
<Space wrap>
{track.signPhotoUrls.map((url) => (
<Image
key={url}
src={url}
width={96}
height={96}
style={{ objectFit: 'cover', borderRadius: 6 }}
/>
))}
</Space>
</Image.PreviewGroup>
</div>
) : null}
<Typography.Title level={5} style={{ marginTop: 0 }}>
物流动态
</Typography.Title>
{nodes.length > 0 ? (
<Timeline
items={nodes.map((node, index) => {
const isLatest = index === nodes.length - 1;
return {
color: isLatest ? (timelineComplete ? 'green' : 'blue') : 'gray',
children: (
<div>
{node.statusName ? (
<Typography.Text strong style={{ display: 'block' }}>
{node.statusName}
</Typography.Text>
) : null}
<Typography.Text style={{ display: 'block', whiteSpace: 'pre-wrap' }}>
{node.trackInfo || '—'}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{fmtTime(node.createTime)}
</Typography.Text>
</div>
),
};
})}
/>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
track?.manualQueryUrl
? '暂无实时路由节点,可使用上方物流查询链接'
: '暂无路由信息,请稍后刷新'
}
/>
)}
</Spin>
</Drawer>
);
}