feat(admin): 数据源页转 RAG 运维台 + 支付/模型菜单重组 + 概览升级为仪表盘
后端: - 新增 POST /admin/kb/search:管理端跨租户检索,支持 mode 指定单路 (vector/fulltext/graph/hybrid),不走 scopedKB(否则会被强制锁到调用者 自己的 space,跨租户排障就没法做了) - KB 清单补 space_id(检索键是 <space_id>/<name>,缺它前端拼不出 key) 前端: - 数据源&RAG 页补「检索试验台」:同一 query 并排跑生产链路 + 四路诊断, 召回不准时能直接定位是向量/分词/图谱哪一环挂了 - 支付拆成「配置 / 订单与对账」两个子页,挂到运维 > 支付 下; 导航支持二级菜单(NavParent 命中子路由自动展开) - SettingsPage → ModelConfigPage「模型配置」,模型参数与计费规则合一 - 概览 → 仪表盘:并入计费与用量(UsagePage → UsageSection), 去掉系统健康拓扑(与服务状态页重复,同一份 /admin/status 数据) - 全局隐藏滚动条(保留滚动) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import { UsageSection } from "../components/UsageSection";
|
||||
import { adminOverview, getStatus, type AdminOverview, type SystemStatus } from "../api";
|
||||
|
||||
// 管理端「概览」= 系统控制塔:统筹全系统的吞吐 / 配置态 / 健康,而非某个账号的个人工作台。
|
||||
@@ -150,6 +151,75 @@ export function DashboardPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* C. 全局任务吞吐 */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm lg:col-span-2">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-700">全平台任务吞吐</h4>
|
||||
<p className="text-[11px] text-gray-400">近 7 天调度中心处理的任务总数(所有租户/用户)</p>
|
||||
</div>
|
||||
<span className="rounded bg-violet-50 px-2 py-0.5 text-[10px] font-medium text-violet-700">真实数据</span>
|
||||
</div>
|
||||
<svg viewBox={`0 0 ${W} ${H}`} className="h-48 w-full overflow-visible">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#7c3aed" stopOpacity="0.25" />
|
||||
<stop offset="100%" stopColor="#7c3aed" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<line x1={pad} y1={H - pad} x2={W - pad} y2={H - pad} stroke="#f1f5f9" />
|
||||
{areaPath && <path d={areaPath} fill="url(#g)" />}
|
||||
{dPath && <path d={dPath} fill="none" stroke="#7c3aed" strokeWidth="2.5" strokeLinecap="round" />}
|
||||
{pts.map((p, i) => (
|
||||
<g key={i}>
|
||||
<circle cx={p.x} cy={p.y} r="4" fill="#fff" stroke="#7c3aed" strokeWidth="2" />
|
||||
<title>{`${trend[i].key}: ${trend[i].count} 任务`}</title>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
<div className="mt-2 flex justify-between px-2 text-[10px] text-gray-400">
|
||||
{trend.map((d) => (
|
||||
<span key={d.key}>{d.key}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700">任务终态分布</h4>
|
||||
<p className="text-[11px] text-gray-400">近 7 天全局终态占比</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{ov.status_count.length === 0 && <div className="text-xs text-gray-400">暂无任务</div>}
|
||||
{ov.status_count
|
||||
.slice()
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.map((d) => {
|
||||
const st = statusStyle(d.key);
|
||||
const pct = (d.count / totalStatus) * 100;
|
||||
return (
|
||||
<div key={d.key}>
|
||||
<div className="mb-1 flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5 text-gray-600">
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: st.color }} />
|
||||
{st.label}
|
||||
</span>
|
||||
<span className="font-semibold text-gray-400">{d.count} · {pct.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-100">
|
||||
<div className="h-full rounded-full" style={{ width: `${pct}%`, backgroundColor: st.color }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 计费与用量(原独立页并入:积分消耗/Token/成本 + 趋势 + 各租户排行 + 充值) */}
|
||||
<UsageSection />
|
||||
|
||||
{/* B. 控制面配置态(管理端独有) */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
@@ -224,90 +294,6 @@ export function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* C. 全局任务吞吐 */}
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm lg:col-span-2">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-700">全平台任务吞吐</h4>
|
||||
<p className="text-[11px] text-gray-400">近 7 天调度中心处理的任务总数(所有租户/用户)</p>
|
||||
</div>
|
||||
<span className="rounded bg-violet-50 px-2 py-0.5 text-[10px] font-medium text-violet-700">真实数据</span>
|
||||
</div>
|
||||
<svg viewBox={`0 0 ${W} ${H}`} className="h-48 w-full overflow-visible">
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#7c3aed" stopOpacity="0.25" />
|
||||
<stop offset="100%" stopColor="#7c3aed" stopOpacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<line x1={pad} y1={H - pad} x2={W - pad} y2={H - pad} stroke="#f1f5f9" />
|
||||
{areaPath && <path d={areaPath} fill="url(#g)" />}
|
||||
{dPath && <path d={dPath} fill="none" stroke="#7c3aed" strokeWidth="2.5" strokeLinecap="round" />}
|
||||
{pts.map((p, i) => (
|
||||
<g key={i}>
|
||||
<circle cx={p.x} cy={p.y} r="4" fill="#fff" stroke="#7c3aed" strokeWidth="2" />
|
||||
<title>{`${trend[i].key}: ${trend[i].count} 任务`}</title>
|
||||
</g>
|
||||
))}
|
||||
</svg>
|
||||
<div className="mt-2 flex justify-between px-2 text-[10px] text-gray-400">
|
||||
{trend.map((d) => (
|
||||
<span key={d.key}>{d.key}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-semibold text-gray-700">任务终态分布</h4>
|
||||
<p className="text-[11px] text-gray-400">近 7 天全局终态占比</p>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{ov.status_count.length === 0 && <div className="text-xs text-gray-400">暂无任务</div>}
|
||||
{ov.status_count
|
||||
.slice()
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.map((d) => {
|
||||
const st = statusStyle(d.key);
|
||||
const pct = (d.count / totalStatus) * 100;
|
||||
return (
|
||||
<div key={d.key}>
|
||||
<div className="mb-1 flex items-center justify-between text-xs">
|
||||
<span className="flex items-center gap-1.5 text-gray-600">
|
||||
<span className="h-2.5 w-2.5 rounded-full" style={{ backgroundColor: st.color }} />
|
||||
{st.label}
|
||||
</span>
|
||||
<span className="font-semibold text-gray-400">{d.count} · {pct.toFixed(0)}%</span>
|
||||
</div>
|
||||
<div className="h-1.5 w-full overflow-hidden rounded-full bg-gray-100">
|
||||
<div className="h-full rounded-full" style={{ width: `${pct}%`, backgroundColor: st.color }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* D. 系统健康拓扑 */}
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow-sm">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-gray-700">系统健康拓扑</h4>
|
||||
<p className="text-[11px] text-gray-400">基建 + 应用服务 + MCP 工具注册(NATS 实时探活)</p>
|
||||
</div>
|
||||
<span className="rounded bg-emerald-50 px-2 py-0.5 text-[10px] font-medium text-emerald-700">实时</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-x-8 gap-y-1 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<HealthGroup title="基建" items={status.infra.map((i) => ({ name: i.name, up: i.up, detail: i.detail }))} />
|
||||
<HealthGroup title="应用服务" items={status.services.map((s) => ({ name: s.name, up: s.up, detail: s.detail }))} />
|
||||
<HealthGroup
|
||||
title="MCP 工具组"
|
||||
items={status.tools.map((t) => ({ name: t.server, up: t.up, detail: `${t.tools?.length ?? 0} 个工具` }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -321,23 +307,6 @@ function Row({ label, children }: { label: string; children: ReactNode }) {
|
||||
);
|
||||
}
|
||||
|
||||
function HealthGroup({ title, items }: { title: string; items: { name: string; up: boolean; detail?: string }[] }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-1 mt-2 text-[10px] font-semibold uppercase tracking-wider text-gray-300">{title}</div>
|
||||
{items.map((it) => (
|
||||
<div key={it.name} className="flex items-center justify-between border-b border-gray-50 py-1.5 text-xs">
|
||||
<span className="flex items-center gap-1.5 text-gray-600">
|
||||
<span className={`h-2 w-2 rounded-full ${it.up ? "bg-emerald-500" : "bg-rose-500"}`} />
|
||||
{it.name}
|
||||
</span>
|
||||
<span className={`text-[10px] ${it.up ? "text-gray-400" : "text-rose-500"}`}>{it.up ? it.detail || "在线" : "离线"}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Tone = "violet" | "emerald" | "cyan" | "amber" | "rose";
|
||||
const TONE: Record<Tone, string> = {
|
||||
violet: "bg-violet-50 text-violet-600",
|
||||
|
||||
Reference in New Issue
Block a user