feat(profile): 网站个人中心加「登录与绑定」 —— 绑定手机号, 解绑 手机/小程序/微信
- Profile 页加「登录与绑定」区块: 绑定手机号(验证码), 展示+解绑 手机/小程序/微信 - services/auth.js 加 bindPhone(合并返新令牌)/unbind/me
This commit is contained in:
@@ -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' });
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
{/* 登录方式绑定 */}
|
||||
<section className="section">
|
||||
<SectionHead no="02" title="登录与绑定" en="Security" />
|
||||
<div className="form-card">
|
||||
<div className="bk-line"><span>手机号</span>
|
||||
{bind.phoneBound
|
||||
? <span className="bk-bound">{bind.phone} <button type="button" className="btn btn-dark btn-sm" disabled={binding} onClick={() => doUnbind('phone')}>解绑</button></span>
|
||||
: (
|
||||
<form onSubmit={doBind} className="pf-bindform">
|
||||
<input value={bphone} onChange={(e) => setBphone(e.target.value)} placeholder="11 位手机号" />
|
||||
<input value={bcode} onChange={(e) => setBcode(e.target.value)} placeholder="验证码" style={{ width: 100 }} />
|
||||
<button type="button" className="btn btn-dark btn-sm" disabled={binding || cd > 0} onClick={send}>{cd > 0 ? `${cd}s` : '验证码'}</button>
|
||||
<button className="btn btn-cta btn-sm" disabled={binding || !bphone || !bcode}>绑定</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
<div className="bk-line"><span>小程序</span>
|
||||
<span className="bk-bound">{bind.wxMiniBound ? '已绑定' : '未绑定'}
|
||||
{bind.wxMiniBound && <button type="button" className="btn btn-dark btn-sm" disabled={binding} onClick={() => doUnbind('wx_mini')}>解绑</button>}
|
||||
</span>
|
||||
</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>}
|
||||
</span>
|
||||
</div>
|
||||
{bindMsg && <div className="bk-ok"><Icon name="check" size="sm" /> {bindMsg}</div>}
|
||||
{bindErr && <div className="bk-err">{bindErr}</div>}
|
||||
<p className="pf-tip">小程序绑定:用同一手机号登录过小程序(微信)后会自动与当前账号合并为同一账号。</p>
|
||||
</div>
|
||||
</section>
|
||||
</ContentTemplate>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user