feat(订单): 添加关联订单显示功能并创建订单商品和订阅订单页面
feat(订阅): 增加订阅订单关联功能及详情查看 feat(商品): 新增订单商品概览页面 style(订单): 优化订单详情显示格式
This commit is contained in:
parent
737cfef733
commit
a1e8e81407
|
|
@ -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;
|
||||
|
|
@ -893,7 +893,23 @@ const Detail: React.FC<{
|
|||
<ul>
|
||||
{record?.items?.map((item: any) => (
|
||||
<li key={item.id}>
|
||||
{item.name}:{item.quantity}
|
||||
{item.name}:{item.quantity}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{/* 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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useRef } from 'react';
|
||||
import React, { useRef, useState } from 'react';
|
||||
import {
|
||||
ActionType,
|
||||
DrawerForm,
|
||||
|
|
@ -7,7 +7,9 @@ import {
|
|||
ProFormSelect,
|
||||
ProTable,
|
||||
} 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 {
|
||||
subscriptioncontrollerList,
|
||||
subscriptioncontrollerSync,
|
||||
|
|
@ -32,6 +34,12 @@ const SUBSCRIPTION_STATUS_ENUM: Record<string, { text: string }> = {
|
|||
const ListPage: React.FC = () => {
|
||||
// 表格操作引用:用于在同步后触发表格刷新
|
||||
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>[] = [
|
||||
|
|
@ -55,6 +63,13 @@ const ListPage: React.FC = () => {
|
|||
hideInSearch: true,
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '父订单号',
|
||||
dataIndex: 'parent_id',
|
||||
hideInSearch: true,
|
||||
width: 120,
|
||||
render: (_, row) => (row?.parent_id ? <Tag>{row.parent_id}</Tag> : '-'),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
|
|
@ -114,6 +129,69 @@ const ListPage: React.FC = () => {
|
|||
hideInSearch: true,
|
||||
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 (
|
||||
|
|
@ -138,6 +216,34 @@ const ListPage: React.FC = () => {
|
|||
// 工具栏:订阅同步入口
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
Loading…
Reference in New Issue