Files
training/website/src/user/Profile.jsx
T
Pine d0979aa8ef feat(profile): 网站个人中心加「登录与绑定」 —— 绑定手机号, 解绑 手机/小程序/微信
- Profile 页加「登录与绑定」区块: 绑定手机号(验证码), 展示+解绑 手机/小程序/微信
- services/auth.js 加 bindPhone(合并返新令牌)/unbind/me
2026-08-26 12:37:31 +08:00

143 lines
6.9 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { 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() || {};
const [form, setForm] = React.useState({
name: user.name || '',
status: user.status || '',
topics: user.topics || [],
source: user.source || ''
});
const [msg, setMsg] = React.useState('');
const [err, setErr] = React.useState('');
const [busy, setBusy] = React.useState(false);
const set = (k) => (v) => setForm((f) => ({ ...f, [k]: v }));
/* 绑定管理 */
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 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) {
return (
<ContentTemplate active="profile" kicker="Profile" title="个人中心" desc="完善报名资料,报名活动时自动带入,无需重复填写。">
<section className="section">
<AuthGate subtitle="登录后可设置并保存报名资料,报名活动自动带入。" onAuthed={() => setAuthed(true)} />
{err && <div className="bk-err">{err}</div>}
</section>
</ContentTemplate>
);
}
return (
<ContentTemplate active="profile" kicker="Profile" title="个人中心" desc="完善报名资料,报名活动时自动带入,无需重复填写。">
<section className="section">
<SectionHead no="01" title="报名资料" en="Profile" />
<form className="form-card" onSubmit={save}>
<FieldText label="姓名 / 称呼" placeholder="怎么称呼你" value={form.name} onChange={set('name')} />
<FieldSelect label="你的状态" options={BK_STATUS_OPTIONS} value={form.status} onChange={set('status')} />
<FieldCheck label="感兴趣的主题(可多选)" options={BK_TOPIC_OPTIONS} value={form.topics} onChange={set('topics')} />
<FieldSelect label="从哪知道我们" options={BK_SOURCE_OPTIONS} value={form.source} onChange={set('source')} />
{msg && <div className="bk-ok"><Icon name="check" size="sm" /> {msg}</div>}
{err && <div className="bk-err">{err}</div>}
<div className="form-actions">
<button className="btn btn-cta" type="submit" disabled={busy}>{busy ? '保存中…' : '保存资料'}</button>
</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>
);
}