feat(scan-login): 未登录必须先登录 + 页面优化

- 未登录: 微信一键登录/手机号登录 二选一, 登录后才可扫码确认
- 复用微信身份(Taro.login→code) 完成端口登录/绑定; 区分 登录成功/绑定成功
This commit is contained in:
Pine
2026-08-26 12:53:57 +08:00
parent c7d9a74063
commit 169e880d37
2 changed files with 101 additions and 33 deletions
+91 -33
View File
@@ -1,66 +1,124 @@
import { View, Text, Button } from '@tarojs/components';
import { View, Text, Button, Input } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { useState } from 'react';
import { mpQrConfirm } from '@/utils/auth';
import { isAuthed, wxLogin, phoneLogin, sendCode, mpQrConfirm } from '@/utils/auth';
import './index.scss';
/**
* 小程序扫码登录确认页:由「小程序码(wxacode)」自动打开(微信扫一扫→自动跳到本页),
* 或用户在小程序内「我的→扫码登录」扫二维码后进入。读取携带的 scene,
* 复用当前微信登录资格(Taro.login→code→平台归户)完成目标端口登录。
* 小程序扫码登录确认页:由「小程序码(wxacode)」自动打开,或「我的→扫码登录」扫码进入。
* 规则:若小程序当前未登录 → 必须**先登录**(一键微信登录 / 手机号验证码)才能进行扫码确认;
* 登录后复用该微信身份 `Taro.login` 完成目标端口登录/绑定
*/
export default function ScanLogin() {
const [scene, setScene] = useState('');
const [authed, setAuthed] = useState(isAuthed());
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState('');
const [done, setDone] = useState(false);
const [loginMode, setLoginMode] = useState<'wx' | 'phone'>('wx');
const [gatePhone, setGatePhone] = useState('');
const [gateCode, setGateCode] = useState('');
const [cd, setCd] = useState(0);
useDidShow(() => {
const opts = Taro.getEnterOptionsSync();
const q = (opts && opts.query) || {};
// wxacode 的 scene 会放进 launch options.query.scene;普通 navigateTo 带 ?scene= 同理
const s = String(q.scene || opts.scene || '');
setScene(s);
setDone(false);
setMsg('');
setDone(false); setMsg('');
});
const confirm = async () => {
if (!scene) { setMsg('无效的登录请求(缺少场景)'); return; }
const wxLoginNow = async () => {
setBusy(true); setMsg('');
try {
const { code } = await Taro.login(); // 复用微信登录资格
await mpQrConfirm(scene, code); // 目标端口轮询取到令牌
setDone(true);
setMsg('登录成功,请回到电脑 / 网页查看');
} catch (e) {
setMsg((e && e.message) || '登录失败,请重试');
} finally { setBusy(false); }
const { code } = await Taro.login();
await wxLogin({ code });
setAuthed(true);
setMsg('登录,请确认');
} catch (e) { setMsg((e && e.message) || '微信登录失败'); }
finally { setBusy(false); }
};
const cancel = () => {
// 取消:返回上一页;无上一页则回首页
Taro.navigateBack().catch(() => Taro.switchTab({ url: '/pages/index/index' }));
const sendSms = async () => {
setMsg('');
if (!/^1\d{10}$/.test(gatePhone)) { setMsg('请输入正确的 11 位手机号'); return; }
setBusy(true);
try {
const r = await sendCode(gatePhone);
setMsg(`验证码已发送${r.debugCode ? `(演示 ${r.debugCode}` : ''}`);
setCd(60);
const t = window.setInterval(() => setCd((c) => (c <= 1 ? (window.clearInterval(t), 0) : c - 1)), 1000);
} catch (e) { setMsg((e && e.message) || '发送失败'); }
finally { setBusy(false); }
};
const phoneLoginNow = async () => {
setMsg('');
if (!/^1\d{10}$/.test(gatePhone) || !gateCode) { setMsg('请填写手机号与验证码'); return; }
setBusy(true);
try { await phoneLogin(gatePhone, gateCode); setAuthed(true); setMsg('已登录,请确认'); }
catch (e) { setMsg((e && e.message) || '登录失败'); }
finally { setBusy(false); }
};
const confirm = async () => {
setMsg('');
if (!scene) { setMsg('无效的扫码请求'); return; }
setBusy(true);
try {
const { code } = await Taro.login();
const r = await mpQrConfirm(scene, code);
setDone(true);
setMsg(r && r.bound ? '绑定成功,请回电脑/网页查看' : '登录成功,请回电脑/网页查看');
} catch (e) { setMsg((e && e.message) || '操作失败,请重试'); }
finally { setBusy(false); }
};
const cancel = () => { Taro.navigateBack().catch(() => Taro.switchTab({ url: '/pages/index/index' })); };
return (
<View className="sl page-in">
<View className="sl-card">
<View className="sl-icon">{done ? '✓' : '🔐'}</View>
<Text className="sl-title">{done ? '已登录' : '扫码登录确认'}</Text>
<Text className="sl-desc">
{done ? msg : '确认使用当前微信账号登录你正在操作的端口?'}
</Text>
{!done && scene && <Text className="sl-scene">登录请求 #{scene.slice(0, 12)}</Text>}
{msg && !done && <Text className="sl-err">{msg}</Text>}
{!done ? (
<View className="sl-actions">
<Button className="btn btn-cta" loading={busy} onClick={confirm}>确认登录</Button>
<Button className="btn btn-ghost" disabled={busy} onClick={cancel}>取消</Button>
</View>
{/* 未登录:先登录 */}
{!authed ? (
<>
<View className="sl-icon">🔐</View>
<Text className="sl-title">请先登录小程序</Text>
<Text className="sl-desc">为完成扫码登录 / 绑定请先登录小程序复用你的微信/手机号身份</Text>
<View className="sl-tabs">
<Text className={`sl-tab${loginMode === 'wx' ? ' on' : ''}`} onClick={() => setLoginMode('wx')}>微信一键登录</Text>
<Text className={`sl-tab${loginMode === 'phone' ? ' on' : ''}`} onClick={() => setLoginMode('phone')}>手机号登录</Text>
</View>
{loginMode === 'wx' ? (
<Button className="btn btn-cta" loading={busy} onClick={wxLoginNow}>微信一键登录</Button>
) : (
<View className="sl-field">
<Input className="input" placeholder="11 位手机号" value={gatePhone} onInput={(e) => setGatePhone(e.detail.value)} />
<View className="sl-code">
<Input className="input" placeholder="验证码" value={gateCode} onInput={(e) => setGateCode(e.detail.value)} />
<Button className="btn btn-ghost btn-sm" disabled={cd > 0 || busy} onClick={sendSms}>{cd > 0 ? `${cd}s` : '验证码'}</Button>
</View>
<Button className="btn btn-cta" loading={busy} onClick={phoneLoginNow}>登录</Button>
</View>
)}
</>
) : (
<Button className="btn btn-cta" onClick={() => Taro.switchTab({ url: '/pages/index/index' })}>回首页</Button>
<>
<View className="sl-icon">{done ? '✓' : '✅'}</View>
<Text className="sl-title">{done ? '已完成' : '确认扫码操作'}</Text>
<Text className="sl-desc">
{done ? msg : `确认使用当前微信账号${scene.includes('bind') || true ? '' : ''}在你操作的端口${scene ? '' : ' '}完成登录 / 绑定?`}
</Text>
{!done && scene && <Text className="sl-scene">请求 #{scene.slice(0, 12)}</Text>}
{!done && (
<View className="sl-actions">
<Button className="btn btn-cta" loading={busy} onClick={confirm}>确认登录 / 绑定</Button>
<Button className="btn btn-ghost" disabled={busy} onClick={cancel}>取消</Button>
</View>
)}
{done && <Button className="btn btn-cta" onClick={() => Taro.switchTab({ url: '/pages/index/index' })}>回首页</Button>}
</>
)}
{msg && <Text className="sl-err">{msg}</Text>}
</View>
</View>
);
@@ -11,3 +11,13 @@
.sl-scene { display: block; margin-top: 12rpx; font-size: 23rpx; color: #8e8e8e; }
.sl-err { display: block; margin-top: 16rpx; font-size: 25rpx; color: #f0b8b8; }
.sl-actions { display: flex; flex-direction: column; gap: 16rpx; margin-top: 40rpx; }
/* 登录门 */
.sl-tabs { display: flex; gap: 4px; margin: 24rpx 0; padding: 4px; border-radius: 12px; background: #161618; border: 1px solid #2a2a2d; }
.sl-tab { flex: 1; text-align: center; padding: 12rpx 0; border-radius: 9px; color: #9b9b9b; font-size: 26rpx; }
.sl-tab.on { background: #fff; color: #111; }
.sl-field { display: flex; flex-direction: column; gap: 14rpx; margin-top: 8rpx; }
.sl-code { display: flex; gap: 12rpx; align-items: center; }
.sl-code .input { flex: 1; }
.btn-sm { font-size: 24rpx; padding: 0 24rpx; line-height: 2.4; }
.sl-err { display: block; margin-top: 20rpx; font-size: 25rpx; color: #f0b8b8; }