1238 lines
46 KiB
TypeScript
1238 lines
46 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||
import { Link } from 'react-router-dom';
|
||
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 HqProfile, type Paginated } from '../lib/api';
|
||
import {
|
||
ADMIN_OPTIONS_PAGE_SIZE,
|
||
DELIVERY_PROVIDER_LABELS,
|
||
ORDER_STATUS_COLORS,
|
||
ORDER_STATUS_LABELS,
|
||
ORDER_STATUS_OPERATOR_LABELS,
|
||
fmtTime,
|
||
} from '../lib/constants';
|
||
import { FULFILLMENT_HOLD_REASON_LABELS } from '@dukang/shared-types';
|
||
import ProxyOrderModal from '../components/ProxyOrderModal';
|
||
|
||
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 OrderRedeemRecord = {
|
||
id: string;
|
||
redeemNo: string;
|
||
amount: number;
|
||
settleAmount: number;
|
||
/** 本单权益券在该核销单中的分摊额 */
|
||
couponAmount?: number;
|
||
role?: 'PRIMARY' | 'SECONDARY';
|
||
createdAt: string;
|
||
store?: { id: string; name: string; cityName?: string | null } | null;
|
||
};
|
||
|
||
type OrderRedeemSummary = {
|
||
couponNo: string;
|
||
totalAmount: number;
|
||
usedAmount: number;
|
||
balance: number;
|
||
status: string;
|
||
redeemCount: number;
|
||
redeemRecordSum: number;
|
||
};
|
||
|
||
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;
|
||
operator?: string | null;
|
||
remark?: string | null;
|
||
createdAt: string;
|
||
}>;
|
||
benefitCoupons?: Array<Record<string, unknown>>;
|
||
redeemSummary?: OrderRedeemSummary | null;
|
||
redeemRecords?: OrderRedeemRecord[];
|
||
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 [profile, setProfile] = useState<HqProfile | null>(null);
|
||
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 [redeemDetail, setRedeemDetail] = useState<Record<string, unknown> | null>(null);
|
||
const [redeemDrawerOpen, setRedeemDrawerOpen] = useState(false);
|
||
const [redeemDetailLoading, setRedeemDetailLoading] = useState(false);
|
||
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
||
const [batchDeleteOpen, setBatchDeleteOpen] = useState(false);
|
||
const [batchDeleting, setBatchDeleting] = useState(false);
|
||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||
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 [proxyOpen, setProxyOpen] = useState(false);
|
||
const canDeleteOrders = (profile?.permissionKeys ?? []).includes('orders_delete');
|
||
const canProxyOrder = (profile?.permissionKeys ?? []).includes('orders');
|
||
|
||
const selectedOrders = useMemo(
|
||
() => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)),
|
||
[data?.items, selectedRowKeys],
|
||
);
|
||
|
||
useEffect(() => {
|
||
request<HqProfile>('/admin/auth/me').then(setProfile).catch(() => {});
|
||
}, []);
|
||
|
||
async function openRedeemDetail(redeemId: string) {
|
||
setRedeemDetailLoading(true);
|
||
setRedeemDrawerOpen(true);
|
||
try {
|
||
const res = await request<Record<string, unknown>>(`/admin/redeem-records/${redeemId}`);
|
||
setRedeemDetail(res);
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '加载核销详情失败');
|
||
setRedeemDrawerOpen(false);
|
||
} finally {
|
||
setRedeemDetailLoading(false);
|
||
}
|
||
}
|
||
|
||
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.orderType) qs.set('orderType', values.orderType);
|
||
if (values.cityId) qs.set('cityId', values.cityId);
|
||
if (values.receiverPhone) qs.set('receiverPhone', values.receiverPhone);
|
||
if (values.fulfillmentHold) qs.set('fulfillmentHold', 'true');
|
||
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);
|
||
}
|
||
}
|
||
|
||
function askDeleteOrder(id: string, orderNo?: string) {
|
||
Modal.confirm({
|
||
title: '确认删除订单?',
|
||
content: (
|
||
<div>
|
||
<p>将永久删除订单{orderNo ? ` ${orderNo}` : ''} 及其配送、权益券、核销、发票等关联数据。</p>
|
||
<p style={{ color: 'var(--ant-color-error)', marginBottom: 0 }}>此操作不可恢复,请确认不是真实线上订单。</p>
|
||
</div>
|
||
),
|
||
okText: '确认删除',
|
||
okType: 'danger',
|
||
cancelText: '取消',
|
||
onOk: () => deleteOrder(id),
|
||
});
|
||
}
|
||
|
||
async function deleteOrder(id: string) {
|
||
setDeletingId(id);
|
||
try {
|
||
const res = await request<{ deleted: number; message: string }>(`/admin/orders/${id}`, {
|
||
method: 'DELETE',
|
||
});
|
||
message.success(res.message || '订单已删除');
|
||
if (detail?.id === id) {
|
||
setDrawerOpen(false);
|
||
setDetail(null);
|
||
}
|
||
setSelectedRowKeys((prev) => prev.filter((k) => k !== id));
|
||
void load();
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '删除失败');
|
||
throw e;
|
||
} finally {
|
||
setDeletingId(null);
|
||
}
|
||
}
|
||
|
||
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: 140,
|
||
render: (s, row) => (
|
||
<Space size={4} wrap>
|
||
<Tag color={ORDER_STATUS_COLORS[s] || 'default'}>{ORDER_STATUS_LABELS[s] || s}</Tag>
|
||
{row.fulfillmentHold ? <Tag color="orange">大单待确认</Tag> : null}
|
||
{row.orderType === 'PROXY' || row.isProxyOrder ? (
|
||
<Tag color="purple" title={row.proxyPartnerName || undefined}>
|
||
代下单
|
||
</Tag>
|
||
) : null}
|
||
</Space>
|
||
),
|
||
},
|
||
{
|
||
title: '配送',
|
||
dataIndex: 'deliveryType',
|
||
width: 90,
|
||
render: (v) =>
|
||
v === 'ON_SITE_PICKUP' ? '现场提货' : 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: 180,
|
||
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>
|
||
)}
|
||
{canDeleteOrders && (
|
||
<Button
|
||
type="link"
|
||
size="small"
|
||
danger
|
||
loading={deletingId === row.id}
|
||
onClick={() => askDeleteOrder(row.id, row.orderNo)}
|
||
>
|
||
删除
|
||
</Button>
|
||
)}
|
||
</Space>
|
||
),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<div>
|
||
<Space style={{ marginBottom: 16, width: '100%', justifyContent: 'space-between' }}>
|
||
<Typography.Title level={4} style={{ margin: 0 }}>订单监控</Typography.Title>
|
||
<Space>
|
||
{canProxyOrder ? (
|
||
<Button type="primary" onClick={() => setProxyOpen(true)}>
|
||
代下单
|
||
</Button>
|
||
) : null}
|
||
{canDeleteOrders ? (
|
||
<Button
|
||
danger
|
||
disabled={!selectedRowKeys.length}
|
||
onClick={() => setBatchDeleteOpen(true)}
|
||
>
|
||
批量删除{selectedRowKeys.length ? ` (${selectedRowKeys.length})` : ''}
|
||
</Button>
|
||
) : null}
|
||
</Space>
|
||
</Space>
|
||
|
||
{!canDeleteOrders && profile ? (
|
||
<Alert
|
||
type="info"
|
||
showIcon
|
||
style={{ marginBottom: 16 }}
|
||
message="当前账号无「删除订单」权限"
|
||
description={
|
||
<span>
|
||
非超管账号需在 <Link to="/hq-permissions">权限分配</Link> 中勾选危险操作「删除订单」。
|
||
</span>
|
||
}
|
||
/>
|
||
) : null}
|
||
|
||
<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="orderType" label="类型">
|
||
<Select
|
||
allowClear
|
||
style={{ width: 120 }}
|
||
placeholder="全部"
|
||
options={[
|
||
{ value: 'NORMAL', label: '普通订单' },
|
||
{ value: 'PROXY', 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 name="fulfillmentHold" label="大单拦截" valuePropName="checked">
|
||
<Select
|
||
allowClear
|
||
style={{ width: 140 }}
|
||
placeholder="全部"
|
||
options={[{ value: true, label: '仅待确认大单' }]}
|
||
/>
|
||
</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={canDeleteOrders ? {
|
||
selectedRowKeys,
|
||
onChange: (keys) => setSelectedRowKeys(keys as string[]),
|
||
} : undefined}
|
||
pagination={{
|
||
current: page,
|
||
pageSize,
|
||
total: data?.total ?? 0,
|
||
showSizeChanger: true,
|
||
onChange: (p, ps) => {
|
||
setPage(p);
|
||
setPageSize(ps);
|
||
},
|
||
}}
|
||
/>
|
||
|
||
<Drawer
|
||
title="订单详情"
|
||
width={720}
|
||
open={drawerOpen}
|
||
onClose={() => setDrawerOpen(false)}
|
||
extra={detail && (
|
||
<Space>
|
||
{canShip(detail) && (
|
||
<Button type="primary" onClick={() => void openShipModal(detail.id)}>
|
||
配送发货
|
||
</Button>
|
||
)}
|
||
{canDeleteOrders && (
|
||
<Button
|
||
danger
|
||
loading={deletingId === detail.id}
|
||
onClick={() => askDeleteOrder(detail.id, detail.orderNo)}
|
||
>
|
||
删除订单
|
||
</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="状态">
|
||
<Space>
|
||
<Tag color={ORDER_STATUS_COLORS[detail.status] || 'default'}>
|
||
{ORDER_STATUS_LABELS[detail.status] || detail.status}
|
||
</Tag>
|
||
{detail.fulfillmentHold ? (
|
||
<Tag color="orange">
|
||
{FULFILLMENT_HOLD_REASON_LABELS[detail.fulfillmentHoldReason || ''] ||
|
||
'大单待确认'}
|
||
</Tag>
|
||
) : null}
|
||
{detail.orderType === 'PROXY' || detail.isProxyOrder ? (
|
||
<Tag color="purple">代下单</Tag>
|
||
) : null}
|
||
</Space>
|
||
</Descriptions.Item>
|
||
{detail.orderType === 'PROXY' || detail.isProxyOrder || detail.proxyPartnerName ? (
|
||
<Descriptions.Item label="代下单人">
|
||
{[detail.proxyPartnerName, detail.proxyPartnerPhone].filter(Boolean).join(' / ') || '—'}
|
||
</Descriptions.Item>
|
||
) : null}
|
||
<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>
|
||
|
||
<Typography.Title level={5} style={{ marginTop: 16, marginBottom: 8 }}>
|
||
权益核销
|
||
</Typography.Title>
|
||
{detail.redeemSummary ? (
|
||
<>
|
||
<Descriptions column={2} bordered size="small" style={{ marginBottom: 12 }}>
|
||
<Descriptions.Item label="权益券号">{detail.redeemSummary.couponNo}</Descriptions.Item>
|
||
<Descriptions.Item label="券状态">{detail.redeemSummary.status}</Descriptions.Item>
|
||
<Descriptions.Item label="权益总额">
|
||
¥{Number(detail.redeemSummary.totalAmount).toFixed(2)}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="已核销">
|
||
<Typography.Text type="danger" strong>
|
||
¥{Number(detail.redeemSummary.usedAmount).toFixed(2)}
|
||
</Typography.Text>
|
||
<Typography.Text type="secondary" style={{ marginLeft: 8 }}>
|
||
({detail.redeemSummary.redeemCount} 笔核销单)
|
||
</Typography.Text>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="剩余余额">
|
||
¥{Number(detail.redeemSummary.balance).toFixed(2)}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="核销单合计额">
|
||
¥{Number(detail.redeemSummary.redeemRecordSum).toFixed(2)}
|
||
<Typography.Text type="secondary" style={{ marginLeft: 8 }}>
|
||
(本券分摊合计)
|
||
</Typography.Text>
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
<Table
|
||
size="small"
|
||
rowKey="id"
|
||
pagination={false}
|
||
locale={{ emptyText: '暂无关联核销单' }}
|
||
dataSource={detail.redeemRecords ?? []}
|
||
columns={[
|
||
{ title: '核销号', dataIndex: 'redeemNo', width: 160, ellipsis: true },
|
||
{
|
||
title: '角色',
|
||
dataIndex: 'role',
|
||
width: 70,
|
||
render: (v: string | undefined) =>
|
||
v === 'SECONDARY' ? <Tag color="orange">次券</Tag> : <Tag color="blue">主券</Tag>,
|
||
},
|
||
{
|
||
title: '门店',
|
||
dataIndex: ['store', 'name'],
|
||
ellipsis: true,
|
||
render: (v: string | undefined, row) =>
|
||
v ? `${v}${row.store?.cityName ? `(${row.store.cityName})` : ''}` : '—',
|
||
},
|
||
{
|
||
title: '本券分摊',
|
||
dataIndex: 'couponAmount',
|
||
width: 95,
|
||
render: (v: number | undefined, row) =>
|
||
`¥${Number(v ?? row.amount).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '核销总额',
|
||
dataIndex: 'amount',
|
||
width: 90,
|
||
render: (v: number) => `¥${Number(v).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '结算额',
|
||
dataIndex: 'settleAmount',
|
||
width: 90,
|
||
render: (v: number) => `¥${Number(v).toFixed(2)}`,
|
||
},
|
||
{
|
||
title: '时间',
|
||
dataIndex: 'createdAt',
|
||
width: 150,
|
||
render: (v: string) => fmtTime(v),
|
||
},
|
||
{
|
||
title: '操作',
|
||
width: 70,
|
||
render: (_, row) => (
|
||
<Button type="link" size="small" onClick={() => void openRedeemDetail(row.id)}>
|
||
详情
|
||
</Button>
|
||
),
|
||
},
|
||
]}
|
||
/>
|
||
</>
|
||
) : (
|
||
<Typography.Text type="secondary">该订单尚未生成权益券,无核销记录</Typography.Text>
|
||
)}
|
||
|
||
<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={(_, i) => String(i)}
|
||
pagination={false}
|
||
dataSource={detail.statusLogs}
|
||
columns={[
|
||
{
|
||
title: '从',
|
||
dataIndex: 'fromStatus',
|
||
width: 100,
|
||
render: (v) => (v ? ORDER_STATUS_LABELS[v] || v : '—'),
|
||
},
|
||
{
|
||
title: '到',
|
||
dataIndex: 'toStatus',
|
||
width: 100,
|
||
render: (v) => ORDER_STATUS_LABELS[v] || v,
|
||
},
|
||
{
|
||
title: '操作人',
|
||
dataIndex: 'operator',
|
||
width: 120,
|
||
render: (v) => ORDER_STATUS_OPERATOR_LABELS[v] || v || '—',
|
||
},
|
||
{
|
||
title: '备注',
|
||
dataIndex: 'remark',
|
||
ellipsis: true,
|
||
render: (v) => v || '—',
|
||
},
|
||
{
|
||
title: '时间',
|
||
dataIndex: 'createdAt',
|
||
width: 170,
|
||
render: (v) => new Date(v).toLocaleString('zh-CN'),
|
||
},
|
||
]}
|
||
/>
|
||
</>
|
||
)}
|
||
</>
|
||
)}
|
||
</Drawer>
|
||
|
||
<Drawer
|
||
title="核销单详情"
|
||
width={520}
|
||
open={redeemDrawerOpen}
|
||
onClose={() => {
|
||
setRedeemDrawerOpen(false);
|
||
setRedeemDetail(null);
|
||
}}
|
||
destroyOnClose
|
||
>
|
||
{redeemDetailLoading ? (
|
||
<Typography.Text type="secondary">加载中…</Typography.Text>
|
||
) : redeemDetail ? (
|
||
<Descriptions column={1} bordered size="small">
|
||
<Descriptions.Item label="核销号">{String(redeemDetail.redeemNo ?? '—')}</Descriptions.Item>
|
||
<Descriptions.Item label="核销额">
|
||
¥{Number(redeemDetail.amount ?? 0).toFixed(2)}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="结算额">
|
||
¥{Number(redeemDetail.settleAmount ?? 0).toFixed(2)}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="时间">{fmtTime(String(redeemDetail.createdAt ?? ''))}</Descriptions.Item>
|
||
<Descriptions.Item label="用户">
|
||
{String((redeemDetail.user as { userNo?: string } | undefined)?.userNo ?? '—')}
|
||
{(redeemDetail.user as { phone?: string | null } | undefined)?.phone
|
||
? ` / ${(redeemDetail.user as { phone?: string | null }).phone}`
|
||
: ''}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="门店">
|
||
{String((redeemDetail.store as { name?: string } | undefined)?.name ?? '—')}
|
||
{(redeemDetail.store as { cityName?: string } | undefined)?.cityName
|
||
? `(${(redeemDetail.store as { cityName?: string }).cityName})`
|
||
: ''}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="门店地址">
|
||
{String((redeemDetail.store as { address?: string } | undefined)?.address ?? '—')}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="合伙人">
|
||
{String(
|
||
(redeemDetail.store as { partnerAccount?: { companyName?: string } } | undefined)
|
||
?.partnerAccount?.companyName ?? '—',
|
||
)}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="权益券号">
|
||
{String((redeemDetail.coupon as { couponNo?: string } | undefined)?.couponNo ?? '—')}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="关联订单">
|
||
{String(
|
||
(redeemDetail.coupon as { order?: { orderNo?: string } } | undefined)?.order?.orderNo ??
|
||
'—',
|
||
)}
|
||
</Descriptions.Item>
|
||
{Array.isArray(redeemDetail.allocations) &&
|
||
(redeemDetail.allocations as unknown[]).length > 0 ? (
|
||
<Descriptions.Item label="券分摊">
|
||
{(
|
||
redeemDetail.allocations as Array<{
|
||
couponNo?: string;
|
||
orderNo?: string | null;
|
||
amount?: number;
|
||
sortOrder?: number;
|
||
}>
|
||
)
|
||
.map((a, idx) => {
|
||
const role = idx === 0 ? '主' : '次';
|
||
return `${role} ${a.couponNo ?? '—'}¥${Number(a.amount ?? 0).toFixed(2)}${
|
||
a.orderNo ? `(订单 ${a.orderNo})` : ''
|
||
}`;
|
||
})
|
||
.join(';')}
|
||
</Descriptions.Item>
|
||
) : null}
|
||
{redeemDetail.payout ? (
|
||
<Descriptions.Item label="门店结算单">
|
||
¥{Number((redeemDetail.payout as { settleAmount?: number }).settleAmount ?? 0).toFixed(2)}
|
||
{' / '}
|
||
{String((redeemDetail.payout as { status?: string }).status ?? '—')}
|
||
</Descriptions.Item>
|
||
) : null}
|
||
{redeemDetail.rating ? (
|
||
<Descriptions.Item label="评价">
|
||
服务 {(redeemDetail.rating as { serviceScore?: number }).serviceScore ?? '—'} 分 / 环境{' '}
|
||
{(redeemDetail.rating as { envScore?: number }).envScore ?? '—'} 分
|
||
</Descriptions.Item>
|
||
) : null}
|
||
</Descriptions>
|
||
) : null}
|
||
</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 && (
|
||
<>
|
||
{shipTarget.fulfillmentHold ? (
|
||
<Alert
|
||
type="warning"
|
||
showIcon
|
||
style={{ marginBottom: 12 }}
|
||
message="大单已拦截自动推小飞侠"
|
||
description={
|
||
FULFILLMENT_HOLD_REASON_LABELS[shipTarget.fulfillmentHoldReason || ''] ||
|
||
'≥10箱订单需总部确认:可选仓推小飞侠,或改用快递自配送。'
|
||
}
|
||
/>
|
||
) : null}
|
||
<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>
|
||
|
||
<ProxyOrderModal
|
||
open={proxyOpen}
|
||
onClose={() => setProxyOpen(false)}
|
||
onSuccess={(order) => {
|
||
setProxyOpen(false);
|
||
void load();
|
||
void openDetail(order.id);
|
||
}}
|
||
/>
|
||
</div>
|
||
);
|
||
}
|