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