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,7 +1,7 @@
|
||||
import React, { Suspense, useEffect, useState } from "react";
|
||||
import { NavLink, Routes, Route, Navigate, useLocation } from "react-router-dom";
|
||||
|
||||
import { routes, navGroups, defaultPath } from "../routes";
|
||||
import { routes, navGroups, defaultPath, type RouteDef } from "../routes";
|
||||
import { getStatus, type AuthUser } from "../api";
|
||||
|
||||
const ADMIN_BASE = "/admin";
|
||||
@@ -18,7 +18,7 @@ const ICON_MAP: Record<string, React.ReactNode> = {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 8h6m-5 0a3 3 0 110 6m0-6V7a1 1 0 112 0v1m-1 5a1.5 1.5 0 100-3m0 3v1m0-1a1.5 1.5 0 100-3m-3-3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
),
|
||||
settings: (
|
||||
models: (
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
@@ -69,6 +69,26 @@ const ICON_MAP: Record<string, React.ReactNode> = {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
"payment/config": (
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
),
|
||||
"payment/orders": (
|
||||
<svg className="h-3.5 w-3.5" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 14l2 2 4-4M7 21h10a2 2 0 002-2V7l-4-4H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
// 二级菜单(parent)自己的图标,按 parent 名索引。
|
||||
const PARENT_ICON: Record<string, React.ReactNode> = {
|
||||
支付: (
|
||||
<svg className="h-4 w-4" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
|
||||
</svg>
|
||||
),
|
||||
};
|
||||
|
||||
export function AppShell({ user, onLogout }: { user: AuthUser; onLogout: () => void }) {
|
||||
@@ -130,23 +150,13 @@ export function AppShell({ user, onLogout }: { user: AuthUser; onLogout: () => v
|
||||
{navGroups().map((g) => (
|
||||
<div key={g.group} className="space-y-1">
|
||||
<div className="px-3 py-1 text-[9px] font-bold uppercase tracking-wider text-gray-400">{g.group}</div>
|
||||
{g.items.map((r) => (
|
||||
<NavLink
|
||||
key={r.path}
|
||||
to={`${ADMIN_BASE}/${r.path}`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-2.5 rounded-lg px-3 py-2 text-xs font-semibold transition-all ${
|
||||
isActive
|
||||
? "bg-violet-600 text-white shadow-sm shadow-violet-100"
|
||||
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<span className="shrink-0">{ICON_MAP[r.path]}</span>
|
||||
<span className="flex-1 truncate">{r.label}</span>
|
||||
{!r.ready && <span className="bg-gray-100 text-gray-400 px-1 py-0.2 rounded text-[8px]">规划</span>}
|
||||
</NavLink>
|
||||
))}
|
||||
{g.nodes.map((n) =>
|
||||
n.kind === "item" ? (
|
||||
<NavItem key={n.route.path} route={n.route} />
|
||||
) : (
|
||||
<NavParent key={n.label} label={n.label} items={n.items} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
@@ -211,3 +221,65 @@ export function AppShell({ user, onLogout }: { user: AuthUser; onLogout: () => v
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// 单条导航项(顶层或二级子项共用)。
|
||||
function NavItem({ route: r }: { route: RouteDef }) {
|
||||
return (
|
||||
<NavLink
|
||||
to={`${ADMIN_BASE}/${r.path}`}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-2.5 rounded-lg px-3 py-2 text-xs font-semibold transition-all ${
|
||||
isActive
|
||||
? "bg-violet-600 text-white shadow-sm shadow-violet-100"
|
||||
: "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<span className="shrink-0">{ICON_MAP[r.path]}</span>
|
||||
<span className="flex-1 truncate">{r.label}</span>
|
||||
{!r.ready && <span className="rounded bg-gray-100 px-1 text-[8px] text-gray-400">规划</span>}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
// 可展开的二级菜单(如 运维 > 支付 > 配置 / 订单与对账)。
|
||||
// 子项命中时自动展开;父项本身不可导航,只负责收起/展开。
|
||||
function NavParent({ label, items }: { label: string; items: RouteDef[] }) {
|
||||
const loc = useLocation();
|
||||
const hasActive = items.some((r) => loc.pathname === `${ADMIN_BASE}/${r.path}`);
|
||||
const [open, setOpen] = useState(hasActive);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasActive) setOpen(true);
|
||||
}, [hasActive]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className={`flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-xs font-semibold transition-all ${
|
||||
hasActive ? "text-violet-700" : "text-gray-600 hover:bg-gray-100 hover:text-gray-900"
|
||||
}`}
|
||||
>
|
||||
<span className="shrink-0">{PARENT_ICON[label]}</span>
|
||||
<span className="flex-1 truncate text-left">{label}</span>
|
||||
<svg
|
||||
className={`h-3 w-3 shrink-0 text-gray-400 transition-transform ${open ? "rotate-90" : ""}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</button>
|
||||
{open && (
|
||||
<div className="ml-4 mt-1 space-y-1 border-l border-gray-200 pl-2">
|
||||
{items.map((r) => (
|
||||
<NavItem key={r.path} route={r} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user