From 2251eb36a8a6645d670591be5345b7cd75620f89 Mon Sep 17 00:00:00 2001 From: Pine Date: Thu, 27 Aug 2026 20:36:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=85=AC=E5=BC=80=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E5=A2=99=20AuthGate=20=E6=94=AF=E6=8C=81=E3=80=8C?= =?UTF-8?q?=E5=B0=8F=E7=A8=8B=E5=BA=8F=E6=89=AB=E7=A0=81=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E3=80=8D(=E5=AE=8C=E6=95=B4=E5=BE=AE=E4=BF=A1=E7=99=BB?= =?UTF-8?q?=E5=BD=95)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 components/MpQrLogin.jsx:小程序码轮询(mpQrStart/mpQrPoll) → applyAuthResult(profile) 登录 - AuthGate 改「手机号登录 / 小程序扫码」双 tab;小程序扫码已在微信小程序登录过的账号可一键登录网页端 - auth.css 加 .auth-debug 样式 --- website/src/components/AuthGate.jsx | 78 +++++++++++++++------------- website/src/components/MpQrLogin.jsx | 60 +++++++++++++++++++++ website/src/styles/auth.css | 3 ++ 3 files changed, 105 insertions(+), 36 deletions(-) create mode 100644 website/src/components/MpQrLogin.jsx diff --git a/website/src/components/AuthGate.jsx b/website/src/components/AuthGate.jsx index 27d4cfc..04e0b48 100644 --- a/website/src/components/AuthGate.jsx +++ b/website/src/components/AuthGate.jsx @@ -1,22 +1,24 @@ import React from 'react'; import { sendCode, phoneLogin } from '@/services/auth'; +import MpQrLogin from '@/components/MpQrLogin'; import '@/styles/auth.css'; /** - * 登录 / 注册墙(AuthGate)· 手机号 + 短信验证码 - * 用于"测评结果需登录"等场景。手机号即账号,未注册自动注册,无密码。 + * 登录 / 注册墙(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 [cd, setCd] = React.useState(0); const send = async () => { if (!/^1\d{10}$/.test(phone)) { setErr('请输入正确的 11 位手机号'); return; } @@ -50,45 +52,49 @@ export default function AuthGate({ title, subtitle, onAuthed }) { } }; + const TABS = [['phone', '手机号登录'], ['mp', '小程序扫码']]; + return ( -
+
-

{title || '手机号登录 / 注册'}

+

{title || '登录 / 注册'}

{subtitle &&

{subtitle}

} -
- - setPhone(e.target.value)} - inputMode="numeric" - maxLength={11} - placeholder="11 位手机号" - autoComplete="tel" - /> -
-
- -
- setCode(e.target.value)} - inputMode="numeric" - maxLength={6} - placeholder="6 位验证码" - style={{ flex: 1 }} - autoComplete="one-time-code" - /> - -
+ ))}
- {debug &&
{debug}
} - {err &&
{err}
} - -
未注册的手机号将自动注册为账号
- + {mode === 'phone' ? ( +
+
+ + setPhone(e.target.value)} inputMode="numeric" maxLength={11} placeholder="11 位手机号" autoComplete="tel" /> +
+
+ +
+ setCode(e.target.value)} inputMode="numeric" maxLength={6} placeholder="6 位验证码" style={{ flex: 1 }} autoComplete="one-time-code" /> + +
+
+ {debug &&
{debug}
} + {err &&
{err}
} + +
未注册的手机号将自动注册为账号
+
+ ) : ( +
+ { if (onAuthed) onAuthed(user); }} /> + {err &&
{err}
} +
已在微信小程序登录过的账号,扫码即可一键登录网页端
+
+ )} +
); } diff --git a/website/src/components/MpQrLogin.jsx b/website/src/components/MpQrLogin.jsx new file mode 100644 index 0000000..17ac44d --- /dev/null +++ b/website/src/components/MpQrLogin.jsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { QRCodeSVG } from '@rc-component/qrcode'; +import { mpQrStart, mpQrPoll, applyAuthResult, getUser } from '@/services/auth'; + +/** 小程序扫码登录:微信扫小程序码 → 打开小程序确认 → web 轮询拿 token。 + * props: tip, onAuthed(user) — 登录成功回调(已写入 token 与用户信息)。 */ +export default function MpQrLogin({ tip, onAuthed }) { + const [qrImage, setQrImage] = React.useState(''); + const [qrUrl, setQrUrl] = React.useState(''); + const [status, setStatus] = React.useState('loading'); + const [error, setError] = React.useState(''); + + React.useEffect(() => { + let timer = null; + (async () => { + setStatus('loading'); + try { + const r = await mpQrStart(); + const { scene, qr_image, qr_url } = r || {}; + setQrImage(qr_image || ''); + setQrUrl(qr_url || ''); + setStatus('pending'); + timer = window.setInterval(async () => { + const res = await mpQrPoll(scene); + if (res.status === 'done' && res.profile) { + window.clearInterval(timer); + applyAuthResult(res.profile); + if (onAuthed) onAuthed(getUser()); + } else if (res.status === 'expired') { + window.clearInterval(timer); + setStatus('error'); + setError('二维码已过期,请刷新'); + } + }, 2000); + } catch (e) { + setStatus('error'); + setError(e.message || '获取小程序码失败'); + } + })(); + return () => { if (timer) window.clearInterval(timer); }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (status === 'loading') return
加载中…
; + if (status === 'error') + return ( +
+
{error}
+ +
+ ); + return ( +
+
+ {qrImage ? 小程序码 : } +
+

{tip || '使用微信扫一扫打开小程序,在小程序内确认登录'}

+
+ ); +} diff --git a/website/src/styles/auth.css b/website/src/styles/auth.css index d3adf83..01661c5 100644 --- a/website/src/styles/auth.css +++ b/website/src/styles/auth.css @@ -56,3 +56,6 @@ background: var(--panel); border: 1px dashed var(--line); border-radius: 12px; padding: 13px 16px; } .pine-note .logout-link { margin-left: 8px; color: var(--primary); cursor: pointer; text-decoration: underline; } + +/* 登录调试信息(演示验证码提示) */ +.auth-debug { margin: 6px 0; font-size: 12px; color: var(--success); background: var(--success-soft); border-radius: 8px; padding: 6px 10px; }