f6d97b4ee8
CI / verify (pull_request) Has been cancelled
同城有仓按仓库绑定承运商自动推单或自管填单,无仓/跨城走总部快递;新增仓配注册表、FulfillmentService 及三端运单追踪。 Co-authored-by: Cursor <cursoragent@cursor.com>
227 lines
9.9 KiB
TypeScript
227 lines
9.9 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
|
import { isLoggedIn, request } from '../lib/api';
|
|
|
|
type DateFilter = 'today' | '7d' | '30d';
|
|
type StatusFilter = 'ALL' | 'PENDING_SHIP' | 'SHIPPING' | 'COMPLETED' | 'ABNORMAL';
|
|
|
|
function orderStatusLabel(status: string) {
|
|
const s = String(status).toUpperCase();
|
|
if (s.includes('SHIP') || s === 'OUT_WAREHOUSE') return { label: '运输中', color: 'var(--color-status-blue)' };
|
|
if (s === 'COMPLETED') return { label: '已完成', color: 'var(--color-success-green)' };
|
|
if (s.includes('PENDING') || s === 'PAID') return { label: '待发货', color: 'var(--color-secondary)' };
|
|
if (s.includes('ERROR') || s.includes('ABNORMAL')) return { label: '异常', color: 'var(--color-error)' };
|
|
return { label: status, color: 'var(--color-subtle-gray)' };
|
|
}
|
|
|
|
const DATE_FILTERS: { key: DateFilter; label: string }[] = [
|
|
{ key: 'today', label: '今日' },
|
|
{ key: '7d', label: '近7天' },
|
|
{ key: '30d', label: '近30天' },
|
|
];
|
|
|
|
const STATUS_FILTERS: { key: StatusFilter; label: string }[] = [
|
|
{ key: 'ALL', label: '全部' },
|
|
{ key: 'PENDING_SHIP', label: '待发货' },
|
|
{ key: 'SHIPPING', label: '运输中' },
|
|
{ key: 'COMPLETED', label: '已完成' },
|
|
{ key: 'ABNORMAL', label: '异常' },
|
|
];
|
|
|
|
type OrderListPageProps = {
|
|
/** Tab 根页:无返回顶栏 */
|
|
tabRoot?: boolean;
|
|
};
|
|
|
|
export default function OrderListPage({ tabRoot = false }: OrderListPageProps) {
|
|
const navigate = useNavigate();
|
|
const [data, setData] = useState<{ list: Array<Record<string, unknown>> }>({ list: [] });
|
|
const [tab, setTab] = useState<'orders' | 'coupons'>('orders');
|
|
const [dateFilter, setDateFilter] = useState<DateFilter>('today');
|
|
const [statusFilter, setStatusFilter] = useState<StatusFilter>('ALL');
|
|
const [shippingId, setShippingId] = useState<string | null>(null);
|
|
const [shipForm, setShipForm] = useState({ logisticsCompany: '', trackingNo: '', manualQueryUrl: '' });
|
|
const [shipModalId, setShipModalId] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
document.title = '订单管理';
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (!isLoggedIn()) { navigate('/login'); return; }
|
|
request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders').then(setData);
|
|
}, [navigate]);
|
|
|
|
const filtered = useMemo(() => data.list.filter((o) => {
|
|
if (statusFilter === 'ALL') return true;
|
|
const s = String(o.status).toUpperCase();
|
|
if (statusFilter === 'PENDING_SHIP') return s.includes('PENDING') || s === 'PAID';
|
|
if (statusFilter === 'SHIPPING') return s.includes('SHIP') || s === 'OUT_WAREHOUSE';
|
|
if (statusFilter === 'COMPLETED') return s === 'COMPLETED';
|
|
if (statusFilter === 'ABNORMAL') return s.includes('ERROR') || s.includes('ABNORMAL');
|
|
return true;
|
|
}), [data.list, statusFilter]);
|
|
|
|
async function handleShip(orderId: string, e: React.MouseEvent) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setShipModalId(orderId);
|
|
setShipForm({ logisticsCompany: '', trackingNo: '', manualQueryUrl: '' });
|
|
}
|
|
|
|
async function submitManualShip() {
|
|
if (!shipModalId) return;
|
|
if (!shipForm.logisticsCompany.trim() || !shipForm.trackingNo.trim()) {
|
|
window.alert('请填写快递公司和运单号');
|
|
return;
|
|
}
|
|
setShippingId(shipModalId);
|
|
try {
|
|
await request('PARTNER_H5', `/partner/orders/${shipModalId}/manual-ship`, {
|
|
method: 'POST',
|
|
body: JSON.stringify(shipForm),
|
|
});
|
|
setShipModalId(null);
|
|
const next = await request<{ list: Array<Record<string, unknown>> }>('PARTNER_H5', '/partner/orders');
|
|
setData(next);
|
|
} catch (err) {
|
|
window.alert(err instanceof Error ? err.message : '发货失败');
|
|
} finally {
|
|
setShippingId(null);
|
|
}
|
|
}
|
|
|
|
function canShip(status: string) {
|
|
const s = status.toUpperCase();
|
|
return s.includes('PENDING') || s === 'PAID' || s === 'PENDING_SHIP';
|
|
}
|
|
|
|
return (
|
|
<div className={`page partner-orders-page${tabRoot ? '' : ' page-no-tab'}`}>
|
|
{!tabRoot && <PageHeader title="订单中心" onBack={() => navigate('/')} />}
|
|
|
|
<div className="partner-segment">
|
|
<button type="button" className={tab === 'orders' ? 'active' : ''} onClick={() => setTab('orders')}>订单列表</button>
|
|
{!tabRoot && (
|
|
<button type="button" className={tab === 'coupons' ? 'active' : ''} onClick={() => setTab('coupons')}>权益记录</button>
|
|
)}
|
|
</div>
|
|
|
|
{tab === 'orders' && (
|
|
<>
|
|
<div className="partner-filter-row" style={{ marginBottom: 8 }}>
|
|
{DATE_FILTERS.map((f) => (
|
|
<button key={f.key} type="button" className={`partner-chip${dateFilter === f.key ? ' active' : ''}`} style={{ border: dateFilter === f.key ? 'none' : '1px solid var(--color-outline-variant)', background: dateFilter === f.key ? undefined : '#fff' }} onClick={() => setDateFilter(f.key)}>
|
|
{f.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="partner-filter-row" style={{ borderBottom: '1px solid var(--color-surface-container-high)', paddingBottom: 8, marginBottom: 16 }}>
|
|
{STATUS_FILTERS.map((f) => (
|
|
<button key={f.key} type="button" className={`partner-filter-tab${statusFilter === f.key ? ' active' : ''}`} onClick={() => setStatusFilter(f.key)}>
|
|
{f.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{filtered.length === 0 && <div className="empty">暂无订单</div>}
|
|
|
|
{filtered.map((o) => {
|
|
const st = orderStatusLabel(String(o.status));
|
|
const payAmount = Number(o.payAmount || 0);
|
|
const orderId = String(o.id);
|
|
return (
|
|
<div key={orderId} className="partner-order-card-wrap">
|
|
<Link to={`/orders/${orderId}`} className="partner-order-card">
|
|
<div className="partner-order-card-top">
|
|
<span className="label-md text-muted">NO. {String(o.orderNo)}</span>
|
|
<span className="label-md" style={{ color: st.color, fontWeight: 600 }}>{st.label}</span>
|
|
</div>
|
|
<div className="partner-order-product">
|
|
<div className="partner-order-product-img" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<span className="material-symbols-outlined text-muted">liquor</span>
|
|
</div>
|
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
<h3 className="headline-md line-2-clamp">{String(o.productName || '杜康好酒')}</h3>
|
|
<p className="text-variant body-md" style={{ marginTop: 4 }}>¥{payAmount.toFixed(2)}</p>
|
|
</div>
|
|
</div>
|
|
<div className="partner-order-address">
|
|
<span className="material-symbols-outlined" style={{ fontSize: 18 }}>location_on</span>
|
|
<p className="line-2-clamp">{String(o.receiverAddress || '收货地址')}</p>
|
|
</div>
|
|
</Link>
|
|
{canShip(String(o.status)) && !(o.delivery as { trackingNo?: string } | undefined)?.trackingNo && (
|
|
<button
|
|
type="button"
|
|
className="partner-order-ship-btn"
|
|
disabled={shippingId === orderId}
|
|
onClick={(e) => void handleShip(orderId, e)}
|
|
>
|
|
填写运单
|
|
</button>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</>
|
|
)}
|
|
|
|
{tab === 'coupons' && !tabRoot && (
|
|
<div style={{ padding: '0 20px' }}>
|
|
<div className="partner-revenue-card" style={{ marginBottom: 16 }}>
|
|
<p className="partner-revenue-label">累计已发放权益金额</p>
|
|
<div className="partner-revenue-amount" style={{ fontSize: 28 }}>42,800.00</div>
|
|
</div>
|
|
<p className="text-muted body-md text-center">权益记录 preV1 占位</p>
|
|
</div>
|
|
)}
|
|
|
|
{shipModalId && (
|
|
<div className="partner-ship-modal-backdrop" role="presentation" onClick={() => setShipModalId(null)}>
|
|
<div className="partner-ship-modal" role="dialog" onClick={(e) => e.stopPropagation()}>
|
|
<h3 className="headline-md">填写运单</h3>
|
|
<label className="partner-ship-field">
|
|
<span>快递公司</span>
|
|
<input
|
|
value={shipForm.logisticsCompany}
|
|
onChange={(e) => setShipForm((f) => ({ ...f, logisticsCompany: e.target.value }))}
|
|
placeholder="如 顺丰速运"
|
|
/>
|
|
</label>
|
|
<label className="partner-ship-field">
|
|
<span>运单号</span>
|
|
<input
|
|
value={shipForm.trackingNo}
|
|
onChange={(e) => setShipForm((f) => ({ ...f, trackingNo: e.target.value }))}
|
|
/>
|
|
</label>
|
|
<label className="partner-ship-field">
|
|
<span>查询链接(可选)</span>
|
|
<input
|
|
value={shipForm.manualQueryUrl}
|
|
onChange={(e) => setShipForm((f) => ({ ...f, manualQueryUrl: e.target.value }))}
|
|
placeholder="https://..."
|
|
/>
|
|
</label>
|
|
<div className="partner-ship-actions">
|
|
<button type="button" className="partner-btn-secondary" onClick={() => setShipModalId(null)}>
|
|
取消
|
|
</button>
|
|
<button
|
|
type="button"
|
|
className="partner-btn-primary"
|
|
disabled={shippingId === shipModalId}
|
|
onClick={() => void submitManualShip()}
|
|
>
|
|
{shippingId === shipModalId ? '提交中…' : '确认发货'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|