刘文武 пре 7 часа
родитељ
комит
93f1180f89

+ 263 - 821
knowhub/frontend/src/components/dashboard/CategoryTree.tsx

@@ -1,67 +1,15 @@
 import { useState, useRef, useEffect, useCallback, useMemo } from "react";
 import { createPortal } from "react-dom";
 import { cn } from "../../lib/utils";
-import {
-  ChevronRight,
-  ChevronDown,
-  ChevronLeft,
-  ChevronUp,
-  ChevronsDown,
-  ChevronsUp,
-  RotateCcw,
-  ZoomIn,
-  ZoomOut,
-  Maximize,
-  FolderTree,
-  Info,
-  Search,
-} from "lucide-react";
-
-// 统一的数字徽标样式 (参考 .node-count) — 蓝色 accent + monospace
-const COUNT_BADGE_BASE =
-  "inline-flex items-center text-[11px] font-semibold leading-[1.5] px-[7px] py-px rounded-full tabular-nums [letter-spacing:-0.02em] [font-family:'JetBrains_Mono','SF_Mono',Consolas,monospace]";
-const COUNT_BADGE_BLUE = "bg-[#dbeafe] text-[#2563eb]";
-// expandable 版本: hover 时背景加深 (.node-count.expandable:hover)
-const COUNT_BADGE_BLUE_EXPANDABLE = "bg-[#dbeafe] text-[#2563eb] hover:bg-[#bfdbfe] cursor-pointer transition-colors";
-const COUNT_BADGE_BLUE_EXPANDED = "bg-[#bfdbfe] text-[#1e40af] cursor-pointer transition-colors";
-
-// 节点 border-left + hover 高亮统一使用蓝色 (#60a5fa, rgb 96,165,250)
-// hover 公式见 className 中的 hover:border-[rgba(96,165,250,...)] / hover:shadow-[...]
-const NODE_BORDER = "#60a5fa";
-
-// 与参考稿一致的维度配色 (border-left + hover ring + section header)
-type DimStyle = {
-  label: string;
-  border: string;
-  rgb: string;
-  badge: string;
-  badgeBg: string;
-};
-const DIM_STYLES: Record<string, DimStyle> = {
-  实质: { label: "实质", border: "#f9a825", rgb: "249, 168, 37", badge: "#a16207", badgeBg: "#fef3c7" },
-  形式: { label: "形式", border: "#42a5f5", rgb: "66, 165, 245", badge: "#1d4ed8", badgeBg: "#dbeafe" },
-  意图: { label: "意图", border: "#66bb6a", rgb: "102, 187, 106", badge: "#15803d", badgeBg: "#dcfce7" },
-  作用: { label: "作用", border: "#ab47bc", rgb: "171, 71, 188", badge: "#7e22ce", badgeBg: "#f3e8ff" },
-  感受: { label: "感受", border: "#ec407a", rgb: "236, 64, 122", badge: "#be185d", badgeBg: "#fce7f3" },
-};
-const DEFAULT_DIM: DimStyle = {
-  label: "其他",
-  border: "#94a3b8",
-  rgb: "148, 163, 184",
-  badge: "#475569",
-  badgeBg: "#e2e8f0",
-};
-const DIM_ORDER = ["实质", "形式", "意图", "作用", "感受"];
-
-type ElementItem = { name?: string; count?: number; element_id?: string | number };
+import { ChevronRight, ChevronDown, ChevronLeft, ZoomIn, ZoomOut, Maximize, FolderTree, FileText } from "lucide-react";
 
 interface NodeProps {
   node: any;
   onSelect: (node: any) => void;
   onOpenDetail?: (node: any) => void;
   selectedIds: Set<string>;
-  depth: number;
-  highlightLeafNames: Set<string> | null;
+  level: number;
+  highlightLeafNames: Set<string> | null; // null = no filter active
   subtreeHighlightNodeIds: Set<string> | null;
   sourceNodeIds: Set<string> | null;
   patternNodeIds: Set<string> | null;
@@ -76,18 +24,18 @@ interface NodeProps {
       patternCount: number;
     }
   >;
-  dimStyle: DimStyle;
+  dimensionColor: string; // hex color for the node's dimension
   focusedTreeNodeId?: string | number | null;
   focusTrigger?: number;
   isOpen: (nodeId: string) => boolean;
   onToggleOpen: (nodeId: string) => void;
+  // 与 RelationCard 4 态视觉对齐所需的输入
   directHighlightNodeIds: Set<string> | null;
   indirectHighlightNodeIds: Set<string> | null;
   hasActiveFilter: boolean;
-  searchTokens: string[];
-  visibleDepth: number; // -1 = 不限
 }
 
