diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e83c115 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +data/neo4j.dump diff --git a/README.md b/README.md index 576e3f4..3215d24 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,37 @@ -➜ 3022202376 tree +# 知识图谱管理系统 -. +## 环境要求 + jdk 11 + nodejs 22.19.0 + angular_cli 20.3.2 + neo4j 4.4 + express 4.18.2 + AntV_G6 4.3.3 -├── code code of project +## 安装依赖 +在 ./code/backend 目录运行 -├── data data set + npm install + npm install express@4.18.2 + npm install --save @antv/g6@4.3.3 -├── member_info.txt information of group members +## 加载数据 +1. 确保neo4j处于停机状态 +2. 在neo4j/bin目录下运行如下命令 -├── report.mp4 report videm + neo4j-admin load --from="" --database=neo4j --force -└── report.ppt report slides +## 启动服务 +1. 在neo4j的/bin目录运行 + + neo4j.bat console + +2. 在./code/backend目录运行 + + npm run dev + +3. 在./code/frontend目录运行 + + python -m http.server 8000 + +4. 从8000端口访问管理界面 \ No newline at end of file diff --git a/code/backend/README.md b/code/backend/README.md deleted file mode 100644 index c25d301..0000000 --- a/code/backend/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# 知识图谱后端服务 - -## 环境要求 - jdk 11 - nodejs 22.19.0 - angular_cli 20.3.2 - neo4j 4.4 - express 4.18.2 - - -## 安装依赖 -在 ./code/backend 目录运行 - - npm install - npm install express@4.18.2 - -## 启动服务 -在neo4j的/bin目录运行 - - neo4j.bat start - -./code/backend目录运行 - - npm run dev - -./code/frontend目录运行 - - python -m http.server 8000 \ No newline at end of file diff --git a/code/backend/routes/edges.js b/code/backend/routes/edges.js index 418d3be..175d388 100644 --- a/code/backend/routes/edges.js +++ b/code/backend/routes/edges.js @@ -1,4 +1,3 @@ -// routes/edges.js const express = require('express'); const router = express.Router(); const edgeService = require('../services/edge.service'); @@ -11,22 +10,22 @@ const edgeService = require('../services/edge.service'); router.get('/', async (req, res) => { try { const { - limit = 100, + limit = 10000, skip = 0, type } = req.query; + console.log(`边路由接收参数: limit=${limit}, skip=${skip}, type=${type}`); + let edges; if (type) { - // 如果提供了类型参数,按类型过滤 edges = await edgeService.getEdgesByType( type, parseInt(limit), parseInt(skip) ); } else { - // 否则获取所有关系 edges = await edgeService.getAllEdges( parseInt(limit), parseInt(skip) @@ -36,7 +35,12 @@ router.get('/', async (req, res) => { res.json({ success: true, count: edges.length, - data: edges + data: edges, + meta: { + limit: parseInt(limit), + skip: parseInt(skip), + total: edges.length + } }); } catch (error) { console.error('获取关系列表失败:', error); @@ -88,7 +92,6 @@ router.post('/', async (req, res) => { try { const { startNodeId, endNodeId, type, properties } = req.body; - // 验证必要参数 if (!startNodeId || !endNodeId || !type) { return res.status(400).json({ success: false, @@ -128,7 +131,6 @@ router.put('/:id', async (req, res) => { const { id } = req.params; const { properties } = req.body; - // 验证必要参数 if (!properties) { return res.status(400).json({ success: false, @@ -169,6 +171,8 @@ router.put('/:id', async (req, res) => { router.delete('/:id', async (req, res) => { try { const { id } = req.params; + console.log(`删除关系路由: ID=${id}, 类型=${typeof id}`); + const deleted = await edgeService.deleteEdge(id); if (!deleted) { @@ -200,7 +204,7 @@ router.delete('/:id', async (req, res) => { router.get('/type/:type', async (req, res) => { try { const { type } = req.params; - const { limit = 100, skip = 0 } = req.query; + const { limit = 10000, skip = 0 } = req.query; const edges = await edgeService.getEdgesByType( type, @@ -233,11 +237,10 @@ router.get('/node/:nodeId', async (req, res) => { const { nodeId } = req.params; const { direction = 'both', - limit = 100, + limit = 10000, skip = 0 } = req.query; - // 验证方向参数 const validDirections = ['incoming', 'outgoing', 'both']; if (!validDirections.includes(direction)) { return res.status(400).json({ diff --git a/code/backend/routes/index.js b/code/backend/routes/index.js index 9c74d0a..e1ea4a1 100644 --- a/code/backend/routes/index.js +++ b/code/backend/routes/index.js @@ -1,11 +1,9 @@ const express = require('express'); const router = express.Router(); -// 引入各个子路由模块 const nodesRouter = require('./nodes'); const edgesRouter = require('./edges'); -// 根路径欢迎信息 router.get('/', (req, res) => { res.json({ success: true, @@ -20,7 +18,6 @@ router.get('/', (req, res) => { }); }); -// 健康检查端点 router.get('/health', (req, res) => { res.json({ success: true, @@ -30,7 +27,6 @@ router.get('/health', (req, res) => { }); }); -// API信息端点 router.get('/api', (req, res) => { res.json({ success: true, @@ -59,14 +55,10 @@ router.get('/api', (req, res) => { }); }); -// 使用节点路由 router.use('/nodes', nodesRouter); -// 使用边路由 router.use('/edges', edgesRouter); -// 404处理 - 使用更兼容的方式处理不存在的API端点 -// 为所有可能的路径定义处理程序,而不是使用通配符 router.get('*', (req, res) => { handle404(req, res); }); @@ -83,7 +75,6 @@ router.delete('*', (req, res) => { handle404(req, res); }); -// 404处理函数 function handle404(req, res) { res.status(404).json({ success: false, @@ -98,7 +89,6 @@ function handle404(req, res) { }); } -// 错误处理中间件 router.use((err, req, res, next) => { console.error('服务器错误:', err); res.status(500).json({ diff --git a/code/backend/routes/nodes.js b/code/backend/routes/nodes.js index 8d715f7..81fa9b9 100644 --- a/code/backend/routes/nodes.js +++ b/code/backend/routes/nodes.js @@ -1,4 +1,3 @@ -// routes/nodes.js const express = require('express'); const router = express.Router(); const nodeService = require('../services/node.service'); @@ -11,11 +10,14 @@ const nodeService = require('../services/node.service'); router.get('/', async (req, res) => { try { const { - limit = 100, + limit = 10000, // 修改默认值为10000 skip = 0, label } = req.query; + // 添加调试日志 + console.log(`节点路由接收参数: limit=${limit}, skip=${skip}, label=${label}`); + let nodes; if (label) { @@ -36,7 +38,12 @@ router.get('/', async (req, res) => { res.json({ success: true, count: nodes.length, - data: nodes + data: nodes, + meta: { + limit: parseInt(limit), + skip: parseInt(skip), + total: nodes.length + } }); } catch (error) { console.error('获取节点列表失败:', error); @@ -123,7 +130,6 @@ router.put('/:id', async (req, res) => { const { id } = req.params; const { properties } = req.body; - // 验证必要参数 if (!properties) { return res.status(400).json({ success: false, @@ -135,8 +141,8 @@ router.put('/:id', async (req, res) => { res.json({ success: true, - message: '节点更新成功', - data: node + message: '节点删除成功', + deletedCount: 1 // 明确返回删除的数量 }); } catch (error) { console.error('更新节点失败:', error); @@ -195,7 +201,7 @@ router.delete('/:id', async (req, res) => { router.get('/search/:key/:value', async (req, res) => { try { const { key, value } = req.params; - const { limit = 100, skip = 0 } = req.query; + const { limit = 10000, skip = 0 } = req.query; const nodes = await nodeService.searchNodes( key, @@ -227,7 +233,7 @@ router.get('/search/:key/:value', async (req, res) => { router.get('/label/:label', async (req, res) => { try { const { label } = req.params; - const { limit = 100, skip = 0 } = req.query; + const { limit = 10000, skip = 0 } = req.query; const nodes = await nodeService.getNodesByLabel( label, diff --git a/code/backend/server.js b/code/backend/server.js index 442c8b6..bd48c79 100644 --- a/code/backend/server.js +++ b/code/backend/server.js @@ -1,7 +1,7 @@ -require('dotenv').config(); // 加载.env环境变量 +require('dotenv').config(); const express = require('express'); const cors = require('cors'); -const routes = require('./routes'); // 导入所有路由 +const routes = require('./routes'); const app = express(); const PORT = process.env.PORT || 3000; @@ -28,8 +28,7 @@ app.use(cors({ })); app.use(express.json()); // 解析JSON请求体 -// 路由 -app.use('/api', routes); // 所有API路由都以/api开头 +app.use('/api', routes); // 全局错误处理中间件 app.use((err, req, res, next) => { @@ -41,7 +40,6 @@ app.use((err, req, res, next) => { }); }); -// 启动服务器 app.listen(PORT, () => { console.log(`后端服务器已启动在 http://localhost:${PORT}`); }); \ No newline at end of file diff --git a/code/backend/services/edge.service.js b/code/backend/services/edge.service.js index 045e8ab..014d5b1 100644 --- a/code/backend/services/edge.service.js +++ b/code/backend/services/edge.service.js @@ -13,21 +13,35 @@ class EdgeService { * @param {number} skip - 跳过数量 * @returns {Promise} 关系列表 */ - async getAllEdges(limit = 100, skip = 0) { + /** + * 获取所有关系(带极高上限) + * @param {number} limit - 每页数量(前端控制) + * @param {number} skip - 跳过数量 + * @returns {Promise} 关系列表 + */ + async getAllEdges(limit = 10000, skip = 0) { try { + // 强制使用传入的参数 + const actualLimit = limit; + const actualSkip = skip; + + // 设置安全上限 + const safeLimit = Math.min(actualLimit, 10000); + const query = ` - MATCH ()-[r]->() - RETURN r, startNode(r) as start, endNode(r) as end - SKIP $skip - LIMIT $limit - `; + MATCH ()-[r]->() + RETURN r, startNode(r) as start, endNode(r) as end + SKIP $skip + LIMIT $limit + `; - // const params = { skip, limit }; - // 确保参数是整数 const params = { - skip: int(parseInt(skip)), - limit: int(parseInt(limit)) + skip: int(parseInt(actualSkip)), + limit: int(parseInt(safeLimit)) }; + + console.log(`边服务: skip=${actualSkip}, limit=${safeLimit} (传入limit=${limit})`); + const result = await neo4j.read(query, params); return result.records.map(record => { @@ -49,24 +63,45 @@ class EdgeService { */ async getEdgeById(id) { try { + console.log(`边服务: 根据ID获取关系, ID: ${id}`); + + // 改进查询,确保返回统一的ID格式 const query = ` - MATCH ()-[r {id: $id}]->() - RETURN r, startNode(r) as start, endNode(r) as end - `; + MATCH (start)-[r]->(end) + WHERE r.id = $id + RETURN + r.id as id, + type(r) as type, + properties(r) as properties, + COALESCE(start.id, toString(ID(start))) as startNodeId, + COALESCE(end.id, toString(ID(end))) as endNodeId, + labels(start) as startLabels, + labels(end) as endLabels + `; + + const params = { id: String(id) }; - const params = { id }; const result = await neo4j.read(query, params); if (result.records.length === 0) { + console.log(`未找到关系,ID: ${id}`); return null; } const record = result.records[0]; - const relationship = record.get('r'); - const startNode = record.get('start'); - const endNode = record.get('end'); - - return this.formatEdge(relationship, startNode, endNode); + return { + id: String(record.get('id')), + type: record.get('type'), + properties: record.get('properties') || {}, + startNode: { + id: String(record.get('startNodeId')), + labels: record.get('startLabels') || [] + }, + endNode: { + id: String(record.get('endNodeId')), + labels: record.get('endLabels') || [] + } + }; } catch (error) { console.error('根据ID获取关系失败:', error); throw error; @@ -80,7 +115,7 @@ class EdgeService { * @param {number} skip - 跳过数量 * @returns {Promise} 关系列表 */ - async getEdgesByType(type, limit = 100, skip = 0) { + async getEdgesByType(type, limit = 10000, skip = 0) { try { const query = ` MATCH ()-[r:${type}]->() @@ -89,7 +124,6 @@ class EdgeService { LIMIT $limit `; - // const params = { skip, limit }; // 确保参数是整数 const params = { skip: int(parseInt(skip)), @@ -119,7 +153,6 @@ class EdgeService { */ async createEdge(startNodeId, endNodeId, type, properties = {}) { try { - // 生成唯一ID const id = uuidv4(); const edgeProperties = { ...properties, id }; @@ -161,13 +194,24 @@ class EdgeService { */ async updateEdge(id, properties) { try { + console.log(`更新关系服务: ID=${id}, 类型=${typeof id}`); + + // 使用与deleteEdge相同的查询逻辑,同时支持字符串ID和Neo4j内部ID const query = ` - MATCH ()-[r {id: $id}]->() + MATCH ()-[r]->() + WHERE r.id = $id OR ID(r) = $neo4jId SET r += $properties RETURN r, startNode(r) as start, endNode(r) as end `; - const params = { id, properties }; + const params = { + id: id, + neo4jId: int(parseInt(id)), // 同时尝试作为Neo4j内部ID + properties: properties + }; + + console.log('更新关系参数:', params); + const result = await neo4j.write(query, params); if (result.records.length === 0) { @@ -191,18 +235,31 @@ class EdgeService { * @param {string} id - 关系ID * @returns {Promise} 是否成功删除 */ + // 在 edge.service.js 中修改 deleteEdge 方法 async deleteEdge(id) { try { + console.log(`删除关系服务: ID=${id}, 类型=${typeof id}`); + + // 使用更灵活的查询,同时支持字符串ID和Neo4j内部ID const query = ` - MATCH ()-[r {id: $id}]->() + MATCH ()-[r]->() + WHERE r.id = $id OR ID(r) = $neo4jId DELETE r - `; + `; + + const params = { + id: id, + neo4jId: int(parseInt(id)) // 同时尝试作为Neo4j内部ID + }; + + console.log('删除关系参数:', params); - const params = { id }; const result = await neo4j.write(query, params); + const deletedCount = result.summary.counters.updates().relationshipsDeleted; + + console.log(`删除关系结果: 删除了 ${deletedCount} 个关系`); - // 检查是否有关系被删除 - return result.summary.counters.updates().relationshipsDeleted > 0; + return deletedCount > 0; } catch (error) { console.error('删除关系失败:', error); throw error; @@ -217,7 +274,7 @@ class EdgeService { * @param {number} skip - 跳过数量 * @returns {Promise} 关系列表 */ - async getNodeEdges(nodeId, direction = 'both', limit = 100, skip = 0) { + async getNodeEdges(nodeId, direction = 'both', limit = 10000, skip = 0) { try { let pattern; @@ -240,11 +297,6 @@ class EdgeService { LIMIT $limit `; - // const params = { - // nodeId, - // skip, - // limit - // }; // 确保参数是整数 const params = { nodeId, @@ -274,26 +326,43 @@ class EdgeService { * @param {Object} endNode - 目标节点 * @returns {Object} 格式化后的关系 */ + /** + * 格式化关系数据,确保节点ID格式统一 + */ formatEdge(relationship, startNode, endNode) { + // 统一的ID提取逻辑 + const getNodeId = (node) => { + if (node.properties && node.properties.id) { + return String(node.properties.id); // 优先使用自定义ID + } + return node.identity.toString(); // 回退到Neo4j内部ID + }; + + const getNodeLabels = (node) => { + return node.labels || []; + }; + + const getNodeProperties = (node) => { + return node.properties || {}; + }; + return { - id: relationship.properties.id || relationship.identity.toString(), + id: relationship.properties?.id || relationship.identity.toString(), type: relationship.type, - properties: relationship.properties, + properties: relationship.properties || {}, startNode: { - id: startNode.properties.id || startNode.identity.toString(), - labels: startNode.labels, - properties: startNode.properties + id: getNodeId(startNode), + labels: getNodeLabels(startNode), + properties: getNodeProperties(startNode) }, endNode: { - id: endNode.properties.id || endNode.identity.toString(), - labels: endNode.labels, - properties: endNode.properties + id: getNodeId(endNode), + labels: getNodeLabels(endNode), + properties: getNodeProperties(endNode) }, - // 如果需要,可以添加Neo4j内部ID neo4jId: relationship.identity.toString() }; } } -// 导出单例实例 module.exports = new EdgeService(); \ No newline at end of file diff --git a/code/backend/services/neo4j.js b/code/backend/services/neo4j.js index 3d15118..1b5e152 100644 --- a/code/backend/services/neo4j.js +++ b/code/backend/services/neo4j.js @@ -2,18 +2,15 @@ const neo4j = require('neo4j-driver'); require('dotenv').config(); // 确保加载环境变量 -// 数据库连接配置 - 使用默认的本地Neo4j设置 const URI = process.env.NEO4J_URI; const USER = process.env.NEO4J_USER; const PASSWORD = process.env.NEO4J_PASSWORD; -// 验证必要的环境变量 if (!URI || !USER || !PASSWORD) { console.error('缺少必要的数据库连接环境变量'); process.exit(1); } -// 创建驱动实例 let driver; /** @@ -28,7 +25,6 @@ function initDriver() { } catch (error) { console.error('Neo4j驱动初始化失败:', error); process.exit(1); // 退出进程 - // throw error; } } @@ -83,10 +79,8 @@ async function write(query, params = {}) { } } -// 初始化驱动 initDriver(); -// 导出功能 module.exports = { getDriver: () => driver, closeDriver: async () => { @@ -111,6 +105,6 @@ module.exports = { await session.close(); } }, - int: neo4j.int, // 导出neo4j.int函数 - isInt: neo4j.isInt // 导出neo4j.isInt函数 + int: neo4j.int, + isInt: neo4j.isInt }; \ No newline at end of file diff --git a/code/backend/services/node.service.js b/code/backend/services/node.service.js index 9f25bca..f72fcf8 100644 --- a/code/backend/services/node.service.js +++ b/code/backend/services/node.service.js @@ -8,26 +8,32 @@ const { v4: uuidv4 } = require('uuid'); // 用于生成唯一ID */ class NodeService { /** - * 获取所有节点(带分页) - * @param {number} limit - 每页数量 + * 获取所有节点(带极高上限) + * @param {number} limit - 每页数量(前端控制) * @param {number} skip - 跳过数量 * @returns {Promise} 节点列表 */ - async getAllNodes(limit = 100, skip = 0) { + async getAllNodes(limit = 10000, skip = 0) { try { + const actualLimit = limit; + const actualSkip = skip; + + const safeLimit = Math.min(actualLimit, 10000); + const query = ` - MATCH (n) - RETURN n - SKIP $skip - LIMIT $limit - `; + MATCH (n) + RETURN n + SKIP $skip + LIMIT $limit + `; - // const params = { skip, limit }; - // 确保参数是整数 const params = { - skip: int(parseInt(skip)), - limit: int(parseInt(limit)) + skip: int(parseInt(actualSkip)), + limit: int(parseInt(safeLimit)) }; + + console.log(`节点服务: skip=${actualSkip}, limit=${safeLimit} (传入limit=${limit})`); + const result = await neo4j.read(query, params); return result.records.map(record => { @@ -47,9 +53,14 @@ class NodeService { */ async getNodeById(id) { try { + // 优化查询,只返回必要字段 const query = ` - MATCH (n {id: $id}) - RETURN n + MATCH (n) + WHERE n.id = $id + RETURN + n.id as id, + labels(n) as labels, + properties(n) as properties `; const params = { id }; @@ -59,8 +70,12 @@ class NodeService { return null; } - const node = result.records[0].get('n'); - return this.formatNode(node); + const record = result.records[0]; + return { + id: record.get('id'), + labels: record.get('labels'), + properties: record.get('properties') + }; } catch (error) { console.error('根据ID获取节点失败:', error); throw error; @@ -74,7 +89,7 @@ class NodeService { * @param {number} skip - 跳过数量 * @returns {Promise} 节点列表 */ - async getNodesByLabel(label, limit = 100, skip = 0) { + async getNodesByLabel(label, limit = 10000, skip = 0) { try { const query = ` MATCH (n:${label}) @@ -83,7 +98,6 @@ class NodeService { LIMIT $limit `; - // const params = { skip, limit }; // 确保参数是整数 const params = { skip: int(parseInt(skip)), @@ -109,11 +123,9 @@ class NodeService { */ async createNode(properties, labels = []) { try { - // 生成唯一ID const id = uuidv4(); const nodeProperties = { ...properties, id }; - // 构建标签部分 const labelString = labels.length > 0 ? `:${labels.join(':')}` : ''; @@ -174,16 +186,39 @@ class NodeService { */ async deleteNode(id) { try { - const query = ` - MATCH (n {id: $id}) - DETACH DELETE n - `; + console.log(`删除节点服务: ID=${id}, 类型=${typeof id}`); - const params = { id }; - const result = await neo4j.write(query, params); + // 首先删除与该节点相关的所有关系 + const deleteRelationsQuery = ` + MATCH (n)-[r]-() + WHERE n.id = $id OR ID(n) = $neo4jId + DELETE r + `; + + // 然后删除节点 + const deleteNodeQuery = ` + MATCH (n) + WHERE n.id = $id OR ID(n) = $neo4jId + DELETE n + `; + + const params = { + id: id, + neo4jId: int(parseInt(id)) + }; + + console.log('删除节点参数:', params); + + // 先删除关系 + await neo4j.write(deleteRelationsQuery, params); + + // 再删除节点 + const result = await neo4j.write(deleteNodeQuery, params); + const deletedCount = result.summary.counters.updates().nodesDeleted; + + console.log(`删除节点结果: 删除了 ${deletedCount} 个节点`); - // 检查是否有节点被删除 - return result.summary.counters.updates().nodesDeleted > 0; + return deletedCount > 0; } catch (error) { console.error('删除节点失败:', error); throw error; @@ -198,7 +233,7 @@ class NodeService { * @param {number} skip - 跳过数量 * @returns {Promise} 匹配的节点列表 */ - async searchNodes(key, value, limit = 100, skip = 0) { + async searchNodes(key, value, limit = 10000, skip = 0) { try { const query = ` MATCH (n) @@ -208,11 +243,6 @@ class NodeService { LIMIT $limit `; - // const params = { - // value, - // skip, - // limit - // }; // 确保参数是整数 const params = { value, @@ -242,11 +272,9 @@ class NodeService { id: node.properties.id || node.identity.toString(), labels: node.labels, properties: node.properties, - // 如果需要,可以添加Neo4j内部ID neo4jId: node.identity.toString() }; } } -// 导出单例实例 module.exports = new NodeService(); \ No newline at end of file diff --git a/code/frontend/README.md b/code/frontend/README.md deleted file mode 100644 index e1ecac7..0000000 --- a/code/frontend/README.md +++ /dev/null @@ -1,59 +0,0 @@ -# Frontend - -This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.2. - -## Development server - -To start a local development server, run: - -```bash -ng serve -``` - -Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files. - -## Code scaffolding - -Angular CLI includes powerful code scaffolding tools. To generate a new component, run: - -```bash -ng generate component component-name -``` - -For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run: - -```bash -ng generate --help -``` - -## Building - -To build the project run: - -```bash -ng build -``` - -This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed. - -## Running unit tests - -To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command: - -```bash -ng test -``` - -## Running end-to-end tests - -For end-to-end (e2e) testing, run: - -```bash -ng e2e -``` - -Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs. - -## Additional Resources - -For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page. diff --git a/code/frontend/api-service.js b/code/frontend/api-service.js index b462475..d74f798 100644 --- a/code/frontend/api-service.js +++ b/code/frontend/api-service.js @@ -3,11 +3,55 @@ class ApiService { this.baseUrl = 'http://localhost:3000/api'; } + + // async request(endpoint, options = {}) { + // const url = `${this.baseUrl}${endpoint}`; + + // try { + // if (options.body && typeof options.body !== 'string') { + // options.body = JSON.stringify(options.body); + // } + + // const response = await fetch(url, { + // headers: { + // 'Content-Type': 'application/json', + // ...options.headers + // }, + // ...options + // }); + + // if (!response.ok) { + // const errorText = await response.text(); + // throw new Error(`API请求失败: ${response.status} ${response.statusText} - ${errorText}`); + // } + + // const result = await response.json(); + + // if (result && typeof result.success !== 'undefined') { + // return result; + // } else { + // return { + // success: true, + // data: result + // }; + // } + // } catch (error) { + // console.error('API请求错误:', error); + // throw error; + // } + // } + async request(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; + // 根据操作类型设置不同的超时时间 + const isDeleteOperation = options.method === 'DELETE'; + const timeout = 30000; + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + try { - // 确保请求体是JSON字符串 if (options.body && typeof options.body !== 'string') { options.body = JSON.stringify(options.body); } @@ -17,26 +61,49 @@ class ApiService { 'Content-Type': 'application/json', ...options.headers }, + signal: controller.signal, ...options }); + clearTimeout(timeoutId); + if (!response.ok) { const errorText = await response.text(); throw new Error(`API请求失败: ${response.status} ${response.statusText} - ${errorText}`); } - return response.json(); + const result = await response.json(); + + if (result && typeof result.success !== 'undefined') { + return result; + } else { + return { + success: true, + data: result + }; + } } catch (error) { + clearTimeout(timeoutId); console.error('API请求错误:', error); + + if (error.name === 'AbortError') { + throw new Error(`请求超时(${timeout}ms),请检查网络连接或服务器状态`); + } throw error; } } // 节点相关API - async getNodes(limit = 100, skip = 0, label = null) { - const params = new URLSearchParams({ limit, skip }); + async getNodes(limit = 10000, skip = 0, label = null) { + const params = new URLSearchParams({ + limit: limit.toString(), + skip: skip.toString() + }); if (label) params.append('label', label); + console.log("API请求节点,参数:", { limit, skip, label }); + console.log("请求URL:", `/nodes?${params}`); + return this.request(`/nodes?${params}`); } @@ -52,9 +119,7 @@ class ApiService { } } - // 修改创建节点方法 async createNode(nodeData) { - // 确保数据格式正确 const requestData = { properties: nodeData.properties || {}, labels: nodeData.labels || [] @@ -76,20 +141,48 @@ class ApiService { }); } + // 在 api-service.js 中增强 deleteNode 方法 async deleteNode(id) { - return this.request(`/nodes/${id}`, { - method: 'DELETE' - }); + try { + console.log("删除节点API调用,ID:", id); + + // 增加超时时间到30秒 + const response = await this.request(`/nodes/${id}`, { + method: 'DELETE', + timeout: 30000 // 30秒超时 + }); + + console.log("删除节点API响应:", response); + return response; + } catch (error) { + console.error("删除节点API错误:", error); + + if (error.message.includes('timeout') || error.name === 'AbortError') { + return { + success: 'unknown', + message: '请求超时,请稍后手动确认删除状态' + }; + } + throw error; + } } + // 关系相关API - async getEdges(limit = 100, skip = 0, type = null) { - const params = new URLSearchParams({ limit, skip }); + async getEdges(limit = 10000, skip = 0, type = null) { + const params = new URLSearchParams({ + limit: limit.toString(), + skip: skip.toString() + }); if (type) params.append('type', type); + console.log("API请求关系,参数:", { limit, skip, type }); + console.log("请求URL:", `/edges?${params}`); + return this.request(`/edges?${params}`); } + async getEdge(id) { try { console.log("请求关系详情,ID:", id); @@ -102,9 +195,7 @@ class ApiService { } } - // 修改创建关系方法 async createEdge(edgeData) { - // 确保数据格式正确 const requestData = { startNodeId: edgeData.startNodeId, endNodeId: edgeData.endNodeId, @@ -127,19 +218,31 @@ class ApiService { }); } + // 在 api-service.js 中修改 deleteEdge 方法 async deleteEdge(id) { - return this.request(`/edges/${id}`, { - method: 'DELETE' - }); + try { + console.log("删除关系API调用,ID:", id); + + // 同样增加关系删除的超时时间 + const response = await this.request(`/edges/${id}`, { + method: 'DELETE', + timeout: 30000 // 30秒超时 + }); + + console.log("删除关系API响应:", response); + return response; + } catch (error) { + console.error("删除关系API错误:", error); + throw error; + } } - // 搜索功能 - async searchNodes(key, value, limit = 100, skip = 0) { + async searchNodes(key, value, limit = 10000, skip = 0) { const params = new URLSearchParams({ limit, skip }); return this.request(`/nodes/search/${key}/${value}?${params}`); } - async getNodeEdges(nodeId, direction = 'both', limit = 100, skip = 0) { + async getNodeEdges(nodeId, direction = 'both', limit = 10000, skip = 0) { const params = new URLSearchParams({ direction, limit, skip }); return this.request(`/edges/node/${nodeId}?${params}`); } diff --git a/code/frontend/app.js b/code/frontend/app.js index 9d54279..85f29a1 100644 --- a/code/frontend/app.js +++ b/code/frontend/app.js @@ -8,10 +8,73 @@ class KnowledgeGraphApp { edges: [] }; + this.config = { + maxNodes: 10000, + maxEdges: 10000 + }; + + this.graphRenderer.setAppInstance(this); + + // 确保缓存对象正确初始化 + this.nodeCache = new Map(); // 节点缓存 + this.edgeCache = new Map(); // 关系缓存 + this.cacheTimeout = 30000; // 缓存30秒 + + // 确保缓存方法存在 + // this.preloadCaches = this.preloadCaches.bind(this); + this.init(); console.log("KnowledgeGraphApp 初始化完成"); } + // 预加载缓存 - 添加安全检查 + preloadCaches() { + try { + // 确保缓存对象存在 + if (!this.nodeCache) { + console.warn('nodeCache 未初始化,正在重新初始化'); + this.nodeCache = new Map(); + } + if (!this.edgeCache) { + console.warn('edgeCache 未初始化,正在重新初始化'); + this.edgeCache = new Map(); + } + + // 清空旧缓存 + if (this.nodeCache.clear) { + this.nodeCache.clear(); + } + if (this.edgeCache.clear) { + this.edgeCache.clear(); + } + + // 预缓存节点数据 + this.currentData.nodes.forEach(node => { + if (node && node.id) { + this.nodeCache.set(node.id, { + data: node, + timestamp: Date.now() + }); + } + }); + + // 预缓存边数据 + this.currentData.edges.forEach(edge => { + if (edge && edge.id) { + this.edgeCache.set(edge.id, { + data: edge, + timestamp: Date.now() + }); + } + }); + + console.log(`预缓存完成: ${this.nodeCache.size} 个节点, ${this.edgeCache.size} 条边`); + } catch (error) { + console.error('预加载缓存失败:', error); + // 即使缓存失败也不应该阻止应用运行 + } + } + init() { console.log("开始初始化事件绑定和数据加载"); this.bindEvents(); @@ -69,6 +132,28 @@ class KnowledgeGraphApp { this.hideModal(); }); + // 节点数控制事件 + bindEventWhenReady('#debug-apply-limit', 'click', () => { + const limitInput = document.getElementById('debug-node-limit'); + const maxNodes = parseInt(limitInput.value); + if (!isNaN(maxNodes) && maxNodes > 0) { + this.config.maxNodes = maxNodes; + console.log(`手动设置最大节点数为: ${maxNodes}`); + this.loadData(); + } + }); + + // 边数控制事件 + bindEventWhenReady('#debug-apply-edge-limit', 'click', () => { + const limitInput = document.getElementById('debug-edge-limit'); + const maxEdges = parseInt(limitInput.value); + if (!isNaN(maxEdges) && maxEdges > 0) { + this.config.maxEdges = maxEdges; + console.log(`手动设置最大边数为: ${maxEdges}`); + this.loadData(); + } + }); + // 点击模态框外部关闭 window.addEventListener('click', (event) => { const modal = document.getElementById('modal'); @@ -80,148 +165,176 @@ class KnowledgeGraphApp { console.log("所有事件绑定完成"); } + + /* + * 数据管理 + */ + // 加载所有数据 + // async loadData() { + // try { + // console.log("开始加载数据..."); + // console.log("请求参数 - maxNodes:", this.config.maxNodes, "maxEdges:", this.config.maxEdges); + + // const [nodesResponse, edgesResponse] = await Promise.all([ + // this.api.getNodes(this.config.maxNodes), + // this.api.getEdges(this.config.maxEdges) + // ]); + + // console.log("节点响应结构:", nodesResponse); + // console.log("关系响应结构:", edgesResponse); + + // console.log("节点响应数据量:", nodesResponse.data ? nodesResponse.data.length : 0); + // console.log("关系响应数据量:", edgesResponse.data ? edgesResponse.data.length : 0); + + // this.currentData.nodes = nodesResponse.data || []; + // this.currentData.edges = edgesResponse.data || []; + + // console.log("=== 数据诊断 ==="); + // console.log("配置限制 - 节点:", this.config.maxNodes, "边:", this.config.maxEdges); + // console.log("实际获取 - 节点:", this.currentData.nodes.length, "边:", this.currentData.edges.length); + + // if (this.currentData.nodes.length === this.config.maxNodes) { + // console.log("✅ 节点数量达到限制,后端限制生效"); + // } else { + // console.log("⚠️ 节点数量未达到限制,可能原因:"); + // console.log(" - 数据库中没有足够节点"); + // console.log(" - 后端服务未正确处理limit参数"); + // console.log(" - 网络请求参数错误"); + // } + + // // 如果返回的节点数超过限制,进行截断 + // if (this.currentData.nodes.length > this.config.maxNodes) { + // console.warn(`节点数量超过限制,显示前 ${this.config.maxNodes} 个节点`); + // this.currentData.nodes = this.currentData.nodes.slice(0, this.config.maxNodes); + // } + + // // 如果返回的边数超过限制,进行截断 + // if (this.currentData.edges.length > this.config.maxEdges) { + // console.warn(`边数量超过限制,显示前 ${this.config.maxEdges} 条边`); + // this.currentData.edges = this.currentData.edges.slice(0, this.config.maxEdges); + // } + + // console.log("最终显示节点数量:", this.currentData.nodes.length); + // console.log("最终显示关系数量:", this.currentData.edges.length); + + // this.renderSidebarLists(); + // this.graphRenderer.render(this.currentData); + + // console.log("数据加载完成"); + // } catch (error) { + // console.error('加载数据失败:', error); + // alert('加载数据失败: ' + error.message); + // } + // } + async loadData() { try { console.log("开始加载数据..."); - // 同时获取节点和关系 const [nodesResponse, edgesResponse] = await Promise.all([ - this.api.getNodes(), - this.api.getEdges() + this.api.getNodes(this.config.maxNodes), + this.api.getEdges(this.config.maxEdges) ]); - console.log("节点响应:", nodesResponse); - console.log("关系响应:", edgesResponse); - - // 确保数据存在 this.currentData.nodes = nodesResponse.data || []; this.currentData.edges = edgesResponse.data || []; - // 打印所有节点ID以供调试 - console.log("所有节点ID:", this.currentData.nodes.map(n => n.id)); - console.log("所有关系ID:", this.currentData.edges.map(e => e.id)); + // 添加详细的数据诊断 + console.log("=== 数据诊断 ==="); + console.log("节点数量:", this.currentData.nodes.length); + console.log("边数量:", this.currentData.edges.length); - this.renderSidebarLists(); - this.graphRenderer.render(this.currentData); + // 检查边的source和target是否能在节点中找到 + const nodeIds = new Set(this.currentData.nodes.map(node => node.id)); + let validEdges = 0; - console.log("数据加载完成"); - } catch (error) { - console.error('加载数据失败:', error); - alert('加载数据失败: ' + error.message); - } - } - - debugInfo() { - console.log("=== 调试信息开始 ==="); - console.log("当前数据:", this.currentData); - console.log("节点数量:", this.currentData.nodes.length); - console.log("关系数量:", this.currentData.edges.length); + this.currentData.edges.forEach(edge => { + const sourceId = edge.startNode ? edge.startNode.id : edge.startNodeId; + const targetId = edge.endNode ? edge.endNode.id : edge.endNodeId; - // 检查API服务状态 - console.log("API基础URL:", this.api.baseUrl); - - // 测试API连接 - 使用更简单的方式 - console.log("测试API连接..."); + const sourceExists = nodeIds.has(sourceId); + const targetExists = nodeIds.has(targetId); - // 直接测试API端点 - fetch(`${this.api.baseUrl}/nodes?limit=1`) - .then(response => { - console.log("API响应状态:", response.status, response.statusText); - return response.json(); - }) - .then(data => { - console.log("API测试成功:", data); - }) - .catch(error => { - console.error("API测试失败:", error); + if (sourceExists && targetExists) { + validEdges++; + } else { + console.warn(`无效的边: ${edge.id}, source存在: ${sourceExists}, target存在: ${targetExists}`); + console.warn(` source: ${sourceId}, target: ${targetId}`); + } }); - // 检查图谱渲染器状态 - console.log("图谱容器:", this.graphRenderer.containerId); - console.log("图谱实例:", this.graphRenderer.graph ? "已初始化" : "未初始化"); - - // 检查所有按钮状态 - const buttons = ['refresh-btn', 'add-node-btn', 'add-edge-btn', 'debug-btn', 'search-btn']; - buttons.forEach(btnId => { - const btn = document.getElementById(btnId); - console.log(`按钮 ${btnId}:`, btn ? "存在" : "不存在"); - }); + console.log(`有效边数量: ${validEdges}/${this.currentData.edges.length}`); - console.log("=== 调试信息结束 ==="); + // 预加载缓存 + try { + this.preloadCaches(); + } catch (cacheError) { + console.warn('缓存预加载失败,但不影响主要功能:', cacheError); + } - // 显示简单提示 - alert("调试信息已输出到控制台,请按F12查看"); - } + this.renderSidebarLists(); + this.graphRenderer.render(this.currentData); - isValidId(id) { - return id && typeof id === 'string' && id.length > 0; + console.log("数据加载完成"); + } catch (error) { + console.error('加载数据失败:', error); + alert('加载数据失败: ' + error.message); + } } - // 修改 renderSidebarLists 方法中的节点点击处理 - renderSidebarLists() { - // 渲染节点列表 - const nodesContainer = document.getElementById('nodes-container'); - nodesContainer.innerHTML = ''; + // 预加载缓存 + preloadCaches() { + // 清空旧缓存 + this.nodeCache.clear(); + this.edgeCache.clear(); + // 预缓存节点数据 this.currentData.nodes.forEach(node => { - const li = document.createElement('li'); - // 使用更安全的显示方式 - const displayName = node.properties && node.properties.name - ? node.properties.name - : `节点 ${node.id ? node.id.substring(0, 8) : '未知'}`; - - li.textContent = displayName; - li.dataset.id = node.id; - li.addEventListener('click', () => { - console.log("点击节点,ID:", node.id); - this.showNodeDetail(node.id); + this.nodeCache.set(node.id, { + data: node, + timestamp: Date.now() }); - nodesContainer.appendChild(li); }); - // 渲染关系列表 - const edgesContainer = document.getElementById('edges-container'); - edgesContainer.innerHTML = ''; - + // 预缓存边数据 this.currentData.edges.forEach(edge => { - const li = document.createElement('li'); - const startName = edge.startNode && edge.startNode.properties && edge.startNode.properties.name - ? edge.startNode.properties.name - : (edge.startNodeId || '未知'); - const endName = edge.endNode && edge.endNode.properties && edge.endNode.properties.name - ? edge.endNode.properties.name - : (edge.endNodeId || '未知'); - - li.textContent = `${edge.type || '关系'}: ${startName} → ${endName}`; - li.dataset.id = edge.id; - li.addEventListener('click', () => { - console.log("点击关系,ID:", edge.id); - this.showEdgeDetail(edge.id); + this.edgeCache.set(edge.id, { + data: edge, + timestamp: Date.now() }); - edgesContainer.appendChild(li); }); - } - async handleSearch() { - const query = document.getElementById('search-input').value.trim(); - if (!query) return; + console.log(`预缓存完成: ${this.nodeCache.size} 个节点, ${this.edgeCache.size} 条边`); + } - try { - // 简单搜索实现:搜索节点名称包含关键词的节点 - const response = await this.api.searchNodes('name', query); + // 设置最大节点数 + setMaxNodes(maxNodes) { + if (maxNodes > 0 && maxNodes <= 10000) { + this.config.maxNodes = maxNodes; + console.log(`设置最大节点数为: ${maxNodes}`); + this.loadData(); + } else { + alert('最大节点数必须在1-10000之间'); + } + } - if (response.data.length > 0) { - // 高亮显示搜索结果 - this.graphRenderer.highlightNodes(response.data.map(node => node.id)); - } else { - alert('未找到匹配的节点'); - } - } catch (error) { - console.error('搜索失败:', error); - alert('搜索失败'); + // 设置最大边数 + setMaxEdges(maxEdges) { + if (maxEdges > 0 && maxEdges <= 10000) { + this.config.maxEdges = maxEdges; + console.log(`设置最大边数为: ${maxEdges}`); + this.loadData(); + } else { + alert('最大边数必须在1-10000之间'); } } + + /* + * UI 交互 + */ + // 显示表单 + // 编辑时传入节点对象,添加时传入null showNodeForm(node = null) { const isEdit = !!node; const title = isEdit ? '编辑节点' : '添加节点'; @@ -250,7 +363,7 @@ class KnowledgeGraphApp { this.showModal(formHtml); - // 绑定表单提交事件 + // 表单提交 document.getElementById('node-form').addEventListener('submit', (e) => { e.preventDefault(); this.saveNode(node); @@ -261,38 +374,7 @@ class KnowledgeGraphApp { }); } - async saveNode(existingNode) { - try { - const name = document.getElementById('node-name').value; - const labels = document.getElementById('node-labels').value.split(',').map(l => l.trim()).filter(l => l); - const propertiesText = document.getElementById('node-properties').value || '{}'; - - // 安全地解析JSON - let properties; - try { - properties = JSON.parse(propertiesText); - } catch (e) { - throw new Error('属性JSON格式不正确'); - } - - properties.name = name; // 确保名称属性 - - if (existingNode) { - console.log("更新节点:", existingNode.id, properties); - await this.api.updateNode(existingNode.id, properties); - } else { - console.log("创建节点:", properties, labels); - await this.api.createNode({ properties, labels }); - } - - this.hideModal(); - this.loadData(); // 刷新数据 - } catch (error) { - console.error('保存节点失败:', error); - alert('保存节点失败: ' + error.message); - } - } - + // 编辑时传入关系对象,添加时传入null showEdgeForm(edge = null) { const isEdit = !!edge; const title = isEdit ? '编辑关系' : '添加关系'; @@ -325,7 +407,7 @@ class KnowledgeGraphApp { this.showModal(formHtml); - // 绑定表单提交事件 + // 表单提交事件 document.getElementById('edge-form').addEventListener('submit', (e) => { e.preventDefault(); this.saveEdge(edge); @@ -336,194 +418,382 @@ class KnowledgeGraphApp { }); } - async saveEdge(existingEdge) { - try { - const startNodeId = document.getElementById('edge-start').value; - const endNodeId = document.getElementById('edge-end').value; - const type = document.getElementById('edge-type').value; - const propertiesText = document.getElementById('edge-properties').value || '{}'; - - // 安全地解析JSON - let properties; - try { - properties = JSON.parse(propertiesText); - } catch (e) { - throw new Error('属性JSON格式不正确'); - } - - if (existingEdge) { - console.log("更新关系:", existingEdge.id, properties); - await this.api.updateEdge(existingEdge.id, properties); - } else { - console.log("创建关系:", startNodeId, endNodeId, type, properties); - await this.api.createEdge({ startNodeId, endNodeId, type, properties }); - } - - this.hideModal(); - this.loadData(); // 刷新数据 - } catch (error) { - console.error('保存关系失败:', error); - alert('保存关系失败: ' + error.message); - } - } - - async showNodeDetail(nodeId) { + // 显示节点详情 + async showNodeDetail(nodeId, cachedData = null) { if (!this.isValidId(nodeId)) { alert('无效的节点ID'); return; } + // 立即显示加载状态 + this.showModal(` +

