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
);
}