feat(订单): 添加关联订单显示功能并创建订单商品和订阅订单页面

feat(订阅): 增加订阅订单关联功能及详情查看
feat(商品): 新增订单商品概览页面
style(订单): 优化订单详情显示格式
This commit is contained in:
tikkhun 2025-11-19 15:55:40 +08:00
parent 737cfef733
commit a1e8e81407
4 changed files with 504 additions and 3 deletions

View File

@ -0,0 +1,149 @@
import React, { useRef } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import type { ProColumns, ActionType, ProTableProps } from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import { App } from 'antd';
import dayjs from 'dayjs';
import { ordercontrollerGetordersales } from '@/servers/api/order';
import { sitecontrollerAll } from '@/servers/api/site';
// 列表行数据结构(订单商品聚合)
interface OrderItemAggRow {
externalProductId: number; // 商品ID来自 WooCommerce 产品ID
externalVariationId: number; // 变体ID来自 WooCommerce 变体ID
name: string; // 商品名称
totalQuantity: number; // 总售出数量(时间范围内)
totalOrders: number; // 涉及订单数(去重)
firstOrderCount: number; // 客户首单次数(该商品)
secondOrderCount: number; // 客户第二次购买次数(该商品)
thirdOrderCount: number; // 客户第三次购买次数(该商品)
moreThirdOrderCount: number; // 客户超过三次购买次数(该商品)
}
const OrderItemsPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
// 列配置(中文标题,符合当前项目风格;显示英文默认语言可后续走国际化)
const columns: ProColumns<OrderItemAggRow>[] = [
{
title: '商品名称',
dataIndex: 'name',
width: 220,
},
{
title: '商品ID',
dataIndex: 'externalProductId',
width: 120,
hideInSearch: true,
},
{
title: '变体ID',
dataIndex: 'externalVariationId',
width: 120,
hideInSearch: true,
},
{
title: '总售出数量',
dataIndex: 'totalQuantity',
width: 130,
hideInSearch: true,
},
{
title: '订单数',
dataIndex: 'totalOrders',
width: 110,
hideInSearch: true,
},
{
title: '首单次数',
dataIndex: 'firstOrderCount',
width: 120,
hideInSearch: true,
},
{
title: '第二次购买',
dataIndex: 'secondOrderCount',
width: 120,
hideInSearch: true,
},
{
title: '第三次购买',
dataIndex: 'thirdOrderCount',
width: 120,
hideInSearch: true,
},
{
title: '超过三次购买',
dataIndex: 'moreThirdOrderCount',
width: 140,
hideInSearch: true,
},
// 搜索区域字段
{
title: '站点',
dataIndex: 'siteId',
valueType: 'select',
request: async () => {
// 拉取站点列表(后台 /site/all
const { data = [] } = await sitecontrollerAll();
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
},
},
{
title: '时间范围',
dataIndex: 'dateRange',
valueType: 'dateRange',
hideInTable: true,
},
{
title: '商品关键字',
dataIndex: 'name',
hideInTable: true,
},
];
// 表格请求方法:调用 /order/getOrderSales 接口并设置 isSource=true 获取订单项聚合
const request: ProTableProps<OrderItemAggRow>['request'] = async (params:any) => {
try {
const { current = 1, pageSize = 10, siteId, name } = params as any;
const [startDate, endDate] = (params as any).dateRange || [];
// 调用后端接口isSource=true 表示按订单项聚合)
const resp = await ordercontrollerGetordersales({
current,
pageSize,
siteId,
name,
isSource: true as any,
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
} as any);
const { success, data, message: errMsg } = resp as any;
if (!success) throw new Error(errMsg || '获取失败');
return {
data: (data?.items ?? []) as OrderItemAggRow[],
total: data?.total ?? 0,
success: true,
};
} catch (e: any) {
message.error(e?.message || '获取失败');
return { data: [], total: 0, success: false };
}
};
return (
<PageContainer title='订单商品概览'>
<ProTable<OrderItemAggRow>
actionRef={actionRef}
rowKey={(r) => `${r.externalProductId}-${r.externalVariationId}-${r.name}`}
columns={columns}
request={request}
pagination={{ showSizeChanger: true }}
search={{ labelWidth: 90, span: 6 }}
toolBarRender={false}
/>
</PageContainer>
);
};
export default OrderItemsPage;

