From d0979aa8efc540eab4d5b6b34e9d605389293dea Mon Sep 17 00:00:00 2001 From: Pine Date: Wed, 26 Aug 2026 12:37:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(profile):=20=E7=BD=91=E7=AB=99=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E4=B8=AD=E5=BF=83=E5=8A=A0=E3=80=8C=E7=99=BB=E5=BD=95?= =?UTF-8?q?=E4=B8=8E=E7=BB=91=E5=AE=9A=E3=80=8D=20=E2=80=94=E2=80=94=20?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=E6=89=8B=E6=9C=BA=E5=8F=B7,=20=E8=A7=A3?= =?UTF-8?q?=E7=BB=91=20=E6=89=8B=E6=9C=BA/=E5=B0=8F=E7=A8=8B=E5=BA=8F/?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Profile 页加「登录与绑定」区块: 绑定手机号(验证码), 展示+解绑 手机/小程序/微信 - services/auth.js 加 bindPhone(合并返新令牌)/unbind/me --- website/src/services/auth.js | 14 +++++ website/src/styles/booking.css | 15 +++++ website/src/user/Profile.jsx | 100 +++++++++++++++++++++++++++++---- 3 files changed, 117 insertions(+), 12 deletions(-) diff --git a/website/src/services/auth.js b/website/src/services/auth.js index 1dbf5a8..87bf47f 100644 --- a/website/src/services/auth.js +++ b/website/src/services/auth.js @@ -66,6 +66,20 @@ export async function login(username, password) { return data; } +/* ---------------- 绑定 / 解绑(统一账号,按手机号合并,绝不新建账号) ---------------- */ +export async function bindPhone(phone, code) { + const data = await req('/auth/bind-phone', { body: { phone, code } }); + if (data.token) saveSession(data); // 可能合并成另一账号,切换新令牌 + return data; +} +export async function unbind(type) { + return req('/auth/unbind', { body: { type }, auth: true }); +} +/** 拉取当前账号(含 phoneBound/wxBound/wxMiniBound) */ +export async function me() { + return req('/auth/me', { method: 'GET', auth: true }); +} + /* ---------------- 微信扫码 / 小程序扫码登录 ---------------- */ export async function wxQrStart() { return req('/auth/wx-qr/start', { method: 'GET' }); diff --git a/website/src/styles/booking.css b/website/src/styles/booking.css index 7435b64..9051551 100644 --- a/website/src/styles/booking.css +++ b/website/src/styles/booking.css @@ -122,3 +122,18 @@ .bk-ev-status.done { color: #9a9a9a; background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.18); } .bk-ev-status.pending { color: #8e8e8e; background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.18); } .bk-ev-status.invite { color: #a8c4ff; background: rgba(168,196,255,0.12); border-color: rgba(168,196,255,0.4); } + +/* 个人中心 · 登录与绑定 */ +.bk-line { + display: flex; align-items: center; justify-content: space-between; + padding: 12px 0; border-bottom: 1px solid var(--line); font-size: 14px; color: #fff; +} +.bk-line > span:first-child { color: #c4c2c3; } +.bk-bound { display: inline-flex; align-items: center; gap: 10px; color: #fff; } +.pf-bindform { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; } +.pf-bindform input { + flex: 1; min-width: 130px; padding: 9px 12px; border-radius: 10px; + background: #161618; border: 1px solid #2a2a2d; color: #fff; font-size: 13.5px; +} +.pf-tip { margin-top: 12px; font-size: 12.5px; color: #8e8e8e; } +.btn-sm { font-size: 12.5px; padding: 4px 12px; line-height: 1.7; } diff --git a/website/src/user/Profile.jsx b/website/src/user/Profile.jsx index accfc01..9d91ed8 100644 --- a/website/src/user/Profile.jsx +++ b/website/src/user/Profile.jsx @@ -2,11 +2,11 @@ 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 } from '@/services/auth'; +import { getUser, updateProfile, bindPhone, unbind, sendCode, me } from '@/services/auth'; import { BK_STATUS_OPTIONS, BK_TOPIC_OPTIONS, BK_SOURCE_OPTIONS } from '@/data/booking'; import '@/styles/booking.css'; -/* 个人中心:报名资料设置(姓名/状态/主题/来源),报名时自动带入 */ +/* 个人中心:报名资料设置(姓名/状态/主题/来源),报名时自动带入 + 登录方式绑定 */ export default function Profile() { const [authed, setAuthed] = React.useState(!!getUser()); const user = getUser() || {}; @@ -21,17 +21,61 @@ export default function Profile() { const [busy, setBusy] = React.useState(false); const set = (k) => (v) => setForm((f) => ({ ...f, [k]: v })); - const save = async (e) => { - e.preventDefault(); - setErr(''); setMsg(''); setBusy(true); + /* 绑定管理 */ + const [bind, setBind] = React.useState({ phone: '', phoneBound: false, wxBound: false, wxMiniBound: false }); + const [bphone, setBphone] = React.useState(''); + const [bcode, setBcode] = React.useState(''); + const [cd, setCd] = React.useState(0); + const [bindMsg, setBindMsg] = React.useState(''); + const [bindErr, setBindErr] = React.useState(''); + const [binding, setBinding] = React.useState(false); + + const refreshBind = async () => { try { - const r = await updateProfile({ name: form.name.trim(), status: form.status, topics: form.topics, source: form.source }); - if (r.ok) { setMsg('已保存,报名时将自动带入。'); } - else throw new Error(r.detail || r.error || '保存失败'); - setAuthed(true); // 若此前未登录,登录卡回来就刷新 - } catch (ex) { - setErr(ex && ex.message ? ex.message : '保存失败,请重试'); - } finally { setBusy(false); } + const p = await me(); + setBind({ + phone: p.phone || '', phoneBound: !!p.phoneBound, wxBound: !!p.wxBound, wxMiniBound: !!p.wxMiniBound, + }); + } catch (e) { /* 未登录 */ } + }; + React.useEffect(() => { if (authed) void refreshBind(); }, [authed]); + + const send = async () => { + setBindErr(''); + if (!/^1\d{10}$/.test(bphone)) { setBindErr('请输入 11 位手机号'); return; } + setBinding(true); + try { + const r = await sendCode(bphone); + setBindMsg(`验证码已发送${r.debugCode ? `(演示:${r.debugCode})` : ''}`); + setCd(60); + const t = window.setInterval(() => setCd((c) => (c <= 1 ? (window.clearInterval(t), 0) : c - 1)), 1000); + } catch (e) { setBindErr((e && e.message) || '发送失败'); } + finally { setBinding(false); } + }; + + const doBind = async (e) => { + e.preventDefault(); + setBindErr(''); setBindMsg(''); + if (!/^1\d{10}$/.test(bphone)) { setBindErr('请输入 11 位手机号'); return; } + if (!bcode) { setBindErr('请输入验证码'); return; } + setBinding(true); + try { + await bindPhone(bphone, bcode); // 可能合并成另一账号并切换新令牌 + setBindMsg('绑定成功'); setBphone(''); setBcode(''); + setAuthed(true); + await refreshBind(); + } catch (ex) { setBindErr((ex && ex.message) || '绑定失败'); } + finally { setBinding(false); } + }; + + const doUnbind = async (type) => { + setBinding(true); setBindErr(''); + try { + await unbind(type); + setBindMsg('已解绑'); + await refreshBind(); + } catch (ex) { setBindErr((ex && ex.message) || '解绑失败'); } + finally { setBinding(false); } }; if (!authed) { @@ -61,6 +105,38 @@ export default function Profile() { + + {/* 登录方式绑定 */} +
+ +
+
手机号 + {bind.phoneBound + ? {bind.phone} + : ( +
+ setBphone(e.target.value)} placeholder="11 位手机号" /> + setBcode(e.target.value)} placeholder="验证码" style={{ width: 100 }} /> + + +
+ )} +
+
小程序 + {bind.wxMiniBound ? '已绑定' : '未绑定'} + {bind.wxMiniBound && } + +
+
微信(开放平台) + {bind.wxBound ? '已绑定' : '未绑定'} + {bind.wxBound && } + +
+ {bindMsg &&
{bindMsg}
} + {bindErr &&
{bindErr}
} +

小程序绑定:用同一手机号登录过小程序(微信)后会自动与当前账号合并为同一账号。

+
+
); }