refactor(订单): 抽离订单详情抽屉为独立组件并复用

将订单列表页的详情抽屉抽离为独立组件 OrderDetailDrawer
在订阅订单页面复用该组件,替换原有实现
This commit is contained in:
tikkhun 2025-11-21 14:59:49 +08:00
parent 66ba46e4d0
commit 1ffeff514d
3 changed files with 439 additions and 6 deletions

View File

@ -0,0 +1,381 @@
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;

View File

@ -84,7 +84,11 @@ import Item from 'antd/es/list/Item';
import RelatedOrders from '../../Subscription/Orders/RelatedOrders'; import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
======= =======
import RelatedOrders from './RelatedOrders'; import RelatedOrders from './RelatedOrders';
<<<<<<< HEAD
>>>>>>> 43be89b (feat(): ) >>>>>>> 43be89b (feat(): )
=======
import OrderDetailDrawer from './OrderDetailDrawer';
>>>>>>> 1f4128f (refactor(): )
import React, { useMemo, useRef, useState } from 'react'; import React, { useMemo, useRef, useState } from 'react';
import { printPDF } from '@/utils/util'; import { printPDF } from '@/utils/util';
@ -93,6 +97,9 @@ const ListPage: React.FC = () => {
const [activeKey, setActiveKey] = useState<string>('all'); const [activeKey, setActiveKey] = useState<string>('all');
const [count, setCount] = useState<any[]>([]); const [count, setCount] = useState<any[]>([]);
const [activeLine, setActiveLine] = useState<number>(-1); const [activeLine, setActiveLine] = useState<number>(-1);
const [detailOpen, setDetailOpen] = useState(false);
const [detailRecord, setDetailRecord] = useState<any | null>(null);
const [detailOrderId, setDetailOrderId] = useState<number | null>(null);
const tabs: TabsProps['items'] = useMemo(() => { const tabs: TabsProps['items'] = useMemo(() => {
const total = count.reduce((acc, cur) => acc + Number(cur.count), 0); const total = count.reduce((acc, cur) => acc + Number(cur.count), 0);
const tabs = [ const tabs = [
@ -316,13 +323,31 @@ const ListPage: React.FC = () => {
) : ( ) : (
<></> <></>
)} )}
<Detail <Button
key={record.id} key={record.id}
record={record} type="primary"
tableRef={actionRef} onClick={() => {
orderId={record.id as number} setDetailRecord(record);
setActiveLine={setActiveLine} setDetailOrderId(record.id as number);
/> setDetailOpen(true);
setActiveLine(record.id);
}}
>
<FileDoneOutlined />
</Button>
{detailRecord && detailOrderId !== null && (
<OrderDetailDrawer
open={detailOpen}
onClose={() => setDetailOpen(false)}
tableRef={actionRef}
orderId={detailOrderId as number}
record={detailRecord as any}
setActiveLine={setActiveLine}
OrderNoteComponent={OrderNote}
SalesChangeComponent={SalesChange}
/>
)}
<Divider type="vertical" /> <Divider type="vertical" />
<Dropdown <Dropdown
menu={{ menu={{

View File

@ -3,6 +3,7 @@ import { PageContainer } from '@ant-design/pro-layout';
import type { ProColumns, ActionType, ProTableProps } from '@ant-design/pro-components'; import type { ProColumns, ActionType, ProTableProps } from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components'; import { ProTable } from '@ant-design/pro-components';
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
import { App, Tag, Button } from 'antd'; import { App, Tag, Button } from 'antd';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { ordercontrollerGetorders } from '@/servers/api/order'; import { ordercontrollerGetorders } from '@/servers/api/order';
@ -10,8 +11,12 @@ import OrderDetailDrawer from './OrderDetailDrawer';
import { sitecontrollerAll } from '@/servers/api/site'; import { sitecontrollerAll } from '@/servers/api/site';
======= =======
import { App, Tag, Button, Drawer, List } from 'antd'; import { App, Tag, Button, Drawer, List } from 'antd';
=======
import { App, Tag, Button } from 'antd';
>>>>>>> 1f4128f (refactor(): )
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { ordercontrollerGetorders } from '@/servers/api/order'; import { ordercontrollerGetorders } from '@/servers/api/order';
import OrderDetailDrawer from '@/pages/Order/List/OrderDetailDrawer';
import { sitecontrollerAll } from '@/servers/api/site'; import { sitecontrollerAll } from '@/servers/api/site';
<<<<<<< HEAD <<<<<<< HEAD
import { request } from 'umi'; import { request } from 'umi';
@ -33,12 +38,17 @@ interface OrderItemRow {
const OrdersPage: React.FC = () => { const OrdersPage: React.FC = () => {
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const { message } = App.useApp(); const { message } = App.useApp();
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
// 抽屉状态:改为复用订单详情抽屉组件 // 抽屉状态:改为复用订单详情抽屉组件
=======
// 抽屉状态:改为复用订单详情抽屉组件
>>>>>>> 1f4128f (refactor(): )
const [detailOpen, setDetailOpen] = useState(false); const [detailOpen, setDetailOpen] = useState(false);
const [detailRecord, setDetailRecord] = useState<any | null>(null); const [detailRecord, setDetailRecord] = useState<any | null>(null);
const [detailOrderId, setDetailOrderId] = useState<number | null>(null); const [detailOrderId, setDetailOrderId] = useState<number | null>(null);
const Noop: React.FC<any> = () => null; const Noop: React.FC<any> = () => null;
<<<<<<< HEAD
======= =======
// 抽屉状态用于展示与订阅相关的订单详情含行项目meta // 抽屉状态用于展示与订阅相关的订单详情含行项目meta
const [drawerOpen, setDrawerOpen] = useState(false); const [drawerOpen, setDrawerOpen] = useState(false);
@ -47,6 +57,8 @@ const OrdersPage: React.FC = () => {
const [drawerMeta, setDrawerMeta] = useState<any[]>([]); const [drawerMeta, setDrawerMeta] = useState<any[]>([]);
const [isSubscription, setIsSubscription] = useState<boolean>(false); const [isSubscription, setIsSubscription] = useState<boolean>(false);
>>>>>>> 90ea0f5 (feat(): ) >>>>>>> 90ea0f5 (feat(): )
=======
>>>>>>> 1f4128f (refactor(): )
const columns: ProColumns<OrderItemRow>[] = [ const columns: ProColumns<OrderItemRow>[] = [
{ {
@ -105,10 +117,14 @@ const OrdersPage: React.FC = () => {
<Button <Button
size="small" size="small"
<<<<<<< HEAD <<<<<<< HEAD
<<<<<<< HEAD
=======
>>>>>>> 1f4128f (refactor(): )
onClick={() => { onClick={() => {
setDetailRecord(row as any); setDetailRecord(row as any);
setDetailOrderId(row.id as number); setDetailOrderId(row.id as number);
setDetailOpen(true); setDetailOpen(true);
<<<<<<< HEAD
======= =======
onClick={async () => { onClick={async () => {
try { try {
@ -146,6 +162,8 @@ const OrdersPage: React.FC = () => {
message.error(e?.message || '获取失败'); message.error(e?.message || '获取失败');
} }
>>>>>>> 90ea0f5 (feat(): ) >>>>>>> 90ea0f5 (feat(): )
=======
>>>>>>> 1f4128f (refactor(): )
}} }}
> >
@ -207,8 +225,12 @@ const OrdersPage: React.FC = () => {
}} }}
toolBarRender={false} toolBarRender={false}
/> />
<<<<<<< HEAD
<<<<<<< HEAD <<<<<<< HEAD
{/* 订阅关联:直接使用订单详情抽屉组件 */} {/* 订阅关联:直接使用订单详情抽屉组件 */}
=======
{/* 订阅关联:直接使用订单详情抽屉组件 */}
>>>>>>> 1f4128f (refactor(): )
{detailRecord && detailOrderId !== null && ( {detailRecord && detailOrderId !== null && (
<OrderDetailDrawer <OrderDetailDrawer
open={detailOpen} open={detailOpen}
@ -219,6 +241,7 @@ const OrdersPage: React.FC = () => {
setActiveLine={() => {}} setActiveLine={() => {}}
OrderNoteComponent={Noop} OrderNoteComponent={Noop}
SalesChangeComponent={Noop} SalesChangeComponent={Noop}
<<<<<<< HEAD
/> />
)} )}
======= =======
@ -265,6 +288,10 @@ const OrdersPage: React.FC = () => {
/> />
</Drawer> </Drawer>
>>>>>>> 90ea0f5 (feat(): ) >>>>>>> 90ea0f5 (feat(): )
=======
/>
)}
>>>>>>> 1f4128f (refactor(): )
</PageContainer> </PageContainer>
); );
}; };