b121eb748b
- TaskDisplay.jsx:右上角加 总数、翻页倒计时(s, 每秒递减翻页时复位)、页码 x/y、接单方式说明。 - task-display.css 加 tasks-head-info / tasks-info-* 样式。 Co-Authored-By: Claude <noreply@anthropic.com>
154 lines
6.5 KiB
React
154 lines
6.5 KiB
React
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||
import { QRCodeSVG } from 'qrcode.react';
|
||
import { useTasks } from '../utils/taskData';
|
||
import '../styles/task-display.css';
|
||
|
||
/* 任务展示页:轮播卡片 + 二维码扫码接单。
|
||
· 数据:useTasks() 轮询 /park/api/tasks(系统任务中心上墙任务)
|
||
· 布局:一屏固定 2 行;宽度 ≥1600 → 3 列(2×3=6 张),否则 → 2 列(2×2=4 张)
|
||
· 卡片:分类/状态徽标、标题、摘要(2行)、标签、预算、任务ID + 二维码(扫码即领)
|
||
· 轮播:每页 列数×行数 张,自动翻页;MQTT task_refresh 可触发刷新 */
|
||
|
||
const ROTATE_MS = 14_000;
|
||
const ROWS = 2; // 一屏固定 2 行
|
||
|
||
/* 按宽度计算列数:宽≥1600→3 列,否则 2 列。导出便于测试。 */
|
||
export function columnsFor(width) {
|
||
const w = width || (typeof window !== 'undefined' ? window.innerWidth : 1280);
|
||
return w >= 1600 ? 3 : 2;
|
||
}
|
||
|
||
export default function TaskDisplay() {
|
||
const { tasks, loading, error } = useTasks();
|
||
const [page, setPage] = useState(0);
|
||
const [refreshTick, setRefreshTick] = useState(0);
|
||
const [cols, setCols] = useState(() => columnsFor());
|
||
const tickRef = useRef(0);
|
||
|
||
// 视口宽度 → 列数(2 或 3 列),随屏幕大小自适应(行数固定 2)
|
||
useEffect(() => {
|
||
const update = () => setCols(columnsFor());
|
||
update();
|
||
window.addEventListener('resize', update);
|
||
return () => window.removeEventListener('resize', update);
|
||
}, []);
|
||
|
||
// 接受 MQTT task_refresh → 触发重新轮询
|
||
useEffect(() => {
|
||
const onRefresh = () => { tickRef.current += 1; setRefreshTick(tickRef.current); };
|
||
window.addEventListener('dpm:task-control', onRefresh);
|
||
return () => window.removeEventListener('dpm:task-control', onRefresh);
|
||
}, []);
|
||
|
||
const pageSize = cols * ROWS; // 2×2=4 或 2×3=6
|
||
const pageCount = Math.max(1, Math.ceil(tasks.length / pageSize));
|
||
const [countdown, setCountdown] = useState(Math.round(ROTATE_MS / 1000));
|
||
useEffect(() => {
|
||
if (pageCount <= 1) { setPage(0); return; }
|
||
const t = setInterval(() => setPage((p) => (p + 1) % pageCount), ROTATE_MS);
|
||
return () => clearInterval(t);
|
||
}, [pageCount]);
|
||
|
||
// 轮播倒计时:每秒递减,翻页时重置(页数或页变更即复位)
|
||
useEffect(() => { setCountdown(Math.round(ROTATE_MS / 1000)); }, [page, pageCount]);
|
||
useEffect(() => {
|
||
const t = setInterval(() => setCountdown((c) => (c > 0 ? c - 1 : 0)), 1000);
|
||
return () => clearInterval(t);
|
||
}, []);
|
||
|
||
const visible = useMemo(() => {
|
||
if (tasks.length === 0) return [];
|
||
const start = page * pageSize;
|
||
// 不足一页时按行补位(循环取),保证 2 行填满
|
||
const items = tasks.slice(start, start + pageSize);
|
||
return items.length ? items : Array.from({ length: pageSize }, (_, i) => tasks[i % tasks.length]);
|
||
}, [tasks, page, pageSize]);
|
||
|
||
return (
|
||
<div className="tasks-screen">
|
||
<div className="tasks-head">
|
||
<span className="tasks-head-dot" />
|
||
<span className="tasks-head-title">系统任务中心</span>
|
||
<span className="tasks-head-sub">扫码即可接单 · 园区企业发标 · 合伙人接单</span>
|
||
{/* 右上角信息面板:说明 / 总数 / 轮播倒计时 / 页码 */}
|
||
<div className="tasks-head-info">
|
||
<div className="tasks-info-item"><span className="tasks-info-label">总数</span><span className="tasks-info-val">{tasks.length}</span></div>
|
||
<div className="tasks-info-item"><span className="tasks-info-label">翻页</span><span className="tasks-info-val">{pageCount > 1 ? `${countdown}s` : '—'}</span></div>
|
||
<div className="tasks-info-item"><span className="tasks-info-label">页码</span><span className="tasks-info-val">{pageCount > 1 ? `${page + 1}/${pageCount}` : '1/1'}</span></div>
|
||
<div className="tasks-info-hint">接单方式:抢单 / 竞标 / 指派 / 推荐</div>
|
||
</div>
|
||
</div>
|
||
|
||
{loading && tasks.length === 0 ? (
|
||
<div className="tasks-loading">正在加载任务…</div>
|
||
) : error && tasks.length === 0 ? (
|
||
<div className="tasks-loading">{error}</div>
|
||
) : (
|
||
<>
|
||
<div className="tasks-grid" style={{
|
||
gridTemplateColumns: `repeat(${cols}, 1fr)`,
|
||
gridTemplateRows: `repeat(${ROWS}, minmax(0, 1fr))`,
|
||
}}>
|
||
{visible.map((t) => (
|
||
<TaskCard key={t.key} task={t} />
|
||
))}
|
||
</div>
|
||
{pageCount > 1 && (
|
||
<div className="tasks-dots">
|
||
{Array.from({ length: pageCount }).map((_, i) => (
|
||
<span key={i} className={i === page ? 'tasks-dot on' : 'tasks-dot'} />
|
||
))}
|
||
</div>
|
||
)}
|
||
</>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TaskCard({ task }) {
|
||
const qrValue = task.scan_payload || task.task_code;
|
||
return (
|
||
<div className={task.claimed ? 'task-card claimed' : 'task-card'}>
|
||
<div className="task-top">
|
||
<span className="task-cat">{task.category}</span>
|
||
{task.claimed ? (
|
||
<span className="task-status taken">已接单</span>
|
||
) : (
|
||
<span className="task-status open">可接单</span>
|
||
)}
|
||
</div>
|
||
|
||
<div className="task-title">{task.title}</div>
|
||
{task.summary ? <p className="task-summary">{task.summary}</p> : null}
|
||
|
||
<div className="task-tags">
|
||
{task.tags.slice(0, 4).map((tg) => (
|
||
<span key={tg} className="task-tag">{tg}</span>
|
||
))}
|
||
</div>
|
||
|
||
<div className="task-budget">预算 {task.budgetLabel} · {task.modeLabel}</div>
|
||
|
||
<div className="task-meta-row">
|
||
{task.deadline && <span className="task-meta-item">接单截止 {task.deadline}</span>}
|
||
{task.deliveryDays > 0 && <span className="task-meta-item">交付时限 {task.deliveryDays} 天</span>}
|
||
{task.exclusive
|
||
? <span className="task-meta-item">独占接单</span>
|
||
: task.headcount > 0 ? <span className="task-meta-item">限 {task.headcount} 人</span> : null}
|
||
</div>
|
||
|
||
<div className="task-foot">
|
||
<div className="task-meta">
|
||
<span className="task-id-label">任务ID · {task.publisherName ? `${task.publisherName} 发包` : '园区发包'}</span>
|
||
<span className="task-id">{task.task_code || task.id}</span>
|
||
</div>
|
||
<div className="task-qr">
|
||
<QRCodeSVG value={qrValue || 'TASK'} size={72} fgColor="#1d2c44" level="M" />
|
||
<span className="task-qr-hint">扫码接单</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|