Parcourir la source

Merge branch 'master' of http://192.168.0.115:3000/lanxin/Chengzhebei

lanxin il y a 6 jours
Parent
commit
960644b0e1

Fichier diff supprimé car celui-ci est trop grand
+ 1360 - 1358
public/myData/myData.js


+ 6 - 0
src/pages/A9knowlege/components/Chart/index.module.scss

@@ -54,6 +54,12 @@
   white-space: pre-wrap;
 }
 
+.helpImprove {
+  margin-top: 6px;
+  font-size: 12px;
+  color: #af8764;
+}
+
 .message_user .messageContent {
   background-color: rgba(124, 75, 54, 0.1);
 }

+ 61 - 17
src/pages/A9knowlege/components/Chart/index.tsx

@@ -5,6 +5,7 @@ import { decrypt } from '@/utils/encrypt'
 import ChartInput from '../ChartInput'
 import { useSelector } from 'react-redux'
 import { RootState } from '@/store'
+import HelpImprove from '../HelpImprove'
 
 const questions = [
   '程哲碑有哪些特殊之处?',
@@ -28,6 +29,9 @@ function Chart() {
   const [isLoading, setIsLoading] = useState(false)
   const [showQuickQuestions, setShowQuickQuestions] = useState(true)
   const [showChartInput, setShowChartInput] = useState(false)
+  const [showHelpImprove, setShowHelpImprove] = useState(false)
+  const [currentQuestion, setCurrentQuestion] = useState('')
+  const [currentAnswer, setCurrentAnswer] = useState('')
   const messagesEndRef = useRef<HTMLDivElement>(null)
   const inputRef = useRef<HTMLInputElement>(null)
   const savedTransformRef = useRef<string>('')
@@ -161,13 +165,11 @@ function Chart() {
     setShowQuickQuestions(true)
   }
 
-  const handleInputClick = () => {
-    if (isHH) {
-      return
-    }
-
+  const handleRotateRoot = (normal: boolean) => {
     const rootElement = document.querySelector('#root') as HTMLElement
-    if (rootElement) {
+    if (!rootElement) return
+
+    if (normal) {
       savedTransformRef.current = rootElement.style.transform || ''
       savedLeftRef.current = rootElement.style.left || ''
 
@@ -176,17 +178,23 @@ function Chart() {
         rootElement.style.transform = ''
         rootElement.style.left = '0'
       }
+    } else {
+      rootElement.style.transform = savedTransformRef.current || ''
+      rootElement.style.left = savedLeftRef.current || ''
+    }
+  }
 
-      setShowChartInput(true)
+  const handleInputClick = () => {
+    if (isHH) {
+      return
     }
+
+    handleRotateRoot(true)
+    setShowChartInput(true)
   }
 
   const handleCloseChartInput = () => {
-    const rootElement = document.querySelector('#root') as HTMLElement
-    if (rootElement) {
-      rootElement.style.transform = savedTransformRef.current || ''
-      rootElement.style.left = savedLeftRef.current || ''
-    }
+    handleRotateRoot(false)
     setShowChartInput(false)
   }
 
@@ -210,11 +218,37 @@ function Chart() {
         {/* 聊天消息列表 */}
         {messages.length > 0 && (
           <div className={styles.messagesContainer}>
-            {messages.map((msg, index) => (
-              <div key={index} className={`${styles.message} ${styles[`message_${msg.role}`]}`}>
-                <div className={styles.messageContent}>{msg.content}</div>
-              </div>
-            ))}
+            {messages.map((msg, index) => {
+              // 找到对应的question(上一个user消息)
+              const findQuestion = () => {
+                for (let i = index - 1; i >= 0; i--) {
+                  if (messages[i].role === 'user') {
+                    return messages[i].content
+                  }
+                }
+                return ''
+              }
+
+              return (
+                <div key={index} className={`${styles.message} ${styles[`message_${msg.role}`]}`}>
+                  <div className={styles.messageContent}>{msg.content}</div>
+                  {msg.role === 'assistant' && msg.content.includes('暂时无法解答这个问题') && (
+                    <div
+                      className={styles.helpImprove}
+                      onClick={() => {
+                        const question = findQuestion()
+                        handleRotateRoot(true)
+                        setCurrentQuestion(question)
+                        setCurrentAnswer(msg.content)
+                        setShowHelpImprove(true)
+                      }}
+                    >
+                      帮助改进
+                    </div>
+                  )}
+                </div>
+              )
+            })}
             {isLoading && (
               <div className={`${styles.message} ${styles.message_assistant}`}>
                 <div className={styles.messageContent}>
@@ -253,6 +287,16 @@ function Chart() {
           isLoading={isLoading}
         />
       )}
+      {showHelpImprove && (
+        <HelpImprove
+          question={currentQuestion}
+          answer={currentAnswer}
+          onClose={() => {
+            handleRotateRoot(false)
+            setShowHelpImprove(false)
+          }}
+        />
+      )}
     </div>
   )
 }

+ 158 - 0
src/pages/A9knowlege/components/HelpImprove/index.module.scss

@@ -0,0 +1,158 @@
+@function vh-calc($num) {
+  @return calc(100vh * ($num / 752));
+}
+
+.helpImprove {
+  position: fixed;
+  inset: 0;
+  background: rgba(0, 0, 0, 0.8);
+  backdrop-filter: blur(10px);
+  z-index: 999;
+}
+
+.helpImproveContent {
+  position: absolute;
+  top: 50%;
+  left: 50%;
+  padding: vh-calc(70) vh-calc(20) 0 vh-calc(40);
+  width: vh-calc(371);
+  height: vh-calc(641);
+  background: url('../../images/img_pop_2-min.png') no-repeat center / contain;
+  transform: translate(-50%, -50%);
+
+  :global {
+    h3 {
+      font-size: vh-calc(20);
+      color: #7c4b36;
+    }
+  }
+}
+
+.helpImproveDialog {
+  margin-top: vh-calc(20);
+  color: #5b472e;
+  font-size: vh-calc(12);
+
+  :global {
+    div {
+      display: flex;
+
+      &:last-child {
+        margin-top: vh-calc(5);
+      }
+      span {
+        flex-shrink: 0;
+        width: vh-calc(50);
+      }
+    }
+  }
+}
+
+.helpImproveType {
+  margin-top: vh-calc(18);
+
+  :global {
+    p {
+      margin-bottom: vh-calc(8);
+      font-size: vh-calc(12);
+      font-weight: bold;
+      color: #5b472e;
+    }
+    .ant-radio-wrapper {
+      align-items: center;
+
+      span.ant-radio + * {
+        padding-left: vh-calc(3);
+        padding-right: vh-calc(20);
+        color: #5b472e;
+        font-size: vh-calc(12);
+      }
+    }
+    .ant-radio {
+      .ant-radio-inner {
+        background: none;
+        border: 1px solid #5b472e;
+      }
+      &.ant-radio-checked {
+        .ant-radio-inner {
+          background: #5b472e;
+          border-color: #5b472e;
+        }
+        &::after {
+          border-color: #5b472e;
+        }
+      }
+    }
+  }
+}
+
+.helpImproveTextarea {
+  margin: vh-calc(10) 0 vh-calc(17);
+  padding: vh-calc(10) vh-calc(13);
+  height: vh-calc(153);
+  border-radius: vh-calc(5);
+  color: #5b472e;
+  border: 1px solid #7c4b36;
+
+  textarea {
+    font-size: vh-calc(12);
+
+    &::placeholder {
+      color: #5b472e;
+      opacity: 0.6;
+    }
+  }
+}
+
+.helpImproveLine {
+  margin-bottom: vh-calc(10);
+  display: flex;
+  align-items: center;
+  color: #5b472e;
+  font-weight: bold;
+  font-size: vh-calc(12);
+  white-space: nowrap;
+  gap: vh-calc(7);
+}
+
+.helpImproveInput {
+  height: vh-calc(30);
+  border-radius: vh-calc(5);
+  border: vh-calc(1) solid #7c4b36;
+  padding: 0 vh-calc(10);
+
+  input {
+    color: #5b472e;
+    font-size: vh-calc(12);
+    &::placeholder {
+      color: #5b472e;
+      opacity: 0.6;
+    }
+  }
+}
+
+.helpImproveFooter {
+  margin-top: vh-calc(27);
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: vh-calc(10);
+}
+
+.helpImproveSubmit,
+.helpImproveCancel {
+  --border-width: 0;
+  padding: 0;
+  width: vh-calc(116);
+  height: vh-calc(42);
+  font-size: vh-calc(14);
+  line-height: vh-calc(33);
+}
+.helpImproveSubmit {
+  color: #fee8ac;
+  background: url('../../images/btn_active-min.png') no-repeat center / contain;
+}
+.helpImproveCancel {
+  color: #7c4b36;
+  background: url('../../images/btn_normal-min.png') no-repeat center / contain;
+}

+ 136 - 0
src/pages/A9knowlege/components/HelpImprove/index.tsx

@@ -0,0 +1,136 @@
+import React, { useState } from 'react'
+import styles from './index.module.scss'
+import { Radio } from 'antd'
+import { TextArea, Input, Button, Toast } from 'antd-mobile'
+import http from '@/utils/axios'
+
+const TYPE = ['回答不相关', '回答不完整', '答案错误', '无法回答问题', '其他']
+
+interface HelpImproveProps {
+  question: string
+  answer: string
+  onClose: () => void
+}
+
+function HelpImprove({ question, answer, onClose }: HelpImproveProps) {
+  const [type, setType] = useState<string>('')
+  const [suggest, setSuggest] = useState<string>('')
+  const [userName, setUserName] = useState<string>('')
+  const [phone, setPhone] = useState<string>('')
+  const [isSubmitting, setIsSubmitting] = useState(false)
+
+  const handleSubmit = async () => {
+    if (!type) {
+      Toast.show({
+        icon: 'fail',
+        content: '请选择反馈类型'
+      })
+      return
+    }
+
+    setIsSubmitting(true)
+    try {
+      const response = await http.post('/show/aiSave', {
+        answer,
+        phone,
+        question,
+        suggest,
+        type,
+        userName
+      })
+
+      if (response.code === 0) {
+        Toast.show({
+          icon: 'success',
+          content: '提交成功'
+        })
+        onClose()
+      }
+    } catch (error) {
+      console.error('提交失败:', error)
+    } finally {
+      setIsSubmitting(false)
+    }
+  }
+
+  const handleCancel = () => {
+    onClose()
+  }
+
+  return (
+    <div className={styles.helpImprove}>
+      <div className={styles.helpImproveContent}>
+        <h3>AI问答反馈</h3>
+
+        <div className={styles.helpImproveDialog}>
+          <div>
+            <span>原问题:</span>
+            <p>{question}</p>
+          </div>
+          <div>
+            <span>原回答:</span>
+            <p>{answer}</p>
+          </div>
+        </div>
+
+        <div className={styles.helpImproveType}>
+          <p>反馈类型</p>
+          <Radio.Group buttonStyle='outline' value={type} onChange={e => setType(e.target.value)}>
+            {TYPE.map(item => (
+              <Radio key={item} value={item}>
+                {item}
+              </Radio>
+            ))}
+          </Radio.Group>
+        </div>
+
+        <TextArea
+          className={styles.helpImproveTextarea}
+          placeholder='请输入您的建议,不超过200个字'
+          maxLength={200}
+          value={suggest}
+          onChange={val => setSuggest(val)}
+        />
+
+        <div className={styles.helpImproveLine}>
+          <label>您的称呼</label>
+          <Input
+            className={styles.helpImproveInput}
+            placeholder='请输入内容,不超过20个字'
+            maxLength={20}
+            value={userName}
+            onChange={val => setUserName(val)}
+          />
+        </div>
+        <div className={styles.helpImproveLine}>
+          <label>联系方式</label>
+          <Input
+            type='tel'
+            className={styles.helpImproveInput}
+            placeholder='请输入内容,不超过20个字'
+            maxLength={20}
+            value={phone}
+            onChange={val => setPhone(val)}
+          />
+        </div>
+
+        <div className={styles.helpImproveFooter}>
+          <Button
+            className={styles.helpImproveSubmit}
+            onClick={handleSubmit}
+            loading={isSubmitting}
+          >
+            提交
+          </Button>
+          <Button className={styles.helpImproveCancel} onClick={handleCancel}>
+            取消
+          </Button>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+const MemoHelpImprove = React.memo(HelpImprove)
+
+export default MemoHelpImprove

BIN
src/pages/A9knowlege/images/bg_jiaohu-min.jpg


BIN
src/pages/A9knowlege/images/btn_active-min.png


BIN
src/pages/A9knowlege/images/btn_normal-min.png


BIN
src/pages/A9knowlege/images/icon_left@2x-min.png


BIN
src/pages/A9knowlege/images/icon_right@2x-min.png


BIN
src/pages/A9knowlege/images/img_pop_02@2x-min.png


BIN
src/pages/A9knowlege/images/img_pop_2-min.png


BIN
src/pages/A9knowlege/images/level_01-min.png


BIN
src/pages/A9knowlege/images/level_02-min.png


BIN
src/pages/A9knowlege/images/level_03-min.png


BIN
src/pages/A9knowlege/images/level_04-min.png


BIN
src/pages/A9knowlege/images/level_05-min.png


BIN
src/pages/A9knowlege/images/level_06-min.png


+ 31 - 1
src/pages/A9knowlege/index.module.scss

@@ -26,6 +26,36 @@
   position: relative;
   width: 298px;
   height: 100%;
-  overflow: hidden;
   background: url('./images/img_pop_02-min.png') no-repeat center / cover;
+  transition: transform 0.3s ease, width 0.3s ease;
+
+  &.sidebarHidden {
+    width: 0;
+    transform: translateX(100%);
+  }
+}
+
+.sidebarHideBtn {
+  position: absolute;
+  top: 50%;
+  left: -16px;
+  width: 45px;
+  height: 45px;
+  transform: translateY(-50%);
+  transition: left 0.3s ease, width 0.3s ease;
+  background: url('./images/icon_right@2x-min.png') no-repeat center / cover;
+
+  &.hide {
+    left: -67px;
+    background-image: url('./images/icon_left@2x-min.png');
+  }
+}
+
+.loading {
+  position: absolute;
+  inset: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 20px;
 }

+ 282 - 31
src/pages/A9knowlege/index.tsx

@@ -6,11 +6,86 @@ import Panel2 from './components/Panel2'
 import ModelPanel from './components/ModelPanel'
 import { echPageBackFu } from '@/components/MenuSider/data'
 import Zback from '@/components/Zback'
-import ChartInput from './components/ChartInput'
+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'
+
+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)
+
+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
+  })
+}
+
+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() {
   // 是否显示返回按钮
   const [backShow, setBackShow] = useState(false)
+  // 控制 sidebar 显示
+  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')) {
@@ -21,8 +96,30 @@ function A9knowlege() {
   const echartRef = useRef<HTMLDivElement | null>(null)
   const chartInstance = useRef<any>(null)
   const [detail, setDetail] = useState<any>(null)
+  const [detailLoading, setDetailLoading] = useState(false)
 
-  const buildGraphFromKnowlege = (data: any[]) => {
+  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
@@ -32,14 +129,35 @@ function A9knowlege() {
     const traverse = (item: any, parentId: string | null, depth: number) => {
       const id = genId(item.label)
       const category = `level${depth}`
-      const symbolSize = depth === 0 ? 60 : depth === 1 ? 40 : 20
-      nodes.push({ id, name: item.label, category, symbolSize, raw: item })
-      if (parentId) links.push({ source: parentId, target: id })
+      const symbolSize = getLevelSize(depth)
+      const symbol = `image://${getLevelImage(depth)}`
+      // 保存 thumb 信息用于后续处理
+      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 match = (n.category || '').match(/level(\d+)/)
       const d = match ? Number(match[1]) : 0
@@ -50,12 +168,11 @@ function A9knowlege() {
     return { nodes, links, categories }
   }
 
-  const initChart = useCallback(() => {
+  const initChart = useCallback(async () => {
     // @ts-ignore
     const echarts = (window as any).echarts
-    // @ts-ignore
     const data = knowlegeData || []
-    if (!echarts || !echartRef.current) return
+    if (!echarts || !echartRef.current || dataLoading || data.length === 0) return
 
     if (chartInstance.current) {
       try {
@@ -70,7 +187,7 @@ function A9knowlege() {
     })
     chartInstance.current = myChart
 
-    const graph = buildGraphFromKnowlege(data)
+    const graph = await buildGraphFromKnowlege(data)
 
     const option = {
       tooltip: { show: false },
@@ -79,18 +196,66 @@ function A9knowlege() {
           name: 'KnowledgeGraph',
           type: 'graph',
           layout: 'force',
-          data: graph.nodes.map((n: any) => ({
-            ...n,
-            label: { show: true, formatter: n.name, fontSize: 12 }
-          })),
-          links: graph.links,
+          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
+              }
+            }
+          }),
+          links: graph.links.map((link: any) => {
+            const depth = link.targetDepth
+            let lineColor = '#FFE9B6'
+            if (depth === 3) {
+              lineColor = '#FF9807'
+            } else if (depth >= 4) {
+              lineColor = '#D1C9B2'
+            }
+            return {
+              ...link,
+              lineStyle: {
+                color: lineColor
+              }
+            }
+          }),
           categories: graph.categories,
           roam: true,
+          zoom: 0.5,
           scaleLimit: {
-            min: 0.5,
-            max: 3
+            min: 0.2,
+            max: 1.2
           },
-          label: { position: 'right' },
+          label: { position: 'bottom', distance: 6, align: 'center' },
           focusNodeAdjacency: true,
           emphasis: {
             focus: 'adjacency',
@@ -99,8 +264,15 @@ function A9knowlege() {
               opacity: 1
             }
           },
-          force: { repulsion: 200, edgeLength: [50, 120] },
-          lineStyle: { color: 'source', curveness: 0.2 },
+          // force 布局参数:调大 repulsion 让斥力更明显
+          // 降低 gravity 避免被拉回中心过紧
+          force: {
+            repulsion: 900,
+            edgeLength: [80, 80],
+            gravity: 0.05
+          },
+          // 连线为直线:curveness 设为 0
+          lineStyle: { curveness: 0 },
           edgeLabel: { show: false },
           edgeSymbol: ['none', 'none']
         }
@@ -122,6 +294,7 @@ function A9knowlege() {
       })
 
       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)
@@ -141,25 +314,83 @@ function A9knowlege() {
           }, 10)
         }
       } else {
-        // 点击的是节点
+        // 节点
         myChart.dispatchAction({
           type: 'highlight',
           dataIndex: params.dataIndex
         })
 
         const node = params.data
-        if (node && node.raw && node.raw.type) {
-          setDetail(node.raw)
+        if (node && node.raw && node.raw.id) {
+          if (currentId.current === node.raw.id) 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) {
+                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)
+                  } 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)
+                  } catch (e) {
+                    console.error('解析 rtf 失败:', e)
+                  }
+                } else {
+                  setDetail(null)
+                }
+              }
+            } catch (error) {
+              setDetail(null)
+            } finally {
+              setDetailLoading(false)
+            }
+          }
+          fetchDetail()
         }
       }
     })
     const resizeHandler = () => myChart.resize()
     window.addEventListener('resize', resizeHandler)
     ;(chartInstance.current as any)._resizeHandler = resizeHandler