节点详情

+
+
加载中...
+
+ `); + + // 检查缓存 + const cached = this.nodeCache.get(nodeId); + if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { + console.log("使用缓存的节点数据"); + this.hideModal(); // 先关闭加载模态框 + this.renderNodeDetail(cached.data); + return; + } + try { console.log("显示节点详情,ID:", nodeId); - // 首先检查节点是否在当前数据中 + // 先从当前数据中查找 const nodeInCurrentData = this.currentData.nodes.find(n => n.id === nodeId); - if (!nodeInCurrentData) { - console.warn("节点不在当前数据中,尝试从API获取"); + if (nodeInCurrentData) { + console.log("从当前数据加载节点详情"); + this.hideModal(); // 关闭加载模态框 + this.renderNodeDetail(nodeInCurrentData); + + // 异步更新缓存 + this.fetchAndCacheNode(nodeId); + return; } - const response = await this.api.getNode(nodeId); + // 如果有传入的缓存数据,先使用它 + if (cachedData) { + this.hideModal(); + this.renderNodeDetail(cachedData); + this.fetchAndCacheNode(nodeId); + return; + } + // 没有缓存也没有当前数据,才调用API + const response = await this.api.getNode(nodeId); if (!response.success) { throw new Error(response.message || "获取节点详情失败"); } const node = response.data; - if (!node) { throw new Error("节点数据为空"); } - const detailHtml = ` -

