console: 重排版为侧边导航 + 三栏工作台 + 任务详情内页签

旧排版(顶栏页签 + 数据卡片置顶 + 长页堆叠 + 详情卡片堆在对话下)全部去掉,功能不变:
- 左侧栏:导航按任务/管理分组,底部紧凑概览卡片始终可见;整页不再单页长滚动,各 pane 各自滚动
- 任务工作台:左任务列表轨(搜索/筛选/分页),中派发(可收起)+ 单列对话,右任务详情检查器
- 详情检查器:概览/协作/评审与审批/线程四个内页签,一次只显示一组
- 其余功能各成独立页面:项目与成员、环境与 Workers、技能与自动化、系统与 Key
- 视图同步到 location.hash(#view-tasks 等)

构建:npm run build(tsc + vite singlefile)通过,产物 332130 字节;本地全量 cloud 1275 项全过(skipped 4)。
This commit is contained in:
host committed 2026-10-30 03:22:02 +08:00
1 parent f0aba700e0
commit 44b080971e
4 files changed
+187 -148

No files matched your search

+86 -113
View File
@@ -20,156 +20,129 @@ import SkillsToolsPanel from "./sections/SkillsToolsPanel";
import AutomationsSection from "./sections/AutomationsSection";
import TriageSection from "./sections/TriageSection";
// 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"],
// Redesign (2026-10-30): the old layout is gone — no top tab bar, no
// data-cards row on top of a long stacked page, no detail stack
// hanging under the conversation. The new shell is an IDE-style
// workspace that never scrolls as one page:
// 左侧栏:导航(按工作流分组)+ 数据卡片(紧凑版,始终可见)
// 任务工作台:左 = 任务列表轨(搜索/筛选/分页),中 = 派发 + 对话,
// 右 = 任务详情检查器(概览/协作/评审与审批/线程四个内页签,
// 见 TaskDetailPanel)
// 其余功能各成独立页面(项目 / 资源 / 自动化 / 系统),从左侧栏
// 切入,页面内只放该组功能,不再把 11 个模块堆在同一页。
// Selected task survives view switches: the store lives in App,
// above the views, so currentTask / detail / chat state is not
// unmounted with a view. The view is synced to location.hash
// (#view-tasks etc.) so a reload or a shared link lands on the
// same view; unknown hashes fall back to the task workspace.
type ViewId = "tasks" | "projects" | "resources" | "automation" | "admin";
const VIEWS: Array<[ViewId, string, string]> = [
["tasks", "任务工作台", "任务"],
["projects", "项目与成员", "管理"],
["resources", "环境与 Workers", "管理"],
["automation", "技能与自动化", "管理"],
["admin", "系统与 Key", "管理"],
];
function tabFromHash(): TabId {
const h = window.location.hash.replace("#tab-", "");
return (TABS.some(([id]) => id === h) ? h : "workbench") as TabId;
const VIEW_TITLES: Record<ViewId, string> = Object.fromEntries(
VIEWS.map(([id, title]) => [id, title]),
) as Record<ViewId, string>;
function viewFromHash(): ViewId {
const h = window.location.hash.replace("#view-", "");
return (VIEWS.some(([id]) => id === h) ? h : "tasks") as ViewId;
}
function TopBar({ tab, setTab }: { tab: TabId; setTab: (t: TabId) => void }) {
function SideNav({ view, setView }: { view: ViewId; setView: (v: ViewId) => void }) {
return (
<header className="dash-topbar">
<div className="dash-topbar-row">
<span className="dash-brand">Codex 命令台</span>
<TopStatus />
<span className="muted dash-hide-sm"><TopHealth /></span>
<span className="muted dash-hide-sm"><TopQuota /></span>
<span className="muted dash-stamp"><TopStamp /></span>
</div>
<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>
<aside className="shell-side">
<div className="shell-brand">Codex 命令台</div>
<nav className="shell-nav" aria-label="导航">
{["任务", "管理"].map((group) => (
<div key={group} className="shell-nav-group">
<div className="shell-nav-group-label">{group}</div>
{VIEWS.filter(([, , g]) => g === group).map(([id, label]) => (
<button key={id} type="button" className={view === id ? "active" : ""} onClick={() => setView(id)}>{label}</button>
))}
</div>
))}
</nav>
</header>
<div className="shell-stats">
<div className="shell-stats-label">概览</div>
<DataCards />
</div>
</aside>
);
}
// Tiny readers so the top bar re-renders from the store.
function TopStatus() {
const ctx = useConsole();
return <span className={ctx.keySaved ? "dash-status ok" : "dash-status off"}>{ctx.keySaved ? "● 已连接" : "○ 未连接"}</span>;
}
function TopHealth() {
return <>{useConsole().healthText}</>;
}
function TopQuota() {
return <>{useConsole().quotaText}</>;
}
function TopStamp() {
return <>自动刷新 · 每 10 秒 · 已刷新 {useConsole().refreshStamp}</>;
}
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 */ }
const [view, setViewState] = useState<ViewId>(() => viewFromHash());
const [composerOpen, setComposerOpen] = useState(true);
const setView = (v: ViewId) => {
setViewState(v);
try { window.history.replaceState(null, "", "#view-" + v); } catch { /* noop */ }
};
useEffect(() => {
const onHash = () => setTabState(tabFromHash());
const onHash = () => setViewState(viewFromHash());
window.addEventListener("hashchange", onHash);
return () => window.removeEventListener("hashchange", onHash);
}, []);
const scrollToDispatch = () => {
setTab("workbench");
setView("tasks");
setComposerOpen(true);
setTimeout(() => document.getElementById("sec-dispatch")?.scrollIntoView({ behavior: "smooth" }), 0);
};
return (
<ConsoleContext.Provider value={store}>
<TopBar tab={tab} setTab={setTab} />
<main className="dash-page dash-page-tabs">
<DataCards />
{tab === "workbench" ? (
<div className="tab-content tab-workbench">
<div className="dash-col dash-col-left">
<section id="sec-tasks" className="dash-section card">
<div className="shell">
<SideNav view={view} setView={setView} />
<div className="shell-main">
<header className="shell-header">
<h1>{VIEW_TITLES[view]}</h1>
<TopStatus />
<span className="muted shell-hide-sm">{store.healthText}</span>
<span className="muted shell-hide-sm">{store.quotaText}</span>
<span className="muted shell-stamp">自动刷新 · 每 10 秒 · 已刷新 {store.refreshStamp}</span>
</header>
{view === "tasks" ? (
<div className="workspace">
<section className="workspace-rail" aria-label="任务列表">
<h2>任务列表</h2>
<TaskListSection onNew={scrollToDispatch} />
</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">
<section className="workspace-center">
<div className="composer-toggle">
<button type="button" onClick={() => setComposerOpen(!composerOpen)}>
{composerOpen ? "收起新建任务" : "新建任务"}
</button>
</div>
{composerOpen ? <section id="sec-dispatch"><DispatchSection /></section> : null}
<section className="card workspace-conv">
<h2>对话</h2>
<ConversationSection />
</div>
</section>
</section>
<section className="workspace-inspector" aria-label="任务详情">
<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>
) : (
<main className="page">
{view === "projects" ? (<><ProjectsSection /><MembersPanel /></>)
: view === "resources" ? (<><EnvSection /><EnvContextPanel /><WorkersSection /><HealthPanel /></>)
: view === "automation" ? (<><SkillsSection /><SkillsToolsPanel /><AutomationsSection /><TriageSection /></>)
: (<><AdminSection /><ApiKeySection /></>)}
</main>
)}
</div>
</div>
</ConsoleContext.Provider>
);
}