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

+108 -74
View File
@@ -1,3 +1,4 @@
import { useEffect, useState } from "react";
import { ConsoleContext, useConsole } from "./context";
import { useConsoleStore } from "./hooks/useConsoleStore";
import { useAutoRefresh, useIntervalMs } from "./hooks/useAutoRefresh";
@@ -19,30 +20,36 @@ import SkillsToolsPanel from "./sections/SkillsToolsPanel";
import AutomationsSection from "./sections/AutomationsSection";
import TriageSection from "./sections/TriageSection";
// Layout only: ONE scrollable console page in GitHub Primer style —
// a dark header bar (name + real connection status + health + quota
// + refresh stamp + anchor nav as header links), a data-cards row,
// then every module as a bordered section. At medium/wide widths the
// sections live in three independent column wrappers (left: tasks +
// projects; middle: dispatch + conversation; right: environments +
// Workers + skills + automations + triage + admin + API key) so each
// column's modules stack tightly on their own, instead of sharing
// grid rows with the other columns — shared rows were the blank-gap
// bug (a tall task list in row 2 held the whole row tall, so the
// conversation in row 3 started far below a short dispatch box).
// At narrow width the wrappers use display:contents plus an explicit
// order per section, so the single column stacks in the original
// DOM order (dispatch, tasks, conversation, projects, …), not in
// column-group order. Detail/context panels sit under their own
// sections below (detail under 对话, members under 项目, and so on).
const NAV: Array<[string, string]> = [
["#sec-dispatch", "派发"], ["#sec-tasks", "任务"], ["#sec-conv", "对话"],
["#sec-projects", "项目"], ["#sec-env", "环境"], ["#sec-workers", "Workers"],
["#sec-skills", "技能"], ["#sec-automations", "自动化"], ["#sec-triage", "分诊"],
["#sec-admin", "管理区"], ["#sec-key", "API Key"],
// Layout: GitHub Primer style, data cards on top, then TAB pages
// instead of one endless page. The old layout stacked 11 sections
// (dispatch, tasks, conversation, projects, env, workers, skills,
// automations, triage, admin, API Key, plus detail panels) on one
// page; tabbing is what actually cuts the visible length, while
// list pagination (see useTaskList) cuts the data fetched per page.
// Tabs:
// 工作台:派发 + 任务列表(分页)+ 对话 + 任务详情
// 项目:项目 + 成员
// 环境与 Workers:环境 + 环境上下文 + Workers + 健康
// 技能与自动化:技能 + 技能工具 + 自动化 + 分诊
// 管理与 Key:管理区 + API Key
// Selected task survives tab switches: the store (useConsoleStore)
// lives in App, above the tabs, so currentTask / detail / chat
// state is not unmounted with a tab — switching back shows the same
// task, and the 10 s refresh re-syncs its detail lists. The tab is
// synced to location.hash (#tab-workbench etc.) so a reload or a
// shared link lands on the same tab; unknown hashes fall back to
// the workbench.
type TabId = "workbench" | "projects" | "env" | "skills" | "admin";
const TABS: Array<[TabId, string]> = [
["workbench", "工作台"], ["projects", "项目"], ["env", "环境与 Workers"],
["skills", "技能与自动化"], ["admin", "管理与 Key"],
];
function tabFromHash(): TabId {
const h = window.location.hash.replace("#tab-", "");
return (TABS.some(([id]) => id === h) ? h : "workbench") as TabId;
}
function TopBar() {
function TopBar({ tab, setTab }: { tab: TabId; setTab: (t: TabId) => void }) {
return (
<header className="dash-topbar">
<div className="dash-topbar-row">
@@ -52,8 +59,10 @@ function TopBar() {
<span className="muted dash-hide-sm"><TopQuota /></span>
<span className="muted dash-stamp"><TopStamp /></span>
</div>
<nav className="dash-nav" aria-label="页面导航">
{NAV.map(([href, label]) => <a key={href} href={href}>{label}</a>)}
<nav className="dash-nav tab-nav" aria-label="页面签">
{TABS.map(([id, label]) => (
<button key={id} type="button" className={tab === id ? "active" : ""} onClick={() => setTab(id)}>{label}</button>
))}
</nav>
</header>
);
@@ -78,63 +87,88 @@ export default function App() {
const store = useConsoleStore();
useAutoRefresh((selectFocused) => store.refreshAll(selectFocused), 10000);
useIntervalMs(() => store.refreshSlow(), 60000);
const [tab, setTabState] = useState<TabId>(() => tabFromHash());
const setTab = (t: TabId) => {
setTabState(t);
try { window.history.replaceState(null, "", "#tab-" + t); } catch { /* noop */ }
};
useEffect(() => {
const onHash = () => setTabState(tabFromHash());
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
const scrollToDispatch = () => {
document.getElementById("sec-dispatch")?.scrollIntoView({ behavior: "smooth" });
setTab("workbench");
setTimeout(() => document.getElementById("sec-dispatch")?.scrollIntoView({ behavior: "smooth" }), 0);
};
return (
<ConsoleContext.Provider value={store}>
<TopBar />
<main className="dash-page">
<TopBar tab={tab} setTab={setTab} />
<main className="dash-page dash-page-tabs">
<DataCards />
<div className="dash-col dash-col-left">
<section id="sec-tasks" className="dash-section card">
<h2>任务列表</h2>
<TaskListSection onNew={scrollToDispatch} />
</section>
<section id="sec-projects" className="dash-section">
<ProjectsSection />
<MembersPanel />
</section>
</div>
<div className="dash-col dash-col-mid">
<section id="sec-dispatch" className="dash-section">
<DispatchSection />
</section>
<section id="sec-conv" className="dash-section">
<div className="card">
<h2>对话</h2>
<ConversationSection />
{tab === "workbench" ? (
<div className="tab-content tab-workbench">
<div className="dash-col dash-col-left">
<section id="sec-tasks" className="dash-section card">
<h2>任务列表</h2>
<TaskListSection onNew={scrollToDispatch} />
</section>
</div>
<TaskDetailPanel />
</section>
</div>
<div className="dash-col dash-col-right">
<section id="sec-env" className="dash-section">
<EnvSection />
<EnvContextPanel />
</section>
<section id="sec-workers" className="dash-section">
<WorkersSection />
<HealthPanel />
</section>
<section id="sec-skills" className="dash-section">
<SkillsSection />
<SkillsToolsPanel />
</section>
<section id="sec-automations" className="dash-section">
<AutomationsSection />
</section>
<section id="sec-triage" className="dash-section">
<TriageSection />
</section>
<section id="sec-admin" className="dash-section">
<AdminSection />
</section>
<section id="sec-key" className="dash-section">
<ApiKeySection />
</section>
</div>
<div className="dash-col dash-col-mid">
<section id="sec-dispatch" className="dash-section">
<DispatchSection />
</section>
<section id="sec-conv" className="dash-section">
<div className="card">
<h2>对话</h2>
<ConversationSection />
</div>
<TaskDetailPanel />
</section>
</div>
</div>
) : tab === "projects" ? (
<div className="tab-content">
<section id="sec-projects" className="dash-section">
<ProjectsSection />
<MembersPanel />
</section>
</div>
) : tab === "env" ? (
<div className="tab-content">
<section id="sec-env" className="dash-section">
<EnvSection />
<EnvContextPanel />
</section>
<section id="sec-workers" className="dash-section">
<WorkersSection />
<HealthPanel />
</section>
</div>
) : tab === "skills" ? (
<div className="tab-content">
<section id="sec-skills" className="dash-section">
<SkillsSection />
<SkillsToolsPanel />
</section>
<section id="sec-automations" className="dash-section">
<AutomationsSection />
</section>
<section id="sec-triage" className="dash-section">
<TriageSection />
</section>
</div>
) : (
<div className="tab-content">
<section id="sec-admin" className="dash-section">
<AdminSection />
</section>
<section id="sec-key" className="dash-section">
<ApiKeySection />
</section>
</div>
)}
</main>
</ConsoleContext.Provider>
);