chenlei 2 тижнів тому
батько
коміт
e475297486
1 змінених файлів з 297 додано та 168 видалено
  1. 297 168
      src/pages/A9knowlege/index.tsx

+ 297 - 168
src/pages/A9knowlege/index.tsx

@@ -21,6 +21,102 @@ const levelImages = [level1, level1, level1, level2, level3, level4, level5, lev
 const getLevelImage = (depth: number) => levelImages[Math.min(depth, 5)]
 const getLevelImage = (depth: number) => levelImages[Math.min(depth, 5)]
 const getLevelSize = (depth: number) => (depth <= 2 ? 60 : 40)
 const getLevelSize = (depth: number) => (depth <= 2 ? 60 : 40)
 
 
+// 节点样式配置
+interface NodeStyleConfig {
+  fontColor: string
+  fontSize: number
+  fontWeight: 'bold' | 'normal'
+  lineColor: string
+}
+
+const getNodeStyleByDepth = (depth: number): NodeStyleConfig => {
+  if (depth === 3) {
+    return {
+      fontColor: '#FF9807',
+      fontSize: 16,
+      fontWeight: 'normal',
+      lineColor: '#FF9807'
+    }
+  } else if (depth >= 4) {
+    return {
+      fontColor: '#D1C9B2',
+      fontSize: 14,
+      fontWeight: 'normal',
+      lineColor: '#D1C9B2'
+    }
+  }
+  return {
+    fontColor: '#FFE9B6',
+    fontSize: 16,
+    fontWeight: 'bold',
+    lineColor: '#FFE9B6'
+  }
+}
+
+// 从节点 category 中提取 depth
+const getDepthFromCategory = (category: string): number => {
+  const match = (category || '').match(/level(\d+)/)
+  return match ? Number(match[1]) : 0
+}
+
+// 获取图表中所有节点的位置信息
+const getNodePositions = (chartInstance: any): Map<string, { x?: number; y?: number }> => {
+  const nodePositionMap = new Map<string, { x?: number; y?: number }>()
+  if (!chartInstance) return nodePositionMap
+
+  const currentOption = chartInstance.getOption()
+  const currentSeries = currentOption.series?.[0]
+  const currentNodes = currentSeries?.data || []
+
+  currentNodes.forEach((node: any) => {
+    if (node.id && (node.x !== undefined || node.y !== undefined)) {
+      nodePositionMap.set(node.id, { x: node.x, y: node.y })
+    }
+  })
+
+  return nodePositionMap
+}
+
+// 创建节点配置
+interface CreateNodeConfigOptions {
+  node: any
+  position?: { x?: number; y?: number }
+  opacity?: number
+  fixed?: boolean
+}
+
+const createNodeConfig = (options: CreateNodeConfigOptions) => {
+  const { node, position = {}, opacity = 1, fixed = true } = options
+  const depth = getDepthFromCategory(node.category || '')
+  const style = getNodeStyleByDepth(depth)
+
+  return {
+    ...node,
+    ...position,
+    fixed,
+    symbol: node.symbol,
+    symbolSize: node.symbolSize,
+    itemStyle: {
+      color: style.fontColor,
+      opacity
+    },
+    label: {
+      show: true,
+      formatter: node.name,
+      fontSize: style.fontSize,
+      fontWeight: style.fontWeight,
+      color: style.fontColor,
+      position: 'bottom',
+      distance: 6,
+      align: 'center',
+      textShadowColor: 'rgba(0, 0, 0, 0.5)',
+      textShadowBlur: 4,
+      textShadowOffsetX: 0,
+      textShadowOffsetY: 0
+    }
+  }
+}
+
 const transformData = (data: any[]): any[] => {
 const transformData = (data: any[]): any[] => {
   return data.map(item => ({
   return data.map(item => ({
     ...item,
     ...item,
@@ -40,9 +136,26 @@ const loadImage = (src: string): Promise<HTMLImageElement> => {
 }
 }
 
 
 // 缩放区间和默认值(与 ECharts 的 scaleLimit / zoom 保持一致)
 // 缩放区间和默认值(与 ECharts 的 scaleLimit / zoom 保持一致)
-const SCALE_MIN = 0.2
+const SCALE_MIN = 0.1
 const SCALE_MAX = 1.2
 const SCALE_MAX = 1.2
 const DEFAULT_ZOOM = 0.5
 const DEFAULT_ZOOM = 0.5
+const IS_PC = window.innerWidth > 1024
+
+// Force 布局配置
+const FORCE_LAYOUT_CONFIG = {
+  repulsion: 1500,
+  edgeLength: [80, 80],
+  gravity: 0.05,
+  layoutAnimation: false
+}
+
+// 禁用布局的配置(用于保持节点位置)
+const DISABLED_FORCE_CONFIG = {
+  initLayout: null,
+  friction: 0,
+  repulsion: 0,
+  gravity: 0
+}
 
 
 const createCombinedSymbol = async (
 const createCombinedSymbol = async (
   levelImageSrc: string,
   levelImageSrc: string,
@@ -142,7 +255,7 @@ function A9knowlege() {
       const category = `level${depth}`
       const category = `level${depth}`
       const symbolSize = getLevelSize(depth)
       const symbolSize = getLevelSize(depth)
       const symbol = `image://${getLevelImage(depth)}`
       const symbol = `image://${getLevelImage(depth)}`
-      // 保存 thumb 信息用于后续处理
+
       nodes.push({
       nodes.push({
         id,
         id,
         name: item.label,
         name: item.label,
@@ -153,9 +266,14 @@ function A9knowlege() {
         depth,
         depth,
         thumb: item.thumb ? baseUrl + item.thumb : null
         thumb: item.thumb ? baseUrl + item.thumb : null
       })
       })
-      if (parentId) links.push({ source: parentId, target: id, targetDepth: depth })
-      if (Array.isArray(item.children))
+
+      if (parentId) {
+        links.push({ source: parentId, target: id, targetDepth: depth })
+      }
+
+      if (Array.isArray(item.children)) {
         item.children.forEach((child: any) => traverse(child, id, depth + 1))
         item.children.forEach((child: any) => traverse(child, id, depth + 1))
+      }
     }
     }
 
 
     data.forEach((root: any) => traverse(root, null, 0))
     data.forEach((root: any) => traverse(root, null, 0))
@@ -170,12 +288,14 @@ function A9knowlege() {
     await Promise.all(thumbPromises)
     await Promise.all(thumbPromises)
 
 
     const maxDepth = nodes.reduce((m, n) => {
     const maxDepth = nodes.reduce((m, n) => {
-      const match = (n.category || '').match(/level(\d+)/)
-      const d = match ? Number(match[1]) : 0
+      const d = getDepthFromCategory(n.category || '')
       return Math.max(m, d)
       return Math.max(m, d)
     }, 0)
     }, 0)
-    const categories: any[] = []
-    for (let i = 0; i <= maxDepth; i++) categories.push({ name: `level${i}` })
+
+    const categories = Array.from({ length: maxDepth + 1 }, (_, i) => ({
+      name: `level${i}`
+    }))
+
     return { nodes, links, categories }
     return { nodes, links, categories }
   }
   }
 
 
@@ -208,87 +328,41 @@ function A9knowlege() {
           name: 'KnowledgeGraph',
           name: 'KnowledgeGraph',
           type: 'graph',
           type: 'graph',
           layout: 'force',
           layout: 'force',
-          data: graph.nodes.map((n: any) => {
-            const match = (n.category || '').match(/level(\d+)/)
-            const depth = match ? Number(match[1]) : 0
-            let fontColor = '#FFE9B6'
-            let fontSize = 16
-            let fontWeight: 'bold' | 'normal' = 'bold'
-            if (depth === 3) {
-              fontColor = '#FF9807'
-              fontWeight = 'normal'
-            } else if (depth >= 4) {
-              fontColor = '#D1C9B2'
-              fontSize = 14
-              fontWeight = 'normal'
-            }
-            return {
-              ...n,
-              symbol: n.symbol,
-              symbolSize: n.symbolSize,
-              itemStyle: {
-                color: fontColor
-              },
-              label: {
-                show: true,
-                formatter: n.name,
-                fontSize,
-                fontWeight,
-                color: fontColor,
-                position: 'bottom',
-                distance: 6,
-                align: 'center',
-                textShadowColor: 'rgba(0, 0, 0, 0.5)',
-                textShadowBlur: 4,
-                textShadowOffsetX: 0,
-                textShadowOffsetY: 0
-              }
-            }
-          }),
+          data: graph.nodes.map((n: any) => createNodeConfig({ node: n, fixed: false })),
           links: graph.links.map((link: any) => {
           links: graph.links.map((link: any) => {
-            const depth = link.targetDepth
-            let lineColor = '#FFE9B6'
-            if (depth === 3) {
-              lineColor = '#FF9807'
-            } else if (depth >= 4) {
-              lineColor = '#D1C9B2'
-            }
+            const style = getNodeStyleByDepth(link.targetDepth)
             return {
             return {
               ...link,
               ...link,
               lineStyle: {
               lineStyle: {
-                color: lineColor
+                color: style.lineColor
               }
               }
             }
             }
           }),
           }),
           categories: graph.categories,
           categories: graph.categories,
-          roam: 'move',
+          roam: IS_PC ? true : 'move',
           zoom: DEFAULT_ZOOM,
           zoom: DEFAULT_ZOOM,
           scaleLimit: {
           scaleLimit: {
             min: SCALE_MIN,
             min: SCALE_MIN,
             max: SCALE_MAX
             max: SCALE_MAX
           },
           },
           label: { position: 'bottom', distance: 6, align: 'center' },
           label: { position: 'bottom', distance: 6, align: 'center' },
-          focusNodeAdjacency: true,
-          emphasis: {
-            focus: 'adjacency',
-            lineStyle: {
-              width: 3,
-              opacity: 1
-            }
-          },
+          // emphasis: {
+          //   focus: 'adjacency',
+          //   lineStyle: {
+          //     width: 3,
+          //     opacity: 1
+          //   }
+          // },
           // force 布局参数:调大 repulsion 让斥力更明显
           // force 布局参数:调大 repulsion 让斥力更明显
           // 降低 gravity 避免被拉回中心过紧
           // 降低 gravity 避免被拉回中心过紧
-          force: {
-            repulsion: 900,
-            edgeLength: [80, 80],
-            gravity: 0.05
-          },
+          force: FORCE_LAYOUT_CONFIG,
           // 连线为直线:curveness 设为 0
           // 连线为直线:curveness 设为 0
           lineStyle: { curveness: 0 },
           lineStyle: { curveness: 0 },
           edgeLabel: { show: false },
           edgeLabel: { show: false },
           edgeSymbol: ['none', 'none']
           edgeSymbol: ['none', 'none']
         }
         }
-      ]
+      ],
+      animationDurationUpdate: 0
     }
     }
 
 
     myChart.setOption(option, true)
     myChart.setOption(option, true)
@@ -296,32 +370,62 @@ function A9knowlege() {
     // 初始化滑块位置
     // 初始化滑块位置
     setZoomPercent((DEFAULT_ZOOM - SCALE_MIN) / (SCALE_MAX - SCALE_MIN))
     setZoomPercent((DEFAULT_ZOOM - SCALE_MIN) / (SCALE_MAX - SCALE_MIN))
 
 
-    const keepHighlight = () => {
-      if (currentId.current && graphRef.current) {
-        const graph = graphRef.current
-        const nodeIndex = graph.nodes.findIndex((n: any) => n.raw && n.raw.id === currentId.current)
-        if (nodeIndex !== -1) {
-          // 重新高亮当前节点
-          myChart.dispatchAction({
-            type: 'highlight',
-            dataIndex: nodeIndex
-          })
-        }
-      } else {
-        myChart.dispatchAction({
-          type: 'downplay'
+    // 更新图表节点和连线配置的通用函数
+    const updateChartNodesAndLinks = (
+      nodes: any[],
+      links: any[],
+      opacityMap?: Map<string, number>
+    ) => {
+      const nodePositionMap = getNodePositions(myChart)
+      const updatedNodes = nodes.map((n: any) => {
+        const position = nodePositionMap.get(n.id) || {}
+        const opacity = opacityMap?.get(n.id) ?? 1
+        return createNodeConfig({
+          node: n,
+          position,
+          opacity
         })
         })
-      }
+      })
+
+      const updatedLinks = links.map((link: any) => {
+        const linkKey = `${link.source}-${link.target}`
+        const reverseKey = `${link.target}-${link.source}`
+        // 如果没有提供 opacityMap,默认透明度为 1(重置状态)
+        // 如果提供了 opacityMap,检查是否有关联的连线
+        const opacity = opacityMap
+          ? opacityMap.get(linkKey) ?? opacityMap.get(reverseKey) ?? 0.1
+          : 1
+        return {
+          ...link,
+          lineStyle: {
+            ...link.lineStyle,
+            opacity
+          }
+        }
+      })
+
+      myChart.setOption(
+        {
+          series: [
+            {
+              data: updatedNodes,
+              links: updatedLinks,
+              force: DISABLED_FORCE_CONFIG
+            }
+          ]
+        },
+        { notMerge: false }
+      )
+    }
+
+    // 恢复所有节点和连线的高亮状态(取消高亮)
+    const resetHighlight = () => {
+      updateChartNodesAndLinks(graph.nodes, graph.links)
     }
     }
 
 
     myChart.on('click', function (params: any) {
     myChart.on('click', function (params: any) {
       if (!params || !params.data) return
       if (!params || !params.data) return
 
 
-      // 取消所有高亮
-      myChart.dispatchAction({
-        type: 'downplay'
-      })
-
       if (params.dataType === 'edge') {
       if (params.dataType === 'edge') {
         // 关系线
         // 关系线
         const edge = params.data
         const edge = params.data
@@ -330,75 +434,102 @@ function A9knowlege() {
 
 
         if (sourceIndex !== -1 && targetIndex !== -1) {
         if (sourceIndex !== -1 && targetIndex !== -1) {
           // 高亮源节点
           // 高亮源节点
-          myChart.dispatchAction({
-            type: 'highlight',
-            dataIndex: sourceIndex
-          })
-          // 高亮目标节点
-          setTimeout(() => {
-            myChart.dispatchAction({
-              type: 'highlight',
-              dataIndex: targetIndex
-            })
-          }, 10)
         }
         }
       } else {
       } else {
         // 节点
         // 节点
-        myChart.dispatchAction({
-          type: 'highlight',
-          dataIndex: params.dataIndex
+        const node = params.data
+        const targetId = params.data.id
+        const related = new Set([targetId])
+
+        // 创建节点和连线的透明度映射
+        const opacityMap = new Map<string, number>()
+
+        // 标记关联元素并设置透明度映射
+        graph.links.forEach((link: any) => {
+          if (link.source === targetId || link.target === targetId) {
+            related.add(link.source)
+            related.add(link.target)
+            // 设置关联连线的透明度
+            const linkKey = `${link.source}-${link.target}`
+            const reverseKey = `${link.target}-${link.source}`
+            opacityMap.set(linkKey, 1)
+            opacityMap.set(reverseKey, 1)
+          }
         })
         })
 
 
-        const node = params.data
+        // 设置节点透明度
+        graph.nodes.forEach((n: any) => {
+          opacityMap.set(n.id, related.has(n.id) ? 1 : 0.1)
+        })
+
+        // 更新图表节点和连线
+        updateChartNodesAndLinks(graph.nodes, graph.links, opacityMap)
+
         if (node && node.raw && node.raw.id) {
         if (node && node.raw && node.raw.id) {
-          if (currentId.current === node.raw.id) return
+          // 如果点击的节点是已选中的节点,则取消选中状态
+          if (currentId.current === node.raw.id) {
+            currentId.current = null
+            setDetail(null)
+            resetHighlight() // 恢复所有节点和连线的高亮状态
+            return
+          }
+
+          // 高亮选中的节点
 
 
           const fetchDetail = async () => {
           const fetchDetail = async () => {
             try {
             try {
               currentId.current = node.raw.id
               currentId.current = node.raw.id
               setDetailLoading(true)
               setDetailLoading(true)
               const response = await http.get(`/show/dict/detail/${node.raw.id}`)
               const response = await http.get(`/show/dict/detail/${node.raw.id}`)
-              if (response.code === 0 && response.data) {
-                const { label, name, rtf } = response.data
-                let detailData: any = { name }
-
-                // 存在 label 显示 Panel
-                if (label) {
-                  try {
-                    const parsedLabel = JSON.parse(label.replace(/\\\\"/g, '\\"'))
-                    detailData = {
-                      ...detailData,
-                      label: name,
-                      content: Array.isArray(parsedLabel) ? parsedLabel : [],
-                      type: 'label'
-                    }
-                    setDetail(detailData)
-                    setSidebarVisible(true)
-                  } catch (e) {
-                    console.error('解析 label 失败:', e)
+
+              if (response.code !== 0 || !response.data) {
+                setDetail(null)
+                return
+              }
+
+              const { label, name, rtf } = response.data
+              let detailData: any = { name }
+
+              // 存在 label 显示 Panel
+              if (label) {
+                try {
+                  const parsedLabel = JSON.parse(label.replace(/\\\\"/g, '\\"'))
+                  detailData = {
+                    ...detailData,
+                    label: name,
+                    content: Array.isArray(parsedLabel) ? parsedLabel : [],
+                    type: 'label'
                   }
                   }
+                  setDetail(detailData)
+                  setSidebarVisible(true)
+                  return
+                } catch (e) {
+                  console.error('解析 label 失败:', e)
                 }
                 }
-                // 存在 rtf 显示 Panel2
-                else if (rtf) {
-                  try {
-                    const parsedRtf = JSON.parse(rtf)
-                    const content = parsedRtf?.txtArr?.[0]?.txt || ''
-                    detailData = {
-                      ...detailData,
-                      label: name,
-                      content: content,
-                      type: 'rtf'
-                    }
-                    setDetail(detailData)
-                    setSidebarVisible(true)
-                  } catch (e) {
-                    console.error('解析 rtf 失败:', e)
+              }
+
+              // 存在 rtf 显示 Panel2
+              if (rtf) {
+                try {
+                  const parsedRtf = JSON.parse(rtf)
+                  const content = parsedRtf?.txtArr?.[0]?.txt || ''
+                  detailData = {
+                    ...detailData,
+                    label: name,
+                    content: content,
+                    type: 'rtf'
                   }
                   }
-                } else {
-                  setDetail(null)
+                  setDetail(detailData)
+                  setSidebarVisible(true)
+                  return
+                } catch (e) {
+                  console.error('解析 rtf 失败:', e)
                 }
                 }
               }
               }
+
+              setDetail(null)
             } catch (error) {
             } catch (error) {
+              console.error('获取详情失败:', error)
               setDetail(null)
               setDetail(null)
             } finally {
             } finally {
               setDetailLoading(false)
               setDetailLoading(false)
@@ -410,26 +541,22 @@ function A9knowlege() {
     })
     })
     myChart.getZr().on('click', function (params: any) {
     myChart.getZr().on('click', function (params: any) {
       if (!params.target) {
       if (!params.target) {
+        // 点击空白处,取消选中状态和高亮
         currentId.current = null
         currentId.current = null
-        myChart.dispatchAction({
-          type: 'downplay'
-        })
-      }
-    })
-    myChart.on('mouseover', function () {
-      keepHighlight()
-    })
-    myChart.on('graphroam', function (params: any) {
-      keepHighlight()
-      if (typeof params?.zoom === 'number') {
-        const p = (params.zoom - SCALE_MIN) / (SCALE_MAX - SCALE_MIN)
-        setZoomPercent(Math.max(0, Math.min(1, p)))
+        resetHighlight() // 恢复所有节点和连线的高亮状态
       }
       }
     })
     })
+    if (!IS_PC) {
+      myChart.on('graphroam', function (params: any) {
+        if (typeof params?.zoom === 'number') {
+          const p = (params.zoom - SCALE_MIN) / (SCALE_MAX - SCALE_MIN)
+          setZoomPercent(Math.max(0, Math.min(1, p)))
+        }
+      })
+    }
 
 
     const resizeHandler = () => myChart.resize()
     const resizeHandler = () => myChart.resize()
     window.addEventListener('resize', resizeHandler)
     window.addEventListener('resize', resizeHandler)
-    ;(chartInstance.current as any)._resizeHandler = resizeHandler
   }, [knowlegeData, dataLoading])
   }, [knowlegeData, dataLoading])
 
 
   const handleClosePanel = () => {
   const handleClosePanel = () => {
@@ -499,22 +626,24 @@ function A9knowlege() {
         <div id='echart-container' ref={echartRef} />
         <div id='echart-container' ref={echartRef} />
       </div>
       </div>
 
 
-      <div className={styles.scaleControl}>
-        <div className={styles.scaleControlItem} onClick={() => handleZoomStep(-0.1)}>
-          <img src={require('./images/icon_zoomin.png')} alt='' />
-          <div className={styles.scaleControlItemText}>缩小</div>
-        </div>
-        <div className={styles.scaleControlItemLine} ref={scaleLineRef}>
-          <div
-            className={styles.scaleControlItemLineInner}
-            style={{ left: `${zoomPercent * 100}%` }}
-          />
-        </div>
-        <div className={styles.scaleControlItem} onClick={() => handleZoomStep(0.1)}>
-          <img src={require('./images/icon_zoomax.png')} alt='' />
-          <div className={styles.scaleControlItemText}>放大</div>
+      {!IS_PC && (
+        <div className={styles.scaleControl}>
+          <div className={styles.scaleControlItem} onClick={() => handleZoomStep(-0.1)}>
+            <img src={require('./images/icon_zoomin.png')} alt='' />
+            <div className={styles.scaleControlItemText}>缩小</div>
+          </div>
+          <div className={styles.scaleControlItemLine} ref={scaleLineRef}>
+            <div
+              className={styles.scaleControlItemLineInner}
+              style={{ left: `${zoomPercent * 100}%` }}
+            />
+          </div>
+          <div className={styles.scaleControlItem} onClick={() => handleZoomStep(0.1)}>
+            <img src={require('./images/icon_zoomax.png')} alt='' />
+            <div className={styles.scaleControlItemText}>放大</div>
+          </div>
         </div>
         </div>
-      </div>
+      )}
 
 
       <div className={`${styles.sidebar} ${!sidebarVisible ? styles.sidebarHidden : ''}`}>
       <div className={`${styles.sidebar} ${!sidebarVisible ? styles.sidebarHidden : ''}`}>
         {detailLoading ? (
         {detailLoading ? (