+// Returns true if this node or any descendant is in the highlight set
 function nodeHasHighlightedLeaf(node: any, highlightLeafNames: Set<string> | null): boolean {
   if (!highlightLeafNames) return true;
   const hasChildren = node.children && node.children.length > 0;
@@ -95,59 +43,18 @@ function nodeHasHighlightedLeaf(node: any, highlightLeafNames: Set<string> | nul
   return node.children.some((child: any) => nodeHasHighlightedLeaf(child, highlightLeafNames));
 }
 
-function HighlightedText({ text, tokens }: { text: string; tokens: string[] }) {
-  if (!tokens.length) return <>{text}</>;
-  const lower = text.toLowerCase();
-  const ranges: Array<[number, number]> = [];
-  for (const t of tokens) {
-    if (!t) continue;
-    let from = 0;
-    while (from <= lower.length) {
-      const idx = lower.indexOf(t, from);
-      if (idx < 0) break;
-      ranges.push([idx, idx + t.length]);
-      from = idx + t.length;
-    }
-  }
-  if (!ranges.length) return <>{text}</>;
-  ranges.sort((a, b) => a[0] - b[0] || a[1] - b[1]);
-  const merged: Array<[number, number]> = [ranges[0].slice() as [number, number]];
-  for (let i = 1; i < ranges.length; i++) {
-    const [s, e] = ranges[i];
-    const last = merged[merged.length - 1];
-    if (s <= last[1]) last[1] = Math.max(last[1], e);
-    else merged.push([s, e]);
-  }
-  const out: React.ReactNode[] = [];
-  let cursor = 0;
-  merged.forEach(([s, e], i) => {
-    if (s > cursor) out.push(text.slice(cursor, s));
-    out.push(
-      <mark
-        key={i}
-        className="rounded-[2px] bg-[#fff3a3] text-inherit px-[1px] font-semibold"
-      >
-        {text.slice(s, e)}
-      </mark>,
-    );
-    cursor = e;
-  });
-  if (cursor < text.length) out.push(text.slice(cursor));
-  return <>{out}</>;
-}
-
 function HorizontalTreeNode({
   node,
   onSelect,
   onOpenDetail,
   selectedIds,
-  depth,
+  level,
   highlightLeafNames,
   subtreeHighlightNodeIds,
   sourceNodeIds,
   patternNodeIds,
   nodeMetricsMap,
-  dimStyle,
+  dimensionColor,
   focusedTreeNodeId = null,
   focusTrigger = 0,
   isOpen,
@@ -155,8 +62,6 @@ function HorizontalTreeNode({
   directHighlightNodeIds,
   indirectHighlightNodeIds,
   hasActiveFilter,
-  searchTokens,
-  visibleDepth,
 }: NodeProps) {
   const expanded = isOpen(String(node.id));
   const [hoveredMetric, setHoveredMetric] = useState<null | {
@@ -166,10 +71,8 @@ function HorizontalTreeNode({
     x: number;
     y: number;
   }>(null);
-  const [elementsOpen, setElementsOpen] = useState(false);
   const nodeRef = useRef<HTMLDivElement>(null);
   const hasChildren = node.children && node.children.length > 0;
-  const elements: ElementItem[] = Array.isArray(node.elements) ? node.elements : [];
   const metrics = nodeMetricsMap[String(node.id)] || {
     reqCount: 0,
     procCount: 0,
@@ -178,21 +81,18 @@ function HorizontalTreeNode({
     nodeCount: 0,
     patternCount: 0,
   };
+  let textColor = "text-slate-800";
+  if (hasChildren) textColor = cn(textColor, "font-extrabold");
 
+  // Highlight/dim logic for reverse filtering (legacy:仅 inHighlight 用于 subtree fade-out)
   const inHighlight = nodeHasHighlightedLeaf(node, highlightLeafNames);
   const isSelected = node.id !== null && node.id !== undefined && selectedIds.has(String(node.id));
+
+  // 4 态分层(叶子 & 非叶一视同仁):非叶节点按 "子树聚合" 判断
+  // 聚合集由 CategoryTree 根预计算,passed-in sets 已包含"自己或任一后代命中"的 id
   const isDirect = directHighlightNodeIds?.has(String(node.id)) ?? false;
   const isIndirect = !isDirect && (indirectHighlightNodeIds?.has(String(node.id)) ?? false);
   const isNoMatch = hasActiveFilter && !isSelected && !isDirect && !isIndirect;
-
-  const totalCount = node.total_element_count ?? node.element_count ?? 0;
-  const ownCount = node.element_count ?? 0;
-  const showElementsToggle = !hasChildren && elements.length > 0;
-
-  // visibleDepth 限制下,该节点的子树是否应该折叠
-  const limitedByDepth = visibleDepth >= 0 && depth >= visibleDepth - 1;
-  const shouldShowChildren = expanded && hasChildren && !limitedByDepth;
-
   const shouldScrollIntoView =
     focusedTreeNodeId !== null &&
     focusedTreeNodeId !== undefined &&
@@ -200,6 +100,10 @@ function HorizontalTreeNode({
     node.id !== undefined &&
     String(focusedTreeNodeId) === String(node.id);
 
+  // 只在"明确的用户导航行为"下 scroll,避免 filter 变化导致 focused 节点切换时整页跳动:
+  //   1. isSelected 变为 true(用户点了这个树节点本身)
+  //   2. focusTrigger 递增(用户按 < / > 切换匹配节点 / 点 tag 跳转)
+  // 过滤条件变化导致 shouldScrollIntoView 从 false→true 的情况**不**触发 scroll。
   const prevFocusTriggerRef = useRef(focusTrigger);
   useEffect(() => {
     if (!nodeRef.current) return;
@@ -213,214 +117,158 @@ function HorizontalTreeNode({
     }
   }, [isSelected, shouldScrollIntoView, focusTrigger]);
 
-  // 4 态视觉:selected / direct / no-match / default — 选中色统一为 #3b82f6
-  const labelStateStyle: React.CSSProperties = (() => {
-    if (isSelected) {
-      return {
-        borderColor: "#3b82f6",
-        boxShadow: "0 0 0 2px rgba(59,130,246,0.32), 0 1px 3px rgba(0,0,0,0.07)",
-      };
-    }
-    if (isDirect) {
-      return {
-        borderColor: "rgba(56,189,248,0.55)",
-        boxShadow: "0 0 0 1.5px rgba(125,211,252,0.5), 0 1px 3px rgba(0,0,0,0.07)",
-        background: "rgba(240,249,255,0.7)",
-      };
-    }
-    return {};
-  })();
-
   return (
     <div
       className={cn(
-        "tree-node flex flex-row items-start transition-opacity duration-200",
+        "flex flex-row items-start transition-opacity duration-200",
         highlightLeafNames && !inHighlight && "opacity-20",
       )}
     >
       {/* Node Card */}
       <div
         ref={nodeRef}
-        onClick={() => onSelect(node)}
         className={cn(
-          "node-label group inline-flex items-center gap-1.5 cursor-pointer shrink-0",
-          "bg-white border border-slate-200 rounded-[9px] px-2.5 py-1.5",
-          "text-[13px] font-medium text-slate-800 whitespace-nowrap select-none",
-          "shadow-[0_1px_3px_rgba(0,0,0,0.07),0_0_0_1px_rgba(0,0,0,0.04)]",
-          "transition-[box-shadow,transform,border-color,background] duration-150",
-          // hover: --node-dim-rgb 公式 (96,165,250) — 上浮 + 蓝色淡边框 + 双层阴影
-          "hover:-translate-y-px",
-          "hover:border-[rgba(96,165,250,0.45)]",
-          "hover:shadow-[0_4px_16px_rgba(96,165,250,0.14),0_0_0_1.5px_rgba(96,165,250,0.3)]",
-          isNoMatch && "opacity-40 saturate-50",
+          "flex items-center px-3 py-1.5 rounded-md border shadow-[0_1px_2px_rgba(0,0,0,0.05)] cursor-pointer whitespace-nowrap transition-all z-10 h-[34px] bg-white border-slate-200",
+          isSelected
+            ? "ring-2 ring-orange-400 ring-offset-2 border-orange-400 shadow-[0_0_0_2px_rgba(251,146,60,0.16)]"
+            : isDirect
+              ? // direct:与 RelationCard direct-match 视觉一致
+                "border-sky-300 bg-sky-50/70 shadow-[0_0_0_1px_rgba(125,211,252,0.5)]"
+              : isNoMatch
+                ? // 无关联:置灰但仍可点击
+                  "border-slate-200 bg-slate-50 opacity-45 saturate-50"
+                : // indirect 或无筛选:默认态
+                  "hover:brightness-95",
         )}
-        style={{
-          borderLeft: `3px solid ${NODE_BORDER}`,
-          ...labelStateStyle,
-        }}
+        onClick={() => onSelect(node)}
       >
-        {hasChildren && (
-          <button
-            onClick={(e) => {
-              e.stopPropagation();
-              onToggleOpen(String(node.id));
-            }}
+        <span className={cn("text-xs font-bold mr-3", textColor)}>{node.name || "Root"}</span>
+        <div className="flex text-[9px] bg-slate-100 rounded px-1.5 shadow-sm items-center font-bold text-slate-700">
+          {node.total_posts_count || 0} 帖
+        </div>
+        <div className="relative flex items-center gap-2 ml-2">
+          <span
+            onMouseEnter={(e) =>
+              metrics.patternCount > 0 &&
+              setHoveredMetric({
+                key: "pattern",
+                count: metrics.patternCount,
+                colorClass: "bg-blue-500 border-blue-500 text-white",
+                x: e.currentTarget.getBoundingClientRect().left + e.currentTarget.getBoundingClientRect().width / 2,
+                y: e.currentTarget.getBoundingClientRect().top - 8,
+              })
+            }
+            onMouseLeave={() => setHoveredMetric((prev) => (prev?.key === "pattern" ? null : prev))}
             className={cn(
-              "node-arrow flex items-center justify-center w-[15px] h-[15px] rounded-[4px]",
-              "bg-slate-100 border border-slate-200 text-slate-400",
-              "hover:bg-blue-50 hover:border-blue-400 hover:text-blue-600 transition-colors",
+              "w-2.5 h-2.5 rounded-full",
+              metrics.patternCount > 0 ? "bg-blue-500 opacity-100" : "opacity-0",
             )}
-            title={expanded ? "收起" : "展开"}
-          >
-            {expanded ? <ChevronDown size={9} strokeWidth={2} /> : <ChevronRight size={9} strokeWidth={2} />}
-          </button>
-        )}
-
-        <span className={cn("px-0.5", hasChildren && "font-bold text-slate-900")}>
-          <HighlightedText
-            text={node.name || "Root"}
-            tokens={searchTokens}
           />
-        </span>
-
-        {/* 总计数 — 仅在父节点上显示,叶节点交给 element toggle */}
-        {totalCount > 0 && !showElementsToggle && (
           <span
-            className={cn(COUNT_BADGE_BASE, COUNT_BADGE_BLUE)}
-            title={`子树元素总数 ${totalCount}`}
-          >
-            {totalCount.toLocaleString()}
-          </span>
-        )}
-
-        {/* 帖子数 */}
-        {node.total_posts_count > 0 && (
+            onMouseEnter={(e) =>
+              metrics.reqCount > 0 &&
+              setHoveredMetric({
+                key: "req",
+                count: metrics.reqCount,
+                colorClass: "bg-cyan-500 border-cyan-500 text-white",
+                x: e.currentTarget.getBoundingClientRect().left + e.currentTarget.getBoundingClientRect().width / 2,
+                y: e.currentTarget.getBoundingClientRect().top - 8,
+              })
+            }
+            onMouseLeave={() => setHoveredMetric((prev) => (prev?.key === "req" ? null : prev))}
+            className={cn("w-2.5 h-2.5 rounded-full", metrics.reqCount > 0 ? "bg-cyan-500" : "opacity-0")}
+          />
           <span
-            className="inline-flex items-center text-[10px] font-semibold leading-[1.5] px-[6px] py-px rounded-full bg-slate-100 text-slate-600 tabular-nums"
-            title={`相关帖子 ${node.total_posts_count}`}
-          >
-            {node.total_posts_count} 帖
-          </span>
-        )}
+            onMouseEnter={(e) =>
+              metrics.procCount > 0 &&
+              setHoveredMetric({
+                key: "proc",
+                count: metrics.procCount,
+                colorClass: "bg-green-500 border-green-500 text-white",
+                x: e.currentTarget.getBoundingClientRect().left + e.currentTarget.getBoundingClientRect().width / 2,
+                y: e.currentTarget.getBoundingClientRect().top - 8,
+              })
+            }
+            onMouseLeave={() => setHoveredMetric((prev) => (prev?.key === "proc" ? null : prev))}
+            className={cn("w-2.5 h-2.5 rounded-full", metrics.procCount > 0 ? "bg-green-500" : "opacity-0")}
+          />
+          <span
+            onMouseEnter={(e) =>
+              metrics.capCount > 0 &&
+              setHoveredMetric({
+                key: "cap",
+                count: metrics.capCount,
+                colorClass: "bg-amber-400 border-amber-400 text-white",
+                x: e.currentTarget.getBoundingClientRect().left + e.currentTarget.getBoundingClientRect().width / 2,
+                y: e.currentTarget.getBoundingClientRect().top - 8,
+              })
+            }
+            onMouseLeave={() => setHoveredMetric((prev) => (prev?.key === "cap" ? null : prev))}
+            className={cn("w-2.5 h-2.5 rounded-full", metrics.capCount > 0 ? "bg-amber-400" : "opacity-0")}
+          />
+          <span
+            onMouseEnter={(e) =>
+              metrics.toolCount > 0 &&
+              setHoveredMetric({
+                key: "tool",
+                count: metrics.toolCount,
+                colorClass: "bg-orange-500 border-orange-500 text-white",
+                x: e.currentTarget.getBoundingClientRect().left + e.currentTarget.getBoundingClientRect().width / 2,
+                y: e.currentTarget.getBoundingClientRect().top - 8,
+              })
+            }
+            onMouseLeave={() => setHoveredMetric((prev) => (prev?.key === "tool" ? null : prev))}
+            className={cn("w-2.5 h-2.5 rounded-full", metrics.toolCount > 0 ? "bg-orange-500" : "opacity-0")}
+          />
+        </div>
 
-        {/* 当前节点 elements 列表 (叶节点) — expandable 版本 */}
-        {showElementsToggle && (
+        {onOpenDetail && (
           <button
-            type="button"
+            className="ml-2 px-1.5 py-1 text-slate-400 hover:text-slate-700 hover:bg-slate-100 rounded focus:outline-none transition-colors"
             onClick={(e) => {
               e.stopPropagation();
-              setElementsOpen((v) => !v);
+              onOpenDetail(node);
             }}
-            className={cn(
-              COUNT_BADGE_BASE,
-              "gap-[3px]",
-              elementsOpen ? COUNT_BADGE_BLUE_EXPANDED : COUNT_BADGE_BLUE_EXPANDABLE,
-            )}
-            title={elementsOpen ? "收起元素" : "展开元素"}
+            title="查看详情"
           >
-            {ownCount || elements.length}
-            <ChevronDown
-              size={9}
-              strokeWidth={2.4}
-              className={cn("transition-transform", elementsOpen ? "" : "-rotate-90")}
-            />
+            <FileText size={13} />
           </button>
         )}
 
-        {/* 关系指标点 */}
-        {(metrics.patternCount || metrics.reqCount || metrics.procCount || metrics.capCount || metrics.toolCount) >
-          0 && (
-          <div className="flex items-center gap-1 ml-1">
-            {[
-              { key: "pattern", count: metrics.patternCount, color: "bg-blue-500" },
-              { key: "req", count: metrics.reqCount, color: "bg-cyan-500" },
-              { key: "proc", count: metrics.procCount, color: "bg-green-500" },
-              { key: "cap", count: metrics.capCount, color: "bg-amber-400" },
-              { key: "tool", count: metrics.toolCount, color: "bg-orange-500" },
-            ]
-              .filter((m) => m.count > 0)
-              .map((m) => (
-                <span
-                  key={m.key}
-                  className={cn("w-2 h-2 rounded-full cursor-help", m.color)}
-                  onMouseEnter={(e) =>
-                    setHoveredMetric({
-                      key: m.key,
-                      count: m.count,
-                      colorClass: cn(m.color, "border", m.color.replace("bg-", "border-"), "text-white"),
-                      x:
-                        e.currentTarget.getBoundingClientRect().left +
-                        e.currentTarget.getBoundingClientRect().width / 2,
-                      y: e.currentTarget.getBoundingClientRect().top - 8,
-                    })
-                  }
-                  onMouseLeave={() => setHoveredMetric((prev) => (prev?.key === m.key ? null : prev))}
-                />
-              ))}
-          </div>
-        )}
-
-        {/* hover-only 详情按钮 */}
-        {onOpenDetail && (
+        {hasChildren && (
           <button
-            type="button"
+            className="ml-2 px-1 text-slate-400 hover:text-slate-700 focus:outline-none transition-transform"
             onClick={(e) => {
               e.stopPropagation();
-              onOpenDetail(node);
+              onToggleOpen(String(node.id));
             }}
-            className={cn(
-              "ml-1 inline-flex items-center justify-center w-[18px] h-[18px] rounded-[4px]",
-              "border border-slate-200 bg-slate-100 text-slate-400",
-              "opacity-0 group-hover:opacity-100 pointer-events-none group-hover:pointer-events-auto",
-              "hover:bg-blue-50 hover:border-blue-400 hover:text-blue-600",
-              "transition-[opacity,background,border-color,color]",
-            )}
-            title="查看详情"
           >
-            <Info size={11} strokeWidth={1.8} />
+            {expanded ? (
+              <ChevronDown
+                size={14}
+                className="opacity-70"
+              />
+            ) : (
+              <ChevronRight
+                size={14}
+                className="opacity-70"
+              />
+            )}
           </button>
         )}
       </div>
 
-      {/* 横向布局:children 在父节点右侧 */}
-      {shouldShowChildren && (
-        <div className="flex flex-col relative ml-8 shrink-0">
-          {/* 父节点右侧到第一个子节点的横向连接线 */}
-          <div className="absolute -left-8 top-[17px] w-8 h-px bg-[#d1d9e6]" />
-
-          {/* leaf elements 行 - 仅当节点本身是叶子且 elementsOpen */}
-          {showElementsToggle && elementsOpen && (
-            <div className="flex flex-wrap gap-[4px_6px] items-start max-w-[720px] pb-2">
-              {elements.map((el, i) => (
-                <span
-                  key={el.element_id ?? `${el.name}-${i}`}
-                  className={cn(
-                    "inline-flex items-center gap-1 text-[11px] font-medium px-2 py-[3px] rounded-full",
-                    "bg-white border border-slate-200 text-slate-600",
-                    "hover:bg-blue-50 hover:border-blue-400 hover:text-blue-600 transition-colors",
-                  )}
-                >
-                  <span>
-                    <HighlightedText
-                      text={el.name || ""}
-                      tokens={searchTokens}
-                    />
-                  </span>
-                  <span className="text-[10px] font-semibold text-slate-400 tabular-nums">{el.count ?? 0}</span>
-                </span>
-              ))}
-            </div>
-          )}
-
+      {/* Children */}
+      {expanded && hasChildren && (
+        <div className="flex flex-col relative ml-8">
+          <div className="absolute -left-8 top-[17px] w-8 h-px bg-slate-300"></div>
           {node.children.map((child: any, i: number) => (
             <div
               key={child.id || child.path || i}
               className={cn(
                 "relative pl-8 pb-3 flex items-start",
-                // 横向小线 (从主干到子节点)
-                "before:absolute before:left-0 before:top-[17px] before:w-8 before:h-px before:bg-[#d1d9e6]",
-                // 主干竖线
-                "after:absolute after:left-0 after:w-px after:bg-[#d1d9e6]",
+                "before:absolute before:left-0 before:top-[17px] before:w-8 before:h-px before:bg-slate-300",
+                "after:absolute after:left-0 after:w-px after:bg-slate-300",
                 "first:after:top-[17px] first:after:bottom-0",
                 "last:after:top-0 last:after:bottom-[calc(100%-17px)]",
                 "[&:not(:first-child):not(:last-child)]:after:top-0 [&:not(:first-child):not(:last-child)]:after:bottom-0",
@@ -432,28 +280,24 @@ function HorizontalTreeNode({
                 onSelect={onSelect}
                 onOpenDetail={onOpenDetail}
                 selectedIds={selectedIds}
-                depth={depth + 1}
+                level={level + 1}
                 highlightLeafNames={highlightLeafNames}
                 subtreeHighlightNodeIds={subtreeHighlightNodeIds}
                 sourceNodeIds={sourceNodeIds}
                 patternNodeIds={patternNodeIds}
                 nodeMetricsMap={nodeMetricsMap}
-                dimStyle={dimStyle}
+                dimensionColor={dimensionColor}
                 focusedTreeNodeId={focusedTreeNodeId}
-                focusTrigger={focusTrigger}
                 isOpen={isOpen}
                 onToggleOpen={onToggleOpen}
                 directHighlightNodeIds={directHighlightNodeIds}
                 indirectHighlightNodeIds={indirectHighlightNodeIds}
                 hasActiveFilter={hasActiveFilter}
-                searchTokens={searchTokens}
-                visibleDepth={visibleDepth}
               />
             </div>
           ))}
         </div>
       )}
-
       {hoveredMetric &&
         createPortal(
           <div
@@ -471,16 +315,6 @@ function HorizontalTreeNode({
   );
 }
 
-// ---------- 工具函数 ----------
-function computeMaxDepth(nodes: any[], d = 0): number {
-  if (!nodes || !nodes.length) return d - 1;
-  let m = d;
-  for (const n of nodes) {
-    if (n.children && n.children.length) m = Math.max(m, computeMaxDepth(n.children, d + 1));
-  }
-  return m;
-}
-
 export function CategoryTree({
   data,
   onSelect,
@@ -499,7 +333,6 @@ export function CategoryTree({
   treeFocusIndex = 0,
   treeMatchedCount = 0,
   focusedTreeNodeId = null,
-  focusTrigger = 0,
   onTreeFocusPrev,
   onTreeFocusNext,
   directHighlightNodeIds = null,
@@ -533,7 +366,6 @@ export function CategoryTree({
   treeFocusIndex?: number;
   treeMatchedCount?: number;
   focusedTreeNodeId?: string | number | null;
-  focusTrigger?: number;
   onTreeFocusPrev?: () => void;
   onTreeFocusNext?: () => void;
   directHighlightNodeIds?: Set<string> | null;
@@ -541,35 +373,12 @@ export function CategoryTree({
   hasActiveFilter?: boolean;
 }) {
   const [scale, setScale] = useState(1);
-  const [searchInput, setSearchInput] = useState("");
 
-  // 展开状态
+  // 展开状态:defaultOpen 是基线,openStates 记录显式覆盖
   const [defaultOpen, setDefaultOpen] = useState(true);
   const [openStates, setOpenStates] = useState<Map<string, boolean>>(new Map());
   const [autoCollapse, setAutoCollapse] = useState(false);
 
-  // dim section 折叠 (按维度名)
-  const [collapsedDims, setCollapsedDims] = useState<Set<string>>(new Set());
-
-  // visibleDepth: -1 表示无限制(全展开),N 表示只显示 0..N-1 层
-  const [visibleDepth, setVisibleDepth] = useState<number>(-1);
-  // 底部按钮的 primary 状态
-  const [primaryAction, setPrimaryAction] = useState<string>("expand-all");
-
-  // 浮动控制条 hint
-  const [hintText, setHintText] = useState<string | null>(null);
-  const hintTimerRef = useRef<number | null>(null);
-  const showHint = useCallback((msg: string) => {
-    setHintText(msg);
-    if (hintTimerRef.current) window.clearTimeout(hintTimerRef.current);
-    hintTimerRef.current = window.setTimeout(() => setHintText(null), 1600);
-  }, []);
-
-  // 滚动容器引用 + dim section 引用 (用于 HUD anchor 与 scroll spy)
-  const scrollContainerRef = useRef<HTMLDivElement>(null);
-  const dimSectionRefs = useRef<Map<string, HTMLDivElement>>(new Map());
-  const [activeDim, setActiveDim] = useState<string | null>(null);
-
   const isOpen = useCallback(
     (id: string) => {
       if (openStates.has(id)) return openStates.get(id)!;
@@ -590,6 +399,7 @@ export function CategoryTree({
     [defaultOpen],
   );
 
+  // 从根路径找到目标节点的所有祖先 id(不含目标自己)
   const findAncestorIds = useMemo(() => {
     return (targetId: string): string[] => {
       if (!data?.children) return [];
@@ -613,79 +423,16 @@ export function CategoryTree({
   const expandAll = () => {
     setDefaultOpen(true);
     setOpenStates(new Map());
-    setVisibleDepth(-1);
-    setPrimaryAction("expand-all");
-    showHint("全部展开");
   };
   const collapseAll = () => {
     setDefaultOpen(false);
     setOpenStates(new Map());
-    setVisibleDepth(-1);
-    setPrimaryAction("collapse-all");
-    showHint("全部收起");
   };
-
-  // 维度内最大深度 (dim 子树最深)
-  const maxDepth = useMemo(() => {
-    if (!data?.children) return 0;
-    return Math.max(0, computeMaxDepth(data.children));
-  }, [data]);
-
-  const stepExpand = () => {
-    const max = maxDepth + 1;
-    let cur = visibleDepth < 0 ? max : visibleDepth;
-    if (cur >= max) {
-      setVisibleDepth(-1);
-      showHint("已全部展开");
-    } else {
-      cur += 1;
-      setVisibleDepth(cur >= max ? -1 : cur);
-      showHint(`展开至第 ${cur} 层`);
-    }
-    setDefaultOpen(true);
-    setOpenStates(new Map());
-    setPrimaryAction("step-expand");
-  };
-  const stepCollapse = () => {
-    let cur = visibleDepth < 0 ? maxDepth + 1 : visibleDepth;
-    if (cur <= 1) {
-      showHint("已收起至一级分类");
-      setVisibleDepth(1);
-    } else {
-      cur -= 1;
-      setVisibleDepth(cur);
-      showHint(`收起至第 ${cur} 层`);
-    }
-    setDefaultOpen(true);
-    setOpenStates(new Map());
-    setPrimaryAction("step-collapse");
-  };
-  const stepCollapseReverse = () => {
-    let cur = visibleDepth < 0 ? maxDepth + 1 : visibleDepth;
-    if (cur <= 1) {
-      showHint("已收起至一级分类");
-      return;
-    }
-    cur -= 1;
-    setVisibleDepth(cur);
-    setDefaultOpen(true);
-    setOpenStates(new Map());
-    setPrimaryAction("step-collapse-rev");
-    showHint(`反向收起至第 ${cur} 层`);
-  };
-  const reset = () => {
-    setDefaultOpen(true);
-    setOpenStates(new Map());
-    setVisibleDepth(-1);
-    setCollapsedDims(new Set());
-    setPrimaryAction("expand-all");
-    showHint("已重置");
-  };
-
   const toggleAutoCollapse = () => {
     setAutoCollapse((v) => {
       const nextAuto = !v;
       if (nextAuto && focusedTreeNodeId != null) {
+        // 切换到自动折叠模式:立刻执行"全折叠 + 展开当前焦点的根通路"
         const ancestors = findAncestorIds(String(focusedTreeNodeId));
         setDefaultOpen(false);
         const next = new Map<string, boolean>();
@@ -696,6 +443,7 @@ export function CategoryTree({
     });
   };
 
+  // 焦点变化时展开路径
   useEffect(() => {
     if (focusedTreeNodeId == null) return;
     const ancestors = findAncestorIds(String(focusedTreeNodeId));
@@ -707,6 +455,7 @@ export function CategoryTree({
       ancestors.forEach((id) => next.set(id, true));
       setOpenStates(next);
     } else {
+      // 只在树处于折叠状态时才加路径;全展开状态下是 no-op
       setOpenStates((prev) => {
         let changed = false;
         const next = new Map(prev);
@@ -722,86 +471,6 @@ export function CategoryTree({
     }
   }, [focusedTreeNodeId, autoCollapse, findAncestorIds]);
 
-  // 搜索 token (逗号分隔)
-  const searchTokens = useMemo(
-    () =>
-      searchInput
-        .split(/[,,]/)
-        .map((s) => s.trim().toLowerCase())
-        .filter(Boolean),
-    [searchInput],
-  );
-
-  // 维度分组
-  const grouped = useMemo(() => {
-    if (!data?.children) return [];
-    const buckets: Record<string, any[]> = {};
-    for (const node of data.children) {
-      const t = node.source_type || "其他";
-      (buckets[t] ||= []).push(node);
-    }
-    const ordered = [...DIM_ORDER, ...Object.keys(buckets).filter((k) => !DIM_ORDER.includes(k))];
-    return ordered
-      .filter((dim) => buckets[dim] && buckets[dim].length)
-      .map((dim) => {
-        const nodes = buckets[dim];
-        const total = nodes.reduce(
-          (s, n) => s + (n.total_element_count ?? n.element_count ?? 0),
-          0,
-        );
-        return { dim, style: DIM_STYLES[dim] || { ...DEFAULT_DIM, label: dim }, nodes, total };
-      });
-  }, [data]);
-
-  // scroll spy 更新当前可见 dim — 用 getBoundingClientRect 计算相对偏移
-  // (offsetTop 取自 offsetParent — 由于外层容器是 position:relative,offsetTop 会包含 toolbar 高度,
-  //  导致 scrollTo 多滚动一段、目标被 toolbar 遮住)
-  useEffect(() => {
-    const sc = scrollContainerRef.current;
-    if (!sc) return;
-    let raf = 0;
-    const update = () => {
-      raf = 0;
-      const scTop = sc.getBoundingClientRect().top;
-      let bestDim: string | null = null;
-      let bestDist = Infinity;
-      for (const [dim, el] of dimSectionRefs.current) {
-        const offset = el.getBoundingClientRect().top - scTop; // 相对滚动容器顶部
-        if (offset <= 60) {
-          const dist = Math.abs(offset);
-          if (dist < bestDist) {
-            bestDist = dist;
-            bestDim = dim;
-          }
-        }
-      }
-      if (!bestDim && grouped.length) bestDim = grouped[0].dim;
-      setActiveDim(bestDim);
-    };
-    update();
-    const onScroll = () => {
-      if (raf) return;
-      raf = requestAnimationFrame(update);
-    };
-    sc.addEventListener("scroll", onScroll, { passive: true });
-    return () => {
-      sc.removeEventListener("scroll", onScroll);
-      if (raf) cancelAnimationFrame(raf);
-    };
-  }, [grouped]);
-
-  const scrollToDim = (dim: string) => {
-    const el = dimSectionRefs.current.get(dim);
-    const sc = scrollContainerRef.current;
-    if (el && sc) {
-      const scRect = sc.getBoundingClientRect();
-      const elRect = el.getBoundingClientRect();
-      // 让目标 dim 顶部刚好出现在滚动视口顶部下方 16px
-      const target = sc.scrollTop + (elRect.top - scRect.top) - 16;
-      sc.scrollTo({ top: Math.max(0, target), behavior: "smooth" });
-    }
-  };
-
   if (!data || !data.children)
     return (
       <div className="bg-slate-100/50 rounded-2xl border border-slate-200 flex flex-col items-center justify-center min-h-[400px] text-slate-400">
@@ -811,406 +480,179 @@ export function CategoryTree({
     );
 
   return (
-    <div className="bg-[#eef0f4] rounded-2xl border border-[#e2e6ed] overflow-hidden flex flex-col h-full relative shadow-sm">
-      {/* 顶部工具栏 */}
-      <div className="px-3 py-2 bg-white border-b border-[#e2e6ed] flex items-center gap-2 shrink-0 shadow-[0_1px_4px_rgba(0,0,0,0.05)]">
-        <FolderTree
-          size={15}
-          className="text-slate-500 shrink-0"
-        />
-        <span className="text-[13px] font-bold text-slate-700 shrink-0">分类树</span>
-
-        <div className="w-px h-5 bg-[#e2e6ed] mx-0.5 shrink-0" />
-
-        {/* 搜索框 */}
-        <div className="relative flex items-center shrink-0">
-          <Search
-            size={13}
-            className="absolute left-2 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none"
-          />
-          <input
-            type="text"
-            value={searchInput}
-            onChange={(e) => setSearchInput(e.target.value)}
-            onKeyDown={(e) => {
-              if (e.key === "Escape") setSearchInput("");
-            }}
-            placeholder="搜索分类… (逗号分隔多关键字)"
-            className={cn(
-              "h-[28px] pl-7 pr-2 rounded-[7px] border border-[#e2e6ed] bg-[#eef0f4]",
-              "text-[12.5px] text-slate-800 placeholder:text-slate-400 outline-none w-[200px]",
-              "transition-[border-color,background,width,box-shadow] duration-200",
-              "focus:border-blue-500 focus:bg-white focus:w-[260px] focus:shadow-[0_0_0_3px_rgba(59,130,246,0.1)]",
-            )}
+    <div className="bg-slate-100/50 rounded-2xl border border-slate-200 overflow-hidden flex flex-col h-full relative">
+      {/* 标题栏 */}
+      <div className="px-4 py-3 font-bold text-slate-700 border-b bg-slate-200/50 flex justify-between shrink-0 items-center">
+        <div className="flex items-center gap-2">
+          <FolderTree
+            size={14}
+            className="text-slate-500"
           />
+          内容树
         </div>
-
-        <div className="flex-1 min-w-0" />
-
-        {filterLabel && (
-          <button
-            type="button"
-            onClick={onClearFilter}
-            className="text-[10px] font-bold px-2 py-1 rounded-md bg-sky-100 text-sky-700 hover:bg-sky-200 transition-colors shrink-0"
-            title="清除筛选"
-          >
-            <span className="max-w-[140px] inline-block truncate align-bottom">{filterLabel}</span> ×
-          </button>
-        )}
-        {treeMatchedCount > 1 && (
-          <div className="flex items-center gap-1 shrink-0">
+        <div className="flex items-center gap-2">
+          {filterLabel && (
             <button
               type="button"
-              onClick={onTreeFocusPrev}
-              disabled={treeFocusIndex === 0}
-              className="p-0.5 rounded text-slate-400 hover:text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
+              onClick={onClearFilter}
+              className="text-[10px] font-bold px-2 py-1 rounded-md bg-sky-100 text-sky-700 hover:bg-sky-200 transition-colors"
             >
-              <ChevronLeft size={13} />
+              {filterLabel} ×
             </button>
-            <span className="text-[10px] font-bold text-slate-500 min-w-[28px] text-center tabular-nums">
-              {treeFocusIndex + 1}/{treeMatchedCount}
-            </span>
-            <button
-              type="button"
-              onClick={onTreeFocusNext}
-              disabled={treeFocusIndex === treeMatchedCount - 1}
-              className="p-0.5 rounded text-slate-400 hover:text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
-            >
-              <ChevronRight size={13} />
-            </button>
-          </div>
-        )}
-
-        {/* 顶部统计 */}
-        <div className="flex items-center gap-2 shrink-0">
-          <div className="inline-flex items-baseline gap-1">
-            <span className="text-[13px] font-bold text-blue-600 tabular-nums">
-              {(totalNodeCount ?? 0).toLocaleString()}
-            </span>
-            <span className="text-[11.5px] text-slate-400">分类</span>
-          </div>
-        </div>
-
-        <div className="w-px h-5 bg-[#e2e6ed] mx-0.5 shrink-0" />
-
-        {/* 自动折叠 + 宽窄模式 */}
-        <button
-          onClick={toggleAutoCollapse}
-          className={cn(
-            "text-[10px] font-bold px-2 py-1 rounded transition-colors shrink-0",
-            autoCollapse ? "bg-blue-100 text-blue-600" : "bg-slate-100 text-slate-600 hover:bg-slate-200",
           )}
-          title="开启后:每次定位到新节点都先全折叠再展开该节点的根路径"
-        >
-          自动折叠
-        </button>
-        {onToggleWideMode && (
+          {treeMatchedCount > 1 && (
+            <div className="flex items-center gap-1">
+              <button
+                type="button"
+                onClick={onTreeFocusPrev}
+                disabled={treeFocusIndex === 0}
+                className="p-0.5 rounded text-slate-400 hover:text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
+              >
+                <ChevronLeft size={13} />
+              </button>
+              <span className="text-[10px] font-bold text-slate-500 min-w-[28px] text-center">
+                {treeFocusIndex + 1}/{treeMatchedCount}
+              </span>
+              <button
+                type="button"
+                onClick={onTreeFocusNext}
+                disabled={treeFocusIndex === treeMatchedCount - 1}
+                className="p-0.5 rounded text-slate-400 hover:text-slate-600 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
+              >
+                <ChevronRight size={13} />
+              </button>
+            </div>
+          )}
+          <button
+            onClick={expandAll}
+            className="text-[10px] font-bold px-2 py-0.5 rounded bg-slate-200 text-slate-500 hover:bg-slate-300 transition-colors"
+          >
+            全展开
+          </button>
+          <button
+            onClick={collapseAll}
+            className="text-[10px] font-bold px-2 py-0.5 rounded bg-slate-200 text-slate-500 hover:bg-slate-300 transition-colors"
+          >
+            全折叠
+          </button>
+          <button
+            onClick={toggleAutoCollapse}
+            className={cn(
+              "text-[10px] font-bold px-2 py-0.5 rounded transition-colors",
+              autoCollapse ? "bg-indigo-100 text-[#3b82f6]" : "bg-slate-200 text-slate-500 hover:bg-slate-300",
+            )}
+            title="开启后:每次定位到新节点都先全折叠再展开该节点的根路径"
+          >
+            自动折叠
+          </button>
           <button
             onClick={onToggleWideMode}
             className={cn(
-              "text-[10px] font-bold px-2 py-1 rounded transition-colors shrink-0",
-              wideMode ? "bg-blue-100 text-blue-600" : "bg-slate-100 text-slate-600 hover:bg-slate-200",
+              "text-[10px] font-bold px-2 py-0.5 rounded transition-colors",
+              wideMode ? "bg-indigo-100 text-[#3b82f6]" : "bg-slate-200 text-slate-500 hover:bg-slate-300",
             )}
           >
             {wideMode ? "宽模式" : "窄模式"}
           </button>
-        )}
+          <span className="text-slate-400">{totalNodeCount ?? 0}</span>
+        </div>
       </div>
 
-      {/* 缩放控件 - 放在右下角避开 HUD */}
-      <div className="absolute bottom-4 right-4 z-50 flex gap-1 bg-white/96 backdrop-blur p-1 rounded-[10px] shadow-[0_2px_8px_rgba(0,0,0,0.08)] border border-[#e2e6ed]">
+      {/* 缩放控件 */}
+      <div className="absolute top-[52px] right-3 z-50 flex gap-1 bg-white/90 backdrop-blur p-1 rounded-lg shadow-sm border border-slate-200">
         <button
           onClick={() => setScale((s) => Math.min(s + 0.15, 3))}
           className="p-1 hover:bg-slate-100 rounded text-slate-600 transition-colors"
           title="放大"
         >
-          <ZoomIn size={13} />
+          <ZoomIn size={14} />
         </button>
         <button
           onClick={() => setScale((s) => Math.max(s - 0.15, 0.3))}
           className="p-1 hover:bg-slate-100 rounded text-slate-600 transition-colors"
           title="缩小"
         >
-          <ZoomOut size={13} />
+          <ZoomOut size={14} />
         </button>
         <button
           onClick={() => setScale(1)}
           className="p-1 hover:bg-slate-100 rounded text-slate-600 transition-colors"
-          title="重置缩放"
+          title="重置"
         >
-          <Maximize size={13} />
+          <Maximize size={14} />
         </button>
       </div>
 
-      {/* 树面板 */}
-      <div
-        ref={scrollContainerRef}
-        className="flex-1 w-full h-full overflow-x-auto overflow-y-auto bg-[#eef0f4] px-5 pt-4 pb-24 custom-scrollbar"
-      >
+      <div className="flex-1 w-full h-full overflow-x-auto overflow-y-auto bg-slate-50/30 p-4 custom-scrollbar">
         <div
-          className="flex flex-col gap-3 select-none min-w-max origin-top-left transition-[zoom] duration-200"
+          className="flex flex-col gap-8 select-none min-w-max pb-8 origin-top-left transition-all duration-200"
           style={{ zoom: scale } as any}
         >
-          {grouped.map(({ dim, style, nodes, total }) => {
-            const isCollapsed = collapsedDims.has(dim);
-            // 把 dim 当成一个 root 节点,name=维度名,children=L1 nodes
-            const dimRoot = {
-              id: `__dim__${dim}`,
-              name: style.label,
-              source_type: dim,
-              total_element_count: total,
-              children: nodes,
-              total_posts_count: 0,
-              element_count: 0,
-              elements: [],
-              __isDim: true,
-              __dimCount: total,
-            };
-            return (
-              <div
-                key={dim}
-                ref={(el) => {
-                  if (el) dimSectionRefs.current.set(dim, el);
-                  else dimSectionRefs.current.delete(dim);
-                }}
-                className="flex flex-row items-start"
-              >
-                {/* dim section header (chip-like, 同时是一个伪 root 节点) */}
-                <button
-                  type="button"
-                  onClick={() =>
-                    setCollapsedDims((prev) => {
-                      const next = new Set(prev);
-                      if (next.has(dim)) next.delete(dim);
-                      else next.add(dim);
-                      return next;
-                    })
-                  }
-                  className={cn(
-                    "node-label inline-flex items-center gap-1.5 cursor-pointer shrink-0",
-                    "bg-white border border-slate-200 rounded-[9px] px-2.5 py-1.5",
-                    "text-[13px] font-bold text-slate-800 whitespace-nowrap select-none",
-                    "shadow-[0_1px_3px_rgba(0,0,0,0.07),0_0_0_1px_rgba(0,0,0,0.04)]",
-                    "transition-[box-shadow,transform,border-color,background,opacity] duration-150",
-                    "hover:-translate-y-px",
-                    "hover:border-[rgba(96,165,250,0.45)]",
-                    "hover:shadow-[0_4px_16px_rgba(96,165,250,0.14),0_0_0_1.5px_rgba(96,165,250,0.3)]",
-                    isCollapsed && "opacity-50",
-                  )}
-                  style={{ borderLeft: `3px solid ${NODE_BORDER}` }}
+          {(() => {
+            const orderKeyWords = ["形式", "实质", "意图"];
+            const groups: Record<string, any[]> = { 形式: [], 实质: [], 意图: [] };
+            data.children.forEach((node: any) => {
+              const type = node.source_type;
+              if (type && groups[type]) groups[type].push(node);
+            });
+
+            return orderKeyWords.map((dimensionName: string) => {
+              const nodesInDimension = groups[dimensionName] || [];
+              if (nodesInDimension.length === 0) return null;
+
+              let color = { bg: "bg-[#3b82f6]", border: "border-[#3b82f6]", text: "text-[#3b82f6]", hex: "#3b82f6" };
+              if (dimensionName === "形式")
+                color = { bg: "bg-[#E3F2FD]", border: "border-[#2196F3]", text: "text-slate-800", hex: "#2196F3" };
+              else if (dimensionName === "实质")
+                color = { bg: "bg-[#FFF3E0]", border: "border-[#FF9800]", text: "text-slate-800", hex: "#FF9800" };
+              else if (dimensionName === "意图")
+                color = { bg: "bg-[#F1F8E9]", border: "border-[#8BC34A]", text: "text-slate-800", hex: "#8BC34A" };
+
+              return (
+                <div
+                  key={dimensionName}
+                  className="flex flex-col"
                 >
-                  <span
+                  <div
                     className={cn(
-                      "node-arrow flex items-center justify-center w-[15px] h-[15px] rounded-[4px]",
-                      "bg-slate-100 border border-slate-200 text-slate-400",
+                      "px-4 py-3 border-l-[6px] text-sm font-bold w-full mb-4",
+                      color.bg,
+                      color.border,
+                      color.text,
                     )}
                   >
-                    {isCollapsed ? (
-                      <ChevronRight size={9} strokeWidth={2} />
-                    ) : (
-                      <ChevronDown size={9} strokeWidth={2} />
-                    )}
-                  </span>
-                  <span className="px-0.5 tracking-wide">{style.label}</span>
-                  <span className={cn(COUNT_BADGE_BASE, COUNT_BADGE_BLUE)}>
-                    {total.toLocaleString()}
-                  </span>
-                </button>
-
-                {/* dim section children (L1 nodes) */}
-                {!isCollapsed && nodes.length > 0 && (
-                  <div className="flex flex-col relative ml-8 shrink-0">
-                    <div className="absolute -left-8 top-[17px] w-8 h-px bg-[#d1d9e6]" />
-                    {nodes.map((subNode: any, subIdx: number) => (
-                      <div
+                    {dimensionName} 维度
+                  </div>
+                  <div className="flex flex-col gap-6 pl-4">
+                    {nodesInDimension.map((subNode: any, subIdx: number) => (
+                      <HorizontalTreeNode
                         key={subNode.id || subIdx}
-                        className={cn(
-                          "relative pl-8 pb-3 flex items-start",
-                          "before:absolute before:left-0 before:top-[17px] before:w-8 before:h-px before:bg-[#d1d9e6]",
-                          "after:absolute after:left-0 after:w-px after:bg-[#d1d9e6]",
-                          "first:after:top-[17px] first:after:bottom-0",
-                          "last:after:top-0 last:after:bottom-[calc(100%-17px)]",
-                          "[&:not(:first-child):not(:last-child)]:after:top-0 [&:not(:first-child):not(:last-child)]:after:bottom-0",
-                          "first:last:after:hidden",
-                        )}
-                      >
-                        <HorizontalTreeNode
-                          node={subNode}
-                          onSelect={onSelect}
-                          onOpenDetail={onOpenDetail}
-                          selectedIds={selectedIds}
-                          depth={1}
-                          highlightLeafNames={highlightLeafNames}
-                          subtreeHighlightNodeIds={subtreeHighlightNodeIds}
-                          sourceNodeIds={sourceNodeIds}
-                          patternNodeIds={patternNodeIds}
-                          nodeMetricsMap={nodeMetricsMap}
-                          dimStyle={style}
-                          focusedTreeNodeId={focusedTreeNodeId}
-                          focusTrigger={focusTrigger}
-                          isOpen={isOpen}
-                          onToggleOpen={onToggleOpen}
-                          directHighlightNodeIds={directHighlightNodeIds}
-                          indirectHighlightNodeIds={indirectHighlightNodeIds}
-                          hasActiveFilter={hasActiveFilter}
-                          searchTokens={searchTokens}
-                          visibleDepth={visibleDepth}
-                        />
-                      </div>
+                        node={subNode}
+                        onSelect={onSelect}
+                        onOpenDetail={onOpenDetail}
+                        selectedIds={selectedIds}
+                        level={1}
+                        highlightLeafNames={highlightLeafNames}
+                        subtreeHighlightNodeIds={subtreeHighlightNodeIds}
+                        sourceNodeIds={sourceNodeIds}
+                        patternNodeIds={patternNodeIds}
+                        nodeMetricsMap={nodeMetricsMap}
+                        dimensionColor={color.hex}
+                        focusedTreeNodeId={focusedTreeNodeId}
+                        isOpen={isOpen}
+                        onToggleOpen={onToggleOpen}
+                        directHighlightNodeIds={directHighlightNodeIds}
+                        indirectHighlightNodeIds={indirectHighlightNodeIds}
+                        hasActiveFilter={hasActiveFilter}
+                      />
                     ))}
                   </div>
-                )}
-                {/* 不渲染 dimRoot 内容 - 仅作为 schema 占位 */}
-                {/* 占位以避免未使用警告 */}
-                <span className="hidden">{dimRoot.id}</span>
-              </div>
-            );
-          })}
-        </div>
-      </div>
-
-      {/* 右侧 HUD anchor */}
-      {grouped.length > 1 && (
-        <div
-          className={cn(
-            "absolute z-40 flex flex-col gap-0.5 items-end",
-            "top-[64px] right-[14px]",
-            "bg-white/92 backdrop-blur-sm border border-[#e2e6ed] rounded-[10px] py-1.5 px-3",
-            "shadow-[0_1px_6px_rgba(0,0,0,0.07)]",
-          )}
-        >
-          {grouped.map(({ dim, style }) => {
-            const isActive = activeDim === dim;
-            return (
-              <button
-                key={dim}
-                type="button"
-                onClick={() => scrollToDim(dim)}
-                className={cn(
-                  "flex items-center gap-2 cursor-pointer py-[3px] rounded select-none transition-colors",
-                  "hover:bg-black/[0.03]",
-                )}
-                title={`跳转到 ${style.label}`}
-              >
-                <span
-                  className={cn(
-                    "text-[12px] tracking-tight whitespace-nowrap transition-colors",
-                    isActive ? "font-semibold text-blue-600" : "font-medium text-slate-400",
-                  )}
-                >
-                  {style.label}
-                </span>
-                <span
-                  className="rounded transition-all"
-                  style={{
-                    height: 2,
-                    width: isActive ? 22 : 14,
-                    background: isActive ? "#2563eb" : "#d1d5db",
-                  }}
-                />
-              </button>
-            );
-          })}
+                </div>
+              );
+            });
+          })()}
         </div>
-      )}
-
-      {/* 浮动底部控制栏 */}
-      <div
-        className={cn(
-          "absolute z-50 flex items-center gap-px p-1 rounded-[14px]",
-          "bottom-4 left-1/2 -translate-x-1/2",
-          "bg-white/96 backdrop-blur-xl border border-[#e2e6ed]",
-          "shadow-[0_4px_20px_rgba(0,0,0,0.11),0_1px_4px_rgba(0,0,0,0.06)]",
-        )}
-      >
-        <CtrlButton
-          icon={<ChevronsDown size={13} strokeWidth={1.8} />}
-          label="全部展开"
-          primary={primaryAction === "expand-all"}
-          onClick={expandAll}
-        />
-        <CtrlButton
-          icon={<ChevronsUp size={13} strokeWidth={1.8} />}
-          label="全部收起"
-          primary={primaryAction === "collapse-all"}
-          onClick={collapseAll}
-        />
-        <CtrlSep />
-        <CtrlButton
-          icon={<ChevronDown size={13} strokeWidth={1.8} />}
-          label="逐层展开"
-          primary={primaryAction === "step-expand"}
-          onClick={stepExpand}
-        />
-        <CtrlButton
-          icon={<ChevronUp size={13} strokeWidth={1.8} />}
-          label="逐层收起"
-          primary={primaryAction === "step-collapse"}
-          onClick={stepCollapse}
-        />
-        <CtrlSep />
-        <CtrlButton
-          icon={<ChevronUp size={13} strokeWidth={1.8} />}
-          label="反向收起"
-          primary={primaryAction === "step-collapse-rev"}
-          onClick={stepCollapseReverse}
-        />
-        <CtrlSep />
-        <CtrlButton
-          icon={<RotateCcw size={13} strokeWidth={1.8} />}
-          label="重置"
-          primary={false}
-          onClick={reset}
-        />
       </div>
-
-      {/* 操作 hint */}
-      {hintText && (
-        <div
-          className={cn(
-            "absolute z-[51] left-1/2 -translate-x-1/2 bottom-[70px]",
-            "px-3.5 py-1.5 rounded-full whitespace-nowrap text-[12px]",
-            "bg-[rgba(24,32,46,0.82)] text-white pointer-events-none",
-          )}
-          style={{ fontFamily: '"JetBrains Mono","SF Mono",Consolas,monospace' }}
-        >
-          {hintText}
-        </div>
-      )}
     </div>
   );
 }
-
-// 浮动控制栏内的按钮和分隔
-function CtrlButton({
-  icon,
-  label,
-  primary,
-  onClick,
-}: {
-  icon: React.ReactNode;
-  label: string;
-  primary?: boolean;
-  onClick?: () => void;
-}) {
-  return (
-    <button
-      type="button"
-      onClick={onClick}
-      className={cn(
-        "inline-flex items-center gap-1.5 px-3 py-[7px] rounded-[10px]",
-        "text-[12.5px] font-medium whitespace-nowrap transition-colors",
-        primary
-          ? "bg-blue-600 text-white hover:bg-blue-700"
-          : "bg-transparent text-slate-500 hover:bg-slate-100 hover:text-slate-800",
-      )}
-    >
-      {icon}
-      <span>{label}</span>
-    </button>
-  );
-}
-
-function CtrlSep() {
-  return <span className="w-px h-[22px] bg-[#e2e6ed] mx-0.5" />;
-}

+ 1 - 1
knowhub/frontend/src/components/dashboard/CategoryTreeNew.tsx

@@ -828,7 +828,7 @@ export function CategoryTreeNew({
           size={15}
           className="text-slate-500 shrink-0"
         />
-        <span className="text-[13px] font-bold text-slate-700 shrink-0">分类树</span>
+        <span className="text-[13px] font-bold text-slate-700 shrink-0">内容树</span>
 
         <div className="w-px h-5 bg-[#e2e6ed] mx-0.5 shrink-0" />