Files
dukang/apps/h5-shop/src/pages/PhoneRedeemPage.tsx
T
jacy fc2e5b65de
CI / verify (pull_request) Has been cancelled
v3.5.3版本更新1
2026-08-20 18:54:15 +08:00

259 lines
8.9 KiB
TypeScript

import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import type { RedeemPhoneDirectPrepareResult } from '@dukang/shared-types';
import { request } from '../lib/api';
import { toastError, toastSuccess } from '../lib/toast';
import { useStorePageView } from '../lib/usePageView';
function formatAmount(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
export default function PhoneRedeemPage() {
useStorePageView('store_phone_redeem_view');
const navigate = useNavigate();
const [phone, setPhone] = useState('');
const [amount, setAmount] = useState('');
const [confirmCode, setConfirmCode] = useState('');
const [prepared, setPrepared] = useState<RedeemPhoneDirectPrepareResult | null>(null);
const [storeName, setStoreName] = useState('');
const [storeClosed, setStoreClosed] = useState(false);
const [loading, setLoading] = useState(false);
const [confirmCooldown, setConfirmCooldown] = useState(0);
const [showOpenModal, setShowOpenModal] = useState(false);
const [opening, setOpening] = useState(false);
useEffect(() => {
request<Record<string, unknown>>('SHOP_H5', '/shop/store')
.then((s) => {
setStoreName(String(s.name || '当前门店'));
if (s.status && s.status !== 'OPEN') setStoreClosed(true);
})
.catch(() => setStoreName('当前门店'));
}, []);
useEffect(() => {
if (confirmCooldown <= 0) return;
const timer = window.setTimeout(() => setConfirmCooldown((v) => v - 1), 1000);
return () => window.clearTimeout(timer);
}, [confirmCooldown]);
async function prepareDirectRedeem() {
const value = Number(amount);
if (!Number.isFinite(value) || value <= 0) {
toastError('请输入有效核销金额');
return;
}
setLoading(true);
try {
const result = await request<RedeemPhoneDirectPrepareResult>('SHOP_H5', '/shop/redeem/phone/prepare-direct', {
method: 'POST',
body: JSON.stringify({ phone: phone.trim(), amount: value }),
});
setPrepared(result);
setConfirmCode('');
setConfirmCooldown(60);
toastSuccess(`验证码已发送至 ${result.maskedPhone},验证成功后将直接核销`);
} catch (e) {
setPrepared(null);
toastError(e instanceof Error ? e.message : '发送验证码失败');
} finally {
setLoading(false);
}
}
async function sendConfirmSms() {
if (!/^1\d{10}$/.test(phone.trim())) {
toastError('请输入正确的手机号');
return;
}
if (storeClosed) {
setShowOpenModal(true);
return;
}
await prepareDirectRedeem();
}
async function openStoreAndContinue() {
if (opening) return;
setOpening(true);
try {
await request('SHOP_H5', '/shop/store/status', {
method: 'PUT',
body: JSON.stringify({ status: 'OPEN' }),
});
setStoreClosed(false);
setShowOpenModal(false);
await prepareDirectRedeem();
} catch (e) {
setShowOpenModal(false);
toastError(e instanceof Error ? e.message : '开启营业失败');
} finally {
setOpening(false);
}
}
async function confirmRedeem() {
if (!prepared) {
toastError('请先发送核销验证码');
return;
}
if (!confirmCode.trim()) {
toastError('请输入确认验证码');
return;
}
setLoading(true);
try {
const result = await request<Record<string, unknown>>('SHOP_H5', '/shop/redeem/phone/confirm', {
method: 'POST',
body: JSON.stringify({
sessionId: prepared.sessionId,
code: confirmCode.trim(),
}),
});
sessionStorage.setItem('lastRedeemResult', JSON.stringify(result));
navigate('/redeem/success', {
state: { result, storeName, user: prepared.user },
});
} catch (e) {
toastError(e instanceof Error ? e.message : '核销失败');
} finally {
setLoading(false);
}
}
const amountValue = Number(amount);
const canSendCode =
/^1\d{10}$/.test(phone.trim()) && Number.isFinite(amountValue) && amountValue > 0;
return (
<div className="shop-redeem-page">
<header className="shop-redeem-header">
<button type="button" className="shop-redeem-back" onClick={() => navigate(-1)} aria-label="返回">
<span className="material-symbols-outlined">arrow_back</span>
</button>
<h1 className="app-page-title">手机号核销</h1>
</header>
<main className="shop-redeem-main">
{storeClosed && (
<p className="shop-redeem-error" style={{ marginBottom: 12 }}>门店目前休息中无法核销,开启营业后可继续</p>
)}
<section className="shop-redeem-card">
<div className="shop-redeem-banner">
<div className="shop-redeem-banner-icon">
<span className="material-symbols-outlined shop-fill-icon">smartphone</span>
</div>
<div>
<p className="shop-redeem-banner-label">当前登录核销门店</p>
<h2 className="shop-redeem-banner-name">{storeName}</h2>
</div>
</div>
<div className="shop-redeem-body">
<div className="shop-phone-field">
<label className="shop-phone-label">用户手机号</label>
<input
className="shop-phone-input"
type="tel"
maxLength={11}
placeholder="请输入用户手机号"
value={phone}
disabled={loading}
onChange={(e) => {
setPhone(e.target.value.replace(/\D/g, ''));
setPrepared(null);
setConfirmCode('');
setConfirmCooldown(0);
}}
/>
</div>
<div className="shop-phone-field">
<label className="shop-phone-label">待核销金额</label>
<input
className="shop-phone-input"
type="number"
min={0.01}
step={0.01}
placeholder="请输入待核销金额"
value={amount}
disabled={loading}
onChange={(e) => {
setAmount(e.target.value);
setPrepared(null);
setConfirmCode('');
setConfirmCooldown(0);
}}
/>
</div>
<div className="shop-phone-field">
<label className="shop-phone-label">核销验证码</label>
<div className="shop-phone-code-row">
<input
className="shop-phone-input"
type="text"
inputMode="numeric"
maxLength={6}
placeholder="输入用户收到的验证码"
value={confirmCode}
onChange={(e) => setConfirmCode(e.target.value.replace(/\D/g, ''))}
/>
<button
type="button"
className="shop-phone-code-btn"
disabled={loading || confirmCooldown > 0 || !canSendCode}
onClick={() => void sendConfirmSms()}
>
{confirmCooldown > 0 ? `${confirmCooldown}s` : '发送验证码'}
</button>
</div>
<p className="shop-redeem-hint" style={{ marginTop: 8 }}>
验证码将发送到用户手机号,验证成功后直接完成核销。
</p>
</div>
<button
type="button"
className="shop-redeem-confirm-btn"
disabled={loading || storeClosed || !prepared || !confirmCode.trim()}
onClick={() => void confirmRedeem()}
>
{loading ? '正在核销…' : `确认核销 ¥${formatAmount(amountValue || 0)}`}
</button>
</div>
</section>
</main>
{showOpenModal && (
<div className="shop-redeem-modal" role="dialog" aria-modal="true">
<div className="shop-redeem-modal-card">
<h4 className="shop-redeem-modal-title">门店休息中</h4>
<p className="shop-redeem-modal-desc">门店目前休息中无法核销,是否开启营业?</p>
<div className="shop-redeem-modal-actions">
<button
type="button"
className="shop-redeem-modal-cancel"
disabled={opening}
onClick={() => setShowOpenModal(false)}
>
取消
</button>
<button
type="button"
className="shop-redeem-modal-confirm"
disabled={opening}
onClick={() => void openStoreAndContinue()}
>
{opening ? '开启中…' : '确认开启'}
</button>
</div>
</div>
</div>
)}
</div>
);
}