WEB/src/pages/Order/List/index.tsx

2654 lines
84 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import styles from '../../../style/order-list.css';
import InternationalPhoneInput from '@/components/InternationalPhoneInput';
import SyncForm from '@/components/SyncForm';
import { showSyncResult, SyncResultData } from '@/components/SyncResultMessage';
import { UploadOutlined } from '@ant-design/icons';
import { ORDER_STATUS_ENUM } from '@/constants';
import { HistoryOrder } from '@/pages/Statistics/Order';
import {
logisticscontrollerCreateshipment,
logisticscontrollerDelshipment,
logisticscontrollerGetshipmentfee,
logisticscontrollerGetshippingaddresslist,
} from '@/servers/api/logistics';
import {
ordercontrollerCancelorder,
ordercontrollerChangestatus,
ordercontrollerCompletedorder,
ordercontrollerCreatenote,
ordercontrollerCreateorder,
ordercontrollerGetorderbynumber,
ordercontrollerGetorderdetail,
ordercontrollerGetorders,
ordercontrollerRefundorder,
ordercontrollerSyncorderbyid,
ordercontrollerSyncorders,
ordercontrollerUpdateorderitems,
ordercontrollerImportwintopay,
} from '@/servers/api/order';
import { productcontrollerSearchproducts } from '@/servers/api/product';
import { sitecontrollerAll } from '@/servers/api/site';
import { stockcontrollerGetallstockpoints } from '@/servers/api/stock';
import { formatShipmentState, formatSource } from '@/utils/format';
import {
CodeSandboxOutlined,
CopyOutlined,
DeleteFilled,
DownOutlined,
FileDoneOutlined,
TagsOutlined,
} from '@ant-design/icons';
import {
ActionType,
ModalForm,
PageContainer,
ProColumns,
ProDescriptions,
ProForm,
ProFormDatePicker,
ProFormDependency,
ProFormDigit,
ProFormInstance,
ProFormItem,
ProFormList,
ProFormRadio,
ProFormSelect,
ProFormText,
ProFormTextArea,
ProTable,
} from '@ant-design/pro-components';
import { request } from '@umijs/max';
import {
App,
Button,
Card,
Col,
Divider,
Drawer,
Dropdown,
Empty,
message,
Popconfirm,
Radio,
Row,
Space,
Tabs,
TabsProps,
Tag,
Upload,
} from 'antd';
import React, { useMemo, useRef, useState } from 'react';
import RelatedOrders from '../../Subscription/Orders/RelatedOrders';
import dayjs from 'dayjs';
import * as XLSX from 'xlsx';
const ListPage: React.FC = () => {
const [file, setFile] = useState<File | null>(null);
const [csvData, setCsvData] = useState<any[]>([]);
const [processedData, setProcessedData] = useState<any[]>([]);
const actionRef = useRef<ActionType>();
const [activeKey, setActiveKey] = useState<string>('all');
const [count, setCount] = useState<any[]>([]);
const [activeLine, setActiveLine] = useState<number>(-1);
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: '已退款',
// label: '退款申请已通过',
},
{
key: 'refund_cancelled',
label: '已完成',
// label: '已取消退款',
},
// {
// key: 'pending_refund',
// 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 { message } = App.useApp();
const columns: ProColumns<API.Order>[] = [
{
title: 'ID',
dataIndex: 'id',
hideInSearch: true,
},
{
title: '日期',
dataIndex: 'date',
hideInTable: true,
valueType: 'dateRange',
},
{
title: '订阅',
dataIndex: 'isSubscription',
hideInSearch: true,
render: (_, record) => {
const related = Array.isArray((record as any)?.related)
? (record as any).related
: [];
const isSub = related.some(
(it: any) =>
it?.externalSubscriptionId || it?.billing_period || it?.line_items,
);
return (
<Tag color={isSub ? 'green' : 'default'}>{isSub ? '是' : '否'}</Tag>
);
},
},
{
title: '站点',
dataIndex: 'siteId',
valueType: 'select',
request: async () => {
try {
const result = await sitecontrollerAll();
const { success, data } = result;
if (success && data) {
return data.map((site: any) => ({
label: site.name,
value: site.id,
}));
}
return [];
} catch (error) {
console.error('获取站点列表失败:', error);
return [];
}
},
},
{
title: '订单包含',
dataIndex: 'keyword',
hideInTable: true,
},
{
title: '订单ID',
dataIndex: 'externalOrderId',
},
{
title: '订单创建日期',
dataIndex: 'date_created',
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '付款日期',
dataIndex: 'date_paid',
hideInSearch: true,
valueType: 'dateTime',
},
{
title: '金额',
dataIndex: 'total',
hideInSearch: true,
},
{
title: '支付方式',
dataIndex: 'payment_method',
},
{
title: '总订单数',
dataIndex: 'order_count',
hideInSearch: true,
},
{
title: '总订单金额',
dataIndex: 'total_spent',
hideInSearch: true,
},
{
title: '客户邮箱',
dataIndex: 'customer_email',
},
{
title: '联系电话',
dataIndex: 'billing_phone',
render: (_, record) => record.shipping?.phone || record.billing?.phone,
},
{
title: '换货次数',
dataIndex: 'exchange_frequency',
hideInSearch: true,
},
{
title: '州',
hideInSearch: true,
render: (_, record) => record.shipping?.state || record.billing?.state,
},
{
title: '状态',
dataIndex: 'orderStatus',
hideInSearch: true,
valueType: 'select',
valueEnum: ORDER_STATUS_ENUM,
},
{
title: '物流',
dataIndex: 'fulfillments',
hideInSearch: true,
render: (_, record) => {
return (
<div>
{(record as any)?.fulfillments?.map((item: any) => {
if (!item) return;
return (
<div
style={{
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
}}
>
<span>: {item.shipping_provider}</span>
<span>: {item.tracking_number}</span>
</div>
);
})}
</div>
);
},
},
{
title: 'IP',
dataIndex: 'customer_ip_address',
},
{
title: '设备',
dataIndex: 'device_type',
hideInSearch: true,
},
{
title: '来源',
hideInSearch: true,
render: (_, record) =>
formatSource(record.source_type, record.utm_source),
},
{
title: '客户备注',
dataIndex: 'customer_note',
hideInSearch: true,
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
fixed: 'right',
width: '200',
render: (_, record) => {
return (
<>
{['processing', 'pending_reshipment'].includes(
record.orderStatus,
) ? (
<>
<Shipping
id={record.id as number}
tableRef={actionRef}
setActiveLine={setActiveLine}
/>
<Divider type="vertical" />
</>
) : (
<></>
)}
<Detail
key={record.id}
record={record}
tableRef={actionRef}
orderId={record.id as number}
setActiveLine={setActiveLine}
/>
<Divider type="vertical" />
<Dropdown
menu={{
items: [
{
key: 'sync',
label: (
<Button
type="primary"
onClick={async () => {
try {
if (!record.siteId || !record.externalOrderId) {
message.error('站点ID或外部订单ID不存在');
return;
}
const {
success,
message: errMsg,
data,
} = await ordercontrollerSyncorderbyid({
siteId: record.siteId,
orderId: record.externalOrderId,
});
if (!success) {
throw new Error(errMsg);
}
showSyncResult(data as SyncResultData, '订单');
actionRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
}
}}
>
</Button>
),
style: {
display: [
'after_sale_pending',
'pending_reshipment',
].includes(record.orderStatus)
? 'none'
: 'block',
},
},
{
key: 'history',
label: (
<HistoryOrder
email={record.customer_email}
tableRef={actionRef}
/>
),
},
{
key: 'note',
label: <OrderNote id={record.id as number} />,
},
{
key: 'cancel',
label: (
<Popconfirm
title="转至售后"
description="确认转至售后?"
onConfirm={async () => {
try {
if (!record.id) {
message.error('订单ID不存在');
return;
}
const { success, message: errMsg } =
await ordercontrollerChangestatus(
{
id: record.id,
},
{
status: 'after_sale_pending',
},
);
if (!success) {
throw new Error(errMsg);
}
actionRef.current?.reload();
} catch (error: any) {
message.error(error.message);
}
}}
>
<Button type="primary" ghost>
</Button>
</Popconfirm>
),
style: {
display: [
'processing',
'pending_reshipment',
'completed',
'pending_refund',
].includes(record.orderStatus)
? 'block'
: 'none',
},
},
],
}}
>
<a onClick={(e) => e.preventDefault()}>
<Space>
<DownOutlined />
</Space>
</a>
</Dropdown>
</>
);
},
},
];
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
/**
* @description 处理文件上传
*/
const handleFileUpload = (uploadedFile: File) => {
// 检查文件类型
if (!uploadedFile.name.match(/\.(xlsx)$/)) {
message.error('请上传 xlsx 格式的文件!');
return false;
}
setFile(uploadedFile);
const reader = new FileReader();
// 对于Excel文件继续使用readAsArrayBuffer
reader.onload = (e) => {
try {
const data = e.target?.result;
// 如果是ArrayBuffer使用type: 'array'来处理
const workbook = XLSX.read(data, { type: 'array' });
const sheetName = workbook.SheetNames[0];
const worksheet = workbook.Sheets[sheetName];
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
if (jsonData.length < 2) {
message.error('文件为空或缺少表头!');
setCsvData([]);
return;
}
// 将数组转换为对象数组
const headers = jsonData[0] as string[];
const rows = jsonData.slice(1).map((rowArray: any) => {
const rowData: { [key: string]: any } = {};
headers.forEach((header, index) => {
rowData[header] = rowArray[index];
});
return rowData;
});
message.success(`成功解析 ${rows.length} 条数据.`);
setCsvData(rows);
setProcessedData([]); // 清空旧的处理结果
} catch (error) {
message.error('Excel文件解析失败,请检查文件格式!');
console.error('Excel Parse Error:', error);
setCsvData([]);
}
};
reader.readAsArrayBuffer(uploadedFile);
reader.onerror = (error) => {
message.error('文件读取失败!');
console.error('File Read Error:', error);
};
return false; // 阻止antd Upload组件的默认上传行为
};
return (
<PageContainer ghost>
<Tabs items={tabs} activeKey={activeKey} onChange={setActiveKey} />
<ProTable
params={{ status: activeKey }}
headerTitle="查询表格"
scroll={{ x: 'max-content' }}
actionRef={actionRef}
rowKey="id"
columns={columns}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys),
}}
rowClassName={(record) => {
return record.id === activeLine
? styles['selected-line-order-protable']
: '';
}}
pagination={{
pageSizeOptions: ['10', '20', '50', '100', '1000'],
showSizeChanger: true,
showQuickJumper: true,
defaultPageSize: 10,
}}
toolBarRender={() => [
// <CreateOrder tableRef={actionRef} />,
<Upload
// beforeUpload={handleFileUpload}
name="file"
accept=".xlsx"
showUploadList={false}
maxCount={1}
customRequest={async (options) => {
const { file, onSuccess, onError } = options;
console.log(file);
const formData = new FormData();
formData.append('file', file);
try {
const res = await request('/order/import', {
method: 'POST',
data: formData,
requestType: 'form',
});
if (res?.success && res.data) {
// 使用xlsx将JSON数据转换为Excel
const XLSX = require('xlsx');
const worksheet = XLSX.utils.json_to_sheet(res.data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Orders');
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' });
// 否则按原逻辑处理二进制数据
const blob = new Blob([excelBuffer], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'orders.xlsx';
a.click();
URL.revokeObjectURL(url);
} else {
message.error(res.message || '导出失败');
}
actionRef.current?.reload();
setSelectedRowKeys([]);
} catch (error: any) {
message.error('导入失败: ' + (error.message || '未知错误'));
onError?.(error);
}
}}
>
<Button icon={<UploadOutlined />}></Button>
</Upload>,
<SyncForm
onFinish={async (values: any) => {
try {
console.log('values',values);
const {
success,
message: errMsg,
data,
} = await ordercontrollerSyncorders(values, {
after: values.dateRange?.[0],
before: values.dateRange?.[1],
});
if (!success) {
throw new Error(errMsg);
}
// 使用 showSyncResult 函数显示详细的同步结果
showSyncResult(data as SyncResultData, '订单');
actionRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
}
}}
tableRef={actionRef}
/>,
<Popconfirm
title="批量导出"
description="确认导出选中的订单吗?"
onConfirm={async () => {
console.log(selectedRowKeys);
try {
const res = await request('/order/export', {
method: 'POST',
data: {
ids: selectedRowKeys,
},
});
if (res?.success && res.data) {
const blob = new Blob([res.data], {
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 || '导出失败');
}
actionRef.current?.reload();
setSelectedRowKeys([]);
} catch (error: any) {
message.error(error?.message || '导出失败');
}
}}
>
<Button type="primary" ghost>
</Button>
</Popconfirm>,
]}
request={async ({ date, ...param }: any) => {
if (param.status === 'all') {
delete param.status;
}
if (date) {
const [startDate, endDate] = date;
param.startDate = `${startDate} 00:00:00`;
param.endDate = `${endDate} 23:59:59`;
}
const { data, success } = await ordercontrollerGetorders(param);
if (success) {
setCount(data?.count || []);
return {
total: data?.total || 0,
data: data?.items || [],
};
}
return {
data: [],
};
}}
columns={columns}
/>
</PageContainer>
);
};
const Detail: React.FC<{
tableRef: React.MutableRefObject<ActionType | undefined>;
orderId: number;
record: API.Order;
setActiveLine: Function;
}> = ({ tableRef, orderId, record, setActiveLine }) => {
const [visiable, setVisiable] = useState(false);
const { message } = App.useApp();
const ref = useRef<ActionType>();
const initRequest = async () => {
const { data, success }: API.OrderDetailRes =
await ordercontrollerGetorderdetail({
orderId,
});
if (!success || !data) return { data: {} };
// 合并订单中相同的sku,只显示一次记录总数
data.sales = data.sales?.reduce(
(acc: API.OrderSale[], cur: API.OrderSale) => {
let 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,
};
};
return (
<>
<Button
key="detail"
type="primary"
onClick={() => {
setVisiable(true);
setActiveLine(record.id);
}}
>
<FileDoneOutlined />
</Button>
<Drawer
title="订单详情"
open={visiable}
destroyOnHidden
size="large"
onClose={() => setVisiable(false)}
footer={[
<OrderNote id={orderId} descRef={ref} />,
...(['after_sale_pending', 'pending_reshipment'].includes(
record.orderStatus,
)
? []
: [
<Divider type="vertical" />,
<Button
type="primary"
onClick={async () => {
try {
if (!record.siteId || !record.externalOrderId) {
message.error('站点ID或外部订单ID不存在');
return;
}
const {
success,
message: errMsg,
data,
} = await ordercontrollerSyncorderbyid({
siteId: record.siteId,
orderId: record.externalOrderId,
});
if (!success) {
throw new Error(errMsg);
}
showSyncResult(data as SyncResultData, '订单');
tableRef.current?.reload();
} catch (error: any) {
message.error(error?.message || '同步失败');
}
}}
>
</Button>,
]),
// ...(['processing', 'pending_reshipment'].includes(record.orderStatus)
// ? [
// <Divider type="vertical" />,
// <Shipping
// id={record.id as number}
// tableRef={tableRef}
// descRef={ref}
// reShipping={true}
// />,
// ]
// : []),
...([
'processing',
'pending_reshipment',
'completed',
'pending_refund',
].includes(record.orderStatus)
? [
<Divider type="vertical" />,
<Popconfirm
title="转至售后"
description="确认转至售后?"
onConfirm={async () => {
try {
if (!record.id) {
message.error('订单ID不存在');
return;
}
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 type="vertical" />,
<Popconfirm
title="转至取消"
description="确认转至取消?"
onConfirm={async () => {
try {
if (!record.id) {
message.error('订单ID不存在');
return;
}
const { success, message: errMsg } =
await ordercontrollerCancelorder({
id: record.id,
});
if (!success) {
throw new Error(errMsg);
}
tableRef.current?.reload();
} catch (error: any) {
message.error(error.message);
}
}}
>
<Button type="primary" ghost>
</Button>
</Popconfirm>,
<Divider type="vertical" />,
<Popconfirm
title="转至退款"
description="确认转至退款?"
onConfirm={async () => {
try {
if (!record.id) {
message.error('订单ID不存在');
return;
}
const { success, message: errMsg } =
await ordercontrollerRefundorder({
id: record.id,
});
if (!success) {
throw new Error(errMsg);
}
tableRef.current?.reload();
} catch (error: any) {
message.error(error.message);
}
}}
>
<Button type="primary" ghost>
退
</Button>
</Popconfirm>,
<Divider type="vertical" />,
<Popconfirm
title="转至完成"
description="确认转至完成?"
onConfirm={async () => {
try {
if (!record.id) {
message.error('订单ID不存在');
return;
}
const { success, message: errMsg } =
await ordercontrollerCompletedorder({
id: record.id,
});
if (!success) {
throw new Error(errMsg);
}
tableRef.current?.reload();
} catch (error: any) {
message.error(error.message);
}
}}
>
<Button type="primary" ghost>
</Button>
</Popconfirm>,
<Divider type="vertical" />,
<Popconfirm
title="转至待补发"
description="确认转至待补发?"
onConfirm={async () => {
try {
const { success, message: errMsg } =
await ordercontrollerChangestatus(
{
id: record.id,
},
{
status: 'pending_reshipment',
},
);
if (!success) {
throw new Error(errMsg);
}
tableRef.current?.reload();
} catch (error: any) {
message.error(error.message);
}
}}
>
<Button type="primary" ghost>
</Button>
</Popconfirm>,
]
: []),
]}
>
<ProDescriptions
labelStyle={{ width: '100px' }}
actionRef={ref}
request={initRequest}
>
<ProDescriptions.Item
label="站点"
dataIndex="siteId"
valueType="select"
request={async () => {
const { data = [] } = await sitecontrollerAll();
return data.map((item) => ({
label: item.name,
value: item.id,
}));
}}
/>
<ProDescriptions.Item
label="订单日期"
dataIndex="date_created"
valueType="dateTime"
/>
<ProDescriptions.Item
label="订单状态"
dataIndex="orderStatus"
valueType="select"
valueEnum={ORDER_STATUS_ENUM}
/>
<ProDescriptions.Item label="金额" dataIndex="total" />
<ProDescriptions.Item label="客户邮箱" dataIndex="customer_email" />
<ProDescriptions.Item
label="联系电话"
span={3}
render={(_, record) => {
return (
<div>
<span>
{record?.shipping?.phone || record?.billing?.phone || '-'}
</span>
</div>
);
}}
/>
<ProDescriptions.Item label="交易Id" dataIndex="transaction_id" />
<ProDescriptions.Item label="IP" dataIndex="customer_id_address" />
<ProDescriptions.Item label="设备" dataIndex="device_type" />
<ProDescriptions.Item
label="来源"
render={(_, record) =>
formatSource(record.source_type, record.utm_source)
}
/>
<ProDescriptions.Item
label="原订单状态"
dataIndex="status"
valueType="select"
valueEnum={ORDER_STATUS_ENUM}
/>
<ProDescriptions.Item
label="支付链接"
dataIndex="payment_url"
span={3}
copyable
/>
<ProDescriptions.Item
label="客户备注"
dataIndex="customer_note"
span={3}
/>
<ProDescriptions.Item
label="发货信息"
span={3}
render={(_, record) => {
return (
<div>
<div>
company:
<span>
{record?.shipping?.company ||
record?.billing?.company ||
'-'}
</span>
</div>
<div>
first_name:
<span>
{record?.shipping?.first_name ||
record?.billing?.first_name ||
'-'}
</span>
</div>
<div>
last_name:
<span>
{record?.shipping?.last_name ||
record?.billing?.last_name ||
'-'}
</span>
</div>
<div>
country:
<span>
{record?.shipping?.country ||
record?.billing?.country ||
'-'}
</span>
</div>
<div>
state:
<span>
{record?.shipping?.state || record?.billing?.state || '-'}
</span>
</div>
<div>
city:
<span>
{record?.shipping?.city || record?.billing?.city || '-'}
</span>
</div>
<div>
postcode:
<span>
{record?.shipping?.postcode ||
record?.billing?.postcode ||
'-'}
</span>
</div>
<div>
phone:
<span>
{record?.shipping?.phone || record?.billing?.phone || '-'}
</span>
</div>
<div>
address_1:
<span>
{record?.shipping?.address_1 ||
record?.billing?.address_1 ||
'-'}
</span>
</div>
</div>
);
}}
/>
{/* 原始订单 */}
<ProDescriptions.Item
label="原始订单"
span={3}
render={(_, record) => {
return (
<ul>
{record?.items?.map((item: any) => (
<li key={item.id}>
{item.name}({item.sku}):{item.quantity}
</li>
))}
</ul>
);
}}
/>
{/* 显示 related order */}
<ProDescriptions.Item
label="关联"
span={3}
render={(_, record) => {
return <RelatedOrders data={record?.related} />;
}}
/>
{/* 订单内容 */}
<ProDescriptions.Item
label="订单内容"
span={3}
render={(_, record) => {
return (
<ul>
{record?.sales?.map((item: any) => (
<li key={item.id}>
{item.name}({item.sku}):{item.quantity}
</li>
))}
</ul>
);
}}
/>
<ProDescriptions.Item
label="换货"
span={3}
render={(_, record) => {
return <SalesChange detailRef={ref} id={record.id as number} />;
}}
/>
<ProDescriptions.Item
label="备注"
span={3}
render={(_, record) => {
if (!record.notes || record.notes.length === 0)
return <Empty description="暂无备注" />;
return (
<div style={{ width: '100%' }}>
{record.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>
);
}}
/>
<ProDescriptions.Item
label="物流信息"
span={3}
render={(_, record) => {
console.log('record', record);
if (!record.shipment || record.shipment.length === 0) {
return <Empty description="暂无物流信息" />;
}
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: '100%',
}}
>
{record.shipment.map((v) => (
<Card
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
title="取消运单"
description="确认取消运单?"
onConfirm={async () => {
try {
const { success, message: errMsg } =
await logisticscontrollerDelshipment({
id: v.id,
});
if (!success) {
throw new Error(errMsg);
}
tableRef.current?.reload();
initRequest();
} catch (error: any) {
message.error(error.message);
}
}}
>
<DeleteFilled />
</Popconfirm>,
]
: []
}
>
<div>: {v?.orderIds?.join(',')}</div>
{v?.items?.map((item) => (
<div>
{item.name}: {item.quantity}
</div>
))}
</Card>
))}
</div>
);
}}
/>
</ProDescriptions>
</Drawer>
</>
);
};
const OrderNote: React.FC<{
id: number;
descRef?: React.MutableRefObject<ActionType | undefined>;
}> = ({ id, descRef }) => {
const { message } = App.useApp();
return (
<ModalForm
title="添加备注"
trigger={
<Button type="primary" ghost>
<TagsOutlined />
</Button>
}
onFinish={async (values: any) => {
try {
const { success, message: errMsg } = await ordercontrollerCreatenote({
...values,
orderId: id,
});
if (!success) {
throw new Error(errMsg);
}
descRef?.current?.reload();
message.success('提交成功');
return true;
} catch (error: any) {
message.error(error.message);
}
}}
>
<ProFormTextArea
name="content"
label="内容"
width="lg"
placeholder="请输入备注"
rules={[{ required: true, message: '请输入备注' }]}
/>
</ModalForm>
);
};
const region = {
AB: 'Alberta',
BC: 'British',
MB: 'Manitoba',
NB: 'New',
NL: 'Newfoundland',
NS: 'Nova',
ON: 'Ontario',
PE: 'Prince',
QC: 'Quebec',
SK: 'Saskatchewan',
NT: 'Northwest',
NU: 'Nunavut',
YT: 'Yukon',
// 美国州名
// AL: 'Alabama',
// AK: 'Alaska',
// AZ: 'Arizona',
// AR: 'Arkansas',
// CA: 'California',
// CO: 'Colorado',
// CT: 'Connecticut',
// DE: 'Delaware',
// FL: 'Florida',
// GA: 'Georgia',
// HI: 'Hawaii',
// ID: 'Idaho',
// IL: 'Illinois',
// IN: 'Indiana',
// IA: 'Iowa',
// KS: 'Kansas',
// KY: 'Kentucky',
// LA: 'Louisiana',
// ME: 'Maine',
// MD: 'Maryland',
// MA: 'Massachusetts',
// MI: 'Michigan',
// MN: 'Minnesota',
// MS: 'Mississippi',
// MO: 'Missouri',
// MT: 'Montana',
// NE: 'Nebraska',
// NV: 'Nevada',
// NH: 'New Hampshire',
// NJ: 'New Jersey',
// NM: 'New Mexico',
// NY: 'New York',
// NC: 'North Carolina',
// ND: 'North Dakota',
// OH: 'Ohio',
// OK: 'Oklahoma',
// OR: 'Oregon',
// PA: 'Pennsylvania',
// RI: 'Rhode Island',
// SC: 'South Carolina',
// SD: 'South Dakota',
// TN: 'Tennessee',
// TX: 'Texas',
// UT: 'Utah',
// VT: 'Vermont',
// VA: 'Virginia',
// WA: 'Washington',
// WV: 'West Virginia',
// WI: 'Wisconsin',
// WY: 'Wyoming',
};
const Shipping: React.FC<{
id: number;
tableRef?: React.MutableRefObject<ActionType | undefined>;
descRef?: React.MutableRefObject<ActionType | undefined>;
reShipping?: boolean;
setActiveLine: Function;
}> = ({ id, tableRef, descRef, reShipping = false, setActiveLine }) => {
const [options, setOptions] = useState<any[]>([]);
const formRef = useRef<ProFormInstance>();
const [shipmentFee, setShipmentFee] = useState<number>(0);
const [rates, setRates] = useState<API.RateDTO[]>([]);
const [ratesLoading, setRatesLoading] = useState(false);
const { message } = App.useApp();
const [shipmentPlatforms, setShipmentPlatforms] = useState([
{ label: 'uniuni', value: 'uniuni' },
{ label: 'tms.freightwaves', value: 'freightwaves' },
]);
return (
<ModalForm
formRef={formRef}
title="创建运单"
size="large"
width="80vw"
modalProps={{
destroyOnHidden: true,
styles: {
body: { maxHeight: '65vh', overflowY: 'auto', overflowX: 'hidden' },
},
}}
trigger={
<Button
type="primary"
onClick={() => {
setActiveLine(id);
}}
>
<CodeSandboxOutlined />
</Button>
}
request={async () => {
const { data, success }: API.OrderDetailRes =
await ordercontrollerGetorderdetail({
orderId: id,
});
console.log('success data',success,data)
if (!success || !data) return {};
data.sales = data.sales?.reduce(
(acc: API.OrderSale[], cur: API.OrderSale) => {
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
if (idx === -1) {
acc.push(cur);
} else {
acc[idx].quantity += cur.quantity;
}
return acc;
},
[],
);
setOptions(
data.sales?.map((item) => ({
label: item.name,
value: item.sku,
})) || [],
);
if (reShipping) data.sales = [{}];
let shipmentInfo = localStorage.getItem('shipmentInfo');
if (shipmentInfo) shipmentInfo = JSON.parse(shipmentInfo);
const a = {
shipmentPlatform: 'uniuni',
...data,
// payment_method_id: shipmentInfo?.payment_method_id,
stockPointId: shipmentInfo?.stockPointId,
details: {
destination: {
name: data?.shipping?.company || data?.billing?.company || ' ',
address: {
address_line_1:
data?.shipping?.address_1 || data?.billing?.address_1,
city: data?.shipping?.city || data?.billing?.city,
region: data?.shipping?.state || data?.billing?.state,
postal_code:
data?.shipping?.postcode || data?.billing?.postcode,
},
contact_name:
data?.shipping?.first_name || data?.shipping?.last_name
? `${data?.shipping?.first_name} ${data?.shipping?.last_name}`
: `${data?.billing?.first_name} ${data?.billing?.last_name}`,
phone_number: {
phone: data?.shipping?.phone || data?.billing?.phone,
},
email_addresses: data?.shipping?.email || data?.billing?.email,
signature_requirement: 'not-required',
},
origin: {
name: data?.name,
email_addresses: data?.email,
contact_name: data?.name,
phone_number: shipmentInfo?.phone_number,
address: {
region: shipmentInfo?.region,
city: shipmentInfo?.city,
postal_code: shipmentInfo?.postal_code,
address_line_1: shipmentInfo?.address_line_1,
},
},
packaging_type: 'package',
expected_ship_date: dayjs(),
packaging_properties: {
packages: [
{
measurements: {
weight: {
unit: 'LBS',
value: 1,
},
cuboid: {
unit: 'IN',
l: 6,
w: 4,
h: 4,
},
},
description: 'food',
},
],
},
},
};
return a
}}
onFinish={async ({
customer_note,
notes,
items,
details,
externalOrderId,
...data
}) => {
details.origin.email_addresses =
details.origin.email_addresses.split(',');
details.destination.email_addresses =
details.destination.email_addresses.split(',');
details.destination.phone_number.number =
details.destination.phone_number.phone;
details.origin.phone_number.number = details.origin.phone_number.phone;
try {
const {
success,
message: errMsg,
...resShipment
} = await logisticscontrollerCreateshipment(
{ orderId: id },
{
details,
...data,
},
);
if (!success) throw new Error(errMsg);
message.success('创建成功');
tableRef?.current?.reload();
descRef?.current?.reload();
localStorage.setItem(
'shipmentInfo',
JSON.stringify({
// payment_method_id: data.payment_method_id,
stockPointId: data.stockPointId,
region: details.origin.address.region,
city: details.origin.address.city,
postal_code: details.origin.address.postal_code,
address_line_1: details.origin.address.address_line_1,
phone_number: details.origin.phone_number,
}),
);
// todo, 直接打印label
// const { resLabel } = await logisticscontrollerGetShipmentLabel(resShipment.data.shipmentId);
// console.log('res', resShipment.data.shipmentId, resLabel);
// const labelContent = resLabel.content;
// printPDF([labelContent]);
return true;
} catch (error: any) {
message.error(error?.message || '创建失败');
}
}}
onFinishFailed={() => {
const element = document.querySelector('.ant-form-item-explain-error');
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}}
>
<Row gutter={16}>
<Col span={8}>
<ProFormSelect
name="shipmentPlatform"
label="发货平台"
options={shipmentPlatforms}
placeholder="请选择发货平台"
rules={[{ required: true, message: '请选择一个选项' }]}
/>
</Col>
</Row>
<ProFormText label="订单号" readonly name='externalOrderId' />
<ProFormText label="客户备注" readonly name="customer_note" />
<ProFormList
label="后台备注"
name="notes"
actionRender={() => []}
readonly
>
<ProFormText readonly name="content" />
</ProFormList>
<Row gutter={16}>
<Col span={12}>
<ProFormSelect
label="合并发货订单号"
name="orderIds"
showSearch
mode="multiple"
request={async ({ keyWords: number }) => {
if (!number) return [];
const { data, success } = await ordercontrollerGetorderbynumber({
number,
});
if (success) {
return data.map((v) => ({
label: `${v.name} ${v.externalOrderId}`,
value: v.id,
}));
}
return [];
}}
/>
</Col>
{/* <Col span={12}>
<ProFormSelect
label="付款方式"
name="payment_method_id"
rules={[{ required: true, message: '请选择付款方式' }]}
request={async () => {
const { data, success } =
await logisticscontrollerGetpaymentmethods();
if (success) {
return data.map((v) => ({
label: v.label,
value: v.id,
}));
}
return [];
}}
/>
</Col> */}
</Row>
<Row gutter={16}>
<Col span={12}>
<ProFormList
label="原始订单"
name="items"
readonly
actionRender={() => []}
>
<ProForm.Group>
<ProFormText name="name" readonly />
<ProFormDigit name="quantity" readonly />
</ProForm.Group>
</ProFormList>
</Col>
<Col span={12}>
<ProFormList
label="发货产品"
name="sales"
// rules={[
// {
// required: true,
// message: '至少需要一个商品',
// validator: (_, value) =>
// value && value.length > 0
//</Col> ? Promise.resolve()
// : Promise.reject('至少需要一个商品'),
// },
// ]}
>
<ProForm.Group>
<ProFormSelect
params={{ options }}
request={async ({ keyWords, options }) => {
if (!keyWords || keyWords.length < 2) return options;
try {
const { data } = await productcontrollerSearchproducts({
name: keyWords,
});
return (
data?.map((item) => {
return {
label: `${item.name} - ${item.nameCn}`,
value: item?.sku,
};
}) || options
);
} catch (error: any) {
return options;
}
}}
name="sku"
label="产品"
placeholder="请选择产品"
tooltip="至少输入3个字符"
fieldProps={{
showSearch: true,
filterOption: false,
}}
debounceTime={300} // 防抖,减少请求频率
rules={[{ required: true, message: '请选择产品' }]}
/>
<ProFormDigit
name="quantity"
colProps={{ span: 12 }}
label="数量"
placeholder="请输入数量"
rules={[{ required: true, message: '请输入数量' }]}
fieldProps={{
precision: 0,
}}
/>
</ProForm.Group>
</ProFormList>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<ProForm.Group
title="发货信息"
extra={
<AddressPicker
onChange={(row) => {
console.log(row);
const {
address,
phone_number,
phone_number_extension,
stockPointId,
email,
} = row;
formRef?.current?.setFieldsValue({
stockPointId,
// address_id: row.id,
details: {
origin: {
email_addresses:email,
address,
phone_number: {
phone: phone_number,
extension: phone_number_extension,
},
},
},
});
}}
/>
}
>
{/* <ProFormText
label="address_id"
name={'address_id'}
rules={[{ required: true, message: '请输入ID' }]}
/> */}
<ProFormSelect
name="stockPointId"
width="md"
label="发货仓库点"
placeholder="请选择仓库点"
rules={[{ required: true, message: '请选择发货仓库点' }]}
request={async () => {
const { data = [] } = await stockcontrollerGetallstockpoints();
return data.map((item) => ({
label: item.name,
value: item.id,
}));
}}
/>
<ProFormText
label="公司名称"
name={['details', 'origin', 'name']}
rules={[{ required: true, message: '请输入公司名称' }]}
/>
<ProFormItem
name={['details', 'origin', 'address', 'country']}
label="国家"
initialValue={'CA'}
hidden
/>
<ProFormSelect
name={['details', 'origin', 'address', 'region']}
label="地区"
placeholder={'请选择地区'}
showSearch
required
valueEnum={region}
rules={[{ required: true, message: '请选择地区' }]}
/>
<ProFormText
name={['details', 'origin', 'address', 'city']}
label="城市"
placeholder="请输入城市"
required
rules={[{ required: true, message: '请输入城市' }]}
/>
<ProFormText
name={['details', 'origin', 'address', 'postal_code']}
label="邮编"
placeholder="请输入邮编"
required
rules={[{ required: true, message: '请输入邮编' }]}
/>
<ProFormText
name={['details', 'origin', 'address', 'address_line_1']}
label="详细地址"
placeholder="请输入详细地址"
width="lg"
required
rules={[{ required: true, message: '请输入详细地址' }]}
/>
<ProFormText
label="联系人"
name={['details', 'origin', 'contact_name']}
rules={[{ required: true, message: '请输入联系人' }]}
/>
<ProFormItem
name={['details', 'origin', 'phone_number']}
label="联系电话"
required
rules={[
{
required: true,
async validator(_, value) {
if (!value?.phone) {
return Promise.reject('请输入联系电话');
}
return Promise.resolve();
},
},
]}
>
<InternationalPhoneInput />
</ProFormItem>
<ProFormText
label="邮箱"
tooltip="多个邮箱用英文逗号隔开"
name={['details', 'origin', 'email_addresses']}
rules={[{ required: true, message: '请输入邮箱' }]}
/>
</ProForm.Group>
</Col>
<Col span={12}>
<ProForm.Group title="收货信息">
<ProFormText
label="公司名称"
name={['details', 'destination', 'name']}
/>
<ProFormItem
name={['details', 'destination', 'address', 'country']}
label="国家"
initialValue={'CA'}
hidden
/>
<ProFormSelect
name={['details', 'destination', 'address', 'region']}
label="地区"
placeholder={'请选择地区'}
showSearch
required
valueEnum={region}
rules={[{ required: true, message: '请选择地区' }]}
/>
<ProFormText
name={['details', 'destination', 'address', 'city']}
label="城市"
placeholder="请输入城市"
required
rules={[{ required: true, message: '请输入城市' }]}
/>
<ProFormText
name={['details', 'destination', 'address', 'postal_code']}
label="邮编"
placeholder="请输入邮编"
required
rules={[{ required: true, message: '请输入邮编' }]}
/>
<ProFormText
name={['details', 'destination', 'address', 'address_line_1']}
label="详细地址1"
placeholder="请输入详细地址"
width="lg"
required
rules={[{ required: true, message: '请输入详细地址' }]}
/>
<ProFormText
name={['details', 'destination', 'address', 'address_line_2']}
label="详细地址2"
placeholder="请输入详细地址"
width="lg"
/>
<ProFormText
label="联系人"
name={['details', 'destination', 'contact_name']}
rules={[{ required: true, message: '请输入联系人' }]}
/>
<ProFormItem
name={['details', 'destination', 'phone_number']}
label="联系电话"
required
rules={[
{
required: true,
async validator(_, value) {
if (!value?.phone) {
return Promise.reject('请输入联系电话');
}
return Promise.resolve();
},
},
]}
>
<InternationalPhoneInput />
</ProFormItem>
<ProFormText
label="邮箱"
tooltip="多个邮箱用英文逗号隔开"
name={['details', 'destination', 'email_addresses']}
rules={[{ required: true, message: '请输入邮箱' }]}
/>
</ProForm.Group>
<ProFormRadio.Group
name={['details', 'destination', 'signature_requirement']}
label="签收"
options={[
{ label: '否', value: 'not-required' },
{ label: '是', value: 'required' },
{ label: '成人签收', value: 'adult-required' },
]}
/>
</Col>
</Row>
<ProFormDatePicker
name={['details', 'expected_ship_date']}
label="预计发货日期"
rules={[{ required: true, message: '请选择预计发货日期' }]}
fieldProps={{
disabledDate: (current) => {
return current && current < dayjs().startOf('day');
},
}}
transform={(value) => {
return {
details: {
expected_ship_date: {
year: dayjs(value).year(),
month: dayjs(value).month() + 1,
day: dayjs(value).date(),
},
},
};
}}
/>
<ProFormRadio.Group
name={['details', 'packaging_type']}
label="包装类型"
options={[
{ label: '木托', value: 'pallet', disabled: true },
{ label: '纸箱', value: 'package' },
{ label: '文件袋', value: 'courier-pak', disabled: true },
{ label: '信封', value: 'envelope', disabled: true },
]}
/>
<ProFormDependency name={[['details', 'packaging_type']]}>
{({ details }) => {
// 根据包装类型决定渲染内容
const selectedPackagingType = details?.packaging_type;
// 判断是否为纸箱
if (selectedPackagingType === 'package') {
return (
<ProFormList
min={1}
max={1}
label="纸箱尺寸"
name={['details', 'packaging_properties', 'packages']}
rules={[
{
required: true,
validator: (_, value) =>
value && value.length > 0
? Promise.resolve()
: Promise.reject('至少选择一个包裹'),
},
]}
>
{(f, idx, action) => {
return (
<ProForm.Item label={`纸箱${idx + 1}`}>
<Radio.Group>
<Radio.Button
onClick={() => {
action.setCurrentRowData({
measurements: {
weight: {
unit: 'lb',
value: 1,
},
cuboid: {
unit: 'in',
l: 6,
w: 4,
h: 4,
},
},
});
}}
>
1
</Radio.Button>
<Radio.Button
onClick={() => {
action.setCurrentRowData({
measurements: {
weight: {
unit: 'lb',
value: 5,
},
cuboid: {
unit: 'in',
l: 8,
w: 6,
h: 6,
},
},
});
}}
>
2
</Radio.Button>
<Radio.Button
onClick={() => {
action.setCurrentRowData({
measurements: {
weight: {
unit: 'lb',
value: 5,
},
cuboid: {
unit: 'in',
l: 12,
w: 8,
h: 6,
},
},
});
}}
>
3
</Radio.Button>
</Radio.Group>
</ProForm.Item>
);
}}
<ProForm.Group>
<ProFormDigit
label="长"
name={['measurements', 'cuboid', 'l']}
placeholder="请输入长"
rules={[{ required: true, message: '请输入长' }]}
/>
<ProFormDigit
label="宽"
name={['measurements', 'cuboid', 'w']}
placeholder="请输入宽"
rules={[{ required: true, message: '请输入宽' }]}
/>
<ProFormDigit
label="高"
name={['measurements', 'cuboid', 'h']}
placeholder="请输入高"
rules={[{ required: true, message: '请输入高' }]}
/>
<ProFormSelect
label="单位"
name={['measurements', 'cuboid', 'unit']}
valueEnum={{
CM: '厘米',
IN: '英寸',
}}
placeholder="请输入单位"
rules={[{ required: true, message: '请输入单位' }]}
/>
</ProForm.Group>
<ProForm.Group>
<ProFormDigit
label="重量"
name={['measurements', 'weight', 'value']}
placeholder="请输入重量"
rules={[{ required: true, message: '请输入重量' }]}
/>
<ProFormSelect
label="单位"
name={['measurements', 'weight', 'unit']}
valueEnum={{ KGS: '千克', LBS: '磅' }}
// valueEnum={{ KGS: '千克', LBS: '磅', OZS: '盎司' }}
placeholder="请输入单位"
rules={[{ required: true, message: '请输入单位' }]}
/>
<ProFormText
label="描述"
name="description"
placeholder="请输入描述"
width="lg"
// rules={[{ required: true, message: '请输入描述' }]}
/>
</ProForm.Group>
</ProFormList>
);
}
// 判断是否为文件袋
if (selectedPackagingType === 'courier-pak') {
return (
<ProFormList
label="文件袋"
name={['details', 'packaging_properties', 'courier_paks']}
rules={[
{
required: true,
validator: (_, value) =>
value && value.length > 0
? Promise.resolve()
: Promise.reject('至少选择一个'),
},
]}
>
<ProForm.Group>
<ProFormDigit
label="重量"
name={['weight', 'value']}
placeholder="请输入重量"
rules={[{ required: true, message: '请输入重量' }]}
/>
<ProFormSelect
label="单位"
name={['weight', 'unit']}
valueEnum={{ kg: '千克', lb: '磅' }}
// valueEnum={{ kg: '千克', lb: '磅', oz: '盎司' }}
placeholder="请输入单位"
rules={[{ required: true, message: '请输入单位' }]}
/>
<ProFormText
label="描述"
name="description"
placeholder="请输入描述"
width="lg"
rules={[{ required: true, message: '请输入描述' }]}
/>
</ProForm.Group>
</ProFormList>
);
}
}}
</ProFormDependency>
<Button
loading={ratesLoading}
onClick={async () => {
try {
const {
customer_note,
notes,
items,
details,
externalOrderId,
...data
} = formRef.current?.getFieldsValue();
const originEmail = details.origin.email_addresses;
const destinationEmail = details.destination.email_addresses;
details.origin.email_addresses =
details.origin.email_addresses.split(',');
details.destination.email_addresses =
details.destination.email_addresses.split(',');
details.destination.phone_number.number =
details.destination.phone_number.phone;
details.origin.phone_number.number =
details.origin.phone_number.phone;
const res = await logisticscontrollerGetshipmentfee({
shipmentPlatform: data.shipmentPlatform,
stockPointId: data.stockPointId,
sender: details.origin.contact_name,
startPhone: details.origin.phone_number,
startPostalCode: details.origin.address.postal_code.replace(
/\s/g,
'',
),
pickupAddress: details.origin.address.address_line_1,
shipperCountryCode: details.origin.address.country,
receiver: details.destination.contact_name,
city: details.destination.address.city,
province: details.destination.address.region,
country: details.destination.address.country,
postalCode: details.destination.address.postal_code.replace(
/\s/g,
'',
),
deliveryAddress: details.destination.address.address_line_1,
receiverPhone: details.destination.phone_number.number,
receiverEmail: details.destination.email_addresses,
length:
details.packaging_properties.packages[0].measurements.cuboid.l,
width:
details.packaging_properties.packages[0].measurements.cuboid.w,
height:
details.packaging_properties.packages[0].measurements.cuboid.h,
dimensionUom:
details.packaging_properties.packages[0].measurements.cuboid
.unit,
weight:
details.packaging_properties.packages[0].measurements.weight
.value,
weightUom:
details.packaging_properties.packages[0].measurements.weight
.unit,
});
if (!res?.success) throw new Error(res?.message);
const fee = res.data;
setShipmentFee(fee);
details.origin.email_addresses = originEmail;
details.destination.email_addresses = destinationEmail;
formRef.current?.setFieldValue('details', {
...details,
shipmentFee: fee,
});
message.success('获取运费成功');
} catch (error: any) {
message.error(error?.message || '获取运费失败');
}
}}
>
</Button>
<ProFormText
readonly
name={['details', 'shipmentFee']}
fieldProps={{
value: (shipmentFee / 100.0).toFixed(2),
}}
/>
</ModalForm>
);
};
const SalesChange: React.FC<{
id: number;
detailRef?: React.MutableRefObject<ActionType | undefined>;
reShipping?: boolean;
}> = ({ id, detailRef }) => {
const formRef = useRef<ProFormInstance>();
return (
<ModalForm
formRef={formRef}
title="换货"
size="large"
width="80vw"
modalProps={{
destroyOnHidden: true,
styles: {
body: { maxHeight: '65vh', overflowY: 'auto', overflowX: 'hidden' },
},
}}
trigger={
<Button type="primary">
<CodeSandboxOutlined />
</Button>
}
request={async () => {
const { data, success }: API.OrderDetailRes =
await ordercontrollerGetorderdetail({
orderId: id,
});
if (!success || !data) return {};
data.sales = data.sales?.reduce(
(acc: API.OrderSale[], cur: API.OrderSale) => {
let idx = acc.findIndex((v: any) => v.productId === cur.productId);
if (idx === -1) {
acc.push(cur);
} else {
acc[idx].quantity += cur.quantity;
}
return acc;
},
[],
);
// setOptions(
// data.sales?.map((item) => ({
// label: item.name,
// value: item.sku,
// })) || [],
// );
return { ...data };
}}
onFinish={async (formData: any) => {
const { sales } = formData;
const res = await ordercontrollerUpdateorderitems(
{ orderId: id },
sales,
);
if (!res.success) {
message.error(`更新货物信息失败: ${res.message}`);
return false;
}
message.success('更新成功');
detailRef?.current?.reload();
return true;
}}
>
<ProFormList label="换货订单" name="items">
<ProForm.Group>
<ProFormSelect
params={{}}
request={async ({ keyWords }) => {
try {
const { data } = await productcontrollerSearchproducts({
name: keyWords,
});
return data?.map((item) => {
return {
label: `${item.name} - ${item.nameCn}`,
value: item?.sku,
};
});
} catch (error: any) {
return [];
}
}}
name="sku"
label="订单"
placeholder="请选择订单"
tooltip="至少输入3个字符"
fieldProps={{
showSearch: true,
filterOption: false,
}}
debounceTime={300} // 防抖,减少请求频率
rules={[{ required: true, message: '请选择订单' }]}
/>
<ProFormDigit
name="quantity"
colProps={{ span: 12 }}
label="订单数量"
placeholder="请输入数量"
rules={[{ required: true, message: '请输入数量' }]}
fieldProps={{
precision: 0,
}}
/>
</ProForm.Group>
</ProFormList>
<ProFormList label="换货产品" name="sales">
<ProForm.Group>
<ProFormSelect
params={{}}
request={async ({ keyWords }) => {
try {
const { data } = await productcontrollerSearchproducts({
name: keyWords,
});
return data?.map((item) => {
return {
label: `${item.name} - ${item.nameCn}`,
value: item?.sku,
};
});
} catch (error: any) {
return [];
}
}}
name="sku"
label="产品"
placeholder="请选择产品"
tooltip="至少输入3个字符"
fieldProps={{
showSearch: true,
filterOption: false,
}}
debounceTime={300} // 防抖,减少请求频率
rules={[{ required: true, message: '请选择产品' }]}
/>
<ProFormDigit
name="quantity"
colProps={{ span: 12 }}
label="数量"
placeholder="请输入数量"
rules={[{ required: true, message: '请输入数量' }]}
fieldProps={{
precision: 0,
}}
/>
</ProForm.Group>
</ProFormList>
</ModalForm>
);
};
const CreateOrder: React.FC<{
tableRef?: React.MutableRefObject<ActionType | undefined>;
}> = ({ tableRef }) => {
const formRef = useRef<ProFormInstance>();
const { message } = App.useApp();
return (
<ModalForm
formRef={formRef}
title="创建订单"
size="large"
width="80vw"
modalProps={{
destroyOnHidden: true,
styles: {
body: { maxHeight: '65vh', overflowY: 'auto', overflowX: 'hidden' },
},
}}
trigger={
<Button type="primary">
<CodeSandboxOutlined />
</Button>
}
params={{
source_type: 'admin',
}}
onFinish={async ({ items, details, ...data }) => {
try {
const { success, message: errMsg } = await ordercontrollerCreateorder(
{
...data,
customer_email: data?.billing?.email,
billing_phone: data?.billing?.phone,
},
);
if (!success) throw new Error(errMsg);
message.success('创建成功');
tableRef?.current?.reload();
return true;
} catch (error) {
message.error(error?.message || '创建失败');
}
}}
onFinishFailed={() => {
const element = document.querySelector('.ant-form-item-explain-error');
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}}
>
<ProFormList label="订单内容" name="sales">
<ProForm.Group>
<ProFormSelect
request={async ({ keyWords, options }) => {
if (!keyWords || keyWords.length < 2) return options;
try {
const { data } = await productcontrollerSearchproducts({
name: keyWords,
});
return (
data?.map((item) => {
return {
label: `${item.name} - ${item.nameCn}`,
value: item?.sku,
};
}) || options
);
} catch (error) {
return options;
}
}}
name="sku"
label="产品"
placeholder="请选择产品"
tooltip="至少输入3个字符"
fieldProps={{
showSearch: true,
filterOption: false,
}}
debounceTime={300} // 防抖,减少请求频率
rules={[{ required: true, message: '请选择产品' }]}
/>
<ProFormDigit
name="quantity"
colProps={{ span: 12 }}
label="数量"
placeholder="请输入数量"
rules={[{ required: true, message: '请输入数量' }]}
fieldProps={{
precision: 0,
}}
/>
</ProForm.Group>
</ProFormList>
<ProFormDigit
label="金额"
name="total"
rules={[{ required: true, message: '请输入金额' }]}
/>
<ProForm.Group title="收货信息">
<ProFormText
label="公司名称"
name={['billing', 'company']}
rules={[{ message: '请输入公司名称' }]}
/>
<ProFormItem
name={['billing', 'country']}
label="国家"
initialValue={'CA'}
hidden
/>
<ProFormSelect
name={['billing', 'state']}
label="地区"
placeholder={'请选择地区'}
showSearch
required
valueEnum={region}
rules={[{ required: true, message: '请选择地区' }]}
/>
<ProFormText
name={['billing', 'city']}
label="城市"
placeholder="请输入城市"
required
rules={[{ required: true, message: '请输入城市' }]}
/>
<ProFormText
name={['billing', 'postcode']}
label="邮编"
placeholder="请输入邮编"
required
rules={[{ required: true, message: '请输入邮编' }]}
/>
<ProFormText
name={['billing', 'address_1']}
label="详细地址1"
placeholder="请输入详细地址"
width="lg"
required
rules={[{ required: true, message: '请输入详细地址' }]}
/>
<ProFormText
name={['billing', 'address_2']}
label="详细地址2"
placeholder="请输入详细地址"
width="lg"
/>
<ProFormText
label="联系人"
name={['billing', 'first_name']}
rules={[{ required: true, message: '请输入联系人' }]}
/>
<ProFormText
name={['billing', 'phone']}
label="联系电话"
required
rules={[{ required: true, message: '请输入联系电话' }]}
/>
<ProFormText
label="邮箱"
name={['billing', 'email']}
rules={[{ required: true, message: '请输入邮箱' }]}
/>
</ProForm.Group>
{/* <ProFormText label="客户邮箱" name="customer_email" /> */}
</ModalForm>
);
};
const AddressPicker: React.FC<{
value?: any;
onChange?: (value: any) => void;
}> = ({ onChange, value }) => {
const [selectedRow, setSelectedRow] = useState(null);
const { message } = App.useApp();
const columns: ProColumns<API.ShippingAddress>[] = [
{
title: '仓库点',
dataIndex: 'stockPointId',
hideInSearch: true,
valueType: 'select',
request: async () => {
const { data = [] } = await stockcontrollerGetallstockpoints();
return data.map((item) => ({
label: item.name,
value: item.id,
}));
},
},
{
title: 'id',
dataIndex: ['id'],
hideInSearch: true,
},
{
title: '地区',
dataIndex: ['address', 'region'],
hideInSearch: true,
},
{
title: '城市',
dataIndex: ['address', 'city'],
hideInSearch: true,
},
{
title: '邮编',
dataIndex: ['address', 'postal_code'],
hideInSearch: true,
},
{
title: '详细地址',
dataIndex: ['address', 'address_line_1'],
hideInSearch: true,
},
{
title: '联系电话',
render: (_, record) =>
`+${record.phone_number_extension} ${record.phone_number}`,
hideInSearch: true,
},
{
title: '邮箱',
dataIndex: [ 'email'],
hideInSearch: true,
},
];
return (
<ModalForm
title="选择地址"
trigger={<Button type="primary"></Button>}
modalProps={{ destroyOnHidden: true }}
onFinish={async () => {
if (!selectedRow) {
message.error('请选择地址');
return false;
}
if (onChange) onChange(selectedRow);
return true;
}}
>
<ProTable
rowKey="id"
request={async () => {
const { data, success } =
await logisticscontrollerGetshippingaddresslist();
if (success) {
return {
data: data,
};
}
return {
data: [],
};
}}
columns={columns}
search={false}
rowSelection={{
type: 'radio',
onChange: (_, selectedRows) => {
setSelectedRow(selectedRows[0]);
},
}}
/>
</ModalForm>
);
};
export default ListPage;