forked from yoone/WEB
449 lines
14 KiB
TypeScript
449 lines
14 KiB
TypeScript
import { ORDER_STATUS_ENUM } from '@/constants';
|
|
import { HistoryOrder } from '@/pages/Statistics/Order';
|
|
import styles from '@/style/order-list.css';
|
|
import { DeleteFilled, EllipsisOutlined } from '@ant-design/icons';
|
|
import {
|
|
ActionType,
|
|
ModalForm,
|
|
PageContainer,
|
|
ProColumns,
|
|
ProFormTextArea,
|
|
ProTable,
|
|
} from '@ant-design/pro-components';
|
|
import { request, useParams } from '@umijs/max';
|
|
import { App, Button, Dropdown, Popconfirm, Tabs, TabsProps } from 'antd';
|
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
import {
|
|
BatchEditOrders,
|
|
CreateOrder,
|
|
EditOrder,
|
|
OrderNote,
|
|
} from '../components/Order/Forms';
|
|
|
|
const OrdersPage: React.FC = () => {
|
|
const actionRef = useRef<ActionType>();
|
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
|
const [activeKey, setActiveKey] = useState<string>('all');
|
|
const [count, setCount] = useState<any[]>([]);
|
|
const [activeLine, setActiveLine] = useState<number>(-1);
|
|
const { siteId } = useParams<{ siteId: string }>();
|
|
const { message } = App.useApp();
|
|
|
|
useEffect(() => {
|
|
actionRef.current?.reload();
|
|
}, [siteId]);
|
|
|
|
const tabs: TabsProps['items'] = useMemo(() => {
|
|
// 统计全部数量,依赖状态统计数组
|
|
const total = count.reduce((acc, cur) => acc + Number(cur.count), 0);
|
|
const tabs = [
|
|
{ key: 'pending', label: '待确认' },
|
|
{ key: 'processing', label: '待发货' },
|
|
{ key: 'completed', label: '已完成' },
|
|
{ key: 'cancelled', label: '已取消' },
|
|
{ key: 'refunded', label: '已退款' },
|
|
{ key: 'failed', label: '失败' },
|
|
{ key: 'after_sale_pending', label: '售后处理中' },
|
|
{ key: 'pending_reshipment', label: '待补发' },
|
|
// 退款相关状态
|
|
{ key: 'refund_requested', label: '已申请退款' },
|
|
{ key: 'refund_approved', label: '退款申请已通过' },
|
|
{ key: 'refund_cancelled', label: '已取消退款' },
|
|
].map((v) => {
|
|
// 根据状态键匹配统计数量
|
|
const number = count.find((el) => el.status === v.key)?.count || '0';
|
|
return {
|
|
label: `${v.label}(${number})`,
|
|
key: v.key,
|
|
};
|
|
});
|
|
|
|
return [{ key: 'all', label: `全部(${total})` }, ...tabs];
|
|
}, [count]);
|
|
|
|
const columns: ProColumns<API.Order>[] = [
|
|
{
|
|
title: '订单号',
|
|
dataIndex: 'id',
|
|
hideInSearch: true,
|
|
},
|
|
{
|
|
title: '状态',
|
|
dataIndex: 'status',
|
|
valueType: 'select',
|
|
valueEnum: ORDER_STATUS_ENUM,
|
|
},
|
|
{
|
|
title: '订单日期',
|
|
dataIndex: 'date_created',
|
|
hideInSearch: true,
|
|
valueType: 'dateTime',
|
|
},
|
|
{
|
|
title: '金额',
|
|
dataIndex: 'total',
|
|
hideInSearch: true,
|
|
},
|
|
{
|
|
title: '币种',
|
|
dataIndex: 'currency',
|
|
hideInSearch: true,
|
|
},
|
|
{
|
|
title: '客户邮箱',
|
|
dataIndex: 'email',
|
|
},
|
|
{
|
|
title: '客户姓名',
|
|
dataIndex: 'customer_name',
|
|
hideInSearch: true,
|
|
},
|
|
{
|
|
title: '商品',
|
|
dataIndex: 'line_items',
|
|
hideInSearch: true,
|
|
width: 200,
|
|
ellipsis: true,
|
|
render: (_, record) => {
|
|
// 检查 record.line_items 是否是数组并且有内容
|
|
if (Array.isArray(record.line_items) && record.line_items.length > 0) {
|
|
// 遍历 line_items 数组, 显示每个商品的名称和数量
|
|
return (
|
|
<div>
|
|
{record.line_items.map((item: any) => (
|
|
<div key={item.id}>{`${item.name} x ${item.quantity}`}</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
// 如果 line_items 不存在或不是数组, 则显示占位符
|
|
return '-';
|
|
},
|
|
},
|
|
{
|
|
title: '支付方式',
|
|
dataIndex: 'payment_method',
|
|
},
|
|
{
|
|
title: '联系电话',
|
|
hideInSearch: true,
|
|
render: (_, record) => record.shipping?.phone || record.billing?.phone,
|
|
},
|
|
{
|
|
title: '账单地址',
|
|
dataIndex: 'billing_full_address',
|
|
hideInSearch: true,
|
|
width: 200,
|
|
ellipsis: true,
|
|
copyable: true,
|
|
},
|
|
{
|
|
title: '收货地址',
|
|
dataIndex: 'shipping_full_address',
|
|
hideInSearch: true,
|
|
width: 200,
|
|
ellipsis: true,
|
|
copyable: true,
|
|
},
|
|
{
|
|
title: '操作',
|
|
dataIndex: 'option',
|
|
valueType: 'option',
|
|
fixed: 'right',
|
|
width: '200',
|
|
render: (_, record) => {
|
|
return (
|
|
<>
|
|
<EditOrder
|
|
key={record.id}
|
|
record={record}
|
|
tableRef={actionRef}
|
|
orderId={record.id as number}
|
|
setActiveLine={setActiveLine}
|
|
siteId={siteId}
|
|
/>
|
|
<Dropdown
|
|
menu={{
|
|
items: [
|
|
// Sync button removed
|
|
{
|
|
key: 'history',
|
|
label: (
|
|
<HistoryOrder
|
|
email={(record as any).email}
|
|
tableRef={actionRef}
|
|
/>
|
|
),
|
|
},
|
|
{
|
|
key: 'note',
|
|
label: (
|
|
<OrderNote id={record.id as number} siteId={siteId} />
|
|
),
|
|
},
|
|
],
|
|
}}
|
|
>
|
|
<Button type="text" icon={<EllipsisOutlined />} />
|
|
</Dropdown>
|
|
<Popconfirm
|
|
title="确定删除订单?"
|
|
onConfirm={async () => {
|
|
try {
|
|
const res = await request(
|
|
`/site-api/${siteId}/orders/${record.id}`,
|
|
{ method: 'DELETE' },
|
|
);
|
|
if (res.success) {
|
|
message.success('删除成功');
|
|
actionRef.current?.reload();
|
|
} else {
|
|
message.error(res.message || '删除失败');
|
|
}
|
|
} catch (e) {
|
|
message.error('删除失败');
|
|
}
|
|
}}
|
|
>
|
|
<Button type="link" danger title="删除" icon={<DeleteFilled />} />
|
|
</Popconfirm>
|
|
</>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
return (
|
|
<PageContainer ghost header={{ title: null, breadcrumb: undefined }}>
|
|
<Tabs items={tabs} activeKey={activeKey} onChange={setActiveKey} />
|
|
<ProTable
|
|
columns={columns}
|
|
params={{ status: activeKey }}
|
|
headerTitle="查询表格"
|
|
scroll={{ x: 'max-content' }}
|
|
actionRef={actionRef}
|
|
rowKey="id"
|
|
rowSelection={{ selectedRowKeys, onChange: setSelectedRowKeys }}
|
|
rowClassName={(record) => {
|
|
return record.id === activeLine
|
|
? styles['selected-line-order-protable']
|
|
: '';
|
|
}}
|
|
pagination={{
|
|
pageSizeOptions: ['10', '20', '50', '100', '1000'],
|
|
showSizeChanger: true,
|
|
defaultPageSize: 10,
|
|
}}
|
|
toolBarRender={() => [
|
|
<CreateOrder tableRef={actionRef} siteId={siteId} />,
|
|
<BatchEditOrders
|
|
tableRef={actionRef}
|
|
selectedRowKeys={selectedRowKeys}
|
|
setSelectedRowKeys={setSelectedRowKeys}
|
|
siteId={siteId}
|
|
/>,
|
|
<Button
|
|
title="批量删除"
|
|
danger
|
|
icon={<DeleteFilled />}
|
|
disabled={!selectedRowKeys.length}
|
|
onClick={async () => {
|
|
if (!siteId) return;
|
|
const res = await request(`/site-api/${siteId}/orders/batch`, {
|
|
method: 'POST',
|
|
data: { delete: selectedRowKeys },
|
|
});
|
|
setSelectedRowKeys([]);
|
|
actionRef.current?.reload();
|
|
if (res.success) {
|
|
message.success('批量删除成功');
|
|
} else {
|
|
message.warning(res.message || '部分删除失败');
|
|
}
|
|
}}
|
|
/>,
|
|
<Button
|
|
onClick={async () => {
|
|
if (!siteId) return;
|
|
const idsParam = selectedRowKeys.length
|
|
? (selectedRowKeys as any[]).join(',')
|
|
: undefined;
|
|
const res = await request(`/site-api/${siteId}/orders/export`, {
|
|
params: { ids: idsParam },
|
|
});
|
|
if (res?.success && res?.data?.csv) {
|
|
const blob = new Blob([res.data.csv], {
|
|
type: 'text/csv;charset=utf-8;',
|
|
});
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = 'orders.csv';
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
} else {
|
|
message.error(res.message || '导出失败');
|
|
}
|
|
}}
|
|
>
|
|
批量导出
|
|
</Button>,
|
|
<ModalForm
|
|
title="批量导入订单"
|
|
trigger={
|
|
<Button type="primary" ghost>
|
|
批量导入
|
|
</Button>
|
|
}
|
|
width={600}
|
|
modalProps={{ destroyOnHidden: true }}
|
|
onFinish={async (values) => {
|
|
if (!siteId) return false;
|
|
const csv = values.csv || '';
|
|
const items = values.items || [];
|
|
const res = await request(`/site-api/${siteId}/orders/import`, {
|
|
method: 'POST',
|
|
data: { csv, items },
|
|
});
|
|
if (res.success) {
|
|
message.success('导入完成');
|
|
actionRef.current?.reload();
|
|
return true;
|
|
}
|
|
message.error(res.message || '导入失败');
|
|
return false;
|
|
}}
|
|
>
|
|
<ProFormTextArea
|
|
name="csv"
|
|
label="CSV文本"
|
|
placeholder="粘贴CSV,首行为表头"
|
|
/>
|
|
</ModalForm>,
|
|
]}
|
|
request={async (params, sort, filter) => {
|
|
const p: any = params || {};
|
|
const current = p.current;
|
|
const pageSize = p.pageSize;
|
|
const date = p.date;
|
|
const status = p.status;
|
|
const {
|
|
current: _c,
|
|
pageSize: _ps,
|
|
date: _d,
|
|
status: _s,
|
|
...rest
|
|
} = p;
|
|
const where: Record<string, any> = { ...(filter || {}), ...rest };
|
|
if (status && status !== 'all') {
|
|
where.status = status;
|
|
}
|
|
if (date) {
|
|
const [startDate, endDate] = date;
|
|
// 将日期范围转为后端筛选参数
|
|
where.startDate = `${startDate} 00:00:00`;
|
|
where.endDate = `${endDate} 23:59:59`;
|
|
}
|
|
let orderObj: Record<string, 'asc' | 'desc'> | undefined = undefined;
|
|
if (sort && typeof sort === 'object') {
|
|
const [field, dir] = Object.entries(sort)[0] || [];
|
|
if (field && dir) {
|
|
orderObj = { [field]: dir === 'descend' ? 'desc' : 'asc' };
|
|
}
|
|
}
|
|
const response = await request(`/site-api/${siteId}/orders`, {
|
|
params: {
|
|
page: current,
|
|
page_size: pageSize,
|
|
where,
|
|
...(orderObj ? { order: orderObj } : {}),
|
|
},
|
|
});
|
|
|
|
if (!response.success) {
|
|
message.error(response.message || '获取订单列表失败');
|
|
return {
|
|
data: [],
|
|
success: false,
|
|
};
|
|
}
|
|
|
|
const { data } = response;
|
|
// 计算顶部状态数量,通过按状态并发查询站点接口
|
|
if (siteId) {
|
|
try {
|
|
// 定义需要统计的状态键集合
|
|
const statusKeys: string[] = [
|
|
'pending',
|
|
'processing',
|
|
'completed',
|
|
'cancelled',
|
|
'refunded',
|
|
'failed',
|
|
// 站点接口不支持的扩展状态,默认统计为0
|
|
'after_sale_pending',
|
|
'pending_reshipment',
|
|
'refund_requested',
|
|
'refund_approved',
|
|
'refund_cancelled',
|
|
];
|
|
// 构造基础筛选参数,移除当前状态避免重复过滤
|
|
const { status: _status, ...baseWhere } = where;
|
|
// 并发请求各状态的总数,对站点接口不支持的状态使用0
|
|
const results = await Promise.all(
|
|
statusKeys.map(async (key) => {
|
|
// 将前端退款状态映射为站点接口可能识别的原始状态
|
|
const mapToRawStatus: Record<string, string> = {
|
|
refund_requested: 'return-requested',
|
|
refund_approved: 'return-approved',
|
|
refund_cancelled: 'return-cancelled',
|
|
};
|
|
const rawStatus = mapToRawStatus[key] || key;
|
|
// 对扩展状态直接返回0,减少不必要的请求
|
|
const unsupported = [
|
|
'after_sale_pending',
|
|
'pending_reshipment',
|
|
];
|
|
if (unsupported.includes(key)) {
|
|
return { status: key, count: 0 };
|
|
}
|
|
try {
|
|
const res = await request(`/site-api/${siteId}/orders`, {
|
|
params: {
|
|
page: 1,
|
|
per_page: 1,
|
|
where: { ...baseWhere, status: rawStatus },
|
|
},
|
|
});
|
|
const totalCount = Number(res?.data?.total || 0);
|
|
return { status: key, count: totalCount };
|
|
} catch (err) {
|
|
// 请求失败时该状态数量记为0
|
|
return { status: key, count: 0 };
|
|
}
|
|
}),
|
|
);
|
|
setCount(results);
|
|
} catch (e) {
|
|
// 统计失败时不影响列表展示
|
|
}
|
|
}
|
|
|
|
if (data) {
|
|
return {
|
|
total: data?.total || 0,
|
|
data: data?.items || [],
|
|
success: true,
|
|
};
|
|
}
|
|
return {
|
|
data: [],
|
|
success: false,
|
|
};
|
|
}}
|
|
/>
|
|
</PageContainer>
|
|
);
|
|
};
|
|
|
|
export default OrdersPage;
|