You've already forked DataMate
核心功能: - 实体/关系编辑表单(创建/更新/删除) - 批量操作(批量删除节点/边) - 审核流程(提交审核 → 待审核列表 → 通过/拒绝) - 编辑模式切换(查看/编辑模式) - 权限控制(knowledgeGraphWrite 权限) 新增文件(后端,9 个): - EditReview.java - 审核记录领域模型(Neo4j 节点) - EditReviewRepository.java - 审核记录仓储(CRUD + 分页查询) - EditReviewService.java - 审核业务服务(提交/通过/拒绝,通过时自动执行变更) - EditReviewController.java - REST API(POST submit, POST approve/reject, GET pending) - DTOs: SubmitReviewRequest, EditReviewVO, ReviewActionRequest, BatchDeleteRequest - EditReviewServiceTest.java - 单元测试(21 tests) - EditReviewControllerTest.java - 集成测试(10 tests) 新增文件(前端,3 个): - EntityEditForm.tsx - 实体创建/编辑表单(Modal,支持名称/类型/描述/别名/置信度) - RelationEditForm.tsx - 关系创建/编辑表单(Modal,支持源/目标实体搜索、关系类型、权重/置信度) - ReviewPanel.tsx - 审核面板(待审核列表,通过/拒绝操作,拒绝带备注) 修改文件(后端,7 个): - GraphEntityService.java - 新增 batchDeleteEntities(),updateEntity 支持 confidence - GraphRelationService.java - 新增 batchDeleteRelations() - GraphEntityController.java - 删除批量删除端点(改为审核流程) - GraphRelationController.java - 删除批量删除端点(改为审核流程) - UpdateEntityRequest.java - 添加 confidence 字段 - KnowledgeGraphErrorCode.java - 新增 REVIEW_NOT_FOUND、REVIEW_ALREADY_PROCESSED - PermissionRuleMatcher.java - 添加 /api/knowledge-graph/** 写操作权限规则 修改文件(前端,8 个): - knowledge-graph.model.ts - 新增 EditReviewVO、ReviewOperationType、ReviewStatus 类型 - knowledge-graph.api.ts - BASE 改为 /api/knowledge-graph(走网关权限链),新增审核相关 API,删除批量删除直删方法 - vite.config.ts - 更新 dev proxy 路径 - NodeDetail.tsx - 新增 editMode 属性,编辑模式下显示编辑/删除按钮 - RelationDetail.tsx - 新增 editMode 属性,编辑模式下显示编辑/删除按钮 - KnowledgeGraphPage.tsx - 新增编辑模式开关(需要 knowledgeGraphWrite 权限)、创建实体/关系工具栏按钮、审核 Tab、批量操作 - GraphCanvas.tsx - 支持多选(editMode 时)、onSelectionChange 回调 - graphConfig.ts - 支持 multiSelect 参数 审核流程: - 所有编辑操作(创建/更新/删除/批量删除)都通过 submitReview 提交审核 - 审核通过后,EditReviewService.applyChange() 自动执行变更 - 批量删除端点已删除,只能通过审核流程 权限控制: - API 路径从 /knowledge-graph 改为 /api/knowledge-graph,走网关权限链 - 编辑模式开关需要 knowledgeGraphWrite 权限 - PermissionRuleMatcher 添加 /api/knowledge-graph/** 写操作规则 Bug 修复(Codex 审查后修复): - P0: 权限绕过(API 路径改为 /api/knowledge-graph) - P1: 审核流程未接入(所有编辑操作改为 submitReview) - P1: 批量删除绕过审核(删除直删端点,改为审核流程) - P1: confidence 字段丢失(UpdateEntityRequest 添加 confidence) - P2: 审核提交校验不足(添加跨字段校验器) - P2: 批量删除安全(添加 @Size(max=100) 限制,收集失败 ID) - P2: 前端错误处理(分开处理表单校验和 API 失败) 测试结果: - 后端: 311 tests pass ✅ (280 → 311, +31 new) - 前端: eslint clean ✅, tsc clean ✅, vite build success ✅
168 lines
5.4 KiB
TypeScript
168 lines
5.4 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { Drawer, Descriptions, Tag, Spin, Empty, Button, Popconfirm, Space, message } from "antd";
|
|
import { Pencil, Trash2 } from "lucide-react";
|
|
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;
|
|
editMode?: boolean;
|
|
onClose: () => void;
|
|
onEntityNavigate: (entityId: string) => void;
|
|
onEditRelation?: (relation: RelationVO) => void;
|
|
onDeleteRelation?: (relationId: string) => void;
|
|
}
|
|
|
|
export default function RelationDetail({
|
|
graphId,
|
|
relationId,
|
|
open,
|
|
editMode = false,
|
|
onClose,
|
|
onEntityNavigate,
|
|
onEditRelation,
|
|
onDeleteRelation,
|
|
}: 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]);
|
|
|
|
const handleDelete = () => {
|
|
if (relationId) {
|
|
onDeleteRelation?.(relationId);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Drawer
|
|
title="关系详情"
|
|
open={open}
|
|
onClose={onClose}
|
|
width={400}
|
|
extra={
|
|
editMode && relation && (
|
|
<Space>
|
|
<Button
|
|
size="small"
|
|
icon={<Pencil className="w-3 h-3" />}
|
|
onClick={() => onEditRelation?.(relation)}
|
|
>
|
|
编辑
|
|
</Button>
|
|
<Popconfirm
|
|
title="确认删除此关系?"
|
|
onConfirm={handleDelete}
|
|
okText="确认"
|
|
cancelText="取消"
|
|
>
|
|
<Button
|
|
size="small"
|
|
danger
|
|
icon={<Trash2 className="w-3 h-3" />}
|
|
>
|
|
删除
|
|
</Button>
|
|
</Popconfirm>
|
|
</Space>
|
|
)
|
|
}
|
|
>
|
|
<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>
|
|
);
|
|
}
|