From 384b9576178a8fdcd385550db4a312da5f7f43c6 Mon Sep 17 00:00:00 2001 From: Pine Date: Thu, 27 Aug 2026 19:18:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=B0=8F=E7=A8=8B=E5=BA=8F):=20=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E5=B9=BF=E5=9C=BA=20+=20=E4=BB=BB=E5=8A=A1=E8=AF=A6?= =?UTF-8?q?=E6=83=85=20+=20=E6=8A=A2=E5=8D=95/=E6=8A=A5=E5=90=8D(=E4=B8=8E?= =?UTF-8?q?=20web=20=E5=90=8C=E6=AD=A5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - utils/tasks.js 增 listSquare/getTask/grabById(/opc/tasks 平台公开端点,带 token 取 can_accept) - pages/tasks 任务广场(公开可见,卡片含可接/条件不符徽标+原因,点击进详情,抢单) - pages/task-detail 任务详情(说明/预算/模式/截止/交付/发包方 + 按 can_accept 放行抢单/报名) - app.config 注册两页;「我的」页加「任务广场」入口(在扫码接单前) - Soft UI 浅色柔彩样式;build:weapp 通过 --- miniprogram/src/app.config.js | 2 + miniprogram/src/pages/mine/index.jsx | 10 +++ .../src/pages/task-detail/index.config.js | 6 ++ miniprogram/src/pages/task-detail/index.jsx | 84 +++++++++++++++++++ miniprogram/src/pages/task-detail/index.scss | 25 ++++++ miniprogram/src/pages/tasks/index.config.js | 6 ++ miniprogram/src/pages/tasks/index.jsx | 80 ++++++++++++++++++ miniprogram/src/pages/tasks/index.scss | 26 ++++++ miniprogram/src/utils/tasks.js | 15 ++++ 9 files changed, 254 insertions(+) create mode 100644 miniprogram/src/pages/task-detail/index.config.js create mode 100644 miniprogram/src/pages/task-detail/index.jsx create mode 100644 miniprogram/src/pages/task-detail/index.scss create mode 100644 miniprogram/src/pages/tasks/index.config.js create mode 100644 miniprogram/src/pages/tasks/index.jsx create mode 100644 miniprogram/src/pages/tasks/index.scss diff --git a/miniprogram/src/app.config.js b/miniprogram/src/app.config.js index 9723aba..02fb094 100644 --- a/miniprogram/src/app.config.js +++ b/miniprogram/src/app.config.js @@ -14,6 +14,8 @@ export default { 'pages/park-transfer/index', 'pages/content/index', 'pages/content-detail/index', + 'pages/tasks/index', + 'pages/task-detail/index', 'pages/scan-login/index' ], window: { diff --git a/miniprogram/src/pages/mine/index.jsx b/miniprogram/src/pages/mine/index.jsx index 3196a81..8c16900 100644 --- a/miniprogram/src/pages/mine/index.jsx +++ b/miniprogram/src/pages/mine/index.jsx @@ -391,6 +391,16 @@ export default function Mine() { )} + {/* 任务广场入口:浏览待接单任务 */} + Taro.navigateTo({ url: '/pages/tasks/index' })}> + + + 任务广场 + 浏览平台待接单任务,点击抢单 / 报名 + + + + {/* 扫码接单入口:扫描大屏任务二维码领取任务 */} Taro.navigateTo({ url: '/pages/scan/index' })}> QR diff --git a/miniprogram/src/pages/task-detail/index.config.js b/miniprogram/src/pages/task-detail/index.config.js new file mode 100644 index 0000000..7f44dbc --- /dev/null +++ b/miniprogram/src/pages/task-detail/index.config.js @@ -0,0 +1,6 @@ +export default definePageConfig({ + navigationBarTitleText: '任务详情', + navigationBarBackgroundColor: '#f7f9ff', + navigationBarTextStyle: 'black', + backgroundColor: '#f7f9ff' +}); diff --git a/miniprogram/src/pages/task-detail/index.jsx b/miniprogram/src/pages/task-detail/index.jsx new file mode 100644 index 0000000..016ff96 --- /dev/null +++ b/miniprogram/src/pages/task-detail/index.jsx @@ -0,0 +1,84 @@ +import { View, Text, Button } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import { useState, useEffect } from 'react'; +import { getCurrentInstance } from '@tarojs/taro'; +import { isAuthed } from '@/utils/auth'; +import { getTask, grabById } 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 getId = () => (getCurrentInstance().router && getCurrentInstance().router.params) || {}; + +export default function TaskDetail() { + const [task, setTask] = useState(null); + const [loaded, setLoaded] = useState(false); + const [err, setErr] = useState(''); + const [busy, setBusy] = useState(false); + + useEffect(() => { + const id = getId().id; + if (!id) { setLoaded(true); return; } + getTask(id, isAuthed()).then((t) => { setTask(t || null); }).catch(() => setTask(null)).finally(() => setLoaded(true)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const grab = async () => { + if (!isAuthed()) { setErr('抢单需先登录(我的 → 登录绑定)。'); return; } + setBusy(true); setErr(''); + try { + await grabById(task.id); + Taro.showToast({ title: '接单成功', icon: 'success' }); + setErr('接单成功,可在「我的」查看。'); + } catch (e) { setErr((e && e.message) || '抢单失败'); } + finally { setBusy(false); } + }; + + if (!loaded) return 加载中…; + if (!task) return 任务不存在或已下线。; + + const can = task.can_accept; + const reasons = Array.isArray(task.accept_reasons) ? task.accept_reasons : []; + + return ( + + + + {task.category || '综合'} + {MODE[task.mode] || '抢单'} + {task.exclusive && 独占} + {can === true && ✓ 你可抢} + {can === false && 条件不符} + + {task.title} + {task.task_code ? 短码 {task.task_code} : null} + + + + 任务说明 + {task.description} + {task.tags ? {(task.tags || '').split(',').filter(Boolean).map((x) => {x})} : null} + + + + 预算{money(task.budget_min, task.budget_max)} + 分类{task.category}{task.sub_category ? ` · ${task.sub_category}` : ''} + 模式{MODE[task.mode] || task.mode} + {task.deadline ? 截止{task.deadline} : null} + {task.delivery_days ? 交付{task.delivery_days} 天 : null} + {task.headcount ? 人数{task.headcount} 人 : null} + {task.publisher_name ? 发包方{task.publisher_name} : null} + + + {can === false && {reasons.join(';')}} + {err ? {err} : null} + + {can === false ? ( + + ) : ( + + )} + {!isAuthed() && 抢单/报名需登录,去「我的」绑定手机号。} + + ); +} diff --git a/miniprogram/src/pages/task-detail/index.scss b/miniprogram/src/pages/task-detail/index.scss new file mode 100644 index 0000000..d727bcb --- /dev/null +++ b/miniprogram/src/pages/task-detail/index.scss @@ -0,0 +1,25 @@ +/* 任务详情 · Soft UI(浅色柔彩) */ +.tkd { position: relative; z-index: 2; padding: 20rpx 24rpx 60rpx; min-height: 100vh; + background: linear-gradient(165deg, #edf2ff 0%, #f6eefc 34%, #ecf7f6 68%, #eaf4ff 100%); } +.tkd-empty { display: block; padding: 120rpx 0; text-align: center; color: #9aa4c6; font-size: 26rpx; } +.tkd-hero { padding: 8rpx 4rpx 20rpx; } +.tkd-row { display: flex; align-items: center; gap: 8rpx; flex-wrap: wrap; } +.tk-cat { font-size: 20rpx; font-weight: 700; color: #6284ff; background: rgba(98,132,255,.12); border-radius: 999rpx; padding: 2rpx 14rpx; } +.tk-mode { font-size: 20rpx; font-weight: 600; color: #23315c; background: #eef1ff; border-radius: 999rpx; padding: 2rpx 14rpx; } +.tk-excl { font-size: 20rpx; font-weight: 600; color: #c98a1f; background: rgba(245,196,81,.16); border-radius: 999rpx; padding: 2rpx 14rpx; } +.tk-can { font-size: 20rpx; font-weight: 700; color: #2aa06a; background: rgba(63,182,139,.16); border-radius: 999rpx; padding: 2rpx 14rpx; } +.tk-nocan { font-size: 20rpx; font-weight: 600; color: #9aa4c6; background: #eef1ff; border-radius: 999rpx; padding: 2rpx 14rpx; } +.tkd-title { display: block; margin-top: 16rpx; font-size: 40rpx; font-weight: 800; color: #2b3a66; line-height: 1.25; } +.tkd-code { display: inline-block; margin-top: 10rpx; font-size: 22rpx; color: #6284ff; background: rgba(98,132,255,.1); border-radius: 999rpx; padding: 2rpx 14rpx; } +.tkd-card { background: rgba(255,255,255,.82); border: 1rpx solid rgba(99,132,255,.12); border-radius: 22rpx; padding: 22rpx; margin-bottom: 16rpx; box-shadow: 0 8rpx 24rpx rgba(59,79,125,.08); } +.tkd-sec { display: block; font-size: 28rpx; font-weight: 700; color: #2b3a66; margin-bottom: 12rpx; } +.tkd-desc { display: block; font-size: 27rpx; color: #4a5580; line-height: 1.75; } +.tkd-tags { display: flex; flex-wrap: wrap; gap: 8rpx; margin-top: 14rpx; } +.tkd-tag { font-size: 22rpx; color: #7a85ad; background: #eef1ff; border-radius: 999rpx; padding: 2rpx 14rpx; } +.tkd-fact { display: flex; align-items: baseline; justify-content: space-between; padding: 8rpx 0; } +.tkd-k { font-size: 24rpx; color: #8a94c0; } +.tkd-v { font-size: 27rpx; font-weight: 600; color: #2b3a66; } +.tkd-v.money { color: #6284ff; } +.tkd-why { display: block; margin: 6rpx 4rpx 14rpx; font-size: 24rpx; color: #8a94c0; } +.tkd-err { display: block; margin: 12rpx 0; padding: 14rpx 18rpx; border-radius: 16rpx; background: rgba(240,117,117,.12); border: 1rpx solid rgba(240,117,117,.4); color: #e05252; font-size: 24rpx; } +.tkd-note { display: block; margin-top: 16rpx; text-align: center; font-size: 23rpx; color: #8a94c0; } diff --git a/miniprogram/src/pages/tasks/index.config.js b/miniprogram/src/pages/tasks/index.config.js new file mode 100644 index 0000000..b82993c --- /dev/null +++ b/miniprogram/src/pages/tasks/index.config.js @@ -0,0 +1,6 @@ +export default definePageConfig({ + navigationBarTitleText: '任务广场', + navigationBarBackgroundColor: '#f7f9ff', + navigationBarTextStyle: 'black', + backgroundColor: '#f7f9ff' +}); diff --git a/miniprogram/src/pages/tasks/index.jsx b/miniprogram/src/pages/tasks/index.jsx new file mode 100644 index 0000000..afbcb5d --- /dev/null +++ b/miniprogram/src/pages/tasks/index.jsx @@ -0,0 +1,80 @@ +import { View, Text, Button } from '@tarojs/components'; +import Taro from '@tarojs/taro'; +import { useState, useEffect } from 'react'; +import { isAuthed } from '@/utils/auth'; +import { listSquare, grabById } 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}`) : '面议'; + +export default function Tasks() { + const [items, setItems] = useState([]); + const [loaded, setLoaded] = useState(false); + const [err, setErr] = useState(''); + const [busyId, setBusyId] = useState(''); + + const load = async () => { + setErr(''); + try { + const r = await listSquare(isAuthed()); + setItems((r && r.items) || []); + } catch (e) { + setErr((e && e.message) || '加载失败'); + } finally { setLoaded(true); } + }; + useEffect(() => { load(); }, []); + + const open = (id) => () => Taro.navigateTo({ url: `/pages/task-detail/index?id=${id}` }); + + const grab = async (t) => { + if (!isAuthed()) { setErr('抢单需先登录(我的 → 登录绑定)。'); return; } + setBusyId(t.id); setErr(''); + try { + await grabById(t.id); + Taro.showToast({ title: '接单成功', icon: 'success' }); + load(); + } catch (e) { setErr((e && e.message) || '抢单失败'); } + finally { setBusyId(''); } + }; + + return ( + + + 任务广场 + 浏览平台发布的待接单任务,点击查看详情;登录后可抢单。 + + + {err && {err}} + {!loaded ? 加载中… + : items.length === 0 ? 暂无待接单任务。 + : items.map((t) => { + const can = t.can_accept; + const reasons = Array.isArray(t.accept_reasons) ? t.accept_reasons : []; + return ( + + + {t.category || '综合'} + {MODE[t.mode] || '抢单'} + {t.exclusive && 独占} + {can === true && ✓可抢} + {can === false && 条件不符} + + {t.title} + {t.description} + + {money(t.budget_min, t.budget_max)} + {t.deadline ? {t.deadline} : null} + {t.delivery_days ? {t.delivery_days} 天交付 : null} + + {can === false && {reasons.join(';')}} + + + ); + })} + + ); +} diff --git a/miniprogram/src/pages/tasks/index.scss b/miniprogram/src/pages/tasks/index.scss new file mode 100644 index 0000000..d0700fc --- /dev/null +++ b/miniprogram/src/pages/tasks/index.scss @@ -0,0 +1,26 @@ +/* 任务广场 · Soft UI(浅色柔彩) */ +.tk { position: relative; z-index: 2; padding: 20rpx 24rpx 60rpx; min-height: 100vh; + background: linear-gradient(165deg, #edf2ff 0%, #f6eefc 34%, #ecf7f6 68%, #eaf4ff 100%); } +.tk::before { content: ''; position: fixed; inset: 0; z-index: -1; + background: radial-gradient(50% 40% at 12% 4%, rgba(229,167,224,.26), transparent 60%), + radial-gradient(46% 40% at 92% 6%, rgba(158,225,236,.30), transparent 60%); } +.tk-head { padding: 8rpx 4rpx 20rpx; } +.tk-title { display: block; font-size: 44rpx; font-weight: 800; color: #23315c; letter-spacing: -0.02em; } +.tk-sub { display: block; margin-top: 10rpx; font-size: 25rpx; color: #8a94c0; line-height: 1.6; } +.tk-err { display: block; margin: 12rpx 0; padding: 14rpx 18rpx; border-radius: 16rpx; background: rgba(240,117,117,.12); border: 1rpx solid rgba(240,117,117,.4); color: #e05252; font-size: 24rpx; } +.tk-empty { display: block; padding: 80rpx 0; text-align: center; font-size: 26rpx; color: #9aa4c6; } +.tk-card { background: rgba(255,255,255,.82); border: 1rpx solid rgba(99,132,255,.12); border-radius: 22rpx; + padding: 20rpx 22rpx; margin-bottom: 16rpx; box-shadow: 0 8rpx 24rpx rgba(59,79,125,.08); } +.tk-row { display: flex; align-items: center; gap: 8rpx; flex-wrap: wrap; } +.tk-cat { font-size: 20rpx; font-weight: 700; color: #6284ff; background: rgba(98,132,255,.12); border-radius: 999rpx; padding: 2rpx 14rpx; } +.tk-mode { font-size: 20rpx; font-weight: 600; color: #23315c; background: #eef1ff; border-radius: 999rpx; padding: 2rpx 14rpx; } +.tk-excl { font-size: 20rpx; font-weight: 600; color: #c98a1f; background: rgba(245,196,81,.16); border-radius: 999rpx; padding: 2rpx 14rpx; } +.tk-can { font-size: 20rpx; font-weight: 700; color: #2aa06a; background: rgba(63,182,139,.16); border-radius: 999rpx; padding: 2rpx 14rpx; } +.tk-nocan { font-size: 20rpx; font-weight: 600; color: #9aa4c6; background: #eef1ff; border-radius: 999rpx; padding: 2rpx 14rpx; } +.tk-name { display: block; margin-top: 14rpx; font-size: 32rpx; font-weight: 700; color: #2b3a66; line-height: 1.4; } +.tk-desc { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-top: 8rpx; font-size: 25rpx; color: #7a85ad; line-height: 1.6; } +.tk-meta { display: flex; align-items: center; gap: 16rpx; margin-top: 14rpx; } +.tk-price { font-size: 27rpx; font-weight: 700; color: #6284ff; } +.tk-dot { font-size: 23rpx; color: #8a94c0; } +.tk-why { display: block; margin-top: 12rpx; font-size: 23rpx; color: #8a94c0; } +.tk-btn { margin-top: 16rpx; width: 180rpx; height: 64rpx; line-height: 64rpx; font-size: 26rpx; } diff --git a/miniprogram/src/utils/tasks.js b/miniprogram/src/utils/tasks.js index f949575..230b9d0 100644 --- a/miniprogram/src/utils/tasks.js +++ b/miniprogram/src/utils/tasks.js @@ -14,6 +14,21 @@ export async function getMyTasks() { return request('GET', '/api/tasks/my', {}, true); } +/** 任务广场(公开):已发布 + C端可见任务,登录后附带 can_accept/accept_reasons */ +export async function listSquare(withAuth = false) { + return request('GET', '/opc/tasks', {}, withAuth); +} + +/** 任务详情(公开) */ +export async function getTask(id, withAuth = false) { + return request('GET', `/opc/tasks/${encodeURIComponent(id)}`, {}, withAuth); +} + +/** 抢单/报名(需登录) */ +export async function grabById(id) { + return request('POST', `/opc/tasks/${encodeURIComponent(id)}/grab`, {}, true); +} + /** 解析扫码文本 → task_code:支持纯短码 / URL(?c=、?task_code=、末段) */ export function parseTaskCode(raw) { const s = (raw || '').trim();