feat(bigscreen): 任务展示页 + 暂时关闭语音对话页 + 导航/文档同步

- App.jsx 移除 /voice、新增 /tasks→TaskDisplay。
- pageNav PAGE_ORDER 移除 voice、加 tasks(方向键/页眉/MQTT navigate_rel 自动跟随)。
- useMqttControl:PAGE_ALIAS 加 tasks;移除 voice_*、改 task_refresh。
- PageHeader STATUS_BY_PATH 加 /tasks。
- 新增 TaskDisplay.jsx + task-display.css:轮播卡(分类/状态/标题/摘要/标签/预算/任务ID+QRCodeSVG 扫码接单)、useTasks 轮询 /park/api/tasks、接受 MQTT task_refresh。
- 新增 utils/taskData.js(useTasks, tenantFetch 带租户身份)。
- docs/mqtt-commands.md:navigate 列表/语音章节标注语音页关闭、tasks 页指令。

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Pine
2026-08-25 18:42:32 +08:00
parent 1566477b97
commit d61b59acf5
13 changed files with 441 additions and 41 deletions
+68
View File
@@ -0,0 +1,68 @@
/* 任务展示页数据:轮询 /park/api/tasks(系统任务中心上墙任务),带租户身份。
参考 parkData.js 的 useParkSim 轮询 + 归一化防崩模式。 */
import { useEffect, useRef, useState } from 'react';
import { tenantFetch } from './tenant';
const POLL_MS = 8_000;
function normalizeTask(raw, idx) {
const t = raw || {};
return {
id: t.id || '',
task_code: t.task_code || '',
scan_payload: t.scan_payload || t.task_code || '',
title: t.title || '未命名任务',
category: t.category || '综合',
summary: t.summary || '',
tags: Array.isArray(t.tags) ? t.tags : [],
budgetMin: Number(t.budget_min) || 0,
budgetMax: Number(t.budget_max) || 0,
budgetLabel: fmtBudget(t.budget_min, t.budget_max),
status: t.status || 'published',
claimed: t.status === 'claimed' || t.status === 'doing' || !!t.claimed_by,
claimedBy: t.claimed_by || '',
claimedAt: t.claimed_at || '',
key: t.id || `task-${idx}`,
};
}
function fmtBudget(min, max) {
const n = Number(min) || 0, x = Number(max) || 0;
if (!n && !x) return '面议';
if (!x) return `¥${n}`;
if (n === x) return `¥${n}`;
return `¥${n}~¥${x}`;
}
export function useTasks() {
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const aliveRef = useRef(true);
useEffect(() => {
aliveRef.current = true;
setLoading(true);
const load = async () => {
try {
const res = await tenantFetch('/api/tasks');
if (!res.ok) throw new Error(`任务加载失败(${res.status})`);
const body = await res.json();
const items = Array.isArray(body?.items) ? body.items : [];
if (aliveRef.current) {
setTasks(items.map(normalizeTask));
setError('');
}
} catch (e) {
if (aliveRef.current) setError(e.message || '任务加载失败');
} finally {
if (aliveRef.current) setLoading(false);
}
};
load();
const timer = setInterval(load, POLL_MS);
return () => { aliveRef.current = false; clearInterval(timer); };
}, []);
return { tasks, loading, error };
}