任务列表分页(服务端 + 前端): - GET /tasks 新增 limit(1..100,非法回 400)、offset、 order=desc、status/executor/q 服务端过滤;回 total(过滤后 计数)与 status_counts(全项目统计,不受过滤与分页影响, 数据卡片不再把当前页当成全项目);无 limit 时旧行为不变 (全量、正序,协调子任务与旧调用不受影响)。 - 前端每页 20 条、最新在前,上一页/下一页 + 页码;筛选与 搜索改服务端过滤(搜索防抖 300ms),切筛选/切项目/派发 后回第 1 页,10 秒刷新与刷新按钮停在当前页;新测试 test_task_list_pagination.py 5 项。 分页面签: - 顶栏锚点导航改成 5 个页签:工作台(派发 + 任务列表 + 对话 + 详情)、项目(项目 + 成员)、环境与 Workers、 技能与自动化(技能 + 自动化 + 分诊)、管理与 Key; 数据卡片仍在页签上方常驻。选中任务存在 App 层 store 里,切页不丢,回来仍是同一任务、10 秒刷新续同步; 页签同步到 location.hash(#tab-workbench 等),刷新与 分享链接落在同一页签;窄屏工作台仍保持派发在前的顺序。 - 工作台页签仍是左(任务)右(派发/对话/详情)两栏, 沿用等高内部滚动规则;其它页签单列堆叠。
82 lines
4.1 KiB
TypeScript
82 lines
4.1 KiB
TypeScript
import { useConsole } from "../context";
|
||
import { workerAgeText } from "../api/client";
|
||
|
||
// Dashboard data cards: computed ONLY from data the store already
|
||
// fetches (health, quota, tasks, workers, projects, envs, members,
|
||
// skills/apps/MCP). No new API endpoint, no new API call. A card never
|
||
// presents 0 as a real count when its dataset is not loaded or its
|
||
// fetch failed: it shows 未加载 / 先选项目 / 加载失败 plus the
|
||
// existing error string instead.
|
||
function Card({ label, value, sub }: { label: string; value: string; sub: string }) {
|
||
return (
|
||
<div className="dash-card data-card">
|
||
<div className="dash-card-label">{label}</div>
|
||
<div className="dash-card-value">{value}</div>
|
||
<div className="muted">{sub}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default function DataCards() {
|
||
const ctx = useConsole();
|
||
|
||
const healthValue = ctx.healthText === "未加载" ? "未加载"
|
||
: ctx.healthText.includes("worker_ok=true") ? "正常"
|
||
: ctx.healthText.includes("worker_ok=false") ? "异常"
|
||
: ctx.healthText.includes("加载失败") ? "加载失败" : "未知";
|
||
|
||
const quotaMatch = ctx.quotaText.match(/剩余 \$([0-9.]+)/);
|
||
const quotaValue = ctx.quotaText.includes("加载失败") ? "加载失败"
|
||
: ctx.quotaText.includes("未填 key") || ctx.quotaText === "未加载" ? "未加载"
|
||
: quotaMatch ? "$" + quotaMatch[1] : "配额";
|
||
|
||
// Status counts come from the server (all project rows), not from
|
||
// ctx.tasks, which is now only the current page — counting the page
|
||
// would present 20 tasks as the whole project.
|
||
const statusCounts = (["running", "completed", "failed", "queued", "merged", "interrupted"] as const)
|
||
.map((s) => s + " " + (ctx.taskStatusCounts[s] || 0))
|
||
.join(" | ");
|
||
const selectedProject = ctx.projects.find((p) => p.id === ctx.projectId);
|
||
const tasksCard = ctx.taskError
|
||
? { value: "加载失败", sub: ctx.taskError }
|
||
: !ctx.projectId
|
||
? { value: "先选项目", sub: ctx.projectMsg || "项目加载后自动选第一个" }
|
||
: { value: String(ctx.taskTotal), sub: ctx.taskCountText + " | " + statusCounts + (selectedProject ? " | " + selectedProject.name : "") };
|
||
|
||
let workersCard: { value: string; sub: string };
|
||
if (ctx.workersError) {
|
||
workersCard = { value: "加载失败", sub: ctx.workersError };
|
||
} else if (ctx.workers === null) {
|
||
workersCard = { value: ctx.workersSummary || "未加载", sub: "完整 worker 清单仅全局管理员可见" };
|
||
} else {
|
||
const online = ctx.workers.filter((w) => {
|
||
if (w.alive !== undefined && w.alive !== null) return !!w.alive;
|
||
const age = workerAgeText(w).age;
|
||
return age !== null && age <= 120;
|
||
}).length;
|
||
workersCard = { value: online + " 在线 / " + ctx.workers.length, sub: (ctx.workers.length - online) + " 离线" };
|
||
}
|
||
|
||
const projectsCard = ctx.projectMsg.includes("加载失败")
|
||
? { value: "加载失败", sub: ctx.projectMsg }
|
||
: { value: ctx.projects.length + " 个", sub: selectedProject ? "当前项目:" + selectedProject.name : "未选项目(加载后自动选第一个)" };
|
||
|
||
const projectErrors = [ctx.envMsg, ctx.memberError, ctx.skillError].filter((s) => s && s.includes("失败"));
|
||
const projectDataCard = !ctx.projectId
|
||
? { value: "先选项目", sub: "选项目后显示环境 / 成员 / 技能计数" }
|
||
: projectErrors.length
|
||
? { value: "部分加载失败", sub: projectErrors.join(" | ") }
|
||
: { value: ctx.envs.length + " 个环境", sub: ctx.members.length + " 成员 | " + ctx.skills.length + " 技能 | " + ctx.apps.length + " 应用 | " + ctx.mcpServers.length + " MCP" };
|
||
|
||
return (
|
||
<section className="dash-cards" aria-label="数据卡片">
|
||
<Card label="健康" value={healthValue} sub={ctx.healthText} />
|
||
<Card label="配额" value={quotaValue} sub={ctx.quotaText} />
|
||
<Card label="任务" value={tasksCard.value} sub={tasksCard.sub} />
|
||
<Card label="Workers" value={workersCard.value} sub={workersCard.sub} />
|
||
<Card label="项目" value={projectsCard.value} sub={projectsCard.sub} />
|
||
<Card label="项目数据" value={projectDataCard.value} sub={projectDataCard.sub} />
|
||
</section>
|
||
);
|
||
}
|