Files
codex-app-server/cloud/console-react/src/sections/DataCards.tsx
T
host e5b6c45c83 console: 任务列表服务端分页 + 控制台分页面签
任务列表分页(服务端 + 前端):
- 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 等),刷新与
  分享链接落在同一页签;窄屏工作台仍保持派发在前的顺序。
- 工作台页签仍是左(任务)右(派发/对话/详情)两栏,
  沿用等高内部滚动规则;其它页签单列堆叠。
2026-10-30 02:25:48 +08:00

82 lines
4.1 KiB
TypeScript
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 { 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>
);
}