From d93e1d42265765161f4a91216b9239f6f9fa2008 Mon Sep 17 00:00:00 2001 From: Pine Date: Tue, 25 Aug 2026 18:43:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(tasks):=20=E5=B0=8F=E7=A8=8B=E5=BA=8F?= =?UTF-8?q?=E6=89=AB=E7=A0=81=E6=8E=A5=E5=8D=95=20=E2=80=94=20=E4=B8=AA?= =?UTF-8?q?=E4=BA=BA=E4=B8=AD=E5=BF=83=E5=85=A5=E5=8F=A3=20+=20scan=20?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - miniprogram/src/utils/tasks.js:claimByCode/getMyTasks/parseTaskCode。 - 新增 pages/scan/index(jsx/scss/config):Taro.scanCode→解析 task_code→领单→展示我的接单。 - app.config.js 注册 pages/scan/index。 - mine/index.jsx 加「扫码接单」入口卡(navigateTo scan);mine/index.scss 加样式。 - 接单走 OPC/创业者身份(后端按账号 find-or-create);属小程序增量,web C 端暂不同步。 Co-Authored-By: Claude --- miniprogram/src/app.config.js | 3 +- miniprogram/src/pages/mine/index.jsx | 12 +- miniprogram/src/pages/mine/index.scss | 21 ++++ miniprogram/src/pages/plan/index.jsx | 7 +- miniprogram/src/pages/policy/index.jsx | 20 ++-- miniprogram/src/pages/scan/index.config.js | 1 + miniprogram/src/pages/scan/index.jsx | 121 +++++++++++++++++++++ miniprogram/src/pages/scan/index.scss | 28 +++++ miniprogram/src/pages/test/index.jsx | 24 ++-- miniprogram/src/utils/tasks.js | 28 +++++ website/src/components/organisms/index.jsx | 2 - website/src/data/site.js | 7 +- 12 files changed, 246 insertions(+), 28 deletions(-) create mode 100644 miniprogram/src/pages/scan/index.config.js create mode 100644 miniprogram/src/pages/scan/index.jsx create mode 100644 miniprogram/src/pages/scan/index.scss create mode 100644 miniprogram/src/utils/tasks.js 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 下,需登录)