import React from 'react'; import { ContentTemplate } from '@/components/templates'; import { SectionHead, FieldText, FieldSelect, FieldCheck, Icon } from '@/components/atoms'; import AuthGate from '@/components/AuthGate'; import { getUser, saveUser, updateProfile, bindPhone, unbind, sendCode, me, mpQrBindStart, mpQrPoll, } from '@/services/auth'; import { myBookings } from '@/services/booking'; import { myTasks } from '@/services/tasks'; import { myParkAdmission, uploadParkDoc } from '@/services/park'; import { getOpCertMine } from '@/services/opcProfile'; import { BK_STATUS_OPTIONS, BK_TOPIC_OPTIONS, BK_SOURCE_OPTIONS } from '@/data/booking'; import '@/styles/booking.css'; import '@/styles/user-center.css'; function maskPhone(p) { if (!p) return ''; if (/^1\d{10}$/.test(p)) return p.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); return p; } function parkText(s) { const k = String(s || '').toLowerCase(); if (['approved', 'verified', 'admitted'].includes(k) || s === '已入驻' || s === '通过') return '已入驻'; if (['pending', 'reviewing'].includes(k) || s === '审核中') return '审核中'; if (['rejected'].includes(k) || s === '未通过') return '未通过'; return s ? String(s) : '未提交'; } function certText(s) { const k = String(s || '').toLowerCase(); if (k === 'certified' || s === '已认证') return '已认证'; if (k === 'reviewing' || s === '审核中') return '审核中'; if (k === 'pending' || s === '待提交') return '待提交'; return s ? String(s) : '未认证'; } /* 个人中心:用户卡 + 我的服务 + 报名资料 + 登录绑定(玻璃卡片) */ export default function Profile() { const [user, setUser] = React.useState(getUser() || {}); const [authed, setAuthed] = React.useState(!!user.username || !!getUser()?.username); 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 [avBusy, setAvBusy] = React.useState(false); const set = (k) => (v) => setForm((f) => ({ ...f, [k]: v })); /* 我的服务实时状态 */ const [svc, setSvc] = React.useState({ bk: null, tk: null, park: '', cert: '' }); React.useEffect(() => { if (!authed) return; refreshBind(); // 挂载即拉取真实绑定状态(手机号/小程序/微信),不只绑定时才刷新 Promise.all([ myBookings().then((l) => setSvc((s) => ({ ...s, bk: Array.isArray(l) ? l.length : 0 }))).catch(() => {}), myTasks().then((r) => setSvc((s) => ({ ...s, tk: r.ok ? (r.items || []).length : 0 }))).catch(() => {}), myParkAdmission().then((r) => setSvc((s) => ({ ...s, park: parkText(r?.admission?.status || r?.status || '') }))).catch(() => {}), getOpCertMine().then((r) => setSvc((s) => ({ ...s, cert: certText(r?.certification_status || '') }))).catch(() => {}), ]); }, [authed]); /* 绑定管理 */ 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 }); // 用 fresh /me 回写本地用户缓存与用户卡,避免读 stale localStorage 导致手机号/绑定不同步 saveUser({ ...getUser(), phone: p.phone || '', phoneBound: !!p.phoneBound, wxBound: !!p.wxBound, wxMiniBound: !!p.wxMiniBound }); setUser(getUser() || {}); } catch (e) { /* 未登录 */ } }; const save = async (e) => { e.preventDefault(); setErr(''); setMsg(''); if (!form.name.trim()) { setErr('请填写姓名 / 称呼'); return; } setBusy(true); try { await updateProfile(form); setUser(getUser() || {}); setMsg('资料已保存'); } catch (ex) { setErr((ex && ex.message) || '保存失败'); } finally { setBusy(false); } }; const onAvatar = async (e) => { const f = e.target.files && e.target.files[0]; if (!f) return; setErr(''); setAvBusy(true); try { const url = await uploadParkDoc(f); await updateProfile({ avatar: url }); saveUser({ ...getUser(), avatar: url }); setUser(getUser() || {}); setMsg('头像已更新'); } catch (ex) { setErr((ex && ex.message) || '头像上传失败'); } finally { setAvBusy(false); e.target.value = ''; } }; 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); } }; /* 绑定小程序 */ 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 (
setAuthed(true)} /> {err &&
{err}
}
); } const svcGrid = [ ['calendar', '我的报名', svc.bk == null ? '加载中…' : `${svc.bk} 条报名`, '#/my-bookings'], ['route', '我的任务', svc.tk == null ? '加载中…' : `${svc.tk} 个任务`, '#/my-tasks'], ['map-pin', '我的园区', svc.park, '#/my-park'], ['shield', '我的认证', svc.cert, '#/my-cert'], ]; return (
{/* 用户卡 */}
{user.name || '用户'}
{maskPhone(user.phone) || user.username || '未绑定手机'}
{(user.account_type_label || user.role) && (
{user.account_type_label || user.role}
)}
{/* 我的服务 */}
{svcGrid.map(([icon, t, d, href]) => ( {t} {d} ))}
{msg &&
{msg}
} {err &&
{err}
}
{/* 登录方式绑定 */}
手机号 {bind.phoneBound ? {bind.phone} : (
setBphone(e.target.value)} placeholder="11 位手机号" /> setBcode(e.target.value)} placeholder="验证码" style={{ width: 100 }} />
)}
小程序 {bind.wxMiniBound ? '已绑定' : '未绑定'} {!bind.wxMiniBound && } {bind.wxMiniBound && }
{mpImg && (
小程序码

用微信扫一扫打开小程序,在你手机上点「确认登录」完成绑定

)}
微信(开放平台) {bind.wxBound ? '已绑定' : '未绑定'} {bind.wxBound && }
{bindMsg &&
{bindMsg}
} {bindErr &&
{bindErr}
}

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

); }