Files
training/website/src/components/AuthGate.jsx
T
Pine 97054a9699 feat: 培训业务 Web(Vite+React)
- user 端:课程/活动/报名/测评/政策/调研
- 后端已并入 server-core/app/training,/api/* 走 opc.pinesound.cn
2026-08-23 22:32:42 +08:00

95 lines
3.5 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 '@/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>
);
}