Files
park-desktop/src/pages/TaskDisplay.jsx
T
Pine b121eb748b feat(tasks): 任务页右上角信息面板 —— 说明/总数/轮播倒计时/页码
- TaskDisplay.jsx:右上角加 总数、翻页倒计时(s, 每秒递减翻页时复位)、页码 x/y、接单方式说明。
- task-display.css 加 tasks-head-info / tasks-info-* 样式。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-25 19:55:36 +08:00

154 lines
6.5 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
);
}