树状知识图谱

This commit is contained in:
yans 2025-11-25 16:12:25 +08:00
parent 0a6fff3cf2
commit c58a029c10
12 changed files with 1160 additions and 448 deletions

32
.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,10 @@
import { request } from '@/utils/request/index.ts';
// 列表接口
export const dataApi = async (data?: Record<string, any>) => {
return await request({
url: `/v1.2/graph-connections/1/gremlin-query`,
method: 'POST',
params: data
})
};

View File

@ -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<string, any>) => {
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,
}
}

View File

@ -0,0 +1,273 @@
import { ref, computed } from 'vue';
/**
*
*
*/
export const useLocationHooks = () => {
// 位置映射表用于存储节点ID到位置的映射
const locationMap = ref(new Map<string, { x: number; y: number }>());
// 配置参数
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<string, any>, relationships: any[], rootIds: string[]) => {
const tree: Record<string, any> = {};
const childrenMap = new Map<string, string[]>();
const parentMap = new Map<string, string>();
// 初始化每个节点的子节点列表
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<string, { x: number; y: number }>) => {
Object.entries(positions).forEach(([id, position]) => {
locationMap.value.set(id, position);
});
};
/**
*
*/
const clearLocations = () => {
locationMap.value.clear();
};
/**
*
* @param newConfig
*/
const updateConfig = (newConfig: Partial<typeof config.value>) => {
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
};
};

View File

@ -0,0 +1,266 @@
import { ref } from 'vue';
/**
*
*
*/
export const useLocationHooks = () => {
// 位置映射表用于存储节点ID到位置的映射
const locationMap = ref(new Map<string, { x: number; y: number }>());
// 配置参数
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<string, any>, relationships: any[], rootIds: string[]) => {
const tree: Record<string, any> = {};
const childrenMap = new Map<string, string[]>();
const parentMap = new Map<string, string>();
// 初始化每个节点的子节点列表
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<string, { x: number; y: number }>) => {
Object.entries(positions).forEach(([id, position]) => {
locationMap.value.set(id, position);
});
};
/**
*
*/
const clearLocations = () => {
locationMap.value.clear();
};
/**
*
* @param newConfig
*/
const updateConfig = (newConfig: Partial<typeof config.value>) => {
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
};
};

View File

