Files
dukang/apps/h5-partner/src/pages/OrderDetailPage.tsx
T
jacy 89fd333702 feat(analytics): persona logging upgrade and Sentry system config
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>
2026-08-03 22:30:02 +08:00

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>
);
}