89fd333702
Add client-logging SDK, expanded event taxonomy, API observability, admin domain events UI, and move SENTRY_DSN to HQ system settings with @sentry/node bootstrap after config preload. Co-authored-by: Cursor <cursoragent@cursor.com>
195 lines
8.0 KiB
TypeScript
195 lines
8.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useNavigate, useParams } from 'react-router-dom';
|
|
import PageHeader from '@dukang/shared-ui/PageHeader';
|
|
import { request } from '../lib/api';
|
|
import { toastError, toastSuccess } from '../lib/toast';
|
|
import { usePartnerPageView } from '../lib/usePageView';
|
|
|
|
const TIMELINE = [
|
|
{ key: 'confirm', label: '待确认' },
|
|
{ key: 'accepted', label: '已接单' },
|
|
{ key: 'delivering', label: '配送中' },
|
|
{ key: 'shipping', label: '运输中', desc: '包裹正在送往目的地' },
|
|
{ key: 'done', label: '已送达' },
|
|
];
|
|
|
|
function statusIndex(status: string) {
|
|
const s = String(status).toUpperCase();
|
|
if (s === 'COMPLETED') return 4;
|
|
if (s.includes('SHIP') || s === 'OUT_WAREHOUSE') return 3;
|
|
if (s === 'PENDING_RECEIVE') return 3;
|
|
if (s.includes('DELIVER')) return 2;
|
|
return 1;
|
|
}
|
|
|
|
function statusBanner(status: string) {
|
|
const s = String(status).toUpperCase();
|
|
if (s.includes('SHIP') || s === 'OUT_WAREHOUSE') return { title: '运输中', desc: '预计今日 18:00 前送达', icon: 'local_shipping' };
|
|
if (s === 'COMPLETED') return { title: '已完成', desc: '订单已送达', icon: 'check_circle' };
|
|
if (s.includes('PENDING')) return { title: '待发货', desc: '商家正在备货', icon: 'inventory_2' };
|
|
return { title: status, desc: '', icon: 'receipt_long' };
|
|
}
|
|
|
|
function canShip(status: string) {
|
|
const s = status.toUpperCase();
|
|
return s.includes('PENDING') || s === 'PAID' || s === 'PENDING_SHIP';
|
|
}
|
|
|
|
export default function OrderDetailPage() {
|
|
const { id } = useParams();
|
|
usePartnerPageView('partner_order_detail_view', id ? { orderId: id } : undefined);
|
|
const navigate = useNavigate();
|
|
const [order, setOrder] = useState<Record<string, unknown> | null>(null);
|
|
const [shipping, setShipping] = useState(false);
|
|
|
|
useEffect(() => {
|
|
document.title = '订单详情';
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (id) void request<Record<string, unknown>>('PARTNER_H5', `/partner/orders/${id}`).then(setOrder);
|
|
}, [id]);
|
|
|
|
async function reload() {
|
|
if (!id) return;
|
|
const next = await request<Record<string, unknown>>('PARTNER_H5', `/partner/orders/${id}`);
|
|
setOrder(next);
|
|
}
|
|
|
|
async function advance(status: string) {
|
|
if (!id) return;
|
|
setShipping(true);
|
|
try {
|
|
await request('PARTNER_H5', `/partner/orders/${id}/mock-advance-delivery`, {
|
|
method: 'POST',
|
|
body: JSON.stringify({ targetStatus: status }),
|
|
});
|
|
await reload();
|
|
toastSuccess('已更新配送状态');
|
|
} finally {
|
|
setShipping(false);
|
|
}
|
|
}
|
|
|
|
function handleContactCourier() {
|
|
const delivery = order?.delivery as Record<string, unknown> | undefined;
|
|
const trackingNo = String(delivery?.trackingNo || delivery?.providerOrderNo || '').trim();
|
|
if (trackingNo) {
|
|
void navigator.clipboard?.writeText(trackingNo);
|
|
toastSuccess(`运单号已复制:${trackingNo}`);
|
|
return;
|
|
}
|
|
toastError('暂无配送员联系方式');
|
|
}
|
|
|
|
if (!order) return <div className="empty">加载中...</div>;
|
|
|
|
const banner = statusBanner(String(order.status));
|
|
const currentIdx = statusIndex(String(order.status));
|
|
const payAmount = Number(order.payAmount || 0);
|
|
const showShip = canShip(String(order.status));
|
|
|
|
return (
|
|
<div className="partner-order-detail">
|
|
<PageHeader title="订单详情" onBack={() => navigate('/orders')} />
|
|
|
|
<section className="partner-status-banner">
|
|
<div style={{ position: 'relative', zIndex: 1 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
|
|
<span className="material-symbols-outlined" style={{ fontVariationSettings: "'FILL' 1" }}>{banner.icon}</span>
|
|
<h2 className="headline-md" style={{ color: '#fff' }}>{banner.title}</h2>
|
|
</div>
|
|
{banner.desc && <p className="body-md" style={{ color: 'rgba(255,255,255,0.9)' }}>{banner.desc}</p>}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="partner-timeline">
|
|
<div className="partner-timeline-line" />
|
|
{TIMELINE.map((item, i) => {
|
|
const done = i < currentIdx;
|
|
const current = i === currentIdx;
|
|
const pending = i > currentIdx;
|
|
return (
|
|
<div key={item.key} className="partner-timeline-item">
|
|
<div className={`partner-timeline-dot${done ? ' partner-timeline-dot--done' : ''}${current ? ' partner-timeline-dot--current' : ''}${pending ? ' partner-timeline-dot--pending' : ''}`}>
|
|
{done && <span className="material-symbols-outlined">check</span>}
|
|
</div>
|
|
<div>
|
|
<p className={current ? 'headline-md text-primary' : 'label-md'} style={{ fontWeight: current ? 700 : 500 }}>{item.label}</p>
|
|
{item.desc && current && <p className="text-muted body-md" style={{ marginTop: 4 }}>{item.desc}</p>}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</section>
|
|
|
|
<section className="partner-detail-section partner-detail-section--accent">
|
|
<div style={{ display: 'flex', gap: 16 }}>
|
|
<div className="partner-order-product-img" style={{ width: 96, height: 96, position: 'relative' }}>
|
|
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--color-surface-container)' }}>
|
|
<span className="material-symbols-outlined text-muted" style={{ fontSize: 32 }}>liquor</span>
|
|
</div>
|
|
</div>
|
|
<div style={{ flex: 1 }}>
|
|
<h3 className="headline-md">{String(order.productName || '杜康好酒')}</h3>
|
|
<p className="text-muted body-md" style={{ marginTop: 4 }}>x{Number(order.quantity || 1)}</p>
|
|
<div style={{ marginTop: 8, display: 'flex', alignItems: 'baseline', gap: 2 }}>
|
|
<span className="label-md text-primary">¥</span>
|
|
<span className="amount-lg">{payAmount.toFixed(2)}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="partner-detail-section">
|
|
<h4 className="headline-md" style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<span style={{ width: 4, height: 16, background: 'var(--color-heritage-red)', borderRadius: 2 }} />
|
|
收货地址
|
|
</h4>
|
|
<div style={{ display: 'flex', gap: 12 }}>
|
|
<span className="material-symbols-outlined text-muted">location_on</span>
|
|
<div>
|
|
<p className="headline-md" style={{ fontSize: 16 }}>
|
|
{String(order.receiverName)} <span className="body-md text-muted" style={{ fontWeight: 400 }}>{String(order.receiverPhone)}</span>
|
|
</p>
|
|
<p className="text-muted body-md" style={{ marginTop: 4, lineHeight: 1.5 }}>{String(order.receiverAddress)}</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<footer className="partner-order-footer">
|
|
<div className="partner-order-footer-actions">
|
|
{showShip && (
|
|
<button
|
|
type="button"
|
|
className="btn btn-primary partner-order-ship-btn--footer"
|
|
disabled={shipping}
|
|
onClick={() => void advance('OUT_WAREHOUSE')}
|
|
>
|
|
{shipping ? '发货中…' : '确认发货'}
|
|
</button>
|
|
)}
|
|
<button
|
|
type="button"
|
|
className="btn btn-outline"
|
|
style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 24px' }}
|
|
onClick={handleContactCourier}
|
|
>
|
|
<span className="material-symbols-outlined" style={{ fontSize: 18 }}>support_agent</span>
|
|
联系配送员
|
|
</button>
|
|
</div>
|
|
</footer>
|
|
|
|
{import.meta.env.DEV && (
|
|
<details className="partner-dev-tools">
|
|
<summary>Dev: Mock 推进配送</summary>
|
|
{['OUT_WAREHOUSE', 'SHIPPING', 'PENDING_RECEIVE', 'COMPLETED'].map((s) => (
|
|
<button key={s} type="button" onClick={() => void advance(s)}>{s}</button>
|
|
))}
|
|
</details>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|