Просмотр исходного кода

style(frontend): 统一代码格式并更新颜色变量

- 将所有单引号替换为双引号以符合项目代码风格
- 将硬编码的 indigo-600 颜色替换为统一的 #3b82f6 颜色变量
- 调整 JSX 格式以提高可读性,包括多行参数和条件渲染
- 保持功能完全不变,仅进行代码风格优化
刘文武 1 месяц назад
Родитель
Сommit
1d70a4ad10

+ 244 - 84
knowhub/frontend/src/components/dashboard/CategoryTree.tsx

@@ -1,7 +1,7 @@
-import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
-import { createPortal } from 'react-dom';
-import { cn } from '../../lib/utils';
-import { ChevronRight, ChevronDown, ChevronLeft, ZoomIn, ZoomOut, Maximize, FolderTree, FileText } from 'lucide-react';
+import { useState, useRef, useEffect, useCallback, useMemo } from "react";
+import { createPortal } from "react-dom";
+import { cn } from "../../lib/utils";
+import { ChevronRight, ChevronDown, ChevronLeft, ZoomIn, ZoomOut, Maximize, FolderTree, FileText } from "lucide-react";
 
 
 interface NodeProps {
 interface NodeProps {
   node: any;
   node: any;
@@ -13,7 +13,17 @@ interface NodeProps {
   subtreeHighlightNodeIds: Set<string> | null;
   subtreeHighlightNodeIds: Set<string> | null;
   sourceNodeIds: Set<string> | null;
   sourceNodeIds: Set<string> | null;
   patternNodeIds: Set<string> | null;
   patternNodeIds: Set<string> | null;
-  nodeMetricsMap: Record<string, { reqCount: number; procCount: number; capCount: number; toolCount: number; nodeCount: number; patternCount: number }>;
+  nodeMetricsMap: Record<
+    string,
+    {
+      reqCount: number;
+      procCount: number;
+      capCount: number;
+      toolCount: number;
+      nodeCount: number;
+      patternCount: number;
+    }
+  >;
   dimensionColor: string; // hex color for the node's dimension
   dimensionColor: string; // hex color for the node's dimension
   focusedTreeNodeId?: string | number | null;
   focusedTreeNodeId?: string | number | null;
   focusTrigger?: number;
   focusTrigger?: number;
@@ -33,21 +43,50 @@ function nodeHasHighlightedLeaf(node: any, highlightLeafNames: Set<string> | nul
   return node.children.some((child: any) => nodeHasHighlightedLeaf(child, highlightLeafNames));
   return node.children.some((child: any) => nodeHasHighlightedLeaf(child, highlightLeafNames));
 }
 }
 
 
-function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level, highlightLeafNames, subtreeHighlightNodeIds, sourceNodeIds, patternNodeIds, nodeMetricsMap, dimensionColor, focusedTreeNodeId = null, focusTrigger = 0, isOpen, onToggleOpen, directHighlightNodeIds, indirectHighlightNodeIds, hasActiveFilter }: NodeProps) {
+function HorizontalTreeNode({
+  node,
+  onSelect,
+  onOpenDetail,
+  selectedIds,
+  level,
+  highlightLeafNames,
+  subtreeHighlightNodeIds,
+  sourceNodeIds,
+  patternNodeIds,
+  nodeMetricsMap,
+  dimensionColor,
+  focusedTreeNodeId = null,
+  focusTrigger = 0,
+  isOpen,
+  onToggleOpen,
+  directHighlightNodeIds,
+  indirectHighlightNodeIds,
+  hasActiveFilter,
+}: NodeProps) {
   const expanded = isOpen(String(node.id));
   const expanded = isOpen(String(node.id));
-  const [hoveredMetric, setHoveredMetric] = useState<null | { key: string; count: number; colorClass: string; x: number; y: number }>(null);
+  const [hoveredMetric, setHoveredMetric] = useState<null | {
+    key: string;
+    count: number;
+    colorClass: string;
+    x: number;
+    y: number;
+  }>(null);
   const nodeRef = useRef<HTMLDivElement>(null);
   const nodeRef = useRef<HTMLDivElement>(null);
   const hasChildren = node.children && node.children.length > 0;
   const hasChildren = node.children && node.children.length > 0;
-  const metrics = nodeMetricsMap[String(node.id)] || { reqCount: 0, procCount: 0, capCount: 0, toolCount: 0, nodeCount: 0, patternCount: 0 };
+  const metrics = nodeMetricsMap[String(node.id)] || {
+    reqCount: 0,
+    procCount: 0,
+    capCount: 0,
+    toolCount: 0,
+    nodeCount: 0,
+    patternCount: 0,
+  };
   let textColor = "text-slate-800";
   let textColor = "text-slate-800";
   if (hasChildren) textColor = cn(textColor, "font-extrabold");
   if (hasChildren) textColor = cn(textColor, "font-extrabold");
 
 
   // Highlight/dim logic for reverse filtering (legacy:仅 inHighlight 用于 subtree fade-out)
   // Highlight/dim logic for reverse filtering (legacy:仅 inHighlight 用于 subtree fade-out)
   const inHighlight = nodeHasHighlightedLeaf(node, highlightLeafNames);
   const inHighlight = nodeHasHighlightedLeaf(node, highlightLeafNames);
-  const isSelected =
-    node.id !== null &&
-    node.id !== undefined &&
-    selectedIds.has(String(node.id));
+  const isSelected = node.id !== null && node.id !== undefined && selectedIds.has(String(node.id));
 
 
   // 4 态分层(叶子 & 非叶一视同仁):非叶节点按 "子树聚合" 判断
   // 4 态分层(叶子 & 非叶一视同仁):非叶节点按 "子树聚合" 判断
   // 聚合集由 CategoryTree 根预计算,passed-in sets 已包含"自己或任一后代命中"的 id
   // 聚合集由 CategoryTree 根预计算,passed-in sets 已包含"自己或任一后代命中"的 id
@@ -72,14 +111,19 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
     prevFocusTriggerRef.current = focusTrigger;
     prevFocusTriggerRef.current = focusTrigger;
 
 
     if (isSelected) {
     if (isSelected) {
-      nodeRef.current.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
+      nodeRef.current.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
     } else if (focusBumped && shouldScrollIntoView) {
     } else if (focusBumped && shouldScrollIntoView) {
-      nodeRef.current.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
+      nodeRef.current.scrollIntoView({ behavior: "smooth", block: "center", inline: "center" });
     }
     }
   }, [isSelected, shouldScrollIntoView, focusTrigger]);
   }, [isSelected, shouldScrollIntoView, focusTrigger]);
 
 
   return (
   return (
-    <div className={cn("flex flex-row items-start transition-opacity duration-200", highlightLeafNames && !inHighlight && "opacity-20")}>
+    <div
+      className={cn(
+        "flex flex-row items-start transition-opacity duration-200",
+        highlightLeafNames && !inHighlight && "opacity-20",
+      )}
+    >
       {/* Node Card */}
       {/* Node Card */}
       <div
       <div
         ref={nodeRef}
         ref={nodeRef}
@@ -88,13 +132,13 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
           isSelected
           isSelected
             ? "ring-2 ring-orange-400 ring-offset-2 border-orange-400 shadow-[0_0_0_2px_rgba(251,146,60,0.16)]"
             ? "ring-2 ring-orange-400 ring-offset-2 border-orange-400 shadow-[0_0_0_2px_rgba(251,146,60,0.16)]"
             : isDirect
             : isDirect
-              // direct:与 RelationCard direct-match 视觉一致
-              ? "border-sky-300 bg-sky-50/70 shadow-[0_0_0_1px_rgba(125,211,252,0.5)]"
+              ? // direct:与 RelationCard direct-match 视觉一致
+                "border-sky-300 bg-sky-50/70 shadow-[0_0_0_1px_rgba(125,211,252,0.5)]"
               : isNoMatch
               : isNoMatch
-                // 无关联:置灰但仍可点击
-                ? "border-slate-200 bg-slate-50 opacity-45 saturate-50"
-                // indirect 或无筛选:默认态
-                : "hover:brightness-95"
+                ? // 无关联:置灰但仍可点击
+                  "border-slate-200 bg-slate-50 opacity-45 saturate-50"
+                : // indirect 或无筛选:默认态
+                  "hover:brightness-95",
         )}
         )}
         onClick={() => onSelect(node)}
         onClick={() => onSelect(node)}
       >
       >
