import { useCallback, useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; import { Alert, Button, Checkbox, 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 { triggerBigScreenDemo } from '../lib/admin-events'; 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'; import OrderTrackDrawer from '../components/OrderTrackDrawer'; 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 | null; statusLogs?: Array<{ fromStatus: string | null; toStatus: string; operator?: string | null; remark?: string | null; createdAt: string; }>; benefitCoupons?: Array>; 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(null); 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 [redeemDetail, setRedeemDetail] = useState | null>(null); const [redeemDrawerOpen, setRedeemDrawerOpen] = useState(false); const [redeemDetailLoading, setRedeemDetailLoading] = useState(false); const [selectedRowKeys, setSelectedRowKeys] = useState([]); const [batchDeleteOpen, setBatchDeleteOpen] = useState(false); const [batchDeleting, setBatchDeleting] = useState(false); const [deletingId, setDeletingId] = useState(null); const [shipDefaults, setShipDefaults] = useState(null); const [shipping, setShipping] = useState(false); const [logisticsShipping, setLogisticsShipping] = useState(false); const [cities, setCities] = useState([]); const [shipModalOpen, setShipModalOpen] = useState(false); const [shipTarget, setShipTarget] = useState(null); const [shipMode, setShipMode] = useState('EXPRESS'); const [warehouses, setWarehouses] = useState([]); const [proxyOpen, setProxyOpen] = useState(false); const [trackOpen, setTrackOpen] = useState(false); const canDeleteOrders = (profile?.permissionKeys ?? []).includes('orders_delete'); const canProxyOrder = (profile?.permissionKeys ?? []).includes('orders'); const isSuperAdmin = profile?.adminRole === 'SUPER_ADMIN'; const selectedOrders = useMemo( () => (data?.items ?? []).filter((row) => selectedRowKeys.includes(row.id)), [data?.items, selectedRowKeys], ); useEffect(() => { request('/admin/auth/me').then(setProfile).catch(() => {}); }, []); async function openRedeemDetail(redeemId: string) { setRedeemDetailLoading(true); setRedeemDrawerOpen(true); try { const res = await request>(`/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'); if (values.excludeTest) qs.set('excludeTest', 'true'); 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(() => {}); void request>(`/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>( `/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(`/admin/orders/${id}`); setDetail(res); setDrawerOpen(true); } async function openShipModal(id: string) { const res = await request(`/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('/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(`/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(`/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: (

将永久删除订单{orderNo ? ` ${orderNo}` : ''} 及其配送、权益券、核销、发票等关联数据。

此操作不可恢复,请确认不是真实线上订单。

), 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 = [ { title: '订单号', dataIndex: 'orderNo', width: 200, render: (v, row) => ( {v} {row.isTest ? 测试 : null} ), }, { 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 ( {name}{qty} ); }, }, { title: '状态', dataIndex: 'status', width: 140, render: (s, row) => ( {ORDER_STATUS_LABELS[s] || s} {row.fulfillmentHold ? 大单待确认 : null} {row.orderType === 'PROXY' || row.isProxyOrder ? ( 代下单 ) : null} ), }, { 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) => ( {canShip(row) && ( )} {canDeleteOrders && ( )} ), }, ]; return (
订单监控 {isSuperAdmin ? ( ) : null} {canProxyOrder ? ( ) : null} {canDeleteOrders ? ( ) : null} {!canDeleteOrders && profile ? ( 非超管账号需在 权限分配 中勾选危险操作「删除订单」。 } /> ) : null}
{ setPage(1); void load(); }}> ({ value, label }))} onChange={async (status) => { await request(`/admin/orders/${detail.id}/status`, { method: 'PUT', body: JSON.stringify({ status }), }); message.success('状态已更新(调试)'); const res = await request(`/admin/orders/${detail.id}`); setDetail(res); void load(); }} /> )} > {detail && ( <> {detail.orderNo} {detail.city?.name || '—'} {detail.city?.code ? `(${detail.city.code})` : ''} {detail.fulfillmentWarehouse?.name || '未分配'} {ORDER_STATUS_LABELS[detail.status] || detail.status} {detail.fulfillmentHold ? ( {FULFILLMENT_HOLD_REASON_LABELS[detail.fulfillmentHoldReason || ''] || '大单待确认'} ) : null} {detail.orderType === 'PROXY' || detail.isProxyOrder ? ( 代下单 ) : null} {detail.orderType === 'PROXY' || detail.isProxyOrder || detail.proxyPartnerName ? ( {[detail.proxyPartnerName, detail.proxyPartnerPhone].filter(Boolean).join(' / ') || '—'} ) : null} {detail.user?.userNo} / {detail.user?.nickname} ¥{detail.productAmount} ¥{detail.payAmount} ¥{detail.benefitAmount} {new Date(detail.createdAt).toLocaleString('zh-CN')} 商品 String(i)} pagination={false} dataSource={orderProductRows(detail)} locale={{ emptyText: '无商品信息' }} columns={[ { title: '商品', dataIndex: 'productName', render: (name: string, row) => ( {row.productImage ? ( ) : null} {name || '—'} {row.productSpec ? ( {row.productSpec} ) : null} ), }, { title: '单价', dataIndex: 'unitPrice', width: 90, render: (v: number) => `¥${v}`, }, { title: '数量', dataIndex: 'quantity', width: 70, }, ]} /> {detail.receiverName} {detail.receiverPhone} {detail.receiverAddress} 权益核销 {detail.redeemSummary ? ( <> {detail.redeemSummary.couponNo} {detail.redeemSummary.status} ¥{Number(detail.redeemSummary.totalAmount).toFixed(2)} ¥{Number(detail.redeemSummary.usedAmount).toFixed(2)} ({detail.redeemSummary.redeemCount} 笔核销单) ¥{Number(detail.redeemSummary.balance).toFixed(2)} ¥{Number(detail.redeemSummary.redeemRecordSum).toFixed(2)} (本券分摊合计)
v === 'SECONDARY' ? 次券 : 主券, }, { 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) => ( ), }, ]} /> ) : ( 该订单尚未生成权益券,无核销记录 )} {detail.clientIp || '—'} {[detail.ipProvince, detail.ipCity, detail.ipDistrict].filter(Boolean).join(' / ') || '—'} {[detail.gpsProvince, detail.gpsCity, detail.gpsDistrict].filter(Boolean).join(' / ') || '—'} {detail.gpsLatitude != null ? `${detail.gpsLatitude}, ${detail.gpsLongitude}` : '—'} {detail.delivery && ( 配送 } style={{ marginTop: 16 }} > {detail.delivery.logisticsCompany || DELIVERY_PROVIDER_LABELS[detail.delivery.provider] || detail.delivery.provider} {detail.delivery.trackingNo || '—'} {detail.delivery.providerOrderNo || '—'} {detail.delivery.manualQueryUrl && ( 打开物流查询 )} )} {detail.statusLogs && detail.statusLogs.length > 0 && ( <> 状态流转
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'), }, ]} /> )} )} { setRedeemDrawerOpen(false); setRedeemDetail(null); }} destroyOnClose > {redeemDetailLoading ? ( 加载中… ) : redeemDetail ? ( {String(redeemDetail.redeemNo ?? '—')} ¥{Number(redeemDetail.amount ?? 0).toFixed(2)} ¥{Number(redeemDetail.settleAmount ?? 0).toFixed(2)} {fmtTime(String(redeemDetail.createdAt ?? ''))} {String((redeemDetail.user as { userNo?: string } | undefined)?.userNo ?? '—')} {(redeemDetail.user as { phone?: string | null } | undefined)?.phone ? ` / ${(redeemDetail.user as { phone?: string | null }).phone}` : ''} {String((redeemDetail.store as { name?: string } | undefined)?.name ?? '—')} {(redeemDetail.store as { cityName?: string } | undefined)?.cityName ? `(${(redeemDetail.store as { cityName?: string }).cityName})` : ''} {String((redeemDetail.store as { address?: string } | undefined)?.address ?? '—')} {String( (redeemDetail.store as { partnerAccount?: { companyName?: string } } | undefined) ?.partnerAccount?.companyName ?? '—', )} {String((redeemDetail.coupon as { couponNo?: string } | undefined)?.couponNo ?? '—')} {String( (redeemDetail.coupon as { order?: { orderNo?: string } } | undefined)?.order?.orderNo ?? '—', )} {Array.isArray(redeemDetail.allocations) && (redeemDetail.allocations as unknown[]).length > 0 ? ( {( 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(';')} ) : null} {redeemDetail.payout ? ( ¥{Number((redeemDetail.payout as { settleAmount?: number }).settleAmount ?? 0).toFixed(2)} {' / '} {String((redeemDetail.payout as { status?: string }).status ?? '—')} ) : null} {redeemDetail.rating ? ( 服务 {(redeemDetail.rating as { serviceScore?: number }).serviceScore ?? '—'} 分 / 环境{' '} {(redeemDetail.rating as { envScore?: number }).envScore ?? '—'} 分 ) : null} ) : null} setShipModalOpen(false)} width={560} destroyOnClose={false} forceRender footer={ {shipMode === 'WAREHOUSE' ? ( ) : ( )} } > {shipTarget && ( <> {shipTarget.fulfillmentHold ? ( ) : null} {shipTarget.city?.name || '—'} {shipTarget.receiverName} {shipTarget.receiverPhone} {shipTarget.productName || '—'} {shipTarget.quantity != null ? ` ×${shipTarget.quantity}` : ''} setShipMode(e.target.value as ShipMode)} optionType="button" buttonStyle="solid" style={{ marginBottom: 16 }} options={[ { value: 'WAREHOUSE', label: '选仓配送' }, { value: 'EXPRESS', label: '填写快递单号' }, ]} />
), }]} />
填写快递公司与运单号即可完成发货
)}
void confirmBatchDelete()} onCancel={() => setBatchDeleteOpen(false)} width={720} destroyOnClose >
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) => ( {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 }, ]} /> setProxyOpen(false)} onSuccess={(order) => { setProxyOpen(false); void load(); void openDetail(order.id); }} /> setTrackOpen(false)} /> ); }