import { View, Text, Button, Input, Image } from '@tarojs/components'; import Taro from '@tarojs/taro'; import { useState, useEffect } from 'react'; import { useTabIndex } from '@/utils/tab'; import { isAuthed, getUser, logout, wxLogin, phoneLogin, bindPhone, sendCode, wxPhoneBind, updateProfile, getMe } from '@/utils/auth'; import { getMyBookings, checkIn, parseIso, BK_STATUS_OPTIONS, BK_TOPIC_OPTIONS, BK_SOURCE_OPTIONS } from '@/utils/booking'; import { getMyAdmission } from '@/utils/parks'; import { API_BASE } from '@/utils/api'; import Skeleton from '@/components/skeleton'; import CountdownBox from '@/components/CountdownBox'; import './index.scss'; import { PageHeader } from '@/components/TopInset'; /* 报名标签:按 auditStatus + event_status 推断 待参加 / 审核中 / 已结束 / 未通过 */ const PHASE_META = { pending: { text: '审核中', cls: 'pending' }, rejected: { text: '未通过', cls: 'rejected' }, done: { text: '已结束', cls: 'done' }, upcoming: { text: '待参加', cls: 'upcoming' } }; const phaseOf = (b) => { // 兼容 camelCase(后端新契约) 与 snake_case(旧数据) const audit = b.auditStatus !== undefined ? b.auditStatus : b.audit_status; const evStatus = b.event_status !== undefined ? b.event_status : b.eventStatus; if (audit === 'pending') return PHASE_META.pending; if (audit === 'rejected') return PHASE_META.rejected; if (evStatus === 'done') return PHASE_META.done; return PHASE_META.upcoming; }; /* 报名状态筛选(对齐活动页 全部/公益/... 的筛选形式) */ const PHASE_FILTERS = [ { key: 'all', label: '全部' }, { key: 'upcoming', label: '待参加' }, { key: 'pending', label: '审核中' }, { key: 'rejected', label: '未通过' }, { key: 'done', label: '已结束' } ]; const fmtTime = (iso) => { if (!iso) return ''; const d = new Date(iso); const p = (n) => String(n).padStart(2, '0'); const week = ['日', '一', '二', '三', '四', '五', '六'][d.getDay()]; return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())} 周${week}`; }; export default function Mine() { useTabIndex(4); const [user, setUser] = useState(getUser()); const [bookings, setBookings] = useState([]); const [phase, setPhase] = useState('all'); // 报名状态筛选 const [admission, setAdmission] = useState([]); // 我的园区入驻记录 const [loaded, setLoaded] = useState(false); const [busy, setBusy] = useState(false); const [err, setErr] = useState(''); /* 绑定手机 */ const [showBind, setShowBind] = useState(false); const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); const [cd, setCd] = useState(0); const [bindDebug, setBindDebug] = useState(''); /* 手机号登录 */ const [showPhone, setShowPhone] = useState(false); const [pPhone, setPPhone] = useState(''); const [pCode, setPCode] = useState(''); const [pCd, setPCd] = useState(0); const [pDebug, setPDebug] = useState(''); const refresh = async () => { if (!isAuthed()) { setUser(null); setLoaded(true); return; } try { const me = await getMe(); if (me.ok && me.user) setUser(getUser()); } catch (e) { // 401 时 api.js 已清 pine_token:同步清掉本地假登录态,显示未登录卡片 if (!isAuthed()) { setUser(null); logout(); } } try { const d = await getMyBookings(); setBookings((d && d.list) || []); } catch { /* 保留上次列表,避免重进闪空 */ } try { const a = await getMyAdmission(); setAdmission(Array.isArray(a) ? a : []); } catch { /* 保留上次 */ } finally { setLoaded(true); } }; Taro.useDidShow(() => { refresh(); }); /* 微信一键登录 */ const onWxLogin = async () => { setErr(''); setBusy(true); try { let nickName = '', avatarUrl = ''; try { const { userInfo } = await Taro.getUserProfile({ desc: '用于完善会员资料' }); nickName = (userInfo && userInfo.nickName) || ''; avatarUrl = (userInfo && userInfo.avatarUrl) || ''; } catch { /* 用户拒绝授权昵称,仍可用 code 登录 */ } const { code: loginCode } = await Taro.login(); const data = await wxLogin({ code: loginCode, nickName, avatarUrl }); setUser(getUser()); refresh(); } catch (e) { setErr((e && e.message) || '微信登录失败,请重试'); } finally { setBusy(false); } }; const sendBind = async () => { if (!/^1\d{10}$/.test(phone)) { setErr('请输入正确的 11 位手机号'); return; } setErr(''); setBusy(true); try { const r = await sendCode(phone); setBindDebug(`验证码已发送${r.debugCode ? ',演示验证码:' + r.debugCode : ''}`); setCd(60); const t = setInterval(() => setCd((v) => { if (v <= 1) { clearInterval(t); return 0; } return v - 1; }), 1000); } catch (e) { setErr((e && e.message) || '发送失败'); } finally { setBusy(false); } }; const doBind = async () => { if (!/^1\d{10}$/.test(phone)) { setErr('请输入正确的 11 位手机号'); return; } if (!code) { setErr('请输入验证码'); return; } setErr(''); setBusy(true); try { const r = await bindPhone(phone, code); if (r.ok) { setShowBind(false); setPhone(''); setCode(''); setBindDebug(''); setUser(getUser()); Taro.showToast({ title: '手机绑定成功', icon: 'success' }); } else { setErr(r.error || '绑定失败'); } } catch (e) { setErr((e && e.message) || '绑定失败'); } finally { setBusy(false); } }; /* 微信官方一键绑定手机号(getPhoneNumber 回调) */ const onWxPhone = async (e) => { const detail = (e.detail && e.detail) || {}; const pcode = detail.code; if (!pcode) { Taro.showToast({ title: '已取消授权', icon: 'none' }); return; } setBusy(true); setErr(''); try { const r = await wxPhoneBind(pcode); if (r.ok) { setShowBind(false); setUser(getUser()); Taro.showToast({ title: '手机绑定成功', icon: 'success' }); } else { Taro.showToast({ title: r.error || '绑定失败', icon: 'none' }); } } catch (ex) { Taro.showToast({ title: (ex && ex.message) || '绑定失败', icon: 'none' }); } finally { setBusy(false); } }; const sendPhone = async () => { if (!/^1\d{10}$/.test(pPhone)) { setErr('请输入正确的 11 位手机号'); return; } setErr(''); setBusy(true); try { const r = await sendCode(pPhone); setPDebug(`验证码已发送${r.debugCode ? ',演示验证码:' + r.debugCode : ''}`); setPCd(60); const t = setInterval(() => setPCd((v) => { if (v <= 1) { clearInterval(t); return 0; } return v - 1; }), 1000); } catch (e) { setErr((e && e.message) || '发送失败'); } finally { setBusy(false); } }; const doPhoneLogin = async () => { if (!/^1\d{10}$/.test(pPhone)) { setErr('请输入正确的 11 位手机号'); return; } if (!pCode) { setErr('请输入验证码'); return; } setErr(''); setBusy(true); try { await phoneLogin(pPhone, pCode); setUser(getUser()); setShowPhone(false); refresh(); } catch (e) { setErr((e && e.message) || '登录失败'); } finally { setBusy(false); } }; const onCheckin = async (id) => { setErr(''); try { const r = await checkIn(id); if (r.ok) { Taro.showToast({ title: '签到成功', icon: 'success' }); refresh(); } else { Taro.showToast({ title: r.error || '签到失败', icon: 'none' }); } } catch (e) { Taro.showToast({ title: (e && e.message) || '签到失败', icon: 'none' }); } }; /* 微信头像昵称填写能力:选头像→上传;填昵称→保存。确保登录后能渲染真实头像/昵称 */ const [nickDraft, setNickDraft] = useState(() => (getUser() || {}).name || ''); /* 登录/登出/刷新用户后,昵称草稿随 user.name 同步,避免登出残留或登录后不显示昵称 */ useEffect(() => { setNickDraft((user && user.name) || ''); }, [user]); /* 报名资料(姓名/状态/主题/来源)——个人中心设置,报名自动带入 */ const [pName, setPName] = useState((user && user.name) || ''); const [pStatus, setPStatus] = useState((user && user.status) || ''); const [pTopics, setPTopics] = useState((user && user.topics) || []); const [pSource, setPSource] = useState((user && user.source) || ''); const [saving, setSaving] = useState(false); useEffect(() => { setPName((user && user.name) || ''); setPStatus((user && user.status) || ''); setPTopics((user && user.topics) || []); setPSource((user && user.source) || ''); }, [user]); const savePrefs = async () => { setErr(''); if (!pName.trim()) { setErr('请输入姓名'); return; } setSaving(true); try { const r = await updateProfile({ name: pName.trim(), status: pStatus, topics: pTopics, source: pSource }); if (r.ok) { setUser(getUser()); Taro.showToast({ title: '已保存', icon: 'success' }); } else { setErr(r.error || '保存失败'); } } catch (e) { setErr((e && e.message) || '保存失败'); } finally { setSaving(false); } }; const onChooseAvatar = async (e) => { const tempPath = e.detail && e.detail.avatarUrl; if (!tempPath) return; try { const token = Taro.getStorageSync('pine_token') || ''; const up = await Taro.uploadFile({ url: `${API_BASE}/api/upload`, filePath: tempPath, name: 'file', formData: { dir: 'avatar' }, header: { Authorization: `Bearer ${token}` } }); const data = JSON.parse(up.data || '{}'); if (!data.ok) throw new Error(data.error || '上传失败'); await updateProfile({ avatar: data.url }); setUser(getUser()); Taro.showToast({ title: '头像已更新', icon: 'success' }); } catch (ex) { Taro.showToast({ title: (ex && ex.message) || '头像更新失败', icon: 'none' }); } }; const onSaveNick = async () => { const v = (nickDraft || '').trim(); if (!v) { setNickDraft((getUser() || {}).name || ''); return; } try { await updateProfile({ name: v }); setUser(getUser()); Taro.showToast({ title: '昵称已更新', icon: 'success' }); } catch (ex) { Taro.showToast({ title: (ex && ex.message) || '昵称更新失败', icon: 'none' }); } }; const onLogout = () => { logout(); setUser(null); setBookings([]); setShowBind(false); setShowPhone(false); setPhone(''); setCode(''); setBindDebug(''); setErr(''); Taro.showToast({ title: '已退出登录', icon: 'none' }); }; /* ============ 未登录:登录入口 ============ */ if (!user || !isAuthed()) { return ( 登录后可绑定手机、查看报名场次与活动签到 setShowPhone(!showPhone)}>{showPhone ? '收起手机号登录' : '或用手机号登录'} {showPhone && ( 手机号 setPPhone(e.detail.value)} type="number" maxlength={11} placeholder="11 位手机号" className="input" /> 验证码 setPCode(e.detail.value)} type="number" maxlength={6} placeholder="6 位验证码" className="input flex" /> {pDebug && {pDebug}} )} {err && {err}} 云南省首个由政府主办的免费大学生创业服务平台 云南派音人工智能科技有限公司 && 云南汇浙青创科技有限公司 像素字体 BubbledotICG-FinePos · CC BY 4.0 ); } /* ============ 已登录:资料 + 我的报名 ============ */ return ( {/* 资料卡 */} setNickDraft(e.detail.value)} placeholder="点此填写昵称" /> {user.phoneBound ? (user.phone ? `已绑定 ${user.phone}` : '已绑定手机') : '未绑定手机'} {user.account_type_label || user.role ? ( {user.account_type_label || user.role} ) : null} {nickDraft && nickDraft !== ((user && user.name) || '') && ( )} {/* 下一场活动倒计时(个人信息卡之后显示;取最早的待参加且未开始的报名场次) */} {(() => { const now = Date.now(); const upcoming = bookings .filter((b) => b.event_status !== 'done' && !b.checkinAt && parseIso(b.eventStart) && parseIso(b.eventStart).getTime() > now) .sort((a, b) => parseIso(a.eventStart).getTime() - parseIso(b.eventStart).getTime()); const nxt = upcoming[0]; if (!nxt) return null; return ( 您参加的下一场活动 {nxt.eventTitle} {nxt.eventStart && {fmtTime(nxt.eventStart)} · 敬请准时到场} ); })()} {/* 绑定手机 */} {!user.phoneBound && ( 绑定手机 绑定后用于接收活动通知与报名确认 {!showBind ? ( ) : ( 手机号 setPhone(e.detail.value)} type="number" maxlength={11} placeholder="11 位手机号" className="input" /> 验证码 setCode(e.detail.value)} type="number" maxlength={6} placeholder="6 位验证码" className="input flex" /> {bindDebug && {bindDebug}} )} )} {/* 我的报名 */} BOOK 我的报名({bookings.length}) {PHASE_FILTERS.map((f) => ( setPhase(f.key)}>{f.label} ))} {(() => { const shown = phase === 'all' ? bookings : bookings.filter((b) => phaseOf(b).cls === phase); if (!loaded) return ; if (shown.length === 0) return ( {bookings.length === 0 ? '还没有报名记录。' : '该状态下暂无报名。'} Taro.switchTab({ url: '/pages/booking/index' })}>去看活动 → ); return ( {shown.map((b) => { const p = phaseOf(b); const ended = b.event_status === 'done'; const ready = b.can_checkin; return ( {b.eventTitle || b.want || '报名'} {p.text} {b.eventStart && 场次时间:{fmtTime(b.eventStart)}} 报名时间:{fmtTime(b.createdAt)} 编号 {b.id} · {b.name || '—'} {b.checkinAt ? ( 已于 {fmtTime(b.checkinAt)} 签到 ) : ended ? ( 本场已结束 ) : ready ? ( ) : ( 未到签到时间,活动开始后即可签到 )} ); })} ); })()} {/* 我的接单:扫码接单;任务广场移到 Web / 桌面端 */} ACCEPT 我的接单 任务广场请到 Web 端或桌面端查看 {/* 算力管理:余额 / API Key / 算力充值(对齐桌面端算力中心) */} COMPUTE 算力管理 查看余额与 API Key,支持微信充值算力 {/* 我的园区:未入驻 → 申请入驻 + OPC认证;已入驻 → 园区信息 + OPC认证 + 转园 */} PARK 我的园区 {(() => { const ad = admission[0] || {}; const st = String(ad.status || ad.audit_status || '').toLowerCase(); const admitted = ['approved', 'verified', 'admitted'].includes(st) || ['已入驻', '通过', 'approved'].includes(ad.status); if (!admitted) { return ( 暂未入驻园区,可申请入驻并完成 OPC 认证。 ); } const name = ad.tenant_name || ad.park_name || ad.company_name || ad.company || '已入驻园区'; return ( {name} {ad.region ? 区域:{ad.region} : null} ); })()} {err && {err}} 云南省首个由政府主办的免费大学生创业服务平台 云南派音人工智能科技有限公司 · 云南汇浙青创科技有限公司 像素字体 BubbledotICG-FinePos · CC BY 4.0 ); }