@@ -104,28 +148,76 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
         </div>
         </div>
         <div className="relative flex items-center gap-2 ml-2">
         <div className="relative flex items-center gap-2 ml-2">
           <span
           <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("w-2.5 h-2.5 rounded-full", metrics.patternCount > 0 ? "bg-blue-500 opacity-100" : "opacity-0")}
+            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(
+              "w-2.5 h-2.5 rounded-full",
+              metrics.patternCount > 0 ? "bg-blue-500 opacity-100" : "opacity-0",
+            )}
           />
           />
           <span
           <span
-            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))}
+            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")}
             className={cn("w-2.5 h-2.5 rounded-full", metrics.reqCount > 0 ? "bg-cyan-500" : "opacity-0")}
           />
           />
           <span
           <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))}
+            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")}
             className={cn("w-2.5 h-2.5 rounded-full", metrics.procCount > 0 ? "bg-green-500" : "opacity-0")}
           />
           />
           <span
           <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))}
+            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")}
             className={cn("w-2.5 h-2.5 rounded-full", metrics.capCount > 0 ? "bg-amber-400" : "opacity-0")}
           />
           />
           <span
           <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))}
+            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")}
             className={cn("w-2.5 h-2.5 rounded-full", metrics.toolCount > 0 ? "bg-orange-500" : "opacity-0")}
           />
           />
         </div>
         </div>
