feat(v3.4.15): admin delivery track drawer and mini-user store list polish
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user