-  }, [])
+  }, [knowlegeData, dataLoading])
+
+  const handleClosePanel = () => {
+    setDetail(null)
+    currentId.current = null
+  }
 
   useEffect(() => {
-    initChart()
+    if (!dataLoading && knowlegeData.length > 0) {
+      initChart()
+    }
     return () => {
       if (chartInstance.current) {
         try {
@@ -171,7 +402,16 @@ function A9knowlege() {
       }
     }
     // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [])
+  }, [knowlegeData, dataLoading, initChart])
+
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      if (chartInstance.current) {
+        chartInstance.current.resize()
+      }
+    }, 300)
+    return () => clearTimeout(timer)
+  }, [sidebarVisible])
 
   return (
     <div className={styles.A9knowlege}>
@@ -181,16 +421,27 @@ function A9knowlege() {
         <div id='echart-container' ref={echartRef} />
       </div>
 
-      <div className={styles.sidebar}>
-        {!detail ? (
+      <div className={`${styles.sidebar} ${!sidebarVisible ? styles.sidebarHidden : ''}`}>
+        {detailLoading ? (
+          <div className={styles.loading}>
+            <DotLoading color='#7c4b36' />
+          </div>
+        ) : !detail ? (
           <Chart />
-        ) : detail.type === 1 ? (
-          <Panel detail={detail} onClose={() => setDetail(null)} />
+        ) : detail.type === 'label' ? (
+          <Panel detail={detail} onClose={handleClosePanel} />
+        ) : detail.type === 'rtf' ? (
+          <Panel2 detail={detail} onClose={handleClosePanel} />
         ) : detail.type === 5 ? (
-          <ModelPanel detail={detail} onClose={() => setDetail(null)} />
+          <ModelPanel detail={detail} onClose={handleClosePanel} />
         ) : (
-          <Panel2 detail={detail} onClose={() => setDetail(null)} />
+          <Panel2 detail={detail} onClose={handleClosePanel} />
         )}
+
+        <div
+          className={`${styles.sidebarHideBtn} ${!sidebarVisible ? styles.hide : ''}`}
+          onClick={() => setSidebarVisible(!sidebarVisible)}
+        ></div>
       </div>
     </div>
   )