feat(auth): 小程序扫码登录其它端口 —— 确认页+我的扫码登录

- 小程序 pages/scan-login(读scene→复用微信登录→/auth/mp-qr/confirm→目标端口登录)
- 我的页「跨端口登录·扫码登录其它端口」; auth.js mpQrConfirm
- 网站 Login.jsx PollingQr 支持 qr_image(小程序码)渲染
This commit is contained in:
Pine
2026-08-26 12:00:09 +08:00
parent 0b264ee9b3
commit a39fbc7955
7 changed files with 124 additions and 4 deletions
+28 -1
View File
@@ -2,7 +2,7 @@ import { View, Text, Button, Input, Image } from '@tarojs/components';
import Taro from '@tarojs/taro';
import { useState, useEffect } from 'react';
import { useTabIndex } from '@/utils/tab';
import { isAuthed, getUser, logout, wxLogin, phoneLogin, bindPhone, sendCode, wxPhoneBind, updateProfile, getMe } from '@/utils/auth';
import { isAuthed, getUser, logout, wxLogin, phoneLogin, bindPhone, sendCode, wxPhoneBind, updateProfile, getMe, mpQrConfirm } from '@/utils/auth';
import { getMyBookings, checkIn, parseIso, BK_STATUS_OPTIONS, BK_TOPIC_OPTIONS, BK_SOURCE_OPTIONS } from '@/utils/booking';
import Skeleton from '@/components/skeleton';
import CountdownBox from '@/components/CountdownBox';
@@ -200,6 +200,8 @@ export default function Mine() {
const [pTopics, setPTopics] = useState((user && user.topics) || []);
const [pSource, setPSource] = useState((user && user.source) || '');
const [saving, setSaving] = useState(false);
const [scanBusy, setScanBusy] = useState(false);
const [scanErr, setScanErr] = useState('');
useEffect(() => {
setPName((user && user.name) || '');
setPStatus((user && user.status) || '');
@@ -262,6 +264,23 @@ export default function Mine() {
Taro.showToast({ title: '已退出登录', icon: 'none' });
};
/* ============ 跨端口扫码登录:扫电脑/网页登录页二维码 → 小程序确认 ============ */
const onScanLogin = async () => {
setScanErr(''); setScanBusy(true);
try {
const res = await Taro.scanCode(); // 扫其它端口登录页的「小程序扫码」二维码
const str = res.result || '';
const m = str.match(/[?&]scene=([^&#]+)/); // 二维码内容=跨端口 login url,含 scene
if (!m) throw new Error('无效的扫码登录二维码');
const scene = decodeURIComponent(m[1]);
const { code } = await Taro.login(); // 微信登录 code
await mpQrConfirm(scene, code); // 确认 → 目标端口轮询取到登录令牌
Taro.showToast({ title: '已登录目标端口,请回电脑/网页查看', icon: 'none' });
} catch (e) {
setScanErr((e && e.message) || '扫码登录失败');
} finally { setScanBusy(false); }
};
/* ============ 未登录:登录入口 ============ */
if (!user || !isAuthed()) {
return (
@@ -335,6 +354,14 @@ export default function Mine() {
<Button className="btn-ghost logout-btn" onClick={onLogout}>退出</Button>
</View>
{/* 跨端口扫码登录:扫电脑/网页登录页的「小程序扫码」二维码 → 用本小程序确认登录该端口 */}
<View className="card next-card">
<Text className="next-lbl">跨端口登录</Text>
<Text className="bind-tip">在电脑/网页登录页点小程序扫码用本小程序扫一扫即可登录该端口</Text>
<Button className="btn-main" loading={scanBusy} onClick={onScanLogin}>扫码登录其它端口</Button>
{scanErr && <Text className="scan-err">{scanErr}</Text>}
</View>
{/* 下一场活动倒计时(个人信息卡之后显示;取最早的待参加且未开始的报名场次) */}
{(() => {
const now = Date.now();
+2
View File
@@ -376,3 +376,5 @@
font-size: 34rpx; font-weight: 700; color: #fff; padding: 0; margin: 0;
}
.save-nick { margin-top: 12rpx; align-self: flex-start; line-height: 2.2; padding: 0 28rpx; font-size: 24rpx; }
.scan-err { display:block; margin-top:12rpx; font-size:24rpx; color:#f0b8b8; text-align:center; }
@@ -0,0 +1 @@
export default { navigationBarTitleText: '扫码登录' };
@@ -0,0 +1,67 @@
import { View, Text, Button } from '@tarojs/components';
import Taro, { useDidShow } from '@tarojs/taro';
import { useState } from 'react';
import { mpQrConfirm } from '@/utils/auth';
import './index.scss';
/**
* 小程序扫码登录确认页:由「小程序码(wxacode)」自动打开(微信扫一扫→自动跳到本页),
* 或用户在小程序内「我的→扫码登录」扫二维码后进入。读取携带的 scene,
* 复用当前微信登录资格(Taro.login→code→平台归户)完成目标端口登录。
*/
export default function ScanLogin() {
const [scene, setScene] = useState('');
const [busy, setBusy] = useState(false);
const [msg, setMsg] = useState('');
const [done, setDone] = useState(false);
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('');
});
const confirm = async () => {
if (!scene) { setMsg('无效的登录请求(缺少场景)'); return; }
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 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>
) : (
<Button className="btn btn-cta" onClick={() => Taro.switchTab({ url: '/pages/index/index' })}>回首页</Button>
)}
</View>
</View>
);
}
@@ -0,0 +1,13 @@
/* 小程序扫码登录确认页 */
.sl { position: relative; z-index: 2; }
.sl-card {
margin: 40rpx 32rpx 0; padding: 60rpx 40rpx;
background: linear-gradient(180deg, #0e0e12, #0a0a0d);
border: 1px solid rgba(255,255,255,.12); border-radius: 20rpx; text-align: center;
}
.sl-icon { font-size: 72rpx; margin-bottom: 20rpx; }
.sl-title { display: block; font-size: 40rpx; font-weight: 700; color: #fff; }
.sl-desc { display: block; margin: 16rpx 0 0; font-size: 27rpx; color: #c4c2c3; line-height: 1.6; }
.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; }
+7
View File
@@ -57,10 +57,17 @@ export async function wxLogin({ code, nickName, avatarUrl }) {
if (data.token) {
Taro.setStorageSync('pine_token', data.token);
saveUser(data);
return data;
}
return data;
}
/** 小程序扫码登录其它端口:扫跨端登录页二维码 → 用 wx.login code 确认 → 目标端口轮询取令牌 */
export async function mpQrConfirm(scene, code) {
const data = await request('POST', '/auth/mp-qr/confirm', { scene, code });
return data;
}
/** 拉取当前账号资料(刷新昵称/头像/手机绑定状态)——平台 /auth/me 返回扁平 profile */
export async function getMe() {
const data = await request('GET', '/auth/me', {}, true);