Files
dukang/apps/h5-user/src/pages/LoginPage.tsx
T

233 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}