487 lines
16 KiB
TypeScript
487 lines
16 KiB
TypeScript
import { CopyOutlined, DeleteFilled } from '@ant-design/icons';
|
|
import { ActionType, ProDescriptions } from '@ant-design/pro-components';
|
|
import { App, Button, Card, Divider, Drawer, Empty, Popconfirm } from 'antd';
|
|
import React, { useEffect, useRef } from 'react';
|
|
|
|
// 服务器 API 引用(保持与原 index.tsx 一致)
|
|
import { logisticscontrollerDelshipment } from '@/servers/api/logistics';
|
|
import {
|
|
ordercontrollerChangestatus,
|
|
ordercontrollerGetorderdetail,
|
|
ordercontrollerSyncorderbyid,
|
|
} from '@/servers/api/order';
|
|
import { sitecontrollerAll } from '@/servers/api/site';
|
|
|
|
// 工具与子组件
|
|
import { ORDER_STATUS_ENUM } from '@/constants';
|
|
import { formatShipmentState, formatSource } from '@/utils/format';
|
|
import RelatedOrders from './RelatedOrders';
|
|
|
|
// 为保持原文件结构简单,此处从 index.tsx 引入的子组件仍由原文件导出或保持原状
|
|
// 若后续需要彻底解耦,可将 OrderNote / Shipping / SalesChange 也独立到文件
|
|
// 当前按你的要求仅抽离详情 Drawer
|
|
|
|
type OrderRecord = API.Order;
|
|
|
|
interface OrderDetailDrawerProps {
|
|
tableRef: React.MutableRefObject<ActionType | undefined>; // 列表刷新引用
|
|
orderId: number; // 订单主键 ID
|
|
record: OrderRecord; // 订单行记录
|
|
open: boolean; // 是否打开抽屉
|
|
onClose: () => void; // 关闭抽屉回调
|
|
setActiveLine: (id: number) => void; // 高亮当前行
|
|
OrderNoteComponent: React.ComponentType<any>; // 备注组件(从外部注入)
|
|
SalesChangeComponent: React.ComponentType<any>; // 换货组件(从外部注入)
|
|
}
|
|
|
|
const OrderDetailDrawer: React.FC<OrderDetailDrawerProps> = ({
|
|
tableRef,
|
|
orderId,
|
|
record,
|
|
open,
|
|
onClose,
|
|
setActiveLine,
|
|
OrderNoteComponent,
|
|
SalesChangeComponent,
|
|
}) => {
|
|
const { message } = App.useApp();
|
|
const ref = useRef<ActionType>();
|
|
|
|
// 加载详情数据(与 index.tsx 中完全保持一致)
|
|
const initRequest = async () => {
|
|
const { data, success }: API.OrderDetailRes =
|
|
await ordercontrollerGetorderdetail({ orderId });
|
|
if (!success || !data) return { data: {} } as any;
|
|
data.sales = data.sales?.reduce(
|
|
(acc: API.OrderSale[], cur: API.OrderSale) => {
|
|
const idx = acc.findIndex((v: any) => v.productId === cur.productId);
|
|
if (idx === -1) acc.push(cur);
|
|
else acc[idx].quantity += cur.quantity;
|
|
return acc;
|
|
},
|
|
[],
|
|
);
|
|
return { data } as any;
|
|
};
|
|
|
|
useEffect(() => {
|
|
if (open && record?.id) {
|
|
setActiveLine(record.id as number);
|
|
}
|
|
}, [open, record?.id]);
|
|
|
|
return (
|
|
<Drawer
|
|
title="订单详情"
|
|
open={open}
|
|
destroyOnHidden
|
|
size="large"
|
|
onClose={onClose}
|
|
footer={[
|
|
// 备注组件(外部传入以避免循环依赖)
|
|
<OrderNoteComponent key="order-note" id={orderId} descRef={ref} />,
|
|
...(['after_sale_pending', 'pending_reshipment'].includes(
|
|
record.orderStatus,
|
|
)
|
|
? []
|
|
: [
|
|
<Divider key="divider-sync" type="vertical" />,
|
|
<Button
|
|
key="btn-sync"
|
|
type="primary"
|
|
onClick={async () => {
|
|
try {
|
|
const { success, message: errMsg } =
|
|
await ordercontrollerSyncorderbyid({
|
|
siteId: record.siteId as string,
|
|
orderId: record.externalOrderId as string,
|
|
});
|
|
if (!success) throw new Error(errMsg);
|
|
message.success('同步成功');
|
|
tableRef.current?.reload();
|
|
} catch (error) {
|
|
message.error(error?.message || '同步失败');
|
|
}
|
|
}}
|
|
>
|
|
同步订单
|
|
</Button>,
|
|
]),
|
|
...([
|
|
'processing',
|
|
'pending_reshipment',
|
|
'completed',
|
|
'pending_refund',
|
|
].includes(record.orderStatus)
|
|
? [
|
|
<Divider key="divider-after-sale" type="vertical" />,
|
|
<Popconfirm
|
|
key="btn-after-sale"
|
|
title="转至售后"
|
|
description="确认转至售后?"
|
|
onConfirm={async () => {
|
|
try {
|
|
const { success, message: errMsg } =
|
|
await ordercontrollerChangestatus(
|
|
{ id: record.id },
|
|
{ status: 'after_sale_pending' },
|
|
);
|
|
if (!success) throw new Error(errMsg);
|
|
tableRef.current?.reload();
|
|
} catch (error: any) {
|
|
message.error(error.message);
|
|
}
|
|
}}
|
|
>
|
|
<Button type="primary" ghost>
|
|
转至售后
|
|
</Button>
|
|
</Popconfirm>,
|
|
]
|
|
: []),
|
|
...(record.orderStatus === 'after_sale_pending'
|
|
? [
|
|
<Divider key="divider-cancel" type="vertical" />,
|
|
<Popconfirm
|
|
key="btn-cancel"
|
|
title="转至取消"
|
|
description="确认转至取消?"
|
|
onConfirm={async () => {
|
|
try {
|
|
const { success, message: errMsg } =
|
|
await ordercontrollerChangestatus(
|
|
{ id: record.id },
|
|
{ status: 'cancelled' },
|
|
);
|
|
if (!success) throw new Error(errMsg);
|
|
tableRef.current?.reload();
|
|
} catch (error: any) {
|
|
message.error(error.message);
|
|
}
|
|
}}
|
|
>
|
|
<Button type="primary" ghost>
|
|
转至取消
|
|
</Button>
|
|
</Popconfirm>,
|
|
<Divider key="divider-refund" type="vertical" />,
|
|
<Popconfirm
|
|
key="btn-refund"
|
|
title="转至退款"
|
|
description="确认转至退款?"
|
|
onConfirm={async () => {
|
|
try {
|
|
const { success, message: errMsg } =
|
|
await ordercontrollerChangestatus(
|
|
{ id: record.id },
|
|
{ status: 'refund_requested' },
|
|
);
|
|
if (!success) throw new Error(errMsg);
|
|
tableRef.current?.reload();
|
|
} catch (error: any) {
|
|
message.error(error.message);
|
|
}
|
|
}}
|
|
>
|
|
<Button type="primary" ghost>
|
|
转至退款
|
|
</Button>
|
|
</Popconfirm>,
|
|
<Divider key="divider-completed" type="vertical" />,
|
|
<Popconfirm
|
|
key="btn-completed"
|
|
title="转至完成"
|
|
description="确认转至完成?"
|
|
onConfirm={async () => {
|
|
try {
|
|
const { success, message: errMsg } =
|
|
await ordercontrollerChangestatus(
|
|
{ id: record.id },
|
|
{ status: 'completed' },
|
|
);
|
|
if (!success) throw new Error(errMsg);
|
|
tableRef.current?.reload();
|
|
} catch (error: any) {
|
|
message.error(error.message);
|
|
}
|
|
}}
|
|
>
|
|
<Button type="primary" ghost>
|
|
转至完成
|
|
</Button>
|
|
</Popconfirm>,
|
|
]
|
|
: []),
|
|
]}
|
|
>
|
|
<ProDescriptions
|
|
labelStyle={{ width: '100px' }}
|
|
actionRef={ref}
|
|
request={initRequest}
|
|
>
|
|
<ProDescriptions.Item
|
|
label="站点"
|
|
dataIndex="siteId"
|
|
valueType="select"
|
|
request={async () => {
|
|
const { data = [] } = await sitecontrollerAll();
|
|
return data.map((item) => ({
|
|
label: item.name,
|
|
value: item.id,
|
|
}));
|
|
}}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="订单日期"
|
|
dataIndex="date_created"
|
|
valueType="dateTime"
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="订单状态"
|
|
dataIndex="orderStatus"
|
|
valueType="select"
|
|
valueEnum={ORDER_STATUS_ENUM as any}
|
|
/>
|
|
<ProDescriptions.Item label="金额" dataIndex="total" />
|
|
<ProDescriptions.Item label="客户邮箱" dataIndex="customer_email" />
|
|
<ProDescriptions.Item
|
|
label="联系电话"
|
|
span={3}
|
|
render={(_, r: any) => (
|
|
<div>
|
|
<span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span>
|
|
</div>
|
|
)}
|
|
/>
|
|
<ProDescriptions.Item label="交易Id" dataIndex="transaction_id" />
|
|
<ProDescriptions.Item label="IP" dataIndex="customer_id_address" />
|
|
<ProDescriptions.Item label="设备" dataIndex="device_type" />
|
|
<ProDescriptions.Item
|
|
label="来源"
|
|
render={(_, r: any) => formatSource(r.source_type, r.utm_source)}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="原订单状态"
|
|
dataIndex="status"
|
|
valueType="select"
|
|
valueEnum={ORDER_STATUS_ENUM as any}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="支付链接"
|
|
dataIndex="payment_url"
|
|
span={3}
|
|
copyable
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="客户备注"
|
|
dataIndex="customer_note"
|
|
span={3}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="发货信息"
|
|
span={3}
|
|
render={(_, r: any) => (
|
|
<div>
|
|
<div>
|
|
company:
|
|
<span>
|
|
{r?.shipping?.company || r?.billing?.company || '-'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
first_name:
|
|
<span>
|
|
{r?.shipping?.first_name || r?.billing?.first_name || '-'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
last_name:
|
|
<span>
|
|
{r?.shipping?.last_name || r?.billing?.last_name || '-'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
country:
|
|
<span>
|
|
{r?.shipping?.country || r?.billing?.country || '-'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
state:
|
|
<span>{r?.shipping?.state || r?.billing?.state || '-'}</span>
|
|
</div>
|
|
<div>
|
|
city:<span>{r?.shipping?.city || r?.billing?.city || '-'}</span>
|
|
</div>
|
|
<div>
|
|
postcode:
|
|
<span>
|
|
{r?.shipping?.postcode || r?.billing?.postcode || '-'}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
phone:
|
|
<span>{r?.shipping?.phone || r?.billing?.phone || '-'}</span>
|
|
</div>
|
|
<div>
|
|
address_1:
|
|
<span>
|
|
{r?.shipping?.address_1 || r?.billing?.address_1 || '-'}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="原始订单"
|
|
span={3}
|
|
render={(_, r: any) => (
|
|
<ul>
|
|
{(r?.items || []).map((item: any) => (
|
|
<li key={item.id}>
|
|
{item.name}:{item.quantity}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="关联"
|
|
span={3}
|
|
render={(_, r: any) => <RelatedOrders data={r?.related} />}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="订单内容"
|
|
span={3}
|
|
render={(_, r: any) => (
|
|
<ul>
|
|
{(r?.sales || []).map((item: any) => (
|
|
<li key={item.id}>
|
|
{item.name}:{item.quantity}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="换货"
|
|
span={3}
|
|
render={(_, r: any) => (
|
|
<SalesChangeComponent detailRef={ref} id={r.id as number} />
|
|
)}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="备注"
|
|
span={3}
|
|
render={(_, r: any) => {
|
|
if (!r.notes || r.notes.length === 0)
|
|
return <Empty description="暂无备注" />;
|
|
return (
|
|
<div style={{ width: '100%' }}>
|
|
{r.notes.map((note: any) => (
|
|
<div style={{ marginBottom: 10 }} key={note.id}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
justifyContent: 'space-between',
|
|
}}
|
|
>
|
|
<span>{note.username}</span>
|
|
<span>{note.createdAt}</span>
|
|
</div>
|
|
<div>{note.content}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}}
|
|
/>
|
|
<ProDescriptions.Item
|
|
label="物流信息"
|
|
span={3}
|
|
render={(_, r: any) => {
|
|
if (!r.shipment || r.shipment.length === 0)
|
|
return <Empty description="暂无物流信息" />;
|
|
return (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
width: '100%',
|
|
}}
|
|
>
|
|
{r.shipment.map((v: any) => (
|
|
<Card
|
|
key={v.id}
|
|
style={{ marginBottom: '10px' }}
|
|
extra={formatShipmentState(v.state)}
|
|
title={
|
|
<>
|
|
{v.tracking_provider}
|
|
{v.primary_tracking_number}
|
|
<CopyOutlined
|
|
onClick={async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(
|
|
v.tracking_url,
|
|
);
|
|
message.success('复制成功!');
|
|
} catch {
|
|
message.error('复制失败!');
|
|
}
|
|
}}
|
|
/>
|
|
</>
|
|
}
|
|
actions={
|
|
v.state === 'waiting-for-scheduling' ||
|
|
v.state === 'waiting-for-transit'
|
|
? [
|
|
<Popconfirm
|
|
key="action-cancel"
|
|
title="取消运单"
|
|
description="确认取消运单?"
|
|
onConfirm={async () => {
|
|
try {
|
|
const { success, message: errMsg } =
|
|
await logisticscontrollerDelshipment({
|
|
id: v.id,
|
|
});
|
|
if (!success) throw new Error(errMsg);
|
|
tableRef.current?.reload();
|
|
ref.current?.reload?.();
|
|
} catch (error: any) {
|
|
message.error(error.message);
|
|
}
|
|
}}
|
|
>
|
|
<DeleteFilled />
|
|
取消运单
|
|
</Popconfirm>,
|
|
]
|
|
: []
|
|
}
|
|
>
|
|
<div>
|
|
订单号:{' '}
|
|
{Array.isArray(v?.orderIds) ? v.orderIds.join(',') : '-'}
|
|
</div>
|
|
{Array.isArray(v?.items) &&
|
|
v.items.map((item: any) => (
|
|
<div key={item.id}>
|
|
{item.name}: {item.quantity}
|
|
</div>
|
|
))}
|
|
</Card>
|
|
))}
|
|
</div>
|
|
);
|
|
}}
|
|
/>
|
|
</ProDescriptions>
|
|
</Drawer>
|
|
);
|
|
};
|
|
|
|
export default OrderDetailDrawer;
|