You've already forked DataMate
核心功能: - G6 v5 力导向图,支持交互式缩放、平移、拖拽 - 5 种布局模式:force, circular, grid, radial, concentric - 双击展开节点邻居到图中(增量探索) - 全文搜索,类型过滤,结果高亮(变暗/高亮状态) - 节点详情抽屉:实体属性、别名、置信度、关系列表(可导航) - 关系详情抽屉:类型、源/目标、权重、置信度、属性 - 查询构建器:最短路径/全路径查询,可配置 maxDepth/maxPaths - 基于 UUID 的图加载(输入或 URL 参数 ?graphId=...) - 大图性能优化(200 节点阈值,超过时禁用动画) 新增文件(13 个): - knowledge-graph.model.ts - TypeScript 接口,匹配 Java DTOs - knowledge-graph.api.ts - API 服务,包含所有 KG REST 端点 - knowledge-graph.const.ts - 实体类型颜色、关系类型标签、中文显示名称 - graphTransform.ts - 后端数据 → G6 节点/边格式转换 + 合并工具 - graphConfig.ts - G6 v5 图配置(节点/边样式、行为、布局) - hooks/useGraphData.ts - 数据钩子:加载子图、展开节点、搜索、合并 - hooks/useGraphLayout.ts - 布局钩子:5 种布局类型 - components/GraphCanvas.tsx - G6 v5 画布,力导向布局,缩放/平移/拖拽 - components/SearchPanel.tsx - 全文实体搜索,类型过滤 - components/NodeDetail.tsx - 实体详情抽屉 - components/RelationDetail.tsx - 关系详情抽屉 - components/QueryBuilder.tsx - 路径查询构建器 - Home/KnowledgeGraphPage.tsx - 主页面,整合所有组件 修改文件(5 个): - package.json - 添加 @antv/g6 v5 依赖 - vite.config.ts - 添加 /knowledge-graph 代理规则 - auth/permissions.ts - 添加 knowledgeGraphRead/knowledgeGraphWrite - pages/Layout/menu.tsx - 添加知识图谱菜单项(Network 图标) - routes/routes.ts - 添加 /data/knowledge-graph 路由 新增文档(10 个): - docs/knowledge-graph/ - 完整的知识图谱设计文档 Bug 修复(Codex 审查后修复): - P1: 详情抽屉状态与选中状态不一致(显示旧数据) - P1: 查询构建器未实现(最短路径/多路径查询) - P2: 实体类型映射 Organization → Org(匹配后端) - P2: getSubgraph depth 参数无效(改用正确端点) - P2: AllPathsVO 字段名不一致(totalPaths → pathCount) - P2: 搜索取消逻辑无效(传递 AbortController.signal) - P2: 大图性能优化(动画降级) - P3: 移除未使用的类型导入 构建验证: - tsc --noEmit ✅ clean - eslint ✅ 0 errors/warnings - vite build ✅ successful
123 lines
4.3 KiB
TypeScript
123 lines
4.3 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Drawer, Descriptions, Tag, Spin, Empty, message } from "antd";
|
|
import type { RelationVO } from "../knowledge-graph.model";
|
|
import {
|
|
ENTITY_TYPE_LABELS,
|
|
ENTITY_TYPE_COLORS,
|
|
DEFAULT_ENTITY_COLOR,
|
|
RELATION_TYPE_LABELS,
|
|
} from "../knowledge-graph.const";
|
|
import * as api from "../knowledge-graph.api";
|
|
|
|
interface RelationDetailProps {
|
|
graphId: string;
|
|
relationId: string | null;
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onEntityNavigate: (entityId: string) => void;
|
|
}
|
|
|
|
export default function RelationDetail({
|
|
graphId,
|
|
relationId,
|
|
open,
|
|
onClose,
|
|
onEntityNavigate,
|
|
}: RelationDetailProps) {
|
|
const [relation, setRelation] = useState<RelationVO | null>(null);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (!relationId || !graphId) {
|
|
setRelation(null);
|
|
return;
|
|
}
|
|
if (!open) return;
|
|
|
|
setLoading(true);
|
|
api
|
|
.getRelation(graphId, relationId)
|
|
.then((data) => setRelation(data))
|
|
.catch(() => message.error("加载关系详情失败"))
|
|
.finally(() => setLoading(false));
|
|
}, [graphId, relationId, open]);
|
|
|
|
return (
|
|
<Drawer title="关系详情" open={open} onClose={onClose} width={400}>
|
|
<Spin spinning={loading}>
|
|
{relation ? (
|
|
<div className="flex flex-col gap-4">
|
|
<Descriptions column={1} size="small" bordered>
|
|
<Descriptions.Item label="关系类型">
|
|
<Tag color="blue">
|
|
{RELATION_TYPE_LABELS[relation.relationType] ?? relation.relationType}
|
|
</Tag>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="源实体">
|
|
<div className="flex items-center gap-1.5">
|
|
<Tag
|
|
color={
|
|
ENTITY_TYPE_COLORS[relation.sourceEntityType] ?? DEFAULT_ENTITY_COLOR
|
|
}
|
|
>
|
|
{ENTITY_TYPE_LABELS[relation.sourceEntityType] ?? relation.sourceEntityType}
|
|
</Tag>
|
|
<a
|
|
className="text-blue-500 cursor-pointer hover:underline"
|
|
onClick={() => onEntityNavigate(relation.sourceEntityId)}
|
|
>
|
|
{relation.sourceEntityName}
|
|
</a>
|
|
</div>
|
|
</Descriptions.Item>
|
|
<Descriptions.Item label="目标实体">
|
|
<div className="flex items-center gap-1.5">
|
|
<Tag
|
|
color={
|
|
ENTITY_TYPE_COLORS[relation.targetEntityType] ?? DEFAULT_ENTITY_COLOR
|
|
}
|
|
>
|
|
{ENTITY_TYPE_LABELS[relation.targetEntityType] ?? relation.targetEntityType}
|
|
</Tag>
|
|
<a
|
|
className="text-blue-500 cursor-pointer hover:underline"
|
|
onClick={() => onEntityNavigate(relation.targetEntityId)}
|
|
>
|
|
{relation.targetEntityName}
|
|
</a>
|
|
</div>
|
|
</Descriptions.Item>
|
|
{relation.weight != null && (
|
|
<Descriptions.Item label="权重">{relation.weight}</Descriptions.Item>
|
|
)}
|
|
{relation.confidence != null && (
|
|
<Descriptions.Item label="置信度">
|
|
{(relation.confidence * 100).toFixed(0)}%
|
|
</Descriptions.Item>
|
|
)}
|
|
{relation.createdAt && (
|
|
<Descriptions.Item label="创建时间">{relation.createdAt}</Descriptions.Item>
|
|
)}
|
|
</Descriptions>
|
|
|
|
{relation.properties && Object.keys(relation.properties).length > 0 && (
|
|
<>
|
|
<h4 className="font-medium text-sm">扩展属性</h4>
|
|
<Descriptions column={1} size="small" bordered>
|
|
{Object.entries(relation.properties).map(([key, value]) => (
|
|
<Descriptions.Item key={key} label={key}>
|
|
{String(value)}
|
|
</Descriptions.Item>
|
|
))}
|
|
</Descriptions>
|
|
</>
|
|
)}
|
|
</div>
|
|
) : !loading ? (
|
|
<Empty description="选择一条边查看详情" />
|
|
) : null}
|
|
</Spin>
|
|
</Drawer>
|
|
);
|
|
}
|