|
@@ -1,670 +1,159 @@
|
|
|
-import React, { useEffect, useRef, useCallback, useState } from 'react'
|
|
|
|
|
|
|
+import React, { useRef, useState, useEffect } from 'react'
|
|
|
|
|
+import http from '@/utils/axios'
|
|
|
import styles from './index.module.scss'
|
|
import styles from './index.module.scss'
|
|
|
|
|
+import { echPageBackFu } from '@/components/MenuSider/data'
|
|
|
|
|
+import Zback from '@/components/Zback'
|
|
|
import Chart from './components/Chart'
|
|
import Chart from './components/Chart'
|
|
|
import Panel from './components/Panel'
|
|
import Panel from './components/Panel'
|
|
|
import Panel2 from './components/Panel2'
|
|
import Panel2 from './components/Panel2'
|
|
|
import ModelPanel from './components/ModelPanel'
|
|
import ModelPanel from './components/ModelPanel'
|
|
|
-import { echPageBackFu } from '@/components/MenuSider/data'
|
|
|
|
|
-import Zback from '@/components/Zback'
|
|
|
|
|
-import http from '@/utils/axios'
|
|
|
|
|
-
|
|
|
|
|
-import level1 from './images/level_01-min.png'
|
|
|
|
|
-import level2 from './images/level_02-min.png'
|
|
|
|
|
-import level3 from './images/level_03-min.png'
|
|
|
|
|
-import level4 from './images/level_04-min.png'
|
|
|
|
|
-import level5 from './images/level_05-min.png'
|
|
|
|
|
-import level6 from './images/level_06-min.png'
|
|
|
|
|
-import { baseUrl } from '@/utils/http'
|
|
|
|
|
import { DotLoading } from 'antd-mobile'
|
|
import { DotLoading } from 'antd-mobile'
|
|
|
|
|
|
|
|
-const levelImages = [level1, level1, level1, level2, level3, level4, level5, level6]
|
|
|
|
|
-const getLevelImage = (depth: number) => levelImages[Math.min(depth, 5)]
|
|
|
|
|
-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[] => {
|
|
|
|
|
- return data.map(item => ({
|
|
|
|
|
- ...item,
|
|
|
|
|
- label: item.name,
|
|
|
|
|
- children: item.children ? transformData(item.children) : undefined
|
|
|
|
|
- }))
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-const loadImage = (src: string): Promise<HTMLImageElement> => {
|
|
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
|
|
- const img = new Image()
|
|
|
|
|
- img.crossOrigin = 'anonymous'
|
|
|
|
|
- img.onload = () => resolve(img)
|
|
|
|
|
- img.onerror = () => reject(new Error(`Failed to load: ${src}`))
|
|
|
|
|
- img.src = src
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 缩放区间和默认值(与 ECharts 的 scaleLimit / zoom 保持一致)
|
|
|
|
|
-const SCALE_MIN = 0.1
|
|
|
|
|
-const SCALE_MAX = 1.2
|
|
|
|
|
-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 (
|
|
|
|
|
- levelImageSrc: string,
|
|
|
|
|
- thumbSrc: string,
|
|
|
|
|
- symbolSize: number
|
|
|
|
|
-): Promise<string> => {
|
|
|
|
|
- try {
|
|
|
|
|
- const canvas = document.createElement('canvas')
|
|
|
|
|
- canvas.width = symbolSize
|
|
|
|
|
- canvas.height = symbolSize
|
|
|
|
|
- const ctx = canvas.getContext('2d')!
|
|
|
|
|
-
|
|
|
|
|
- const levelImg = await loadImage(levelImageSrc)
|
|
|
|
|
- ctx.drawImage(levelImg, 0, 0, symbolSize, symbolSize)
|
|
|
|
|
-
|
|
|
|
|
- const thumbImg = await loadImage(thumbSrc)
|
|
|
|
|
- const thumbSize = symbolSize === 40 ? 30 : 48
|
|
|
|
|
- const centerX = symbolSize / 2 - (symbolSize === 60 ? 1 : 0)
|
|
|
|
|
- const centerY = symbolSize / 2 - (symbolSize === 60 ? 1 : 0)
|
|
|
|
|
- const imgW = thumbImg.width
|
|
|
|
|
- const imgH = thumbImg.height
|
|
|
|
|
- const scale = Math.max(thumbSize / imgW, thumbSize / imgH)
|
|
|
|
|
- const drawW = imgW * scale
|
|
|
|
|
- const drawH = imgH * scale
|
|
|
|
|
- const drawX = centerX - drawW / 2
|
|
|
|
|
- const drawY = centerY - drawH / 2
|
|
|
|
|
-
|
|
|
|
|
- ctx.save()
|
|
|
|
|
- ctx.beginPath()
|
|
|
|
|
- ctx.arc(centerX, centerY, thumbSize / 2, 0, Math.PI * 2)
|
|
|
|
|
- ctx.clip()
|
|
|
|
|
- ctx.drawImage(thumbImg, drawX, drawY, drawW, drawH)
|
|
|
|
|
- ctx.restore()
|
|
|
|
|
-
|
|
|
|
|
- return `image://${canvas.toDataURL()}`
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- return `image://${levelImageSrc}`
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
function A9knowlege() {
|
|
function A9knowlege() {
|
|
|
- // 是否显示返回按钮
|
|
|
|
|
- const [backShow, setBackShow] = useState(false)
|
|
|
|
|
// 控制 sidebar 显示
|
|
// 控制 sidebar 显示
|
|
|
const [sidebarVisible, setSidebarVisible] = useState(true)
|
|
const [sidebarVisible, setSidebarVisible] = useState(true)
|
|
|
- const [knowlegeData, setKnowlegeData] = useState<any[]>([])
|
|
|
|
|
- const [dataLoading, setDataLoading] = useState(true)
|
|
|
|
|
- const currentId = useRef<string | null>(null)
|
|
|
|
|
-
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- if (!window.location.href.includes('?l=look')) {
|
|
|
|
|
- setBackShow(true)
|
|
|
|
|
- }
|
|
|
|
|
- }, [])
|
|
|
|
|
-
|
|
|
|
|
- const echartRef = useRef<HTMLDivElement | null>(null)
|
|
|
|
|
- const chartInstance = useRef<any>(null)
|
|
|
|
|
- const graphRef = useRef<any>(null)
|
|
|
|
|
- const scaleLineRef = useRef<HTMLDivElement | null>(null)
|
|
|
|
|
const [detail, setDetail] = useState<any>(null)
|
|
const [detail, setDetail] = useState<any>(null)
|
|
|
const [detailLoading, setDetailLoading] = useState(false)
|
|
const [detailLoading, setDetailLoading] = useState(false)
|
|
|
- // 缩放百分比
|
|
|
|
|
- const [zoomPercent, setZoomPercent] = useState(
|
|
|
|
|
- (DEFAULT_ZOOM - SCALE_MIN) / (SCALE_MAX - SCALE_MIN)
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- const fetchKnowlegeData = async () => {
|
|
|
|
|
- try {
|
|
|
|
|
- setDataLoading(true)
|
|
|
|
|
- const response = await http.get('/show/dict/getTree')
|
|
|
|
|
- if (response.code === 0 && response.data) {
|
|
|
|
|
- const transformedData = transformData(Array.isArray(response.data) ? response.data : [])
|
|
|
|
|
- setKnowlegeData(transformedData)
|
|
|
|
|
- } else {
|
|
|
|
|
- setKnowlegeData([])
|
|
|
|
|
- }
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('获取知识图谱数据失败:', error)
|
|
|
|
|
- setKnowlegeData([])
|
|
|
|
|
- } finally {
|
|
|
|
|
- setDataLoading(false)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- fetchKnowlegeData()
|
|
|
|
|
- }, [])
|
|
|
|
|
-
|
|
|
|
|
- const buildGraphFromKnowlege = async (data: any[]) => {
|
|
|
|
|
- const nodes: any[] = []
|
|
|
|
|
- const links: any[] = []
|
|
|
|
|
- let idCounter = 0
|
|
|
|
|
- const genId = (label: string) =>
|
|
|
|
|
- `${idCounter++}-${String(label || 'node').replace(/\s+/g, '_')}`
|
|
|
|
|
-
|
|
|
|
|
- const traverse = (item: any, parentId: string | null, depth: number) => {
|
|
|
|
|
- const id = genId(item.label)
|
|
|
|
|
- const category = `level${depth}`
|
|
|
|
|
- const symbolSize = getLevelSize(depth)
|
|
|
|
|
- const symbol = `image://${getLevelImage(depth)}`
|
|
|
|
|
-
|
|
|
|
|
- nodes.push({
|
|
|
|
|
- id,
|
|
|
|
|
- name: item.label,
|
|
|
|
|
- category,
|
|
|
|
|
- symbolSize,
|
|
|
|
|
- symbol,
|
|
|
|
|
- raw: item,
|
|
|
|
|
- depth,
|
|
|
|
|
- thumb: item.thumb ? baseUrl + item.thumb : null
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- 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))
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- data.forEach((root: any) => traverse(root, null, 0))
|
|
|
|
|
-
|
|
|
|
|
- // 对于有 thumb 的节点,异步生成合成 symbol
|
|
|
|
|
- const thumbPromises = nodes
|
|
|
|
|
- .filter(n => n.thumb)
|
|
|
|
|
- .map(async n => {
|
|
|
|
|
- const levelImageSrc = getLevelImage(n.depth)
|
|
|
|
|
- n.symbol = await createCombinedSymbol(levelImageSrc, n.thumb, n.symbolSize)
|
|
|
|
|
- })
|
|
|
|
|
- await Promise.all(thumbPromises)
|
|
|
|
|
-
|
|
|
|
|
- const maxDepth = nodes.reduce((m, n) => {
|
|
|
|
|
- const d = getDepthFromCategory(n.category || '')
|
|
|
|
|
- return Math.max(m, d)
|
|
|
|
|
- }, 0)
|
|
|
|
|
-
|
|
|
|
|
- const categories = Array.from({ length: maxDepth + 1 }, (_, i) => ({
|
|
|
|
|
- name: `level${i}`
|
|
|
|
|
- }))
|
|
|
|
|
-
|
|
|
|
|
- return { nodes, links, categories }
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const [animationCompleted, setAnimationCompleted] = useState(true)
|
|
|
|
|
+ const currentId = useRef<string | null>(null)
|
|
|
|
|
+ const iframeRef = useRef<HTMLIFrameElement>(null)
|
|
|
|
|
|
|
|
- const initChart = useCallback(async () => {
|
|
|
|
|
|
|
+ const handleClosePanel = () => {
|
|
|
// @ts-ignore
|
|
// @ts-ignore
|
|
|
- const echarts = (window as any).echarts
|
|
|
|
|
- const data = knowlegeData || []
|
|
|
|
|
- if (!echarts || !echartRef.current || dataLoading || data.length === 0) return
|
|
|
|
|
-
|
|
|
|
|
- if (chartInstance.current) {
|
|
|
|
|
- try {
|
|
|
|
|
- chartInstance.current.dispose()
|
|
|
|
|
- } catch (e) {}
|
|
|
|
|
- chartInstance.current = null
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const myChart = echarts.init(echartRef.current, null, {
|
|
|
|
|
- renderer: 'canvas',
|
|
|
|
|
- useDirtyRect: false
|
|
|
|
|
- })
|
|
|
|
|
- chartInstance.current = myChart
|
|
|
|
|
-
|
|
|
|
|
- const graph = await buildGraphFromKnowlege(data)
|
|
|
|
|
- graphRef.current = graph
|
|
|
|
|
-
|
|
|
|
|
- const option = {
|
|
|
|
|
- tooltip: { show: false },
|
|
|
|
|
- series: [
|
|
|
|
|
- {
|
|
|
|
|
- name: 'KnowledgeGraph',
|
|
|
|
|
- type: 'graph',
|
|
|
|
|
- layout: 'force',
|
|
|
|
|
- data: graph.nodes.map((n: any) => createNodeConfig({ node: n, fixed: false })),
|
|
|
|
|
- links: graph.links.map((link: any) => {
|
|
|
|
|
- const style = getNodeStyleByDepth(link.targetDepth)
|
|
|
|
|
- return {
|
|
|
|
|
- ...link,
|
|
|
|
|
- lineStyle: {
|
|
|
|
|
- color: style.lineColor
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }),
|
|
|
|
|
- categories: graph.categories,
|
|
|
|
|
- roam: IS_PC ? true : 'move',
|
|
|
|
|
- zoom: DEFAULT_ZOOM,
|
|
|
|
|
- scaleLimit: {
|
|
|
|
|
- min: SCALE_MIN,
|
|
|
|
|
- max: SCALE_MAX
|
|
|
|
|
- },
|
|
|
|
|
- label: { position: 'bottom', distance: 6, align: 'center' },
|
|
|
|
|
- // emphasis: {
|
|
|
|
|
- // focus: 'adjacency',
|
|
|
|
|
- // lineStyle: {
|
|
|
|
|
- // width: 3,
|
|
|
|
|
- // opacity: 1
|
|
|
|
|
- // }
|
|
|
|
|
- // },
|
|
|
|
|
- // force 布局参数:调大 repulsion 让斥力更明显
|
|
|
|
|
- // 降低 gravity 避免被拉回中心过紧
|
|
|
|
|
- force: FORCE_LAYOUT_CONFIG,
|
|
|
|
|
- // 连线为直线:curveness 设为 0
|
|
|
|
|
- lineStyle: { curveness: 0 },
|
|
|
|
|
- edgeLabel: { show: false },
|
|
|
|
|
- edgeSymbol: ['none', 'none']
|
|
|
|
|
- }
|
|
|
|
|
- ],
|
|
|
|
|
- animationDurationUpdate: 0
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- myChart.setOption(option, true)
|
|
|
|
|
-
|
|
|
|
|
- // 初始化滑块位置
|
|
|
|
|
- setZoomPercent((DEFAULT_ZOOM - SCALE_MIN) / (SCALE_MAX - SCALE_MIN))
|
|
|
|
|
-
|
|
|
|
|
- // 更新图表节点和连线配置的通用函数
|
|
|
|
|
- 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)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ iframeRef.current?.contentWindow?.blurNode()
|
|
|
|
|
+ setDetail(null)
|
|
|
|
|
+ currentId.current = null
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- myChart.on('click', function (params: any) {
|
|
|
|
|
- if (!params || !params.data) return
|
|
|
|
|
|
|
+ const handleTransitionEnd = () => {
|
|
|
|
|
+ setAnimationCompleted(true)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (params.dataType === 'edge') {
|
|
|
|
|
- // 关系线
|
|
|
|
|
- const edge = params.data
|
|
|
|
|
- const sourceIndex = graph.nodes.findIndex((n: any) => n.id === edge.source)
|
|
|
|
|
- const targetIndex = graph.nodes.findIndex((n: any) => n.id === edge.target)
|
|
|
|
|
|
|
+ const toggleSidebar = () => {
|
|
|
|
|
+ setAnimationCompleted(false)
|
|
|
|
|
+ setSidebarVisible(!sidebarVisible)
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- if (sourceIndex !== -1 && targetIndex !== -1) {
|
|
|
|
|
- // 高亮源节点
|
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ const handleMessage = (event: MessageEvent) => {
|
|
|
|
|
+ if (event.data && event.data.id) {
|
|
|
|
|
+ const id = event.data.id
|
|
|
|
|
+ if (currentId.current !== id) {
|
|
|
|
|
+ fetchNodeDetail(id)
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // 节点
|
|
|
|
|
- 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)
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- // 设置节点透明度
|
|
|
|
|
- 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 (currentId.current === node.raw.id) {
|
|
|
|
|
- currentId.current = null
|
|
|
|
|
- setDetail(null)
|
|
|
|
|
- resetHighlight() // 恢复所有节点和连线的高亮状态
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 高亮选中的节点
|
|
|
|
|
-
|
|
|
|
|
- const fetchDetail = async () => {
|
|
|
|
|
- try {
|
|
|
|
|
- currentId.current = node.raw.id
|
|
|
|
|
- setDetailLoading(true)
|
|
|
|
|
- const response = await http.get(`/show/dict/detail/${node.raw.id}`)
|
|
|
|
|
-
|
|
|
|
|
- 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
|
|
|
|
|
- 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)
|
|
|
|
|
- return
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- console.error('解析 rtf 失败:', e)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- setDetail(null)
|
|
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('获取详情失败:', error)
|
|
|
|
|
- setDetail(null)
|
|
|
|
|
- } finally {
|
|
|
|
|
- setDetailLoading(false)
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- fetchDetail()
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
- myChart.getZr().on('click', function (params: any) {
|
|
|
|
|
- if (!params.target) {
|
|
|
|
|
- // 点击空白处,取消选中状态和高亮
|
|
|
|
|
|
|
+ setDetail(null)
|
|
|
currentId.current = null
|
|
currentId.current = null
|
|
|
- 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()
|
|
|
|
|
- window.addEventListener('resize', resizeHandler)
|
|
|
|
|
- }, [knowlegeData, dataLoading])
|
|
|
|
|
|
|
+ window.addEventListener('message', handleMessage)
|
|
|
|
|
|
|
|
- const handleClosePanel = () => {
|
|
|
|
|
- setDetail(null)
|
|
|
|
|
- currentId.current = null
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- if (!dataLoading && knowlegeData.length > 0) {
|
|
|
|
|
- initChart()
|
|
|
|
|
- }
|
|
|
|
|
return () => {
|
|
return () => {
|
|
|
- if (chartInstance.current) {
|
|
|
|
|
- try {
|
|
|
|
|
- const h = (chartInstance.current as any)._resizeHandler
|
|
|
|
|
- if (h) window.removeEventListener('resize', h)
|
|
|
|
|
- chartInstance.current.dispose()
|
|
|
|
|
- } catch (e) {}
|
|
|
|
|
- chartInstance.current = null
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ window.removeEventListener('message', handleMessage)
|
|
|
}
|
|
}
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
- }, [knowlegeData, dataLoading, initChart])
|
|
|
|
|
|
|
+ }, [])
|
|
|
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- const timer = setTimeout(() => {
|
|
|
|
|
- if (chartInstance.current) {
|
|
|
|
|
- chartInstance.current.resize()
|
|
|
|
|
|
|
+ // 获取节点详情
|
|
|
|
|
+ const fetchNodeDetail = async (id: string) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ currentId.current = id
|
|
|
|
|
+ setDetailLoading(true)
|
|
|
|
|
+ const response = await http.get(`/show/dict/detail/${id}`)
|
|
|
|
|
+
|
|
|
|
|
+ if (response.code !== 0 || !response.data) {
|
|
|
|
|
+ setDetail(null)
|
|
|
|
|
+ return
|
|
|
}
|
|
}
|
|
|
- }, 300)
|
|
|
|
|
- return () => clearTimeout(timer)
|
|
|
|
|
- }, [sidebarVisible])
|
|
|
|
|
|
|
|
|
|
- const percentToZoom = (percent: number) => {
|
|
|
|
|
- const p = Math.max(0, Math.min(1, percent))
|
|
|
|
|
- return SCALE_MIN + p * (SCALE_MAX - SCALE_MIN)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const { label, name, rtf } = response.data
|
|
|
|
|
+ let detailData: any = { name }
|
|
|
|
|
|
|
|
- // 外部控制:当 zoomPercent 改变时,同步到 ECharts
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- if (!chartInstance.current) return
|
|
|
|
|
- const zoom = percentToZoom(zoomPercent)
|
|
|
|
|
- chartInstance.current.setOption(
|
|
|
|
|
- {
|
|
|
|
|
- series: [
|
|
|
|
|
- {
|
|
|
|
|
- zoom
|
|
|
|
|
|
|
+ // 存在 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
|
|
|
|
|
+ if (rtf) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const parsedRtf = JSON.parse(rtf)
|
|
|
|
|
+ const content = parsedRtf?.txtArr?.[0]?.txt || ''
|
|
|
|
|
+ detailData = {
|
|
|
|
|
+ ...detailData,
|
|
|
|
|
+ label: name,
|
|
|
|
|
+ content: content,
|
|
|
|
|
+ type: 'rtf'
|
|
|
}
|
|
}
|
|
|
- ]
|
|
|
|
|
- },
|
|
|
|
|
- false
|
|
|
|
|
- )
|
|
|
|
|
- }, [zoomPercent])
|
|
|
|
|
|
|
+ setDetail(detailData)
|
|
|
|
|
+ setSidebarVisible(true)
|
|
|
|
|
+ return
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ console.error('解析 rtf 失败:', e)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- const handleZoomStep = (deltaPercent: number) => {
|
|
|
|
|
- setZoomPercent(prev => {
|
|
|
|
|
- const next = prev + deltaPercent
|
|
|
|
|
- return Math.max(0, Math.min(1, next))
|
|
|
|
|
- })
|
|
|
|
|
|
|
+ setDetail(null)
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取详情失败:', error)
|
|
|
|
|
+ setDetail(null)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setDetailLoading(false)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
<div className={styles.A9knowlege}>
|
|
<div className={styles.A9knowlege}>
|
|
|
- {backShow ? <Zback clickFu={() => echPageBackFu()} /> : null}
|
|
|
|
|
-
|
|
|
|
|
- <div className={styles.main}>
|
|
|
|
|
- <div id='echart-container' ref={echartRef} />
|
|
|
|
|
- </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 className={`${styles.sidebar} ${!sidebarVisible ? styles.sidebarHidden : ''}`}>
|
|
|
|
|
- {detailLoading ? (
|
|
|
|
|
- <div className={styles.loading}>
|
|
|
|
|
- <DotLoading color='#7c4b36' />
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : !detail ? (
|
|
|
|
|
- <Chart />
|
|
|
|
|
- ) : detail.type === 'label' ? (
|
|
|
|
|
- <Panel detail={detail} onClose={handleClosePanel} />
|
|
|
|
|
- ) : detail.type === 'rtf' ? (
|
|
|
|
|
- <Panel2 detail={detail} onClose={handleClosePanel} />
|
|
|
|
|
- ) : detail.type === 5 ? (
|
|
|
|
|
- <ModelPanel detail={detail} onClose={handleClosePanel} />
|
|
|
|
|
- ) : (
|
|
|
|
|
- <Panel2 detail={detail} onClose={handleClosePanel} />
|
|
|
|
|
|
|
+ <Zback clickFu={() => echPageBackFu()} />
|
|
|
|
|
+
|
|
|
|
|
+ <iframe
|
|
|
|
|
+ ref={iframeRef}
|
|
|
|
|
+ className={styles.iframe}
|
|
|
|
|
+ src='knowlege/index.html'
|
|
|
|
|
+ title='knowlege'
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ className={`${styles.sidebar} ${!sidebarVisible ? styles.sidebarHidden : ''}`}
|
|
|
|
|
+ onTransitionEnd={handleTransitionEnd}
|
|
|
|
|
+ >
|
|
|
|
|
+ {animationCompleted && sidebarVisible && (
|
|
|
|
|
+ <>
|
|
|
|
|
+ {detailLoading ? (
|
|
|
|
|
+ <div className={styles.loading}>
|
|
|
|
|
+ <DotLoading color='#7c4b36' />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : !detail ? (
|
|
|
|
|
+ <Chart />
|
|
|
|
|
+ ) : detail.type === 'label' ? (
|
|
|
|
|
+ <Panel detail={detail} onClose={handleClosePanel} />
|
|
|
|
|
+ ) : detail.type === 'rtf' ? (
|
|
|
|
|
+ <Panel2 detail={detail} onClose={handleClosePanel} />
|
|
|
|
|
+ ) : detail.type === 5 ? (
|
|
|
|
|
+ <ModelPanel detail={detail} onClose={handleClosePanel} />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <Panel2 detail={detail} onClose={handleClosePanel} />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </>
|
|
|
)}
|
|
)}
|
|
|
|
|
|
|
|
<div
|
|
<div
|
|
|
className={`${styles.sidebarHideBtn} ${!sidebarVisible ? styles.hide : ''}`}
|
|
className={`${styles.sidebarHideBtn} ${!sidebarVisible ? styles.hide : ''}`}
|
|
|
- onClick={() => setSidebarVisible(!sidebarVisible)}
|
|
|
|
|
|
|
+ onClick={toggleSidebar}
|
|
|
></div>
|
|
></div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|