diff --git a/miniprogram/src/app.config.js b/miniprogram/src/app.config.js
index 9d34ed3..fd49e56 100644
--- a/miniprogram/src/app.config.js
+++ b/miniprogram/src/app.config.js
@@ -7,7 +7,8 @@ export default {
'pages/plan/index',
'pages/survey/index',
'pages/mine/index',
- 'pages/event-detail/index'
+ 'pages/event-detail/index',
+ 'pages/scan/index'
],
window: {
backgroundTextStyle: 'light',
diff --git a/miniprogram/src/pages/mine/index.jsx b/miniprogram/src/pages/mine/index.jsx
index 247df87..ad62e15 100644
--- a/miniprogram/src/pages/mine/index.jsx
+++ b/miniprogram/src/pages/mine/index.jsx
@@ -2,7 +2,7 @@ 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 } from '@/utils/auth';
+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 Skeleton from '@/components/skeleton';
import CountdownBox from '@/components/CountdownBox';
@@ -385,6 +385,16 @@ export default function Mine() {
)}
+ {/* 扫码接单入口:扫描大屏任务二维码领取任务 */}
+ Taro.navigateTo({ url: '/pages/scan/index' })}>
+ QR
+
+ 扫码接单
+ 扫描大屏任务码领取任务,做任务赚预算
+
+ ›
+
+
{/* 我的报名 */}
BOOK
diff --git a/miniprogram/src/pages/mine/index.scss b/miniprogram/src/pages/mine/index.scss
index 489caf1..4d57742 100644
--- a/miniprogram/src/pages/mine/index.scss
+++ b/miniprogram/src/pages/mine/index.scss
@@ -45,6 +45,27 @@
margin: 40rpx 0 16rpx;
}
+/* 扫码接单入口卡 */
+.scan-entry-card {
+ display: flex;
+ align-items: center;
+ gap: 20rpx;
+ padding: 22rpx 24rpx;
+ margin-bottom: 8rpx;
+ cursor: pointer;
+}
+.scan-entry-icon {
+ width: 68rpx; height: 68rpx; border-radius: 16rpx;
+ background: linear-gradient(135deg, #4747ff, #8b2bf5);
+ color: #fff; font-size: 26rpx; font-weight: 700;
+ display: flex; align-items: center; justify-content: center;
+ font-family: ui-monospace, monospace;
+}
+.scan-entry-body { flex: 1; display: flex; flex-direction: column; gap: 4rpx; }
+.scan-entry-title { font-size: 30rpx; color: #fff; font-weight: 600; }
+.scan-entry-sub { font-size: 24rpx; color: #8a8a93; }
+.scan-entry-arrow { font-size: 36rpx; color: #8a8a93; }
+
/* 报名状态筛选(对齐活动页的 全部/公益/... 形式) */
.phase-filters {
display: flex;
diff --git a/miniprogram/src/pages/plan/index.jsx b/miniprogram/src/pages/plan/index.jsx
index 5dee373..ce287a5 100644
--- a/miniprogram/src/pages/plan/index.jsx
+++ b/miniprogram/src/pages/plan/index.jsx
@@ -1,4 +1,5 @@
import { View, Text, Button, Picker } from '@tarojs/components';
+import Taro from '@tarojs/taro';
import { useState, useEffect } from 'react';
import { getUser } from '@/utils/auth';
import { request, cachedRequest } from '@/utils/api';
@@ -30,7 +31,11 @@ export default function Plan() {
const p = (res && res.plan) || [];
setPlan(p);
const u = getUser();
- request('POST', '/api/plan-logs', { username: u ? u.username : '', region, status, needPark, hasStaff, stepsCount: p.length }).catch(() => {});
+ try {
+ await request('POST', '/api/plan-logs', { username: u ? u.username : '', region, status, needPark, hasStaff, stepsCount: p.length });
+ } catch (e) {
+ Taro.showToast({ title: '流程记录上报失败,请稍后重试', icon: 'none' });
+ }
};
const reset = () => setPlan(null);
diff --git a/miniprogram/src/pages/policy/index.jsx b/miniprogram/src/pages/policy/index.jsx
index 502968f..0ec5ff5 100644
--- a/miniprogram/src/pages/policy/index.jsx
+++ b/miniprogram/src/pages/policy/index.jsx
@@ -52,14 +52,18 @@ export default function Policy() {
const r = res.result;
setResult(r);
const u = getUser();
- request('POST', '/api/policy-logs', {
- username: u ? u.username : '',
- answers: a,
- policiesCount: r.policies.length,
- subsidiesCount: r.subsidies.length,
- loansCount: r.loans.length,
- summary: r.summary
- }).catch(() => {});
+ try {
+ await request('POST', '/api/policy-logs', {
+ username: u ? u.username : '',
+ answers: a,
+ policiesCount: r.policies.length,
+ subsidiesCount: r.subsidies.length,
+ loansCount: r.loans.length,
+ summary: r.summary
+ });
+ } catch (e) {
+ Taro.showToast({ title: '测评结果上报失败,请稍后重试', icon: 'none' });
+ }
};
const reset = () => {
diff --git a/miniprogram/src/pages/scan/index.config.js b/miniprogram/src/pages/scan/index.config.js
new file mode 100644
index 0000000..37146cf
--- /dev/null
+++ b/miniprogram/src/pages/scan/index.config.js
@@ -0,0 +1 @@
+export default { navigationBarTitleText: '扫码接单' };
diff --git a/miniprogram/src/pages/scan/index.jsx b/miniprogram/src/pages/scan/index.jsx
new file mode 100644
index 0000000..a181a1d
--- /dev/null
+++ b/miniprogram/src/pages/scan/index.jsx
@@ -0,0 +1,121 @@
+import { View, Text, Button } from '@tarojs/components';
+import Taro from '@tarojs/taro';
+import { useState, useEffect } from 'react';
+import { isAuthed } from '@/utils/auth';
+import { claimByCode, getMyTasks, parseTaskCode } from '@/utils/tasks';
+import Skeleton from '@/components/skeleton';
+import './index.scss';
+
+/* 扫码接单:扫描大屏任务卡二维码 → 解析 task_code → 领单(OPC 身份)→ 展示我的接单 */
+
+export default function Scan() {
+ const [busy, setBusy] = useState(false);
+ const [ordering, setOrdering] = useState(false);
+ const [code, setCode] = useState('');
+ const [err, setErr] = useState('');
+ const [claimed, setClaimed] = useState(null);
+ const [mine, setMine] = useState([]);
+ const [loaded, setLoaded] = useState(false);
+
+ const loadMine = async () => {
+ try {
+ const r = await getMyTasks();
+ setMine((r && r.items) || []);
+ } catch { /* ignore */ }
+ finally { setLoaded(true); }
+ };
+ useEffect(() => { if (isAuthed()) loadMine(); else setLoaded(true); }, []);
+
+ const doClaim = async (taskCode) => {
+ setOrdering(true); setErr('');
+ try {
+ const r = await claimByCode(taskCode);
+ setClaimed((r && r.task) || null);
+ Taro.showToast({ title: '接单成功', icon: 'success' });
+ loadMine();
+ } catch (e) {
+ setErr((e && e.message) || '接单失败,请确认任务可扫码接单');
+ }
+ finally { setOrdering(false); }
+ };
+
+ const onScan = async () => {
+ setErr(''); setClaimed(null);
+ if (!isAuthed()) {
+ setErr('请先登录后再扫码接单');
+ Taro.navigateTo({ url: '/pages/mine/index' });
+ return;
+ }
+ try {
+ setBusy(true);
+ const res = await Taro.scanCode({ onlyFromCamera: false });
+ const raw = res && res.result;
+ const taskCode = parseTaskCode(raw);
+ if (!taskCode) { setErr('未识别到有效任务码,请扫描大屏上的任务二维码'); return; }
+ setCode(taskCode);
+ await doClaim(taskCode);
+ } catch (e) {
+ if (e && e.errMsg && e.errMsg.includes('cancel')) { /* 用户取消扫码 */ }
+ else setErr((e && e.message) || '扫码失败,请重试');
+ }
+ finally { setBusy(false); }
+ };
+
+ return (
+
+
+ 扫码接单
+ 扫描园区大屏上的任务二维码,即可领取任务
+
+
+
+
+ {code && !err && (
+
+ 已识别任务码
+ {code}
+
+ )}
+ {err && {err}}
+ {claimed && (
+
+ 接单成功
+ {claimed.title}
+ 任务码 {claimed.task_code || claimed.id} · 请在「我的」中跟进进度
+
+ )}
+
+
+ ORDERS
+ 我的接单({mine.length})
+
+ {!loaded ? (
+
+ ) : mine.length === 0 ? (
+ 还没有接单任务,去扫码领取吧。
+ ) : (
+
+ {mine.map((t) => (
+
+
+ {t.title}
+ 已接单
+
+ 任务码 {t.task_code || t.id}
+ 状态 {statusLabel(t.status)}
+
+ ))}
+
+ )}
+
+ );
+}
+
+function statusLabel(s) {
+ switch (s) {
+ case 'claimed': return '已接单 / 进行中';
+ case 'doing': return '做单中';
+ case 'completed': return '已完成';
+ default: return s || '—';
+ }
+}
diff --git a/miniprogram/src/pages/scan/index.scss b/miniprogram/src/pages/scan/index.scss
new file mode 100644
index 0000000..a7f0aeb
--- /dev/null
+++ b/miniprogram/src/pages/scan/index.scss
@@ -0,0 +1,28 @@
+/* 扫码接单 · 小程序样式(暗色 + 蓝紫科技,与全站一致) */
+.scan { padding: 24rpx 32rpx 120rpx; position: relative; z-index: 2; }
+
+.scan-head { display: flex; flex-direction: column; align-items: flex-start; gap: 10rpx; margin: 32rpx 0 24rpx; }
+.scan-title { font-size: 40rpx; font-weight: 700; color: #fff; }
+.scan-sub { font-size: 26rpx; color: #8a8a93; }
+
+.scan-btn { margin: 8rpx 0 24rpx; }
+
+.scan-result { margin-bottom: 20rpx; }
+.scan-code { font-size: 34rpx; font-weight: 700; color: #ffd28a; font-family: ui-monospace, monospace; }
+
+.scan-ok { margin-bottom: 24rpx; border-color: rgba(23, 178, 106, 0.4); }
+.scan-ok-title { font-size: 30rpx; font-weight: 700; color: #17b26a; }
+.scan-ok-task { font-size: 28rpx; color: #fff; margin: 8rpx 0; }
+
+/* 复用 mine 的区块/卡样式 */
+.mine-section-head { display: flex; align-items: baseline; gap: 14rpx; margin: 32rpx 0 20rpx; }
+.mine-no { font-size: 22rpx; letter-spacing: 2rpx; color: #6b6b75; font-family: ui-monospace, monospace; }
+.mine-subtitle { font-size: 28rpx; color: #fff; font-weight: 600; }
+
+.scan-list { display: flex; flex-direction: column; gap: 16rpx; }
+.bk-card { border: 1rpx solid #26262f; border-radius: 16rpx; padding: 20rpx 22rpx; background: #0c0d14; }
+.bk-head { display: flex; justify-content: space-between; align-items: center; }
+.bk-want { font-size: 28rpx; color: #fff; font-weight: 600; flex: 1; }
+.bk-phase { font-size: 22rpx; padding: 4rpx 14rpx; border-radius: 999rpx; }
+.bk-phase.claimed { color: #ffd28a; background: rgba(255, 210, 138, 0.14); }
+.bk-time { display: block; font-size: 24rpx; color: #8a8a93; margin-top: 8rpx; }
diff --git a/miniprogram/src/pages/test/index.jsx b/miniprogram/src/pages/test/index.jsx
index 26a1fef..8ad2ed1 100644
--- a/miniprogram/src/pages/test/index.jsx
+++ b/miniprogram/src/pages/test/index.jsx
@@ -68,17 +68,21 @@ export default function Test() {
const r = d.result;
setResult(r);
setStage('result');
- // 上报测评记录(登录用户关联 username;失败不影响展示)
+ // 上报测评记录(登录用户关联 username;失败不阻断报告展示,但不再静默吞错)
const u = getUser();
- request('POST', '/api/tests', {
- typeCode: r.typeCode,
- persona: (r.persona && r.persona.name) || '',
- adaptIndex: r.adaptIndex,
- adaptLevel: (r.adaptLevel && r.adaptLevel.level) || '',
- tracks: (r.tracks || []).map((t) => t.code),
- version,
- username: u ? u.username : ''
- }).catch(() => {});
+ try {
+ await request('POST', '/api/tests', {
+ typeCode: r.typeCode,
+ persona: (r.persona && r.persona.name) || '',
+ adaptIndex: r.adaptIndex,
+ adaptLevel: (r.adaptLevel && r.adaptLevel.level) || '',
+ tracks: (r.tracks || []).map((t) => t.code),
+ version,
+ username: u ? u.username : ''
+ });
+ } catch (e) {
+ Taro.showToast({ title: '测评记录上报失败,请稍后重试', icon: 'none' });
+ }
} catch (e) {
setErr((e && e.message) || '计算失败,请稍后重试');
} finally {
diff --git a/miniprogram/src/utils/tasks.js b/miniprogram/src/utils/tasks.js
new file mode 100644
index 0000000..f949575
--- /dev/null
+++ b/miniprogram/src/utils/tasks.js
@@ -0,0 +1,28 @@
+import { request } from './api';
+
+/* 扫码接单(任务中心):大屏任务卡二维码 → 扫码 → 按 task_code 领取。
+ 接单走 OPC/创业者身份:由后端按本账号 find-or-create 平台 OPC 身份后领单。
+ 属小程序「我的」页增量,web C 端暂不做(见 小程序同步约束.md 第5节)。 */
+
+/** 扫码领单:task_code → claimed */
+export async function claimByCode(code) {
+ return request('POST', '/api/tasks/claim-by-code', { task_code: code }, true);
+}
+
+/** 我的接单(已领取任务) */
+export async function getMyTasks() {
+ return request('GET', '/api/tasks/my', {}, true);
+}
+
+/** 解析扫码文本 → task_code:支持纯短码 / URL(?c=、?task_code=、末段) */
+export function parseTaskCode(raw) {
+ const s = (raw || '').trim();
+ if (!s) return '';
+ if (s.includes('://') || s.includes('/') || s.includes('?')) {
+ const m = s.match(/[?&](?:c|task_code|taskCode)=([^&]+)/);
+ if (m) return decodeURIComponent(m[1]);
+ const seg = s.split('?')[0].split('#')[0].split('/').filter(Boolean).pop();
+ return (seg && /^TK-/i.test(seg)) ? seg : '';
+ }
+ return s;
+}
diff --git a/website/src/components/organisms/index.jsx b/website/src/components/organisms/index.jsx
index a121110..d837011 100644
--- a/website/src/components/organisms/index.jsx
+++ b/website/src/components/organisms/index.jsx
@@ -32,7 +32,6 @@ export function Header({ active }) {
) : (
<>
{user && {user.name}}
- 活动
个人中心
>
)}
@@ -71,7 +70,6 @@ export function Header({ active }) {
{user && (
{ logout(); setOpen(false); window.location.hash = '#/'; }}>退出登录({user.name})
)}
- setOpen(false)}>活动
>
)}
diff --git a/website/src/data/site.js b/website/src/data/site.js
index d91bfb2..6b1e663 100644
--- a/website/src/data/site.js
+++ b/website/src/data/site.js
@@ -1,12 +1,9 @@
// 站点级数据:导航 / 信任行 / 首页文案 / 统计
-// 公开导航(对外):首页 / 活动 / OPC测评 / 政策测评 / 启动流程 / 调研;内部工具在 /pine 下、不进公开导航
+// 公开导航(对外):精简为首页 / 活动 / 测评三项;政策/流程/调研/内部工具不再进公开导航(其余仍是合法路由)
export const NAV = [
{ path: '', label: '首页' },
{ path: 'events', label: '活动' },
- { path: 'opc-test', label: 'O测评' },
- { path: 'policy-test', label: '政策' },
- { path: 'start-plan', label: '流程' },
- { path: 'survey', label: '调研' }
+ { path: 'opc-test', label: '测评' }
];
// 内部工具导航(/pine 下,需登录)