@ -1,8 +1,12 @@
export const useUtils = () => {
export const useUtils = (options: Record<string, any>) => {
// 预设的颜色
const presetColors = [...(options?.colors || [])]
console.log('presetColors', presetColors)
// 存储已生成的颜色,确保不重复
const usedColors = new Set<string>();
// 生成随机颜色
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)

View File

@ -1,24 +1,33 @@
<script setup lang="ts">
import { ref, inject, defineComponent, nextTick, computed, onUnmounted, onMounted } from "vue";
import Chat from "@/views/knowledge/components/Chat/chat.vue";
import { useServices } from "@/views/knowledge/services/index";
import dayjs from "dayjs";
import { Input, Select, SelectOption } from "ant-design-vue";
import _ from "lodash";
import {
ref,
inject,
defineComponent,
nextTick,
computed,
onUnmounted,
onMounted,
} from 'vue'
import Chat from '@/views/knowledge/components/Chat/chat.vue'
import { useServices } from '@/views/knowledge/services/index'
import dayjs from 'dayjs'
import { Input, Select, SelectOption } from 'ant-design-vue'
import _ from 'lodash'
const services = useServices();
const services = useServices()
const echartRef = ref<any>(null);
const echartRef = ref<any>(null)
const currentNode = ref({});
const showRelationData = ref([]);
const currentNode = ref({})
const showRelationData = ref([])
const echartInit = (chart: any) => {
echartRef.value = chart;
echartRef.value.on("click", function (params: any) {
console.log("params", params);
const nodeId = params.data.id;
services.db.api.onPointClick(nodeId);
echartRef.value = chart
echartRef.value.on('click', function (params: any) {
console.log('params', params)
const nodeId = params.data.id
services.db.api.onPointClick(nodeId)
// onPointClick(params.data.id);
// console.log("params", params);
// // console.log("params", params);
@ -27,30 +36,30 @@ const echartInit = (chart: any) => {
// currentNode.value = params.data.raw;
// showRelationData.value = nodes;
// console.log("nodes", showRelationData.value);
});
};
})
}
const showData = computed(() => {
// console.log('useNode', showOptions)
// return {};
const curServer = "db";
return services[curServer].state.showOptions.value;
const curServer = 'db'
return services[curServer].state.showOptions.value
// return {};
});
})
const onChange = _.debounce((id: string) => {
console.log(id, "id");
services.db.api.onSearch(id);
}, 1000);
console.log(id, 'id')
services.db.api.onSearch(id)
}, 1000)
const prefix = "knowledge-chat-page";
const prefix = 'knowledge-chat-page'
onMounted(() => {
services.db.api.getData();
services.db.api.getQuery();
});
services.db.api.getData()
services.db.api.getQuery()
})
onUnmounted(() => {});
onUnmounted(() => {})
</script>
<template>
@ -103,19 +112,31 @@ onUnmounted(() => {});
<div class="node-wrapper">
<div class="label">切片</div>
<a-tooltip>
<template #title>{{ currentNode.properties.origin_text }}</template>
<div class="node-desc">{{ currentNode.properties.origin_text }}</div>
<template #title>{{
currentNode.properties.origin_text
}}</template>
<div class="node-desc">
{{ currentNode.properties.origin_text }}
</div>
</a-tooltip>
</div>
<div class="node-wrapper">
<div class="label">创建时间</div>
{{ dayjs(currentNode.properties.created_at).format("YYYY-MM-DD hh:mm:ss") }}
{{
dayjs(currentNode.properties.created_at).format(
'YYYY-MM-DD hh:mm:ss'
)
}}
</div>
<div class="node-wrapper">
<div class="label">更新时间</div>
{{ dayjs(currentNode.properties.last_updated).format("YYYY-MM-DD hh:mm:ss") }}
{{
dayjs(currentNode.properties.last_updated).format(
'YYYY-MM-DD hh:mm:ss'
)
}}
</div>
</div>
<!-- 关联 -->
@ -142,18 +163,28 @@ onUnmounted(() => {});
<div class="label">切片</div>
<a-tooltip>
<template #title>{{ item.node.properties.origin_text }}</template>
<div class="node-desc">{{ item.node.properties.origin_text }}</div>
<div class="node-desc">
{{ item.node.properties.origin_text }}
</div>
</a-tooltip>
</div>
<div class="node-wrapper">
<div class="label">创建时间</div>
{{ dayjs(item.node.properties.created_at).format("YYYY-MM-DD hh:mm:ss") }}
{{
dayjs(item.node.properties.created_at).format(
'YYYY-MM-DD hh:mm:ss'
)
}}
</div>
<div class="node-wrapper">
<div class="label">更新时间</div>
{{ dayjs(item.node.properties.last_updated).format("YYYY-MM-DD hh:mm:ss") }}
{{
dayjs(item.node.properties.last_updated).format(
'YYYY-MM-DD hh:mm:ss'
)
}}
</div>
</div>
</div>
@ -179,7 +210,7 @@ onUnmounted(() => {});
</template>
<style lang="scss" scoped>
$prefix: "knowledge-chat-page";
$prefix: 'knowledge-chat-page';
.#{$prefix} {
width: 100%;

View File

