262 lines
10 KiB
TypeScript
262 lines
10 KiB
TypeScript
import { useEffect, useMemo, useState } from 'react';
|
|
import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
|
|
import SubPageHeader from '../components/SubPageHeader';
|
|
import AppImage from '@dukang/shared-ui/AppImage';
|
|
import { request } from '../lib/api';
|
|
import { STITCH_ORDER_PRODUCT_IMAGE } from '../lib/order-images';
|
|
|
|
type OrderItem = {
|
|
productName: string;
|
|
productSpec: string;
|
|
productImage: string;
|
|
unitPrice: number;
|
|
quantity: number;
|
|
};
|
|
|
|
type StatusLog = {
|
|
toStatus: string;
|
|
createdAt: string;
|
|
remark?: string;
|
|
};
|
|
|
|
type Order = {
|
|
id: string;
|
|
orderNo: string;
|
|
orderType?: string;
|
|
status: string;
|
|
originOrderId?: string | null;
|
|
remark?: string | null;
|
|
receiverName: string;
|
|
receiverPhone: string;
|
|
receiverAddress: string;
|
|
payAmount: number;
|
|
benefitAmount: number;
|
|
createdAt: string;
|
|
items?: OrderItem[];
|
|
statusLogs?: StatusLog[];
|
|
};
|
|
|
|
const RESHIP_TIMELINE = [
|
|
{ key: 'placed', title: '补发单已下达', desc: '' },
|
|
{ key: 'pickup', title: '包裹揽收中', desc: '包裹正由物流网点揽收处理' },
|
|
{ key: 'transit', title: '运输中', desc: '暂无物流信息' },
|
|
] as const;
|
|
|
|
const STATUS_BANNER: Record<string, { title: string; subtitle: string }> = {
|
|
PENDING_PAY: { title: '待付款', subtitle: '请尽快完成支付' },
|
|
PENDING_SHIP: { title: '待发货', subtitle: '商家正在备货' },
|
|
OUT_WAREHOUSE: { title: '出库中', subtitle: '商品正在出库' },
|
|
SHIPPING: { title: '配送中', subtitle: '包裹正在配送途中' },
|
|
PENDING_RECEIVE: { title: '待收货', subtitle: '请注意查收' },
|
|
COMPLETED: { title: '已完成', subtitle: '感谢您的购买' },
|
|
RESHIP: { title: '补发中', subtitle: '包裹正在揽收,请耐心等待' },
|
|
};
|
|
|
|
function maskPhone(phone: string) {
|
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
|
|
}
|
|
|
|
function formatDateTime(value: string) {
|
|
const d = new Date(value);
|
|
if (Number.isNaN(d.getTime())) return value;
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
}
|
|
|
|
function reshipTimelineStep(status: string, index: number): 'done' | 'active' | 'pending' {
|
|
if (status === 'COMPLETED') return 'done';
|
|
let activeIndex = 1;
|
|
if (['SHIPPING', 'PENDING_RECEIVE'].includes(status)) activeIndex = 2;
|
|
if (index < activeIndex) return 'done';
|
|
if (index === activeIndex) return 'active';
|
|
return 'pending';
|
|
}
|
|
|
|
export default function OrderDetailPage() {
|
|
const { id } = useParams();
|
|
const [params] = useSearchParams();
|
|
const navigate = useNavigate();
|
|
const [order, setOrder] = useState<Order | null>(null);
|
|
const [showCs, setShowCs] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (id) request<Order>('USER_H5', `/trade/orders/${id}`).then(setOrder);
|
|
}, [id]);
|
|
|
|
const isReship = order?.orderType === 'RESHIPMENT' || params.get('type') === 'reship';
|
|
|
|
const banner = useMemo(() => {
|
|
if (!order) return { title: '', subtitle: '' };
|
|
if (isReship) return STATUS_BANNER.RESHIP;
|
|
return STATUS_BANNER[order.status] ?? { title: order.status, subtitle: '' };
|
|
}, [order, isReship]);
|
|
|
|
const item = order?.items?.[0];
|
|
const productImage = item?.productImage || STITCH_ORDER_PRODUCT_IMAGE;
|
|
const placedAt = order?.statusLogs?.find((l) => l.toStatus === 'PENDING_SHIP')?.createdAt
|
|
?? order?.createdAt
|
|
?? '';
|
|
|
|
if (!order) return <div className="empty">加载中...</div>;
|
|
|
|
return (
|
|
<div className="order-detail-page">
|
|
<SubPageHeader title="我的订单" onBack={() => navigate(-1)} />
|
|
|
|
<main className="order-detail-main sub-page-body">
|
|
<section className="order-detail-banner">
|
|
<div className="order-detail-banner-text">
|
|
<h2>{banner.title}</h2>
|
|
<p>{banner.subtitle}</p>
|
|
</div>
|
|
<span className="material-symbols-outlined order-detail-banner-icon">local_shipping</span>
|
|
</section>
|
|
|
|
<div className="order-detail-cards">
|
|
{isReship && (
|
|
<section className="order-detail-card">
|
|
<h3 className="order-detail-section-title">物流动态</h3>
|
|
<div className="order-detail-timeline">
|
|
{RESHIP_TIMELINE.map((step, index) => {
|
|
const state = reshipTimelineStep(order.status, index);
|
|
return (
|
|
<div
|
|
key={step.key}
|
|
className={`order-detail-step order-detail-step--${state}`}
|
|
>
|
|
<div className="order-detail-step-dot">
|
|
{state === 'done' && (
|
|
<span className="material-symbols-outlined">check</span>
|
|
)}
|
|
{state === 'active' && <span className="order-detail-step-pulse" />}
|
|
{state === 'pending' && <span className="order-detail-step-idle" />}
|
|
</div>
|
|
<div className="order-detail-step-body">
|
|
<p className="order-detail-step-title">{step.title}</p>
|
|
{index === 0 && placedAt && (
|
|
<p className="order-detail-step-time">{formatDateTime(placedAt)}</p>
|
|
)}
|
|
{step.desc && state !== 'pending' && (
|
|
<p className="order-detail-step-desc">{step.desc}</p>
|
|
)}
|
|
{step.desc && state === 'pending' && (
|
|
<p className="order-detail-step-desc">{step.desc}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{isReship && (
|
|
<section className="order-detail-card order-detail-reship-info">
|
|
<div className="order-detail-info-row">
|
|
<span>补发原因</span>
|
|
<span>{order.remark || '商品破损'}</span>
|
|
</div>
|
|
<div className="order-detail-info-row">
|
|
<span>关联原订单</span>
|
|
{order.originOrderId ? (
|
|
<Link to={`/orders/${order.originOrderId}`} className="order-detail-origin-link">
|
|
查看原订单
|
|
</Link>
|
|
) : (
|
|
<span className="order-detail-origin-link">DK20231005002</span>
|
|
)}
|
|
</div>
|
|
</section>
|
|
)}
|
|
|
|
{item && (
|
|
<section className="order-detail-card">
|
|
<div className="order-detail-product">
|
|
<div className="order-detail-product-thumb">
|
|
<AppImage
|
|
src={productImage}
|
|
alt={item.productName}
|
|
wrapperClassName="app-image--fill"
|
|
/>
|
|
</div>
|
|
<div className="order-detail-product-info">
|
|
<div>
|
|
<h4 className="order-detail-product-name">{item.productName}</h4>
|
|
<p className="order-detail-product-spec">{item.productSpec}</p>
|
|
</div>
|
|
<div className="order-detail-product-meta">
|
|
<span className="order-detail-product-price">
|
|
¥{isReship ? '0.00' : Number(item.unitPrice).toFixed(2)}
|
|
</span>
|
|
<span className="order-detail-product-qty">x{item.quantity}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{Number(order.benefitAmount) > 0 && (
|
|
<div className="order-detail-benefit-row">
|
|
<div className="order-detail-benefit-left">
|
|
<span className="order-detail-benefit-badge">
|
|
¥{order.benefitAmount}好客权益
|
|
</span>
|
|
<span className="order-detail-benefit-note">随单赠送</span>
|
|
</div>
|
|
<span className="material-symbols-outlined order-detail-benefit-info">info</span>
|
|
</div>
|
|
)}
|
|
</section>
|
|
)}
|
|
|
|
<section className="order-detail-card">
|
|
<div className="order-detail-address-head">
|
|
<span className="material-symbols-outlined fill-icon">location_on</span>
|
|
<h3>收货信息</h3>
|
|
</div>
|
|
<div className="order-detail-address-body">
|
|
<div className="order-detail-address-row">
|
|
<span className="order-detail-address-name">{order.receiverName}</span>
|
|
<span>{maskPhone(order.receiverPhone)}</span>
|
|
</div>
|
|
<p>{order.receiverAddress}</p>
|
|
</div>
|
|
</section>
|
|
|
|
{!isReship && (
|
|
<section className="order-detail-card order-detail-meta">
|
|
<div className="order-detail-info-row">
|
|
<span>订单号</span>
|
|
<span className="order-detail-order-no">{order.orderNo}</span>
|
|
</div>
|
|
<div className="order-detail-info-row">
|
|
<span>实付金额</span>
|
|
<span className="order-detail-pay-amount">¥{order.payAmount}</span>
|
|
</div>
|
|
</section>
|
|
)}
|
|
</div>
|
|
</main>
|
|
|
|
<footer className="order-detail-footer">
|
|
<button type="button" className="order-detail-cs-btn" onClick={() => setShowCs(true)}>
|
|
<span className="material-symbols-outlined">support_agent</span>
|
|
联系客服
|
|
</button>
|
|
</footer>
|
|
|
|
{showCs && (
|
|
<div className="modal-overlay" onClick={() => setShowCs(false)}>
|
|
<div className="modal-sheet" onClick={(e) => e.stopPropagation()}>
|
|
<div className="modal-grabber" />
|
|
<h3 className="headline-md" style={{ marginBottom: 12 }}>联系客服</h3>
|
|
<p className="text-variant body-md" style={{ marginBottom: 16 }}>
|
|
preV1 Mock:客服工作时间 9:00-18:00,请描述订单号 {order.orderNo}
|
|
</p>
|
|
<button type="button" className="btn btn-primary btn-block" onClick={() => setShowCs(false)}>
|
|
知道了
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|