From a39fbc7955a48a59bcf67a6321d4ace83d93bf4f Mon Sep 17 00:00:00 2001 From: Pine Date: Wed, 26 Aug 2026 12:00:09 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E6=89=AB=E7=A0=81=E7=99=BB=E5=BD=95=E5=85=B6=E5=AE=83=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3=20=E2=80=94=E2=80=94=20=E7=A1=AE=E8=AE=A4=E9=A1=B5+?= =?UTF-8?q?=E6=88=91=E7=9A=84=E6=89=AB=E7=A0=81=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 小程序 pages/scan-login(读scene→复用微信登录→/auth/mp-qr/confirm→目标端口登录) - 我的页「跨端口登录·扫码登录其它端口」; auth.js mpQrConfirm - 网站 Login.jsx PollingQr 支持 qr_image(小程序码)渲染 --- miniprogram/src/pages/mine/index.jsx | 29 +++++++- miniprogram/src/pages/mine/index.scss | 2 + .../src/pages/scan-login/index.config.js | 1 + miniprogram/src/pages/scan-login/index.jsx | 67 +++++++++++++++++++ miniprogram/src/pages/scan-login/index.scss | 13 ++++ miniprogram/src/utils/auth.js | 7 ++ website/src/platform/Login.jsx | 9 ++- 7 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 miniprogram/src/pages/scan-login/index.config.js create mode 100644 miniprogram/src/pages/scan-login/index.jsx create mode 100644 miniprogram/src/pages/scan-login/index.scss diff --git a/miniprogram/src/pages/mine/index.jsx b/miniprogram/src/pages/mine/index.jsx index ad62e15..72867cf 100644 --- a/miniprogram/src/pages/mine/index.jsx +++ b/miniprogram/src/pages/mine/index.jsx @@ -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() { + {/* 跨端口扫码登录:扫电脑/网页登录页的「小程序扫码」二维码 → 用本小程序确认登录该端口 */} + + 跨端口登录 + 在电脑/网页登录页点「小程序扫码」,用本小程序「扫一扫」即可登录该端口 + + {scanErr && {scanErr}} + + {/* 下一场活动倒计时(个人信息卡之后显示;取最早的待参加且未开始的报名场次) */} {(() => { const now = Date.now(); diff --git a/miniprogram/src/pages/mine/index.scss b/miniprogram/src/pages/mine/index.scss index 4d57742..93a282b 100644 --- a/miniprogram/src/pages/mine/index.scss +++ b/miniprogram/src/pages/mine/index.scss @@ -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; } diff --git a/miniprogram/src/pages/scan-login/index.config.js b/miniprogram/src/pages/scan-login/index.config.js new file mode 100644 index 0000000..4e65251 --- /dev/null +++ b/miniprogram/src/pages/scan-login/index.config.js @@ -0,0 +1 @@ +export default { navigationBarTitleText: '扫码登录' }; diff --git a/miniprogram/src/pages/scan-login/index.jsx b/miniprogram/src/pages/scan-login/index.jsx new file mode 100644 index 0000000..e80ec42 --- /dev/null +++ b/miniprogram/src/pages/scan-login/index.jsx @@ -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 ( + + + {done ? '✓' : '🔐'} + {done ? '已登录' : '扫码登录确认'} + + {done ? msg : '确认使用当前微信账号登录你正在操作的端口?'} + + {!done && scene && 登录请求 #{scene.slice(0, 12)}} + {msg && !done && {msg}} + {!done ? ( + + + + + ) : ( + + )} + + + ); +} diff --git a/miniprogram/src/pages/scan-login/index.scss b/miniprogram/src/pages/scan-login/index.scss new file mode 100644 index 0000000..48f4307 --- /dev/null +++ b/miniprogram/src/pages/scan-login/index.scss @@ -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; } diff --git a/miniprogram/src/utils/auth.js b/miniprogram/src/utils/auth.js index 9f0f557..920c1cb 100644 --- a/miniprogram/src/utils/auth.js +++ b/miniprogram/src/utils/auth.js @@ -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); diff --git a/website/src/platform/Login.jsx b/website/src/platform/Login.jsx index 4678117..96aa918 100644 --- a/website/src/platform/Login.jsx +++ b/website/src/platform/Login.jsx @@ -13,6 +13,7 @@ import '@/styles/auth.css'; /** 二维码轮询登录(微信扫码 / 小程序扫码共用) */ function PollingQr({ start, poll, tip, onResult }) { const [qrUrl, setQrUrl] = React.useState(''); + const [qrImage, setQrImage] = React.useState(''); const [status, setStatus] = React.useState('loading'); const [error, setError] = React.useState(''); @@ -21,8 +22,10 @@ function PollingQr({ start, poll, tip, onResult }) { (async () => { setStatus('loading'); try { - const { scene, qr_url } = await start(); - setQrUrl(qr_url); + const r = await start(); + const { scene, qr_url, qr_image } = r || {}; + setQrUrl(qr_url || ''); + setQrImage(qr_image || ''); setStatus('pending'); timer = window.setInterval(async () => { const res = await poll(scene); @@ -55,7 +58,7 @@ function PollingQr({ start, poll, tip, onResult }) { return (
- + {qrImage ? 小程序码 : }

{tip}