节点详情

-

ID: ${node.id}

-

标签: ${node.labels ? node.labels.join(', ') : '无'}

-

属性:

-
${JSON.stringify(node.properties || {}, null, 2)}
-
- - -
- `; - - this.showModal(detailHtml); - - document.getElementById('edit-node-btn').addEventListener('click', () => { - this.hideModal(); - this.showNodeForm(node); + // 缓存结果 + this.nodeCache.set(nodeId, { + data: node, + timestamp: Date.now() }); - document.getElementById('delete-node-btn').addEventListener('click', () => { - if (confirm('确定要删除这个节点吗?')) { - this.deleteNode(node.id); - } - }); + this.hideModal(); + this.renderNodeDetail(node); } catch (error) { console.error('获取节点详情失败:', error); - alert('获取节点详情失败: ' + error.message); - - // 显示错误详情 - const errorHtml = ` -

错误

-

获取节点详情失败

-

错误信息: ${error.message}

-

节点ID: ${nodeId}

- `; - this.showModal(errorHtml); + this.hideModal(); + this.showNodeError(nodeId, error); } } + // 显示关系详情 async showEdgeDetail(edgeId) { - if (!this.isValidId(edgeId)) { - alert('无效的关系ID'); + // 检查缓存 + const cached = this.edgeCache.get(edgeId); + if (cached && Date.now() - cached.timestamp < this.cacheTimeout) { + console.log("使用缓存的边数据"); + this.renderEdgeDetail(cached.data); return; } try { console.log("显示关系详情,ID:", edgeId); - const response = await this.api.getEdge(edgeId); + // 先从当前数据中查找 + const edgeInCurrentData = this.currentData.edges.find(e => e.id === edgeId); + if (edgeInCurrentData) { + console.log("从当前数据加载边详情"); + this.renderEdgeDetail(edgeInCurrentData); + + // 异步更新缓存 + this.fetchAndCacheEdge(edgeId); + return; + } + // 没有缓存也没有当前数据,才调用API + const response = await this.api.getEdge(edgeId); if (!response.success) { throw new Error(response.message || "获取关系详情失败"); } const edge = response.data; - if (!edge) { throw new Error("关系数据为空"); } - const startName = edge.startNode && edge.startNode.properties && edge.startNode.properties.name - ? edge.startNode.properties.name - : (edge.startNodeId || '未知'); - const endName = edge.endNode && edge.endNode.properties && edge.endNode.properties.name - ? edge.endNode.properties.name - : (edge.endNodeId || '未知'); + // 缓存结果 + this.edgeCache.set(edgeId, { + data: edge, + timestamp: Date.now() + }); - const detailHtml = ` -