@ -1,356 +0,0 @@
import { useChartHooks } from '@/views/knowledge/hooks/chart.ts'
import { computed, ref } from 'vue'
import { router } from "@/router/index.ts";
import { createData } from './mockData'
// 存储已生成的颜色,确保不重复
const usedColors = new Set<string>()
/**
*
* @returns
*/
const generateRandomColor = (): string => {
// 生成随机颜色
const generateColor = (): string => {
// 生成更亮的颜色,避免太暗
const r = Math.floor(Math.random() * 155) + 100
const g = Math.floor(Math.random() * 155) + 100
const b = Math.floor(Math.random() * 155) + 100
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`
}
let color: string
let attempts = 0
// 尝试生成不重复的颜色最多尝试100次
do {
color = generateColor()
attempts++
} while (usedColors.has(color) && attempts < 100)
// 如果尝试次数过多,清空已使用的颜色重新开始
if (attempts >= 100) {
usedColors.clear()
color = generateColor()
}
usedColors.add(color)
return color
}
/**
*
*/
const relationColors = ref<Map<string, string>>(new Map())
/**
*
*/
const getRelationColor = (type: string): string => {
if (!relationColors.value.has(type)) {
relationColors.value.set(type, generateRandomColor())
}
return relationColors.value.get(type) || '#000000'
}
/**
*
*/
const nodeColors = ref<Map<string, string>>(new Map())
/**
*
*/
const getNodeColor = (type: string): string => {
if (!nodeColors.value.has(type)) {
nodeColors.value.set(type, generateRandomColor())
}
return nodeColors.value.get(type) || '#000000'
}
export const useChartServices = () => {
const { option, createPointData, createLink} = useChartHooks()
// 图表原始数据
const chatData = ref({})
// 类型字典集合
const typesMap = ref<string[]>([])
// 节点集合
const nodes = ref<Record<string, any>[]>([])
// 节点字典集合
const nodesMap = computed(() => {
const dataMap = new Map()
nodes.value.forEach((node) => {
if (!dataMap.has(node.id)) {
dataMap.set(node.id, node)
}
})
return dataMap
})
// 关系字典集合
const relationMap = computed(() => {
const dataMap = new Map()
console.log('relationMap', relations.value)
relations.value.forEach((relation) => {
if (!dataMap.has(relation.id)) {
dataMap.set(relation.raw.id, relation)
}
})
return dataMap
})
// 关系节点集合
const relations = ref<Record<string, any>[]>([])
// 数据规整
const chartConfig = {
chatData,
relationMap: relationMap,
typesMap,
nodes,
relations
}
// 选中的类型
const selectTypes = ref<string[]>([])
// 选中的关系
const selectRelations = ref<string[]>([])
// 切换选中类型
const toggleSelectType = (type: string) => {
const i = selectTypes.value.indexOf(type)
if (i > -1) {
selectTypes.value.splice(i, 1)
} else {
selectTypes.value.push(type)
}
}
// 切换选中关系
const toggleRelation = (relation: string) => {
const i = selectRelations.value.indexOf(relation)
if (i > -1) {
selectRelations.value.splice(i, 1)
} else {
selectRelations.value.push(relation)
}
}
// 重置数据
const resetData = () => {
chatData.value = {}
relations.value = []
typesMap.value = []
nodes.value = []
// 重置颜色映射,以便下次生成新的颜色
relationColors.value.clear()
nodeColors.value.clear()
}
// 新增类型,已存在则跳过
const addTypeMap = (data: Record<string, any>) => {
if (typesMap.value.includes(data.labels)) return
typesMap.value.push(data.labels)
}
// 处理数据
const handleData = (data: Record<string, any>) => {
const { nodes: rawNodes, relationships } = data
rawNodes.forEach((node: Record<string, any>) => {
addTypeMap(node)
// 获取节点类型对应的颜色
const nodeColor = getNodeColor(node.labels)
const nodeInfo = createPointData({
...node,
type: node.labels,
name: node.id,
itemStyle: {
color: nodeColor
},
// name: node.properties.name
})
nodes.value.push(nodeInfo)
})
relationships.forEach((relation: Record<string, any>) => {
// addRelationMap(relation)
// 获取关系类型对应的颜色
const relationColor = getRelationColor(relation.type)
const link = createLink({
...relation,
parentIndex: getNodeIndex(relation.start_node_id),
index: getNodeIndex(relation.end_node_id),
type: relation.type,
lineStyle: {
color: relationColor,
width: 2 // 关系线条宽度
},
itemStyle: {
color: relationColor
}
})
relations.value.push(link)
})
}
// 获取节点索引
const getNodeIndex = (id: string, source?: Record<string, any>) => {
return (source || nodes.value).findIndex((item:Record<string, any>) => item.id === id)
}
// 显示用的数据
const showData = computed(() => {
// 生成带有颜色的分类配置
// const categoriesWithColor = relationsMap.value.map(item => ({
// name: item,
// itemStyle: {
// normal: {
// color: getRelationColor(item)
// }
// }
// }))
// 为每个节点添加category索引使其与对应关系类型关联
const nodesWithCategory = nodes.value.map(node => ({
...node,
// category: relationsMap.value.indexOf(node.type) !== -1 ?
// relationsMap.value.indexOf(node.type) : 0
}))
const useData = {
...option.value,
series: [
{
...option.value.series[0],
// categories: categoriesWithColor,
data: nodesWithCategory,
links: relations.value,
}
]
}
console.log('useData', useData)
return useData
})
const getDataPath = () => {
const { id } = router.currentRoute.value.query;
const jsonMap: Record<string, any> = {
'1': 'http://222.190.139.186:10001/public/demo/knowledge-chat/neo4j_export2.json',
'2': 'http://222.190.139.186:10001/public/demo/knowledge-chat/neo4j_export.json'
}
return jsonMap[id as string] || jsonMap[2]
}
const getMockData = () => {
const data = createData()
return data
}
const getData = async () => {
// const path = getDataPath()
// const res = await fetch(path)
// const mockData = await res.json()
const mockData = getMockData()
console.log('mockData', mockData)
chatData.value = mockData
handleData(chatData.value)
return mockData
}
// 获取节点关系
const getNodeRelationById = (id: string) => {
console.log('relations', relationMap.value)
const relationNode = []
relations.value.forEach((item: Record<string, any>) => {
if (item.raw.end_node_id == id || item.raw.start_node_id == id) {
let targetNodeId = null
// 如果是起始点, 相关点是终点
if (item.raw.start_node_id === id) {
targetNodeId = item.raw.end_node_id
} else {
// 如果是终点, 相关点是起始点
targetNodeId = item.raw.start_node_id
}
const nodeInfo = getNodeById(targetNodeId)
if (!nodeInfo) return
const params = {
type: item.raw.type,
node: nodeInfo.raw,
relation: item.raw
}
relationNode.push(params)
}
// 如果是起始点
// 如果是终点
// if (item.start_node_id === id) {
// targets.push(item)
// } else if (item.end_node_id === id) {
// ends.push(item)
// }
})
return relationNode
}
// 获取节点信息
const getNodeById = (id: string) => {
if (!id) return
return nodesMap.value.get(id)
}
const searchValue = ref('')
// 显示节点
const onShowNodeEven = () => {
}
// 隐藏节点
const onHideNodeEven = () => {}
const state = {
option,
showData,
chartConfig,
relationColors,
nodeColors,
searchValue
}
const api = {
getData,
handleData,
// addRelationMap,
addTypeMap,
resetData,
toggleSelectType,
toggleRelation,
getNodeById,
getNodeRelationById,
onShowNodeEven,
onHideNodeEven
}
return {
state,
api
}
}

View File

@ -0,0 +1,390 @@
import { ref, computed } from 'vue'
import { createData } from './mockData'
import { useChartHooks } from '@/views/knowledge/hooks/chart.ts'
import { useLocationHooks } from '@/views/knowledge/hooks/location.ts'
import { router } from "@/router/index.ts"
import { useUtils } from '@/views/knowledge/hooks/utils.ts'
export const useOperateServices = () => {
const lightColors = ['#D4BDFB', '#9DD0FF', '#57DFB8', '#59E433', '#FFD1A6', '#E1DF62', '#F5B4B4', '#8FE4ED']
const colors = ['#997AE7', '#7096E2', '#1FAA86', '#1BBB2B', '#F99D47', '#E4CF67', '#DF8D8D', '#4FC0D2']
const { createColor } = useUtils({
colors: colors
})
const { option, createPointData, createLink} = useChartHooks()
const {
calculateTreeLayout,
applyLocationsToOption,
getLocationById,
setLocations,
clearLocations
} = useLocationHooks()
// 图表数据
const graphData = ref<Record<string, any>>({
roots: [],
nodes: [],
relationships: []
})
// 节点映射
const nodesMap = new Map()
// 关系映射
const relationsMap = new Map()
// 分类映射
const categoriesMap = new Map()
// 更新位置
const updateLocation = (echartRef: Record<string, any>) => {
console.log('echartRef', echartRef)
}
// 获取数据
const getData = () => {
const data = createData()
const roots = getMainNodes(data)
handleData(data)
graphData.value = {
roots: roots,
nodes: Array.from(nodesMap.values()),
relationships: Array.from(relationsMap.values())
}
console.log('graphData', graphData.value)
}
// 更新数据
const updateData = () => {
graphData.value = {
...graphData.value,
nodes: Array.from(nodesMap.values()),
relationships: Array.from(relationsMap.values())
}
}
// 处理数据
const handleData = (data: Record<string, any>) => {
const { nodes, relationships } = data
createCategoriesMap(nodes)
createNodeMap(nodes)
createRelationMap(relationships)
relationships.forEach((item: Record<string, any>) => {
const start = item.start_node_id
const end = item.end_node_id
if (nodesMap.has(start)) {
let target = nodesMap.get(start)
if (!target.childrenLinks) target.childrenLinks = []
target.childrenLinks.push(relationsMap.get(item.id))
}
if (nodesMap.has(end)) {
let target = nodesMap.get(end)
if (!target.parentLinks) target.parentLinks = []
target.parentLinks.push(relationsMap.get(item.id))
}
})
return {
nodes,
relationships
}
}
// 可视的节点
const showNodes = computed(() => {
const { roots, nodes } = graphData.value
const useNodes = nodes.filter((item: Record<string, any>) => {
const isRoots = roots.find((rootNode: Record<string, any>) => rootNode.id === item.id)
if (isRoots) return true
else {
return item.show
}
})
return useNodes
})
// 可视的关系
const showRelations = computed(() => {
const useNode = showNodes.value
const useRelations: Record<string, any>[] = []
useNode?.map((item: Record<string, any>) => {
if (item.parentLinks) {
useRelations.push(...item.parentLinks)
}
})
return useRelations
})
// 可视的echart配置
const showOptions = computed(() => {
const categories = Array.from(categoriesMap, ([key, value]) => {
return {
name: key,
itemStyle: {
color: value.color
}
}
})
const getCategoriesIndex = (category: string) => {
const i = categories.findIndex((item: Record<string, any>) => {
return item.name == category
})
return i
}
console.log('categories', categories)
// 应用固定布局
const nodesWithPositions = calculateTreeLayout(
showNodes.value,
showRelations.value,
graphData.value.roots.map((root: any) => root.id)
)
const useNodes: Record<string, any>[] = []
nodesWithPositions.forEach((node: Record<string, any>) => {
const color = categoriesMap.get(node.label)?.color
const nodeInfo = createPointData({
...node,
itemStyle: {
color: color || '#333'
},
type: node.labels,
name: node.id,
})
useNodes.push(nodeInfo)
}
const useRelations: Record<string, any>[] = []
showRelations.value.forEach((relation: Record<string, any>) => {
const link = createLink({
...relation,
parentIndex: getNodeIndex(relation.start_node_id, useNodes),
index: getNodeIndex(relation.end_node_id, useNodes),
type: relation.type,
lineStyle: {
width: 2 // 关系线条宽度
},
})
useRelations.push(link)
})
const useOptions = {
...option.value,
series: [
{
...option.value.series[0],
// 使用固定布局替代力导向布局
layout: null, // 取消力导向布局
roam: true, // 允许缩放和平移
data: useNodes,
links: useRelations
}
]
}
// 应用位置信息到选项配置
return applyLocationsToOption(useOptions)
})
// 获取节点索引
const getNodeIndex = (id: string, source: Record<string, any> = showNodes.value) => {
return source.findIndex((item: Record<string, any>) => item.id === id)
}
// 找到主节点
const getMainNodes = (data: Record<string, any>) => {
const { nodes, relationships } = data
let roots = JSON.parse(JSON.stringify(nodes))
relationships.forEach((item: Record<string, any>) => {
const end = item.end_node_id
roots = roots.filter((item: Record<string, any>) => {
return item.id !== end
})
})
return roots
}
// 构建nodemap
const createNodeMap = (arr: Record<string, any>[]) => {
nodesMap.clear()
arr.forEach((item: Record<string, any>) => {
const raw = {
...item,
show: false
}
if (nodesMap.has(item.id)) {
// nodesMap.get(item.id).push(raw)
} else {
nodesMap.set(item.id, raw)
}
})
}
// 构建分类categoriesMap
const createCategoriesMap = (arr: Record<string, any>[]) => {
categoriesMap.clear()
arr.forEach((item: Record<string, any>) => {
if (item.label) {
if (!categoriesMap.has(item.label)) {
categoriesMap.set(item.label, {
color: createColor()
})
}
}
})
}
// 构建关系map
const createRelationMap = (arr: Record<string, any>[]) => {
relationsMap.clear()
arr.forEach((item: Record<string, any>) => {
if (relationsMap.has(item.id)) {
// relationsMap.get(item.id).push(item)
} else {
relationsMap.set(item.id, item)
}
})
}
// 根据id检索到对应的主节点
const getMainNodeById = (id: string) => {
const node = nodesMap.get(id)
const roots = []
const links = [...node.parentLinks || []]
if (!links.length) return [node]
while(links.length) {
const link = links.shift()
const start = link.start_node_id
const preNode = nodesMap.get(start)
console.log('preNode', preNode, nodesMap)
if (preNode?.parentLinks?.length) {
links.push(...preNode.parentLinks)
} else {
roots.push(preNode)
}
}
return roots
}
// echart关系图节点点击
const onPointClick = (id: string) => {
const { roots } = graphData.value
const isRoots = roots.find((rootNode: Record<string, any>) => rootNode.id === id)
if (isRoots) {
onToggleMainPoint(id)
} else {
onTogglePoint(id)
}
console.log('categoriesMap', categoriesMap)
updateData()
}
// 点击节点
const onTogglePoint = (id: string) => {
const node = nodesMap.get(id)
if (node) {
// node.show = !node.show
const childrenLinks = node?.childrenLinks || []
console.log('childrenLinks', childrenLinks)
childrenLinks.forEach((item: Record<string, any>) => {
const { end_node_id } = item
const cnode = nodesMap.get(end_node_id)
const type = !cnode.show
setNodeTypeInLink(end_node_id, type)
})
// node.show = !node.show
// setNodeTypeInLink(id, !node.show)
}
}
// 点击主节点,将当前节点下的所有子节点都显示或者隐藏出来
const onToggleMainPoint = (id: string) => {
const node = nodesMap.get(id)
if (node) {
setNodeTypeInLink(id, !node.show)
}
// 将当前节点下的所有子节点都显示或者隐藏出来
}
// 链式更新数据可视信息
const setNodeTypeInLink = (id: string, type: boolean) => {
const node = nodesMap.get(id)
if (node) {
// 如果布尔值一致,不处理
// if (node.show != type) {
node.show = type
// }
const childrenLinks = node?.childrenLinks || []
childrenLinks.forEach((item: Record<string, any>) => {
const { end_node_id } = item
setNodeTypeInLink(end_node_id, type)
})
}
}
// 关键字
const searchValue = ref('')
// 选项过滤
const optionsFilter = (input: string, option: any) => {
const raw = option['data-raw']
const id = raw.id
const name = raw.properties.name
return id.includes(input) || name.includes(input)
}
// 搜索
const onSearch = (id: string) => {
const roots = getMainNodeById(id)
console.log('roots', roots)
roots.forEach(item => {
console.log('item', item)
// if (item.show)
setNodeTypeInLink(item.id, true)
})
updateData()
}
const getQuery = () => {
const { id } = router.currentRoute.value.query;
if (id) {
onSearch(id as string)
}
}
return {
state: {
graphData,
nodesMap,
relationsMap,
showOptions,
searchValue,
},
api: {
getData,
onPointClick,
optionsFilter,
updateLocation,
// onMainSelect,
// onSelectPoint,
onSearch,
getQuery
}
}
}

View File

@ -1,16 +1,32 @@
import { ref, computed } from 'vue'
import { createData } from './mockData'
import { useChartHooks } from '@/views/knowledge/hooks/chart.ts'
import { router } from "@/router/index.ts";
import { useLocationHooks } from '@/views/knowledge/hooks/location.ts'
import { router } from "@/router/index.ts"
import { useUtils } from '@/views/knowledge/hooks/utils.ts'
export const useOperateServices = () => {
const { createColor } = useUtils()
const lightColors = ['#D4BDFB', '#9DD0FF', '#57DFB8', '#59E433', '#FFD1A6', '#E1DF62', '#F5B4B4', '#8FE4ED']
const colors = ['#997AE7', '#7096E2', '#1FAA86', '#1BBB2B', '#F99D47', '#E4CF67', '#DF8D8D', '#4FC0D2']
const { createColor } = useUtils({
colors: colors
})
const { option, createPointData, createLink} = useChartHooks()
const colors = ['#5470C6', '#91CC75', '#EE6666', '#73C0DE', '#3BA272', '#FC8452', '#9A60B4', '#EA7CC3']
const {
calculateTreeLayout,
applyLocationsToOption,
getLocationById,
setLocations,
clearLocations
} = useLocationHooks()
// 图表数据
const graphData = ref<Record<string, any>>({
@ -26,6 +42,11 @@ export const useOperateServices = () => {
// 分类映射
const categoriesMap = new Map()
// 更新位置
const updateLocation = (echartRef: Record<string, any>) => {
console.log('echartRef', echartRef)
}
// 获取数据
const getData = () => {
const data = createData()
@ -112,31 +133,44 @@ export const useOperateServices = () => {
}
})
const getCategoriesIndex = (category: string) => {
const i = categories.findIndex((item: Record<string, any>) => {
return item.name == category
})
return i
}
console.log('categories', categories)
// const link = ()
// 应用固定布局
const nodesWithPositions = calculateTreeLayout(
showNodes.value,
showRelations.value,
graphData.value.roots.map((root: any) => root.id)
)
console.log('nodesWithPositions', nodesWithPositions)
const useNodes: Record<string, any>[] = []
showNodes.value.forEach((node: Record<string, any>) => {
nodesWithPositions.forEach((node: Record<string, any>) => {
const color = categoriesMap.get(node.label)?.color
console.log('color', categoriesMap, color, node.label)
const nodeInfo = createPointData({
...node,
const nodeInfo = {
raw: node,
id: node.id,
name: node.id,
itemStyle: {
color: color || '#333'
},
type: node.labels,
name: node.id,
})
type: node.label,
x: node.x,
y: node.y
}
// const nodeInfo = createPointData({
// ...node,
// itemStyle: {
// color: color || '#333'
// },
// type: node.labels,
// name: node.id,
// // 直接在创建节点时设置位置信息
// x: node.x,
// y: node.y
// })
useNodes.push(nodeInfo)
})
@ -161,12 +195,19 @@ export const useOperateServices = () => {
series: [
{
...option.value.series[0],
// categories: categoriesWithColor,
// 使用固定布局替代力导向布局
layout: null, // 取消力导向布局
roam: true, // 允许缩放和平移
data: useNodes,
links: useRelations
}
]
}
console.log('useOptions', useOptions)
// 位置信息已经在创建节点时设置,不需要再次应用
return useOptions
})
@ -351,6 +392,7 @@ export const useOperateServices = () => {
getData,
onPointClick,
optionsFilter,
updateLocation,
// onMainSelect,
// onSelectPoint,
onSearch,

View File

@ -1,21 +1,37 @@
export const createData = () => {
const size = 3 // 每层级节点个数
const level = 3 // 层级
const data: Record<string, any> = {
nodes: [],
relationships: []
}
for(let i = 1; i < size; i++) {
data.nodes.push(createNode(`${i}`))
for(let j = 0; j< level; j++) {
data.nodes.push(createNode(`${i}${j}`))
data.relationships.push(createRelations(`${i}${j}`, size))
for(let l = 0; l < level; l++) {
data.nodes.push(createNode(`${i}${j}${l}`))
data.relationships.push(createRelations(`${i}${j}${l}`, size))
// 创建根节点
for (let i = 1; i <= 4; i++) {
const rootId = `${i}`;
data.nodes.push(createNode(rootId));
// 为每个根节点创建不同深度和宽度的子树
const childCount = Math.floor(Math.random() * 3) + 2; // 2-4个子节点
for (let j = 0; j < childCount; j++) {
const childId = `${i}${j}`;
data.nodes.push(createNode(childId));
data.relationships.push(createRelations(childId, rootId));
// 创建第三层节点(不同数量)
const grandchildCount = Math.floor(Math.random() * 4) + 2; // 2-5个孙节点
for (let k = 0; k < grandchildCount; k++) {
const grandchildId = `${i}${j}${k}`;
data.nodes.push(createNode(grandchildId));
data.relationships.push(createRelations(grandchildId, childId));
// 随机为一些孙节点创建第四层节点
if (Math.random() > 0.5) {
const greatGrandchildCount = Math.floor(Math.random() * 3) + 1; // 1-3个曾孙节点
for (let l = 0; l < greatGrandchildCount; l++) {
const greatGrandchildId = `${i}${j}${k}${l}`;
data.nodes.push(createNode(greatGrandchildId));
data.relationships.push(createRelations(greatGrandchildId, grandchildId));
}
}
}
}
}
@ -33,12 +49,9 @@ const createNode = (index: number | string) => {
}
}
const createRelations = (index: string, max: number) => {
const len = index.length
const start = index.slice(0, len - 1)
const end = index
const createRelations = (end: string, start: string) => {
const params = {
id: `r${index}`,
id: `r${end}`,
"type": "组成", // 关系类型
"start_node_id": start, // 起始节点id
"end_node_id": end,

View File

@ -13,9 +13,8 @@ function pathResolve(dir: string) {
return resolve(__dirname, dir)
}
const URL_USE = ''
const URL_USE = 'http://ip:8088'
// https://vite.dev/config/
export default defineConfig({
base: './',
server: {