Files
training/website/src/components/AuthGate.jsx
T
Pine 2251eb36a8 feat(web): 公开登录墙 AuthGate 支持「小程序扫码登录」(完整微信登录)
- 新增 components/MpQrLogin.jsx:小程序码轮询(mpQrStart/mpQrPoll) → applyAuthResult(profile) 登录
- AuthGate 改「手机号登录 / 小程序扫码」双 tab;小程序扫码已在微信小程序登录过的账号可一键登录网页端
- auth.css 加 .auth-debug 样式
2026-08-27 20:36:27 +08:00

101 lines
4.2 KiB
React
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 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>
);
}