diff --git a/src/components/EcoTree/EcoTree.css b/src/components/EcoTree/EcoTree.css deleted file mode 100644 index 6ca82e0..0000000 --- a/src/components/EcoTree/EcoTree.css +++ /dev/null @@ -1,219 +0,0 @@ -/** - * EcoTree 组件样式 - React版本 - * 从Vue版本转换而来,移除了Vue特有的样式语法 - */ - -/* Tree View Container Styles */ -.tree-view-container { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: flex-start; - align-items: stretch; - overflow: hidden; - position: relative; -} - -.tree-view-scroll-wrapper { - width: 100%; - flex-grow: 1; - overflow: auto; - /* Enable both horizontal and vertical scrolling */ - position: relative; - min-height: 0; - /* 防止 flex 项目溢出其容器 */ - display: flex; - /* 使用flex布局使子元素能够正确填充 */ - flex-direction: column; - /* 垂直排列 */ -} - -.tree-view-tree-chart { - min-width: 100%; - height: auto !important; - /* 强制使用自动高度 */ - min-height: 100%; - /* 确保至少占满容器 */ - flex: 1 0 auto; - /* 允许伸展但不允许收缩 */ -} - -.tree-view-tree-chart svg { - display: block; - max-width: 100%; - max-height: 100%; - /* 确保SVG不会超出容器 */ - height: auto !important; - /* 强制高度自适应 */ -} - -/* Node and Link Styles */ -.tree-view-link { - fill: none; - stroke: #ccc; - stroke-width: 1.2px; - transition: stroke 0.3s, stroke-width 0.3s; -} - -.tree-view-node-marker { - transition: filter 0.2s ease, r 0.2s ease; - stroke: #fff; - stroke-width: 1.5; -} - -.tree-view-left-marker { - cursor: default; -} - -.tree-view-label-bg { - transition: fill 0.2s ease, stroke 0.2s ease, filter 0.2s ease; -} - -.tree-view-node:hover .tree-view-label-bg { - filter: brightness(1.1); - stroke-width: 1.5; -} - -.tree-view-node-label { - font-family: "Alibaba-PuHuiTi-Regular", sans-serif; - pointer-events: none; - fill: #333; - transition: fill 0.2s ease; -} - -.tree-view-toggle-bg { - transition: filter 0.2s ease, r 0.2s ease; - cursor: pointer; - stroke: #fff; - stroke-width: 1.5; -} - -.tree-view-toggle-bg:hover { - filter: brightness(1.2); - r: 13; -} - -.tree-view-toggle-icon-fo svg { - display: block; - width: 100%; - height: 100%; -} - -.tree-view-color-block { - transition: filter 0.2s ease; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); - rx: 3; - ry: 3; -} - -.tree-view-node-icon { - overflow: visible; - pointer-events: none; -} - -.tree-view-node:hover .tree-view-node-icon img { - transform: scale(1.1); -} - -.depth-0 .tree-view-node-label { - font-weight: bold; -} - -.depth-1 .tree-view-node-label { - font-weight: bold; -} - -/* Loading State Styles */ -.chart-loading { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background-color: #ffffff; - z-index: 10; -} - -.loading-spinner { - width: 50px; - height: 50px; - border: 5px solid #f3f3f3; - border-top: 5px solid #0275f2; - border-radius: 50%; - animation: spin 1s linear infinite; - margin-bottom: 15px; -} - -.loading-text { - font-size: 18px; - color: #0275f2; - font-family: "Alibaba-PuHuiTi-SemiBold", sans-serif; - text-align: center; - padding: 0 20px; -} - -/* Error State Styles */ -.chart-error { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - background-color: rgba(255, 255, 255, 0.9); - z-index: 10; -} - -.error-icon { - width: 60px; - height: 60px; - border-radius: 50%; - background-color: #ff6b6b; - color: white; - display: flex; - justify-content: center; - align-items: center; - font-size: 36px; - font-weight: bold; - margin-bottom: 15px; -} - -.error-text { - font-size: 18px; - color: #666; - margin-bottom: 20px; - font-family: "Alibaba-PuHuiTi-SemiBold", sans-serif; -} - -.retry-button { - padding: 8px 20px; - background-color: #0275f2; - color: white; - border: none; - border-radius: 4px; - font-size: 16px; - cursor: pointer; - transition: background-color 0.3s; -} - -.retry-button:hover { - background-color: #0262d3; -} - -@keyframes spin { - 0% { - transform: rotate(0deg); - } - - 100% { - transform: rotate(360deg); - } -} \ No newline at end of file diff --git a/src/components/EcoTree/EcoTree.jsx b/src/components/EcoTree/EcoTree.jsx deleted file mode 100644 index d5f345d..0000000 --- a/src/components/EcoTree/EcoTree.jsx +++ /dev/null @@ -1,693 +0,0 @@ -import React, { useRef, useEffect, useCallback } from 'react'; -import * as d3 from 'd3'; -import './EcoTree.css'; - -/** - * EcoTree React组件 - 从Vue版本转换而来 - * 使用D3.js绘制可交互的树形图 - */ -const EcoTree = ({ - // Tree data in hierarchical format - treeData, - // Custom icons for nodes at different levels - nodeIcons = {}, - // Initial expanded nodes - initialExpandedNodes = [], - // Whether to expand all nodes initially - expandAll = false, - // Whether to allow clicking on nodes to expand/collapse - expandOnClickNode = false, - // Custom node colors based on depth - nodeColors = {}, - // Custom node sizes based on depth - nodeSizes = {}, - // Custom node text colors based on depth - nodeTextColors = {}, - // Custom node text sizes based on depth - nodeTextSizes = {}, - // Allow customizing background colors - backgroundColor = '#f5f6f9', - // Whether the component is in loading state - isLoading = false, - // Loading state text - loadingText = '正在加载数据...', - // Error state - loadingError = false, - // Error text - errorText = '加载失败,请稍后重试', - // 控制节点之间的水平间距 - horizontalNodeSpace = 320, - // 控制根节点到第一级节点的间距 - rootLevelNodeSpace = 320, - // 是否显示第一级节点的颜色块 - showLevel1ColorBlock = true, - // 控制叶子节点到其父节点的间距 - leafNodeSpace = null, - // 控制不同层级节点之间的间距配置 - levelNodeSpaces = {}, - // 事件回调 - onNodeClick, - onNodeToggle, - onRetry, - onUpdateExpandedNodes -}) => { - const treeContainer = useRef(null); - const canvasContextRef = useRef(null); - const currentHierarchyRootRef = useRef(null); - const nodeStatesRef = useRef(new Map()); - const resizeObserverRef = useRef(null); - - // Helper Functions - const getNodeColor = useCallback((depth) => { - if (nodeColors && nodeColors[depth] !== undefined) { - return nodeColors[depth]; - } - - // Default colors - switch (depth) { - case 0: return '#cfedff'; // Root - Light Blue - case 1: return '#43a047'; // Level 1 - Green - case 2: return '#fb8c00'; // Level 2 - Orange - case 3: return '#8e24aa'; // Level 3 - Purple - default: return '#a0a0a0'; // Level 4+ - Gray - } - }, [nodeColors]); - - const getNodeSize = useCallback((depth) => { - if (nodeSizes && nodeSizes[depth] !== undefined) { - return nodeSizes[depth]; - } - - // Default sizes - switch (depth) { - case 0: return 8; - case 1: return 7; - case 2: return 6; - default: return 5; - } - }, [nodeSizes]); - - const getNodeTextColor = useCallback((depth) => { - if (nodeTextColors && nodeTextColors[depth] !== undefined) { - return nodeTextColors[depth]; - } - return depth > 0 ? '#ffffff' : '#4c5767'; - }, [nodeTextColors]); - - const getNodeTextSize = useCallback((depth) => { - if (nodeTextSizes && nodeTextSizes[depth] !== undefined) { - return nodeTextSizes[depth]; - } - - if (depth === 0) { - return '20px'; - } - return ['19px', '18px', '16px'][depth - 1] || '16px'; - }, [nodeTextSizes]); - - const safeNumber = useCallback((value, fallback = 0) => { - return (value === undefined || isNaN(value)) ? fallback : value; - }, []); - - // Text Processing Functions - const getTextWidth = useCallback((text, fontStyle) => { - if (!canvasContextRef.current) { - const canvas = document.createElement('canvas'); - canvasContextRef.current = canvas.getContext('2d'); - } - canvasContextRef.current.font = fontStyle; - return canvasContextRef.current.measureText(text || '').width; - }, []); - - const truncateText = useCallback((text, maxWidth, fontStyle) => { - if (!text) return ''; - - if (!canvasContextRef.current) { - const canvas = document.createElement('canvas'); - canvasContextRef.current = canvas.getContext('2d'); - } - - canvasContextRef.current.font = fontStyle; - const ellipsis = '...'; - const ellipsisWidth = canvasContextRef.current.measureText(ellipsis).width; - - if (canvasContextRef.current.measureText(text).width <= maxWidth) { - return text; - } - - const targetWidth = maxWidth - ellipsisWidth; - let truncatedText = text; - - while (truncatedText.length > 0) { - truncatedText = truncatedText.slice(0, -1); - if (canvasContextRef.current.measureText(truncatedText).width <= targetWidth) { - return truncatedText + ellipsis; - } - } - - return ellipsis; - }, []); - - // Main render function - const renderTree = useCallback(() => { - if (!treeContainer.current || !currentHierarchyRootRef.current) return; - - try { - // Clear existing visualization - d3.select(treeContainer.current).html(''); - - // Constants for layout - const rectHeight = 36; - const textPaddingLeft = 44; - const textPaddingRight = 28; - - const containerWidth = treeContainer.current.clientWidth; - - // Icon dimensions - const iconWidth = 30; - const iconHeight = 30; - - // Toggle button dimensions - const toggleButtonRadius = 12; - const toggleButtonGap = 10; - - // Calculate node widths and store in Map - const nodeVisualWidths = new Map(); - - // Apply expanded/collapsed state - currentHierarchyRootRef.current.descendants().forEach(node => { - const nodeId = node.id || node.data.code; - const state = nodeId ? nodeStatesRef.current.get(nodeId) : undefined; - const shouldBeExpanded = state !== undefined - ? state.expanded - : (node.depth <= 1 || expandAll); - - if (node.depth > 0) { - if (!shouldBeExpanded) { - if (node.children) { - node._children = node.children; - node.children = null; - } - } else { - if (node._children) { - node.children = node._children; - node._children = null; - } - } - } - }); - - // Calculate tree layout - const treeLayout = d3.tree().nodeSize([60, horizontalNodeSpace]); - treeLayout(currentHierarchyRootRef.current); - - // Adjust node positions - currentHierarchyRootRef.current.descendants().forEach(node => { - if (node.parent) { - node.originalY = node.y; - node.y += 20; - - // Handle level spacing - let levelSpacingApplied = false; - if (levelNodeSpaces && Object.keys(levelNodeSpaces).length > 0) { - const levelKey = `${node.parent.depth}-${node.depth}`; - if (levelNodeSpaces[levelKey] !== undefined) { - if (node.parent.depth === 0) { - const rootOuterSize = Math.max(95 * (safeNumber(getNodeSize(0), 8) / 8), 75); - node.y = rootOuterSize + levelNodeSpaces[levelKey]; - levelSpacingApplied = true; - } else { - const parentY = node.parent.y; - const parentId = node.parent.id || node.parent.data.code; - const parentWidth = nodeVisualWidths.get(parentId) || node.parent._calculatedRectWidth || 0; - const spacingStartY = parentY + parentWidth; - node.y = spacingStartY + levelNodeSpaces[levelKey]; - levelSpacingApplied = true; - } - } - } - - if (!levelSpacingApplied) { - if (node.parent.depth === 0 && node.depth === 1) { - const rootOuterSize = Math.max(95 * (safeNumber(getNodeSize(0), 8) / 8), 75); - node.y = Math.max(rootLevelNodeSpace, rootOuterSize); - } - } - - // Handle leaf nodes - const isLeafNode = !node.children && !node._children; - if (leafNodeSpace !== null && isLeafNode) { - if (node.parent) { - const parentY = node.parent.y; - const parentId = node.parent.id || node.parent.data.code; - const parentWidth = nodeVisualWidths.get(parentId) || node.parent._calculatedRectWidth || 0; - const minSpacing = 30; - const spacing = Math.max(leafNodeSpace, minSpacing); - node.y = parentY + parentWidth + spacing; - } - } - } - }); - - // Filter visible nodes - const visibleNodes = currentHierarchyRootRef.current.descendants().filter(node => { - let current = node; - while (current.parent) { - if (current.parent.children?.indexOf(current) === -1) { - return false; - } - current = current.parent; - } - return true; - }); - - // Calculate node widths - visibleNodes.forEach(node => { - const fontSize = getNodeTextSize(node.depth); - const fontWeight = node.depth < 2 ? 'bold' : 'normal'; - const fontFamily = fontWeight === 'bold' - ? 'Alibaba-PuHuiTi-SemiBold, sans-serif' - : 'Alibaba-PuHuiTi-Regular, sans-serif'; - const fontStyle = `${fontWeight} ${fontSize} ${fontFamily}`; - - const actualTextWidth = getTextWidth(node.data.name || '', fontStyle); - const hasColorBlock = node.depth === 1; - const colorBlockWidth = hasColorBlock ? 10 : 0; - const colorBlockPadding = hasColorBlock ? 5 : 0; - - const calculatedWidth = textPaddingLeft + actualTextWidth + textPaddingRight + colorBlockWidth + colorBlockPadding; - - node._calculatedRectWidth = calculatedWidth; - node._textAvailableWidth = calculatedWidth - textPaddingLeft - (node.depth === 1 ? 10 + 5 : 0) - textPaddingRight + 10; - - const nodeId = node.id || node.data.code; - if (nodeId) { - nodeVisualWidths.set(nodeId, calculatedWidth); - } - }); - - // Calculate boundaries - let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; - - visibleNodes.forEach(node => { - minX = Math.min(minX, node.x); - maxX = Math.max(maxX, node.x); - minY = Math.min(minY, node.y); - maxY = Math.max(maxY, node.y); - maxY = Math.max(maxY, node.y + (nodeVisualWidths.get(node.id || node.data.code) || node._calculatedRectWidth || 0) + 30); - }); - - // Add padding - const padding = { top: 100, right: 20, bottom: 100, left: 120 }; - - // Calculate SVG dimensions - const realWidth = maxY - minY + padding.left + padding.right; - const realHeight = maxX - minX + padding.top + padding.bottom; - - const containerHeight = treeContainer.current.clientHeight; - const containerAvailableWidth = containerWidth - 20; - let scaleFactor = 1; - let needsScaling = false; - - if (realWidth > containerAvailableWidth) { - scaleFactor = containerAvailableWidth / realWidth; - needsScaling = true; - } - - const finalHeight = realHeight * (needsScaling ? scaleFactor : 1); - const useFullHeight = finalHeight < containerHeight; - - // Create SVG - const svg = d3.select(treeContainer.current) - .append('svg') - .attr('width', '100%') - .attr('height', useFullHeight ? '100%' : finalHeight) - .attr('viewBox', `0 0 ${realWidth} ${realHeight}`) - .attr('preserveAspectRatio', 'xMinYMid meet') - .style('display', 'block') - .style('max-height', '100%'); - - // Add gradients - const defs = svg.append('defs'); - - const incrementGradient = defs.append('linearGradient') - .attr('id', 'increment-gradient') - .attr('x1', '0%').attr('y1', '0%') - .attr('x2', '100%').attr('y2', '0%'); - incrementGradient.append('stop').attr('offset', '0%').attr('stop-color', '#E3F5FF'); - incrementGradient.append('stop').attr('offset', '100%').attr('stop-color', '#FF8989'); - - const stockGradient = defs.append('linearGradient') - .attr('id', 'stock-gradient') - .attr('x1', '0%').attr('y1', '0%') - .attr('x2', '100%').attr('y2', '0%'); - stockGradient.append('stop').attr('offset', '0%').attr('stop-color', '#E3F5FF'); - stockGradient.append('stop').attr('offset', '100%').attr('stop-color', '#7ABAFF'); - - const declineGradient = defs.append('linearGradient') - .attr('id', 'decline-gradient') - .attr('x1', '0%').attr('y1', '0%') - .attr('x2', '100%').attr('y2', '0%'); - declineGradient.append('stop').attr('offset', '0%').attr('stop-color', '#E3F5FF'); - declineGradient.append('stop').attr('offset', '100%').attr('stop-color', '#FFBD7F'); - - // Main group - const mainGroup = svg.append('g') - .attr('transform', `translate(${padding.left - minY}, ${padding.top - minX})`); - - // Draw links - mainGroup.selectAll('.tree-view-link') - .data(visibleNodes.filter(d => d.parent)) - .enter().append('path') - .attr('class', 'tree-view-link') - .attr('d', d => { - const parent = d.parent; - const sourceX = parent.x; - const targetX = d.x; - const targetY = d.y; - let sourceY; - - if (parent.depth === 0) { - sourceY = parent.y + 95; - } else { - const parentId = parent.id || parent.data.code; - const parentWidth = safeNumber(nodeVisualWidths.get(parentId) || parent._calculatedRectWidth || 0, 0); - sourceY = parent.y + parentWidth + toggleButtonGap + toggleButtonRadius; - } - - return `M ${sourceY},${sourceX} - C ${(sourceY + targetY) / 2},${sourceX} - ${(sourceY + targetY) / 2},${targetX} - ${targetY},${targetX}`; - }) - .attr('stroke', '#ccc') - .attr('stroke-width', '1.2px') - .attr('fill', 'none'); - - // Draw nodes - const node = mainGroup.selectAll('.tree-view-node') - .data(visibleNodes, d => d.id || d.data.code || `${d.depth}-${d.data.name}`) - .enter().append('g') - .attr('class', d => `tree-view-node depth-${d.depth}`) - .attr('transform', d => `translate(${d.y}, ${d.x})`); - - // Root nodes - const rootNodeGroup = node.filter(d => d.depth === 0); - - // Root outer circle - rootNodeGroup.append('circle') - .attr('class', 'tree-view-root-outer') - .attr('r', d => { - const baseSize = 95; - const configSize = safeNumber(getNodeSize(0), 8); - return Math.max(baseSize * (configSize / 8), 75); - }) - .attr('fill', backgroundColor) - .attr('cx', 0) - .attr('cy', 0); - - // Root inner circle - rootNodeGroup.append('circle') - .attr('class', 'tree-view-root-inner') - .attr('r', d => { - const baseSize = 75; - const configSize = safeNumber(getNodeSize(0), 8); - return Math.max(baseSize * (configSize / 8), 60); - }) - .attr('fill', d => getNodeColor(0)) - .attr('cx', 0) - .attr('cy', 0); - - // Root node text - rootNodeGroup.append('foreignObject') - .attr('x', -60) - .attr('y', -25) - .attr('width', 120) - .attr('height', 50) - .attr('pointer-events', 'none') - .html(d => { - const name = d.data.name || ''; - const textColor = getNodeTextColor(d.depth); - const fontSize = getNodeTextSize(d.depth); - return `