@@ -133,7 +225,10 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
         {onOpenDetail && (
         {onOpenDetail && (
           <button
           <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"
             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(); onOpenDetail(node); }}
+            onClick={(e) => {
+              e.stopPropagation();
+              onOpenDetail(node);
+            }}
             title="查看详情"
             title="查看详情"
           >
           >
             <FileText size={13} />
             <FileText size={13} />
@@ -143,9 +238,22 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
         {hasChildren && (
         {hasChildren && (
           <button
           <button
             className="ml-2 px-1 text-slate-400 hover:text-slate-700 focus:outline-none transition-transform"
             className="ml-2 px-1 text-slate-400 hover:text-slate-700 focus:outline-none transition-transform"
-            onClick={(e) => { e.stopPropagation(); onToggleOpen(String(node.id)); }}
+            onClick={(e) => {
+              e.stopPropagation();
+              onToggleOpen(String(node.id));
+            }}
           >
           >
-            {expanded ? <ChevronDown size={14} className="opacity-70" /> : <ChevronRight size={14} className="opacity-70" />}
+            {expanded ? (
+              <ChevronDown
+                size={14}
+                className="opacity-70"
+              />
+            ) : (
+              <ChevronRight
+                size={14}
+                className="opacity-70"
+              />
+            )}
           </button>
           </button>
         )}
         )}
       </div>
       </div>
@@ -164,7 +272,7 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
                 "first:after:top-[17px] first:after:bottom-0",
                 "first:after:top-[17px] first:after:bottom-0",
                 "last:after:top-0 last:after:bottom-[calc(100%-17px)]",
                 "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",
                 "[&:not(:first-child):not(:last-child)]:after:top-0 [&:not(:first-child):not(:last-child)]:after:bottom-0",
-                "first:last:after:hidden"
+                "first:last:after:hidden",
               )}
               )}
             >
             >
               <HorizontalTreeNode
               <HorizontalTreeNode
@@ -190,15 +298,19 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
           ))}
           ))}
         </div>
         </div>
       )}
       )}
-      {hoveredMetric && createPortal(
-        <div
-          className={cn("fixed z-[99999] rounded-lg border shadow-lg min-w-[40px] px-3 py-2 flex items-center justify-center -translate-x-1/2 -translate-y-full", hoveredMetric.colorClass)}
-          style={{ left: hoveredMetric.x, top: hoveredMetric.y }}
-        >
-          <div className="text-sm font-black leading-none text-center">{hoveredMetric.count}</div>
-        </div>,
-        document.body
-      )}
+      {hoveredMetric &&
+        createPortal(
+          <div
+            className={cn(
+              "fixed z-[99999] rounded-lg border shadow-lg min-w-[40px] px-3 py-2 flex items-center justify-center -translate-x-1/2 -translate-y-full",
+              hoveredMetric.colorClass,
+            )}
+            style={{ left: hoveredMetric.x, top: hoveredMetric.y }}
+          >
+            <div className="text-sm font-black leading-none text-center">{hoveredMetric.count}</div>
+          </div>,
+          document.body,
+        )}
     </div>
     </div>
   );
   );
 }
 }
