diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba86b70 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +dist.zip +build +dist-ssr +coverage +*.local + +/cypress/videos/ +/cypress/screenshots/ + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +*.tsbuildinfo diff --git a/src/apis/knowledge/chart.ts b/src/apis/knowledge/chart.ts new file mode 100644 index 0000000..005eea2 --- /dev/null +++ b/src/apis/knowledge/chart.ts @@ -0,0 +1,10 @@ +import { request } from '@/utils/request/index.ts'; + +// 列表接口 +export const dataApi = async (data?: Record) => { + return await request({ + url: `/v1.2/graph-connections/1/gremlin-query`, + method: 'POST', + params: data + }) +}; \ No newline at end of file diff --git a/src/views/knowledge/hooks/chart.ts b/src/views/knowledge/hooks/chart.ts index 3790039..81b2b12 100644 --- a/src/views/knowledge/hooks/chart.ts +++ b/src/views/knowledge/hooks/chart.ts @@ -41,14 +41,16 @@ export const useChartHooks = () => { }, }, animation: false, // 完全关闭动画 - animationDuration: 0, // 动画时长设为0 - animationDurationUpdate: 0, // 更新动画时长设为0 + animationDuration: 10, // 动画时长设为0 + animationDurationUpdate: 10, // 更新动画时长设为0 animationEasingUpdate: "linear", // 使用线性缓动 force: { - repulsion: 1000, + + repulsion: 800, // 减小排斥力,避免节点分布过散 gravity: 0.1, // 添加重力防止节点飞散 - friction: 0.6, // 添加摩擦力 + friction: 1, // 添加摩擦力 layoutAnimation: false, // 关闭力导向布局动画 + edgeLength: 100, // 设置边的理想长度,影响关系文本位置 }, // animationDurationUpdate: 100, // animationEasingUpdate: "quinticInOut", @@ -56,7 +58,7 @@ export const useChartHooks = () => { normal: { show: true, textStyle: { - fontSize: 12, + fontSize: 10, // 减小文本大小,使其与节点大小更匹配 }, }, }, @@ -69,7 +71,7 @@ export const useChartHooks = () => { { type: "graph", layout: "force", - symbolSize: 60, + symbolSize: 30, // 增大节点大小,使其与文本大小比例更协调 focusNodeAdjacency: true, roam: true, categories: [ @@ -102,7 +104,7 @@ export const useChartHooks = () => { normal: { show: true, textStyle: { - fontSize: 12, + fontSize: 10, // 减小文本大小,使其与节点大小更匹配 }, formatter: (e: Record) => { const name = e.data.raw.properties.name @@ -111,16 +113,19 @@ export const useChartHooks = () => { }, }, force: { - repulsion: 1000, + repulsion: 800, // 减小排斥力,避免节点分布过散 + edgeLength: 100, // 设置边的理想长度 }, - edgeSymbolSize: [4, 50], + edgeSymbolSize: [4, 30], // 减小边的箭头大小,使其更协调 edgeLabel: { normal: { show: true, textStyle: { - fontSize: 10, + fontSize: 9, // 减小关系文本大小 }, formatter: "{c}", + position: 'middle', // 确保文本位于边的中间 + offset: [0, 5], // 微调文本位置,避免与边重叠 }, }, data: [ @@ -309,8 +314,10 @@ export const useChartHooks = () => { raw: data, id: data.id, name: data.name, - draggable: options?.draggable || false, - itemStyle: data?.itemStyle || null + draggable: false, + itemStyle: data?.itemStyle || null, + x: data?.x|| null, + y: data?.y || null, } } diff --git a/src/views/knowledge/hooks/location copy.ts b/src/views/knowledge/hooks/location copy.ts new file mode 100644 index 0000000..b6c80a2 --- /dev/null +++ b/src/views/knowledge/hooks/location copy.ts @@ -0,0 +1,273 @@ +import { ref, computed } from 'vue'; + +/** + * 位置管理钩子函数 + * 用于生成和管理知识图谱节点的树状布局位置 + */ +export const useLocationHooks = () => { + // 位置映射表,用于存储节点ID到位置的映射 + const locationMap = ref(new Map()); + + // 配置参数 + const config = ref({ + // 层级之间的垂直间距 + levelGap: 200, // 纵向间距20 + // 同一层级内节点的水平间距 + nodeGap: 100, // 横向间距10 + // 画布中心位置 + centerX: 400, + centerY: 300, + // 初始缩放比例 + scale: 1 + }); + + /** + * 计算树状布局的节点位置 + * @param nodes 节点列表 + * @param relationships 关系列表 + * @param rootIds 根节点ID列表 + * @returns 更新后的节点列表(包含位置信息) + */ + const calculateTreeLayout = (nodes: any[], relationships: any[], rootIds: string[]) => { + // 清除之前的位置信息 + clearLocations(); + + // 构建节点ID到节点的映射 + const nodeMap = new Map(nodes.map(node => [node.id, node])); + + // 构建父-子关系树 + const tree = buildTreeStructure(nodeMap, relationships, rootIds); + +console.log('tree', tree) + + // 计算每个子树的边界 + rootIds.forEach(rootId => { + calculateSubtreeBounds(tree[rootId]); + }); + + // 计算每个根树的位置偏移 + let totalWidth = 0; + const rootOffsets: number[] = []; + + // 计算总宽度 + rootIds.forEach(rootId => { + const root = tree[rootId]; + rootOffsets.push(totalWidth); + totalWidth += root.width * config.value.nodeGap; + }); + + // 应用位置计算(从下往上) + rootIds.forEach((rootId, index) => { + // 计算根节点的水平位置(居中分布) + const offsetX = config.value.centerX - totalWidth * config.value.nodeGap * 0.5 + rootOffsets[index] * config.value.nodeGap; + + // 递归计算每个节点的最终位置 + calculateFinalPosition(tree[rootId], offsetX, config.value.centerY); + }); + + // 更新节点列表,添加位置信息 + return nodes.map(node => ({ + ...node, + x: locationMap.value.get(node.id)?.x, + y: locationMap.value.get(node.id)?.y + })); + }; + + /** + * 构建树状数据结构 + * @param nodeMap 节点映射表 + * @param relationships 关系列表 + * @param rootIds 根节点ID列表 + * @returns 树状结构 + */ + const buildTreeStructure = (nodeMap: Map, relationships: any[], rootIds: string[]) => { + const tree: Record = {}; + const childrenMap = new Map(); + const parentMap = new Map(); + + // 初始化每个节点的子节点列表 + nodeMap.forEach((node, id) => { + tree[id] = { + id, + children: [], + // 用于布局计算的属性 + width: 0, // 子树宽度(以节点数为单位) + left: 0, // 子树左边界(以节点数为单位) + right: 0, // 子树右边界(以节点数为单位) + center: 0 // 子树中心位置(以节点数为单位) + }; + childrenMap.set(id, []); + }); + + // 构建父-子关系 + relationships.forEach(relation => { + const parentId = relation.start_node_id; + const childId = relation.end_node_id; + if (childrenMap.has(parentId)) { + childrenMap.get(parentId)?.push(childId); + } + parentMap.set(childId, parentId); + }); + + // 构建完整的树结构 + childrenMap.forEach((childrenIds, parentId) => { + if (tree[parentId]) { + tree[parentId].children = childrenIds.map(childId => tree[childId]); + } + }); + + return tree; + }; + + /** + * 计算子树的边界和宽度(后序遍历) + * @param node 当前节点 + */ + const calculateSubtreeBounds = (node: any) => { + // 如果是叶子节点 + if (!node.children || node.children.length === 0) { + node.width = 1; + node.left = 0; + node.right = 0; + node.center = 0; + return; + } + + // 递归计算所有子节点的边界 + node.children.forEach((child: any) => { + calculateSubtreeBounds(child); + }); + + // 计算当前节点的子树边界 + let totalLeft = 0; + let totalRight = 0; + let totalWidth = 0; + + node.children.forEach((child: any) => { + // 更新总宽度 + totalWidth += child.width; + if (node.children.length > 1) { + totalWidth += 1; // 添加子节点之间的间距(以节点数为单位) + } + }); + + // 计算每个子节点的相对位置 + let currentPos = 0; + node.children.forEach((child: any, index: number) => { + // 记录子节点的相对位置 + child.relativeX = currentPos - child.center; + currentPos += child.width; + + // 添加子节点之间的间距 + if (index < node.children.length - 1) { + currentPos += 1; // 添加一个节点宽度的间距 + } + }); + + // 设置当前节点的边界信息 + node.width = totalWidth; + node.left = 0; + node.right = totalWidth - 1; + node.center = Math.floor(totalWidth / 2); + }; + + /** + * 计算节点的最终位置(前序遍历) + * @param node 当前节点 + * @param offsetX 水平偏移量 + * @param y Y坐标 + * @param level 当前层级 + */ + const calculateFinalPosition = (node: any, offsetX: number, y: number, level: number = 0) => { + // 计算当前节点的X坐标 + const nodeX = offsetX + node.center * config.value.nodeGap; + + // 计算当前节点的Y坐标(从上往下排列) + const nodeY = y + level * config.value.levelGap; + + // 存储节点位置 + locationMap.value.set(node.id, { + x: nodeX, + y: nodeY + }); + + // 递归计算子节点的位置 + if (node.children && node.children.length > 0) { + node.children.forEach((child: any) => { + const childOffsetX = offsetX + child.relativeX * config.value.nodeGap; + calculateFinalPosition(child, childOffsetX, y, level + 1); + }); + } + }; + + /** + * 根据节点ID获取位置 + * @param id 节点ID + * @returns 节点位置 + */ + const getLocationById = (id: string) => { + return locationMap.value.get(id); + }; + + /** + * 批量设置节点位置 + * @param positions 位置映射对象 {nodeId: {x, y}} + */ + const setLocations = (positions: Record) => { + Object.entries(positions).forEach(([id, position]) => { + locationMap.value.set(id, position); + }); + }; + + /** + * 清除所有位置信息 + */ + const clearLocations = () => { + locationMap.value.clear(); + }; + + /** + * 更新配置参数 + * @param newConfig 新的配置参数 + */ + const updateConfig = (newConfig: Partial) => { + config.value = { ...config.value, ...newConfig }; + }; + + /** + * 应用位置信息到ECharts选项配置 + * @param option ECharts选项配置 + * @returns 更新后的ECharts选项配置 + */ + const applyLocationsToOption = (option: any) => { + if (!option.series || !option.series[0]) return option; + + const updatedSeries = { + ...option.series[0], + // 更新节点数据,添加位置信息 + data: option.series[0].data.map((node: any) => ({ + ...node, + ...locationMap.value.get(node.id) + })) + }; + + return { + ...option, + series: [updatedSeries] + }; + }; + + return { + // 状态 + locationMap, + config, + + // 方法 + calculateTreeLayout, + getLocationById, + setLocations, + clearLocations, + updateConfig, + applyLocationsToOption + }; +}; \ No newline at end of file diff --git a/src/views/knowledge/hooks/location.ts b/src/views/knowledge/hooks/location.ts new file mode 100644 index 0000000..0929c54 --- /dev/null +++ b/src/views/knowledge/hooks/location.ts @@ -0,0 +1,266 @@ +import { ref } from 'vue'; + +/** + * 位置管理钩子函数 + * 用于生成和管理知识图谱节点的树状布局位置 + */ +export const useLocationHooks = () => { + // 位置映射表,用于存储节点ID到位置的映射 + const locationMap = ref(new Map()); + + // 配置参数 + const config = ref({ + // 层级之间的垂直间距 + levelGap: 120, // 增加垂直间距,避免节点垂直重叠 + // 同一层级内节点的水平间距 + nodeGap: 180, // 增加水平间距,避免节点水平重叠 + // 树之间的间距(增加以避免不同树的叶节点重叠) + treeGap: 200, + // 画布中心位置 + centerX: 400, + centerY: 300, + // 初始缩放比例 + scale: 1 + }); + + /** + * 计算树状布局的节点位置 + * @param nodes 节点列表 + * @param relationships 关系列表 + * @param rootIds 根节点ID列表 + * @returns 更新后的节点列表(包含位置信息) + */ + const calculateTreeLayout = (nodes: any[], relationships: any[], rootIds: string[]) => { + // 清除旧的位置信息 + clearLocations(); + + // 构建节点ID到节点的映射 + const nodeMap = new Map(nodes.map(node => [node.id, node])); + + // 构建父-子关系树(倒序) + const tree = buildTreeStructure(nodeMap, relationships, rootIds); + + // 后序遍历计算每个子树的边界和中心位置 + rootIds.slice().reverse().forEach(rootId => { + calculateSubtreeBounds(tree[rootId]); + }); + + // 计算所有根树的总宽度(包括树间距) + const totalWidth = rootIds.reduce((sum, rootId) => { + return sum + tree[rootId].width + config.value.treeGap; + }, 0) - config.value.treeGap; // 减去最后一个树间距 + + // 根据总宽度计算起始X位置,使整个布局居中 + let currentX = config.value.centerX - totalWidth / 2; + + // 前序遍历计算每个节点的最终位置 + rootIds.slice().reverse().forEach((rootId, index) => { + const rootNode = tree[rootId]; + + // 计算当前根树的起始位置 + const rootX = currentX + rootNode.width / 2; + + // 递归计算每个节点的最终位置 + calculateFinalPosition(rootNode, rootX, config.value.centerY, 0); + + // 更新当前X位置,为下一个根树留出足够空间 + currentX += rootNode.width + config.value.treeGap; + }); + + // 更新节点列表,添加位置信息 + return nodes.map(node => ({ + ...node, + x: locationMap.value.get(node.id)?.x, + y: locationMap.value.get(node.id)?.y + })); + }; + + /** + * 构建树状数据结构(倒序) + * @param nodeMap 节点映射表 + * @param relationships 关系列表 + * @param rootIds 根节点ID列表 + * @returns 树状结构 + */ + const buildTreeStructure = (nodeMap: Map, relationships: any[], rootIds: string[]) => { + const tree: Record = {}; + const childrenMap = new Map(); + const parentMap = new Map(); + + // 初始化每个节点的子节点列表 + nodeMap.forEach((node, id) => { + tree[id] = { + id, + children: [], + // 布局相关属性 + width: 0, // 子树宽度 + left: 0, // 子树最左边界 + right: 0, // 子树最右边界 + center: 0, // 子树中心位置 + relativeX: 0 // 相对于父节点的X偏移 + }; + childrenMap.set(id, []); + }); + + // 构建父-子关系 + relationships.forEach(relation => { + const parentId = relation.start_node_id; + const childId = relation.end_node_id; + if (childrenMap.has(parentId)) { + childrenMap.get(parentId)?.push(childId); + parentMap.set(childId, parentId); + } + }); + + // 构建完整的树结构(子节点倒序排列) + childrenMap.forEach((childrenIds, parentId) => { + if (tree[parentId]) { + // 倒序添加子节点,确保从右到左布局 + tree[parentId].children = childrenIds.slice().reverse().map(childId => tree[childId]); + } + }); + + return tree; + }; + + /** + * 后序遍历计算子树边界 + * @param node 当前节点 + */ + const calculateSubtreeBounds = (node: any) => { + if (!node || !node.children || node.children.length === 0) { + // 叶子节点 + node.width = config.value.nodeGap; + node.left = -config.value.nodeGap / 2; + node.right = config.value.nodeGap / 2; + node.center = 0; + return; + } + + // 先计算所有子节点的边界 + node.children.forEach((child: any) => { + calculateSubtreeBounds(child); + }); + + // 计算当前节点的边界 + let totalWidth = 0; + const childCount = node.children.length; + + // 计算所有子树的总宽度(包括间距) + node.children.forEach((child: any) => { + totalWidth += child.width + config.value.nodeGap; + }); + + // 减去最后一个子树后面的间距 + totalWidth -= config.value.nodeGap; + + // 计算每个子节点的相对位置 + let currentX = -totalWidth / 2; + node.children.forEach((child: any) => { + child.relativeX = currentX + child.width / 2; + currentX += child.width + config.value.nodeGap; + }); + + // 更新当前节点的边界信息 + node.width = totalWidth; + node.left = -totalWidth / 2; + node.right = totalWidth / 2; + node.center = 0; + }; + + /** + * 前序遍历计算节点的最终位置 + * @param node 当前节点 + * @param x 父节点的X坐标 + * @param y 父节点的Y坐标 + * @param level 当前层级 + */ + const calculateFinalPosition = (node: any, x: number, y: number, level: number) => { + if (!node) return; + + // 计算当前节点的最终位置 + const nodeX = x + node.relativeX; + const nodeY = y + (level * config.value.levelGap); + + // 存储节点位置 + locationMap.value.set(node.id, { x: nodeX, y: nodeY }); + + // 递归计算子节点的位置 + if (node.children && node.children.length > 0) { + node.children.forEach((child: any) => { + calculateFinalPosition(child, nodeX, nodeY, level + 1); + }); + } + }; + + /** + * 根据节点ID获取位置 + * @param id 节点ID + * @returns 节点位置 + */ + const getLocationById = (id: string) => { + return locationMap.value.get(id); + }; + + /** + * 批量设置节点位置 + * @param positions 位置映射对象 {nodeId: {x, y}} + */ + const setLocations = (positions: Record) => { + Object.entries(positions).forEach(([id, position]) => { + locationMap.value.set(id, position); + }); + }; + + /** + * 清除所有位置信息 + */ + const clearLocations = () => { + locationMap.value.clear(); + }; + + /** + * 更新配置参数 + * @param newConfig 新的配置参数 + */ + const updateConfig = (newConfig: Partial) => { + config.value = { ...config.value, ...newConfig }; + }; + + /** + * 应用位置信息到ECharts选项配置 + * @param option ECharts选项配置 + * @returns 更新后的ECharts选项配置 + */ + const applyLocationsToOption = (option: any) => { + if (!option.series || !option.series[0]) return option; + + const updatedSeries = { + ...option.series[0], + // 更新节点数据,添加位置信息 + data: option.series[0].data.map((node: any) => ({ + ...node, + ...locationMap.value.get(node.id) + })) + }; + + return { + ...option, + series: [updatedSeries] + }; + }; + + return { + // 状态 + locationMap, + config, + + // 方法 + calculateTreeLayout, + getLocationById, + setLocations, + clearLocations, + updateConfig, + applyLocationsToOption + }; +}; \ No newline at end of file diff --git a/src/views/knowledge/hooks/utils.ts b/src/views/knowledge/hooks/utils.ts index 348d7fd..ae60949 100644 --- a/src/views/knowledge/hooks/utils.ts +++ b/src/views/knowledge/hooks/utils.ts @@ -1,8 +1,12 @@ -export const useUtils = () => { - +export const useUtils = (options: Record) => { + // 预设的颜色 + const presetColors = [...(options?.colors || [])] + console.log('presetColors', presetColors) // 存储已生成的颜色,确保不重复 const usedColors = new Set(); + + // 生成随机颜色 const generateColor = (): string => { // 生成更亮的颜色,避免太暗 @@ -19,7 +23,8 @@ export const useUtils = () => { // 尝试生成不重复的颜色,最多尝试100次 do { - color = generateColor() + // 有预设先用预设 + color = presetColors.shift() || generateColor() attempts++ } while (usedColors.has(color) && attempts < 100) diff --git a/src/views/knowledge/index.vue b/src/views/knowledge/index.vue index af1acb9..a3681ec 100644 --- a/src/views/knowledge/index.vue +++ b/src/views/knowledge/index.vue @@ -1,24 +1,33 @@