Files
dukang/apps/h5-user/src/components/PhoneVerifySheet.tsx
T

169 lines
5.4 KiB
TypeScript

import { useEffect, useState } from 'react';
import type { WechatLoginResult } from '@dukang/shared-types';
import { SmsScene } from '@dukang/shared-types';
import { bindPhone, request, type SessionPayload } from '../lib/api';
import { normalizePhoneInput, validateMobilePhone } from '../lib/phone';
import { useSmsCode } from '../lib/use-sms-code';
import { useUserSession } from '../contexts/UserSessionContext';
type PhoneVerifySheetProps = {
open: boolean;
/** 打开时预填手机号(如收货地址中的手机号) */
defaultPhone?: string;
mode?: 'bind_phone' | 'wechat_bind_phone';
wxSessionKey?: string;
title?: string;
description?: string;
onClose: () => void;
onSuccess: () => void;
};
export default function PhoneVerifySheet({
open,
defaultPhone,
mode = 'bind_phone',
wxSessionKey,
title,
description,
onClose,
onSuccess,
}: PhoneVerifySheetProps) {
const { applySession } = useUserSession();
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [loading, setLoading] = useState(false);
const { sendCode, sending, codeCooldown, sentHint, error, setError, clearMessages } = useSmsCode();
useEffect(() => {
if (!open) {
setPhone('');
setCode('');
setError('');
clearMessages();
return;
}
if (defaultPhone) {
const normalized = normalizePhoneInput(defaultPhone);
if (validateMobilePhone(normalized).ok) {
setPhone(normalized);
}
}
}, [open, defaultPhone, clearMessages, setError]);
async function onSendCode() {
clearMessages();
await sendCode(phone, SmsScene.BIND_PHONE);
}
async function submit() {
const phoneCheck = validateMobilePhone(phone);
if (!phoneCheck.ok) {
setError(phoneCheck.message ?? '请输入正确的手机号码');
return;
}
if (!code.trim()) {
setError('请输入验证码');
return;
}
setLoading(true);
setError('');
try {
if (mode === 'wechat_bind_phone') {
if (!wxSessionKey) {
setError('微信会话已过期,请重新授权');
return;
}
const data = await request<WechatLoginResult>('USER_H5', '/auth/wechat/bind-phone', {
method: 'POST',
body: JSON.stringify({ wxSessionKey, phone, code }),
});
if (data.accessToken) {
applySession({
accessToken: data.accessToken,
refreshToken: data.refreshToken ?? '',
deviceKey: data.deviceKey,
phoneVerified: !!data.phoneVerified,
user: data.user as SessionPayload['user'],
});
}
} else {
const session = await bindPhone(phone, code);
applySession(session as SessionPayload);
}
onSuccess();
onClose();
} catch (e) {
setError(e instanceof Error ? e.message : '验证失败');
} finally {
setLoading(false);
}
}
if (!open) return null;
const sheetTitle = title ?? (mode === 'wechat_bind_phone' ? '绑定手机号' : '验证手机号');
const sheetDesc =
description ??
(mode === 'wechat_bind_phone'
? '建议绑定手机号,便于订单通知与售后;关闭可跳过继续支付'
: '建议绑定手机号,便于订单通知与售后;关闭可跳过继续下单');
return (
<div className="phone-verify-overlay" role="dialog" aria-modal="true">
<button type="button" className="phone-verify-backdrop" aria-label="关闭" onClick={onClose} />
<div className="phone-verify-sheet">
<h3 className="phone-verify-title">{sheetTitle}</h3>
<p className="phone-verify-desc">{sheetDesc}</p>
<div className="login-field">
<span className="login-field-prefix">+86</span>
<input
type="tel"
className="login-field-input"
placeholder="请输入手机号"
maxLength={11}
inputMode="numeric"
value={phone}
onChange={(e) => {
setPhone(normalizePhoneInput(e.target.value));
setError('');
clearMessages();
}}
/>
</div>
<div className="login-field">
<input
type="text"
inputMode="numeric"
className="login-field-input"
placeholder="请输入验证码"
maxLength={6}
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
/>
<button
type="button"
className={`login-get-code${codeCooldown > 0 || sending ? ' disabled' : ''}`}
disabled={codeCooldown > 0 || sending}
onClick={onSendCode}
>
{sending
? '发送中...'
: codeCooldown > 0
? `${codeCooldown}s 后重新获取`
: '获取验证码'}
</button>
</div>
{(error || sentHint) && (
<p className={`login-msg${sentHint && !error ? ' login-msg--hint' : ''}`}>{error || sentHint}</p>
)}
<button type="button" className="login-sms-btn" disabled={loading} onClick={submit}>
{loading ? '验证中...' : mode === 'wechat_bind_phone' ? '确认绑定' : '确认验证'}
</button>
<button type="button" className="phone-verify-skip" onClick={onClose}>
暂不绑定
</button>
</div>
</div>
);
}