|
@@ -19,6 +19,10 @@ interface NodeProps {
|
|
|
focusTrigger?: number;
|
|
focusTrigger?: number;
|
|
|
isOpen: (nodeId: string) => boolean;
|
|
isOpen: (nodeId: string) => boolean;
|
|
|
onToggleOpen: (nodeId: string) => void;
|
|
onToggleOpen: (nodeId: string) => void;
|
|
|
|
|
+ // 与 RelationCard 4 态视觉对齐所需的输入
|
|
|
|
|
+ directHighlightNodeIds: Set<string> | null;
|
|
|
|
|
+ indirectHighlightNodeIds: Set<string> | null;
|
|
|
|
|
+ hasActiveFilter: boolean;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Returns true if this node or any descendant is in the highlight set
|
|
// Returns true if this node or any descendant is in the highlight set
|
|
@@ -29,7 +33,7 @@ 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 }: 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);
|
|
@@ -38,17 +42,18 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
|
|
|
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
|
|
|
|
|
|
|
+ // Highlight/dim logic for reverse filtering (legacy:仅 inHighlight 用于 subtree fade-out)
|
|
|
const inHighlight = nodeHasHighlightedLeaf(node, highlightLeafNames);
|
|
const inHighlight = nodeHasHighlightedLeaf(node, highlightLeafNames);
|
|
|
- const isLeafHighlighted = highlightLeafNames && highlightLeafNames.has(node.name);
|
|
|
|
|
- const isInSubtreeHighlight = subtreeHighlightNodeIds?.has(String(node.id)) ?? false;
|
|
|
|
|
- const isSourceNode = sourceNodeIds?.has(String(node.id)) ?? false;
|
|
|
|
|
- const isPatternNode = patternNodeIds?.has(String(node.id)) ?? false;
|
|
|
|
|
- const isAssociatedNode = isSourceNode || isPatternNode;
|
|
|
|
|
const isSelected =
|
|
const isSelected =
|
|
|
node.id !== null &&
|
|
node.id !== null &&
|
|
|
node.id !== undefined &&
|
|
node.id !== undefined &&
|
|
|
selectedIds.has(String(node.id));
|
|
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 shouldScrollIntoView =
|
|
const shouldScrollIntoView =
|
|
|
focusedTreeNodeId !== null &&
|
|
focusedTreeNodeId !== null &&
|
|
|
focusedTreeNodeId !== undefined &&
|
|
focusedTreeNodeId !== undefined &&
|
|
@@ -71,10 +76,14 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
|
|
|
"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",
|
|
"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
|
|
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)]"
|
|
|
- : isAssociatedNode || isInSubtreeHighlight || isLeafHighlighted
|
|
|
|
|
- // 与 RelationCard 的 direct-match 样式统一:实线 sky 边 + sky-50 浅底 + 外发光
|
|
|
|
|
|
|
+ : isDirect
|
|
|
|
|
+ // direct:与 RelationCard direct-match 视觉一致
|
|
|
? "border-sky-300 bg-sky-50/70 shadow-[0_0_0_1px_rgba(125,211,252,0.5)]"
|
|
? "border-sky-300 bg-sky-50/70 shadow-[0_0_0_1px_rgba(125,211,252,0.5)]"
|
|
|
- : "hover:brightness-95"
|
|
|
|
|
|
|
+ : isNoMatch
|
|
|
|
|
+ // 无关联:置灰但仍可点击
|
|
|
|
|
+ ? "border-slate-200 bg-slate-50 opacity-45 saturate-50"
|
|
|
|
|
+ // indirect 或无筛选:默认态
|
|
|
|
|
+ : "hover:brightness-95"
|
|
|
)}
|
|
)}
|
|
|
onClick={() => onSelect(node)}
|
|
onClick={() => onSelect(node)}
|
|
|
>
|
|
>
|
|
@@ -162,6 +171,9 @@ function HorizontalTreeNode({ node, onSelect, onOpenDetail, selectedIds, level,
|
|
|
focusedTreeNodeId={focusedTreeNodeId}
|
|
focusedTreeNodeId={focusedTreeNodeId}
|
|
|
isOpen={isOpen}
|
|
isOpen={isOpen}
|
|
|
onToggleOpen={onToggleOpen}
|
|
onToggleOpen={onToggleOpen}
|
|
|
|
|
+ directHighlightNodeIds={directHighlightNodeIds}
|
|
|
|
|
+ indirectHighlightNodeIds={indirectHighlightNodeIds}
|
|
|
|
|
+ hasActiveFilter={hasActiveFilter}
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
@@ -200,6 +212,9 @@ export function CategoryTree({
|
|
|
focusedTreeNodeId = null,
|
|
focusedTreeNodeId = null,
|
|
|
onTreeFocusPrev,
|
|
onTreeFocusPrev,
|
|
|
onTreeFocusNext,
|
|
onTreeFocusNext,
|
|
|
|
|
+ directHighlightNodeIds = null,
|
|
|
|
|
+ indirectHighlightNodeIds = null,
|
|
|
|
|
+ hasActiveFilter = false,
|
|
|
}: {
|
|
}: {
|
|
|
data: any;
|
|
data: any;
|
|
|
onSelect: (node: any) => void;
|
|
onSelect: (node: any) => void;
|
|
@@ -220,6 +235,9 @@ export function CategoryTree({
|
|
|
focusedTreeNodeId?: string | number | null;
|
|
focusedTreeNodeId?: string | number | null;
|
|
|
onTreeFocusPrev?: () => void;
|
|
onTreeFocusPrev?: () => void;
|
|
|
onTreeFocusNext?: () => void;
|
|
onTreeFocusNext?: () => void;
|
|
|
|
|
+ directHighlightNodeIds?: Set<string> | null;
|
|
|
|
|
+ indirectHighlightNodeIds?: Set<string> | null;
|
|
|
|
|
+ hasActiveFilter?: boolean;
|
|
|
}) {
|
|
}) {
|
|
|
const [scale, setScale] = useState(1);
|
|
const [scale, setScale] = useState(1);
|
|
|
|
|
|
|
@@ -452,6 +470,9 @@ export function CategoryTree({
|
|
|
focusedTreeNodeId={focusedTreeNodeId}
|
|
focusedTreeNodeId={focusedTreeNodeId}
|
|
|
isOpen={isOpen}
|
|
isOpen={isOpen}
|
|
|
onToggleOpen={onToggleOpen}
|
|
onToggleOpen={onToggleOpen}
|
|
|
|
|
+ directHighlightNodeIds={directHighlightNodeIds}
|
|
|
|
|
+ indirectHighlightNodeIds={indirectHighlightNodeIds}
|
|
|
|
|
+ hasActiveFilter={hasActiveFilter}
|
|
|
/>
|
|
/>
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|