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,11 +1,15 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { routes, navGroups, defaultPath } from "./routes";
|
||||
import { routes, navGroups, defaultPath, type NavNode, type RouteDef } from "./routes";
|
||||
|
||||
// 把嵌套导航拍平成路由数组(顶层项 + 二级子项),顺序即渲染顺序。
|
||||
function flatten(nodes: NavNode[]): RouteDef[] {
|
||||
return nodes.flatMap((n) => (n.kind === "item" ? [n.route] : n.items));
|
||||
}
|
||||
|
||||
// routes 是控制台导航的单一事实源 —— 派生分组必须保序、不丢项、不串组。
|
||||
describe("navGroups", () => {
|
||||
it("按注册顺序归并分组,每条路由都落到对应组", () => {
|
||||
const groups = navGroups();
|
||||
const flat = groups.flatMap((g) => g.items);
|
||||
const flat = navGroups().flatMap((g) => flatten(g.nodes));
|
||||
// 不丢项、不重复
|
||||
expect(flat).toHaveLength(routes.length);
|
||||
expect(new Set(flat.map((r) => r.path)).size).toBe(routes.length);
|
||||
@@ -17,10 +21,31 @@ describe("navGroups", () => {
|
||||
expect(navGroups().map((g) => g.group)).toEqual(seen);
|
||||
});
|
||||
|
||||
it("同组路由保持注册时的相对顺序", () => {
|
||||
it("同组路由保持注册时的相对顺序(含二级子项)", () => {
|
||||
for (const g of navGroups()) {
|
||||
const expected = routes.filter((r) => r.group === g.group).map((r) => r.path);
|
||||
expect(g.items.map((r) => r.path)).toEqual(expected);
|
||||
expect(flatten(g.nodes).map((r) => r.path)).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("带 parent 的路由聚成一个二级菜单,不散落成顶层项", () => {
|
||||
for (const g of navGroups()) {
|
||||
for (const n of g.nodes) {
|
||||
if (n.kind === "item") {
|
||||
expect(n.route.parent).toBeUndefined(); // 顶层项不该带 parent
|
||||
} else {
|
||||
expect(n.items.length).toBeGreaterThan(0);
|
||||
// 同一个二级菜单下的子项,parent 必须都等于该菜单名
|
||||
for (const r of n.items) expect(r.parent).toBe(n.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("同 parent 的多条路由只生成一个二级菜单节点", () => {
|
||||
for (const g of navGroups()) {
|
||||
const labels = g.nodes.filter((n) => n.kind === "parent").map((n) => (n as Extract<NavNode, { kind: "parent" }>).label);
|
||||
expect(new Set(labels).size).toBe(labels.length);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user