View File

@ -900,6 +900,22 @@ const Detail: React.FC<{
); );
}} }}
/> />
{/* TODO 显示 related order */}
<ProDescriptions.Item
label="关联订单"
span={3}
render={(_, record) => {
return (
<ul>
{record?.related?.map((item: any) => (
<li key={item.id}>
{JSON.stringify(item)}
</li>
))}
</ul>
);
}}
/>
<ProDescriptions.Item <ProDescriptions.Item
label="订单内容" label="订单内容"
span={3} span={3}

View File

@ -1,4 +1,4 @@
import React, { useRef } from 'react'; import React, { useRef, useState } from 'react';
import { import {
ActionType, ActionType,
DrawerForm, DrawerForm,
@ -7,7 +7,9 @@ import {
ProFormSelect, ProFormSelect,
ProTable, ProTable,
} from '@ant-design/pro-components'; } from '@ant-design/pro-components';
import { App, Button, Tag } from 'antd'; import { App, Button, Tag, Drawer, List } from 'antd';
import dayjs from 'dayjs';
import { request } from 'umi';
import { import {
subscriptioncontrollerList, subscriptioncontrollerList,
subscriptioncontrollerSync, subscriptioncontrollerSync,
@ -32,6 +34,12 @@ const SUBSCRIPTION_STATUS_ENUM: Record<string, { text: string }> = {
const ListPage: React.FC = () => { const ListPage: React.FC = () => {
// 表格操作引用:用于在同步后触发表格刷新 // 表格操作引用:用于在同步后触发表格刷新
const actionRef = useRef<ActionType>(); const actionRef = useRef<ActionType>();
const { message } = App.useApp();
// 关联订单抽屉状态
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerTitle, setDrawerTitle] = useState('详情');
const [relatedOrders, setRelatedOrders] = useState<any[]>([]);
// 表格列定义(尽量与项目风格保持一致) // 表格列定义(尽量与项目风格保持一致)
const columns: ProColumns<API.Subscription>[] = [ const columns: ProColumns<API.Subscription>[] = [
@ -55,6 +63,13 @@ const ListPage: React.FC = () => {
hideInSearch: true, hideInSearch: true,
width: 120, width: 120,
}, },
{
title: '父订单号',
dataIndex: 'parent_id',
hideInSearch: true,
width: 120,
render: (_, row) => (row?.parent_id ? <Tag>{row.parent_id}</Tag> : '-'),
},
{ {
title: '状态', title: '状态',
dataIndex: 'status', dataIndex: 'status',
@ -114,6 +129,69 @@ const ListPage: React.FC = () => {
hideInSearch: true, hideInSearch: true,
width: 160, width: 160,
}, },
{
title: '操作',
dataIndex: 'actions',
hideInSearch: true,
width: 120,
render: (_, row) => (
<Button
size="small"
onClick={async () => {
try {
const parentNumber = String(row?.parent_id || '');
if (!parentNumber) {
message.warning('该订阅缺少父订单号');
return;
}
// 通过父订单号查询关联订单(模糊匹配)
const resp = await request('/order/getOrderByNumber', {
method: 'POST',
data: { number: parentNumber },
});
const { success, data, message: errMsg } = resp as any;
if (!success) throw new Error(errMsg || '获取失败');
// 仅保留与父订单号完全一致的订单(避免模糊匹配误入)
const candidates: any[] = (Array.isArray(data) ? data : []).filter(
(c: any) => String(c?.externalOrderId) === parentNumber
);
// 拉取详情,补充状态、金额、时间
const details = [] as any[];
for (const c of candidates) {
const d = await request(`/order/${c.id}`, { method: 'GET' });
if ((d as any)?.success) {
const od = (d as any)?.data || {};
details.push({
id: c.id,
externalOrderId: c.externalOrderId,
siteName: c.siteName,
status: od?.status,
total: od?.total,
currency_symbol: od?.currency_symbol,
date_created: od?.date_created,
relationship: 'Parent Order',
});
} else {
details.push({
id: c.id,
externalOrderId: c.externalOrderId,
siteName: c.siteName,
relationship: 'Parent Order',
});
}
}
setRelatedOrders(details);
setDrawerTitle(`详情`);
setDrawerOpen(true);
} catch (e: any) {
message.error(e?.message || '获取失败');
}
}}
>
</Button>
),
},
]; ];
return ( return (
@ -138,6 +216,34 @@ const ListPage: React.FC = () => {
// 工具栏:订阅同步入口 // 工具栏:订阅同步入口
toolBarRender={() => [<SyncForm key="sync" tableRef={actionRef} />]} toolBarRender={() => [<SyncForm key="sync" tableRef={actionRef} />]}
/> />
{/* 关联订单抽屉:展示订单号、关系、时间、状态与金额 */}
<Drawer
open={drawerOpen}
title={drawerTitle}
width={720}
onClose={() => setDrawerOpen(false)}
>
<List
header={<div></div>}
dataSource={relatedOrders}
renderItem={(item: any) => (
<List.Item>
<List.Item.Meta
title={`#${item?.externalOrderId || '-'}`}
description={`关系:${item?.relationship || '-'},站点:${item?.siteName || '-'}`}
/>
<div style={{ display: 'flex', gap: 12, alignItems: 'center' }}>
<span>{item?.date_created ? dayjs(item.date_created).format('YYYY-MM-DD HH:mm') : '-'}</span>
<Tag>{item?.status || '-'}</Tag>
<span>
{item?.currency_symbol || ''}
{typeof item?.total === 'number' ? item.total.toFixed(2) : item?.total ?? '-'}
</span>
</div>
</List.Item>
)}
/>
</Drawer>
</PageContainer> </PageContainer>
); );
}; };

View File

@ -0,0 +1,230 @@
import React, { useRef, useState } from 'react';
import { PageContainer } from '@ant-design/pro-layout';
import type { ProColumns, ActionType, ProTableProps } from '@ant-design/pro-components';
import { ProTable } from '@ant-design/pro-components';
import { App, Tag, Button, Drawer, List } from 'antd';
import dayjs from 'dayjs';
import { ordercontrollerGetorders } from '@/servers/api/order';
import { sitecontrollerAll } from '@/servers/api/site';
import { request } from 'umi';
interface OrderItemRow {
id: number;
externalOrderId: string;
siteId: string;
date_created: string;
customer_email: string;
payment_method: string;
total: number;
orderStatus: string;
}
const OrdersPage: React.FC = () => {
const actionRef = useRef<ActionType>();
const { message } = App.useApp();
// 抽屉状态用于展示与订阅相关的订单详情含行项目meta
const [drawerOpen, setDrawerOpen] = useState(false);
const [drawerTitle, setDrawerTitle] = useState<string>('订阅关联');
const [drawerItems, setDrawerItems] = useState<any[]>([]);
const [drawerMeta, setDrawerMeta] = useState<any[]>([]);
const [isSubscription, setIsSubscription] = useState<boolean>(false);
const columns: ProColumns<OrderItemRow>[] = [
{
title: '订单ID',
dataIndex: 'externalOrderId',
width: 120,
ellipsis: true,
hideInSearch: true,
},
{
title: '站点',
dataIndex: 'siteId',
width: 120,
valueType: 'select',
request: async () => {
const { data = [] } = await sitecontrollerAll();
return (data || []).map((item: any) => ({ label: item.siteName, value: item.id }));
},
},
{
title: '下单时间',
dataIndex: 'date_created',
width: 180,
hideInSearch: true,
render: (_, row) => (row?.date_created ? dayjs(row.date_created).format('YYYY-MM-DD HH:mm') : '-'),
},
{
title: '邮箱',
dataIndex: 'customer_email',
width: 200,
},
{
title: '支付方式',
dataIndex: 'payment_method',
width: 140,
},
{
title: '金额',
dataIndex: 'total',
width: 100,
hideInSearch: true,
},
{
title: 'ERP状态',
dataIndex: 'orderStatus',
width: 120,
hideInSearch: true,
render: (_, row) => <Tag>{row.orderStatus}</Tag>,
},
{
title: '订阅关联',
dataIndex: 'subscription_related',
width: 120,
hideInSearch: true,
render: (_, row) => (
<Button
size="small"
onClick={async () => {
try {
// 拉取订单详情(包含 items 与 meta_data用于判断是否为订阅订单
const resp = await request(`/order/${row.id}`, { method: 'GET' });
const { success, data, message: errMsg } = resp as any;
if (!success) throw new Error(errMsg || '获取失败');
const items: any[] = data?.items || [];
const orderMeta: any[] = data?.meta_data || [];
// 订阅识别:检查行项目 meta_data 中的关键键
const keys = [
'is_subscription',
'_wcs_bought_as_subscription',
'subscription_product_type',
'subscription_period',
'subscription_interval',
'_subscription',
'_subscription_period',
'_subscription_interval',
];
let detected = false;
for (const it of items) {
const md = Array.isArray(it?.meta_data) ? it.meta_data : [];
if (md.some((m: any) => keys.includes(String(m?.key)))) {
detected = true;
break;
}
}
setIsSubscription(detected);
setDrawerItems(items);
setDrawerMeta(orderMeta);
setDrawerTitle(`订阅关联(订单号:${row.externalOrderId}`);
setDrawerOpen(true);
} catch (e: any) {
message.error(e?.message || '获取失败');
}
}}
>
</Button>
),
},
{
title: '时间范围',
dataIndex: 'dateRange',
valueType: 'dateRange',
hideInTable: true,
},
{
title: '商品关键字',
dataIndex: 'keyword',
hideInTable: true,
},
];
const request: ProTableProps<OrderItemRow>['request'] = async (params) => {
try {
const { current = 1, pageSize = 10, siteId, keyword, customer_email, payment_method } = params as any;
const [startDate, endDate] = (params as any).dateRange || [];
const resp = await ordercontrollerGetorders({
current,
pageSize,
siteId,
keyword,
customer_email,
payment_method,
isSubscriptionOnly: true as any,
startDate: startDate ? (dayjs(startDate).toISOString() as any) : undefined,
endDate: endDate ? (dayjs(endDate).toISOString() as any) : undefined,
} as any);
const { success, data, message: errMsg } = resp as any;
if (!success) throw new Error(errMsg || '获取失败');
return {
data: (data?.items ?? []) as OrderItemRow[],
total: data?.total ?? 0,
success: true,
};
} catch (e: any) {
message.error(e?.message || '获取失败');
return { data: [], total: 0, success: false };
}
};
return (
<PageContainer title='订阅订单'>
<ProTable<OrderItemRow>
actionRef={actionRef}
rowKey='id'
columns={columns}
request={request}
pagination={{ showSizeChanger: true }}
search={{
labelWidth: 90,
span: 6,
}}
toolBarRender={false}
/>
{/* 订阅关联抽屉:展示行项目与订单元数据,标注是否订阅 */}
<Drawer
open={drawerOpen}
title={drawerTitle}
width={720}
onClose={() => setDrawerOpen(false)}
>
<div style={{ marginBottom: 12 }}>
<Tag color={isSubscription ? 'green' : 'default'}>
{isSubscription ? '订阅订单' : '非订阅订单'}
</Tag>
</div>
{/* 行项目列表,展示 meta_data 关键键值 */}
<List
header={<div></div>}
dataSource={drawerItems}
renderItem={(item: any) => (
<List.Item>
<List.Item.Meta
title={`${item?.name || '-'}(数量:${item?.quantity || 0}`}
description={`SKU${item?.sku || '-'}产品ID${item?.externalProductId || '-'}变体ID${item?.externalVariationId || '-'}`}
/>
<div style={{ maxWidth: 420 }}>
{(Array.isArray(item?.meta_data) ? item.meta_data : []).map((m: any) => (
<Tag key={`${m?.key}-${m?.id}`}>{`${m?.key}: ${m?.value}`}</Tag>
))}
</div>
</List.Item>
)}
/>
{/* 订单级元数据 */}
<List
style={{ marginTop: 16 }}
header={<div></div>}
dataSource={drawerMeta}
renderItem={(m: any) => (
<List.Item>
<Tag>{`${m?.key}: ${m?.value}`}</Tag>
</List.Item>
)}
/>
</Drawer>
</PageContainer>
);
};
export default OrdersPage;