console: 重排版为侧边导航 + 三栏工作台 + 任务详情内页签
旧排版(顶栏页签 + 数据卡片置顶 + 长页堆叠 + 详情卡片堆在对话下)全部去掉,功能不变: - 左侧栏:导航按任务/管理分组,底部紧凑概览卡片始终可见;整页不再单页长滚动,各 pane 各自滚动 - 任务工作台:左任务列表轨(搜索/筛选/分页),中派发(可收起)+ 单列对话,右任务详情检查器 - 详情检查器:概览/协作/评审与审批/线程四个内页签,一次只显示一组 - 其余功能各成独立页面:项目与成员、环境与 Workers、技能与自动化、系统与 Key - 视图同步到 location.hash(#view-tasks 等) 构建:npm run build(tsc + vite singlefile)通过,产物 332130 字节;本地全量 cloud 1275 项全过(skipped 4)。
This commit is contained in:
1 parent
f0aba700e0
commit
44b080971e
4 files changed
+187
-148
No files matched your search
+86
-113
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user