diff --git a/miniprogram/src/app.config.js b/miniprogram/src/app.config.js
index 02fb094..1828deb 100644
--- a/miniprogram/src/app.config.js
+++ b/miniprogram/src/app.config.js
@@ -16,7 +16,11 @@ export default {
'pages/content-detail/index',
'pages/tasks/index',
'pages/task-detail/index',
- 'pages/scan-login/index'
+ 'pages/scan-login/index',
+ 'pages/my-bookings/index',
+ 'pages/my-tasks/index',
+ 'pages/my-park/index',
+ 'pages/my-cert/index'
],
window: {
backgroundTextStyle: 'light',
diff --git a/miniprogram/src/pages/mine/index.jsx b/miniprogram/src/pages/mine/index.jsx
index 8c16900..506022d 100644
--- a/miniprogram/src/pages/mine/index.jsx
+++ b/miniprogram/src/pages/mine/index.jsx
@@ -41,6 +41,15 @@ const fmtTime = (iso) => {
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())} 周${week}`;
};
+/** 我的服务宫格项:label → 子页 */
+const SVC = [
+ ['报名', '/pages/my-bookings/index'],
+ ['任务', '/pages/my-tasks/index'],
+ ['园区', '/pages/my-park/index'],
+ ['认证', '/pages/my-cert/index'],
+];
+const maskPhone = (p) => (/^1\d{10}$/.test(p || '') ? p.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : (p || ''));
+
export default function Mine() {
useTabIndex(4);
const [user, setUser] = useState(getUser());
@@ -328,6 +337,9 @@ export default function Mine() {
{user.phoneBound ? (user.phone ? `已绑定 ${user.phone}` : '已绑定手机') : '未绑定手机'}
+ {user.account_type_label || user.role ? (
+ {user.account_type_label || user.role}
+ ) : null}
{nickDraft && nickDraft !== ((user && user.name) || '') && (
)}
@@ -335,6 +347,30 @@ export default function Mine() {
+ {/* 我的服务宫格:报名 / 任务 / 园区 / 认证 */}
+
+ {SVC.map(([label, url]) => (
+ Taro.navigateTo({ url })}>
+ {label.slice(0, 1)}
+ 我的{label}
+
+ ))}
+
+
+ {/* 账号绑定状态 */}
+
+ 账号绑定
+ 手机号
+ {user.phoneBound ? maskPhone(user.phone) : '未绑定'}
+
+ 微信(开放平台)
+ {user.wxBound ? '已绑定' : '未绑定'}
+
+ 小程序
+ {user.wxMiniBound ? '已绑定' : '未绑定'}
+
+
+
{/* 跨端口扫码登录:由电脑/网页登录页展示「小程序码」,微信扫码自动进入小程序授权页确认 */}
登录其它端
diff --git a/miniprogram/src/pages/mine/index.scss b/miniprogram/src/pages/mine/index.scss
index 29b8467..6d1b2db 100644
--- a/miniprogram/src/pages/mine/index.scss
+++ b/miniprogram/src/pages/mine/index.scss
@@ -378,3 +378,18 @@
.save-nick { margin-top: 12rpx; align-self: flex-start; line-height: 2.2; padding: 0 28rpx; font-size: 24rpx; }
.scan-err { display:block; margin-top:12rpx; font-size:24rpx; color:#f0b8b8; text-align:center; }
+
+/* ---- 我的服务宫格 ---- */
+.role-tag { display:inline-block; margin-top:10rpx; padding:2rpx 20rpx; border-radius:999px; font-size:24rpx; color:#1f8ce0; background:#eaf6ff; align-self:flex-start; }
+.mk-grid { display:flex; gap:16rpx; padding:0 24rpx; margin-top:24rpx; }
+.mk-grid-item { flex:1; background:#fff; border-radius:24rpx; padding:24rpx 8rpx; display:flex; flex-direction:column; align-items:center; gap:10rpx; box-shadow:0 8rpx 22rpx -12rpx rgba(13,47,78,.16); }
+.mk-grid-ico { width:72rpx; height:72rpx; border-radius:50%; display:flex; align-items:center; justify-content:center; background:#f2f8ff; color:#06acf1; font-size:30rpx; font-weight:700; }
+.mk-grid-label { font-size:24rpx; color:#23315c; font-weight:600; }
+
+/* ---- 账号绑定状态 ---- */
+.bind-state { margin-top:24rpx; }
+.bind-line { display:flex; align-items:center; justify-content:space-between; padding:16rpx 2rpx; border-bottom:1px solid #f0f2f6; }
+.bind-line:last-child { border-bottom:none; }
+.bind-key { font-size:26rpx; color:#6a6a73; }
+.bind-val { font-size:26rpx; color:#98a0ab; }
+.bind-val.on { color:#00a06e; font-weight:600; }
diff --git a/miniprogram/src/pages/my-bookings/index.config.js b/miniprogram/src/pages/my-bookings/index.config.js
new file mode 100644
index 0000000..154648a
--- /dev/null
+++ b/miniprogram/src/pages/my-bookings/index.config.js
@@ -0,0 +1 @@
+export default { navigationBarTitleText: '我的报名' };
diff --git a/miniprogram/src/pages/my-bookings/index.jsx b/miniprogram/src/pages/my-bookings/index.jsx
new file mode 100644
index 0000000..3fbf0b9
--- /dev/null
+++ b/miniprogram/src/pages/my-bookings/index.jsx
@@ -0,0 +1,92 @@
+import { View, Text, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { isAuthed } from '@/utils/auth';
+import { getMyBookings, checkIn, parseIso } from '@/utils/booking';
+import './index.scss';
+
+const PHASE_META = {
+ pending: { text: '审核中', cls: 'pending' },
+ rejected: { text: '未通过', cls: 'rejected' },
+ done: { text: '已结束', cls: 'done' },
+ upcoming: { text: '待参加', cls: 'upcoming' }
+};
+const phaseOf = (b) => {
+ 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 fmtTime = (iso) => {
+ if (!iso) return '';
+ const d = new Date(iso);
+ if (Number.isNaN(+d)) return iso;
+ const p = (n) => String(n).padStart(2, '0');
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
+};
+
+export default function MyBookings() {
+ const [bookings, setBookings] = useState([]);
+ const [loaded, setLoaded] = useState(false);
+ const [err, setErr] = useState('');
+ const [busyId, setBusyId] = useState('');
+
+ const load = async () => {
+ try {
+ const d = await getMyBookings();
+ setBookings((d && d.list) || []);
+ } catch (e) { setErr((e && e.message) || '加载失败'); }
+ finally { setLoaded(true); }
+ };
+ useEffect(() => { load(); }, []);
+
+ const onCheckin = async (id) => {
+ setBusyId(id); setErr('');
+ try {
+ const r = await checkIn(id);
+ if (r.ok) Taro.showToast({ title: '签到成功', icon: 'success' });
+ else Taro.showToast({ title: r.error || '签到失败', icon: 'none' });
+ load();
+ } catch (e) { Taro.showToast({ title: (e && e.message) || '签到失败', icon: 'none' }); }
+ finally { setBusyId(''); }
+ };
+ const goActivities = () => Taro.switchTab({ url: '/pages/booking/index' });
+
+ return (
+
+
+ 我的报名
+ 你参加过的公益课 / 活动报名与审核状态。
+
+ {!isAuthed() && 请先登录(我的 → 登录绑定)。}
+ {err && {err}}
+ {!loaded ? 加载中…
+ : bookings.length === 0 ? (
+
+ 还没有报名记录。
+ 去看活动 →
+
+ ) : bookings.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)} : null}
+ 报名于 {fmtTime(b.createdAt)}
+ {b.checkinAt ? 已于 {fmtTime(b.checkinAt)} 签到
+ : ended ? 本场已结束
+ : ready ?
+ : 未到签到时间,活动开始后即可签到}
+
+ );
+ })}
+
+ );
+}
diff --git a/miniprogram/src/pages/my-bookings/index.scss b/miniprogram/src/pages/my-bookings/index.scss
new file mode 100644
index 0000000..d3ebe78
--- /dev/null
+++ b/miniprogram/src/pages/my-bookings/index.scss
@@ -0,0 +1,17 @@
+.mk-head { padding: 18px 20px 6px; }
+.mk-title { font-size: 24px; font-weight: 700; color: #0d2f4e; display: block; }
+.mk-sub { font-size: 13px; color: #8a8a93; margin-top: 4px; display: block; }
+.mk-err { color: #e0544c; font-size: 13px; padding: 0 20px; }
+.mk-card { background: #fff; border-radius: 18px; padding: 16px 18px; margin: 10px 20px; box-shadow: 0 6px 18px -8px rgba(13,47,78,.12); }
+.mk-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
+.mk-name { font-size: 16px; font-weight: 600; color: #12233b; flex: 1; }
+.mk-phase { flex: 0 0 auto; font-size: 12px; padding: 2px 12px; border-radius: 999px; }
+.mk-phase.upcoming { color: #1f8ce0; background: #eaf6ff; }
+.mk-phase.pending { color: #e6a400; background: #fff6e2; }
+.mk-phase.rejected { color: #98a0ab; background: #f2f2f6; }
+.mk-phase.done { color: #98a0ab; background: #f2f2f6; }
+.mk-meta { display: block; font-size: 12px; color: #98a0ab; margin-top: 6px; }
+.mk-checked { display: block; margin-top: 10px; font-size: 13px; color: #00a06e; }
+.mk-checked.done { color: #98a0ab; }
+.mk-checkin { margin-top: 12px; }
+.mk-empty { text-align: center; padding: 40px 20px; }
diff --git a/miniprogram/src/pages/my-cert/index.config.js b/miniprogram/src/pages/my-cert/index.config.js
new file mode 100644
index 0000000..0ebef4f
--- /dev/null
+++ b/miniprogram/src/pages/my-cert/index.config.js
@@ -0,0 +1 @@
+export default { navigationBarTitleText: '认证中心' };
diff --git a/miniprogram/src/pages/my-cert/index.jsx b/miniprogram/src/pages/my-cert/index.jsx
new file mode 100644
index 0000000..0de16d3
--- /dev/null
+++ b/miniprogram/src/pages/my-cert/index.jsx
@@ -0,0 +1,59 @@
+import { View, Text } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { isAuthed } from '@/utils/auth';
+import { getOpCertMine } from '@/utils/profile';
+import './index.scss';
+
+const CERT = {
+ uncertified: ['未认证', 'muted'],
+ pending: ['待提交', 'pending'],
+ reviewing: ['审核中', 'pending'],
+ certified: ['已认证', 'ok'],
+ rejected: ['未通过', 'rejected'],
+};
+const badge = (s) => CERT[String(s || '').toLowerCase()] || [s || '未认证', 'muted'];
+
+export default function MyCert() {
+ const [r, setR] = useState({});
+ const [loaded, setLoaded] = useState(false);
+ const [err, setErr] = useState('');
+
+ const load = async () => {
+ try {
+ const d = await getOpCertMine();
+ setR(d || {});
+ } catch (e) { setErr((e && e.message) || '加载失败'); }
+ finally { setLoaded(true); }
+ };
+ useEffect(() => { load(); }, []);
+
+ const status = r.certification_status || '';
+ const cert = r.certification || {};
+ const [txt, cls] = badge(status);
+ const certified = String(status).toLowerCase() === 'certified';
+ const goApply = () => Taro.navigateTo({ url: '/pages/cert-apply/index' });
+
+ return (
+
+
+ 认证中心
+ 你的 OPC 创业伙伴认证进度。
+
+ {!isAuthed() && 请先登录(我的 → 登录绑定)。}
+ {err && {err}}
+ {!loaded ? 加载中… : (
+
+
+ OPC 创业伙伴认证
+ {txt}
+
+ {cert.cert_no ? 证书编号:{cert.cert_no} : null}
+ {cert.certified_at ? 认证时间:{cert.certified_at} : null}
+ 在平台完成指定指标后即可获得认证,享受政策 / 资源对接权益。
+ {!certified && 去认证 →}
+
+ )}
+
+ );
+}
diff --git a/miniprogram/src/pages/my-cert/index.scss b/miniprogram/src/pages/my-cert/index.scss
new file mode 100644
index 0000000..8e3483e
--- /dev/null
+++ b/miniprogram/src/pages/my-cert/index.scss
@@ -0,0 +1,13 @@
+.mk-head { padding: 18px 20px 6px; }
+.mk-title { font-size: 24px; font-weight: 700; color: #0d2f4e; display: block; }
+.mk-sub { font-size: 13px; color: #8a8a93; margin-top: 4px; display: block; }
+.mk-err { color: #e0544c; font-size: 13px; padding: 0 20px; }
+.mk-card { background: #fff; border-radius: 18px; padding: 18px; margin: 12px 20px; box-shadow: 0 6px 18px -8px rgba(13,47,78,.12); }
+.mk-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
+.mk-name { font-size: 16px; font-weight: 600; color: #12233b; flex: 1; }
+.mk-phase { flex: 0 0 auto; font-size: 12px; padding: 2px 12px; border-radius: 999px; }
+.mk-phase.ok { color: #00a06e; background: #e6f8f0; }
+.mk-phase.pending { color: #e6a400; background: #fff6e2; }
+.mk-phase.rejected { color: #98a0ab; background: #f2f2f6; }
+.mk-phase.muted { color: #98a0ab; background: #f2f2f6; }
+.mk-meta { display: block; font-size: 13px; color: #98a0ab; margin-top: 8px; }
diff --git a/miniprogram/src/pages/my-park/index.config.js b/miniprogram/src/pages/my-park/index.config.js
new file mode 100644
index 0000000..2ced61a
--- /dev/null
+++ b/miniprogram/src/pages/my-park/index.config.js
@@ -0,0 +1 @@
+export default { navigationBarTitleText: '我的园区' };
diff --git a/miniprogram/src/pages/my-park/index.jsx b/miniprogram/src/pages/my-park/index.jsx
new file mode 100644
index 0000000..6a7704e
--- /dev/null
+++ b/miniprogram/src/pages/my-park/index.jsx
@@ -0,0 +1,63 @@
+import { View, Text } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { isAuthed } from '@/utils/auth';
+import { getMyAdmission } from '@/utils/parks';
+import './index.scss';
+
+function statusText(s) {
+ const k = String(s || '').toLowerCase();
+ if (['approved', 'verified', 'admitted'].includes(k) || s === '已入驻' || s === '通过') return ['已入驻', 'ok'];
+ if (['pending', 'reviewing'].includes(k) || s === '审核中') return ['审核中', 'pending'];
+ if (['rejected'].includes(k) || s === '未通过') return ['未通过', 'rejected'];
+ return [s || '待审核', 'pending'];
+}
+
+export default function MyPark() {
+ const [items, setItems] = useState([]);
+ const [loaded, setLoaded] = useState(false);
+ const [err, setErr] = useState('');
+
+ const load = async () => {
+ try {
+ const r = await getMyAdmission();
+ setItems(Array.isArray(r) ? r : []);
+ } catch (e) { setErr((e && e.message) || '加载失败'); }
+ finally { setLoaded(true); }
+ };
+ useEffect(() => { load(); }, []);
+
+ const goApply = () => Taro.navigateTo({ url: '/pages/park-apply/index' });
+ const ad = items[0] || {};
+
+ return (
+
+
+ 我的园区
+ 查看你的园区入驻申请与所属园区。
+
+ {!isAuthed() && 请先登录(我的 → 登录绑定)。}
+ {err && {err}}
+ {!loaded ? 加载中…
+ : items.length === 0 ? (
+
+ 你还没有提交园区入驻申请。
+ 去申请入驻 →
+
+ ) : (() => {
+ const [txt, cls] = statusText(ad.status || ad.audit_status);
+ const name = ad.tenant_name || ad.park_name || ad.company_name || ad.company || '昆明市大学生创业园';
+ return (
+
+
+ {name}
+ {txt}
+
+ {ad.region ? 区域:{ad.region} : null}
+ {ad.created_at ? 申请时间:{ad.created_at} : null}
+
+ );
+ })()}
+
+ );
+}
diff --git a/miniprogram/src/pages/my-park/index.scss b/miniprogram/src/pages/my-park/index.scss
new file mode 100644
index 0000000..bc673b9
--- /dev/null
+++ b/miniprogram/src/pages/my-park/index.scss
@@ -0,0 +1,13 @@
+.mk-head { padding: 18px 20px 6px; }
+.mk-title { font-size: 24px; font-weight: 700; color: #0d2f4e; display: block; }
+.mk-sub { font-size: 13px; color: #8a8a93; margin-top: 4px; display: block; }
+.mk-err { color: #e0544c; font-size: 13px; padding: 0 20px; }
+.mk-card { background: #fff; border-radius: 18px; padding: 18px; margin: 12px 20px; box-shadow: 0 6px 18px -8px rgba(13,47,78,.12); }
+.mk-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
+.mk-name { font-size: 16px; font-weight: 600; color: #12233b; flex: 1; }
+.mk-phase { flex: 0 0 auto; font-size: 12px; padding: 2px 12px; border-radius: 999px; }
+.mk-phase.ok { color: #00a06e; background: #e6f8f0; }
+.mk-phase.pending { color: #e6a400; background: #fff6e2; }
+.mk-phase.rejected { color: #98a0ab; background: #f2f2f6; }
+.mk-meta { display: block; font-size: 13px; color: #98a0ab; margin-top: 6px; }
+.mk-empty { text-align: center; padding: 40px 20px; }
diff --git a/miniprogram/src/pages/my-tasks/index.config.js b/miniprogram/src/pages/my-tasks/index.config.js
new file mode 100644
index 0000000..e75630b
--- /dev/null
+++ b/miniprogram/src/pages/my-tasks/index.config.js
@@ -0,0 +1 @@
+export default { navigationBarTitleText: '我的任务' };
diff --git a/miniprogram/src/pages/my-tasks/index.jsx b/miniprogram/src/pages/my-tasks/index.jsx
new file mode 100644
index 0000000..d94ae70
--- /dev/null
+++ b/miniprogram/src/pages/my-tasks/index.jsx
@@ -0,0 +1,59 @@
+import { View, Text } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { isAuthed } from '@/utils/auth';
+import { getMyTasks } from '@/utils/tasks';
+import './index.scss';
+
+const MODE = { grab: '抢单', bid: '竞标', assign: '指派', recommend: '推荐' };
+const money = (a, b) => (a || b) ? (a && b && a !== b ? `¥${a}–${b}` : `¥${a || b}`) : '面议';
+const itemsOf = (r) => (r && r.items) || (r && r.list) || (Array.isArray(r) ? r : []);
+
+export default function MyTasks() {
+ const [items, setItems] = useState([]);
+ const [loaded, setLoaded] = useState(false);
+ const [err, setErr] = useState('');
+
+ const load = async () => {
+ try {
+ const r = await getMyTasks();
+ setItems(itemsOf(r));
+ } catch (e) {
+ setErr((e && e.message) || '加载失败');
+ } finally { setLoaded(true); }
+ };
+ useEffect(() => { load(); }, []);
+
+ const open = (id) => () => Taro.navigateTo({ url: `/pages/task-detail/index?id=${id}` });
+ const goSquare = () => Taro.navigateTo({ url: '/pages/tasks/index' });
+
+ return (
+
+
+ 我的任务
+ 你承接 / 参与的任务,点击查看详情。
+
+ {!isAuthed() && 请先登录(我的 → 登录绑定)。}
+ {err && {err}}
+ {!loaded ? 加载中…
+ : items.length === 0 ? (
+
+ 还没有承接的任务。
+ 去任务广场 →
+
+ ) : items.map((t) => (
+
+
+ {t.category || '综合'}
+ {MODE[t.mode] || '抢单'}
+
+ {t.title}
+
+ {money(t.budget_min, t.budget_max)}
+ {t.deadline ? 截止 {t.deadline} : null}
+
+
+ ))}
+
+ );
+}
diff --git a/miniprogram/src/pages/my-tasks/index.scss b/miniprogram/src/pages/my-tasks/index.scss
new file mode 100644
index 0000000..340d8d2
--- /dev/null
+++ b/miniprogram/src/pages/my-tasks/index.scss
@@ -0,0 +1,13 @@
+.mk-head { padding: 18px 20px 6px; }
+.mk-title { font-size: 24px; font-weight: 700; color: #0d2f4e; display: block; }
+.mk-sub { font-size: 13px; color: #8a8a93; margin-top: 4px; display: block; }
+.mk-err { color: #e0544c; font-size: 13px; padding: 0 20px; }
+.mk-card { background: #fff; border-radius: 18px; padding: 16px 18px; margin: 10px 20px; box-shadow: 0 6px 18px -8px rgba(13,47,78,.12); }
+.mk-row { display: flex; gap: 8px; }
+.mk-tag { background: #eaf6ff; color: #1f8ce0; border-radius: 999px; padding: 2px 12px; font-size: 12px; }
+.mk-mode { background: #f2f2f6; color: #6a6a73; border-radius: 999px; padding: 2px 12px; font-size: 12px; }
+.mk-name { display: block; font-size: 16px; font-weight: 600; color: #12233b; margin: 10px 0 6px; }
+.mk-meta { display: flex; gap: 12px; }
+.mk-price { color: #06acf1; font-weight: 600; font-size: 14px; }
+.mk-dot { color: #98a0ab; font-size: 13px; }
+.mk-empty { text-align: center; padding: 40px 20px; }
diff --git a/miniprogram/src/utils/auth.js b/miniprogram/src/utils/auth.js
index 8ddb474..515b976 100644
--- a/miniprogram/src/utils/auth.js
+++ b/miniprogram/src/utils/auth.js
@@ -13,6 +13,10 @@ function pickUser(d) {
avatar: d.avatar || '',
phone: d.phone || '',
phoneBound: !!d.phoneBound,
+ wxBound: !!d.wxBound,
+ wxMiniBound: !!d.wxMiniBound,
+ account_type_label: d.account_type_label || d.role || '',
+ role: d.role || '',
status: d.status || '',
topics: Array.isArray(d.topics) ? d.topics : [],
source: d.source || ''