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}