feat(profile): 网站个人中心「绑定小程序」—— 小程序码+扫码确认绑定

- 小程序行加「绑定」→ 显示bind-start小程序码, 轮询确认; services/auth 加 mpQrBindStart
This commit is contained in:
Pine
2026-08-26 12:48:17 +08:00
parent d0979aa8ef
commit c7d9a74063
3 changed files with 40 additions and 1 deletions
+4
View File
@@ -93,6 +93,10 @@ export async function mpQrStart() {
export async function mpQrPoll(scene) {
return req(`/auth/mp-qr/poll?scene=${encodeURIComponent(scene)}`, { method: 'GET' });
}
/** 发起「绑定小程序」:返回小程序码(微信扫码自动打开小程序确认),绑定到当前账号 */
export async function mpQrBindStart() {
return req('/auth/mp-qr/bind-start', { method: 'GET', auth: true });
}
/** 扫码/短信登录成功:落 session 后返回完整平台 profile */
export function applyAuthResult(data) {
if (data && data.token) saveSession(data);
+4
View File
@@ -137,3 +137,7 @@
}
.pf-tip { margin-top: 12px; font-size: 12.5px; color: #8e8e8e; }
.btn-sm { font-size: 12.5px; padding: 4px 12px; line-height: 1.7; }
/* 绑定小程序 */
.pf-mpbox { display: flex; flex-direction: column; align-items: center; gap: 10px; margin-top: 12px; padding: 10px; }
.pf-mpimg { width: 180px; height: 180px; border-radius: 8px; }
+32 -1
View File
@@ -2,7 +2,7 @@ import React from 'react';
import { ContentTemplate } from '@/components/templates';
import { SectionHead, FieldText, FieldSelect, FieldCheck, Button, Icon } from '@/components/atoms';
import AuthGate from '@/components/AuthGate';
import { getUser, updateProfile, bindPhone, unbind, sendCode, me } from '@/services/auth';
import { getUser, updateProfile, bindPhone, unbind, sendCode, me, mpQrBindStart, mpQrPoll } from '@/services/auth';
import { BK_STATUS_OPTIONS, BK_TOPIC_OPTIONS, BK_SOURCE_OPTIONS } from '@/data/booking';
import '@/styles/booking.css';
@@ -78,6 +78,30 @@ export default function Profile() {
finally { setBinding(false); }
};
/* 绑定小程序:发起 bind-start → 展示小程序码 → 轮询确认 */
const [mpImg, setMpImg] = React.useState('');
const [mpStarting, setMpStarting] = React.useState(false);
const bindMini = async () => {
setBindErr(''); setMpStarting(true); setMpImg('');
try {
const r = await mpQrBindStart();
setMpImg(r.qr_image || '');
setBindMsg('请用微信「扫一扫」打开小程序并确认绑定');
if (r.scene) {
const t = window.setInterval(async () => {
try {
const poll = await mpQrPoll(r.scene);
if (poll.status === 'done') {
window.clearInterval(t); setMpImg(''); setBindMsg('小程序绑定成功');
await refreshBind();
} else if (poll.status === 'expired') { window.clearInterval(t); setMpImg(''); setBindMsg('二维码已过期,请重试'); }
} catch { /* ignore */ }
}, 2000);
}
} catch (ex) { setBindErr((ex && ex.message) || '发起失败'); }
finally { setMpStarting(false); }
};
if (!authed) {
return (
<ContentTemplate active="profile" kicker="Profile" title="个人中心" desc="完善报名资料,报名活动时自动带入,无需重复填写。">
@@ -124,9 +148,16 @@ export default function Profile() {
</div>
<div className="bk-line"><span>小程序</span>
<span className="bk-bound">{bind.wxMiniBound ? '已绑定' : '未绑定'}
{!bind.wxMiniBound && <button type="button" className="btn btn-cta btn-sm" disabled={binding || mpStarting} onClick={bindMini}>{mpStarting ? '发起中…' : '绑定'}</button>}
{bind.wxMiniBound && <button type="button" className="btn btn-dark btn-sm" disabled={binding} onClick={() => doUnbind('wx_mini')}>解绑</button>}
</span>
</div>
{mpImg && (
<div className="pf-mpbox">
<img src={mpImg} alt="小程序码" className="pf-mpimg" />
<p className="pf-tip">用微信扫一扫打开小程序在你手机上点确认登录完成绑定</p>
</div>
)}
<div className="bk-line"><span>微信(开放平台)</span>
<span className="bk-bound">{bind.wxBound ? '已绑定' : '未绑定'}
{bind.wxBound && <button type="button" className="btn btn-dark btn-sm" disabled={binding} onClick={() => doUnbind('wx')}>解绑</button>}