@@ -47,13 +47,25 @@ const STATUS_LABELS: Record<string, string> = {
|
||||
PENDING_SHIP: '待发货',
|
||||
OUT_WAREHOUSE: '出库中',
|
||||
SHIPPING: '配送中',
|
||||
SHIPPED: '配送中',
|
||||
PENDING_RECEIVE: '待签收',
|
||||
DELIVERED: '待签收',
|
||||
COMPLETED: '已完成',
|
||||
CANCELLED: '已取消',
|
||||
REFUNDING: '退款中',
|
||||
REFUNDED: '已退款',
|
||||
};
|
||||
|
||||
/** 已付款未完成:可选现场取货并确认收货 */
|
||||
const ON_SITE_PICKUP_STATUSES = new Set([
|
||||
'PENDING_SHIP',
|
||||
'OUT_WAREHOUSE',
|
||||
'SHIPPING',
|
||||
'SHIPPED',
|
||||
'PENDING_RECEIVE',
|
||||
'DELIVERED',
|
||||
]);
|
||||
|
||||
function fullReceiverAddress(order: OrderDetail) {
|
||||
const detail = (order.receiverAddress || '').trim();
|
||||
const region = [order.receiverProvince, order.receiverCity, order.receiverDistrict]
|
||||
@@ -68,6 +80,8 @@ export default function OrderDetailPage() {
|
||||
const router = useRouter();
|
||||
const orderId = router.params.id ?? '';
|
||||
const [order, setOrder] = useState<OrderDetail | null>(null);
|
||||
const [onSitePickup, setOnSitePickup] = useState(false);
|
||||
const [confirming, setConfirming] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!orderId) return;
|
||||
@@ -76,7 +90,16 @@ export default function OrderDetailPage() {
|
||||
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
|
||||
}, [orderId]);
|
||||
|
||||
const canPay = !!order && order.status === 'PENDING_PAY' && !order.originOrderId;
|
||||
const isReship = !!order?.originOrderId;
|
||||
const canPay = !!order && order.status === 'PENDING_PAY' && !isReship;
|
||||
const canOnSitePickup =
|
||||
!!order && !isReship && ON_SITE_PICKUP_STATUSES.has(order.status || '');
|
||||
const canConfirmReceive =
|
||||
!!order &&
|
||||
!isReship &&
|
||||
(['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '') ||
|
||||
(onSitePickup && canOnSitePickup));
|
||||
|
||||
const item = order?.items?.[0];
|
||||
const productName = item?.productName || order?.productName || '杜康商品';
|
||||
const quantity = item?.quantity ?? order?.quantity ?? order?.qty ?? 1;
|
||||
@@ -111,10 +134,41 @@ export default function OrderDetailPage() {
|
||||
Taro.navigateTo({ url: '/pages/customer-service/index' });
|
||||
}
|
||||
|
||||
async function confirmReceive() {
|
||||
if (!order || !canConfirmReceive || confirming) return;
|
||||
|
||||
const useOnSite =
|
||||
onSitePickup || !['PENDING_RECEIVE', 'DELIVERED'].includes(order.status || '');
|
||||
const { confirm } = await Taro.showModal({
|
||||
title: useOnSite ? '确认现场取货?' : '确认收货?',
|
||||
content: useOnSite
|
||||
? '请确认您已在现场拿到商品。确认后订单将完成,好客权益即时可用,无法再安排配送。若尚未取到酒,请勿确认。'
|
||||
: '请确认已收到商品。确认后订单将完成,好客权益可正常使用。',
|
||||
confirmText: '确认收货',
|
||||
cancelText: '再想想',
|
||||
});
|
||||
if (!confirm) return;
|
||||
|
||||
setConfirming(true);
|
||||
try {
|
||||
const updated = await request<OrderDetail>(`/trade/orders/${order.id}/confirm-receive`, {
|
||||
method: 'POST',
|
||||
data: { onSitePickup: useOnSite },
|
||||
});
|
||||
setOrder(updated);
|
||||
setOnSitePickup(false);
|
||||
toast(useOnSite ? '现场取货已确认,订单完成' : '已确认收货');
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : '确认收货失败');
|
||||
} finally {
|
||||
setConfirming(false);
|
||||
}
|
||||
}
|
||||
|
||||
const pageClass = [
|
||||
'order-detail-page',
|
||||
order ? 'order-detail-page--with-actions' : '',
|
||||
canPay ? 'order-detail-page--with-pay' : '',
|
||||
canPay || canConfirmReceive ? 'order-detail-page--with-pay' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
@@ -171,6 +225,27 @@ export default function OrderDetailPage() {
|
||||
<Text className="u-muted">地址信息待完善</Text>
|
||||
)}
|
||||
</View>
|
||||
{canOnSitePickup ? (
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">取货方式</Text>
|
||||
<View
|
||||
className="order-pickup-option"
|
||||
onClick={() => setOnSitePickup((v) => !v)}
|
||||
>
|
||||
<View
|
||||
className={`order-pickup-check${onSitePickup ? ' order-pickup-check--on' : ''}`}
|
||||
>
|
||||
{onSitePickup ? <Text className="order-pickup-check-mark">✓</Text> : null}
|
||||
</View>
|
||||
<View className="order-pickup-copy">
|
||||
<Text className="order-pickup-title">现场取货</Text>
|
||||
<Text className="order-pickup-desc">
|
||||
已在活动现场或门店拿到商品时勾选,确认后订单直接完成
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
<View className="order-card">
|
||||
<Text className="order-card-title">订单信息</Text>
|
||||
<View className="order-row">
|
||||
@@ -189,7 +264,11 @@ export default function OrderDetailPage() {
|
||||
</View>
|
||||
|
||||
{order ? (
|
||||
<View className={`order-detail-actionbar${canPay ? ' order-detail-actionbar--with-pay' : ''}`}>
|
||||
<View
|
||||
className={`order-detail-actionbar${
|
||||
canPay || canConfirmReceive ? ' order-detail-actionbar--with-pay' : ''
|
||||
}`}
|
||||
>
|
||||
{isWeapp ? (
|
||||
<ContactCsButton
|
||||
className="order-detail-cs-btn"
|
||||
@@ -219,6 +298,14 @@ export default function OrderDetailPage() {
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
{canConfirmReceive ? (
|
||||
<View
|
||||
className={`order-confirm-submit${confirming ? ' order-confirm-submit--disabled' : ''}`}
|
||||
onClick={confirming ? undefined : () => void confirmReceive()}
|
||||
>
|
||||
{confirming ? '提交中…' : onSitePickup ? '确认现场取货' : '确认收货'}
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
</PageShell>
|
||||
|
||||
@@ -17,17 +17,29 @@ function formatMoney(amount: number) {
|
||||
return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||
}
|
||||
|
||||
/** 核销金额输入:最多两位小数,禁止非法字符 */
|
||||
/** 核销金额输入:最多两位小数;去掉前导 0;禁止非法字符 */
|
||||
function sanitizeRedeemAmountInput(raw: string): string {
|
||||
let next = raw.replace(/[^\d.]/g, '');
|
||||
let next = String(raw ?? '').replace(/[^\d.]/g, '');
|
||||
if (!next) return '';
|
||||
|
||||
const firstDot = next.indexOf('.');
|
||||
if (firstDot >= 0) {
|
||||
next =
|
||||
next.slice(0, firstDot + 1) + next.slice(firstDot + 1).replace(/\./g, '');
|
||||
const [intPart, decPart = ''] = next.split('.');
|
||||
next = `${intPart}.${decPart.slice(0, 2)}`;
|
||||
const intRaw = next.slice(0, firstDot).replace(/\D/g, '');
|
||||
const decRaw = next
|
||||
.slice(firstDot + 1)
|
||||
.replace(/\D/g, '')
|
||||
.replace(/\./g, '')
|
||||
.slice(0, 2);
|
||||
const intPart = intRaw.replace(/^0+(?=\d)/, '') || '0';
|
||||
// 正在输入小数点或小数位时保留点
|
||||
if (decRaw.length > 0 || next.endsWith('.')) {
|
||||
return `${intPart}.${decRaw}`;
|
||||
}
|
||||
return intPart;
|
||||
}
|
||||
if (next.startsWith('.')) next = `0${next}`;
|
||||
|
||||
// 纯整数:忽略前导 0(保留单个 0)
|
||||
next = next.replace(/^0+(?=\d)/, '');
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -37,7 +49,7 @@ export default function RedeemPage() {
|
||||
const initialAmount = router.params.amount ?? '';
|
||||
const [balance, setBalance] = useState(0);
|
||||
const [couponBalance, setCouponBalance] = useState<number | null>(null);
|
||||
const [amount, setAmount] = useState(initialAmount);
|
||||
const [amount, setAmount] = useState(() => sanitizeRedeemAmountInput(initialAmount));
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const redeemableMax = couponId ? (couponBalance ?? 0) : balance;
|
||||
@@ -67,7 +79,11 @@ export default function RedeemPage() {
|
||||
|
||||
function fillMaxAmount() {
|
||||
if (redeemableMax < MIN_REDEEM_AMOUNT) return;
|
||||
setAmount(String(redeemableMax));
|
||||
setAmount(sanitizeRedeemAmountInput(redeemableMax.toFixed(2)));
|
||||
}
|
||||
|
||||
function onAmountChange(raw: string) {
|
||||
setAmount(sanitizeRedeemAmountInput(raw));
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
@@ -119,7 +135,8 @@ export default function RedeemPage() {
|
||||
placeholder="输入核销金额"
|
||||
placeholderClass="redeem-input-placeholder"
|
||||
value={amount}
|
||||
onInput={(e) => setAmount(sanitizeRedeemAmountInput(e.detail.value))}
|
||||
onInput={(e) => onAmountChange(e.detail.value)}
|
||||
onBlur={(e) => onAmountChange(e.detail.value)}
|
||||
style={{ textAlign: 'center' }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -68,6 +68,63 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.order-confirm-submit--disabled {
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.order-pickup-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.order-pickup-check {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 2px;
|
||||
flex-shrink: 0;
|
||||
border: 1.5px solid var(--color-outline, #c8c4be);
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.order-pickup-check--on {
|
||||
border-color: var(--color-primary, #8b1a1a);
|
||||
background: var(--color-primary, #8b1a1a);
|
||||
}
|
||||
|
||||
.order-pickup-check-mark {
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.order-pickup-copy {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.order-pickup-title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-on-surface);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.order-pickup-desc {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-on-surface-variant, #78716c);
|
||||
}
|
||||
|
||||
.order-card {
|
||||
background: var(--color-card);
|
||||
border-radius: var(--radius-lg);
|
||||
|
||||
Reference in New Issue
Block a user