2251eb36a8
- 新增 components/MpQrLogin.jsx:小程序码轮询(mpQrStart/mpQrPoll) → applyAuthResult(profile) 登录 - AuthGate 改「手机号登录 / 小程序扫码」双 tab;小程序扫码已在微信小程序登录过的账号可一键登录网页端 - auth.css 加 .auth-debug 样式
101 lines
4.2 KiB
React
101 lines
4.2 KiB
React
import React from 'react';
|
||
import { sendCode, phoneLogin } from '@/services/auth';
|
||
import MpQrLogin from '@/components/MpQrLogin';
|
||
import '@/styles/auth.css';
|
||
|
||
/**
|
||
* 登录 / 注册墙(AuthGate)· 手机号 + 短信验证码 / 小程序扫码登录
|
||
* 用于"测评结果需登录"等场景。手机号即账号,未注册自动注册,无密码;小程序扫码为统一微信登录。
|
||
* props:
|
||
* title — 卡片标题(覆盖默认)
|
||
* subtitle — 一句话说明
|
||
* onAuthed(user) — 登录 / 注册成功回调(已写入 token 与用户信息)
|
||
*/
|
||
export default function AuthGate({ title, subtitle, onAuthed }) {
|
||
const [mode, setMode] = React.useState('phone'); // phone | mp
|
||
const [phone, setPhone] = React.useState('');
|
||
const [code, setCode] = React.useState('');
|
||
const [debug, setDebug] = React.useState('');
|
||
const [err, setErr] = React.useState('');
|
||
const [busy, setBusy] = React.useState(false);
|
||
const [cd, setCd] = React.useState(0);
|
||
|
||
const send = async () => {
|
||
if (!/^1\d{10}$/.test(phone)) { setErr('请输入正确的 11 位手机号'); return; }
|
||
setErr('');
|
||
setBusy(true);
|
||
try {
|
||
const r = await sendCode(phone.trim());
|
||
setDebug(`验证码已发送${r.debugCode ? ',演示验证码:' + r.debugCode : ''}(5 分钟内有效)`);
|
||
setCd(60);
|
||
const t = setInterval(() => setCd((v) => { if (v <= 1) { clearInterval(t); return 0; } return v - 1; }), 1000);
|
||
} catch (ex) {
|
||
setErr(ex && ex.message ? ex.message : '发送失败,请重试');
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
};
|
||
|
||
const submit = async (e) => {
|
||
e.preventDefault();
|
||
setErr('');
|
||
if (!/^1\d{10}$/.test(phone)) { setErr('请输入正确的 11 位手机号'); return; }
|
||
if (!code.trim()) { setErr('请输入验证码'); return; }
|
||
setBusy(true);
|
||
try {
|
||
const user = await phoneLogin(phone.trim(), code.trim());
|
||
if (onAuthed) onAuthed(user);
|
||
} catch (ex) {
|
||
setErr(ex && ex.message ? ex.message : '登录失败,请重试');
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
};
|
||
|
||
const TABS = [['phone', '手机号登录'], ['mp', '小程序扫码']];
|
||
|
||
return (
|
||
<div className="auth-card" style={{ margin: '0 auto' }}>
|
||
<div className="auth-logo"><i className="fa-solid fa-tree" /></div>
|
||
<h1>{title || '登录 / 注册'}</h1>
|
||
{subtitle && <p className="auth-sub">{subtitle}</p>}
|
||
|
||
<div className="auth-tabs" style={{ marginBottom: 18 }}>
|
||
{TABS.map(([k, l]) => (
|
||
<button type="button" key={k} className={`auth-tab${mode === k ? ' active' : ''}`} onClick={() => { setErr(''); setMode(k); }}>
|
||
{l}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
{mode === 'phone' ? (
|
||
<form onSubmit={submit}>
|
||
<div className="field">
|
||
<label>手机号</label>
|
||
<input value={phone} onChange={(e) => setPhone(e.target.value)} inputMode="numeric" maxLength={11} placeholder="11 位手机号" autoComplete="tel" />
|
||
</div>
|
||
<div className="field">
|
||
<label>验证码</label>
|
||
<div style={{ display: 'flex', gap: 8 }}>
|
||
<input value={code} onChange={(e) => setCode(e.target.value)} inputMode="numeric" maxLength={6} placeholder="6 位验证码" style={{ flex: 1 }} autoComplete="one-time-code" />
|
||
<button type="button" className="btn btn-dark" onClick={send} disabled={cd > 0 || busy} style={{ whiteSpace: 'nowrap', flexShrink: 0 }}>
|
||
{cd > 0 ? `${cd}s 后重发` : '发送验证码'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{debug && <div className="auth-debug">{debug}</div>}
|
||
{err && <div className="auth-err">{err}</div>}
|
||
<button className="btn btn-cta auth-btn" disabled={busy}>{busy ? '处理中…' : '登录 / 注册'}</button>
|
||
<div className="auth-foot">未注册的手机号将自动注册为账号</div>
|
||
</form>
|
||
) : (
|
||
<div>
|
||
<MpQrLogin onAuthed={(user) => { if (onAuthed) onAuthed(user); }} />
|
||
{err && <div className="auth-err">{err}</div>}
|
||
<div className="auth-foot">已在微信小程序登录过的账号,扫码即可一键登录网页端</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|