feat(profile): 桌面端账户管理「绑定小程序」—— 小程序码+扫码确认绑定

- 小程序行加「绑定」按钮 → Modal(bind-start小程序码, 轮询确认); auth.ts 加 bindMpStart
This commit is contained in:
Pine
2026-08-26 12:48:17 +08:00
parent f0ad772b92
commit b300c682a8
2 changed files with 43 additions and 2 deletions
+5
View File
@@ -190,6 +190,11 @@ export const authApi = {
});
},
/** 发起「绑定小程序」:返回小程序码(微信扫码自动打开小程序确认),绑定到当前账号 */
bindMpStart: async (): Promise<{ scene: string; qr_image?: string; qr_url?: string; mp_enabled: boolean }> => {
return request("/auth/mp-qr/bind-start");
},
/** 小程序扫码登录:轮询登录态 */
mpQrPoll: async (scene: string): Promise<{ status: string; token?: string; profile?: UserProfile }> => {
return request(`/auth/mp-qr/poll?scene=${encodeURIComponent(scene)}`);
+38 -2
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
import { Avatar, Button, Divider, Form, Input, Space, Tag } from "antd";
import { useEffect, useState, useRef } from "react";
import { Avatar, Button, Divider, Form, Input, Modal, Space, Tag, Spin } from "antd";
import { PageHeader } from "@/components/PageHeader";
import { useAppMessage } from "../../../hooks/useAppMessage";
@@ -81,6 +81,31 @@ function AccountPage() {
finally { setBinding(false); }
};
// 绑定小程序 modal
const [bindMpOpen, setBindMpOpen] = useState(false);
const [mpImg, setMpImg] = useState("");
const timerRef = useRef<number | null>(null);
useEffect(() => {
if (!bindMpOpen) return;
let scene = "";
(async () => {
try {
const r = await authApi.bindMpStart();
scene = r.scene;
setMpImg(r.qr_image || "");
timerRef.current = window.setInterval(async () => {
try {
const poll = await authApi.mpQrPoll(scene);
if (poll.status === "done") { window.clearInterval(timerRef.current || 0); setBindMpOpen(false); message.success("小程序绑定成功"); await refreshBindings(); }
else if (poll.status === "expired") { window.clearInterval(timerRef.current || 0); setBindMpOpen(false); }
} catch { /* ignore */ }
}, 2000);
} catch (e) { message.error(e instanceof Error ? e.message : "发起失败"); setBindMpOpen(false); }
})();
return () => { if (timerRef.current) window.clearInterval(timerRef.current); };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [bindMpOpen]);
const onSaveProfile = async (values: { nickname?: string }) => {
setSaving(true);
try {
@@ -197,6 +222,7 @@ function AccountPage() {
<span></span>
<Space>
<Tag color={bind.wxMiniBound ? "green" : "default"}>{bind.wxMiniBound ? "已绑定" : "未绑定"}</Tag>
{!bind.wxMiniBound && <Button size="small" type="primary" onClick={() => setBindMpOpen(true)}></Button>}
{bind.wxMiniBound && <Button size="small" loading={binding} onClick={() => doUnbind("wx_mini")}></Button>}
</Space>
</div>
@@ -234,7 +260,17 @@ function AccountPage() {
</Button>
</Form>
<Divider />
<div className={styles.bindTip}> </div>
</div>
<Modal open={bindMpOpen} footer={null} onCancel={() => setBindMpOpen(false)} title="绑定小程序">
<div style={{ textAlign: "center", padding: "12px 0" }}>
<p style={{ color: "#888", marginBottom: 12 }}></p>
{mpImg ? <img src={mpImg} alt="小程序码" style={{ width: 200, height: 200 }} /> : <Spin />}
</div>
</Modal>
</div>
);
}