import { useCallback, useEffect, useMemo, useState } from 'react'; import { Alert, Button, Collapse, Descriptions, Drawer, Form, Input, InputNumber, Modal, Select, Space, Table, Tag, Typography, 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'; type ShipDefaults = { provider: string; providerLabel: string; fromName: string; fromMobile: string; fromAddress: string; fromAddressDetail: string; fromLng: number; fromLat: number; weight: number; payMode: string; }; 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; items?: Array>; payment?: Record | null; statusLogs?: Array<{ fromStatus: string | null; toStatus: string; createdAt: string }>; benefitCoupons?: Array>; city?: { name: string; code: string }; }; export default function OrdersPage() { const [form] = Form.useForm(); const [shipForm] = Form.useForm(); const [data, setData] = useState | null>(null); const [loading, setLoading] = useState(false); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(20); const [detail, setDetail] = useState(null); const [drawerOpen, setDrawerOpen] = useState(false); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchDeleteOpen, setBatchDeleteOpen] = useState(false); const [batchDeleting, setBatchDeleting] = useState(false); const [shipDefaults, setShipDefaults] = useState(null); const [shipping, setShipping] = useState(false); 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.receiverPhone) qs.set('receiverPhone', values.receiverPhone); const res = await request>(`/admin/orders?${qs}`); setData(res); } finally { setLoading(false); } }, [form, page, pageSize]); useEffect(() => { void load(); }, [load]); useEffect(() => { void request('/admin/orders/ship-defaults').then(setShipDefaults).catch(() => {}); }, []); async function openDetail(id: string) { const res = await request(`/admin/orders/${id}`); setDetail(res); setDrawerOpen(true); const defaults = shipDefaults ?? await request('/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, }); } } async function submitShip() { if (!detail) return; const values = await shipForm.validateFields(); setShipping(true); try { const res = await request(`/admin/orders/${detail.id}/ship`, { method: 'POST', body: JSON.stringify(values), }); message.success('发货成功'); setDetail(res); void load(); } catch (e) { message.error(e instanceof Error ? e.message : '发货失败'); } finally { setShipping(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 = [ { title: '订单号', dataIndex: 'orderNo', width: 180 }, { title: '状态', dataIndex: 'status', width: 100, render: (s) => ( {ORDER_STATUS_LABELS[s] || s} ), }, { 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: 80, render: (_, row) => ( ), }, ]; return (
订单监控
{ setPage(1); void load(); }}>
setSelectedRowKeys(keys as string[]), }} pagination={{ current: page, pageSize, total: data?.total ?? 0, showSizeChanger: true, onChange: (p, ps) => { setPage(p); setPageSize(ps); }, }} /> setDrawerOpen(false)} extra={detail && ( 调试改状态 ), }]} /> )} {detail.statusLogs && detail.statusLogs.length > 0 && ( <> 状态流转
v || '—' }, { title: '到', dataIndex: 'toStatus', render: (v) => ORDER_STATUS_LABELS[v] || v }, { title: '时间', dataIndex: 'createdAt', render: (v) => new Date(v).toLocaleString('zh-CN') }, ]} /> )} )} void confirmBatchDelete()} onCancel={() => setBatchDeleteOpen(false)} width={720} destroyOnClose >
( {ORDER_STATUS_LABELS[s] || s} ), }, { title: '实付', dataIndex: 'payAmount', width: 90, render: (v) => `¥${v}` }, { title: '收货人', dataIndex: 'receiverName', width: 90 }, { title: '下单时间', dataIndex: 'createdAt', width: 160, render: fmtTime }, ]} /> ); }