169 lines
5.4 KiB
TypeScript
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>
|
|
);
|
|
}
|