关系详情

-

ID: ${edge.id}

-

类型: ${edge.type || '未知'}

-

起始节点: ${startName} (ID: ${edge.startNode ? edge.startNode.id : edge.startNodeId})

-

目标节点: ${endName} (ID: ${edge.endNode ? edge.endNode.id : edge.endNodeId})

+ this.renderEdgeDetail(edge); + } catch (error) { + console.error('获取关系详情失败:', error); + this.showEdgeError(edgeId, error); + } + } + + // 异步更新节点缓存 + async fetchAndCacheNode(nodeId) { + try { + const response = await this.api.getNode(nodeId); + if (response.success && response.data) { + this.nodeCache.set(nodeId, { + data: response.data, + timestamp: Date.now() + }); + } + } catch (error) { + console.warn('异步更新节点缓存失败:', error); + } + } + + // 专用的节点详情渲染方法 + renderNodeDetail(node) { + const detailHtml = ` +

节点详情

+

ID: ${node.id}

+

标签: ${node.labels ? node.labels.join(', ') : '无'}

属性:

-
${JSON.stringify(edge.properties || {}, null, 2)}
+
${JSON.stringify(node.properties || {}, null, 2)}
- - + +
`; - this.showModal(detailHtml); + this.showModal(detailHtml); - document.getElementById('edit-edge-btn').addEventListener('click', () => { - this.hideModal(); - this.showEdgeForm(edge); - }); + document.getElementById('edit-node-btn').addEventListener('click', () => { + this.hideModal(); + this.showNodeForm(node); + }); - document.getElementById('delete-edge-btn').addEventListener('click', () => { - if (confirm('确定要删除这个关系吗?')) { - this.deleteEdge(edge.id); - } - }); + document.getElementById('delete-node-btn').addEventListener('click', () => { + if (confirm('确定要删除这个节点吗?')) { + this.deleteNode(node.id); + } + }); + } + + // 异步更新缓存 + async fetchAndCacheEdge(edgeId) { + try { + const response = await this.api.getEdge(edgeId); + if (response.success && response.data) { + this.edgeCache.set(edgeId, { + data: response.data, + timestamp: Date.now() + }); + } } catch (error) { - console.error('获取关系详情失败:', error); - alert('获取关系详情失败: ' + error.message); + console.warn('异步更新边缓存失败:', error); + } + } + + // 专用的边详情渲染方法 + renderEdgeDetail(edge) { + const startName = edge.startNode && edge.startNode.properties && edge.startNode.properties.name + ? edge.startNode.properties.name + : (edge.startNodeId || '未知'); + const endName = edge.endNode && edge.endNode.properties && edge.endNode.properties.name + ? edge.endNode.properties.name + : (edge.endNodeId || '未知'); + + const detailHtml = ` +