@@ -235,7 +347,17 @@ export function CategoryTree({
   subtreeHighlightNodeIds?: Set<string> | null;
   subtreeHighlightNodeIds?: Set<string> | null;
   sourceNodeIds?: Set<string> | null;
   sourceNodeIds?: Set<string> | null;
   patternNodeIds?: Set<string> | null;
   patternNodeIds?: Set<string> | null;
-  nodeMetricsMap?: Record<string, { reqCount: number; procCount: number; capCount: number; toolCount: number; nodeCount: number; patternCount: number }>;
+  nodeMetricsMap?: Record<
+    string,
+    {
+      reqCount: number;
+      procCount: number;
+      capCount: number;
+      toolCount: number;
+      nodeCount: number;
+      patternCount: number;
+    }
+  >;
   filterLabel?: string | null;
   filterLabel?: string | null;
   onClearFilter?: () => void;
   onClearFilter?: () => void;
   totalNodeCount?: number;
   totalNodeCount?: number;
@@ -257,19 +379,25 @@ export function CategoryTree({
   const [openStates, setOpenStates] = useState<Map<string, boolean>>(new Map());
   const [openStates, setOpenStates] = useState<Map<string, boolean>>(new Map());
   const [autoCollapse, setAutoCollapse] = useState(false);
   const [autoCollapse, setAutoCollapse] = useState(false);
 
 
-  const isOpen = useCallback((id: string) => {
-    if (openStates.has(id)) return openStates.get(id)!;
-    return defaultOpen;
-  }, [openStates, defaultOpen]);
+  const isOpen = useCallback(
+    (id: string) => {
+      if (openStates.has(id)) return openStates.get(id)!;
+      return defaultOpen;
+    },
+    [openStates, defaultOpen],
+  );
 
 
-  const onToggleOpen = useCallback((id: string) => {
-    setOpenStates(prev => {
-      const next = new Map(prev);
-      const current = next.has(id) ? next.get(id)! : defaultOpen;
-      next.set(id, !current);
-      return next;
-    });
-  }, [defaultOpen]);
+  const onToggleOpen = useCallback(
+    (id: string) => {
+      setOpenStates((prev) => {
+        const next = new Map(prev);
+        const current = next.has(id) ? next.get(id)! : defaultOpen;
+        next.set(id, !current);
+        return next;
+      });
+    },
+    [defaultOpen],
+  );
 
 
   // 从根路径找到目标节点的所有祖先 id(不含目标自己)
   // 从根路径找到目标节点的所有祖先 id(不含目标自己)
   const findAncestorIds = useMemo(() => {
   const findAncestorIds = useMemo(() => {
@@ -301,14 +429,14 @@ export function CategoryTree({
     setOpenStates(new Map());
     setOpenStates(new Map());
   };
   };
   const toggleAutoCollapse = () => {
   const toggleAutoCollapse = () => {
-    setAutoCollapse(v => {
+    setAutoCollapse((v) => {
       const nextAuto = !v;
       const nextAuto = !v;
       if (nextAuto && focusedTreeNodeId != null) {
       if (nextAuto && focusedTreeNodeId != null) {
         // 切换到自动折叠模式:立刻执行"全折叠 + 展开当前焦点的根通路"
         // 切换到自动折叠模式:立刻执行"全折叠 + 展开当前焦点的根通路"
         const ancestors = findAncestorIds(String(focusedTreeNodeId));
         const ancestors = findAncestorIds(String(focusedTreeNodeId));
         setDefaultOpen(false);
         setDefaultOpen(false);
         const next = new Map<string, boolean>();
         const next = new Map<string, boolean>();
-        ancestors.forEach(id => next.set(id, true));
+        ancestors.forEach((id) => next.set(id, true));
         setOpenStates(next);
         setOpenStates(next);
       }
       }
       return nextAuto;
       return nextAuto;
@@ -324,35 +452,42 @@ export function CategoryTree({
     if (autoCollapse) {
     if (autoCollapse) {
       setDefaultOpen(false);
       setDefaultOpen(false);
       const next = new Map<string, boolean>();
       const next = new Map<string, boolean>();
-      ancestors.forEach(id => next.set(id, true));
+      ancestors.forEach((id) => next.set(id, true));
       setOpenStates(next);
       setOpenStates(next);
     } else {
     } else {
       // 只在树处于折叠状态时才加路径;全展开状态下是 no-op
       // 只在树处于折叠状态时才加路径;全展开状态下是 no-op
-      setOpenStates(prev => {
+      setOpenStates((prev) => {
         let changed = false;
         let changed = false;
         const next = new Map(prev);
         const next = new Map(prev);
-        ancestors.forEach(id => {
+        ancestors.forEach((id) => {
           const cur = next.has(id) ? next.get(id)! : defaultOpen;
           const cur = next.has(id) ? next.get(id)! : defaultOpen;
-          if (!cur) { next.set(id, true); changed = true; }
+          if (!cur) {
+            next.set(id, true);
+            changed = true;
+          }
         });
         });
         return changed ? next : prev;
         return changed ? next : prev;
       });
       });
     }
     }
   }, [focusedTreeNodeId, autoCollapse, findAncestorIds]);
   }, [focusedTreeNodeId, autoCollapse, findAncestorIds]);
 
 
-  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">
-      <div className="w-8 h-8 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mb-4"></div>
-      加载树形结构中...
-    </div>
-  );
+  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">
+        <div className="w-8 h-8 border-4 border-indigo-200 border-t-indigo-600 rounded-full animate-spin mb-4"></div>
+        加载树形结构中...
+      </div>
+    );
 
 
   return (
   return (
     <div className="bg-slate-100/50 rounded-2xl border border-slate-200 overflow-hidden flex flex-col h-full relative">
     <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="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">
         <div className="flex items-center gap-2">
-          <FolderTree size={14} className="text-slate-500" />
+          <FolderTree
+            size={14}
+            className="text-slate-500"
+          />
           内容树
           内容树
         </div>
         </div>
         <div className="flex items-center gap-2">
         <div className="flex items-center gap-2">
@@ -404,7 +539,7 @@ export function CategoryTree({
             onClick={toggleAutoCollapse}
             onClick={toggleAutoCollapse}
             className={cn(
             className={cn(
               "text-[10px] font-bold px-2 py-0.5 rounded transition-colors",
               "text-[10px] font-bold px-2 py-0.5 rounded transition-colors",
-              autoCollapse ? "bg-indigo-100 text-indigo-600" : "bg-slate-200 text-slate-500 hover:bg-slate-300"
+              autoCollapse ? "bg-indigo-100 text-[#3b82f6]" : "bg-slate-200 text-slate-500 hover:bg-slate-300",
             )}
             )}
             title="开启后:每次定位到新节点都先全折叠再展开该节点的根路径"
             title="开启后:每次定位到新节点都先全折叠再展开该节点的根路径"
           >
           >
@@ -414,10 +549,10 @@ export function CategoryTree({
             onClick={onToggleWideMode}
             onClick={onToggleWideMode}
             className={cn(
             className={cn(
               "text-[10px] font-bold px-2 py-0.5 rounded transition-colors",
               "text-[10px] font-bold px-2 py-0.5 rounded transition-colors",
-              wideMode ? "bg-indigo-100 text-indigo-600" : "bg-slate-200 text-slate-500 hover:bg-slate-300"
+              wideMode ? "bg-indigo-100 text-[#3b82f6]" : "bg-slate-200 text-slate-500 hover:bg-slate-300",
             )}
             )}
           >
           >
-            {wideMode ? '宽模式' : '窄模式'}
+            {wideMode ? "宽模式" : "窄模式"}
           </button>
           </button>
           <span className="text-slate-400">{totalNodeCount ?? 0}</span>
           <span className="text-slate-400">{totalNodeCount ?? 0}</span>
         </div>
         </div>
@@ -425,13 +560,25 @@ export function CategoryTree({
 
 
       {/* 缩放控件 */}
       {/* 缩放控件 */}
       <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">
       <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="放大">
+        <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={14} />
           <ZoomIn size={14} />
         </button>
         </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="缩小">
+        <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={14} />
           <ZoomOut size={14} />
         </button>
         </button>
-        <button onClick={() => setScale(1)} className="p-1 hover:bg-slate-100 rounded text-slate-600 transition-colors" title="重置">
+        <button
+          onClick={() => setScale(1)}
+          className="p-1 hover:bg-slate-100 rounded text-slate-600 transition-colors"
+          title="重置"
+        >
           <Maximize size={14} />
           <Maximize size={14} />
         </button>
         </button>
       </div>
       </div>
@@ -443,7 +590,7 @@ export function CategoryTree({
         >
         >
           {(() => {
           {(() => {
             const orderKeyWords = ["形式", "实质", "意图"];
             const orderKeyWords = ["形式", "实质", "意图"];
-            const groups: Record<string, any[]> = { "形式": [], "实质": [], "意图": [] };
+            const groups: Record<string, any[]> = { 形式: [], 实质: [], 意图: [] };
             data.children.forEach((node: any) => {
             data.children.forEach((node: any) => {
               const type = node.source_type;
               const type = node.source_type;
               if (type && groups[type]) groups[type].push(node);
               if (type && groups[type]) groups[type].push(node);
@@ -453,14 +600,27 @@ export function CategoryTree({
               const nodesInDimension = groups[dimensionName] || [];
               const nodesInDimension = groups[dimensionName] || [];
               if (nodesInDimension.length === 0) return null;
               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' };
+              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 (
               return (
-                <div key={dimensionName} className="flex flex-col">
-                  <div className={cn("px-4 py-3 border-l-[6px] text-sm font-bold w-full mb-4", color.bg, color.border, color.text)}>
+                <div
+                  key={dimensionName}
+                  className="flex flex-col"
+                >
+                  <div
+                    className={cn(
+                      "px-4 py-3 border-l-[6px] text-sm font-bold w-full mb-4",
+                      color.bg,
+                      color.border,
+                      color.text,
+                    )}
+                  >
                     {dimensionName} 维度
                     {dimensionName} 维度
                   </div>
                   </div>
                   <div className="flex flex-col gap-6 pl-4">
                   <div className="flex flex-col gap-6 pl-4">

Разница между файлами не показана из-за своего большого размера
+ 368 - 167
knowhub/frontend/src/components/dashboard/details/DrawerContentComponents.tsx


+ 32 - 20
knowhub/frontend/src/components/layout/VersionSwitcher.tsx

@@ -1,12 +1,12 @@
-import { useState } from 'react';
-import { Database, Check, ChevronDown } from 'lucide-react';
-import { SUPPORTED_VERSIONS, getActiveVersion, setActiveVersion, type DataVersion } from '../../services/api';
-import { cn } from '../../lib/utils';
+import { useState } from "react";
+import { Database, Check, ChevronDown } from "lucide-react";
+import { SUPPORTED_VERSIONS, getActiveVersion, setActiveVersion, type DataVersion } from "../../services/api";
+import { cn } from "../../lib/utils";
 
 
 const VERSION_LABEL: Record<DataVersion, { title: string; desc: string }> = {
 const VERSION_LABEL: Record<DataVersion, { title: string; desc: string }> = {
-  tao_dev:      { title: 'tao_dev',      desc: '原始入库(不去重、LLM raw cap)' },
-  dev_dedup:    { title: 'dev_dedup',    desc: '去重后未抽象(99 concrete strategy)' },
-  dev_abstract: { title: 'dev_abstract', desc: '抽象化后(26 pattern + knowledge)' },
+  tao_dev: { title: "tao_dev", desc: "原始入库(不去重、LLM raw cap)" },
+  dev_dedup: { title: "dev_dedup", desc: "去重后未抽象(99 concrete strategy)" },
+  dev_abstract: { title: "dev_abstract", desc: "抽象化后(26 pattern + knowledge)" },
 };
 };
 
 
 export function VersionSwitcher() {
 export function VersionSwitcher() {
@@ -14,7 +14,10 @@ export function VersionSwitcher() {
   const [current, setCurrent] = useState<DataVersion>(getActiveVersion());
   const [current, setCurrent] = useState<DataVersion>(getActiveVersion());
 
 
   const onPick = (v: DataVersion) => {
   const onPick = (v: DataVersion) => {
-    if (v === current) { setOpen(false); return; }
+    if (v === current) {
+      setOpen(false);
+      return;
+    }
     setActiveVersion(v);
     setActiveVersion(v);
     setCurrent(v);
     setCurrent(v);
     setOpen(false);
     setOpen(false);
@@ -25,22 +28,26 @@ export function VersionSwitcher() {
   return (
   return (
     <div className="relative">
     <div className="relative">
       <button
       <button
-        onClick={() => setOpen(o => !o)}
-        className="h-8 px-3 flex items-center gap-2 rounded-xl bg-slate-50 border border-slate-100 text-xs font-medium text-slate-600 hover:text-indigo-600 hover:border-indigo-100 transition-colors"
+        onClick={() => setOpen((o) => !o)}
+        className="h-8 px-3 flex items-center gap-2 rounded-xl bg-slate-50 border border-slate-100 text-xs font-medium text-slate-600 hover:text-[#3b82f6] hover:border-indigo-100 transition-colors"
         title="切换数据版本"
         title="切换数据版本"
       >
       >
         <Database size={14} />
         <Database size={14} />
         <span className="tabular-nums">{current}</span>
         <span className="tabular-nums">{current}</span>
-        <ChevronDown size={12} className={cn('transition-transform', open && 'rotate-180')} />
+        <ChevronDown
+          size={12}
+          className={cn("transition-transform", open && "rotate-180")}
+        />
       </button>
       </button>
       {open && (
       {open && (
         <>
         <>
-          <div className="fixed inset-0 z-40" onClick={() => setOpen(false)} />
+          <div
+            className="fixed inset-0 z-40"
+            onClick={() => setOpen(false)}
+          />
           <div className="absolute left-0 top-9 z-50 w-72 bg-white rounded-xl shadow-lg shadow-slate-200/60 border border-slate-100 p-1.5">
           <div className="absolute left-0 top-9 z-50 w-72 bg-white rounded-xl shadow-lg shadow-slate-200/60 border border-slate-100 p-1.5">
-            <div className="px-2 py-1.5 text-[10px] font-bold uppercase tracking-wider text-slate-400">
-              数据版本
-            </div>
-            {SUPPORTED_VERSIONS.map(v => {
+            <div className="px-2 py-1.5 text-[10px] font-bold uppercase tracking-wider text-slate-400">数据版本</div>
+            {SUPPORTED_VERSIONS.map((v) => {
               const info = VERSION_LABEL[v];
               const info = VERSION_LABEL[v];
               const active = v === current;
               const active = v === current;
               return (
               return (
@@ -48,15 +55,20 @@ export function VersionSwitcher() {
                   key={v}
                   key={v}
                   onClick={() => onPick(v)}
                   onClick={() => onPick(v)}
                   className={cn(
                   className={cn(
-                    'w-full text-left px-3 py-2 rounded-lg flex items-start gap-2 transition-colors',
-                    active ? 'bg-indigo-50' : 'hover:bg-slate-50'
+                    "w-full text-left px-3 py-2 rounded-lg flex items-start gap-2 transition-colors",
+                    active ? "bg-indigo-50" : "hover:bg-slate-50",
                   )}
                   )}
                 >
                 >
                   <div className="mt-0.5 w-4 h-4 flex items-center justify-center">
                   <div className="mt-0.5 w-4 h-4 flex items-center justify-center">
-                    {active && <Check size={14} className="text-indigo-600" />}
+                    {active && (
+                      <Check
+                        size={14}
+                        className="text-[#3b82f6]"
+                      />
+                    )}
                   </div>
                   </div>
                   <div className="flex-1 min-w-0">
                   <div className="flex-1 min-w-0">
-                    <div className={cn('text-sm font-semibold', active ? 'text-indigo-700' : 'text-slate-700')}>
+                    <div className={cn("text-sm font-semibold", active ? "text-[#3b82f6]" : "text-slate-700")}>
                       {info.title}
                       {info.title}
                     </div>
                     </div>
                     <div className="text-[11px] text-slate-500 mt-0.5">{info.desc}</div>
                     <div className="text-[11px] text-slate-500 mt-0.5">{info.desc}</div>

Разница между файлами не показана из-за своего большого размера
+ 439 - 218
knowhub/frontend/src/pages/Dashboard.tsx


Некоторые файлы не были показаны из-за большого количества измененных файлов