feat(index): add homepage with dashboard cards and knowledge graph support

- Add new Index view with dashboard layout and quick action cards
- Create TopCardList component to display core data management options
- Add GuideCard and VersionCard components for user guidance
- Add QuickActions component for common operations
- Create KnowledgeGraph and KnowledgeGraphCard components for knowledge base visualization
- Add comprehensive SVG assets for index page (apps, arrows, management icons)
- Add common API module for shared request utilities
- Extend knowledgeBase API with knowledge graph endpoints (getKnowledgeGraph, getKnowledgeGraphEntityTypes)
- Update i18n translations for English and Chinese with new index page strings
- Update routing configuration to include new Index route
- Update menu configuration to reflect new navigation structure
- Update KnowledgeBase CreateModal and Private view components
- Add TypeScript types for Index page components
- Improve overall UI/UX with new dashboard-style homepage
This commit is contained in:
yujiangping
2026-01-05 10:37:08 +08:00
parent ebd2abbfa0
commit f31341151f
40 changed files with 2165 additions and 64 deletions

View File

@@ -0,0 +1,451 @@
import React, { type FC, useEffect, useState, useRef, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { Col } from 'antd'
import RbCard from '@/components/RbCard/Card'
import ReactEcharts from 'echarts-for-react'
import zoom from '@/assets/images/userMemory/zoom.svg'
import drag from '@/assets/images/userMemory/drag.svg'
import pointer from '@/assets/images/userMemory/pointer.svg'
import empty from '@/assets/images/userMemory/empty.svg'
import Empty from '@/components/Empty'
// 知识图谱数据类型定义
export interface KnowledgeNode {
id: string
entity_name: string
entity_type: string
description: string
pagerank: number
source_id: string[]
// ECharts 需要的属性
name: string
category: number
symbolSize: number
itemStyle: {
color: string
}
}
export interface KnowledgeEdge {
src_id: string
tgt_id: string
description: string
keywords: string[]
weight: number
source_id: string[]
source: string
target: string
// ECharts 需要的属性
value: number
}
export interface KnowledgeGraphData {
directed: boolean
multigraph: boolean
graph: {
source_id: string[]
}
nodes: KnowledgeNode[]
edges: KnowledgeEdge[]
}
export interface KnowledgeGraphResponse {
graph: KnowledgeGraphData
mind_map: Record<string, unknown>
}
interface KnowledgeGraphProps {
data?: KnowledgeGraphResponse
loading?: boolean
}
const operations = [
{ name: 'click', icon: pointer },
{ name: 'drag', icon: drag },
{ name: 'zoom', icon: zoom },
]
// 预定义的颜色调色板
const colorPalette = [
'#155EEF', '#4DA8FF', '#9C6FFF', '#8BAEF7', '#369F21',
'#FF5D34', '#FF8A4C', '#FFB048', '#E74C3C', '#9B59B6',
'#3498DB', '#1ABC9C', '#F39C12', '#D35400', '#C0392B',
'#8E44AD', '#2980B9', '#16A085', '#F1C40F', '#E67E22'
]
// 动态生成实体类型颜色映射
const generateEntityTypeColors = (entityTypes: string[]): Record<string, string> => {
const colorMap: Record<string, string> = {}
entityTypes.forEach((type, index) => {
colorMap[type] = colorPalette[index % colorPalette.length]
})
return colorMap
}
const KnowledgeGraph: FC<KnowledgeGraphProps> = ({ data, loading = false }) => {
const { t } = useTranslation()
const chartRef = useRef<ReactEcharts>(null)
const resizeScheduledRef = useRef(false)
const modalRef = useRef<HTMLDivElement>(null)
const [nodes, setNodes] = useState<KnowledgeNode[]>([])
const [links, setLinks] = useState<KnowledgeEdge[]>([])
const [categories, setCategories] = useState<{ name: string }[]>([])
const [selectedNode, setSelectedNode] = useState<KnowledgeNode | null>(null)
const [entityTypeColors, setEntityTypeColors] = useState<Record<string, string>>({})
// 弹框拖动相关状态
const [modalPosition, setModalPosition] = useState({ x: 20, y: 20 })
const [isDragging, setIsDragging] = useState(false)
const [dragStart, setDragStart] = useState({ x: 0, y: 0 })
// 拖动处理函数
const handleMouseDown = useCallback((e: React.MouseEvent) => {
setIsDragging(true)
setDragStart({
x: e.clientX - modalPosition.x,
y: e.clientY - modalPosition.y
})
}, [modalPosition])
const handleMouseMove = useCallback((e: MouseEvent) => {
if (!isDragging) return
const newX = e.clientX - dragStart.x
const newY = e.clientY - dragStart.y
// 限制拖动范围,确保弹框不会超出容器
const container = chartRef.current?.getEchartsInstance().getDom().parentElement
if (container && modalRef.current) {
const containerRect = container.getBoundingClientRect()
const modalRect = modalRef.current.getBoundingClientRect()
const maxX = containerRect.width - modalRect.width
const maxY = containerRect.height - modalRect.height
setModalPosition({
x: Math.max(0, Math.min(newX, maxX)),
y: Math.max(0, Math.min(newY, maxY))
})
}
}, [isDragging, dragStart])
const handleMouseUp = useCallback(() => {
setIsDragging(false)
}, [])
// 添加全局鼠标事件监听
useEffect(() => {
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
return () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
}
}, [isDragging, handleMouseMove, handleMouseUp])
// 关闭弹框
const handleCloseModal = useCallback(() => {
setSelectedNode(null)
}, [])
// 处理知识图谱数据
const processGraphData = useCallback(() => {
if (!data?.graph) {
setNodes([])
setLinks([])
setCategories([])
setSelectedNode(null)
return
}
const { nodes: rawNodes, edges: rawEdges } = data.graph
const processedNodes: KnowledgeNode[] = []
const processedEdges: KnowledgeEdge[] = []
// 获取所有实体类型
const entityTypes = [...new Set(rawNodes.map(node => node.entity_type))]
const categoryMap = entityTypes.reduce((acc, type, index) => {
acc[type] = index
return acc
}, {} as Record<string, number>)
// 动态生成实体类型颜色映射
const dynamicEntityTypeColors = generateEntityTypeColors(entityTypes)
setEntityTypeColors(dynamicEntityTypeColors)
// 计算每个节点的连接数
const connectionCount: Record<string, number> = {}
rawEdges.forEach(edge => {
// 使用 src_id 和 tgt_id 计算连接数
connectionCount[edge.src_id] = (connectionCount[edge.src_id] || 0) + 1
connectionCount[edge.tgt_id] = (connectionCount[edge.tgt_id] || 0) + 1
})
// 处理节点数据
rawNodes.forEach(node => {
const connections = connectionCount[node.id] || 0
const categoryIndex = categoryMap[node.entity_type] || 0
// 根据 pagerank 和连接数计算节点大小
let symbolSize = Math.max(10, Math.min(50, node.pagerank * 200 + connections * 2))
processedNodes.push({
...node,
name: node.entity_name,
category: categoryIndex,
symbolSize,
itemStyle: {
color: dynamicEntityTypeColors[node.entity_type] || colorPalette[0]
}
})
})
// 处理边数据
rawEdges.forEach(edge => {
// 注意根据数据结构source 和 target 字段可能与 src_id 和 tgt_id 相反
// 我们使用 src_id 和 tgt_id 作为正确的连接关系
processedEdges.push({
...edge, // 保留所有原始字段
source: edge.src_id, // 使用 src_id 作为源节点
target: edge.tgt_id, // 使用 tgt_id 作为目标节点
value: edge.weight || 1
})
})
// 验证节点ID和边的连接
const nodeIds = new Set(processedNodes.map(n => n.id))
const validEdges = processedEdges.filter(edge => {
const sourceExists = nodeIds.has(edge.source)
const targetExists = nodeIds.has(edge.target)
if (!sourceExists || !targetExists) {
console.warn('Invalid edge:', edge, 'Source exists:', sourceExists, 'Target exists:', targetExists)
}
return sourceExists && targetExists
})
// 调试信息
console.log('Total nodes:', processedNodes.length)
console.log('Total edges:', processedEdges.length)
console.log('Valid edges:', validEdges.length)
console.log('Node IDs:', Array.from(nodeIds).slice(0, 5))
console.log('Edge sample:', validEdges.slice(0, 3))
// 设置分类
const processedCategories = entityTypes.map(type => ({ name: type }))
setNodes(processedNodes)
setLinks(validEdges) // 只使用有效的边
setCategories(processedCategories)
}, [data])
useEffect(() => {
processGraphData()
}, [processGraphData])
useEffect(() => {
const handleResize = () => {
if (chartRef.current && !resizeScheduledRef.current) {
resizeScheduledRef.current = true
requestAnimationFrame(() => {
chartRef.current?.getEchartsInstance().resize()
resizeScheduledRef.current = false
})
}
}
const resizeObserver = new ResizeObserver(handleResize)
const chartElement = chartRef.current?.getEchartsInstance().getDom().parentElement
if (chartElement) {
resizeObserver.observe(chartElement)
}
return () => {
resizeObserver.disconnect()
}
}, [nodes])
return (
<Col span={24}>
<RbCard
title={t('knowledgeBase.knowledgeGraph')}
headerType="borderless"
headerClassName="rb:text-[18px]! rb:leading-[24px]"
>
<div className="rb:h-124 rb:relative">
{loading ? (
<div className="rb:h-full rb:flex rb:items-center rb:justify-center">
<div className="rb:text-[#5B6167]">...</div>
</div>
) : nodes.length === 0 ? (
<Empty className="rb:h-full" />
) : (
<>
<ReactEcharts
ref={chartRef}
option={{
colors: Object.values(entityTypeColors),
tooltip: {
show: true,
formatter: (params: any) => {
if (params.dataType === 'node') {
const node = params.data as KnowledgeNode
return `
<div>
<div><strong>${node.entity_name}</strong></div>
<div>类型: ${node.entity_type}</div>
<div>重要度: ${(node.pagerank * 100).toFixed(2)}%</div>
</div>
`
} else if (params.dataType === 'edge') {
const edge = params.data as KnowledgeEdge
return `
<div>
<div><strong>关系</strong></div>
<div>权重: ${edge.weight}</div>
<div>${edge.description}</div>
</div>
`
}
return ''
}
},
legend: {
data: categories.map(cat => cat.name),
orient: 'vertical',
left: 'right',
top: 'center'
},
series: [
{
type: 'graph',
layout: 'force',
data: nodes,
links: links,
categories: categories,
roam: true,
label: {
show: true,
position: 'right',
formatter: '{b}',
fontSize: 12
},
lineStyle: {
color: '#5B6167',
curveness: 0.3,
width: 2, // 固定线宽,避免函数问题
opacity: 0.8
},
force: {
repulsion: 300,
edgeLength: 150,
gravity: 0.1,
layoutAnimation: true,
preventOverlap: true
},
selectedMode: 'single',
draggable: true,
animationDurationUpdate: 0,
select: {
itemStyle: {
borderWidth: 2,
borderColor: '#ffffff',
shadowBlur: 10,
}
},
emphasis: {
focus: 'adjacency',
lineStyle: {
width: 3
}
}
}
]
}}
style={{ height: '496px', width: '100%' }}
notMerge={false}
lazyUpdate={true}
onEvents={{
click: (params: { dataType: string; data: KnowledgeNode }) => {
if (params.dataType === 'node') {
console.log('Knowledge node clicked:', params.data)
setSelectedNode(params.data)
}
}
}}
/>
{/* 实体详情弹框 */}
{selectedNode && (
<div
ref={modalRef}
className="rb:absolute rb:bg-white rb:border rb:border-[#EBEBEB] rb:rounded-[12px] rb:shadow-lg rb:p-4 rb:w-80 rb:z-10"
style={{
left: modalPosition.x,
top: modalPosition.y,
cursor: isDragging ? 'grabbing' : 'grab'
}}
>
{/* 弹框头部 - 可拖动区域 */}
<div
className="rb:flex rb:items-center rb:justify-between rb:mb-3 rb:pb-2 rb:border-b rb:border-[#EBEBEB] rb:cursor-grab"
onMouseDown={handleMouseDown}
style={{ cursor: isDragging ? 'grabbing' : 'grab' }}
>
<div className="rb:text-[16px] rb:font-medium rb:text-[#1A1A1A]">
{t('knowledgeBase.entityDetails')}
</div>
<button
onClick={handleCloseModal}
className="rb:w-6 rb:h-6 rb:flex rb:items-center rb:justify-center rb:text-[#5B6167] hover:rb:text-[#1A1A1A] hover:rb:bg-[#F0F3F8] rb:rounded rb:transition-colors"
>
×
</button>
</div>
{/* 弹框内容 */}
<div>
<div className="rb:font-medium rb:mb-4">
<div className="rb:text-[16px] rb:mb-2">{selectedNode.entity_name}</div>
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mb-2">
<span className="rb:inline-block rb:px-2 rb:py-1 rb:bg-[#F0F3F8] rb:rounded rb:mr-2">
{selectedNode.entity_type}
</span>
<span>: {(selectedNode.pagerank * 100).toFixed(2)}%</span>
</div>
</div>
<div className="rb:font-medium rb:mb-4">
{t('knowledgeBase.entityDescription')}
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2 rb:leading-5">
{selectedNode.description}
</div>
</div>
<div className="rb:font-medium rb:mb-2">
{t('knowledgeBase.sourceDocuments')}
<div className="rb:text-[12px] rb:text-[#5B6167] rb:mt-2">
{selectedNode.source_id.length}
</div>
</div>
</div>
</div>
)}
</>
)}
</div>
<div className="rb:bg-[#F0F3F8] rb:flex rb:items-center rb:gap-6 rb:rounded-[0px_0px_12px_12px] rb:p-[14px_40px] rb:m-[0_-20px_-16px_-16px]">
{operations.map((item) => (
<div key={item.name} className="rb:flex rb:items-center rb:text-[#5B6167] rb:leading-5">
<img src={item.icon} className="rb:w-5 rb:h-5 rb:mr-1" />
{t(`userMemory.${item.name}`)}
</div>
))}
</div>
</RbCard>
</Col>
)
}
export default React.memo(KnowledgeGraph)