381 lines
14 KiB
TypeScript
381 lines
14 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||
import {
|
||
App,
|
||
Button,
|
||
Card,
|
||
Divider,
|
||
Drawer,
|
||
Empty,
|
||
Popconfirm,
|
||
Space,
|
||
Tag,
|
||
} from 'antd';
|
||
import { ActionType } from '@ant-design/pro-components';
|
||
import { CopyOutlined, DownOutlined, FileDoneOutlined } from '@ant-design/icons';
|
||
|
||
// 服务器 API 引用(保持与原 index.tsx 一致)
|
||
import {
|
||
ordercontrollerChangestatus,
|
||
ordercontrollerGetorderdetail,
|
||
ordercontrollerSyncorderbyid,
|
||
} from '@/servers/api/order';
|
||
import { logisticscontrollerDelshipment } from '@/servers/api/logistics';
|
||
|
||
// 工具与子组件
|
||
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>();
|
||
const [detail, setDetail] = useState<any | null>(null);
|
||
|
||
// 中文注释:加载详情数据(与 index.tsx 中完全保持一致)
|
||
const initRequest = async () => {
|
||
const { data, success }: API.OrderDetailRes =
|
||
await ordercontrollerGetorderdetail({ orderId });
|
||
if (!success || !data) return null;
|
||
// 中文注释:销售项合并 SKU,展示数量汇总
|
||
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;
|
||
};
|
||
|
||
useEffect(() => {
|
||
(async () => {
|
||
if (open && orderId) {
|
||
try {
|
||
const data = await initRequest();
|
||
setDetail(data);
|
||
} catch (e) {
|
||
setDetail(null);
|
||
}
|
||
} else {
|
||
setDetail(null);
|
||
}
|
||
})();
|
||
}, [open, orderId]);
|
||
|
||
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>,
|
||
]
|
||
: []),
|
||
]}
|
||
>
|
||
{(() => { setActiveLine(record.id as number); return null; })()}
|
||
{/** 中文注释:优先使用详情数据,其次使用列表行数据 */}
|
||
{(() => {
|
||
const drec: any = detail || record;
|
||
{/* 中文注释:基本信息展示(保持原有格式) */}
|
||
return (<>
|
||
<div style={{ marginBottom: 12 }}>
|
||
<Space>
|
||
<Tag>{drec?.orderStatus}</Tag>
|
||
<Tag>{formatSource(drec.source_type, drec.utm_source)}</Tag>
|
||
</Space>
|
||
</div>
|
||
|
||
{/* 中文注释:原始订单行项目 */}
|
||
<div style={{ marginBottom: 12 }}>
|
||
<div style={{ fontWeight: 600, marginBottom: 6 }}>原始订单</div>
|
||
{Array.isArray((drec as any)?.items) && drec.items.length > 0 ? (
|
||
<ul>
|
||
{drec.items.map((item: any) => (
|
||
<li key={item.id}>
|
||
{item.name}:{item.quantity}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<Empty description="暂无数据" />
|
||
)}
|
||
</div>
|
||
|
||
{/* 中文注释:关联(订阅/订单),使用已存在的 RelatedOrders 组件 */}
|
||
<div style={{ marginBottom: 12 }}>
|
||
<div style={{ fontWeight: 600, marginBottom: 6 }}>关联</div>
|
||
<RelatedOrders data={(drec as any)?.related} />
|
||
</div>
|
||
|
||
{/* 中文注释:订单内容(销售项) */}
|
||
<div style={{ marginBottom: 12 }}>
|
||
<div style={{ fontWeight: 600, marginBottom: 6 }}>订单内容</div>
|
||
{Array.isArray((drec as any)?.sales) && drec.sales.length > 0 ? (
|
||
<ul>
|
||
{drec.sales.map((item: any) => (
|
||
<li key={item.id}>
|
||
{item.name}:{item.quantity}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
) : (
|
||
<Empty description="暂无数据" />
|
||
)}
|
||
</div>
|
||
|
||
{/* 中文注释:备注 */}
|
||
<div style={{ marginBottom: 12 }}>
|
||
<div style={{ fontWeight: 600, marginBottom: 6 }}>备注</div>
|
||
{Array.isArray((drec as any)?.notes) && drec.notes.length > 0 ? (
|
||
<div style={{ width: '100%' }}>
|
||
{drec.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>
|
||
) : (
|
||
<Empty description="暂无备注" />
|
||
)}
|
||
</div>
|
||
|
||
{/* 中文注释:物流信息 */}
|
||
<div style={{ marginBottom: 12 }}>
|
||
<div style={{ fontWeight: 600, marginBottom: 6 }}>物流信息</div>
|
||
{Array.isArray((drec as any)?.shipment) && drec.shipment.length > 0 ? (
|
||
<div style={{ display: 'flex', flexDirection: 'column', width: '100%' }}>
|
||
{(drec as any).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 (err) {
|
||
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();
|
||
} catch (error: any) {
|
||
message.error(error.message);
|
||
}
|
||
}}
|
||
>
|
||
取消运单
|
||
</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>
|
||
) : (
|
||
<Empty description="暂无物流信息" />
|
||
)}
|
||
</div>
|
||
|
||
{/* 中文注释:换货(外部传入组件) */}
|
||
<div style={{ marginBottom: 12 }}>
|
||
<SalesChangeComponent detailRef={ref} id={drec.id as number} />
|
||
</div>
|
||
</>);
|
||
})()}
|
||
</Drawer>
|
||
);
|
||
};
|
||
|
||
export default OrderDetailDrawer; |