console: 一页式控制台排版 + 数据卡片

保持 Muse 风格(白底、细线、灰/浅蓝气泡),不再四栏切视图:
顶栏(连接/健康/配额/刷新 + 锚点导航)+ 6 张数据卡片
(健康、配额、任务、Workers、项目、项目数据,均取自已有
store 数据,未加载不以 0 充数)+ 所有模块按顺序铺在一页
(派发、任务列表、对话+详情、项目、环境、Workers、技能、
自动化、分诊、管理区、API Key)。图标栏与右侧栏删除,
内容分别移到顶栏与各自模块下;今天列表与任务列表重复,
不再单独保留;对话为空时不再重复渲染派发表单。
含自动选第一个项目修复(主要聊天不再空)与继续消息
历史,console 标记单测本地 53 项通过。
This commit is contained in:
host committed 2026-10-29 09:57:26 +08:00
1 parent a13762cfbe
commit 8b455c6554
9 files changed
+263 -272

No files matched your search

@@ -0,0 +1,79 @@
import { useConsole } from "../context";
import { workerAgeText } from "../api/client";
// Dashboard data cards: computed ONLY from data the store already
// fetches (health, quota, tasks, workers, projects, envs, members,
// skills/apps/MCP). No new API endpoint, no new API call. A card never
// presents 0 as a real count when its dataset is not loaded or its
// fetch failed: it shows 未加载 / 先选项目 / 加载失败 plus the
// existing error string instead.
function Card({ label, value, sub }: { label: string; value: string; sub: string }) {
return (
<div className="dash-card data-card">
<div className="dash-card-label">{label}</div>
<div className="dash-card-value">{value}</div>
<div className="muted">{sub}</div>
</div>
);
}
export default function DataCards() {
const ctx = useConsole();
const healthValue = ctx.healthText === "未加载" ? "未加载"
: ctx.healthText.includes("worker_ok=true") ? "正常"
: ctx.healthText.includes("worker_ok=false") ? "异常"
: ctx.healthText.includes("加载失败") ? "加载失败" : "未知";
const quotaMatch = ctx.quotaText.match(/剩余 \$([0-9.]+)/);
const quotaValue = ctx.quotaText.includes("加载失败") ? "加载失败"
: ctx.quotaText.includes("未填 key") || ctx.quotaText === "未加载" ? "未加载"
: quotaMatch ? "$" + quotaMatch[1] : "配额";
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)
.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 : "") };
let workersCard: { value: string; sub: string };
if (ctx.workersError) {
workersCard = { value: "加载失败", sub: ctx.workersError };
} else if (ctx.workers === null) {
workersCard = { value: ctx.workersSummary || "未加载", sub: "完整 worker 清单仅全局管理员可见" };
} else {
const online = ctx.workers.filter((w) => {
if (w.alive !== undefined && w.alive !== null) return !!w.alive;
const age = workerAgeText(w).age;
return age !== null && age <= 120;
}).length;
workersCard = { value: online + " 在线 / " + ctx.workers.length, sub: (ctx.workers.length - online) + " 离线" };
}
const projectsCard = ctx.projectMsg.includes("加载失败")
? { value: "加载失败", sub: ctx.projectMsg }
: { value: ctx.projects.length + " 个", sub: selectedProject ? "当前项目:" + selectedProject.name : "未选项目(加载后自动选第一个)" };
const projectErrors = [ctx.envMsg, ctx.memberError, ctx.skillError].filter((s) => s && s.includes("失败"));
const projectDataCard = !ctx.projectId
? { value: "先选项目", sub: "选项目后显示环境 / 成员 / 技能计数" }
: projectErrors.length
? { value: "部分加载失败", sub: projectErrors.join(" | ") }
: { value: ctx.envs.length + " 个环境", sub: ctx.members.length + " 成员 | " + ctx.skills.length + " 技能 | " + ctx.apps.length + " 应用 | " + ctx.mcpServers.length + " MCP" };
return (
<section className="dash-cards" aria-label="数据卡片">
<Card label="健康" value={healthValue} sub={ctx.healthText} />
<Card label="配额" value={quotaValue} sub={ctx.quotaText} />
<Card label="任务" value={tasksCard.value} sub={tasksCard.sub} />
<Card label="Workers" value={workersCard.value} sub={workersCard.sub} />
<Card label="项目" value={projectsCard.value} sub={projectsCard.sub} />
<Card label="项目数据" value={projectDataCard.value} sub={projectDataCard.sub} />
</section>
);
}