4d217cc9 by lihua

添加关系网和桑基图

1 parent d2a5ec72
...@@ -27,6 +27,7 @@ declare module '@vue/runtime-core' { ...@@ -27,6 +27,7 @@ declare module '@vue/runtime-core' {
27 FileUpload: typeof import('./src/components/FileUpload/index.vue')['default'] 27 FileUpload: typeof import('./src/components/FileUpload/index.vue')['default']
28 FixedActionBar: typeof import('./src/components/FixedActionBar/index.vue')['default'] 28 FixedActionBar: typeof import('./src/components/FixedActionBar/index.vue')['default']
29 Form: typeof import('./src/components/Form/index.vue')['default'] 29 Form: typeof import('./src/components/Form/index.vue')['default']
30 GraphTopbar: typeof import('./src/components/RelationNetwork/graphTopbar.vue')['default']
30 Hour: typeof import('./src/components/Schedule/component/hour.vue')['default'] 31 Hour: typeof import('./src/components/Schedule/component/hour.vue')['default']
31 ImagePreview: typeof import('./src/components/ImagePreview/index.vue')['default'] 32 ImagePreview: typeof import('./src/components/ImagePreview/index.vue')['default']
32 ImagesUpload: typeof import('./src/components/ImagesUpload/index.vue')['default'] 33 ImagesUpload: typeof import('./src/components/ImagesUpload/index.vue')['default']
...@@ -41,6 +42,7 @@ declare module '@vue/runtime-core' { ...@@ -41,6 +42,7 @@ declare module '@vue/runtime-core' {
41 PageNav: typeof import('./src/components/PageNav/index.vue')['default'] 42 PageNav: typeof import('./src/components/PageNav/index.vue')['default']
42 PcasCascader: typeof import('./src/components/PcasCascader/index.vue')['default'] 43 PcasCascader: typeof import('./src/components/PcasCascader/index.vue')['default']
43 Popover: typeof import('./src/components/Popover/index.vue')['default'] 44 Popover: typeof import('./src/components/Popover/index.vue')['default']
45 RelationNetwork: typeof import('./src/components/RelationNetwork/index.vue')['default']
44 RouterLink: typeof import('vue-router')['RouterLink'] 46 RouterLink: typeof import('vue-router')['RouterLink']
45 RouterView: typeof import('vue-router')['RouterView'] 47 RouterView: typeof import('vue-router')['RouterView']
46 Schedule: typeof import('./src/components/Schedule/index.vue')['default'] 48 Schedule: typeof import('./src/components/Schedule/index.vue')['default']
......
1 <script lang="ts" setup name="topbar">
2 import { ref, watch } from 'vue';
3
4 const props = defineProps({
5 isGraphDisplay: {
6 type: Boolean,
7 default: true
8 },
9 });
10
11 const isGraph = ref(false);
12
13 watch(() => props.isGraphDisplay, (val) => {
14 isGraph.value = val;
15 }, {
16 immediate: true
17 })
18
19 const emits = defineEmits(["displaySwitchChange"]);
20
21 const switchChange = (val) => {
22 isGraph.value = val
23 emits('displaySwitchChange', val);
24 }
25
26 </script>
27
28 <template>
29 <div className='g6-component-topbar-content'>
30 <div :class="isGraph ? 'selected g6-component-topbar-item' : 'g6-component-topbar-item'" @click="switchChange(true)">
31 关系网
32 </div>
33 <div :class="!isGraph ? 'selected g6-component-topbar-item' : 'g6-component-topbar-item'" @click="switchChange(false)">
34 桑基图
35 </div>
36 </div>
37 </template>
38
39 <style scoped lang="scss">
40 .g6-component-topbar-content {
41 display: flex;
42 flex-direction: row;
43 justify-content: center;
44 align-items: center;
45 background: #fff;
46 border: 1px solid var(--el-color-primary);
47 border-radius: 32px;
48 padding: 4px;
49 width: 138px;
50 height: 32px;
51 }
52
53 .g6-component-topbar-item {
54 width: 50%;
55 height: 100%;
56 display: flex;
57 align-items: center;
58 justify-content: center;
59 font-size: 14px;
60 color: #999999;
61 cursor: pointer;
62
63 &.selected {
64 background: #4FA1A4;
65 border-radius: 32px;
66 color: #fff;
67 }
68 }
69 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <template>
2 <div ref="containerRef" className='canvas-wrapper'>
3 <div class="main" ref="tooltip1Ref" style="display: none;position: absolute;" v-loading="detailLoading">
4 <div class="title">{{ detailInfo.name }}</div>
5 <div class="row" v-for="item in Object.keys(detailInfo)">
6 <span>{{ item + ':' }}</span>
7 <span>{{ detailInfo[item] }}</span>
8 </div>
9 </div>
10 </div>
11 </template>
12
13 <script lang="ts" setup name="RelationNetwork">
14 import { ref, onMounted } from 'vue'
15 import G6 from '@antv/g6';
16 import insertCss from 'insert-css';
17
18 const props = defineProps({
19 treeData: {
20 type: Object,
21 default: {}
22 }
23 })
24
25 const emits = defineEmits([
26 'nodeItemClick',
27 'contextMenu'
28 ]);
29
30 const detailLoading = ref(false);
31
32 const detailInfo: any = ref({
33 guid: '1',
34 '标识符': '124',
35 name: '字段12345字段字段字段字段字段字段字段字段字段字段',
36 '中文名称': '字段12345字段字段字段字段字段字段字段字段字段字段',
37 '英文名称': 'fieldName'
38 });
39
40 const containerRef = ref();
41
42 const graphRef = ref();
43
44 const resizeObserver = ref();
45
46 watch(() => props.treeData, (val) => {
47 tooltip1Ref.value.style.display = 'none';
48 if (lastSelectNode.value) {
49 graphRef.value.updateItem(lastSelectNode.value, {
50 labelCfg: {
51 color: '#212121'
52 },
53 style: {
54 stroke: '#4fa1a4',
55 fill: '#ebf6f7',
56 cursor: 'pointer'
57 }
58 });
59 }
60 lastSelectNode.value = null;
61 if (val) {
62 renderGraph(graphRef.value, val);
63 }
64 })
65
66 const renderGraph = (graph: any, lineageData: any) => {
67 if (!graph || !lineageData) return;
68 graph.setMinZoom(0.5);
69 graph.setMaxZoom(1);
70 graph.data(lineageData);
71 graph.render();
72
73 graph.fitView(40, { direction: 'both' });
74 graph.fitCenter();
75
76 graph.setMinZoom(0.5);
77 graph.setMaxZoom(5);
78
79 };
80
81 const detectLanguage = (text) => {
82 if (!text) {
83 return 'English';
84 }
85 let chineseCount = 0;
86 let englishCount = 0;
87
88 for (let char of text) {
89 if (/[\u4e00-\u9fa5]/.test(char)) {
90 chineseCount++;
91 } else if (/[a-zA-Z]/.test(char)) {
92 englishCount++;
93 }
94 }
95
96 if (chineseCount > englishCount) {
97 return 'Chinese';
98 } else if (englishCount > chineseCount) {
99 return 'English';
100 }
101 return 'English';
102 }
103
104 const handleLabelLength = (label: string) => {
105 if (detectLanguage(label) == 'English') {
106 return label?.length > 30 ? label.slice(0, 30) + '...' : label;
107 }
108 return label?.length > 16 ? label.slice(0, 16) + '...' : label;
109 };
110
111 insertCss(`
112 .g6-component-contextmenu {
113 padding: 8px 0px;
114 background-color: #fff;
115 }
116 .context-menu {
117 display: flex;
118 flex-direction: column;
119 }
120 .menu-item {
121 line-height: 32px;
122 color: #212121;
123 cursor: pointer;
124 padding: 0 12px;
125 }
126 .menu-item:hover {
127 background-color: #f5f5f5;
128 }
129 `);
130
131 const tooltip = ref();
132
133 onMounted(() => {
134 nextTick(() => {
135 if (!graphRef.value) {
136 const container: any = containerRef.value;
137 const width = container.clientWidth;
138 const height = container.clientHeight - 10;
139
140 tooltip.value = new G6.Tooltip({
141 offsetX: 10,
142 offsetY: 10,
143 trigger: 'mouseenter',
144 // 允许出现 tooltip 的 item 类型
145 itemTypes: ['node'],
146 // 自定义 tooltip 内容
147 shouldBegin: (evt: any) => {
148 const { item, target } = evt;
149 const currentAnchor = target.get('name');
150 const name = item._cfg.model?.name;
151 if (currentAnchor == 'text-shape') {
152 if (detectLanguage(name) == 'English') {
153 return name?.length > 30;
154 }
155 return name?.length > 16;
156 }
157 return false;
158 },
159 getContent: (e: any) => {
160
161 const { item, target } = e;
162 const currentAnchor = target.get('name');
163 const outDiv = document.createElement('div');
164 outDiv.className = 'node';
165 outDiv.style.width = 'fit-content';
166 const name = item._cfg.model.name;
167 if (currentAnchor == 'text-shape') {
168 outDiv.innerHTML = `<h4>${name}</h4>`
169 }
170 return outDiv;
171 },
172 });
173
174 // tooltip1.value = new G6.Tooltip({
175 // offsetX: 10,
176 // offsetY: 10,
177 // trigger: 'click',
178 // // 允许出现 tooltip 的 item 类型
179 // itemTypes: ['node'],
180 // // 自定义 tooltip 内容
181 // shouldBegin: (evt: any) => {
182 // const { item, target } = evt;
183 // let model = item._cfg.model;
184 // if (model.isField) {
185 // return true
186 // }
187 // return false;
188 // },
189 // getContent: async (e: any) => {
190 // const { item, target } = e;
191 // let model = item._cfg.model;
192 // if (!model.isField) {
193 // return '';
194 // }
195 // const template = ` <div class="main" v-loading="detailLoading">
196 // <div class="title">{{ titleName + ':' }}</div>
197 // <div class="row" v-for="item in Object.keys(detailInfo)">
198 // <span>{{ item }}</span>
199 // <span>{{ detailInfo[item] }}</span>
200 // </div>
201 // </div>`
202 // await setTimeout(() => {
203 // }, 200);
204 // const outDiv = document.createElement('div');
205
206 // outDiv.className = 'tooltip-main';
207 // outDiv.style.width = '320px';
208
209 // for (const key in detailInfo.value) {
210 // const domRow = document.createElement('div');
211 // domRow.className = 'row';
212 // let span1 = document.createElement('span');
213 // span1.innerHTML = key + ':';
214 // let span2 = document.createElement('span');
215 // span2.innerHTML = detailInfo[key];
216 // domRow.appendChild(span1);
217 // domRow.appendChild(span2);
218 // outDiv.appendChild(domRow);
219 // }
220 // return outDiv;
221
222 // },
223 // });
224
225 const contextMenu = new G6.Menu({
226 getContent(evt: any) {
227 const { item, target } = evt;
228 let model = item._cfg.model;
229 return `
230 <div class='context-menu'>
231 <span class='menu-item'>引用标准新建数据集</span>
232 </div>`
233 },
234 shouldBegin: (evt: any) => {
235 const { item, target } = evt;
236 let model = item._cfg.model;
237 if (model && !model.isField && !model.children?.length) {
238 return true;
239 }
240 return false;
241 },
242 handleMenuClick: (target, item: any) => {
243 let model = item._cfg?.model;
244 if (!model) {
245 return;
246 }
247 emits('contextMenu', model);
248 },
249 // offsetX and offsetY include the padding of the parent container
250 // 需要加上父级容器的 padding-left 16 与自身偏移量 10
251 offsetX: 16,
252 // 需要加上父级容器的 padding-top 24 、画布兄弟元素高度、与自身偏移量 10
253 offsetY: 0,
254 // the types of items that allow the menu show up
255 // 在哪些类型的元素上响应
256 itemTypes: ['node'],
257 });
258
259 const graph = new G6.TreeGraph({
260 container: container,
261 width,
262 height,
263 plugins: [contextMenu, tooltip.value],
264 fitCenter: true,
265 fitView: true,
266 fitViewPadding: 40,
267 minZoom: 0.5,
268 maxZoom: 1,
269 modes: {
270 default: [
271 {
272 type: 'collapse-expand',
273 onChange: (item, collapsed) => {
274 if (!item) {
275 return;
276 }
277 const data = item.getModel();
278 data.collapsed = collapsed;
279 return true;
280 },
281 shouldBegin: (e) => {
282 // 若当前操作的节点 id 为 'node1',则不发生 collapse-expand
283 if (e.item && e.item.getModel().id === 'node1') return false;
284 return true;
285 },
286 },
287 'drag-canvas',
288 'zoom-canvas',
289 ],
290 },
291 defaultNode: {
292 size: 24,
293 anchorPoints: [
294 [0, 0.5],
295 [1, 0.5],
296 ],
297 style: {
298 // stroke: '#4fa1a4',
299 fill: '#ebf6f7',
300 cursor: 'pointer'
301 },
302 },
303 defaultEdge: {
304 type: 'cubic-horizontal',
305 },
306 layout: {
307 type: 'compactBox',
308 direction: 'LR',
309 getId: function getId(d) {
310 return d.id;
311 },
312 getHeight: function getHeight() {
313 return 16;
314 },
315 getWidth: function getWidth() {
316 return 16;
317 },
318 getVGap: function getVGap() {
319 return 30;
320 },
321 getHGap: function getHGap() {
322 return 120;
323 },
324 },
325 });
326 graphRef.value = graph;
327 graph.node((node) => {
328 return {
329 id: node.guid as string,
330 label: handleLabelLength(node.name as string),
331 collapsed: node.children?.length ? false : true,
332 labelCfg: {
333 offset: 10,
334 style: {
335 fontSize: 16,
336 fill: '#212121',
337 },
338 position: !node.isField ? 'left' : 'right', //只有字段是最后一层级,不需要展开
339 },
340 style: {
341 stroke: '#4fa1a4',
342 cursor: 'pointer'
343 }
344 };
345 });
346 // data不是数组,第一级是根节点
347 graph.data(props.treeData);
348 graph.render();
349 graph.fitView(40, { direction: 'both' });
350 graph.fitCenter();
351
352 graph.setMinZoom(0.5);
353 graph.setMaxZoom(5);
354 }
355
356 observeResize();
357
358 bindEvents();
359 })
360
361 })
362
363 const observeResize = () => {
364 resizeObserver.value = new ResizeObserver(() => {
365 let domWidth = document.documentElement.clientWidth;
366 if (domWidth < 992) {//根据setting.ts里的设置,小于992,会隐藏左边的菜单栏,
367 setTimeout(() => {
368 const container: any = containerRef.value;
369 const width = container.clientWidth;
370 const height = container.clientHeight - 10;
371 if (!width) {//会把隐藏的给消失。
372 return;
373 }
374 graphRef.value.changeSize(width, height);
375 graphRef.value.setMinZoom(0.5);
376 graphRef.value.setMaxZoom(1);
377 graphRef.value.fitView(40, { direction: 'both' });
378 graphRef.value.fitCenter();
379 graphRef.value.setMinZoom(0.5);
380 graphRef.value.setMaxZoom(5);
381 }, 500)
382 }
383 const container: any = containerRef.value;
384 const width = container.clientWidth;
385 const height = container.clientHeight - 10;
386 if (!width) {//会把隐藏的给消失。
387 return;
388 }
389 graphRef.value.changeSize(width, height);
390 graphRef.value.setMinZoom(0.5);
391 graphRef.value.setMaxZoom(1);
392 graphRef.value.fitView(40, { direction: 'both' });
393 graphRef.value.fitCenter();
394 graphRef.value.setMinZoom(0.5);
395 graphRef.value.setMaxZoom(5);
396 });
397 resizeObserver.value.observe(containerRef.value);
398 }
399
400
401 const tooltip1Ref = ref();
402
403 // 更新tooltip的位置
404 function updateTooltipPosition(evt) {
405 var width = graphRef.value.get("width");
406 var height = graphRef.value.get("height");
407 var offsetX = 10;
408 var offsetY = 10;
409 var point = graphRef.value.getPointByClient(evt.clientX, evt.clientY);
410 var _a2 = graphRef.value.getCanvasByPoint(point.x, point.y), x4 = _a2.x, y4 = _a2.y;
411 var graphContainer = graphRef.value.getContainer();
412 var res = {
413 x: x4 + graphContainer.offsetLeft + offsetX,
414 y: y4 + graphContainer.offsetTop + offsetY
415 };
416 let bboxHeight = tooltip1Ref.value.getBoundingClientRect().height;
417 if (x4 + 320 + offsetX > width) {
418 res.x -= 320 + offsetX;
419 }
420 if (y4 + bboxHeight + offsetY > height) {
421 res.y -= bboxHeight + offsetY;
422 if (res.y < 0) {
423 res.y = 0;
424 }
425 }
426 tooltip1Ref.value.style.left = `${res.x}px`;
427 tooltip1Ref.value.style.top = `${res.y}px`;
428 tooltip1Ref.value.style.display = 'block';
429 }
430
431 const lastSelectNode = ref(null);
432
433 const bindEvents = () => {
434 let graph = graphRef.value;
435 graph.on('node:click', function (evt) {
436 const item = evt.item;
437 if (!item) {
438 return;
439 }
440 const nodeId = item.get('guid');
441 const model = <any>item.getModel();
442 if (lastSelectNode.value) {
443 if (lastSelectNode.value == item) {
444 return;
445 }
446 tooltip1Ref.value.style.display = 'none';
447 graphRef.value.updateItem(lastSelectNode.value, {
448 labelCfg: {
449 style: {
450 fill: '#212121',
451 }
452 },
453 style: {
454 stroke: '#4fa1a4',
455 fill: '#ebf6f7',
456 cursor: 'pointer'
457 }
458 });
459 lastSelectNode.value = null;
460 }
461 const children = model.children;
462 if (children?.length && !model.isField) { //是字段级别,就不需要再展开了
463 return;
464 }
465 if (model.isField) {
466 graphRef.value.updateItem(item, {
467 labelCfg: {
468 style: {
469 fill: '#4fa1a4'
470 }
471 },
472 style: {
473 stroke: '#4fa1a4',
474 fill: '#4fa1a4',
475 cursor: 'pointer'
476 }
477 });
478 lastSelectNode.value = item;
479 detailInfo.value.guid = model.guid;
480 updateTooltipPosition(evt);
481 detailLoading.value = true;
482
483 setTimeout(() => {
484 detailLoading.value = false;
485 }, 500);
486 return;
487 }
488 emits('nodeItemClick', graph, item);
489 evt.preventDefault();
490 evt.stopPropagation();
491 });
492 graph.on('dragstart', (evt: any) => {
493 if (evt.item?.getType() == 'node') {
494 return;
495 }
496 // 清除状态
497 if (lastSelectNode.value) {
498 tooltip1Ref.value.style.display = 'none';
499 graphRef.value.updateItem(lastSelectNode.value, {
500 labelCfg: {
501 style: {
502 fill: '#212121',
503 },
504 },
505 style: {
506 stroke: '#4fa1a4',
507 fill: '#ebf6f7',
508 cursor: 'pointer'
509 }
510 });
511 }
512 lastSelectNode.value = null;
513 })
514 graph.on('click', (evt: any) => {
515 if (evt.item?.getType() == 'node') {
516 return;
517 }
518 // 清除状态
519 if (lastSelectNode.value) {
520 tooltip1Ref.value.style.display = 'none';
521 graphRef.value.updateItem(lastSelectNode.value, {
522 labelCfg: {
523 style: {
524 fill: '#212121',
525 },
526 },
527 style: {
528 stroke: '#4fa1a4',
529 fill: '#ebf6f7',
530 cursor: 'pointer'
531 }
532 });
533 }
534 lastSelectNode.value = null;
535 });
536 graph.on('canvas:click', (evt: any) => {
537 // 清除状态
538 if (lastSelectNode.value) {
539 tooltip1Ref.value.style.display = 'none';
540 graphRef.value.updateItem(lastSelectNode.value, {
541 labelCfg: {
542 style: {
543 fill: '#212121',
544 },
545 },
546 style: {
547 stroke: '#4fa1a4',
548 fill: '#ebf6f7',
549 cursor: 'pointer'
550 }
551 });
552 }
553 lastSelectNode.value = null;
554 });
555 }
556
557 const destroy = () => {
558 graphRef.value?.destroy();
559 }
560
561 defineExpose({
562 destroy,
563 });
564
565 </script>
566
567 <style lang="scss" scoped>
568 .canvas-wrapper {
569 width: 100%;
570 height: 100%;
571 position: relative;
572 }
573
574 .canvas-wrapper:-webkit-full-screen {
575 background-color: white;
576 }
577
578 .canvas-wrapper:-moz-full-screen {
579 background-color: white;
580 }
581
582 .canvas-wrapper:-ms-fullscreen {
583 background-color: white;
584 }
585
586 .canvas-wrapper:fullscreen {
587 background-color: white;
588 }
589
590 .main {
591 padding: 16px;
592 box-shadow: 0px 2px 8px 0px rgba(0, 0, 0, 0.08);
593 background-color: #fff;
594 max-width: 320px;
595 max-height: 400px;
596 overflow-y: auto;
597
598 .title {
599 font-size: 16px;
600 color: #212121;
601 line-height: 24px;
602 font-weight: 600;
603 }
604
605 .row {
606 margin-top: 8px;
607 font-size: 14px;
608 color: #666666;
609 line-height: 21px;
610 }
611 }
612 </style>
...\ No newline at end of file ...\ No newline at end of file
...@@ -27,6 +27,7 @@ declare module '@vue/runtime-core' { ...@@ -27,6 +27,7 @@ declare module '@vue/runtime-core' {
27 FileUpload: typeof import('./../components/FileUpload/index.vue')['default'] 27 FileUpload: typeof import('./../components/FileUpload/index.vue')['default']
28 FixedActionBar: typeof import('./../components/FixedActionBar/index.vue')['default'] 28 FixedActionBar: typeof import('./../components/FixedActionBar/index.vue')['default']
29 Form: typeof import('./../components/Form/index.vue')['default'] 29 Form: typeof import('./../components/Form/index.vue')['default']
30 GraphTopbar: typeof import('./../components/RelationNetwork/graphTopbar.vue')['default']
30 Hour: typeof import('./../components/Schedule/component/hour.vue')['default'] 31 Hour: typeof import('./../components/Schedule/component/hour.vue')['default']
31 ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default'] 32 ImagePreview: typeof import('./../components/ImagePreview/index.vue')['default']
32 ImagesUpload: typeof import('./../components/ImagesUpload/index.vue')['default'] 33 ImagesUpload: typeof import('./../components/ImagesUpload/index.vue')['default']
...@@ -41,6 +42,7 @@ declare module '@vue/runtime-core' { ...@@ -41,6 +42,7 @@ declare module '@vue/runtime-core' {
41 PageNav: typeof import('./../components/PageNav/index.vue')['default'] 42 PageNav: typeof import('./../components/PageNav/index.vue')['default']
42 PcasCascader: typeof import('./../components/PcasCascader/index.vue')['default'] 43 PcasCascader: typeof import('./../components/PcasCascader/index.vue')['default']
43 Popover: typeof import('./../components/Popover/index.vue')['default'] 44 Popover: typeof import('./../components/Popover/index.vue')['default']
45 RelationNetwork: typeof import('./../components/RelationNetwork/index.vue')['default']
44 RouterLink: typeof import('vue-router')['RouterLink'] 46 RouterLink: typeof import('vue-router')['RouterLink']
45 RouterView: typeof import('vue-router')['RouterView'] 47 RouterView: typeof import('vue-router')['RouterView']
46 Schedule: typeof import('./../components/Schedule/index.vue')['default'] 48 Schedule: typeof import('./../components/Schedule/index.vue')['default']
......
1 <template>
2 <div ref="containerRef" class='canvas-wrapper'>
3
4 </div>
5 </template>
6
7 <script lang="ts" setup name="topbar">
8 import { ref, watch } from 'vue';
9 import * as echarts from "echarts";
10
11 const props = defineProps({
12 treeData: {
13 type: Array<any>,
14 default: [
15 {
16 source: 'a',
17 target: 'a1',
18 value: 5
19 },
20 {
21 source: 'a',
22 target: 'a2',
23 value: 3
24 },
25 {
26 source: 'b',
27 target: 'b1',
28 value: 8
29 },
30 {
31 source: 'a',
32 target: 'b1',
33 value: 3
34 },
35 {
36 source: 'b1',
37 target: 'a1',
38 value: 1
39 },
40 {
41 source: 'b1',
42 target: 'c',
43 value: 2
44 }
45 ]
46 },
47 names: {
48 type: Array<any>,
49 default: [
50 {
51 name: 'a'
52 },
53 {
54 name: 'b'
55 },
56 {
57 name: 'a1'
58 },
59 {
60 name: 'a2'
61 },
62 {
63 name: 'b1'
64 },
65 {
66 name: 'c'
67 }
68 ]
69 }
70 })
71
72 watch(() => props.treeData, (val) => {
73 setChartsOption();
74 })
75
76 const sankeyInstance: any = ref();
77
78 const containerRef = ref();
79
80 const setChartsOption = () => {
81 let option = {
82 tooltip: {
83 trigger: 'item'
84 },
85 series: [
86 {
87 type: 'sankey',
88 top: 80,
89 bottom: 40,
90 left: 50,
91 right: 50,
92 data: props.names,
93 links: props.treeData,
94 lineStyle: {
95 color: 'source',
96 curveness: 0.5
97 },
98 }
99 ]
100 }
101 sankeyInstance.value.setOption(option);
102 sankeyInstance.value.resize();
103 }
104
105 const resizeObserver = ref();
106
107 const observeResize = () => {
108 resizeObserver.value = new ResizeObserver(() => {
109 sankeyInstance.value?.resize();
110 });
111 resizeObserver.value.observe(containerRef.value);
112 }
113
114 onMounted(() => {
115 nextTick(() => {
116 sankeyInstance.value = echarts.init(containerRef.value);
117 setChartsOption();
118 observeResize();
119 })
120 })
121
122 </script>
123
124 <style lang="scss" scoped>
125 .canvas-wrapper {
126 width: 100%;
127 height: 100%;
128 position: relative;
129 }
130 </style>
...\ No newline at end of file ...\ No newline at end of file
1 <route lang="yaml">
2 name: metadataStandardQuery
3 </route>
4
5 <script lang="ts" setup name="metadataStandardQuery">
6 import { ref } from 'vue';
7 import { ElMessage, ElMessageBox } from "element-plus";
8 import Sankey from './components/Sankey.vue';
9 import Tree from '@/components/Tree/index.vue';
10 import RelationNetwork from '@/components/RelationNetwork/index.vue';
11 import {
12 } from '@/api/modules/dataMetaService';
13
14 import { useRouter, useRoute } from "vue-router";
15 import useDataMetaStore from "@/store/modules/dataMeta"
16
17 const router = useRouter();
18 const route = useRoute()
19
20 const switchGraphDisplay = ref(true); //true表示关系网,false表示桑基图
21
22 const { proxy } = getCurrentInstance() as any;
23
24 const relationNetworkRef = ref();
25
26 const treeInfo = ref({
27 id: "data-meta-standard-tree",
28 filter: true,
29 queryValue: "",
30 queryPlaceholder: "输入库/表名称搜索",
31 props: {
32 label: "name",
33 value: "guid",
34 isLeaf: "isLeaf",
35 },
36 nodeKey: 'guid',
37 expandedKey: ['1'],
38 currentNodeKey: '',
39 expandOnNodeClick: false,
40 data: <any>[],
41 loading: false
42 });
43
44 /** 获取左侧树数据. */
45 const getTreeData = async () => {
46 treeInfo.value.loading = true
47 // let params = {}
48 // const res: any = await getMetaTreeData(params)
49 // if (res.code == proxy.$passCode) {
50 // const data = res.data || [];
51 // let treeData: any = Object.keys(data).length ? [data] : [];
52 // treeInfo.value.data = treeData;
53 // allTreeData.value = treeData;
54 // if (treeData.length) {
55 // treeInfo.value.currentNodeKey = treeData[0].guid;
56 // treeInfo.value.expandedKey = <any>[treeData[0].guid];
57 // }
58 // } else {
59 // ElMessage.error(res.msg);
60 // }
61 treeInfo.value.data = [{
62 guid: '1',
63 name: '第一级1',
64 children: [{
65 guid: '1-1',
66 name: '第二级1-1',
67 children: [{
68 guid: '1-1-1',
69 name: '第三级1-1-1'
70 }]
71 }, {
72 guid: '1-2',
73 name: '第二级1-2'
74 }, {
75 guid: '1-3',
76 name: '第二级1-3'
77 }]
78 }, {
79 guid: '2',
80 name: '第一级2',
81 children: [{
82 guid: '2-1',
83 name: '第二级1-1'
84 }, {
85 guid: '2-2',
86 name: '第二级1-2'
87 }]
88 }];
89 treeInfo.value.loading = false
90 treeInfo.value.currentNodeKey = '1';
91 nodeClick(treeInfo.value.data[0])
92 }
93 /** 左侧树的的组件引用. */
94 const treeInfoRef = ref();
95
96 /** 当前选中的树节点数据data */
97 const lastClickNode: any = ref({});
98
99 const treeDataLoading = ref(false);
100
101 /** 数据血缘关系图组件 */
102 const lineageGraph: any = ref();
103
104 /** 点击左侧树节点,更新对应的血缘关系图. */
105 const nodeClick = (data) => {
106 console.log(data);
107 const ele = <HTMLElement>document.querySelector(".g6-component-contextmenu")
108 if (ele) {
109 ele.style.display = "none"
110 }
111 nextTick(() => {
112 lineageGraph.value?.tooltip1.hide()
113 })
114 treeInfo.value.currentNodeKey = data.guid;
115 treeInfo.value.expandedKey = <any>[data.guid];
116 lastClickNode.value = data;
117 }
118
119 /** 选中树节点后自动滚动到可视范围内. */
120 const scrollToNode = (nodeId) => {
121 nextTick(() => {
122 const nodeElement = treeInfoRef.value.treeRef.$el.querySelector(`[data-key="${nodeId}"]`);
123 if (nodeElement) {
124 nodeElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
125 }
126 });
127 }
128
129 /** 处理从详情处跳转而来的默认展示. */
130 const processRouter = () => {
131 const { guid, databaseName, tableName, databaseChName, databaseGuid, fieldGuid, fieldEnName, set } = useDataMetaStore()
132 let isFL = useDataMetaStore().isFieldLineage;
133 if (fieldGuid) {//查看字段血缘的
134 nextTick(() => {
135 treeInfo.value.expandedKey = <any>[databaseGuid, guid];
136 treeInfo.value.currentNodeKey = fieldGuid as string;
137 scrollToNode(fieldGuid);
138 treeInfoRef.value.setCurrentKey(fieldGuid);
139 })
140 lastClickNode.value = { guid: fieldGuid, tableGuid: guid, databaseGuid: databaseGuid, enName: fieldEnName, tableName, databaseName, type: 4, isLeaf: true, databaseChName };
141 // getTableFieldLineageMap();
142 set()
143 } else if (guid) {
144 treeInfo.value.currentNodeKey = guid as string;
145 treeInfo.value.expandedKey = <any>[databaseGuid, guid];
146 lastClickNode.value = { guid: guid, tableName, databaseName, type: 3, databaseChName };
147 scrollToNode(guid);
148 switchGraphDisplay.value = isFL;
149 // if (isFL) {
150 // getAllTableFieldLineageMap();
151 // } else {
152 // getTableLineageMap()
153 // }
154 set()
155 }
156 }
157
158 onActivated(() => {
159 processRouter();
160 });
161
162 onBeforeMount(async () => {
163 await getTreeData()
164 processRouter();
165 })
166
167 onMounted(() => { })
168
169 const isGraphDisplay = ref(true);
170
171 const displaySwitchChange = (val) => {
172 isGraphDisplay.value = val;
173 }
174
175 const handleNodeItemClick = (graph, nodeItem) => {
176 const nodeId = nodeItem.get('id');
177 const parentData = graph.findDataById(nodeId);
178 if (!parentData.children) {
179 parentData.children = [];
180 }
181 treeDataLoading.value = true;
182 let childData = [{
183 guid: '33',
184 isField: true,
185 name: '字段1'
186 }, {
187 guid: '44',
188 isField: true,
189 name: '字段2字段2字段2字段2字段2字段2字段2字段2字段2xx2字段22字段22字段22字段22字段22字段2'
190 }, {
191 guid: '55',
192 isField: true,
193 name: '字段3'
194 }]
195 // parentData.collapsed = true;
196 // nodeItem.getModel().collapsed = true;
197 parentData.children = childData;
198 setTimeout(() => {
199 treeDataLoading.value = false;
200 graph.changeData();
201 // parentData.collapsed = true;
202 // graph.updateChildren(childData, parentData.id);
203 // graph.changeData(lastClickNode.value);
204 //graph.data(lastClickNode.value);
205 }, 2000)
206 }
207
208 const handleContextMenu = (nodeData) => {
209 //TODO,新建引用数据集
210 }
211
212 onBeforeUnmount(() => {
213 relationNetworkRef.value.destroy();
214 })
215
216 </script>
217
1 <template> 218 <template>
2 <div>元数据标准查询</div> 219 <div class="container_wrap full flex">
220 <div class="aside_wrap">
221 <div class="aside_title">数据库目录列表</div>
222 <Tree ref="treeInfoRef" :treeInfo="treeInfo" @nodeClick="nodeClick" />
223 </div>
224 <div class="main_wrap">
225 <div className='g6-component-topbar'>
226 <graphTopbar ref="topBarRef" @displaySwitchChange="displaySwitchChange" :isGraphDisplay="isGraphDisplay" />
227 </div>
228 <RelationNetwork v-show="isGraphDisplay" ref="relationNetworkRef" :tree-data="lastClickNode"
229 v-loading="treeDataLoading" @nodeItemClick="handleNodeItemClick" @contextMenu="handleContextMenu">
230 </RelationNetwork>
231 <Sankey v-show="!isGraphDisplay"></Sankey>
232 <!-- <div v-show="lastClickNode && lastClickNode?.type !== 3 && lastClickNode?.type !== 4" class="main-placeholder">
233 <img src="../../assets/images/no-data.png" :style="{ width: '96px', height: '96px' }" />
234 <div class="empty-text">暂无标准数据</div>
235 </div> -->
236 </div>
237 </div>
3 </template> 238 </template>
4 239
5 <script> 240 <style scoped lang="scss">
6 export default { 241 .container_wrap {
242
243 .aside_wrap {
244 width: 200px;
245 margin-right: 1px;
246 }
247
248 .main_wrap {
249 position: relative;
250
251 :deep(.canvas-wrapper) {
252 background-color: #f7f7f9;
253 }
254
255 .main-placeholder {
256 height: 100%;
257 display: flex;
258 justify-content: center;
259 align-items: center;
260 flex-direction: column;
261
262 .empty-text {
263 font-size: 14px;
264 color: #b2b2b2;
265 }
266 }
267 }
268
7 } 269 }
8 </script>
9 270
10 <style>
11 </style>
...\ No newline at end of file ...\ No newline at end of file
271 .g6-component-topbar {
272 position: absolute;
273 left: 24px;
274 bottom: unset;
275 top: 14px;
276 padding: 0;
277 text-align: center;
278 z-index: 999;
279 }
280
281 .container_wrap.flex .main_wrap {
282 padding: 0px;
283 }
284
285 .tree_panel {
286 height: calc(100% - 36px);
287 padding-top: 0;
288
289 :deep(.el-tree) {
290 margin: 0;
291 overflow: hidden auto;
292 }
293 }
294
295 .card-noData {
296 height: 100%;
297 width: 100%;
298 background: #fafafa;
299 display: flex;
300 flex-direction: column;
301 justify-content: center;
302 align-items: center;
303 color: #909399;
304 font-size: 14px;
305 }
306
307 :deep(.el-form .el-form-item) {
308 width: calc(100%);
309 // margin-right: 8px;
310 }
311
312 :deep(.el-message) {
313 position: fixed;
314 /* 使用fixed或absolute定位 */
315 z-index: 10000;
316 /* 设置一个较高的z-index值确保在最上层显示 */
317 }
318 </style>
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!