webadmin端订单修改
This commit is contained in:
@@ -121,15 +121,36 @@ export type AdminUserRow = {
|
||||
orderCount: number;
|
||||
};
|
||||
|
||||
export type AdminOrderItem = {
|
||||
productName: string;
|
||||
productSpec: string;
|
||||
productImage: string;
|
||||
unitPrice: number;
|
||||
quantity: number;
|
||||
};
|
||||
|
||||
export type AdminOrderRow = {
|
||||
id: string;
|
||||
orderNo: string;
|
||||
status: string;
|
||||
deliveryType: string;
|
||||
payAmount: number;
|
||||
productName?: string;
|
||||
productSpec?: string;
|
||||
quantity?: number;
|
||||
receiverName: string;
|
||||
receiverPhone: string;
|
||||
createdAt: string;
|
||||
cityId?: string;
|
||||
city?: { id: string; name: string; code: string };
|
||||
fulfillmentWarehouseId?: string | null;
|
||||
fulfillmentWarehouse?: { id: string; name: string } | null;
|
||||
user?: { id: string; userNo: string; phone: string | null; nickname: string | null };
|
||||
delivery?: { provider: string; trackingNo: string | null; providerOrderNo: string | null };
|
||||
delivery?: {
|
||||
provider: string;
|
||||
trackingNo: string | null;
|
||||
providerOrderNo: string | null;
|
||||
logisticsCompany?: string | null;
|
||||
manualQueryUrl?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Radio,
|
||||
Select,
|
||||
Space,
|
||||
Table,
|
||||
@@ -17,8 +18,14 @@ import {
|
||||
message,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { request, type AdminOrderRow, type Paginated } from '../lib/api';
|
||||
import { DELIVERY_PROVIDER_LABELS, ORDER_STATUS_COLORS, ORDER_STATUS_LABELS, fmtTime } from '../lib/constants';
|
||||
import { request, type AdminOrderItem, type AdminOrderRow, type Paginated } from '../lib/api';
|
||||
import {
|
||||
ADMIN_OPTIONS_PAGE_SIZE,
|
||||
DELIVERY_PROVIDER_LABELS,
|
||||
ORDER_STATUS_COLORS,
|
||||
ORDER_STATUS_LABELS,
|
||||
fmtTime,
|
||||
} from '../lib/constants';
|
||||
|
||||
type ShipDefaults = {
|
||||
provider: string;
|
||||
@@ -33,6 +40,19 @@ type ShipDefaults = {
|
||||
payMode: string;
|
||||
};
|
||||
|
||||
type CityOption = { id: string; name: string; code: string };
|
||||
|
||||
type WarehouseOption = {
|
||||
id: string;
|
||||
name: string;
|
||||
status?: string;
|
||||
contactName: string;
|
||||
contactPhone: string;
|
||||
address: string;
|
||||
lng?: number | null;
|
||||
lat?: number | null;
|
||||
};
|
||||
|
||||
type OrderDetail = AdminOrderRow & {
|
||||
receiverAddress?: string;
|
||||
receiverProvince?: string;
|
||||
@@ -51,17 +71,58 @@ type OrderDetail = AdminOrderRow & {
|
||||
productAmount?: number;
|
||||
freightAmount?: number;
|
||||
benefitAmount?: number;
|
||||
deliveryType?: string;
|
||||
fulfillmentWarehouseId?: string | null;
|
||||
paidAt?: string | null;
|
||||
payExpireAt?: string | null;
|
||||
items?: Array<Record<string, unknown>>;
|
||||
productImage?: string;
|
||||
listUnitPrice?: number;
|
||||
items?: AdminOrderItem[];
|
||||
payment?: Record<string, unknown> | null;
|
||||
statusLogs?: Array<{ fromStatus: string | null; toStatus: string; createdAt: string }>;
|
||||
benefitCoupons?: Array<Record<string, unknown>>;
|
||||
city?: { name: string; code: string };
|
||||
fulfillmentWarehouse?: {
|
||||
id: string;
|
||||
name: string;
|
||||
contactName?: string;
|
||||
contactPhone?: string;
|
||||
address?: string;
|
||||
lng?: number | null;
|
||||
lat?: number | null;
|
||||
} | null;
|
||||
};
|
||||
|
||||
type ShipMode = 'WAREHOUSE' | 'EXPRESS';
|
||||
|
||||
function orderProductRows(detail: OrderDetail): AdminOrderItem[] {
|
||||
if (detail.items?.length) return detail.items;
|
||||
if (!detail.productName) return [];
|
||||
return [{
|
||||
productName: detail.productName,
|
||||
productSpec: detail.productSpec ?? '',
|
||||
productImage: detail.productImage ?? '',
|
||||
unitPrice: Number(detail.listUnitPrice ?? 0),
|
||||
quantity: detail.quantity ?? 1,
|
||||
}];
|
||||
}
|
||||
|
||||
function canShip(row: { status: string; delivery?: { trackingNo?: string | null } | null }) {
|
||||
return ['PENDING_SHIP', 'OUT_WAREHOUSE'].includes(row.status) && !row.delivery?.trackingNo;
|
||||
}
|
||||
|
||||
function warehouseToDefaults(wh: WarehouseOption, base?: ShipDefaults | null): ShipDefaults {
|
||||
return {
|
||||
provider: 'XFX',
|
||||
providerLabel: '小飞侠',
|
||||
fromName: wh.contactName || base?.fromName || '杜康仓库',
|
||||
fromMobile: wh.contactPhone || base?.fromMobile || '13800000000',
|
||||
fromAddress: wh.address || base?.fromAddress || '',
|
||||
fromAddressDetail: wh.name || base?.fromAddressDetail || '',
|
||||
fromLng: wh.lng != null ? Number(wh.lng) : (base?.fromLng ?? 113.665),
|
||||
fromLat: wh.lat != null ? Number(wh.lat) : (base?.fromLat ?? 34.757),
|
||||
weight: base?.weight ?? 2,
|
||||
payMode: base?.payMode ?? '1',
|
||||
};
|
||||
}
|
||||
|
||||
export default function OrdersPage() {
|
||||
const [form] = Form.useForm();
|
||||
const [shipForm] = Form.useForm();
|
||||
@@ -78,6 +139,11 @@ export default function OrdersPage() {
|
||||
const [shipDefaults, setShipDefaults] = useState<ShipDefaults | null>(null);
|
||||
const [shipping, setShipping] = useState(false);
|
||||
const [logisticsShipping, setLogisticsShipping] = useState(false);
|
||||
const [cities, setCities] = useState<CityOption[]>([]);
|
||||
const [shipModalOpen, setShipModalOpen] = useState(false);
|
||||
const [shipTarget, setShipTarget] = useState<OrderDetail | null>(null);
|
||||
const [shipMode, setShipMode] = useState<ShipMode>('EXPRESS');
|
||||
const [warehouses, setWarehouses] = useState<WarehouseOption[]>([]);
|
||||
|
||||
const selectedOrders = useMemo(
|
||||
() => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)),
|
||||
@@ -91,6 +157,7 @@ export default function OrdersPage() {
|
||||
const qs = new URLSearchParams({ page: String(page), pageSize: String(pageSize) });
|
||||
if (values.orderNo) qs.set('orderNo', values.orderNo);
|
||||
if (values.status) qs.set('status', values.status);
|
||||
if (values.cityId) qs.set('cityId', values.cityId);
|
||||
if (values.receiverPhone) qs.set('receiverPhone', values.receiverPhone);
|
||||
const res = await request<Paginated<AdminOrderRow>>(`/admin/orders?${qs}`);
|
||||
setData(res);
|
||||
@@ -105,40 +172,82 @@ export default function OrdersPage() {
|
||||
|
||||
useEffect(() => {
|
||||
void request<ShipDefaults>('/admin/orders/ship-defaults').then(setShipDefaults).catch(() => {});
|
||||
void request<Paginated<CityOption>>(`/admin/cities?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}`)
|
||||
.then((res) => setCities(res.items ?? []))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
function applyShipDefaults(defaults: ShipDefaults, warehouseId?: string | null) {
|
||||
shipForm.setFieldsValue({
|
||||
warehouseId: warehouseId || undefined,
|
||||
provider: defaults.provider,
|
||||
weight: defaults.weight,
|
||||
payMode: defaults.payMode,
|
||||
fromName: defaults.fromName,
|
||||
fromMobile: defaults.fromMobile,
|
||||
fromAddress: defaults.fromAddress,
|
||||
fromAddressDetail: defaults.fromAddressDetail,
|
||||
fromLng: defaults.fromLng,
|
||||
fromLat: defaults.fromLat,
|
||||
});
|
||||
}
|
||||
|
||||
async function loadWarehousesForOrder(order: OrderDetail) {
|
||||
const cityId = order.city?.id || order.cityId;
|
||||
if (!cityId) {
|
||||
setWarehouses([]);
|
||||
return [] as WarehouseOption[];
|
||||
}
|
||||
const rows = await request<WarehouseOption[]>(`/admin/cities/${cityId}/warehouses`).catch(() => []);
|
||||
const active = rows.filter((w) => !w.status || w.status === 'ACTIVE');
|
||||
setWarehouses(active);
|
||||
return active;
|
||||
}
|
||||
|
||||
async function openDetail(id: string) {
|
||||
const res = await request<OrderDetail>(`/admin/orders/${id}`);
|
||||
setDetail(res);
|
||||
setDrawerOpen(true);
|
||||
const defaults = shipDefaults ?? await request<ShipDefaults>('/admin/orders/ship-defaults').catch(() => null);
|
||||
}
|
||||
|
||||
async function openShipModal(id: string) {
|
||||
const res = await request<OrderDetail>(`/admin/orders/${id}`);
|
||||
setShipTarget(res);
|
||||
setDetail(res);
|
||||
logisticsForm.resetFields();
|
||||
const rows = await loadWarehousesForOrder(res);
|
||||
const preferredWarehouseId = res.fulfillmentWarehouseId || res.fulfillmentWarehouse?.id || rows[0]?.id;
|
||||
const preferred = rows.find((w) => w.id === preferredWarehouseId) || rows[0];
|
||||
const defaults = preferred
|
||||
? warehouseToDefaults(preferred, shipDefaults)
|
||||
: (shipDefaults ?? await request<ShipDefaults>('/admin/orders/ship-defaults').catch(() => null));
|
||||
if (defaults) {
|
||||
setShipDefaults(defaults);
|
||||
shipForm.setFieldsValue({
|
||||
provider: defaults.provider,
|
||||
weight: defaults.weight,
|
||||
payMode: defaults.payMode,
|
||||
fromName: defaults.fromName,
|
||||
fromMobile: defaults.fromMobile,
|
||||
fromAddress: defaults.fromAddress,
|
||||
fromAddressDetail: defaults.fromAddressDetail,
|
||||
fromLng: defaults.fromLng,
|
||||
fromLat: defaults.fromLat,
|
||||
});
|
||||
applyShipDefaults(defaults, preferred?.id);
|
||||
}
|
||||
setShipMode(preferred ? 'WAREHOUSE' : 'EXPRESS');
|
||||
setShipModalOpen(true);
|
||||
}
|
||||
|
||||
async function onWarehouseChange(warehouseId: string) {
|
||||
const wh = warehouses.find((w) => w.id === warehouseId);
|
||||
if (!wh) return;
|
||||
applyShipDefaults(warehouseToDefaults(wh, shipDefaults), warehouseId);
|
||||
}
|
||||
|
||||
async function submitShip() {
|
||||
if (!detail) return;
|
||||
if (!shipTarget) return;
|
||||
const values = await shipForm.validateFields();
|
||||
setShipping(true);
|
||||
try {
|
||||
const res = await request<OrderDetail>(`/admin/orders/${detail.id}/ship`, {
|
||||
const res = await request<OrderDetail>(`/admin/orders/${shipTarget.id}/ship`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
message.success('发货成功');
|
||||
message.success('仓配发货成功');
|
||||
setShipTarget(res);
|
||||
setDetail(res);
|
||||
setShipModalOpen(false);
|
||||
void load();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '发货失败');
|
||||
@@ -148,16 +257,18 @@ export default function OrdersPage() {
|
||||
}
|
||||
|
||||
async function submitLogisticsShip() {
|
||||
if (!detail) return;
|
||||
if (!shipTarget) return;
|
||||
const values = await logisticsForm.validateFields();
|
||||
setLogisticsShipping(true);
|
||||
try {
|
||||
const res = await request<OrderDetail>(`/admin/orders/${detail.id}/logistics-ship`, {
|
||||
const res = await request<OrderDetail>(`/admin/orders/${shipTarget.id}/logistics-ship`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
message.success('快递单已录入');
|
||||
setShipTarget(res);
|
||||
setDetail(res);
|
||||
setShipModalOpen(false);
|
||||
void load();
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '填单失败');
|
||||
@@ -191,6 +302,25 @@ export default function OrdersPage() {
|
||||
|
||||
const columns: ColumnsType<AdminOrderRow> = [
|
||||
{ title: '订单号', dataIndex: 'orderNo', width: 180 },
|
||||
{
|
||||
title: '城市',
|
||||
width: 90,
|
||||
render: (_, row) => row.city?.name || '—',
|
||||
},
|
||||
{
|
||||
title: '商品',
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
render: (_, row) => {
|
||||
const name = row.productName || '—';
|
||||
const qty = row.quantity != null ? ` ×${row.quantity}` : '';
|
||||
return (
|
||||
<span title={row.productSpec ? `${name}(${row.productSpec})` : name}>
|
||||
{name}{qty}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
@@ -227,11 +357,19 @@ export default function OrdersPage() {
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
width: 140,
|
||||
fixed: 'right',
|
||||
render: (_, row) => (
|
||||
<Button type="link" size="small" onClick={() => openDetail(row.id)}>
|
||||
详情
|
||||
</Button>
|
||||
<Space size={0}>
|
||||
<Button type="link" size="small" onClick={() => openDetail(row.id)}>
|
||||
详情
|
||||
</Button>
|
||||
{canShip(row) && (
|
||||
<Button type="link" size="small" onClick={() => void openShipModal(row.id)}>
|
||||
配送
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
@@ -255,6 +393,16 @@ export default function OrdersPage() {
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select allowClear style={{ width: 120 }} options={Object.entries(ORDER_STATUS_LABELS).map(([value, label]) => ({ value, label }))} />
|
||||
</Form.Item>
|
||||
<Form.Item name="cityId" label="城市">
|
||||
<Select
|
||||
allowClear
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: 140 }}
|
||||
placeholder="全部"
|
||||
options={cities.map((c) => ({ value: c.id, label: c.name }))}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="receiverPhone" label="收货手机">
|
||||
<Input allowClear />
|
||||
</Form.Item>
|
||||
@@ -271,7 +419,7 @@ export default function OrdersPage() {
|
||||
loading={loading}
|
||||
columns={columns}
|
||||
dataSource={data?.items ?? []}
|
||||
scroll={{ x: 1200 }}
|
||||
scroll={{ x: 1500 }}
|
||||
rowSelection={{
|
||||
selectedRowKeys,
|
||||
onChange: (keys) => setSelectedRowKeys(keys as string[]),
|
||||
@@ -295,6 +443,11 @@ export default function OrdersPage() {
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
extra={detail && (
|
||||
<Space>
|
||||
{canShip(detail) && (
|
||||
<Button type="primary" onClick={() => void openShipModal(detail.id)}>
|
||||
配送发货
|
||||
</Button>
|
||||
)}
|
||||
<Typography.Text type="secondary">调试改状态</Typography.Text>
|
||||
<Select
|
||||
value={detail.status}
|
||||
@@ -318,6 +471,13 @@ export default function OrdersPage() {
|
||||
<>
|
||||
<Descriptions column={1} bordered size="small" title="基本信息">
|
||||
<Descriptions.Item label="订单号">{detail.orderNo}</Descriptions.Item>
|
||||
<Descriptions.Item label="开城城市">
|
||||
{detail.city?.name || '—'}
|
||||
{detail.city?.code ? `(${detail.city.code})` : ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="履约仓">
|
||||
{detail.fulfillmentWarehouse?.name || '未分配'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={ORDER_STATUS_COLORS[detail.status] || 'default'}>
|
||||
{ORDER_STATUS_LABELS[detail.status] || detail.status}
|
||||
@@ -330,6 +490,51 @@ export default function OrdersPage() {
|
||||
<Descriptions.Item label="下单时间">{new Date(detail.createdAt).toLocaleString('zh-CN')}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Typography.Title level={5} style={{ marginTop: 16, marginBottom: 8 }}>商品</Typography.Title>
|
||||
<Table
|
||||
size="small"
|
||||
rowKey={(_, i) => String(i)}
|
||||
pagination={false}
|
||||
dataSource={orderProductRows(detail)}
|
||||
locale={{ emptyText: '无商品信息' }}
|
||||
columns={[
|
||||
{
|
||||
title: '商品',
|
||||
dataIndex: 'productName',
|
||||
render: (name: string, row) => (
|
||||
<Space>
|
||||
{row.productImage ? (
|
||||
<img
|
||||
src={row.productImage}
|
||||
alt=""
|
||||
style={{ width: 40, height: 40, objectFit: 'cover', borderRadius: 4 }}
|
||||
/>
|
||||
) : null}
|
||||
<span>
|
||||
{name || '—'}
|
||||
{row.productSpec ? (
|
||||
<Typography.Text type="secondary" style={{ display: 'block', fontSize: 12 }}>
|
||||
{row.productSpec}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '单价',
|
||||
dataIndex: 'unitPrice',
|
||||
width: 90,
|
||||
render: (v: number) => `¥${v}`,
|
||||
},
|
||||
{
|
||||
title: '数量',
|
||||
dataIndex: 'quantity',
|
||||
width: 70,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<Descriptions column={1} bordered size="small" title="收货信息" style={{ marginTop: 16 }}>
|
||||
<Descriptions.Item label="收货人">{detail.receiverName} {detail.receiverPhone}</Descriptions.Item>
|
||||
<Descriptions.Item label="地址">{detail.receiverAddress}</Descriptions.Item>
|
||||
@@ -363,99 +568,6 @@ export default function OrdersPage() {
|
||||
</Descriptions>
|
||||
)}
|
||||
|
||||
{['PENDING_SHIP', 'OUT_WAREHOUSE'].includes(detail.status) && !detail.delivery?.trackingNo && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
{(detail.deliveryType === 'CROSS_CITY' || !detail.fulfillmentWarehouseId) && (
|
||||
<>
|
||||
<Typography.Title level={5}>总部快递填单</Typography.Title>
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
|
||||
适用于跨城订单或同城无仓订单
|
||||
</Typography.Paragraph>
|
||||
<Form form={logisticsForm} layout="vertical" size="small">
|
||||
<Form.Item name="logisticsCompany" label="快递公司" rules={[{ required: true }]}>
|
||||
<Input placeholder="如 顺丰速运、京东物流" />
|
||||
</Form.Item>
|
||||
<Form.Item name="trackingNo" label="运单号" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="manualQueryUrl" label="物流查询链接(可选)">
|
||||
<Input placeholder="https://..." />
|
||||
</Form.Item>
|
||||
<Button type="primary" loading={logisticsShipping} onClick={() => void submitLogisticsShip()}>
|
||||
提交快递单
|
||||
</Button>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
|
||||
{detail.fulfillmentWarehouseId && (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>
|
||||
仓配小飞侠重试
|
||||
</Typography.Title>
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
|
||||
仓配订单通常支付后自动推单;失败时可手动重试
|
||||
</Typography.Paragraph>
|
||||
<Form form={shipForm} layout="vertical" size="small">
|
||||
<Form.Item name="provider" label="快递公司" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[{ value: 'XFX', label: '小飞侠' }]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="weight" label="重量(kg)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0.01} step={0.5} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="payMode" label="付费方式" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '1', label: '寄付' },
|
||||
{ value: '2', label: '到付' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} placeholder="可选" />
|
||||
</Form.Item>
|
||||
<Collapse
|
||||
ghost
|
||||
items={[{
|
||||
key: 'from',
|
||||
label: '寄件信息(默认仓库,可修改)',
|
||||
children: (
|
||||
<>
|
||||
<Form.Item name="fromName" label="寄件人" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="fromMobile" label="寄件手机" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="fromAddress" label="寄件区域" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="fromAddressDetail" label="寄件详细地址" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Form.Item name="fromLng" label="经度">
|
||||
<InputNumber step={0.001} />
|
||||
</Form.Item>
|
||||
<Form.Item name="fromLat" label="纬度">
|
||||
<InputNumber step={0.001} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</>
|
||||
),
|
||||
}]}
|
||||
/>
|
||||
<Button type="primary" loading={shipping} onClick={() => void submitShip()}>
|
||||
调用小飞侠发货
|
||||
</Button>
|
||||
</Form>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{detail.statusLogs && detail.statusLogs.length > 0 && (
|
||||
<>
|
||||
<Typography.Title level={5} style={{ marginTop: 16 }}>状态流转</Typography.Title>
|
||||
@@ -476,6 +588,134 @@ export default function OrdersPage() {
|
||||
)}
|
||||
</Drawer>
|
||||
|
||||
<Modal
|
||||
title={shipTarget ? `配送发货 · ${shipTarget.orderNo}` : '配送发货'}
|
||||
open={shipModalOpen}
|
||||
onCancel={() => setShipModalOpen(false)}
|
||||
width={560}
|
||||
destroyOnClose
|
||||
footer={
|
||||
<Space>
|
||||
<Button onClick={() => setShipModalOpen(false)}>取消</Button>
|
||||
{shipMode === 'WAREHOUSE' ? (
|
||||
<Button type="primary" loading={shipping} onClick={() => void submitShip()}>
|
||||
确认仓配发货
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="primary" loading={logisticsShipping} onClick={() => void submitLogisticsShip()}>
|
||||
提交快递单
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
{shipTarget && (
|
||||
<>
|
||||
<Descriptions size="small" column={1} style={{ marginBottom: 16 }}>
|
||||
<Descriptions.Item label="城市">{shipTarget.city?.name || '—'}</Descriptions.Item>
|
||||
<Descriptions.Item label="收货">
|
||||
{shipTarget.receiverName} {shipTarget.receiverPhone}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="商品">
|
||||
{shipTarget.productName || '—'}
|
||||
{shipTarget.quantity != null ? ` ×${shipTarget.quantity}` : ''}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Radio.Group
|
||||
value={shipMode}
|
||||
onChange={(e) => setShipMode(e.target.value as ShipMode)}
|
||||
optionType="button"
|
||||
buttonStyle="solid"
|
||||
style={{ marginBottom: 16 }}
|
||||
options={[
|
||||
{ value: 'WAREHOUSE', label: '仓库配送(小飞侠)' },
|
||||
{ value: 'EXPRESS', label: '填写快递单号' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{shipMode === 'WAREHOUSE' ? (
|
||||
<Form form={shipForm} layout="vertical" size="small">
|
||||
<Form.Item
|
||||
name="warehouseId"
|
||||
label="履约仓库"
|
||||
rules={[{ required: true, message: '请选择仓库' }]}
|
||||
>
|
||||
<Select
|
||||
placeholder={warehouses.length ? '选择仓库' : '该城市暂无仓库'}
|
||||
options={warehouses.map((w) => ({ value: w.id, label: w.name }))}
|
||||
onChange={(id) => void onWarehouseChange(id)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="provider" label="承运商" rules={[{ required: true }]} initialValue="XFX">
|
||||
<Select options={[{ value: 'XFX', label: '小飞侠' }]} />
|
||||
</Form.Item>
|
||||
<Form.Item name="weight" label="重量(kg)" rules={[{ required: true }]}>
|
||||
<InputNumber min={0.01} step={0.5} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
<Form.Item name="payMode" label="付费方式" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '1', label: '寄付' },
|
||||
{ value: '2', label: '到付' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} placeholder="可选" />
|
||||
</Form.Item>
|
||||
<Collapse
|
||||
ghost
|
||||
items={[{
|
||||
key: 'from',
|
||||
label: '寄件信息(默认仓库,可修改)',
|
||||
children: (
|
||||
<>
|
||||
<Form.Item name="fromName" label="寄件人" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="fromMobile" label="寄件手机" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="fromAddress" label="寄件区域" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="fromAddressDetail" label="寄件详细地址" rules={[{ required: true }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Space>
|
||||
<Form.Item name="fromLng" label="经度">
|
||||
<InputNumber step={0.001} />
|
||||
</Form.Item>
|
||||
<Form.Item name="fromLat" label="纬度">
|
||||
<InputNumber step={0.001} />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
</>
|
||||
),
|
||||
}]}
|
||||
/>
|
||||
</Form>
|
||||
) : (
|
||||
<Form form={logisticsForm} layout="vertical" size="small">
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12, marginTop: 0 }}>
|
||||
与仓库端手动发货一致:填写快递公司与运单号即可完成发货
|
||||
</Typography.Paragraph>
|
||||
<Form.Item name="logisticsCompany" label="快递公司" rules={[{ required: true, message: '请填写快递公司' }]}>
|
||||
<Input placeholder="如 顺丰速运、京东物流" />
|
||||
</Form.Item>
|
||||
<Form.Item name="trackingNo" label="运单号" rules={[{ required: true, message: '请填写运单号' }]}>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="manualQueryUrl" label="物流查询链接(可选)">
|
||||
<Input placeholder="https://..." />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
title={`确认批量删除(${selectedOrders.length} 笔)`}
|
||||
open={batchDeleteOpen}
|
||||
@@ -501,13 +741,27 @@ export default function OrdersPage() {
|
||||
dataSource={selectedOrders}
|
||||
columns={[
|
||||
{ title: '订单号', dataIndex: 'orderNo', width: 170 },
|
||||
{
|
||||
title: '城市',
|
||||
width: 80,
|
||||
render: (_, row) => row.city?.name || '—',
|
||||
},
|
||||
{
|
||||
title: '商品',
|
||||
width: 160,
|
||||
ellipsis: true,
|
||||
render: (_, row) =>
|
||||
row.productName
|
||||
? `${row.productName}${row.quantity != null ? ` ×${row.quantity}` : ''}`
|
||||
: '—',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (s) => (
|
||||
<Tag color={ORDER_STATUS_COLORS[s] || 'default'}>{ORDER_STATUS_LABELS[s] || s}</Tag>
|
||||
),
|
||||
<Tag color={ORDER_STATUS_COLORS[s] || 'default'}>{ORDER_STATUS_LABELS[s] || s}</Tag>
|
||||
),
|
||||
},
|
||||
{ title: '实付', dataIndex: 'payAmount', width: 90, render: (v) => `¥${v}` },
|
||||
{ title: '收货人', dataIndex: 'receiverName', width: 90 },
|
||||
|
||||
Reference in New Issue
Block a user