关系详情

+

ID: ${edge.id}

+

类型: ${edge.type || '未知'}

+

起始节点: ${startName} (ID: ${edge.startNode ? edge.startNode.id : edge.startNodeId})

+

目标节点: ${endName} (ID: ${edge.endNode ? edge.endNode.id : edge.endNodeId})

+

属性:

+
${JSON.stringify(edge.properties || {}, null, 2)}
+
+ + +
+ `; + + this.showModal(detailHtml); + + document.getElementById('edit-edge-btn').addEventListener('click', () => { + this.hideModal(); + this.showEdgeForm(edge); + }); - // 显示错误详情 - const errorHtml = ` + document.getElementById('delete-edge-btn').addEventListener('click', () => { + if (confirm('确定要删除这个关系吗?')) { + this.deleteEdge(edge.id); + } + }); + } + + showNodeError(nodeId, error) { + const errorHtml = `

错误

-

获取关系详情失败

+

获取节点详情失败

错误信息: ${error.message}

-

关系ID: ${edgeId}

+

节点ID: ${nodeId}

`; - this.showModal(errorHtml); - } + this.showModal(errorHtml); + } + + showEdgeError(edgeId, error) { + const errorHtml = ` +

错误

+

获取关系详情失败

+

错误信息: ${error.message}

+

关系ID: ${edgeId}

+ `; + this.showModal(errorHtml); } + // 删除节点 + // 在 app.js 中修改 deleteNode 方法 async deleteNode(nodeId) { try { - await this.api.deleteNode(nodeId); - this.hideModal(); - this.loadData(); // 刷新数据 + console.log("开始删除节点,ID:", nodeId); + + if (!this.isValidId(nodeId)) { + throw new Error('无效的节点ID'); + } + + const response = await this.api.deleteNode(nodeId); + console.log("删除节点API响应:", response); + + if (response && (response.success === true || response.success === undefined || response.success === 'unknown')) { + // 清理缓存 + this.nodeCache.delete(nodeId); + + // 清理与该节点相关的边缓存 + this.currentData.edges + .filter(edge => + edge.startNode?.id === nodeId || + edge.endNode?.id === nodeId + ) + .forEach(edge => this.edgeCache.delete(edge.id)); + + // 从当前数据中移除已删除的节点 + this.currentData.nodes = this.currentData.nodes.filter(node => node.id !== nodeId); + + // 同时移除相关的边 + this.currentData.edges = this.currentData.edges.filter(edge => + edge.startNode?.id !== nodeId && edge.endNode?.id !== nodeId + ); + + if (response.success === 'unknown') { + this.showMessage('删除请求已发送,但由于超时请刷新页面确认删除状态', 'warning'); + } else { + this.showSuccessMessage('节点删除成功!'); + } + + this.hideModal(); + + // 刷新UI但不重新加载所有数据 + this.renderSidebarLists(); + this.graphRenderer.render(this.currentData); + + // 如果是超时情况,建议用户刷新数据 + if (response.success === 'unknown') { + setTimeout(() => { + if (confirm('由于删除操作超时,建议刷新页面确认数据状态。是否立即刷新?')) { + this.loadData(); + } + }, 2000); + } + } else { + throw new Error(response?.message || '删除操作失败'); + } } catch (error) { console.error('删除节点失败:', error); - alert('删除节点失败'); + + let errorMessage = '删除节点失败: ' + error.message; + if (error.message.includes('timeout') || error.name === 'AbortError') { + errorMessage = '删除请求超时,但节点可能已在后台删除,请刷新页面确认'; + } + + this.showErrorMessage(errorMessage); } } + // 删除关系 + // 添加缓存清理 + // 在 app.js 的 deleteEdge 方法中增强错误处理 async deleteEdge(edgeId) { try { - await this.api.deleteEdge(edgeId); - this.hideModal(); - this.loadData(); // 刷新数据 + console.log("开始删除关系,ID:", edgeId); + + if (!this.isValidId(edgeId)) { + throw new Error('无效的关系ID'); + } + + const response = await this.api.deleteEdge(edgeId); + console.log("删除关系API响应:", response); + + if (response && (response.success === true || response.success === 'unknown')) { + // 清理缓存 + this.edgeCache.delete(edgeId); + + // 从当前数据中移除已删除的关系 + this.currentData.edges = this.currentData.edges.filter(edge => edge.id !== edgeId); + + if (response.success === 'unknown') { + this.showMessage('删除请求已发送,但由于超时请刷新页面确认删除状态', 'warning'); + } else { + this.showSuccessMessage('关系删除成功!'); + } + + this.hideModal(); + + // 刷新UI但不重新加载所有数据 + this.renderSidebarLists(); + this.graphRenderer.render(this.currentData); + + // 如果是超时情况,建议用户刷新数据 + if (response.success === 'unknown') { + setTimeout(() => { + if (confirm('由于删除操作超时,建议刷新页面确认数据状态。是否立即刷新?')) { + this.loadData(); + } + }, 2000); + } + } else { + throw new Error(response?.message || '删除操作失败'); + } } catch (error) { console.error('删除关系失败:', error); - alert('删除关系失败'); + this.showErrorMessage('删除关系失败: ' + error.message); + } + } + + // 搜索 + async handleSearch() { + const query = document.getElementById('search-input').value.trim(); + if (!query) return; + + try { + const response = await this.api.searchNodes('name', query); + + if (response.data.length > 0) { + this.graphRenderer.highlightNodes(response.data.map(node => node.id)); + } else { + alert('未找到匹配的节点'); + } + } catch (error) { + console.error('搜索失败:', error); + alert('搜索失败'); } } + /* + * 模态框 + */ showModal(content) { document.getElementById('modal-body').innerHTML = content; document.getElementById('modal').style.display = 'block'; @@ -532,14 +802,334 @@ class KnowledgeGraphApp { hideModal() { document.getElementById('modal').style.display = 'none'; } + + debugInfo() { + console.log("=== 调试信息开始 ==="); + console.log("当前配置:", this.config); + console.log("最大节点数:", this.config.maxNodes); + console.log("最大边数:", this.config.maxEdges); + + console.log("API基础URL:", this.api.baseUrl); + + console.log("测试API连接..."); + + fetch(`${this.api.baseUrl}/nodes?limit=1`) + .then(response => { + console.log("API节点响应状态:", response.status, response.statusText); + return response.json(); + }) + .then(data => { + console.log("API节点测试成功:", data); + }) + .catch(error => { + console.error("API节点测试失败:", error); + }); + + fetch(`${this.api.baseUrl}/edges?limit=1`) + .then(response => { + console.log("API边响应状态:", response.status, response.statusText); + return response.json(); + }) + .then(data => { + console.log("API边测试成功:", data); + }) + .catch(error => { + console.error("API边测试失败:", error); + }); + + console.log("图谱容器:", this.graphRenderer.containerId); + console.log("图谱实例:", this.graphRenderer.graph ? "已初始化" : "未初始化"); + + const buttons = ['refresh-btn', 'add-node-btn', 'add-edge-btn', 'debug-btn', 'search-btn', 'debug-apply-limit', 'debug-apply-edge-limit']; + buttons.forEach(btnId => { + const btn = document.getElementById(btnId); + console.log(`按钮 ${btnId}:`, btn ? "存在" : "不存在"); + }); + + console.log("=== 调试信息结束 ==="); + + alert("调试信息已输出到控制台,请按F12查看"); + } + + isValidId(id) { + return id && typeof id === 'string' && id.length > 0; + } + + // renderSidebarLists() { + // const nodesContainer = document.getElementById('nodes-container'); + // const edgesContainer = document.getElementById('edges-container'); + + // nodesContainer.innerHTML = ''; + // edgesContainer.innerHTML = ''; + + // // 统计信息 + // const statsHtml = ` + //
+ // 统计信息:
+ // 节点: ${this.currentData.nodes.length}/${this.config.maxNodes}
+ // 关系: ${this.currentData.edges.length}/${this.config.maxEdges} + //
+ // `; + + // nodesContainer.innerHTML = statsHtml; + + // // 使用事件委托优化节点点击 + // this.currentData.nodes.forEach(node => { + // const li = document.createElement('li'); + // const displayName = node.properties && node.properties.name + // ? node.properties.name + // : `节点 ${node.id ? node.id.substring(0, 8) : '未知'}`; + + // li.textContent = displayName; + // li.dataset.id = node.id; + // li.dataset.type = 'node'; + // nodesContainer.appendChild(li); + // }); + + // // 使用事件委托优化边点击 + // this.currentData.edges.forEach(edge => { + // const li = document.createElement('li'); + // const startName = edge.startNode && edge.startNode.properties && edge.startNode.properties.name + // ? edge.startNode.properties.name + // : (edge.startNodeId || '未知'); + // const endName = edge.endNode && edge.endNode.properties && edge.endNode.properties.name + // ? edge.endNode.properties.name + // : (edge.endNodeId || '未知'); + + // li.textContent = `${edge.type || '关系'}: ${startName} → ${endName}`; + // li.dataset.id = edge.id; + // li.dataset.type = 'edge'; + // li.dataset.edge = JSON.stringify(edge); // 预存边数据 + // edgesContainer.appendChild(li); + // }); + + // // 使用事件委托处理点击 + // nodesContainer.addEventListener('click', (e) => { + // const li = e.target.closest('li'); + // if (li && li.dataset.type === 'node') { + // this.showNodeDetail(li.dataset.id); + // } + // }); + + // edgesContainer.addEventListener('click', (e) => { + // const li = e.target.closest('li'); + // if (li && li.dataset.type === 'edge') { + // // 直接从dataset获取预存的边数据,避免立即调用API + // const edgeData = JSON.parse(li.dataset.edge); + // this.showEdgeDetail(li.dataset.id, edgeData); + // } + // }); + // } + + renderSidebarLists() { + const nodesContainer = document.getElementById('nodes-container'); + const edgesContainer = document.getElementById('edges-container'); + + nodesContainer.innerHTML = ''; + edgesContainer.innerHTML = ''; + + // 统计信息 + const statsHtml = ` +
+ 统计信息:
+ 节点: ${this.currentData.nodes.length}/${this.config.maxNodes}
+ 关系: ${this.currentData.edges.length}/${this.config.maxEdges} +
+ `; + + nodesContainer.innerHTML = statsHtml; + + // 批量处理节点列表 + const nodeFragment = document.createDocumentFragment(); + this.currentData.nodes.forEach(node => { + const li = document.createElement('li'); + const displayName = node.properties && node.properties.name + ? node.properties.name + : `节点 ${node.id ? node.id.substring(0, 8) : '未知'}`; + + li.textContent = displayName; + li.dataset.id = node.id; + li.dataset.type = 'node'; + li.dataset.node = JSON.stringify(node); // 预存节点数据 + nodeFragment.appendChild(li); + }); + nodesContainer.appendChild(nodeFragment); + + // 批量处理边列表 + const edgeFragment = document.createDocumentFragment(); + this.currentData.edges.forEach(edge => { + const li = document.createElement('li'); + const startName = edge.startNode && edge.startNode.properties && edge.startNode.properties.name + ? edge.startNode.properties.name + : (edge.startNodeId || '未知'); + const endName = edge.endNode && edge.endNode.properties && edge.endNode.properties.name + ? edge.endNode.properties.name + : (edge.endNodeId || '未知'); + + li.textContent = `${edge.type || '关系'}: ${startName} → ${endName}`; + li.dataset.id = edge.id; + li.dataset.type = 'edge'; + li.dataset.edge = JSON.stringify(edge); + edgeFragment.appendChild(li); + }); + edgesContainer.appendChild(edgeFragment); + + // 统一的事件委托处理 + this.setupSidebarEventDelegation(); + } + + // 单独设置事件委托的方法 + setupSidebarEventDelegation() { + const nodesContainer = document.getElementById('nodes-container'); + const edgesContainer = document.getElementById('edges-container'); + + // 移除旧的事件监听器(避免重复绑定) + nodesContainer.replaceWith(nodesContainer.cloneNode(true)); + edgesContainer.replaceWith(edgesContainer.cloneNode(true)); + + // 重新获取元素引用 + const newNodesContainer = document.getElementById('nodes-container'); + const newEdgesContainer = document.getElementById('edges-container'); + + // 节点点击事件委托 + newNodesContainer.addEventListener('click', (e) => { + const li = e.target.closest('li[data-type="node"]'); + if (li) { + const nodeData = JSON.parse(li.dataset.node); + this.showNodeDetail(li.dataset.id, nodeData); + } + }); + + // 边点击事件委托 + newEdgesContainer.addEventListener('click', (e) => { + const li = e.target.closest('li[data-type="edge"]'); + if (li) { + const edgeData = JSON.parse(li.dataset.edge); + this.showEdgeDetail(li.dataset.id, edgeData); + } + }); + } + + async saveNode(existingNode) { + try { + const name = document.getElementById('node-name').value; + const labels = document.getElementById('node-labels').value.split(',').map(l => l.trim()).filter(l => l); + const propertiesText = document.getElementById('node-properties').value || '{}'; + + let properties; + try { + properties = JSON.parse(propertiesText); + } catch (e) { + throw new Error('属性JSON格式不正确'); + } + + properties.name = name; // 确保名称属性 + + let result; + if (existingNode) { + console.log("更新节点:", existingNode.id, properties); + // 修改这里:将 properties 包装在对象中 + result = await this.api.updateNode(existingNode.id, { properties }); + } else { + console.log("创建节点:", properties, labels); + result = await this.api.createNode({ properties, labels }); + } + + // 检查API响应是否成功 + if (result && result.success) { + this.showSuccessMessage(existingNode ? '节点更新成功!' : '节点创建成功!'); + this.hideModal(); + this.loadData(); // 刷新数据 + } else { + throw new Error(result?.message || '操作失败,请检查服务器响应'); + } + } catch (error) { + console.error('保存节点失败:', error); + this.showErrorMessage('保存节点失败: ' + error.message); + } + } + + showSuccessMessage(message) { + this.showMessage(message, 'success'); + } + + showErrorMessage(message) { + this.showMessage(message, 'error'); + } + + showMessage(message, type = 'info') { + const existingMessage = document.getElementById('operation-message'); + if (existingMessage) { + existingMessage.remove(); + } + + const messageDiv = document.createElement('div'); + messageDiv.id = 'operation-message'; + messageDiv.className = `message ${type}`; + messageDiv.innerHTML = ` + ${message} + + `; + + document.body.appendChild(messageDiv); + + document.getElementById('close-message-btn').addEventListener('click', () => { + messageDiv.remove(); + }); + + if (type === 'success') { + setTimeout(() => { + if (messageDiv.parentNode) { + messageDiv.remove(); + } + }, 3000); + } + } + + async saveEdge(existingEdge) { + try { + const startNodeId = document.getElementById('edge-start').value; + const endNodeId = document.getElementById('edge-end').value; + const type = document.getElementById('edge-type').value; + const propertiesText = document.getElementById('edge-properties').value || '{}'; + + let properties; + try { + properties = JSON.parse(propertiesText); + } catch (e) { + throw new Error('属性JSON格式不正确'); + } + + let result; + if (existingEdge) { + console.log("更新关系:", existingEdge.id, properties); + // 修改这里:将 properties 包装在对象中 + result = await this.api.updateEdge(existingEdge.id, { properties }); + } else { + console.log("创建关系:", startNodeId, endNodeId, type, properties); + result = await this.api.createEdge({ startNodeId, endNodeId, type, properties }); + } + + if (result && result.success) { + this.showSuccessMessage(existingEdge ? '关系更新成功!' : '关系创建成功!'); + this.hideModal(); + this.loadData(); + } else { + throw new Error(result?.message || '操作失败,请检查服务器响应'); + } + } catch (error) { + console.error('保存关系失败:', error); + this.showErrorMessage('保存关系失败: ' + error.message); + } + } } // 初始化应用 document.addEventListener('DOMContentLoaded', () => { - new KnowledgeGraphApp(); + const app = new KnowledgeGraphApp(); + window.app = app; }); -// 在文件末尾添加 // 全局错误处理 window.addEventListener('error', function (e) { console.error('全局错误:', e.error); diff --git a/code/frontend/graph-renderer.js b/code/frontend/graph-renderer.js index d502758..45f8db7 100644 --- a/code/frontend/graph-renderer.js +++ b/code/frontend/graph-renderer.js @@ -2,9 +2,15 @@ class GraphRenderer { constructor(containerId) { this.containerId = containerId; this.graph = null; + this.app = null; // 添加应用实例引用 this.init(); } + // 设置应用实例的方法 + setAppInstance(app) { + this.app = app; + } + init() { const container = document.getElementById(this.containerId); const width = container.clientWidth; @@ -32,7 +38,9 @@ class GraphRenderer { }, defaultEdge: { style: { - stroke: '#e2e2e2' + stroke: '#e2e2e2', + lineWidth: 2, // 增加线宽确保可见 + opacity: 1 // 确保不透明 }, labelCfg: { autoRotate: true, @@ -50,20 +58,23 @@ class GraphRenderer { } }); - // 调整画布大小 - window.addEventListener('resize', () => { - if (!this.graph || this.graph.get('destroyed')) return; - const container = document.getElementById(this.containerId); - const width = container.clientWidth; - const height = container.clientHeight; - this.graph.changeSize(width, height); + // 添加渲染完成事件监听 + this.graph.on('afterrender', () => { + console.log('图谱渲染完成'); + const edges = this.graph.getEdges(); + console.log(`实际渲染的边数量: ${edges.length}`); }); } + //将数据转换为G6格式并渲染 render(data) { - if (!this.graph) return; + if (!this.graph) { + console.error("图谱实例未初始化"); + return; + } + + console.log("原始数据 - 节点:", data.nodes.length, "边:", data.edges.length); - // 确保节点有正确的ID和标签 const nodes = data.nodes.map(node => ({ id: node.id, label: node.properties && node.properties.name @@ -73,45 +84,134 @@ class GraphRenderer { labels: node.labels || [] })); - // 确保边有正确的源和目标 - const edges = data.edges.map(edge => ({ - id: edge.id, - source: edge.startNode ? edge.startNode.id : edge.startNodeId, - target: edge.endNode ? edge.endNode.id : edge.endNodeId, - label: edge.type || '关系', - properties: edge.properties || {} - })); + // 增强边的转换逻辑 + const edges = data.edges.map(edge => { + // 多种方式获取source和target + const source = edge.startNode ? edge.startNode.id : + edge.startNodeId ? edge.startNodeId : + edge.source; + + const target = edge.endNode ? edge.endNode.id : + edge.endNodeId ? edge.endNodeId : + edge.target; + + const edgeObj = { + id: edge.id, + source: source, + target: target, + label: edge.type || '关系', + properties: edge.properties || {} + }; + + // 检查source和target是否存在 + const sourceExists = nodes.some(n => n.id === source); + const targetExists = nodes.some(n => n.id === target); + + if (!sourceExists || !targetExists) { + console.warn(`边 ${edge.id} 的节点引用不存在:`, { + source, sourceExists, target, targetExists, + edgeData: edge + }); + } - console.log("渲染节点:", nodes); - console.log("渲染边:", edges); + return edgeObj; + }); + + // 过滤掉节点引用不存在的边 + const validEdges = edges.filter(edge => { + const sourceExists = nodes.some(n => n.id === edge.source); + const targetExists = nodes.some(n => n.id === edge.target); + return sourceExists && targetExists; + }); + + console.log("转换后 - 节点:", nodes.length, "有效边:", validEdges.length); + console.log("节点ID示例:", nodes.slice(0, 3).map(n => n.id)); + console.log("边示例:", validEdges.slice(0, 3)); this.graph.data({ nodes, - edges + edges: validEdges }); this.graph.render(); this.graph.fitView(); - // 添加节点点击事件 + this.bindGraphEvents(); + + try { + this.graph.data({ + nodes, + edges: validEdges + }); + + this.graph.render(); + this.graph.fitView(); + + // 检查渲染结果 + const renderedNodes = this.graph.getNodes(); + const renderedEdges = this.graph.getEdges(); + + console.log(`图谱渲染结果 - 节点: ${renderedNodes.length}, 边: ${renderedEdges.length}`); + + if (renderedEdges.length === 0 && validEdges.length > 0) { + console.warn("边数据存在但未被渲染,检查G6配置"); + // 强制重新设置边样式 + this.graph.getEdges().forEach(edge => { + this.graph.updateItem(edge, { + style: { + stroke: '#ff0000', // 红色,便于调试 + lineWidth: 2 + } + }); + }); + } + + this.bindGraphEvents(); + } catch (error) { + console.error("图谱渲染错误:", error); + } + } + + // ………………… + + // 事件绑定 + bindGraphEvents() { + this.graph.off('node:click'); + this.graph.off('edge:click'); + this.graph.on('node:click', (evt) => { const node = evt.item; const nodeId = node.get('model').id; - document.dispatchEvent(new CustomEvent('nodeClick', { detail: nodeId })); + console.log('节点被点击:', nodeId); + + if (this.app && this.app.showNodeDetail) { + this.app.showNodeDetail(nodeId); + } else { + console.error('应用实例未找到或showNodeDetail方法不存在'); + document.dispatchEvent(new CustomEvent('nodeClick', { detail: nodeId })); + } }); - // 添加边点击事件 this.graph.on('edge:click', (evt) => { const edge = evt.item; - const edgeId = edge.get('model').id; - document.dispatchEvent(new CustomEvent('edgeClick', { detail: edgeId })); + const edgeModel = edge.get('model'); + const edgeId = edgeModel.id; + console.log('边被点击 - 模型数据:', edgeModel); + console.log('边被点击 - ID:', edgeId); + + if (this.app && this.app.showEdgeDetail) { + this.app.showEdgeDetail(edgeId); + } else { + console.error('应用实例未找到或showEdgeDetail方法不存在'); + document.dispatchEvent(new CustomEvent('edgeClick', { detail: edgeId })); + } }); } + // 高亮节点 highlightNodes(nodeIds) { if (!this.graph) return; - // 先将所有节点恢复默认样式 this.graph.getNodes().forEach(node => { this.graph.updateItem(node, { style: { @@ -121,7 +221,6 @@ class GraphRenderer { }); }); - // 高亮选中的节点 nodeIds.forEach(nodeId => { const node = this.graph.findById(nodeId); if (node) { @@ -134,19 +233,32 @@ class GraphRenderer { } }); - // 聚焦到选中的节点 this.graph.focusItem(nodeIds[0]); } } -// 监听节点和边点击事件 +// document.addEventListener('nodeClick', (e) => { +// console.log('节点被点击:', e.detail); +// }); + +// document.addEventListener('edgeClick', (e) => { +// console.log('边被点击:', e.detail); +// }); + document.addEventListener('nodeClick', (e) => { - // 在实际应用中,这里可以调用应用实例的方法 - console.log('节点被点击:', e.detail); - // 例如: app.showNodeDetail(e.detail); + console.log('通过全局事件处理节点点击:', e.detail); + if (window.app && window.app.showNodeDetail) { + window.app.showNodeDetail(e.detail); + } else { + console.warn('无法处理节点点击事件:应用实例未找到'); + } }); document.addEventListener('edgeClick', (e) => { - console.log('边被点击:', e.detail); - // 例如: app.showEdgeDetail(e.detail); + console.log('通过全局事件处理边点击:', e.detail); + if (window.app && window.app.showEdgeDetail) { + window.app.showEdgeDetail(e.detail); + } else { + console.warn('无法处理边点击事件:应用实例未找到'); + } }); \ No newline at end of file diff --git a/code/frontend/index.html b/code/frontend/index.html index a6375a9..0e957fe 100644 --- a/code/frontend/index.html +++ b/code/frontend/index.html @@ -6,7 +6,6 @@ 知识图谱管理系统 - @@ -18,6 +17,19 @@

知识图谱管理系统

+ +
+ 节点: + + +
+ +
+ 边: + + +
+ @@ -45,7 +57,6 @@

关系列表

-