feat(auth): 小程序扫码登录其它端口 —— 确认页+我的扫码登录
- 小程序 pages/scan-login(读scene→复用微信登录→/auth/mp-qr/confirm→目标端口登录) - 我的页「跨端口登录·扫码登录其它端口」; auth.js mpQrConfirm - 网站 Login.jsx PollingQr 支持 qr_image(小程序码)渲染
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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; }
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user