feat(web): 任务广场/详情透出 can_accept(可接/原因) + 按角色放行抢单

- 任务卡/详情显示「✓可抢 / 条件不符(附原因)」徽标;原因展示在卡内
- 条件不符时禁用抢单按钮;未登录仅浏览、抢单/报名引导登录
- 竞标任务按钮显示「报名」;详情页同样按 can_accept 放行/禁用
- 首页「接单生态」CTA 指向 #/tasks 任务广场
This commit is contained in:
Pine
2026-08-27 19:09:35 +08:00
parent 45367204e5
commit 7547de82ec
4 changed files with 30 additions and 7 deletions
+3
View File
@@ -20,6 +20,9 @@
.tk-cat { font-size: 11px; font-weight: 700; color: var(--primary); background: var(--primary-soft); border-radius: 999px; padding: 3px 10px; }
.tk-mode { font-size: 11px; font-weight: 600; color: var(--on-surface); background: var(--surface); border: 1px solid var(--border); border-radius: 999px; padding: 3px 10px; }
.tk-excl { font-size: 11px; font-weight: 600; color: var(--warning); background: var(--warning-soft); border-radius: 999px; padding: 3px 10px; }
.tk-can { font-size: 11px; font-weight: 700; color: var(--success); background: var(--success-soft); border-radius: 999px; padding: 3px 10px; }
.tk-nocan { font-size: 11px; font-weight: 700; color: var(--muted); background: var(--panel-2); border: 1px solid var(--line); border-radius: 999px; padding: 3px 10px; }
.tk-why { margin-top: 10px; font-size: 12px; color: var(--muted); line-height: 1.5; }
.tk-title { font-size: 17px; font-weight: 700; color: var(--heading); line-height: 1.4; }
.tk-desc { margin-top: 8px; font-size: 13.5px; color: var(--text-soft); line-height: 1.65; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
.tk-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
+1 -1
View File
@@ -165,7 +165,7 @@ export default function Home() {
<h2 className="pt-h">我们开放<span className="gr">接单生态</span></h2>
<p className="pt-p">把你的技能经验兴趣变成能交付的东西一段录课一个模板一次咨询一份陪跑到生态里接下属于自己的单</p>
<p className="pt-p dim">先来交流沙龙聊聊你那门手艺怎么变现</p>
<div className="mk-cta"><button className="btn-dark2" onClick={() => { location.hash = '#/events'; }}>了解接单生态 <Icon name="arrow-right" size="sm" /></button></div>
<div className="mk-cta"><button className="btn-dark2" onClick={() => { location.hash = '#/tasks'; }}>去任务广场接单 <Icon name="arrow-right" size="sm" /></button></div>
</div>
<div className="pt-panel tint-cream"><div className="pt-window"><div className="pt-window-in">
<div className="pt-window-head"><span className="pt-win-title">待接单</span><span className="pt-live"><i />Live</span></div>
+14 -2
View File
@@ -19,6 +19,9 @@ export default function TaskDetail() {
getId() && getTask(getId()).then((t) => { setTask(t || null); setLoaded(true); });
}, []);
const reasons = Array.isArray(task && task.accept_reasons) ? task.accept_reasons : [];
const can = task && task.can_accept; // true/false/undefined
const onGrab = async () => {
if (!isAuthed()) { setErr('抢单需先登录(OPC 端身份)。'); return; }
setBusy(true); setErr('');
@@ -42,6 +45,8 @@ export default function TaskDetail() {
<span className="tk-mode">{MODE[task.mode] || '抢单'}</span>
{task.exclusive && <span className="tk-excl">独占</span>}
<span className="tk-status">{task.status === 'published' ? '待接单' : task.status}</span>
{can === true && <span className="tk-can"> 你可抢</span>}
{can === false && <span className="tk-nocan" title={reasons.join('')}>条件不符</span>}
</div>
<h1 className="tk-detail-title">{task.title}</h1>
{task.task_code && <span className="tk-code">短码 {task.task_code}</span>}
@@ -64,9 +69,16 @@ export default function TaskDetail() {
{task.publisher_name ? <div className="tk-fact"><dt>发包方</dt><dd>{task.publisher_name}</dd></div> : null}
</dl>
<div className="tk-detail-actions">
<Button variant="cta" onClick={onGrab} loading={busy}>抢单</Button>
{can === false ? (
<>
<Button variant="ghost" disabled>暂不可抢</Button>
<p className="tk-note tk-why">{reasons.join('')}</p>
</>
) : (
<Button variant="cta" onClick={onGrab} loading={busy}>{task.mode === 'bid' ? '报名' : '抢单'}</Button>
)}
{err && <p className="tk-err">{err}</p>}
{!isAuthed() && <p className="tk-note">抢单需登录<a href="#/profile">去登录 / 注册</a></p>}
{!isAuthed() && <p className="tk-note">抢单/报名需登录<a href="#/profile">去登录 / 注册</a></p>}
</div>
</aside>
</div>
+12 -4
View File
@@ -14,12 +14,16 @@ const money = (a, b) => {
};
function TaskCard({ t, onGrab, busy, mine }) {
const can = t.can_accept; // true/false/undefined(未登录)
const reasons = Array.isArray(t.accept_reasons) ? t.accept_reasons : [];
return (
<a className="tk-card" href={`#/task-detail?id=${t.id}`}>
<div className="tk-card-top">
<span className="tk-cat">{t.category || '综合'}</span>
<span className="tk-mode">{MODE[t.mode] || '抢单'}</span>
{t.mode === 'bid' && <span className="tk-mode">竞标</span>}
{t.exclusive && <span className="tk-excl">独占</span>}
{can === true && <span className="tk-can"> 可抢</span>}
{can === false && <span className="tk-nocan" title={reasons.join('')}>条件不符</span>}
</div>
<h3 className="tk-title">{t.title}</h3>
<p className="tk-desc">{t.description}</p>
@@ -29,10 +33,14 @@ function TaskCard({ t, onGrab, busy, mine }) {
{t.deadline && <span><Icon name="calendar" size="sm" />{t.deadline}</span>}
{t.delivery_days ? <span><Icon name="clock" size="sm" />{t.delivery_days} 天交付</span> : null}
</div>
{can === false && <p className="tk-why">{reasons.join('')}</p>}
<div className="tk-actions">
<Button variant={mine ? 'dark' : 'ghost'} loading={busy} onClick={(e) => { e.preventDefault(); e.stopPropagation(); onGrab(t); }}>
{mine ? '查看详情' : '抢单'}
</Button>
{!mine && can !== false && (
<Button variant="ghost" loading={busy} onClick={(e) => { e.preventDefault(); e.stopPropagation(); onGrab(t); }}>
{t.mode === 'bid' ? '报名' : '抢单'}
</Button>
)}
{mine && !t.can_accept && <Button variant="dark" onClick={(e) => { e.preventDefault(); e.stopPropagation(); }}>我的接单</Button>}
<span className="tk-code" title="任务短码">{t.task_code}</span>
</div>
</a>