Files
dukang/apps/h5-partner/src/pages/ProxyOrderDetailPage.tsx
T

387 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import QRCode from 'qrcode';
import PageHeader from '@dukang/shared-ui/PageHeader';
import { invokeWechatPay } from '@dukang/weixin-sdk';
import type { PartnerProxyOrderListItem, ProxyOrderPayResponse, ProxyPayMethod } from '@dukang/shared-types';
import { getToken, request } from '../lib/api';
import {
proxyOrderPayLabel,
proxyOrderStatusColor,
proxyOrderStatusLabel,
} from '../lib/proxyOrderStatus';
import { toastError, toastSuccess } from '../lib/toast';
import { isWechatEnv } from '../lib/weixin';
import { fetchPartnerProfile, partnerHasWechatBinding } from '../lib/wechat-auth';
function fmtMoney(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function fmtTime(value: string | undefined) {
if (!value) return '—';
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())}`;
}
function deliveryLabel(order: PartnerProxyOrderListItem) {
const t = String(order.deliveryType || '').toUpperCase();
if (t === 'ON_SITE_PICKUP') return '现场提货';
if (t === 'CROSS_CITY') return '跨城配送';
if (t === 'LOCAL') return '同城配送';
return '代下单';
}
type TrackNode = {
time?: string;
status?: string;
description?: string;
};
type TrackResult = {
nodes?: TrackNode[];
trackingNo?: string | null;
provider?: string | null;
manualQueryUrl?: string | null;
};
export default function ProxyOrderDetailPage() {
const { id } = useParams();
const navigate = useNavigate();
const [order, setOrder] = useState<PartnerProxyOrderListItem | null>(null);
const [error, setError] = useState('');
const [track, setTrack] = useState<TrackResult | null>(null);
const [trackError, setTrackError] = useState('');
const [paying, setPaying] = useState(false);
const [payMsg, setPayMsg] = useState('');
const [payMethod, setPayMethod] = useState<ProxyPayMethod>('NATIVE');
const [codeUrl, setCodeUrl] = useState<string | null>(null);
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
const pollRef = useRef<number | null>(null);
function stopPoll() {
if (pollRef.current != null) {
window.clearInterval(pollRef.current);
pollRef.current = null;
}
}
useEffect(() => () => stopPoll(), []);
useEffect(() => {
document.title = '代下单详情';
}, []);
useEffect(() => {
if (!id) return;
void request<PartnerProxyOrderListItem>('PARTNER_H5', `/partner/proxy-orders/${id}`)
.then(setOrder)
.catch((e) => setError(e instanceof Error ? e.message : '加载失败'));
}, [id]);
useEffect(() => {
if (!id || !order) return;
const dt = String(order.deliveryType || '').toUpperCase();
if (dt === 'ON_SITE_PICKUP' || order.payStatus !== 'PAID') {
setTrack(null);
return;
}
void request<TrackResult>('PARTNER_H5', `/partner/proxy-orders/${id}/track`, { silent: true })
.then(setTrack)
.catch((e) => setTrackError(e instanceof Error ? e.message : '物流暂不可用'));
}, [id, order]);
useEffect(() => {
if (!codeUrl) {
setQrDataUrl(null);
return;
}
void QRCode.toDataURL(codeUrl, { width: 220, margin: 1 })
.then(setQrDataUrl)
.catch(() => setQrDataUrl(null));
}, [codeUrl]);
const unpaid = order?.payStatus === 'UNPAID' || order?.status === 'PENDING_PAY';
function startPoll(orderId: string) {
stopPoll();
pollRef.current = window.setInterval(() => {
void request<{ payStatus: string; orderNo: string }>(
'PARTNER_H5',
`/partner/proxy-orders/${orderId}/pay-status`,
{ silent: true },
)
.then((st) => {
if (st.payStatus === 'PAID') {
stopPoll();
toastSuccess(`支付成功:${st.orderNo}`);
void request<PartnerProxyOrderListItem>('PARTNER_H5', `/partner/proxy-orders/${orderId}`)
.then(setOrder)
.catch(() => undefined);
setCodeUrl(null);
}
})
.catch(() => undefined);
}, 2000);
}
async function startPay(method: ProxyPayMethod) {
if (!id) return;
setPaying(true);
setPayMsg('');
setPayMethod(method);
try {
if (method === 'JSAPI') {
if (!isWechatEnv()) {
throw new Error('请在微信内打开合伙人端以使用微信代付');
}
const profile = await fetchPartnerProfile();
if (!partnerHasWechatBinding(profile)) {
throw new Error('请先绑定微信后再代付');
}
}
const pay = await request<ProxyOrderPayResponse>(
'PARTNER_H5',
`/partner/proxy-orders/${id}/pay`,
{
method: 'POST',
body: JSON.stringify({ payMethod: method }),
silent: true,
},
);
if (pay.mode === 'mock') {
toastSuccess('支付成功');
const refreshed = await request<PartnerProxyOrderListItem>(
'PARTNER_H5',
`/partner/proxy-orders/${id}`,
);
setOrder(refreshed);
setCodeUrl(null);
return;
}
if (pay.mode === 'native' && pay.codeUrl) {
setCodeUrl(pay.codeUrl);
startPoll(id);
return;
}
if (pay.mode === 'jsapi' && pay.prepay) {
setCodeUrl(null);
await invokeWechatPay(pay.prepay, {
apiBase: '/api/v1',
clientApp: 'PARTNER_H5',
getAccessToken: getToken,
platform: 'wechat-h5',
});
startPoll(id);
return;
}
throw new Error('支付发起失败');
} catch (e) {
setPayMsg(e instanceof Error ? e.message : '支付失败');
toastError(e instanceof Error ? e.message : '支付失败');
} finally {
setPaying(false);
}
}
const autoPayStartedRef = useRef(false);
useEffect(() => {
if (!id || !unpaid || autoPayStartedRef.current) return;
autoPayStartedRef.current = true;
void startPay('NATIVE');
// 仅首次进入未支付详情时自动拉收款码
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, unpaid]);
const img = order?.imageResource?.url || '';
const isOnSite = String(order?.deliveryType || '').toUpperCase() === 'ON_SITE_PICKUP';
return (
<div className="page-no-tab partner-orders-page">
<PageHeader title="代下单详情" onBack={() => navigate('/center/proxy-orders')} />
{error && (
<p className="partner-form-error" role="alert" style={{ margin: '16px 20px' }}>
{error}
</p>
)}
{!order && !error && <p className="label-md text-muted" style={{ margin: 20 }}>加载中…</p>}
{order && (
<div style={{ padding: '0 16px 32px' }}>
<div className="partner-order-card" style={{ pointerEvents: 'none' }}>
<div className="partner-order-card-top">
<span className="label-md text-muted">NO. {order.orderNo}</span>
<span
className="label-md"
style={{ color: proxyOrderStatusColor(order.status), fontWeight: 600 }}
>
{proxyOrderStatusLabel(order.status)}
</span>
</div>
<div className="partner-order-product">
<div
className="partner-order-product-img"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
overflow: 'hidden',
background: img ? `center/cover no-repeat url(${img})` : undefined,
}}
>
{!img && <span className="material-symbols-outlined text-muted">liquor</span>}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<h3 className="headline-md">{order.productName || '杜康好酒'}</h3>
{order.productSpec && (
<p className="label-md text-muted" style={{ marginTop: 4 }}>
{order.productSpec}
</p>
)}
<p className="body-md" style={{ marginTop: 8 }}>
×{order.quantity} · ¥{fmtMoney(Number(order.payAmount || 0))}
</p>
<p className="label-md text-muted" style={{ marginTop: 4 }}>
权益 ¥{fmtMoney(Number(order.benefitAmount || 0))}
</p>
</div>
</div>
</div>
<section className="partner-form-card" style={{ marginTop: 12 }}>
<h3 className="headline-md" style={{ marginBottom: 12 }}>客户信息</h3>
<p className="body-md">姓名:{order.receiverName || '—'}</p>
<p className="body-md" style={{ marginTop: 8 }}>手机:{order.receiverPhone || '—'}</p>
<p className="body-md" style={{ marginTop: 8 }}>
地址:{order.receiverAddress || (isOnSite ? '现场提货' : '—')}
</p>
</section>
<section className="partner-form-card" style={{ marginTop: 12 }}>
<h3 className="headline-md" style={{ marginBottom: 12 }}>订单信息</h3>
<p className="body-md">配送方式:{deliveryLabel(order)}</p>
<p className="body-md" style={{ marginTop: 8 }}>下单时间:{fmtTime(order.createdAt)}</p>
<p className="body-md" style={{ marginTop: 8 }}>
支付状态:{proxyOrderPayLabel(order.payStatus)}
</p>
<p className="body-md" style={{ marginTop: 8 }}>
订单状态:{proxyOrderStatusLabel(order.status)}
</p>
</section>
{unpaid ? (
<section className="partner-form-card" style={{ marginTop: 12 }}>
<h3 className="headline-md" style={{ marginBottom: 12 }}>继续支付</h3>
<div className="partner-proxy-mode-tabs">
<button
type="button"
className={`partner-proxy-mode-tab${payMethod === 'NATIVE' ? ' is-active' : ''}`}
onClick={() => void startPay('NATIVE')}
>
收款码
</button>
<button
type="button"
className={`partner-proxy-mode-tab${payMethod === 'JSAPI' ? ' is-active' : ''}`}
onClick={() => void startPay('JSAPI')}
>
微信代付
</button>
</div>
{payMethod === 'NATIVE' ? (
<div style={{ textAlign: 'center', marginTop: 16 }}>
{qrDataUrl ? (
<img src={qrDataUrl} alt="收款码" width={220} height={220} style={{ margin: '0 auto' }} />
) : (
<p className="label-md text-muted">{paying ? '生成收款码中…' : '暂无收款码'}</p>
)}
<p className="label-md text-muted" style={{ marginTop: 8 }}>
请使用微信扫码支付,支付成功后自动更新
</p>
<button
type="button"
className="btn btn-block"
style={{ marginTop: 12 }}
disabled={paying}
onClick={() => void startPay('NATIVE')}
>
{paying ? '生成中…' : '重新生成收款码'}
</button>
</div>
) : (
<div style={{ marginTop: 16 }}>
<p className="label-md text-muted">将调起微信支付,由合伙人微信完成代付</p>
<button
type="button"
className="btn btn-primary btn-block"
style={{ marginTop: 12 }}
disabled={paying}
onClick={() => void startPay('JSAPI')}
>
{paying ? '支付中…' : '调起微信代付'}
</button>
</div>
)}
{payMsg ? (
<p className="partner-form-error" role="alert" style={{ marginTop: 12 }}>
{payMsg}
</p>
) : null}
</section>
) : null}
{!isOnSite && order.payStatus === 'PAID' ? (
<section className="partner-form-card" style={{ marginTop: 12 }}>
<h3 className="headline-md" style={{ marginBottom: 12 }}>物流信息</h3>
{trackError ? (
<p className="label-md text-muted">{trackError}</p>
) : !track ? (
<p className="label-md text-muted">加载物流…</p>
) : (
<>
{track.trackingNo ? (
<p className="body-md" style={{ marginBottom: 8 }}>
运单号:{track.trackingNo}
{track.provider ? `${track.provider}` : ''}
</p>
) : null}
{track.manualQueryUrl ? (
<p className="body-md" style={{ marginBottom: 8 }}>
<a href={track.manualQueryUrl} target="_blank" rel="noreferrer">
查看物流官网
</a>
</p>
) : null}
{(track.nodes ?? []).length === 0 ? (
<p className="label-md text-muted">暂无物流轨迹(待总部发货后更新)</p>
) : (
<ul style={{ paddingLeft: 18, margin: 0 }}>
{(track.nodes ?? []).map((n, i) => (
<li key={`${n.time}-${i}`} className="body-md" style={{ marginBottom: 8 }}>
<div className="label-md text-muted">{fmtTime(n.time)}</div>
<div>{n.description || n.status || '—'}</div>
</li>
))}
</ul>
)}
</>
)}
</section>
) : null}
</div>
)}
</div>
);
}