VisNetwork.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. <template>
  2. <div class="vis-network-container">
  3. <div v-if="isEdit" class="toolbar">
  4. 说明:双击节点可以进行操作
  5. <el-button type="primary" @click="addNode">添加子节点</el-button>
  6. <el-button type="danger" @click="deleteNode">删除</el-button>
  7. </div>
  8. <div ref="network" class="network">
  9. <p class="noData">{{ loadingTitle }}</p>
  10. </div>
  11. <el-dialog
  12. title="编辑"
  13. :visible.sync="showEditModal"
  14. width="30%"
  15. :close-on-click-modal="false"
  16. :append-to-body="true"
  17. >
  18. <el-form :model="editForm" label-width="80px">
  19. <el-form-item label="所属层级">
  20. <el-input v-model="editForm.fullPath" type="textarea" disabled />
  21. </el-form-item>
  22. <el-form-item label="节点名称">
  23. <el-input v-model="editForm.label" type="textarea" placeholder="请输入节点名称" />
  24. </el-form-item>
  25. <el-form-item v-if="false" label="节点类型">
  26. <select v-model="editForm.type" class="form-select">
  27. <option value="0">章节</option>
  28. <option value="3">资源类型</option>
  29. <option value="4">具体文件</option>
  30. </select>
  31. </el-form-item>
  32. </el-form>
  33. <!-- 按钮区域 -->
  34. <div slot="footer" class="dialog-footer">
  35. <el-button type="primary" @click="cancelEdit">取消</el-button>
  36. <el-button type="success" @click="saveEdit">保存</el-button>
  37. <el-button type="danger" @click="deleteNode">删除</el-button>
  38. </div>
  39. </el-dialog>
  40. </div>
  41. </template>
  42. <script>
  43. import { DataSet } from 'vis-data';
  44. import { Network } from 'vis-network';
  45. import { GetBookKnowledgeGraph } from '@/api/book';
  46. export default {
  47. name: 'VisNetwork',
  48. props: {
  49. isEdit: {
  50. type: Boolean,
  51. default: false,
  52. },
  53. bookId: {
  54. type: String,
  55. required: true,
  56. },
  57. },
  58. data() {
  59. return {
  60. loadingTitle: '数据加载中...',
  61. network: null,
  62. nodes: null,
  63. edges: null,
  64. selectedNode: null,
  65. showEditModal: false,
  66. editingNode: null,
  67. positions: {},
  68. jsonData: {},
  69. hasData: false,
  70. editForm: {
  71. label: '',
  72. type: '0',
  73. fullPath: '',
  74. },
  75. };
  76. },
  77. computed: {},
  78. created() {
  79. this.hasData = false;
  80. this.getBookVisNetWork();
  81. },
  82. mounted() {},
  83. beforeDestroy() {
  84. this.destroyNetwork();
  85. },
  86. methods: {
  87. /**
  88. * 得到教材知识图谱
  89. * @param {string} id - 教材ID
  90. */
  91. getBookVisNetWork() {
  92. GetBookKnowledgeGraph({ book_id: this.bookId })
  93. .then((res) => {
  94. this.convertToVisNetworkData(res);
  95. })
  96. .catch(() => {
  97. this.loadingTitle = '数据未生成';
  98. });
  99. },
  100. init() {
  101. this.destroyNetwork();
  102. this.initNetwork(this.jsonData.nodes, this.jsonData.edges);
  103. },
  104. destroyNetwork() {
  105. if (this.network) {
  106. this.network.destroy();
  107. }
  108. this.positions = {};
  109. },
  110. initNetwork(nodeData, edgeData) {
  111. this.nodes = new DataSet(nodeData);
  112. this.edges = new DataSet(edgeData);
  113. const data = {
  114. nodes: this.nodes,
  115. edges: this.edges,
  116. };
  117. // 创建网络图
  118. const container = this.$refs.network;
  119. let options = {
  120. nodes: {
  121. shape: 'box',
  122. margin: 10,
  123. size: 20,
  124. font: {
  125. size: 14,
  126. },
  127. borderWidth: 2,
  128. },
  129. edges: {
  130. width: 2,
  131. color: { color: '#848484' },
  132. smooth: {
  133. type: 'continuous',
  134. },
  135. },
  136. groups: {
  137. default: {
  138. color: { background: '#97C2FC', border: '#6AA6F8' },
  139. borderWidth: 2,
  140. font: { color: '#FFFFFF' },
  141. },
  142. type0: {
  143. color: { background: '#97C2FC', border: '#84B7FD' },
  144. borderWidth: 3,
  145. },
  146. type1: {
  147. color: { background: '#FFA807', border: '#FDA400' },
  148. borderWidth: 2,
  149. },
  150. type2: {
  151. color: { background: '#2ECC71', border: '#0BCC5D' },
  152. borderWidth: 2,
  153. },
  154. type3: {
  155. color: { background: '#FB7E81', border: '#F85C5F' },
  156. borderWidth: 2,
  157. },
  158. },
  159. physics: {
  160. enabled: true,
  161. stabilization: {
  162. iterations: 100,
  163. },
  164. },
  165. interaction: {
  166. hover: true,
  167. tooltipDelay: 200,
  168. },
  169. };
  170. this.network = new Network(container, data, options);
  171. // 添加事件监听
  172. this.network.on('click', (params) => {
  173. if (params.nodes.length > 0) {
  174. const nodeId = params.nodes[0];
  175. if (!this.isEdit) {
  176. this.$emit('child-click', nodeId);
  177. return;
  178. }
  179. this.selectedNode = this.nodes.get(nodeId);
  180. this.editingNode = nodeId;
  181. } else {
  182. this.selectedNode = null;
  183. }
  184. });
  185. this.network.on('doubleClick', (params) => {
  186. if (!this.isEdit) return;
  187. if (params.nodes.length > 0) {
  188. const nodeId = params.nodes[0];
  189. this.handleNodeDoubleClick(nodeId);
  190. }
  191. });
  192. this.network.on('oncontext', (params) => {
  193. params.event.preventDefault();
  194. });
  195. this.network.on('hoverNode', (params) => {});
  196. },
  197. // 树形数据转换工具
  198. convertToVisData(treeData) {
  199. const nodes = [];
  200. const edges = [];
  201. let x = 0;
  202. let y = 0;
  203. const traverse = (node, parentId = null, level = 0, parentPath = '') => {
  204. const nodeId = node.data.uid;
  205. // 创建节点
  206. nodes.push({
  207. id: nodeId,
  208. label: node.data.text,
  209. type: node.data.type,
  210. level,
  211. fullPath: parentPath, // 保存所有父级路径,用->分隔
  212. group: `type${node.data.type || 0}`,
  213. });
  214. // 创建边(如果有父节点)
  215. if (parentId) {
  216. edges.push({
  217. from: parentId,
  218. to: nodeId,
  219. arrows: 'to',
  220. smooth: { type: 'cubicBezier' },
  221. });
  222. }
  223. // 递归处理子节点
  224. if (node.children && node.children.length > 0) {
  225. // 构建当前节点的完整路径
  226. const currentPath = parentPath ? `${parentPath}->${node.data.text}` : node.data.text;
  227. node.children.forEach((child, index) => {
  228. if (level < 3) traverse(child, nodeId, level + 1, currentPath);
  229. });
  230. }
  231. };
  232. // 调用时传入空字符串作为初始路径
  233. traverse(treeData.root, null, 0, '');
  234. return { nodes, edges };
  235. },
  236. handleNodeDoubleClick(nodeId) {
  237. const node = this.nodes.get(nodeId);
  238. console.info(node);
  239. if (node) {
  240. this.editingNode = nodeId;
  241. this.editForm = {
  242. label: node.label,
  243. type: node.type,
  244. fullPath: node.fullPath,
  245. };
  246. this.showEditModal = true;
  247. }
  248. },
  249. addOrUpdate(data) {
  250. if (!data) return;
  251. let oldNodes = this.nodes.get();
  252. let oldEdges = this.edges.get();
  253. const node = this.nodes.get(data.id);
  254. if (node) {
  255. oldNodes = oldNodes.filter((x) => x.id !== node.id);
  256. let newData = Object.assign({}, node, data);
  257. oldNodes.push(newData);
  258. } else {
  259. oldNodes.push(data);
  260. }
  261. this.positions = this.network.getPositions() || {};
  262. oldNodes.map((x) => {
  263. if (x.id && this.positions[x.id]) {
  264. let tmpPosition = this.positions[x.id];
  265. x.x = tmpPosition.x;
  266. x.y = tmpPosition.y;
  267. }
  268. });
  269. console.info('oldNodes', oldNodes);
  270. this.initNetwork(oldNodes, oldEdges);
  271. },
  272. addNode() {
  273. if (!this.selectedNode) {
  274. this.$message.error('请先选择父节点!');
  275. return;
  276. }
  277. let newId = this.generateUniqueId();
  278. const newNode = Object.assign(
  279. {},
  280. {
  281. id: newId,
  282. label: '新增节点',
  283. fullPath: `${this.selectedNode.fullPath}->${this.selectedNode.label}`,
  284. type: (this.selectedNode.type || 0) + 1,
  285. level: (this.selectedNode.level || 0) + 1,
  286. },
  287. );
  288. newNode.group = `type${newNode.type}`;
  289. newId = this.generateUniqueId();
  290. this.edges.add({
  291. id: newId,
  292. from: this.selectedNode.id,
  293. to: newNode.id,
  294. arrows: 'to',
  295. });
  296. // this.nodes.add(newNode);
  297. this.addOrUpdate(newNode);
  298. },
  299. // 保存编辑
  300. saveEdit() {
  301. if (!this.editingNode) return;
  302. const updatedNode = {
  303. id: this.editingNode,
  304. label: this.editForm.label,
  305. type: parseInt(this.editForm.type),
  306. };
  307. // this.nodes.update(updatedNode);
  308. // this.$emit('node-updated', updatedNode);
  309. this.addOrUpdate(updatedNode);
  310. this.cancelEdit();
  311. },
  312. // 取消编辑
  313. cancelEdit() {
  314. this.showEditModal = false;
  315. this.editingNode = null;
  316. this.editForm = { label: '', type: '0' };
  317. },
  318. // 删除节点
  319. deleteNode() {
  320. if (!this.editingNode) return;
  321. this.$confirm('确定要删除此节点吗?', '提示', {
  322. confirmButtonText: '确定',
  323. cancelButtonText: '取消',
  324. type: 'warning',
  325. })
  326. .then(() => {
  327. this.nodes.remove({ id: this.editingNode });
  328. this.$emit('node-deleted', this.editingNode);
  329. this.cancelEdit();
  330. })
  331. .catch(() => {});
  332. },
  333. saveData() {
  334. if (!this.hasData) return null;
  335. let nodes = this.nodes.get();
  336. let edges = this.edges.get();
  337. this.positions = this.network.getPositions() || {};
  338. nodes.map((x) => {
  339. if (x.id && this.positions[x.id]) {
  340. let tmpPosition = this.positions[x.id];
  341. x.x = tmpPosition.x;
  342. x.y = tmpPosition.y;
  343. }
  344. });
  345. return { nodes, edges };
  346. },
  347. generateUniqueId() {
  348. return Date.now() + Math.random().toString(36).substr(2, 9);
  349. },
  350. convertToVisNetworkData(res) {
  351. if (res && res.status === 1) {
  352. this.loadingTitle = '数据渲染中...';
  353. if (res.content_node && res.content_edge) {
  354. let nodes = (JSON.parse(res.content_node) || {}).nodes;
  355. let edges = (JSON.parse(res.content_edge) || {}).edges;
  356. nodes.map((x) => (x.group = `type${x.type || 0}`));
  357. // nodes=nodes.filter(x=>x.level<4)
  358. this.jsonData = {
  359. nodes,
  360. edges,
  361. };
  362. this.hasData = true;
  363. this.init();
  364. }
  365. }
  366. this.loadingTitle = '数据未生成';
  367. },
  368. },
  369. };
  370. </script>
  371. <style scoped>
  372. .vis-network-container {
  373. width: 100%;
  374. padding: 1px;
  375. }
  376. .network {
  377. width: 100%;
  378. height: calc(100vh - 280px);
  379. margin-bottom: 5px;
  380. border: 1px solid #e0e0e0;
  381. border-radius: 8px;
  382. }
  383. .noData {
  384. display: flex;
  385. align-items: center; /* 垂直居中 */
  386. justify-content: center; /* 水平居中 */
  387. height: calc(100vh - 250px);
  388. }
  389. </style>