Files
dukang/apps/admin-web/src/pages/OrdersPage.tsx
T
jacy e9537ce052
CI / verify (pull_request) Has been cancelled
后端仓库配送逻辑完善
2026-07-17 08:57:19 +08:00

821 lines
30 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 { useCallback, useEffect, useMemo, useState } from 'react';
import {
Alert,
Button,
Collapse,
Descriptions,
Drawer,
Form,
Input,
InputNumber,
Modal,
Radio,
Select,
Space,
Table,
Tag,
Typography,
message,
} from 'antd';
import type { ColumnsType } from 'antd/es/table';
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;
providerLabel: string;
fromName: string;
fromMobile: string;
fromAddress: string;
fromAddressDetail: string;
fromLng: number;
fromLat: number;
weight: number;
payMode: string;
};
type CityOption = { id: string; name: string; code: string };
type WarehouseOption = {
id: string;
name: string;
cityId?: string;
cityName?: string;
status?: string;
contactName: string;
contactPhone: string;
address: string;
lng?: number | null;
lat?: number | null;
};
type OrderDetail = AdminOrderRow & {
receiverAddress?: string;
receiverProvince?: string;
receiverCity?: string;
receiverDistrict?: string;
clientIp?: string | null;
ipProvince?: string | null;
ipCity?: string | null;
ipDistrict?: string | null;
gpsProvince?: string | null;
gpsCity?: string | null;
gpsDistrict?: string | null;
gpsLatitude?: number | null;
gpsLongitude?: number | null;
gpsAddress?: string | null;
productAmount?: number;
freightAmount?: number;
benefitAmount?: number;
paidAt?: string | null;
payExpireAt?: string | null;
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>>;
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();
const [logisticsForm] = Form.useForm();
const [data, setData] = useState<Paginated<AdminOrderRow> | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [detail, setDetail] = useState<OrderDetail | null>(null);
const [drawerOpen, setDrawerOpen] = useState(false);
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false);
const [batchDeleting, setBatchDeleting] = useState(false);
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)),
[data?.items, selectedRowKeys],
);
const load = useCallback(async () => {
setLoading(true);
try {
const values = form.getFieldsValue();
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);
} finally {
setLoading(false);
}
}, [form, page, pageSize]);
useEffect(() => {
void load();
}, [load]);
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 loadWarehouses() {
const res = await request<Paginated<WarehouseOption>>(
`/admin/city-warehouses?pageSize=${ADMIN_OPTIONS_PAGE_SIZE}&status=ACTIVE`,
).catch(() => null);
const rows = (res?.items ?? []).filter((w) => !w.status || w.status === 'ACTIVE');
setWarehouses(rows);
return rows;
}
async function openDetail(id: string) {
const res = await request<OrderDetail>(`/admin/orders/${id}`);
setDetail(res);
setDrawerOpen(true);
}
async function openShipModal(id: string) {
const res = await request<OrderDetail>(`/admin/orders/${id}`);
setShipTarget(res);
setDetail(res);
logisticsForm.setFieldsValue({
logisticsCompany: '',
trackingNo: '',
manualQueryUrl: '',
});
const rows = await loadWarehouses();
const preferredWarehouseId = res.fulfillmentWarehouseId || res.fulfillmentWarehouse?.id;
const preferred =
rows.find((w) => w.id === preferredWarehouseId) ||
rows.find((w) => w.cityId === (res.city?.id || res.cityId)) ||
rows[0];
const defaults = preferred
? warehouseToDefaults(preferred, shipDefaults)
: (shipDefaults ?? await request<ShipDefaults>('/admin/orders/ship-defaults').catch(() => null));
if (defaults) {
setShipDefaults(defaults);
applyShipDefaults(defaults, preferred?.id);
} else {
shipForm.setFieldsValue({ warehouseId: preferred?.id, provider: 'XFX' });
}
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 (!shipTarget) return;
const values = await shipForm.validateFields();
setShipping(true);
try {
const res = await request<OrderDetail>(`/admin/orders/${shipTarget.id}/ship`, {
method: 'POST',
body: JSON.stringify({
provider: values.provider || 'XFX',
warehouseId: values.warehouseId,
weight: values.weight,
payMode: values.payMode,
remark: values.remark,
fromName: values.fromName,
fromMobile: values.fromMobile,
fromAddress: values.fromAddress,
fromAddressDetail: values.fromAddressDetail,
fromLng: values.fromLng,
fromLat: values.fromLat,
}),
});
message.success('选仓发货成功');
setShipTarget(res);
setDetail(res);
setShipModalOpen(false);
void load();
} catch (e) {
message.error(e instanceof Error ? e.message : '发货失败');
} finally {
setShipping(false);
}
}
async function submitLogisticsShip() {
if (!shipTarget) return;
const values = await logisticsForm.validateFields(['logisticsCompany', 'trackingNo', 'manualQueryUrl']);
const logisticsCompany = String(values.logisticsCompany ?? '').trim();
const trackingNo = String(values.trackingNo ?? '').trim();
if (!logisticsCompany || !trackingNo) {
message.error('请填写快递公司与运单号');
return;
}
setLogisticsShipping(true);
try {
const res = await request<OrderDetail>(`/admin/orders/${shipTarget.id}/logistics-ship`, {
method: 'POST',
body: JSON.stringify({
logisticsCompany,
trackingNo,
manualQueryUrl: String(values.manualQueryUrl ?? '').trim() || undefined,
}),
});
message.success('快递单已录入');
setShipTarget(res);
setDetail(res);
setShipModalOpen(false);
void load();
} catch (e) {
message.error(e instanceof Error ? e.message : '填单失败');
} finally {
setLogisticsShipping(false);
}
}
async function confirmBatchDelete() {
if (!selectedRowKeys.length) return;
setBatchDeleting(true);
try {
const res = await request<{ deleted: number; message: string }>('/admin/orders/batch-delete', {
method: 'POST',
body: JSON.stringify({ ids: selectedRowKeys }),
});
message.success(res.message || `已删除 ${res.deleted} 笔订单`);
setBatchDeleteOpen(false);
setSelectedRowKeys([]);
if (detail && selectedRowKeys.includes(detail.id)) {
setDrawerOpen(false);
setDetail(null);
}
void load();
} catch (e) {
message.error(e instanceof Error ? e.message : '批量删除失败');
} finally {
setBatchDeleting(false);
}
}
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',
width: 100,
render: (s) => (
<Tag color={ORDER_STATUS_COLORS[s] || 'default'}>{ORDER_STATUS_LABELS[s] || s}</Tag>
),
},
{
title: '配送',
dataIndex: 'deliveryType',
width: 90,
render: (v) => (v === 'LOCAL' ? '同城' : '跨城'),
},
{ title: '实付', dataIndex: 'payAmount', width: 90, render: (v) => ${v}` },
{ title: '收货人', dataIndex: 'receiverName', width: 90 },
{ title: '手机', dataIndex: 'receiverPhone', width: 120 },
{
title: '用户',
dataIndex: ['user', 'userNo'],
width: 110,
render: (_, row) => row.user?.userNo || '—',
},
{
title: '快递',
width: 100,
render: (_, row) => row.delivery?.trackingNo || row.delivery?.provider || '—',
},
{
title: '下单时间',
dataIndex: 'createdAt',
width: 170,
render: (v) => new Date(v).toLocaleString('zh-CN'),
},
{
title: '操作',
width: 140,
fixed: 'right',
render: (_, row) => (
<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>
),
},
];
return (
<div>
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
<Typography.Title level={4} style={{ margin: 0 }}>订单监控</Typography.Title>
<Button
danger
disabled={!selectedRowKeys.length}
onClick={() => setBatchDeleteOpen(true)}
>
批量删除{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
</Button>
</Space>
<Form form={form} layout="inline" style={{ marginBottom: 16 }} onFinish={() => { setPage(1); void load(); }}>
<Form.Item name="orderNo" label="订单号">
<Input placeholder="DK..." allowClear />
</Form.Item>
<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>
<Form.Item>
<Space>
<Button type="primary" htmlType="submit">查询</Button>
<Button onClick={() => { form.resetFields(); setPage(1); void load(); }}>重置</Button>
</Space>
</Form.Item>
</Form>
<Table
rowKey="id"
loading={loading}
columns={columns}
dataSource={data?.items ?? []}
scroll={{ x: 1500 }}
rowSelection={{
selectedRowKeys,
onChange: (keys) => setSelectedRowKeys(keys as string[]),
}}
pagination={{
current: page,
pageSize,
total: data?.total ?? 0,
showSizeChanger: true,
onChange: (p, ps) => {
setPage(p);
setPageSize(ps);
},
}}
/>
<Drawer
title="订单详情"
width={640}
open={drawerOpen}
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}
style={{ width: 120 }}
options={Object.entries(ORDER_STATUS_LABELS).map(([value, label]) => ({ value, label }))}
onChange={async (status) => {
await request(`/admin/orders/${detail.id}/status`, {
method: 'PUT',
body: JSON.stringify({ status }),
});
message.success('状态已更新(调试)');
const res = await request<OrderDetail>(`/admin/orders/${detail.id}`);
setDetail(res);
void load();
}}
/>
</Space>
)}
>
{detail && (
<>
<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}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="用户">{detail.user?.userNo} / {detail.user?.nickname}</Descriptions.Item>
<Descriptions.Item label="商品额">¥{detail.productAmount}</Descriptions.Item>
<Descriptions.Item label="实付">¥{detail.payAmount}</Descriptions.Item>
<Descriptions.Item label="好客权益">¥{detail.benefitAmount}</Descriptions.Item>
<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>
</Descriptions>
<Descriptions column={1} bordered size="small" title="位置快照(方案C" style={{ marginTop: 16 }}>
<Descriptions.Item label="clientIp">{detail.clientIp || '—'}</Descriptions.Item>
<Descriptions.Item label="IP解析">{[detail.ipProvince, detail.ipCity, detail.ipDistrict].filter(Boolean).join(' / ') || '—'}</Descriptions.Item>
<Descriptions.Item label="GPS">{[detail.gpsProvince, detail.gpsCity, detail.gpsDistrict].filter(Boolean).join(' / ') || '—'}</Descriptions.Item>
<Descriptions.Item label="坐标">
{detail.gpsLatitude != null ? `${detail.gpsLatitude}, ${detail.gpsLongitude}` : '—'}
</Descriptions.Item>
</Descriptions>
{detail.delivery && (
<Descriptions column={1} bordered size="small" title="配送" style={{ marginTop: 16 }}>
<Descriptions.Item label="快递公司">
{detail.delivery.logisticsCompany ||
DELIVERY_PROVIDER_LABELS[detail.delivery.provider] ||
detail.delivery.provider}
</Descriptions.Item>
<Descriptions.Item label="运单号">{detail.delivery.trackingNo || '—'}</Descriptions.Item>
<Descriptions.Item label="三方单号">{detail.delivery.providerOrderNo || '—'}</Descriptions.Item>
{detail.delivery.manualQueryUrl && (
<Descriptions.Item label="查询链接">
<a href={detail.delivery.manualQueryUrl} target="_blank" rel="noreferrer">
打开物流查询
</a>
</Descriptions.Item>
)}
</Descriptions>
)}
{detail.statusLogs && detail.statusLogs.length > 0 && (
<>
<Typography.Title level={5} style={{ marginTop: 16 }}>状态流转</Typography.Title>
<Table
size="small"
rowKey="createdAt"
pagination={false}
dataSource={detail.statusLogs}
columns={[
{ title: '从', dataIndex: 'fromStatus', render: (v) => v || '—' },
{ title: '到', dataIndex: 'toStatus', render: (v) => ORDER_STATUS_LABELS[v] || v },
{ title: '时间', dataIndex: 'createdAt', render: (v) => new Date(v).toLocaleString('zh-CN') },
]}
/>
</>
)}
</>
)}
</Drawer>
<Modal
title={shipTarget ? `配送发货 · ${shipTarget.orderNo}` : '配送发货'}
open={shipModalOpen}
onCancel={() => setShipModalOpen(false)}
width={560}
destroyOnClose={false}
forceRender
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: '填写快递单号' },
]}
/>
<div style={{ display: shipMode === 'WAREHOUSE' ? 'block' : 'none' }}>
<Form form={shipForm} layout="vertical" size="small" preserve>
<Form.Item
name="warehouseId"
label="选择仓库"
rules={[{ required: true, message: '请选择仓库' }]}
>
<Select
showSearch
optionFilterProp="label"
placeholder={warehouses.length ? '选择仓库' : '暂无可用仓库'}
options={warehouses.map((w) => ({
value: w.id,
label: w.cityName ? `${w.cityName} · ${w.name}` : 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>
</div>
<div style={{ display: shipMode === 'EXPRESS' ? 'block' : 'none' }}>
<Form form={logisticsForm} layout="vertical" size="small" preserve>
<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 placeholder="请输入运单号" />
</Form.Item>
<Form.Item name="manualQueryUrl" label="物流查询链接(可选)">
<Input placeholder="https://..." />
</Form.Item>
</Form>
</div>
</>
)}
</Modal>
<Modal
title={`确认批量删除(${selectedOrders.length} 笔)`}
open={batchDeleteOpen}
okText="确认删除"
okButtonProps={{ danger: true, loading: batchDeleting }}
onOk={() => void confirmBatchDelete()}
onCancel={() => setBatchDeleteOpen(false)}
width={720}
destroyOnClose
>
<Alert
type="error"
showIcon
style={{ marginBottom: 16 }}
message="此操作不可恢复"
description="将删除所选订单及其配送单、权益券、核销记录等关联业务数据。订单状态流转等业务日志将保留。"
/>
<Table
size="small"
rowKey="id"
pagination={false}
scroll={{ x: 600, y: 280 }}
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>
),
},
{ title: '实付', dataIndex: 'payAmount', width: 90, render: (v) => ${v}` },
{ title: '收货人', dataIndex: 'receiverName', width: 90 },
{ title: '下单时间', dataIndex: 'createdAt', width: 160, render: fmtTime },
]}
/>
</Modal>
</div>
);
}