97054a9699
- user 端:课程/活动/报名/测评/政策/调研 - 后端已并入 server-core/app/training,/api/* 走 opc.pinesound.cn
95 lines
3.5 KiB
React
95 lines
3.5 KiB
React
import React from 'react';
|
||
import { sendCode, phoneLogin } from '@/services/auth';
|
||
import '@/styles/auth.css';
|
||
|
||
/**
|
||
* 登录 / 注册墙(AuthGate)· 手机号 + 短信验证码
|
||
* 用于"测评结果需登录"等场景。手机号即账号,未注册自动注册,无密码。
|
||
* props:
|
||
* title — 卡片标题(覆盖默认)
|
||
* subtitle — 一句话说明
|
||
* onAuthed(user) — 登录 / 注册成功回调(已写入 token 与用户信息)
|
||
*/
|
||
export default function AuthGate({ title, subtitle, onAuthed }) {
|
||
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);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<form className="auth-card" style={{ margin: '0 auto' }} onSubmit={submit}>
|
||
<div className="auth-logo"><i className="fa-solid fa-tree" /></div>
|
||
<h1>{title || '手机号登录 / 注册'}</h1>
|
||
{subtitle && <p className="auth-sub">{subtitle}</p>}
|
||
|
||
<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 style={{ margin: '6px 0', fontSize: 12, color: '#a8f0c8', background: 'rgba(120,220,160,.1)', borderRadius: 8, padding: '6px 10px' }}>{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>
|
||
);
|
||
}
|