233 lines
7.3 KiB
TypeScript
233 lines
7.3 KiB
TypeScript
import { useEffect, useState } from 'react';
|
||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||
import AppImage from '@dukang/shared-ui/AppImage';
|
||
import type { WechatLoginResult } from '@dukang/shared-types';
|
||
import { request, saveSession, type UserProfile } from '../lib/api';
|
||
import { normalizePhoneInput, validateMobilePhone } from '../lib/phone';
|
||
import { isWechatEnv, weixinSdk } from '../lib/weixin';
|
||
|
||
export default function LoginPage() {
|
||
const navigate = useNavigate();
|
||
const [searchParams] = useSearchParams();
|
||
const returnTo = searchParams.get('return') || '/';
|
||
const [phone, setPhone] = useState('13800000001');
|
||
const [code, setCode] = useState('123456');
|
||
const [loading, setLoading] = useState(false);
|
||
const [agreed, setAgreed] = useState(true);
|
||
const [codeCooldown, setCodeCooldown] = useState(0);
|
||
const [msg, setMsg] = useState('');
|
||
const [wxSessionKey, setWxSessionKey] = useState<string | null>(null);
|
||
const [bindMode, setBindMode] = useState(false);
|
||
|
||
useEffect(() => {
|
||
if (!isWechatEnv()) return;
|
||
weixinSdk
|
||
.handleOAuthCallback()
|
||
.then((result) => {
|
||
if (!result) return;
|
||
handleWechatLoginResult(result);
|
||
})
|
||
.catch((e) => setMsg(e instanceof Error ? e.message : '微信登录失败'));
|
||
}, []);
|
||
|
||
function handleWechatLoginResult(result: WechatLoginResult) {
|
||
if (result.needBindPhone && result.wxSessionKey) {
|
||
setBindMode(true);
|
||
setWxSessionKey(result.wxSessionKey);
|
||
setMsg('微信授权成功,请绑定手机号完成登录');
|
||
return;
|
||
}
|
||
if (result.accessToken) {
|
||
saveSession({
|
||
accessToken: result.accessToken,
|
||
refreshToken: result.refreshToken ?? '',
|
||
deviceKey: result.deviceKey,
|
||
phoneVerified: !!result.phoneVerified,
|
||
user: result.user as never,
|
||
});
|
||
navigate(returnTo.startsWith('/') ? returnTo : '/');
|
||
}
|
||
}
|
||
|
||
function ensureAgreed() {
|
||
if (!agreed) {
|
||
setMsg('请先勾选并同意用户协议');
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
async function sendCode() {
|
||
if (!ensureAgreed()) return;
|
||
const phoneCheck = validateMobilePhone(phone);
|
||
if (!phoneCheck.ok) {
|
||
setMsg(phoneCheck.message ?? '请输入正确的手机号码');
|
||
return;
|
||
}
|
||
setMsg('');
|
||
await request('USER_H5', '/auth/sms/send', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ phone, scene: bindMode ? 'BIND_PHONE' : 'USER_LOGIN' }),
|
||
});
|
||
setMsg('验证码已发送(Mock: 123456)');
|
||
setCodeCooldown(60);
|
||
const timer = setInterval(() => {
|
||
setCodeCooldown((c) => {
|
||
if (c <= 1) {
|
||
clearInterval(timer);
|
||
return 0;
|
||
}
|
||
return c - 1;
|
||
});
|
||
}, 1000);
|
||
}
|
||
|
||
async function login() {
|
||
if (!ensureAgreed()) return;
|
||
const phoneCheck = validateMobilePhone(phone);
|
||
if (!phoneCheck.ok) {
|
||
setMsg(phoneCheck.message ?? '请输入正确的手机号码');
|
||
return;
|
||
}
|
||
if (!code.trim()) {
|
||
setMsg('请输入验证码');
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
setMsg('');
|
||
try {
|
||
if (bindMode && wxSessionKey) {
|
||
const data = await request<WechatLoginResult>('USER_H5', '/auth/wechat/bind-phone', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ wxSessionKey, phone, code }),
|
||
});
|
||
handleWechatLoginResult(data);
|
||
return;
|
||
}
|
||
const data = await request<{
|
||
accessToken: string;
|
||
refreshToken: string;
|
||
deviceKey?: string;
|
||
}>('USER_H5', '/auth/login/sms', {
|
||
method: 'POST',
|
||
body: JSON.stringify({ phone, code }),
|
||
});
|
||
saveSession(data);
|
||
navigate(returnTo.startsWith('/') ? returnTo : '/');
|
||
} catch (e) {
|
||
setMsg(e instanceof Error ? e.message : '登录失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}
|
||
|
||
async function wechatLogin() {
|
||
if (!ensureAgreed()) return;
|
||
setMsg('');
|
||
try {
|
||
if (!isWechatEnv()) {
|
||
setMsg('请在微信内打开以使用微信一键授权');
|
||
return;
|
||
}
|
||
const result = await weixinSdk.login();
|
||
if (result) handleWechatLoginResult(result);
|
||
} catch (e) {
|
||
setMsg(e instanceof Error ? e.message : '微信登录失败');
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="login-page">
|
||
<header className="login-header">
|
||
<div className="login-logo-wrap">
|
||
<AppImage src="/logo.png" alt="杜康好客" wrapperClassName="login-logo" fit="contain" />
|
||
<span className="login-logo-badge">官方</span>
|
||
</div>
|
||
<div className="login-welcome">
|
||
<h1 className="login-welcome-title">欢迎来到杜康好客</h1>
|
||
<p className="login-welcome-sub">买美酒,享好礼</p>
|
||
</div>
|
||
</header>
|
||
|
||
<main className="login-main">
|
||
<div className="login-card">
|
||
<h3 className="login-card-title">{bindMode ? '绑定手机号' : '手机验证码登录'}</h3>
|
||
<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));
|
||
setMsg('');
|
||
}}
|
||
/>
|
||
</div>
|
||
<div className="login-field">
|
||
<input
|
||
type="text"
|
||
inputMode="numeric"
|
||
className="login-field-input"
|
||
placeholder="请输入验证码"
|
||
value={code}
|
||
onChange={(e) => setCode(e.target.value)}
|
||
/>
|
||
<button
|
||
type="button"
|
||
className={`login-get-code${codeCooldown > 0 ? ' disabled' : ''}`}
|
||
disabled={codeCooldown > 0}
|
||
onClick={sendCode}
|
||
>
|
||
{codeCooldown > 0 ? `${codeCooldown}s 后重新获取` : '获取验证码'}
|
||
</button>
|
||
</div>
|
||
{msg && <p className="login-msg">{msg}</p>}
|
||
<button
|
||
type="button"
|
||
className="login-sms-btn"
|
||
disabled={loading}
|
||
onClick={login}
|
||
>
|
||
{loading ? '登录中...' : bindMode ? '绑定并登录' : '登录'}
|
||
</button>
|
||
</div>
|
||
|
||
{!bindMode && (
|
||
<>
|
||
<div className="login-divider">
|
||
<span className="login-divider-line" />
|
||
<span className="login-divider-text">或者</span>
|
||
<span className="login-divider-line" />
|
||
</div>
|
||
|
||
<button type="button" className="login-wechat-btn" onClick={wechatLogin}>
|
||
<span className="material-symbols-outlined login-wechat-icon">chat</span>
|
||
<span>微信一键授权</span>
|
||
</button>
|
||
</>
|
||
)}
|
||
</main>
|
||
|
||
<footer className="login-footer">
|
||
<label className="login-agreement">
|
||
<input
|
||
type="checkbox"
|
||
checked={agreed}
|
||
onChange={(e) => setAgreed(e.target.checked)}
|
||
/>
|
||
<span>
|
||
我已阅读并同意
|
||
<a href="#user-agreement">《用户协议》</a>
|
||
和
|
||
<a href="#privacy">《隐私政策》</a>
|
||
</span>
|
||
</label>
|
||
</footer>
|
||
</div>
|
||
);
|
||
}
|