e93e4b8f84
Co-authored-by: Cursor <cursoragent@cursor.com>
438 lines
16 KiB
TypeScript
438 lines
16 KiB
TypeScript
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 [cancelling, setCancelling] = useState(false);
|
||
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((data) => {
|
||
setOrder(data);
|
||
if (data.proxyPayMethod) {
|
||
setPayMethod(data.proxyPayMethod);
|
||
}
|
||
})
|
||
.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;
|
||
const locked = order?.proxyPayMethod;
|
||
if (locked && locked !== method) {
|
||
setPayMsg(
|
||
locked === 'JSAPI'
|
||
? '该订单已发起微信代付,请先取消支付后重新下单'
|
||
: '该订单已生成收款码,请先取消支付后重新下单',
|
||
);
|
||
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);
|
||
setOrder((prev) => (prev ? { ...prev, proxyPayMethod: method } : prev));
|
||
startPoll(id);
|
||
return;
|
||
}
|
||
|
||
if (pay.mode === 'jsapi' && pay.prepay) {
|
||
setCodeUrl(null);
|
||
setOrder((prev) => (prev ? { ...prev, proxyPayMethod: method } : prev));
|
||
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);
|
||
}
|
||
}
|
||
|
||
async function cancelPay() {
|
||
if (!id) return;
|
||
setCancelling(true);
|
||
setPayMsg('');
|
||
stopPoll();
|
||
try {
|
||
await request('PARTNER_H5', `/partner/proxy-orders/${id}/cancel-pay`, {
|
||
method: 'POST',
|
||
body: '{}',
|
||
});
|
||
toastSuccess('已取消支付,可重新下单并选择其他支付方式');
|
||
navigate('/center/proxy-orders');
|
||
} catch (e) {
|
||
setPayMsg(e instanceof Error ? e.message : '取消失败');
|
||
toastError(e instanceof Error ? e.message : '取消失败');
|
||
} finally {
|
||
setCancelling(false);
|
||
}
|
||
}
|
||
|
||
const lockedPayMethod = order?.proxyPayMethod ?? null;
|
||
|
||
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' : ''}`}
|
||
disabled={!!lockedPayMethod && lockedPayMethod !== 'NATIVE'}
|
||
onClick={() => void startPay('NATIVE')}
|
||
>
|
||
收款码
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className={`partner-proxy-mode-tab${payMethod === 'JSAPI' ? ' is-active' : ''}`}
|
||
disabled={!!lockedPayMethod && lockedPayMethod !== 'JSAPI'}
|
||
onClick={() => void startPay('JSAPI')}
|
||
>
|
||
微信代付
|
||
</button>
|
||
</div>
|
||
{lockedPayMethod ? (
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||
已选择{lockedPayMethod === 'NATIVE' ? '收款码' : '微信代付'},切换方式请先取消支付
|
||
</p>
|
||
) : (
|
||
<p className="label-md text-muted" style={{ marginTop: 8 }}>
|
||
请选择支付方式并点击下方按钮发起支付
|
||
</p>
|
||
)}
|
||
|
||
{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}
|
||
|
||
<button
|
||
type="button"
|
||
className="btn btn-block"
|
||
style={{ marginTop: 16 }}
|
||
disabled={cancelling || paying}
|
||
onClick={() => void cancelPay()}
|
||
>
|
||
{cancelling ? '取消中…' : '取消支付'}
|
||
</button>
|
||
</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>
|
||
);
|
||
}
|