左侧栏 ProjectNav:项目完整列表(名 + 短 id + 我的角色, 选中高亮,点击切换)+ 创建项目(名/GitHub 仓库/repo_url) + 选中项目成员(邀请/改角色/新增移除);工作台删除 sec-projects 一节与任务列表里的项目下拉框,侧栏为唯一 项目选择器,自动选第一个项目逻辑不变。 薄模块补齐(均为已核实后端路由):成员移除 DELETE、 自动化详情(GET 单条 + 产出)与编辑 PATCH、分诊条目点进 任务/自动化详情、Workers 搜索与在线筛选、管理区设置配额 POST。主线新功能(Todos、Browser、taskMeta 等)原样保留, 未做任何回退。console 标记单测本地 53 项通过。
155 lines
7.1 KiB
TypeScript
155 lines
7.1 KiB
TypeScript
import { useEffect, useState } from "react";
|
||
import { ConsoleContext, useConsole } from "./context";
|
||
import { useConsoleStore } from "./hooks/useConsoleStore";
|
||
import { useAutoRefresh, useIntervalMs } from "./hooks/useAutoRefresh";
|
||
import DataCards from "./sections/DataCards";
|
||
import DispatchSection from "./sections/DispatchSection";
|
||
import TaskListSection from "./sections/TaskListSection";
|
||
import ConversationSection from "./sections/ConversationSection";
|
||
import TaskDetailPanel from "./sections/TaskDetailPanel";
|
||
import ApiKeySection from "./sections/ApiKeySection";
|
||
import AdminSection from "./sections/AdminSection";
|
||
import ProjectNav from "./sections/ProjectNav";
|
||
import EnvSection from "./sections/EnvSection";
|
||
import EnvContextPanel from "./sections/EnvContextPanel";
|
||
import WorkersSection from "./sections/WorkersSection";
|
||
import HealthPanel from "./sections/HealthPanel";
|
||
import SkillsSection from "./sections/SkillsSection";
|
||
import SkillsToolsPanel from "./sections/SkillsToolsPanel";
|
||
import AutomationsSection from "./sections/AutomationsSection";
|
||
import TriageSection from "./sections/TriageSection";
|
||
import TodosSection from "./sections/TodosSection";
|
||
|
||
// 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:
|
||
// 左侧栏:导航(按工作流分组)+ 项目模块(ProjectNav:项目列表 +
|
||
// 创建项目 + 成员,id nav-projects,是全站唯一的项目选择器;
|
||
// 因此不再有独立的“项目与成员”视图,旧 ProjectsSection 已删,
|
||
// 工作台也不再带项目选择器)+ 数据卡片(紧凑版,始终可见)
|
||
// 任务工作台:左 = 任务列表轨(搜索/筛选/分页),中 = 派发 + 对话,
|
||
// 右 = 任务详情检查器(概览/协作/评审与审批/线程四个内页签,
|
||
// 见 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" | "todos" | "resources" | "automation" | "admin";
|
||
const VIEWS: Array<[ViewId, string, string]> = [
|
||
["tasks", "任务工作台", "任务"],
|
||
["todos", "项目待办", "任务"],
|
||
["resources", "环境与 Workers", "管理"],
|
||
["automation", "技能与自动化", "管理"],
|
||
["admin", "系统与 Key", "管理"],
|
||
];
|
||
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 SideNav({ view, setView }: { view: ViewId; setView: (v: ViewId) => void }) {
|
||
return (
|
||
<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>
|
||
<div id="nav-projects" className="shell-project-nav">
|
||
<ProjectNav />
|
||
</div>
|
||
<div className="shell-stats">
|
||
<div className="shell-stats-label">概览</div>
|
||
<DataCards />
|
||
</div>
|
||
</aside>
|
||
);
|
||
}
|
||
|
||
function TopStatus() {
|
||
const ctx = useConsole();
|
||
return <span className={ctx.keySaved ? "dash-status ok" : "dash-status off"}>{ctx.keySaved ? "● 已连接" : "○ 未连接"}</span>;
|
||
}
|
||
|
||
export default function App() {
|
||
const store = useConsoleStore();
|
||
useAutoRefresh((selectFocused) => store.refreshAll(selectFocused), 10000);
|
||
useIntervalMs(() => store.refreshSlow(), 60000);
|
||
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 = () => setViewState(viewFromHash());
|
||
window.addEventListener("hashchange", onHash);
|
||
return () => window.removeEventListener("hashchange", onHash);
|
||
}, []);
|
||
const scrollToDispatch = () => {
|
||
setView("tasks");
|
||
setComposerOpen(true);
|
||
setTimeout(() => document.getElementById("sec-dispatch")?.scrollIntoView({ behavior: "smooth" }), 0);
|
||
};
|
||
|
||
return (
|
||
<ConsoleContext.Provider value={store}>
|
||
<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>
|
||
<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 />
|
||
</section>
|
||
</section>
|
||
<section className="workspace-inspector" aria-label="任务详情">
|
||
<TaskDetailPanel />
|
||
</section>
|
||
</div>
|
||
) : (
|
||
<main className="page">
|
||
{view === "todos" ? (<><TodosSection /></>)
|
||
: view === "resources" ? (<><EnvSection /><EnvContextPanel /><WorkersSection /><HealthPanel /></>)
|
||
: view === "automation" ? (<><SkillsSection /><SkillsToolsPanel /><AutomationsSection /><TriageSection /></>)
|
||
: (<><AdminSection /><ApiKeySection /></>)}
|
||
</main>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</ConsoleContext.Provider>
|
||
);
|
||
}
|