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 等),刷新与
  分享链接落在同一页签;窄屏工作台仍保持派发在前的顺序。
- 工作台页签仍是左(任务)右(派发/对话/详情)两栏,
  沿用等高内部滚动规则;其它页签单列堆叠。
This commit is contained in:
host committed 2026-10-30 02:25:48 +08:00
1 parent 9a11ef27b8
commit e5b6c45c83
11 files changed
+443 -122

No files matched your search

@@ -30,16 +30,18 @@ export default function DataCards() {
: 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) => ctx.tasks.filter((t) => t.status === s).length)
.map((n, i) => (["running", "completed", "failed", "queued", "merged", "interrupted"] as const)[i] + " " + n)
.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.tasks.length), sub: ctx.taskCountText + " | " + statusCounts + (selectedProject ? " | " + selectedProject.name : "") };
: { value: String(ctx.taskTotal), sub: ctx.taskCountText + " | " + statusCounts + (selectedProject ? " | " + selectedProject.name : "") };
let workersCard: { value: string; sub: string };
if (ctx.workersError) {
@@ -45,12 +45,17 @@ export default function TaskListSection({ onNew }: { onNew: () => void }) {
<button onClick={ctx.refreshTasks}>刷新</button>
<span className="muted">{ctx.taskCountText}</span>
</div>
<div className="bar">
<button disabled={ctx.taskPage <= 0} onClick={() => ctx.goToTaskPage(ctx.taskPage - 1)}>上一页</button>
<span className="muted">第 {ctx.taskPage + 1} / {ctx.taskTotalPages} 页(每页 20 条,最新在前)</span>
<button disabled={ctx.taskPage + 1 >= ctx.taskTotalPages} onClick={() => ctx.goToTaskPage(ctx.taskPage + 1)}>下一页</button>
</div>
{ctx.taskError ? <div className="err">{ctx.taskError}</div>
: !ctx.projectId ? <div className="muted">先选项目</div>
: ctx.filteredTasks.length === 0 ? <div className="muted">{ctx.tasks.length ? "没有符合筛选条件的任务" : "该项目暂无任务"}</div>
: ctx.filteredTasks.length === 0 ? <div className="muted">{ctx.taskTotal ? "没有符合筛选条件的任务" : "该项目暂无任务"}</div>
: (
<ul className="issue-list">
{ctx.filteredTasks.slice().reverse().map((t) => {
{ctx.filteredTasks.map((t) => {
const [dur, durTitle] = durationOf(t);
const [tok, tokTitle] = tokenOf(t);
const fc = firstChar(t);